diff --git a/.circleci/config.yml b/.circleci/config.yml
index b86b39f752a38aee24f50bb260f7b6b45d8b722f..9cf3c2a4e36338e88a9f08201a4df200395fdcf9 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -7,6 +7,7 @@ jobs:
         environment:
           CC: ccache gcc
           CCACHE_COMPRESS: true
+          CFLAGS: -Wno-error=unused-result
       #- image: ubuntu:trusty
       #  environment:
       #    CC: ccache gcc -m32
@@ -49,12 +50,15 @@ jobs:
           paths:
             - /home/circleci/.cache/apt
       - checkout
+      - run:
+         name: Create deps folder as needed
+         command: mkdir -p make/linux/64/SDL/deps/
       - run:
           name: make master depend file
-          command: find make/linux64/SDL/deps/ -type f -print0 | sort -z | xargs -r0 cat > make/linux64/SDL.deps
+          command: find make/linux/64/SDL/deps/ -type f -print0 | sort -z | xargs -r0 cat > make/linux/64/SDL.deps
       - restore_cache:
           keys:
-            - v1-SRB2-{{ .Branch }}-{{ checksum "make/linux64/SDL.deps" }}
+            - v1-SRB2-{{ .Branch }}-{{ checksum "make/linux/64/SDL.deps" }}
       - run:
           name: Compile
           command: make -C src LINUX64=1 ERRORMODE=1 -k -j4
@@ -62,6 +66,6 @@ jobs:
           path: /home/circleci/SRB2/bin/
           destination: bin
       - save_cache:
-          key: v1-SRB2-{{ .Branch }}-{{ checksum "make/linux64/SDL.deps" }}
+          key: v1-SRB2-{{ .Branch }}-{{ checksum "make/linux/64/SDL.deps" }}
           paths:
             - /home/circleci/.ccache
diff --git a/.gitignore b/.gitignore
index 4ba05f7ca698b968eec6391857fa5ec68e1b422e..a3d823fa2182edef86c35ee2810560e5d8f7408e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,4 @@ Win32_LIB_ASM_Release
 /CMakeUserPresets.json
 /out
 /objs/VC10
+/thirdparty/vcpkg-overlays
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b21ce1b00313089705addb772b455a8308d21795..14c36213e051f0af306b2d392af1f8da491ce289 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,848 +1,20 @@
-variables:
-  GIT_STRATEGY: clone
-  GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH
-
-default:
-  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:stable
-
-  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
-
-    - key: apk-$CI_JOB_IMAGE
-      paths:
-        - apk-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"
+include:
+  - '.gitlab/ci/templates/*.yml'
+  - '.gitlab/ci/jobs/*.yml'
 
-    - - |
-          # 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 cmake ca-certificates
-      - |
-          # apt_common
-          echo -e "\e[0Ksection_end:`date +%s`:apt_common\r\e[0K"
+workflow:
+  auto_cancel:
+    on_new_commit: interruptible
 
-    - - |
-          # 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
-      - ccache --show-log-stats || true
-      - |
-          # ccahe_stats
-          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
+variables:
+  GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH
+  GIT_DEPTH: 20
 
 stages:
   - build
+  - osxcross
 
-Debian testing GCC:
-  stage: build
-
-  when: manual
-
-  image: debian:testing-slim
-
-  allow_failure: true
-
-  artifacts:
-    paths:
-      - "bin/"
-      - "src/comptime.h"
-    expose_as: "testing-gcc"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-gcc"
-
-  variables:
-    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 libminiupnpc-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
-
-  when: on_success
-
-  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
-
-  when: on_success
-
-  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 libminiupnpc-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 oldstable:amd64:
-  extends: Debian stable:amd64
-
-  when: manual
-
-  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:oldstable
-
-  allow_failure: true
-
-  artifacts:
-    paths:
-      - "bin/"
-      - "src/comptime.h"
-    expose_as: "Debian old amd64"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-old-x86-64"
-
-  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 libopenmpt-dev:amd64 libminiupnpc-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 NOGME=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NOGME=1
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-Debian stable:i386:
-  stage: build
-
-  when: manual
-
-  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 libminiupnpc-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
-
-  when: manual
-
-  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 libminiupnpc-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 ARM64=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1 ARM64=1
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-Debian oldstable:arm64:
-  extends: Debian stable:arm64
-
-  when: manual
-
-  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:oldstable
-
-  allow_failure: true
-
-  artifacts:
-    paths:
-      - "bin/"
-      - "src/comptime.h"
-    expose_as: "Debian old arm64"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-old-aarch64"
-
-  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 libopenmpt-dev:arm64 libminiupnpc-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 ARM64=1 NOGME=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1 ARM64=1 NOGME=1
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-batocera:arm64:
-  extends: Debian stable:arm64
-
-  when: manual
-
-  allow_failure: true
-
-  artifacts:
-    paths:
-      - "bin/"
-      - "src/comptime.h"
-    expose_as: "Debian old arm64"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-batocera-aarch64"
-
-  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 libopenmpt-dev:arm64 libminiupnpc-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 ARM64=1 NOGME=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1 ARM64=1 NOGME=1
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-Windows x64:
-  stage: build
-
-  when: manual
-
-  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
-
-  when: on_success
-
-  allow_failure: false
-
-  artifacts:
-    paths:
-      - "build.clang/bin/"
-      - "build.clang/src/config.h"
-    expose_as: "clang"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-clang"
-
-  variables:
-    CC: clang
-    CXX: clang
-    WFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror
-    CFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror
-    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 libminiupnpc-dev
-      - |
-          # apt_development
-          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
-
-    - - |
-          # cmake
-          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
-      - cmake -B build.clang -DCPM_USE_LOCAL_PACKAGES:BOOL=ON -DSRB2_CONFIG_ENABLE_TESTS:BOOL=OFF -DSRB2_CONFIG_SYSTEM_LIBRARIES:BOOL=ON -DSRB2_USE_LIBGME:BOOL=OFF -G "Unix Makefiles"
-      - |
-          # cmake
-          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
-
-    - - |
-          # make
-          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
-      - make --directory=build.clang --keep-going || make --directory=build.clang --keep-going
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-Debian testing Clang:
-  extends: Debian stable Clang
-
-  when: manual
-
-  allow_failure: true
-
-  image: debian:testing-slim
-
-  artifacts:
-    paths:
-      - "build.clang/bin/"
-      - "build.clang/src/config.h"
-    expose_as: "testing-clang"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-clang"
-
-  variables:
-    CC: clang
-    CXX: clang
-    WFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror -Wno-deprecated-non-prototype -Wno-single-bit-bitfield-constant-conversion
-    CFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror -Wno-deprecated-non-prototype -Wno-single-bit-bitfield-constant-conversion
-    LDFLAGS: -Wl,-fuse-ld=gold
-
-Alpine 3 GCC:
-  stage: build
-
-  when: on_success
-
-  image: alpine:3
-
-  allow_failure: true
-
-  artifacts:
-    paths:
-      - "bin/"
-      - "src/comptime.h"
-    expose_as: "Apline-3"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Apline-3"
-
-  before_script:
-    - - |
-          # apk_cache
-          echo -e "\e[0Ksection_start:`date +%s`:apk_cache[collapsed=true]\r\e[0KUpdating APK listing"
-      - export APK_CACHE_DIR=`pwd`/apk-cache
-      - mkdir --parents --verbose $APK_CACHE_DIR/
-      - ln -sf /etc/apk/cache $APK_CACHE_DIR
-      - |
-          # apk_cache
-          echo -e "\e[0Ksection_end:`date +%s`:apk_cache\r\e[0K"
-
-    - - |
-          # apk_update
-          echo -e "\e[0Ksection_start:`date +%s`:apk_update[collapsed=true]\r\e[0KUpdating APK listing"
-      - apk update
-      - |
-          # apk_update
-          echo -e "\e[0Ksection_end:`date +%s`:apk_update\r\e[0K"
-
-    - - |
-          # apk_upgrade
-          echo -e "\e[0Ksection_start:`date +%s`:apk_upgrade[collapsed=true]\r\e[0KUpdating existing packages"
-      - apk upgrade
-      - |
-          # apk_update
-          echo -e "\e[0Ksection_end:`date +%s`:apk_upgrade\r\e[0K"
-
-    - - |
-          # apk_common
-          echo -e "\e[0Ksection_start:`date +%s`:apk_common[collapsed=true]\r\e[0KInstalling common packages"
-      - apk add make git ccache nasm
-      - |
-          # apk_common
-          echo -e "\e[0Ksection_end:`date +%s`:apk_common\r\e[0K"
-
-    - - |
-          # ccache_config
-          echo -e "\e[0Ksection_start:`date +%s`:ccache_config[collapsed=true]\r\e[0KSetting up ccache config"
-      - mkdir --parents --verbose ~/.ccache/
-      - touch ~/.ccache/ccache.conf
-      - |
-          # cache.conf
-          echo Adding ccache configution option
-      - |
-          # base_dir
-          echo base_dir = $PWD                  | tee -a ~/.ccache/ccache.conf
-      - |
-          # cache_dir
-          echo cache_dir = $PWD/ccache          | tee -a ~/.ccache/ccache.conf
-      - |
-          # compiler_check
-          echo compiler_check = content         | tee -a ~/.ccache/ccache.conf
-      - |
-          # stats_log
-          echo stats_log = $PWD/ccache_statslog | tee -a ~/.ccache/ccache.conf
-      - |
-          # max_size
-          echo max_size = 50M                   | tee -a ~/.ccache/ccache.conf
-      - |
-          # ccache_config
-          echo -e "\e[0Ksection_end:`date +%s`:ccache_config\r\e[0K"
-
-    - - |
-          # cache_reset
-          echo -e "\e[0Ksection_start:`date +%s`:ccache_reset[collapsed=true]\r\e[0KResetting ccache statistics"
-      - ccache --zero-stats
-      - ccache --show-stats
-      - |
-          # ccache_reset
-          echo -e "\e[0Ksection_end:`date +%s`:ccache_reset\r\e[0K"
-
-  script:
-    - - |
-          # apk_toolchain
-          echo -e "\e[0Ksection_start:`date +%s`:apk_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
-      - apk add gcc
-      - |
-          # apk_toolchain
-          echo -e "\e[0Ksection_end:`date +%s`:apk_toolchain\r\e[0K"
-
-    - - |
-          # apk_development
-          echo -e "\e[0Ksection_start:`date +%s`:apk_development[collapsed=true]\r\e[0KInstalling development packages"
-      - apk add musl-dev sdl2_mixer-dev libpng-dev curl-dev libgme-dev libopenmpt-dev miniupnpc-dev
-      - |
-          # apk_development
-          echo -e "\e[0Ksection_end:`date +%s`:apk_development\r\e[0K"
-
-    - - |
-          # make
-          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
-      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-  after_script:
-    - - |
-           # apk_clean
-           echo -e "\e[0Ksection_start:`date +%s`:apk_clean[collapsed=true]\r\e[0KCleaning of unneeded APK packages"
-      - apk cache clean
-      - |
-          # apk_clean
-          echo -e "\e[0Ksection_end:`date +%s`:apk_clean\r\e[0K"
-
-    - - |
-          # ccache_stats
-          echo -e "\e[0Ksection_start:`date +%s`:ccache_stats[collapsed=true]\r\e[0Kccache statistics:"
-      - ccache --show-stats --verbose
-      - ccache --show-log-stats --verbose
-      - |
-          # ccahe_stats
-          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
-
-Alpine 3 GCC Dedicated:
-  extends: Alpine 3 GCC
-
-  artifacts:
-    paths:
-      - "bin/"
-      - "src/comptime.h"
-    expose_as: "Apline-3-Dedicated"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Apline-3-Dedicated"
-
-  script:
-    - - |
-          # apk_toolchain
-          echo -e "\e[0Ksection_start:`date +%s`:apk_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
-      - apk add gcc
-      - |
-          # apk_toolchain
-          echo -e "\e[0Ksection_end:`date +%s`:apk_toolchain\r\e[0K"
-
-    - - |
-          # apk_development
-          echo -e "\e[0Ksection_start:`date +%s`:apk_development[collapsed=true]\r\e[0KInstalling development packages"
-      - apk add musl-dev libpng-dev curl-dev
-      - |
-          # apk_development
-          echo -e "\e[0Ksection_end:`date +%s`:apk_development\r\e[0K"
-
-    - - |
-          # make
-          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
-      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1 DEDICATED=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1 DEDICATED=1
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-osxcross x86_64:
-  stage: build
-
-  artifacts:
-    paths:
-      - "build.osxcross/bin/"
-      - "build.osxcross/src/config.h"
-    expose_as: "Mac x86_64"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-clang"
-
-  variables:
-    OSXCROSS_HOST: x86_64-apple-darwin21.4
-    LD: x86_64-apple-darwin21.4-ld
-
-  script:
-    - - |
-          # apt_development
-          echo -e "\e[0Ksection_start:`date +%s`:macports_development[collapsed=true]\r\e[0KInstalling development packages"
-      - osxcross-macports install curl libopenmpt libsdl2_mixer
-      - |
-          # apt_development
-          echo -e "\e[0Ksection_end:`date +%s`:macports_development\r\e[0K"
-
-    - - |
-          # make
-          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KBuilding Makefiles"
-      - cmake -B build.osxcross --toolchain /osxcross/toolchain.cmake -DCPM_USE_LOCAL_PACKAGES:BOOL=ON -DOPENMPT_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/include" -DSDL2_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/lib" -DSRB2_CONFIG_ENABLE_TESTS:BOOL=OFF -DSRB2_CONFIG_SYSTEM_LIBRARIES:BOOL=ON -DSRB2_CONFIG_USE_GME:BOOL=OFF -G "Unix Makefiles"
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-    - - |
-          # make
-          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
-      - make --directory=build.osxcross --keep-going || make --directory=build.osxcross --keep-going
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-osxcross arm64:
-  stage: build
-
-  when: manual
-
-  allow_failure: true
-
+default:
+  interruptible: true
   artifacts:
-    paths:
-      - "build.osxcross/bin/"
-      - "build.osxcross/src/config.h"
-    expose_as: "Mac arm64"
-    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-clang"
-
-  variables:
-    OSXCROSS_HOST: arm64-apple-darwin21.4
-    LD: arm64-apple-darwin21.4-ld
-
-  script:
-    - - |
-          # apt_development
-          echo -e "\e[0Ksection_start:`date +%s`:macports_development[collapsed=true]\r\e[0KInstalling development packages"
-      - osxcross-macports install --arm64 curl libopenmpt libsdl2_mixer
-      - |
-          # apt_development
-          echo -e "\e[0Ksection_end:`date +%s`:macports_development\r\e[0K"
-
-    - - |
-          # make
-          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KBuilding Makefiles"
-      - cmake -B build.osxcross --toolchain /osxcross/toolchain.cmake -DCPM_USE_LOCAL_PACKAGES:BOOL=ON -DOPENMPT_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/include" -DSDL2_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/lib" -DSRB2_CONFIG_ENABLE_TESTS:BOOL=OFF -DSRB2_CONFIG_SYSTEM_LIBRARIES:BOOL=ON -DSRB2_CONFIG_USE_GME:BOOL=OFF -G "Unix Makefiles"
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
-
-    - - |
-          # make
-          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
-      - make --directory=build.osxcross --keep-going || make --directory=build.osxcross --keep-going
-      - |
-          # make
-          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+    expire_in: 1 day
diff --git a/.gitlab/ci/jobs/alpine-3-gcc-dedicated-makefile.yml b/.gitlab/ci/jobs/alpine-3-gcc-dedicated-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5d875d8657694b4899a31f33ba3440a2cd47e314
--- /dev/null
+++ b/.gitlab/ci/jobs/alpine-3-gcc-dedicated-makefile.yml
@@ -0,0 +1,34 @@
+Alpine 3 GCC Dedicated Makefile:
+  extends: Alpine 3 GCC Makefile
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Apline-3-Dedicated-makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Apline-3-Dedicated-makefile"
+
+  script:
+    - - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apk_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apk add g++
+      - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_end:`date +%s`:apk_toolchain\r\e[0K"
+
+    - - |
+          # apk_development
+          echo -e "\e[0Ksection_start:`date +%s`:apk_development[collapsed=true]\r\e[0KInstalling development packages"
+      - apk add musl-dev libpng-dev curl-dev
+      - |
+          # apk_development
+          echo -e "\e[0Ksection_end:`date +%s`:apk_development\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1 DEDICATED=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1 DEDICATED=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/alpine-3-gcc-makefile.yml b/.gitlab/ci/jobs/alpine-3-gcc-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..13bcea070253f4f4733f8f956f8e6d4132d0a98b
--- /dev/null
+++ b/.gitlab/ci/jobs/alpine-3-gcc-makefile.yml
@@ -0,0 +1,133 @@
+Alpine 3 GCC Makefile:
+  stage: build
+
+  when: manual
+
+  image: alpine:3
+
+  allow_failure: true
+
+  cache:
+    - key: apk-$CI_JOB_IMAGE-makefile
+      paths:
+        - apk-cache
+      unprotect: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Apline-3-makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Apline-3-makefile"
+
+  before_script:
+    - - |
+          # apk_cache
+          echo -e "\e[0Ksection_start:`date +%s`:apk_cache[collapsed=true]\r\e[0KUpdating APK listing"
+      - export APK_CACHE_DIR=`pwd`/apk-cache
+      - mkdir --parents --verbose $APK_CACHE_DIR/
+      - ln -sf /etc/apk/cache $APK_CACHE_DIR
+      - |
+          # apk_cache
+          echo -e "\e[0Ksection_end:`date +%s`:apk_cache\r\e[0K"
+
+    - - |
+          # apk_update
+          echo -e "\e[0Ksection_start:`date +%s`:apk_update[collapsed=true]\r\e[0KUpdating APK listing"
+      - apk update
+      - |
+          # apk_update
+          echo -e "\e[0Ksection_end:`date +%s`:apk_update\r\e[0K"
+
+    - - |
+          # apk_upgrade
+          echo -e "\e[0Ksection_start:`date +%s`:apk_upgrade[collapsed=true]\r\e[0KUpdating existing packages"
+      - apk upgrade
+      - |
+          # apk_update
+          echo -e "\e[0Ksection_end:`date +%s`:apk_upgrade\r\e[0K"
+
+    - - |
+          # apk_common
+          echo -e "\e[0Ksection_start:`date +%s`:apk_common[collapsed=true]\r\e[0KInstalling common packages"
+      - apk add make git ccache nasm
+      - |
+          # apk_common
+          echo -e "\e[0Ksection_end:`date +%s`:apk_common\r\e[0K"
+
+    - - |
+          # ccache_config
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_config[collapsed=true]\r\e[0KSetting up ccache config"
+      - |
+          # cache.conf
+          echo Adding ccache configution option
+      - |
+          # base_dir
+          ccache --set-config base_dir=$CI_PROJECT_DIR
+      - |
+          # cache_dir
+          ccache --set-config cache_dir=$CI_PROJECT_DIR/build/ccache
+      - |
+          # compiler_check
+          ccache --set-config compiler_check=content
+      - |
+          # stats_log
+          ccache --set-config stats_log=$CI_PROJECT_DIR/build/ccache_statslog
+      - |
+          # max_size
+          ccache --set-config max_size=300M
+      - |
+          # ccache_config
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_config\r\e[0K"
+
+    - - |
+          # cache_reset
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_reset[collapsed=true]\r\e[0KResetting ccache statistics"
+      - ccache --zero-stats
+      - ccache --show-stats
+      - |
+          # ccache_reset
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_reset\r\e[0K"
+
+  script:
+    - - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apk_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apk add g++
+      - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_end:`date +%s`:apk_toolchain\r\e[0K"
+
+    - - |
+          # apk_development
+          echo -e "\e[0Ksection_start:`date +%s`:apk_development[collapsed=true]\r\e[0KInstalling development packages"
+      - apk add musl-dev sdl2_mixer-dev libpng-dev curl-dev libgme-dev libopenmpt-dev miniupnpc-dev elfutils-dev
+      - |
+          # apk_development
+          echo -e "\e[0Ksection_end:`date +%s`:apk_development\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+  after_script:
+    - - |
+           # apk_clean
+           echo -e "\e[0Ksection_start:`date +%s`:apk_clean[collapsed=true]\r\e[0KCleaning of unneeded APK packages"
+      - apk cache clean
+      - |
+          # apk_clean
+          echo -e "\e[0Ksection_end:`date +%s`:apk_clean\r\e[0K"
+
+    - - |
+          # ccache_stats
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_stats[collapsed=true]\r\e[0Kccache statistics:"
+      - ccache --show-stats --verbose
+      - ccache --show-log-stats --verbose
+      - |
+          # ccahe_stats
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
diff --git a/.gitlab/ci/jobs/alpine-3-gcc.yml b/.gitlab/ci/jobs/alpine-3-gcc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5d828ce58b9a5a217a0c3e4d7bfccca31c279530
--- /dev/null
+++ b/.gitlab/ci/jobs/alpine-3-gcc.yml
@@ -0,0 +1,149 @@
+Alpine 3 GCC:
+  stage: build
+
+  when: manual
+
+  image: alpine:3
+
+  allow_failure: true
+
+  cache:
+    - key: apk-$CI_JOB_IMAGE
+      paths:
+        - apk-cache
+      unprotect: true
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "Apline-3"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Apline-3"
+
+  before_script:
+    - - |
+          # apk_cache
+          echo -e "\e[0Ksection_start:`date +%s`:apk_cache[collapsed=true]\r\e[0KUpdating APK listing"
+      - export APK_CACHE_DIR=`pwd`/apk-cache
+      - mkdir --parents --verbose $APK_CACHE_DIR/
+      - ln -sf /etc/apk/cache $APK_CACHE_DIR
+      - |
+          # apk_cache
+          echo -e "\e[0Ksection_end:`date +%s`:apk_cache\r\e[0K"
+
+    - - |
+          # apk_update
+          echo -e "\e[0Ksection_start:`date +%s`:apk_update[collapsed=true]\r\e[0KUpdating APK listing"
+      - apk update
+      - |
+          # apk_update
+          echo -e "\e[0Ksection_end:`date +%s`:apk_update\r\e[0K"
+
+    - - |
+          # apk_upgrade
+          echo -e "\e[0Ksection_start:`date +%s`:apk_upgrade[collapsed=true]\r\e[0KUpdating existing packages"
+      - apk upgrade
+      - |
+          # apk_update
+          echo -e "\e[0Ksection_end:`date +%s`:apk_upgrade\r\e[0K"
+
+    - - |
+          # apk_common
+          echo -e "\e[0Ksection_start:`date +%s`:apk_common[collapsed=true]\r\e[0KInstalling common packages"
+      - apk add cmake make git ccache nasm
+      - |
+          # apk_common
+          echo -e "\e[0Ksection_end:`date +%s`:apk_common\r\e[0K"
+
+    - - |
+          # ccache_config
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_config[collapsed=true]\r\e[0KSetting up ccache config"
+      - |
+          # cache.conf
+          echo Adding ccache configution option
+      - |
+          # base_dir
+          ccache --set-config base_dir=$CI_PROJECT_DIR
+      - |
+          # cache_dir
+          ccache --set-config cache_dir=$CI_PROJECT_DIR/build/ccache
+      - |
+          # compiler_check
+          ccache --set-config compiler_check=content
+      - |
+          # stats_log
+          ccache --set-config stats_log=$CI_PROJECT_DIR/build/ccache_statslog
+      - |
+          # max_size
+          ccache --set-config max_size=300M
+      - |
+          # ccache_config
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_config\r\e[0K"
+
+    - - |
+          # cache_reset
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_reset[collapsed=true]\r\e[0KResetting ccache statistics"
+      - ccache --zero-stats
+      - ccache --show-stats
+      - |
+          # ccache_reset
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_reset\r\e[0K"
+
+  script:
+    - - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apk_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apk add g++
+      - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_end:`date +%s`:apk_toolchain\r\e[0K"
+
+    - - |
+          # apk_development
+          echo -e "\e[0Ksection_start:`date +%s`:apk_development[collapsed=true]\r\e[0KInstalling development packages"
+      - apk add cmake musl-dev sdl2_mixer-dev libpng-dev curl-dev libgme-dev libopenmpt-dev miniupnpc-dev elfutils-dev
+      - |
+          # apk_development
+          echo -e "\e[0Ksection_end:`date +%s`:apk_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_EXECINFO=NO \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+  after_script:
+    - - |
+           # apk_clean
+           echo -e "\e[0Ksection_start:`date +%s`:apk_clean[collapsed=true]\r\e[0KCleaning of unneeded APK packages"
+      - apk cache clean
+      - |
+          # apk_clean
+          echo -e "\e[0Ksection_end:`date +%s`:apk_clean\r\e[0K"
+
+    - - |
+          # ccache_stats
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_stats[collapsed=true]\r\e[0Kccache statistics:"
+      - ccache --show-stats --verbose
+      - ccache --show-log-stats --verbose
+      - |
+          # ccahe_stats
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
diff --git a/.gitlab/ci/jobs/batocera-arm64-makefile.yml b/.gitlab/ci/jobs/batocera-arm64-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dd8f57759d5428130b09be59141c0aaebd4e9ece
--- /dev/null
+++ b/.gitlab/ci/jobs/batocera-arm64-makefile.yml
@@ -0,0 +1,40 @@
+batocera:arm64 Makefile:
+  extends: Debian stable:arm64 Makefile
+
+  stage: build
+
+  when: manual
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian old arm64 makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-batocera-aarch64-makefile"
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-aarch64-linux-gnu || apt-get install g++
+      - |
+          # 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 libopenmpt-dev:arm64 libminiupnpc-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 ARM64=1 NOGME=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1 ARM64=1 NOGME=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/batocera-arm64.yml b/.gitlab/ci/jobs/batocera-arm64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5f928ab50649728bcb3732e80a8a7b74790c070c
--- /dev/null
+++ b/.gitlab/ci/jobs/batocera-arm64.yml
@@ -0,0 +1,56 @@
+batocera:arm64:
+  extends: Debian stable:arm64
+
+  stage: build
+
+  when: manual
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "Debian old arm64"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-batocera-aarch64"
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-aarch64-linux-gnu || apt-get install g++
+      - |
+          # 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 libopenmpt-dev:arm64 libminiupnpc-dev:arm64
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_FORCE_NO_MS_BITFIELDS=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=OFF
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-oldstable-amd64-makefile.yml b/.gitlab/ci/jobs/debian-oldstable-amd64-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f365a79274021e70e3e486b39f617778dd5ddde3
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-oldstable-amd64-makefile.yml
@@ -0,0 +1,42 @@
+Debian oldstable:amd64 Makefile:
+  extends: Debian stable:amd64 Makefile
+
+  stage: build
+
+  when: manual
+
+  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:oldstable
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian old amd64 makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-old-x86-64-makefile"
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-x86-64-linux-gnu || apt-get install g++
+      - |
+          # 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 libopenmpt-dev:amd64 libminiupnpc-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 NOGME=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NOGME=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-oldstable-amd64.yml b/.gitlab/ci/jobs/debian-oldstable-amd64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3aaf30698f91b3606d07e692536dc419f49cc3c9
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-oldstable-amd64.yml
@@ -0,0 +1,57 @@
+Debian oldstable:amd64:
+  extends: Debian stable:amd64
+
+  stage: build
+
+  when: manual
+
+  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:oldstable
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "Debian old amd64"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-old-x86-64"
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-x86-64-linux-gnu || apt-get install g++
+      - |
+          # 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 libopenmpt-dev:amd64 libminiupnpc-dev:amd64
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-oldstable-arm64-makefile.yml b/.gitlab/ci/jobs/debian-oldstable-arm64-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..25782baf23ce7d82ae017ccc699e6cd8c67c708c
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-oldstable-arm64-makefile.yml
@@ -0,0 +1,42 @@
+Debian oldstable:arm64 Makefile:
+  extends: Debian stable:arm64 Makefile
+
+  stage: build
+
+  when: manual
+
+  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:oldstable
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian old arm64 makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-old-aarch64-makefile"
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-aarch64-linux-gnu || apt-get install g++
+      - |
+          # 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 libopenmpt-dev:arm64 libminiupnpc-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 ARM64=1 NOGME=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1 ARM64=1 NOGME=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-oldstable-arm64.yml b/.gitlab/ci/jobs/debian-oldstable-arm64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..96db9f486e30545f37b0f34a285decf4a7655cf3
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-oldstable-arm64.yml
@@ -0,0 +1,58 @@
+Debian oldstable:arm64:
+  extends: Debian stable:arm64
+
+  stage: build
+
+  when: manual
+
+  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:oldstable
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "Debian old arm64"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-old-aarch64"
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-aarch64-linux-gnu || apt-get install g++
+      - |
+          # 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 libopenmpt-dev:arm64 libminiupnpc-dev:arm64
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_FORCE_NO_MS_BITFIELDS=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-stable-amd64-makefile.yml b/.gitlab/ci/jobs/debian-stable-amd64-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fee52c5a793015212c397c2907f1b927489ea93a
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-stable-amd64-makefile.yml
@@ -0,0 +1,45 @@
+Debian stable:amd64 Makefile:
+  extends: .srb2ci
+
+  stage: build
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian amd64 makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-x86-64-makefile"
+
+  variables:
+    CC: x86_64-linux-gnu-gcc
+    CXX: x86_64-linux-gnu-g++
+    OBJCOPY: x86_64-linux-gnu-objcopy
+    OBJDUMP: x86_64-linux-gnu-objdump
+    PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig
+
+  when: on_success
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-x86-64-linux-gnu || apt-get install g++
+      - |
+          # 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 libminiupnpc-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"
diff --git a/.gitlab/ci/jobs/debian-stable-amd64.yml b/.gitlab/ci/jobs/debian-stable-amd64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..da3b3a4ad8f3d55d73c9023292df8f0b8e3ec47a
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-stable-amd64.yml
@@ -0,0 +1,60 @@
+Debian stable:amd64:
+  extends: .srb2ci
+
+  stage: build
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.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
+    CXX: x86_64-linux-gnu-g++
+    OBJCOPY: x86_64-linux-gnu-objcopy
+    OBJDUMP: x86_64-linux-gnu-objdump
+    PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig
+
+  when: on_success
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-x86-64-linux-gnu || apt-get install g++
+      - |
+          # 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 libminiupnpc-dev:amd64
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-stable-arm64-makefile.yml b/.gitlab/ci/jobs/debian-stable-arm64-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aa0ea3780bc3aad545ce70b2667374a62b839673
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-stable-arm64-makefile.yml
@@ -0,0 +1,46 @@
+Debian stable:arm64 Makefile:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian arm64 makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-aarch64-makefile"
+
+  variables:
+    CC: aarch64-linux-gnu-gcc
+    CXX: aarch64-linux-gnu-g++
+    OBJCOPY: aarch64-linux-gnu-objcopy
+    OBJDUMP: aarch64-linux-gnu-objdump
+    LD: aarch64-linux-gnu-ld
+    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 gg++-aarch64-linux-gnu || apt-get install g++
+      - |
+          # 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 libminiupnpc-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 ARM64=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1 ARM64=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-stable-arm64.yml b/.gitlab/ci/jobs/debian-stable-arm64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1f929a06bee485f5bd58778c77446bf4d09f103b
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-stable-arm64.yml
@@ -0,0 +1,62 @@
+Debian stable:arm64:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.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
+    CXX: aarch64-linux-gnu-g++
+    OBJCOPY: aarch64-linux-gnu-objcopy
+    OBJDUMP: aarch64-linux-gnu-objdump
+    LD: aarch64-linux-gnu-ld
+    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 g++-aarch64-linux-gnu || apt-get install g++
+      - |
+          # 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 libminiupnpc-dev:arm64
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_FORCE_NO_MS_BITFIELDS=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-stable-clang-amd64.yml b/.gitlab/ci/jobs/debian-stable-clang-amd64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fb363c7aecd53d5f98eafdc1709784d9976c4c23
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-stable-clang-amd64.yml
@@ -0,0 +1,64 @@
+Debian stable Clang:
+  extends: .srb2ci
+
+  stage: build
+
+  when: on_success
+
+  allow_failure: false
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "clang"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-clang"
+
+  variables:
+    CC: clang
+    CXX: clang++
+    WFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror
+    CFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror
+
+  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 libminiupnpc-dev
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DCPM_USE_LOCAL_PACKAGES:BOOL=ON \
+            -DSRB2_CONFIG_ENABLE_TESTS:BOOL=OFF \
+            -DSRB2_CONFIG_SYSTEM_LIBRARIES:BOOL=ON \
+            -DSRB2_USE_LIBGME:BOOL=OFF \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-stable-i386-makefile.yml b/.gitlab/ci/jobs/debian-stable-i386-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dd572ec383c489d3414a312348806f47c65f3cbc
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-stable-i386-makefile.yml
@@ -0,0 +1,46 @@
+Debian stable:i386 Makefile:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian i386 makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-i686-makefile"
+
+  variables:
+    CC: i686-linux-gnu-gcc
+    CXX: i686-linux-gnu-g++
+    OBJCOPY: i686-linux-gnu-objcopy
+    OBJDUMP: i686-linux-gnu-objdump
+    LD: i686-linux-gnu-ld
+    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 g++-i686-linux-gnu || apt-get install g++
+      - |
+          # 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 libminiupnpc-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"
diff --git a/.gitlab/ci/jobs/debian-stable-i386.yml b/.gitlab/ci/jobs/debian-stable-i386.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f27c91533e70db5eff83143bfae92cddf1500870
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-stable-i386.yml
@@ -0,0 +1,61 @@
+Debian stable:i386:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.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
+    CXX: i686-linux-gnu-g++
+    OBJCOPY: i686-linux-gnu-objcopy
+    OBJDUMP: i686-linux-gnu-objdump
+    LD: i686-linux-gnu-ld
+    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 g++-i686-linux-gnu || apt-get install g++
+      - |
+          # 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 libminiupnpc-dev:i386
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/debian-testing-clang-amd64.yml b/.gitlab/ci/jobs/debian-testing-clang-amd64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..132e7264267fbb9906d0ffe09326db7f1074f9ec
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-testing-clang-amd64.yml
@@ -0,0 +1,21 @@
+Debian testing Clang:
+  extends: Debian stable Clang
+
+  when: manual
+
+  allow_failure: true
+
+  image: debian:testing-slim
+
+  artifacts:
+    paths:
+      - "build.clang/bin/"
+      - "build.clang/src/config.h"
+    expose_as: "testing-clang"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-clang"
+
+  variables:
+    CC: clang
+    CXX: clang++
+    WFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror -Wno-deprecated-non-prototype -Wno-single-bit-bitfield-constant-conversion
+    CFLAGS: -Wno-cast-align -Wno-implicit-const-int-float-conversion -Werror -Wno-deprecated-non-prototype -Wno-single-bit-bitfield-constant-conversion
diff --git a/.gitlab/ci/jobs/debian-testing-gcc-amd64-makefile.yml b/.gitlab/ci/jobs/debian-testing-gcc-amd64-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1165b1fea3e9ef2eb8db122f51a3dcb14174b15b
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-testing-gcc-amd64-makefile.yml
@@ -0,0 +1,46 @@
+Debian testing GCC Makefile:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  image: debian:testing-slim
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "testing-gcc-makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-gcc-makefile"
+
+  variables:
+    CC: gcc
+    CXX: g++
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++
+      - |
+          # 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 libminiupnpc-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"
diff --git a/.gitlab/ci/jobs/debian-testing-gcc-amd64.yml b/.gitlab/ci/jobs/debian-testing-gcc-amd64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9d7020e4c6c3ed0661a4652c485f29e5e37c90ff
--- /dev/null
+++ b/.gitlab/ci/jobs/debian-testing-gcc-amd64.yml
@@ -0,0 +1,61 @@
+Debian testing GCC:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  image: debian:testing-slim
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "testing-gcc"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-gcc"
+
+  variables:
+    CC: gcc
+    CXX: g++
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++
+      - |
+          # 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 libminiupnpc-dev
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=ON
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/macos-arm64.yml b/.gitlab/ci/jobs/macos-arm64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..558f7faff1d69c928cabd2e8ab872562065bd592
--- /dev/null
+++ b/.gitlab/ci/jobs/macos-arm64.yml
@@ -0,0 +1,89 @@
+osxcross arm64:
+  extends: .srb2ci
+
+  stage: build
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "build.arm64/bin/"
+      - "build.arm64/dist/arm64.h"
+      - "build.arm64/src/config.h"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-arm64-apple-darwin"
+
+  variables:
+    OSXCROSS_HOST: oa64
+    CMAKE_TOOLCHAIN_FILE: /osxcross/toolchain.cmake
+    LD: /opt/osxcross.arm64/ld
+
+  script:
+    - - |
+          # apt_development
+          echo -e "\e[0Ksection_start:`date +%s`:macports_development[collapsed=true]\r\e[0KInstalling development packages"
+      - osxcross-macports install --arm64 libopenmpt      || osxcross-macports install --verbose --arm64 libopenmpt || true
+      - osxcross-macports install --arm64 wavpack         || osxcross-macports install --verbose --arm64 wavpack    || true
+      - osxcross-macports install --arm64 libxmp opusfile || osxcross-macports install --verbose --arm64 libxmp opusfile
+      - osxcross-macports install --static --arm64 libsdl2_mixer || osxcross-macports install --verbose --static --arm64 libsdl2_mixer || true
+      - osxcross-macports install --static --arm64 curl          || osxcross-macports install --verbose --static --arm64 curl          || true
+      - osxcross-macports install --static --arm64 miniupnpc libpng || osxcross-macports install --verbose --static --arm64 miniupnpc libpng
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:macports_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.arm64 \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DCPM_USE_LOCAL_PACKAGES:BOOL=ON \
+            -DOPENMPT_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/include" \
+            -DSDL2_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/lib" \
+            -DSRB2_CONFIG_ENABLE_TESTS:BOOL=OFF \
+            -DSRB2_CONFIG_SYSTEM_LIBRARIES:BOOL=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=OFF \
+            -DSRB2_SDL2_EXE_NAME=srb2_$CI_PIPELINE_ID \
+            -DSRB2_CONFIG_FORCE_NO_MS_BITFIELDS:BOOL=ON
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.arm64 --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+    - - |
+          # copy config.h
+          echo -e "\e[0Ksection_start:`date +%s`:copy[collapsed=false]\r\e[0KCopying config.h"
+      - mkdir --parents --verbose build.arm64/dist
+      - cp --reflink=auto --sparse=always --verbose build.arm64/src/config.h build.arm64/dist/arm64.h
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:copy\r\e[0K"
+
+
+  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
+      - ccache --show-log-stats || true
+      - |
+          # ccahe_stats
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
diff --git a/.gitlab/ci/jobs/macos-x86_64.yml b/.gitlab/ci/jobs/macos-x86_64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..73b4034b00cedeb0d9667b0cdebcbce0621ab595
--- /dev/null
+++ b/.gitlab/ci/jobs/macos-x86_64.yml
@@ -0,0 +1,88 @@
+osxcross x86_64:
+  extends: .srb2ci
+
+  stage: build
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "build.x86_64/bin/"
+      - "build.x86_64/dist/x86_64.h"
+      - "build.x86_64/src/config.h"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-x86_64-apple-darwin"
+
+  variables:
+    OSXCROSS_HOST: o64
+    CMAKE_TOOLCHAIN_FILE: /osxcross/toolchain.cmake
+    LD: /opt/osxcross.x86_64/ld
+
+  script:
+    - - |
+          # apt_development
+          echo -e "\e[0Ksection_start:`date +%s`:macports_development[collapsed=true]\r\e[0KInstalling development packages"
+      - osxcross-macports install         libopenmpt      || osxcross-macports install --verbose         libopenmpt || true
+      - osxcross-macports install         wavpack         || osxcross-macports install --verbose         wavpack    || true
+      - osxcross-macports install         libxmp opusfile || osxcross-macports install --verbose         libxmp opusfile
+      - osxcross-macports install --static         libsdl2_mixer || osxcross-macports install --verbose --static         libsdl2_mixer || true
+      - osxcross-macports install --static         curl          || osxcross-macports install --verbose --static         curl          || true
+      - osxcross-macports install --static         miniupnpc libpng || osxcross-macports install --verbose --static         miniupnpc libpng
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:macports_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.x86_64 \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DCPM_USE_LOCAL_PACKAGES:BOOL=ON \
+            -DOPENMPT_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/include" \
+            -DSDL2_INCLUDE_DIR:PATH="/osxcross/macports/pkgs/opt/local/lib" \
+            -DSRB2_CONFIG_ENABLE_TESTS:BOOL=OFF \
+            -DSRB2_CONFIG_SYSTEM_LIBRARIES:BOOL=ON \
+            -DSRB2_CONFIG_USE_GME:BOOL=OFF \
+            -DSRB2_SDL2_EXE_NAME=srb2_$CI_PIPELINE_ID
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.x86_64 --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+    - - |
+          # copy config.h
+          echo -e "\e[0Ksection_start:`date +%s`:copy[collapsed=false]\r\e[0KCopying config.h"
+      - mkdir --parents --verbose build.x86_64/dist
+      - cp --reflink=auto --sparse=always --verbose build.x86_64/src/config.h build.x86_64/dist/x86_64.h
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:copy\r\e[0K"
+
+
+  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
+      - ccache --show-log-stats || true
+      - |
+          # ccahe_stats
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
diff --git a/.gitlab/ci/jobs/osxcross-universal.yml b/.gitlab/ci/jobs/osxcross-universal.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e76f6dda4be1d45b291683667be24fa120ab468f
--- /dev/null
+++ b/.gitlab/ci/jobs/osxcross-universal.yml
@@ -0,0 +1,69 @@
+osxcross universal:
+  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:stable
+
+  dependencies:
+    - osxcross arm64
+    - osxcross x86_64
+  needs:
+    - job: osxcross arm64
+    - job: osxcross x86_64
+
+  stage: osxcross
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "dist/bin"
+      - "dist/src"
+    expose_as: "Mac Universal"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-lipo-apple-darwin"
+
+  script:
+    - - |
+         # mkdir
+         echo -e "\e[0Ksection_start:`date +%s`:mkdir[collapsed=true]\r\e[0KMaking dist folder"
+         mkdir --parents --verbose dist/src dist/bin
+      - |
+          # mkdir
+          echo -e "\e[0Ksection_end:`date +%s`:mkdir\r\e[0K"
+
+    - - |
+         # copy-config
+         echo -e "\e[0Ksection_start:`date +%s`:x86_64-config[collapsed=true]\r\e[0KCopying x86_64 config"
+      - cp --reflink=auto --sparse=always --verbose --target-directory=dist/src/ build.*/dist/*.h
+      - |
+          # x86_64-config
+          echo -e "\e[0Ksection_end:`date +%s`:x86_64-config\r\e[0K"
+
+    - - |
+         # copy-build
+         echo -e "\e[0Ksection_start:`date +%s`:copy-build[collapsed=true]\r\e[0KCopying ALL build"
+      - cp --reflink=auto --sparse=always --recursive --verbose --target-directory=dist/ build.*/bin/
+      - |
+          # copy-build
+          echo -e "\e[0Ksection_end:`date +%s`:copy-build\r\e[0K"
+
+    - - |
+         # link-build
+         echo -e "\e[0Ksection_start:`date +%s`:link-build[collapsed=true]\r\e[0KLinking universal build"
+      - lipo -create -output dist/bin/srb2_$CI_PIPELINE_ID.app/Contents/MacOS/srb2_$CI_PIPELINE_ID build.*/bin/srb2_$CI_PIPELINE_ID.app/Contents/MacOS/srb2_$CI_PIPELINE_ID
+      - |
+          # universal-build
+          echo -e "\e[0Ksection_end:`date +%s`:link-build\r\e[0K"
+
+    - - |
+         # arm64-verify
+         echo -e "\e[0Ksection_start:`date +%s`:arm64-verify[collapsed=true]\r\e[0KVerifying arm64"
+      - lipo dist/bin/srb2_$CI_PIPELINE_ID.app/Contents/MacOS/srb2_$CI_PIPELINE_ID -verify_arch arm64
+      - |
+          # arm64-verify
+          echo -e "\e[0Ksection_end:`date +%s`:arm64-verify\r\e[0K"
+
+    - - |
+         # x86_64-verify
+         echo -e "\e[0Ksection_start:`date +%s`:x86_64-verify[collapsed=true]\r\e[0KVerifying x86_64"
+      - lipo dist/bin/srb2_$CI_PIPELINE_ID.app/Contents/MacOS/srb2_$CI_PIPELINE_ID -verify_arch x86_64
+      - |
+          # x86_64-verify
+          echo -e "\e[0Ksection_end:`date +%s`:x86_64-verify\r\e[0K"
diff --git a/.gitlab/ci/jobs/windows-x64-makefile.yml b/.gitlab/ci/jobs/windows-x64-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aea9de4ce1ec95466acf60c2875377266a6a3fde
--- /dev/null
+++ b/.gitlab/ci/jobs/windows-x64-makefile.yml
@@ -0,0 +1,37 @@
+Windows x64 Makefile:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Win64-makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Win64-makefile"
+
+  variables:
+    PREFIX: x86_64-w64-mingw32
+    CC: /usr/bin/x86_64-w64-mingw32-gcc
+    CXX: /usr/bin/x86_64-w64-mingw32-g++
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-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"
diff --git a/.gitlab/ci/jobs/windows-x64.yml b/.gitlab/ci/jobs/windows-x64.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6db0de9a766453f9d6790d381b1a7fef9dbe86e7
--- /dev/null
+++ b/.gitlab/ci/jobs/windows-x64.yml
@@ -0,0 +1,92 @@
+Windows x64:
+  extends: .srb2ci
+
+  stage: build
+
+  when: manual
+
+  allow_failure: true
+
+  cache:
+    - key: ccache-$CI_JOB_NAME_SLUG-$CI_COMMIT_REF_SLUG
+      fallback_keys:
+        - ccache-$CI_JOB_NAME_SLUG-$CI_DEFAULT_BRANCH
+        - ccache-$CI_JOB_NAME_SLUG-master
+      paths:
+        - build/ccache
+        - build/ccache_statslog
+
+    - key: apt-$CI_JOB_IMAGE
+      paths:
+        - build/apt-cache
+      unprotect: true
+
+    - key: vcpkg-binary-cache-x64-mingw-static
+      paths:
+        - build/vcpkg-binary-cache
+      unprotect: true
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "Win64"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Win64"
+
+  variables:
+    VCPKG_TARGET_TRIPLET: x64-mingw-static
+    CC: x86_64-w64-mingw32-gcc
+    CXX: x86_64-w64-mingw32-g++
+    LD: x86_64-w64-mingw32-ld
+
+  script:
+    - |
+        # vcpkg
+        echo -e "\e[0Ksection_start:`date +%s`:vcpkg-root[collapsed=true]\r\e[0KSetting vcpkg cache"
+
+        export VCPKG_DEFAULT_BINARY_CACHE="$(pwd)/build/vcpkg-binary-cache"
+
+        mkdir -p "build/vcpkg-binary-cache"
+
+        echo -e "\e[0Ksection_end:`date +%s`:vcpkg-root\r\e[0K"
+
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-mingw-w64-x86-64-win32
+      - |
+          # 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 ninja-build
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
+            -DVCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET} \
+            -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/toolchains/mingw.cmake
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/jobs/windows-x86-makefile.yml b/.gitlab/ci/jobs/windows-x86-makefile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..213342cda81e119827937a79df8b1f74242d7ef2
--- /dev/null
+++ b/.gitlab/ci/jobs/windows-x86-makefile.yml
@@ -0,0 +1,35 @@
+Windows x86 Makefile:
+  extends: .srb2ci
+
+  stage: build
+
+  when: on_success
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Win32-makefile"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Win32-makefile"
+
+  variables:
+    PREFIX: i686-w64-mingw32
+    CC: /usr/bin/i686-w64-mingw32-gcc-posix
+    CXX: /usr/bin/i686-w64-mingw32-g++-posix
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-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"
diff --git a/.gitlab/ci/jobs/windows-x86.yml b/.gitlab/ci/jobs/windows-x86.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0a1b24e18146efedd16715d66927778b6965213a
--- /dev/null
+++ b/.gitlab/ci/jobs/windows-x86.yml
@@ -0,0 +1,88 @@
+Windows x86:
+  extends: .srb2ci
+
+  stage: build
+
+  cache:
+    - key: ccache-$CI_JOB_NAME_SLUG-$CI_COMMIT_REF_SLUG
+      fallback_keys:
+        - ccache-$CI_JOB_NAME_SLUG-$CI_DEFAULT_BRANCH
+        - ccache-$CI_JOB_NAME_SLUG-master
+      paths:
+        - build/ccache
+        - build/ccache_statslog
+
+    - key: apt-$CI_JOB_IMAGE
+      paths:
+        - build/apt-cache
+      unprotect: true
+
+    - key: vcpkg-binary-cache-x86-mingw-static
+      paths:
+        - build/vcpkg-binary-cache
+      unprotect: true
+
+  artifacts:
+    paths:
+      - "build.cmake/bin/"
+      - "build.cmake/src/config.h"
+    expose_as: "Win32"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Win32"
+
+  variables:
+    VCPKG_TARGET_TRIPLET: x86-mingw-static
+    CC: i686-w64-mingw32-gcc
+    CXX: i686-w64-mingw32-g++
+    LD: i686-w64-mingw32-ld
+
+  script:
+    - |
+        # vcpkg
+        echo -e "\e[0Ksection_start:`date +%s`:vcpkg-root[collapsed=true]\r\e[0KSetting vcpkg cache"
+
+        export VCPKG_DEFAULT_BINARY_CACHE="$(pwd)/build/vcpkg-binary-cache"
+
+        mkdir -p "build/vcpkg-binary-cache"
+
+        echo -e "\e[0Ksection_end:`date +%s`:vcpkg-root\r\e[0K"
+
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install g++-mingw-w64-i686-win32
+      - |
+          # 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 ninja-build
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # cmake
+          echo -e "\e[0Ksection_start:`date +%s`:cmake[collapsed=false]\r\e[0KBuilding Makefiles"
+      - |
+          cmake \
+            -B build.cmake \
+            -G "Unix Makefiles" \
+            -DCMAKE_COLOR_DIAGNOSTICS=OFF \
+            -DSRB2_CONFIG_ENABLE_WEBM_MOVIES=OFF \
+            -DSRB2_CONFIG_ERRORMODE=ON \
+            -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
+            -DVCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET} \
+            -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/toolchains/mingw.cmake
+      - |
+          # cmake
+          echo -e "\e[0Ksection_end:`date +%s`:cmake\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - cmake --build build.cmake --parallel 1 -- --keep-going
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
diff --git a/.gitlab/ci/templates/srb2ci.yml b/.gitlab/ci/templates/srb2ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..765048614669797010b8af65afcd4cf7bb27f72a
--- /dev/null
+++ b/.gitlab/ci/templates/srb2ci.yml
@@ -0,0 +1,143 @@
+.srb2ci:
+  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:stable
+
+  cache:
+    - key: ccache-$CI_JOB_NAME_SLUG-$CI_COMMIT_REF_SLUG
+      fallback_keys:
+        - ccache-$CI_JOB_NAME_SLUG-$CI_DEFAULT_BRANCH
+        - ccache-$CI_JOB_NAME_SLUG-master
+      paths:
+        - build/ccache
+        - build/ccache_statslog
+
+    - key: apt-$CI_JOB_IMAGE
+      paths:
+        - build/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=$CI_PROJECT_DIR/build/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"
+      - timeout 2m apt-get update || timeout 2m 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 cmake ca-certificates
+      - |
+          # 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"
+      - |
+          # cache.conf
+          echo Adding ccache configution option
+      - |
+          # base_dir
+          ccache --set-config base_dir=$CI_PROJECT_DIR
+      - |
+          # cache_dir
+          ccache --set-config cache_dir=$CI_PROJECT_DIR/build/ccache
+      - |
+          # compiler_check
+          ccache --set-config compiler_check=content
+      - |
+          # stats_log
+          ccache --set-config stats_log=$CI_PROJECT_DIR/build/ccache_statslog || true
+      - |
+          # max_size
+          ccache --set-config max_size=300M
+      - |
+          # 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"
+
+  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
+      - ccache --show-log-stats || true
+      - |
+          # ccahe_stats
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 358e62cc4d5bb72631cbcbe50f7b512a1e97da2a..a4c631102606ee3177c94f1bb7a35b100694bf4c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,7 +8,7 @@ endif()
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
 
 include(CMakeDependentOption)
-include(cmake/CPM.cmake)
+include(CheckCXXCompilerFlag)
 
 file(STRINGS src/version.h SRB2_VERSION)
 string(REGEX MATCH "[0-9]+\\.[0-9.]+" SRB2_VERSION ${SRB2_VERSION})
@@ -46,28 +46,23 @@ SET(CPACK_OUTPUT_FILE_PREFIX package)
 include(CPack)
 
 # Options
-
-if("${CMAKE_SYSTEM_NAME}" MATCHES Linux)
-	set(SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT ON)
+if("${CMAKE_SYSTEM_NAME}" MATCHES Windows)
+	if(DEFINED VCPKG_TARGET_TRIPLET)
+		set(SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT ON)
+	else()
+		set(SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT OFF)
+	endif()
 else()
-	set(SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT OFF)
+	set(SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT ON)
 endif()
 
 # Clang tidy options will be ignored if CMAKE_<LANG>_CLANG_TIDY are set.
 option(SRB2_CONFIG_ENABLE_CLANG_TIDY_C "Enable default clang-tidy check configuration for C" OFF)
 option(SRB2_CONFIG_ENABLE_CLANG_TIDY_CXX "Enable default clang-tidy check configuration for C++" OFF)
 option(
-	SRB2_CONFIG_SYSTEM_LIBRARIES
-	"Link dependencies using CMake's find_package and do not use internal builds"
-	${SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT}
-)
-option(SRB2_CONFIG_ENABLE_TESTS "Build the test suite" ON)
-# This option isn't recommended for distribution builds and probably won't work (yet).
-cmake_dependent_option(
-	SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES
-	"Use dynamic libraries when compiling internal dependencies"
-	OFF "NOT SRB2_CONFIG_SYSTEM_LIBRARIES"
-	OFF
+	SRB2_CONFIG_STATIC_STDLIB
+	"Link static version of standard library. All dependencies must also be static"
+	ON
 )
 option(SRB2_CONFIG_HWRENDER "Enable hardware render (OpenGL) support" ON)
 option(SRB2_CONFIG_USE_GME "Enable GME playback support" OFF)
@@ -78,67 +73,32 @@ option(SRB2_CONFIG_MOBJCONSISTANCY "Compile with MOBJCONSISTANCY defined." OFF)
 option(SRB2_CONFIG_PACKETDROP "Compile with PACKETDROP defined." OFF)
 option(SRB2_CONFIG_EXECINFO "Enable stack trace dump support." ON)
 option(SRB2_CONFIG_ZDEBUG "Compile with ZDEBUG defined." OFF)
+option(SRB2_CONFIG_FORCE_NO_MS_BITFIELDS "Compile without -mno-ms-bitfields compiler flag" OFF)
 # SRB2_CONFIG_PROFILEMODE is probably superceded by some CMake setting.
 option(SRB2_CONFIG_PROFILEMODE "Compile for profiling (GCC only)." OFF)
 set(SRB2_CONFIG_ASSET_DIRECTORY "" CACHE PATH "Path to directory that contains all asset files for the installer. If set, assets will be part of installation and cpack.")
 
-if(SRB2_CONFIG_ENABLE_TESTS)
-	# https://github.com/catchorg/Catch2
-	CPMAddPackage(
-		NAME Catch2
-		VERSION 3.4.0
-		GITHUB_REPOSITORY catchorg/Catch2
-		OPTIONS
-			"CATCH_INSTALL_DOCS OFF"
-	)
-	list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/extras")
-	include(CTest)
-	include(Catch)
-	add_executable(srb2tests)
-	# To add tests, use target_sources to add individual test files to the target in subdirs.
-	target_link_libraries(srb2tests PRIVATE Catch2::Catch2 Catch2::Catch2WithMain)
-	target_compile_features(srb2tests PRIVATE c_std_11 cxx_std_17)
-	catch_discover_tests(srb2tests)
-endif()
+# Dependencies
+add_subdirectory(thirdparty)
 
-# Enable CCache
-# (Set USE_CCACHE=ON to use, CCACHE_OPTIONS for options)
-if("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL Windows)
-	option(USE_CCACHE "Enable ccache support" OFF)
-
-	if(USE_CCACHE)
-		find_program(CCACHE_TOOL_PATH ccache)
-		if(CCACHE_TOOL_PATH)
-			set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_TOOL_PATH} CACHE STRING "" FORCE)
-			set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_TOOL_PATH} CACHE STRING "" FORCE)
-		else()
-			message(WARNING "USE_CCACHE was set but ccache is not found (set CCACHE_TOOL_PATH)")
-		endif()
-	endif()
+if(SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES)
+	set(SRB2_INTERNAL_LIBRARY_TYPE SHARED)
+	set(NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES OFF)
 else()
-	CPMAddPackage(
-		NAME Ccache.cmake
-		GITHUB_REPOSITORY TheLartians/Ccache.cmake
-		VERSION 1.2
-	)
+	set(SRB2_INTERNAL_LIBRARY_TYPE STATIC)
+	set(NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES ON)
 endif()
 
-# Dependencies
-add_subdirectory(thirdparty)
 
-if("${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	find_package(ZLIB REQUIRED)
-	find_package(PNG REQUIRED)
-	find_package(SDL2 REQUIRED)
-	find_package(SDL2_mixer REQUIRED)
-	find_package(CURL REQUIRED)
-	find_package(OPENMPT REQUIRED)
-
-	# libgme defaults to "Nuked" YM2612 emulator, which is
-	# very SLOW. The system library probably uses the
-	# default so just always build it.
-	#find_package(GME REQUIRED)
+find_package(ZLIB REQUIRED)
+find_package(PNG REQUIRED)
+find_package(CURL REQUIRED)
+find_package(libopenmpt QUIET)
+if("${SRB2_CONFIG_USE_GME}")
+	find_package(libgme QUIET)
 endif()
+find_package(miniupnpc QUIET)
+
 
 if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR})
 	message(FATAL_ERROR "In-source builds will bring you a world of pain. Please make a separate directory to invoke CMake from.")
diff --git a/CMakePresets.json b/CMakePresets.json
index effea7260656c6235c75b07669edea01eeb682b7..8cb95d797f7f4c3ae708effa15e0d38112175937 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -2,26 +2,263 @@
 	"version": 3,
 	"configurePresets": [
 		{
-			"name": "default",
-			"description": "Build using default generator",
-			"binaryDir": "build",
+			"name": "__debug",
+			"hidden": true,
 			"cacheVariables": {
+				"SRB2_CONFIG_DEV_BUILD": "ON",
+				"CMAKE_BUILD_TYPE": "Debug"
+			}
+		},
+		{
+			"name": "__develop",
+			"hidden": true,
+			"cacheVariables": {
+				"CMAKE_C_FLAGS_RELWITHDEBINFO": "-DNDEBUG",
+				"CMAKE_CXX_FLAGS_RELWITHDEBINFO": "-DNDEBUG",
+				"SRB2_CONFIG_DEV_BUILD": "ON",
 				"CMAKE_BUILD_TYPE": "RelWithDebInfo"
 			}
 		},
 		{
-			"name": "debug",
-			"description": "Build for development (no optimizations)",
-			"inherits": "default",
+			"name": "__release",
+			"hidden": true,
 			"cacheVariables": {
-				"CMAKE_BUILD_TYPE": "Debug"
+				"CMAKE_C_FLAGS_RELWITHDEBINFO": "-DNDEBUG",
+				"CMAKE_CXX_FLAGS_RELWITHDEBINFO": "-DNDEBUG",
+				"SRB2_CONFIG_DEV_BUILD": "OFF",
+				"CMAKE_BUILD_TYPE": "RelWithDebInfo"
+			}
+		},
+		{
+			"name": "__testers",
+			"hidden": true,
+			"cacheVariables": {
+				"CMAKE_C_FLAGS_RELWITHDEBINFO": "-DNDEBUG",
+				"CMAKE_CXX_FLAGS_RELWITHDEBINFO": "-DNDEBUG",
+				"SRB2_CONFIG_DEV_BUILD": "ON",
+				"CMAKE_BUILD_TYPE": "RelWithDebInfo",
+				"SRB2_CONFIG_TESTERS": "ON"
+			}
+		},
+		{
+			"name": "__ninja",
+			"hidden": true,
+			"generator": "Ninja",
+			"cacheVariables": {
+				"CMAKE_COLOR_DIAGNOSTICS": "ON"
+			}
+		},
+		{
+			"name": "__vcpkg-toolchain",
+			"hidden": true,
+			"cacheVariables": {
+				"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
+			}
+		},
+		{
+			"name": "__compiler-mingw-w64-i686",
+			"hidden": true,
+			"cacheVariables": {
+				"CMAKE_C_COMPILER": "i686-w64-mingw32-gcc",
+				"CMAKE_CXX_COMPILER": "i686-w64-mingw32-g++"
+			}
+		},
+		{
+			"name": "__mingw-dynamic",
+			"hidden": true,
+			"cacheVariables": {
+				"VCPKG_TARGET_TRIPLET": "x86-mingw-dynamic"
 			}
+		},
+		{
+			"name": "__mingw-static",
+			"hidden": true,
+			"cacheVariables": {
+				"VCPKG_HOST_TRIPLET": "x86-mingw-static",
+				"VCPKG_TARGET_TRIPLET": "x86-mingw-static"
+			}
+		},
+		{
+			"name": "__osx_x64",
+			"hidden": true,
+			"cacheVariables": {
+				"VCPKG_TARGET_TRIPLET": "x64-osx"
+			}
+		},
+		{
+			"name": "__osx_arm64",
+			"hidden": true,
+			"cacheVariables": {
+				"VCPKG_TARGET_TRIPLET": "arm64-osx"
+			}
+		},
+
+		{
+			"name": "ninja-debug",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__debug", "__ninja"]
+		},
+		{
+			"name": "ninja-develop",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__develop", "__ninja"]
+		},
+		{
+			"name": "ninja-release",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__release", "__ninja"]
+		},
+		{
+			"name": "ninja-testers",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__testers", "__ninja"]
+		},
+
+		{
+			"name": "ninja-vcpkg-debug",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__debug", "__ninja", "__vcpkg-toolchain"]
+		},
+		{
+			"name": "ninja-vcpkg-develop",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__develop", "__ninja", "__vcpkg-toolchain"]
+		},
+		{
+			"name": "ninja-vcpkg-release",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__release", "__ninja", "__vcpkg-toolchain"]
+		},
+		{
+			"name": "ninja-vcpkg-testers",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__testers", "__ninja", "__vcpkg-toolchain"]
+		},
+
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-debug",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__debug", "__compiler-mingw-w64-i686", "__ninja", "__vcpkg-toolchain", "__mingw-static"]
+		},
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-develop",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__develop", "__compiler-mingw-w64-i686", "__ninja", "__vcpkg-toolchain", "__mingw-static"]
+		},
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-release",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__release", "__compiler-mingw-w64-i686", "__ninja", "__vcpkg-toolchain", "__mingw-static"]
+		},
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-testers",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__testers", "__compiler-mingw-w64-i686", "__ninja", "__vcpkg-toolchain", "__mingw-static"]
+		},
+
+		{
+			"name": "ninja-x64_osx_vcpkg-debug",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__debug", "__ninja", "__vcpkg-toolchain", "__osx_x64"]
+		},
+		{
+			"name": "ninja-x64_osx_vcpkg-develop",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__develop", "__ninja", "__vcpkg-toolchain", "__osx_x64"]
+		},
+		{
+			"name": "ninja-x64_osx_vcpkg-release",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__release", "__ninja", "__vcpkg-toolchain", "__osx_x64"]
+		},
+
+		{
+			"name": "ninja-arm64_osx_vcpkg-debug",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__debug", "__ninja", "__vcpkg-toolchain", "__osx_arm64"]
+		},
+		{
+			"name": "ninja-arm64_osx_vcpkg-develop",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__develop", "__ninja", "__vcpkg-toolchain", "__osx_arm64"]
+		},
+		{
+			"name": "ninja-arm64_osx_vcpkg-release",
+			"hidden": false,
+			"binaryDir": "build/${presetName}",
+			"inherits": ["__release", "__ninja", "__vcpkg-toolchain", "__osx_arm64"]
 		}
 	],
+
 	"buildPresets": [
 		{
-			"name": "default",
-			"configurePreset": "default"
+			"name": "ninja-debug",
+			"configurePreset": "ninja-debug"
+		},
+		{
+			"name": "ninja-develop",
+			"configurePreset": "ninja-develop"
+		},
+		{
+			"name": "ninja-release",
+			"configurePreset": "ninja-release"
+		},
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-debug",
+			"configurePreset": "ninja-x86_mingw_static_vcpkg-debug"
+		},
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-develop",
+			"configurePreset": "ninja-x86_mingw_static_vcpkg-develop"
+		},
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-release",
+			"configurePreset": "ninja-x86_mingw_static_vcpkg-release"
+		},
+		{
+			"name": "ninja-x86_mingw_static_vcpkg-testers",
+			"configurePreset": "ninja-x86_mingw_static_vcpkg-testers"
+		},
+		{
+			"name": "ninja-x64_osx_vcpkg-debug",
+			"configurePreset": "ninja-x64_osx_vcpkg-debug"
+		},
+		{
+			"name": "ninja-x64_osx_vcpkg-develop",
+			"configurePreset": "ninja-x64_osx_vcpkg-develop"
+		},
+		{
+			"name": "ninja-x64_osx_vcpkg-release",
+			"configurePreset": "ninja-x64_osx_vcpkg-release"
+		},
+		{
+			"name": "ninja-arm64_osx_vcpkg-debug",
+			"configurePreset": "ninja-arm64_osx_vcpkg-debug"
+		},
+		{
+			"name": "ninja-arm64_osx_vcpkg-develop",
+			"configurePreset": "ninja-arm64_osx_vcpkg-develop"
+		},
+		{
+			"name": "ninja-arm64_osx_vcpkg-release",
+			"configurePreset": "ninja-arm64_osx_vcpkg-release"
 		}
 	]
-}
+}
\ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
index d0d94b9828b56bc70f2e60bb18064bf9c933a099..130b0deccbf43e52ebfa40f4984cea82a24c5135 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.2.14.{branch}-{build}
+version: 2.2.16.{branch}-{build}
 os: MinGW
 
 environment:
diff --git a/assets/CMakeLists.txt b/assets/CMakeLists.txt
index dfb2f4180db7dbbae9a454a6f939aeda9e6f26a4..68c84819513075a90e21917ce797acf893c2a40d 100644
--- a/assets/CMakeLists.txt
+++ b/assets/CMakeLists.txt
@@ -27,10 +27,10 @@ list(TRANSFORM SRB2_ASSETS_DOCS PREPEND "${SRB2_ASSET_DIRECTORY_ABSOLUTE}")
 
 set(SRB2_ASSETS_GAME
 	"srb2.pk3"
-	"player.dta"
+	"characters.pk3"
 	"zones.pk3"
 	"patch.pk3"
-	"music.dta"
+	"music.pk3"
 	"models.dat"
 )
 list(TRANSFORM SRB2_ASSETS_GAME PREPEND "/")
diff --git a/assets/README.txt b/assets/README.txt
index e384333ed762459cd140fff13fdb777036d3d698..1ddacc999d7bde07f18e9c4fbb9041467dbda619 100644
--- a/assets/README.txt
+++ b/assets/README.txt
@@ -33,13 +33,10 @@ https://discord.gg/b3BGb8A
 Twitter:
 https://twitter.com/SonicTeamJr
 
-Facebook:
-https://facebook.com/SonicRoboBlast2
-
 
 COPYRIGHT AND DISCLAIMER
 
-Design and content in Sonic Robo Blast 2 is copyright 1998-2023 by Sonic Team Jr.
+Design and content in Sonic Robo Blast 2 is copyright 1998-2025 by Sonic Team Jr.
 
 All original material in this game is copyrighted by their respective owners, and no copyright infringement is intended. Sonic Team Jr. is in no way affiliated with SEGA or Sonic Team, and we do not claim ownership of any of SEGA's intellectual property used in SRB2.
 
diff --git a/assets/debian-template/copyright b/assets/debian-template/copyright
index cc47c453bf2b0ccc6db34c57a03dfe1ce2fc75a7..71d2cf583d6e6e624ad7a12638ea65df775f4223 100644
--- a/assets/debian-template/copyright
+++ b/assets/debian-template/copyright
@@ -12,7 +12,7 @@ Upstream Author(s):
 
 Copyright:
 
-    Copyright (C) 1998-2018 by Sonic Team Junior
+    Copyright (C) 1998-2025 by Sonic Team Junior
 
 License:
 
@@ -21,7 +21,7 @@ License:
 The Debian packaging is:
 
     Copyright (C) 2010 Callum Dickinson <gcfreak_ag20@hotmail.com>
-    Copyright (C) 2010-2018 by Sonic Team Junior <stjr@srb2.org>
+    Copyright (C) 2010-2025 by Sonic Team Junior <stjr@srb2.org>
 
 and is licensed under the GPL version 2,
 see "/usr/share/common-licenses/GPL-2".
diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake
deleted file mode 100644
index fba27d2fef3d2bf2edb50ea41687157dc0730c20..0000000000000000000000000000000000000000
--- a/cmake/CPM.cmake
+++ /dev/null
@@ -1,21 +0,0 @@
-set(CPM_DOWNLOAD_VERSION 0.38.7)
-
-if(CPM_SOURCE_CACHE)
-  set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
-elseif(DEFINED ENV{CPM_SOURCE_CACHE})
-  set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
-else()
-  set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
-endif()
-
-# Expand relative path. This is important if the provided path contains a tilde (~)
-get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE)
-if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION}))
-  message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}")
-  file(DOWNLOAD
-       https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake
-       ${CPM_DOWNLOAD_LOCATION}
-  )
-endif()
-
-include(${CPM_DOWNLOAD_LOCATION})
diff --git a/cmake/Modules/FindGME.cmake b/cmake/Modules/FindGME.cmake
deleted file mode 100644
index 3af0a94be604f44cf0eda200f3d90a129ea77809..0000000000000000000000000000000000000000
--- a/cmake/Modules/FindGME.cmake
+++ /dev/null
@@ -1,33 +0,0 @@
-include(LibFindMacros)
-
-libfind_pkg_check_modules(GME_PKGCONF GME)
-
-find_path(GME_INCLUDE_DIR
-	NAMES gme.h
-	PATHS
-		${GME_PKGCONF_INCLUDE_DIRS}
-		"/usr/include/gme"
-		"/usr/local/include/gme"
-)
-
-find_library(GME_LIBRARY
-	NAMES gme
-	PATHS
-		${GME_PKGCONF_LIBRARY_DIRS}
-		"/usr/lib"
-		"/usr/local/lib"
-)
-
-set(GME_PROCESS_INCLUDES GME_INCLUDE_DIR)
-set(GME_PROCESS_LIBS GME_LIBRARY)
-libfind_process(GME)
-
-if(GME_FOUND AND NOT TARGET gme)
-	add_library(gme UNKNOWN IMPORTED)
-	set_target_properties(
-		gme
-		PROPERTIES
-		IMPORTED_LOCATION "${GME_LIBRARY}"
-		INTERFACE_INCLUDE_DIRECTORIES "${GME_INCLUDE_DIR}"
-	)
-endif()
diff --git a/cmake/Modules/FindOPENMPT.cmake b/cmake/Modules/FindOPENMPT.cmake
deleted file mode 100644
index 7e5b2d5a3966be22d22afdb815934cf20fe0e4d7..0000000000000000000000000000000000000000
--- a/cmake/Modules/FindOPENMPT.cmake
+++ /dev/null
@@ -1,33 +0,0 @@
-include(LibFindMacros)
-
-libfind_pkg_check_modules(OPENMPT_PKGCONF OPENMPT)
-
-find_path(OPENMPT_INCLUDE_DIR
-	NAMES libopenmpt.h
-	PATHS
-		${OPENMPT_PKGCONF_INCLUDE_DIRS}
-		"/usr/include/libopenmpt"
-		"/usr/local/include/libopenmpt"
-)
-
-find_library(OPENMPT_LIBRARY
-	NAMES openmpt
-	PATHS
-		${OPENMPT_PKGCONF_LIBRARY_DIRS}
-		"/usr/lib"
-		"/usr/local/lib"
-)
-
-set(OPENMPT_PROCESS_INCLUDES OPENMPT_INCLUDE_DIR)
-set(OPENMPT_PROCESS_LIBS OPENMPT_LIBRARY)
-libfind_process(OPENMPT)
-
-if(OPENMPT_FOUND AND NOT TARGET openmpt)
-	add_library(openmpt UNKNOWN IMPORTED)
-	set_target_properties(
-		openmpt
-		PROPERTIES
-		IMPORTED_LOCATION "${OPENMPT_LIBRARY}"
-		INTERFACE_INCLUDE_DIRECTORIES "${OPENMPT_INCLUDE_DIR}"
-	)
-endif()
diff --git a/cmake/Modules/Findlibgme.cmake b/cmake/Modules/Findlibgme.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..6b693a7fd14234eb89c24798ea340b6a39030d33
--- /dev/null
+++ b/cmake/Modules/Findlibgme.cmake
@@ -0,0 +1,38 @@
+include(LibFindMacros)
+
+libfind_pkg_check_modules(libgme_PKGCONF gme libgme)
+
+find_path(libgme_INCLUDE_DIR
+	NAMES gme.h
+	PATHS
+		${libgme_PKGCONF_INCLUDE_DIRS}
+		"/usr/include"
+		"/usr/local/include"
+	PATH_SUFFIXES
+		gme
+)
+
+find_library(libgme_LIBRARY
+	NAMES gme
+	PATHS
+		${libgme_PKGCONF_LIBRARY_DIRS}
+		"/usr/lib"
+		"/usr/local/lib"
+)
+
+include(FindPackageHandleStandardArgs)
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(libgme
+    REQUIRED_VARS libgme_LIBRARY libgme_INCLUDE_DIR)
+
+if(libgme_FOUND AND NOT TARGET gme)
+	add_library(gme UNKNOWN IMPORTED)
+	set_target_properties(
+		gme
+		PROPERTIES
+		IMPORTED_LOCATION "${libgme_LIBRARY}"
+		INTERFACE_INCLUDE_DIRECTORIES "${libgme_INCLUDE_DIR}"
+	)
+	add_library(gme::gme ALIAS gme)
+endif()
+
+mark_as_advanced(libgme_LIBRARY libgme_INCLUDE_DIR)
diff --git a/cmake/Modules/Findlibopenmpt.cmake b/cmake/Modules/Findlibopenmpt.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..96cc3102677c0639bed76fe28cf60efd13e63429
--- /dev/null
+++ b/cmake/Modules/Findlibopenmpt.cmake
@@ -0,0 +1,39 @@
+include(LibFindMacros)
+
+libfind_pkg_check_modules(libopenmpt_PKGCONF openmpt libopenmpt)
+
+find_path(libopenmpt_INCLUDE_DIR
+	NAMES libopenmpt.h
+	PATHS
+		${libopenmpt_PKGCONF_INCLUDE_DIRS}
+		"${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include"
+		"/usr/include"
+		"/usr/local/include"
+	PATH_SUFFIXES
++		libopenmpt
+)
+
+find_library(libopenmpt_LIBRARY
+	NAMES openmpt
+	PATHS
+		${libopenmpt_PKGCONF_LIBRARY_DIRS}
+		"/usr/lib"
+		"/usr/local/lib"
+)
+
+include(FindPackageHandleStandardArgs)
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(libopenmpt
+    REQUIRED_VARS libopenmpt_LIBRARY libopenmpt_INCLUDE_DIR)
+
+if(libopenmpt_FOUND AND NOT TARGET openmpt)
+	add_library(openmpt UNKNOWN IMPORTED)
+	set_target_properties(
+		openmpt
+		PROPERTIES
+		IMPORTED_LOCATION "${libopenmpt_LIBRARY}"
+		INTERFACE_INCLUDE_DIRECTORIES "${libopenmpt_INCLUDE_DIR}"
+	)
+	add_library(libopenmpt::libopenmpt ALIAS openmpt)
+endif()
+
+mark_as_advanced(libopenmpt_LIBRARY libopenmpt_INCLUDE_DIR)
diff --git a/cmake/Modules/Findminiupnpc.cmake b/cmake/Modules/Findminiupnpc.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..f4931ad832eb856583be1c12daaf4d9883847ed3
--- /dev/null
+++ b/cmake/Modules/Findminiupnpc.cmake
@@ -0,0 +1,39 @@
+include(LibFindMacros)
+
+libfind_pkg_check_modules(libminiupnpc_PKGCONF miniupnpc libminiupnpc)
+
+find_path(libminiupnpc_INCLUDE_DIR
+	NAMES miniupnpc.h
+	PATHS
+		${libminiupnpc_PKGCONF_INCLUDE_DIRS}
+		"${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include"
+		"/usr/include"
+		"/usr/local/include"
+	PATH_SUFFIXES
+		miniupnpc
+)
+
+find_library(libminiupnpc_LIBRARY
+	NAMES miniupnpc
+	PATHS
+		${libminiupnpc_PKGCONF_LIBRARY_DIRS}
+		"/usr/lib"
+		"/usr/local/lib"
+)
+
+include(FindPackageHandleStandardArgs)
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(miniupnpc
+    REQUIRED_VARS libminiupnpc_LIBRARY libminiupnpc_INCLUDE_DIR)
+
+if(miniupnpc_FOUND AND NOT TARGET miniupnpc)
+	add_library(miniupnpc UNKNOWN IMPORTED)
+	set_target_properties(
+		miniupnpc
+		PROPERTIES
+		IMPORTED_LOCATION "${libminiupnpc_LIBRARY}"
+		INTERFACE_INCLUDE_DIRECTORIES "${libminiupnpc_INCLUDE_DIR}"
+	)
+	add_library(miniupnpc::miniupnpc ALIAS miniupnpc)
+endif()
+
+mark_as_advanced(libminiupnpc_LIBRARY libminiupnpc_INCLUDE_DIR)
diff --git a/cmake/PatchFile.cmake b/cmake/PatchFile.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..f80da3434d764fdbc1eb1f0219c38204517ee19a
--- /dev/null
+++ b/cmake/PatchFile.cmake
@@ -0,0 +1,30 @@
+# use GNU Patch from any platform
+
+if(WIN32)
+  # prioritize Git Patch on Windows as other Patches may be very old and incompatible.
+  find_package(Git)
+  if(Git_FOUND)
+    get_filename_component(GIT_DIR ${GIT_EXECUTABLE} DIRECTORY)
+    get_filename_component(GIT_DIR ${GIT_DIR} DIRECTORY)
+  endif()
+endif()
+
+find_program(PATCH
+NAMES patch
+HINTS ${GIT_DIR}
+PATH_SUFFIXES usr/bin
+)
+
+if(NOT PATCH)
+  message(FATAL_ERROR "Did not find GNU Patch")
+endif()
+
+execute_process(COMMAND ${PATCH} ${in_file} --input=${patch_file} --output=${out_file} --ignore-whitespace
+TIMEOUT 15
+COMMAND_ECHO STDOUT
+RESULT_VARIABLE ret
+)
+
+if(NOT ret EQUAL 0)
+  message(FATAL_ERROR "Failed to apply patch ${patch_file} to ${in_file} with ${PATCH}")
+endif()
\ No newline at end of file
diff --git a/cmake/Toolchains/mingw-w64-i686.cmake b/cmake/Toolchains/mingw-w64-i686.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..6b327291d79d905e7fd97e99196e8b0809fa8d06
--- /dev/null
+++ b/cmake/Toolchains/mingw-w64-i686.cmake
@@ -0,0 +1,16 @@
+set(CMAKE_SYSTEM_NAME Windows)
+set(TOOLCHAIN_PREFIX i686-w64-mingw32)
+
+# cross compilers to use for C, C++ and Fortran
+set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
+set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
+set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran)
+set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres)
+
+# target environment on the build host system
+set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX})
+
+# modify default behavior of FIND_XXX() commands
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
diff --git a/cmake/Toolchains/mingw-w64-x86_64.cmake b/cmake/Toolchains/mingw-w64-x86_64.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..2f22e13d8a5dc80e869c5ca2f0089e5d9917ff7d
--- /dev/null
+++ b/cmake/Toolchains/mingw-w64-x86_64.cmake
@@ -0,0 +1,16 @@
+set(CMAKE_SYSTEM_NAME Windows)
+set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)
+
+# cross compilers to use for C, C++ and Fortran
+set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
+set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
+set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran)
+set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres)
+
+# target environment on the build host system
+set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX})
+
+# modify default behavior of FIND_XXX() commands
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
diff --git a/debian-template/copyright b/debian-template/copyright
index cc47c453bf2b0ccc6db34c57a03dfe1ce2fc75a7..71d2cf583d6e6e624ad7a12638ea65df775f4223 100644
--- a/debian-template/copyright
+++ b/debian-template/copyright
@@ -12,7 +12,7 @@ Upstream Author(s):
 
 Copyright:
 
-    Copyright (C) 1998-2018 by Sonic Team Junior
+    Copyright (C) 1998-2025 by Sonic Team Junior
 
 License:
 
@@ -21,7 +21,7 @@ License:
 The Debian packaging is:
 
     Copyright (C) 2010 Callum Dickinson <gcfreak_ag20@hotmail.com>
-    Copyright (C) 2010-2018 by Sonic Team Junior <stjr@srb2.org>
+    Copyright (C) 2010-2025 by Sonic Team Junior <stjr@srb2.org>
 
 and is licensed under the GPL version 2,
 see "/usr/share/common-licenses/GPL-2".
diff --git a/doc/specs/udmf_srb2.txt b/doc/specs/udmf_srb2.txt
index b25ed1af38ee55b6c2ecf568c824520d800c58e7..eaa3a8a9704439b72569aee0a0c4db956e8d8482 100644
--- a/doc/specs/udmf_srb2.txt
+++ b/doc/specs/udmf_srb2.txt
@@ -1,5 +1,5 @@
 ===============================================================================
-Universal Doom Map Format Sonic Robo Blast 2 extensions v1.0 19.02.2024
+Universal Doom Map Format Sonic Robo Blast 2 extensions v1.0 19.06.2024
 
     Copyright (c) 2024 Sonic Team Junior
        uses Universal Doom Map Format Specification v1.1 as a template,
@@ -126,7 +126,7 @@ Sonic Robo Blast 2 defines the following standardized fields:
       texturebottom = <string>; // Lower texture. Default = "-".
       texturemiddle = <string>; // Middle texture. Default = "-".
 
-      repeatcnt = <string>; // Number of middle texture repetitions. Default = 0
+      repeatcnt = <integer>; // Number of middle texture repetitions. Default = 0.
 
       sector = <integer>; // Sector index. No valid default.
 
@@ -143,6 +143,9 @@ Sonic Robo Blast 2 defines the following standardized fields:
       offsetx_bottom = <float>;        // X offset for lower texture. Default = 0.0.
       offsety_bottom = <float>;        // Y offset for lower texture. Default = 0.0.
 
+      light = <integer>;               // Light level, relative to 'sector' light level. Default = 0.
+      lightabsolute = <bool>;          // true = 'light' is an absolute value, ignoring 'sector' light level.
+
       comment = <string>; // A comment. Implementors should attach no special
                           // semantic meaning to this field.
    }
diff --git a/extras/conf/SRB2-22.cfg b/extras/conf/SRB2-22.cfg
index 41ad998891154f56fe850cdfe457a48189dd648f..7ef5ab1e170dcb00041a0a341a8d7a82da0072d2 100644
--- a/extras/conf/SRB2-22.cfg
+++ b/extras/conf/SRB2-22.cfg
@@ -41,7 +41,7 @@ linetagindicatesectors = true;
 
 // The format interface handles the map data format - DoomMapSetIO for SRB2DB2, SRB2MapSetIO for Zone Builder
 formatinterface = "SRB2MapSetIO";
-	
+
 //Maximum safe map size check (0 means skip check)
 safeboundary = 0;
 
@@ -502,7 +502,7 @@ gen_sectortypes
 	{
 		0 = "Normal";
 		512 = "Wind/Current <deprecated>";
-		1024 = "Conveyor Belt <deprecated>";		 
+		1024 = "Conveyor Belt <deprecated>";
 		1280 = "Speed Pad";
 		1536 = "Flip Gravity on Jump";
 	}
@@ -3730,7 +3730,7 @@ thingtypes
 
 		3328 = "3D Mode Start";
 	}
-	
+
 	starts
 	{
 		color = 1; // Blue
diff --git a/extras/conf/udb/Includes/SRB222_common.cfg b/extras/conf/udb/Includes/SRB222_common.cfg
index aee7a70e4e0a4b806d8c0a055d77f06fce476ea6..9bf882f56a11346709b7c7c263e37dd86ffad173 100644
--- a/extras/conf/udb/Includes/SRB222_common.cfg
+++ b/extras/conf/udb/Includes/SRB222_common.cfg
@@ -39,6 +39,7 @@ common
 	defaulttexturescale = 1.0f;
 	defaultflatscale = 1.0f;
 	scaledtextureoffsets = true;
+	scaledflatoffsets = true;
 
 	// Colormap/fade related options
 	maxcolormapalpha = 25;
@@ -95,7 +96,7 @@ mapformat_udmf
 	{
 		include("SRB222_misc.cfg", "universalfields");
 	}
-	
+
 	// Disable Doom-related modes that don't make sense for SRB2
 	soundsupport = false;
 	automapsupport = false;
@@ -114,7 +115,7 @@ mapformat_udmf
 
 	// Enables setting distinct brightness for floor, ceiling, and walls
 	distinctfloorandceilingbrightness = true;
-	distinctwallbrightness = false;
+	distinctwallbrightness = true;
 
 	// Enables setting distinct brightness for upper, middle, and lower sidedef parts
 	distinctsidedefpartbrightness = false;
@@ -194,4 +195,4 @@ mapformat_udmf
 	{
 		include("SRB222_linedefs.cfg", "udmf");
 	}
-}
\ No newline at end of file
+}
diff --git a/extras/conf/udb/Includes/SRB222_linedefs.cfg b/extras/conf/udb/Includes/SRB222_linedefs.cfg
index e297f473fd46d2848f3835765988bb913147e84f..ce1979581fb7e1251ad76882154eab80ec204f06 100644
--- a/extras/conf/udb/Includes/SRB222_linedefs.cfg
+++ b/extras/conf/udb/Includes/SRB222_linedefs.cfg
@@ -7,13 +7,11 @@ udmf
 		0
 		{
 			title = "None";
-			prefix = "(0)";
 		}
-		
+
 		6
 		{
 			title = "Sector Set Portal";
-			prefix = "(6)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -52,7 +50,6 @@ udmf
 		7
 		{
 			title = "Sector Flat Alignment";
-			prefix = "(7)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -70,7 +67,6 @@ udmf
 		10
 		{
 			title = "Culling Plane";
-			prefix = "(10)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -91,20 +87,17 @@ udmf
 		40
 		{
 			title = "Visual Portal Between Tagged Linedefs";
-			prefix = "(40)";
 		}
 
 		41
 		{
 			title = "Horizon Effect";
 			id = "srb2_horizonline";
-			prefix = "(41)";
 		}
 
 		63
 		{
 			title = "Fake Floor/Ceiling Planes";
-			prefix = "(63)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -120,7 +113,6 @@ udmf
 		2
 		{
 			title = "Custom Exit";
-			prefix = "(2)";
 			arg0
 			{
 				title = "Next map";
@@ -144,7 +136,6 @@ udmf
 		3
 		{
 			title = "Zoom Tube Parameters";
-			prefix = "(3)";
 			arg0
 			{
 				title = "Speed";
@@ -166,7 +157,6 @@ udmf
 		4
 		{
 			title = "Speed Pad Parameters";
-			prefix = "(4)";
 			arg0
 			{
 				title = "Speed";
@@ -193,7 +183,6 @@ udmf
 		8
 		{
 			title = "Set Camera Collision Planes";
-			prefix = "(8)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -204,7 +193,6 @@ udmf
 		11
 		{
 			title = "Rope Hang Parameters";
-			prefix = "(11)";
 			arg0
 			{
 				title = "Speed";
@@ -226,7 +214,6 @@ udmf
 		14
 		{
 			title = "Bustable Block Parameters";
-			prefix = "(14)";
 			arg0
 			{
 				title = "Debris spacing";
@@ -252,13 +239,11 @@ udmf
 		15
 		{
 			title = "Fan Particle Generator Heights";
-			prefix = "(15)";
 		}
 
 		16
 		{
 			title = "Minecart Parameters";
-			prefix = "(16)";
 			arg0
 			{
 				title = "Order";
@@ -268,7 +253,6 @@ udmf
 		64
 		{
 			title = "Continuously Appearing/Disappearing FOF";
-			prefix = "(64)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -307,7 +291,6 @@ udmf
 		20
 		{
 			title = "PolyObject First Line";
-			prefix = "(20)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -348,7 +331,6 @@ udmf
 		30
 		{
 			title = "Waving PolyObject Flag";
-			prefix = "(30)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -369,7 +351,6 @@ udmf
 		31
 		{
 			title = "Move PolyObject by Front Sector Displacement";
-			prefix = "(31)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -385,7 +366,6 @@ udmf
 		32
 		{
 			title = "Rotate PolyObject by Front Sector Displacement";
-			prefix = "(32)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -421,7 +401,6 @@ udmf
 		52
 		{
 			title = "Continuously Falling Sector";
-			prefix = "(52)";
 			arg0
 			{
 				title = "Speed";
@@ -442,7 +421,6 @@ udmf
 		53
 		{
 			title = "Continuous Plane Mover (Slowdown)";
-			prefix = "(53)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -477,7 +455,6 @@ udmf
 		56
 		{
 			title = "Continuous Plane Mover (Constant)";
-			prefix = "(56)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -512,7 +489,6 @@ udmf
 		60
 		{
 			title = "Activate Moving Platform";
-			prefix = "(60)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -542,7 +518,6 @@ udmf
 		61
 		{
 			title = "Ceiling Crusher";
-			prefix = "(61)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -573,7 +548,6 @@ udmf
 		66
 		{
 			title = "Move Planes by Front Sector Displacement";
-			prefix = "(66)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -600,7 +574,6 @@ udmf
 		70
 		{
 			title = "Add Raise Thinker";
-			prefix = "(70)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -626,7 +599,6 @@ udmf
 		71
 		{
 			title = "Add Air Bobbing Thinker";
-			prefix = "(71)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -652,7 +624,6 @@ udmf
 		72
 		{
 			title = "Add Thwomp Thinker";
-			prefix = "(72)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -679,7 +650,6 @@ udmf
 		73
 		{
 			title = "Add Laser Thinker";
-			prefix = "(73)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -696,7 +666,6 @@ udmf
 		74
 		{
 			title = "Make FOF Bustable";
-			prefix = "(74)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -735,7 +704,6 @@ udmf
 		75
 		{
 			title = "Make FOF Quicksand";
-			prefix = "(75)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -755,7 +723,6 @@ udmf
 		76
 		{
 			title = "Make FOF Bouncy";
-			prefix = "(76)";
 			arg0
 			{
 				title = "Control linedef tag";
@@ -775,7 +742,6 @@ udmf
 		100
 		{
 			title = "Solid";
-			prefix = "(100)";
 			id = "srb2_fofsolid";
 			arg0
 			{
@@ -819,7 +785,6 @@ udmf
 		120
 		{
 			title = "Water";
-			prefix = "(120)";
 			id = "srb2_fofwater";
 			arg0
 			{
@@ -857,7 +822,6 @@ udmf
 		150
 		{
 			title = "Air Bobbing";
-			prefix = "(150)";
 			id = "srb2_fofsolidopaque";
 			arg0
 			{
@@ -884,7 +848,6 @@ udmf
 		160
 		{
 			title = "Water Bobbing";
-			prefix = "(160)";
 			id = "srb2_fofsolidopaque";
 			arg0
 			{
@@ -896,7 +859,6 @@ udmf
 		170
 		{
 			title = "Crumbling";
-			prefix = "(170)";
 			id = "srb2_fofcrumbling";
 			arg0
 			{
@@ -935,11 +897,10 @@ udmf
 				}
 			}
 		}
-		
+
 		190
 		{
 			title = "Rising";
-			prefix = "(190)";
 			id = "srb2_fofsolid";
 			arg0
 			{
@@ -998,7 +959,6 @@ udmf
 		200
 		{
 			title = "Light Block";
-			prefix = "(200)";
 			id = "srb2_foflight";
 			arg0
 			{
@@ -1016,7 +976,6 @@ udmf
 		202
 		{
 			title = "Fog Block";
-			prefix = "(202)";
 			id = "srb2_foffog";
 			arg0
 			{
@@ -1028,7 +987,6 @@ udmf
 		220
 		{
 			title = "Intangible";
-			prefix = "(220)";
 			id = "srb2_fofintangible";
 			arg0
 			{
@@ -1066,7 +1024,6 @@ udmf
 		223
 		{
 			title = "Intangible, Invisible";
-			prefix = "(223)";
 			id = "srb2_fofintangibleinvisible";
 			arg0
 			{
@@ -1078,7 +1035,6 @@ udmf
 		250
 		{
 			title = "Mario Block";
-			prefix = "(250)";
 			id = "srb2_fofsolidopaque";
 			arg0
 			{
@@ -1100,7 +1056,6 @@ udmf
 		251
 		{
 			title = "Thwomp Block";
-			prefix = "(251)";
 			id = "srb2_fofsolidopaque";
 			arg0
 			{
@@ -1128,7 +1083,6 @@ udmf
 		254
 		{
 			title = "Bustable Block";
-			prefix = "(254)";
 			id = "srb2_fofbustable";
 			arg0
 			{
@@ -1181,7 +1135,6 @@ udmf
 		257
 		{
 			title = "Quicksand";
-			prefix = "(257)";
 			id = "srb2_fofsolidopaque";
 			arg0
 			{
@@ -1208,7 +1161,6 @@ udmf
 		258
 		{
 			title = "Laser";
-			prefix = "(258)";
 			id = "srb2_foflaser";
 			arg0
 			{
@@ -1242,7 +1194,6 @@ udmf
 		259
 		{
 			title = "Custom";
-			prefix = "(259)";
 			id = "srb2_fofcustom";
 			arg0
 			{
@@ -1307,7 +1258,6 @@ udmf
 		300
 		{
 			title = "Basic";
-			prefix = "(300)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1319,7 +1269,6 @@ udmf
 		303
 		{
 			title = "Ring Count";
-			prefix = "(303)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1347,7 +1296,6 @@ udmf
 		305
 		{
 			title = "Character Ability";
-			prefix = "(305)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1383,7 +1331,6 @@ udmf
 		308
 		{
 			title = "Gametype";
-			prefix = "(308)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1441,7 +1388,6 @@ udmf
 		309
 		{
 			title = "CTF Team";
-			prefix = "(309)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1459,7 +1405,6 @@ udmf
 		313
 		{
 			title = "No More Enemies";
-			prefix = "(313)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -1470,7 +1415,6 @@ udmf
 		314
 		{
 			title = "Number of Pushables";
-			prefix = "(314)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1492,7 +1436,6 @@ udmf
 		317
 		{
 			title = "Condition Set Trigger";
-			prefix = "(317)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1508,7 +1451,6 @@ udmf
 		319
 		{
 			title = "Unlockable";
-			prefix = "(319)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1524,7 +1466,6 @@ udmf
 		321
 		{
 			title = "Trigger After X Calls";
-			prefix = "(321)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1550,7 +1491,6 @@ udmf
 		323
 		{
 			title = "NiGHTSerize";
-			prefix = "(323)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1617,7 +1557,6 @@ udmf
 		325
 		{
 			title = "De-NiGHTSerize";
-			prefix = "(325)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1680,7 +1619,6 @@ udmf
 		327
 		{
 			title = "NiGHTS Lap";
-			prefix = "(327)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1732,7 +1670,6 @@ udmf
 		329
 		{
 			title = "Ideya Capture Touch";
-			prefix = "(329)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1800,7 +1737,6 @@ udmf
 		331
 		{
 			title = "Player Skin";
-			prefix = "(331)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1823,7 +1759,6 @@ udmf
 		334
 		{
 			title = "Object Dye";
-			prefix = "(334)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1846,7 +1781,6 @@ udmf
 		337
 		{
 			title = "Emerald Check";
-			prefix = "(337)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1879,7 +1813,6 @@ udmf
 		340
 		{
 			title = "NiGHTS Mare";
-			prefix = "(340)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1901,7 +1834,6 @@ udmf
 		343
 		{
 			title = "Gravity Check";
-			prefix = "(343)";
 			arg0
 			{
 				title = "Trigger type";
@@ -1924,7 +1856,6 @@ udmf
 		399
 		{
 			title = "Level Load";
-			prefix = "(399)";
 		}
 	}
 
@@ -1935,7 +1866,6 @@ udmf
 		400
 		{
 			title = "Set Tagged Sector's Heights/Textures";
-			prefix = "(400)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -1958,7 +1888,6 @@ udmf
 		402
 		{
 			title = "Copy Light Level to Tagged Sectors";
-			prefix = "(402)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -1980,7 +1909,6 @@ udmf
 		408
 		{
 			title = "Set Tagged Sector's Flats";
-			prefix = "(408)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -1997,7 +1925,6 @@ udmf
 		409
 		{
 			title = "Change Tagged Sector's Tag";
-			prefix = "(409)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2025,7 +1952,6 @@ udmf
 		410
 		{
 			title = "Change Front Sector's Tag";
-			prefix = "(410)";
 			arg0
 			{
 				title = "Tag";
@@ -2048,7 +1974,6 @@ udmf
 		416
 		{
 			title = "Start Adjustable Flickering Light";
-			prefix = "(416)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2077,7 +2002,6 @@ udmf
 		417
 		{
 			title = "Start Adjustable Pulsating Light";
-			prefix = "(417)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2106,7 +2030,6 @@ udmf
 		418
 		{
 			title = "Start Adjustable Blinking Light";
-			prefix = "(418)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2143,7 +2066,6 @@ udmf
 		420
 		{
 			title = "Fade Light Level";
-			prefix = "(420)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2173,7 +2095,6 @@ udmf
 		421
 		{
 			title = "Stop Lighting Effect";
-			prefix = "(421)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2184,7 +2105,6 @@ udmf
 		435
 		{
 			title = "Change Plane Scroller Direction";
-			prefix = "(435)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2200,7 +2120,6 @@ udmf
 		467
 		{
 			title = "Set Tagged Sector's Light Level";
-			prefix = "(467)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2232,7 +2151,6 @@ udmf
 		469
 		{
 			title = "Change Tagged Sector's Gravity";
-			prefix = "(469)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2284,7 +2202,6 @@ udmf
 		403
 		{
 			title = "Move Tagged Sector's Planes";
-			prefix = "(403)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2317,7 +2234,6 @@ udmf
 		405
 		{
 			title = "Move Planes by Set Distance";
-			prefix = "(405)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2349,7 +2265,6 @@ udmf
 		411
 		{
 			title = "Stop Plane Movement";
-			prefix = "(411)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2360,7 +2275,6 @@ udmf
 		428
 		{
 			title = "Start Platform Movement";
-			prefix = "(428)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2394,7 +2308,6 @@ udmf
 		429
 		{
 			title = "Crush Planes Once";
-			prefix = "(429)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2426,7 +2339,6 @@ udmf
 		412
 		{
 			title = "Teleporter";
-			prefix = "(412)";
 			arg0
 			{
 				title = "Destination thing tag";
@@ -2461,7 +2373,6 @@ udmf
 		425
 		{
 			title = "Change Object State";
-			prefix = "(425)";
 			stringarg0
 			{
 				title = "State";
@@ -2472,7 +2383,6 @@ udmf
 		426
 		{
 			title = "Stop Object";
-			prefix = "(426)";
 			arg0
 			{
 				title = "Move to center?";
@@ -2484,7 +2394,6 @@ udmf
 		427
 		{
 			title = "Award Score";
-			prefix = "(427)";
 			arg0
 			{
 				title = "Score";
@@ -2494,7 +2403,6 @@ udmf
 		432
 		{
 			title = "Enable/Disable 2D Mode";
-			prefix = "(432)";
 			arg0
 			{
 				title = "Mode";
@@ -2510,7 +2418,6 @@ udmf
 		433
 		{
 			title = "Enable/Disable Gravity Flip";
-			prefix = "(433)";
 			arg0
 			{
 				title = "Set gravity";
@@ -2546,7 +2453,6 @@ udmf
 		434
 		{
 			title = "Award Power-Up";
-			prefix = "(434)";
 			stringarg0
 			{
 				title = "Power";
@@ -2562,7 +2468,6 @@ udmf
 		437
 		{
 			title = "Disable Player Control";
-			prefix = "(437)";
 			arg0
 			{
 				title = "Time";
@@ -2578,7 +2483,6 @@ udmf
 		438
 		{
 			title = "Change Object Size";
-			prefix = "(438)";
 			arg0
 			{
 				title = "Size (%)";
@@ -2589,7 +2493,6 @@ udmf
 		442
 		{
 			title = "Change Object Type State";
-			prefix = "(442)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2620,7 +2523,6 @@ udmf
 		457
 		{
 			title = "Track Object's Angle";
-			prefix = "(457)";
 			arg0
 			{
 				title = "Anchor tag";
@@ -2651,13 +2553,11 @@ udmf
 		458
 		{
 			title = "Stop Tracking Object's Angle";
-			prefix = "(458)";
 		}
 
 		460
 		{
 			title = "Award Rings";
-			prefix = "(460)";
 			arg0
 			{
 				title = "Rings";
@@ -2671,7 +2571,6 @@ udmf
 		461
 		{
 			title = "Spawn Object";
-			prefix = "(461)";
 			arg0
 			{
 				title = "X position";
@@ -2717,13 +2616,11 @@ udmf
 		462
 		{
 			title = "Stop Timer/Exit Stage in Record Attack";
-			prefix = "(462)";
 		}
 
 		463
 		{
 			title = "Dye Object";
-			prefix = "(463)";
 			stringarg0
 			{
 				title = "Skin color";
@@ -2734,7 +2631,6 @@ udmf
 		464
 		{
 			title = "Trigger Egg Capsule";
-			prefix = "(464)";
 			arg0
 			{
 				title = "Egg Capsule tag";
@@ -2751,7 +2647,6 @@ udmf
 		466
 		{
 			title = "Set Level Failure State";
-			prefix = "(466)";
 			arg0
 			{
 				title = "State";
@@ -2772,7 +2667,6 @@ udmf
 		413
 		{
 			title = "Change Music";
-			prefix = "(413)";
 			arg0
 			{
 				title = "Flags";
@@ -2823,7 +2717,6 @@ udmf
 		414
 		{
 			title = "Play Sound Effect";
-			prefix = "(414)";
 			arg0
 			{
 				title = "Source";
@@ -2863,7 +2756,6 @@ udmf
 		415
 		{
 			title = "Run Script";
-			prefix = "(415)";
 			stringarg0
 			{
 				title = "Lump name";
@@ -2874,7 +2766,6 @@ udmf
 		422
 		{
 			title = "Switch to Cut-Away View";
-			prefix = "(422)";
 			arg0
 			{
 				title = "Viewpoint tag";
@@ -2889,7 +2780,6 @@ udmf
 		423
 		{
 			title = "Change Sky";
-			prefix = "(423)";
 			arg0
 			{
 				title = "Sky number";
@@ -2905,7 +2795,6 @@ udmf
 		424
 		{
 			title = "Change Weather";
-			prefix = "(424)";
 			arg0
 			{
 				title = "Weather";
@@ -2932,7 +2821,6 @@ udmf
 		436
 		{
 			title = "Shatter FOF";
-			prefix = "(436)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2948,7 +2836,6 @@ udmf
 		439
 		{
 			title = "Change Tagged Linedef's Textures";
-			prefix = "(439)";
 			arg0
 			{
 				title = "Target linedef tag";
@@ -2977,13 +2864,11 @@ udmf
 		440
 		{
 			title = "Start Metal Sonic Race";
-			prefix = "(440)";
 		}
 
 		441
 		{
 			title = "Condition Set Trigger";
-			prefix = "(441)";
 			arg0
 			{
 				title = "Trigger number";
@@ -2993,7 +2878,6 @@ udmf
 		443
 		{
 			title = "Call Lua Function";
-			prefix = "(443)";
 			stringarg0
 			{
 				title = "Function name";
@@ -3004,7 +2888,6 @@ udmf
 		444
 		{
 			title = "Earthquake";
-			prefix = "(444)";
 			arg0
 			{
 				title = "Duration";
@@ -3018,7 +2901,6 @@ udmf
 		445
 		{
 			title = "Make FOF Disappear/Reappear";
-			prefix = "(445)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3044,7 +2926,6 @@ udmf
 		446
 		{
 			title = "Make FOF Crumble";
-			prefix = "(446)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3072,7 +2953,6 @@ udmf
 		447
 		{
 			title = "Change Tagged Sector's Colormap";
-			prefix = "(447)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3108,7 +2988,6 @@ udmf
 		448
 		{
 			title = "Change Skybox";
-			prefix = "(448)";
 			arg0
 			{
 				title = "Viewpoint thing tag";
@@ -3139,7 +3018,6 @@ udmf
 		449
 		{
 			title = "Enable Bosses with Parameter";
-			prefix = "(449)";
 			arg0
 			{
 				title = "Boss ID";
@@ -3159,7 +3037,6 @@ udmf
 		450
 		{
 			title = "Execute Linedef Executor (specific tag)";
-			prefix = "(450)";
 			arg0
 			{
 				title = "Trigger linedef tag";
@@ -3170,7 +3047,6 @@ udmf
 		451
 		{
 			title = "Execute Linedef Executor (random tag in range)";
-			prefix = "(451)";
 			arg0
 			{
 				title = "Start of tag range";
@@ -3186,7 +3062,6 @@ udmf
 		452
 		{
 			title = "Set FOF Translucency";
-			prefix = "(452)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3217,7 +3092,6 @@ udmf
 		453
 		{
 			title = "Fade FOF";
-			prefix = "(453)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3260,7 +3134,6 @@ udmf
 		454
 		{
 			title = "Stop Fading FOF";
-			prefix = "(454)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3282,7 +3155,6 @@ udmf
 		455
 		{
 			title = "Fade Tagged Sector's Colormap";
-			prefix = "(455)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3324,7 +3196,6 @@ udmf
 		456
 		{
 			title = "Stop Fading Tagged Sector's Colormap";
-			prefix = "(456)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3335,7 +3206,6 @@ udmf
 		459
 		{
 			title = "Control Text Prompt";
-			prefix = "(459)";
 			arg0
 			{
 				title = "Prompt number";
@@ -3371,7 +3241,6 @@ udmf
 		465
 		{
 			title = "Set Linedef Executor Delay";
-			prefix = "(465)";
 			arg0
 			{
 				title = "Linedef tag";
@@ -3392,7 +3261,6 @@ udmf
 		468
 		{
 			title = "Change Linedef Argument";
-			prefix = "(468)";
 			arg0
 			{
 				title = "Linedef tag";
@@ -3422,7 +3290,6 @@ udmf
 		480
 		{
 			title = "PolyObject Door Slide";
-			prefix = "(480)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3446,7 +3313,6 @@ udmf
 		481
 		{
 			title = "PolyObject Door Swing";
-			prefix = "(481)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3471,7 +3337,6 @@ udmf
 		482
 		{
 			title = "Move PolyObject";
-			prefix = "(482)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3497,7 +3362,6 @@ udmf
 		484
 		{
 			title = "Rotate PolyObject";
-			prefix = "(484)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3531,7 +3395,6 @@ udmf
 		488
 		{
 			title = "Move PolyObject by Waypoints";
-			prefix = "(488)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3573,7 +3436,6 @@ udmf
 		489
 		{
 			title = "Set PolyObject Visibility/Tangibility";
-			prefix = "(489)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3606,7 +3468,6 @@ udmf
 		491
 		{
 			title = "Set PolyObject Translucency";
-			prefix = "(491)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3628,7 +3489,6 @@ udmf
 		492
 		{
 			title = "Fade PolyObject Translucency";
-			prefix = "(492)";
 			arg0
 			{
 				title = "PolyObject tag";
@@ -3666,7 +3526,6 @@ udmf
 		500
 		{
 			title = "Scroll Wall";
-			prefix = "(500)";
 			arg0
 			{
 				title = "Side";
@@ -3686,7 +3545,6 @@ udmf
 		502
 		{
 			title = "Scroll Tagged Walls";
-			prefix = "(502)";
 			arg0
 			{
 				title = "Linedef tag";
@@ -3717,7 +3575,6 @@ udmf
 		510
 		{
 			title = "Scroll Planes";
-			prefix = "(510)";
 			arg0
 			{
 				title = "Sector tag";
@@ -3755,7 +3612,6 @@ udmf
 		541
 		{
 			title = "Wind/Current";
-			prefix = "(541)";
 			arg0
 			{
 				title = "Sector tag";
@@ -3800,7 +3656,6 @@ udmf
 		600
 		{
 			title = "Copy Light Level to Tagged Sector's Planes";
-			prefix = "(600)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3817,7 +3672,6 @@ udmf
 		602
 		{
 			title = "Adjustable Pulsating Light";
-			prefix = "(602)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3846,7 +3700,6 @@ udmf
 		603
 		{
 			title = "Adjustable Flickering Light";
-			prefix = "(603)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3875,7 +3728,6 @@ udmf
 		604
 		{
 			title = "Adjustable Blinking Light";
-			prefix = "(604)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3912,7 +3764,6 @@ udmf
 		606
 		{
 			title = "Copy Colormap";
-			prefix = "(606)";
 			arg0
 			{
 				title = "Target sector tag";
@@ -3933,7 +3784,6 @@ udmf
 		700
 		{
 			title = "Create Sector-Based Slope";
-			prefix = "(700)";
 			id = "plane_align";
 			arg0
 			{
@@ -3963,7 +3813,6 @@ udmf
 		704
 		{
 			title = "Create Vertex-Based Slope";
-			prefix = "(704)";
 			id = "srb2_vertexslope";
 			arg0
 			{
@@ -4007,7 +3856,6 @@ udmf
 		720
 		{
 			title = "Copy Slope";
-			prefix = "(720)";
 			id = "plane_copy";
 			arg0
 			{
@@ -4046,7 +3894,6 @@ udmf
 		799
 		{
 			title = "Set Tagged Dynamic Slope Vertex to Front Sector Height";
-			prefix = "(799)";
 			arg0
 			{
 				title = "Apply height";
diff --git a/extras/conf/udb/Includes/SRB222_misc.cfg b/extras/conf/udb/Includes/SRB222_misc.cfg
index 0b94a5a87a37a5ce7cf6a2a4c9e5799f7c738eab..e5786977bd4e9c3a543c2113d0f20fa8b84939a1 100644
--- a/extras/conf/udb/Includes/SRB222_misc.cfg
+++ b/extras/conf/udb/Includes/SRB222_misc.cfg
@@ -240,7 +240,7 @@ universalfields
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		comment
 		{
 			type = 2;
@@ -252,19 +252,19 @@ universalfields
 			type = 2;
 			default = "";
 		}
-		
+
 		stringarg0
 		{
 			type = 2;
 			default = "";
 		}
-		
+
 		stringarg1
 		{
 			type = 2;
 			default = "";
 		}
-		
+
 		executordelay
 		{
 			type = 0;
@@ -279,19 +279,19 @@ universalfields
 			type = 2;
 			default = "";
 		}
-		
-		//light
-		//{
-		//	type = 0;
-		//	default = 0;
-		//}
-		//
-		//lightabsolute
-		//{
-		//	type = 3;
-		//	default = false;
-		//}
-		//
+
+		light
+		{
+			type = 0;
+			default = 0;
+		}
+
+		lightabsolute
+		{
+			type = 3;
+			default = false;
+		}
+
 		//light_top
 		//{
 		//	type = 0;
@@ -326,8 +326,8 @@ universalfields
 		//{
 		//	type = 3;
 		//	default = false;
-		//}		
-		
+		//}
+
 		offsetx_bottom
 		{
 			type = 1;
@@ -339,7 +339,7 @@ universalfields
 			type = 1;
 			default = 0.0;
 		}
-		
+
 		offsetx_top
 		{
 			type = 1;
@@ -357,43 +357,43 @@ universalfields
 			type = 1;
 			default = 0.0;
 		}
-		
+
 		offsety_top
 		{
 			type = 1;
 			default = 0.0;
 		}
-		
+
 		scalex_bottom
 		{
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		scalex_mid
 		{
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		scalex_top
 		{
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		scaley_bottom
 		{
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		scaley_mid
 		{
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		scaley_top
 		{
 			type = 1;
@@ -408,41 +408,41 @@ universalfields
 			type = 2;
 			default = "";
 		}
-		
+
 		pitch
 		{
 			type = 0;
 		}
-		
+
 		roll
 		{
 			type = 0;
 		}
-		
+
 		scalex
 		{
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		scaley
 		{
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		stringarg0
 		{
 			type = 2;
 			default = "";
 		}
-		
+
 		stringarg1
 		{
 			type = 2;
 			default = "";
 		}
-		
+
 		mobjscale
 		{
 			type = 1;
@@ -450,7 +450,7 @@ universalfields
 			managed = false;
 		}
 	}
-	
+
 	sector
 	{
 		comment
@@ -530,7 +530,7 @@ universalfields
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		yscalefloor
 		{
 			type = 1;
@@ -542,7 +542,7 @@ universalfields
 			type = 0;
 			default = 0;
 		}
-		
+
 		lightfloorabsolute
 		{
 			type = 3;
@@ -572,7 +572,7 @@ universalfields
 			type = 1;
 			default = 1.0;
 		}
-		
+
 		yscaleceiling
 		{
 			type = 1;
@@ -584,12 +584,18 @@ universalfields
 			type = 0;
 			default = 0;
 		}
-		
+
 		lightceilingabsolute
 		{
 			type = 3;
 			default = false;
 		}
+
+		triggertag
+		{
+			type = 0;
+			default = 0;
+		}
 	}
 }
 
diff --git a/extras/conf/udb/Includes/SRB222_things.cfg b/extras/conf/udb/Includes/SRB222_things.cfg
index 990f8dca9f505b57f1797c49ed0d8ae003d18d5d..8b1e29751b7bb4b3e771813b414d80f9017a4f68 100644
--- a/extras/conf/udb/Includes/SRB222_things.cfg
+++ b/extras/conf/udb/Includes/SRB222_things.cfg
@@ -1127,12 +1127,12 @@ udmf
 				}
 			}
 		}
-		
+
 		bossinvisibles
 		{
 			title = "Misc. Invisible";
 			color = 15; // White
-			
+
 			290
 			{
 				arrow = 0;
@@ -2021,7 +2021,7 @@ udmf
 			}
 		}
 	}
-	
+
 	hazards
 	{
 		color = 17; // Orange
@@ -2983,7 +2983,7 @@ udmf
 		{
 			title = "Mace Spawnpoints";
 			color = 11;
-			
+
 			1104
 			{
 				title = "Mace Spawn";
@@ -3188,7 +3188,7 @@ udmf
 					title = "Flags";
 					type = 12;
 					enum = "maceflags";
-	
+
 				}
 			}
 			1108
@@ -3320,7 +3320,7 @@ udmf
 				}
 			}
 		}
-		
+
 		1100
 		{
 			title = "Chain (Decorative)";
@@ -3570,12 +3570,12 @@ udmf
 		color = 2; // Green
 		title = "Arid Canyon";
 
-		
+
 		cacti
 		{
 			title = "Cacti";
 			color = 17;
-			
+
 			1203
 			{
 				title = "Tiny Red Flower Cactus";
@@ -3654,12 +3654,12 @@ udmf
 				height = 60;
 			}
 		}
-		
+
 		minecarts
 		{
 			title = "Minecart";
 			color = 11;
-			
+
 			1219
 			{
 				title = "Minecart Spawner";
@@ -3706,7 +3706,7 @@ udmf
 				}
 			}
 		}
-		
+
 		1200
 		{
 			title = "Tumbleweed (Big)";
@@ -3938,6 +3938,7 @@ udmf
 			width = 30;
 			height = 32;
 			color = 17;
+			hangs = 1;
 			arg0
 			{
 				title = "Initial delay";
@@ -4610,7 +4611,7 @@ udmf
 		title = "Botanic Serenity";
 		width = 16;
 		height = 32;
-		
+
 		flowers
 		{
 			title = "Flowers";
@@ -4705,7 +4706,7 @@ udmf
 				sprite = "BSZ3F0";
 			}
 		}
-		
+
 		tulips
 		{
 			title = "Tulips";
@@ -4770,7 +4771,7 @@ udmf
 				sprite = "BSZ5F0";
 			}
 		}
-		
+
 		bushes
 		{
 			title = "Bushes";
@@ -4805,7 +4806,7 @@ udmf
 				sprite = "BSZ6F0";
 			}
 		}
-		
+
 		vines
 		{
 			title = "Vines";
@@ -5062,7 +5063,7 @@ udmf
 			}
 		}
 	}
-	
+
 	mario
 	{
 		color = 2; // Green
@@ -5573,7 +5574,7 @@ udmf
 			}
 		}
 	}
-	
+
 	editor
 	{
 		color = 15; // White
@@ -5586,4 +5587,4 @@ udmf
 
 		3328 = "3D Mode Start";
 	}
-}
\ No newline at end of file
+}
diff --git a/extras/conf/udb/SRB2-22binary.cfg b/extras/conf/udb/SRB2-22binary.cfg
index 3073b76c4570cb9a8f7f62a3e675cbffc7814614..2144ff7a61591b1c9a6865013cd44499179f2183 100644
--- a/extras/conf/udb/SRB2-22binary.cfg
+++ b/extras/conf/udb/SRB2-22binary.cfg
@@ -41,7 +41,7 @@ linetagindicatesectors = true;
 
 // The format interface handles the map data format - DoomMapSetIO for SRB2DB2, SRB2MapSetIO for Zone Builder
 formatinterface = "DoomMapSetIO";
-	
+
 //Maximum safe map size check (0 means skip check)
 safeboundary = 0;
 
@@ -502,7 +502,7 @@ gen_sectortypes
 	{
 		0 = "Normal";
 		512 = "Wind/Current <deprecated>";
-		1024 = "Conveyor Belt <deprecated>";		 
+		1024 = "Conveyor Belt <deprecated>";
 		1280 = "Speed Pad";
 		1536 = "Flip Gravity on Jump";
 	}
@@ -3636,7 +3636,7 @@ thingtypes
 
 		3328 = "3D Mode Start";
 	}
-	
+
 	starts
 	{
 		color = 1; // Blue
diff --git a/libs/miniupnpc/include/igd_desc_parse.h b/libs/miniupnpc/include/miniupnpc/igd_desc_parse.h
similarity index 100%
rename from libs/miniupnpc/include/igd_desc_parse.h
rename to libs/miniupnpc/include/miniupnpc/igd_desc_parse.h
diff --git a/libs/miniupnpc/include/miniupnpc.h b/libs/miniupnpc/include/miniupnpc/miniupnpc.h
similarity index 100%
rename from libs/miniupnpc/include/miniupnpc.h
rename to libs/miniupnpc/include/miniupnpc/miniupnpc.h
diff --git a/libs/miniupnpc/include/miniupnpc_declspec.h b/libs/miniupnpc/include/miniupnpc/miniupnpc_declspec.h
similarity index 100%
rename from libs/miniupnpc/include/miniupnpc_declspec.h
rename to libs/miniupnpc/include/miniupnpc/miniupnpc_declspec.h
diff --git a/libs/miniupnpc/include/miniupnpctypes.h b/libs/miniupnpc/include/miniupnpc/miniupnpctypes.h
similarity index 100%
rename from libs/miniupnpc/include/miniupnpctypes.h
rename to libs/miniupnpc/include/miniupnpc/miniupnpctypes.h
diff --git a/libs/miniupnpc/include/miniwget.h b/libs/miniupnpc/include/miniupnpc/miniwget.h
similarity index 100%
rename from libs/miniupnpc/include/miniwget.h
rename to libs/miniupnpc/include/miniupnpc/miniwget.h
diff --git a/libs/miniupnpc/include/portlistingparse.h b/libs/miniupnpc/include/miniupnpc/portlistingparse.h
similarity index 100%
rename from libs/miniupnpc/include/portlistingparse.h
rename to libs/miniupnpc/include/miniupnpc/portlistingparse.h
diff --git a/libs/miniupnpc/include/upnpcommands.h b/libs/miniupnpc/include/miniupnpc/upnpcommands.h
similarity index 100%
rename from libs/miniupnpc/include/upnpcommands.h
rename to libs/miniupnpc/include/miniupnpc/upnpcommands.h
diff --git a/libs/miniupnpc/include/upnpdev.h b/libs/miniupnpc/include/miniupnpc/upnpdev.h
similarity index 100%
rename from libs/miniupnpc/include/upnpdev.h
rename to libs/miniupnpc/include/miniupnpc/upnpdev.h
diff --git a/libs/miniupnpc/include/upnperrors.h b/libs/miniupnpc/include/miniupnpc/upnperrors.h
similarity index 100%
rename from libs/miniupnpc/include/upnperrors.h
rename to libs/miniupnpc/include/miniupnpc/upnperrors.h
diff --git a/libs/miniupnpc/include/upnpreplyparse.h b/libs/miniupnpc/include/miniupnpc/upnpreplyparse.h
similarity index 100%
rename from libs/miniupnpc/include/upnpreplyparse.h
rename to libs/miniupnpc/include/miniupnpc/upnpreplyparse.h
diff --git a/scripts/arm64-osx-1015.cmake b/scripts/arm64-osx-1015.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..224430bc1fe787c70db8480cf86e84f1e8bcb637
--- /dev/null
+++ b/scripts/arm64-osx-1015.cmake
@@ -0,0 +1,7 @@
+set(VCPKG_TARGET_ARCHITECTURE arm64)
+set(VCPKG_OSX_DEPLOYMENT_TARGET 10.15)
+set(VCPKG_CRT_LINKAGE dynamic)
+set(VCPKG_LIBRARY_LINKAGE static)
+
+set(VCPKG_CMAKE_SYSTEM_NAME Darwin)
+set(VCPKG_OSX_ARCHITECTURES arm64)
diff --git a/scripts/make-macos-universal.sh b/scripts/make-macos-universal.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f1b17516c16fec2d333e5bfc9e3cc9fbfadb0499
--- /dev/null
+++ b/scripts/make-macos-universal.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+# Makes a fused macOS Universal app bundle in the arm64 release preset dir
+# Only works if in master branch or in source tarball
+
+set -e
+
+rm -rf "build/ninja-x64_osx_vcpkg-release"
+rm -rf "build/ninja-arm64_osx_vcpkg-release"
+cmake --preset ninja-x64_osx_vcpkg-release -DVCPKG_OVERLAY_TRIPLETS=scripts/ -DVCPKG_TARGET_TRIPLET=x64-osx-1015 -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 "-DSRB2_SDL2_EXE_NAME=srb2"
+cmake --build --preset ninja-x64_osx_vcpkg-release
+cmake --preset ninja-arm64_osx_vcpkg-release -DVCPKG_OVERLAY_TRIPLETS=scripts/ -DVCPKG_TARGET_TRIPLET=arm64-osx-1015 -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 "-DSRB2_SDL2_EXE_NAME=srb2"
+cmake --build --preset ninja-arm64_osx_vcpkg-release
+
+mkdir -p build/dist
+rm -rf "build/dist/Sonic Robo Blast 2.app" "build/dist/srb2.app"
+
+cp -r build/ninja-arm64_osx_vcpkg-release/bin/srb2.app build/dist/
+
+lipo -create \
+	-output "build/dist/srb2.app/Contents/MacOS/srb2" \
+	build/ninja-x64_osx_vcpkg-release/bin/srb2.app/Contents/MacOS/srb2 \
+	build/ninja-arm64_osx_vcpkg-release/bin/srb2.app/Contents/MacOS/srb2
+
+mv build/dist/srb2.app "build/dist/Sonic Robo Blast 2.app"
diff --git a/scripts/x64-osx-1015.cmake b/scripts/x64-osx-1015.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..5f0c7b66e0e4c1c9969ed6963ddcc99cef9db64c
--- /dev/null
+++ b/scripts/x64-osx-1015.cmake
@@ -0,0 +1,7 @@
+set(VCPKG_TARGET_ARCHITECTURE x64)
+set(VCPKG_OSX_DEPLOYMENT_TARGET 10.15)
+set(VCPKG_CRT_LINKAGE dynamic)
+set(VCPKG_LIBRARY_LINKAGE static)
+
+set(VCPKG_CMAKE_SYSTEM_NAME Darwin)
+set(VCPKG_OSX_ARCHITECTURES x86_64)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 486f4e913057af02c22a6acd6c554bd7b76801b3..30b342c66e0d308170de6cc6ae89ad644a8f90c2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -104,6 +104,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	lua_hudlib_drawlist.c
 	lua_colorlib.c
 	lua_inputlib.c
+	lua_interceptlib.c
 )
 
 # This updates the modification time for comptime.c at the
@@ -134,10 +135,13 @@ add_dependencies(SRB2SDL2 _SRB2_reconf)
 
 if("${CMAKE_COMPILER_IS_GNUCC}" AND "${CMAKE_SYSTEM_NAME}" MATCHES "Windows")
 	target_link_options(SRB2SDL2 PRIVATE "-Wl,--disable-dynamicbase")
-	if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}" AND NOT "${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}")
+	if("${SRB2_CONFIG_STATIC_STDLIB}")
 		# On MinGW with internal libraries, link the standard library statically
 		target_link_options(SRB2SDL2 PRIVATE "-static")
 	endif()
+	if(CMAKE_SIZEOF_VOID_P EQUAL 4)
+		target_link_options(SRB2SDL2 PRIVATE "-Wl,--large-address-aware")
+	endif()
 endif()
 
 target_compile_features(SRB2SDL2 PRIVATE c_std_11 cxx_std_17)
@@ -169,6 +173,11 @@ if("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
 	endif()
 endif()
 
+if("${CMAKE_SYSTEM_NAME}" MATCHES "Haiku")
+	target_compile_definitions(SRB2SDL2 PRIVATE -DNOEXECINFO)
+	target_link_libraries(SRB2SDL2 PRIVATE network)
+endif()
+
 if("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
 	target_compile_definitions(SRB2SDL2 PRIVATE -DMACOSX)
 endif()
@@ -182,12 +191,7 @@ if("${SRB2_CONFIG_USE_GME}")
 	endif()
 endif()
 
-target_link_libraries(SRB2SDL2 PRIVATE openmpt)
-target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_OPENMPT)
-
-target_link_libraries(SRB2SDL2 PRIVATE ZLIB::ZLIB PNG::PNG CURL::libcurl)
-target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_ZLIB -DHAVE_PNG -DHAVE_CURL -D_LARGEFILE64_SOURCE)
-target_sources(SRB2SDL2 PRIVATE apng.c)
+target_compile_definitions(SRB2SDL2 PRIVATE -D_LARGEFILE64_SOURCE)
 
 set(SRB2_HAVE_THREADS ON)
 target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_THREADS)
@@ -231,9 +235,12 @@ endif()
 
 # Compatibility flag with later versions of GCC
 # We should really fix our code to not need this
-if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
-	if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|x64|amd64|AMD64|em64t|EM64T)")
-		target_compile_options(SRB2SDL2 PRIVATE -mno-ms-bitfields)
+if (NOT SRB2_CONFIG_FORCE_NO_MS_BITFIELDS)
+	if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+		check_cxx_compiler_flag("-mno-ms-bitfields" HAS_NO_MS_BITFIELDS)
+		if(HAS_NO_MS_BITFIELDS)
+			target_compile_options(SRB2SDL2 PRIVATE -mno-ms-bitfields)
+		endif()
 	endif()
 endif()
 
@@ -403,8 +410,71 @@ if(SRB2_CONFIG_PROFILEMODE AND "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
 endif()
 
 add_subdirectory(sdl)
-if(SRB2_CONFIG_ENABLE_TESTS)
-	add_subdirectory(tests)
+
+if(TARGET ZLIB::ZLIB)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_ZLIB)
+	message(STATUS "Zlib Found")
+else()
+	message(STATUS "No Zlib Found")
+endif()
+
+if(TARGET PNG::PNG)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_PNG)
+	target_sources(SRB2SDL2 PRIVATE apng.c ${libapng_HEADER})
+	target_include_directories(SRB2SDL2 PRIVATE ${libapng_INCLUDE_DIRS} ${libpng_BINARY_DIR})
+	#message(STATUS "libpng inc DIRS at ${libapng_INCLUDE_DIRS}")
+	#message(STATUS "libpng bin DIRS at ${libpng_BINARY_DIR}")
+	#message(STATUS "png.h at ${libapng_HEADER}")
+	message(STATUS "libpng Found")
+else()
+	message(STATUS "No libpng Found")
+endif()
+
+if(TARGET PNG::PNG AND TARGET ZLIB::ZLIB)
+#libpng links zlib too?
+target_link_libraries(SRB2SDL2 PRIVATE PNG::PNG)
+endif()
+
+if(NOT TARGET PNG::PNG AND TARGET ZLIB::ZLIB)
+#got no libpng? we need zlib
+target_link_libraries(SRB2SDL2 PRIVATE ZLIB::ZLIB)
+endif()
+
+if(TARGET gme::gme)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_GME)
+	target_link_libraries(SRB2SDL2 PRIVATE gme::gme)
+	message(STATUS "libgme Found")
+else()
+	message(STATUS "No libgme Found")
+endif()
+
+if(TARGET libopenmpt::libopenmpt)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_OPENMPT)
+	target_link_libraries(SRB2SDL2 PRIVATE libopenmpt::libopenmpt)
+	message(STATUS "libopenmpt Found")
+else()
+	message(STATUS "No libopenmpt Found")
+endif()
+
+if(TARGET CURL::libcurl)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_CURL)
+	target_link_libraries(SRB2SDL2 PRIVATE CURL::libcurl)
+	message(STATUS "libcurl Found")
+else()
+	message(STATUS "No libcurl Found")
+endif()
+
+if(TARGET miniupnpc::miniupnpc)
+	if("${VCPKG_TARGET_TRIPLET}" MATCHES "-mingw-static$")
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_MINIUPNPC -DMINIUPNP_STATICLIB)
+		target_link_libraries(SRB2SDL2 PRIVATE miniupnpc::miniupnpc -liphlpapi)
+	else()
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_MINIUPNPC)
+		target_link_libraries(SRB2SDL2 PRIVATE miniupnpc::miniupnpc)
+	endif()
+	message(STATUS "miniupnpc Found")
+else()
+	message(STATUS "No miniupnpc Found")
 endif()
 
 # strip debug symbols into separate file when using gcc.
diff --git a/src/Makefile.d/detect.mk b/src/Makefile.d/detect.mk
index 9e27369462b00c407f02cb95dbda22ef284a91d1..853ad3d6df728d7c69ea64b204cf099547f59e45 100644
--- a/src/Makefile.d/detect.mk
+++ b/src/Makefile.d/detect.mk
@@ -4,7 +4,6 @@
 
 # Previously featured:\
 	PANDORA\
-	HAIKU\
 	DUMMY\
 	DJGPPDOS\
 	SOLARIS\
@@ -17,6 +16,7 @@ all_systems:=\
 	UNIX\
 	LINUX\
 	FREEBSD\
+	HAIKU\
 
 # check for user specified system
 ifeq (,$(filter $(all_systems),$(.VARIABLES)))
@@ -35,6 +35,8 @@ system:=$(shell uname -s)
 
 ifeq ($(system),Linux)
 new_system:=LINUX
+else ifeq ($(system),Haiku)
+new_system:=HAIKU
 else
 
 $(error \
diff --git a/src/Makefile.d/haiku.mk b/src/Makefile.d/haiku.mk
new file mode 100644
index 0000000000000000000000000000000000000000..6742ec75b58f992a916a99752c72ca827f4dab5e
--- /dev/null
+++ b/src/Makefile.d/haiku.mk
@@ -0,0 +1,71 @@
+#
+# Makefile options for Haiku
+#
+
+opts+=-DUNIXCOMMON -DLUA_USE_POSIX
+
+ifndef DEDICATED
+ifndef DUMMY
+SDL?=1
+DEDICATED?=0
+endif
+endif
+
+NOEXECINFO=1
+
+ifeq (${SDL},1)
+EXENAME?=srb2haiku
+else ifeq (${DEDICATED},1)
+EXENAME?=srb2haikud
+endif
+
+ifndef NONET
+libs+=-lnetwork
+endif
+
+define _set =
+$(1)_CFLAGS?=$($(1)_opts)
+$(1)_LDFLAGS?=$($(1)_libs)
+endef
+
+lib:=../libs/gme
+LIBGME_opts:=-I$(lib)/include
+LIBGME_libs:=-l:libgme.so.0
+$(eval $(call _set,LIBGME))
+
+lib:=../libs/libopenmpt
+LIBOPENMPT_opts:=-I$(lib)/inc
+LIBOPENMPT_libs:=-l:libopenmpt.so.0
+$(eval $(call _set,LIBOPENMPT))
+
+ifdef SDL
+lib:=../libs/SDL2_mixer
+mixer_opts:=-I$(lib)/include
+mixer_libs:=-l:libSDL2_mixer-2.0.so.0
+
+lib:=../libs/SDL2
+SDL_opts:=-I$(lib)/include $(mixer_opts)
+SDL_libs:=$(mixer_libs) -l:libSDL2-2.0.so.0
+$(eval $(call _set,SDL))
+endif
+
+lib:=../libs/zlib
+ZLIB_opts:=-I$(lib)
+ZLIB_libs:=-l:libz.so.1
+$(eval $(call _set,ZLIB))
+
+ifndef PNG_CONFIG
+PNG_opts:=
+PNG_libs:=-l:libpng16.so.16
+$(eval $(call _set,PNG))
+endif
+
+lib:=../libs/curl
+CURL_opts:=-I$(lib)/include
+CURL_libs:=-l:libcurl.so.4
+$(eval $(call _set,CURL))
+
+lib:=../libs/miniupnpc
+MINIUPNPC_opts:=-I$(lib)/include
+MINIUPNPC_libs:=-l:libminiupnpc.so.17
+$(eval $(call _set,MINIUPNPC))
diff --git a/src/Makefile.d/platform.mk b/src/Makefile.d/platform.mk
index d9a2954f60c54b30c121c0467022c1707f720fc0..d5423a397384922689260546b71948447846f3e1 100644
--- a/src/Makefile.d/platform.mk
+++ b/src/Makefile.d/platform.mk
@@ -35,6 +35,10 @@ endif
 else ifdef FREEBSD
 UNIX=1
 platform=freebsd
+else ifdef HAIKU
+# Give Haiku its own configuration, since it
+# isn't actually UNIX.
+include Makefile.d/haiku.mk
 else ifdef SOLARIS # FIXME: UNTESTED
 UNIX=1
 platform=solaris
diff --git a/src/Makefile.d/sdl.mk b/src/Makefile.d/sdl.mk
index a1bfa33038bbacebada85790a7565fceb9440985..d5e83989c2d1955146a8c228c15034d9f2b9e091 100644
--- a/src/Makefile.d/sdl.mk
+++ b/src/Makefile.d/sdl.mk
@@ -33,11 +33,13 @@ else
 opts+=-DHAVE_MIXER
 sources+=sdl/mixer_sound.c
 
-  ifdef HAVE_MIXERX
-  opts+=-DHAVE_MIXERX
-  libs+=-lSDL2_mixer_ext
-  else
-  libs+=-lSDL2_mixer
+  ifndef HAIKU # Haiku has a special import path
+    ifdef HAVE_MIXERX
+    opts+=-DHAVE_MIXERX
+    libs+=-lSDL2_mixer_ext
+    else
+    libs+=-lSDL2_mixer
+    endif
   endif
 endif
 
diff --git a/src/Sourcefile b/src/Sourcefile
index 60ee5db5baf16fe20657c93e5143afe60615bc89..3e926e31850a2055d335326096de6034a87b0a69 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -98,3 +98,4 @@ lua_hudlib.c
 lua_hudlib_drawlist.c
 lua_inputlib.c
 lua_colorlib.c
+lua_interceptlib.c
diff --git a/src/am_map.c b/src/am_map.c
index df3a45cfff504e046ddaa6ecc03cf9f9d548e45f..36b62f2f9b0157c58a2becd8b55624302d29ae0b 100644
--- a/src/am_map.c
+++ b/src/am_map.c
@@ -1076,6 +1076,9 @@ static inline void AM_drawPlayers(void)
 		if (!playeringame[i] || players[i].spectator)
 			continue;
 
+		if (!players[i].mo)
+			continue;
+
 		p = &players[i];
 		if (p->skincolor > 0)
 			color = R_GetTranslationColormap(TC_DEFAULT, p->skincolor, GTC_CACHE)[GREENS + 8];
diff --git a/src/apng.c b/src/apng.c
index 11d3ab9f58ea9c26530374b7b18d8454fd367ed8..cfb1473a40269703985bc727cafd5f27f2c14f02 100644
--- a/src/apng.c
+++ b/src/apng.c
@@ -71,7 +71,7 @@ apng_create_info_struct (png_structp pngp)
 {
 	apng_infop ainfop;
 	(void)pngp;
-	if (( ainfop = calloc(sizeof (apng_info),1) ))
+	if (( ainfop = calloc(1,sizeof (apng_info)) ))
 	{
 		apng_set_write_fn(pngp, ainfop, 0, 0, 0, 0, 0);
 		apng_set_set_acTL_fn(pngp, ainfop, 0);
diff --git a/src/b_bot.c b/src/b_bot.c
index af57d65ecf6e3c24e725ed38b9533b9ab6509763..97dcd1078dfd91f9c5caf03c352420cbc98198de 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2007-2016 by John "JTE" Muniz.
-// Copyright (C) 2011-2023 by Sonic Team Junior.
+// Copyright (C) 2011-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -387,7 +387,8 @@ void B_BuildTiccmd(player_t *player, ticcmd_t *cmd)
 	}
 
 	// Bot AI isn't programmed in analog.
-	CV_SetValue(&cv_analog[1], false);
+	if (!dedicated)
+		CV_SetValue(&cv_analog[1], false);
 
 	// Let Lua scripts build ticcmds
 	if (LUA_HookTiccmd(player, cmd, HOOK(BotTiccmd)))
diff --git a/src/blua/CMakeLists.txt b/src/blua/CMakeLists.txt
index 892bf534addc810434bdd00960ebe7a92ef7bde4..21278f2bbb1dc75b1e004d6b234d757776f07904 100644
--- a/src/blua/CMakeLists.txt
+++ b/src/blua/CMakeLists.txt
@@ -1,6 +1,7 @@
 target_sources(SRB2SDL2 PRIVATE
 	lapi.c
 	lbaselib.c
+	ldblib.c
 	ldo.c
 	lfunc.c
 	linit.c
diff --git a/src/blua/Sourcefile b/src/blua/Sourcefile
index dae94310981fce03d6aefd30b6e7e5781a4a4eae..2b6b9636604844687b3837cf18b30781512f184b 100644
--- a/src/blua/Sourcefile
+++ b/src/blua/Sourcefile
@@ -1,6 +1,7 @@
 lapi.c
 lbaselib.c
 ldo.c
+ldblib.c
 lfunc.c
 linit.c
 liolib.c
diff --git a/src/blua/ldblib.c b/src/blua/ldblib.c
new file mode 100644
index 0000000000000000000000000000000000000000..2eacef929bedd4e3e034158542a09dc13431f10d
--- /dev/null
+++ b/src/blua/ldblib.c
@@ -0,0 +1,310 @@
+/*
+** $Id: ldblib.c,v 1.104.1.4 2009/08/04 18:50:18 roberto Exp $
+** Interface from Lua to its debug API
+** See Copyright Notice in lua.h
+*/
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define ldblib_c
+#define LUA_LIB
+
+#include "lua.h"
+
+#include "lauxlib.h"
+#include "lualib.h"
+
+
+
+static void settabss (lua_State *L, const char *i, const char *v) {
+  lua_pushstring(L, v);
+  lua_setfield(L, -2, i);
+}
+
+
+static void settabsi (lua_State *L, const char *i, int v) {
+  lua_pushinteger(L, v);
+  lua_setfield(L, -2, i);
+}
+
+
+static lua_State *getthread (lua_State *L, int *arg) {
+  if (lua_isthread(L, 1)) {
+    *arg = 1;
+    return lua_tothread(L, 1);
+  }
+  else {
+    *arg = 0;
+    return L;
+  }
+}
+
+
+static void treatstackoption (lua_State *L, lua_State *L1, const char *fname) {
+  if (L == L1) {
+    lua_pushvalue(L, -2);
+    lua_remove(L, -3);
+  }
+  else
+    lua_xmove(L1, L, 1);
+  lua_setfield(L, -2, fname);
+}
+
+
+static int db_getinfo (lua_State *L) {
+  lua_Debug ar;
+  int arg;
+  lua_State *L1 = getthread(L, &arg);
+  const char *options = luaL_optstring(L, arg+2, "flnSu");
+  if (lua_isnumber(L, arg+1)) {
+    if (!lua_getstack(L1, (int)lua_tointeger(L, arg+1), &ar)) {
+      lua_pushnil(L);  /* level out of range */
+      return 1;
+    }
+  }
+  else if (lua_isfunction(L, arg+1)) {
+    lua_pushfstring(L, ">%s", options);
+    options = lua_tostring(L, -1);
+    lua_pushvalue(L, arg+1);
+    lua_xmove(L, L1, 1);
+  }
+  else
+    return luaL_argerror(L, arg+1, "function or level expected");
+  if (!lua_getinfo(L1, options, &ar))
+    return luaL_argerror(L, arg+2, "invalid option");
+  lua_createtable(L, 0, 2);
+  if (strchr(options, 'S')) {
+    settabss(L, "source", ar.source);
+    settabss(L, "short_src", ar.short_src);
+    settabsi(L, "linedefined", ar.linedefined);
+    settabsi(L, "lastlinedefined", ar.lastlinedefined);
+    settabss(L, "what", ar.what);
+  }
+  if (strchr(options, 'l'))
+    settabsi(L, "currentline", ar.currentline);
+  if (strchr(options, 'u'))
+    settabsi(L, "nups", ar.nups);
+  if (strchr(options, 'n')) {
+    settabss(L, "name", ar.name);
+    settabss(L, "namewhat", ar.namewhat);
+  }
+  if (strchr(options, 'L'))
+    treatstackoption(L, L1, "activelines");
+  if (strchr(options, 'f'))
+    treatstackoption(L, L1, "func");
+  return 1;  /* return table */
+}
+
+
+static int db_getlocal (lua_State *L) {
+  int arg;
+  lua_State *L1 = getthread(L, &arg);
+  lua_Debug ar;
+  const char *name;
+  if (!lua_getstack(L1, luaL_checkint(L, arg+1), &ar))  /* out of range? */
+    return luaL_argerror(L, arg+1, "level out of range");
+  name = lua_getlocal(L1, &ar, luaL_checkint(L, arg+2));
+  if (name) {
+    lua_xmove(L1, L, 1);
+    lua_pushstring(L, name);
+    lua_pushvalue(L, -2);
+    return 2;
+  }
+  else {
+    lua_pushnil(L);
+    return 1;
+  }
+}
+
+
+static int auxupvalue (lua_State *L, int get) {
+  const char *name;
+  int n = luaL_checkint(L, 2);
+  luaL_checktype(L, 1, LUA_TFUNCTION);
+  if (lua_iscfunction(L, 1)) return 0;  /* cannot touch C upvalues from Lua */
+  name = get ? lua_getupvalue(L, 1, n) : lua_setupvalue(L, 1, n);
+  if (name == NULL) return 0;
+  lua_pushstring(L, name);
+  lua_insert(L, -(get+1));
+  return get + 1;
+}
+
+
+static int db_getupvalue (lua_State *L) {
+  return auxupvalue(L, 1);
+}
+
+
+
+static char KEY_HOOK = 'h';
+
+
+static void hookf (lua_State *L, lua_Debug *ar) {
+  static const char *const hooknames[] =
+    {"call", "return", "line", "count", "tail return"};
+  lua_pushlightuserdata(L, (void *)&KEY_HOOK);
+  lua_rawget(L, LUA_REGISTRYINDEX);
+  lua_pushlightuserdata(L, L);
+  lua_rawget(L, -2);
+  if (lua_isfunction(L, -1)) {
+    lua_pushstring(L, hooknames[(int)ar->event]);
+    if (ar->currentline >= 0)
+      lua_pushinteger(L, ar->currentline);
+    else lua_pushnil(L);
+    lua_assert(lua_getinfo(L, "lS", ar));
+    lua_call(L, 2, 0);
+  }
+}
+
+
+static int makemask (const char *smask, int count) {
+  int mask = 0;
+  if (strchr(smask, 'c')) mask |= LUA_MASKCALL;
+  if (strchr(smask, 'r')) mask |= LUA_MASKRET;
+  if (strchr(smask, 'l')) mask |= LUA_MASKLINE;
+  if (count > 0) mask |= LUA_MASKCOUNT;
+  return mask;
+}
+
+
+static char *unmakemask (int mask, char *smask) {
+  int i = 0;
+  if (mask & LUA_MASKCALL) smask[i++] = 'c';
+  if (mask & LUA_MASKRET) smask[i++] = 'r';
+  if (mask & LUA_MASKLINE) smask[i++] = 'l';
+  smask[i] = '\0';
+  return smask;
+}
+
+
+static void gethooktable (lua_State *L) {
+  lua_pushlightuserdata(L, (void *)&KEY_HOOK);
+  lua_rawget(L, LUA_REGISTRYINDEX);
+  if (!lua_istable(L, -1)) {
+    lua_pop(L, 1);
+    lua_createtable(L, 0, 1);
+    lua_pushlightuserdata(L, (void *)&KEY_HOOK);
+    lua_pushvalue(L, -2);
+    lua_rawset(L, LUA_REGISTRYINDEX);
+  }
+}
+
+
+static int db_sethook (lua_State *L) {
+  int arg, mask, count;
+  lua_Hook func;
+  lua_State *L1 = getthread(L, &arg);
+  if (lua_isnoneornil(L, arg+1)) {
+    lua_settop(L, arg+1);
+    func = NULL; mask = 0; count = 0;  /* turn off hooks */
+  }
+  else {
+    const char *smask = luaL_checkstring(L, arg+2);
+    luaL_checktype(L, arg+1, LUA_TFUNCTION);
+    count = luaL_optint(L, arg+3, 0);
+    func = hookf; mask = makemask(smask, count);
+  }
+  gethooktable(L);
+  lua_pushlightuserdata(L, L1);
+  lua_pushvalue(L, arg+1);
+  lua_rawset(L, -3);  /* set new hook */
+  lua_pop(L, 1);  /* remove hook table */
+  lua_sethook(L1, func, mask, count);  /* set hooks */
+  return 0;
+}
+
+
+static int db_gethook (lua_State *L) {
+  int arg;
+  lua_State *L1 = getthread(L, &arg);
+  char buff[5];
+  int mask = lua_gethookmask(L1);
+  lua_Hook hook = lua_gethook(L1);
+  if (hook != NULL && hook != hookf)  /* external hook? */
+    lua_pushliteral(L, "external hook");
+  else {
+    gethooktable(L);
+    lua_pushlightuserdata(L, L1);
+    lua_rawget(L, -2);   /* get hook */
+    lua_remove(L, -2);  /* remove hook table */
+  }
+  lua_pushstring(L, unmakemask(mask, buff));
+  lua_pushinteger(L, lua_gethookcount(L1));
+  return 3;
+}
+
+
+#define LEVELS1	12	/* size of the first part of the stack */
+#define LEVELS2	10	/* size of the second part of the stack */
+
+static int db_errorfb (lua_State *L) {
+  int level;
+  int firstpart = 1;  /* still before eventual `...' */
+  int arg;
+  lua_State *L1 = getthread(L, &arg);
+  lua_Debug ar;
+  if (lua_isnumber(L, arg+2)) {
+    level = (int)lua_tointeger(L, arg+2);
+    lua_pop(L, 1);
+  }
+  else
+    level = (L == L1) ? 1 : 0;  /* level 0 may be this own function */
+  if (lua_gettop(L) == arg)
+    lua_pushliteral(L, "");
+  else if (!lua_isstring(L, arg+1)) return 1;  /* message is not a string */
+  else lua_pushliteral(L, "\n");
+  lua_pushliteral(L, "stack traceback:");
+  while (lua_getstack(L1, level++, &ar)) {
+    if (level > LEVELS1 && firstpart) {
+      /* no more than `LEVELS2' more levels? */
+      if (!lua_getstack(L1, level+LEVELS2, &ar))
+        level--;  /* keep going */
+      else {
+        lua_pushliteral(L, "\n\t...");  /* too many levels */
+        while (lua_getstack(L1, level+LEVELS2, &ar))  /* find last levels */
+          level++;
+      }
+      firstpart = 0;
+      continue;
+    }
+    lua_pushliteral(L, "\n\t");
+    lua_getinfo(L1, "Snl", &ar);
+    lua_pushfstring(L, "%s:", ar.short_src);
+    if (ar.currentline > 0)
+      lua_pushfstring(L, "%d:", ar.currentline);
+    if (*ar.namewhat != '\0')  /* is there a name? */
+        lua_pushfstring(L, " in function " LUA_QS, ar.name);
+    else {
+      if (*ar.what == 'm')  /* main? */
+        lua_pushfstring(L, " in main chunk");
+      else if (*ar.what == 'C' || *ar.what == 't')
+        lua_pushliteral(L, " ?");  /* C function or tail call */
+      else
+        lua_pushfstring(L, " in function <%s:%d>",
+                           ar.short_src, ar.linedefined);
+    }
+    lua_concat(L, lua_gettop(L) - arg);
+  }
+  lua_concat(L, lua_gettop(L) - arg);
+  return 1;
+}
+
+
+static const luaL_Reg dblib[] = {
+  {"gethook", db_gethook},
+  {"getinfo", db_getinfo},
+  {"getlocal", db_getlocal},
+  {"getupvalue", db_getupvalue},
+  {"sethook", db_sethook},
+  {"traceback", db_errorfb},
+  {NULL, NULL}
+};
+
+
+LUALIB_API int luaopen_debug (lua_State *L) {
+  luaL_register(L, LUA_DBLIBNAME, dblib);
+  return 1;
+}
diff --git a/src/blua/linit.c b/src/blua/linit.c
index dcf05d9f2c925e49457514c57f99afe7bf66c1d8..392d45dce47adf40ce753e400dcf1df2e2f46d91 100644
--- a/src/blua/linit.c
+++ b/src/blua/linit.c
@@ -20,6 +20,7 @@ static const luaL_Reg lualibs[] = {
   {LUA_IOLIBNAME, luaopen_io},
   {LUA_OSLIBNAME, luaopen_os},
   {LUA_STRLIBNAME, luaopen_string},
+  {LUA_DBLIBNAME, luaopen_debug},
   {NULL, NULL}
 };
 
diff --git a/src/blua/lualib.h b/src/blua/lualib.h
index 7127e4d77e56c1d2aee9c7079b59c5a3adc959a2..70facb70cca96db5924b35ad2f74bc6148509759 100644
--- a/src/blua/lualib.h
+++ b/src/blua/lualib.h
@@ -30,6 +30,9 @@ LUALIB_API int (luaopen_os) (lua_State *L);
 #define LUA_STRLIBNAME	"string"
 LUALIB_API int (luaopen_string) (lua_State *L);
 
+#define LUA_DBLIBNAME	"debug"
+LUALIB_API int (luaopen_debug) (lua_State *L);
+
 
 /* open all previous libraries */
 LUALIB_API void (luaL_openlibs) (lua_State *L);
diff --git a/src/byteptr.h b/src/byteptr.h
index 8ab359c4c4d9d00a0f613a86588b96a46112cd68..61cefd350b758835a1b5b0c0f2853d04e95c2954 100644
--- a/src/byteptr.h
+++ b/src/byteptr.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -11,9 +11,6 @@
 /// \brief Macros to read/write from/to a UINT8 *,
 ///        used for packet creation and such
 
-#if defined (__alpha__) || defined (__arm__) || defined (__mips__) || defined (__ia64__) || defined (__clang__)
-#define DEALIGNED
-#endif
 
 #include "endian.h"
 
@@ -21,7 +18,6 @@
 //
 // Little-endian machines
 //
-#ifdef DEALIGNED
 #define WRITEUINT8(p,b)     do {   UINT8 *p_tmp = (void *)p; const   UINT8 tv = (  UINT8)(b); memcpy(p, &tv, sizeof(  UINT8)); p_tmp++; p = (void *)p_tmp; } while (0)
 #define WRITESINT8(p,b)     do {   SINT8 *p_tmp = (void *)p; const   SINT8 tv = (  UINT8)(b); memcpy(p, &tv, sizeof(  UINT8)); p_tmp++; p = (void *)p_tmp; } while (0)
 #define WRITEINT16(p,b)     do {   INT16 *p_tmp = (void *)p; const   INT16 tv = (  INT16)(b); memcpy(p, &tv, sizeof(  INT16)); p_tmp++; p = (void *)p_tmp; } while (0)
@@ -31,20 +27,8 @@
 #define WRITECHAR(p,b)      do {    char *p_tmp = (void *)p; const    char tv = (   char)(b); memcpy(p, &tv, sizeof(   char)); p_tmp++; p = (void *)p_tmp; } while (0)
 #define WRITEFIXED(p,b)     do { fixed_t *p_tmp = (void *)p; const fixed_t tv = (fixed_t)(b); memcpy(p, &tv, sizeof(fixed_t)); p_tmp++; p = (void *)p_tmp; } while (0)
 #define WRITEANGLE(p,b)     do { angle_t *p_tmp = (void *)p; const angle_t tv = (angle_t)(b); memcpy(p, &tv, sizeof(angle_t)); p_tmp++; p = (void *)p_tmp; } while (0)
-#else
-#define WRITEUINT8(p,b)     do {   UINT8 *p_tmp = (  UINT8 *)p; *p_tmp = (  UINT8)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITESINT8(p,b)     do {   SINT8 *p_tmp = (  SINT8 *)p; *p_tmp = (  SINT8)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITEINT16(p,b)     do {   INT16 *p_tmp = (  INT16 *)p; *p_tmp = (  INT16)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITEUINT16(p,b)    do {  UINT16 *p_tmp = ( UINT16 *)p; *p_tmp = ( UINT16)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITEINT32(p,b)     do {   INT32 *p_tmp = (  INT32 *)p; *p_tmp = (  INT32)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITEUINT32(p,b)    do {  UINT32 *p_tmp = ( UINT32 *)p; *p_tmp = ( UINT32)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITECHAR(p,b)      do {    char *p_tmp = (   char *)p; *p_tmp = (   char)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITEFIXED(p,b)     do { fixed_t *p_tmp = (fixed_t *)p; *p_tmp = (fixed_t)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#define WRITEANGLE(p,b)     do { angle_t *p_tmp = (angle_t *)p; *p_tmp = (angle_t)(b); p_tmp++; p = (void *)p_tmp; } while (0)
-#endif
 
 #ifdef __GNUC__
-#ifdef DEALIGNED
 #define READUINT8(p)        ({   UINT8 *p_tmp = (void *)p;   UINT8 b; memcpy(&b, p, sizeof(  UINT8)); p_tmp++; p = (void *)p_tmp; b; })
 #define READSINT8(p)        ({   SINT8 *p_tmp = (void *)p;   SINT8 b; memcpy(&b, p, sizeof(  SINT8)); p_tmp++; p = (void *)p_tmp; b; })
 #define READINT16(p)        ({   INT16 *p_tmp = (void *)p;   INT16 b; memcpy(&b, p, sizeof(  INT16)); p_tmp++; p = (void *)p_tmp; b; })
@@ -55,17 +39,6 @@
 #define READFIXED(p)        ({ fixed_t *p_tmp = (void *)p; fixed_t b; memcpy(&b, p, sizeof(fixed_t)); p_tmp++; p = (void *)p_tmp; b; })
 #define READANGLE(p)        ({ angle_t *p_tmp = (void *)p; angle_t b; memcpy(&b, p, sizeof(angle_t)); p_tmp++; p = (void *)p_tmp; b; })
 #else
-#define READUINT8(p)        ({   UINT8 *p_tmp = (  UINT8 *)p;   UINT8 b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READSINT8(p)        ({   SINT8 *p_tmp = (  SINT8 *)p;   SINT8 b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READINT16(p)        ({   INT16 *p_tmp = (  INT16 *)p;   INT16 b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READUINT16(p)       ({  UINT16 *p_tmp = ( UINT16 *)p;  UINT16 b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READINT32(p)        ({   INT32 *p_tmp = (  INT32 *)p;   INT32 b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READUINT32(p)       ({  UINT32 *p_tmp = ( UINT32 *)p;  UINT32 b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READCHAR(p)         ({    char *p_tmp = (   char *)p;    char b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READFIXED(p)        ({ fixed_t *p_tmp = (fixed_t *)p; fixed_t b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#define READANGLE(p)        ({ angle_t *p_tmp = (angle_t *)p; angle_t b = *p_tmp; p_tmp++; p = (void *)p_tmp; b; })
-#endif
-#else
 #define READUINT8(p)        *((  UINT8 *)p)++
 #define READSINT8(p)        *((  SINT8 *)p)++
 #define READINT16(p)        *((  INT16 *)p)++
@@ -148,8 +121,6 @@ FUNCINLINE static ATTRINLINE UINT32 readulong(void *ptr)
 #define READANGLE(p)        ({ angle_t *p_tmp = (angle_t *)p; angle_t b =  readulong(p); p_tmp++; p = (void *)p_tmp; b; })
 #endif //SRB2_BIG_ENDIAN
 
-#undef DEALIGNED
-
 #define WRITESTRINGN(p, s, n) {                            \
 	size_t tmp_i;                                           \
                                                             \
diff --git a/src/command.c b/src/command.c
index a46cc98bcde4ad01557a31e439994135341a5c06..bc24d5e0524b00aa9123007190b3dd68689cf1cd 100644
--- a/src/command.c
+++ b/src/command.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -648,7 +648,7 @@ static void COM_ExecuteString(char *ptext)
 		{
 			if ((com_flags & COM_LUA) && !(cmd->flags & COM_LUA))
 			{
-				CONS_Alert(CONS_WARNING, "Command '%s' cannot be run from Lua.\n", cmd->name);
+				CONS_Alert(CONS_WARNING, "Command '%s' cannot be run from a script.\n", cmd->name);
 				return;
 			}
 
@@ -705,7 +705,7 @@ static void add_alias(char *newname, char *newcmd)
 	{
 		if (!stricmp(newname, a->name))
 		{
-			Z_Free(a->value); // Free old cmd 
+			Z_Free(a->value); // Free old cmd
 			a->value = newcmd;
 			return;
 		}
@@ -809,49 +809,60 @@ static void COM_CEchoDuration_f(void)
 
 /** Executes a script file.
   */
-static void COM_Exec_f(void)
+boolean COM_ExecFile(const char *scriptname, com_flags_t flags, boolean silent)
 {
 	UINT8 *buf = NULL;
 	char filename[256];
 
-	if (COM_Argc() < 2 || COM_Argc() > 3)
-	{
-		CONS_Printf(M_GetText("exec <filename>: run a script file\n"));
-		return;
-	}
-
-	if (!D_CheckPathAllowed(COM_Argv(1), "tried to exec"))
-		return;
+	if (!D_CheckPathAllowed(scriptname, "tried to exec"))
+		return false;
 
 	// load file
 	// Try with Argv passed verbatim first, for back compat
-	FIL_ReadFile(COM_Argv(1), &buf);
+	FIL_ReadFile(scriptname, &buf);
 
 	if (!buf)
 	{
 		// Now try by searching the file path
 		// filename is modified with the full found path
-		strcpy(filename, COM_Argv(1));
+		strlcpy(filename, scriptname, sizeof(filename));
 		if (findfile(filename, NULL, true) != FS_NOTFOUND)
 			FIL_ReadFile(filename, &buf);
 
 		if (!buf)
-		{
-			if (!COM_CheckParm("-noerror"))
-				CONS_Printf(M_GetText("couldn't execute file %s\n"), COM_Argv(1));
-			return;
-		}
+			return false;
 	}
 
-	if (!COM_CheckParm("-silent"))
-		CONS_Printf(M_GetText("executing %s\n"), COM_Argv(1));
+	if (!silent)
+		CONS_Printf(M_GetText("Executing %s\n"), scriptname);
 
 	// insert text file into the command buffer
-	COM_BufAddTextEx((char *)buf, com_flags);
-	COM_BufAddTextEx("\n", com_flags);
+	COM_BufAddTextEx((char *)buf, flags);
+	COM_BufAddTextEx("\n", flags);
 
 	// free buffer
 	Z_Free(buf);
+
+	return true;
+}
+
+static void COM_Exec_f(void)
+{
+	boolean silent;
+
+	if (COM_Argc() < 2 || COM_Argc() > 3)
+	{
+		CONS_Printf(M_GetText("exec <filename>: run a script file\n"));
+		return;
+	}
+
+	silent = COM_CheckParm("-silent");
+
+	if (COM_ExecFile(COM_Argv(1), com_flags, silent))
+		return;
+
+	if (!COM_CheckParm("-noerror"))
+		CONS_Printf(M_GetText("Couldn't execute file %s\n"), COM_Argv(1));
 }
 
 /** Delays execution of the rest of the commands until the next frame.
@@ -1722,8 +1733,7 @@ badinput:
 
 static boolean serverloading = false;
 
-static consvar_t *
-ReadNetVar (UINT8 **p, char **return_value, boolean *return_stealth)
+static consvar_t *ReadNetVar(save_t *p, char **return_value, boolean *return_stealth)
 {
 	UINT16  netid;
 	char   *val;
@@ -1731,10 +1741,10 @@ ReadNetVar (UINT8 **p, char **return_value, boolean *return_stealth)
 
 	consvar_t *cvar;
 
-	netid   = READUINT16 (*p);
-	val     = (char *)*p;
-	SKIPSTRING (*p);
-	stealth = READUINT8  (*p);
+	netid   = P_ReadUINT16(p);
+	val     = (char *)&p->buf[p->pos];
+	P_SkipString(p);
+	stealth = P_ReadUINT8(p);
 
 	cvar = CV_FindNetVar(netid);
 
@@ -1752,8 +1762,7 @@ ReadNetVar (UINT8 **p, char **return_value, boolean *return_stealth)
 }
 
 #ifdef OLD22DEMOCOMPAT
-static consvar_t *
-ReadOldDemoVar (UINT8 **p, char **return_value, boolean *return_stealth)
+static consvar_t *ReadOldDemoVar(save_t *p, char **return_value, boolean *return_stealth)
 {
 	UINT16  id;
 	char   *val;
@@ -1761,10 +1770,10 @@ ReadOldDemoVar (UINT8 **p, char **return_value, boolean *return_stealth)
 
 	old_demo_var_t *demovar;
 
-	id      = READUINT16 (*p);
-	val     = (char *)*p;
-	SKIPSTRING (*p);
-	stealth = READUINT8  (*p);
+	id      = P_ReadUINT16(p);
+	val     = (char *)&p->buf[p->pos];
+	P_SkipString(p);
+	stealth = P_ReadUINT8(p);
 
 	demovar = CV_FindOldDemoVar(id);
 
@@ -1783,8 +1792,7 @@ ReadOldDemoVar (UINT8 **p, char **return_value, boolean *return_stealth)
 }
 #endif/*OLD22DEMOCOMPAT*/
 
-static consvar_t *
-ReadDemoVar (UINT8 **p, char **return_value, boolean *return_stealth)
+static consvar_t *ReadDemoVar(save_t *p, char **return_value, boolean *return_stealth)
 {
 	char   *name;
 	char   *val;
@@ -1792,11 +1800,11 @@ ReadDemoVar (UINT8 **p, char **return_value, boolean *return_stealth)
 
 	consvar_t *cvar;
 
-	name    = (char *)*p;
-	SKIPSTRING (*p);
-	val     = (char *)*p;
-	SKIPSTRING (*p);
-	stealth = READUINT8  (*p);
+	name    = (char *)&p->buf[p->pos];
+	P_SkipString(p);
+	val     = (char *)&p->buf[p->pos];
+	P_SkipString(p);
+	stealth = P_ReadUINT8(p);
 
 	cvar = CV_FindVar(name);
 
@@ -1826,41 +1834,46 @@ static void Got_NetVar(UINT8 **p, INT32 playernum)
 		return;
 	}
 
-	cvar = ReadNetVar(p, &svalue, &stealth);
+	save_t save_p;
+	save_p.buf = *p;
+	save_p.size = MAXTEXTCMD;
+	save_p.pos = 0;
+	cvar = ReadNetVar(&save_p, &svalue, &stealth);
+	*p = &save_p.buf[save_p.pos];
 
 	if (cvar)
 		Setvalue(cvar, svalue, stealth);
 }
 
-void CV_SaveVars(UINT8 **p, boolean in_demo)
+void CV_SaveVars(save_t *p, boolean in_demo)
 {
 	consvar_t *cvar;
-	UINT8 *count_p = *p;
+	UINT8 *count_p = &p->buf[p->pos];
 	UINT16 count = 0;
 
 	// send only changed cvars ...
 	// the client will reset all netvars to default before loading
-	WRITEUINT16(*p, 0x0000);
+	P_WriteUINT16(p, 0x0000);
 	for (cvar = consvar_vars; cvar; cvar = cvar->next)
 		if ((cvar->flags & CV_NETVAR) && !CV_IsSetToDefault(cvar))
 		{
 			if (in_demo)
 			{
-				WRITESTRING(*p, cvar->name);
+				P_WriteString(p, cvar->name);
 			}
 			else
 			{
-				WRITEUINT16(*p, cvar->netid);
+				P_WriteUINT16(p, cvar->netid);
 			}
-			WRITESTRING(*p, cvar->string);
-			WRITEUINT8(*p, false);
+			P_WriteString(p, cvar->string);
+			P_WriteUINT8(p, false);
 			++count;
 		}
 	WRITEUINT16(count_p, count);
 }
 
-static void CV_LoadVars(UINT8 **p,
-		consvar_t *(*got)(UINT8 **p, char **ret_value, boolean *ret_stealth))
+static void CV_LoadVars(save_t *p,
+		consvar_t *(*got)(save_t *p, char **ret_value, boolean *ret_stealth))
 {
 	const boolean store = (client || demoplayback);
 
@@ -1888,7 +1901,7 @@ static void CV_LoadVars(UINT8 **p,
 		}
 	}
 
-	count = READUINT16(*p);
+	count = P_ReadUINT16(p);
 	while (count--)
 	{
 		cvar = (*got)(p, &val, &stealth);
@@ -1921,19 +1934,19 @@ void CV_RevertNetVars(void)
 	}
 }
 
-void CV_LoadNetVars(UINT8 **p)
+void CV_LoadNetVars(save_t *p)
 {
 	CV_LoadVars(p, ReadNetVar);
 }
 
 #ifdef OLD22DEMOCOMPAT
-void CV_LoadOldDemoVars(UINT8 **p)
+void CV_LoadOldDemoVars(save_t *p)
 {
 	CV_LoadVars(p, ReadOldDemoVar);
 }
 #endif
 
-void CV_LoadDemoVars(UINT8 **p)
+void CV_LoadDemoVars(save_t *p)
 {
 	CV_LoadVars(p, ReadDemoVar);
 }
@@ -1986,13 +1999,13 @@ static void CV_SetCVar(consvar_t *var, const char *value, boolean stealth)
 	if (!var->string)
 		I_Error("CV_Set: %s no string set!\n", var->name);
 #endif
-	if (!var || !var->string || !value || !stricmp(var->string, value))
+	if (!var || !var->string || !value || (var->can_change == NULL && !stricmp(var->string, value)))
 		return; // no changes
 
 	if (var->flags & CV_NETVAR)
 	{
 		// send the value of the variable
-		UINT8 buf[128];
+		UINT8 buf[512];
 		UINT8 *p = buf;
 
 		// Loading from a config in a netgame? Set revert value.
@@ -2065,11 +2078,10 @@ static void CV_SetValueMaybeStealth(consvar_t *var, INT32 value, boolean stealth
 	if (var == &cv_forceskin) // Special handling.
 	{
 		const char *tmpskin = NULL;
-		if ((value < 0) || (value >= numskins))
-			;
-		else
+		if (value >= 0 && value < numskins)
 			tmpskin = skins[value]->name;
-		memcpy(val, tmpskin, SKINNAMESIZE);
+		if (tmpskin)
+			memcpy(val, tmpskin, SKINNAMESIZE);
 	}
 	else
 		sprintf(val, "%d", value);
@@ -2470,6 +2482,22 @@ static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr)
 		if (!CV_FilterJoyAxisVars(v, valstr))
 			return false;
 	}
+
+	if (GETMAJOREXECVERSION(cv_execversion.value) < 57) // 57 = 2.2.16
+	{
+		if (
+			(!stricmp(v->name, "movebob") && atoi(valstr) == FRACUNIT) ||
+			(!stricmp(v->name, "playersforexit") && atoi(valstr) == 4) || // 4 = all
+			(!stricmp(v->name, "advancemap") && atoi(valstr) == 1) || // 1 = next
+			(!stricmp(v->name, "cam_speed") && !stricmp(valstr, "0.3")) ||
+			(!stricmp(v->name, "cam2_speed") && !stricmp(valstr, "0.3")) ||
+			(!stricmp(v->name, "timerres") && atoi(valstr) == 0) || // 0 = classic
+			(!stricmp(v->name, "gr_modelinterpolation")) || // Force reset
+			(!stricmp(v->name, "fov") && atoi(valstr) == 90)
+		)
+			return false;
+	}
+
 	return true;
 }
 
@@ -2492,7 +2520,7 @@ static boolean CV_Command(void)
 
 	if (CV_Immutable(v))
 	{
-		CONS_Alert(CONS_WARNING, "Variable '%s' cannot be changed from Lua.\n", v->name);
+		CONS_Alert(CONS_WARNING, "Variable '%s' cannot be changed from a script.\n", v->name);
 		return true;
 	}
 
diff --git a/src/command.h b/src/command.h
index f0dc62418a8815abfa9e7c9b84d9357aac406b73..70342a78512783e6ea3bf3fc2122bee21d074099 100644
--- a/src/command.h
+++ b/src/command.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -15,6 +15,7 @@
 
 #include <stdio.h>
 #include "doomdef.h"
+#include "p_saveg.h"
 
 //===================================
 // Command buffer & command execution
@@ -65,6 +66,9 @@ void COM_ImmedExecute(const char *ptext);
 // Execute commands in buffer, flush them
 void COM_BufExecute(void);
 
+// Executes a script from a file
+boolean COM_ExecFile(const char *scriptname, com_flags_t flags, boolean silent);
+
 // As above; and progress the wait timer.
 void COM_BufTicker(void);
 
@@ -218,19 +222,19 @@ void CV_AddValue(consvar_t *var, INT32 increment);
 void CV_SaveVariables(FILE *f);
 
 // load/save gamesate (load and save option and for network join in game)
-void CV_SaveVars(UINT8 **p, boolean in_demo);
+void CV_SaveVars(save_t *p, boolean in_demo);
 
 #define CV_SaveNetVars(p) CV_SaveVars(p, false)
-void CV_LoadNetVars(UINT8 **p);
+void CV_LoadNetVars(save_t *p);
 
 // then revert after leaving a netgame
 void CV_RevertNetVars(void);
 
 #define CV_SaveDemoVars(p) CV_SaveVars(p, true)
-void CV_LoadDemoVars(UINT8 **p);
+void CV_LoadDemoVars(save_t *p);
 
 #ifdef OLD22DEMOCOMPAT
-void CV_LoadOldDemoVars(UINT8 **p);
+void CV_LoadOldDemoVars(save_t *p);
 #endif
 
 // reset cheat netvars after cheats is deactivated
diff --git a/src/config.h.in b/src/config.h.in
index 6d49a698934bf8b498f6bfb402bbaa7f8ff93191..0f24cfc4466e3b8387777ba68a1b83ead3f3dbc3 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -40,12 +40,14 @@
  * 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
+ * Last updated 2025 / 01 / 16 - v2.2.14 - main assets
+ * Last updated 2025 / 01 / 24 - v2.2.15 - main assets
  */
-#define ASSET_HASH_SRB2_PK3   "ad911f29a28a18968ee5b2d11c2acb39"
-#define ASSET_HASH_ZONES_PK3  "1c8adf8d079ecb87d00081f158acf3c7"
-#define ASSET_HASH_PLAYER_DTA "2e7aaae8a6b1b77d90ffe7606ceadb6c"
+#define ASSET_HASH_SRB2_PK3       "3182ce524acc2072ddaa81acf4b6a9aa"
+#define ASSET_HASH_ZONES_PK3      "88ff4c300851ccdb0406698eadd89907"
+#define ASSET_HASH_CHARACTERS_PK3 "5c5936b8a690e007c0939bd0785a41fb"
 #ifdef USE_PATCH_DTA
-#define ASSET_HASH_PATCH_PK3  "3c7b73f34af7e9a7bceb2d5260f76172"
+#define ASSET_HASH_PATCH_PK3      "00000000000000000000000000000000"
 #endif
 
 #endif
diff --git a/src/console.c b/src/console.c
index 874fc2a4f288eb261a367efd68c488dcd770fb72..29510435ae445396fd9bf3de25bec87d48dc5c09 100644
--- a/src/console.c
+++ b/src/console.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -34,6 +34,7 @@
 #include "m_menu.h"
 #include "filesrch.h"
 #include "m_misc.h"
+#include "lua_libs.h"
 
 #ifdef _WINDOWS
 #include "win32/win_main.h"
@@ -918,12 +919,26 @@ static void CON_InputDelChar(void)
 // ----
 //
 
+//
+// Same as CON_Responder, but is process before everything else, so it cannot be blocked.
+//
+boolean CON_PreResponder(event_t *ev)
+{
+	if (ev->type == ev_keydown && shiftdown == 1 && ev->key == KEY_ESCAPE)
+	{
+		I_SetTextInputMode(con_destlines == 0 ? true : textinputmodeenabledbylua); // inverse, since this is changed next tic.
+		consoletoggle = true;
+		return true;
+	}
+
+	return false;
+}
+
+//
 // Handles console key input
 //
 boolean CON_Responder(event_t *ev)
 {
-	static UINT8 consdown = false; // console is treated differently due to rare usage
-
 	// sequential completions a la 4dos
 	static char completioncmd[80 + sizeof("find ")] = "find ";
 	static char *completion = &completioncmd[sizeof("find ")-1];
@@ -943,32 +958,31 @@ boolean CON_Responder(event_t *ev)
 	// let go keyup events, don't eat them
 	if (ev->type != ev_keydown && ev->type != ev_text && ev->type != ev_console)
 	{
-		if (ev->key == gamecontrol[GC_CONSOLE][0] || ev->key == gamecontrol[GC_CONSOLE][1])
-			consdown = false;
 		return false;
 	}
 
 	key = ev->key;
 
 	// check for console toggle key
-	if (ev->type != ev_console)
+	if (ev->type == ev_keydown)
 	{
 		if (modeattacking || metalrecording || marathonmode)
 			return false;
 
-		if (ev->type == ev_keydown && ((key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1]) && !shiftdown))
+		if ((key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1]) && !shiftdown)
 		{
-			if (consdown) // ignore repeat
-				return true;
+			if (con_destlines == 0 && I_GetTextInputMode())
+				return false; // some other component is holding keyboard input, don't hijack it!
+
+			I_SetTextInputMode(con_destlines == 0 ? true : textinputmodeenabledbylua); // inverse, since this is changed next tic.
 			consoletoggle = true;
-			consdown = true;
 			return true;
 		}
 
 		// check other keys only if console prompt is active
 		if (!consoleready && key < NUMINPUTS) // metzgermeister: boundary check!!
 		{
-			if (ev->type == ev_keydown && !menuactive && bindtable[key])
+			if (!menuactive && bindtable[key])
 			{
 				COM_BufAddText(bindtable[key]);
 				COM_BufAddText("\n");
@@ -980,14 +994,14 @@ boolean CON_Responder(event_t *ev)
 		// escape key toggle off console
 		if (key == KEY_ESCAPE)
 		{
+			I_SetTextInputMode(textinputmodeenabledbylua);
 			consoletoggle = true;
 			return true;
 		}
 	}
-
-	if (ev->type == ev_text)
+	else if (ev->type == ev_text)
 	{
-		if (!consoletoggle)
+		if (!consoletoggle && consoleready)
 			CON_InputAddChar(key);
 		return true;
 	}
@@ -1036,7 +1050,7 @@ boolean CON_Responder(event_t *ev)
 	}
 	else if (key == KEY_BACKSPACE)
 	{
-		if (ctrldown)
+		if (ctrldown && input_cur != 0)
 		{
 			input_sel = M_JumpWordReverse(inputlines[inputline], input_cur);
 			CON_InputDelSelection();
@@ -1094,7 +1108,9 @@ boolean CON_Responder(event_t *ev)
 
 		if (key == 'x' || key == 'X')
 		{
-			if (input_sel > input_cur)
+			if (input_sel == input_cur) // Don't replace the clipboard without a text selection
+				return true;
+			else if (input_sel > input_cur)
 				I_ClipboardCopy(&inputlines[inputline][input_cur], input_sel-input_cur);
 			else
 				I_ClipboardCopy(&inputlines[inputline][input_sel], input_cur-input_sel);
@@ -1104,7 +1120,9 @@ boolean CON_Responder(event_t *ev)
 		}
 		else if (key == 'c' || key == 'C')
 		{
-			if (input_sel > input_cur)
+			if (input_sel == input_cur) // Don't replace the clipboard without a text selection
+				return true;
+			else if (input_sel > input_cur)
 				I_ClipboardCopy(&inputlines[inputline][input_cur], input_sel-input_cur);
 			else
 				I_ClipboardCopy(&inputlines[inputline][input_sel], input_cur-input_sel);
@@ -1730,12 +1748,12 @@ static void CON_DrawBackpic(void)
 
 	// Get the lumpnum for CONSBACK, STARTUP (Only during game startup) or fallback into MISSING.
 	if (con_startup)
-		piclump = W_CheckNumForName("STARTUP");
+		piclump = W_CheckNumForPatchName("STARTUP");
 	else
-		piclump = W_CheckNumForName("CONSBACK");
+		piclump = W_CheckNumForPatchName("CONSBACK");
 
 	if (piclump == LUMPERROR)
-		piclump = W_GetNumForName("MISSING");
+		piclump = W_GetNumForPatchName("MISSING");
 
 	// Cache the patch.
 	con_backpic = W_CachePatchNum(piclump, PU_PATCH);
diff --git a/src/console.h b/src/console.h
index 6ad1dba1e458c19d47bb2599f9d3a87ce0fb17c1..664b74a223826ee755078f56a046529c553c15b4 100644
--- a/src/console.h
+++ b/src/console.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -19,6 +19,7 @@ void CON_Init(void);
 void CON_StartRefresh(void);
 void CON_StopRefresh(void);
 
+boolean CON_PreResponder(event_t *ev);
 boolean CON_Responder(event_t *ev);
 
 #ifdef HAVE_THREADS
diff --git a/src/d_main.c b/src/d_main.c
index c139650d1eb039a057da10d063f392f33c94fac2..2b8d4326a0868fe2cbced6bc40274b96d2598c95 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -227,6 +227,9 @@ void D_ProcessEvents(void)
 			}
 		}
 
+		if (CON_PreResponder(ev))
+			continue;
+
 		// Screenshots over everything so that they can be taken anywhere.
 		if (M_ScreenshotResponder(ev))
 			continue; // ate the event
@@ -679,13 +682,13 @@ static void D_Display(void)
 			s[sizeof s - 1] = '\0';
 
 			snprintf(s, sizeof s - 1, "get %d b/s", getbps);
-			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-40, V_YELLOWMAP, s);
+			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-40, V_YELLOWMAP, s);
 			snprintf(s, sizeof s - 1, "send %d b/s", sendbps);
-			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-30, V_YELLOWMAP, s);
+			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-30, V_YELLOWMAP, s);
 			snprintf(s, sizeof s - 1, "GameMiss %.2f%%", gamelostpercent);
-			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-20, V_YELLOWMAP, s);
+			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-20, V_YELLOWMAP, s);
 			snprintf(s, sizeof s - 1, "SysMiss %.2f%%", lostpercent);
-			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-10, V_YELLOWMAP, s);
+			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-10, V_YELLOWMAP, s);
 		}
 
 		if (cv_perfstats.value)
@@ -758,9 +761,9 @@ void D_SRB2Loop(void)
 	/* Smells like a hack... Don't fade Sonic's ass into the title screen. */
 	if (gamestate != GS_TITLESCREEN)
 	{
-		gstartuplumpnum = W_CheckNumForName("STARTUP");
+		gstartuplumpnum = W_CheckNumForPatchName("STARTUP");
 		if (gstartuplumpnum == LUMPERROR)
-			gstartuplumpnum = W_GetNumForName("MISSING");
+			gstartuplumpnum = W_GetNumForPatchName("MISSING");
 		V_DrawScaledPatch(0, 0, 0, W_CachePatchNum(gstartuplumpnum, PU_PATCH));
 	}
 
@@ -983,7 +986,7 @@ void D_StartTitle(void)
 	emeralds = 0;
 	memset(&luabanks, 0, sizeof(luabanks));
 	lastmaploaded = 0;
-	pickedchar = R_SkinAvailable(cv_defaultskin.string);
+	pickedchar = R_SkinAvailable(cv_skin.string);
 
 	// In case someone exits out at the same time they start a time attack run,
 	// reset modeattacking
@@ -1024,7 +1027,7 @@ void D_StartTitle(void)
 #define REALLOC_FILE_LIST \
 	if (list->files == NULL) \
 	{ \
-		list->files = calloc(sizeof(list->files), 2); \
+		list->files = calloc(2, sizeof(list->files)); \
 		list->numfiles = 1; \
 	} \
 	else \
@@ -1179,8 +1182,8 @@ static void IdentifyVersion(void)
 	// Add the maps
 	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "zones.pk3"));
 
-	// Add the players
-	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "player.dta"));
+	// Add the characters
+	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "characters.pk3"));
 
 #ifdef USE_PATCH_DTA
 	// Add our crappy patches to fix our bugs
@@ -1199,7 +1202,7 @@ static void IdentifyVersion(void)
 				I_Error("File "str" has been modified with non-music/sound lumps"); \
 		}
 
-		MUSICTEST("music.dta")
+		MUSICTEST("music.pk3")
 		//MUSICTEST("patch_music.pk3")
 	}
 #endif
@@ -1250,7 +1253,7 @@ void D_SRB2Main(void)
 	// Print GPL notice for our console users (Linux)
 	CONS_Printf(
 	"\n\nSonic Robo Blast 2\n"
-	"Copyright (C) 1998-2023 by Sonic Team Junior\n\n"
+	"Copyright (C) 1998-2025 by Sonic Team Junior\n\n"
 	"This program comes with ABSOLUTELY NO WARRANTY.\n\n"
 	"This is free software, and you are welcome to redistribute it\n"
 	"and/or modify it under the terms of the GNU General Public License\n"
@@ -1431,7 +1434,7 @@ void D_SRB2Main(void)
 	// Make backups of some SOCcable tables.
 	P_BackupTables();
 
-	mainwads = 3; // doesn't include music.dta
+	mainwads = 3; // doesn't include music.pk3
 #ifdef USE_PATCH_DTA
 	mainwads++;
 #endif
@@ -1446,11 +1449,11 @@ void D_SRB2Main(void)
 	// Check MD5s of autoloaded files
 	W_VerifyFileMD5(0, ASSET_HASH_SRB2_PK3); // srb2.pk3
 	W_VerifyFileMD5(1, ASSET_HASH_ZONES_PK3); // zones.pk3
-	W_VerifyFileMD5(2, ASSET_HASH_PLAYER_DTA); // player.dta
+	W_VerifyFileMD5(2, ASSET_HASH_CHARACTERS_PK3); // characters.pk3
 #ifdef USE_PATCH_DTA
 	W_VerifyFileMD5(3, ASSET_HASH_PATCH_PK3); // patch.pk3
 #endif
-	// don't check music.dta because people like to modify it, and it doesn't matter if they do
+	// don't check music.pk3 because people like to modify it, and it doesn't matter if they do
 	// ...except it does if they slip maps in there, and that's what W_VerifyNMUSlumps is for.
 #endif //ifndef DEVELOP
 
@@ -1535,7 +1538,7 @@ void D_SRB2Main(void)
 			I_Error("Cannot find a map remotely named '%s'\n", word);
 		else
 		{
-			if (!M_CheckParm("-server"))
+			if (!(M_CheckParm("-server") || dedicated))
 				G_SetUsedCheats(true);
 			autostart = true;
 		}
@@ -1600,7 +1603,7 @@ void D_SRB2Main(void)
 	{
 		if (!M_IsNextParm())
 			I_Error("usage: -room <room_id>\nCheck the Master Server's webpage for room ID numbers.\n");
-		ms_RoomId = atoi(M_GetNextParm());
+		CV_SetValue(&cv_masterserver_room_id, atoi(M_GetNextParm()));
 
 #ifdef UPDATE_ALERT
 		GetMODVersion_Console();
@@ -1842,17 +1845,21 @@ static boolean check_top_dir(const char **path, const char *top)
 	return true;
 }
 
-static int cmp_strlen_desc(const void *a, const void *b)
+static int cmp_strlen_desc(const void *A, const void *B)
 {
-	return ((int)strlen(*(const char*const*)b) - (int)strlen(*(const char*const*)a));
+	const char *pA = A;
+	const char *pB = B;
+	size_t As = strlen(pA);
+	size_t Bs = strlen(pB);
+	return ((int)Bs - (int)As);
 }
 
 boolean D_IsPathAllowed(const char *path)
 {
-	const char *paths[] = {
+	char *paths[] = {
 		srb2home,
 		srb2path,
-		cv_addons_folder.string
+		cv_addons_folder.zstring
 	};
 
 	const size_t n_paths = sizeof paths / sizeof *paths;
diff --git a/src/d_player.h b/src/d_player.h
index 95d2f609dab322189fcb3892d7beace6d0fd45dc..1dc3fb5293d7fd56baae6518f2ea29adea5baecc 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -45,14 +45,16 @@ typedef enum
 	SF_MARIODAMAGE      = SF_NOJUMPDAMAGE|SF_STOMPDAMAGE, // The Mario method of being able to damage enemies, etc.
 	SF_MACHINE          = 1<<10, // Beep boop. Are you a robot?
 	SF_DASHMODE         = 1<<11, // Sonic Advance 2 style top speed increase?
-	SF_FASTEDGE         = 1<<12, // Faster edge teeter?
-	SF_MULTIABILITY     = 1<<13, // Revenge of Final Demo.
-	SF_NONIGHTSROTATION = 1<<14, // Disable sprite rotation for NiGHTS
-	SF_NONIGHTSSUPER    = 1<<15, // Disable super colors for NiGHTS (if you have SF_SUPER)
-	SF_NOSUPERSPRITES   = 1<<16, // Don't use super sprites while super
-	SF_NOSUPERJUMPBOOST = 1<<17, // Disable the jump boost given while super (i.e. Knuckles)
-	SF_CANBUSTWALLS     = 1<<18, // Can naturally bust walls on contact? (i.e. Knuckles)
-	SF_NOSHIELDABILITY  = 1<<19, // Disable shield abilities
+	SF_FASTWAIT         = 1<<12, // Faster wait animation?
+	SF_FASTEDGE         = 1<<13, // Faster edge teeter?
+	SF_JETFUME          = 1<<14, // Follow item uses Metal Sonic's jet fume behavior
+	SF_MULTIABILITY     = 1<<15, // Revenge of Final Demo.
+	SF_NONIGHTSROTATION = 1<<16, // Disable sprite rotation for NiGHTS
+	SF_NONIGHTSSUPER    = 1<<17, // Disable super sprites and colors for NiGHTS
+	SF_NOSUPERSPRITES   = 1<<18, // Don't use super sprites while super
+	SF_NOSUPERJUMPBOOST = 1<<19, // Disable the jump boost given while super (i.e. Knuckles)
+	SF_CANBUSTWALLS     = 1<<20, // Can naturally bust walls on contact? (i.e. Knuckles)
+	SF_NOSHIELDABILITY  = 1<<21, // Disable shield abilities
 
 	// free up to and including 1<<31
 } skinflags_t;
@@ -158,10 +160,6 @@ typedef enum
 	PF_FORCESTRAFE = 1<<28, // Turning inputs are translated into strafing inputs
 	PF_CANCARRY    = 1<<29, // Can carry another player?
 	PF_FINISHED    = 1<<30, // The player finished the level. NOT the same as exiting
-	
-	// True if shield button down last tic
-	// This may be the final flag, but 2.3 could free up the others
-	PF_SHIELDDOWN    = 1<<31,
 
 	// up to 1<<31 is free
 } pflags_t;
diff --git a/src/d_think.h b/src/d_think.h
index 58912458783a2f3b2187e50ebaf00a667a0e0a78..76c1bb5b8af11a5557bab32705897445e699ca5b 100644
--- a/src/d_think.h
+++ b/src/d_think.h
@@ -51,6 +51,8 @@ typedef struct thinker_s
 	// killough 11/98: count of how many other objects reference
 	// this one using pointers. Used for garbage collection.
 	INT32 references;
+
+	boolean removing;
 	boolean cachable;
 
 #ifdef PARANOIA
diff --git a/src/d_ticcmd.h b/src/d_ticcmd.h
index 43eb0f00b8df58da69c2dc7123d274d69070c104..2481ed738b23769a546fef840500d10ba7b024f5 100644
--- a/src/d_ticcmd.h
+++ b/src/d_ticcmd.h
@@ -26,23 +26,20 @@
 // Button/action code definitions.
 typedef enum
 {
-	// First 3 bits are weapon change info, DO NOT USE!
-	BT_WEAPONMASK = 0x07,  //our first three bits.
-	
-	BT_SHIELD     = 1<<3,  // shield or super action
+	// First 4 bits are weapon change info, DO NOT USE!
+	BT_WEAPONMASK = 0x0F, //our first four bits.
 
-	BT_WEAPONNEXT = 1<<4,  // select next weapon
-	BT_WEAPONPREV = 1<<5,  // select previous weapon
+	BT_WEAPONNEXT = 1<<4,
+	BT_WEAPONPREV = 1<<5,
+
+	BT_ATTACK     = 1<<6, // shoot rings
+	BT_SPIN       = 1<<7,
+	BT_CAMLEFT    = 1<<8, // turn camera left
+	BT_CAMRIGHT   = 1<<9, // turn camera right
+	BT_TOSSFLAG   = 1<<10,
+	BT_JUMP       = 1<<11,
+	BT_FIRENORMAL = 1<<12, // Fire a normal ring no matter what
 
-	BT_ATTACK     = 1<<6,  // shoot rings
-	BT_SPIN       = 1<<7,  // spin action
-	BT_CAMLEFT    = 1<<8,  // turn camera left
-	BT_CAMRIGHT   = 1<<9,  // turn camera right
-	BT_TOSSFLAG   = 1<<10, // toss flag or emeralds
-	BT_JUMP       = 1<<11, // jump action
-	BT_FIRENORMAL = 1<<12, // fire a normal ring no matter what
-	
-	// custom lua buttons
 	BT_CUSTOM1    = 1<<13,
 	BT_CUSTOM2    = 1<<14,
 	BT_CUSTOM3    = 1<<15,
diff --git a/src/dedicated/i_system.c b/src/dedicated/i_system.c
index 858dfaf20234cb6898ffe008bce3c3f76e8b950b..643e24f5afb677f450dab073097388661c35c98a 100644
--- a/src/dedicated/i_system.c
+++ b/src/dedicated/i_system.c
@@ -3,7 +3,7 @@
 //
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Portions Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 2014-2023 by Sonic Team Junior.
+// Copyright (C) 2014-2025 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
@@ -65,10 +65,11 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #pragma warning(default : 4214 4244)
 #endif
 
-#if defined (__unix__) || defined(__APPLE__) || (defined (UNIXCOMMON) && !defined (__HAIKU__))
-#if defined (__linux__)
-#include <sys/vfs.h>
+#if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
+#if defined (__linux__) || defined (__HAIKU__)
+#include <sys/statvfs.h>
 #else
+#include <sys/statvfs.h>
 #include <sys/param.h>
 #include <sys/mount.h>
 /*For meminfo*/
@@ -81,7 +82,7 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #endif
 #endif
 
-#if defined (__linux__) || (defined (UNIXCOMMON) && !defined (__HAIKU__))
+#if defined (__linux__) || defined (UNIXCOMMON)
 #ifndef NOTERMIOS
 #include <termios.h>
 #include <sys/ioctl.h> // ioctl
@@ -96,8 +97,10 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #if defined (__unix__) || (defined (UNIXCOMMON) && !defined (__APPLE__))
 #include <errno.h>
 #include <sys/wait.h>
+#ifndef __HAIKU__ // haiku's crash dialog is just objectively better
 #define NEWSIGNALHANDLER
 #endif
+#endif
 
 #ifndef NOMUMBLE
 #ifdef __linux__ // need -lrt
@@ -374,15 +377,24 @@ void I_Sleep(UINT32 ms)
 
 void I_SleepDuration(precise_t duration)
 {
-#if defined(__linux__) || defined(__FreeBSD__)
+#if defined(__linux__) || defined(__FreeBSD__) || defined(__HAIKU__)
 	UINT64 precision = I_GetPrecisePrecision();
-	struct timespec ts = {
-		.tv_sec = duration / precision,
-		.tv_nsec = duration * 1000000000 / precision % 1000000000,
-	};
-	int status;
-	do status = clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts);
-	while (status == EINTR);
+	precise_t dest = I_GetPreciseTime() + duration;
+	precise_t slack = (precision / 5000); // 0.2 ms slack
+	if (duration > slack)
+	{
+		duration -= slack;
+		struct timespec ts = {
+			.tv_sec = duration / precision,
+			.tv_nsec = duration * 1000000000 / precision % 1000000000,
+		};
+		int status;
+		do status = clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts);
+		while (status == EINTR);
+	}
+
+	// busy-wait the rest
+	while (((INT64)dest - (INT64)I_GetPreciseTime()) > 0);
 #else
 	UINT64 precision = I_GetPrecisePrecision();
 	INT32 sleepvalue = cv_sleep.value;
@@ -705,10 +717,9 @@ typedef struct
 
 static feild_t tty_con;
 
-// when printing general stuff to stdout stderr (Sys_Printf)
-//   we need to disable the tty console stuff
-// this increments so we can recursively disable
-static INT32 ttycon_hide = 0;
+// lock to prevent clearing partial lines, since not everything
+// printed ends on a newline.
+static boolean ttycon_ateol = true;
 // some key codes that the terminal may be using
 // TTimo NOTE: I'm not sure how relevant this is
 static INT32 tty_erase;
@@ -736,63 +747,31 @@ static inline void tty_FlushIn(void)
 // TTimo NOTE: it seems on some terminals just sending '\b' is not enough
 //   so for now, in any case we send "\b \b" .. yeah well ..
 //   (there may be a way to find out if '\b' alone would work though)
+// Hanicef NOTE: using \b this way is unreliable because of terminal state,
+//   it's better to use \r to reset the cursor to the beginning of the
+//   line and clear from there.
 static void tty_Back(void)
 {
-	char key;
-	ssize_t d;
-	key = '\b';
-	d = write(STDOUT_FILENO, &key, 1);
-	key = ' ';
-	d = write(STDOUT_FILENO, &key, 1);
-	key = '\b';
-	d = write(STDOUT_FILENO, &key, 1);
-	(void)d;
-}
-
-static void tty_Clear(void)
-{
-	size_t i;
+	write(STDOUT_FILENO, "\r", 1);
 	if (tty_con.cursor>0)
 	{
-		for (i=0; i<tty_con.cursor; i++)
-		{
-			tty_Back();
-		}
+		write(STDOUT_FILENO, tty_con.buffer, tty_con.cursor);
 	}
-
+	write(STDOUT_FILENO, " \b", 2);
 }
 
-// clear the display of the line currently edited
-// bring cursor back to beginning of line
-static inline void tty_Hide(void)
-{
-	//I_Assert(consolevent);
-	if (ttycon_hide)
-	{
-		ttycon_hide++;
-		return;
-	}
-	tty_Clear();
-	ttycon_hide++;
-}
-
-// show the current line
-// FIXME TTimo need to position the cursor if needed??
-static inline void tty_Show(void)
+static void tty_Clear(void)
 {
 	size_t i;
-	ssize_t d;
-	//I_Assert(consolevent);
-	I_Assert(ttycon_hide>0);
-	ttycon_hide--;
-	if (ttycon_hide == 0 && tty_con.cursor)
+	write(STDOUT_FILENO, "\r", 1);
+	if (tty_con.cursor>0)
 	{
 		for (i=0; i<tty_con.cursor; i++)
 		{
-			d = write(STDOUT_FILENO, tty_con.buffer+i, 1);
+			write(STDOUT_FILENO, " ", 1);
 		}
+		write(STDOUT_FILENO, "\r", 1);
 	}
-	(void)d;
 }
 
 // never exit without calling this, or your terminal will be left in a pretty bad state
@@ -900,6 +879,11 @@ static void I_GetConsoleEvents(void)
 				tty_con.cursor = 0;
 				ev.key = KEY_ENTER;
 			}
+			else if (key == 0x4) // ^D, aka EOF
+			{
+				// shut down, most unix programs behave this way
+				I_Quit();
+			}
 			else continue;
 		}
 		else if (tty_con.cursor < sizeof(tty_con.buffer))
@@ -999,20 +983,8 @@ static void I_GetConsoleEvents(void)
 static void I_StartupConsole(void)
 {
 	HANDLE ci, co;
-	const INT32 ded = M_CheckParm("-dedicated");
-	BOOL gotConsole = FALSE;
-	if (M_CheckParm("-console") || ded)
-		gotConsole = AllocConsole();
-#ifdef _DEBUG
-	else if (M_CheckParm("-noconsole") && !ded)
-#else
-	else if (!M_CheckParm("-console") && !ded)
-#endif
-	{
-		FreeConsole();
-		gotConsole = FALSE;
-	}
-
+	BOOL gotConsole = AllocConsole();
+	consolevent = !M_CheckParm("-noconsole");
 	if (gotConsole)
 	{
 		SetConsoleTitleA("SRB2 Console");
@@ -1040,12 +1012,7 @@ static inline void I_ShutdownConsole(void){}
 static void I_GetConsoleEvents(void){}
 static inline void I_StartupConsole(void)
 {
-#ifdef _DEBUG
 	consolevent = !M_CheckParm("-noconsole");
-#else
-	consolevent = M_CheckParm("-console");
-#endif
-
 	framebuffer = M_CheckParm("-framebuffer");
 
 	if (framebuffer)
@@ -1063,6 +1030,9 @@ void I_OutputMsg(const char *fmt, ...)
 	va_start(argptr,fmt);
 	len = vsnprintf(NULL, 0, fmt, argptr);
 	va_end(argptr);
+	if (len == 0)
+		return;
+
 	txt = malloc(len+1);
 	va_start(argptr,fmt);
 	vsprintf(txt, fmt, argptr);
@@ -1152,18 +1122,20 @@ void I_OutputMsg(const char *fmt, ...)
 	}
 #else
 #ifdef HAVE_TERMIOS
-	if (consolevent)
+	if (consolevent && ttycon_ateol)
 	{
-		tty_Hide();
+		tty_Clear();
+		ttycon_ateol = false;
 	}
 #endif
 
 	if (!framebuffer)
 		fprintf(stderr, "%s", txt);
 #ifdef HAVE_TERMIOS
-	if (consolevent)
+	if (consolevent && txt[len-1] == '\n')
 	{
-		tty_Show();
+		write(STDOUT_FILENO, tty_con.buffer, tty_con.cursor);
+		ttycon_ateol = true;
 	}
 #endif
 
@@ -1243,18 +1215,13 @@ void I_ShutdownSystem(void)
 void I_GetDiskFreeSpace(INT64* freespace)
 {
 #if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
-#if defined (SOLARIS) || defined (__HAIKU__)
-	*freespace = INT32_MAX;
-	return;
-#else // Both Linux and BSD have this, apparently.
-	struct statfs stfs;
-	if (statfs(srb2home, &stfs) == -1)
+	struct statvfs stfs;
+	if (statvfs(srb2home, &stfs) == -1)
 	{
 		*freespace = INT32_MAX;
 		return;
 	}
 	*freespace = stfs.f_bavail * stfs.f_bsize;
-#endif
 #elif defined (_WIN32)
 	static p_GetDiskFreeSpaceExA pfnGetDiskFreeSpaceEx = NULL;
 	static boolean testwin95 = false;
@@ -1390,8 +1357,8 @@ static const char *searchWad(const char *searchDir)
 
 #define CHECKWADPATH(ret) \
 do { \
-	I_OutputMsg(",%s", returnWadPath); \
-	if (isWadPathOk(returnWadPath)) \
+	I_OutputMsg(",%s", ret); \
+	if (isWadPathOk(ret)) \
 		return ret; \
 } while (0)
 
@@ -1416,7 +1383,9 @@ static const char *locateWad(void)
 #ifndef NOCWD
 	// examine current dir
 	strcpy(returnWadPath, ".");
-	CHECKWADPATH(NULL);
+	I_OutputMsg(",%s", returnWadPath);
+	if (isWadPathOk(returnWadPath))
+		return NULL;
 #endif
 
 #ifdef __APPLE__
@@ -1433,9 +1402,15 @@ static const char *locateWad(void)
 
 #ifndef NOHOME
 	// find in $HOME
-	I_OutputMsg(",HOME");
 	if ((envstr = I_GetEnv("HOME")) != NULL)
-		SEARCHWAD(envstr);
+	{
+		char *tmp = malloc(strlen(envstr) + 1 + sizeof(DEFAULTDIR));
+		strcpy(tmp, envstr);
+		strcat(tmp, "/");
+		strcat(tmp, DEFAULTDIR);
+		CHECKWADPATH(tmp);
+		free(tmp);
+	}
 #endif
 
 	// search paths
@@ -1462,8 +1437,10 @@ const char *I_LocateWad(void)
 	{
 		// change to the directory where we found srb2.pk3
 #if defined (_WIN32)
+		waddir = _fullpath(NULL, waddir, MAX_PATH);
 		SetCurrentDirectoryA(waddir);
 #else
+		waddir = realpath(waddir, NULL);
 		if (chdir(waddir) == -1)
 			I_OutputMsg("Couldn't change working directory\n");
 #endif
@@ -1585,5 +1562,14 @@ void I_GetCursorPosition(INT32 *x, INT32 *y)
 	(void)y;
 }
 
-#include "../sdl/dosstr.c"
+void I_SetTextInputMode(boolean active)
+{
+	(void)active;
+}
+
+boolean I_GetTextInputMode(void)
+{
+	return false;
+}
 
+#include "../sdl/dosstr.c"
diff --git a/src/dedicated/i_threads.c b/src/dedicated/i_threads.c
index 6902e23a52da14cf1861df5c1f194a4ea617af17..55c0a069e5d3f42e03c8286d4862f87e9c8f4a8e 100644
--- a/src/dedicated/i_threads.c
+++ b/src/dedicated/i_threads.c
@@ -157,7 +157,7 @@ void I_wake_all_cond(I_cond *anchor)
 	pthread_mutex_lock(&thread_lock);
 	if (*anchor == NULL)
 	{
-		*anchor = malloc(sizeof(pthread_t));
+		*anchor = malloc(sizeof(pthread_cond_t));
 		pthread_cond_init(*anchor, NULL);
 	}
 	pthread_mutex_unlock(&thread_lock);
diff --git a/src/dedicated/i_video.c b/src/dedicated/i_video.c
index 2c998117accb71be25850438bdac2ee6c4db0509..19f2d0cbd51bba44266ca0b92627b542cb8c948b 100644
--- a/src/dedicated/i_video.c
+++ b/src/dedicated/i_video.c
@@ -1,4 +1,4 @@
-#include "../doomdef.h"
+#include "../doomdef.h"
 #include "../command.h"
 #include "../i_video.h"
 
@@ -76,4 +76,3 @@ void I_ReadScreen(UINT8 *scr)
 void I_BeginRead(void){}
 
 void I_EndRead(void){}
-
diff --git a/src/deh_lua.c b/src/deh_lua.c
index 64fb52fc7423a7486c9d6736762fedd0fcde64ba..48f737a1ff5c6587414b2efe2edbc566c9c25034 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -11,7 +11,6 @@
 /// \brief Lua SOC library
 
 #include "deh_lua.h"
-#include "g_input.h"
 
 // freeslot takes a name (string only!)
 // and allocates it to the appropriate free slot.
@@ -28,6 +27,12 @@ static inline int lib_freeslot(lua_State *L)
 	if (!lua_lumploading)
 		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
 
+	if (!deh_loaded)
+	{
+		initfreeslots();
+		deh_loaded = true;
+	}
+
 	while (n-- > 0)
 	{
 		s = Z_StrDup(luaL_checkstring(L,1));
@@ -594,20 +599,12 @@ static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
 		return luaL_error(L, "translation '%s' could not be found.\n", word);
 	}
 
-	// TODO: 2.3: Delete these aliases
-	else if (fastcmp(word, "BT_USE"))
+	// TODO: 2.3: Delete this alias
+	if (fastcmp(word, "BT_USE"))
 	{
 		CacheAndPushConstant(L, word, (lua_Integer)BT_SPIN);
 		return 1;
 	}
-	else if (fastcmp(word, "GC_WEPSLOT8") || fastcmp(word, "GC_WEPSLOT9") || fastcmp(word, "GC_WEPSLOT10"))
-	{
-		// Using GC_WEPSLOT7 isn't accurate, but ensures that "if x >= GC_WEPSLOT1 and x <= GC_WEPSLOT10" keeps the intended effect
-		CacheAndPushConstant(L, word, (lua_Integer)GC_WEPSLOT7);
-		if (!mathlib)
-			LUA_Deprecated(L, "GC_WEPSLOT8\"-\"GC_WEPSLOT10", "GC_WEPSLOT1\"-\"GC_WEPSLOT7");
-		return 1;
-	}
 
 	for (i = 0; INT_CONST[i].n; i++)
 		if (fastcmp(word,INT_CONST[i].n)) {
diff --git a/src/deh_soc.c b/src/deh_soc.c
index d1643fd4eda3b8f141b410ab57286c04f90381fe..343beb3012676b93256af35c0bd57ff6ba7e9076 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -286,6 +286,7 @@ void readPlayer(MYFILE *f, INT32 num)
 				}
 				if (playertext)
 				{
+					// PLAYERTEXT is really weird, so this doesn't use deh_strlcpy.
 					strlcpy(description[num].notes, playertext, NOTE_SIZE);
 					strlcat(description[num].notes,
 						myhashfgets(playertext, NOTE_SIZE, f), NOTE_SIZE);
@@ -324,7 +325,8 @@ void readPlayer(MYFILE *f, INT32 num)
 			if (fastcmp(word, "PICNAME"))
 			{
 				SLOTFOUND
-				strncpy(description[num].picname, word2, sizeof(description[num].picname)-1);
+				deh_strlcpy(description[num].picname, word2, sizeof description[num].picname,
+					va("Character %d: picname", num));
 			}
 			else if (fastcmp(word, "DISPLAYNAME"))
 			{
@@ -345,7 +347,8 @@ void readPlayer(MYFILE *f, INT32 num)
 					cur = strchr(cur, '#');
 				}
 
-				strlcpy(description[num].displayname, stringvalue, sizeof description[num].displayname);
+				deh_strlcpy(description[num].displayname, stringvalue, sizeof description[num].displayname,
+					va("Character %d: displayname", num));
 			}
 			else if (fastcmp(word, "OPPOSITECOLOR") || fastcmp(word, "OPPOSITECOLOUR"))
 			{
@@ -355,7 +358,8 @@ void readPlayer(MYFILE *f, INT32 num)
 			else if (fastcmp(word, "NAMETAG") || fastcmp(word, "TAGNAME"))
 			{
 				SLOTFOUND
-				strncpy(description[num].nametag, word2, sizeof(description[num].nametag)-1);
+				deh_strlcpy(description[num].nametag, word2, sizeof description[num].nametag,
+					va("Character %d: nametag", num));
 			}
 			else if (fastcmp(word, "TAGTEXTCOLOR") || fastcmp(word, "TAGTEXTCOLOUR"))
 			{
@@ -387,7 +391,8 @@ void readPlayer(MYFILE *f, INT32 num)
 			{
 				// Send to free slot.
 				SLOTFOUND
-				strlcpy(description[num].skinname, word2, sizeof description[num].skinname);
+				deh_strlcpy(description[num].skinname, word2, sizeof description[num].skinname,
+					va("Character %d: skinname", num));
 				strlwr(description[num].skinname);
 			}
 			else if (!failure)
@@ -405,7 +410,6 @@ void readfreeslots(MYFILE *f)
 {
 	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
 	char *word,*type;
-	char *tmp;
 	int i;
 
 	do
@@ -415,10 +419,13 @@ void readfreeslots(MYFILE *f)
 			if (s[0] == '\n')
 				break;
 
-			tmp = strchr(s, '#');
-			if (tmp)
-				*tmp = '\0';
-			if (s == tmp)
+			char *hashtag = strchr(s, '#');
+			char *space = strchr(s, ' ');
+			if (hashtag)
+				*hashtag = '\0';
+			if (space)
+				*space = '\0';
+			if (s == hashtag || s == space)
 				continue; // Skip comment lines, but don't break.
 
 			type = strtok(s, "_");
@@ -1194,6 +1201,7 @@ void readgametype(MYFILE *f, char *gtname)
 				}
 				if (descr)
 				{
+					// DESCRIPTION is really weird, so this doesn't use deh_strlcpy.
 					strlcpy(gtdescription, descr, sizeof (gtdescription));
 					strlcat(gtdescription,
 						myhashfgets(descr, sizeof (gtdescription), f),
@@ -1400,7 +1408,7 @@ void readlevelheader(MYFILE *f, INT32 num)
 			{
 				deh_strlcpy(mapheaderinfo[num-1]->lvlttl, word2,
 					sizeof(mapheaderinfo[num-1]->lvlttl), va("Level header %d: levelname", num));
-				strlcpy(mapheaderinfo[num-1]->selectheading, word2, sizeof(mapheaderinfo[num-1]->selectheading)); // not deh_ so only complains once
+				strlcpy(mapheaderinfo[num-1]->selectheading, word2, sizeof(mapheaderinfo[num-1]->selectheading)); // not deh_strlcpy so only complains once
 				continue;
 			}
 			// CHEAP HACK: move this over here for lowercase subtitles
@@ -1443,10 +1451,10 @@ void readlevelheader(MYFILE *f, INT32 num)
 				// Newly allocated
 				modoption = &mapheaderinfo[num-1]->customopts[j];
 
-				strncpy(modoption->option, word,  31);
-				modoption->option[31] = '\0';
-				strncpy(modoption->value,  word2, 255);
-				modoption->value[255] = '\0';
+				deh_strlcpy(modoption->option, word, sizeof(modoption->option),
+					va("Level header %d: custom option %d key", num, j));
+				deh_strlcpy(modoption->value, word2, sizeof(modoption->value),
+					va("Level header %d: custom option %d value", num, j));
 				continue;
 			}
 
@@ -1624,7 +1632,7 @@ void readlevelheader(MYFILE *f, INT32 num)
 			else if (fastcmp(word, "KEYWORDS"))
 			{
 				deh_strlcpy(mapheaderinfo[num-1]->keywords, word2,
-						sizeof(mapheaderinfo[num-1]->keywords), va("Level header %d: keywords", num));
+					sizeof(mapheaderinfo[num-1]->keywords), va("Level header %d: keywords", num));
 			}
 			else if (fastcmp(word, "MUSIC"))
 			{
@@ -1673,7 +1681,8 @@ void readlevelheader(MYFILE *f, INT32 num)
 			}
 			else if (fastcmp(word, "FORCECHARACTER"))
 			{
-				strlcpy(mapheaderinfo[num-1]->forcecharacter, word2, SKINNAMESIZE+1);
+				deh_strlcpy(mapheaderinfo[num-1]->forcecharacter, word2, sizeof mapheaderinfo[num-1]->forcecharacter,
+					va("Level header %d: forcecharacter", num));
 				strlwr(mapheaderinfo[num-1]->forcecharacter); // skin names are lowercase
 			}
 			else if (fastcmp(word, "WEATHER"))
@@ -1681,7 +1690,10 @@ void readlevelheader(MYFILE *f, INT32 num)
 			else if (fastcmp(word, "SKYNUM"))
 				mapheaderinfo[num-1]->skynum = (INT16)i;
 			else if (fastcmp(word, "INTERSCREEN"))
-				strncpy(mapheaderinfo[num-1]->interscreen, word2, sizeof(mapheaderinfo[num-1]->interscreen)-1);
+			{
+				deh_strlcpy(mapheaderinfo[num-1]->interscreen, word2, sizeof mapheaderinfo[num-1]->interscreen,
+					va("Level header %d: interscreen", num));
+			}
 			else if (fastcmp(word, "PRECUTSCENENUM"))
 				mapheaderinfo[num-1]->precutscenenum = (UINT8)i;
 			else if (fastcmp(word, "CUTSCENENUM"))
@@ -1983,14 +1995,17 @@ static void readcutscenescene(MYFILE *f, INT32 num, INT32 scenenum)
 				picid = (UINT8)atoi(word + 3);
 				if (picid > 8 || picid == 0)
 				{
-					deh_warning("CutSceneScene %d: unknown word '%s'", num, word);
+					deh_warning("Cutscene %d, scene %d: pic number %d out of range (1 - %d)",
+						num + 1, scenenum + 1, picid, 8);
 					continue;
 				}
 				--picid;
 
 				if (fastcmp(word+4, "NAME"))
 				{
-					strncpy(cutscenes[num]->scene[scenenum].picname[picid], word2, 8);
+					deh_strlcpy(cutscenes[num]->scene[scenenum].picname[picid], word2,
+						sizeof cutscenes[num]->scene[scenenum].picname[picid],
+						va("Cutscene %d, scene %d, pic %d: name", num + 1, scenenum + 1, picid + 1));
 				}
 				else if (fastcmp(word+4, "HIRES"))
 				{
@@ -2009,12 +2024,13 @@ static void readcutscenescene(MYFILE *f, INT32 num, INT32 scenenum)
 					cutscenes[num]->scene[scenenum].ycoord[picid] = usi;
 				}
 				else
-					deh_warning("CutSceneScene %d: unknown word '%s'", num, word);
+					deh_warning("Cutscene %d, scene %d: unknown word '%s'", num + 1, scenenum + 1, word);
 			}
 			else if (fastcmp(word, "MUSIC"))
 			{
-				strncpy(cutscenes[num]->scene[scenenum].musswitch, word2, 7);
-				cutscenes[num]->scene[scenenum].musswitch[6] = 0;
+				deh_strlcpy(cutscenes[num]->scene[scenenum].musswitch, word2,
+					sizeof cutscenes[num]->scene[scenenum].musswitch,
+					va("Cutscene %d, scene %d: music", num + 1, scenenum + 1));
 			}
 			else if (fastcmp(word, "MUSICTRACK"))
 			{
@@ -2049,7 +2065,7 @@ static void readcutscenescene(MYFILE *f, INT32 num, INT32 scenenum)
 				cutscenes[num]->scene[scenenum].fadecolor = (UINT8)i;
 			}
 			else
-				deh_warning("CutSceneScene %d: unknown word '%s'", num, word);
+				deh_warning("Cutscene %d, scene %d: unknown word '%s'", num + 1, scenenum + 1, word);
 		}
 	} while (!myfeof(f)); // finish when the line is empty
 
@@ -2107,11 +2123,10 @@ void readcutscene(MYFILE *f, INT32 num)
 					readcutscenescene(f, num, value - 1);
 				}
 				else
-					deh_warning("Scene number %d out of range (1 - 128)", value);
-
+					deh_warning("Cutscene %d: scene number %d out of range (1 - 128)", num + 1, value);
 			}
 			else
-				deh_warning("Cutscene %d: unknown word '%s', Scene <num> expected.", num, word);
+				deh_warning("Cutscene %d: unknown word '%s', Scene <num> expected.", num + 1, word);
 		}
 	} while (!myfeof(f)); // finish when the line is empty
 
@@ -2232,7 +2247,8 @@ static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
 
 					for (picid = 0; picid < MAX_PROMPT_PICS; picid++)
 					{
-						strncpy(textprompts[num]->page[pagenum].picname[picid], textprompts[num]->page[metapagenum].picname[picid], 8);
+						// Doesn't use deh_strlcpy because it's not copying input.
+						strlcpy(textprompts[num]->page[pagenum].picname[picid], textprompts[num]->page[metapagenum].picname[picid], sizeof textprompts[num]->page[pagenum].picname[picid]);
 						textprompts[num]->page[pagenum].pichires[picid] = textprompts[num]->page[metapagenum].pichires[picid];
 						textprompts[num]->page[pagenum].picduration[picid] = textprompts[num]->page[metapagenum].picduration[picid];
 						textprompts[num]->page[pagenum].xcoord[picid] = textprompts[num]->page[metapagenum].xcoord[picid];
@@ -2245,14 +2261,17 @@ static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
 				picid = (UINT8)atoi(word + 3);
 				if (picid > MAX_PROMPT_PICS || picid == 0)
 				{
-					deh_warning("textpromptscene %d: unknown word '%s'", num, word);
+					deh_warning("Text prompt %d, page %d: pic number %d out of range (1 - %d)",
+						num + 1, pagenum + 1, picid, MAX_PROMPT_PICS);
 					continue;
 				}
 				--picid;
 
 				if (fastcmp(word+4, "NAME"))
 				{
-					strncpy(textprompts[num]->page[pagenum].picname[picid], word2, 8);
+					deh_strlcpy(textprompts[num]->page[pagenum].picname[picid], word2,
+						sizeof textprompts[num]->page[pagenum].picname[picid],
+						va("Text prompt %d, page %d, pic %d: name", num + 1, pagenum + 1, picid + 1));
 				}
 				else if (fastcmp(word+4, "HIRES"))
 				{
@@ -2271,12 +2290,16 @@ static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
 					textprompts[num]->page[pagenum].ycoord[picid] = usi;
 				}
 				else
-					deh_warning("textpromptscene %d: unknown word '%s'", num, word);
+				{
+					deh_warning("Text prompt %d, page %d: unknown word '%s'",
+						num + 1, pagenum + 1, word);
+				}
 			}
 			else if (fastcmp(word, "MUSIC"))
 			{
-				strncpy(textprompts[num]->page[pagenum].musswitch, word2, 7);
-				textprompts[num]->page[pagenum].musswitch[6] = 0;
+				deh_strlcpy(textprompts[num]->page[pagenum].musswitch, word2,
+					sizeof textprompts[num]->page[pagenum].musswitch,
+					va("Text prompt %d, page %d: music", num + 1, pagenum + 1));
 			}
 			else if (fastcmp(word, "MUSICTRACK"))
 			{
@@ -2291,30 +2314,35 @@ static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
 			{
 				if (*word2 != '\0')
 				{
-					INT32 j;
+					size_t j;
 
 					// HACK: Add yellow control char now
 					// so the drawing function doesn't call it repeatedly
-					char name[34];
+					char name[32 + 2];
 					name[0] = '\x82'; // color yellow
-					name[1] = 0;
-					strncat(name, word2, 32);
-					name[33] = 0;
+
+					// So that we still get a warning.
+					deh_strlcpy(name + 1, word2, (sizeof(name)) - 1,
+						va("Text prompt %d, page %d: name", num + 1, pagenum + 1));
 
 					// Replace _ with ' '
-					for (j = 0; j < 32 && name[j]; j++)
+					for (j = 1; j < sizeof(name) && name[j]; j++)
 					{
 						if (name[j] == '_')
 							name[j] = ' ';
 					}
 
-					strncpy(textprompts[num]->page[pagenum].name, name, sizeof(textprompts[num]->page[pagenum].name));
+					strlcpy(textprompts[num]->page[pagenum].name, name, sizeof(textprompts[num]->page[pagenum].name));
 				}
 				else
 					*textprompts[num]->page[pagenum].name = '\0';
 			}
 			else if (fastcmp(word, "ICON"))
-				strncpy(textprompts[num]->page[pagenum].iconname, word2, 8);
+			{
+				deh_strlcpy(textprompts[num]->page[pagenum].iconname, word2,
+					sizeof textprompts[num]->page[pagenum].iconname,
+					va("Text prompt %d, page %d: icon", num + 1, pagenum + 1));
+			}
 			else if (fastcmp(word, "ICONALIGN"))
 				textprompts[num]->page[pagenum].rightside = (i || word2[0] == 'R');
 			else if (fastcmp(word, "ICONFLIP"))
@@ -2381,8 +2409,9 @@ static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
 				{
 					UINT8 metapagenum = usi - 1;
 
-					strncpy(textprompts[num]->page[pagenum].name, textprompts[num]->page[metapagenum].name, 32);
-					strncpy(textprompts[num]->page[pagenum].iconname, textprompts[num]->page[metapagenum].iconname, 8);
+					// Doesn't use deh_strlcpy because it's not copying input.
+					strlcpy(textprompts[num]->page[pagenum].name, textprompts[num]->page[metapagenum].name, sizeof textprompts[num]->page[pagenum].name);
+					strlcpy(textprompts[num]->page[pagenum].iconname, textprompts[num]->page[metapagenum].iconname, sizeof textprompts[num]->page[pagenum].iconname);
 					textprompts[num]->page[pagenum].rightside = textprompts[num]->page[metapagenum].rightside;
 					textprompts[num]->page[pagenum].iconflip = textprompts[num]->page[metapagenum].iconflip;
 					textprompts[num]->page[pagenum].lines = textprompts[num]->page[metapagenum].lines;
@@ -2397,17 +2426,25 @@ static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
 				}
 			}
 			else if (fastcmp(word, "TAG"))
-				strncpy(textprompts[num]->page[pagenum].tag, word2, 33);
+			{
+				deh_strlcpy(textprompts[num]->page[pagenum].tag, word2,
+					sizeof textprompts[num]->page[pagenum].tag,
+					va("Text prompt %d, page %d: tag", num + 1, pagenum + 1));
+			}
 			else if (fastcmp(word, "NEXTPROMPT"))
 				textprompts[num]->page[pagenum].nextprompt = usi;
 			else if (fastcmp(word, "NEXTPAGE"))
 				textprompts[num]->page[pagenum].nextpage = usi;
 			else if (fastcmp(word, "NEXTTAG"))
-				strncpy(textprompts[num]->page[pagenum].nexttag, word2, 33);
+			{
+				deh_strlcpy(textprompts[num]->page[pagenum].nexttag, word2,
+					sizeof textprompts[num]->page[pagenum].nexttag,
+					va("Text prompt %d, page %d: nexttag", num + 1, pagenum + 1));
+			}
 			else if (fastcmp(word, "TIMETONEXT"))
 				textprompts[num]->page[pagenum].timetonext = get_number(word2);
 			else
-				deh_warning("PromptPage %d: unknown word '%s'", num, word);
+				deh_warning("Text prompt %d, page %d: unknown word '%s'", num + 1, pagenum + 1, word);
 		}
 	} while (!myfeof(f)); // finish when the line is empty
 
@@ -2467,11 +2504,11 @@ void readtextprompt(MYFILE *f, INT32 num)
 					readtextpromptpage(f, num, value - 1);
 				}
 				else
-					deh_warning("Page number %d out of range (1 - %d)", value, MAX_PAGES);
+					deh_warning("Prompt %d: page number %d out of range (1 - %d)", num + 1, value, MAX_PAGES);
 
 			}
 			else
-				deh_warning("Prompt %d: unknown word '%s', Page <num> expected.", num, word);
+				deh_warning("Prompt %d: unknown word '%s', Page <num> expected.", num + 1, word);
 		}
 	} while (!myfeof(f)); // finish when the line is empty
 
@@ -2520,7 +2557,8 @@ void readmenu(MYFILE *f, INT32 num)
 
 			if (fastcmp(word, "BACKGROUNDNAME"))
 			{
-				strncpy(menupres[num].bgname, word2, 8);
+				deh_strlcpy(menupres[num].bgname, word2,
+					sizeof menupres[num].bgname, va("Menu %d: backgroundname", num));
 				titlechanged = true;
 			}
 			else if (fastcmp(word, "HIDEBACKGROUND"))
@@ -2563,7 +2601,8 @@ void readmenu(MYFILE *f, INT32 num)
 			}
 			else if (fastcmp(word, "TITLEPICSNAME"))
 			{
-				strncpy(menupres[num].ttname, word2, 9);
+				deh_strlcpy(menupres[num].ttname, word2,
+					sizeof menupres[num].ttname, va("Menu %d: titlepicsname", num));
 				titlechanged = true;
 			}
 			else if (fastcmp(word, "TITLEPICSX"))
@@ -2599,8 +2638,8 @@ void readmenu(MYFILE *f, INT32 num)
 			}
 			else if (fastcmp(word, "MUSIC"))
 			{
-				strncpy(menupres[num].musname, word2, 7);
-				menupres[num].musname[6] = 0;
+				deh_strlcpy(menupres[num].musname, word2,
+					sizeof menupres[num].musname, va("Menu %d: music", num));
 				titlechanged = true;
 			}
 			else if (fastcmp(word, "MUSICTRACK"))
@@ -2789,7 +2828,7 @@ void readframe(MYFILE *f, INT32 num)
 				size_t z;
 				boolean found = false;
 				size_t actionlen = strlen(word2) + 1;
-				char *actiontocompare = calloc(actionlen, 1);
+				char *actiontocompare = calloc(1, actionlen);
 
 				strcpy(actiontocompare, word2);
 				strupr(actiontocompare);
@@ -3582,22 +3621,20 @@ void readmaincfg(MYFILE *f)
 			if (fastcmp(word, "EXECCFG"))
 			{
 				if (strchr(word2, '.'))
-					COM_BufAddText(va("exec %s\n", word2));
+					COM_ExecFile(word2, COM_LUA, false);
 				else
 				{
 					lumpnum_t lumpnum;
 					char newname[9];
 
-					strncpy(newname, word2, 8);
-
-					newname[8] = '\0';
+					deh_strlcpy(newname, word2, sizeof newname, va("Maincfg: execcfg"));
 
 					lumpnum = W_CheckNumForName(newname);
 
 					if (lumpnum == LUMPERROR || W_LumpLength(lumpnum) == 0)
 						CONS_Debug(DBG_SETUP, "SOC Error: script lump %s not found/not valid.\n", newname);
 					else
-						COM_BufInsertText(W_CacheLumpNum(lumpnum, PU_CACHE));
+						COM_BufInsertTextEx(W_CacheLumpNum(lumpnum, PU_CACHE), COM_LUA);
 				}
 			}
 
@@ -3798,7 +3835,7 @@ void readmaincfg(MYFILE *f)
 			}
 			else if (fastcmp(word, "TITLEPICSNAME"))
 			{
-				strncpy(ttname, word2, sizeof(ttname)-1);
+				deh_strlcpy(ttname, word2, sizeof ttname, va("Maincfg: titlepicsname"));
 				titlechanged = true;
 			}
 			else if (fastcmp(word, "TITLEPICSX"))
@@ -3908,7 +3945,7 @@ void readmaincfg(MYFILE *f)
 			}
 			else if (fastcmp(word, "CUSTOMVERSION"))
 			{
-				strlcpy(customversionstring, word2, sizeof (customversionstring));
+				deh_strlcpy(customversionstring, word2, sizeof customversionstring, va("Maincfg: customversion"));
 				//titlechanged = true;
 			}
 			else if (fastcmp(word, "BOOTMAP"))
@@ -3923,6 +3960,7 @@ void readmaincfg(MYFILE *f)
 					value = get_number(word2);
 
 				bootmap = (INT16)value;
+				bootmapchanged = true;
 				//titlechanged = true;
 			}
 			else if (fastcmp(word, "STARTCHAR"))
diff --git a/src/deh_tables.c b/src/deh_tables.c
index c7c7c604068cd4761ea3944a92cd4efecd9a0ddb..57a243f5b737f8ab91c60414af09f0d5eb176ae3 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,6 +25,7 @@
 #include "g_game.h" // Joystick axes (for lua)
 #include "i_joy.h"
 #include "g_input.h" // Game controls (for lua)
+#include "p_maputl.h" // P_PathTraverse constants (for lua)
 
 #include "deh_tables.h"
 
@@ -1081,11 +1082,11 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_FANG_FIRE1",
 	"S_FANG_FIRE2",
 	"S_FANG_FIRE3",
-	"S_FANG_FIRE4",
 	"S_FANG_FIREREPEAT",
 	"S_FANG_LOBSHOT0",
 	"S_FANG_LOBSHOT1",
 	"S_FANG_LOBSHOT2",
+	"S_FANG_LOBSHOT3",
 	"S_FANG_WAIT1",
 	"S_FANG_WAIT2",
 	"S_FANG_WALLHIT",
@@ -1107,6 +1108,7 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_FANG_PINCHLOBSHOT2",
 	"S_FANG_PINCHLOBSHOT3",
 	"S_FANG_PINCHLOBSHOT4",
+	"S_FANG_PINCHLOBSHOT5",
 	"S_FANG_DIE1",
 	"S_FANG_DIE2",
 	"S_FANG_DIE3",
@@ -2245,6 +2247,10 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_LAMPPOST2",  // with snow
 	"S_HANGSTAR",
 	"S_MISTLETOE",
+	"S_SSZTREE",
+	"S_SSZTREE_BRANCH",
+	"S_SSZTREE2",
+	"S_SSZTREE2_BRANCH",
 	// Xmas GFZ bushes
 	"S_XMASBLUEBERRYBUSH",
 	"S_XMASBERRYBUSH",
@@ -2252,11 +2258,9 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	// FHZ
 	"S_FHZICE1",
 	"S_FHZICE2",
-	"S_ROSY_IDLE1",
-	"S_ROSY_IDLE2",
-	"S_ROSY_IDLE3",
-	"S_ROSY_IDLE4",
+	"S_ROSY_IDLE",
 	"S_ROSY_JUMP",
+	"S_ROSY_FALL",
 	"S_ROSY_WALK",
 	"S_ROSY_HUG",
 	"S_ROSY_PAIN",
@@ -2365,6 +2369,9 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_DBALL5",
 	"S_DBALL6",
 	"S_EGGSTATUE2",
+	"S_GINE",
+	"S_PPAL",
+	"S_PPEL",
 
 	// Shield Orb
 	"S_ARMA1",
@@ -3249,6 +3256,7 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_MARIOBUSH2",
 	"S_TOAD",
 
+
 	// Nights-specific stuff
 	"S_NIGHTSDRONE_MAN1",
 	"S_NIGHTSDRONE_MAN2",
@@ -3552,6 +3560,12 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_YELLOWBRICKDEBRIS",
 
 	"S_NAMECHECK",
+
+	// LJ Knuckles
+	"S_OLDK_STND",
+	"S_OLDK_DIE0",
+	"S_OLDK_DIE1",
+	"S_OLDK_DIE2",
 };
 
 // RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1",
@@ -4023,6 +4037,10 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	"MT_LAMPPOST2",  // with snow
 	"MT_HANGSTAR",
 	"MT_MISTLETOE",
+	"MT_SSZTREE",
+	"MT_SSZTREE_BRANCH",
+	"MT_SSZTREE2",
+	"MT_SSZTREE2_BRANCH",
 	// Xmas GFZ bushes
 	"MT_XMASBLUEBERRYBUSH",
 	"MT_XMASBERRYBUSH",
@@ -4102,6 +4120,9 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	// Misc scenery
 	"MT_DBALL",
 	"MT_EGGSTATUE2",
+	"MT_GINE",
+	"MT_PPAL",
+	"MT_PPEL",
 
 	// Powerup Indicators
 	"MT_ELEMENTAL_ORB", // Elemental shield mobj
@@ -4329,6 +4350,8 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 
 	"MT_NAMECHECK",
 	"MT_RAY",
+
+	"MT_OLDK",
 };
 
 const char *const MOBJFLAG_LIST[] = {
@@ -4483,6 +4506,8 @@ const char *const PLAYERFLAG_LIST[] = {
 	"CANCARRY", // Can carry?
 	"FINISHED",
 
+	"SHIELDDOWN", // Shield has been pressed.
+
 	NULL // stop loop here.
 };
 
@@ -5234,7 +5259,9 @@ struct int_const_s const INT_CONST[] = {
 	{"SF_MARIODAMAGE",SF_MARIODAMAGE},
 	{"SF_MACHINE",SF_MACHINE},
 	{"SF_DASHMODE",SF_DASHMODE},
+	{"SF_FASTWAIT",SF_FASTWAIT},
 	{"SF_FASTEDGE",SF_FASTEDGE},
+	{"SF_JETFUME",SF_JETFUME},
 	{"SF_MULTIABILITY",SF_MULTIABILITY},
 	{"SF_NONIGHTSROTATION",SF_NONIGHTSROTATION},
 	{"SF_NONIGHTSSUPER",SF_NONIGHTSSUPER},
@@ -5582,8 +5609,7 @@ struct int_const_s const INT_CONST[] = {
 	{"ROTAXIS_Z",ROTAXIS_Z},
 
 	// Buttons (ticcmd_t)
-	{"BT_WEAPONMASK",BT_WEAPONMASK}, //our first three bits.
-	{"BT_SHIELD",BT_SHIELD},
+	{"BT_WEAPONMASK",BT_WEAPONMASK}, //our first four bits.
 	{"BT_WEAPONNEXT",BT_WEAPONNEXT},
 	{"BT_WEAPONPREV",BT_WEAPONPREV},
 	{"BT_ATTACK",BT_ATTACK}, // shoot rings
@@ -5742,7 +5768,6 @@ struct int_const_s const INT_CONST[] = {
 	{"JA_DIGITAL",JA_DIGITAL},
 	{"JA_JUMP",JA_JUMP},
 	{"JA_SPIN",JA_SPIN},
-	{"JA_SHIELD",JA_SHIELD},
 	{"JA_FIRE",JA_FIRE},
 	{"JA_FIRENORMAL",JA_FIRENORMAL},
 	{"JOYAXISRANGE",JOYAXISRANGE},
@@ -5764,7 +5789,9 @@ struct int_const_s const INT_CONST[] = {
 	{"GC_WEPSLOT5",GC_WEPSLOT5},
 	{"GC_WEPSLOT6",GC_WEPSLOT6},
 	{"GC_WEPSLOT7",GC_WEPSLOT7},
-	{"GC_SHIELD",GC_SHIELD},
+	{"GC_WEPSLOT8",GC_WEPSLOT8},
+	{"GC_WEPSLOT9",GC_WEPSLOT9},
+	{"GC_WEPSLOT10",GC_WEPSLOT10},
 	{"GC_FIRE",GC_FIRE},
 	{"GC_FIRENORMAL",GC_FIRENORMAL},
 	{"GC_TOSSFLAG",GC_TOSSFLAG},
@@ -5803,6 +5830,15 @@ struct int_const_s const INT_CONST[] = {
 	{"MB_BUTTON8",MB_BUTTON8},
 	{"MB_SCROLLUP",MB_SCROLLUP},
 	{"MB_SCROLLDOWN",MB_SCROLLDOWN},
+	
+	// P_PathTraverse constants
+	{"PT_ADDLINES",PT_ADDLINES},
+	{"PT_ADDTHINGS",PT_ADDTHINGS},
+	{"PT_EARLYOUT",PT_EARLYOUT},
+
+	// screen.h constants
+	{"BASEVIDWIDTH",BASEVIDWIDTH},
+	{"BASEVIDHEIGHT",BASEVIDHEIGHT},
 
 	{NULL,0}
 };
diff --git a/src/deh_tables.h b/src/deh_tables.h
index b6986adff0166c3132be5f70ca78c7835030998f..94af2fa52ed89180d61a3c6eed0468229a727f34 100644
--- a/src/deh_tables.h
+++ b/src/deh_tables.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/dehacked.c b/src/dehacked.c
index 2050a117f67128dae4e40b992e5a49f1538457d7..470e468a0ba01c2d6bdda441dad3b195111cab2c 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -20,6 +20,7 @@ boolean deh_loaded = false;
 boolean gamedataadded = false;
 boolean titlechanged = false;
 boolean introchanged = false;
+boolean bootmapchanged = false;
 
 static int dbg_line;
 static INT32 deh_num_warning = 0;
@@ -192,11 +193,14 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile)
 	INT32 i;
 
 	if (!deh_loaded)
+	{
 		initfreeslots();
+		deh_loaded = true;
+	}
 
 	deh_num_warning = 0;
 
-	gamedataadded = titlechanged = introchanged = false;
+	gamedataadded = titlechanged = introchanged = bootmapchanged = false;
 
 	// it doesn't test the version of SRB2 and version of dehacked file
 	dbg_line = -1; // start at -1 so the first line is 0.
@@ -587,7 +591,12 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile)
 
 	if (gamestate == GS_TITLESCREEN)
 	{
-		if (introchanged)
+		if (bootmapchanged && bootmap)
+		{
+			menuactive = false;
+			D_MapChange(bootmap, gametype, ultimatemode, true, 0, false, false);
+		}
+		else if (introchanged)
 		{
 			menuactive = false;
 			I_UpdateMouseGrab();
@@ -605,14 +614,10 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile)
 	if (deh_num_warning)
 	{
 		CONS_Printf(M_GetText("%d warning%s in the SOC lump\n"), deh_num_warning, deh_num_warning == 1 ? "" : "s");
-		if (devparm) {
+		if (devparm)
 			I_Error("%s%s",va(M_GetText("%d warning%s in the SOC lump\n"), deh_num_warning, deh_num_warning == 1 ? "" : "s"), M_GetText("See log.txt for details.\n"));
-			//while (!I_GetKey())
-				//I_OsPolling();
-		}
 	}
 
-	deh_loaded = true;
 	Z_Free(s);
 }
 
diff --git a/src/dehacked.h b/src/dehacked.h
index d985b14b031d648b35f991d03e6797ab72f52137..e7b9b46566411f51654f2b60270ff745b28b18c2 100644
--- a/src/dehacked.h
+++ b/src/dehacked.h
@@ -39,6 +39,7 @@ extern boolean deh_loaded;
 extern boolean gamedataadded;
 extern boolean titlechanged;
 extern boolean introchanged;
+extern boolean bootmapchanged;
 
 #define MAX_ACTION_RECURSION 30
 extern const char *luaactions[MAX_ACTION_RECURSION];
diff --git a/src/doomdef.h b/src/doomdef.h
index 1b0e76314b13d080fc18c8f3463c066616c4b8c8..81ab7bbfb5505507f3df07b6b5709b6d4f995e81 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -168,7 +168,7 @@ extern char logfilename[1024];
 
 // Does this version require an added patch file?
 // Comment or uncomment this as necessary.
-#define USE_PATCH_DTA
+//#define USE_PATCH_DTA
 
 // Enforce a limit of loaded WAD files.
 //#define ENFORCE_WAD_LIMIT
diff --git a/src/doomstat.h b/src/doomstat.h
index bde2bacc72890d47b61a6085a26c5cda33dcfad0..b5b2984407cc7cf03d213de8cb70f3bab720fc88 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -209,19 +209,19 @@ typedef struct
 	UINT8 picmode; // sequence mode after displaying last pic, 0 = persist, 1 = loop, 2 = destroy
 	UINT8 pictoloop; // if picmode == loop, which pic to loop to?
 	UINT8 pictostart; // initial pic number to show
-	char picname[MAX_PROMPT_PICS][8];
+	char picname[MAX_PROMPT_PICS][8+1];
 	UINT8 pichires[MAX_PROMPT_PICS];
 	UINT16 xcoord[MAX_PROMPT_PICS]; // gfx
 	UINT16 ycoord[MAX_PROMPT_PICS]; // gfx
 	UINT16 picduration[MAX_PROMPT_PICS];
 
-	char   musswitch[7];
+	char   musswitch[6+1];
 	UINT16 musswitchflags;
 	UINT8 musicloop;
 
-	char tag[33]; // page tag
-	char name[34]; // narrator name, extra char for color
-	char iconname[8]; // narrator icon lump
+	char tag[32+1]; // page tag
+	char name[32+2]; // narrator name, extra char for color
+	char iconname[8+1]; // narrator icon lump
 	boolean rightside; // narrator side, false = left, true = right
 	boolean iconflip; // narrator flip icon horizontally
 	UINT8 hidehud; // hide hud, 0 = show all, 1 = hide depending on prompt position (top/bottom), 2 = hide all
@@ -233,7 +233,7 @@ typedef struct
 	sfxenum_t textsfx; // sfx_ id for printing text
 	UINT8 nextprompt; // next prompt to jump to, one-based. 0 = current prompt
 	UINT8 nextpage; // next page to jump to, one-based. 0 = next page within prompt->numpages
-	char nexttag[33]; // next tag to jump to. If set, this overrides nextprompt and nextpage.
+	char nexttag[32+1]; // next tag to jump to. If set, this overrides nextprompt and nextpage.
 	INT32 timetonext; // time in tics to jump to next page automatically. 0 = don't jump automatically
 	char *text;
 } textpage_t;
@@ -287,8 +287,8 @@ typedef struct
 // (This is not ifdeffed so the map header structure can stay identical, just in case.)
 typedef struct
 {
-	char option[32]; // 31 usable characters
-	char value[256]; // 255 usable characters. If this seriously isn't enough then wtf.
+	char option[31+1]; // 31 usable characters
+	char value[255+1]; // 255 usable characters. If this seriously isn't enough then wtf.
 } customoption_t;
 
 /** Map header information.
@@ -303,7 +303,7 @@ typedef struct
 	INT16 nextlevel;            ///< Map number of next level, or 1100-1102 to end.
 	INT16 marathonnext;         ///< See nextlevel, but for Marathon mode. Necessary to support hub worlds ala SUGOI.
 	char keywords[32+1];        ///< Keywords separated by space to search for. 32 characters.
-	char musname[7];            ///< Music track to play. "" for no music.
+	char musname[6+1];          ///< Music track to play. "" for no music.
 	UINT16 mustrack;            ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore.
 	UINT32 muspos;              ///< Music position to jump to.
 	char forcecharacter[16+1];  ///< (SKINNAMESIZE+1) Skin to switch to or "" to disable.
@@ -330,7 +330,7 @@ typedef struct
 	UINT16 levelflags;          ///< LF_flags:  merged booleans into one UINT16 for space, see below
 	UINT8 menuflags;            ///< LF2_flags: options that affect record attack / nights mode menus
 
-	char selectheading[22];     ///< Level select heading. Allows for controllable grouping.
+	char selectheading[21+1];   ///< Level select heading. Allows for controllable grouping.
 	UINT16 startrings;          ///< Number of rings players start with.
 	INT32 sstimer;              ///< Timer for special stages.
 	UINT32 ssspheres;           ///< Sphere requirement in special stages.
@@ -352,9 +352,9 @@ typedef struct
 
 	// Music stuff.
 	UINT32 musinterfadeout;     ///< Fade out level music on intermission screen in milliseconds
-	char musintername[7];       ///< Intermission screen music.
+	char musintername[6+1];     ///< Intermission screen music.
 
-	char muspostbossname[7];    ///< Post-bossdeath music.
+	char muspostbossname[6+1];  ///< Post-bossdeath music.
 	UINT16 muspostbosstrack;    ///< Post-bossdeath track.
 	UINT32 muspostbosspos;      ///< Post-bossdeath position
 	UINT32 muspostbossfadein;   ///< Post-bossdeath fade-in milliseconds.
diff --git a/src/doomtype.h b/src/doomtype.h
index fa8616793e0aeab7bea18c77ef78a4520770b802..415669ac479e928250d429bad545d34eff2770d5 100644
--- a/src/doomtype.h
+++ b/src/doomtype.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/dummy/i_system.c b/src/dummy/i_system.c
index ecabe3576d3f15b06513315a945562076587aff5..99bfa9f363ca04c9281b17b0cffeda5cc4314586 100644
--- a/src/dummy/i_system.c
+++ b/src/dummy/i_system.c
@@ -211,5 +211,15 @@ const char *I_GetSysName(void)
 	return NULL;
 }
 
+void I_SetTextInputMode(boolean active)
+{
+	(void)active;
+}
+
+boolean I_GetTextInputMode(void)
+{
+	return false;
+}
+
 #include "../sdl/dosstr.c"
 
diff --git a/src/f_finale.c b/src/f_finale.c
index f6c84613fc7a50d8731d1369935dcaa48cc1f6c9..49ff820d5a524cff7cf97bb9c1ef03addf002046 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1065,12 +1065,12 @@ static const char *credits[] = {
 	"Julio \"Chaos Zero 64\" Guir",
 	"\"Hanicef\"",
 	"\"Hannu_Hanhi\"", // For many OpenGL performance improvements!
+	"\"hazepastel\"",
 	"Kepa \"Nev3r\" Iceta",
 	"Thomas \"Shadow Hog\" Igoe",
 	"Iestyn \"Monster Iestyn\" Jealous",
 	"\"Kaito Sinclaire\"",
 	"\"Kalaron\"", // Coded some of Sryder13's collection of OpenGL fixes, especially fog
-	"\"katsy\"",
 	"Ronald \"Furyhunter\" Kinard", // The SDL2 port
 	"\"Lat'\"", // SRB2-CHAT, the chat window from Kart
 	"\"LZA\"",
@@ -1080,6 +1080,7 @@ static const char *credits[] = {
 	"Louis-Antoine \"LJ Sonic\" de Moulins", // de Rochefort doesn't quite fit on the screen sorry lol
 	"John \"JTE\" Muniz",
 	"Colin \"Sonict\" Pfaff",
+	"\"Radicalicious\"",
 	"James \"james\" Robert Roman",
 	"Sean \"Sryder13\" Ryder",
 	"Ehab \"Wolfy\" Saeed",
@@ -1103,7 +1104,8 @@ static const char *credits[] = {
 	"\"ChrispyPixels\"",
 	"Paul \"Boinciel\" Clempson",
 	"Sally \"TehRealSalt\" Cochenour",
-	"\"Dave Lite\"",
+	"\"DaJumpJump\"", // New Ringslinger graphics (2.2.14)
+	"\"DeltaSanic\"",
 	"Desmond \"Blade\" DesJardins",
 	"Sherman \"CoatRack\" DesJardins",
 	"\"DirkTheHusky\"",
@@ -1119,6 +1121,7 @@ static const char *credits[] = {
 	"Alice \"Alacroix\" de Lemos",
 	"Logan \"Hyperchaotix\" McCloud",
 	"Alexander \"DrTapeworm\" Moench-Ford",
+    "\"orbitalviolet\"", // summit showdown hehehehe (aka Evertone)
 	"Andrew \"Senku Niola\" Moran",
 	"\"MotorRoach\"",
 	"Phillip \"TelosTurntable\" Robinson",
@@ -1127,6 +1130,7 @@ static const char *credits[] = {
 	"David \"Instant Sonic\" Spencer Jr.",
 	"\"SSNTails\"",
 	"Daniel \"Inazuma\" Trinh",
+	"Samuel \"Spectorious\" Tuttle",
 	"\"VelocitOni\"",
 	"Jarrett \"JEV3\" Voight",
 	"",
@@ -1135,6 +1139,7 @@ static const char *credits[] = {
 	"Victor \"VAdaPEGA\" Ara\x1Fjo", // Araújo
 	"Malcolm \"RedXVI\" Brown",
 	"Dave \"DemonTomatoDave\" Bulmer",
+	"Dan Cidoni", // aka Krabs
 	"Paul \"Boinciel\" Clempson",
 	"\"Cyan Helkaraxe\"",
 	"Claire \"clairebun\" Ellis",
@@ -1153,24 +1158,30 @@ static const char *credits[] = {
 	"Colette \"fickleheart\" Bordelon",
 	"Hank \"FuriousFox\" Brannock",
 	"Matthew \"Fawfulfan\" Chapman",
+	"Dan Cidoni", // aka Krabs
 	"Paul \"Boinciel\" Clempson",
 	"Sally \"TehRealSalt\" Cochenour",
 	"Desmond \"Blade\" DesJardins",
 	"Sherman \"CoatRack\" DesJardins",
 	"Ben \"Mystic\" Geyer",
 	"Nathan \"Jazz\" Giroux",
+	"\"GomaTheMascar\"",
 	"Vivian \"toaster\" Grannell",
 	"James \"SeventhSentinel\" Hall",
 	"Kepa \"Nev3r\" Iceta",
 	"Thomas \"Shadow Hog\" Igoe",
+	"Mujamel \"MK\" Khan",
 	"\"Kaito Sinclaire\"",
 	"Alexander \"DrTapeworm\" Moench-Ford",
+	"\"Radicalicious\"",
 	"\"Revan\"",
 	"Anna \"QueenDelta\" Sandlin",
 	"Wessel \"sphere\" Smit",
 	"\"SSNTails\"",
+	"Aaron \"Othius\" Stojkov",
 	"Rob Tisdell",
 	"\"Torgo\"",
+	"Samuel \"Spectorious\" Tuttle",
 	"Jarrett \"JEV3\" Voight",
 	"Johnny \"Sonikku\" Wallbank",
 	"Marco \"mazmazz\" Zafra",
@@ -2336,7 +2347,7 @@ void F_SkyScroll(const char *patchname)
 }
 
 #define LOADTTGFX(arr, name, maxf) \
-lumpnum = W_CheckNumForName(name); \
+lumpnum = W_CheckNumForPatchName(name); \
 if (lumpnum != LUMPERROR) \
 { \
 	arr[0] = W_CachePatchName(name, PU_PATCH_LOWPRIORITY); \
@@ -2350,7 +2361,7 @@ else if (strlen(name) <= 6) \
 	{ \
 		sprintf(&lumpname[cnt], "%.2hu", (UINT16)(i+1)); \
 		lumpname[8] = 0; \
-		lumpnum = W_CheckNumForName(lumpname); \
+		lumpnum = W_CheckNumForPatchName(lumpname); \
 		if (lumpnum != LUMPERROR) \
 			arr[i] = W_CachePatchName(lumpname, PU_PATCH_LOWPRIORITY); \
 		else \
@@ -3435,7 +3446,7 @@ void F_TitleScreenTicker(boolean run)
 		{
 			for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 			{
-				if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				if (th->removing)
 					continue;
 
 				mo2 = (mobj_t *)th;
@@ -3645,7 +3656,7 @@ static void F_DrawContinueCharacter(INT32 dx, INT32 dy, UINT8 n)
 		(
 			HUD_HOOK(continue), luahuddrawlist_continue[n], contPlayers[n],
 			dx, dy, contskins[n]->highresscale,
-			(INT32)(&contskins[n] - skins), cont_spr2[n][0], cont_spr2[n][1], cont_spr2[n][2] + 1, contColors[n], // add 1 to rotation to convert internal angle numbers (0-7) to WAD editor angle numbers (1-8)
+			(INT32)(contskins[n]->skinnum), cont_spr2[n][0], cont_spr2[n][1], cont_spr2[n][2] + 1, contColors[n], // add 1 to rotation to convert internal angle numbers (0-7) to WAD editor angle numbers (1-8)
 			imcontinuing ? continuetime : timetonext, imcontinuing
 		);
 	}
@@ -3711,7 +3722,7 @@ void F_ContinueDrawer(void)
 	else if (ncontinues > 10)
 	{
 		if (!(continuetime & 1) || continuetime > 17)
-			V_DrawContinueIcon(x, 68, 0, (INT32)(&contskins[0] - skins), contColors[0]);
+			V_DrawContinueIcon(x, 68, 0, contskins[0]->skinnum, contColors[0]);
 		V_DrawScaledPatch(x+12, 66, 0, stlivex);
 		V_DrawRightAlignedString(x+38, 64, 0,
 			va("%d",(imcontinuing ? ncontinues-1 : ncontinues)));
@@ -3725,7 +3736,7 @@ void F_ContinueDrawer(void)
 		{
 			if (i == (ncontinues/2) && ((continuetime & 1) || continuetime > 17))
 				continue;
-			V_DrawContinueIcon(x - (i*30), 68, 0, (INT32)(&contskins[0] - skins), contColors[0]);
+			V_DrawContinueIcon(x - (i*30), 68, 0, contskins[0]->skinnum, contColors[0]);
 		}
 		x = BASEVIDWIDTH>>1;
 	}
@@ -4116,7 +4127,7 @@ static void F_GetPageTextGeometry(UINT8 *pagelines, boolean *rightside, INT32 *b
 	// reuse:
 	// cutnum -> promptnum
 	// scenenum -> pagenum
-	lumpnum_t iconlump = W_CheckNumForName(textprompts[cutnum]->page[scenenum].iconname);
+	lumpnum_t iconlump = W_CheckNumForPatchName(textprompts[cutnum]->page[scenenum].iconname);
 
 	*pagelines = textprompts[cutnum]->page[scenenum].lines ? textprompts[cutnum]->page[scenenum].lines : 4;
 	*rightside = (iconlump != LUMPERROR && textprompts[cutnum]->page[scenenum].rightside);
@@ -4508,7 +4519,7 @@ void F_TextPromptDrawer(void)
 	if (!promptactive)
 		return;
 
-	iconlump = W_CheckNumForName(textprompts[cutnum]->page[scenenum].iconname);
+	iconlump = W_CheckNumForPatchName(textprompts[cutnum]->page[scenenum].iconname);
 	F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr);
 
 	// Draw gfx first
diff --git a/src/f_finale.h b/src/f_finale.h
index 4b777eb11fe45e65b6df4a81a24d47955763551b..e12a81cc62509543b26a443a4f3e954d64a8d851 100644
--- a/src/f_finale.h
+++ b/src/f_finale.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/filesrch.c b/src/filesrch.c
index 6429b6fa2f13b49979c7e5efa1614bf47cac02be..7f104f8cac58ba1e109605b5b0598f14b3a86ce6 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -444,12 +444,11 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 		strcpy(&searchpath[searchpathindex[depthleft]],dent->d_name);
 
 #if defined(__linux__) || defined(__FreeBSD__)
-		if (dent->d_type == DT_UNKNOWN)
-			if (lstat(searchpath,&fsstat) == 0 && S_ISDIR(fsstat.st_mode))
+		if (dent->d_type == DT_UNKNOWN || dent->d_type == DT_LNK)
+			if (stat(searchpath,&fsstat) == 0 && S_ISDIR(fsstat.st_mode))
 				dent->d_type = DT_DIR;
 
 		// Linux and FreeBSD has a special field for file type on dirent, so use that to speed up lookups.
-		// FIXME: should we also follow symlinks?
 		if (dent->d_type == DT_DIR && depthleft)
 #else
 		if (stat(searchpath,&fsstat) < 0) // do we want to follow symlinks? if not: change it to lstat
@@ -699,6 +698,15 @@ static void initdirpath(char *dirpath, size_t *dirpathindex, int depthleft)
 		dirpathindex[depthleft]--;
 }
 
+//sortdir by name?
+static int lumpnamecompare(const void *A, const void *B)
+{
+	const lumpinfo_t *pA = A;
+	const lumpinfo_t *pB = B;
+	return strcmp((pA->fullname), (pB->fullname));
+
+}
+
 lumpinfo_t *getdirectoryfiles(const char *path, UINT16 *nlmp, UINT16 *nfolders)
 {
 	DIR **dirhandle;
@@ -889,6 +897,9 @@ lumpinfo_t *getdirectoryfiles(const char *path, UINT16 *nlmp, UINT16 *nfolders)
 	free(dirpathindex);
 	free(dirhandle);
 
+	//sort files and directories
+	qsort (lumpinfo, numlumps, sizeof(lumpinfo_t), lumpnamecompare);
+
 	(*nlmp) = numlumps;
 	return lumpinfo;
 }
@@ -1179,13 +1190,13 @@ boolean preparefilemenu(boolean samedepth)
 					size_t i;
 
 					if (filenamebuf == NULL)
-						filenamebuf = calloc(sizeof(char) * MAX_WADPATH, numwadfiles);
+						filenamebuf = calloc(numwadfiles, sizeof(char) * MAX_WADPATH);
 
 					for (i = 0; i < numwadfiles; i++)
 					{
 						if (!filenamebuf[i][0])
 						{
-							strncpy(filenamebuf[i], wadfiles[i]->filename, MAX_WADPATH);
+							strncpy(filenamebuf[i], wadfiles[i]->filename, MAX_WADPATH-1);
 							filenamebuf[i][MAX_WADPATH - 1] = '\0';
 							nameonly(filenamebuf[i]);
 						}
diff --git a/src/g_demo.c b/src/g_demo.c
index 0efba5a59724f3b64f3a9ed9dbcf1d137a472e47..479020905ee9bb12f78fda614b4da70d13c75bbc 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -67,6 +67,8 @@ static UINT8 *metalbuffer = NULL;
 static UINT8 *metal_p;
 static UINT16 metalversion;
 
+consvar_t cv_resyncdemo = CVAR_INIT("resyncdemo", "On", 0, CV_OnOff, NULL);
+
 // extra data stuff (events registered this frame while recording)
 static struct {
 	UINT8 flags; // EZT flags
@@ -98,7 +100,7 @@ demoghost *ghosts = NULL;
 // DEMO RECORDING
 //
 
-#define DEMOVERSION 0x0012
+#define DEMOVERSION 0x0011
 #define DEMOHEADER  "\xF0" "SRB2Replay" "\x0F"
 
 #define DF_GHOST        0x01 // This demo contains ghost data too!
@@ -183,11 +185,7 @@ void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
 	if (ziptic & ZT_ANGLE)
 		oldcmd.angleturn = READINT16(demo_p);
 	if (ziptic & ZT_BUTTONS)
-	{
 		oldcmd.buttons = (oldcmd.buttons & (BT_CAMLEFT|BT_CAMRIGHT)) | (READUINT16(demo_p) & ~(BT_CAMLEFT|BT_CAMRIGHT));
-		if (demoversion < 0x0012 && oldcmd.buttons & BT_SPIN)
-			oldcmd.buttons |= BT_SHIELD; // Copy BT_SPIN to BT_SHIELD for pre-Shield-button demos
-	}
 	if (ziptic & ZT_AIMING)
 		oldcmd.aiming = READINT16(demo_p);
 	if (ziptic & ZT_LATENCY)
@@ -549,6 +547,9 @@ void G_ConsGhostTic(void)
 
 	testmo = players[0].mo;
 
+	if (P_MobjWasRemoved(testmo))
+		return; // No valid mobj exists, probably because of unexpected quit
+
 	// Grab ghost data.
 	ziptic = READUINT8(demo_p);
 	if (ziptic & GZT_XYZ)
@@ -609,7 +610,7 @@ void G_ConsGhostTic(void)
 				mobj = NULL;
 				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 				{
-					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+					if (th->removing)
 						continue;
 					mobj = (mobj_t *)th;
 					if (mobj->type == (mobjtype_t)type && mobj->x == x && mobj->y == y && mobj->z == z)
@@ -664,11 +665,14 @@ void G_ConsGhostTic(void)
 			CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
 		demosynced = false;
 
-		P_UnsetThingPosition(testmo);
-		testmo->x = oldghost.x;
-		testmo->y = oldghost.y;
-		P_SetThingPosition(testmo);
-		testmo->z = oldghost.z;
+		if (cv_resyncdemo.value)
+		{
+			P_UnsetThingPosition(testmo);
+			testmo->x = oldghost.x;
+			testmo->y = oldghost.y;
+			P_SetThingPosition(testmo);
+			testmo->z = oldghost.z;
+		}
 	}
 
 	if (*demo_p == DEMOMARKER)
@@ -1446,6 +1450,7 @@ void G_BeginRecording(void)
 	char *filename;
 	UINT16 totalfiles;
 	UINT8 *m;
+	save_t savebuffer;
 
 	if (demo_p)
 		return;
@@ -1595,7 +1600,11 @@ void G_BeginRecording(void)
 	}
 
 	// Save netvar data
-	CV_SaveDemoVars(&demo_p);
+	savebuffer.buf = demo_p;
+	savebuffer.size = demoend - demo_p;
+	savebuffer.pos = 0;
+	CV_SaveDemoVars(&savebuffer);
+	demo_p = &savebuffer.buf[savebuffer.pos];
 
 	memset(&oldcmd,0,sizeof(oldcmd));
 	memset(&oldghost,0,sizeof(oldghost));
@@ -2228,10 +2237,24 @@ void G_DoPlayDemo(char *defdemoname)
 	// net var data
 #ifdef OLD22DEMOCOMPAT
 	if (demoversion < 0x000d)
-		CV_LoadOldDemoVars(&demo_p);
+	{
+		save_t savebuffer;
+		savebuffer.buf = demo_p;
+		savebuffer.size = demoend - demo_p;
+		savebuffer.pos = 0;
+		CV_LoadOldDemoVars(&savebuffer);
+		demo_p = &savebuffer.buf[savebuffer.pos];
+	}
 	else
 #endif
-		CV_LoadDemoVars(&demo_p);
+	{
+		save_t savebuffer;
+		savebuffer.buf = demo_p;
+		savebuffer.size = demoend - demo_p;
+		savebuffer.pos = 0;
+		CV_LoadDemoVars(&savebuffer);
+		demo_p = &savebuffer.buf[savebuffer.pos];
+	}
 
 	// Sigh ... it's an empty demo.
 	if (*demo_p == DEMOMARKER)
@@ -2669,7 +2692,7 @@ void G_DoPlayMetal(void)
 	// find metal sonic
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo = (mobj_t *)th;
diff --git a/src/g_demo.h b/src/g_demo.h
index e8c0c8d956187161ea5bbaffe9c145c764a40ba4..d7c68287170f6f76a66f14b8980150167872cea0 100644
--- a/src/g_demo.h
+++ b/src/g_demo.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -35,6 +35,7 @@ typedef enum
 } demo_file_override_e;
 
 extern demo_file_override_e demofileoverride;
+extern consvar_t cv_resyncdemo;
 
 // Quit after playing a demo from cmdline.
 extern boolean singledemo;
diff --git a/src/g_game.c b/src/g_game.c
index 8d19c9e7cb68df142e68e7d0277b2425153f9206..4e5c3e668b2f83ad5a73f84737428a5f2a8f99a3 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -256,8 +256,6 @@ boolean precache = true; // if true, load all graphics at start
 
 INT16 prevmap, nextmap;
 
-static UINT8 *savebuffer;
-
 // Analog Control
 static void UserAnalog_OnChange(void);
 static void UserAnalog2_OnChange(void);
@@ -403,29 +401,27 @@ consvar_t cv_cam_lockonboss[2] = {
 	CVAR_INIT ("cam2_lockaimassist", "Full", CV_SAVE|CV_ALLOWLUA, lockedassist_cons_t, NULL),
 };
 
-consvar_t cv_moveaxis   = CVAR_INIT ("joyaxis_move",       "Y-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_sideaxis   = CVAR_INIT ("joyaxis_side",       "X-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_lookaxis   = CVAR_INIT ("joyaxis_look",       "X-Rudder-", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_turnaxis   = CVAR_INIT ("joyaxis_turn",       "Z-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_jumpaxis   = CVAR_INIT ("joyaxis_jump",       "None",      CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_spinaxis   = CVAR_INIT ("joyaxis_spin",       "None",      CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_shieldaxis = CVAR_INIT ("joyaxis_shield",     "None",      CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_fireaxis   = CVAR_INIT ("joyaxis_fire",       "Z-Rudder",  CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_firenaxis  = CVAR_INIT ("joyaxis_firenormal", "Z-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_deadzone        = CVAR_INIT ("joy_deadzone",    "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
-consvar_t cv_digitaldeadzone = CVAR_INIT ("joy_digdeadzone", "0.25",  CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
-
-consvar_t cv_moveaxis2   = CVAR_INIT ("joyaxis2_move",       "Y-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_sideaxis2   = CVAR_INIT ("joyaxis2_side",       "X-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_lookaxis2   = CVAR_INIT ("joyaxis2_look",       "X-Rudder-", CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_turnaxis2   = CVAR_INIT ("joyaxis2_turn",       "Z-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_jumpaxis2   = CVAR_INIT ("joyaxis2_jump",       "None",      CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_spinaxis2   = CVAR_INIT ("joyaxis2_spin",       "None",      CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_shieldaxis2 = CVAR_INIT ("joyaxis2_shield",     "None",      CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_fireaxis2   = CVAR_INIT ("joyaxis2_fire",       "Z-Rudder",  CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_firenaxis2  = CVAR_INIT ("joyaxis2_firenormal", "Z-Axis",    CV_SAVE, joyaxis_cons_t, NULL);
-consvar_t cv_deadzone2        = CVAR_INIT ("joy_deadzone2",    "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
-consvar_t cv_digitaldeadzone2 = CVAR_INIT ("joy_digdeadzone2", "0.25",  CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
+consvar_t cv_moveaxis = CVAR_INIT ("joyaxis_move", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_sideaxis = CVAR_INIT ("joyaxis_side", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_lookaxis = CVAR_INIT ("joyaxis_look", "X-Rudder-", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_turnaxis = CVAR_INIT ("joyaxis_turn", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_jumpaxis = CVAR_INIT ("joyaxis_jump", "None", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_spinaxis = CVAR_INIT ("joyaxis_spin", "None", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_fireaxis = CVAR_INIT ("joyaxis_fire", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_firenaxis = CVAR_INIT ("joyaxis_firenormal", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_deadzone = CVAR_INIT ("joy_deadzone", "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
+consvar_t cv_digitaldeadzone = CVAR_INIT ("joy_digdeadzone", "0.25", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
+
+consvar_t cv_moveaxis2 = CVAR_INIT ("joyaxis2_move", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_sideaxis2 = CVAR_INIT ("joyaxis2_side", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_lookaxis2 = CVAR_INIT ("joyaxis2_look", "X-Rudder-", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_turnaxis2 = CVAR_INIT ("joyaxis2_turn", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_jumpaxis2 = CVAR_INIT ("joyaxis2_jump", "None", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_spinaxis2 = CVAR_INIT ("joyaxis2_spin", "None", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_fireaxis2 = CVAR_INIT ("joyaxis2_fire", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_firenaxis2 = CVAR_INIT ("joyaxis2_firenormal", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL);
+consvar_t cv_deadzone2 = CVAR_INIT ("joy_deadzone2", "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
+consvar_t cv_digitaldeadzone2 = CVAR_INIT ("joy_digdeadzone2", "0.25", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
 
 player_t *seenplayer; // player we're aiming at right now
 
@@ -896,9 +892,6 @@ INT32 JoyAxis(joyaxis_e axissel)
 		case JA_SPIN:
 			axisval = cv_spinaxis.value;
 			break;
-		case JA_SHIELD:
-			axisval = cv_shieldaxis.value;
-			break;
 		case JA_FIRE:
 			axisval = cv_fireaxis.value;
 			break;
@@ -972,9 +965,6 @@ INT32 Joy2Axis(joyaxis_e axissel)
 		case JA_SPIN:
 			axisval = cv_spinaxis2.value;
 			break;
-		case JA_SHIELD:
-			axisval = cv_shieldaxis2.value;
-			break;
 		case JA_FIRE:
 			axisval = cv_fireaxis2.value;
 			break;
@@ -1342,10 +1332,10 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	if (PLAYERINPUTDOWN(ssplayer, GC_WEAPONPREV))
 		cmd->buttons |= BT_WEAPONPREV; // Previous Weapon
 
-#if NUM_WEAPONS > 7
-"Add extra inputs to g_input.h/gamecontrols_e, and fix conflicts in d_ticcmd.h/ticcmd_t/buttons"
+#if NUM_WEAPONS > 10
+"Add extra inputs to g_input.h/gamecontrols_e"
 #endif
-	//use the three avaliable bits to determine the weapon.
+	//use the four avaliable bits to determine the weapon.
 	cmd->buttons &= ~BT_WEAPONMASK;
 	for (i = 0; i < NUM_WEAPONS; ++i)
 		if (PLAYERINPUTDOWN(ssplayer, GC_WEPSLOT1 + i))
@@ -1363,15 +1353,9 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	axis = PlayerJoyAxis(ssplayer, JA_FIRENORMAL);
 	if (PLAYERINPUTDOWN(ssplayer, GC_FIRENORMAL) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_FIRENORMAL;
-	
-	// Toss flag button
+
 	if (PLAYERINPUTDOWN(ssplayer, GC_TOSSFLAG))
 		cmd->buttons |= BT_TOSSFLAG;
-	
-	// Shield button
-	axis = PlayerJoyAxis(ssplayer, JA_SHIELD);
-	if (PLAYERINPUTDOWN(ssplayer, GC_SHIELD) || (usejoystick && axis > 0))
-		cmd->buttons |= BT_SHIELD;
 
 	// Lua scriptable buttons
 	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM1))
@@ -1386,6 +1370,13 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	if (PLAYERINPUTDOWN(ssplayer, GC_SPIN) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_SPIN;
 
+	if (gamestate == GS_INTRO) // prevent crash in intro
+	{
+		cmd->angleturn = ticcmd_oldangleturn[forplayer];
+		cmd->aiming = G_ClipAimingPitch(myaiming);
+		return;
+	}
+
 	// Centerview can be a toggle in simple mode!
 	{
 		static boolean last_centerviewdown[2], centerviewhold[2]; // detect taps for toggle behavior
@@ -1420,7 +1411,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 		ticcmd_centerviewdown[forplayer] = true;
 	}
-	else if (ticcmd_centerviewdown[forplayer])
+	else if (ticcmd_centerviewdown[forplayer] || (leveltime < 5))
 	{
 		if (controlstyle == CS_SIMPLE)
 		{
@@ -1435,6 +1426,9 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	{
 		if (
 			P_MobjWasRemoved(ticcmd_ztargetfocus[forplayer]) ||
+			(leveltime < 5) ||
+			(player->playerstate != PST_LIVE) ||
+			player->exiting ||
 			!ticcmd_ztargetfocus[forplayer]->health ||
 			(ticcmd_ztargetfocus[forplayer]->type == MT_EGGMOBILE3 && !ticcmd_ztargetfocus[forplayer]->movecount) // Sea Egg is moving around underground and shouldn't be tracked
 		)
@@ -1466,7 +1460,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			P_SetTarget(&newtarget->target, ticcmd_ztargetfocus[forplayer]);
 			newtarget->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
 
-			if (player->mo && P_AproxDistance(
+			if (player->mo && R_PointToDist2(0, 0,
 				player->mo->x - ticcmd_ztargetfocus[forplayer]->x,
 				player->mo->y - ticcmd_ztargetfocus[forplayer]->y
 			) > 50*player->mo->scale)
@@ -1926,6 +1920,8 @@ void G_DoLoadLevel(boolean resetplayer)
 //
 void G_StartTitleCard(void)
 {
+	ST_stopTitleCard();
+
 	// The title card has been disabled for this map.
 	// Oh well.
 	if (!G_IsTitleCardAvailable())
@@ -2274,6 +2270,11 @@ boolean G_LuaResponder(event_t *ev)
 		cancelled = LUA_HookKey(ev, HOOK(KeyUp));
 		LUA_InvalidateUserdata(ev);
 	}
+	else if (ev->type == ev_text)
+	{
+		cancelled = LUA_HookText(ev, HOOK(TextInput));
+		LUA_InvalidateUserdata(ev);
+	}
 
 	return cancelled;
 }
@@ -2614,6 +2615,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	boolean spectator;
 	boolean outofcoop;
 	boolean removing;
+	boolean muted;
 	INT16 bot;
 	SINT8 pity;
 	INT16 rings;
@@ -2631,6 +2633,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	spectator = players[player].spectator;
 	outofcoop = players[player].outofcoop;
 	removing = players[player].removing;
+	muted = players[player].muted;
 	pflags = (players[player].pflags & (PF_FLIPCAM|PF_ANALOGMODE|PF_DIRECTIONCHAR|PF_AUTOBRAKE|PF_TAGIT|PF_GAMETYPEOVER));
 	playerangleturn = players[player].angleturn;
 	oldrelangleturn = players[player].oldrelangleturn;
@@ -2708,6 +2711,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	p->spectator = spectator;
 	p->outofcoop = outofcoop;
 	p->removing = removing;
+	p->muted = muted;
 	p->angleturn = playerangleturn;
 	p->oldrelangleturn = oldrelangleturn;
 
@@ -2764,7 +2768,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	p->pflags |= PF_SPINDOWN;
 	p->pflags |= PF_ATTACKDOWN;
 	p->pflags |= PF_JUMPDOWN;
-	p->pflags |= PF_SHIELDDOWN;
 
 	p->playerstate = PST_LIVE;
 	p->panim = PA_IDLE; // standing animation
@@ -3059,7 +3062,7 @@ void G_ChangePlayerReferences(mobj_t *oldmo, mobj_t *newmo)
 	// scan all thinkers
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -3342,7 +3345,7 @@ void G_AddPlayer(INT32 playernum)
 
 	p->playerstate = PST_REBORN;
 
-	p->height = mobjinfo[MT_PLAYER].height;
+	p->height = skins[p->skin]->height;
 
 	if (G_GametypeUsesLives() || ((netgame || multiplayer) && (gametyperules & GTR_FRIENDLY)))
 		p->lives = cv_startinglives.value;
@@ -4018,7 +4021,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 	INT32 i;
 	INT16 newmapnum;
 	boolean spec = G_IsSpecialStage(gamemap);
-	
+
 	// go to next level
 	// newmapnum is 0-based, unlike gamemap
 	if (nextmapoverride != 0)
@@ -4122,7 +4125,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 
 	if (spec && (!gottoken || ignoretokens) && !nextmapoverride)
 		newmapnum = lastmap; // Exiting from a special stage? Go back to the game. Tails 08-11-2001
-	
+
 	if (!(gametyperules & GTR_CAMPAIGN))
 	{
 		if (cv_advancemap.value == 0) // Stay on same map.
@@ -4130,7 +4133,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 		else if (cv_advancemap.value == 2) // Go to random map.
 			newmapnum = RandMap(G_TOLFlag(gametype_to_use), prevmap);
 	}
-	
+
 	return newmapnum;
 }
 
@@ -4140,7 +4143,7 @@ INT16 G_GetNextMap(boolean ignoretokens, boolean silent)
 static void G_DoCompleted(void)
 {
 	INT32 i;
-	
+
 	tokenlist = 0; // Reset the list
 
 	if (modeattacking && pausedelay)
@@ -4168,7 +4171,7 @@ static void G_DoCompleted(void)
 	//Get and set prevmap/nextmap
 	prevmap = (INT16)(gamemap-1);
 	nextmap = G_GetNextMap(false, false);
-	
+
 	automapactive = false;
 
 	// We are committed to this map now.
@@ -4386,7 +4389,7 @@ void G_LoadGameSettings(void)
 // Loads the main data file, which stores information such as emblems found, etc.
 void G_LoadGameData(gamedata_t *data)
 {
-	size_t length;
+	save_t savebuffer;
 	INT32 i, j;
 
 	UINT32 versionID;
@@ -4428,18 +4431,18 @@ void G_LoadGameData(gamedata_t *data)
 		return;
 	}
 
-	length = FIL_ReadFile(va(pandf, srb2home, gamedatafilename), &savebuffer);
-	if (!length)
+	savebuffer.size = FIL_ReadFile(va(pandf, srb2home, gamedatafilename), &savebuffer.buf);
+	if (!savebuffer.size)
 	{
 		// No gamedata. We can save a new one.
 		data->loaded = true;
 		return;
 	}
 
-	save_p = savebuffer;
+	savebuffer.pos = 0;
 
 	// Version check
-	versionID = READUINT32(save_p);
+	versionID = P_ReadUINT32(&savebuffer);
 	if (versionID != GAMEDATA_ID
 #ifdef COMPAT_GAMEDATA_ID // backwards compat behavior
 		&& versionID != COMPAT_GAMEDATA_ID
@@ -4450,8 +4453,7 @@ void G_LoadGameData(gamedata_t *data)
 		if (strcmp(srb2home,"."))
 			gdfolder = srb2home;
 
-		Z_Free(savebuffer);
-		save_p = NULL;
+		Z_Free(savebuffer.buf);
 		I_Error("Game data is from another version of SRB2.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder);
 	}
 
@@ -4463,14 +4465,14 @@ void G_LoadGameData(gamedata_t *data)
 	}
 #endif
 
-	data->totalplaytime = READUINT32(save_p);
+	data->totalplaytime = P_ReadUINT32(&savebuffer);
 
 #ifdef COMPAT_GAMEDATA_ID
 	if (versionID == COMPAT_GAMEDATA_ID)
 	{
 		// We'll temporarily use the old condition when loading an older file.
 		// The proper mod-specific hash will get saved in afterwards.
-		boolean modded = READUINT8(save_p);
+		boolean modded = P_ReadUINT8(&savebuffer);
 
 		if (modded && !savemoddata)
 		{
@@ -4490,13 +4492,13 @@ void G_LoadGameData(gamedata_t *data)
 		strcpy(currentfilename, gamedatafilename);
 		STRBUFCPY(backupfilename, strcat(currentfilename, bak));
 
-		FIL_WriteFile(va(pandf, srb2home, backupfilename), savebuffer, length);
+		FIL_WriteFile(va(pandf, srb2home, backupfilename), &savebuffer.buf, savebuffer.size);
 	}
 	else
 #endif
 	{
 		// Quick & dirty hash for what mod this save file is for.
-		UINT32 modID = READUINT32(save_p);
+		UINT32 modID = P_ReadUINT32(&savebuffer);
 		UINT32 expectedID = quickncasehash(timeattackfolder, sizeof timeattackfolder);
 
 		if (modID != expectedID)
@@ -4508,50 +4510,50 @@ void G_LoadGameData(gamedata_t *data)
 
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
-		if ((data->mapvisited[i] = READUINT8(save_p)) > MV_MAX)
+		if ((data->mapvisited[i] = P_ReadUINT8(&savebuffer)) > MV_MAX)
 			goto datacorrupt;
 
 	// To save space, use one bit per collected/achieved/unlocked flag
 	for (i = 0; i < max_emblems;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(&savebuffer);
 		for (j = 0; j < 8 && j+i < max_emblems; ++j)
 			data->collected[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 	for (i = 0; i < max_extraemblems;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(&savebuffer);
 		for (j = 0; j < 8 && j+i < max_extraemblems; ++j)
 			data->extraCollected[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 	for (i = 0; i < max_unlockables;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(&savebuffer);
 		for (j = 0; j < 8 && j+i < max_unlockables; ++j)
 			data->unlocked[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 	for (i = 0; i < max_conditionsets;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(&savebuffer);
 		for (j = 0; j < 8 && j+i < max_conditionsets; ++j)
 			data->achieved[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 
-	data->timesBeaten = READUINT32(save_p);
-	data->timesBeatenWithEmeralds = READUINT32(save_p);
-	data->timesBeatenUltimate = READUINT32(save_p);
+	data->timesBeaten = P_ReadUINT32(&savebuffer);
+	data->timesBeatenWithEmeralds = P_ReadUINT32(&savebuffer);
+	data->timesBeatenUltimate = P_ReadUINT32(&savebuffer);
 
 	// Main records
 	for (i = 0; i < NUMMAPS; ++i)
 	{
-		recscore = READUINT32(save_p);
-		rectime  = (tic_t)READUINT32(save_p);
-		recrings = READUINT16(save_p);
-		save_p++; // compat
+		recscore = P_ReadUINT32(&savebuffer);
+		rectime  = (tic_t)P_ReadUINT32(&savebuffer);
+		recrings = P_ReadUINT16(&savebuffer);
+		P_ReadUINT8(&savebuffer); // compat
 
 		if (recrings > 10000 || recscore > MAXSCORE)
 			goto datacorrupt;
@@ -4568,16 +4570,16 @@ void G_LoadGameData(gamedata_t *data)
 	// Nights records
 	for (i = 0; i < NUMMAPS; ++i)
 	{
-		if ((recmares = READUINT8(save_p)) == 0)
+		if ((recmares = P_ReadUINT8(&savebuffer)) == 0)
 			continue;
 
 		G_AllocNightsRecordData((INT16)i, data);
 
 		for (curmare = 0; curmare < (recmares+1); ++curmare)
 		{
-			data->nightsrecords[i]->score[curmare] = READUINT32(save_p);
-			data->nightsrecords[i]->grade[curmare] = READUINT8(save_p);
-			data->nightsrecords[i]->time[curmare] = (tic_t)READUINT32(save_p);
+			data->nightsrecords[i]->score[curmare] = P_ReadUINT32(&savebuffer);
+			data->nightsrecords[i]->grade[curmare] = P_ReadUINT8(&savebuffer);
+			data->nightsrecords[i]->time[curmare] = (tic_t)P_ReadUINT32(&savebuffer);
 
 			if (data->nightsrecords[i]->grade[curmare] > GRADE_S)
 			{
@@ -4589,8 +4591,7 @@ void G_LoadGameData(gamedata_t *data)
 	}
 
 	// done
-	Z_Free(savebuffer);
-	save_p = NULL;
+	Z_Free(savebuffer.buf);
 
 	// Don't consider loaded until it's a success!
 	// It used to do this much earlier, but this would cause the gamedata to
@@ -4611,8 +4612,7 @@ void G_LoadGameData(gamedata_t *data)
 		if (strcmp(srb2home,"."))
 			gdfolder = srb2home;
 
-		Z_Free(savebuffer);
-		save_p = NULL;
+		Z_Free(savebuffer.buf);
 
 		I_Error("Corrupt game data file.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder);
 	}
@@ -4622,9 +4622,8 @@ void G_LoadGameData(gamedata_t *data)
 // Saves the main data file, which stores information such as emblems found, etc.
 void G_SaveGameData(gamedata_t *data)
 {
-	UINT8 *data_p;
+	save_t savebuffer;
 
-	size_t length;
 	INT32 i, j;
 	UINT8 btemp;
 
@@ -4636,30 +4635,31 @@ void G_SaveGameData(gamedata_t *data)
 	if (!data->loaded)
 		return; // If never loaded (-nodata), don't save
 
-	data_p = savebuffer = (UINT8 *)malloc(GAMEDATASIZE);
-	if (!data_p)
+	savebuffer.size = GAMEDATASIZE;
+	savebuffer.buf = (UINT8 *)malloc(savebuffer.size);
+	if (!savebuffer.buf)
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for saving game data\n"));
 		return;
 	}
+	savebuffer.pos = 0;
 
 	if (usedCheats)
 	{
-		free(savebuffer);
-		savebuffer = NULL;
+		free(savebuffer.buf);
 		return;
 	}
 
 	// Version test
-	WRITEUINT32(data_p, GAMEDATA_ID);
+	P_WriteUINT32(&savebuffer, GAMEDATA_ID);
 
-	WRITEUINT32(data_p, data->totalplaytime);
+	P_WriteUINT32(&savebuffer, data->totalplaytime);
 
-	WRITEUINT32(data_p, quickncasehash(timeattackfolder, sizeof timeattackfolder));
+	P_WriteUINT32(&savebuffer, quickncasehash(timeattackfolder, sizeof timeattackfolder));
 
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
-		WRITEUINT8(data_p, (data->mapvisited[i] & MV_MAX));
+		P_WriteUINT8(&savebuffer, (data->mapvisited[i] & MV_MAX));
 
 	// To save space, use one bit per collected/achieved/unlocked flag
 	for (i = 0; i < MAXEMBLEMS;)
@@ -4667,7 +4667,7 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
 			btemp |= (data->collected[j+i] << j);
-		WRITEUINT8(data_p, btemp);
+		P_WriteUINT8(&savebuffer, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXEXTRAEMBLEMS;)
@@ -4675,7 +4675,7 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
 			btemp |= (data->extraCollected[j+i] << j);
-		WRITEUINT8(data_p, btemp);
+		P_WriteUINT8(&savebuffer, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXUNLOCKABLES;)
@@ -4683,7 +4683,7 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
 			btemp |= (data->unlocked[j+i] << j);
-		WRITEUINT8(data_p, btemp);
+		P_WriteUINT8(&savebuffer, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXCONDITIONSETS;)
@@ -4691,30 +4691,30 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
 			btemp |= (data->achieved[j+i] << j);
-		WRITEUINT8(data_p, btemp);
+		P_WriteUINT8(&savebuffer, btemp);
 		i += j;
 	}
 
-	WRITEUINT32(data_p, data->timesBeaten);
-	WRITEUINT32(data_p, data->timesBeatenWithEmeralds);
-	WRITEUINT32(data_p, data->timesBeatenUltimate);
+	P_WriteUINT32(&savebuffer, data->timesBeaten);
+	P_WriteUINT32(&savebuffer, data->timesBeatenWithEmeralds);
+	P_WriteUINT32(&savebuffer, data->timesBeatenUltimate);
 
 	// Main records
 	for (i = 0; i < NUMMAPS; i++)
 	{
 		if (data->mainrecords[i])
 		{
-			WRITEUINT32(data_p, data->mainrecords[i]->score);
-			WRITEUINT32(data_p, data->mainrecords[i]->time);
-			WRITEUINT16(data_p, data->mainrecords[i]->rings);
+			P_WriteUINT32(&savebuffer, data->mainrecords[i]->score);
+			P_WriteUINT32(&savebuffer, data->mainrecords[i]->time);
+			P_WriteUINT16(&savebuffer, data->mainrecords[i]->rings);
 		}
 		else
 		{
-			WRITEUINT32(data_p, 0);
-			WRITEUINT32(data_p, 0);
-			WRITEUINT16(data_p, 0);
+			P_WriteUINT32(&savebuffer, 0);
+			P_WriteUINT32(&savebuffer, 0);
+			P_WriteUINT16(&savebuffer, 0);
 		}
-		WRITEUINT8(data_p, 0); // compat
+		P_WriteUINT8(&savebuffer, 0); // compat
 	}
 
 	// NiGHTS records
@@ -4722,25 +4722,22 @@ void G_SaveGameData(gamedata_t *data)
 	{
 		if (!data->nightsrecords[i] || !data->nightsrecords[i]->nummares)
 		{
-			WRITEUINT8(data_p, 0);
+			P_WriteUINT8(&savebuffer, 0);
 			continue;
 		}
 
-		WRITEUINT8(data_p, data->nightsrecords[i]->nummares);
+		P_WriteUINT8(&savebuffer, data->nightsrecords[i]->nummares);
 
 		for (curmare = 0; curmare < (data->nightsrecords[i]->nummares + 1); ++curmare)
 		{
-			WRITEUINT32(data_p, data->nightsrecords[i]->score[curmare]);
-			WRITEUINT8(data_p, data->nightsrecords[i]->grade[curmare]);
-			WRITEUINT32(data_p, data->nightsrecords[i]->time[curmare]);
+			P_WriteUINT32(&savebuffer, data->nightsrecords[i]->score[curmare]);
+			P_WriteUINT8(&savebuffer, data->nightsrecords[i]->grade[curmare]);
+			P_WriteUINT32(&savebuffer, data->nightsrecords[i]->time[curmare]);
 		}
 	}
 
-	length = data_p - savebuffer;
-
-	FIL_WriteFile(va(pandf, srb2home, gamedatafilename), savebuffer, length);
-	free(savebuffer);
-	savebuffer = NULL;
+	FIL_WriteFile(va(pandf, srb2home, gamedatafilename), savebuffer.buf, savebuffer.pos);
+	free(savebuffer.buf);
 }
 
 #define VERSIONSIZE 16
@@ -4751,7 +4748,7 @@ void G_SaveGameData(gamedata_t *data)
 //
 void G_LoadGame(UINT32 slot, INT16 mapoverride)
 {
-	size_t length;
+	save_t savebuffer;
 	char vcheck[VERSIONSIZE];
 	char savename[255];
 
@@ -4768,18 +4765,18 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 	else
 		sprintf(savename, savegamename, slot);
 
-	length = FIL_ReadFile(savename, &savebuffer);
-	if (!length)
+	savebuffer.size = FIL_ReadFile(savename, &savebuffer.buf);
+	if (!savebuffer.size)
 	{
 		CONS_Printf(M_GetText("Couldn't read file %s\n"), savename);
 		return;
 	}
 
-	save_p = savebuffer;
+	savebuffer.pos = 0;
 
 	memset(vcheck, 0, sizeof (vcheck));
 	sprintf(vcheck, (marathonmode ? "back-up %d" : "version %d"), VERSION);
-	if (strcmp((const char *)save_p, (const char *)vcheck))
+	if (strcmp((const char *)&savebuffer.buf[savebuffer.pos], (const char *)vcheck))
 	{
 #ifdef SAVEGAME_OTHERVERSIONS
 		M_StartMessage(M_GetText("Save game from different version.\nYou can load this savegame, but\nsaving afterwards will be disabled.\n\nDo you want to continue anyway?\n\n(Press 'Y' to confirm)\n"),
@@ -4789,15 +4786,14 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 		M_ClearMenus(true); // so ESC backs out to title
 		M_StartMessage(M_GetText("Save game from different version\n\nPress ESC\n"), NULL, MM_NOTHING);
 		Command_ExitGame_f();
-		Z_Free(savebuffer);
-		save_p = savebuffer = NULL;
+		Z_Free(savebuffer.buf);
 
 		// no cheating!
 		memset(&savedata, 0, sizeof(savedata));
 #endif
 		return; // bad version
 	}
-	save_p += VERSIONSIZE;
+	savebuffer.pos += VERSIONSIZE;
 
 //	if (demoplayback) // reset game engine
 //		G_StopDemo();
@@ -4806,13 +4802,12 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 //	automapactive = false;
 
 	// dearchive all the modifications
-	if (!P_LoadGame(mapoverride))
+	if (!P_LoadGame(&savebuffer, mapoverride))
 	{
 		M_ClearMenus(true); // so ESC backs out to title
 		M_StartMessage(M_GetText("Savegame file corrupted\n\nPress ESC\n"), NULL, MM_NOTHING);
 		Command_ExitGame_f();
-		Z_Free(savebuffer);
-		save_p = savebuffer = NULL;
+		Z_Free(savebuffer.buf);
 
 		// no cheating!
 		memset(&savedata, 0, sizeof(savedata));
@@ -4820,13 +4815,12 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 	}
 	if (marathonmode)
 	{
-		marathontime = READUINT32(save_p);
-		marathonmode |= READUINT8(save_p);
+		marathontime = P_ReadUINT32(&savebuffer);
+		marathonmode |= P_ReadUINT8(&savebuffer);
 	}
 
 	// done
-	Z_Free(savebuffer);
-	save_p = savebuffer = NULL;
+	Z_Free(savebuffer.buf);
 
 	displayplayer = consoleplayer;
 	multiplayer = splitscreen = false;
@@ -4844,6 +4838,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 //
 void G_SaveGame(UINT32 slot, INT16 mapnum)
 {
+	save_t savebuffer;
 	boolean saved;
 	char savename[256] = "";
 	const char *backup;
@@ -4857,33 +4852,32 @@ void G_SaveGame(UINT32 slot, INT16 mapnum)
 	gameaction = ga_nothing;
 	{
 		char name[VERSIONSIZE];
-		size_t length;
 
-		save_p = savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
-		if (!save_p)
+		savebuffer.size = SAVEGAMESIZE;
+		savebuffer.buf = (UINT8 *)malloc(savebuffer.size);
+		if (!savebuffer.buf)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("No more free memory for saving game data\n"));
 			return;
 		}
+		savebuffer.pos = 0;
 
 		memset(name, 0, sizeof (name));
 		sprintf(name, (marathonmode ? "back-up %d" : "version %d"), VERSION);
-		WRITEMEM(save_p, name, VERSIONSIZE);
+		P_WriteMem(&savebuffer, name, VERSIONSIZE);
 
-		P_SaveGame(mapnum);
+		P_SaveGame(&savebuffer, mapnum);
 		if (marathonmode)
 		{
 			UINT32 writetime = marathontime;
 			if (!(marathonmode & MA_INGAME))
 				writetime += TICRATE*5; // live event backup penalty because we don't know how long it takes to get to the next map
-			WRITEUINT32(save_p, writetime);
-			WRITEUINT8(save_p, (marathonmode & ~MA_INIT));
+			P_WriteUINT32(&savebuffer, writetime);
+			P_WriteUINT8(&savebuffer, (marathonmode & ~MA_INIT));
 		}
 
-		length = save_p - savebuffer;
-		saved = FIL_WriteFile(backup, savebuffer, length);
-		free(savebuffer);
-		save_p = savebuffer = NULL;
+		saved = FIL_WriteFile(backup, savebuffer.buf, savebuffer.pos);
+		free(savebuffer.buf);
 	}
 
 	gameaction = ga_nothing;
@@ -4895,11 +4889,10 @@ void G_SaveGame(UINT32 slot, INT16 mapnum)
 }
 
 #define BADSAVE goto cleanup;
-#define CHECKPOS if (save_p >= end_p) BADSAVE
 void G_SaveGameOver(UINT32 slot, boolean modifylives)
 {
+	save_t savebuffer;
 	boolean saved = false;
-	size_t length;
 	char vcheck[VERSIONSIZE];
 	char savename[255];
 	const char *backup;
@@ -4910,42 +4903,38 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
 		sprintf(savename, savegamename, slot);
 	backup = va("%s",savename);
 
-	length = FIL_ReadFile(savename, &savebuffer);
-	if (!length)
+	savebuffer.size = FIL_ReadFile(savename, &savebuffer.buf);
+	if (!savebuffer.size)
 	{
 		CONS_Printf(M_GetText("Couldn't read file %s\n"), savename);
 		return;
 	}
 
+	savebuffer.pos = 0;
+
 	{
 		char temp[sizeof(timeattackfolder)];
-		UINT8 *end_p = savebuffer + length;
 		UINT8 *lives_p;
 		SINT8 pllives;
 #ifdef NEWSKINSAVES
 		INT16 backwardsCompat = 0;
 #endif
 
-		save_p = savebuffer;
 		// Version check
 		memset(vcheck, 0, sizeof (vcheck));
 		sprintf(vcheck, (marathonmode ? "back-up %d" : "version %d"), VERSION);
-		if (strcmp((const char *)save_p, (const char *)vcheck)) BADSAVE
-		save_p += VERSIONSIZE;
+		if (strcmp((const char *)&savebuffer.buf[savebuffer.pos], (const char *)vcheck)) BADSAVE
+		savebuffer.pos += VERSIONSIZE;
 
 		// P_UnArchiveMisc()
-		(void)READINT16(save_p);
-		CHECKPOS
-		(void)READUINT16(save_p); // emeralds
-		CHECKPOS
-		READSTRINGN(save_p, temp, sizeof(temp)); // mod it belongs to
+		(void)P_ReadINT16(&savebuffer);
+		(void)P_ReadUINT16(&savebuffer); // emeralds
+		P_ReadStringN(&savebuffer, temp, sizeof(temp)); // mod it belongs to
 		if (strcmp(temp, timeattackfolder)) BADSAVE
 
 		// P_UnArchivePlayer()
-		CHECKPOS
 #ifdef NEWSKINSAVES
-		backwardsCompat = READUINT16(save_p);
-		CHECKPOS
+		backwardsCompat = P_ReadUINT16(&savebuffer);
 
 		if (backwardsCompat == NEWSKINSAVES) // New save, read skin names
 #endif
@@ -4953,47 +4942,37 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
 			char ourSkinName[SKINNAMESIZE+1];
 			char botSkinName[SKINNAMESIZE+1];
 
-			READSTRINGN(save_p, ourSkinName, SKINNAMESIZE);
-			CHECKPOS
+			P_ReadStringN(&savebuffer, ourSkinName, SKINNAMESIZE);
 
-			READSTRINGN(save_p, botSkinName, SKINNAMESIZE);
-			CHECKPOS
+			P_ReadStringN(&savebuffer, botSkinName, SKINNAMESIZE);
 		}
 
-		WRITEUINT8(save_p, numgameovers);
-		CHECKPOS
+		P_WriteUINT8(&savebuffer, numgameovers);
 
-		lives_p = save_p;
-		pllives = READSINT8(save_p); // lives
-		CHECKPOS
+		lives_p = &savebuffer.buf[savebuffer.pos];
+		pllives = P_ReadSINT8(&savebuffer); // lives
 		if (modifylives && pllives < startinglivesbalance[numgameovers])
 		{
-			pllives = startinglivesbalance[numgameovers];
-			WRITESINT8(lives_p, pllives);
+			*lives_p = startinglivesbalance[numgameovers];
 		}
 
-		(void)READINT32(save_p); // Score
-		CHECKPOS
-		(void)READINT32(save_p); // continues
+		(void)P_ReadINT32(&savebuffer); // Score
+		(void)P_ReadINT32(&savebuffer); // continues
 
 		// File end marker check
-		CHECKPOS
-		switch (READUINT8(save_p))
+		switch (P_ReadUINT8(&savebuffer))
 		{
 			case 0xb7:
 				{
 					UINT8 i, banksinuse;
-					CHECKPOS
-					banksinuse = READUINT8(save_p);
-					CHECKPOS
+					banksinuse = P_ReadUINT8(&savebuffer);
 					if (banksinuse > NUM_LUABANKS)
 						BADSAVE
 					for (i = 0; i < banksinuse; i++)
 					{
-						(void)READINT32(save_p);
-						CHECKPOS
+						(void)P_ReadINT32(&savebuffer);
 					}
-					if (READUINT8(save_p) != 0x1d)
+					if (P_ReadUINT8(&savebuffer) != 0x1d)
 						BADSAVE
 				}
 			case 0x1d:
@@ -5003,7 +4982,7 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
 		}
 
 		// done
-		saved = FIL_WriteFile(backup, savebuffer, length);
+		saved = FIL_WriteFile(backup, savebuffer.buf, savebuffer.size);
 	}
 
 cleanup:
@@ -5011,11 +4990,9 @@ cleanup:
 		CONS_Printf(M_GetText("Game saved.\n"));
 	else if (!saved)
 		CONS_Alert(CONS_ERROR, M_GetText("Error while writing to %s for save slot %u, base: %s\n"), backup, slot, (marathonmode ? liveeventbackup : savegamename));
-	Z_Free(savebuffer);
-	save_p = savebuffer = NULL;
+	Z_Free(savebuffer.buf);
 
 }
-#undef CHECKPOS
 #undef BADSAVE
 
 //
diff --git a/src/g_game.h b/src/g_game.h
index 0d5fc7e373179f9b9905ab9361bb2dafb146afb7..f72ea6b41b1b29b4b263b9039c0ad588bc78de17 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -71,8 +71,8 @@ typedef enum {
 #define P_ControlStyle(player) ((((player)->pflags & PF_ANALOGMODE) ? CS_LMAOGALOG : 0) | (((player)->pflags & PF_DIRECTIONCHAR) ? CS_STANDARD : 0))
 
 extern consvar_t cv_autobrake, cv_autobrake2;
-extern consvar_t cv_sideaxis, cv_turnaxis, cv_moveaxis, cv_lookaxis, cv_jumpaxis, cv_spinaxis, cv_shieldaxis, cv_fireaxis, cv_firenaxis, cv_deadzone, cv_digitaldeadzone;
-extern consvar_t cv_sideaxis2,cv_turnaxis2,cv_moveaxis2,cv_lookaxis2,cv_jumpaxis2,cv_spinaxis2,cv_shieldaxis2,cv_fireaxis2,cv_firenaxis2,cv_deadzone2,cv_digitaldeadzone2;
+extern consvar_t cv_sideaxis,cv_turnaxis,cv_moveaxis,cv_lookaxis,cv_jumpaxis,cv_spinaxis,cv_fireaxis,cv_firenaxis,cv_deadzone,cv_digitaldeadzone;
+extern consvar_t cv_sideaxis2,cv_turnaxis2,cv_moveaxis2,cv_lookaxis2,cv_jumpaxis2,cv_spinaxis2,cv_fireaxis2,cv_firenaxis2,cv_deadzone2,cv_digitaldeadzone2;
 extern consvar_t cv_ghost_bestscore, cv_ghost_besttime, cv_ghost_bestrings, cv_ghost_last, cv_ghost_guest;
 
 // hi here's some new controls
@@ -100,7 +100,6 @@ typedef enum
 
 	JA_JUMP = JA_DIGITAL,
 	JA_SPIN,
-	JA_SHIELD,
 	JA_FIRE,
 	JA_FIRENORMAL,
 } joyaxis_e;
diff --git a/src/g_input.c b/src/g_input.c
index 4a9692e8812ad89b20a85fb5f727acf9f8e52e86..4fbdf5e7586d41c4dd09f0a5e5c3e2aafa67c99d 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -576,7 +576,9 @@ static const char *gamecontrolname[NUM_GAMECONTROLS] =
 	"weapon5",
 	"weapon6",
 	"weapon7",
-	"shield",
+	"weapon8",
+	"weapon9",
+	"weapon10",
 	"fire",
 	"firenormal",
 	"tossflag",
@@ -691,7 +693,6 @@ void G_DefineDefaultControls(void)
 	gamecontroldefault[gcs_fps][GC_CENTERVIEW ][0] = KEY_LCTRL;
 	gamecontroldefault[gcs_fps][GC_JUMP       ][0] = KEY_SPACE;
 	gamecontroldefault[gcs_fps][GC_SPIN       ][0] = KEY_LSHIFT;
-	gamecontroldefault[gcs_fps][GC_SHIELD     ][0] = KEY_LALT;
 	gamecontroldefault[gcs_fps][GC_FIRE       ][0] = KEY_RCTRL;
 	gamecontroldefault[gcs_fps][GC_FIRE       ][1] = KEY_MOUSE1+0;
 	gamecontroldefault[gcs_fps][GC_FIRENORMAL ][0] = KEY_RALT;
@@ -712,7 +713,6 @@ void G_DefineDefaultControls(void)
 	gamecontroldefault[gcs_platform][GC_CENTERVIEW ][0] = KEY_END;
 	gamecontroldefault[gcs_platform][GC_JUMP       ][0] = KEY_SPACE;
 	gamecontroldefault[gcs_platform][GC_SPIN       ][0] = KEY_LSHIFT;
-	gamecontroldefault[gcs_platform][GC_SHIELD     ][0] = KEY_LALT;
 	gamecontroldefault[gcs_platform][GC_FIRE       ][0] = 's';
 	gamecontroldefault[gcs_platform][GC_FIRE       ][1] = KEY_MOUSE1+0;
 	gamecontroldefault[gcs_platform][GC_FIRENORMAL ][0] = 'w';
@@ -728,6 +728,9 @@ void G_DefineDefaultControls(void)
 		gamecontroldefault[i][GC_WEPSLOT5     ][0] = '5';
 		gamecontroldefault[i][GC_WEPSLOT6     ][0] = '6';
 		gamecontroldefault[i][GC_WEPSLOT7     ][0] = '7';
+		gamecontroldefault[i][GC_WEPSLOT8     ][0] = '8';
+		gamecontroldefault[i][GC_WEPSLOT9     ][0] = '9';
+		gamecontroldefault[i][GC_WEPSLOT10    ][0] = '0';
 		gamecontroldefault[i][GC_TOSSFLAG     ][0] = '\'';
 		gamecontroldefault[i][GC_CAMTOGGLE    ][0] = 'v';
 		gamecontroldefault[i][GC_CAMRESET     ][0] = 'r';
@@ -743,34 +746,34 @@ void G_DefineDefaultControls(void)
 		// Gamepad controls -- same for both schemes
 		gamecontroldefault[i][GC_JUMP         ][1] = KEY_JOY1+0; // A
 		gamecontroldefault[i][GC_SPIN         ][1] = KEY_JOY1+2; // X
-		gamecontroldefault[i][GC_SHIELD       ][1] = KEY_JOY1+1; // B
-		gamecontroldefault[i][GC_CUSTOM1      ][1] = KEY_JOY1+3; // Y
-		gamecontroldefault[i][GC_CUSTOM2      ][1] = KEY_JOY1+4; // LB
-		gamecontroldefault[i][GC_CENTERVIEW   ][1] = KEY_JOY1+5; // RB
+		gamecontroldefault[i][GC_CUSTOM1      ][1] = KEY_JOY1+1; // B
+		gamecontroldefault[i][GC_CUSTOM2      ][1] = KEY_JOY1+3; // Y
 		gamecontroldefault[i][GC_CUSTOM3      ][1] = KEY_JOY1+8; // Left Stick
-		gamecontroldefault[i][GC_CAMTOGGLE    ][1] = KEY_JOY1+9; // Right Stick
-		gamecontroldefault[i][GC_SCORES       ][1] = KEY_JOY1+6; // Back
+		gamecontroldefault[i][GC_CAMTOGGLE    ][1] = KEY_JOY1+4; // LB
+		gamecontroldefault[i][GC_CENTERVIEW   ][1] = KEY_JOY1+5; // RB
+		gamecontroldefault[i][GC_SCREENSHOT   ][1] = KEY_JOY1+6; // Back
 		gamecontroldefault[i][GC_SYSTEMMENU   ][0] = KEY_JOY1+7; // Start
-		gamecontroldefault[i][GC_VIEWPOINTNEXT][1] = KEY_HAT1+0; // D-Pad Up
-		gamecontroldefault[i][GC_TOSSFLAG     ][1] = KEY_HAT1+1; // D-Pad Down
 		gamecontroldefault[i][GC_WEAPONPREV   ][1] = KEY_HAT1+2; // D-Pad Left
 		gamecontroldefault[i][GC_WEAPONNEXT   ][1] = KEY_HAT1+3; // D-Pad Right
+		gamecontroldefault[i][GC_VIEWPOINTNEXT][1] = KEY_JOY1+9; // Right Stick
+		gamecontroldefault[i][GC_TOSSFLAG     ][1] = KEY_HAT1+0; // D-Pad Up
+		gamecontroldefault[i][GC_SCORES       ][1] = KEY_HAT1+1; // D-Pad Down
 
 		// Second player controls only have joypad defaults
 		gamecontrolbisdefault[i][GC_JUMP         ][1] = KEY_2JOY1+0; // A
 		gamecontrolbisdefault[i][GC_SPIN         ][1] = KEY_2JOY1+2; // X
-		gamecontrolbisdefault[i][GC_SHIELD       ][1] = KEY_2JOY1+1; // B
-		gamecontrolbisdefault[i][GC_CUSTOM1      ][1] = KEY_2JOY1+3; // Y
-		gamecontrolbisdefault[i][GC_CUSTOM2      ][1] = KEY_2JOY1+4; // LB
-		gamecontrolbisdefault[i][GC_CENTERVIEW   ][1] = KEY_2JOY1+5; // RB
+		gamecontrolbisdefault[i][GC_CUSTOM1      ][1] = KEY_2JOY1+1; // B
+		gamecontrolbisdefault[i][GC_CUSTOM2      ][1] = KEY_2JOY1+3; // Y
 		gamecontrolbisdefault[i][GC_CUSTOM3      ][1] = KEY_2JOY1+8; // Left Stick
-		gamecontrolbisdefault[i][GC_CAMTOGGLE    ][1] = KEY_2JOY1+9; // Right Stick
-		//gamecontrolbisdefault[i][GC_SCORES       ][1] = KEY_2JOY1+6; // Back
+		gamecontrolbisdefault[i][GC_CAMTOGGLE    ][1] = KEY_2JOY1+4; // LB
+		gamecontrolbisdefault[i][GC_CENTERVIEW   ][1] = KEY_2JOY1+5; // RB
+		gamecontrolbisdefault[i][GC_SCREENSHOT   ][1] = KEY_2JOY1+6; // Back
 		//gamecontrolbisdefault[i][GC_SYSTEMMENU   ][0] = KEY_2JOY1+7; // Start
-		gamecontrolbisdefault[i][GC_VIEWPOINTNEXT][1] = KEY_2HAT1+0; // D-Pad Up
-		gamecontrolbisdefault[i][GC_TOSSFLAG     ][1] = KEY_2HAT1+1; // D-Pad Down
 		gamecontrolbisdefault[i][GC_WEAPONPREV   ][1] = KEY_2HAT1+2; // D-Pad Left
 		gamecontrolbisdefault[i][GC_WEAPONNEXT   ][1] = KEY_2HAT1+3; // D-Pad Right
+		gamecontrolbisdefault[i][GC_VIEWPOINTNEXT][1] = KEY_2JOY1+9; // Right Stick
+		gamecontrolbisdefault[i][GC_TOSSFLAG     ][1] = KEY_2HAT1+0; // D-Pad Up
+		//gamecontrolbisdefault[i][GC_SCORES       ][1] = KEY_2HAT1+1; // D-Pad Down
 	}
 }
 
diff --git a/src/g_input.h b/src/g_input.h
index 48c103076667df50884767685386b98bd4865a02..e9c909e6e2c222360086874ddb27e2495b4e0381 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -74,7 +74,9 @@ typedef enum
 	GC_WEPSLOT5,
 	GC_WEPSLOT6,
 	GC_WEPSLOT7,
-	GC_SHIELD,
+	GC_WEPSLOT8,
+	GC_WEPSLOT9,
+	GC_WEPSLOT10,
 	GC_FIRE,
 	GC_FIRENORMAL,
 	GC_TOSSFLAG,
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
index b9ab2592d3f00a4fcdb4c1af31e2eaae484cd584..5d1af10e2a35e073472e9e4d6cbd90e31bdaec40 100644
--- a/src/hardware/hw_batching.c
+++ b/src/hardware/hw_batching.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Sonic Team Junior.
+// Copyright (C) 2020-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -114,7 +114,7 @@ void HWR_ProcessPolygon(FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPt
 		polygonArray[polygonArraySize].numVerts = iNumPts;
 		polygonArray[polygonArraySize].polyFlags = PolyFlags;
 		polygonArray[polygonArraySize].texture = current_texture;
-		polygonArray[polygonArraySize].shader = (shader_target != -1) ? HWR_GetShaderFromTarget(shader_target) : shader_target;
+		polygonArray[polygonArraySize].shader = (shader_target != SHADER_NONE) ? HWR_GetShaderFromTarget(shader_target) : shader_target;
 		polygonArray[polygonArraySize].horizonSpecial = horizonSpecial;
 		// default to polygonArraySize so we don't lose order on horizon lines
 		// (yes, it's supposed to be negative, since we're sorting in that direction)
@@ -311,7 +311,6 @@ void HWR_RenderBatches(void)
 			int nextIndex = polygonIndexArray[polygonReadPos];
 			if (polygonArray[index].hash != polygonArray[nextIndex].hash)
 			{
-				changeState = true;
 				nextShader = polygonArray[nextIndex].shader;
 				nextTexture = polygonArray[nextIndex].texture;
 				nextPolyFlags = polygonArray[nextIndex].polyFlags;
@@ -320,14 +319,17 @@ void HWR_RenderBatches(void)
 					nextTexture = 0;
 				if (currentShader != nextShader && cv_glshaders.value && gl_shadersavailable)
 				{
+					changeState = true;
 					changeShader = true;
 				}
 				if (currentTexture != nextTexture)
 				{
+					changeState = true;
 					changeTexture = true;
 				}
 				if (currentPolyFlags != nextPolyFlags)
 				{
+					changeState = true;
 					changePolyFlags = true;
 				}
 				if (cv_glshaders.value && gl_shadersavailable)
@@ -339,6 +341,7 @@ void HWR_RenderBatches(void)
 						currentSurfaceInfo.LightInfo.fade_start != nextSurfaceInfo.LightInfo.fade_start ||
 						currentSurfaceInfo.LightInfo.fade_end != nextSurfaceInfo.LightInfo.fade_end)
 					{
+						changeState = true;
 						changeSurfaceInfo = true;
 					}
 				}
@@ -346,6 +349,7 @@ void HWR_RenderBatches(void)
 				{
 					if (currentSurfaceInfo.PolyColor.rgba != nextSurfaceInfo.PolyColor.rgba)
 					{
+						changeState = true;
 						changeSurfaceInfo = true;
 					}
 				}
diff --git a/src/hardware/hw_batching.h b/src/hardware/hw_batching.h
index df1a4c38be5ceeea84fb61fa22ad31b8a8063dea..4214ca410c071994bb1e16576e5f4d6c4ebfb311 100644
--- a/src/hardware/hw_batching.h
+++ b/src/hardware/hw_batching.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Sonic Team Junior.
+// Copyright (C) 2020-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hardware/hw_cache.c b/src/hardware/hw_cache.c
index 56f5416cf13f8d4c3176d93b122094d4bfd0bacb..b71bf7007e03ab2cdb7d229d84aa5a57deb8cc8c 100644
--- a/src/hardware/hw_cache.c
+++ b/src/hardware/hw_cache.c
@@ -450,15 +450,10 @@ static void HWR_GenerateTexture(INT32 texnum, GLMapTexture_t *grtex, GLMipmap_t
 
 	texture = textures[texnum];
 
-	mipmap->flags = TF_WRAPXY;
-	mipmap->width = (UINT16)texture->width;
-	mipmap->height = (UINT16)texture->height;
-	mipmap->format = textureformat;
-
 	blockwidth = texture->width;
 	blockheight = texture->height;
-	blocksize = (blockwidth * blockheight);
-	block = MakeBlock(&grtex->mipmap);
+	blocksize = blockwidth * blockheight;
+	block = MakeBlock(mipmap);
 
 	// Composite the columns together.
 	for (i = 0, patch = texture->patches; i < texture->patchcount; i++, patch++)
@@ -488,7 +483,7 @@ static void HWR_GenerateTexture(INT32 texnum, GLMapTexture_t *grtex, GLMipmap_t
 				realpatch = W_CachePatchNumPwad(wadnum, lumpnum, PU_PATCH);
 		}
 
-		HWR_DrawTexturePatchInCache(&grtex->mipmap, blockwidth, blockheight, texture, patch, realpatch);
+		HWR_DrawTexturePatchInCache(mipmap, blockwidth, blockheight, texture, patch, realpatch);
 
 		if (free_patch)
 			Patch_Free(realpatch);
@@ -680,25 +675,24 @@ void HWR_InitMapTextures(void)
 	gl_maptexturesloaded = false;
 }
 
-static void DeleteTextureMipmap(GLMipmap_t *grMipmap)
+static void DeleteTextureMipmap(GLMipmap_t *grMipmap, boolean delete_mipmap)
 {
 	HWD.pfnDeleteTexture(grMipmap);
 
-	// Chroma-keyed textures do not own their texture data, so do not free it
-	if (!(grMipmap->flags & TF_CHROMAKEYED))
+	if (delete_mipmap)
 		Z_Free(grMipmap->data);
 }
 
-static void FreeMapTexture(GLMapTexture_t *tex)
+static void FreeMapTexture(GLMapTexture_t *tex, boolean delete_chromakeys)
 {
 	if (tex->mipmap.nextcolormap)
 	{
-		DeleteTextureMipmap(tex->mipmap.nextcolormap);
+		DeleteTextureMipmap(tex->mipmap.nextcolormap, delete_chromakeys);
 		free(tex->mipmap.nextcolormap);
 		tex->mipmap.nextcolormap = NULL;
 	}
 
-	DeleteTextureMipmap(&tex->mipmap);
+	DeleteTextureMipmap(&tex->mipmap, true);
 }
 
 void HWR_FreeMapTextures(void)
@@ -707,8 +701,8 @@ void HWR_FreeMapTextures(void)
 
 	for (i = 0; i < gl_numtextures; i++)
 	{
-		FreeMapTexture(&gl_textures[i]);
-		FreeMapTexture(&gl_flats[i]);
+		FreeMapTexture(&gl_textures[i], true);
+		FreeMapTexture(&gl_flats[i], false);
 	}
 
 	// now the heap don't have any 'user' pointing to our
@@ -741,22 +735,7 @@ void HWR_LoadMapTextures(size_t pnumtextures)
 // --------------------------------------------------------------------------
 // Make sure texture is downloaded and set it as the source
 // --------------------------------------------------------------------------
-static void GetMapTexture(INT32 tex, GLMapTexture_t *grtex, GLMipmap_t *mipmap)
-{
-	// Generate texture if missing from the cache
-	if (!mipmap->data && !mipmap->downloaded)
-		HWR_GenerateTexture(tex, grtex, mipmap);
-
-	// If hardware does not have the texture, then call pfnSetTexture to upload it
-	if (!mipmap->downloaded)
-		HWD.pfnSetTexture(mipmap);
-	HWR_SetCurrentTexture(mipmap);
-
-	// The system-memory data can be purged now.
-	Z_ChangeTag(mipmap->data, PU_HWRCACHE_UNLOCKED);
-}
-
-GLMapTexture_t *HWR_GetTexture(INT32 tex)
+GLMapTexture_t *HWR_GetTexture(INT32 tex, boolean chromakeyed)
 {
 	if (tex < 0 || tex >= (signed)gl_numtextures)
 	{
@@ -769,7 +748,46 @@ GLMapTexture_t *HWR_GetTexture(INT32 tex)
 
 	GLMapTexture_t *grtex = &gl_textures[tex];
 
-	GetMapTexture(tex, grtex, &grtex->mipmap);
+	GLMipmap_t *grMipmap = &grtex->mipmap;
+	GLMipmap_t *originalMipmap = grMipmap;
+
+	if (!originalMipmap->downloaded)
+	{
+		originalMipmap->flags = TF_WRAPXY;
+		originalMipmap->width = (UINT16)textures[tex]->width;
+		originalMipmap->height = (UINT16)textures[tex]->height;
+		originalMipmap->format = textureformat;
+	}
+
+	// If chroma-keyed, create or use a different mipmap for the variant
+	if (chromakeyed && !textures[tex]->transparency)
+	{
+		// Allocate it if it wasn't already
+		if (!originalMipmap->nextcolormap)
+		{
+			GLMipmap_t *newMipmap = calloc(1, sizeof (*grMipmap));
+			if (newMipmap == NULL)
+				I_Error("%s: Out of memory", "HWR_GetTexture");
+
+			newMipmap->flags = originalMipmap->flags | TF_CHROMAKEYED;
+			newMipmap->width = originalMipmap->width;
+			newMipmap->height = originalMipmap->height;
+			newMipmap->format = originalMipmap->format;
+			originalMipmap->nextcolormap = newMipmap;
+		}
+
+		// Generate, upload and bind the variant texture instead of the original one
+		grMipmap = originalMipmap->nextcolormap;
+	}
+
+	if (!grMipmap->data)
+		HWR_GenerateTexture(tex, grtex, grMipmap);
+
+	if (!grMipmap->downloaded)
+		HWD.pfnSetTexture(grMipmap);
+	HWR_SetCurrentTexture(grMipmap);
+
+	Z_ChangeTag(grMipmap->data, PU_HWRCACHE_UNLOCKED);
 
 	return grtex;
 }
@@ -1240,20 +1258,29 @@ void HWR_SetMapPalette(void)
 
 // Creates a hardware lighttable from the supplied lighttable.
 // Returns the id of the hw lighttable, usable in FSurfaceInfo.
-UINT32 HWR_CreateLightTable(UINT8 *lighttable)
+UINT32 HWR_CreateLightTable(UINT8 *lighttable, RGBA_t *hw_lighttable)
 {
-	UINT32 i, id;
+	UINT32 i;
 	RGBA_t *palette = HWR_GetTexturePalette();
-	RGBA_t *hw_lighttable = Z_Malloc(256 * 32 * sizeof(RGBA_t), PU_STATIC, NULL);
 
 	// To make the palette index -> RGBA mapping easier for the shader,
 	// the hardware lighttable is composed of RGBA colors instead of palette indices.
 	for (i = 0; i < 256 * 32; i++)
 		hw_lighttable[i] = palette[lighttable[i]];
 
-	id = HWD.pfnCreateLightTable(hw_lighttable);
-	Z_Free(hw_lighttable);
-	return id;
+	return HWD.pfnCreateLightTable(hw_lighttable);
+}
+
+// Updates a hardware lighttable of a given id from the supplied lighttable.
+void HWR_UpdateLightTable(UINT32 id, UINT8 *lighttable, RGBA_t *hw_lighttable)
+{
+	UINT32 i;
+	RGBA_t *palette = HWR_GetTexturePalette();
+
+	for (i = 0; i < 256 * 32; i++)
+		hw_lighttable[i] = palette[lighttable[i]];
+
+	HWD.pfnUpdateLightTable(id, hw_lighttable);
 }
 
 // get hwr lighttable id for colormap, create it if it doesn't already exist
@@ -1267,25 +1294,41 @@ UINT32 HWR_GetLightTableID(extracolormap_t *colormap)
 		default_colormap = true;
 	}
 
+	UINT8 *colormap_pointer;
+
+	if (default_colormap)
+		colormap_pointer = colormaps; // don't actually use the data from the "default colormap"
+	else
+		colormap_pointer = colormap->colormap;
+
 	// create hw lighttable if there isn't one
-	if (!colormap->gl_lighttable_id)
+	if (colormap->gl_lighttable.data == NULL)
 	{
-		UINT8 *colormap_pointer;
+		Z_Malloc(256 * 32 * sizeof(RGBA_t), PU_HWRLIGHTTABLEDATA, &colormap->gl_lighttable.data);
+	}
 
-		if (default_colormap)
-			colormap_pointer = colormaps; // don't actually use the data from the "default colormap"
-		else
-			colormap_pointer = colormap->colormap;
-		colormap->gl_lighttable_id = HWR_CreateLightTable(colormap_pointer);
+	// Generate the texture for this light table
+	if (!colormap->gl_lighttable.id)
+	{
+		colormap->gl_lighttable.id = HWR_CreateLightTable(colormap_pointer, colormap->gl_lighttable.data);
+	}
+	// Update the texture if it was directly changed by a script
+	else if (colormap->gl_lighttable.needs_update)
+	{
+		HWR_UpdateLightTable(colormap->gl_lighttable.id, colormap_pointer, colormap->gl_lighttable.data);
 	}
 
-	return colormap->gl_lighttable_id;
+	colormap->gl_lighttable.needs_update = false;
+
+	return colormap->gl_lighttable.id;
 }
 
 // Note: all hardware lighttable ids assigned before this
 // call become invalid and must not be used.
 void HWR_ClearLightTables(void)
 {
+	Z_FreeTag(PU_HWRLIGHTTABLEDATA);
+
 	if (vid.glstate == VID_GL_LIBRARY_LOADED)
 		HWD.pfnClearLightTables();
 }
diff --git a/src/hardware/hw_defs.h b/src/hardware/hw_defs.h
index 7d06ceb60b86d8ef3fff3d30a21ce29213d6c600..77d7133b67112ec6498ebcf79d3c6432952d38c3 100644
--- a/src/hardware/hw_defs.h
+++ b/src/hardware/hw_defs.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index ddce7d9885d7607e1cabf9bfc5c74ecf6258612b..152501f60e1dcb0ee04eee6e100fdfbe3878eb10 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -256,8 +256,8 @@ void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t p
 	}
 
 	// positions of the cx, cy, are between 0 and vid.width/vid.height now, we need them to be between -1 and 1
-	cx = -1 + (cx / (vid.width/2));
-	cy = 1 - (cy / (vid.height/2));
+	cx = -1.0f + (cx / (vid.width / 2.0f));
+	cy = 1.0f - (cy / (vid.height / 2.0f));
 
 	// fwidth and fheight are similar
 	fwidth /= vid.width / 2;
@@ -598,43 +598,76 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 // --------------------------------------------------------------------------
 // Fills a box of pixels using a flat texture as a pattern
+// Fixed to properly align like the other draw functions -luigi budd
 // --------------------------------------------------------------------------
-void HWR_DrawFlatFill (INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatlumpnum)
+void HWR_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatlumpnum)
 {
 	FOutVector v[4];
 	const size_t len = W_LumpLength(flatlumpnum);
-	UINT16 flatflag = R_GetFlatSize(len) - 1;
-	double dflatsize = (double)(flatflag + 1);
+	UINT16 flatflag = R_GetFlatSize(len);
+	double dflatsize = (double)(flatflag);
+
+    // compilers are COOL!
+	float dup = (float)vid.dup;
+	float fx = (float)x * dup;
+	float fy = (float)y * dup;
+	float fw = (float)w * dup;
+	float fh = (float)h * dup;
+
+    /*
+	fx *= dup;
+	fy *= dup;
+	fw *= dup;
+	fh *= dup;
+    */
+
+	if (fw <= 0 || fh <= 0)
+		return;	
+	
+	if (fabsf((float)vid.width - (float)BASEVIDWIDTH * vid.dup) > 1.0E-36f)
+	{
+		fx += ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup)) / 2;
+	}
+	if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * vid.dup) > 1.0E-36f)
+	{
+		fy += ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup)) / 2;
+	}
 
+	if (fx >= vid.width || fy >= vid.height)
+	    return;
+	  
+	fx = -1.0f + (fx / (vid.width / 2.0f));
+	fy = 1.0f - (fy / (vid.height / 2.0f));
+	fw /= vid.width / 2;
+	fh /= vid.height / 2;
+	
 //  3--2
 //  | /|
 //  |/ |
 //  0--1
+	
+    // position vertices
+	v[0].x = v[3].x = fx;
+	v[2].x = v[1].x = fx + fw;
 
-	v[0].x = v[3].x = (x - 160.0f)/160.0f;
-	v[2].x = v[1].x = ((x+w) - 160.0f)/160.0f;
-	v[0].y = v[1].y = -(y - 100.0f)/100.0f;
-	v[2].y = v[3].y = -((y+h) - 100.0f)/100.0f;
+	v[0].y = v[1].y = fy;
+	v[2].y = v[3].y = fy - fh;
 
 	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
 
-	v[0].s = v[3].s = (float)((x & flatflag)/dflatsize);
-	v[2].s = v[1].s = (float)(v[0].s + w/dflatsize);
-	v[0].t = v[1].t = (float)((y & flatflag)/dflatsize);
-	v[2].t = v[3].t = (float)(v[0].t + h/dflatsize);
+    // sides
+	v[0].s = v[3].s = (float)(flatflag/dflatsize) * 2;
+	v[2].s = v[1].s = (float)(v[0].s + w/dflatsize) * 2;
 
-	HWR_GetRawFlat(flatlumpnum);
+    // top/bottom
+	v[0].t = v[1].t = (float)(flatflag/dflatsize) * 2;
+	v[2].t = v[3].t = (float)(v[0].t + h/dflatsize) * 2;
 
-	//Hurdler: Boris, the same comment as above... but maybe for pics
-	// it not a problem since they don't have any transparent pixel
-	// if I'm right !?
-	// BTW, I see we put 0 for PFs, and If I'm right, that
-	// means we take the previous PFs as default
-	// how can we be sure they are ok?
+    // needed to texture the poly
+	HWR_GetRawFlat(flatlumpnum);
 	HWD.pfnDrawPolygon(NULL, v, 4, PF_NoDepthTest); //PF_Translucent);
 }
 
-
 // --------------------------------------------------------------------------
 // Fade down the screen so that the menu drawn on top of it looks brighter
 // --------------------------------------------------------------------------
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index 45ee2933a69bca328be875ec2e449ca7f74cab39..a5fdc00a47c17e6b5cbeba5d074554a66672cde7 100644
--- a/src/hardware/hw_drv.h
+++ b/src/hardware/hw_drv.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -71,6 +71,7 @@ EXPORT void HWRAPI(SetShaderInfo) (hwdshaderinfo_t info, INT32 value);
 
 EXPORT void HWRAPI(SetPaletteLookup)(UINT8 *lut);
 EXPORT UINT32 HWRAPI(CreateLightTable)(RGBA_t *hw_lighttable);
+EXPORT void HWRAPI(UpdateLightTable)(UINT32 id, RGBA_t *hw_lighttable);
 EXPORT void HWRAPI(ClearLightTables)(void);
 EXPORT void HWRAPI(SetScreenPalette)(RGBA_t *palette);
 
@@ -125,6 +126,7 @@ struct hwdriver_s
 
 	SetPaletteLookup    pfnSetPaletteLookup;
 	CreateLightTable    pfnCreateLightTable;
+	UpdateLightTable    pfnUpdateLightTable;
 	ClearLightTables    pfnClearLightTables;
 	SetScreenPalette    pfnSetScreenPalette;
 };
diff --git a/src/hardware/hw_glob.h b/src/hardware/hw_glob.h
index 807c7098946b5589ce7d3394f864d6dccb484194..bf9f7da3b4c126154fed2faf81761f348fbeed63 100644
--- a/src/hardware/hw_glob.h
+++ b/src/hardware/hw_glob.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -120,7 +120,7 @@ void HWR_GetPatch(patch_t *patch);
 void HWR_GetMappedPatch(patch_t *patch, const UINT8 *colormap);
 void HWR_GetFadeMask(lumpnum_t fademasklumpnum);
 
-GLMapTexture_t *HWR_GetTexture(INT32 tex);
+GLMapTexture_t *HWR_GetTexture(INT32 tex, boolean chromakeyed);
 void HWR_GetLevelFlat(levelflat_t *levelflat, boolean chromakeyed);
 void HWR_GetRawFlat(lumpnum_t flatlumpnum);
 
@@ -133,7 +133,8 @@ void HWR_UnlockCachedPatch(GLPatch_t *gpatch);
 
 void HWR_SetPalette(RGBA_t *palette);
 void HWR_SetMapPalette(void);
-UINT32 HWR_CreateLightTable(UINT8 *lighttable);
+UINT32 HWR_CreateLightTable(UINT8 *lighttable, RGBA_t *hw_lighttable);
+void HWR_UpdateLightTable(UINT32 id, UINT8 *lighttable, RGBA_t *hw_lighttable);
 UINT32 HWR_GetLightTableID(extracolormap_t *colormap);
 void HWR_ClearLightTables(void);
 
diff --git a/src/hardware/hw_light.c b/src/hardware/hw_light.c
index e7769edbd8da2dbaa38debfabae9a34f25d121ee..9011ff05ec855c45291c95946adf110365c2839b 100644
--- a/src/hardware/hw_light.c
+++ b/src/hardware/hw_light.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -395,6 +395,8 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[NOLIGHT],     // SPR_XMS4
 	&lspr[NOLIGHT],     // SPR_XMS5
 	&lspr[NOLIGHT],     // SPR_XMS6
+	&lspr[NOLIGHT],     // SPR_SNTT
+	&lspr[NOLIGHT],     // SPR_SSTT
 	&lspr[NOLIGHT],     // SPR_FHZI
 	&lspr[NOLIGHT],     // SPR_ROSY
 
@@ -428,6 +430,8 @@ light_t *t_lspr[NUMSPRITES] =
 	// Misc Scenery
 	&lspr[NOLIGHT],     // SPR_STLG
 	&lspr[NOLIGHT],     // SPR_DBAL
+	&lspr[NOLIGHT],     // SPR_GINE
+	&lspr[NOLIGHT],     // SPR_PPAL
 
 	// Powerup Indicators
 	&lspr[NOLIGHT],     // SPR_ARMA
@@ -614,6 +618,9 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[NOLIGHT],     // SPR_GWLG
 	&lspr[NOLIGHT],     // SPR_GWLR
 
+	// LJ Knuckles
+	&lspr[NOLIGHT],		// SPR_OLDK,
+
 	// Free slots
 	&lspr[NOLIGHT],
 	&lspr[NOLIGHT],
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index f533082f76e6db8af21e90c6dcf4f98949f7f532..d84322dfda647d5ae34bbd09d8cd715ff906fbc5 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -309,6 +309,43 @@ static FUINT HWR_CalcSlopeLight(FUINT lightnum, angle_t dir, fixed_t delta)
 	return (FUINT)finallight;
 }
 
+static UINT8 HWR_SideLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light +
+		((side->lightabsolute) ? 0 : base_lightlevel)));
+}
+
+/* TODO: implement per-texture lighting
+static UINT8 HWR_TopLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light_top +
+		((side->lightabsolute_top) ? 0 : HWR_SideLightLevel(side, base_lightlevel))));
+}
+
+static UINT8 HWR_MidLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light_mid +
+		((side->lightabsolute_mid) ? 0 : HWR_SideLightLevel(side, base_lightlevel))));
+}
+
+static UINT8 HWR_BottomLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light_bottom +
+		((side->lightabsolute_bottom) ? 0 : HWR_SideLightLevel(side, base_lightlevel))));
+}
+*/
+
+static UINT8 HWR_FloorLightLevel(sector_t *sector, INT16 base_lightlevel)
+{
+	return max(0, min(255, sector->floorlightlevel +
+		((sector->floorlightabsolute) ? 0 : base_lightlevel)));
+}
+
+static UINT8 HWR_CeilingLightLevel(sector_t *sector, INT16 base_lightlevel)
+{
+	return max(0, min(255, sector->ceilinglightlevel +
+		((sector->ceilinglightabsolute) ? 0 : base_lightlevel)));
+}
 // ==========================================================================
 //                                   FLOOR/CEILING GENERATION FROM SUBSECTORS
 // ==========================================================================
@@ -705,8 +742,9 @@ static void HWR_SplitWall(sector_t *sector, FOutVector *wallVerts, INT32 texnum,
 	fixed_t v2x = FloatToFixed(wallVerts[1].x);
 	fixed_t v2y = FloatToFixed(wallVerts[1].z);
 
+	FUINT lightnum = HWR_SideLightLevel(gl_sidedef, sector->lightlevel);
 	const UINT8 alpha = Surf->PolyColor.s.alpha;
-	FUINT lightnum = HWR_CalcWallLight(sector->lightlevel, v1x, v1y, v2x, v2y);
+	lightnum = HWR_CalcWallLight(lightnum, v1x, v1y, v2x, v2y);
 	extracolormap_t *colormap = NULL;
 
 	if (!r_renderwalls)
@@ -750,13 +788,13 @@ static void HWR_SplitWall(sector_t *sector, FOutVector *wallVerts, INT32 texnum,
 		{
 			if (pfloor && (pfloor->fofflags & FOF_FOG))
 			{
-				lightnum = pfloor->master->frontsector->lightlevel;
+				lightnum = HWR_SideLightLevel(gl_sidedef, pfloor->master->frontsector->lightlevel);
 				colormap = pfloor->master->frontsector->extra_colormap;
 				lightnum = colormap ? lightnum : HWR_CalcWallLight(lightnum, v1x, v1y, v2x, v2y);
 			}
 			else
 			{
-				lightnum = *list[i].lightlevel;
+				lightnum = HWR_SideLightLevel(gl_sidedef, *list[i].lightlevel);
 				colormap = *list[i].extra_colormap;
 				lightnum = colormap ? lightnum : HWR_CalcWallLight(lightnum, v1x, v1y, v2x, v2y);
 			}
@@ -916,6 +954,7 @@ static boolean HWR_BlendMidtextureSurface(FSurfaceInfo *pSurf)
 
 static void HWR_RenderMidtexture(INT32 gl_midtexture, float cliplow, float cliphigh, fixed_t worldtop, fixed_t worldbottom, fixed_t worldhigh, fixed_t worldlow, fixed_t worldtopslope, fixed_t worldbottomslope, fixed_t worldhighslope, fixed_t worldlowslope, UINT32 lightnum, FOutVector *inWallVerts)
 {
+	sector_t *front, *back;
 	FOutVector wallVerts[4];
 
 	FSurfaceInfo Surf;
@@ -925,6 +964,16 @@ static void HWR_RenderMidtexture(INT32 gl_midtexture, float cliplow, float cliph
 	if (!HWR_BlendMidtextureSurface(&Surf))
 		return;
 
+	if (gl_linedef->frontsector->heightsec != -1)
+		front = &sectors[gl_linedef->frontsector->heightsec];
+	else
+		front = gl_linedef->frontsector;
+
+	if (gl_linedef->backsector->heightsec != -1)
+		back = &sectors[gl_linedef->backsector->heightsec];
+	else
+		back = gl_linedef->backsector;
+
 	fixed_t texheight = FixedDiv(textureheight[gl_midtexture], abs(gl_sidedef->scaley_mid));
 	INT32 repeats;
 
@@ -934,15 +983,15 @@ static void HWR_RenderMidtexture(INT32 gl_midtexture, float cliplow, float cliph
 	{
 		fixed_t high, low;
 
-		if (gl_frontsector->ceilingheight > gl_backsector->ceilingheight)
-			high = gl_backsector->ceilingheight;
+		if (front->ceilingheight > back->ceilingheight)
+			high = back->ceilingheight;
 		else
-			high = gl_frontsector->ceilingheight;
+			high = front->ceilingheight;
 
-		if (gl_frontsector->floorheight > gl_backsector->floorheight)
-			low = gl_frontsector->floorheight;
+		if (front->floorheight > back->floorheight)
+			low = front->floorheight;
 		else
-			low = gl_backsector->floorheight;
+			low = back->floorheight;
 
 		repeats = (high - low) / texheight;
 		if ((high - low) % texheight)
@@ -951,7 +1000,7 @@ static void HWR_RenderMidtexture(INT32 gl_midtexture, float cliplow, float cliph
 	else
 		repeats = 1;
 
-	GLMapTexture_t *grTex = HWR_GetTexture(gl_midtexture);
+	GLMapTexture_t *grTex = HWR_GetTexture(gl_midtexture, true);
 	float xscale = FixedToFloat(gl_sidedef->scalex_mid);
 	float yscale = FixedToFloat(gl_sidedef->scaley_mid);
 
@@ -969,8 +1018,8 @@ static void HWR_RenderMidtexture(INT32 gl_midtexture, float cliplow, float cliph
 	if (gl_curline->polyseg)
 	{
 		// Change this when polyobjects support slopes
-		popentop = popentopslope = gl_curline->backsector->ceilingheight;
-		popenbottom = popenbottomslope = gl_curline->backsector->floorheight;
+		popentop = popentopslope = back->ceilingheight;
+		popenbottom = popenbottomslope = back->floorheight;
 	}
 	else
 	{
@@ -1167,7 +1216,7 @@ static void HWR_ProcessSeg(void)
 	float cliplow = (float)gl_curline->offset;
 	float cliphigh = cliplow + (gl_curline->flength * FRACUNIT);
 
-	FUINT lightnum = gl_frontsector->lightlevel;
+	FUINT lightnum = HWR_SideLightLevel(gl_sidedef, gl_frontsector->lightlevel);
 	extracolormap_t *colormap = gl_frontsector->extra_colormap;
 	lightnum = colormap ? lightnum : HWR_CalcWallLight(lightnum, vs.x, vs.y, ve.x, ve.y);
 
@@ -1210,7 +1259,7 @@ static void HWR_ProcessSeg(void)
 		// check TOP TEXTURE
 		if ((worldhighslope < worldtopslope || worldhigh < worldtop) && gl_toptexture)
 		{
-			grTex = HWR_GetTexture(gl_toptexture);
+			grTex = HWR_GetTexture(gl_toptexture, false);
 			xscale = FixedToFloat(abs(gl_sidedef->scalex_top));
 			yscale = FixedToFloat(abs(gl_sidedef->scaley_top));
 
@@ -1300,7 +1349,7 @@ static void HWR_ProcessSeg(void)
 		// check BOTTOM TEXTURE
 		if ((worldlowslope > worldbottomslope || worldlow > worldbottom) && gl_bottomtexture)
 		{
-			grTex = HWR_GetTexture(gl_bottomtexture);
+			grTex = HWR_GetTexture(gl_bottomtexture, false);
 			xscale = FixedToFloat(abs(gl_sidedef->scalex_bottom));
 			yscale = FixedToFloat(abs(gl_sidedef->scaley_bottom));
 
@@ -1414,7 +1463,7 @@ static void HWR_ProcessSeg(void)
 		// Single sided line... Deal only with the middletexture (if one exists)
 		if (gl_midtexture && gl_linedef->special != SPECIAL_HORIZON_LINE) // (Ignore horizon line for OGL)
 		{
-			grTex = HWR_GetTexture(gl_midtexture);
+			grTex = HWR_GetTexture(gl_midtexture, false);
 			xscale = FixedToFloat(gl_sidedef->scalex_mid);
 			yscale = FixedToFloat(gl_sidedef->scaley_mid);
 
@@ -1533,14 +1582,14 @@ static void HWR_ProcessSeg(void)
 				if ((high1 < lowcut && highslope1 < lowcutslope) || (low1 > highcut && lowslope1 > highcutslope))
 					continue;
 
-				side_t *side = R_GetFFloorSide(gl_curline, rover);
+				side_t *side = R_GetFFloorSide(gl_curline->linedef, rover, gl_backsector);
 
 				boolean do_texture_skew;
 				boolean dont_peg_bottom;
 
 				if (rover->master->flags & ML_TFERLINE)
 				{
-					line_t *newline = R_GetFFloorLine(gl_curline, rover);
+					line_t *newline = R_GetFFloorLine(gl_curline->linedef, rover, gl_backsector);
 					do_texture_skew = newline->flags & ML_SKEWTD;
 					dont_peg_bottom = newline->flags & ML_DONTPEGBOTTOM;
 				}
@@ -1588,7 +1637,7 @@ static void HWR_ProcessSeg(void)
 					// -- Monster Iestyn 26/06/18
 					fixed_t texturevpeg = side->rowoffset + side->offsety_mid;
 
-					grTex = HWR_GetTexture(texnum);
+					grTex = HWR_GetTexture(texnum, true);
 					xscale = FixedToFloat(side->scalex_mid);
 					yscale = FixedToFloat(side->scaley_mid);
 
@@ -1628,11 +1677,11 @@ static void HWR_ProcessSeg(void)
 				{
 					blendmode = PF_Fog|PF_NoTexture;
 
-					lightnum = rover->master->frontsector->lightlevel;
+					lightnum = HWR_SideLightLevel(gl_sidedef, rover->master->frontsector->lightlevel);
 					colormap = rover->master->frontsector->extra_colormap;
 					lightnum = colormap ? lightnum : HWR_CalcWallLight(lightnum, vs.x, vs.y, ve.x, ve.y);
 
-					Surf.PolyColor.s.alpha = HWR_FogBlockAlpha(rover->master->frontsector->lightlevel, rover->master->frontsector->extra_colormap);
+					Surf.PolyColor.s.alpha = HWR_FogBlockAlpha(HWR_SideLightLevel(gl_sidedef, rover->master->frontsector->lightlevel), rover->master->frontsector->extra_colormap);
 
 					if (gl_frontsector->numlights)
 						HWR_SplitWall(gl_frontsector, wallVerts, 0, &Surf, rover->fofflags, rover, blendmode);
@@ -1643,7 +1692,7 @@ static void HWR_ProcessSeg(void)
 				{
 					blendmode = PF_Masked;
 
-					if ((rover->fofflags & FOF_TRANSLUCENT && !(rover->fofflags & FOF_SPLAT)) || rover->blend)
+					if ((rover->fofflags & FOF_TRANSLUCENT && !((rover->fofflags & FOF_SPLAT) && rover->alpha >= 255)) || rover->blend)
 					{
 						blendmode = rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent;
 						Surf.PolyColor.s.alpha = max(0, min(rover->alpha, 255));
@@ -1689,14 +1738,14 @@ static void HWR_ProcessSeg(void)
 				if ((high1 < lowcut && highslope1 < lowcutslope) || (low1 > highcut && lowslope1 > highcutslope))
 					continue;
 
-				side_t *side = R_GetFFloorSide(gl_curline, rover);
+				side_t *side = R_GetFFloorSide(gl_curline->linedef, rover, gl_backsector);
 
 				boolean do_texture_skew;
 				boolean dont_peg_bottom;
 
 				if (rover->master->flags & ML_TFERLINE)
 				{
-					line_t *newline = R_GetFFloorLine(gl_curline, rover);
+					line_t *newline = R_GetFFloorLine(gl_curline->linedef, rover, gl_backsector);
 					do_texture_skew = newline->flags & ML_SKEWTD;
 					dont_peg_bottom = newline->flags & ML_DONTPEGBOTTOM;
 				}
@@ -1745,7 +1794,7 @@ static void HWR_ProcessSeg(void)
 					// -- Monster Iestyn 26/06/18
 					fixed_t texturevpeg = side->rowoffset + side->offsety_mid;
 
-					grTex = HWR_GetTexture(texnum);
+					grTex = HWR_GetTexture(texnum, true);
 					xscale = FixedToFloat(side->scalex_mid);
 					yscale = FixedToFloat(side->scaley_mid);
 
@@ -1785,7 +1834,7 @@ static void HWR_ProcessSeg(void)
 				{
 					blendmode = PF_Fog|PF_NoTexture;
 
-					lightnum = rover->master->frontsector->lightlevel;
+					lightnum = HWR_SideLightLevel(gl_sidedef, rover->master->frontsector->lightlevel);
 					colormap = rover->master->frontsector->extra_colormap;
 					lightnum = colormap ? lightnum : HWR_CalcWallLight(lightnum, vs.x, vs.y, ve.x, ve.y);
 
@@ -1800,7 +1849,7 @@ static void HWR_ProcessSeg(void)
 				{
 					blendmode = PF_Masked;
 
-					if ((rover->fofflags & FOF_TRANSLUCENT && !(rover->fofflags & FOF_SPLAT)) || rover->blend)
+					if ((rover->fofflags & FOF_TRANSLUCENT && !((rover->fofflags & FOF_SPLAT) && rover->alpha >= 255)) || rover->blend)
 					{
 						blendmode = rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent;
 						Surf.PolyColor.s.alpha = max(0, min(rover->alpha, 255));
@@ -2447,14 +2496,14 @@ static void HWR_Subsector(size_t num)
 				continue;
 			if (sub->validcount == validcount)
 				continue;
-			
+
 			// rendering heights for bottom and top planes
 			bottomCullHeight = P_GetFFloorBottomZAt(rover, viewx, viewy);
 			topCullHeight = P_GetFFloorTopZAt(rover, viewx, viewy);
-			
+
 			if (gl_frontsector->cullheight)
 			{
-				if (HWR_DoCulling(gl_frontsector->cullheight, viewsector->cullheight, gl_viewz, FIXED_TO_FLOAT(bottomCullHeight), FIXED_TO_FLOAT(topCullHeight)))
+				if (HWR_DoCulling(gl_frontsector->cullheight, viewsector->cullheight, gl_viewz, FIXED_TO_FLOAT(*rover->bottomheight), FIXED_TO_FLOAT(*rover->topheight)))
 					continue;
 			}
 
@@ -2471,17 +2520,17 @@ static void HWR_Subsector(size_t num)
 					UINT8 alpha;
 
 					light = R_GetPlaneLight(gl_frontsector, centerHeight, viewz < bottomCullHeight ? true : false);
-					alpha = HWR_FogBlockAlpha(*gl_frontsector->lightlist[light].lightlevel, rover->master->frontsector->extra_colormap);
+					alpha = HWR_FogBlockAlpha(HWR_FloorLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel), rover->master->frontsector->extra_colormap);
 
 					HWR_AddTransparentFloor(0,
 					                       &extrasubsectors[num],
 										   false,
 					                       *rover->bottomheight,
-					                       *gl_frontsector->lightlist[light].lightlevel,
+					                       HWR_FloorLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel),
 					                       alpha, rover->master->frontsector, PF_Fog|PF_NoTexture,
 										   true, false, rover->master->frontsector->extra_colormap);
 				}
-				else if ((rover->fofflags & FOF_TRANSLUCENT && !(rover->fofflags & FOF_SPLAT)) || rover->blend) // SoM: Flags are more efficient
+				else if ((rover->fofflags & FOF_TRANSLUCENT && !((rover->fofflags & FOF_SPLAT) && rover->alpha >= 255)) || rover->blend) // SoM: Flags are more efficient
 				{
 					light = R_GetPlaneLight(gl_frontsector, centerHeight, viewz < bottomCullHeight ? true : false);
 
@@ -2489,7 +2538,7 @@ static void HWR_Subsector(size_t num)
 					                       &extrasubsectors[num],
 										   false,
 					                       *rover->bottomheight,
-					                       *gl_frontsector->lightlist[light].lightlevel,
+					                       HWR_FloorLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel),
 					                       max(0, min(rover->alpha, 255)), rover->master->frontsector,
 					                       HWR_RippleBlend(gl_frontsector, rover, false) | (rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent),
 					                       false, rover->fofflags & FOF_SPLAT, *gl_frontsector->lightlist[light].extra_colormap);
@@ -2498,8 +2547,9 @@ static void HWR_Subsector(size_t num)
 				{
 					HWR_GetLevelFlat(&levelflats[*rover->bottompic], rover->fofflags & FOF_SPLAT);
 					light = R_GetPlaneLight(gl_frontsector, centerHeight, viewz < bottomCullHeight ? true : false);
-					HWR_RenderPlane(sub, &extrasubsectors[num], false, *rover->bottomheight, HWR_RippleBlend(gl_frontsector, rover, false)|PF_Occlude, *gl_frontsector->lightlist[light].lightlevel, &levelflats[*rover->bottompic],
-					                rover->master->frontsector, 255, *gl_frontsector->lightlist[light].extra_colormap);
+					HWR_RenderPlane(sub, &extrasubsectors[num], false, *rover->bottomheight, HWR_RippleBlend(gl_frontsector, rover, false)|PF_Occlude,
+					                HWR_FloorLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel),
+					                &levelflats[*rover->bottompic], rover->master->frontsector, 255, *gl_frontsector->lightlist[light].extra_colormap);
 				}
 			}
 
@@ -2516,17 +2566,17 @@ static void HWR_Subsector(size_t num)
 					UINT8 alpha;
 
 					light = R_GetPlaneLight(gl_frontsector, centerHeight, viewz < topCullHeight ? true : false);
-					alpha = HWR_FogBlockAlpha(*gl_frontsector->lightlist[light].lightlevel, rover->master->frontsector->extra_colormap);
+					alpha = HWR_FogBlockAlpha(HWR_CeilingLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel), rover->master->frontsector->extra_colormap);
 
 					HWR_AddTransparentFloor(0,
 					                       &extrasubsectors[num],
 										   true,
 					                       *rover->topheight,
-					                       *gl_frontsector->lightlist[light].lightlevel,
+					                       HWR_CeilingLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel),
 					                       alpha, rover->master->frontsector, PF_Fog|PF_NoTexture,
 										   true, false, rover->master->frontsector->extra_colormap);
 				}
-				else if ((rover->fofflags & FOF_TRANSLUCENT && !(rover->fofflags & FOF_SPLAT)) || rover->blend)
+				else if ((rover->fofflags & FOF_TRANSLUCENT && !((rover->fofflags & FOF_SPLAT) && rover->alpha >= 255)) || rover->blend)
 				{
 					light = R_GetPlaneLight(gl_frontsector, centerHeight, viewz < topCullHeight ? true : false);
 
@@ -2534,7 +2584,7 @@ static void HWR_Subsector(size_t num)
 					                        &extrasubsectors[num],
 											true,
 					                        *rover->topheight,
-					                        *gl_frontsector->lightlist[light].lightlevel,
+					                        HWR_CeilingLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel),
 					                        max(0, min(rover->alpha, 255)), rover->master->frontsector,
 					                        HWR_RippleBlend(gl_frontsector, rover, false) | (rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent),
 					                        false, rover->fofflags & FOF_SPLAT, *gl_frontsector->lightlist[light].extra_colormap);
@@ -2543,8 +2593,9 @@ static void HWR_Subsector(size_t num)
 				{
 					HWR_GetLevelFlat(&levelflats[*rover->toppic], rover->fofflags & FOF_SPLAT);
 					light = R_GetPlaneLight(gl_frontsector, centerHeight, viewz < topCullHeight ? true : false);
-					HWR_RenderPlane(sub, &extrasubsectors[num], true, *rover->topheight, HWR_RippleBlend(gl_frontsector, rover, false)|PF_Occlude, *gl_frontsector->lightlist[light].lightlevel, &levelflats[*rover->toppic],
-					                  rover->master->frontsector, 255, *gl_frontsector->lightlist[light].extra_colormap);
+					HWR_RenderPlane(sub, &extrasubsectors[num], true, *rover->topheight, HWR_RippleBlend(gl_frontsector, rover, false)|PF_Occlude,
+					                  HWR_CeilingLightLevel(rover->master->frontsector, *gl_frontsector->lightlist[light].lightlevel),
+					                  &levelflats[*rover->toppic], rover->master->frontsector, 255, *gl_frontsector->lightlist[light].extra_colormap);
 				}
 			}
 		}
@@ -2620,45 +2671,31 @@ fixed_t *hwbbox;
 
 static void HWR_RenderBSPNode(INT32 bspnum)
 {
-	node_t *bsp = &nodes[bspnum];
-
-	// Decide which side the view point is on
+	node_t *bsp;
 	INT32 side;
 
 	ps_numbspcalls.value.i++;
 
-	// Found a subsector?
-	if (bspnum & NF_SUBSECTOR)
+	while (!(bspnum & NF_SUBSECTOR))  // Found a subsector?
 	{
-		if (bspnum == -1)
-		{
-			//*(gl_drawsubsector_p++) = 0;
-			HWR_Subsector(0);
-		}
-		else
-		{
-			//*(gl_drawsubsector_p++) = bspnum&(~NF_SUBSECTOR);
-			HWR_Subsector(bspnum&(~NF_SUBSECTOR));
-		}
-		return;
-	}
+		bsp = &nodes[bspnum];
 
-	// Decide which side the view point is on.
-	side = R_PointOnSide(viewx, viewy, bsp);
+		// Decide which side the view point is on.
+		side = R_PointOnSide(viewx, viewy, bsp);
+		// BP: big hack for a test in lighning ref : 1249753487AB
+		hwbbox = bsp->bbox[side];
+		// Recursively divide front space.
+		HWR_RenderBSPNode(bsp->children[side]);
 
-	// BP: big hack for a test in lighning ref : 1249753487AB
-	hwbbox = bsp->bbox[side];
+		// Possibly divide back space.
 
-	// Recursively divide front space.
-	HWR_RenderBSPNode(bsp->children[side]);
+		if (!HWR_CheckBBox(bsp->bbox[side^1]))
+			return;
 
-	// Possibly divide back space.
-	if (HWR_CheckBBox(bsp->bbox[side^1]))
-	{
-		// BP: big hack for a test in lighning ref : 1249753487AB
-		hwbbox = bsp->bbox[side^1];
-		HWR_RenderBSPNode(bsp->children[side^1]);
+		bspnum = bsp->children[side^1];
 	}
+
+	HWR_Subsector(bspnum == -1 ? 0 : bspnum & ~NF_SUBSECTOR);
 }
 
 // ==========================================================================
@@ -2880,7 +2917,7 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	}
 
 	HWR_Lighting(&sSurf, 0, colormap);
-	sSurf.PolyColor.s.alpha = alpha;
+	sSurf.PolyColor.s.alpha = FixedMul(thing->alpha, alpha);
 
 	if (HWR_UseShader())
 	{
@@ -3055,10 +3092,15 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 	// co-ordinates
 	memcpy(wallVerts, baseWallVerts, sizeof(baseWallVerts));
 
+	fixed_t newalpha = spr->mobj->alpha;
+
 	// if sprite has linkdraw, then dont write to z-buffer (by not using PF_Occlude)
 	// this will result in sprites drawn afterwards to be drawn on top like intended when using linkdraw.
 	if ((spr->mobj->flags2 & MF2_LINKDRAW) && spr->mobj->tracer)
+	{
+		newalpha = spr->mobj->tracer->alpha;
 		occlusion = 0;
+	}
 	else
 		occlusion = PF_Occlude;
 
@@ -3095,6 +3137,8 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 		if (!occlusion) use_linkdraw_hack = true;
 	}
 
+	Surf.PolyColor.s.alpha = FixedMul(newalpha, Surf.PolyColor.s.alpha);
+
 	if (HWR_UseShader())
 	{
 		shader = SHADER_SPRITE;
@@ -3291,7 +3335,7 @@ static void HWR_DrawBoundingBox(gl_vissprite_t *vis)
 		v[15].y = v[16].y = v[17].y = v[21].y = v[22].y = v[23].y = vis->gzt; // top
 
 	Surf.PolyColor = V_GetColor(R_GetBoundingBoxColor(vis->mobj));
-	
+
 	HWR_ProcessPolygon(&Surf, v, 24, (cv_renderhitboxgldepth.value ? 0 : PF_NoDepthTest)|PF_Modulated|PF_NoTexture|PF_WireFrame, SHADER_NONE, false);
 }
 
@@ -3543,11 +3587,15 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 		FBITFIELD blend = 0;
 		FBITFIELD occlusion;
 		boolean use_linkdraw_hack = false;
+		fixed_t newalpha = spr->mobj->alpha;
 
 		// if sprite has linkdraw, then dont write to z-buffer (by not using PF_Occlude)
 		// this will result in sprites drawn afterwards to be drawn on top like intended when using linkdraw.
 		if ((spr->mobj->flags2 & MF2_LINKDRAW) && spr->mobj->tracer)
+		{
 			occlusion = 0;
+			newalpha = spr->mobj->tracer->alpha;
+		}
 		else
 			occlusion = PF_Occlude;
 
@@ -3584,6 +3632,8 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 			if (!occlusion) use_linkdraw_hack = true;
 		}
 
+		Surf.PolyColor.s.alpha = FixedMul(newalpha, Surf.PolyColor.s.alpha);
+
 		if (spr->renderflags & RF_SHADOWEFFECTS)
 		{
 			INT32 alpha = Surf.PolyColor.s.alpha;
@@ -3893,7 +3943,7 @@ void HWR_AddTransparentFloor(levelflat_t *levelflat, extrasubsector_t *xsub, boo
 
 	planeinfo[numplanes].isceiling = isceiling;
 	planeinfo[numplanes].fixedheight = fixedheight;
-	planeinfo[numplanes].lightlevel = (planecolormap && (planecolormap->flags & CMF_FOG)) ? 255 : lightlevel;
+	planeinfo[numplanes].lightlevel = (planecolormap && (planecolormap->flags & CMF_FOG)) ? lightlevel : 255; // TODO: 2.3: Make transparent FOF planes always use light level
 	planeinfo[numplanes].levelflat = levelflat;
 	planeinfo[numplanes].xsub = xsub;
 	planeinfo[numplanes].alpha = alpha;
@@ -3925,7 +3975,7 @@ void HWR_AddTransparentPolyobjectFloor(levelflat_t *levelflat, polyobj_t *polyse
 
 	polyplaneinfo[numpolyplanes].isceiling = isceiling;
 	polyplaneinfo[numpolyplanes].fixedheight = fixedheight;
-	polyplaneinfo[numpolyplanes].lightlevel = (planecolormap && (planecolormap->flags & CMF_FOG)) ? 255 : lightlevel;
+	polyplaneinfo[numpolyplanes].lightlevel = (planecolormap && (planecolormap->flags & CMF_FOG)) ? lightlevel : 255;  // TODO: 2.3: Make transparent polyobject planes always use light level
 	polyplaneinfo[numpolyplanes].levelflat = levelflat;
 	polyplaneinfo[numpolyplanes].polysector = polysector;
 	polyplaneinfo[numpolyplanes].alpha = alpha;
@@ -4086,7 +4136,7 @@ static void HWR_CreateDrawNodes(void)
 		else if (sortnode[sortindex[i]].wall)
 		{
 			if (!(sortnode[sortindex[i]].wall->blend & PF_NoTexture))
-				HWR_GetTexture(sortnode[sortindex[i]].wall->texnum);
+				HWR_GetTexture(sortnode[sortindex[i]].wall->texnum, true);
 			HWR_RenderWall(sortnode[sortindex[i]].wall->wallVerts, &sortnode[sortindex[i]].wall->Surf, sortnode[sortindex[i]].wall->blend, sortnode[sortindex[i]].wall->fogwall,
 				sortnode[sortindex[i]].wall->lightlevel, sortnode[sortindex[i]].wall->wallcolormap);
 		}
@@ -5066,6 +5116,8 @@ static void HWR_DrawSkyBackground(player_t *player)
 
 	HWD.pfnSetBlend(PF_Translucent|PF_NoDepthTest|PF_Modulated);
 
+	HWR_GetTexture(texturetranslation[skytexture], false);
+
 	if (cv_glskydome.value)
 	{
 		FTransform dometransform;
@@ -5081,8 +5133,6 @@ static void HWR_DrawSkyBackground(player_t *player)
 		HWR_SetTransformAiming(&dometransform, player, false);
 		dometransform.angley = (float)((viewangle-ANGLE_270)>>ANGLETOFINESHIFT)*(360.0f/(float)FINEANGLES);
 
-		HWR_GetTexture(texturetranslation[skytexture]);
-
 		if (gl_sky.texture != texturetranslation[skytexture])
 		{
 			HWR_ClearSkyDome();
@@ -5102,7 +5152,6 @@ static void HWR_DrawSkyBackground(player_t *player)
 		float aspectratio;
 		float angleturn;
 
-		HWR_GetTexture(texturetranslation[skytexture]);
 		aspectratio = (float)vid.width/(float)vid.height;
 
 		//Hurdler: the sky is the only texture who need 4.0f instead of 1.0
@@ -5221,7 +5270,9 @@ static void HWR_SetTransformAiming(FTransform *trans, player_t *player, boolean
 	if (cv_glshearing.value == 1 || (cv_glshearing.value == 2 && R_IsViewpointThirdPerson(player, skybox)))
 	{
 		fixed_t fixedaiming = AIMINGTODY(aimingangle);
-		trans->viewaiming = FIXED_TO_FLOAT(fixedaiming);
+		trans->viewaiming = FIXED_TO_FLOAT(fixedaiming) * ((float)vid.width / vid.height) / ((float)BASEVIDWIDTH / BASEVIDHEIGHT);
+		if (splitscreen)
+			trans->viewaiming *= 2.125; // splitscreen adjusts fov with 0.8, so compensate (but only halfway, since splitscreen means only half the screen is used)
 		trans->shearing = true;
 		gl_aimingangle = 0;
 	}
@@ -5540,7 +5591,7 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 // Can't have palette rendering if shaders are disabled.
 boolean HWR_ShouldUsePaletteRendering(void)
 {
-	return (cv_glpaletterendering.value && HWR_UseShader());
+	return (pMasterPalette != NULL && cv_glpaletterendering.value && HWR_UseShader());
 }
 
 // enable or disable palette rendering state depending on settings and availability
@@ -5610,7 +5661,6 @@ void HWR_LoadLevel(void)
 // ==========================================================================
 
 static CV_PossibleValue_t glshaders_cons_t[] = {{0, "Off"}, {1, "On"}, {2, "Ignore custom shaders"}, {0, NULL}};
-static CV_PossibleValue_t glmodelinterpolation_cons_t[] = {{0, "Off"}, {1, "Sometimes"}, {2, "Always"}, {0, NULL}};
 static CV_PossibleValue_t glfakecontrast_cons_t[] = {{0, "Off"}, {1, "On"}, {2, "Smooth"}, {0, NULL}};
 static CV_PossibleValue_t glshearing_cons_t[] = {{0, "Off"}, {1, "On"}, {2, "Third-person"}, {0, NULL}};
 
@@ -5639,7 +5689,7 @@ consvar_t cv_glcoronasize = CVAR_INIT ("gr_coronasize", "1", CV_SAVE|CV_FLOAT, 0
 #endif
 
 consvar_t cv_glmodels = CVAR_INIT ("gr_models", "Off", CV_SAVE, CV_OnOff, NULL);
-consvar_t cv_glmodelinterpolation = CVAR_INIT ("gr_modelinterpolation", "Sometimes", CV_SAVE, glmodelinterpolation_cons_t, NULL);
+consvar_t cv_glmodelinterpolation = CVAR_INIT ("gr_modelinterpolation", "On", CV_SAVE, CV_OnOff, NULL);
 consvar_t cv_glmodellighting = CVAR_INIT ("gr_modellighting", "Off", CV_SAVE|CV_CALL, CV_OnOff, CV_glmodellighting_OnChange);
 
 consvar_t cv_glshearing = CVAR_INIT ("gr_shearing", "Off", CV_SAVE, glshearing_cons_t, NULL);
@@ -5657,7 +5707,7 @@ consvar_t cv_glbatching = CVAR_INIT ("gr_batching", "On", 0, CV_OnOff, NULL);
 
 static CV_PossibleValue_t glpalettedepth_cons_t[] = {{16, "16 bits"}, {24, "24 bits"}, {0, NULL}};
 
-consvar_t cv_glpaletterendering = CVAR_INIT ("gr_paletterendering", "Off", CV_SAVE|CV_CALL, CV_OnOff, CV_glpaletterendering_OnChange);
+consvar_t cv_glpaletterendering = CVAR_INIT ("gr_paletterendering", "On", CV_SAVE|CV_CALL, CV_OnOff, CV_glpaletterendering_OnChange);
 consvar_t cv_glpalettedepth = CVAR_INIT ("gr_palettedepth", "16 bits", CV_SAVE|CV_CALL, glpalettedepth_cons_t, CV_glpalettedepth_OnChange);
 
 #define ONLY_IF_GL_LOADED if (vid.glstate != VID_GL_LIBRARY_LOADED) return;
@@ -5729,6 +5779,7 @@ void HWR_AddCommands(void)
 	CV_RegisterVar(&cv_glskydome);
 	CV_RegisterVar(&cv_glspritebillboarding);
 	CV_RegisterVar(&cv_glfakecontrast);
+	CV_RegisterVar(&cv_glslopecontrast);
 	CV_RegisterVar(&cv_glshearing);
 	CV_RegisterVar(&cv_glshaders);
 
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index ea610a48b477824fd1d59e45ce70802bce5e460a..2277c32f3c07b6162148b5b3e56b8ac81dac351b 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index 0bb8de851bd2ad1b1891b92322943df83d349539..ce2c2c34638c2b4a412a11128ce17d200088381c 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1062,15 +1062,11 @@ static boolean HWR_AllowModel(mobj_t *mobj)
 
 static boolean HWR_CanInterpolateModel(mobj_t *mobj, model_t *model)
 {
-	if (cv_glmodelinterpolation.value == 2) // Always interpolate
-		return true;
 	return model->interpolate[(mobj->frame & FF_FRAMEMASK)];
 }
 
 static boolean HWR_CanInterpolateSprite2(modelspr2frames_t *spr2frame)
 {
-	if (cv_glmodelinterpolation.value == 2) // Always interpolate
-		return true;
 	return spr2frame->interpolate;
 }
 
@@ -1093,19 +1089,19 @@ static modelspr2frames_t *HWR_GetModelSprite2Frames(md2_t *md2, UINT16 spr2)
 			return &md2->model->superspr2frames[spr2];
 	}
 
-	if (md2->model->spr2frames)
+	if (md2->model->spr2frames[spr2].numframes)
 		return &md2->model->spr2frames[spr2];
 
 	return NULL;
 }
 
-static modelspr2frames_t *HWR_GetModelSprite2(md2_t *md2, skin_t *skin, UINT16 spr2, player_t *player)
+static UINT16 HWR_GetModelSprite2Num(md2_t *md2, skin_t *skin, UINT16 spr2, player_t *player)
 {
 	UINT16 super = 0;
 	UINT8 i = 0;
 
 	if (!md2 || !md2->model || !skin)
-		return HWR_GetModelSprite2Frames(md2, 0);
+		return 0;
 
 	while (!HWR_GetModelSprite2Frames(md2, spr2)
 		&& spr2 != SPR2_STND
@@ -1145,7 +1141,7 @@ static modelspr2frames_t *HWR_GetModelSprite2(md2_t *md2, skin_t *skin, UINT16 s
 	if (i >= 32) // probably an infinite loop...
 		spr2 = 0;
 
-	return HWR_GetModelSprite2Frames(md2, spr2);
+	return spr2;
 }
 
 // Adjust texture coords of model to fit into a patch's max_s and max_t
@@ -1154,6 +1150,9 @@ static void adjustTextureCoords(model_t *model, patch_t *patch)
 	int i;
 	GLPatch_t *gpatch = ((GLPatch_t *)patch->hardware);
 
+	if (!gpatch)
+		return;
+
 	for (i = 0; i < model->numMeshes; i++)
 	{
 		int j;
@@ -1190,6 +1189,90 @@ static void adjustTextureCoords(model_t *model, patch_t *patch)
 	model->max_t = gpatch->max_t;
 }
 
+static INT32 GetAnimDuration(mobj_t *mobj) //part of p_mobj's setplayermobjstate logic, used to make sure that anim durations are actually correct when the speed gets adjusted on players
+{
+	player_t *player = mobj->player;
+	INT32 tics = mobj->state->tics;
+
+	if (!(mobj->frame & FF_ANIMATE) && mobj->anim_duration) //set manually by something through lua
+		return mobj->anim_duration;
+
+	if (!player && mobj->type == MT_TAILSOVERLAY && mobj->tracer) //so tails overlays interpolate properly
+		player = mobj->tracer->player;
+	if (player)
+	{
+		if (player->panim == PA_EDGE && (player->charflags & SF_FASTEDGE))
+			tics = 2;
+		else if (player->powers[pw_tailsfly] && (!(player->mo->eflags & MFE_UNDERWATER) || (mobj->type == MT_PLAYER))) //tailsoverlay does not get adjusted from these rules when underwater
+		{
+			if (player->fly1 > 0)
+				tics = 1;
+			else if (!(player->mo->eflags & MFE_UNDERWATER))
+				tics = 2;
+			else
+				tics = 4;
+		}
+		else if (!(disableSpeedAdjust || player->charflags & SF_NOSPEEDADJUST))
+		{
+			fixed_t speed;// = FixedDiv(player->speed, FixedMul(mobj->scale, player->mo->movefactor));
+			if (player->panim == PA_FALL)
+			{
+				speed = FixedDiv(abs(mobj->momz), mobj->scale);
+				if (speed < 10<<FRACBITS)
+					tics = 4;
+				else if (speed < 20<<FRACBITS)
+					tics = 3;
+				else if (speed < 30<<FRACBITS)
+					tics = 2;
+				else
+					tics = 1;
+			}
+			else if (player->panim == PA_ABILITY2 && player->charability2 == CA2_SPINDASH)
+			{
+				fixed_t step = (player->maxdash - player->mindash)/4;
+				speed = (player->dashspeed - player->mindash);
+				if (speed > 3*step)
+					tics = 1;
+				else if (speed > step)
+					tics = 2;
+				else
+					tics = 3;
+			}
+			else
+			{
+				speed = FixedDiv(player->speed, FixedMul(mobj->scale, player->mo->movefactor));
+				if (player->panim == PA_ROLL || player->panim == PA_JUMP)
+				{
+					if (speed > 16<<FRACBITS)
+						tics = 1;
+					else
+						tics = 2;
+				}
+				else if (P_IsObjectOnGround(mobj) || ((player->charability == CA_FLOAT || player->charability == CA_SLOWFALL) && player->secondjump == 1) || player->powers[pw_super]) // Only if on the ground or superflying.
+				{
+					if (player->panim == PA_WALK)
+					{
+						if (speed > 12<<FRACBITS)
+							tics = 2;
+						else if (speed > 6<<FRACBITS)
+							tics = 3;
+						else
+							tics = 4;
+					}
+					else if ((player->panim == PA_RUN) || (player->panim == PA_DASH))
+					{
+						if (speed > 52<<FRACBITS)
+							tics = 1;
+						else
+							tics = 2;
+					}
+				}
+			}
+		}
+	}
+	return tics;
+}
+
 //
 // HWR_DrawModel
 //
@@ -1263,12 +1346,13 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 	{
 		patch_t *gpatch, *blendgpatch;
 		GLPatch_t *hwrPatch = NULL, *hwrBlendPatch = NULL;
-		float durs = (float)spr->mobj->state->tics;
+		float durs = GetAnimDuration(spr->mobj);
 		float tics = (float)spr->mobj->tics;
 		const boolean papersprite = (R_ThingIsPaperSprite(spr->mobj) && !R_ThingIsFloorSprite(spr->mobj));
 		const UINT8 flip = (UINT8)(!(spr->mobj->eflags & MFE_VERTICALFLIP) != !R_ThingVerticallyFlipped(spr->mobj));
 		const UINT8 hflip = (UINT8)(!(spr->mobj->mirrored) != !R_ThingHorizontallyFlipped(spr->mobj));
 		spritedef_t *sprdef;
+		UINT16 spr2 = 0;
 		spriteframe_t *sprframe;
 		INT32 mod;
 		interpmobjstate_t interp;
@@ -1283,8 +1367,13 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		}
 
 		// Apparently people don't like jump frames like that, so back it goes
-		//if (tics > durs)
-			//durs = tics;
+		if (tics > durs)
+			durs = tics;
+
+		// Make linkdraw objects use their tracer's alpha value
+		fixed_t newalpha = spr->mobj->alpha;
+		if ((spr->mobj->flags2 & MF2_LINKDRAW) && spr->mobj->tracer)
+			newalpha = spr->mobj->tracer->alpha;
 
 		INT32 blendmode;
 		if (spr->mobj->frame & FF_BLENDMASK)
@@ -1300,6 +1389,8 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 			Surf.PolyFlags = HWR_GetBlendModeFlag(blendmode);
 		}
 
+		Surf.PolyColor.s.alpha = FixedMul(newalpha, Surf.PolyColor.s.alpha);
+
 		// don't forget to enable the depth test because we can't do this
 		// like before: model polygons are not sorted
 
@@ -1438,13 +1529,17 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 
 		frame = (spr->mobj->frame & FF_FRAMEMASK);
 		if (spr->mobj->skin && spr->mobj->sprite == SPR_PLAY)
-			spr2frames = HWR_GetModelSprite2(md2, spr->mobj->skin, spr->mobj->sprite2, spr->mobj->player);
+		{
+			spr2 = HWR_GetModelSprite2Num(md2, spr->mobj->skin, spr->mobj->sprite2, spr->mobj->player);
+			spr2frames = HWR_GetModelSprite2Frames(md2, spr2);
+		}
 		if (spr2frames)
 		{
+			spritedef_t *defaultdef = P_GetSkinSpritedef(spr->mobj->skin, spr2);
 			mod = spr2frames->numframes;
 #ifndef DONTHIDEDIFFANIMLENGTH // by default, different anim length is masked by the mod
-			if (mod > (INT32)sprdef->numframes)
-				mod = sprdef->numframes;
+			if (mod > (INT32)defaultdef->numframes)
+				mod = defaultdef->numframes;
 #endif
 			if (!mod)
 				mod = 1;
@@ -1592,7 +1687,6 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 			HWD.pfnDrawModel(md2->model, frame, durs, tics, nextFrame, &p, md2->scale * xs, md2->scale * ys, flip, hflip, &Surf);
 		}
 	}
-
 	return true;
 }
 
diff --git a/src/hardware/hw_shaders.c b/src/hardware/hw_shaders.c
index 36cbb5db949c7fae0e28c82b6c0fcb6bd1dd3a73..fde67375f016e8132004ee14231cc31e575f5a30 100644
--- a/src/hardware/hw_shaders.c
+++ b/src/hardware/hw_shaders.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2021 by Sonic Team Junior.
+// Copyright (C) 2021-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -448,6 +448,101 @@ void HWR_LoadAllCustomShaders(void)
 		HWR_LoadCustomShadersFromFile(i, W_FileHasFolders(wadfiles[i]));
 }
 
+static const char version_directives[][14] = {
+	"#version 330\n",
+	"#version 150\n",
+	"#version 140\n",
+	"#version 130\n",
+	"#version 120\n",
+	"#version 110\n",
+};
+
+static boolean HWR_VersionDirectiveExists(const char* source)
+{
+    return strncmp(source, "#version", 8) == 0;
+}
+
+static char* HWR_PrependVersionDirective(const char* source, UINT32 version_index)
+{
+	const UINT32 version_len = sizeof(version_directives[version_index]) - 1;
+	const UINT32 source_len = strlen(source);
+
+	char* result = Z_Malloc(source_len + version_len + 1, PU_STATIC, NULL);
+	strcpy(result, version_directives[version_index]);
+	strcpy(result + version_len, source);
+
+	return result;
+}
+
+static void HWR_ReplaceVersionInplace(char* shader, UINT32 version_index)
+{
+	shader[9] = version_directives[version_index][9];
+	shader[10] = version_directives[version_index][10];
+	shader[11] = version_directives[version_index][11];
+}
+
+static boolean HWR_CheckVersionDirectives(const char* vert, const char* frag)
+{
+	return HWR_VersionDirectiveExists(vert) && HWR_VersionDirectiveExists(frag);
+}
+
+static void HWR_TryToCompileShaderWithImplicitVersion(INT32 shader_index, INT32 shaderxlat_id)
+{
+	char* vert_shader = gl_shaders[shader_index].vertex;
+	char* frag_shader = gl_shaders[shader_index].fragment;
+
+	boolean vert_shader_version_exists = HWR_VersionDirectiveExists(vert_shader);
+	boolean frag_shader_version_exists = HWR_VersionDirectiveExists(frag_shader);
+
+	if(!vert_shader_version_exists) {
+		CONS_Alert(CONS_WARNING, "HWR_LoadCustomShadersFromFile: vertex shader '%s' is missing a #version directive\n", HWR_GetShaderName(shaderxlat_id));
+	}
+
+	if(!frag_shader_version_exists) {
+		CONS_Alert(CONS_WARNING, "HWR_LoadCustomShadersFromFile: fragment shader '%s' is missing a #version directive\n", HWR_GetShaderName(shaderxlat_id));
+	}
+
+	// try to compile as is
+	HWR_CompileShader(shader_index);
+	if (gl_shaders[shader_index].compiled)
+		return;
+
+	// try each version directive
+	for(UINT32 i = 0; i < sizeof(version_directives) / sizeof(version_directives[0]); ++i) {
+		CONS_Alert(CONS_NOTICE, "HWR_TryToCompileShaderWithImplicitVersion: Trying %s\n", version_directives[i]);
+
+		if(!vert_shader_version_exists) {
+			// first time reallocation would have to be made
+
+			if(i == 0) {
+				void* old = (void*)gl_shaders[shader_index].vertex;
+				vert_shader = gl_shaders[shader_index].vertex = HWR_PrependVersionDirective(vert_shader, i);
+				Z_Free(old);
+			} else {
+				HWR_ReplaceVersionInplace(vert_shader, i);
+			}
+		}
+
+		if(!frag_shader_version_exists) {
+			if(i == 0) {
+				void* old = (void*)gl_shaders[shader_index].fragment;
+				frag_shader = gl_shaders[shader_index].fragment = HWR_PrependVersionDirective(frag_shader, i);
+				Z_Free(old);
+			} else {
+				HWR_ReplaceVersionInplace(frag_shader, i);
+			}
+		}
+
+		HWR_CompileShader(shader_index);
+		if (gl_shaders[shader_index].compiled) {
+			CONS_Alert(CONS_NOTICE, "HWR_TryToCompileShaderWithImplicitVersion: Compiled with %s\n",
+					   version_directives[i]);
+			CONS_Alert(CONS_WARNING, "Implicit GLSL version is used. Correct behavior is not guaranteed\n");
+			return;
+		}
+	}
+}
+
 void HWR_LoadCustomShadersFromFile(UINT16 wadnum, boolean PK3)
 {
 	UINT16 lump;
@@ -610,7 +705,13 @@ skip_field:
 				gl_shaders[shader_index].fragment = Z_StrDup(gl_shadersources[i].fragment);
 			if (!gl_shaders[shader_index].vertex)
 				gl_shaders[shader_index].vertex = Z_StrDup(gl_shadersources[i].vertex);
-			HWR_CompileShader(shader_index);
+
+			if(!HWR_CheckVersionDirectives(gl_shaders[shader_index].vertex, gl_shaders[shader_index].fragment)) {
+				HWR_TryToCompileShaderWithImplicitVersion(shader_index, i);
+			} else {
+				HWR_CompileShader(shader_index);
+			}
+
 			if (!gl_shaders[shader_index].compiled)
 				CONS_Alert(CONS_ERROR, "HWR_LoadCustomShadersFromFile: A compilation error occured for the %s shader in file %s. See the console messages above for more information.\n", shaderxlat[i].type, wadfiles[wadnum]->filename);
 		}
diff --git a/src/hardware/hw_shaders.h b/src/hardware/hw_shaders.h
index bb0e6a232edc45c2a39ac21a9f56beca4c973ebb..38a2d991191dc7531fb2e72e70b3b5946e98b245 100644
--- a/src/hardware/hw_shaders.h
+++ b/src/hardware/hw_shaders.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2021 by Sonic Team Junior.
+// Copyright (C) 2021-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hardware/r_opengl/ogl_win.c b/src/hardware/r_opengl/ogl_win.c
index c9bf601442a5531b35d0ac629730aa83978d17a6..ec8fc9bdb8b77f99f03b97af7df9cbdedea864f4 100644
--- a/src/hardware/r_opengl/ogl_win.c
+++ b/src/hardware/r_opengl/ogl_win.c
@@ -117,7 +117,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, // handle to DLL module
 #define pwglDeleteContext wglDeleteContext;
 #define pwglMakeCurrent wglMakeCurrent;
 #else
-static HMODULE OGL32, GLU32;
+static HMODULE OGL32;
 typedef void *(WINAPI *PFNwglGetProcAddress) (const char *);
 static PFNwglGetProcAddress pwglGetProcAddress;
 typedef HGLRC (WINAPI *PFNwglCreateContext) (HDC hdc);
@@ -132,13 +132,6 @@ static PFNwglMakeCurrent pwglMakeCurrent;
 void *GetGLFunc(const char *proc)
 {
 	void *func = NULL;
-	if (strncmp(proc, "glu", 3) == 0)
-	{
-		if (GLU32)
-			func = GetProcAddress(GLU32, proc);
-		else
-			return NULL;
-	}
 	if (pwglGetProcAddress)
 		func = pwglGetProcAddress(proc);
 	if (!func)
@@ -155,8 +148,6 @@ boolean LoadGL(void)
 	if (!OGL32)
 		return 0;
 
-	GLU32 = LoadLibrary("GLU32.DLL");
-
 	pwglGetProcAddress = GetGLFunc("wglGetProcAddress");
 	pwglCreateContext = GetGLFunc("wglCreateContext");
 	pwglDeleteContext = GetGLFunc("wglDeleteContext");
@@ -528,7 +519,6 @@ EXPORT void HWRAPI(Shutdown) (void)
 		ReleaseDC(hWnd, hDC);
 		hDC = NULL;
 	}
-	FreeLibrary(GLU32);
 	FreeLibrary(OGL32);
 	GL_DBG_Printf ("HWRAPI Shutdown(DONE)\n");
 }
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index 75a92c2fb33ad542b0931786a9b4688a9f9dccbc..02a32957a2b940ac15490c0e83fb57658da24c5e 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -96,6 +96,7 @@ static GLint min_filter = GL_LINEAR;
 static GLint mag_filter = GL_LINEAR;
 static GLint anisotropic_filter = 0;
 static boolean model_lighting = false;
+boolean supportMipMap = false;
 
 const GLubyte *gl_version = NULL;
 const GLubyte *gl_renderer = NULL;
@@ -417,9 +418,6 @@ static PFNglCopyTexImage2D pglCopyTexImage2D;
 typedef void (APIENTRY * PFNglCopyTexSubImage2D) (GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
 static PFNglCopyTexSubImage2D pglCopyTexSubImage2D;
 #endif
-/* GLU functions */
-typedef GLint (APIENTRY * PFNgluBuild2DMipmaps) (GLenum target, GLint internalFormat, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *data);
-static PFNgluBuild2DMipmaps pgluBuild2DMipmaps;
 
 /* 1.2 functions for 3D textures */
 typedef void (APIENTRY * PFNglTexImage3D) (GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
@@ -708,9 +706,6 @@ void SetupGLFunc4(void)
 	pglUniform3fv = GetGLFunc("glUniform3fv");
 	pglGetUniformLocation = GetGLFunc("glGetUniformLocation");
 #endif
-
-	// GLU
-	pgluBuild2DMipmaps = GetGLFunc("gluBuild2DMipmaps");
 }
 
 EXPORT boolean HWRAPI(InitShaders) (void)
@@ -718,7 +713,7 @@ EXPORT boolean HWRAPI(InitShaders) (void)
 #ifdef GL_SHADERS
 	if (!pglUseProgram)
 		return false;
-	
+
 	gl_fallback_shader.vertex_shader = Z_StrDup(GLSL_FALLBACK_VERTEX_SHADER);
 	gl_fallback_shader.fragment_shader = Z_StrDup(GLSL_FALLBACK_FRAGMENT_SHADER);
 
@@ -1617,7 +1612,8 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		//pglTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 		if (MipMap)
 		{
-			pgluBuild2DMipmaps(GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
+			pglTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
+			pglTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0);
 			if (pTexInfo->flags & TF_TRANSPARENT)
 				pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, 0); // No mippmaps on transparent stuff
@@ -1638,7 +1634,8 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		//pglTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 		if (MipMap)
 		{
-			pgluBuild2DMipmaps(GL_TEXTURE_2D, GL_ALPHA, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
+			pglTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
+			pglTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0);
 			if (pTexInfo->flags & TF_TRANSPARENT)
 				pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, 0); // No mippmaps on transparent stuff
@@ -1658,7 +1655,8 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 	{
 		if (MipMap)
 		{
-			pgluBuild2DMipmaps(GL_TEXTURE_2D, textureformatGL, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
+			pglTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
+			pglTexImage2D(GL_TEXTURE_2D, 0, textureformatGL, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			// Control the mipmap level of detail
 			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0); // the lower the number, the higer the detail
 			if (pTexInfo->flags & TF_TRANSPARENT)
@@ -2241,7 +2239,7 @@ EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value)
 					mag_filter = GL_LINEAR;
 					min_filter = GL_NEAREST;
 			}
-			if (!pgluBuild2DMipmaps)
+			if (!supportMipMap)
 			{
 				MipMap = GL_FALSE;
 				min_filter = GL_LINEAR;
@@ -3257,6 +3255,24 @@ EXPORT UINT32 HWRAPI(CreateLightTable)(RGBA_t *hw_lighttable)
 	return item->id;
 }
 
+EXPORT void HWRAPI(UpdateLightTable)(UINT32 id, RGBA_t *hw_lighttable)
+{
+	LTListItem *item = LightTablesHead;
+	while (item && item->id != id)
+		item = item->next;
+
+	if (item)
+	{
+		pglBindTexture(GL_TEXTURE_2D, item->id);
+
+		// Just update it
+		pglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, 32, GL_RGBA, GL_UNSIGNED_BYTE, hw_lighttable);
+
+		// restore previously bound texture
+		pglBindTexture(GL_TEXTURE_2D, tex_downloaded);
+	}
+}
+
 // Delete light table textures, ids given before become invalid and must not be used.
 EXPORT void HWRAPI(ClearLightTables)(void)
 {
diff --git a/src/hardware/r_opengl/r_opengl.h b/src/hardware/r_opengl/r_opengl.h
index f7e33c46aa36b9c416a80dc9158c5c7321fb78d0..197f75ea8931735b3e10b80e68a6ae47a5a736f5 100644
--- a/src/hardware/r_opengl/r_opengl.h
+++ b/src/hardware/r_opengl/r_opengl.h
@@ -35,7 +35,6 @@
 
 #else
 #include <GL/gl.h>
-#include <GL/glu.h>
 
 #ifdef STATIC_OPENGL // Because of the 1.3 functions, you'll need GLext to compile it if static
 #define GL_GLEXT_PROTOTYPES
@@ -127,6 +126,7 @@ extern GLint			screen_width;
 extern GLint			screen_height;
 extern GLbyte			screen_depth;
 extern GLint			maximumAnisotropy;
+extern boolean 			supportMipMap;
 
 /**	\brief OpenGL flags for video driver
 */
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 4e2f3d492974c0027c3b343a075018dc29682d33..e8796286660188ba70eef269de751a1f0437bb1e 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -29,7 +29,7 @@
 #include "i_video.h"
 #include "i_system.h"
 
-#include "st_stuff.h" // ST_HEIGHT
+#include "st_stuff.h"
 #include "r_local.h"
 
 #include "keys.h"
@@ -52,6 +52,7 @@
 #include "lua_hud.h"
 #include "lua_hudlib_drawlist.h"
 #include "lua_hook.h"
+#include "lua_libs.h"
 
 // coords are scaled
 #define HU_INPUTX 0
@@ -204,7 +205,7 @@ void HU_LoadGraphics(void)
 	HU_SetFontProperties(&hu_font,   0,  4,  8, 12);
 	HU_SetFontProperties(&tny_font,  0,  2,  4, 12);
 	HU_SetFontProperties(&cred_font, 0, 16, 16, 16);
-	HU_SetFontProperties(&lt_font,   0, 16, 20, 20);
+	HU_SetFontProperties(&lt_font,   0, 16, 20, 16);
 	HU_SetFontProperties(&ntb_font,  2,  4, 20, 21);
 	HU_SetFontProperties(&nto_font,  0,  4, 20, 21);
 
@@ -272,7 +273,7 @@ void HU_LoadFontCharacters(fontdef_t *font, const char *prefix)
 		for (i = 0; i < FONTSIZE; i++, j++)
 		{
 			sprintf(buffer, "%.5s%.3d", prefix, j);
-			if (W_CheckNumForName(buffer) == LUMPERROR)
+			if (W_CheckNumForPatchName(buffer) == LUMPERROR)
 				font->chars[i] = NULL;
 			else
 				font->chars[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
@@ -395,8 +396,11 @@ void HU_AddChatText(const char *text, boolean playsound)
 
 	if (OLDCHAT) // if we're using oldchat, print directly in console
 		CONS_Printf("%s\n", text);
-	else			// if we aren't, still save the message to log.txt
-		CON_LogMessage(va("%s\n", text));
+	else			// if we aren't, still save the message to log.txt	
+	{
+		CON_LogMessage(text);
+		CON_LogMessage("\n"); // Add newline. Don't use va for that, since `text` might be refering to va's buffer itself
+	}
 }
 
 /** Runs a say command, sending an ::XD_SAY message.
@@ -587,8 +591,47 @@ static void Command_CSay_f(void)
 	DoSayCommand(0, 1, HU_CSAY);
 }
 
-static tic_t spam_tokens[MAXPLAYERS] = { 1 }; // fill the buffer with 1 so the motd can be sent.
-static tic_t spam_tics[MAXPLAYERS];
+UINT8 spam_tokens[MAXPLAYERS] = { 1 }; // fill the buffer with 1 so the motd can be sent.
+tic_t spam_tics[MAXPLAYERS];
+
+static const char *GetChatColorFromSkinColor(INT32 skincolor)
+{
+	const char *textcolor = NULL;
+	UINT16 chatcolor = skincolors[skincolor].chatcolor;
+	if (!chatcolor || chatcolor%0x1000 || chatcolor>V_INVERTMAP)
+		textcolor = "\x80";
+	else if (chatcolor == V_MAGENTAMAP)
+		textcolor = "\x81";
+	else if (chatcolor == V_YELLOWMAP)
+		textcolor = "\x82";
+	else if (chatcolor == V_GREENMAP)
+		textcolor = "\x83";
+	else if (chatcolor == V_BLUEMAP)
+		textcolor = "\x84";
+	else if (chatcolor == V_REDMAP)
+		textcolor = "\x85";
+	else if (chatcolor == V_GRAYMAP)
+		textcolor = "\x86";
+	else if (chatcolor == V_ORANGEMAP)
+		textcolor = "\x87";
+	else if (chatcolor == V_SKYMAP)
+		textcolor = "\x88";
+	else if (chatcolor == V_PURPLEMAP)
+		textcolor = "\x89";
+	else if (chatcolor == V_AQUAMAP)
+		textcolor = "\x8a";
+	else if (chatcolor == V_PERIDOTMAP)
+		textcolor = "\x8b";
+	else if (chatcolor == V_AZUREMAP)
+		textcolor = "\x8c";
+	else if (chatcolor == V_BROWNMAP)
+		textcolor = "\x8d";
+	else if (chatcolor == V_ROSYMAP)
+		textcolor = "\x8e";
+	else if (chatcolor == V_INVERTMAP)
+		textcolor = "\x8f";
+	return textcolor;
+}
 
 /** Receives a message, processing an ::XD_SAY command.
   * \sa DoSayCommand
@@ -599,6 +642,7 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 	SINT8 target;
 	UINT8 flags;
 	const char *dispname;
+	char buf[HU_MAXMSGLEN + 1];
 	char *msg;
 	boolean action = false;
 	char *ptr;
@@ -608,8 +652,8 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 
 	target = READSINT8(*p);
 	flags = READUINT8(*p);
-	msg = (char *)*p;
-	SKIPSTRINGL(*p, HU_MAXMSGLEN + 1);
+	msg = buf;
+	READSTRINGL(*p, msg, HU_MAXMSGLEN + 1);
 
 	if ((cv_mute.value || players[playernum].muted || flags & (HU_CSAY|HU_SERVER_SAY)) && playernum != serverplayer && !(IsPlayerAdmin(playernum)))
 	{
@@ -649,14 +693,12 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 	else
 		spam_tokens[playernum] -= 1;
 
-	// run the lua hook even if we were supposed to eat the msg, netgame consistency goes first.
+	if (spam_eatmsg)
+		return; // don't proceed if we were supposed to eat the message.
 
 	if (LUA_HookPlayerMsg(playernum, target, flags, msg))
 		return;
 
-	if (spam_eatmsg)
-		return; // don't proceed if we were supposed to eat the message.
-
 	// If it's a CSAY, just CECHO and be done with it.
 	if (flags & HU_CSAY)
 	{
@@ -711,51 +753,27 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 		{
 			if (players[playernum].ctfteam == 1) // red
 			{
-				cstart = "\x85";
-				textcolor = "\x85";
+				cstart = textcolor = GetChatColorFromSkinColor(skincolor_redteam);
 			}
 			else // blue
 			{
-				cstart = "\x84";
-				textcolor = "\x84";
+				cstart = textcolor = GetChatColorFromSkinColor(skincolor_blueteam);
 			}
 		}
 		else
         {
-			UINT16 chatcolor = skincolors[players[playernum].skincolor].chatcolor;
-
-			if (!chatcolor || chatcolor%0x1000 || chatcolor>V_INVERTMAP)
-				cstart = "\x80";
-			else if (chatcolor == V_MAGENTAMAP)
-				cstart = "\x81";
-			else if (chatcolor == V_YELLOWMAP)
-				cstart = "\x82";
-			else if (chatcolor == V_GREENMAP)
-				cstart = "\x83";
-			else if (chatcolor == V_BLUEMAP)
-				cstart = "\x84";
-			else if (chatcolor == V_REDMAP)
-				cstart = "\x85";
-			else if (chatcolor == V_GRAYMAP)
-				cstart = "\x86";
-			else if (chatcolor == V_ORANGEMAP)
-				cstart = "\x87";
-			else if (chatcolor == V_SKYMAP)
-				cstart = "\x88";
-			else if (chatcolor == V_PURPLEMAP)
-				cstart = "\x89";
-			else if (chatcolor == V_AQUAMAP)
-				cstart = "\x8a";
-			else if (chatcolor == V_PERIDOTMAP)
-				cstart = "\x8b";
-			else if (chatcolor == V_AZUREMAP)
-				cstart = "\x8c";
-			else if (chatcolor == V_BROWNMAP)
-				cstart = "\x8d";
-			else if (chatcolor == V_ROSYMAP)
-				cstart = "\x8e";
-			else if (chatcolor == V_INVERTMAP)
-				cstart = "\x8f";
+			cstart = GetChatColorFromSkinColor(players[playernum].skincolor);
+			if (G_GametypeHasTeams())
+			{
+				if (players[playernum].ctfteam == 1) // red
+				{
+					cstart = GetChatColorFromSkinColor(skincolor_redteam);
+				}
+				else // blue
+				{
+					cstart = GetChatColorFromSkinColor(skincolor_blueteam);
+				}
+			}
         }
 		prefix = cstart;
 
@@ -999,6 +1017,7 @@ static void HU_sendChatMessage(void)
 void HU_clearChatChars(void)
 {
 	memset(w_chat, '\0', sizeof(w_chat));
+	I_SetTextInputMode(textinputmodeenabledbylua);
 	chat_on = false;
 	c_input = 0;
 
@@ -1048,6 +1067,7 @@ boolean HU_Responder(event_t *ev)
 		if ((ev->key == gamecontrol[GC_TALKKEY][0] || ev->key == gamecontrol[GC_TALKKEY][1])
 			&& netgame && !OLD_MUTE) // check for old chat mute, still let the players open the chat incase they want to scroll otherwise.
 		{
+			I_SetTextInputMode(true);
 			chat_on = true;
 			chat_on_first_event = false;
 			w_chat[0] = 0;
@@ -1059,6 +1079,7 @@ boolean HU_Responder(event_t *ev)
 		if ((ev->key == gamecontrol[GC_TEAMKEY][0] || ev->key == gamecontrol[GC_TEAMKEY][1])
 			&& netgame && !OLD_MUTE)
 		{
+			I_SetTextInputMode(true);
 			chat_on = true;
 			chat_on_first_event = false;
 			w_chat[0] = 0;
@@ -1133,6 +1154,7 @@ boolean HU_Responder(event_t *ev)
 			if (!CHAT_MUTE)
 				HU_sendChatMessage();
 
+			I_SetTextInputMode(textinputmodeenabledbylua);
 			chat_on = false;
 			c_input = 0; // reset input cursor
 			chat_scrollmedown = true; // you hit enter, so you might wanna autoscroll to see what you just sent. :)
@@ -1143,6 +1165,7 @@ boolean HU_Responder(event_t *ev)
 			|| c == gamecontrol[GC_TEAMKEY][0] || c == gamecontrol[GC_TEAMKEY][1])
 			&& c >= KEY_MOUSE1)) // If it's not a keyboard key, then the chat button is used as a toggle.
 		{
+			I_SetTextInputMode(textinputmodeenabledbylua);
 			chat_on = false;
 			c_input = 0; // reset input cursor
 			I_UpdateMouseGrab();
@@ -1213,27 +1236,36 @@ static void HU_drawMiniChat(void)
 	INT32 charwidth = 4, charheight = 6;
 	INT32 boxw = cv_chatwidth.value;
 	INT32 dx = 0, dy = 0;
+	boolean prev_linereturn = false;
 
 	if (!chat_nummsg_min)
 		return; // needless to say it's useless to do anything if we don't have anything to draw.
 
 	for (size_t i = chat_nummsg_min; i > 0; i--)
 	{
-		char *msg = V_ChatWordWrap(chatx, boxw-charwidth, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i-1]);
+		char *msg = V_ChatWordWrap(0, boxw-charwidth-2, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE|V_MONOSPACE, chat_mini[i-1]);
 		for(size_t j = 0; msg[j]; j++) // iterate through msg
 		{
 			if (msg[j] == '\n') // get back down.
 			{
-				chatheight += charheight;
-				dx = 0;
+				if (!prev_linereturn)
+				{
+					chatheight += charheight;
+					dx = 0;
+				}
+				prev_linereturn = true;
 			}
 			else if (msg[j] >= FONTSTART)
 			{
+				prev_linereturn = false;
+
 				dx += charwidth;
-				if (dx >= boxw)
+
+				if (dx >= boxw-charwidth-2)
 				{
 					dx = 0;
 					chatheight += charheight;
+					prev_linereturn = true;
 				}
 			}
 		}
@@ -1245,35 +1277,43 @@ static void HU_drawMiniChat(void)
 	}
 
 	y = chaty - (chatheight + charheight);
+	prev_linereturn = false;
 
 	for (size_t i = 0; i < chat_nummsg_min; i++) // iterate through our hot messages
 	{
 		INT32 timer = ((cv_chattime.value*TICRATE)-chat_timers[i]) - cv_chattime.value*TICRATE+9; // see below...
 		INT32 transflag = (timer >= 0 && timer <= 9) ? (timer*V_10TRANS) : 0; // you can make bad jokes out of this one.
-		char *msg = V_ChatWordWrap(chatx, boxw-charwidth, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i]); // get the current message, and word wrap it.
+		char *msg = V_ChatWordWrap(0, boxw-charwidth-2, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE|V_MONOSPACE, chat_mini[i]); // get the current message, and word wrap it.
 		UINT8 *colormap = NULL;
 
 		for(size_t j = 0; msg[j]; j++) // iterate through msg
 		{
 			if (msg[j] == '\n') // get back down.
 			{
-				dy += charheight;
-				dx = 0;
+				if (!prev_linereturn)
+				{
+					dy += charheight;
+					dx = 0;
+				}
+				prev_linereturn = true;
 			}
 			else if (msg[j] & 0x80) // get colormap
 				colormap = V_GetStringColormap(((msg[j] & 0x7f) << V_CHARCOLORSHIFT) & V_CHARCOLORMASK);
 			else if (msg[j] >= FONTSTART)
 			{
+				prev_linereturn = false;
+
 				if (cv_chatbacktint.value) // on request of wolfy
 					V_DrawFillConsoleMap(x + dx + 2, y+dy, charwidth, charheight, 239|V_SNAPTOBOTTOM|V_SNAPTOLEFT);
 
-				V_DrawChatCharacter(x + dx + 2, y+dy, msg[j] |V_SNAPTOBOTTOM|V_SNAPTOLEFT|transflag, true, colormap);
-
+				V_DrawChatCharacter(x + dx + 2, y+dy, msg[j] |V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_MONOSPACE|transflag, true, colormap);
 				dx += charwidth;
-				if (dx >= boxw)
+
+				if (dx >= boxw-charwidth-2)
 				{
 					dx = 0;
 					dy += charheight;
+					prev_linereturn = true;
 				}
 			}
 		}
@@ -1298,6 +1338,7 @@ static void HU_drawChatLog(INT32 offset)
 	UINT32 i = 0;
 	INT32 chat_topy, chat_bottomy;
 	boolean atbottom = false;
+	boolean prev_linereturn = false;
 
 	// make sure that our scroll position isn't "illegal";
 	if (chat_scroll > chat_maxscroll)
@@ -1330,27 +1371,38 @@ static void HU_drawChatLog(INT32 offset)
 
 	for (i=0; i<chat_nummsg_log; i++) // iterate through our chatlog
 	{
-		char *msg = V_ChatWordWrap(chatx, boxw-charwidth, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_log[i]); // get the current message, and word wrap it.
+		char *msg = V_ChatWordWrap(0, boxw-charwidth-2, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE|V_MONOSPACE, chat_log[i]); // get the current message, and word wrap it.
 		UINT8 *colormap = NULL;
 		for(size_t j = 0; msg[j]; j++) // iterate through msg
 		{
 			if (msg[j] == '\n') // get back down.
 			{
-				dy += charheight;
-				dx = 0;
+				if (!prev_linereturn)
+				{
+					dy += charheight;
+					dx = 0;
+				}
+				prev_linereturn = true;
 			}
 			else if (msg[j] & 0x80) // get colormap
 				colormap = V_GetStringColormap(((msg[j] & 0x7f) << V_CHARCOLORSHIFT) & V_CHARCOLORMASK);
-			else if (msg[j] >= FONTSTART)
+			else
 			{
-				if ((y+dy+2 >= chat_topy) && (y+dy < (chat_bottomy)))
-					V_DrawChatCharacter(x + dx + 2, y+dy+2, msg[j] |V_SNAPTOBOTTOM|V_SNAPTOLEFT, true, colormap);
+				prev_linereturn = false;
 
-				dx += charwidth;
-				if (dx >= boxw-charwidth-2 && i<chat_nummsg_log) // end of message shouldn't count, nor should invisible characters!!!!
+				if (msg[j] >= FONTSTART)
+				{
+					if ((y+dy+2 >= chat_topy) && (y+dy < (chat_bottomy)))
+						V_DrawChatCharacter(x + dx + 2, y+dy+2, msg[j] |V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_MONOSPACE, true, colormap);
+
+					dx += charwidth;
+				}
+
+				if (dx >= boxw-charwidth-2 && i < chat_nummsg_log) // end of message shouldn't count, nor should invisible characters!!!!
 				{
 					dx = 0;
 					dy += charheight;
+					prev_linereturn = true;
 				}
 			}
 		}
@@ -1507,9 +1559,9 @@ static void HU_DrawChat(void)
 				else if ((n == 1) && !(w_chat[3] == '0') && (!((i == 1) || ((i >= 10) && (i <= 19)))))
 					continue;
 				else if ((n == 2) && !(w_chat[3] == '0') && (!((i == 2) || ((i >= 20) && (i <= 29)))))
-					continue; 
+					continue;
 				else if ((n == 3) && !(w_chat[3] == '0') && (!((i == 3) || ((i >= 30) && (i <= 31)))))
-					continue; 
+					continue;
 				else	// general case.
 					if (i != n) continue;
 			}
@@ -2010,13 +2062,13 @@ static void HU_Draw32TeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		greycheck = greycheckdef;
 		supercheck = supercheckdef;
 
-		if (tab[i].color == skincolor_redteam) //red
+		if (players[tab[i].num].ctfteam == 1) //red
 		{
 			redplayers++;
 			x = 14 + (BASEVIDWIDTH/2);
 			y = (redplayers * 9) + 20;
 		}
-		else if (tab[i].color == skincolor_blueteam) //blue
+		else if (players[tab[i].num].ctfteam == 2) //blue
 		{
 			blueplayers++;
 			x = 14;
@@ -2098,7 +2150,7 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		if (players[tab[i].num].spectator)
 			continue; //ignore them.
 
-		if (tab[i].color == skincolor_redteam) //red
+		if (players[tab[i].num].ctfteam == 1) //red
 		{
 			if (redplayers++ > 8)
 			{
@@ -2106,7 +2158,7 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 				break; // don't make more loops than we need to.
 			}
 		}
-		else if (tab[i].color == skincolor_blueteam) //blue
+		else if (players[tab[i].num].ctfteam == 2) //blue
 		{
 			if (blueplayers++ > 8)
 			{
@@ -2137,14 +2189,14 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		if (players[tab[i].num].spectator)
 			continue; //ignore them.
 
-		if (tab[i].color == skincolor_redteam) //red
+		if (players[tab[i].num].ctfteam == 1) //red
 		{
 			if (redplayers++ > 8)
 				continue;
 			x = 32 + (BASEVIDWIDTH/2);
 			y = (redplayers * 16) + 16;
 		}
-		else if (tab[i].color == skincolor_blueteam) //blue
+		else if (players[tab[i].num].ctfteam == 2) //blue
 		{
 			if (blueplayers++ > 8)
 				continue;
@@ -2458,53 +2510,20 @@ static inline void HU_DrawSpectatorTicker(void)
 {
 	int i;
 	int length = 0, height = 174;
-	int totallength = 0, templength = 0;
+	int totallength = 0;
 
 	for (i = 0; i < MAXPLAYERS; i++)
 		if (playeringame[i] && players[i].spectator)
 			totallength += (signed)strlen(player_names[i]) * 8 + 16;
 
-	length -= (leveltime % (totallength + BASEVIDWIDTH));
-	length += BASEVIDWIDTH;
+	length -= (leveltime % (totallength + (vid.width / vid.dup)));
+	length += (vid.width / vid.dup);
 
 	for (i = 0; i < MAXPLAYERS; i++)
 		if (playeringame[i] && players[i].spectator)
 		{
-			char *pos;
-			char initial[MAXPLAYERNAME+1];
-			char current[MAXPLAYERNAME+1];
-
-			strcpy(initial, player_names[i]);
-			pos = initial;
-
-			if (length >= -((signed)strlen(player_names[i]) * 8 + 16) && length <= BASEVIDWIDTH)
-			{
-				if (length < 0)
-				{
-					UINT8 eatenchars = (UINT8)(abs(length) / 8 + 1);
-
-					if (eatenchars <= strlen(initial))
-					{
-						// Eat one letter off the left side,
-						// then compensate the drawing position.
-						pos += eatenchars;
-						strcpy(current, pos);
-						templength = length % 8 + 8;
-					}
-					else
-					{
-						strcpy(current, " ");
-						templength = length;
-					}
-				}
-				else
-				{
-					strcpy(current, initial);
-					templength = length;
-				}
-
-				V_DrawString(templength, height + 8, V_TRANSLUCENT|V_ALLOWLOWERCASE, current);
-			}
+			if (length >= -((signed)strlen(player_names[i]) * 8 + 16) && length <= (vid.width / vid.dup))
+				V_DrawString(length, height + 8, V_TRANSLUCENT|V_ALLOWLOWERCASE|V_SNAPTOLEFT, player_names[i]);
 
 			length += (signed)strlen(player_names[i]) * 8 + 16;
 		}
diff --git a/src/hu_stuff.h b/src/hu_stuff.h
index ca77ed93002750d6cefa28584d8e4de7be3bfc65..2618d873b1392b7d9ac30d0d5ea2e3e2e4cd81da 100644
--- a/src/hu_stuff.h
+++ b/src/hu_stuff.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -79,6 +79,9 @@ void HU_AddChatText(const char *text, boolean playsound);
 // set true when entering a chat message
 extern boolean chat_on;
 
+extern UINT8 spam_tokens[MAXPLAYERS];
+extern tic_t spam_tics[MAXPLAYERS];
+
 extern patch_t *emeraldpics[3][8];
 extern patch_t *rflagico;
 extern patch_t *bflagico;
diff --git a/src/i_system.h b/src/i_system.h
index 3f0e05d127fa4ed5ce9aa8fbe89ff109773851f4..5348caf4d39e72575c17ecdf4d2c7825bf5fb3c4 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -339,4 +339,12 @@ void I_SetMouseGrab(boolean grab);
 */
 const char *I_GetSysName(void);
 
+/** \brief Sets text input mode. When enabled, keyboard inputs will respect dead keys.
+ */
+void I_SetTextInputMode(boolean active);
+
+/** \brief Retrieves current text input mode.
+ */
+boolean I_GetTextInputMode(void);
+
 #endif
diff --git a/src/i_video.h b/src/i_video.h
index 4b459e25b67bd207bfae911365e1a15eec101db0..f3bbc90b92a61215f1421a5ca4c074cd85ae0875 100644
--- a/src/i_video.h
+++ b/src/i_video.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/info.c b/src/info.c
index 9b33a57ab2f863d24f2355b24acdd6ed208e9765..3998eb88d2b976fc4d718506a4e6ea687c04e49e 100644
--- a/src/info.c
+++ b/src/info.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -306,6 +306,8 @@ char sprnames[NUMSPRITES + 1][MAXSPRITENAME + 1] =
 	"XMS4", // Lamppost
 	"XMS5", // Hanging Star
 	"XMS6", // Mistletoe
+	"SNTT", // Silver Shiver tree
+	"SSTT", // Silver Shiver tree with snow
 	"FHZI", // FHZ ice
 	"ROSY",
 
@@ -339,6 +341,8 @@ char sprnames[NUMSPRITES + 1][MAXSPRITENAME + 1] =
 	// Misc Scenery
 	"STLG", // Stalagmites
 	"DBAL", // Disco
+	"GINE", // Crystalline Heights tree
+	"PPAL", // Pristine Shores palm trees
 
 	// Powerup Indicators
 	"ARMA", // Armageddon Shield Orb
@@ -524,6 +528,9 @@ char sprnames[NUMSPRITES + 1][MAXSPRITENAME + 1] =
 	// Gravity Well Objects
 	"GWLG",
 	"GWLR",
+
+	// LJ Knuckles
+	"OLDK",
 };
 
 char spr2names[NUMPLAYERSPRITES][MAXSPRITENAME + 1] =
@@ -593,6 +600,17 @@ char spr2names[NUMPLAYERSPRITES][MAXSPRITENAME + 1] =
 	"TALB",
 	"TALC",
 
+	"MSC0",
+	"MSC1",
+	"MSC2",
+	"MSC3",
+	"MSC4",
+	"MSC5",
+	"MSC6",
+	"MSC7",
+	"MSC8",
+	"MSC9",
+
 	"CNT1",
 	"CNT2",
 	"CNT3",
@@ -671,6 +689,17 @@ playersprite_t spr2defaults[NUMPLAYERSPRITES] = {
 	SPR2_TAL0, // SPR2_TALB,
 	SPR2_TAL6, // SPR2_TALC,
 
+	0, // SPR2_MSC0,
+	0, // SPR2_MSC1,
+	0, // SPR2_MSC2,
+	0, // SPR2_MSC3,
+	0, // SPR2_MSC4,
+	0, // SPR2_MSC5,
+	0, // SPR2_MSC6,
+	0, // SPR2_MSC7,
+	0, // SPR2_MSC8,
+	0, // SPR2_MSC9,
+
 	SPR2_WAIT, // SPR2_CNT1,
 	SPR2_FALL, // SPR2_CNT2,
 	SPR2_SPNG, // SPR2_CNT3,
@@ -709,7 +738,7 @@ state_t states[NUMSTATES] =
 
 	// Player
 	{SPR_PLAY, SPR2_STND|FF_ANIMATE,    105, {NULL}, 0,  7, S_PLAY_WAIT, 0}, // S_PLAY_STND
-	{SPR_PLAY, SPR2_WAIT|FF_ANIMATE,     -1, {NULL}, 0, 16, S_NULL, 0},      // S_PLAY_WAIT
+	{SPR_PLAY, SPR2_WAIT,                16, {NULL}, 0,  0, S_PLAY_WAIT, 0}, // S_PLAY_WAIT
 	{SPR_PLAY, SPR2_WALK,                 4, {NULL}, 0,  0, S_PLAY_WALK, 0}, // S_PLAY_WALK
 	{SPR_PLAY, SPR2_SKID,                 1, {NULL}, 0,  0, S_PLAY_WALK, 0}, // S_PLAY_SKID
 	{SPR_PLAY, SPR2_RUN ,                 2, {NULL}, 0,  0, S_PLAY_RUN, 0},  // S_PLAY_RUN
@@ -761,12 +790,12 @@ state_t states[NUMSTATES] =
 	{SPR_PLAY, SPR2_MLEL,                35, {NULL},                   0, 0, S_PLAY_WALK, 0},  // S_PLAY_MELEE_LANDING
 
 	// SF_SUPER
-	{SPR_PLAY, SPR2_TRNS|FF_SPR2SUPER|FF_ANIMATE,     7, {NULL},          0, 4, S_PLAY_SUPER_TRANS2, 0}, // S_PLAY_SUPER_TRANS1
-	{SPR_PLAY, SPR2_TRNS|FF_SPR2SUPER,                3, {NULL},          0, 0, S_PLAY_SUPER_TRANS3, 0}, // S_PLAY_SUPER_TRANS2
-	{SPR_PLAY, SPR2_TRNS|FF_SPR2SUPER|FF_FULLBRIGHT,  2, {NULL},          0, 0, S_PLAY_SUPER_TRANS4, 0}, // S_PLAY_SUPER_TRANS3
-	{SPR_PLAY, SPR2_TRNS|FF_SPR2SUPER|FF_FULLBRIGHT,  2, {NULL},          0, 0, S_PLAY_SUPER_TRANS5, 0}, // S_PLAY_SUPER_TRANS4
-	{SPR_PLAY, SPR2_TRNS|FF_SPR2SUPER|FF_FULLBRIGHT,  2, {NULL},          0, 0, S_PLAY_SUPER_TRANS6, 0}, // S_PLAY_SUPER_TRANS5
-	{SPR_PLAY, SPR2_TRNS|FF_SPR2SUPER|FF_FULLBRIGHT, 19, {A_FadeOverlay}, 0, 0, S_PLAY_FALL, 0},         // S_PLAY_SUPER_TRANS6
+	{SPR_PLAY, SPR2_TRNS|SPR2F_SUPER|FF_ANIMATE,     7, {NULL},          0, 4, S_PLAY_SUPER_TRANS2, 0}, // S_PLAY_SUPER_TRANS1
+	{SPR_PLAY, SPR2_TRNS|SPR2F_SUPER,                3, {NULL},          0, 0, S_PLAY_SUPER_TRANS3, 0}, // S_PLAY_SUPER_TRANS2
+	{SPR_PLAY, SPR2_TRNS|SPR2F_SUPER|FF_FULLBRIGHT,  2, {NULL},          0, 0, S_PLAY_SUPER_TRANS4, 0}, // S_PLAY_SUPER_TRANS3
+	{SPR_PLAY, SPR2_TRNS|SPR2F_SUPER|FF_FULLBRIGHT,  2, {NULL},          0, 0, S_PLAY_SUPER_TRANS5, 0}, // S_PLAY_SUPER_TRANS4
+	{SPR_PLAY, SPR2_TRNS|SPR2F_SUPER|FF_FULLBRIGHT,  2, {NULL},          0, 0, S_PLAY_SUPER_TRANS6, 0}, // S_PLAY_SUPER_TRANS5
+	{SPR_PLAY, SPR2_TRNS|SPR2F_SUPER|FF_FULLBRIGHT, 19, {A_FadeOverlay}, 0, 0, S_PLAY_FALL, 0},         // S_PLAY_SUPER_TRANS6
 
 	{SPR_NULL, 0, -1, {NULL}, 0, 0, S_OBJPLACE_DUMMY, 0}, //S_OBJPLACE_DUMMY
 
@@ -1368,116 +1397,117 @@ state_t states[NUMSTATES] =
 	// Boss 5
 	{SPR_NULL, 0, 2, {A_CheckFlags2}, MF2_AMBUSH, S_FANG_IDLE0, S_FANG_INTRO0, 0}, // S_FANG_SETUP
 
-	{SPR_NULL, 0, 2, {NULL}, 0, 0, S_FANG_INTRO1, 0}, // S_FANG_INTRO0
-	{SPR_NULL, 0, 2, {A_Boss5MakeJunk}, -S_FANG_CLONE1, 0, S_FANG_INTRO2, 0}, // S_FANG_INTRO1
-	{SPR_NULL, 0, 0, {A_Repeat}, 25, S_FANG_INTRO1, S_FANG_INTRO3, 0}, // S_FANG_INTRO2
-	{SPR_NULL, 0, 0, {A_Boss5MakeJunk}, 0, 1, S_FANG_INTRO4, 0}, // S_FANG_INTRO3
-	{SPR_FANG, 30, 1, {A_ZThrust}, 9, (1<<16)|1, S_FANG_INTRO5, 0}, // S_FANG_INTRO4
-	{SPR_FANG, 27, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO6, 0}, // S_FANG_INTRO5
-	{SPR_FANG, 28, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO7, 0}, // S_FANG_INTRO6
-	{SPR_FANG, 29, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO8, 0}, // S_FANG_INTRO7
-	{SPR_FANG, 30, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO5, 0}, // S_FANG_INTRO8
-	{SPR_FANG, 23|FF_ANIMATE, 50, {NULL}, 1, 4, S_FANG_INTRO10, 0}, // S_FANG_INTRO9
-	{SPR_FANG, 25, 5, {NULL}, 0, 0, S_FANG_INTRO11, 0}, // S_FANG_INTRO10
-	{SPR_FANG, 26, 2, {A_Boss5MakeJunk}, S_BROKENROBOTD, 2, S_FANG_INTRO12, 0}, // S_FANG_INTRO11
-	{SPR_FANG, 31|FF_ANIMATE, 50, {NULL}, 3, 4, S_FANG_IDLE1, 0}, // S_FANG_INTRO12
-
-	{SPR_FANG, 11, 2, {A_Boss5MakeJunk}, 0, -1, S_FANG_CLONE2, 0}, // S_FANG_CLONE1
-	{SPR_FANG, 11, 0, {A_Repeat}, 49, S_FANG_CLONE1, S_FANG_CLONE3, 0}, // S_FANG_INTRO2
-	{SPR_FANG, 12, 0, {A_SetObjectFlags}, MF_NOGRAVITY, 1, S_FANG_CLONE4, 0}, // S_FANG_CLONE3
-	{SPR_FANG, 12, 1, {A_Boss5CheckOnGround}, S_FANG_IDLE0, 0, S_FANG_CLONE4, 0}, // S_FANG_CLONE4
-
-	{SPR_FANG, 0,  0, {A_SetObjectFlags}, MF_NOCLIPTHING, 1, S_FANG_IDLE1, 0}, // S_FANG_IDLE0
-	{SPR_FANG, 2, 16, {A_Look}, 1, 0, S_FANG_IDLE2, 0}, // S_FANG_IDLE1
-	{SPR_FANG, 3, 16, {A_Look}, 1, 0, S_FANG_IDLE3, 0}, // S_FANG_IDLE2
-	{SPR_FANG, 3, 16, {A_Look}, 1, 0, S_FANG_IDLE4, 0}, // S_FANG_IDLE3
-	{SPR_FANG, 3, 16, {A_Look}, 1, 0, S_FANG_IDLE5, 0}, // S_FANG_IDLE4
-	{SPR_FANG, 2, 16, {A_Look}, 1, 0, S_FANG_IDLE6, 0}, // S_FANG_IDLE5
-	{SPR_FANG, 1, 16, {A_Look}, 1, 0, S_FANG_IDLE7, 0}, // S_FANG_IDLE6
-	{SPR_FANG, 1, 16, {A_Look}, 1, 0, S_FANG_IDLE8, 0}, // S_FANG_IDLE7
-	{SPR_FANG, 1, 16, {A_Look}, 1, 0, S_FANG_IDLE1, 0}, // S_FANG_IDLE8
-
-	{SPR_FANG, 14, 0, {A_DoNPCPain}, FRACUNIT, 0, S_FANG_PAIN2, 0}, // S_FANG_PAIN1
-	{SPR_FANG, 14, 1, {A_Boss5CheckOnGround}, S_FANG_PATHINGSTART1, S_FANG_PINCHPATHINGSTART1, S_FANG_PAIN2, 0}, // S_FANG_PAIN2
-
-	{SPR_FANG,  8, 0, {A_Boss5ExtraRepeat}, 5, 4, S_FANG_PATHINGSTART2, 0}, // S_FANG_PATHINGSTART1
-	{SPR_FANG,  8, 0, {A_PlayActiveSound}, 0, 0, S_FANG_PATHING, 0}, // S_FANG_PATHINGSTART2
-	{SPR_FANG,  8, 0, {A_Boss5FindWaypoint}, 0, 0, S_FANG_BOUNCE1, 0}, // S_FANG_PATHING
-
-	{SPR_FANG,  8, 2, {A_Thrust}, 0, 1, S_FANG_BOUNCE2, 0}, // S_FANG_BOUNCE1
-	{SPR_FANG,  9, 2, {NULL}, 0, 0, S_FANG_BOUNCE3, 0}, // S_FANG_BOUNCE2
-	{SPR_FANG, 10, 1, {A_Boss5Jump}, 0, 0, S_FANG_BOUNCE4, 0}, // S_FANG_BOUNCE3
-	{SPR_FANG, 10, 1, {A_Boss5CheckFalling}, S_FANG_CHECKPATH1, S_FANG_FALL1, S_FANG_BOUNCE4, 0}, // S_FANG_BOUNCE4
-
-	{SPR_FANG, 12, 1, {A_Boss5CheckOnGround}, S_FANG_CHECKPATH1, 0, S_FANG_FALL2, 0}, // S_FANG_FALL1
-	{SPR_FANG, 13, 1, {A_Boss5CheckOnGround}, S_FANG_CHECKPATH1, 0, S_FANG_FALL1, 0}, // S_FANG_FALL2
-
-	{SPR_FANG,  8, 0, {A_Boss5Calm}, 0, 0, S_FANG_CHECKPATH2, 0}, // S_FANG_CHECKPATH1
-	{SPR_FANG,  8, 0, {A_Repeat}, 0, S_FANG_PATHINGCONT1, S_FANG_SKID1, 0}, // S_FANG_CHECKPATH2
-
-	{SPR_FANG,  9, 0, {A_Boss5PinchShot}, MT_FBOMB, -16, S_FANG_PATHINGCONT2, 0}, // S_FANG_PATHINGCONT1
-	{SPR_FANG,  9, 0, {A_PlayActiveSound}, 0, 0, S_FANG_PATHINGCONT3, 0}, // S_FANG_PATHINGCONT2
-	{SPR_FANG,  9, 2, {A_Thrust}, 0, 1, S_FANG_PATHING, 0}, // S_FANG_PATHINGCONT3
-
-	{SPR_FANG,  4,  0, {A_PlayAttackSound}, 0, 0, S_FANG_SKID2, 0}, // S_FANG_SKID1
-	{SPR_FANG,  4,  1, {A_DoNPCSkid}, S_FANG_SKID3, 0, S_FANG_SKID2, 0}, // S_FANG_SKID2
-	{SPR_FANG,  4, 10, {NULL}, 0, 0, S_FANG_CHOOSEATTACK, 0}, // S_FANG_SKID3
-
-	{SPR_FANG,  0, 0, {A_RandomState}, S_FANG_LOBSHOT0, S_FANG_FIRESTART1, S_NULL, 0}, // S_FANG_CHOOSEATTACK
-
-	{SPR_FANG,  5,  0, {A_PrepareRepeat}, 3, 0, S_FANG_FIRESTART2, 0}, // S_FANG_FIRESTART1 // Reset loop
-	{SPR_FANG,  5, 18, {A_LookForBetter}, 1, 0, S_FANG_FIRE1, 0}, // S_FANG_FIRESTART2
-	{SPR_FANG,  5,  5, {A_FireShot}, MT_CORK, -16, S_FANG_FIRE2, 0}, // S_FANG_FIRE1 // Start of loop
-	{SPR_FANG,  6,  5, {NULL}, 0, 0, S_FANG_FIRE3, 0}, // S_FANG_FIRE2
-	{SPR_FANG,  7,  5, {NULL}, 0, 0, S_FANG_FIRE4, 0}, // S_FANG_FIRE3
-	{SPR_FANG,  5,  5, {NULL}, 2, 0, S_FANG_FIREREPEAT, 0}, // S_FANG_FIRE4
-	{SPR_FANG,  5,  0, {A_Repeat}, 3, S_FANG_FIRE1, S_FANG_WAIT1, 0}, // S_FANG_FIREREPEAT // End of loop
-
-	{SPR_FANG, 18, 16, {A_LookForBetter}, 1, 0, S_FANG_LOBSHOT1, 0}, // S_FANG_LOBSHOT0
-	{SPR_FANG, 19,  2, {A_LookForBetter}, 1, 0, S_FANG_LOBSHOT2, 0}, // S_FANG_LOBSHOT1
-	{SPR_FANG, 20, 18, {A_BrakLobShot}, MT_FBOMB, 32+(1<<16), S_FANG_WAIT1, 0}, // S_FANG_LOBSHOT2
-
-	{SPR_FANG, FF_ANIMATE|15, 70, {NULL}, 1, 5, S_FANG_WAIT2, 0}, // S_FANG_WAIT1
-	{SPR_FANG,             0, 35, {A_Look}, 1, 0, S_FANG_IDLE1, 0}, // S_FANG_WAIT2
-
-	{SPR_FANG, 12, 1, {A_Boss5CheckOnGround}, S_FANG_PATHINGSTART2, S_FANG_PINCHPATHINGSTART1, S_FANG_WALLHIT, 0}, // S_FANG_WALLHIT
-
-	{SPR_FANG,  8,  0, {A_PrepareRepeat}, 1, 0, S_FANG_PINCHPATHINGSTART2, 0}, // S_FANG_PINCHPATHINGSTART1
-	{SPR_FANG,  8,  0, {A_PlayActiveSound}, 0, 0, S_FANG_PINCHPATHING, 0}, // S_FANG_PINCHPATHINGSTART2
-	{SPR_FANG,  8,  0, {A_Boss5FindWaypoint}, 1, 0, S_FANG_PINCHBOUNCE0, 0}, // S_FANG_PINCHPATHING
-	{SPR_FANG,  8,  0, {A_SetObjectFlags}, MF_NOCLIP|MF_NOCLIPHEIGHT, 2, S_FANG_PINCHBOUNCE1, 0}, // S_FANG_PINCHBOUNCE0
-	{SPR_FANG,  8,  2, {A_Thrust}, 0, 1, S_FANG_PINCHBOUNCE2, 0}, // S_FANG_PINCHBOUNCE1
-	{SPR_FANG,  9,  2, {NULL}, 0, 0, S_FANG_PINCHBOUNCE3, 0}, // S_FANG_PINCHBOUNCE2
-	{SPR_FANG, 10,  2, {A_Boss5Jump}, 0, 0, S_FANG_PINCHBOUNCE4, 0}, // S_FANG_PINCHBOUNCE3
-	{SPR_FANG, 10,  1, {A_Boss5CheckFalling}, S_FANG_PINCHSKID1, S_FANG_PINCHFALL0, S_FANG_PINCHBOUNCE4, 0}, // S_FANG_PINCHBOUNCE4
-	{SPR_FANG, 12,  0, {A_SetObjectFlags}, MF_NOCLIP|MF_NOCLIPHEIGHT, 1, S_FANG_PINCHFALL1, 0}, // S_FANG_PINCHFALL0
-	{SPR_FANG, 12,  1, {A_Boss5CheckOnGround}, S_FANG_PINCHSKID1, 0, S_FANG_PINCHFALL2, 0}, // S_FANG_PINCHFALL1
-	{SPR_FANG, 13,  1, {A_Boss5CheckOnGround}, S_FANG_PINCHSKID1, 0, S_FANG_PINCHFALL1, 0}, // S_FANG_PINCHFALL2
-	{SPR_FANG,  4,  0, {A_PlayAttackSound}, 0, 0, S_FANG_PINCHSKID2, 0}, // S_FANG_PINCHSKID1
-	{SPR_FANG,  4,  1, {A_DoNPCSkid}, S_FANG_PINCHLOBSHOT0, 0, S_FANG_PINCHSKID2, 0}, // S_FANG_PINCHSKID2
-	{SPR_FANG, 18, 16, {A_FaceTarget}, 3, 0, S_FANG_PINCHLOBSHOT1, 0}, // S_FANG_PINCHLOBSHOT0
-	{SPR_FANG, 19,  2, {A_FaceTarget}, 3, 0, S_FANG_PINCHLOBSHOT2, 0}, // S_FANG_PINCHLOBSHOT1
-	{SPR_FANG, 20, 30, {A_Boss5MakeItRain}, MT_FBOMB, -16, S_FANG_PINCHLOBSHOT3, 0}, // S_FANG_PINCHLOBSHOT2
-	{SPR_FANG, 20, 18, {A_LinedefExecuteFromArg}, 4, 0, S_FANG_PINCHLOBSHOT4, 0}, // S_FANG_PINCHLOBSHOT3
-	{SPR_FANG,  0,  0, {A_Boss5Calm}, 0, 0, S_FANG_PATHINGSTART1, 0}, // S_FANG_PINCHLOBSHOT4
-
-	{SPR_FANG, 21, 0, {A_DoNPCPain},                    0, 0, S_FANG_DIE2, 0}, // S_FANG_DIE1
-	{SPR_FANG, 21, 1, {A_Boss5CheckOnGround}, S_FANG_DIE3, 0, S_FANG_DIE2, 0}, // S_FANG_DIE2
-
-	{SPR_FANG, 22,  0, {A_Scream}, 0, 0, S_FANG_DIE4, 0}, // S_FANG_DIE3
-	{SPR_FANG, 22, -1, {A_SetFuse}, 70, 0, S_FANG_DIE5, 0}, // S_FANG_DIE4
-
-	{SPR_FANG, 11, 0, {A_PlaySound}, sfx_jump, 0, S_FANG_DIE6, 0}, // S_FANG_DIE5
-	{SPR_FANG, 11, 1, {A_ZThrust}, 6, (1<<16)|1, S_FANG_DIE7, 0}, // S_FANG_DIE6
-	{SPR_FANG, 11, 1, {A_Boss5CheckFalling}, S_FANG_FLEEPATHING1, S_FANG_DIE8, S_FANG_DIE7, 0}, // S_FANG_DIE7
-	{SPR_FANG, 12, 1, {A_Boss5CheckOnGround}, S_FANG_FLEEPATHING1, 0, S_FANG_DIE8, 0}, // S_FANG_DIE8
-
-	{SPR_FANG,  9,  0, {A_PlayActiveSound}, 0, 0, S_FANG_FLEEPATHING2, 0}, // S_FANG_FLEEPATHING1
-	{SPR_FANG,  8,  2, {A_Boss5FindWaypoint}, 2, 0, S_FANG_FLEEBOUNCE1, 0}, // S_FANG_FLEEPATHING2
-	{SPR_FANG,  9,  2, {NULL}, 0, 0, S_FANG_FLEEBOUNCE2, 0}, // S_FANG_FLEEBOUNCE1
-	{SPR_FANG, 10, -1, {A_BossDeath}, 0, 0, S_NULL, 0}, // S_FANG_FLEEBOUNCE2
-
-	{SPR_FANG, 17, 7*TICRATE, {NULL}, 0, 0, S_NULL, 0}, // S_FANG_KO
+	{SPR_NULL,         0, 2, {NULL}, 0, 0, S_FANG_INTRO1, 0}, // S_FANG_INTRO0
+	{SPR_NULL,         0, 2, {A_Boss5MakeJunk}, -S_FANG_CLONE1, 0, S_FANG_INTRO2, 0}, // S_FANG_INTRO1
+	{SPR_NULL,         0, 0, {A_Repeat}, 25, S_FANG_INTRO1, S_FANG_INTRO3, 0}, // S_FANG_INTRO2
+	{SPR_NULL,         0, 0, {A_Boss5MakeJunk}, 0, 1, S_FANG_INTRO4, 0}, // S_FANG_INTRO3
+	{SPR_PLAY, SPR2_ROLL, 1, {A_ZThrust}, 9, (1<<16)|1, S_FANG_INTRO5, 0}, // S_FANG_INTRO4
+	{SPR_PLAY, SPR2_ROLL, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO6, 0}, // S_FANG_INTRO5
+	{SPR_PLAY, SPR2_ROLL, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO7, 0}, // S_FANG_INTRO6
+	{SPR_PLAY, SPR2_ROLL, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO8, 0}, // S_FANG_INTRO7
+	{SPR_PLAY, SPR2_ROLL, 1, {A_Boss5CheckOnGround}, S_FANG_INTRO9, 0, S_FANG_INTRO5, 0}, // S_FANG_INTRO8
+	{SPR_PLAY, SPR2_MSC0|FF_ANIMATE, 50, {NULL}, 0, 4, S_FANG_INTRO10, 0}, // S_FANG_INTRO9
+	{SPR_PLAY, SPR2_MSC1, 5, {NULL}, 0, 0, S_FANG_INTRO11, 0}, // S_FANG_INTRO10
+	{SPR_PLAY, SPR2_MSC2, 2, {A_Boss5MakeJunk}, S_BROKENROBOTD, 2, S_FANG_INTRO12, 0}, // S_FANG_INTRO11
+	{SPR_PLAY, SPR2_CNT1|FF_ANIMATE, 50, {NULL}, 0, 4, S_FANG_IDLE1, 0}, // S_FANG_INTRO12
+
+	{SPR_PLAY, SPR2_SPNG, 2, {A_Boss5MakeJunk}, 0, -1, S_FANG_CLONE2, 0}, // S_FANG_CLONE1
+	{SPR_PLAY, SPR2_SPNG, 0, {A_Repeat}, 49, S_FANG_CLONE1, S_FANG_CLONE3, 0}, // S_FANG_CLONE2
+	{SPR_PLAY, SPR2_FALL, 0, {A_SetObjectFlags}, MF_NOGRAVITY, 1, S_FANG_CLONE4, 0}, // S_FANG_CLONE3
+	{SPR_PLAY, SPR2_FALL, 1, {A_Boss5CheckOnGround}, S_FANG_IDLE0, 0, S_FANG_CLONE4, 0}, // S_FANG_CLONE4
+
+	{SPR_PLAY,         0,  0, {A_SetObjectFlags}, MF_NOCLIPTHING, 1, S_FANG_IDLE1, 0}, // S_FANG_IDLE0
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE2, 0}, // S_FANG_IDLE1
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE3, 0}, // S_FANG_IDLE2
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE4, 0}, // S_FANG_IDLE3
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE5, 0}, // S_FANG_IDLE4
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE6, 0}, // S_FANG_IDLE5
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE7, 0}, // S_FANG_IDLE6
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE8, 0}, // S_FANG_IDLE7
+	{SPR_PLAY, SPR2_WAIT, 16, {A_Look}, 1, 0, S_FANG_IDLE1, 0}, // S_FANG_IDLE8
+
+	{SPR_PLAY,         0, 0, {A_DoNPCPain}, FRACUNIT, 0, S_FANG_PAIN2, 0}, // S_FANG_PAIN1
+	{SPR_PLAY, SPR2_PAIN, 1, {A_Boss5CheckOnGround}, S_FANG_PATHINGSTART1, S_FANG_PINCHPATHINGSTART1, S_FANG_PAIN2, 0}, // S_FANG_PAIN2
+
+	{SPR_PLAY,         0, 0, {A_Boss5ExtraRepeat}, 5, 4, S_FANG_PATHINGSTART2, 0}, // S_FANG_PATHINGSTART1
+	{SPR_PLAY,         0, 0, {A_PlayActiveSound}, 0, 0, S_FANG_PATHING, 0}, // S_FANG_PATHINGSTART2
+	{SPR_PLAY,         0, 0, {A_Boss5FindWaypoint}, 0, 0, S_FANG_BOUNCE1, 0}, // S_FANG_PATHING
+
+	{SPR_PLAY, SPR2_LAND, 2, {A_Thrust}, 0, 1, S_FANG_BOUNCE2, 0}, // S_FANG_BOUNCE1
+	{SPR_PLAY, SPR2_LAND, 2, {NULL}, 0, 0, S_FANG_BOUNCE3, 0}, // S_FANG_BOUNCE2
+	{SPR_PLAY, SPR2_LAND, 1, {A_Boss5Jump}, 0, 0, S_FANG_BOUNCE4, 0}, // S_FANG_BOUNCE3
+	{SPR_PLAY, SPR2_BNCE, 1, {A_Boss5CheckFalling}, S_FANG_CHECKPATH1, S_FANG_FALL1, S_FANG_BOUNCE4, 0}, // S_FANG_BOUNCE4
+
+	{SPR_PLAY, SPR2_FALL, 1, {A_Boss5CheckOnGround}, S_FANG_CHECKPATH1, 0, S_FANG_FALL2, 0}, // S_FANG_FALL1
+	{SPR_PLAY, SPR2_FALL, 1, {A_Boss5CheckOnGround}, S_FANG_CHECKPATH1, 0, S_FANG_FALL1, 0}, // S_FANG_FALL2
+
+	{SPR_PLAY,         0, 0, {A_Boss5Calm}, 0, 0, S_FANG_CHECKPATH2, 0}, // S_FANG_CHECKPATH1
+	{SPR_PLAY,         0, 0, {A_Repeat}, 0, S_FANG_PATHINGCONT1, S_FANG_SKID1, 0}, // S_FANG_CHECKPATH2
+
+	{SPR_PLAY,         0, 0, {A_Boss5PinchShot}, MT_FBOMB, -16, S_FANG_PATHINGCONT2, 0}, // S_FANG_PATHINGCONT1
+	{SPR_PLAY,         0, 0, {A_PlayActiveSound}, 0, 0, S_FANG_PATHINGCONT3, 0}, // S_FANG_PATHINGCONT2
+	{SPR_PLAY, SPR2_LAND, 2, {A_Thrust}, 0, 1, S_FANG_PATHING, 0}, // S_FANG_PATHINGCONT3
+
+	{SPR_PLAY,         0,  0, {A_PlayAttackSound}, 0, 0, S_FANG_SKID2, 0}, // S_FANG_SKID1
+	{SPR_PLAY, SPR2_SKID,  1, {A_DoNPCSkid}, S_FANG_SKID3, 0, S_FANG_SKID2, 0}, // S_FANG_SKID2
+	{SPR_PLAY, SPR2_SKID, 10, {NULL}, 0, 0, S_FANG_CHOOSEATTACK, 0}, // S_FANG_SKID3
+
+	{SPR_PLAY,         0, 0, {A_RandomState}, S_FANG_LOBSHOT0, S_FANG_FIRESTART1, S_NULL, 0}, // S_FANG_CHOOSEATTACK
+
+	{SPR_PLAY,         0,  0, {A_PrepareRepeat}, 3, 0, S_FANG_FIRESTART2, 0}, // S_FANG_FIRESTART1 // Reset loop
+	{SPR_PLAY, SPR2_FIRE, 18, {A_LookForBetter}, 1, 0, S_FANG_FIRE1, 0}, // S_FANG_FIRESTART2
+	{SPR_PLAY, SPR2_FIRE,  2, {A_FireShot}, MT_CORK, -16, S_FANG_FIRE2, 0}, // S_FANG_FIRE1 // Start of loop
+	{SPR_PLAY, SPR2_FIRE,  2, {NULL}, 0, 0, S_FANG_FIRE3, 0}, // S_FANG_FIRE2
+	{SPR_PLAY, SPR2_FIRE, 16, {NULL}, 0, 0, S_FANG_FIREREPEAT, 0}, // S_FANG_FIRE3
+	{SPR_PLAY,         0,  0, {A_Repeat}, 3, S_FANG_FIRE1, S_FANG_WAIT1, 0}, // S_FANG_FIREREPEAT // End of loop
+
+	{SPR_PLAY, SPR2_MSC3, 14, {A_LookForBetter}, 1, 0, S_FANG_LOBSHOT1, 0}, // S_FANG_LOBSHOT0
+	{SPR_PLAY, SPR2_MSC3,  2, {A_LookForBetter}, 1, 0, S_FANG_LOBSHOT2, 0}, // S_FANG_LOBSHOT1
+	{SPR_PLAY, SPR2_MSC3,  2, {A_LookForBetter}, 1, 0, S_FANG_LOBSHOT3, 0}, // S_FANG_LOBSHOT2
+	{SPR_PLAY, SPR2_MSC3, 18, {A_BrakLobShot}, MT_FBOMB, 32+(1<<16), S_FANG_WAIT1, 0}, // S_FANG_LOBSHOT3
+
+	{SPR_PLAY, SPR2_MLEL|FF_ANIMATE, 70, {NULL}, 0, 5, S_FANG_WAIT2, 0}, // S_FANG_WAIT1
+	{SPR_PLAY,            SPR2_STND, 35, {A_Look}, 1, 0, S_FANG_IDLE1, 0}, // S_FANG_WAIT2
+
+	{SPR_PLAY, SPR2_FALL, 1, {A_Boss5CheckOnGround}, S_FANG_PATHINGSTART2, S_FANG_PINCHPATHINGSTART1, S_FANG_WALLHIT, 0}, // S_FANG_WALLHIT
+
+	{SPR_PLAY,         0,  0, {A_PrepareRepeat}, 1, 0, S_FANG_PINCHPATHINGSTART2, 0}, // S_FANG_PINCHPATHINGSTART1
+	{SPR_PLAY,         0,  0, {A_PlayActiveSound}, 0, 0, S_FANG_PINCHPATHING, 0}, // S_FANG_PINCHPATHINGSTART2
+	{SPR_PLAY,         0,  0, {A_Boss5FindWaypoint}, 1, 0, S_FANG_PINCHBOUNCE0, 0}, // S_FANG_PINCHPATHING
+	{SPR_PLAY,         0,  0, {A_SetObjectFlags}, MF_NOCLIP|MF_NOCLIPHEIGHT, 2, S_FANG_PINCHBOUNCE1, 0}, // S_FANG_PINCHBOUNCE0
+	{SPR_PLAY, SPR2_LAND,  2, {A_Thrust}, 0, 1, S_FANG_PINCHBOUNCE2, 0}, // S_FANG_PINCHBOUNCE1
+	{SPR_PLAY, SPR2_LAND,  2, {NULL}, 0, 0, S_FANG_PINCHBOUNCE3, 0}, // S_FANG_PINCHBOUNCE2
+	{SPR_PLAY, SPR2_LAND,  2, {A_Boss5Jump}, 0, 0, S_FANG_PINCHBOUNCE4, 0}, // S_FANG_PINCHBOUNCE3
+	{SPR_PLAY, SPR2_BNCE,  1, {A_Boss5CheckFalling}, S_FANG_PINCHSKID1, S_FANG_PINCHFALL0, S_FANG_PINCHBOUNCE4, 0}, // S_FANG_PINCHBOUNCE4
+	{SPR_PLAY,         0,  0, {A_SetObjectFlags}, MF_NOCLIP|MF_NOCLIPHEIGHT, 1, S_FANG_PINCHFALL1, 0}, // S_FANG_PINCHFALL0
+	{SPR_PLAY, SPR2_FALL,  1, {A_Boss5CheckOnGround}, S_FANG_PINCHSKID1, 0, S_FANG_PINCHFALL2, 0}, // S_FANG_PINCHFALL1
+	{SPR_PLAY, SPR2_FALL,  1, {A_Boss5CheckOnGround}, S_FANG_PINCHSKID1, 0, S_FANG_PINCHFALL1, 0}, // S_FANG_PINCHFALL2
+	{SPR_PLAY,         0,  0, {A_PlayAttackSound}, 0, 0, S_FANG_PINCHSKID2, 0}, // S_FANG_PINCHSKID1
+	{SPR_PLAY, SPR2_SKID,  1, {A_DoNPCSkid}, S_FANG_PINCHLOBSHOT0, 0, S_FANG_PINCHSKID2, 0}, // S_FANG_PINCHSKID2
+	{SPR_PLAY, SPR2_MSC3, 16, {A_FaceTarget}, 1, 5, S_FANG_PINCHLOBSHOT1, 0}, // S_FANG_PINCHLOBSHOT0
+	{SPR_PLAY, SPR2_MSC3,  2, {A_FaceTarget}, 3, 0, S_FANG_PINCHLOBSHOT2, 0}, // S_FANG_PINCHLOBSHOT1
+	{SPR_PLAY, SPR2_MSC3,  2, {A_FaceTarget}, 3, 0, S_FANG_PINCHLOBSHOT3, 0}, // S_FANG_PINCHLOBSHOT2
+	{SPR_PLAY, SPR2_MSC3, 30, {A_Boss5MakeItRain}, MT_FBOMB, -16, S_FANG_PINCHLOBSHOT4, 0}, // S_FANG_PINCHLOBSHOT3
+	{SPR_PLAY, SPR2_STND, 18, {A_LinedefExecuteFromArg}, 4, 0, S_FANG_PINCHLOBSHOT5, 0}, // S_FANG_PINCHLOBSHOT4
+	{SPR_PLAY,         0,  0, {A_Boss5Calm}, 0, 0, S_FANG_PATHINGSTART1, 0}, // S_FANG_PINCHLOBSHOT5
+
+	{SPR_PLAY,         0, 0, {A_DoNPCPain},                    0, 0, S_FANG_DIE2, 0}, // S_FANG_DIE1
+	{SPR_PLAY, SPR2_MSC4, 1, {A_Boss5CheckOnGround}, S_FANG_DIE3, 0, S_FANG_DIE2, 0}, // S_FANG_DIE2
+
+	{SPR_PLAY,         0,  0, {A_Scream}, 0, 0, S_FANG_DIE4, 0}, // S_FANG_DIE3
+	{SPR_PLAY, SPR2_MSC5, -1, {A_SetFuse}, 70, 0, S_FANG_DIE5, 0}, // S_FANG_DIE4
+
+	{SPR_PLAY,         0, 0, {A_PlaySound}, sfx_jump, 0, S_FANG_DIE6, 0}, // S_FANG_DIE5
+	{SPR_PLAY, SPR2_JUMP, 1, {A_ZThrust}, 6, (1<<16)|1, S_FANG_DIE7, 0}, // S_FANG_DIE6
+	{SPR_PLAY, SPR2_JUMP, 1, {A_Boss5CheckFalling}, S_FANG_FLEEPATHING1, S_FANG_DIE8, S_FANG_DIE7, 0}, // S_FANG_DIE7
+	{SPR_PLAY, SPR2_FALL, 1, {A_Boss5CheckOnGround}, S_FANG_FLEEPATHING1, 0, S_FANG_DIE8, 0}, // S_FANG_DIE8
+
+	{SPR_PLAY,         0,  0, {A_PlayActiveSound}, 0, 0, S_FANG_FLEEPATHING2, 0}, // S_FANG_FLEEPATHING1
+	{SPR_PLAY, SPR2_LAND,  2, {A_Boss5FindWaypoint}, 2, 0, S_FANG_FLEEBOUNCE1, 0}, // S_FANG_FLEEPATHING2
+	{SPR_PLAY, SPR2_LAND,  2, {NULL}, 0, 0, S_FANG_FLEEBOUNCE2, 0}, // S_FANG_FLEEBOUNCE1
+	{SPR_PLAY, SPR2_LAND, -1, {A_BossDeath}, 0, 0, S_NULL, 0}, // S_FANG_FLEEBOUNCE2
+
+	{SPR_PLAY, SPR2_DEAD, 7*TICRATE, {NULL}, 0, 0, S_NULL, 0}, // S_FANG_KO
 
 	{SPR_NULL, 0, -1, {A_RandomStateRange}, S_BROKENROBOTA, S_BROKENROBOTF, S_NULL, 0}, // S_BROKENROBOTRANDOM
 	{SPR_BRKN,    FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 3, 4, S_NULL, 0}, // S_BROKENROBOTA
@@ -1760,22 +1790,22 @@ state_t states[NUMSTATES] =
 	// Metal Sonic
 	{SPR_PLAY, SPR2_STND, -1, {NULL}, 0, 0, S_METALSONIC_RACE, 0}, // S_METALSONIC_RACE
 
-	{SPR_METL,  4, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0},             // S_METALSONIC_FLOAT
-	{SPR_METL, 16|FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_METALSONIC_STUN, 0}, // S_METALSONIC_VECTOR
-	{SPR_METL, 15, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0}, // S_METALSONIC_STUN
-	{SPR_METL, 17, 20, {NULL},         0, 0, S_METALSONIC_GATHER, 0},// S_METALSONIC_RAISE
-	{SPR_METL, 18, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0},             // S_METALSONIC_GATHER
-	{SPR_METL,  6|FF_FULLBRIGHT|FF_ANIMATE|FF_GLOBALANIM, -1, {NULL}, 1, 2, S_METALSONIC_BOUNCE, 0},// S_METALSONIC_DASH
-	{SPR_METL, 18|FF_FULLBRIGHT|FF_ANIMATE|FF_GLOBALANIM, -1, {NULL}, 1, 2, S_METALSONIC_FLOAT, 0},             // S_METALSONIC_BOUNCE
-	{SPR_METL, 14, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0},             // S_METALSONIC_BADBOUNCE
-	{SPR_METL, 17, -1, {NULL},         0, 0, S_METALSONIC_GATHER, 0},// S_METALSONIC_SHOOT
-	{SPR_METL, 15, 40, {A_Pain},       0, 0, S_METALSONIC_FLOAT, 0}, // S_METALSONIC_PAIN
-	{SPR_METL, 17,  2, {A_Fall},       0, 0, S_METALSONIC_DEATH2, 0},// S_METALSONIC_DEATH1
-	{SPR_METL, 17,  4, {A_BossScream}, 0, 0, S_METALSONIC_DEATH3, 0},// S_METALSONIC_DEATH2
-	{SPR_METL, 17,  0, {A_Repeat}, 17, S_METALSONIC_DEATH2, S_METALSONIC_DEATH4, 0}, // S_METALSONIC_DEATH3
-	{SPR_METL, 17, -1, {A_BossDeath},  0, 0, S_NULL, 0},             // S_METALSONIC_DEATH4
-	{SPR_METL, 15,  1, {A_BossScream},         0, 0, S_METALSONIC_FLEE2, 0}, // S_METALSONIC_FLEE1
-	{SPR_METL, 15,  7, {NULL},                 0, 0, S_METALSONIC_FLEE1, 0}, // S_METALSONIC_FLEE2
+	{SPR_PLAY,               SPR2_WALK, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0}, // S_METALSONIC_FLOAT
+	{SPR_PLAY, SPR2_MSC1|FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_METALSONIC_STUN, 0}, // S_METALSONIC_VECTOR
+	{SPR_PLAY,               SPR2_MSC0, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0},// S_METALSONIC_STUN
+	{SPR_PLAY, SPR2_SPNG, 20, {NULL},         0, 0, S_METALSONIC_GATHER, 0}, // S_METALSONIC_RAISE
+	{SPR_PLAY, SPR2_MSC2, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0}, // S_METALSONIC_GATHER
+	{SPR_PLAY, SPR2_DASH|FF_FULLBRIGHT, -1, {NULL}, 1, 2, S_METALSONIC_BOUNCE, 0}, // S_METALSONIC_DASH
+	{SPR_PLAY, SPR2_MSC2|FF_FULLBRIGHT, -1, {NULL}, 1, 2, S_METALSONIC_FLOAT, 0}, // S_METALSONIC_BOUNCE
+	{SPR_PLAY, SPR2_PAIN, -1, {NULL},         0, 0, S_METALSONIC_FLOAT, 0}, // S_METALSONIC_BADBOUNCE
+	{SPR_PLAY, SPR2_SPNG, -1, {NULL},         0, 0, S_METALSONIC_GATHER, 0}, // S_METALSONIC_SHOOT
+	{SPR_PLAY, SPR2_FLT, 40, {A_Pain},       0, 0, S_METALSONIC_FLOAT, 0}, // S_METALSONIC_PAIN
+	{SPR_PLAY, SPR2_SPNG,  2, {A_Fall},       0, 0, S_METALSONIC_DEATH2, 0}, // S_METALSONIC_DEATH1
+	{SPR_PLAY, SPR2_SPNG,  4, {A_BossScream}, 0, 0, S_METALSONIC_DEATH3, 0}, // S_METALSONIC_DEATH2
+	{SPR_PLAY, SPR2_SPNG,  0, {A_Repeat}, 17, S_METALSONIC_DEATH2, S_METALSONIC_DEATH4, 0}, // S_METALSONIC_DEATH3
+	{SPR_PLAY, SPR2_SPNG, -1, {A_BossDeath},  0, 0, S_NULL, 0}, // S_METALSONIC_DEATH4
+	{SPR_PLAY, SPR2_FLT,  1, {A_BossScream},         0, 0, S_METALSONIC_FLEE2, 0}, // S_METALSONIC_FLEE1
+	{SPR_PLAY, SPR2_FLT,  7, {NULL},                 0, 0, S_METALSONIC_FLEE1, 0}, // S_METALSONIC_FLEE2
 
 	{SPR_MSCF, FF_FULLBRIGHT|FF_TRANS30|FF_ANIMATE, -1, {NULL}, 11, 1, S_NULL, 0},  // S_MSSHIELD_F1
 	{SPR_MSCF, FF_FULLBRIGHT|FF_ANIMATE|12, -1, {NULL}, 8, 2, S_NULL, 0},  // S_MSSHIELD_F2
@@ -1784,8 +1814,8 @@ state_t states[NUMSTATES] =
 	{SPR_RING, FF_ANIMATE|FF_GLOBALANIM, -1, {NULL}, 23, 1, S_RING, 0}, // S_RING
 
 	// Blue Sphere for special stages
-	{SPR_SPHR, FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL, 0}, // S_BLUESPHERE
-	{SPR_SPHR, FF_FULLBRIGHT
+	{SPR_SPHR, FF_SEMIBRIGHT, -1, {NULL}, 0, 0, S_NULL, 0}, // S_BLUESPHERE
+	{SPR_SPHR, FF_SEMIBRIGHT
 #ifdef MANIASPHERES
 							|FF_ANIMATE|FF_RANDOMANIM
 #endif
@@ -1794,13 +1824,13 @@ state_t states[NUMSTATES] =
 
 	// Bomb Sphere
 	{SPR_SPHR, FF_FULLBRIGHT|3, 2, {NULL}, 0, 0, S_BOMBSPHERE2, 0}, // S_BOMBSPHERE1
-	{SPR_SPHR, FF_FULLBRIGHT|4, 1, {NULL}, 0, 0, S_BOMBSPHERE3, 0}, // S_BOMBSPHERE2
-	{SPR_SPHR, FF_FULLBRIGHT|5, 2, {NULL}, 0, 0, S_BOMBSPHERE4, 0}, // S_BOMBSPHERE3
-	{SPR_SPHR, FF_FULLBRIGHT|4, 1, {NULL}, 0, 0, S_BOMBSPHERE1, 0}, // S_BOMBSPHERE4
+	{SPR_SPHR, FF_SEMIBRIGHT|4, 1, {NULL}, 0, 0, S_BOMBSPHERE3, 0}, // S_BOMBSPHERE2
+	{SPR_SPHR, FF_SEMIBRIGHT|5, 2, {NULL}, 0, 0, S_BOMBSPHERE4, 0}, // S_BOMBSPHERE3
+	{SPR_SPHR, FF_SEMIBRIGHT|4, 1, {NULL}, 0, 0, S_BOMBSPHERE1, 0}, // S_BOMBSPHERE4
 
 	// NiGHTS Chip
-	{SPR_NCHP, FF_FULLBRIGHT|FF_ANIMATE,    -1, {NULL}, 15, 2, S_NULL, 0}, // S_NIGHTSCHIP
-	{SPR_NCHP, FF_FULLBRIGHT|FF_ANIMATE|16, -1, {NULL}, 15, 2, S_NULL, 0}, // S_NIGHTSCHIPBONUS
+	{SPR_NCHP, FF_SEMIBRIGHT|FF_ANIMATE,    -1, {NULL}, 15, 2, S_NULL, 0}, // S_NIGHTSCHIP
+	{SPR_NCHP, FF_SEMIBRIGHT|FF_ANIMATE|16, -1, {NULL}, 15, 2, S_NULL, 0}, // S_NIGHTSCHIPBONUS
 
 	// NiGHTS Star
 	{SPR_NSTR, FF_ANIMATE, -1, {NULL}, 14, 2, S_NULL, 0}, // S_NIGHTSSTAR
@@ -1858,9 +1888,9 @@ state_t states[NUMSTATES] =
 	{SPR_CEMG, FF_FULLBRIGHT|6, -1, {NULL}, 0, 0, S_NULL, 0}, // S_CEMG7
 
 	// Emerald hunt shards
-	{SPR_SHRD, 0, -1, {NULL}, 0, 0, S_NULL, 0}, // S_SHRD1
-	{SPR_SHRD, 1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_SHRD2
-	{SPR_SHRD, 2, -1, {NULL}, 0, 0, S_NULL, 0}, // S_SHRD3
+	{SPR_SHRD, FF_SEMIBRIGHT,   -1, {NULL}, 0, 0, S_NULL, 0}, // S_SHRD1
+	{SPR_SHRD, FF_SEMIBRIGHT|1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_SHRD2
+	{SPR_SHRD, FF_SEMIBRIGHT|2, -1, {NULL}, 0, 0, S_NULL, 0}, // S_SHRD3
 
 	// Bubble Source
 	{SPR_BBLS, 0, 8, {A_BubbleSpawn}, 2048, 0, S_BUBBLES2, 0}, // S_BUBBLES1
@@ -2629,6 +2659,10 @@ state_t states[NUMSTATES] =
 	{SPR_XMS4, 1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_LAMPPOST2
 	{SPR_XMS5, 0, -1, {NULL}, 0, 0, S_NULL, 0}, // S_HANGSTAR
 	{SPR_XMS6, 0, -1, {NULL}, 0, 0, S_NULL, 0}, // S_MISTLETOE
+	{SPR_SNTT, 0, -1, {NULL}, 0, 0, S_NULL, 0}, 				// S_SSZTREE
+	{SPR_SNTT, 1|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL, 0}, 	// S_SSZTREE_BRANCH
+	{SPR_SSTT, 0, -1, {NULL}, 0, 0, S_NULL, 0}, 				// S_SSZTREE2
+	{SPR_SSTT, 1|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL, 0}, 	// S_SSZTREE2_BRANCH
 	// Xmas GFZ bushes
 	{SPR_BUS3, 1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_XMASBLUEBERRYBUSH
 	{SPR_BUS1, 1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_XMASBERRYBUSH
@@ -2636,16 +2670,15 @@ state_t states[NUMSTATES] =
 	// FHZ
 	{SPR_FHZI, 0, -1, {NULL}, 0, 0, S_NULL, 0}, // S_FHZICE1
 	{SPR_FHZI, 1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_FHZICE2
-	{SPR_ROSY, 16, 8, {NULL}, 0, 0, S_ROSY_IDLE2, 0}, // S_ROSY_IDLE1
-	{SPR_ROSY, 17, 4, {NULL}, 0, 0, S_ROSY_IDLE3, 0}, // S_ROSY_IDLE2
-	{SPR_ROSY, 18, 8, {NULL}, 0, 0, S_ROSY_IDLE4, 0}, // S_ROSY_IDLE3
-	{SPR_ROSY, 17, 4, {NULL}, 0, 0, S_ROSY_IDLE1, 0}, // S_ROSY_IDLE4
-	{SPR_ROSY, 14, -1, {NULL}, 1, 0, S_NULL, 0}, // S_ROSY_JUMP
-	{SPR_ROSY,  5, -1, {NULL}, 7, 0, S_NULL, 0}, // S_ROSY_WALK
-	{SPR_ROSY, 19, -1, {NULL}, 0, 0, S_NULL, 0}, // S_ROSY_HUG
-	{SPR_ROSY, 13, -1, {NULL}, 0, 0, S_NULL, 0}, // S_ROSY_PAIN
-	{SPR_ROSY,  1|FF_ANIMATE, -1, {NULL}, 3, 16, S_NULL, 0}, // S_ROSY_STND
-	{SPR_ROSY, 20|FF_ANIMATE, TICRATE, {NULL}, 3, 4, S_ROSY_WALK, 0}, // S_ROSY_UNHAPPY
+	// Amy FHZ cameo
+	{SPR_PLAY, SPR2_CNT1|FF_ANIMATE,      -1, {NULL}, 0, 0, S_NULL, 0}, // S_ROSY_IDLE
+	{SPR_PLAY,            SPR2_MSC0,      -1, {NULL}, 0, 0, S_NULL, 0}, // S_ROSY_JUMP
+	{SPR_PLAY,            SPR2_MSC1,      -1, {NULL}, 0, 0, S_NULL, 0}, // S_ROSY_FALL
+	{SPR_PLAY,            SPR2_WALK,      -1, {NULL}, 7, 0, S_NULL, 0}, // S_ROSY_WALK
+	{SPR_PLAY,            SPR2_MSC2,      -1, {NULL}, 0, 0, S_NULL, 0}, // S_ROSY_HUG
+	{SPR_PLAY,            SPR2_PAIN,      -1, {NULL}, 0, 0, S_NULL, 0}, // S_ROSY_PAIN
+	{SPR_PLAY, SPR2_WAIT|FF_ANIMATE,      -1, {NULL}, 0, 5, S_NULL, 0}, // S_ROSY_STND
+	{SPR_PLAY, SPR2_MSC3|FF_ANIMATE, TICRATE, {NULL}, 0, 4, S_ROSY_WALK, 0}, // S_ROSY_UNHAPPY
 
 	// Halloween Scenery
 	// Pumpkins
@@ -2752,6 +2785,10 @@ state_t states[NUMSTATES] =
 
 	{SPR_ESTA, 1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_EGGSTATUE2
 
+	{SPR_GINE, 0, -1, {NULL}, 0, 0, S_NULL, 0}, // S_GINE
+	{SPR_PPAL, 0, -1, {NULL}, 0, 0, S_NULL, 0}, // S_PPAL
+	{SPR_PPAL, 1, -1, {NULL}, 0, 0, S_NULL, 0}, // S_PPEL
+
 	// Shield Orb
 	{SPR_ARMA, FF_TRANS40   , 2, {NULL}, 0, 0, S_ARMA2 , 0}, // S_ARMA1
 	{SPR_ARMA, FF_TRANS40| 1, 2, {NULL}, 0, 0, S_ARMA3 , 0}, // S_ARMA2
@@ -3995,6 +4032,11 @@ state_t states[NUMSTATES] =
 	{SPR_BRIY, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 31, 1, S_NULL, 0}, // S_YELLOWBRICKDEBRIS
 
 	{SPR_NULL, 0, 1, {NULL}, 0, 0, S_NULL, 0}, // S_NAMECHECK
+
+	{SPR_OLDK, FF_ANIMATE, -1, {NULL}, 1, 16, S_NULL, 0}, // S_OLDK_STND
+	{SPR_OLDK, 2, 0, {A_ForceWin}, 0, 0, S_OLDK_DIE1, 0}, // S_OLDK_DIE0
+	{SPR_OLDK, 2, 0, {A_Scream}, 0, 0, S_OLDK_DIE2, 0}, // S_OLDK_DIE1
+	{SPR_OLDK, 2, -1, {A_ZThrust}, 14, 1|(1<<16), S_NULL, 0}, // S_OLDK_DIE2
 };
 
 mobjinfo_t mobjinfo[NUMMOBJTYPES] =
@@ -4145,7 +4187,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // painstate
 		0,              // painchance
 		sfx_None,       // painsound
-		S_NULL,         // meleestate
+		S_JETFUMEFLASH, // meleestate
 		S_NULL,         // missilestate
 		S_NULL,         // deathstate
 		S_NULL,         // xdeathstate
@@ -14523,6 +14565,114 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_SSZTREE
+		1860,           // doomednum
+		S_SSZTREE,    	// spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		20*FRACUNIT,    // radius
+		256*FRACUNIT,   // height
+		0,              // display offset
+		100,            // mass
+		1,              // damage
+		sfx_None,       // activesound
+		MF_SOLID|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_SSZTREE_BRANCH
+		-1,		        // doomednum
+		S_SSZTREE_BRANCH,    // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		20*FRACUNIT,    // radius
+		256*FRACUNIT,   // height
+		0,              // display offset
+		100,            // mass
+		1,              // damage
+		sfx_None,       // activesound
+		MF_NOBLOCKMAP|MF_NOCLIP|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_SSZTREE2
+		1861,           // doomednum
+		S_SSZTREE2,    	// spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		20*FRACUNIT,    // radius
+		256*FRACUNIT,   // height
+		0,              // display offset
+		100,            // mass
+		1,              // damage
+		sfx_None,       // activesound
+		MF_SOLID|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_SSZTREE2_BRANCH
+		-1,		        // doomednum
+		S_SSZTREE2_BRANCH,    // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		20*FRACUNIT,    // radius
+		256*FRACUNIT,   // height
+		0,              // display offset
+		100,            // mass
+		1,              // damage
+		sfx_None,       // activesound
+		MF_NOBLOCKMAP|MF_NOCLIP|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_XMASBLUEBERRYBUSH
 		1859,           // doomednum
 		S_XMASBLUEBERRYBUSH, // spawnstate
@@ -14660,7 +14810,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 
 	{           // MT_ROSY
 		2104,           // doomednum
-		S_ROSY_IDLE1,   // spawnstate
+		S_ROSY_IDLE,    // spawnstate
 		1000,           // spawnhealth
 		S_NULL,         // seestate
 		sfx_None,       // seesound
@@ -16362,6 +16512,87 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{           // MT_GINE
+		3048,           // doomednum
+		S_GINE,         // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		32*FRACUNIT,    // radius
+		628*FRACUNIT,   // height
+		0,              // display offset
+		100,            // mass
+		1,              // damage
+		sfx_None,       // activesound
+		MF_SOLID|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_PPAL
+		3050,           // doomednum
+		S_PPAL,         // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		16*FRACUNIT,    // radius
+		626*FRACUNIT,   // height
+		0,              // display offset
+		100,            // mass
+		1,              // damage
+		sfx_None,       // activesound
+		MF_SOLID|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
+	{           // MT_PPEL
+		3051,           // doomednum
+		S_PPEL,         // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		0,              // speed
+		16*FRACUNIT,    // radius
+		517*FRACUNIT,   // height
+		0,              // display offset
+		100,            // mass
+		1,              // damage
+		sfx_None,       // activesound
+		MF_SOLID|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
+
 	{           // MT_ELEMENTAL_ORB
 		-1,             // doomednum
 		S_ELEM1,        // spawnstate
@@ -21604,6 +21835,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		MF_NOBLOCKMAP|MF_NOSECTOR|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_SCENERY, // flags
 		S_NULL          // raisestate
 	},
+
+	{			// MT_OLDK
+		666,            // doomednum
+		S_OLDK_STND,	// spawnstate
+		1,              // spawnhealth
+		S_NULL, 		// seestate
+		sfx_None,		// seesound
+		32,             // reactiontime
+		sfx_None,       // attacksound
+		S_OLDK_DIE0,    // painstate
+		128,            // painchance
+		sfx_s3k35,      // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_OLDK_DIE0,	// deathstate
+		S_NULL,         // xdeathstate
+		sfx_s3k35,      // deathsound
+		2*FRACUNIT,		// speed
+		32*FRACUNIT,    // radius
+		64*FRACUNIT,    // height
+		0,              // display offset
+		1000,           // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_SPECIAL|MF_SHOOTABLE|MF_NOGRAVITY|MF_BOSS, // flags
+		S_NULL          // raisestate
+	},
 };
 
 skincolor_t skincolors[MAXSKINCOLORS] = {
@@ -21639,15 +21897,15 @@ skincolor_t skincolors[MAXSKINCOLORS] = {
 	{"Moss",      {0x58, 0x58, 0x59, 0x59, 0x5a, 0x5a, 0x5b, 0x5b, 0x5b, 0x5c, 0x5d, 0x5d, 0x5e, 0x5e, 0x5f, 0x5f}, SKINCOLOR_BEIGE,     13, V_GREENMAP,  true}, // SKINCOLOR_MOSS
 	{"Azure",     {0x90, 0x90, 0x91, 0x91, 0xaa, 0xaa, 0xab, 0xab, 0xab, 0xac, 0xad, 0xad, 0xae, 0xae, 0xaf, 0xaf}, SKINCOLOR_PINK,      5,  V_AZUREMAP,  true}, // SKINCOLOR_AZURE
 	{"Eggplant",  {   4,   8,    11,   11,   16,  195,  195,  195,  196,  186,  187,  187,  254,  254,   30,   31}, SKINCOLOR_ROSEBUSH,  5,  V_PURPLEMAP, true}, // SKINCOLOR_EGGPLANT
-	{"Lavender",  {0xc0, 0xc0, 0xc1, 0xc1, 0xc2, 0xc2, 0xc3, 0xc3, 0xc3, 0xc4, 0xc5, 0xc5, 0xc6, 0xc6, 0xc7, 0xc7}, SKINCOLOR_GOLD,      4,  V_PURPLEMAP, true}, // SKINCOLOR_LAVENDER
+	{"Lavender",  {0xc0, 0xc0, 0xc1, 0xc1, 0xc2, 0xc2, 0xc3, 0xc3, 0xc3, 0xc4, 0xc5, 0xc5, 0xc6, 0xc6, 0xc7, 0xc7}, SKINCOLOR_HEADLIGHT, 8,  V_PURPLEMAP, true}, // SKINCOLOR_LAVENDER
 
 	// Viv's vivid colours (toast 21/07/17)
 	// Tweaks & additions (Lach, Chrispy, sphere, Alice, MotorRoach & Saneko 26/10/22)
 	{"Ruby",       {0xb0, 0xb0, 0xc9, 0xca, 0xcc, 0x26, 0x27, 0x28, 0x29, 0x2a, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xfd}, SKINCOLOR_EMERALD,    10, V_REDMAP,     true}, // SKINCOLOR_RUBY
 	{"Cherry",     { 202,  203,  204,  205,  206,   40,   41,   42,   43,   44,  186,  187,   28,   29,   30,   31}, SKINCOLOR_MIDNIGHT,   10, V_REDMAP,     true}, // SKINCOLOR_CHERRY
 	{"Salmon",     {0xd0, 0xd0, 0xd1, 0xd2, 0x20, 0x21, 0x24, 0x25, 0x26, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e}, SKINCOLOR_FOREST,     6,  V_REDMAP,     true}, // SKINCOLOR_SALMON
-	{"Pepper",     { 210,   32,   33,   34,   35,   35,   36,   37,   38,   39,   41,   43,   45,   45,   46,   47}, SKINCOLOR_MASTER,     8,  V_REDMAP,     true}, // SKINCOLOR_PEPPER
-	{"Red",        {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x47, 0x2e, 0x2f}, SKINCOLOR_GREEN,      10, V_REDMAP,     true}, // SKINCOLOR_RED
+	{"Pepper",     { 210,   32,   33,   34,   35,   35,   36,   37,   38,   39,   41,   43,   45,   45,   46,   47}, SKINCOLOR_GREEN,      10, V_REDMAP,     true}, // SKINCOLOR_PEPPER
+	{"Red",        {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x47, 0x2e, 0x2f}, SKINCOLOR_MASTER,     8,  V_REDMAP,     true}, // SKINCOLOR_RED
 	{"Crimson",    {0x27, 0x27, 0x28, 0x28, 0x29, 0x2a, 0x2b, 0x2b, 0x2c, 0x2d, 0x2e, 0x2e, 0x2e, 0x2f, 0x2f, 0x1f}, SKINCOLOR_ICY,        10, V_REDMAP,     true}, // SKINCOLOR_CRIMSON
 	{"Flame",      {0x31, 0x32, 0x33, 0x36, 0x22, 0x22, 0x25, 0x25, 0x25, 0xcd, 0xcf, 0xcf, 0xc5, 0xc5, 0xc7, 0xc7}, SKINCOLOR_PURPLE,     8,  V_REDMAP,     true}, // SKINCOLOR_FLAME
 	{"Garnet",     {   0,   83,   50,   53,   34,   35,   37,   38,   39,   40,   42,   44,   45,   46,   47,   47}, SKINCOLOR_AQUAMARINE, 6,  V_REDMAP,     true}, // SKINCOLOR_GARNET
@@ -21662,7 +21920,7 @@ skincolor_t skincolors[MAXSKINCOLORS] = {
 	{"Rust",       {0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3c, 0x3d, 0x3d, 0x3d, 0x3f, 0x2c, 0x2d, 0x47, 0x2e, 0x2f, 0x2f}, SKINCOLOR_YOGURT,     8,  V_ORANGEMAP,  true}, // SKINCOLOR_RUST
 	{"Tangerine",  {  81,   83,   64,   64,   51,   52,   53,   54,   56,   58,   60,   61,   63,   45,   46,   47}, SKINCOLOR_OCEAN,      12, V_ORANGEMAP,  true}, // SKINCOLOR_TANGERINE
 	{"Topaz",      {   0,   81,   83,   73,   74,   74,   65,   52,   53,   54,   56,   58,   60,   42,   43,   45}, SKINCOLOR_MOONSTONE,  10, V_YELLOWMAP,  true}, // SKINCOLOR_TOPAZ
-	{"Gold",       {0x51, 0x51, 0x54, 0x54, 0x41, 0x42, 0x43, 0x43, 0x44, 0x45, 0x46, 0x3f, 0x2d, 0x2e, 0x2f, 0x2f}, SKINCOLOR_LAVENDER,   10, V_YELLOWMAP,  true}, // SKINCOLOR_GOLD
+	{"Gold",       {0x51, 0x51, 0x54, 0x54, 0x41, 0x42, 0x43, 0x43, 0x44, 0x45, 0x46, 0x3f, 0x2d, 0x2e, 0x2f, 0x2f}, SKINCOLOR_MAUVE,      8,  V_YELLOWMAP,  true}, // SKINCOLOR_GOLD
 	{"Sandy",      {0x53, 0x40, 0x41, 0x42, 0x43, 0xe6, 0xe9, 0xe9, 0xea, 0xec, 0xec, 0xc6, 0xc6, 0xc7, 0xc7, 0xfe}, SKINCOLOR_SKY,        8,  V_YELLOWMAP,  true}, // SKINCOLOR_SANDY
 	{"Goldenrod",  {   0,   80,   81,   81,   83,   73,   73,   64,   65,   66,   67,   68,   69,   62,   44,   45}, SKINCOLOR_MAJESTY,    8,  V_YELLOWMAP,  true}, // SKINCOLOR_GOLDENROD
 	{"Yellow",     {0x52, 0x53, 0x49, 0x49, 0x4a, 0x4a, 0x4b, 0x4b, 0x4b, 0x4c, 0x4d, 0x4d, 0x4e, 0x4e, 0x4f, 0xed}, SKINCOLOR_CORNFLOWER, 8,  V_YELLOWMAP,  true}, // SKINCOLOR_YELLOW
@@ -21672,19 +21930,19 @@ skincolor_t skincolors[MAXSKINCOLORS] = {
 	{"Lime",       {0x50, 0x51, 0x52, 0x53, 0x48, 0xbc, 0xbd, 0xbe, 0xbe, 0xbf, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f}, SKINCOLOR_MAGENTA,    9,  V_PERIDOTMAP, true}, // SKINCOLOR_LIME
 	{"Peridot",    {0x58, 0x58, 0xbc, 0xbc, 0xbd, 0xbd, 0xbe, 0xbe, 0xbe, 0xbf, 0x5e, 0x5e, 0x5f, 0x5f, 0x77, 0x77}, SKINCOLOR_COBALT,     2,  V_PERIDOTMAP, true}, // SKINCOLOR_PERIDOT
 	{"Apple",      {0x49, 0x49, 0xbc, 0xbd, 0xbe, 0xbe, 0xbe, 0x67, 0x69, 0x6a, 0x6b, 0x6b, 0x6c, 0x6d, 0x6d, 0x6d}, SKINCOLOR_RASPBERRY,  13, V_PERIDOTMAP, true}, // SKINCOLOR_APPLE
-	{"Headlight",  {   0,   80,   81,   82,   73,   84,   64,   65,   91,   91,  124,  125,  126,  137,  138,  139}, SKINCOLOR_MAUVE,      8,  V_YELLOWMAP,  true}, // SKINCOLOR_HEADLIGHT
+	{"Headlight",  {   0,   80,   81,   82,   73,   84,   64,   65,   91,   91,  124,  125,  126,  137,  138,  139}, SKINCOLOR_LAVENDER,   10, V_YELLOWMAP,  true}, // SKINCOLOR_HEADLIGHT
 	{"Chartreuse", {  80,   82,   72,   73,  188,  188,  113,  114,  114,  125,  126,  137,  138,  139,  253,  254}, SKINCOLOR_NOBLE,      9,  V_PERIDOTMAP, true}, // SKINCOLOR_CHARTREUSE
-	{"Green",      {0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f}, SKINCOLOR_RED,        6,  V_GREENMAP,   true}, // SKINCOLOR_GREEN
+	{"Green",      {0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f}, SKINCOLOR_PEPPER,     8,  V_GREENMAP,   true}, // SKINCOLOR_GREEN
 	{"Forest",     {0x65, 0x66, 0x67, 0x68, 0x69, 0x69, 0x6a, 0x6b, 0x6b, 0x6c, 0x6d, 0x6d, 0x6e, 0x6e, 0x6e, 0x6f}, SKINCOLOR_SALMON,     9,  V_GREENMAP,   true}, // SKINCOLOR_FOREST
 	{"Shamrock",   {0x70, 0x70, 0x71, 0x71, 0x72, 0x72, 0x73, 0x73, 0x73, 0x74, 0x75, 0x75, 0x76, 0x76, 0x77, 0x77}, SKINCOLOR_SIBERITE,   10, V_GREENMAP,   true}, // SKINCOLOR_SHAMROCK
-	{"Jade",       { 128,  120,  121,  122,  122,  113,  114,  114,  115,  116,  117,  118,  119,  110,  111,   30}, SKINCOLOR_TAFFY,      10, V_GREENMAP,   true}, // SKINCOLOR_JADE
+	{"Jade",       { 128,  120,  121,  122,  122,  113,  114,  114,  115,  116,  117,  118,  119,  110,  111,   30}, SKINCOLOR_ROSY,       7,  V_GREENMAP,   true}, // SKINCOLOR_JADE
 	{"Mint",       {0x00, 0x00, 0x58, 0x58, 0x59, 0x62, 0x62, 0x62, 0x64, 0x67, 0x7e, 0x7e, 0x8f, 0x8f, 0x8a, 0x8a}, SKINCOLOR_VIOLET,     5,  V_GREENMAP,   true}, // SKINCOLOR_MINT
-	{"Master",     {   0,   80,   88,   96,  112,  113,   99,  100,  124,  125,  126,  117,  107,  118,  119,  111}, SKINCOLOR_PEPPER,     8,  V_GREENMAP,   true}, // SKINCOLOR_MASTER
+	{"Master",     {   0,   80,   88,   96,  112,  113,   99,  100,  124,  125,  126,  117,  107,  118,  119,  111}, SKINCOLOR_RED,        6,  V_GREENMAP,   true}, // SKINCOLOR_MASTER
 	{"Emerald",    {  80,   96,  112,  113,  114,  114,  125,  125,  126,  126,  137,  137,  138,  138,  139,  139}, SKINCOLOR_RUBY,       9,  V_GREENMAP,   true}, // SKINCOLOR_EMERALD
 	{"Seafoam",    {0x01, 0x58, 0x59, 0x5a, 0x7c, 0x7d, 0x7d, 0x7e, 0x7e, 0x8f, 0x8f, 0x8a, 0x8a, 0x8b, 0xfd, 0xfd}, SKINCOLOR_PLUM,       6,  V_AQUAMAP,    true}, // SKINCOLOR_SEAFOAM
 	{"Island",     {  96,   97,  113,  113,  114,  124,  142,  136,  136,  150,  151,  153,  168,  168,  169,  169}, SKINCOLOR_GALAXY,     7,  V_AQUAMAP,    true}, // SKINCOLOR_ISLAND
 	{"Bottle",     {   0,    1,    3,    4,    5,  140,  141,  141,  124,  125,  126,  127,  118,  119,  111,  111}, SKINCOLOR_LATTE,      14, V_AQUAMAP,    true}, // SKINCOLOR_BOTTLE
-	{"Aqua",       {0x78, 0x79, 0x7a, 0x7a, 0x7b, 0x7b, 0x7c, 0x7c, 0x7c, 0x7d, 0x7e, 0x7e, 0x7f, 0x7f, 0x76, 0x77}, SKINCOLOR_ROSY,       7,  V_AQUAMAP,    true}, // SKINCOLOR_AQUA
+	{"Aqua",       {0x78, 0x79, 0x7a, 0x7a, 0x7b, 0x7b, 0x7c, 0x7c, 0x7c, 0x7d, 0x7e, 0x7e, 0x7f, 0x7f, 0x76, 0x77}, SKINCOLOR_TAFFY,      10, V_AQUAMAP,    true}, // SKINCOLOR_AQUA
 	{"Teal",       {0x78, 0x78, 0x8c, 0x8c, 0x8d, 0x8d, 0x8d, 0x8e, 0x8e, 0x8f, 0x8f, 0x8f, 0x8a, 0x8a, 0x8a, 0x8a}, SKINCOLOR_PEACHY,     7,  V_SKYMAP,     true}, // SKINCOLOR_TEAL
 	{"Ocean",      { 120,  121,  122,  122,  123,  141,  142,  142,  136,  137,  138,  138,  139,  139,  253,  253}, SKINCOLOR_TANGERINE,  4,  V_AQUAMAP,    true}, // SKINCOLOR_OCEAN
 	{"Wave",       {0x00, 0x78, 0x78, 0x79, 0x8d, 0x87, 0x88, 0x89, 0x89, 0xae, 0xa8, 0xa8, 0xa9, 0xa9, 0xfd, 0xfd}, SKINCOLOR_QUAIL,      5,  V_SKYMAP,     true}, // SKINCOLOR_WAVE
@@ -21718,12 +21976,12 @@ skincolor_t skincolors[MAXSKINCOLORS] = {
 	{"Violet",     {0xd0, 0xd1, 0xd2, 0xca, 0xcc, 0xb8, 0xb9, 0xb9, 0xba, 0xa8, 0xa8, 0xa9, 0xa9, 0xfd, 0xfe, 0xfe}, SKINCOLOR_MINT,       6,  V_MAGENTAMAP, true}, // SKINCOLOR_VIOLET
 	{"Royal",      { 208,  209,  192,  192,  192,  193,  193,  194,  194,  172,  173,  174,  175,  175,  139,  139}, SKINCOLOR_FANCY,      9,  V_PURPLEMAP,  true}, // SKINCOLOR_ROYAL
 	{"Lilac",      {0x00, 0xd0, 0xd1, 0xd2, 0xd3, 0xc1, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc5, 0xc6, 0xc6, 0xfe, 0x1f}, SKINCOLOR_VAPOR,      4,  V_ROSYMAP,    true}, // SKINCOLOR_LILAC
-	{"Mauve",      { 176,  177,  178,  192,  193,  194,  195,  195,  196,  185,  185,  186,  186,  187,  187,  253}, SKINCOLOR_HEADLIGHT,  8,  V_PURPLEMAP,  true}, // SKINCOLOR_MAUVE
+	{"Mauve",      { 176,  177,  178,  192,  193,  194,  195,  195,  196,  185,  185,  186,  186,  187,  187,  253}, SKINCOLOR_GOLD,       4,  V_PURPLEMAP,  true}, // SKINCOLOR_MAUVE
 	{"Eventide",   {  51,   52,   53,   33,   34,  204,  183,  183,  184,  184,  166,  167,  168,  169,  253,  254}, SKINCOLOR_DAYBREAK,   13, V_MAGENTAMAP, true}, // SKINCOLOR_EVENTIDE
 	{"Plum",       {0xc8, 0xd3, 0xd5, 0xd6, 0xd7, 0xce, 0xcf, 0xb9, 0xb9, 0xba, 0xba, 0xa9, 0xa9, 0xa9, 0xfd, 0xfe}, SKINCOLOR_MINT,       7,  V_ROSYMAP,    true}, // SKINCOLOR_PLUM
 	{"Raspberry",  {0xc8, 0xc9, 0xca, 0xcb, 0xcb, 0xcc, 0xcd, 0xcd, 0xce, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xfe, 0xfe}, SKINCOLOR_APPLE,      13, V_ROSYMAP,    true}, // SKINCOLOR_RASPBERRY
-	{"Taffy",      {   1,  176,  176,  177,  178,  179,  202,  203,  204,  204,  205,  206,  207,   44,   45,   46}, SKINCOLOR_JADE,       8,  V_ROSYMAP,    true}, // SKINCOLOR_TAFFY
-	{"Rosy",       {0xfc, 0xc8, 0xc8, 0xc9, 0xc9, 0xca, 0xca, 0xcb, 0xcb, 0xcc, 0xcc, 0xcd, 0xcd, 0xce, 0xce, 0xcf}, SKINCOLOR_AQUA,       1,  V_ROSYMAP,    true}, // SKINCOLOR_ROSY
+	{"Taffy",      {   1,  176,  176,  177,  178,  179,  202,  203,  204,  204,  205,  206,  207,   44,   45,   46}, SKINCOLOR_AQUA,       1,  V_ROSYMAP,    true}, // SKINCOLOR_TAFFY
+	{"Rosy",       {0xfc, 0xc8, 0xc8, 0xc9, 0xc9, 0xca, 0xca, 0xcb, 0xcb, 0xcc, 0xcc, 0xcd, 0xcd, 0xce, 0xce, 0xcf}, SKINCOLOR_JADE,       8,  V_ROSYMAP,    true}, // SKINCOLOR_ROSY
 	{"Fancy",      {   0,  208,   49,  210,  210,  202,  202,  203,  204,  204,  205,  206,  207,  207,  186,  186}, SKINCOLOR_ROYAL,      9,  V_ROSYMAP,    true}, // SKINCOLOR_FANCY
 	{"Sangria",    { 210,   32,   33,   34,   34,  215,  215,  207,  207,  185,  186,  186,  186,  169,  169,  253}, SKINCOLOR_TURQUOISE,  12, V_ROSYMAP,    true}, // SKINCOLOR_SANGRIA
 	{"Volcanic",   {  54,   36,   42,   44,   45,   46,   46,   47,   28,  253,  253,  254,  254,   30,   31,   31}, SKINCOLOR_BRONZE,     9,  V_REDMAP,     true}, // SKINCOLOR_VOLCANIC
diff --git a/src/info.h b/src/info.h
index 0361f64281150bec03676bd1b8a4baa36a18de22..94b0aeb48111178e2262a1f1144c7ff623fb3a43 100644
--- a/src/info.h
+++ b/src/info.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -298,276 +298,278 @@ enum actionnum
 	NUMACTIONS
 };
 
+struct mobj_s;
+
 // IMPORTANT NOTE: If you add/remove from this list of action
 // functions, don't forget to update them in deh_tables.c!
-void A_Explode();
-void A_Pain();
-void A_Fall();
-void A_MonitorPop();
-void A_GoldMonitorPop();
-void A_GoldMonitorRestore();
-void A_GoldMonitorSparkle();
-void A_Look();
-void A_Chase();
-void A_FaceStabChase();
-void A_FaceStabRev();
-void A_FaceStabHurl();
-void A_FaceStabMiss();
-void A_StatueBurst();
-void A_FaceTarget();
-void A_FaceTracer();
-void A_Scream();
-void A_BossDeath();
-void A_SetShadowScale();
-void A_ShadowScream(); // MARIA!!!!!!
-void A_CustomPower(); // Use this for a custom power
-void A_GiveWeapon(); // Gives the player weapon(s)
-void A_RingBox(); // Obtained Ring Box Tails
-void A_Invincibility(); // Obtained Invincibility Box
-void A_SuperSneakers(); // Obtained Super Sneakers Box
-void A_BunnyHop(); // have bunny hop tails
-void A_BubbleSpawn(); // Randomly spawn bubbles
-void A_FanBubbleSpawn();
-void A_BubbleRise(); // Bubbles float to surface
-void A_BubbleCheck(); // Don't draw if not underwater
-void A_AwardScore();
-void A_ExtraLife(); // Extra Life
-void A_GiveShield(); // Obtained Shield
-void A_GravityBox();
-void A_ScoreRise(); // Rise the score logo
-void A_AttractChase(); // Ring Chase
-void A_DropMine(); // Drop Mine from Skim or Jetty-Syn Bomber
-void A_FishJump(); // Fish Jump
-void A_ThrownRing(); // Sparkle trail for red ring
-void A_SetSolidSteam();
-void A_UnsetSolidSteam();
-void A_SignSpin();
-void A_SignPlayer();
-void A_OverlayThink();
-void A_JetChase();
-void A_JetbThink(); // Jetty-Syn Bomber Thinker
-void A_JetgThink(); // Jetty-Syn Gunner Thinker
-void A_JetgShoot(); // Jetty-Syn Shoot Function
-void A_ShootBullet(); // JetgShoot without reactiontime setting
-void A_MinusDigging();
-void A_MinusPopup();
-void A_MinusCheck();
-void A_ChickenCheck();
-void A_MouseThink(); // Mouse Thinker
-void A_DetonChase(); // Deton Chaser
-void A_CapeChase(); // Fake little Super Sonic cape
-void A_RotateSpikeBall(); // Spike ball rotation
-void A_SlingAppear();
-void A_UnidusBall();
-void A_RockSpawn();
-void A_SetFuse();
-void A_CrawlaCommanderThink(); // Crawla Commander
-void A_SmokeTrailer();
-void A_RingExplode();
-void A_OldRingExplode();
-void A_MixUp();
-void A_RecyclePowers();
-void A_BossScream();
-void A_Boss2TakeDamage();
-void A_GoopSplat();
-void A_Boss2PogoSFX();
-void A_Boss2PogoTarget();
-void A_EggmanBox();
-void A_TurretFire();
-void A_SuperTurretFire();
-void A_TurretStop();
-void A_JetJawRoam();
-void A_JetJawChomp();
-void A_PointyThink();
-void A_CheckBuddy();
-void A_HoodFire();
-void A_HoodThink();
-void A_HoodFall();
-void A_ArrowBonks();
-void A_SnailerThink();
-void A_SharpChase();
-void A_SharpSpin();
-void A_SharpDecel();
-void A_CrushstaceanWalk();
-void A_CrushstaceanPunch();
-void A_CrushclawAim();
-void A_CrushclawLaunch();
-void A_VultureVtol();
-void A_VultureCheck();
-void A_VultureHover();
-void A_VultureBlast();
-void A_VultureFly();
-void A_SkimChase();
-void A_SkullAttack();
-void A_LobShot();
-void A_FireShot();
-void A_SuperFireShot();
-void A_BossFireShot();
-void A_Boss7FireMissiles();
-void A_Boss1Laser();
-void A_FocusTarget();
-void A_Boss4Reverse();
-void A_Boss4SpeedUp();
-void A_Boss4Raise();
-void A_SparkFollow();
-void A_BuzzFly();
-void A_GuardChase();
-void A_EggShield();
-void A_SetReactionTime();
-void A_Boss1Spikeballs();
-void A_Boss3TakeDamage();
-void A_Boss3Path();
-void A_Boss3ShockThink();
-void A_Shockwave();
-void A_LinedefExecute();
-void A_LinedefExecuteFromArg();
-void A_PlaySeeSound();
-void A_PlayAttackSound();
-void A_PlayActiveSound();
-void A_1upThinker();
-void A_BossZoom(); //Unused
-void A_Boss1Chase();
-void A_Boss2Chase();
-void A_Boss2Pogo();
-void A_Boss7Chase();
-void A_BossJetFume();
-void A_SpawnObjectAbsolute();
-void A_SpawnObjectRelative();
-void A_ChangeAngleRelative();
-void A_ChangeAngleAbsolute();
-void A_RollAngle();
-void A_ChangeRollAngleRelative();
-void A_ChangeRollAngleAbsolute();
-void A_PlaySound();
-void A_FindTarget();
-void A_FindTracer();
-void A_SetTics();
-void A_SetRandomTics();
-void A_ChangeColorRelative();
-void A_ChangeColorAbsolute();
-void A_Dye();
-void A_SetTranslation();
-void A_MoveRelative();
-void A_MoveAbsolute();
-void A_Thrust();
-void A_ZThrust();
-void A_SetTargetsTarget();
-void A_SetObjectFlags();
-void A_SetObjectFlags2();
-void A_RandomState();
-void A_RandomStateRange();
-void A_StateRangeByAngle();
-void A_StateRangeByParameter();
-void A_DualAction();
-void A_RemoteAction();
-void A_ToggleFlameJet();
-void A_OrbitNights();
-void A_GhostMe();
-void A_SetObjectState();
-void A_SetObjectTypeState();
-void A_KnockBack();
-void A_PushAway();
-void A_RingDrain();
-void A_SplitShot();
-void A_MissileSplit();
-void A_MultiShot();
-void A_InstaLoop();
-void A_Custom3DRotate();
-void A_SearchForPlayers();
-void A_CheckRandom();
-void A_CheckTargetRings();
-void A_CheckRings();
-void A_CheckTotalRings();
-void A_CheckHealth();
-void A_CheckRange();
-void A_CheckHeight();
-void A_CheckTrueRange();
-void A_CheckThingCount();
-void A_CheckAmbush();
-void A_CheckCustomValue();
-void A_CheckCusValMemo();
-void A_SetCustomValue();
-void A_UseCusValMemo();
-void A_RelayCustomValue();
-void A_CusValAction();
-void A_ForceStop();
-void A_ForceWin();
-void A_SpikeRetract();
-void A_InfoState();
-void A_Repeat();
-void A_SetScale();
-void A_RemoteDamage();
-void A_HomingChase();
-void A_TrapShot();
-void A_VileTarget();
-void A_VileAttack();
-void A_VileFire();
-void A_BrakChase();
-void A_BrakFireShot();
-void A_BrakLobShot();
-void A_NapalmScatter();
-void A_SpawnFreshCopy();
-void A_FlickySpawn();
-void A_FlickyCenter();
-void A_FlickyAim();
-void A_FlickyFly();
-void A_FlickySoar();
-void A_FlickyCoast();
-void A_FlickyHop();
-void A_FlickyFlounder();
-void A_FlickyCheck();
-void A_FlickyHeightCheck();
-void A_FlickyFlutter();
-void A_FlameParticle();
-void A_FadeOverlay();
-void A_Boss5Jump();
-void A_LightBeamReset();
-void A_MineExplode();
-void A_MineRange();
-void A_ConnectToGround();
-void A_SpawnParticleRelative();
-void A_MultiShotDist();
-void A_WhoCaresIfYourSonIsABee();
-void A_ParentTriesToSleep();
-void A_CryingToMomma();
-void A_CheckFlags2();
-void A_Boss5FindWaypoint();
-void A_DoNPCSkid();
-void A_DoNPCPain();
-void A_PrepareRepeat();
-void A_Boss5ExtraRepeat();
-void A_Boss5Calm();
-void A_Boss5CheckOnGround();
-void A_Boss5CheckFalling();
-void A_Boss5PinchShot();
-void A_Boss5MakeItRain();
-void A_Boss5MakeJunk();
-void A_LookForBetter();
-void A_Boss5BombExplode();
-void A_DustDevilThink();
-void A_TNTExplode();
-void A_DebrisRandom();
-void A_TrainCameo();
-void A_TrainCameo2();
-void A_CanarivoreGas();
-void A_KillSegments();
-void A_SnapperSpawn();
-void A_SnapperThinker();
-void A_SaloonDoorSpawn();
-void A_MinecartSparkThink();
-void A_ModuloToState();
-void A_LavafallRocks();
-void A_LavafallLava();
-void A_FallingLavaCheck();
-void A_FireShrink();
-void A_SpawnPterabytes();
-void A_PterabyteHover();
-void A_RolloutSpawn();
-void A_RolloutRock();
-void A_DragonbomberSpawn();
-void A_DragonWing();
-void A_DragonSegment();
-void A_ChangeHeight();
+void A_Explode(struct mobj_s *actor);
+void A_Pain(struct mobj_s *actor);
+void A_Fall(struct mobj_s *actor);
+void A_MonitorPop(struct mobj_s *actor);
+void A_GoldMonitorPop(struct mobj_s *actor);
+void A_GoldMonitorRestore(struct mobj_s *actor);
+void A_GoldMonitorSparkle(struct mobj_s *actor);
+void A_Look(struct mobj_s *actor);
+void A_Chase(struct mobj_s *actor);
+void A_FaceStabChase(struct mobj_s *actor);
+void A_FaceStabRev(struct mobj_s *actor);
+void A_FaceStabHurl(struct mobj_s *actor);
+void A_FaceStabMiss(struct mobj_s *actor);
+void A_StatueBurst(struct mobj_s *actor);
+void A_FaceTarget(struct mobj_s *actor);
+void A_FaceTracer(struct mobj_s *actor);
+void A_Scream(struct mobj_s *actor);
+void A_BossDeath(struct mobj_s *actor);
+void A_SetShadowScale(struct mobj_s *actor);
+void A_ShadowScream(struct mobj_s *actor); // MARIA!!!!!!
+void A_CustomPower(struct mobj_s *actor); // Use this for a custom power
+void A_GiveWeapon(struct mobj_s *actor); // Gives the player weapon(s)
+void A_RingBox(struct mobj_s *actor); // Obtained Ring Box Tails
+void A_Invincibility(struct mobj_s *actor); // Obtained Invincibility Box
+void A_SuperSneakers(struct mobj_s *actor); // Obtained Super Sneakers Box
+void A_BunnyHop(struct mobj_s *actor); // have bunny hop tails
+void A_BubbleSpawn(struct mobj_s *actor); // Randomly spawn bubbles
+void A_FanBubbleSpawn(struct mobj_s *actor);
+void A_BubbleRise(struct mobj_s *actor); // Bubbles float to surface
+void A_BubbleCheck(struct mobj_s *actor); // Don't draw if not underwater
+void A_AwardScore(struct mobj_s *actor);
+void A_ExtraLife(struct mobj_s *actor); // Extra Life
+void A_GiveShield(struct mobj_s *actor); // Obtained Shield
+void A_GravityBox(struct mobj_s *actor);
+void A_ScoreRise(struct mobj_s *actor); // Rise the score logo
+void A_AttractChase(struct mobj_s *actor); // Ring Chase
+void A_DropMine(struct mobj_s *actor); // Drop Mine from Skim or Jetty-Syn Bomber
+void A_FishJump(struct mobj_s *actor); // Fish Jump
+void A_ThrownRing(struct mobj_s *actor); // Sparkle trail for red ring
+void A_SetSolidSteam(struct mobj_s *actor);
+void A_UnsetSolidSteam(struct mobj_s *actor);
+void A_SignSpin(struct mobj_s *actor);
+void A_SignPlayer(struct mobj_s *actor);
+void A_OverlayThink(struct mobj_s *actor);
+void A_JetChase(struct mobj_s *actor);
+void A_JetbThink(struct mobj_s *actor); // Jetty-Syn Bomber Thinker
+void A_JetgThink(struct mobj_s *actor); // Jetty-Syn Gunner Thinker
+void A_JetgShoot(struct mobj_s *actor); // Jetty-Syn Shoot Function
+void A_ShootBullet(struct mobj_s *actor); // JetgShoot without reactiontime setting
+void A_MinusDigging(struct mobj_s *actor);
+void A_MinusPopup(struct mobj_s *actor);
+void A_MinusCheck(struct mobj_s *actor);
+void A_ChickenCheck(struct mobj_s *actor);
+void A_MouseThink(struct mobj_s *actor); // Mouse Thinker
+void A_DetonChase(struct mobj_s *actor); // Deton Chaser
+void A_CapeChase(struct mobj_s *actor); // Fake little Super Sonic cape
+void A_RotateSpikeBall(struct mobj_s *actor); // Spike ball rotation
+void A_SlingAppear(struct mobj_s *actor);
+void A_UnidusBall(struct mobj_s *actor);
+void A_RockSpawn(struct mobj_s *actor);
+void A_SetFuse(struct mobj_s *actor);
+void A_CrawlaCommanderThink(struct mobj_s *actor); // Crawla Commander
+void A_SmokeTrailer(struct mobj_s *actor);
+void A_RingExplode(struct mobj_s *actor);
+void A_OldRingExplode(struct mobj_s *actor);
+void A_MixUp(struct mobj_s *actor);
+void A_RecyclePowers(struct mobj_s *actor);
+void A_BossScream(struct mobj_s *actor);
+void A_Boss2TakeDamage(struct mobj_s *actor);
+void A_GoopSplat(struct mobj_s *actor);
+void A_Boss2PogoSFX(struct mobj_s *actor);
+void A_Boss2PogoTarget(struct mobj_s *actor);
+void A_EggmanBox(struct mobj_s *actor);
+void A_TurretFire(struct mobj_s *actor);
+void A_SuperTurretFire(struct mobj_s *actor);
+void A_TurretStop(struct mobj_s *actor);
+void A_JetJawRoam(struct mobj_s *actor);
+void A_JetJawChomp(struct mobj_s *actor);
+void A_PointyThink(struct mobj_s *actor);
+void A_CheckBuddy(struct mobj_s *actor);
+void A_HoodFire(struct mobj_s *actor);
+void A_HoodThink(struct mobj_s *actor);
+void A_HoodFall(struct mobj_s *actor);
+void A_ArrowBonks(struct mobj_s *actor);
+void A_SnailerThink(struct mobj_s *actor);
+void A_SharpChase(struct mobj_s *actor);
+void A_SharpSpin(struct mobj_s *actor);
+void A_SharpDecel(struct mobj_s *actor);
+void A_CrushstaceanWalk(struct mobj_s *actor);
+void A_CrushstaceanPunch(struct mobj_s *actor);
+void A_CrushclawAim(struct mobj_s *actor);
+void A_CrushclawLaunch(struct mobj_s *actor);
+void A_VultureVtol(struct mobj_s *actor);
+void A_VultureCheck(struct mobj_s *actor);
+void A_VultureHover(struct mobj_s *actor);
+void A_VultureBlast(struct mobj_s *actor);
+void A_VultureFly(struct mobj_s *actor);
+void A_SkimChase(struct mobj_s *actor);
+void A_SkullAttack(struct mobj_s *actor);
+void A_LobShot(struct mobj_s *actor);
+void A_FireShot(struct mobj_s *actor);
+void A_SuperFireShot(struct mobj_s *actor);
+void A_BossFireShot(struct mobj_s *actor);
+void A_Boss7FireMissiles(struct mobj_s *actor);
+void A_Boss1Laser(struct mobj_s *actor);
+void A_FocusTarget(struct mobj_s *actor);
+void A_Boss4Reverse(struct mobj_s *actor);
+void A_Boss4SpeedUp(struct mobj_s *actor);
+void A_Boss4Raise(struct mobj_s *actor);
+void A_SparkFollow(struct mobj_s *actor);
+void A_BuzzFly(struct mobj_s *actor);
+void A_GuardChase(struct mobj_s *actor);
+void A_EggShield(struct mobj_s *actor);
+void A_SetReactionTime(struct mobj_s *actor);
+void A_Boss1Spikeballs(struct mobj_s *actor);
+void A_Boss3TakeDamage(struct mobj_s *actor);
+void A_Boss3Path(struct mobj_s *actor);
+void A_Boss3ShockThink(struct mobj_s *actor);
+void A_Shockwave(struct mobj_s *actor);
+void A_LinedefExecute(struct mobj_s *actor);
+void A_LinedefExecuteFromArg(struct mobj_s *actor);
+void A_PlaySeeSound(struct mobj_s *actor);
+void A_PlayAttackSound(struct mobj_s *actor);
+void A_PlayActiveSound(struct mobj_s *actor);
+void A_1upThinker(struct mobj_s *actor);
+void A_BossZoom(struct mobj_s *actor); //Unused
+void A_Boss1Chase(struct mobj_s *actor);
+void A_Boss2Chase(struct mobj_s *actor);
+void A_Boss2Pogo(struct mobj_s *actor);
+void A_Boss7Chase(struct mobj_s *actor);
+void A_BossJetFume(struct mobj_s *actor);
+void A_SpawnObjectAbsolute(struct mobj_s *actor);
+void A_SpawnObjectRelative(struct mobj_s *actor);
+void A_ChangeAngleRelative(struct mobj_s *actor);
+void A_ChangeAngleAbsolute(struct mobj_s *actor);
+void A_RollAngle(struct mobj_s *actor);
+void A_ChangeRollAngleRelative(struct mobj_s *actor);
+void A_ChangeRollAngleAbsolute(struct mobj_s *actor);
+void A_PlaySound(struct mobj_s *actor);
+void A_FindTarget(struct mobj_s *actor);
+void A_FindTracer(struct mobj_s *actor);
+void A_SetTics(struct mobj_s *actor);
+void A_SetRandomTics(struct mobj_s *actor);
+void A_ChangeColorRelative(struct mobj_s *actor);
+void A_ChangeColorAbsolute(struct mobj_s *actor);
+void A_Dye(struct mobj_s *actor);
+void A_SetTranslation(struct mobj_s *actor);
+void A_MoveRelative(struct mobj_s *actor);
+void A_MoveAbsolute(struct mobj_s *actor);
+void A_Thrust(struct mobj_s *actor);
+void A_ZThrust(struct mobj_s *actor);
+void A_SetTargetsTarget(struct mobj_s *actor);
+void A_SetObjectFlags(struct mobj_s *actor);
+void A_SetObjectFlags2(struct mobj_s *actor);
+void A_RandomState(struct mobj_s *actor);
+void A_RandomStateRange(struct mobj_s *actor);
+void A_StateRangeByAngle(struct mobj_s *actor);
+void A_StateRangeByParameter(struct mobj_s *actor);
+void A_DualAction(struct mobj_s *actor);
+void A_RemoteAction(struct mobj_s *actor);
+void A_ToggleFlameJet(struct mobj_s *actor);
+void A_OrbitNights(struct mobj_s *actor);
+void A_GhostMe(struct mobj_s *actor);
+void A_SetObjectState(struct mobj_s *actor);
+void A_SetObjectTypeState(struct mobj_s *actor);
+void A_KnockBack(struct mobj_s *actor);
+void A_PushAway(struct mobj_s *actor);
+void A_RingDrain(struct mobj_s *actor);
+void A_SplitShot(struct mobj_s *actor);
+void A_MissileSplit(struct mobj_s *actor);
+void A_MultiShot(struct mobj_s *actor);
+void A_InstaLoop(struct mobj_s *actor);
+void A_Custom3DRotate(struct mobj_s *actor);
+void A_SearchForPlayers(struct mobj_s *actor);
+void A_CheckRandom(struct mobj_s *actor);
+void A_CheckTargetRings(struct mobj_s *actor);
+void A_CheckRings(struct mobj_s *actor);
+void A_CheckTotalRings(struct mobj_s *actor);
+void A_CheckHealth(struct mobj_s *actor);
+void A_CheckRange(struct mobj_s *actor);
+void A_CheckHeight(struct mobj_s *actor);
+void A_CheckTrueRange(struct mobj_s *actor);
+void A_CheckThingCount(struct mobj_s *actor);
+void A_CheckAmbush(struct mobj_s *actor);
+void A_CheckCustomValue(struct mobj_s *actor);
+void A_CheckCusValMemo(struct mobj_s *actor);
+void A_SetCustomValue(struct mobj_s *actor);
+void A_UseCusValMemo(struct mobj_s *actor);
+void A_RelayCustomValue(struct mobj_s *actor);
+void A_CusValAction(struct mobj_s *actor);
+void A_ForceStop(struct mobj_s *actor);
+void A_ForceWin(struct mobj_s *actor);
+void A_SpikeRetract(struct mobj_s *actor);
+void A_InfoState(struct mobj_s *actor);
+void A_Repeat(struct mobj_s *actor);
+void A_SetScale(struct mobj_s *actor);
+void A_RemoteDamage(struct mobj_s *actor);
+void A_HomingChase(struct mobj_s *actor);
+void A_TrapShot(struct mobj_s *actor);
+void A_VileTarget(struct mobj_s *actor);
+void A_VileAttack(struct mobj_s *actor);
+void A_VileFire(struct mobj_s *actor);
+void A_BrakChase(struct mobj_s *actor);
+void A_BrakFireShot(struct mobj_s *actor);
+void A_BrakLobShot(struct mobj_s *actor);
+void A_NapalmScatter(struct mobj_s *actor);
+void A_SpawnFreshCopy(struct mobj_s *actor);
+void A_FlickySpawn(struct mobj_s *actor);
+void A_FlickyCenter(struct mobj_s *actor);
+void A_FlickyAim(struct mobj_s *actor);
+void A_FlickyFly(struct mobj_s *actor);
+void A_FlickySoar(struct mobj_s *actor);
+void A_FlickyCoast(struct mobj_s *actor);
+void A_FlickyHop(struct mobj_s *actor);
+void A_FlickyFlounder(struct mobj_s *actor);
+void A_FlickyCheck(struct mobj_s *actor);
+void A_FlickyHeightCheck(struct mobj_s *actor);
+void A_FlickyFlutter(struct mobj_s *actor);
+void A_FlameParticle(struct mobj_s *actor);
+void A_FadeOverlay(struct mobj_s *actor);
+void A_Boss5Jump(struct mobj_s *actor);
+void A_LightBeamReset(struct mobj_s *actor);
+void A_MineExplode(struct mobj_s *actor);
+void A_MineRange(struct mobj_s *actor);
+void A_ConnectToGround(struct mobj_s *actor);
+void A_SpawnParticleRelative(struct mobj_s *actor);
+void A_MultiShotDist(struct mobj_s *actor);
+void A_WhoCaresIfYourSonIsABee(struct mobj_s *actor);
+void A_ParentTriesToSleep(struct mobj_s *actor);
+void A_CryingToMomma(struct mobj_s *actor);
+void A_CheckFlags2(struct mobj_s *actor);
+void A_Boss5FindWaypoint(struct mobj_s *actor);
+void A_DoNPCSkid(struct mobj_s *actor);
+void A_DoNPCPain(struct mobj_s *actor);
+void A_PrepareRepeat(struct mobj_s *actor);
+void A_Boss5ExtraRepeat(struct mobj_s *actor);
+void A_Boss5Calm(struct mobj_s *actor);
+void A_Boss5CheckOnGround(struct mobj_s *actor);
+void A_Boss5CheckFalling(struct mobj_s *actor);
+void A_Boss5PinchShot(struct mobj_s *actor);
+void A_Boss5MakeItRain(struct mobj_s *actor);
+void A_Boss5MakeJunk(struct mobj_s *actor);
+void A_LookForBetter(struct mobj_s *actor);
+void A_Boss5BombExplode(struct mobj_s *actor);
+void A_DustDevilThink(struct mobj_s *actor);
+void A_TNTExplode(struct mobj_s *actor);
+void A_DebrisRandom(struct mobj_s *actor);
+void A_TrainCameo(struct mobj_s *actor);
+void A_TrainCameo2(struct mobj_s *actor);
+void A_CanarivoreGas(struct mobj_s *actor);
+void A_KillSegments(struct mobj_s *actor);
+void A_SnapperSpawn(struct mobj_s *actor);
+void A_SnapperThinker(struct mobj_s *actor);
+void A_SaloonDoorSpawn(struct mobj_s *actor);
+void A_MinecartSparkThink(struct mobj_s *actor);
+void A_ModuloToState(struct mobj_s *actor);
+void A_LavafallRocks(struct mobj_s *actor);
+void A_LavafallLava(struct mobj_s *actor);
+void A_FallingLavaCheck(struct mobj_s *actor);
+void A_FireShrink(struct mobj_s *actor);
+void A_SpawnPterabytes(struct mobj_s *actor);
+void A_PterabyteHover(struct mobj_s *actor);
+void A_RolloutSpawn(struct mobj_s *actor);
+void A_RolloutRock(struct mobj_s *actor);
+void A_DragonbomberSpawn(struct mobj_s *actor);
+void A_DragonWing(struct mobj_s *actor);
+void A_DragonSegment(struct mobj_s *actor);
+void A_ChangeHeight(struct mobj_s *actor);
 
 extern int actionsoverridden[NUMACTIONS][MAX_ACTION_RECURSION];
 
@@ -855,6 +857,8 @@ typedef enum sprite
 	SPR_XMS4, // Lamppost
 	SPR_XMS5, // Hanging Star
 	SPR_XMS6, // Mistletoe
+	SPR_SNTT, // Silver Shiver tree
+	SPR_SSTT, // Silver Shiver tree with snow
 	SPR_FHZI, // FHZ Ice
 	SPR_ROSY,
 
@@ -888,6 +892,8 @@ typedef enum sprite
 	// Misc Scenery
 	SPR_STLG, // Stalagmites
 	SPR_DBAL, // Disco
+	SPR_GINE, // Crystalline Heights tree
+	SPR_PPAL, // Pristine Shores palm trees
 
 	// Powerup Indicators
 	SPR_ARMA, // Armageddon Shield Orb
@@ -1074,6 +1080,9 @@ typedef enum sprite
 	SPR_GWLG,
 	SPR_GWLR,
 
+	// LJ Knuckles
+	SPR_OLDK,
+
 	SPR_FIRSTFREESLOT,
 	SPR_LASTFREESLOT = SPR_FIRSTFREESLOT + NUMSPRITEFREESLOTS - 1,
 	NUMSPRITES
@@ -1147,6 +1156,18 @@ typedef enum playersprite
 	SPR2_TALB,
 	SPR2_TALC,
 
+	// Misc slots
+	SPR2_MSC0,
+	SPR2_MSC1,
+	SPR2_MSC2,
+	SPR2_MSC3,
+	SPR2_MSC4,
+	SPR2_MSC5,
+	SPR2_MSC6,
+	SPR2_MSC7,
+	SPR2_MSC8,
+	SPR2_MSC9,
+
 	SPR2_CNT1, // continue disappointment
 	SPR2_CNT2, // continue lift
 	SPR2_CNT3, // continue spin
@@ -1894,11 +1915,11 @@ typedef enum state
 	S_FANG_FIRE1,
 	S_FANG_FIRE2,
 	S_FANG_FIRE3,
-	S_FANG_FIRE4,
 	S_FANG_FIREREPEAT,
 	S_FANG_LOBSHOT0,
 	S_FANG_LOBSHOT1,
 	S_FANG_LOBSHOT2,
+	S_FANG_LOBSHOT3,
 	S_FANG_WAIT1,
 	S_FANG_WAIT2,
 	S_FANG_WALLHIT,
@@ -1920,6 +1941,7 @@ typedef enum state
 	S_FANG_PINCHLOBSHOT2,
 	S_FANG_PINCHLOBSHOT3,
 	S_FANG_PINCHLOBSHOT4,
+	S_FANG_PINCHLOBSHOT5,
 	S_FANG_DIE1,
 	S_FANG_DIE2,
 	S_FANG_DIE3,
@@ -3058,6 +3080,10 @@ typedef enum state
 	S_LAMPPOST2,  // with snow
 	S_HANGSTAR,
 	S_MISTLETOE,
+	S_SSZTREE,
+	S_SSZTREE_BRANCH,
+	S_SSZTREE2,
+	S_SSZTREE2_BRANCH,
 	// Xmas GFZ bushes
 	S_XMASBLUEBERRYBUSH,
 	S_XMASBERRYBUSH,
@@ -3065,11 +3091,9 @@ typedef enum state
 	// FHZ
 	S_FHZICE1,
 	S_FHZICE2,
-	S_ROSY_IDLE1,
-	S_ROSY_IDLE2,
-	S_ROSY_IDLE3,
-	S_ROSY_IDLE4,
+	S_ROSY_IDLE,
 	S_ROSY_JUMP,
+	S_ROSY_FALL,
 	S_ROSY_WALK,
 	S_ROSY_HUG,
 	S_ROSY_PAIN,
@@ -3178,6 +3202,9 @@ typedef enum state
 	S_DBALL5,
 	S_DBALL6,
 	S_EGGSTATUE2,
+	S_GINE,
+	S_PPAL,
+	S_PPEL,
 
 	// Shield Orb
 	S_ARMA1,
@@ -4062,6 +4089,7 @@ typedef enum state
 	S_MARIOBUSH2,
 	S_TOAD,
 
+
 	// Nights-specific stuff
 	S_NIGHTSDRONE_MAN1,
 	S_NIGHTSDRONE_MAN2,
@@ -4366,6 +4394,12 @@ typedef enum state
 
 	S_NAMECHECK,
 
+	// LJ Knuckles
+	S_OLDK_STND,
+	S_OLDK_DIE0,
+	S_OLDK_DIE1,
+	S_OLDK_DIE2,
+
 	S_FIRSTFREESLOT,
 	S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1,
 	NUMSTATES
@@ -4857,6 +4891,10 @@ typedef enum mobj_type
 	MT_LAMPPOST2,  // with snow
 	MT_HANGSTAR,
 	MT_MISTLETOE,
+	MT_SSZTREE,
+	MT_SSZTREE_BRANCH,
+	MT_SSZTREE2,
+	MT_SSZTREE2_BRANCH,
 	// Xmas GFZ bushes
 	MT_XMASBLUEBERRYBUSH,
 	MT_XMASBERRYBUSH,
@@ -4936,6 +4974,9 @@ typedef enum mobj_type
 	// Misc scenery
 	MT_DBALL,
 	MT_EGGSTATUE2,
+	MT_GINE,
+	MT_PPAL,
+	MT_PPEL,
 
 	// Powerup Indicators
 	MT_ELEMENTAL_ORB, // Elemental shield mobj
@@ -5164,6 +5205,8 @@ typedef enum mobj_type
 	MT_NAMECHECK,
 	MT_RAY, // General purpose mobj
 
+	MT_OLDK,
+
 	MT_FIRSTFREESLOT,
 	MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1,
 	NUMMOBJTYPES
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index f6b8f462b5a41cee30c33145fd62fa28daa01e92..9eaf784defedcf9f6321e91d2f45223cbbd90310 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -184,8 +184,10 @@ static const struct {
 	{META_SKIN,         "skin_t"},
 	{META_POWERS,       "player_t.powers"},
 	{META_SOUNDSID,     "skin_t.soundsid"},
-	{META_SKINSPRITES,  "skin_t.sprites"},
-	{META_SKINSPRITESLIST,  "skin_t.sprites[]"},
+
+	{META_SKINSPRITES,       "skin_t.skinsprites"},
+	{META_SKINSPRITESLIST,   "skin_t.skinsprites[]"},
+	{META_SKINSPRITESCOMPAT, "skin_t.sprites"}, // TODO: 2.3: Delete
 
 	{META_VERTEX,       "vertex_t"},
 	{META_LINE,         "line_t"},
@@ -236,7 +238,10 @@ static const struct {
 	{META_LUABANKS,     "luabanks[]"},
 
 	{META_KEYEVENT,     "keyevent_t"},
+	{META_TEXTEVENT,    "textevent_t"},
 	{META_MOUSE,        "mouse_t"},
+	
+	{META_INTERCEPT,	"intercept_t"},
 	{NULL,              NULL}
 };
 
@@ -345,6 +350,18 @@ static int lib_reserveLuabanks(lua_State *L)
 	return 1;
 }
 
+static int lib_tofixed(lua_State *L)
+{
+	const char *arg = luaL_checkstring(L, 1);
+	char *end;
+	float f = strtof(arg, &end);
+	if (*end != '\0')
+		lua_pushnil(L);
+	else
+		lua_pushnumber(L, FLOAT_TO_FIXED(f));
+	return 1;
+}
+
 // M_MENU
 //////////////
 
@@ -641,7 +658,7 @@ static int lib_pSpawnMobj(lua_State *L)
 	NOHUD
 	INLEVEL
 	NOSPAWNNULL
-	LUA_PushUserdata(L, P_SpawnMobj(x, y, z, type), META_MOBJ);
+	LUA_PushUserdata(L, P_SpawnMobj(x, y, z, type, NULL), META_MOBJ);
 	return 1;
 }
 
@@ -1716,12 +1733,11 @@ static int lib_pResetCamera(lua_State *L)
 static int lib_pSuperReady(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
-	boolean transform = (boolean)lua_opttrueboolean(L, 2);
 	//HUDSAFE
 	INLEVEL
 	if (!player)
 		return LUA_ErrInvalid(L, "player_t");
-	lua_pushboolean(L, P_SuperReady(player, transform));
+	lua_pushboolean(L, P_SuperReady(player));
 	return 1;
 }
 
@@ -1974,7 +1990,7 @@ static int lib_pLineIsBlocking(lua_State *L)
 		return LUA_ErrInvalid(L, "mobj_t");
 	if (!line)
 		return LUA_ErrInvalid(L, "line_t");
-	
+
 	// P_LineOpening in P_LineIsBlocking sets these variables.
 	// We want to keep their old values after so that whatever
 	// map collision code uses them doesn't get messed up.
@@ -1987,9 +2003,9 @@ static int lib_pLineIsBlocking(lua_State *L)
 	pslope_t *oldopenbottomslope = openbottomslope;
 	ffloor_t *oldopenfloorrover = openfloorrover;
 	ffloor_t *oldopenceilingrover = openceilingrover;
-	
+
 	lua_pushboolean(L, P_LineIsBlocking(mo, line));
-	
+
 	opentop = oldopentop;
 	openbottom = oldopenbottom;
 	openrange = oldopenrange;
@@ -1999,7 +2015,7 @@ static int lib_pLineIsBlocking(lua_State *L)
 	openbottomslope = oldopenbottomslope;
 	openfloorrover = oldopenfloorrover;
 	openceilingrover = oldopenceilingrover;
-	
+
 	return 1;
 }
 
@@ -3153,7 +3169,10 @@ static int lib_rCheckTextureNumForName(lua_State *L)
 {
 	const char *name = luaL_checkstring(L, 1);
 	//HUDSAFE
-	lua_pushinteger(L, R_CheckTextureNumForName(name));
+	INT32 num = R_CheckTextureNumForName(name, TEXTURETYPE_TEXTURE);
+	if (num == -1)
+		num = R_CheckTextureNumForName(name, TEXTURETYPE_FLAT);
+	lua_pushinteger(L, num);
 	return 1;
 }
 
@@ -3167,17 +3186,25 @@ static int lib_rTextureNumForName(lua_State *L)
 
 static int lib_rCheckTextureNameForNum(lua_State *L)
 {
+	char s[9];
 	INT32 num = (INT32)luaL_checkinteger(L, 1);
 	//HUDSAFE
-	lua_pushstring(L, R_CheckTextureNameForNum(num));
+
+	M_Memcpy(s, R_CheckTextureNameForNum(num), 8);
+	s[8] = '\0';
+	lua_pushstring(L, s);
 	return 1;
 }
 
 static int lib_rTextureNameForNum(lua_State *L)
 {
+	char s[9];
 	INT32 num = (INT32)luaL_checkinteger(L, 1);
 	//HUDSAFE
-	lua_pushstring(L, R_TextureNameForNum(num));
+
+	M_Memcpy(s, R_TextureNameForNum(num), 8);
+	s[8] = '\0';
+	lua_pushstring(L, s);
 	return 1;
 }
 
@@ -3864,7 +3891,7 @@ static int lib_gAddPlayer(lua_State *L)
 	player_t *newplayer;
 	SINT8 skinnum = 0, bot;
 
-	for (i = 0; i < MAXPLAYERS; i++)
+	for (i = 1; i < MAXPLAYERS; i++)
 	{
 		if (!playeringame[i])
 			break;
@@ -4333,6 +4360,7 @@ static luaL_Reg lib[] = {
 	{"userdataMetatable", lib_userdataMetatable},
 	{"IsPlayerAdmin", lib_isPlayerAdmin},
 	{"reserveLuabanks", lib_reserveLuabanks},
+	{"tofixed", lib_tofixed},
 
 	// m_menu
 	{"M_MoveColorAfter",lib_pMoveColorAfter},
diff --git a/src/lua_blockmaplib.c b/src/lua_blockmaplib.c
index c29eadecc625214615c2546abd2216c9b6630254..f570c229b97b138d6d10e714c4c2cdd8175b1596 100644
--- a/src/lua_blockmaplib.c
+++ b/src/lua_blockmaplib.c
@@ -254,11 +254,10 @@ static int lib_searchBlockmap(lua_State *L)
 	}
 	else // mobj and function only - search around mobj's radius by default
 	{
-		fixed_t radius = mobj->radius + MAXRADIUS;
-		x1 = mobj->x - radius;
-		x2 = mobj->x + radius;
-		y1 = mobj->y - radius;
-		y2 = mobj->y + radius;
+		x1 = mobj->x - mobj->radius;
+		x2 = mobj->x + mobj->radius;
+		y1 = mobj->y - mobj->radius;
+		y2 = mobj->y + mobj->radius;
 	}
 	lua_settop(L, 2); // pop everything except function, mobj
 
diff --git a/src/lua_colorlib.c b/src/lua_colorlib.c
index 3fab589b23ab083df72ae8b7e9d42aa3ee29673c..2743635edfefe88348a93dc45aa26f77d4702067 100644
--- a/src/lua_colorlib.c
+++ b/src/lua_colorlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2021-2022 by "Lactozilla".
-// Copyright (C) 2014-2023 by Sonic Team Junior.
+// Copyright (C) 2021-2024 by Lactozilla.
+// Copyright (C) 2014-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -593,7 +593,7 @@ static int extracolormap_set(lua_State *L)
 		|| exc->fadergba != old_fade_rgba
 		|| exc->fadestart != old_fade_start
 		|| exc->fadeend != old_fade_end)
-	R_GenerateLightTable(exc, true);
+	R_UpdateLightTable(exc, true);
 
 	return 0;
 }
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index aaa676526d1eac6adbad4ac5d8ba34e31388d2cd..b9b24307b0127b41fe0050106c4447ac5d3f7d55 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -31,11 +31,27 @@ return luaL_error(L, "HUD rendering code should not call this function!");
 
 static consvar_t *this_cvar;
 
+static void clear_lua_stack(void)
+{
+	if (gL) // check if Lua is actually turned on first, you dummmy -- Monster Iestyn 04/07/18
+		lua_settop(gL, 0); // clear stack
+}
+
 void Got_Luacmd(UINT8 **cp, INT32 playernum)
 {
 	UINT8 i, argc, flags;
+	const char *argv[256];
 	char buf[256];
 
+	argc = READUINT8(*cp);
+	argv[0] = (const char*)*cp;
+	SKIPSTRINGN(*cp, 255);
+	for (i = 1; i < argc; i++)
+	{
+		argv[i] = (const char*)*cp;
+		SKIPSTRINGN(*cp, 255);
+	}
+
 	// don't use I_Assert here, goto the deny code below
 	// to clean up and kick people who try nefarious exploits
 	// like sending random junk lua commands to crash the server
@@ -48,8 +64,7 @@ void Got_Luacmd(UINT8 **cp, INT32 playernum)
 	lua_getfield(gL, LUA_REGISTRYINDEX, "COM_Command"); // push COM_Command
 	if (!lua_istable(gL, -1)) goto deny;
 
-	argc = READUINT8(*cp);
-	READSTRINGN(*cp, buf, 255);
+	strlcpy(buf, argv[0], 255);
 	strlwr(buf); // must lowercase buffer
 	lua_getfield(gL, -1, buf); // push command info table
 	if (!lua_istable(gL, -1)) goto deny;
@@ -75,10 +90,17 @@ void Got_Luacmd(UINT8 **cp, INT32 playernum)
 
 	lua_remove(gL, -2); // pop command info table
 
+	if (!lua_checkstack(gL, argc)) // player + command arguments
+	{
+		clear_lua_stack();
+		CONS_Alert(CONS_WARNING, "lua command stack overflow from %s (%d, need %d more)\n", player_names[playernum], lua_gettop(gL), argc);
+		return;
+	}
+
 	LUA_PushUserdata(gL, &players[playernum], META_PLAYER);
 	for (i = 1; i < argc; i++)
 	{
-		READSTRINGN(*cp, buf, 255);
+		strlcpy(buf, argv[i], 255);
 		lua_pushstring(gL, buf);
 	}
 	LUA_Call(gL, (int)argc, 0, 1); // argc is 1-based, so this will cover the player we passed too.
@@ -86,8 +108,7 @@ void Got_Luacmd(UINT8 **cp, INT32 playernum)
 
 deny:
 	//must be hacked/buggy client
-	if (gL) // check if Lua is actually turned on first, you dummmy -- Monster Iestyn 04/07/18
-		lua_settop(gL, 0); // clear stack
+	clear_lua_stack();
 
 	CONS_Alert(CONS_WARNING, M_GetText("Illegal lua command received from %s\n"), player_names[playernum]);
 	if (server)
@@ -173,6 +194,11 @@ void COM_Lua_f(void)
 	I_Assert(lua_isfunction(gL, -1));
 	lua_remove(gL, -2); // pop command info table
 
+	if (!lua_checkstack(gL, COM_Argc() + 1))
+	{
+		CONS_Alert(CONS_WARNING, "lua command stack overflow (%d, need %s more)\n", lua_gettop(gL), sizeu1(COM_Argc() + 1));
+		return;
+	}
 	LUA_PushUserdata(gL, &players[playernum], META_PLAYER);
 	for (i = 1; i < COM_Argc(); i++)
 		lua_pushstring(gL, COM_Argv(i));
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 17e0e99a0787d43fa12f817018b29acaac723495..326df376176c892dd3b21f3fc9e1fdf1bbadd99b 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -72,8 +72,10 @@ automatically.
 	X (MusicChange),\
 	X (PlayerHeight),/* override player height */\
 	X (PlayerCanEnterSpinGaps),\
+	X (AddonLoaded),\
 	X (KeyDown),\
 	X (KeyUp),\
+	X (TextInput),\
 
 #define STRING_HOOK_LIST(X) \
 	X (BotAI),/* B_BuildTailsTiccmd by skin name */\
@@ -134,6 +136,7 @@ void LUA_HookBool(boolean value, int hook);
 int  LUA_HookPlayer(player_t *, int hook);
 int  LUA_HookTiccmd(player_t *, ticcmd_t *, int hook);
 int  LUA_HookKey(event_t *event, int hook); // Hooks for key events
+int  LUA_HookText(event_t *event, int hook); // Hooks for text events
 
 void LUA_HookPreThinkFrame(void);
 void LUA_HookThinkFrame(void);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 54381e4aedc47a64d89190f5582c3c5856634ba6..a39745438db6c8b9a690b0a24c3240f74f1975cc 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -498,7 +498,25 @@ static int call_string_hooks(Hook_State *hook)
 
 static int call_mobj_type_hooks(Hook_State *hook, mobjtype_t mobj_type)
 {
-	return call_mapped(hook, &mobjHookIds[mobj_type][hook->hook_type]);
+	int numCalls = call_mapped(hook, &mobjHookIds[mobj_type][hook->hook_type]);
+
+	if (numCalls > 0 && mobj_type == MT_NULL && (
+		   hook->hook_type == MOBJ_HOOK(MobjThinker    )
+		|| hook->hook_type == MOBJ_HOOK(MobjCollide    )
+		|| hook->hook_type == MOBJ_HOOK(MobjLineCollide)
+		|| hook->hook_type == MOBJ_HOOK(MobjMoveCollide)
+		|| hook->hook_type == MOBJ_HOOK(MobjFuse       )
+		|| hook->hook_type == MOBJ_HOOK(MobjThinker    )
+		|| hook->hook_type == MOBJ_HOOK(BossThinker    )
+		|| hook->hook_type == MOBJ_HOOK(MobjMoveBlocked)
+		|| hook->hook_type == MOBJ_HOOK(FollowMobj     )
+	))
+		LUA_UsageWarning(L, va(
+			"%s hooks not attached to a specific mobj type are deprecated and will be removed.",
+			mobjHookNames[hook->hook_type])
+		);
+
+	return numCalls;
 }
 
 static void call_hud_hooks
@@ -672,6 +690,17 @@ int LUA_HookKey(event_t *event, int hook_type)
 	return hook.status;
 }
 
+int LUA_HookText(event_t *event, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		LUA_PushUserdata(gL, event, META_TEXTEVENT);
+		call_hooks(&hook, 1, res_true);
+	}
+	return hook.status;
+}
+
 void LUA_HookHUD(int hook_type, huddrawlist_h list)
 {
 	Hook_State hook;
@@ -751,7 +780,7 @@ static void hook_think_frame(int type)
 					PS_SetThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
 				else if (type == 6)
 					PS_SetPostThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
-				
+
 				hook_index++;
 			}
 		}
diff --git a/src/lua_hud.h b/src/lua_hud.h
index 36ce230ed2a384a73e1f988c41ef79d6d91986a5..ae938e28a9624ab8ced72337c83228daefcdf12f 100644
--- a/src/lua_hud.h
+++ b/src/lua_hud.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2014-2016 by John "JTE" Muniz.
-// Copyright (C) 2014-2023 by Sonic Team Junior.
+// Copyright (C) 2014-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index d6771f1082a635e2bcb9e26ffb4349398113397c..55029828035e99ddb69c22e9c657dc9ccffbb9e3 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2014-2016 by John "JTE" Muniz.
-// Copyright (C) 2014-2023 by Sonic Team Junior.
+// Copyright (C) 2014-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -461,15 +461,29 @@ static int camera_set(lua_State *L)
 
 static int libd_patchExists(lua_State *L)
 {
-	HUDONLY
 	lua_pushboolean(L, W_LumpExists(luaL_checkstring(L, 1)));
 	return 1;
 }
 
 static int libd_cachePatch(lua_State *L)
 {
-	HUDONLY
-	LUA_PushUserdata(L, W_CachePatchLongName(luaL_checkstring(L, 1), PU_PATCH), META_PATCH);
+	const char *name = luaL_checkstring(L, 1);
+	patch_t *patch = W_CachePatchLongName(name, PU_PATCH);
+
+#ifdef ROTSPRITE
+	if (lua_isnumber(L, 2))
+	{
+		angle_t rollangle = luaL_checkangle(L, 2);
+		INT32 rot = R_GetRollAngle(rollangle);
+		if (rot) {
+			patch_t *rotpatch = Patch_GetRotated(patch, rot, false);
+			LUA_PushUserdata(L, rotpatch, META_PATCH);
+			return 1;
+		}
+	}
+#endif
+	
+	LUA_PushUserdata(L, patch, META_PATCH);
 	return 1;
 }
 
@@ -481,7 +495,6 @@ static int libd_getSpritePatch(lua_State *L)
 	UINT8 angle = 0;
 	spritedef_t *sprdef;
 	spriteframe_t *sprframe;
-	HUDONLY
 
 	if (lua_isnumber(L, 1)) // sprite number given, e.g. SPR_THOK
 	{
@@ -555,7 +568,6 @@ static int libd_getSprite2Patch(lua_State *L)
 	spritedef_t *sprdef;
 	spriteframe_t *sprframe;
 	boolean super = false; // add SPR2F_SUPER to sprite2 if true
-	HUDONLY
 
 	// get skin first!
 	if (lua_isnumber(L, 1)) // find skin by number
@@ -613,6 +625,10 @@ static int libd_getSprite2Patch(lua_State *L)
 	if (super)
 		j |= SPR2F_SUPER;
 
+	// If there is no "super" variation of this sprite, try with the normal one.
+	if (!P_IsValidSprite2(skins[i], j))
+		j &= ~SPR2F_SUPER;
+
 	sprdef = P_GetSkinSpritedef(skins[i], j);
 
 	// set frame number
@@ -1126,13 +1142,12 @@ static int libd_levelTitleHeight(lua_State *L)
 
 static int libd_getColormap(lua_State *L)
 {
+	HUDONLY
 	INT32 skinnum = TC_DEFAULT;
 	skincolornum_t color = luaL_optinteger(L, 2, 0);
 	UINT8* colormap = NULL;
 	int translation_id = -1;
 
-	HUDONLY
-
 	if (lua_isnoneornil(L, 1))
 		; // defaults to TC_DEFAULT
 	else if (lua_type(L, 1) == LUA_TNUMBER) // skin number
@@ -1172,9 +1187,10 @@ static int libd_getColormap(lua_State *L)
 
 static int libd_getStringColormap(lua_State *L)
 {
+	HUDONLY
 	INT32 flags = luaL_checkinteger(L, 1);
 	UINT8* colormap = NULL;
-	HUDONLY
+
 	colormap = V_GetStringColormap(flags & V_CHARCOLORMASK);
 	if (colormap) {
 		LUA_PushUserdata(L, colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
@@ -1185,6 +1201,7 @@ static int libd_getStringColormap(lua_State *L)
 
 static int libd_getSectorColormap(lua_State *L)
 {
+	HUDONLY
 	boolean has_sector = false;
 	sector_t *sector = NULL;
 	if (!lua_isnoneornil(L, 1))
@@ -1260,21 +1277,18 @@ static int libd_fadeScreen(lua_State *L)
 
 static int libd_width(lua_State *L)
 {
-	HUDONLY
 	lua_pushinteger(L, vid.width); // push screen width
 	return 1;
 }
 
 static int libd_height(lua_State *L)
 {
-	HUDONLY
 	lua_pushinteger(L, vid.height); // push screen height
 	return 1;
 }
 
 static int libd_dup(lua_State *L)
 {
-	HUDONLY
 	lua_pushinteger(L, vid.dup); // push integral scale (patch scale)
 	lua_pushfixed(L, vid.fdup); // push fixed point scale (position scale)
 	return 2;
@@ -1282,7 +1296,6 @@ static int libd_dup(lua_State *L)
 
 static int libd_renderer(lua_State *L)
 {
-	HUDONLY
 	switch (rendermode) {
 		case render_opengl: lua_pushliteral(L, "opengl");   break; // OpenGL renderer
 		case render_soft:   lua_pushliteral(L, "software"); break; // Software renderer
@@ -1296,14 +1309,12 @@ static int libd_renderer(lua_State *L)
 
 static int libd_RandomFixed(lua_State *L)
 {
-	HUDONLY
 	lua_pushfixed(L, M_RandomFixed());
 	return 1;
 }
 
 static int libd_RandomByte(lua_State *L)
 {
-	HUDONLY
 	lua_pushinteger(L, M_RandomByte());
 	return 1;
 }
@@ -1312,7 +1323,6 @@ static int libd_RandomKey(lua_State *L)
 {
 	INT32 a = (INT32)luaL_checkinteger(L, 1);
 
-	HUDONLY
 	lua_pushinteger(L, M_RandomKey(a));
 	return 1;
 }
@@ -1322,7 +1332,6 @@ static int libd_RandomRange(lua_State *L)
 	INT32 a = (INT32)luaL_checkinteger(L, 1);
 	INT32 b = (INT32)luaL_checkinteger(L, 2);
 
-	HUDONLY
 	lua_pushinteger(L, M_RandomRange(a, b));
 	return 1;
 }
@@ -1330,7 +1339,6 @@ static int libd_RandomRange(lua_State *L)
 // Macros.
 static int libd_SignedRandom(lua_State *L)
 {
-	HUDONLY
 	lua_pushinteger(L, M_SignedRandom());
 	return 1;
 }
@@ -1338,7 +1346,6 @@ static int libd_SignedRandom(lua_State *L)
 static int libd_RandomChance(lua_State *L)
 {
 	fixed_t p = luaL_checkfixed(L, 1);
-	HUDONLY
 	lua_pushboolean(L, M_RandomChance(p));
 	return 1;
 }
@@ -1347,7 +1354,6 @@ static int libd_RandomChance(lua_State *L)
 // Could as well be thrown in global vars for ease of access but I guess it makes sense for it to be a HUD fn
 static int libd_getlocaltransflag(lua_State *L)
 {
-	HUDONLY
 	lua_pushinteger(L, (10-st_translucency)*V_10TRANS);
 	return 1;
 }
@@ -1355,7 +1361,6 @@ static int libd_getlocaltransflag(lua_State *L)
 // Get cv_translucenthud's value for HUD rendering as a normal V_xxTRANS int
 static int libd_getusertransflag(lua_State *L)
 {
-	HUDONLY
 	lua_pushinteger(L, (10-cv_translucenthud.value)*V_10TRANS);	// A bit weird that it's called "translucenthud" yet 10 is fully opaque :V
 	return 1;
 }
@@ -1448,6 +1453,26 @@ static luaL_Reg lib_hud[] = {
 	{"disable", lib_huddisable},
 	{"enabled", lib_hudenabled},
 	{"add", lib_hudadd},
+	{"patchExists", libd_patchExists},
+	{"cachePatch", libd_cachePatch},
+	{"getSpritePatch", libd_getSpritePatch},
+	{"getSprite2Patch", libd_getSprite2Patch},
+	{"width", libd_width},
+	{"height", libd_height},
+	{"scale", libd_dup},
+	{"renderer", libd_renderer},
+	{NULL, NULL}
+};
+
+// globalized client_side random functions.
+static luaL_Reg lib_randomclient[] = {
+	// m_random
+	{"localFixed",libd_RandomFixed},
+	{"localByte",libd_RandomByte},
+	{"localKey",libd_RandomKey},
+	{"localRange",libd_RandomRange},
+	{"localSignedRandom",libd_SignedRandom}, // MACRO
+	{"localChance",libd_RandomChance}, // MACRO
 	{NULL, NULL}
 };
 
@@ -1474,6 +1499,7 @@ int LUA_HudLib(lua_State *L)
 	LUA_RegisterGlobalUserdata(L, "hudinfo", lib_getHudInfo, NULL, lib_hudinfolen);
 
 	luaL_register(L, "hud", lib_hud);
+	luaL_register(L, "random", lib_randomclient);
 	return 0;
 }
 
diff --git a/src/lua_hudlib_drawlist.c b/src/lua_hudlib_drawlist.c
index c518ba52540ff874480d7cda2c8314a31987e993..358d615cfde8d1e26a0098737f8aaa5a39359b11 100644
--- a/src/lua_hudlib_drawlist.c
+++ b/src/lua_hudlib_drawlist.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2014-2016 by John "JTE" Muniz.
-// Copyright (C) 2014-2023 by Sonic Team Junior.
+// Copyright (C) 2014-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -180,7 +180,8 @@ static const char *CopyString(huddrawlist_h list, const char* str)
 		const char *old_offset = list->strbuf;
 		size_t i;
 		if (list->strbuf_capacity == 0) list->strbuf_capacity = 256;
-		else list->strbuf_capacity *= 2;
+		while (list->strbuf_capacity <= list->strbuf_len + lenstr + 1)
+			list->strbuf_capacity *= 2;
 		list->strbuf = (char*) Z_Realloc(list->strbuf, sizeof(char) * list->strbuf_capacity, PU_STATIC, NULL);
 
 		// align the string pointers to make sure old pointers don't point towards invalid addresses
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index eeb1067a335f121f23d89bcf817a0b54f53f4f41..f277f2651dcb0d858b50c1b5bb461de4a3658f62 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -242,17 +242,12 @@ static int lib_getSpriteInfo(lua_State *L)
 	UINT32 i = NUMSPRITES;
 	lua_remove(L, 1);
 
-	if (lua_isstring(L, 1))
+	if (lua_type(L, 1) == LUA_TSTRING)
 	{
 		const char *name = lua_tostring(L, 1);
 		INT32 spr = R_GetSpriteNumByName(name);
 		if (spr == NUMSPRITES)
-		{
-			char *check;
-			i = strtol(name, &check, 10);
-			if (check == name || *check != '\0')
-				return luaL_error(L, "unknown sprite name %s", name);
-		}
+			return luaL_error(L, "unknown sprite name %s", name);
 		i = spr;
 	}
 	else
@@ -1163,6 +1158,7 @@ enum mobjinfo_e
 	mobjinfo_activesound,
 	mobjinfo_flags,
 	mobjinfo_raisestate,
+	mobjinfo_name,
 };
 
 const char *const mobjinfo_opt[] = {
@@ -1190,6 +1186,7 @@ const char *const mobjinfo_opt[] = {
 	"activesound",
 	"flags",
 	"raisestate",
+	"name",
 	NULL,
 };
 
@@ -1204,6 +1201,8 @@ static int mobjinfo_get(lua_State *L)
 	I_Assert(info != NULL);
 	I_Assert(info >= mobjinfo);
 
+	mobjtype_t id = info-mobjinfo;
+
 	switch (field)
 	{
 	case mobjinfo_doomednum:
@@ -1278,6 +1277,21 @@ static int mobjinfo_get(lua_State *L)
 	case mobjinfo_raisestate:
 		lua_pushinteger(L, info->raisestate);
 		break;
+	case mobjinfo_name:
+		if (id < MT_FIRSTFREESLOT)
+		{
+			lua_pushstring(L, MOBJTYPE_LIST[id]+3);
+			return 1;
+		}
+
+		id -= MT_FIRSTFREESLOT;
+		if (id < NUMMOBJFREESLOTS && FREE_MOBJS[id])
+		{
+			lua_pushstring(L, FREE_MOBJS[id]);
+			return 1;
+		}
+
+		return 0;
 	default:
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
@@ -1740,7 +1754,7 @@ static int lib_setSkinColor(lua_State *L)
 		else if (i == 6 || (str && fastcmp(str,"accessible"))) {
 			boolean v = lua_toboolean(L, 3);
 			if (cnum < FIRSTSUPERCOLOR && v != skincolors[cnum].accessible)
-				CONS_Alert(CONS_WARNING, "skincolors[] index %d is a standard color; accessibility changes are prohibited.", cnum);
+				CONS_Alert(CONS_WARNING, "skincolors[] index %d is a standard color; accessibility changes are prohibited.\n", cnum);
 			else
 				info->accessible = v;
 		}
@@ -1835,7 +1849,7 @@ static int skincolor_set(lua_State *L)
 	else if (fastcmp(field,"accessible")) {
 		boolean v = lua_toboolean(L, 3);
 		if (cnum < FIRSTSUPERCOLOR && v != skincolors[cnum].accessible)
-			CONS_Alert(CONS_WARNING, "skincolors[] index %d is a standard color; accessibility changes are prohibited.", cnum);
+			CONS_Alert(CONS_WARNING, "skincolors[] index %d is a standard color; accessibility changes are prohibited.\n", cnum);
 		else
 			info->accessible = v;
 	} else
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index ef3a9011f1c47a27dc8c75b3e43dc2cd47b9a2d8..d4e327c7bba3aae6a5b13bf7265104307613df05 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -10,16 +10,19 @@
 /// \brief input library for Lua scripting
 
 #include "doomdef.h"
+#include "doomstat.h"
 #include "fastcmp.h"
 #include "g_input.h"
 #include "g_game.h"
 #include "hu_stuff.h"
 #include "i_system.h"
+#include "console.h"
 
 #include "lua_script.h"
 #include "lua_libs.h"
 
 boolean mousegrabbedbylua = true;
+boolean textinputmodeenabledbylua = false;
 boolean ignoregameinputs = false;
 
 ///////////////
@@ -129,6 +132,20 @@ static int lib_getCursorPosition(lua_State *L)
 	return 2;
 }
 
+static int lib_setTextInputMode(lua_State *L)
+{
+	textinputmodeenabledbylua = luaL_checkboolean(L, 1);
+	if (!(menuactive || CON_Ready() || chat_on))
+		I_SetTextInputMode(textinputmodeenabledbylua);
+	return 0;
+}
+
+static int lib_getTextInputMode(lua_State *L)
+{
+	lua_pushinteger(L, textinputmodeenabledbylua);
+	return 1;
+}
+
 static luaL_Reg lib[] = {
 	{"gameControlDown", lib_gameControlDown},
 	{"gameControl2Down", lib_gameControl2Down},
@@ -143,6 +160,8 @@ static luaL_Reg lib[] = {
 	{"getMouseGrab", lib_getMouseGrab},
 	{"setMouseGrab", lib_setMouseGrab},
 	{"getCursorPosition", lib_getCursorPosition},
+	{"setTextInputMode", lib_setTextInputMode},
+	{"getTextInputMode", lib_getTextInputMode},
 	{NULL, NULL}
 };
 
@@ -220,6 +239,28 @@ static int lib_lenGameKeyDown(lua_State *L)
 	return 1;
 }
 
+////////////////
+// TEXT EVENT //
+////////////////
+
+static int textevent_get(lua_State *L)
+{
+	event_t *event = *((event_t **)luaL_checkudata(L, 1, META_TEXTEVENT));
+	const char *field = luaL_checkstring(L, 2);
+
+	I_Assert(event != NULL);
+
+	if (fastcmp(field,"text"))
+	{
+		char s[2] = { event->key, 0 };
+		lua_pushstring(L, s);
+	}
+	else
+		return luaL_error(L, "textevent_t has no field named %s", field);
+
+	return 1;
+}
+
 ///////////////
 // KEY EVENT //
 ///////////////
@@ -285,6 +326,7 @@ static int mouse_num(lua_State *L)
 
 int LUA_InputLib(lua_State *L)
 {
+	LUA_RegisterUserdataMetatable(L, META_TEXTEVENT, textevent_get, NULL, NULL);
 	LUA_RegisterUserdataMetatable(L, META_KEYEVENT, keyevent_get, NULL, NULL);
 	LUA_RegisterUserdataMetatable(L, META_MOUSE, mouse_get, NULL, mouse_num);
 
diff --git a/src/lua_interceptlib.c b/src/lua_interceptlib.c
new file mode 100644
index 0000000000000000000000000000000000000000..5907b2bf5ac909bced02fab2107f013370831e15
--- /dev/null
+++ b/src/lua_interceptlib.c
@@ -0,0 +1,115 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024-2024 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  lua_interceptlib.c
+/// \brief intercept library for Lua scripting
+
+#include "doomdef.h"
+#include "fastcmp.h"
+#include "p_local.h"
+#include "lua_script.h"
+#include "lua_libs.h"
+
+#include "lua_hud.h" // hud_running errors
+
+#define NOHUD if (hud_running)\
+return luaL_error(L, "HUD rendering code should not call this function!");
+
+enum intercept_e {
+	intercept_valid = 0,
+	intercept_frac,
+	intercept_thing,
+	intercept_line
+};
+
+static const char *const intercept_opt[] = {
+	"valid",
+	"frac",
+	"thing",
+	"line",
+	NULL};
+
+static int intercept_fields_ref = LUA_NOREF;
+
+static boolean Lua_PathTraverser(intercept_t *in)
+{
+	boolean traverse = false;
+	I_Assert(in != NULL);
+	
+	lua_settop(gL, 6);
+	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	
+	I_Assert(lua_isfunction(gL, -2));
+	
+	lua_pushvalue(gL, -2);
+	LUA_PushUserdata(gL, in, META_INTERCEPT);
+	LUA_Call(gL, 1, 1, -3);
+	
+	traverse = lua_toboolean(gL, -1);
+	lua_pop(gL, 1);
+	
+	return !traverse; // Stay consistent with the MobjMoveCollide hook
+}
+
+static int intercept_get(lua_State *L)
+{
+	intercept_t *in = *((intercept_t **)luaL_checkudata(L, 1, META_INTERCEPT));
+	enum intercept_e field = Lua_optoption(L, 2, intercept_valid, intercept_fields_ref);
+	
+	if (!in)
+	{
+		if (field == intercept_valid) {
+			lua_pushboolean(L, 0);
+			return 1;
+		}
+		return luaL_error(L, "accessed intercept_t doesn't exist anymore.");
+	}
+	
+	switch(field)
+	{
+	case intercept_valid: // valid
+		lua_pushboolean(L, 1);
+		return 1;
+	case intercept_frac:
+		lua_pushfixed(L, in->frac);
+		return 1;
+	case intercept_thing:
+		if (in->isaline)
+			return 0;
+		LUA_PushUserdata(L, in->d.thing, META_MOBJ);
+		return 1;
+	case intercept_line:
+		if (!in->isaline)
+			return 0;
+		LUA_PushUserdata(L, in->d.line, META_LINE);
+		return 1;
+	}
+	return 0;
+}
+
+static int lib_pPathTraverse(lua_State *L)
+{
+	fixed_t px1 = luaL_checkfixed(L, 1);
+	fixed_t py1 = luaL_checkfixed(L, 2);
+	fixed_t px2 = luaL_checkfixed(L, 3);
+	fixed_t py2 = luaL_checkfixed(L, 4);
+	INT32 flags = (INT32)luaL_checkinteger(L, 5);
+	luaL_checktype(L, 6, LUA_TFUNCTION);
+	NOHUD
+	INLEVEL
+	lua_pushboolean(L, P_PathTraverse(px1, py1, px2, py2, flags, Lua_PathTraverser));
+	return 1;
+}
+
+int LUA_InterceptLib(lua_State *L)
+{
+	LUA_RegisterUserdataMetatable(L, META_INTERCEPT, intercept_get, NULL, NULL);
+	intercept_fields_ref = Lua_CreateFieldTable(L, intercept_opt);
+	lua_register(L, "P_PathTraverse", lib_pPathTraverse);
+	return 0;
+}
diff --git a/src/lua_libs.h b/src/lua_libs.h
index a90d8ac7fb800c426e7bef94538803366a835b8b..381c3462955312d97b09b6b8529ff91d6e4ee27b 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -13,6 +13,7 @@
 extern lua_State *gL;
 
 extern boolean mousegrabbedbylua;
+extern boolean textinputmodeenabledbylua;
 extern boolean ignoregameinputs;
 
 #define MUTABLE_TAGS
@@ -42,8 +43,9 @@ extern boolean ignoregameinputs;
 #define META_SKIN "SKIN_T*"
 #define META_POWERS "PLAYER_T*POWERS"
 #define META_SOUNDSID "SKIN_T*SOUNDSID"
-#define META_SKINSPRITES "SKIN_T*SPRITES"
-#define META_SKINSPRITESLIST "SKIN_T*SPRITES[]"
+#define META_SKINSPRITES "SKIN_T*SKINSPRITES"
+#define META_SKINSPRITESLIST "SKIN_T*SKINSPRITES[]"
+#define META_SKINSPRITESCOMPAT "SKIN_T*SPRITES" // TODO: 2.3: Delete
 
 #define META_VERTEX "VERTEX_T*"
 #define META_LINE "LINE_T*"
@@ -92,9 +94,12 @@ extern boolean ignoregameinputs;
 
 #define META_LUABANKS "LUABANKS[]*"
 
+#define META_TEXTEVENT "TEXTEVENT_T*"
 #define META_KEYEVENT "KEYEVENT_T*"
 #define META_MOUSE "MOUSE_T*"
 
+#define META_INTERCEPT "INTERCEPT_T*"
+
 boolean luaL_checkboolean(lua_State *L, int narg);
 
 int LUA_EnumLib(lua_State *L);
@@ -115,3 +120,4 @@ int LUA_BlockmapLib(lua_State *L);
 int LUA_HudLib(lua_State *L);
 int LUA_ColorLib(lua_State *L);
 int LUA_InputLib(lua_State *L);
+int LUA_InterceptLib(lua_State *L);
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 6b489f22b1939941c0ebf83a5a3ae0b7e215de5a..c946b10ce220fd28e2415d46f68233fad4bc7f0b 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -213,6 +213,14 @@ enum side_e {
 	side_sector,
 	side_special,
 	side_repeatcnt,
+	side_light,
+	side_light_top,
+	side_light_mid,
+	side_light_bottom,
+	side_lightabsolute,
+	side_lightabsolute_top,
+	side_lightabsolute_mid,
+	side_lightabsolute_bottom,
 	side_text
 };
 
@@ -241,6 +249,14 @@ static const char *const side_opt[] = {
 	"sector",
 	"special",
 	"repeatcnt",
+	"light",
+	"light_top",
+	"light_mid",
+	"light_bottom",
+	"lightabsolute",
+	"lightabsolute_top",
+	"lightabsolute_mid",
+	"lightabsolute_bottom",
 	"text",
 	NULL};
 
@@ -1311,6 +1327,30 @@ static int side_get(lua_State *L)
 	case side_repeatcnt:
 		lua_pushinteger(L, side->repeatcnt);
 		return 1;
+	case side_light:
+		lua_pushinteger(L, side->light);
+		return 1;
+	case side_light_top:
+		lua_pushinteger(L, side->light_top);
+		return 1;
+	case side_light_mid:
+		lua_pushinteger(L, side->light_mid);
+		return 1;
+	case side_light_bottom:
+		lua_pushinteger(L, side->light_bottom);
+		return 1;
+	case side_lightabsolute:
+		lua_pushboolean(L, side->lightabsolute);
+		return 1;
+	case side_lightabsolute_top:
+		lua_pushboolean(L, side->lightabsolute_top);
+		return 1;
+	case side_lightabsolute_mid:
+		lua_pushboolean(L, side->lightabsolute_mid);
+		return 1;
+	case side_lightabsolute_bottom:
+		lua_pushboolean(L, side->lightabsolute_bottom);
+		return 1;
 	// TODO: 2.3: Delete
 	case side_text:
 		{
@@ -1413,6 +1453,30 @@ static int side_set(lua_State *L)
 	case side_repeatcnt:
 		side->repeatcnt = luaL_checkinteger(L, 3);
 		break;
+	case side_light:
+		side->light = luaL_checkinteger(L, 3);
+		break;
+	case side_light_top:
+		side->light_top = luaL_checkinteger(L, 3);
+		break;
+	case side_light_mid:
+		side->light_mid = luaL_checkinteger(L, 3);
+		break;
+	case side_light_bottom:
+		side->light_bottom = luaL_checkinteger(L, 3);
+		break;
+	case side_lightabsolute:
+		side->lightabsolute = luaL_checkboolean(L, 3);
+		break;
+	case side_lightabsolute_top:
+		side->lightabsolute_top = luaL_checkboolean(L, 3);
+		break;
+	case side_lightabsolute_mid:
+		side->lightabsolute_mid = luaL_checkboolean(L, 3);
+		break;
+	case side_lightabsolute_bottom:
+		side->lightabsolute_bottom = luaL_checkboolean(L, 3);
+		break;
 	}
 	return 0;
 }
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index de7065790abb137e82f505c961215ca6acb10429..6bdebf774df50d0527e3f5713043e008f5c3e33e 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -69,6 +69,7 @@ enum mobj_e {
 	mobj_color,
 	mobj_translation,
 	mobj_blendmode,
+	mobj_alpha,
 	mobj_bnext,
 	mobj_bprev,
 	mobj_hnext,
@@ -150,6 +151,7 @@ static const char *const mobj_opt[] = {
 	"color",
 	"translation",
 	"blendmode",
+	"alpha",
 	"bnext",
 	"bprev",
 	"hnext",
@@ -196,12 +198,12 @@ static int mobj_get(lua_State *L)
 	enum mobj_e field = Lua_optoption(L, 2, -1, mobj_fields_ref);
 	lua_settop(L, 2);
 
-	if (!mo || !ISINLEVEL) {
+	if (P_MobjWasRemoved(mo) || !ISINLEVEL) {
 		if (field == mobj_valid) {
 			lua_pushboolean(L, 0);
 			return 1;
 		}
-		if (!mo) {
+		if (P_MobjWasRemoved(mo)) {
 			return LUA_ErrInvalid(L, "mobj_t");
 		} else
 			return luaL_error(L, "Do not access an mobj_t field outside a level!");
@@ -354,6 +356,9 @@ static int mobj_get(lua_State *L)
 	case mobj_blendmode:
 		lua_pushinteger(L, mo->blendmode);
 		break;
+	case mobj_alpha:
+		lua_pushfixed(L, mo->alpha);
+		break;
 	case mobj_bnext:
 		if (mo->blocknode && mo->blocknode->bnext) {
 			LUA_PushUserdata(L, mo->blocknode->bnext->mobj, META_MOBJ);
@@ -733,6 +738,16 @@ static int mobj_set(lua_State *L)
 		mo->blendmode = blendmode;
 		break;
 	}
+	case mobj_alpha:
+	{
+		fixed_t alpha = luaL_checkfixed(L, 3);
+		if (alpha < 0)
+			alpha = 0;
+		else if (alpha > FRACUNIT)
+			alpha = FRACUNIT;
+		mo->alpha = alpha;
+		break;
+	}
 	case mobj_bnext:
 		return NOSETPOS;
 	case mobj_bprev:
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 744a56f7fe528031dd0777c06da35067fb59dde4..37b42fde5b907bd531514ce4376ddc5cb8680c18 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/lua_script.c b/src/lua_script.c
index 057899555480383611652c552e41bf41398b0e2b..bb3101d1eb48867b96d8f54e85f5e811e1da3b4f 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -62,6 +62,7 @@ static lua_CFunction liblist[] = {
 	LUA_HudLib, // HUD stuff
 	LUA_ColorLib, // general color functions
 	LUA_InputLib, // inputs
+	LUA_InterceptLib, // intercept_t
 	NULL
 };
 
@@ -1098,7 +1099,7 @@ static UINT8 GetUserdataArchType(int index)
 	return ARCH_NULL;
 }
 
-static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
+static UINT8 ArchiveValue(save_t *save_p, int TABLESINDEX, int myindex)
 {
 	if (myindex < 0)
 		myindex = lua_gettop(gL)+1+myindex;
@@ -1106,34 +1107,34 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 	{
 	case LUA_TNONE:
 	case LUA_TNIL:
-		WRITEUINT8(save_p, ARCH_NULL);
+		P_WriteUINT8(save_p, ARCH_NULL);
 		break;
 	// This might be a problem. D:
 	case LUA_TLIGHTUSERDATA:
 	case LUA_TTHREAD:
 	case LUA_TFUNCTION:
-		WRITEUINT8(save_p, ARCH_NULL);
+		P_WriteUINT8(save_p, ARCH_NULL);
 		return 2;
 	case LUA_TBOOLEAN:
-		WRITEUINT8(save_p, lua_toboolean(gL, myindex) ? ARCH_TRUE : ARCH_FALSE);
+		P_WriteUINT8(save_p, lua_toboolean(gL, myindex) ? ARCH_TRUE : ARCH_FALSE);
 		break;
 	case LUA_TNUMBER:
 	{
 		lua_Integer number = lua_tointeger(gL, myindex);
 		if (number >= INT8_MIN && number <= INT8_MAX)
 		{
-			WRITEUINT8(save_p, ARCH_INT8);
-			WRITESINT8(save_p, number);
+			P_WriteUINT8(save_p, ARCH_INT8);
+			P_WriteSINT8(save_p, number);
 		}
 		else if (number >= INT16_MIN && number <= INT16_MAX)
 		{
-			WRITEUINT8(save_p, ARCH_INT16);
-			WRITEINT16(save_p, number);
+			P_WriteUINT8(save_p, ARCH_INT16);
+			P_WriteINT16(save_p, number);
 		}
 		else
 		{
-			WRITEUINT8(save_p, ARCH_INT32);
-			WRITEFIXED(save_p, number);
+			P_WriteUINT8(save_p, ARCH_INT32);
+			P_WriteFixed(save_p, number);
 		}
 		break;
 	}
@@ -1144,23 +1145,23 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		UINT32 i = 0;
 		// if you're wondering why we're writing a string to save_p this way,
 		// it turns out that Lua can have embedded zeros ('\0') in the strings,
-		// so we can't use WRITESTRING as that cuts off when it finds a '\0'.
+		// so we can't use P_WriteString as that cuts off when it finds a '\0'.
 		// Saving the size of the string also allows us to get the size of the string on the other end,
 		// fixing the awful crashes previously encountered for reading strings longer than 1024
 		// (yes I know that's kind of a stupid thing to care about, but it'd be evil to trim or ignore them?)
 		// -- Monster Iestyn 05/08/18
 		if (len < 255)
 		{
-			WRITEUINT8(save_p, ARCH_SMALLSTRING);
-			WRITEUINT8(save_p, len); // save size of string
+			P_WriteUINT8(save_p, ARCH_SMALLSTRING);
+			P_WriteUINT8(save_p, len); // save size of string
 		}
 		else
 		{
-			WRITEUINT8(save_p, ARCH_LARGESTRING);
-			WRITEUINT32(save_p, len); // save size of string
+			P_WriteUINT8(save_p, ARCH_LARGESTRING);
+			P_WriteUINT32(save_p, len); // save size of string
 		}
 		while (i < len)
-			WRITECHAR(save_p, s[i++]); // write chars individually, including the embedded zeros
+			P_WriteChar(save_p, s[i++]); // write chars individually, including the embedded zeros
 		break;
 	}
 	case LUA_TTABLE:
@@ -1186,13 +1187,13 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			if (t == 0)
 			{
 				CONS_Alert(CONS_ERROR, "Too many tables to archive!\n");
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 				return 0;
 			}
 		}
 
-		WRITEUINT8(save_p, ARCH_TABLE);
-		WRITEUINT16(save_p, t);
+		P_WriteUINT8(save_p, ARCH_TABLE);
+		P_WriteUINT16(save_p, t);
 
 		if (!found)
 		{
@@ -1208,25 +1209,25 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		case ARCH_MOBJINFO:
 		{
 			mobjinfo_t *info = *((mobjinfo_t **)lua_touserdata(gL, myindex));
-			WRITEUINT8(save_p, ARCH_MOBJINFO);
-			WRITEUINT16(save_p, info - mobjinfo);
+			P_WriteUINT8(save_p, ARCH_MOBJINFO);
+			P_WriteUINT16(save_p, info - mobjinfo);
 			break;
 		}
 		case ARCH_STATE:
 		{
 			state_t *state = *((state_t **)lua_touserdata(gL, myindex));
-			WRITEUINT8(save_p, ARCH_STATE);
-			WRITEUINT16(save_p, state - states);
+			P_WriteUINT8(save_p, ARCH_STATE);
+			P_WriteUINT16(save_p, state - states);
 			break;
 		}
 		case ARCH_MOBJ:
 		{
 			mobj_t *mobj = *((mobj_t **)lua_touserdata(gL, myindex));
 			if (!mobj)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_MOBJ);
-				WRITEUINT32(save_p, mobj->mobjnum);
+				P_WriteUINT8(save_p, ARCH_MOBJ);
+				P_WriteUINT32(save_p, mobj->mobjnum);
 			}
 			break;
 		}
@@ -1234,10 +1235,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			player_t *player = *((player_t **)lua_touserdata(gL, myindex));
 			if (!player)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_PLAYER);
-				WRITEUINT8(save_p, player - players);
+				P_WriteUINT8(save_p, ARCH_PLAYER);
+				P_WriteUINT8(save_p, player - players);
 			}
 			break;
 		}
@@ -1245,10 +1246,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			mapthing_t *mapthing = *((mapthing_t **)lua_touserdata(gL, myindex));
 			if (!mapthing)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_MAPTHING);
-				WRITEUINT16(save_p, mapthing - mapthings);
+				P_WriteUINT8(save_p, ARCH_MAPTHING);
+				P_WriteUINT16(save_p, mapthing - mapthings);
 			}
 			break;
 		}
@@ -1256,10 +1257,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			vertex_t *vertex = *((vertex_t **)lua_touserdata(gL, myindex));
 			if (!vertex)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_VERTEX);
-				WRITEUINT16(save_p, vertex - vertexes);
+				P_WriteUINT8(save_p, ARCH_VERTEX);
+				P_WriteUINT16(save_p, vertex - vertexes);
 			}
 			break;
 		}
@@ -1267,10 +1268,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			line_t *line = *((line_t **)lua_touserdata(gL, myindex));
 			if (!line)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_LINE);
-				WRITEUINT16(save_p, line - lines);
+				P_WriteUINT8(save_p, ARCH_LINE);
+				P_WriteUINT16(save_p, line - lines);
 			}
 			break;
 		}
@@ -1278,10 +1279,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			side_t *side = *((side_t **)lua_touserdata(gL, myindex));
 			if (!side)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_SIDE);
-				WRITEUINT16(save_p, side - sides);
+				P_WriteUINT8(save_p, ARCH_SIDE);
+				P_WriteUINT16(save_p, side - sides);
 			}
 			break;
 		}
@@ -1289,10 +1290,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			subsector_t *subsector = *((subsector_t **)lua_touserdata(gL, myindex));
 			if (!subsector)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_SUBSECTOR);
-				WRITEUINT16(save_p, subsector - subsectors);
+				P_WriteUINT8(save_p, ARCH_SUBSECTOR);
+				P_WriteUINT16(save_p, subsector - subsectors);
 			}
 			break;
 		}
@@ -1300,10 +1301,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			sector_t *sector = *((sector_t **)lua_touserdata(gL, myindex));
 			if (!sector)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_SECTOR);
-				WRITEUINT16(save_p, sector - sectors);
+				P_WriteUINT8(save_p, ARCH_SECTOR);
+				P_WriteUINT16(save_p, sector - sectors);
 			}
 			break;
 		}
@@ -1312,10 +1313,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			seg_t *seg = *((seg_t **)lua_touserdata(gL, myindex));
 			if (!seg)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_SEG);
-				WRITEUINT16(save_p, seg - segs);
+				P_WriteUINT8(save_p, ARCH_SEG);
+				P_WriteUINT16(save_p, seg - segs);
 			}
 			break;
 		}
@@ -1323,10 +1324,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			node_t *node = *((node_t **)lua_touserdata(gL, myindex));
 			if (!node)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_NODE);
-				WRITEUINT16(save_p, node - nodes);
+				P_WriteUINT8(save_p, ARCH_NODE);
+				P_WriteUINT16(save_p, node - nodes);
 			}
 			break;
 		}
@@ -1335,16 +1336,16 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			ffloor_t *rover = *((ffloor_t **)lua_touserdata(gL, myindex));
 			if (!rover)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
 				UINT16 i = P_GetFFloorID(rover);
 				if (i == UINT16_MAX) // invalid ID
-					WRITEUINT8(save_p, ARCH_NULL);
+					P_WriteUINT8(save_p, ARCH_NULL);
 				else
 				{
-					WRITEUINT8(save_p, ARCH_FFLOOR);
-					WRITEUINT16(save_p, rover->target - sectors);
-					WRITEUINT16(save_p, i);
+					P_WriteUINT8(save_p, ARCH_FFLOOR);
+					P_WriteUINT16(save_p, rover->target - sectors);
+					P_WriteUINT16(save_p, i);
 				}
 			}
 			break;
@@ -1353,10 +1354,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			polyobj_t *polyobj = *((polyobj_t **)lua_touserdata(gL, myindex));
 			if (!polyobj)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_POLYOBJ);
-				WRITEUINT16(save_p, polyobj-PolyObjects);
+				P_WriteUINT8(save_p, ARCH_POLYOBJ);
+				P_WriteUINT16(save_p, polyobj-PolyObjects);
 			}
 			break;
 		}
@@ -1364,10 +1365,10 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			pslope_t *slope = *((pslope_t **)lua_touserdata(gL, myindex));
 			if (!slope)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_SLOPE);
-				WRITEUINT16(save_p, slope->id);
+				P_WriteUINT8(save_p, ARCH_SLOPE);
+				P_WriteUINT16(save_p, slope->id);
 			}
 			break;
 		}
@@ -1375,36 +1376,36 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 		{
 			mapheader_t *header = *((mapheader_t **)lua_touserdata(gL, myindex));
 			if (!header)
-				WRITEUINT8(save_p, ARCH_NULL);
+				P_WriteUINT8(save_p, ARCH_NULL);
 			else {
-				WRITEUINT8(save_p, ARCH_MAPHEADER);
-				WRITEUINT16(save_p, header - *mapheaderinfo);
+				P_WriteUINT8(save_p, ARCH_MAPHEADER);
+				P_WriteUINT16(save_p, header - *mapheaderinfo);
 			}
 			break;
 		}
 		case ARCH_SKINCOLOR:
 		{
 			skincolor_t *info = *((skincolor_t **)lua_touserdata(gL, myindex));
-			WRITEUINT8(save_p, ARCH_SKINCOLOR);
-			WRITEUINT16(save_p, info - skincolors);
+			P_WriteUINT8(save_p, ARCH_SKINCOLOR);
+			P_WriteUINT16(save_p, info - skincolors);
 			break;
 		}
 		case ARCH_MOUSE:
 		{
 			mouse_t *m = *((mouse_t **)lua_touserdata(gL, myindex));
-			WRITEUINT8(save_p, ARCH_MOUSE);
-			WRITEUINT8(save_p, m == &mouse ? 1 : 2);
+			P_WriteUINT8(save_p, ARCH_MOUSE);
+			P_WriteUINT8(save_p, m == &mouse ? 1 : 2);
 			break;
 		}
 		case ARCH_SKIN:
 		{
 			skin_t *skin = *((skin_t **)lua_touserdata(gL, myindex));
-			WRITEUINT8(save_p, ARCH_SKIN);
-			WRITEUINT8(save_p, skin->skinnum); // UINT8 because MAXSKINS must be <= 256
+			P_WriteUINT8(save_p, ARCH_SKIN);
+			P_WriteUINT8(save_p, skin->skinnum); // UINT8 because MAXSKINS must be <= 256
 			break;
 		}
 		default:
-			WRITEUINT8(save_p, ARCH_NULL);
+			P_WriteUINT8(save_p, ARCH_NULL);
 			return 2;
 		}
 		break;
@@ -1412,14 +1413,14 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 	return 0;
 }
 
-static void ArchiveExtVars(void *pointer, const char *ptype)
+static void ArchiveExtVars(save_t *save_p, void *pointer, const char *ptype)
 {
 	int TABLESINDEX;
 	UINT16 i;
 
 	if (!gL) {
 		if (fastcmp(ptype,"player")) // players must always be included, even if no vars
-			WRITEUINT16(save_p, 0);
+			P_WriteUINT16(save_p, 0);
 		return;
 	}
 
@@ -1435,7 +1436,7 @@ static void ArchiveExtVars(void *pointer, const char *ptype)
 	{ // no extra values table
 		lua_pop(gL, 1);
 		if (fastcmp(ptype,"player")) // players must always be included, even if no vars
-			WRITEUINT16(save_p, 0);
+			P_WriteUINT16(save_p, 0);
 		return;
 	}
 
@@ -1447,20 +1448,20 @@ static void ArchiveExtVars(void *pointer, const char *ptype)
 	if (i == 0)
 	{
 		if (fastcmp(ptype,"player")) // always include players even if they have no extra variables
-			WRITEUINT16(save_p, 0);
+			P_WriteUINT16(save_p, 0);
 		lua_pop(gL, 1);
 		return;
 	}
 
 	if (fastcmp(ptype,"mobj")) // mobjs must write their mobjnum as a header
-		WRITEUINT32(save_p, ((mobj_t *)pointer)->mobjnum);
-	WRITEUINT16(save_p, i);
+		P_WriteUINT32(save_p, ((mobj_t *)pointer)->mobjnum);
+	P_WriteUINT16(save_p, i);
 	lua_pushnil(gL);
 	while (lua_next(gL, -2))
 	{
 		I_Assert(lua_type(gL, -2) == LUA_TSTRING);
-		WRITESTRING(save_p, lua_tostring(gL, -2));
-		if (ArchiveValue(TABLESINDEX, -1) == 2)
+		P_WriteString(save_p, lua_tostring(gL, -2));
+		if (ArchiveValue(save_p, TABLESINDEX, -1) == 2)
 			CONS_Alert(CONS_ERROR, "Type of value for %s entry '%s' (%s) could not be archived!\n", ptype, lua_tostring(gL, -2), luaL_typename(gL, -1));
 		lua_pop(gL, 1);
 	}
@@ -1468,16 +1469,19 @@ static void ArchiveExtVars(void *pointer, const char *ptype)
 	lua_pop(gL, 1);
 }
 
+// FIXME: remove and pass as local variable
+static save_t *lua_save_p;
+
 static int NetArchive(lua_State *L)
 {
 	int TABLESINDEX = lua_upvalueindex(1);
 	int i, n = lua_gettop(L);
 	for (i = 1; i <= n; i++)
-		ArchiveValue(TABLESINDEX, i);
+		ArchiveValue(lua_save_p, TABLESINDEX, i);
 	return n;
 }
 
-static void ArchiveTables(void)
+static void ArchiveTables(save_t *save_p)
 {
 	int TABLESINDEX;
 	UINT16 i, n;
@@ -1496,14 +1500,14 @@ static void ArchiveTables(void)
 		while (lua_next(gL, -2))
 		{
 			// Write key
-			e = ArchiveValue(TABLESINDEX, -2); // key should be either a number or a string, ArchiveValue can handle this.
+			e = ArchiveValue(save_p, TABLESINDEX, -2); // key should be either a number or a string, ArchiveValue can handle this.
 			if (e == 1)
 				n++; // the table contained a new table we'll have to archive. :(
 			else if (e == 2) // invalid key type (function, thread, lightuserdata, or anything we don't recognise)
 				CONS_Alert(CONS_ERROR, "Index '%s' (%s) of table %d could not be archived!\n", lua_tostring(gL, -2), luaL_typename(gL, -2), i);
 
 			// Write value
-			e = ArchiveValue(TABLESINDEX, -1);
+			e = ArchiveValue(save_p, TABLESINDEX, -1);
 			if (e == 1)
 				n++; // the table contained a new table we'll have to archive. :(
 			else if (e == 2) // invalid value type
@@ -1511,7 +1515,7 @@ static void ArchiveTables(void)
 
 			lua_pop(gL, 1);
 		}
-		WRITEUINT8(save_p, ARCH_TEND);
+		P_WriteUINT8(save_p, ARCH_TEND);
 
 		// Write metatable ID
 		if (lua_getmetatable(gL, -1))
@@ -1520,19 +1524,19 @@ static void ArchiveTables(void)
 			lua_getfield(gL, LUA_REGISTRYINDEX, LREG_METATABLES);
 			lua_pushvalue(gL, -2);
 			lua_gettable(gL, -2);
-			WRITEUINT16(save_p, lua_isnil(gL, -1) ? 0 : lua_tointeger(gL, -1));
+			P_WriteUINT16(save_p, lua_isnil(gL, -1) ? 0 : lua_tointeger(gL, -1));
 			lua_pop(gL, 3);
 		}
 		else
-			WRITEUINT16(save_p, 0);
+			P_WriteUINT16(save_p, 0);
 
 		lua_pop(gL, 1);
 	}
 }
 
-static UINT8 UnArchiveValue(int TABLESINDEX)
+static UINT8 UnArchiveValue(save_t *save_p, int TABLESINDEX)
 {
-	UINT8 type = READUINT8(save_p);
+	UINT8 type = P_ReadUINT8(save_p);
 	switch (type)
 	{
 	case ARCH_NULL:
@@ -1545,13 +1549,13 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 		lua_pushboolean(gL, false);
 		break;
 	case ARCH_INT8:
-		lua_pushinteger(gL, READSINT8(save_p));
+		lua_pushinteger(gL, P_ReadSINT8(save_p));
 		break;
 	case ARCH_INT16:
-		lua_pushinteger(gL, READINT16(save_p));
+		lua_pushinteger(gL, P_ReadINT16(save_p));
 		break;
 	case ARCH_INT32:
-		lua_pushinteger(gL, READFIXED(save_p));
+		lua_pushinteger(gL, P_ReadFixed(save_p));
 		break;
 	case ARCH_SMALLSTRING:
 	case ARCH_LARGESTRING:
@@ -1562,23 +1566,23 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 
 		// See my comments in the ArchiveValue function;
 		// it's much the same for reading strings as writing them!
-		// (i.e. we can't use READSTRING either)
+		// (i.e. we can't use P_ReadString either)
 		// -- Monster Iestyn 05/08/18
 		if (type == ARCH_SMALLSTRING)
-			len = READUINT8(save_p); // length of string, including embedded zeros
+			len = P_ReadUINT8(save_p); // length of string, including embedded zeros
 		else
-			len = READUINT32(save_p); // length of string, including embedded zeros
+			len = P_ReadUINT32(save_p); // length of string, including embedded zeros
 		value = malloc(len); // make temp buffer of size len
 		// now read the actual string
 		while (i < len)
-			value[i++] = READCHAR(save_p); // read chars individually, including the embedded zeros
+			value[i++] = P_ReadChar(save_p); // read chars individually, including the embedded zeros
 		lua_pushlstring(gL, value, len); // push the string (note: this function supports embedded zeros)
 		free(value); // free the buffer
 		break;
 	}
 	case ARCH_TABLE:
 	{
-		UINT16 tid = READUINT16(save_p);
+		UINT16 tid = P_ReadUINT16(save_p);
 		lua_rawgeti(gL, TABLESINDEX, tid);
 		if (lua_isnil(gL, -1))
 		{
@@ -1591,69 +1595,69 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 		break;
 	}
 	case ARCH_MOBJINFO:
-		LUA_PushUserdata(gL, &mobjinfo[READUINT16(save_p)], META_MOBJINFO);
+		LUA_PushUserdata(gL, &mobjinfo[P_ReadUINT16(save_p)], META_MOBJINFO);
 		break;
 	case ARCH_STATE:
-		LUA_PushUserdata(gL, &states[READUINT16(save_p)], META_STATE);
+		LUA_PushUserdata(gL, &states[P_ReadUINT16(save_p)], META_STATE);
 		break;
 	case ARCH_MOBJ:
-		LUA_PushUserdata(gL, P_FindNewPosition(READUINT32(save_p)), META_MOBJ);
+		LUA_PushUserdata(gL, P_FindNewPosition(P_ReadUINT32(save_p)), META_MOBJ);
 		break;
 	case ARCH_PLAYER:
-		LUA_PushUserdata(gL, &players[READUINT8(save_p)], META_PLAYER);
+		LUA_PushUserdata(gL, &players[P_ReadUINT8(save_p)], META_PLAYER);
 		break;
 	case ARCH_MAPTHING:
-		LUA_PushUserdata(gL, &mapthings[READUINT16(save_p)], META_MAPTHING);
+		LUA_PushUserdata(gL, &mapthings[P_ReadUINT16(save_p)], META_MAPTHING);
 		break;
 	case ARCH_VERTEX:
-		LUA_PushUserdata(gL, &vertexes[READUINT16(save_p)], META_VERTEX);
+		LUA_PushUserdata(gL, &vertexes[P_ReadUINT16(save_p)], META_VERTEX);
 		break;
 	case ARCH_LINE:
-		LUA_PushUserdata(gL, &lines[READUINT16(save_p)], META_LINE);
+		LUA_PushUserdata(gL, &lines[P_ReadUINT16(save_p)], META_LINE);
 		break;
 	case ARCH_SIDE:
-		LUA_PushUserdata(gL, &sides[READUINT16(save_p)], META_SIDE);
+		LUA_PushUserdata(gL, &sides[P_ReadUINT16(save_p)], META_SIDE);
 		break;
 	case ARCH_SUBSECTOR:
-		LUA_PushUserdata(gL, &subsectors[READUINT16(save_p)], META_SUBSECTOR);
+		LUA_PushUserdata(gL, &subsectors[P_ReadUINT16(save_p)], META_SUBSECTOR);
 		break;
 	case ARCH_SECTOR:
-		LUA_PushUserdata(gL, &sectors[READUINT16(save_p)], META_SECTOR);
+		LUA_PushUserdata(gL, &sectors[P_ReadUINT16(save_p)], META_SECTOR);
 		break;
 #ifdef HAVE_LUA_SEGS
 	case ARCH_SEG:
-		LUA_PushUserdata(gL, &segs[READUINT16(save_p)], META_SEG);
+		LUA_PushUserdata(gL, &segs[P_ReadUINT16(save_p)], META_SEG);
 		break;
 	case ARCH_NODE:
-		LUA_PushUserdata(gL, &nodes[READUINT16(save_p)], META_NODE);
+		LUA_PushUserdata(gL, &nodes[P_ReadUINT16(save_p)], META_NODE);
 		break;
 #endif
 	case ARCH_FFLOOR:
 	{
-		sector_t *sector = &sectors[READUINT16(save_p)];
-		UINT16 id = READUINT16(save_p);
+		sector_t *sector = &sectors[P_ReadUINT16(save_p)];
+		UINT16 id = P_ReadUINT16(save_p);
 		ffloor_t *rover = P_GetFFloorByID(sector, id);
 		if (rover)
 			LUA_PushUserdata(gL, rover, META_FFLOOR);
 		break;
 	}
 	case ARCH_POLYOBJ:
-		LUA_PushUserdata(gL, &PolyObjects[READUINT16(save_p)], META_POLYOBJ);
+		LUA_PushUserdata(gL, &PolyObjects[P_ReadUINT16(save_p)], META_POLYOBJ);
 		break;
 	case ARCH_SLOPE:
-		LUA_PushUserdata(gL, P_SlopeById(READUINT16(save_p)), META_SLOPE);
+		LUA_PushUserdata(gL, P_SlopeById(P_ReadUINT16(save_p)), META_SLOPE);
 		break;
 	case ARCH_MAPHEADER:
-		LUA_PushUserdata(gL, mapheaderinfo[READUINT16(save_p)], META_MAPHEADER);
+		LUA_PushUserdata(gL, mapheaderinfo[P_ReadUINT16(save_p)], META_MAPHEADER);
 		break;
 	case ARCH_SKINCOLOR:
-		LUA_PushUserdata(gL, &skincolors[READUINT16(save_p)], META_SKINCOLOR);
+		LUA_PushUserdata(gL, &skincolors[P_ReadUINT16(save_p)], META_SKINCOLOR);
 		break;
 	case ARCH_MOUSE:
-		LUA_PushUserdata(gL, READUINT16(save_p) == 1 ? &mouse : &mouse2, META_MOUSE);
+		LUA_PushUserdata(gL, P_ReadUINT16(save_p) == 1 ? &mouse : &mouse2, META_MOUSE);
 		break;
 	case ARCH_SKIN:
-		LUA_PushUserdata(gL, skins[READUINT8(save_p)], META_SKIN);
+		LUA_PushUserdata(gL, skins[P_ReadUINT8(save_p)], META_SKIN);
 		break;
 	case ARCH_TEND:
 		return 1;
@@ -1661,10 +1665,10 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	return 0;
 }
 
-static void UnArchiveExtVars(void *pointer)
+static void UnArchiveExtVars(save_t *save_p, void *pointer)
 {
 	int TABLESINDEX;
-	UINT16 field_count = READUINT16(save_p);
+	UINT16 field_count = P_ReadUINT16(save_p);
 	UINT16 i;
 	char field[1024];
 
@@ -1677,8 +1681,8 @@ static void UnArchiveExtVars(void *pointer)
 
 	for (i = 0; i < field_count; i++)
 	{
-		READSTRING(save_p, field);
-		UnArchiveValue(TABLESINDEX);
+		P_ReadString(save_p, field);
+		UnArchiveValue(save_p, TABLESINDEX);
 		lua_setfield(gL, -2, field);
 	}
 
@@ -1695,11 +1699,11 @@ static int NetUnArchive(lua_State *L)
 	int TABLESINDEX = lua_upvalueindex(1);
 	int i, n = lua_gettop(L);
 	for (i = 1; i <= n; i++)
-		UnArchiveValue(TABLESINDEX);
+		UnArchiveValue(lua_save_p, TABLESINDEX);
 	return n;
 }
 
-static void UnArchiveTables(void)
+static void UnArchiveTables(save_t *save_p)
 {
 	int TABLESINDEX;
 	UINT16 i, n;
@@ -1716,13 +1720,13 @@ static void UnArchiveTables(void)
 		lua_rawgeti(gL, TABLESINDEX, i);
 		while (true)
 		{
-			UINT8 e = UnArchiveValue(TABLESINDEX); // read key
+			UINT8 e = UnArchiveValue(save_p, TABLESINDEX); // read key
 			if (e == 1) // End of table
 				break;
 			else if (e == 2) // Key contains a new table
 				n++;
 
-			if (UnArchiveValue(TABLESINDEX) == 2) // read value
+			if (UnArchiveValue(save_p, TABLESINDEX) == 2) // read value
 				n++;
 
 			if (lua_isnil(gL, -2)) // if key is nil (if a function etc was accidentally saved)
@@ -1734,7 +1738,7 @@ static void UnArchiveTables(void)
 				lua_rawset(gL, -3);
 		}
 
-		metatableid = READUINT16(save_p);
+		metatableid = P_ReadUINT16(save_p);
 		if (metatableid)
 		{
 			// setmetatable(table, registry.metatables[metatableid])
@@ -1758,7 +1762,7 @@ void LUA_Step(void)
 	lua_gc(gL, LUA_GCSTEP, 1);
 }
 
-void LUA_Archive(void)
+void LUA_Archive(save_t *save_p)
 {
 	INT32 i;
 	thinker_t *th;
@@ -1771,29 +1775,30 @@ void LUA_Archive(void)
 		if (!playeringame[i] && i > 0) // dedicated servers...
 			continue;
 		// all players in game will be archived, even if they just add a 0.
-		ArchiveExtVars(&players[i], "player");
+		ArchiveExtVars(save_p, &players[i], "player");
 	}
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		// archive function will determine when to skip mobjs,
 		// and write mobjnum in otherwise.
-		ArchiveExtVars(th, "mobj");
+		ArchiveExtVars(save_p, th, "mobj");
 	}
 
-	WRITEUINT32(save_p, UINT32_MAX); // end of mobjs marker, replaces mobjnum.
+	P_WriteUINT32(save_p, UINT32_MAX); // end of mobjs marker, replaces mobjnum.
 
+	lua_save_p = save_p;
 	LUA_HookNetArchive(NetArchive); // call the NetArchive hook in archive mode
-	ArchiveTables();
+	ArchiveTables(save_p);
 
 	if (gL)
 		lua_pop(gL, 1); // pop tables
 }
 
-void LUA_UnArchive(void)
+void LUA_UnArchive(save_t *save_p)
 {
 	UINT32 mobjnum;
 	INT32 i;
@@ -1806,23 +1811,24 @@ void LUA_UnArchive(void)
 	{
 		if (!playeringame[i] && i > 0) // dedicated servers...
 			continue;
-		UnArchiveExtVars(&players[i]);
+		UnArchiveExtVars(save_p, &players[i]);
 	}
 
 	do {
-		mobjnum = READUINT32(save_p); // read a mobjnum
+		mobjnum = P_ReadUINT32(save_p); // read a mobjnum
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 			if (((mobj_t *)th)->mobjnum != mobjnum) // find matching mobj
 				continue;
-			UnArchiveExtVars(th); // apply variables
+			UnArchiveExtVars(save_p, th); // apply variables
 		}
 	} while(mobjnum != UINT32_MAX); // repeat until end of mobjs marker.
 
+	lua_save_p = save_p;
 	LUA_HookNetArchive(NetUnArchive); // call the NetArchive hook in unarchive mode
-	UnArchiveTables();
+	UnArchiveTables(save_p);
 
 	if (gL)
 		lua_pop(gL, 1); // pop tables
diff --git a/src/lua_script.h b/src/lua_script.h
index 45d2a37ffd1a636f1e6aaf6848fa868a25fba74b..c5a5069ef1a392ab9010783dd48bde51367bf537 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -13,6 +13,7 @@
 #ifndef LUA_SCRIPT_H
 #define LUA_SCRIPT_H
 
+#include "p_saveg.h"
 #include "m_fixed.h"
 #include "doomtype.h"
 #include "d_player.h"
@@ -52,8 +53,8 @@ void LUA_DumpFile(const char *filename);
 #endif
 fixed_t LUA_EvalMath(const char *word);
 void LUA_Step(void);
-void LUA_Archive(void);
-void LUA_UnArchive(void);
+void LUA_Archive(save_t *save_p);
+void LUA_UnArchive(save_t *save_p);
 int LUA_PushGlobals(lua_State *L, const char *word);
 int LUA_CheckGlobals(lua_State *L, const char *word);
 void Got_Luacmd(UINT8 **cp, INT32 playernum); // lua_consolelib.c
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 24d948a6735c3c723dcbf86c285a23c8367090dd..6650e60e6f353c3250644757c06d87ad1c1c7001 100644
--- a/src/lua_skinlib.c
+++ b/src/lua_skinlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2014-2016 by John "JTE" Muniz.
-// Copyright (C) 2014-2023 by Sonic Team Junior.
+// Copyright (C) 2014-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -54,7 +54,8 @@ enum skin {
 	skin_contspeed,
 	skin_contangle,
 	skin_soundsid,
-	skin_sprites,
+	skin_sprites, // TODO: 2.3: Delete
+	skin_skinsprites,
 	skin_supersprites,
 	skin_natkcolor
 };
@@ -95,7 +96,8 @@ static const char *const skin_opt[] = {
 	"contspeed",
 	"contangle",
 	"soundsid",
-	"sprites",
+	"sprites", // TODO: 2.3: Delete
+	"skinsprites",
 	"supersprites",
 	"natkcolor",
 	NULL};
@@ -219,7 +221,10 @@ static int skin_get(lua_State *L)
 	case skin_soundsid:
 		LUA_PushUserdata(L, skin->soundsid, META_SOUNDSID);
 		break;
-	case skin_sprites:
+	case skin_sprites: // TODO: 2.3: Delete
+		LUA_PushUserdata(L, skin->sprites_compat, META_SKINSPRITESCOMPAT);
+		break;
+	case skin_skinsprites:
 		LUA_PushUserdata(L, skin->sprites, META_SKINSPRITES);
 		break;
 	case skin_supersprites:
@@ -338,15 +343,7 @@ static int soundsid_num(lua_State *L)
 	return 1;
 }
 
-enum spritesopt {
-	numframes = 0
-};
-
-static const char *const sprites_opt[] = {
-	"numframes",
-	NULL};
-
-// skin.sprites[i] -> sprites[i]
+// skin.skinsprites[i] -> sprites[i]
 static int lib_getSkinSprite(lua_State *L)
 {
 	spritedef_t *sksprites = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITES);
@@ -359,13 +356,46 @@ static int lib_getSkinSprite(lua_State *L)
 	return 1;
 }
 
-// #skin.sprites -> NUMPLAYERSPRITES
+// #skin.skinsprites -> NUMPLAYERSPRITES
 static int lib_numSkinsSprites(lua_State *L)
 {
 	lua_pushinteger(L, NUMPLAYERSPRITES);
 	return 1;
 }
 
+// TODO: 2.3: Delete
+// skin.sprites[i] -> sprites[i]
+static int lib_getSkinSpriteCompat(lua_State *L)
+{
+	spritedef_t *sksprites = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITESCOMPAT);
+	INT32 i = luaL_checkinteger(L, 2) & (SPR2F_MASK | SPR2F_SUPER);
+
+	if (i & SPR2F_SUPER)
+		i = (i & ~SPR2F_SUPER) + NUMPLAYERSPRITES;
+
+	if (i < 0 || i >= NUMPLAYERSPRITES*2)
+		return luaL_error(L, "skin sprites index %d out of range (0 - %d)", i, (NUMPLAYERSPRITES*2)-1);
+
+	LUA_PushUserdata(L, &sksprites[i], META_SKINSPRITESLIST);
+	return 1;
+}
+
+// TODO: 2.3: Delete
+// #skin.sprites -> NUMPLAYERSPRITES*2
+static int lib_numSkinsSpritesCompat(lua_State *L)
+{
+	lua_pushinteger(L, NUMPLAYERSPRITES*2);
+	return 1;
+}
+
+enum spritesopt {
+	numframes = 0
+};
+
+static const char *const sprites_opt[] = {
+	"numframes",
+	NULL};
+
 static int sprite_get(lua_State *L)
 {
 	spritedef_t *sprite = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITESLIST);
@@ -387,6 +417,7 @@ int LUA_SkinLib(lua_State *L)
 	LUA_RegisterUserdataMetatable(L, META_SOUNDSID, soundsid_get, NULL, soundsid_num);
 	LUA_RegisterUserdataMetatable(L, META_SKINSPRITES, lib_getSkinSprite, NULL, lib_numSkinsSprites);
 	LUA_RegisterUserdataMetatable(L, META_SKINSPRITESLIST, sprite_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_SKINSPRITESCOMPAT, lib_getSkinSpriteCompat, NULL, lib_numSkinsSpritesCompat); // TODO: 2.3: Delete
 
 	skin_fields_ref = Lua_CreateFieldTable(L, skin_opt);
 
diff --git a/src/m_anigif.c b/src/m_anigif.c
index 6e6ec68aa49760ddc5ab9ffa025356e0a13ffee3..5ebc4892c2d6af8c9a2a31f446d11ef39736306c 100644
--- a/src/m_anigif.c
+++ b/src/m_anigif.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 2013-2016 by Matthew "Kaito Sinclaire" Walsh.
 // Copyright (C) 2013      by "Ninji".
-// Copyright (C) 2013-2023 by Sonic Team Junior.
+// Copyright (C) 2013-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 07f10071711025f93f91d7b7ea2d9d65f3d5ad1d..21ecc3312f2c5fa82f865600518d2863589ca986 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -562,7 +562,7 @@ void Command_Teleport_f(void)
 
 			for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 			{
-				if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				if (th->removing)
 					continue;
 
 				mo2 = (mobj_t *)th;
@@ -1072,7 +1072,7 @@ static mapthing_t *OP_CreateNewMapThing(player_t *player, UINT16 type, boolean c
 
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo = (mobj_t *)th;
@@ -1110,9 +1110,9 @@ static mapthing_t *OP_CreateNewMapThing(player_t *player, UINT16 type, boolean c
 	mt->pitch = mt->roll = 0;
 
 	// Ignore offsets
-	if (mt->type == MT_EMBLEM)
+	if (mt->type == mobjinfo[MT_EMBLEM].doomednum)
 		mt->args[1] = 1;
-	else
+	else if (!(mt->type == mobjinfo[MT_METALSONIC_RACE].doomednum || mt->type == mobjinfo[MT_ROSY].doomednum))
 		mt->args[0] = 1;
 
 	return mt;
diff --git a/src/m_cheat.h b/src/m_cheat.h
index 564c6da77ff7e0761faaff529ac4ed7a87ff3734..6bd1546f85b9cf9d99656e5f3bd1b0d39cddb4ae 100644
--- a/src/m_cheat.h
+++ b/src/m_cheat.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_cond.c b/src/m_cond.c
index 5a5913297157e24ada893ca5fad77a4d38ffd3e3..9706f76c83e06c2537bf1d57051ed80f0b3f0f84 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 2012-2023 by Sonic Team Junior.
+// Copyright (C) 2012-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -475,7 +475,7 @@ UINT8 M_MapLocked(INT32 mapnum, gamedata_t *data)
 		// that's better than making dedicated server's lives hell.
 		return false;
 	}
-	
+
 	if (cv_debug || devparm)
 		return false; // Unlock every level when in devmode.
 
@@ -494,6 +494,12 @@ UINT8 M_MapLocked(INT32 mapnum, gamedata_t *data)
 
 UINT8 M_CampaignWarpIsCheat(INT32 gt, INT32 mapnum, gamedata_t *data)
 {
+	if (dedicated)
+	{
+		// See M_MapLocked; don't make dedicated servers annoying.
+		return false;
+	}
+
 	if (M_MapLocked(mapnum, data) == true)
 	{
 		// Warping to locked maps is definitely always a cheat
diff --git a/src/m_easing.c b/src/m_easing.c
index 48fe1efbc579aee27a0d20dc0dfb4900d447653d..60ca6e9b6328fff8ec7b0abb99d517dfe3a8dbbc 100644
--- a/src/m_easing.c
+++ b/src/m_easing.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2023 by Lactozilla.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_easing.h b/src/m_easing.h
index eaa5d67735e02b831371ba6700d3e10dc8049628..6884833af68cb5b3671dc0cea70cdf734ac03d6b 100644
--- a/src/m_easing.h
+++ b/src/m_easing.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2023 by Lactozilla.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_fixed.c b/src/m_fixed.c
index 19c1e80919315071fe40086dc93ec42b937ffb26..ace05fd3bd624b96320ccb1be6e4545094c4b32e 100644
--- a/src/m_fixed.c
+++ b/src/m_fixed.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 // Copyright (C) 2009 by Stephen McGranahan.
 //
 // This program is free software distributed under the
diff --git a/src/m_fixed.h b/src/m_fixed.h
index f40c7b30870baffdd978cec0ecb447b65f74ba6f..504b53a6eea3ba7d8070cdfd635662bb19628f8e 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_menu.c b/src/m_menu.c
index 50011347550c32622238ef02a6d538be62bbb31f..1e4b832847cac7fb6b431e80709bea804b853c76 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2011-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -48,6 +48,7 @@
 #include "p_setup.h"
 #include "f_finale.h"
 #include "lua_hook.h"
+#include "lua_libs.h"
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -263,7 +264,7 @@ static void M_ConfirmTeamScramble(INT32 choice);
 static void M_ConfirmTeamChange(INT32 choice);
 static void M_SecretsMenu(INT32 choice);
 static void M_SetupChoosePlayer(INT32 choice);
-static UINT16 M_SetupChoosePlayerDirect(INT32 choice);
+static INT32 M_SetupChoosePlayerDirect(INT32 choice);
 static void M_QuitSRB2(INT32 choice);
 menu_t SP_MainDef, OP_MainDef;
 menu_t MISC_ScrambleTeamDef, MISC_ChangeTeamDef;
@@ -1071,9 +1072,8 @@ static menuitem_t OP_ChangeControlsMenu[] =
 	{IT_CALL | IT_STRING2, NULL, "Move Backward",    M_ChangeControl, GC_BACKWARD    },
 	{IT_CALL | IT_STRING2, NULL, "Move Left",        M_ChangeControl, GC_STRAFELEFT  },
 	{IT_CALL | IT_STRING2, NULL, "Move Right",       M_ChangeControl, GC_STRAFERIGHT },
-	{IT_CALL | IT_STRING2, NULL, "Jump",             M_ChangeControl, GC_JUMP        },
-	{IT_CALL | IT_STRING2, NULL, "Spin",             M_ChangeControl, GC_SPIN        },
-	{IT_CALL | IT_STRING2, NULL, "Shield Ability",   M_ChangeControl, GC_SHIELD      },
+	{IT_CALL | IT_STRING2, NULL, "Jump",             M_ChangeControl, GC_JUMP      },
+	{IT_CALL | IT_STRING2, NULL, "Spin",             M_ChangeControl, GC_SPIN     },
 	{IT_HEADER, NULL, "Camera", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
 	{IT_CALL | IT_STRING2, NULL, "Look Up",        M_ChangeControl, GC_LOOKUP      },
@@ -1122,15 +1122,13 @@ static menuitem_t OP_ChangeControlsMenu[] =
 
 static menuitem_t OP_Joystick1Menu[] =
 {
-	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup1PJoystickMenu,  0},
-
-	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis         , 20},
-	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis         , 30},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis         , 40},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis         , 50},
-	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis         , 60},
-	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis         , 70},
-	{IT_STRING | IT_CVAR,  NULL, "Shield Axis"       , &cv_shieldaxis       , 80},
+	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup1PJoystickMenu, 10},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis         , 30},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis         , 40},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis         , 50},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis         , 60},
+	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis         , 70},
+	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis         , 80},
 	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis         , 90},
 	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis        ,100},
 
@@ -1142,15 +1140,13 @@ static menuitem_t OP_Joystick1Menu[] =
 
 static menuitem_t OP_Joystick2Menu[] =
 {
-	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup2PJoystickMenu,  0},
-
-	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis2        , 20},
-	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis2        , 30},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis2        , 40},
-	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis2        , 50},
-	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis2        , 60},
-	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis2        , 70},
-	{IT_STRING | IT_CVAR,  NULL, "Shield Axis"       , &cv_shieldaxis2      , 80},
+	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup2PJoystickMenu, 10},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis2        , 30},
+	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis2        , 40},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis2        , 50},
+	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis2        , 60},
+	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis2        , 70},
+	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis2        , 80},
 	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis2        , 90},
 	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis2       ,100},
 
@@ -2102,6 +2098,12 @@ menu_t OP_PlaystyleDef = {
 	0, 0, 0, NULL
 };
 
+static void M_UpdateItemOn(void)
+{
+	I_SetTextInputMode((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_STRING ||
+		(currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_KEYHANDLER);
+}
+
 static void M_VideoOptions(INT32 choice)
 {
 	(void)choice;
@@ -2329,6 +2331,7 @@ void Nextmap_OnChange(void)
 		{
 			currentMenu->lastOn = itemOn;
 			itemOn = nastart;
+			M_UpdateItemOn();
 		}
 	}
 	else if (currentMenu == &SP_TimeAttackDef)
@@ -2378,6 +2381,7 @@ void Nextmap_OnChange(void)
 		{
 			currentMenu->lastOn = itemOn;
 			itemOn = tastart;
+			M_UpdateItemOn();
 		}
 
 		if (mapheaderinfo[cv_nextmap.value-1] && mapheaderinfo[cv_nextmap.value-1]->forcecharacter[0] != '\0')
@@ -3128,6 +3132,7 @@ static void M_NextOpt(void)
 		else
 			itemOn++;
 	} while (oldItemOn != itemOn && ( (currentMenu->menuitems[itemOn].status & IT_TYPE) & IT_SPACE ));
+	M_UpdateItemOn();
 }
 
 static void M_PrevOpt(void)
@@ -3140,6 +3145,7 @@ static void M_PrevOpt(void)
 		else
 			itemOn--;
 	} while (oldItemOn != itemOn && ( (currentMenu->menuitems[itemOn].status & IT_TYPE) & IT_SPACE ));
+	M_UpdateItemOn();
 }
 
 // lock out further input in a tic when important buttons are pressed
@@ -3306,7 +3312,7 @@ boolean M_Responder(event_t *ev)
 
 	if (ch == -1)
 		return false;
-	else if (ch == gamecontrol[GC_SYSTEMMENU][0] || ch == gamecontrol[GC_SYSTEMMENU][1]) // allow remappable ESC key
+	else if (ev->type != ev_text && (ch == gamecontrol[GC_SYSTEMMENU][0] || ch == gamecontrol[GC_SYSTEMMENU][1])) // allow remappable ESC key
 		ch = KEY_ESCAPE;
 
 	// F-Keys
@@ -3384,9 +3390,16 @@ boolean M_Responder(event_t *ev)
 	// Handle menuitems which need a specific key handling
 	if (routine && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_KEYHANDLER)
 	{
+		// block text input if ctrl is held, to allow using ctrl+c ctrl+v and ctrl+x
+		if (ctrldown)
+		{
+			routine(ch);
+			return true;
+		}
+
 		// ignore ev_keydown events if the key maps to a character, since
 		// the ev_text event will follow immediately after in that case.
-		if (ev->type == ev_keydown && ch >= 32 && ch <= 127)
+		if (ev->type == ev_keydown && ((ch >= 32 && ch <= 127) || (ch >= KEY_KEYPAD7 && ch <= KEY_KPADDEL)))
 			return true;
 
 		routine(ch);
@@ -3414,7 +3427,7 @@ boolean M_Responder(event_t *ev)
 		{
 			// dirty hack: for customising controls, I want only buttons/keys, not moves
 			if (ev->type == ev_mouse || ev->type == ev_mouse2 || ev->type == ev_joystick
-				|| ev->type == ev_joystick2)
+				|| ev->type == ev_joystick2 || ev->type == ev_text)
 				return true;
 			if (routine)
 			{
@@ -3651,21 +3664,27 @@ void M_StartControlPanel(void)
 
 		currentMenu = &MainDef;
 		itemOn = singleplr;
+		M_UpdateItemOn();
 	}
 	else if (modeattacking)
 	{
 		currentMenu = &MAPauseDef;
 		MAPauseMenu[mapause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 		itemOn = mapause_continue;
+		M_UpdateItemOn();
 	}
 	else if (!(netgame || multiplayer)) // Single Player
 	{
-		// Devmode unlocks Pandora's Box in the pause menu
-		boolean pandora = ((M_SecretUnlocked(SECRET_PANDORA, serverGamedata) || cv_debug || devparm) && !marathonmode);
-		
-		if (gamestate != GS_LEVEL || ultimatemode) // intermission, so gray out stuff.
+		// Devmode unlocks Pandora's Box and Level Select in the pause menu
+		boolean isforbidden = (marathonmode || ultimatemode);
+		boolean isdebug = ((cv_debug || devparm) && !isforbidden);
+		boolean pandora = ((M_SecretUnlocked(SECRET_PANDORA, serverGamedata) && !isforbidden) || isdebug);
+		boolean lselect = ((maplistoption != 0 && !isforbidden) || isdebug);
+
+		if (gamestate != GS_LEVEL) // intermission, so gray out stuff.
 		{
 			SPauseMenu[spause_pandora].status = (pandora) ? (IT_GRAYEDOUT) : (IT_DISABLED);
+			SPauseMenu[spause_levelselect].status = (lselect) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 			SPauseMenu[spause_retry].status = IT_GRAYEDOUT;
 		}
 		else
@@ -3675,6 +3694,11 @@ void M_StartControlPanel(void)
 				++numlives;
 
 			SPauseMenu[spause_pandora].status = (pandora) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+			SPauseMenu[spause_levelselect].status = (lselect) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+			if (ultimatemode)
+			{
+				SPauseMenu[spause_retry].status = IT_GRAYEDOUT;
+			}
 
 			// The list of things that can disable retrying is (was?) a little too complex
 			// for me to want to use the short if statement syntax
@@ -3684,13 +3708,6 @@ void M_StartControlPanel(void)
 				SPauseMenu[spause_retry].status = (IT_STRING | IT_CALL);
 		}
 
-		// We can always use level select though. :33
-		// Guarantee it if we have either it unlocked or devmode is enabled
-		if ((maplistoption != 0 || M_SecretUnlocked(SECRET_LEVELSELECT, serverGamedata) || cv_debug || devparm) && !marathonmode)
-			SPauseMenu[spause_levelselect].status = (IT_STRING | IT_CALL);
-		else
-			SPauseMenu[spause_levelselect].status = (IT_DISABLED);
-
 		// And emblem hints.
 		SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
@@ -3703,6 +3720,7 @@ void M_StartControlPanel(void)
 
 		currentMenu = &SPauseDef;
 		itemOn = spause_continue;
+		M_UpdateItemOn();
 	}
 	else // multiplayer
 	{
@@ -3744,6 +3762,7 @@ void M_StartControlPanel(void)
 
 		currentMenu = &MPauseDef;
 		itemOn = mpause_continue;
+		M_UpdateItemOn();
 	}
 
 	CON_ToggleOff(); // move away console
@@ -3775,6 +3794,7 @@ void M_ClearMenus(boolean callexitmenufunc)
 	hidetitlemap = false;
 
 	I_UpdateMouseGrab();
+	I_SetTextInputMode(textinputmodeenabledbylua);
 }
 
 //
@@ -3837,6 +3857,7 @@ void M_SetupNextMenu(menu_t *menudef)
 			}
 		}
 	}
+	M_UpdateItemOn();
 
 	hidetitlemap = false;
 }
@@ -3873,6 +3894,9 @@ void M_Ticker(void)
 		M_SetupScreenshotMenu();
 
 #if defined (MASTERSERVER) && defined (HAVE_THREADS)
+	if (!netgame)
+		return;
+
 	I_lock_mutex(&ms_ServerList_mutex);
 	{
 		if (ms_ServerList)
@@ -4015,11 +4039,11 @@ static void M_DrawThermo(INT32 x, INT32 y, consvar_t *cv)
 	lumpnum_t leftlump, rightlump, centerlump[2], cursorlump;
 	patch_t *p;
 
-	leftlump = W_GetNumForName("M_THERML");
-	rightlump = W_GetNumForName("M_THERMR");
-	centerlump[0] = W_GetNumForName("M_THERMM");
-	centerlump[1] = W_GetNumForName("M_THERMM");
-	cursorlump = W_GetNumForName("M_THERMO");
+	leftlump = W_GetNumForPatchName("M_THERML");
+	rightlump = W_GetNumForPatchName("M_THERMR");
+	centerlump[0] = W_GetNumForPatchName("M_THERMM");
+	centerlump[1] = W_GetNumForPatchName("M_THERMM");
+	cursorlump = W_GetNumForPatchName("M_THERMO");
 
 	V_DrawScaledPatch(xx, y, 0, p = W_CachePatchNum(leftlump,PU_PATCH));
 	xx += p->width - p->leftoffset;
@@ -4141,7 +4165,7 @@ static void M_DrawStaticBox(fixed_t x, fixed_t y, INT32 flags, fixed_t w, fixed_
 			temp = (gametic % temp) * h*2*FRACUNIT; // Which frame to draw
 
 		V_DrawCroppedPatch(x*FRACUNIT, y*FRACUNIT, (w*FRACUNIT) / 160, (h*FRACUNIT) / 100, flags, patch, NULL, 0, temp, w*2*FRACUNIT, h*2*FRACUNIT);
-		
+
 		W_UnlockCachedPatch(patch);
 		return;
 	}
@@ -6109,6 +6133,7 @@ void M_StartMessage(const char *string, void *routine, menumessagetype_t itemtyp
 
 	currentMenu = &MessageDef;
 	itemOn = 0;
+	M_UpdateItemOn();
 }
 
 static void M_DrawMessageMenu(void)
@@ -6183,6 +6208,7 @@ static void M_HandleImageDef(INT32 choice)
 			if (itemOn >= (INT16)(currentMenu->numitems-1))
 				itemOn = 0;
             else itemOn++;
+			M_UpdateItemOn();
 			break;
 
 		case KEY_LEFTARROW:
@@ -6193,6 +6219,7 @@ static void M_HandleImageDef(INT32 choice)
 			if (!itemOn)
 				itemOn = currentMenu->numitems - 1;
 			else itemOn--;
+			M_UpdateItemOn();
 			break;
 
 		case KEY_ESCAPE:
@@ -6982,7 +7009,10 @@ static void M_LevelSelectWarp(INT32 choice)
 	if (currentMenu == &SP_LevelSelectDef || currentMenu == &SP_PauseLevelSelectDef)
 	{
 		if (cursaveslot > 0) // do we have a save slot to load?
+		{
+			CV_StealthSet(&cv_skin, DEFAULTSKIN); // already handled by loadgame so we don't want this
 			G_LoadGame((UINT32)cursaveslot, startmap); // reload from SP save data: this is needed to keep score/lives/continues from reverting to defaults
+		}
 		else // no save slot, start new game but keep the current skin
 		{
 			M_ClearMenus(true);
@@ -7389,6 +7419,7 @@ static void M_EmblemHints(INT32 choice)
 	SR_EmblemHintDef.prevMenu = currentMenu;
 	M_SetupNextMenu(&SR_EmblemHintDef);
 	itemOn = 2; // always start on back.
+	M_UpdateItemOn();
 }
 
 static void M_DrawEmblemHints(void)
@@ -7521,13 +7552,9 @@ static void M_PauseLevelSelect(INT32 choice)
 	SP_PauseLevelSelectDef.prevMenu = currentMenu;
 	levellistmode = LLM_LEVELSELECT;
 
-	// maplistoption is only specified if not set already
-	// and we have the level select unlocked so that it
+	// maplistoption is NOT specified, so that this
 	// transfers the level select list from the menu
 	// used to enter the game to the pause menu.
-	if (maplistoption == 0 && M_SecretUnlocked(SECRET_LEVELSELECT, serverGamedata))
-		maplistoption = 1;
-
 	if (!M_PrepareLevelPlatter(-1, true))
 	{
 		M_StartMessage(M_GetText("No selectable levels found.\n"),NULL,MM_NOTHING);
@@ -8251,6 +8278,7 @@ static void M_StartTutorial(INT32 choice)
 	gamecomplete = 0;
 	cursaveslot = 0;
 	maplistoption = 0;
+	CV_StealthSet(&cv_skin, DEFAULTSKIN); // tutorial accounts for sonic only
 	G_DeferedInitNew(false, G_BuildMapName(tutorialmap), 0, false, false);
 }
 
@@ -8350,7 +8378,7 @@ static void M_DrawLoadGameData(void)
 				if (savegameinfo[savetodraw].lives == -42)
 					col = 26;
 				else if (savegameinfo[savetodraw].botskin == 3) // & knuckles
-					col = 105;
+					col = 106;
 				else if (savegameinfo[savetodraw].botskin) // tailsbot or custom
 					col = 134;
 				else
@@ -8410,7 +8438,17 @@ static void M_DrawLoadGameData(void)
 			if (savegameinfo[savetodraw].lives == -42)
 				V_DrawRightAlignedThinString(x + 79, y, V_GRAYMAP, "NEW GAME");
 			else if (savegameinfo[savetodraw].lives == -666)
-				V_DrawRightAlignedThinString(x + 79, y, V_REDMAP, "CAN'T LOAD!");
+			{
+				if (savegameinfo[savetodraw].continuescore == -62)
+				{
+					V_DrawRightAlignedThinString(x + 79, y, V_REDMAP, "ADDON NOT LOADED");
+					V_DrawRightAlignedThinString(x + 79, y-10, V_REDMAP, savegameinfo[savetodraw].skinname);
+				}
+				else
+				{
+					V_DrawRightAlignedThinString(x + 79, y, V_REDMAP, "CAN'T LOAD!");
+				}
+			}
 			else if (savegameinfo[savetodraw].gamemap & 8192)
 				V_DrawRightAlignedThinString(x + 79, y, V_GREENMAP, "CLEAR!");
 			else
@@ -8630,14 +8668,20 @@ static void M_LoadSelect(INT32 choice)
 		M_NewGame();
 	}
 	else if (savegameinfo[saveSlotSelected-1].gamemap & 8192) // Completed
+	{
 		M_LoadGameLevelSelect(0);
+	}
 	else
+	{
+		CV_StealthSet(&cv_skin, DEFAULTSKIN); // already handled by loadgame so we don't want this
 		G_LoadGame((UINT32)saveSlotSelected, 0);
+	}
 
 	cursaveslot = saveSlotSelected;
 }
 
 #define VERSIONSIZE 16
+#define MISSING { savegameinfo[slot].continuescore = -62; savegameinfo[slot].lives = -666; Z_Free(savebuffer); return; }
 #define BADSAVE { savegameinfo[slot].lives = -666; Z_Free(savebuffer); return; }
 #define CHECKPOS if (sav_p >= end_p) BADSAVE
 // Reads the save file to list lives, level, player, etc.
@@ -8734,10 +8778,11 @@ static void M_ReadSavegameInfo(UINT32 slot)
 		CHECKPOS
 		READSTRINGN(sav_p, ourSkinName, SKINNAMESIZE);
 		savegameinfo[slot].skinnum = R_SkinAvailable(ourSkinName);
+		STRBUFCPY(savegameinfo[slot].skinname, ourSkinName);
 
 		if (savegameinfo[slot].skinnum >= numskins
 		|| !R_SkinUsable(-1, savegameinfo[slot].skinnum))
-			BADSAVE
+			MISSING
 
 		CHECKPOS
 		READSTRINGN(sav_p, botSkinName, SKINNAMESIZE);
@@ -8745,7 +8790,7 @@ static void M_ReadSavegameInfo(UINT32 slot)
 
 		if (savegameinfo[slot].botskin-1 >= numskins
 		|| !R_SkinUsable(-1, savegameinfo[slot].botskin-1))
-			BADSAVE
+			MISSING
 	}
 
 	CHECKPOS
@@ -8790,6 +8835,7 @@ static void M_ReadSavegameInfo(UINT32 slot)
 }
 #undef CHECKPOS
 #undef BADSAVE
+#undef MISSING
 
 //
 // M_ReadSaveStrings
@@ -9053,7 +9099,7 @@ static void M_CacheCharacterSelectEntry(INT32 i, INT32 skinnum)
 		description[i].namepic = W_CachePatchName(description[i].nametag, PU_PATCH);
 }
 
-static UINT16 M_SetupChoosePlayerDirect(INT32 choice)
+static INT32 M_SetupChoosePlayerDirect(INT32 choice)
 {
 	INT32 skinnum, botskinnum;
 	UINT16 i;
@@ -9142,7 +9188,7 @@ static UINT16 M_SetupChoosePlayerDirect(INT32 choice)
 
 static void M_SetupChoosePlayer(INT32 choice)
 {
-	UINT16 skinset = M_SetupChoosePlayerDirect(choice);
+	INT32 skinset = M_SetupChoosePlayerDirect(choice);
 	if (skinset != MAXCHARACTERSLOTS)
 	{
 		M_ChoosePlayer(skinset);
@@ -9491,6 +9537,8 @@ static void M_ChoosePlayer(INT32 choice)
 	//lastmapsaved = 0;
 	gamecomplete = 0;
 
+	CV_StealthSet(&cv_skin, skins[skinnum]->name);
+
 	G_DeferedInitNew(ultmode, G_BuildMapName(startmap), skinnum, false, fromlevelselect);
 	COM_BufAddText("dummyconsvar 1\n"); // G_DeferedInitNew doesn't do this
 
@@ -10050,6 +10098,7 @@ static void M_TimeAttack(INT32 choice)
 		Nextmap_OnChange();
 
 	itemOn = tastart; // "Start" is selected.
+	M_UpdateItemOn();
 }
 
 // Drawing function for Nights Attack
@@ -10166,7 +10215,7 @@ void M_DrawNightsAttackMenu(void)
 			skinnumber = 0; //Default to Sonic
 		else
 			skinnumber = (cv_chooseskin.value-1);
-			
+
 		spritedef_t *sprdef = &skins[skinnumber]->sprites[SPR2_NFLY]; //Make our patch the selected character's NFLY sprite
 		spritetimer = FixedInt(ntsatkdrawtimer/2) % skins[skinnumber]->sprites[SPR2_NFLY].numframes; //Make the sprite timer cycle though all the frames at 2 tics per frame
 		spriteframe_t *sprframe = &sprdef->spriteframes[spritetimer]; //Our animation frame is equal to the number on the timer
@@ -10179,11 +10228,14 @@ void M_DrawNightsAttackMenu(void)
 			color = skins[skinnumber]->supercolor+4;
 		else //If you don't go super in NiGHTS or at all, use prefcolor
 			color = skins[skinnumber]->prefcolor;
-				
+
 		angle_t fa = (FixedAngle(((FixedInt(ntsatkdrawtimer * 4)) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
+		fixed_t scale = skins[skinnumber]->highresscale;
+		if (skins[skinnumber]->shieldscale)
+			scale = FixedDiv(scale, skins[skinnumber]->shieldscale);
 
 		V_DrawFixedPatch(270<<FRACBITS, (186<<FRACBITS) - 8*FINESINE(fa),
-						 FixedDiv(skins[skinnumber]->highresscale, skins[skinnumber]->shieldscale), 
+						 scale,
 						 (sprframe->flip & 1<<6) ? V_FLIP : 0,
 						 natksprite,
 						 R_GetTranslationColormap(TC_BLINK, color, GTC_CACHE));
@@ -10288,6 +10340,7 @@ static void M_NightsAttack(INT32 choice)
 		Nextmap_OnChange();
 
 	itemOn = nastart; // "Start" is selected.
+	M_UpdateItemOn();
 }
 
 // Player has selected the "START" from the nights attack screen
@@ -10607,6 +10660,7 @@ static void M_ModeAttackEndGame(INT32 choice)
 		break;
 	}
 	itemOn = currentMenu->lastOn;
+	M_UpdateItemOn();
 	G_SetGamestate(GS_TIMEATTACK);
 	modeattacking = ATTACKING_NONE;
 	M_ChangeMenuMusic("_title", true);
@@ -10688,6 +10742,7 @@ static void M_Marathon(INT32 choice)
 	titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please
 	M_SetupNextMenu(&SP_MarathonDef);
 	itemOn = marathonstart; // "Start" is selected.
+	M_UpdateItemOn();
 	recatkdrawtimer = (50-8) * FRACUNIT;
 	char_scroll = 0;
 }
@@ -11078,7 +11133,7 @@ static void M_Refresh(INT32 choice)
 
 	// note: this is the one case where 0 is a valid room number
 	// because it corresponds to "All"
-	CL_UpdateServerList(!(ms_RoomId < 0), ms_RoomId);
+	CL_UpdateServerList(cv_masterserver_room_id.value >= 0, cv_masterserver_room_id.value);
 
 	// first page of servers
 	serverlistpage = 0;
@@ -11088,37 +11143,11 @@ static INT32 menuRoomIndex = 0;
 
 static void M_DrawRoomMenu(void)
 {
-	static fixed_t frame = -(12 << FRACBITS);
-	int dot_frame;
-	char text[4];
-
 	const char *rmotd;
 	const char *waiting_message;
 
-	int dots;
-
 	if (m_waiting_mode)
-	{
-		dot_frame = (int)(frame >> FRACBITS) / 4;
-		dots = dot_frame + 3;
-
-		strcpy(text, "   ");
-
-		if (dots > 0)
-		{
-			if (dot_frame < 0)
-				dot_frame = 0;
-
-			if (dot_frame != 3)
-				strncpy(&text[dot_frame], "...", min(dots, 3 - dot_frame));
-		}
-
-		frame += renderdeltatics;
-		while (frame >= (12 << FRACBITS))
-			frame -= 12 << FRACBITS;
-
-		currentMenu->menuitems[0].text = text;
-	}
+		currentMenu->menuitems[0].text = "...";
 
 	// use generic drawer for cursor, items and title
 	M_DrawGenericMenu();
@@ -11164,7 +11193,7 @@ static void M_DrawConnectMenu(void)
 		numPages = 1;
 
 	// Room name
-	if (ms_RoomId < 0)
+	if (cv_masterserver_room_id.value < 0)
 		V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ConnectMenu[mp_connect_room].alphaKey,
 		                         V_YELLOWMAP, (itemOn == mp_connect_room) ? "<Select to change>" : "<Unlisted Mode>");
 	else
@@ -11392,7 +11421,7 @@ static void M_ConnectMenu(INT32 choice)
 
 	// first page of servers
 	serverlistpage = 0;
-	if (ms_RoomId < 0)
+	if (cv_masterserver_room_id.value < 0)
 	{
 		M_RoomMenu(0); // Select a room instead of staring at an empty list
 		// This prevents us from returning to the modified game alert.
@@ -11401,6 +11430,7 @@ static void M_ConnectMenu(INT32 choice)
 	else
 		M_SetupNextMenu(&MP_ConnectDef);
 	itemOn = 0;
+	M_UpdateItemOn();
 	M_Refresh(0);
 }
 
@@ -11487,10 +11517,10 @@ static void M_ChooseRoom(INT32 choice)
 #endif
 
 	if (choice == 0)
-		ms_RoomId = -1;
+		CV_SetValue(&cv_masterserver_room_id, -1);
 	else
 	{
-		ms_RoomId = roomIds[choice-1];
+		CV_SetValue(&cv_masterserver_room_id, roomIds[choice-1]);
 		menuRoomIndex = choice - 1;
 	}
 
@@ -11559,7 +11589,7 @@ static void M_DrawServerMenu(void)
 	if (currentMenu == &MP_ServerDef)
 	{
 		M_DrawLevelPlatterHeader(currentMenu->y - lsheadingheight/2, "Server settings", true, false);
-		if (ms_RoomId < 0)
+		if (cv_masterserver_room_id.value < 0)
 			V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ServerMenu[mp_server_room].alphaKey,
 			                         V_YELLOWMAP, (itemOn == mp_server_room) ? "<Select to change>" : "<Unlisted Mode>");
 		else
@@ -11655,11 +11685,12 @@ static void M_ServerOptions(INT32 choice)
 static void M_StartServerMenu(INT32 choice)
 {
 	(void)choice;
-	ms_RoomId = -1;
+	CV_SetValue(&cv_masterserver_room_id, -1);
 	levellistmode = LLM_CREATESERVER;
 	Newgametype_OnChange();
 	M_SetupNextMenu(&MP_ServerDef);
 	itemOn = 1;
+	M_UpdateItemOn();
 }
 
 // ==============
@@ -11826,8 +11857,7 @@ static void M_HandleConnectIP(INT32 choice)
 
 			if ( ctrldown ) {
 				switch (choice) {
-					case 'v':
-					case 'V': // ctrl+v, pasting
+					case 'v': // ctrl+v, pasting
 					{
 						const char *paste = I_ClipboardPaste();
 
@@ -11840,17 +11870,21 @@ static void M_HandleConnectIP(INT32 choice)
 						break;
 					}
 					case KEY_INS:
-					case 'c':
-					case 'C': // ctrl+c, ctrl+insert, copying
-						I_ClipboardCopy(setupm_ip, l);
-						S_StartSound(NULL,sfx_menu1); // Tails
+					case 'c': // ctrl+c, ctrl+insert, copying
+						if (l != 0) // Don't replace the clipboard without any text
+						{
+							I_ClipboardCopy(setupm_ip, l);
+							S_StartSound(NULL,sfx_menu1); // Tails
+						}
 						break;
 
-					case 'x':
-					case 'X': // ctrl+x, cutting
-						I_ClipboardCopy(setupm_ip, l);
-						S_StartSound(NULL,sfx_menu1); // Tails
-						setupm_ip[0] = 0;
+					case 'x': // ctrl+x, cutting
+						if (l != 0) // Don't replace the clipboard without any text
+						{
+							I_ClipboardCopy(setupm_ip, l);
+							S_StartSound(NULL,sfx_menu1); // Tails
+							setupm_ip[0] = 0;
+						}
 						break;
 
 					default: // otherwise do nothing
@@ -11874,9 +11908,12 @@ static void M_HandleConnectIP(INT32 choice)
 							break;
 						}
 					case KEY_DEL: // shift+delete, cutting
-						I_ClipboardCopy(setupm_ip, l);
-						S_StartSound(NULL,sfx_menu1); // Tails
-						setupm_ip[0] = 0;
+						if (l != 0) // Don't replace the clipboard without any text
+						{
+							I_ClipboardCopy(setupm_ip, l);
+							S_StartSound(NULL,sfx_menu1); // Tails
+							setupm_ip[0] = 0;
+						}
 						break;
 					default: // otherwise do nothing.
 						break;
@@ -11897,15 +11934,6 @@ static void M_HandleConnectIP(INT32 choice)
 				setupm_ip[l] = (char)choice;
 				setupm_ip[l+1] = 0;
 			}
-			else if (choice >= 199 && choice <= 211 && choice != 202 && choice != 206) //numpad too!
-			{
-				char keypad_translation[] = {'7','8','9','-','4','5','6','+','1','2','3','0','.'};
-				choice = keypad_translation[choice - 199];
-				S_StartSound(NULL,sfx_menu1); // Tails
-				setupm_ip[l] = (char)choice;
-				setupm_ip[l+1] = 0;
-			}
-
 			break;
 	}
 
@@ -12256,7 +12284,9 @@ static void M_DrawSetupMultiPlayerMenu(void)
 	if (multi_frame >= sprdef->numframes)
 		multi_frame = 0;
 
-	scale = FixedDiv(skins[setupm_fakeskin]->highresscale, skins[setupm_fakeskin]->shieldscale);
+	scale = skins[setupm_fakeskin]->highresscale;
+	if (skins[setupm_fakeskin]->shieldscale)
+		scale = FixedDiv(scale, skins[setupm_fakeskin]->shieldscale);
 
 #define chary (y+64)
 
@@ -12289,7 +12319,7 @@ static void M_DrawSetupMultiPlayerMenu(void)
 	V_DrawFixedPatch(
 		x<<FRACBITS,
 		chary<<FRACBITS,
-		FixedDiv(skins[setupm_fakeskin]->highresscale, skins[setupm_fakeskin]->shieldscale),
+		scale,
 		flags, patch, colormap);
 
 	goto colordraw;
@@ -13071,7 +13101,10 @@ static void M_SetupScreenshotMenu(void)
 	{
 		item->status = IT_GRAYEDOUT;
 		if ((currentMenu == &OP_ScreenshotOptionsDef) && (itemOn == op_screenshot_colorprofile)) // Can't select that
+		{
 			itemOn = op_screenshot_storagelocation;
+			M_UpdateItemOn();
+		}
 	}
 	else
 #endif
@@ -13269,23 +13302,23 @@ static void M_Setup1PControlsMenu(INT32 choice)
 	currentMenu->lastOn = itemOn;
 
 	// Unhide the nine non-P2 controls and their headers
-	//OP_ChangeControlsMenu[19+0].status = IT_HEADER;
-	//OP_ChangeControlsMenu[19+1].status = IT_SPACE;
+	//OP_ChangeControlsMenu[18+0].status = IT_HEADER;
+	//OP_ChangeControlsMenu[18+1].status = IT_SPACE;
 	// ...
-	OP_ChangeControlsMenu[19+2].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[19+3].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[19+4].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[19+5].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[19+6].status = IT_CALL|IT_STRING2;
-	//OP_ChangeControlsMenu[19+7].status = IT_CALL|IT_STRING2;
-	//OP_ChangeControlsMenu[19+8].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[19+9].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[18+2].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[18+3].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[18+4].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[18+5].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[18+6].status = IT_CALL|IT_STRING2;
+	//OP_ChangeControlsMenu[18+7].status = IT_CALL|IT_STRING2;
+	//OP_ChangeControlsMenu[18+8].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[18+9].status = IT_CALL|IT_STRING2;
 	// ...
-	OP_ChangeControlsMenu[29+0].status = IT_HEADER;
-	OP_ChangeControlsMenu[29+1].status = IT_SPACE;
+	OP_ChangeControlsMenu[28+0].status = IT_HEADER;
+	OP_ChangeControlsMenu[28+1].status = IT_SPACE;
 	// ...
-	OP_ChangeControlsMenu[29+2].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[29+3].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[28+2].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[28+3].status = IT_CALL|IT_STRING2;
 
 	OP_ChangeControlsDef.prevMenu = &OP_P1ControlsDef;
 	OP_ChangeControlsDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS); // remove second level
@@ -13301,23 +13334,23 @@ static void M_Setup2PControlsMenu(INT32 choice)
 	currentMenu->lastOn = itemOn;
 
 	// Hide the nine non-P2 controls and their headers
-	//OP_ChangeControlsMenu[19+0].status = IT_GRAYEDOUT2;
-	//OP_ChangeControlsMenu[19+1].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[18+0].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[18+1].status = IT_GRAYEDOUT2;
 	// ...
-	OP_ChangeControlsMenu[19+2].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[19+3].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[19+4].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[19+5].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[19+6].status = IT_GRAYEDOUT2;
-	//OP_ChangeControlsMenu[19+7].status = IT_GRAYEDOUT2;
-	//OP_ChangeControlsMenu[19+8].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[19+9].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[18+2].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[18+3].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[18+4].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[18+5].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[18+6].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[18+7].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[18+8].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[18+9].status = IT_GRAYEDOUT2;
 	// ...
-	OP_ChangeControlsMenu[29+0].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[29+1].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[28+0].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[28+1].status = IT_GRAYEDOUT2;
 	// ...
-	OP_ChangeControlsMenu[29+2].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[29+3].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[28+2].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[28+3].status = IT_GRAYEDOUT2;
 
 	OP_ChangeControlsDef.prevMenu = &OP_P2ControlsDef;
 	OP_ChangeControlsDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS); // remove second level
@@ -14118,6 +14151,33 @@ static INT32 quitsounds[] =
 	sfx_chchng // Tails 11-09-99
 };
 
+const char *QuitScreenMessages[3] = {
+	(
+	"Design and content in\n"
+	"SRB2 is copyright\n"
+	"1998-2025 by STJr. All\n"
+	"original material in\n"
+	"this game is copyrighted\n"
+	"by their respective\n"
+	"owners, and no copyright\n"
+	"infringement is\n"
+	"intended. STJr's staff\n"
+	"make no profit\n"
+	"whatsoever (in\n"
+	"fact, we lose\n"
+	"money)."
+	),
+
+	(
+	"THIS GAME SHOULD NOT BE SOLD!"
+	),
+
+	(
+	"STJr is in no way affiliated\n"
+	"with SEGA or Sonic Team."
+	)
+};
+
 void M_QuitResponse(INT32 ch)
 {
 	tic_t ptime;
@@ -14139,6 +14199,9 @@ void M_QuitResponse(INT32 ch)
 		while (ptime > I_GetTime())
 		{
 			V_DrawScaledPatch(0, 0, 0, W_CachePatchName("GAMEQUIT", PU_PATCH)); // Demo 3 Quit Screen Tails 06-16-2001
+			V_DrawCenteredString(2+(V_StringWidth(QuitScreenMessages[0], V_ALLOWLOWERCASE)/2), 4, V_ALLOWLOWERCASE, QuitScreenMessages[0]);
+			V_DrawCenteredString(160, 166, V_ALLOWLOWERCASE|V_REDMAP, QuitScreenMessages[1]);
+			V_DrawCenteredString(160, 176, V_ALLOWLOWERCASE, QuitScreenMessages[2]);
 			I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001
 			I_Sleep(cv_sleep.value);
 			I_UpdateTime(cv_timescale.value);
diff --git a/src/m_menu.h b/src/m_menu.h
index 99a5b6de4266be02b1cba0e98a26addac5524548..dc8bef8b136e1d819e7680f5a7bca1e6cf8b94c1 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2011-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -143,7 +143,7 @@ typedef enum
 
 typedef struct
 {
-	char bgname[8]; // name for background gfx lump; lays over titlemap if this is set
+	char bgname[8+1]; // name for background gfx lump; lays over titlemap if this is set
 	SINT8 fadestrength;  // darken background when displaying this menu, strength 0-31 or -1 for undefined
 	INT32 bgcolor; // fill color, overrides bg name. -1 means follow bg name rules.
 	INT32 titlescrollxspeed; // background gfx scroll per menu; inherits global setting
@@ -153,13 +153,13 @@ typedef struct
 	SINT8 hidetitlepics; // hide title gfx per menu; -1 means undefined, inherits global setting
 	ttmode_enum ttmode; // title wing animation mode; default TTMODE_OLD
 	UINT8 ttscale; // scale of title wing gfx (FRACUNIT / ttscale); -1 means undefined, inherits global setting
-	char ttname[9]; // lump name of title wing gfx. If name length is <= 6, engine will attempt to load numbered frames (TTNAMExx)
+	char ttname[8+1]; // lump name of title wing gfx. If name length is <= 6, engine will attempt to load numbered frames (TTNAMExx)
 	INT16 ttx; // X position of title wing
 	INT16 tty; // Y position of title wing
 	INT16 ttloop; // # frame to loop; -1 means dont loop
 	UINT16 tttics; // # of tics per frame
 
-	char musname[7]; ///< Music track to play. "" for no music.
+	char musname[6+1]; ///< Music track to play. "" for no music.
 	UINT16 mustrack; ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore.
 	boolean muslooping; ///< Loop the music
 	boolean musstop; ///< Don't play any music
@@ -369,8 +369,8 @@ extern menu_t OP_JoystickSetDef;
 typedef struct
 {
 	boolean used;
-	char notes[441];
-	char picname[8];
+	char notes[440+1];
+	char picname[8+1];
 	char skinname[SKINNAMESIZE*2+2]; // skin&skin\0
 	patch_t *charpic;
 	UINT8 prev;
@@ -421,6 +421,7 @@ typedef struct
 {
 	char levelname[32];
 	UINT8 skinnum;
+	char skinname [SKINNAMESIZE+1];
 	UINT8 botskin;
 	UINT8 numemeralds;
 	UINT8 numgameovers;
diff --git a/src/m_misc.c b/src/m_misc.c
index 55c5485a149a2b2c43e48fab973c5ad898091036..f8087e8c2b920a44b5642e7ee8d9fb30c72ab62d 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1720,7 +1720,7 @@ char *va(const char *format, ...)
 	static char string[1024];
 
 	va_start(argptr, format);
-	vsprintf(string, format, argptr);
+	vsnprintf(string, 1024, format, argptr);
 	va_end(argptr);
 
 	return string;
@@ -2208,6 +2208,8 @@ int M_JumpWordReverse(const char *line, int offset)
 {
 	int (*is)(int);
 	int c;
+	if (offset == 0) // Don't let "--offset" later result in a negative value
+		return 0;
 	c = line[--offset];
 	if (isspace(c))
 		is = isspace;
diff --git a/src/m_misc.h b/src/m_misc.h
index 04ac66ca65e1ff92d44172b12e1186cdaa04b648..63681094430950d90263165b052d9118e2240524 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index b9948bdc0c3284a3473e7e6e8b95b2e3bd8fb568..f36a1c80949bec45166c57a4630296f8a531c728 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -127,8 +127,8 @@ perfstatrow_t commoncounter_rows[] = {
 };
 
 perfstatrow_t interpolation_rows[] = {
-	{"intpfrc", "Interp frac: ", &ps_interp_frac, PS_TIME},
-	{"intplag", "Interp lag:  ", &ps_interp_lag, PS_TIME},
+	{"intpfrc", "Interp frac: ", &ps_interp_frac, 0}, // PS_TIME is not applicable here, as it is meant for I_GetPreciseTime
+	{"intplag", "Interp lag:  ", &ps_interp_lag, 0},
 	{0}
 };
 
@@ -453,7 +453,7 @@ static int PS_DrawPerfRows(int x, int y, int color, perfstatrow_t *rows)
 	return draw_y;
 }
 
-static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boolean frame_metric, boolean set_user)
+static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boolean frame_metric)
 {
 	int index = frame_metric ? ps_frame_index : ps_tick_index;
 
@@ -461,7 +461,7 @@ static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boo
 	{
 		// allocate history table
 		int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
-		void** memory_user = set_user ? &metric->history : NULL;
+		void** memory_user = &metric->history;
 
 		metric->history = Z_Calloc(value_size * cv_ps_samplesize.value, PU_PERFSTATS,
 				memory_user);
@@ -491,7 +491,7 @@ static void PS_UpdateRowHistories(perfstatrow_t *rows, boolean frame_metric)
 	for (row = rows; row->lores_label; row++)
 	{
 		if (PS_IsRowValid(row))
-			PS_UpdateMetricHistory(row->metric, !!(row->flags & PS_TIME), frame_metric, true);
+			PS_UpdateMetricHistory(row->metric, !!(row->flags & PS_TIME), frame_metric);
 	}
 }
 
@@ -584,7 +584,7 @@ static void PS_CountThinkers(void)
 		for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
 		{
 			ps_thinkercount.value.i++;
-			if (thinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (thinker->removing)
 				ps_removecount.value.i++;
 			else if (i == THINK_POLYOBJ)
 				ps_polythcount.value.i++;
@@ -649,17 +649,17 @@ void PS_UpdateTickStats(void)
 			if (cv_perfstats.value == 3)
 			{
 				for (i = 0; i < thinkframe_hooks_length; i++)
-					PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false, false);
+					PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false);
 			}
 			else if (cv_perfstats.value == 4)
 			{
 				for (i = 0; i < prethinkframe_hooks_length; i++)
-					PS_UpdateMetricHistory(&prethinkframe_hooks[i].time_taken, true, false, false);
+					PS_UpdateMetricHistory(&prethinkframe_hooks[i].time_taken, true, false);
 			}
 			else if (cv_perfstats.value == 5)
 			{
 				for (i = 0; i < postthinkframe_hooks_length; i++)
-					PS_UpdateMetricHistory(&postthinkframe_hooks[i].time_taken, true, false, false);
+					PS_UpdateMetricHistory(&postthinkframe_hooks[i].time_taken, true, false);
 			}
 		}
 		if (cv_perfstats.value)
diff --git a/src/m_random.c b/src/m_random.c
index 536fbfbbd1077abf6ae400bd4d8773a7574e4b08..a063e88f47ccc40f10ffa09d441016d3bd20c13b 100644
--- a/src/m_random.c
+++ b/src/m_random.c
@@ -193,9 +193,9 @@ INT32 M_RandomKey(INT32 a)
   */
 INT32 M_RandomRange(INT32 a, INT32 b)
 {
-  	if (b < a)
+	if (b < a)
 	{
-    	INT32 temp;
+		INT32 temp;
 
 		temp = a;
 		a = b;
diff --git a/src/m_tokenizer.c b/src/m_tokenizer.c
index 8bb094caea7ec94db70ccdffe57fd4191f5a9773..2f89d4dfbb3e40e4f7426a655056f36405d1a0c3 100644
--- a/src/m_tokenizer.c
+++ b/src/m_tokenizer.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2013-2023 by Sonic Team Junior.
+// Copyright (C) 2013-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -28,6 +28,7 @@ tokenizer_t *Tokenizer_Open(const char *inputString, size_t len, unsigned numTok
 	tokenizer->endPos = 0;
 	tokenizer->inputLength = 0;
 	tokenizer->inComment = 0;
+	tokenizer->stringNeedsEscaping = false;
 	tokenizer->inString = 0;
 	tokenizer->get = Tokenizer_Read;
 
@@ -92,6 +93,124 @@ static void DetectComment(tokenizer_t *tokenizer, UINT32 *pos)
 		tokenizer->inComment = 2;
 }
 
+// This function detects escape sequences in a string and attempts to convert them.
+static size_t EscapeString(char *output, const char *input, size_t inputLength)
+{
+	const char *end = input + inputLength;
+
+	size_t i = 0;
+
+	while (input < end)
+	{
+		char chr = *input++;
+
+		if (chr == '\\')
+		{
+			chr = *input++;
+
+			switch (chr)
+			{
+			case 'n':
+				output[i] = '\n';
+				i++;
+				break;
+			case 't':
+				output[i] = '\t';
+				i++;
+				break;
+			case '\\':
+				output[i] = '\\';
+				i++;
+				break;
+			case '"':
+				output[i] = '\"';
+				i++;
+				break;
+			case 'x': {
+				int out = 0, c;
+				int j = 0;
+
+				chr = *input++;
+
+				for (j = 0; j < 5 && isxdigit(chr); j++)
+				{
+					c = ((chr <= '9') ? (chr - '0') : (tolower(chr) - 'a' + 10));
+					out = (out << 4) | c;
+					chr = *input++;
+				}
+
+				input--;
+
+				switch (j)
+				{
+					case 4:
+						output[i] = (out >> 8) & 0xFF;
+						i++;
+						/* FALLTHRU */
+					case 2:
+						output[i] = out & 0xFF;
+						i++;
+						break;
+					default:
+						// TODO: Displaying parsing errors properly will require
+						// some refactoring of the tokenizer itself. For now,
+						// this function will silently return an empty string
+						// if it encounters a malformed escape sequence.
+						// This situation cannot happen for i.e. UDMF comments,
+						// so it's okay to do this right now.
+						// CONS_Alert(CONS_WARNING, "Escape sequence has wrong size\n");
+						i = 0;
+						goto done;
+				}
+
+				break;
+			}
+			default:
+				if (isdigit(chr))
+				{
+					int out = 0;
+					int j = 0;
+					do
+					{
+						out = 10*out + (chr - '0');
+						chr = *input++;
+					} while (++j < 3 && isdigit(chr));
+
+					input--;
+
+					if (out > 255)
+					{
+						// CONS_Alert(CONS_WARNING, "Escape sequence is too large\n");
+						i = 0;
+						goto done;
+					}
+
+					output[i] = out;
+					i++;
+				}
+				else
+				{
+					// CONS_Alert(CONS_WARNING, "Unknown escape sequence '\\%c'\n", chr);
+					i = 0;
+					goto done;
+				}
+				break;
+			}
+		}
+		else
+		{
+			output[i] = chr;
+			i++;
+		}
+	}
+
+done:
+	output[i] = '\0';
+	i++;
+
+	return i;
+}
+
 static void Tokenizer_ReadTokenString(tokenizer_t *tokenizer, UINT32 i)
 {
 	UINT32 tokenLength = tokenizer->endPos - tokenizer->startPos;
@@ -101,10 +220,46 @@ static void Tokenizer_ReadTokenString(tokenizer_t *tokenizer, UINT32 i)
 		// Assign the memory. Don't forget an extra byte for the end of the string!
 		tokenizer->token[i] = (char *)Z_Malloc(tokenizer->capacity[i] * sizeof(char), PU_STATIC, NULL);
 	}
+
 	// Copy the string.
-	M_Memcpy(tokenizer->token[i], tokenizer->input + tokenizer->startPos, (size_t)tokenLength);
-	// Make the final character NUL.
-	tokenizer->token[i][tokenLength] = '\0';
+	if (tokenizer->stringNeedsEscaping)
+	{
+		EscapeString(tokenizer->token[i], tokenizer->input + tokenizer->startPos, (size_t)tokenLength);
+	}
+	else
+	{
+		M_Memcpy(tokenizer->token[i], tokenizer->input + tokenizer->startPos, (size_t)tokenLength);
+
+		// Make the final character NUL.
+		tokenizer->token[i][tokenLength] = '\0';
+	}
+}
+
+static void ScanString(tokenizer_t *tokenizer)
+{
+	tokenizer->stringNeedsEscaping = false;
+
+	while (tokenizer->input[tokenizer->endPos] != '"' && tokenizer->endPos < tokenizer->inputLength)
+	{
+		if (!DetectLineBreak(tokenizer, tokenizer->endPos))
+		{
+			// Skip one character ahead if this looks like an escape sequence
+			if (tokenizer->input[tokenizer->endPos] == '\\')
+			{
+				tokenizer->stringNeedsEscaping = true;
+				tokenizer->endPos++;
+
+				// Oh. Naughty. We hit the end of the input.
+				// Stop scanning, then.
+				if (tokenizer->endPos == tokenizer->inputLength)
+					return;
+
+				DetectLineBreak(tokenizer, tokenizer->endPos);
+			}
+		}
+
+		tokenizer->endPos++;
+	}
 }
 
 const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
@@ -117,11 +272,7 @@ const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
 	// If in a string, return the entire string within quotes, except without the quotes.
 	if (tokenizer->inString == 1)
 	{
-		while (tokenizer->input[tokenizer->endPos] != '"' && tokenizer->endPos < tokenizer->inputLength)
-		{
-			DetectLineBreak(tokenizer, tokenizer->endPos);
-			tokenizer->endPos++;
-		}
+		ScanString(tokenizer);
 
 		Tokenizer_ReadTokenString(tokenizer, i);
 		tokenizer->inString = 2;
@@ -134,6 +285,7 @@ const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
 		tokenizer->token[i][0] = tokenizer->input[tokenizer->startPos];
 		tokenizer->token[i][1] = '\0';
 		tokenizer->inString = 0;
+		tokenizer->stringNeedsEscaping = false;
 		return tokenizer->token[i];
 	}
 
@@ -281,11 +433,7 @@ const char *Tokenizer_SRB2Read(tokenizer_t *tokenizer, UINT32 i)
 	else if (tokenizer->input[tokenizer->startPos] == '"')
 	{
 		tokenizer->endPos = ++tokenizer->startPos;
-		while (tokenizer->input[tokenizer->endPos] != '"' && tokenizer->endPos < tokenizer->inputLength)
-		{
-			DetectLineBreak(tokenizer, tokenizer->endPos);
-			tokenizer->endPos++;
-		}
+		ScanString(tokenizer);
 
 		Tokenizer_ReadTokenString(tokenizer, i);
 		tokenizer->endPos++;
diff --git a/src/m_tokenizer.h b/src/m_tokenizer.h
index 7ee856b3c9331d8dbb0b97711bd5e8bcae7ec2e7..bc7090cd82a87ca7065b8355e0f4be5b73675fbe 100644
--- a/src/m_tokenizer.h
+++ b/src/m_tokenizer.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2013-2023 by Sonic Team Junior.
+// Copyright (C) 2013-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -26,6 +26,7 @@ typedef struct Tokenizer
 	UINT32 inputLength;
 	UINT8 inComment; // 0 = not in comment, 1 = // Single-line, 2 = /* Multi-line */
 	UINT8 inString; // 0 = not in string, 1 = in string, 2 = just left string
+	boolean stringNeedsEscaping;
 	int line;
 	const char *(*get)(struct Tokenizer*, UINT32);
 } tokenizer_t;
diff --git a/src/m_vector.c b/src/m_vector.c
index 3132a869d458c83aac517ac609258b506b6395b8..1b04f8ae1461b6a11f34cc07e6765c3ea640b330 100644
--- a/src/m_vector.c
+++ b/src/m_vector.c
@@ -21,6 +21,32 @@ void DVector3_Load(dvector3_t *vec, double x, double y, double z)
 	vec->z = z;
 }
 
+void DVector3_Copy(dvector3_t *a_o, const dvector3_t *a_i)
+{
+	memcpy(a_o, a_i, sizeof(dvector3_t));
+}
+
+void DVector3_Add(const dvector3_t *a_i, const dvector3_t *a_c, dvector3_t *a_o)
+{
+	a_o->x = a_i->x + a_c->x;
+	a_o->y = a_i->y + a_c->y;
+	a_o->z = a_i->z + a_c->z;
+}
+
+void DVector3_Subtract(const dvector3_t *a_i, const dvector3_t *a_c, dvector3_t *a_o)
+{
+	a_o->x = a_i->x - a_c->x;
+	a_o->y = a_i->y - a_c->y;
+	a_o->z = a_i->z - a_c->z;
+}
+
+void DVector3_Multiply(const dvector3_t *a_i, double a_c, dvector3_t *a_o)
+{
+	a_o->x = a_i->x * a_c;
+	a_o->y = a_i->y * a_c;
+	a_o->z = a_i->z * a_c;
+}
+
 double DVector3_Magnitude(const dvector3_t *a_normal)
 {
 	double xs = a_normal->x * a_normal->x;
diff --git a/src/m_vector.h b/src/m_vector.h
index 55669be037c9ce54ffd70bf3782225e913e1a1b4..1395744f2984050991923bfc714e5a3a028ccfe6 100644
--- a/src/m_vector.h
+++ b/src/m_vector.h
@@ -19,6 +19,10 @@ typedef struct
 } dvector3_t;
 
 void DVector3_Load(dvector3_t *vec, double x, double y, double z);
+void DVector3_Copy(dvector3_t *a_o, const dvector3_t *a_i);
+void DVector3_Add(const dvector3_t *a_i, const dvector3_t *a_c, dvector3_t *a_o);
+void DVector3_Subtract(const dvector3_t *a_i, const dvector3_t *a_c, dvector3_t *a_o);
+void DVector3_Multiply(const dvector3_t *a_i, double a_c, dvector3_t *a_o);
 double DVector3_Magnitude(const dvector3_t *a_normal);
 double DVector3_Normalize(dvector3_t *a_normal);
 void DVector3_Negate(dvector3_t *a_o);
diff --git a/src/netcode/client_connection.c b/src/netcode/client_connection.c
index 917e32b598e03d86f8540b997329069f279817fb..7b21a76cd82ee6e887e5bdc88fcf52d1dc4a4eb6 100644
--- a/src/netcode/client_connection.c
+++ b/src/netcode/client_connection.c
@@ -54,6 +54,8 @@ static boolean IsDownloadingFile(void)
 static void DrawConnectionStatusBox(void)
 {
 	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+	if (cl_mode != CL_DOWNLOADSAVEGAME && filedownload.current != -1)
+		M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-46-8, 32, 1);
 
 	if (cl_mode == CL_CONFIRMCONNECT || IsDownloadingFile())
 		return;
@@ -84,6 +86,33 @@ static void DrawFileProgress(fileneeded_t *file, int y)
 	V_DrawRightAlignedString(BASEVIDWIDTH/2+128, y, V_20TRANS|V_MONOSPACE, va("%3.1fK/s ", ((double)getbps)/1024));
 }
 
+static void DrawOverallProgress(int y)
+{
+	UINT32 totalsize = filedownload.totalsize;
+	INT32 downloadedfiles = filedownload.completednum;
+	INT32 totalfiles = filedownload.remaining + filedownload.completednum;
+	INT32 downloaded = filedownload.completedsize;
+	if (fileneeded[filedownload.current].currentsize != fileneeded[filedownload.current].totalsize)
+		downloaded = filedownload.completedsize + fileneeded[filedownload.current].currentsize;
+
+	INT32 dldlength = (INT32)((downloaded/(double)totalsize) * 256);
+	if (dldlength > 256)
+		dldlength = 256;
+	V_DrawFill(BASEVIDWIDTH/2-128, y, 256, 8, 111);
+	V_DrawFill(BASEVIDWIDTH/2-128, y, dldlength, 8, 96);
+
+	const char *progress_str;
+	if (totalsize >= 1024*1024)
+		progress_str = va(" %.2fMiB/%.2fMiB", (double)downloaded / (1024*1024), (double)totalsize / (1024*1024));
+	else if (totalsize < 1024)
+		progress_str = va(" %4uB/%4uB", downloaded, totalsize);
+	else
+		progress_str = va(" %.2fKiB/%.2fKiB", (double)downloaded / 1024, (double)totalsize / 1024);
+
+	V_DrawString(BASEVIDWIDTH/2-128, y, V_20TRANS|V_ALLOWLOWERCASE, progress_str);
+	V_DrawRightAlignedString(BASEVIDWIDTH/2+128, y, V_20TRANS|V_ALLOWLOWERCASE, va("%2u/%2u Files ", downloadedfiles+1, totalfiles));
+}
+
 //
 // CL_DrawConnectionStatus
 //
@@ -96,7 +125,7 @@ static void CL_DrawConnectionStatus(void)
 	// Draw background fade
 	V_DrawFadeScreen(0xFF00, 16); // force default
 
-	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADHTTPFILES && cl_mode != CL_LOADFILES)
+	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADHTTPFILES && cl_mode != CL_LOADFILES && cl_mode != CL_CHECKFILES && cl_mode != CL_ASKFULLFILELIST)
 	{
 		INT32 animtime = ((ccstime / 4) & 15) + 16;
 		UINT8 palstart;
@@ -179,6 +208,31 @@ static void CL_DrawConnectionStatus(void)
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
 				va(" %2u/%2u files",loadcompletednum,fileneedednum));
 		}
+		else if ((cl_mode == CL_CHECKFILES) || (cl_mode == CL_ASKFULLFILELIST))
+		{
+			INT32 totalfileslength;
+			INT32 checkcompletednum = 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_NOTCHECKED)
+						checkcompletednum++;
+			}
+
+			// Check progress
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Checking server addon list...");
+			totalfileslength = (INT32)((checkcompletednum/(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",checkcompletednum,fileneedednum));
+		}
 		else if (filedownload.current != -1)
 		{
 			char tempname[28];
@@ -224,7 +278,7 @@ static void CL_DrawConnectionStatus(void)
 			const char *download_str = M_GetText("Downloading \"%s\"");
 #endif
 
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_ALLOWLOWERCASE|V_YELLOWMAP,
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-46-24, V_ALLOWLOWERCASE|V_YELLOWMAP,
 				va(download_str, tempname));
 
 			// Rusty: actually lets do this instead
@@ -244,16 +298,18 @@ static void CL_DrawConnectionStatus(void)
 					strlcpy(tempname, http_source, sizeof(tempname));
 				}
 
-				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
+				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-46-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
 					va(M_GetText("from %s"), tempname));
 			}
 			else
 			{
-				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
+				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-46-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
 					M_GetText("from the server"));
 			}
+			DrawFileProgress(file, BASEVIDHEIGHT-46);
 
-			DrawFileProgress(file, BASEVIDHEIGHT-16);
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-14, V_ALLOWLOWERCASE|V_YELLOWMAP, "Total Progress");
+			DrawOverallProgress(BASEVIDHEIGHT-16);
 		}
 		else
 		{
@@ -546,6 +602,7 @@ static void AbortConnection(void)
 {
 	Snake_Free(&snake);
 
+	CURLAbortFile();
 	D_QuitNetGame();
 	CL_Reset();
 	D_StartTitle();
@@ -656,6 +713,7 @@ static void ShowDownloadConsentMessage(void)
 		if (IsFileDownloadable(&fileneeded[i]))
 			totalsize += fileneeded[i].totalsize;
 	}
+	filedownload.totalsize = totalsize;
 
 	const char *downloadsize = GetPrintableFileSize(totalsize);
 
@@ -1062,10 +1120,6 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 				}
 			}
 
-			// Rusty TODO: multithread
-			if (filedownload.http_running)
-				CURLGetFile();
-
 			if (waitmore)
 				break; // exit the case
 
@@ -1217,7 +1271,7 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 
 void CL_ConnectToServer(void)
 {
-	INT32 pnumnodes, nodewaited = doomcom->numnodes, i;
+	INT32 pnumnodes, nodewaited = numnetnodes, i;
 	tic_t oldtic;
 	tic_t asksent;
 	char tmpsave[256];
@@ -1243,7 +1297,7 @@ void CL_ConnectToServer(void)
 	if (gamestate == GS_INTERMISSION)
 		Y_EndIntermission(); // clean up intermission graphics etc
 
-	DEBFILE(va("waiting %d nodes\n", doomcom->numnodes));
+	DEBFILE(va("waiting %d nodes\n", numnetnodes));
 	G_SetGamestate(GS_WAITINGPLAYERS);
 	wipegamestate = GS_WAITINGPLAYERS;
 
@@ -1404,7 +1458,7 @@ void PT_ServerCFG(SINT8 node)
 
 	netnodes[(UINT8)servernode].ingame = true;
 	serverplayer = netbuffer->u.servercfg.serverplayer;
-	doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum);
+	numslots = SHORT(netbuffer->u.servercfg.totalslotnum);
 	mynode = netbuffer->u.servercfg.clientnode;
 	if (serverplayer >= 0)
 		playernode[(UINT8)serverplayer] = servernode;
diff --git a/src/netcode/commands.c b/src/netcode/commands.c
index f3bbc8a8616224b65a481559bd96eccbb3577f5e..8d6dab06b5da5628245f307a1519ef74a0ea9588 100644
--- a/src/netcode/commands.c
+++ b/src/netcode/commands.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
index 2bf9d2dde7cb60266687282748142af5e676c917..92a4cd784e3cfc409cbdf2d78fd7ab4d090999a5 100644
--- a/src/netcode/d_clisrv.c
+++ b/src/netcode/d_clisrv.c
@@ -113,8 +113,11 @@ consvar_t cv_blamecfail = CVAR_INIT ("blamecfail", "Off", CV_SAVE|CV_NETVAR, CV_
 static CV_PossibleValue_t playbackspeed_cons_t[] = {{1, "MIN"}, {10, "MAX"}, {0, NULL}};
 consvar_t cv_playbackspeed = CVAR_INIT ("playbackspeed", "1", 0, playbackspeed_cons_t, NULL);
 
-consvar_t cv_idletime = CVAR_INIT ("idletime", "0", CV_SAVE, CV_Unsigned, NULL);
-consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE, CV_Unsigned, NULL);
+consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE|CV_NETVAR, CV_Unsigned, NULL);
+
+static CV_PossibleValue_t idleaction_cons_t[] = {{1, "Kick"}, {2, "Spectate"}, {0, NULL}};
+consvar_t cv_idleaction = CVAR_INIT ("idleaction", "Spectate", CV_SAVE|CV_NETVAR, idleaction_cons_t, NULL);
+consvar_t cv_idletime = CVAR_INIT ("idletime", "3", CV_SAVE|CV_NETVAR, CV_Unsigned, NULL);
 
 consvar_t cv_httpsource = CVAR_INIT ("http_source", "", CV_SAVE, NULL, NULL);
 
@@ -146,8 +149,8 @@ void CL_Reset(void)
 	multiplayer = false;
 	servernode = 0;
 	server = true;
-	doomcom->numnodes = 1;
-	doomcom->numslots = 1;
+	numnetnodes = 1;
+	numslots = 1;
 	SV_StopServer();
 	SV_ResetServer();
 
@@ -212,8 +215,8 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 			CL_ClearPlayer(newplayernum);
 		playeringame[newplayernum] = true;
 		G_AddPlayer(newplayernum);
-		if (newplayernum+1 > doomcom->numslots)
-			doomcom->numslots = (INT16)(newplayernum+1);
+		if (newplayernum+1 > numslots)
+			numslots = (INT16)(newplayernum+1);
 
 		if (server && I_GetNodeAddress)
 		{
@@ -609,8 +612,8 @@ void CL_RemovePlayer(INT32 playernum, kickreason_t reason)
 
 	// remove avatar of player
 	playeringame[playernum] = false;
-	while (!playeringame[doomcom->numslots-1] && doomcom->numslots > 1)
-		doomcom->numslots--;
+	while (!playeringame[numslots-1] && numslots > 1)
+		numslots--;
 
 	// Reset the name
 	sprintf(player_names[playernum], "Player %d", playernum+1);
@@ -638,6 +641,7 @@ void CL_RemovePlayer(INT32 playernum, kickreason_t reason)
 void D_QuitNetGame(void)
 {
 	mousegrabbedbylua = true;
+	textinputmodeenabledbylua = false;
 	I_UpdateMouseGrab();
 
 	if (!netgame || !netbuffer)
@@ -660,7 +664,7 @@ void D_QuitNetGame(void)
 			if (netnodes[i].ingame)
 				HSendPacket(i, true, 0, 0);
 #ifdef MASTERSERVER
-		if (serverrunning && ms_RoomId > 0)
+		if (serverrunning && cv_masterserver_room_id.value > 0)
 			UnregisterServer();
 #endif
 	}
@@ -670,6 +674,7 @@ void D_QuitNetGame(void)
 		HSendPacket(servernode, true, 0, 0);
 	}
 
+	seenplayer = NULL;
 	D_CloseConnection();
 	ClearAdminPlayers();
 
@@ -750,7 +755,7 @@ void SV_ResetServer(void)
 	if (server)
 		servernode = 0;
 
-	doomcom->numslots = 0;
+	numslots = 0;
 
 	// clear server_context
 	memset(server_context, '-', 8);
@@ -794,7 +799,7 @@ void SV_SpawnServer(void)
 		{
 			I_NetOpenSocket();
 #ifdef MASTERSERVER
-			if (ms_RoomId > 0)
+			if (cv_masterserver_room_id.value > 0)
 				RegisterServer();
 #endif
 		}
@@ -802,7 +807,7 @@ void SV_SpawnServer(void)
 		// non dedicated server just connect to itself
 		if (!dedicated)
 			CL_ConnectToServer();
-		else doomcom->numslots = 1;
+		else numslots = 1;
 	}
 }
 
@@ -1360,19 +1365,33 @@ static void IdleUpdate(void)
 	if (!server || !netgame)
 		return;
 
-	for (i = 1; i < MAXPLAYERS; i++)
+	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		if (cv_idletime.value && playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && !players[i].bot && !IsPlayerAdmin(i) && i != serverplayer && gamestate == GS_LEVEL)
+		if (playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && !players[i].bot && gamestate == GS_LEVEL)
 		{
 			if (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons)
 				players[i].lastinputtime = 0;
 			else
 				players[i].lastinputtime++;
 
-			if (players[i].lastinputtime > (tic_t)cv_idletime.value * TICRATE * 60)
+			if (cv_idletime.value && !IsPlayerAdmin(i) && i != serverplayer && !(players[i].pflags & PF_FINISHED) && players[i].lastinputtime > (tic_t)cv_idletime.value * TICRATE * 60)
 			{
 				players[i].lastinputtime = 0;
-				SendKick(i, KICK_MSG_IDLE | KICK_MSG_KEEP_BODY);
+				if (cv_idleaction.value == 2 && G_GametypeHasSpectators())
+				{
+					changeteam_union NetPacket;
+					UINT16 usvalue;
+					NetPacket.value.l = NetPacket.value.b = 0;
+					NetPacket.packet.newteam = 0;
+					NetPacket.packet.playernum = i;
+					NetPacket.packet.verification = true; // This signals that it's a server change
+					usvalue = SHORT(NetPacket.value.l|NetPacket.value.b);
+					SendNetXCmd(XD_TEAMCHANGE, &usvalue, sizeof(usvalue));
+				}
+				else if (cv_idleaction.value == 1)
+				{
+					SendKick(i, KICK_MSG_IDLE | KICK_MSG_KEEP_BODY);
+				}
 			}
 		}
 		else
@@ -1399,6 +1418,83 @@ static void IdleUpdate(void)
 	}
 }
 
+static void DedicatedIdleUpdate(INT32 *realtics)
+{
+	const tic_t dedicatedidletime = cv_dedicatedidletime.value * TICRATE;
+	static tic_t dedicatedidletimeprev = 0;
+	static tic_t dedicatedidle = 0;
+
+	if (!server || !dedicated || gamestate != GS_LEVEL)
+		return;
+
+	if (dedicatedidletime > 0)
+	{
+		INT32 i;
+
+		boolean empty = true;
+		for (i = 0; i < MAXPLAYERS; i++)
+			if (playeringame[i])
+			{
+				empty = false;
+				break;
+			}
+
+		if (empty)
+		{
+			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
+			{
+				dedicatedidle += (*realtics);
+
+				if (dedicatedidle >= dedicatedidletime)
+				{
+					const char *idlereason = "at round start";
+					if (leveltime > 3)
+						idlereason = va("for %d seconds", dedicatedidle / TICRATE);
+
+					CONS_Printf("DEDICATED: No players %s, idling...\n", idlereason);
+					(*realtics) = 0;
+					dedicatedidle = dedicatedidletime;
+				}
+			}
+		}
+		else
+		{
+			if (dedicatedidle >= dedicatedidletime)
+			{
+				CONS_Printf("DEDICATED: Awakening from idle (Player detected...)\n");
+			}
+			dedicatedidle = 0;
+		}
+	}
+	else
+	{
+		if (dedicatedidletimeprev > 0 && dedicatedidle >= dedicatedidletimeprev)
+		{
+			CONS_Printf("DEDICATED: Awakening from idle (Idle disabled...)\n");
+		}
+		dedicatedidle = 0;
+	}
+
+	dedicatedidletimeprev = dedicatedidletime;
+}
+
 // Handle timeouts to prevent definitive freezes from happenning
 static void HandleNodeTimeouts(void)
 {
@@ -1475,69 +1571,7 @@ void NetUpdate(void)
 			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;
- 	}
+	DedicatedIdleUpdate(&realtics);
 
 	gametime = nowtime;
 
@@ -1784,7 +1818,7 @@ INT16 Consistancy(void)
 	{
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo = (mobj_t *)th;
diff --git a/src/netcode/d_clisrv.h b/src/netcode/d_clisrv.h
index 5aac4693d2fdd166cee7a796166ba7c798e183c8..86af61e9e02eb9528b759ce635970a96e3a28ee4 100644
--- a/src/netcode/d_clisrv.h
+++ b/src/netcode/d_clisrv.h
@@ -73,7 +73,7 @@ extern UINT32 realpingtable[MAXPLAYERS];
 extern UINT32 playerpingtable[MAXPLAYERS];
 extern tic_t servermaxping;
 
-extern consvar_t cv_netticbuffer, cv_resynchattempts, cv_blamecfail, cv_playbackspeed, cv_idletime, cv_dedicatedidletime;
+extern consvar_t cv_netticbuffer, cv_resynchattempts, cv_blamecfail, cv_playbackspeed, cv_idletime, cv_idleaction, cv_dedicatedidletime;
 extern consvar_t cv_httpsource;
 
 // Used in d_net, the only dependence
diff --git a/src/netcode/d_net.c b/src/netcode/d_net.c
index b24409db158909970a73fb5bd7630500a4f3d63f..c6fa86f197a431e9ef1dcda5d16ec09a496f8c24 100644
--- a/src/netcode/d_net.c
+++ b/src/netcode/d_net.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -48,6 +48,10 @@
 #define FORCECLOSE 0x8000
 tic_t connectiontimeout = (10*TICRATE);
 
+INT16 numnetnodes;
+INT16 numslots;
+INT16 extratics;
+
 /// \brief network packet
 doomcom_t *doomcom = NULL;
 /// \brief network packet data, points inside doomcom
@@ -62,16 +66,11 @@ static doomdata_t reboundstore[MAXREBOUND];
 static INT16 reboundsize[MAXREBOUND];
 static INT32 rebound_head, rebound_tail;
 
-/// \brief bandwith of netgame
-INT32 net_bandwidth;
-
 /// \brief max length per packet
 INT16 hardware_MAXPACKETLENGTH;
 
 boolean (*I_NetGet)(void) = NULL;
 void (*I_NetSend)(void) = NULL;
-boolean (*I_NetCanSend)(void) = NULL;
-boolean (*I_NetCanGet)(void) = NULL;
 void (*I_NetCloseSocket)(void) = NULL;
 void (*I_NetFreeNodenum)(INT32 nodenum) = NULL;
 SINT8 (*I_NetMakeNodewPort)(const char *address, const char* port) = NULL;
@@ -168,12 +167,6 @@ typedef struct
 	// ack return to send (like sliding window protocol)
 	UINT8 firstacktosend;
 
-	// when no consecutive packets are received we keep in mind what packets
-	// we already received in a queue
-	UINT8 acktosend_head;
-	UINT8 acktosend_tail;
-	UINT8 acktosend[MAXACKTOSEND];
-
 	// automatically send keep alive packet when not enough trafic
 	tic_t lasttimeacktosend_sent;
 	// detect connection lost
@@ -193,22 +186,17 @@ static node_t nodes[MAXNETNODES];
 //         0 if a = n (mod 256)
 //        >0 if a > b (mod 256)
 // mnemonic: to use it compare to 0: cmpack(a,b)<0 is "a < b" ...
-FUNCMATH static INT32 cmpack(UINT8 a, UINT8 b)
+FUNCMATH static inline INT32 cmpack(UINT8 a, UINT8 b)
 {
-	register INT32 d = a - b;
-
-	if (d >= 127 || d < -128)
-		return -d;
-	return d;
+	return (SINT8)(a - b);
 }
 
 /** Sets freeack to a free acknum and copies the netbuffer in the ackpak table
   *
   * \param freeack  The address to store the free acknum at
-  * \param lowtimer ???
   * \return True if a free acknum was found
   */
-static boolean GetFreeAcknum(UINT8 *freeack, boolean lowtimer)
+static boolean GetFreeAcknum(UINT8 *freeack)
 {
 	node_t *node = &nodes[doomcom->remotenode];
 	INT32 numfreeslot = 0;
@@ -237,17 +225,8 @@ static boolean GetFreeAcknum(UINT8 *freeack, boolean lowtimer)
 				node->nextacknum++;
 			ackpak[i].destinationnode = (UINT8)(node - nodes);
 			ackpak[i].length = doomcom->datalength;
-			if (lowtimer)
-			{
-				// Lowtime means can't be sent now so try it as soon as possible
-				ackpak[i].senttime = 0;
-				ackpak[i].resentnum = 1;
-			}
-			else
-			{
-				ackpak[i].senttime = I_GetTime();
-				ackpak[i].resentnum = 0;
-			}
+			ackpak[i].senttime = I_GetTime();
+			ackpak[i].resentnum = 0;
 			M_Memcpy(ackpak[i].pak.raw, netbuffer, ackpak[i].length);
 
 			*freeack = ackpak[i].acknum;
@@ -264,38 +243,6 @@ static boolean GetFreeAcknum(UINT8 *freeack, boolean lowtimer)
 	return false;
 }
 
-/** Counts how many acks are free
-  *
-  * \param urgent True if the type of the packet meant to
-  *               use an ack is lower than PT_CANFAIL
-  *               If for some reason you don't want use it
-  *               for any packet type in particular,
-  *               just set to false
-  * \return The number of free acks
-  *
-  */
-INT32 Net_GetFreeAcks(boolean urgent)
-{
-	INT32 numfreeslot = 0;
-	INT32 n = 0; // Number of free acks found
-
-	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
-			if (!urgent)
-			{
-				numfreeslot++;
-				if (numfreeslot <= URGENTFREESLOTNUM)
-					continue;
-			}
-
-			n++;
-		}
-
-	return n;
-}
-
 // Get a ack to send in the queue of this node
 static UINT8 GetAcktosend(INT32 node)
 {
@@ -313,9 +260,9 @@ static void RemoveAck(INT32 i)
 }
 
 // We have got a packet, proceed the ack request and ack return
-static int Processackpak(void)
+static boolean Processackpak(void)
 {
-	int goodpacket = 0;
+	boolean goodpacket = true;
 	node_t *node = &nodes[doomcom->remotenode];
 
 	// Received an ack return, so remove the ack in the list
@@ -324,7 +271,7 @@ static int Processackpak(void)
 		node->remotefirstack = netbuffer->ackreturn;
 		// Search the ackbuffer and free it
 		for (INT32 i = 0; i < MAXACKPACKETS; i++)
-			if (ackpak[i].acknum && ackpak[i].destinationnode == node - nodes
+			if (ackpak[i].acknum && ackpak[i].destinationnode == doomcom->remotenode
 				&& cmpack(ackpak[i].acknum, netbuffer->ackreturn) <= 0)
 			{
 				RemoveAck(i);
@@ -340,119 +287,32 @@ static int Processackpak(void)
 		{
 			DEBFILE(va("Discard(1) ack %d (duplicated)\n", ack));
 			duppacket++;
-			goodpacket = 1; // Discard packet (duplicate)
+			goodpacket = false; // Discard packet (duplicate)
 		}
 		else
 		{
-			// Check if it is not already in the queue
-			for (INT32 i = node->acktosend_tail; i != node->acktosend_head; i = (i+1) % MAXACKTOSEND)
-				if (node->acktosend[i] == ack)
-				{
-					DEBFILE(va("Discard(2) ack %d (duplicated)\n", ack));
-					duppacket++;
-					goodpacket = 1; // Discard packet (duplicate)
-					break;
-				}
-			if (goodpacket == 0)
+			// Is a good packet so increment the acknowledge number,
+			// Then search for a "hole" in the queue
+			UINT8 nextfirstack = (UINT8)(node->firstacktosend + 1);
+			if (!nextfirstack)
+				nextfirstack = 1;
+
+			if (ack == nextfirstack)
+			{
+				node->firstacktosend = nextfirstack;
+			}
+			else // Out of order packet
 			{
-				// Is a good packet so increment the acknowledge number,
-				// Then search for a "hole" in the queue
-				UINT8 nextfirstack = (UINT8)(node->firstacktosend + 1);
-				if (!nextfirstack)
-					nextfirstack = 1;
-
-				if (ack == nextfirstack)
-				{
-					UINT8 hm1; // head - 1
-					boolean change = true;
-
-					node->firstacktosend = nextfirstack++;
-					if (!nextfirstack)
-						nextfirstack = 1;
-					hm1 = (UINT8)((node->acktosend_head-1+MAXACKTOSEND) % MAXACKTOSEND);
-					while (change)
-					{
-						change = false;
-						for (INT32 i = node->acktosend_tail; i != node->acktosend_head;
-							i = (i+1) % MAXACKTOSEND)
-						{
-							if (cmpack(node->acktosend[i], nextfirstack) <= 0)
-							{
-								if (node->acktosend[i] == nextfirstack)
-								{
-									node->firstacktosend = nextfirstack++;
-									if (!nextfirstack)
-										nextfirstack = 1;
-									change = true;
-								}
-								if (i == node->acktosend_tail)
-								{
-									node->acktosend[node->acktosend_tail] = 0;
-									node->acktosend_tail = (UINT8)((i+1) % MAXACKTOSEND);
-								}
-								else if (i == hm1)
-								{
-									node->acktosend[hm1] = 0;
-									node->acktosend_head = hm1;
-									hm1 = (UINT8)((hm1-1+MAXACKTOSEND) % MAXACKTOSEND);
-								}
-							}
-						}
-					}
-				}
-				else // Out of order packet
-				{
-					// Don't increment firsacktosend, put it in asktosend queue
-					// Will be incremented when the nextfirstack comes (code above)
-					UINT8 newhead = (UINT8)((node->acktosend_head+1) % MAXACKTOSEND);
-					DEBFILE(va("out of order packet (%d expected)\n", nextfirstack));
-					if (newhead != node->acktosend_tail)
-					{
-						node->acktosend[node->acktosend_head] = ack;
-						node->acktosend_head = newhead;
-					}
-					else // Buffer full discard packet, sender will resend it
-					{ // We can admit the packet but we will not detect the duplication after :(
-						DEBFILE("no more freeackret\n");
-						goodpacket = 2;
-					}
-				}
+				// Don't increment firsacktosend, put it in asktosend queue
+				// Will be incremented when the nextfirstack comes (code above)
+				DEBFILE(va("out of order packet (%d expected)\n", nextfirstack));
+				goodpacket = false;
 			}
 		}
 	}
-	// return values: 0 = ok, 1 = duplicate, 2 = out of order
 	return goodpacket;
 }
 
-// send special packet with only ack on it
-void Net_SendAcks(INT32 node)
-{
-	netbuffer->packettype = PT_NOTHING;
-	M_Memcpy(netbuffer->u.textcmd, nodes[node].acktosend, MAXACKTOSEND);
-	HSendPacket(node, false, 0, MAXACKTOSEND);
-}
-
-static void GotAcks(void)
-{
-	for (INT32 j = 0; j < MAXACKTOSEND; j++)
-		if (netbuffer->u.textcmd[j])
-			for (INT32 i = 0; i < MAXACKPACKETS; i++)
-				if (ackpak[i].acknum && ackpak[i].destinationnode == doomcom->remotenode)
-				{
-					if (ackpak[i].acknum == netbuffer->u.textcmd[j])
-						RemoveAck(i);
-					// nextacknum is first equal to acknum, then when receiving bigger ack
-					// there is big chance the packet is lost
-					// When resent, nextacknum = nodes[node].nextacknum
-					// will redo the same but with different value
-					else if (cmpack(ackpak[i].nextacknum, netbuffer->u.textcmd[j]) <= 0
-							&& ackpak[i].senttime > 0)
-						{
-							ackpak[i].senttime--; // hurry up
-						}
-				}
-}
-
 void Net_ConnectionTimeout(INT32 node)
 {
 	// Don't timeout several times
@@ -506,11 +366,6 @@ void Net_AckTicker(void)
 		// This is something like node open flag
 		if (nodes[i].firstacktosend)
 		{
-			// We haven't sent a packet for a long time
-			// Acknowledge packet if needed
-			if (nodes[i].lasttimeacktosend_sent + ACKTOSENDTIMEOUT < I_GetTime())
-				Net_SendAcks(i);
-
 			if (!(nodes[i].flags & NF_CLOSE)
 				&& nodes[i].lasttimepacketreceived + connectiontimeout < I_GetTime())
 			{
@@ -524,37 +379,12 @@ void Net_AckTicker(void)
 // (the higher layer doesn't have room, or something else ....)
 void Net_UnAcknowledgePacket(INT32 node)
 {
-	INT32 hm1 = (nodes[node].acktosend_head-1+MAXACKTOSEND) % MAXACKTOSEND;
 	DEBFILE(va("UnAcknowledge node %d\n", node));
 	if (!node)
 		return;
-	if (nodes[node].acktosend[hm1] == netbuffer->ack)
-	{
-		nodes[node].acktosend[hm1] = 0;
-		nodes[node].acktosend_head = (UINT8)hm1;
-	}
-	else if (nodes[node].firstacktosend == netbuffer->ack)
-	{
-		nodes[node].firstacktosend--;
-		if (!nodes[node].firstacktosend)
-			nodes[node].firstacktosend = UINT8_MAX;
-	}
-	else
-	{
-		while (nodes[node].firstacktosend != netbuffer->ack)
-		{
-			nodes[node].acktosend_tail = (UINT8)
-				((nodes[node].acktosend_tail-1+MAXACKTOSEND) % MAXACKTOSEND);
-			nodes[node].acktosend[nodes[node].acktosend_tail] = nodes[node].firstacktosend;
-
-			nodes[node].firstacktosend--;
-			if (!nodes[node].firstacktosend)
-				nodes[node].firstacktosend = UINT8_MAX;
-		}
-		nodes[node].firstacktosend++;
-		if (!nodes[node].firstacktosend)
-			nodes[node].firstacktosend = 1;
-	}
+	nodes[node].firstacktosend--;
+	if (!nodes[node].firstacktosend)
+		nodes[node].firstacktosend = UINT8_MAX;
 }
 
 /** Checks if all acks have been received
@@ -597,7 +427,6 @@ void Net_WaitAllAckReceived(UINT32 timeout)
 
 static void InitNode(node_t *node)
 {
-	node->acktosend_head = node->acktosend_tail = 0;
 	node->firstacktosend = 0;
 	node->nextacknum = 1;
 	node->remotefirstack = 0;
@@ -656,11 +485,11 @@ void Net_CloseConnection(INT32 node)
 
 	nodes[node].flags |= NF_CLOSE;
 
-	// try to Send ack back (two army problem)
-	if (GetAcktosend(node))
+	if (nodes[node].firstacktosend)
 	{
-		Net_SendAcks(node);
-		Net_SendAcks(node);
+		// send a PT_NOTHING back to acknowledge the packet
+		netbuffer->packettype = PT_NOTHING;
+		HSendPacket(node, false, 0, 0);
 	}
 
 	// check if we are waiting for an ack from this node
@@ -996,15 +825,7 @@ boolean HSendPacket(INT32 node, boolean reliable, UINT8 acknum, size_t packetlen
 		netbuffer->ackreturn = 0;
 	if (reliable)
 	{
-		if (I_NetCanSend && !I_NetCanSend())
-		{
-			if (netbuffer->packettype < PT_CANFAIL)
-				GetFreeAcknum(&netbuffer->ack, true);
-
-			DEBFILE("HSendPacket: Out of bandwidth\n");
-			return false;
-		}
-		else if (!GetFreeAcknum(&netbuffer->ack, false))
+		if (!GetFreeAcknum(&netbuffer->ack))
 			return false;
 	}
 	else
@@ -1070,7 +891,6 @@ boolean HGetPacket(void)
 	while(true)
 	{
 		//nodejustjoined = I_NetGet();
-		int goodpacket;
 		I_NetGet();
 
 		if (doomcom->remotenode == -1) // No packet received
@@ -1116,22 +936,12 @@ boolean HGetPacket(void)
 		}*/
 
 		// Proceed the ack and ackreturn field
-		goodpacket = Processackpak();
-		if (goodpacket != 0)
-		{
-			// resend the ACK in case the previous ACK didn't reach the client.
-			// prevents the client's netbuffer from locking up.
-			if (goodpacket == 1)
-				Net_SendAcks(doomcom->remotenode);
+		if (!Processackpak())
 			continue; // discarded (duplicated)
-		}
 
 		// A packet with just ackreturn
 		if (netbuffer->packettype == PT_NOTHING)
-		{
-			GotAcks();
 			continue;
-		}
 		break;
 	}
 
@@ -1156,7 +966,7 @@ static void Internal_FreeNodenum(INT32 nodenum)
 
 char *I_NetSplitAddress(char *host, char **port)
 {
-	boolean v4 = (strchr(host, '.') != NULL);
+	boolean v4 = (host[0] != '[');
 
 	host = strtok(host, v4 ? ":" : "[]");
 
@@ -1189,11 +999,8 @@ void D_SetDoomcom(void)
 {
 	if (doomcom) return;
 	doomcom = Z_Calloc(sizeof (doomcom_t), PU_STATIC, NULL);
-	doomcom->id = DOOMCOM_ID;
-	doomcom->numslots = doomcom->numnodes = 1;
-	doomcom->gametype = 0;
-	doomcom->consoleplayer = 0;
-	doomcom->extratics = 0;
+	numslots = numnetnodes = 1;
+	extratics = 0;
 }
 
 //
@@ -1211,13 +1018,11 @@ boolean D_CheckNetGame(void)
 
 	I_NetGet = Internal_Get;
 	I_NetSend = Internal_Send;
-	I_NetCanSend = NULL;
 	I_NetCloseSocket = NULL;
 	I_NetFreeNodenum = Internal_FreeNodenum;
 	I_NetMakeNodewPort = NULL;
 
 	hardware_MAXPACKETLENGTH = MAXPACKETLENGTH;
-	net_bandwidth = 30000;
 	// I_InitNetwork sets doomcom and netgame
 	// check and initialize the network driver
 	multiplayer = false;
@@ -1237,30 +1042,14 @@ boolean D_CheckNetGame(void)
 	server = true; // WTF? server always true???
 		// no! The deault mode is server. Client is set elsewhere
 		// when the client executes connect command.
-	doomcom->ticdup = 1;
 
 	if (M_CheckParm("-extratic"))
 	{
 		if (M_IsNextParm())
-			doomcom->extratics = (INT16)atoi(M_GetNextParm());
-		else
-			doomcom->extratics = 1;
-		CONS_Printf(M_GetText("Set extratics to %d\n"), doomcom->extratics);
-	}
-
-	if (M_CheckParm("-bandwidth"))
-	{
-		if (M_IsNextParm())
-		{
-			net_bandwidth = atoi(M_GetNextParm());
-			if (net_bandwidth < 1000)
-				net_bandwidth = 1000;
-			if (net_bandwidth > 100000)
-				hardware_MAXPACKETLENGTH = MAXPACKETLENGTH;
-			CONS_Printf(M_GetText("Network bandwidth set to %d\n"), net_bandwidth);
-		}
+			extratics = (INT16)atoi(M_GetNextParm());
 		else
-			I_Error("usage: -bandwidth <byte_per_sec>");
+			extratics = 1;
+		CONS_Printf(M_GetText("Set extratics to %d\n"), extratics);
 	}
 
 	software_MAXPACKETLENGTH = hardware_MAXPACKETLENGTH;
@@ -1282,10 +1071,8 @@ boolean D_CheckNetGame(void)
 	if (netgame)
 		multiplayer = true;
 
-	if (doomcom->id != DOOMCOM_ID)
-		I_Error("Doomcom buffer invalid!");
-	if (doomcom->numnodes > MAXNETNODES)
-		I_Error("Too many nodes (%d), max:%d", doomcom->numnodes, MAXNETNODES);
+	if (numnetnodes > MAXNETNODES)
+		I_Error("Too many nodes (%d), max:%d", numnetnodes, MAXNETNODES);
 
 	netbuffer = (doomdata_t *)(void *)&doomcom->data;
 
@@ -1293,7 +1080,7 @@ boolean D_CheckNetGame(void)
 	if (M_CheckParm("-debugfile"))
 	{
 		char filename[21];
-		INT32 k = doomcom->consoleplayer - 1;
+		INT32 k = consoleplayer - 1;
 		if (M_IsNextParm())
 			k = atoi(M_GetNextParm()) - 1;
 		while (!debugfile && k < MAXPLAYERS)
@@ -1400,7 +1187,6 @@ void D_CloseConnection(void)
 
 		I_NetGet = Internal_Get;
 		I_NetSend = Internal_Send;
-		I_NetCanSend = NULL;
 		I_NetCloseSocket = NULL;
 		I_NetFreeNodenum = Internal_FreeNodenum;
 		I_NetMakeNodewPort = NULL;
diff --git a/src/netcode/d_net.h b/src/netcode/d_net.h
index 2f83939f8c5324883eb11e90dbb4e2897a54254c..6b3ee5e6751bd6d19457ea704274b5de9247c483 100644
--- a/src/netcode/d_net.h
+++ b/src/netcode/d_net.h
@@ -60,7 +60,6 @@ extern netnode_t netnodes[MAXNETNODES];
 
 extern boolean serverrunning;
 
-INT32 Net_GetFreeAcks(boolean urgent);
 void Net_AckTicker(void);
 
 // If reliable return true if packet sent, 0 else
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index e073a863c90bbe88025fd6f7ed9f4d0decac23d0..35e28db364fd84c39e7857113f20ec039060b0bb 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -320,7 +320,7 @@ consvar_t cv_overtime = CVAR_INIT ("overtime", "Yes", CV_SAVE|CV_NETVAR|CV_ALLOW
 consvar_t cv_rollingdemos = CVAR_INIT ("rollingdemos", "On", CV_SAVE, CV_OnOff, NULL);
 
 static CV_PossibleValue_t timetic_cons_t[] = {{0, "Classic"}, {1, "Centiseconds"}, {2, "Mania"}, {3, "Tics"}, {0, NULL}};
-consvar_t cv_timetic = CVAR_INIT ("timerres", "Classic", CV_SAVE, timetic_cons_t, NULL);
+consvar_t cv_timetic = CVAR_INIT ("timerres", "Mania", CV_SAVE, timetic_cons_t, NULL);
 
 static CV_PossibleValue_t powerupdisplay_cons_t[] = {{0, "Never"}, {1, "First-person only"}, {2, "Always"}, {0, NULL}};
 consvar_t cv_powerupdisplay = CVAR_INIT ("powerupdisplay", "First-person only", CV_SAVE, powerupdisplay_cons_t, NULL);
@@ -372,10 +372,10 @@ static CV_PossibleValue_t cooplives_cons_t[] = {{0, "Infinite"}, {1, "Per-player
 consvar_t cv_cooplives = CVAR_INIT ("cooplives", "Avoid Game Over", CV_SAVE|CV_NETVAR|CV_CALL|CV_CHEAT|CV_ALLOWLUA, cooplives_cons_t, CoopLives_OnChange);
 
 static CV_PossibleValue_t advancemap_cons_t[] = {{0, "Off"}, {1, "Next"}, {2, "Random"}, {0, NULL}};
-consvar_t cv_advancemap = CVAR_INIT ("advancemap", "Next", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, advancemap_cons_t, NULL);
+consvar_t cv_advancemap = CVAR_INIT ("advancemap", "Random", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, advancemap_cons_t, NULL);
 
 static CV_PossibleValue_t playersforexit_cons_t[] = {{0, "One"}, {1, "1/4"}, {2, "Half"}, {3, "3/4"}, {4, "All"}, {0, NULL}};
-consvar_t cv_playersforexit = CVAR_INIT ("playersforexit", "All", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, playersforexit_cons_t, NULL);
+consvar_t cv_playersforexit = CVAR_INIT ("playersforexit", "3/4", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, playersforexit_cons_t, NULL);
 
 consvar_t cv_exitmove = CVAR_INIT ("exitmove", "On", CV_SAVE|CV_NETVAR|CV_CALL|CV_ALLOWLUA, CV_OnOff, ExitMove_OnChange);
 
@@ -391,7 +391,7 @@ static CV_PossibleValue_t perfstats_cons_t[] = {
 consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", CV_CALL, perfstats_cons_t, PS_PerfStats_OnChange);
 static CV_PossibleValue_t ps_samplesize_cons_t[] = {
 	{1, "MIN"}, {1000, "MAX"}, {0, NULL}};
-consvar_t cv_ps_samplesize = CVAR_INIT ("ps_samplesize", "1", CV_CALL, ps_samplesize_cons_t, PS_SampleSize_OnChange);
+consvar_t cv_ps_samplesize = CVAR_INIT ("ps_samplesize", "175", CV_CALL, ps_samplesize_cons_t, PS_SampleSize_OnChange);
 static CV_PossibleValue_t ps_descriptor_cons_t[] = {
 	{1, "Average"}, {2, "SD"}, {3, "Minimum"}, {4, "Maximum"}, {0, NULL}};
 consvar_t cv_ps_descriptor = CVAR_INIT ("ps_descriptor", "Average", 0, ps_descriptor_cons_t, NULL);
@@ -620,6 +620,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_blamecfail);
 	CV_RegisterVar(&cv_dedicatedidletime);
 	CV_RegisterVar(&cv_idletime);
+	CV_RegisterVar(&cv_idleaction);
 	CV_RegisterVar(&cv_httpsource);
 
 	COM_AddCommand("ping", Command_Ping_f, COM_LUA);
@@ -691,6 +692,7 @@ void D_RegisterClientCommands(void)
 	COM_AddCommand("timedemo", Command_Timedemo_f, 0);
 	COM_AddCommand("stopdemo", Command_Stopdemo_f, COM_LUA);
 	COM_AddCommand("playintro", Command_Playintro_f, COM_LUA);
+	CV_RegisterVar(&cv_resyncdemo);
 
 	COM_AddCommand("resetcamera", Command_ResetCamera_f, COM_LUA);
 
@@ -822,8 +824,6 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_jumpaxis2);
 	CV_RegisterVar(&cv_spinaxis);
 	CV_RegisterVar(&cv_spinaxis2);
-	CV_RegisterVar(&cv_shieldaxis);
-	CV_RegisterVar(&cv_shieldaxis2);
 	CV_RegisterVar(&cv_fireaxis);
 	CV_RegisterVar(&cv_fireaxis2);
 	CV_RegisterVar(&cv_firenaxis);
@@ -1308,7 +1308,7 @@ static void SendNameAndColor(void)
 
 		SetColorLocal(consoleplayer, cv_playercolor.value);
 
-		if (splitscreen)
+		if (splitscreen || (!pickedchar && stricmp(cv_skin.string, skins[consoleplayer]->name) != 0))
 			SetSkinLocal(consoleplayer, R_SkinAvailable(cv_skin.string));
 		else
 			SetSkinLocal(consoleplayer, pickedchar);
@@ -2509,11 +2509,11 @@ static void MutePlayer(boolean mute)
 
 	if (COM_Argc() < 2)
 	{
-		CONS_Printf(M_GetText("muteplayer <playernum>: mute a player\n"));
+		CONS_Printf(M_GetText("muteplayer <playername/playernum>: mute a player\n"));
 		return;
 	}
 
-	data[0] = atoi(COM_Argv(1));
+	data[0] = nametonum(COM_Argv(1));
 	if (data[0] >= MAXPLAYERS || !playeringame[data[0]])
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("There is no player %u!\n"), (unsigned int)data[0]);
@@ -2602,11 +2602,11 @@ static void Command_ServerTeamChange_f(void)
 	if (COM_Argc() < 3)
 	{
 		if (G_TagGametype())
-			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "it, notit, playing, or spectator");
+			CONS_Printf(M_GetText("serverchangeteam <playername/playernum> <team>: switch player to a new team (%s)\n"), "it, notit, playing, or spectator");
 		else if (G_GametypeHasTeams())
-			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "red, blue or spectator");
+			CONS_Printf(M_GetText("serverchangeteam <playername/playernum> <team>: switch player to a new team (%s)\n"), "red, blue or spectator");
 		else if (G_GametypeHasSpectators())
-			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "spectator or playing");
+			CONS_Printf(M_GetText("serverchangeteam <playername/playernum> <team>: switch player to a new team (%s)\n"), "spectator or playing");
 		else
 			CONS_Alert(CONS_NOTICE, M_GetText("This command cannot be used in this gametype.\n"));
 		return;
@@ -2654,19 +2654,19 @@ static void Command_ServerTeamChange_f(void)
 	if (error)
 	{
 		if (G_TagGametype())
-			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "it, notit, playing, or spectator");
+			CONS_Printf(M_GetText("serverchangeteam <playername/playernum> <team>: switch player to a new team (%s)\n"), "it, notit, playing, or spectator");
 		else if (G_GametypeHasTeams())
-			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "red, blue or spectator");
+			CONS_Printf(M_GetText("serverchangeteam <playername/playernum> <team>: switch player to a new team (%s)\n"), "red, blue or spectator");
 		else if (G_GametypeHasSpectators())
-			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "spectator or playing");
+			CONS_Printf(M_GetText("serverchangeteam <playername/playernum> <team>: switch player to a new team (%s)\n"), "spectator or playing");
 		return;
 	}
 
-	NetPacket.packet.playernum = atoi(COM_Argv(1));
+	NetPacket.packet.playernum = nametonum(COM_Argv(1));
 
-	if (!playeringame[NetPacket.packet.playernum])
+	if (NetPacket.packet.playernum == -1 || !playeringame[NetPacket.packet.playernum])
 	{
-		CONS_Alert(CONS_NOTICE, M_GetText("There is no player %d!\n"), NetPacket.packet.playernum);
+		CONS_Alert(CONS_NOTICE, M_GetText("There is no player %s!\n"), COM_Argv(1));
 		return;
 	}
 
@@ -3105,13 +3105,16 @@ static void Command_Verify_f(void)
 
 	if (COM_Argc() != 2)
 	{
-		CONS_Printf(M_GetText("promote <playernum>: give admin privileges to a player\n"));
+		CONS_Printf(M_GetText("promote <playername/playernum>: give admin privileges to a player\n"));
 		return;
 	}
 
-	strlcpy(buf, COM_Argv(1), sizeof (buf));
-
-	playernum = atoi(buf);
+	playernum = nametonum(COM_Argv(1));
+	if (playernum == -1)
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("There is no player %s!\n"), COM_Argv(1));
+		return;
+	}
 
 	temp = buf;
 
@@ -3155,13 +3158,16 @@ static void Command_RemoveAdmin_f(void)
 
 	if (COM_Argc() != 2)
 	{
-		CONS_Printf(M_GetText("demote <playernum>: remove admin privileges from a player\n"));
+		CONS_Printf(M_GetText("demote <playername/playernum>: remove admin privileges from a player\n"));
 		return;
 	}
 
-	strlcpy(buf, COM_Argv(1), sizeof(buf));
-
-	playernum = atoi(buf);
+	playernum = nametonum(COM_Argv(1));
+	if (playernum == -1)
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("There is no player %s!\n"), COM_Argv(1));
+		return;
+	}
 
 	temp = buf;
 
@@ -4247,9 +4253,9 @@ void D_GameTypeChanged(INT32 lastgametype)
 			case GT_TEAMMATCH:
 				if (!cv_timelimit.changed && !cv_pointlimit.changed) // user hasn't changed limits
 				{
-					// default settings for match: timelimit 10 mins, no pointlimit
+					// default settings for match: timelimit 7 mins, no pointlimit
 					CV_SetValue(&cv_pointlimit, 0);
-					CV_SetValue(&cv_timelimit, 10);
+					CV_SetValue(&cv_timelimit, 7);
 				}
 				if (!cv_itemrespawntime.changed)
 					CV_Set(&cv_itemrespawntime, cv_itemrespawntime.defaultvalue); // respawn normally
@@ -4258,9 +4264,9 @@ void D_GameTypeChanged(INT32 lastgametype)
 			case GT_HIDEANDSEEK:
 				if (!cv_timelimit.changed && !cv_pointlimit.changed) // user hasn't changed limits
 				{
-					// default settings for tag: 5 mins, no pointlimit
+					// default settings for tag: 7 mins, no pointlimit
 					// Note that tag mode also uses an alternate timing mechanism in tandem with timelimit.
-					CV_SetValue(&cv_timelimit, 5);
+					CV_SetValue(&cv_timelimit, 7);
 					CV_SetValue(&cv_pointlimit, 0);
 				}
 				if (!cv_itemrespawntime.changed)
@@ -4269,8 +4275,8 @@ void D_GameTypeChanged(INT32 lastgametype)
 			case GT_CTF:
 				if (!cv_timelimit.changed && !cv_pointlimit.changed) // user hasn't changed limits
 				{
-					// default settings for CTF: no timelimit, pointlimit 5
-					CV_SetValue(&cv_timelimit, 0);
+					// default settings for CTF: 15 mins, pointlimit 5
+					CV_SetValue(&cv_timelimit, 15);
 					CV_SetValue(&cv_pointlimit, 5);
 				}
 				if (!cv_itemrespawntime.changed)
@@ -4608,7 +4614,7 @@ static void Command_ExitLevel_f(void)
 			SendNetXCmd(XD_EXITLEVEL, NULL, 0);
 			return;
 		}
-		
+
 		// Allow exiting without cheating if at least one player beat the level
 		// Consistent with just setting playersforexit to one
 		if (splitscreen || multiplayer)
@@ -4622,7 +4628,7 @@ static void Command_ExitLevel_f(void)
 					continue;
 				if (players[i].lives <= 0)
 					continue;
-		
+
 				if ((players[i].pflags & PF_FINISHED) || players[i].exiting)
 				{
 					SendNetXCmd(XD_EXITLEVEL, NULL, 0);
@@ -4630,7 +4636,7 @@ static void Command_ExitLevel_f(void)
 				}
 			}
 		}
-		
+
 		// Only consider it a cheat if we're not allowed to go to the next map
 		if (M_CampaignWarpIsCheat(gametype, G_GetNextMap(true, true) + 1, serverGamedata))
 			CONS_Alert(CONS_NOTICE, M_GetText("Cheats must be enabled to force exit to a locked level!\n"));
@@ -4769,7 +4775,7 @@ static void Command_Cheats_f(void)
 			G_SetUsedCheats(false);
 		return;
 	}
-	
+
 	if (usedCheats)
 		CONS_Printf(M_GetText("Cheats are enabled, the game cannot be saved.\n"));
 	else
@@ -4791,12 +4797,11 @@ static void Command_Togglemodified_f(void)
 	modifiedgame = !modifiedgame;
 }
 
-extern UINT8 *save_p;
 static void Command_Archivetest_f(void)
 {
-	UINT8 *buf;
 	UINT32 i, wrote;
 	thinker_t *th;
+	save_t savebuffer;
 	if (gamestate != GS_LEVEL)
 	{
 		CONS_Printf("This command only works in-game, you dummy.\n");
@@ -4810,28 +4815,29 @@ static void Command_Archivetest_f(void)
 			((mobj_t *)th)->mobjnum = i++;
 
 	// allocate buffer
-	buf = save_p = ZZ_Alloc(1024);
+	savebuffer.size = 1024;
+	savebuffer.buf = malloc(savebuffer.size);
+	savebuffer.pos = 0;
 
 	// test archive
 	CONS_Printf("LUA_Archive...\n");
-	LUA_Archive();
-	WRITEUINT8(save_p, 0x7F);
-	wrote = (UINT32)(save_p-buf);
+	LUA_Archive(&savebuffer);
+	P_WriteUINT8(&savebuffer, 0x7F);
+	wrote = savebuffer.pos;
 
 	// clear Lua state, so we can really see what happens!
 	CONS_Printf("Clearing state!\n");
 	LUA_ClearExtVars();
 
 	// test unarchive
-	save_p = buf;
 	CONS_Printf("LUA_UnArchive...\n");
-	LUA_UnArchive();
-	i = READUINT8(save_p);
-	if (i != 0x7F || wrote != (UINT32)(save_p-buf))
-		CONS_Printf("Savegame corrupted. (write %u, read %u)\n", wrote, (UINT32)(save_p-buf));
+	LUA_UnArchive(&savebuffer);
+	i = P_ReadUINT8(&savebuffer);
+	if (i != 0x7F || wrote != (UINT32)(savebuffer.pos))
+		CONS_Printf("Savegame corrupted. (write %u, read %u)\n", wrote, (UINT32)(savebuffer.pos));
 
 	// free buffer
-	Z_Free(buf);
+	free(savebuffer.buf);
 	CONS_Printf("Done. No crash.\n");
 }
 #endif
@@ -4902,11 +4908,13 @@ static void Name2_OnChange(void)
 
 static boolean Skin_CanChange(const char *valstr)
 {
-	(void)valstr;
-
 	if (!Playing())
 		return true; // do whatever you want
 
+	// You already are that skin.
+	if (stricmp(skins[players[consoleplayer].skin]->name, valstr) == 0)
+		return false;
+
 	if (!(multiplayer || netgame)) // In single player.
 		return true;
 
@@ -4921,11 +4929,13 @@ static boolean Skin_CanChange(const char *valstr)
 
 static boolean Skin2_CanChange(const char *valstr)
 {
-	(void)valstr;
-
 	if (!Playing() || !splitscreen)
 		return true; // do whatever you want
 
+	// You already are that skin.
+	if (stricmp(skins[players[secondarydisplayplayer].skin]->name, valstr) == 0)
+		return false;
+
 	if (CanChangeSkin(secondarydisplayplayer) && !P_PlayerMoving(secondarydisplayplayer))
 		return true;
 	else
@@ -4941,6 +4951,8 @@ static boolean Skin2_CanChange(const char *valstr)
   */
 static void Skin_OnChange(void)
 {
+	pickedchar = R_SkinAvailable(cv_skin.string);
+
 	if (!Playing())
 		return;
 
diff --git a/src/netcode/d_netcmd.h b/src/netcode/d_netcmd.h
index f92878a4e2e87efdae9c441ca30d61bcdb511ee5..c11575575fc025bcce4ad755651e3b77493233db 100644
--- a/src/netcode/d_netcmd.h
+++ b/src/netcode/d_netcmd.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/netcode/d_netfil.c b/src/netcode/d_netfil.c
index a8a10d475d4afcc9c965573c1deaf59969c00b0f..b1d46c96fe56abab161e9ba2e7530d1822ec8067 100644
--- a/src/netcode/d_netfil.c
+++ b/src/netcode/d_netfil.c
@@ -95,6 +95,7 @@ static filetran_t transfer[MAXNETNODES];
 INT32 fileneedednum; // Number of files needed to join the server
 fileneeded_t *fileneeded; // List of needed files
 static tic_t lasttimeackpacketsent = 0;
+static I_mutex downloadmutex;
 char downloaddir[512] = "DOWNLOAD";
 
 // For resuming failed downloads
@@ -128,7 +129,7 @@ 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}};
+static CV_PossibleValue_t maxsend_cons_t[] = {{-1, "MIN"}, {999999999, "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);
@@ -205,10 +206,10 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
 			// Store in the upper four bits
 			if (!cv_downloading.value)
 				filestatus += (WILLSEND_NO << 4); // Won't send
-			else if (wadfiles[i]->filesize <= (UINT32)cv_maxsend.value * 1024)
+			else if (cv_maxsend.value == -1 || wadfiles[i]->filesize <= (UINT32)cv_maxsend.value * 1024)
 				filestatus += (WILLSEND_YES << 4); // Will send if requested
-			// else
-				// filestatus += (0 << 4); -- Won't send, too big
+			else
+				filestatus += (WILLSEND_TOOLARGE << 4); // Won't send, too big
 		}
 
 		WRITEUINT8(p, filestatus);
@@ -606,7 +607,7 @@ void AddLuaFileTransfer(const char *filename, const char *mode)
 		prevnext = &((*prevnext)->next);
 
 	// Allocate file transfer information and append it to the transfer list
-	filetransfer = malloc(sizeof(luafiletransfer_t));
+	filetransfer = calloc(1, sizeof(luafiletransfer_t));
 	if (!filetransfer)
 		I_Error("AddLuaFileTransfer: Out of memory\n");
 	*prevnext = filetransfer;
@@ -848,7 +849,7 @@ static boolean AddFileToSendQueue(INT32 node, UINT8 fileid)
 	strlcpy(p->id.filename, wadfiles[wadnum]->filename, MAX_WADPATH);
 
 	// Handle huge file requests (i.e. bigger than cv_maxsend.value KB)
-	if (wadfiles[wadnum]->filesize > (UINT32)cv_maxsend.value * 1024)
+	if (cv_maxsend.value != -1 && wadfiles[wadnum]->filesize > (UINT32)cv_maxsend.value * 1024)
 	{
 		// Too big
 		// Don't inform client (client sucks, man)
@@ -1033,7 +1034,6 @@ void FileSendTicker(void)
 
 	netbuffer->packettype = PT_FILEFRAGMENT;
 
-	// (((sendbytes-nowsentbyte)*TICRATE)/(I_GetTime()-starttime)<(UINT32)net_bandwidth)
 	while (packetsent-- && filestosend != 0)
 	{
 		for (i = currentnode, j = 0; j < MAXNETNODES;
@@ -1345,9 +1345,9 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 
 	if (!(strcmp(filename, "srb2.pk3")
 		&& strcmp(filename, "zones.pk3")
-		&& strcmp(filename, "player.dta")
+		&& strcmp(filename, "characters.pk3")
 		&& strcmp(filename, "patch.pk3")
-		&& strcmp(filename, "music.dta")
+		&& strcmp(filename, "music.pk3")
 		))
 		I_Error("Tried to download \"%s\"", filename);
 
@@ -1610,11 +1610,13 @@ boolean CURLPrepareFile(const char* url, int dfilenum)
 		I_Error("Attempted to download files in -nodownload mode");
 #endif
 
-	curl_global_init(CURL_GLOBAL_ALL);
+	if (!multi_handle)
+	{
+		curl_global_init(CURL_GLOBAL_ALL);
+		multi_handle = curl_multi_init();
+	}
 
 	http_handle = curl_easy_init();
-	multi_handle = curl_multi_init();
-
 	if (http_handle && multi_handle)
 	{
 		I_mkdir(downloaddir, 0755);
@@ -1673,6 +1675,8 @@ boolean CURLPrepareFile(const char* url, int dfilenum)
 		filedownload.current = dfilenum;
 		filedownload.http_running = true;
 
+		I_spawn_thread("http-download", (I_thread_fn)CURLGetFile, NULL);
+
 		return true;
 	}
 
@@ -1681,103 +1685,119 @@ boolean CURLPrepareFile(const char* url, int dfilenum)
 	return false;
 }
 
+void CURLAbortFile(void)
+{
+	filedownload.http_running = false;
+
+	// lock and unlock to wait for the download thread to exit
+	I_lock_mutex(&downloadmutex);
+	I_unlock_mutex(downloadmutex);
+}
+
 void CURLGetFile(void)
 {
+	I_lock_mutex(&downloadmutex);
 	CURLMcode mc; /* return code used by curl_multi_wait() */
 	CURLcode easyres; /* Return from easy interface */
-	int numfds;
 	CURLMsg *m; /* for picking up messages with the transfer status */
 	CURL *e;
 	int msgs_left; /* how many messages are left */
 	const char *easy_handle_error;
+	boolean running = true;
 
-	if (curl_runninghandles)
+	while (running && filedownload.http_running)
 	{
-		curl_multi_perform(multi_handle, &curl_runninghandles);
+		if (curl_runninghandles)
+		{
+			curl_multi_perform(multi_handle, &curl_runninghandles);
 
-		/* wait for activity, timeout or "nothing" */
-		mc = curl_multi_wait(multi_handle, NULL, 0, 1000, &numfds);
+			/* wait for activity, timeout or "nothing" */
+			mc = curl_multi_wait(multi_handle, NULL, 0, 1000, NULL);
 
-		if (mc != CURLM_OK)
-		{
-			CONS_Alert(CONS_WARNING, "curl_multi_wait() failed, code %d.\n", mc);
-			return;
+			if (mc != CURLM_OK)
+			{
+				CONS_Alert(CONS_WARNING, "curl_multi_wait() failed, code %d.\n", mc);
+				continue;
+			}
+			curl_curfile->currentsize = curl_dlnow;
+			curl_curfile->totalsize = curl_dltotal;
 		}
-		curl_curfile->currentsize = curl_dlnow;
-		curl_curfile->totalsize = curl_dltotal;
-	}
 
-	/* See how the transfers went */
-	while ((m = curl_multi_info_read(multi_handle, &msgs_left)))
-	{
-		if (m && (m->msg == CURLMSG_DONE))
+		/* See how the transfers went */
+		while ((m = curl_multi_info_read(multi_handle, &msgs_left)))
 		{
-			e = m->easy_handle;
-			easyres = m->data.result;
-
-			char *filename = Z_StrDup(curl_realname);
-			nameonly(filename);
-
-			if (easyres != CURLE_OK)
+			if (m && (m->msg == CURLMSG_DONE))
 			{
-				long response_code = 0;
+				running = false;
+				e = m->easy_handle;
+				easyres = m->data.result;
 
-				if (easyres == CURLE_HTTP_RETURNED_ERROR)
-					curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &response_code);
+				char *filename = Z_StrDup(curl_realname);
+				nameonly(filename);
 
-				if (response_code == 404)
-					curl_curfile->failed = FDOWNLOAD_FAIL_NOTFOUND;
-				else
-					curl_curfile->failed = FDOWNLOAD_FAIL_OTHER;
-
-				easy_handle_error = (response_code) ? va("HTTP response code %ld", response_code) : curl_easy_strerror(easyres);
-				curl_curfile->status = FS_FALLBACK;
-				curl_curfile->currentsize = curl_origfilesize;
-				curl_curfile->totalsize = curl_origtotalfilesize;
-				filedownload.http_failed = true;
-				fclose(curl_curfile->file);
-				remove(curl_curfile->filename);
-				CONS_Alert(CONS_ERROR, M_GetText("Failed to download addon \"%s\" (%s)\n"), filename, easy_handle_error);
-			}
-			else
-			{
-				fclose(curl_curfile->file);
+				if (easyres != CURLE_OK)
+				{
+					long response_code = 0;
 
-				CONS_Printf(M_GetText("Finished download of \"%s\"\n"), filename);
+					if (easyres == CURLE_HTTP_RETURNED_ERROR)
+						curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &response_code);
 
-				if (checkfilemd5(curl_curfile->filename, curl_curfile->md5sum) == FS_MD5SUMBAD)
-				{
-					CONS_Alert(CONS_WARNING, M_GetText("File \"%s\" does not match the version used by the server\n"), filename);
+					if (response_code == 404)
+						curl_curfile->failed = FDOWNLOAD_FAIL_NOTFOUND;
+					else
+						curl_curfile->failed = FDOWNLOAD_FAIL_OTHER;
+
+					easy_handle_error = (response_code) ? va("HTTP response code %ld", response_code) : curl_easy_strerror(easyres);
 					curl_curfile->status = FS_FALLBACK;
-					curl_curfile->failed = FDOWNLOAD_FAIL_MD5SUMBAD;
+					curl_curfile->currentsize = curl_origfilesize;
+					curl_curfile->totalsize = curl_origtotalfilesize;
 					filedownload.http_failed = true;
+					fclose(curl_curfile->file);
+					remove(curl_curfile->filename);
+					CONS_Alert(CONS_ERROR, M_GetText("Failed to download addon \"%s\" (%s)\n"), filename, easy_handle_error);
 				}
 				else
 				{
-					filedownload.completednum++;
-					filedownload.completedsize += curl_curfile->totalsize;
-					curl_curfile->status = FS_FOUND;
+					fclose(curl_curfile->file);
+
+					CONS_Printf(M_GetText("Finished download of \"%s\"\n"), filename);
+
+					if (checkfilemd5(curl_curfile->filename, curl_curfile->md5sum) == FS_MD5SUMBAD)
+					{
+						CONS_Alert(CONS_WARNING, M_GetText("File \"%s\" does not match the version used by the server\n"), filename);
+						curl_curfile->status = FS_FALLBACK;
+						curl_curfile->failed = FDOWNLOAD_FAIL_MD5SUMBAD;
+						filedownload.http_failed = true;
+					}
+					else
+					{
+						filedownload.completednum++;
+						filedownload.completedsize += curl_curfile->totalsize;
+						curl_curfile->status = FS_FOUND;
+					}
 				}
-			}
 
-			Z_Free(filename);
+				Z_Free(filename);
 
-			curl_curfile->file = NULL;
-			filedownload.http_running = false;
-			filedownload.remaining--;
-			curl_multi_remove_handle(multi_handle, e);
-			curl_easy_cleanup(e);
+				curl_curfile->file = NULL;
+				filedownload.remaining--;
+				curl_multi_remove_handle(multi_handle, e);
+				curl_easy_cleanup(e);
 
-			if (!filedownload.remaining)
-				break;
+				if (!filedownload.remaining)
+					break;
+			}
 		}
 	}
 
-	if (!filedownload.remaining)
+	if (!filedownload.remaining || !filedownload.http_running)
 	{
 		curl_multi_cleanup(multi_handle);
 		curl_global_cleanup();
+		multi_handle = NULL;
 	}
+	filedownload.http_running = false;
+	I_unlock_mutex(downloadmutex);
 }
 
 HTTP_login *
diff --git a/src/netcode/d_netfil.h b/src/netcode/d_netfil.h
index 4039b5e2d5cc4b0d81fbdc04e6d7c7bef46f724a..98456fe13ee003ea71ff672c40374f031d42639c 100644
--- a/src/netcode/d_netfil.h
+++ b/src/netcode/d_netfil.h
@@ -95,6 +95,7 @@ typedef struct
 	INT32 remaining;
 	INT32 completednum;
 	UINT32 completedsize;
+	UINT64 totalsize;
 
 	boolean http_failed;
 	boolean http_running;
@@ -140,6 +141,7 @@ boolean CL_SendFileRequest(void);
 void PT_RequestFile(SINT8 node);
 
 boolean CURLPrepareFile(const char* url, int dfilenum);
+void CURLAbortFile(void);
 void CURLGetFile(void);
 HTTP_login * CURLGetLogin (const char *url, HTTP_login ***return_prev_next);
 
diff --git a/src/netcode/gamestate.c b/src/netcode/gamestate.c
index f36347c6d88e94a444fb186937d2672cfe3a0df9..967e23650e141fd231a9a740588cc822b6b00c0b 100644
--- a/src/netcode/gamestate.c
+++ b/src/netcode/gamestate.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -54,30 +54,25 @@ boolean SV_ResendingSavegameToAnyone(void)
 void SV_SendSaveGame(INT32 node, boolean resending)
 {
 	size_t length, compressedlen;
-	UINT8 *savebuffer;
+	save_t savebuffer;
 	UINT8 *compressedsave;
 	UINT8 *buffertosend;
 
 	// first save it in a malloced buffer
-	savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
-	if (!savebuffer)
+	savebuffer.size = SAVEGAMESIZE;
+	savebuffer.buf = (UINT8 *)malloc(savebuffer.size);
+	if (!savebuffer.buf)
 	{
 		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);
+	savebuffer.pos = sizeof(UINT32);
 
-	P_SaveNetGame(resending);
+	P_SaveNetGame(&savebuffer, resending);
 
-	length = save_p - savebuffer;
-	if (length > SAVEGAMESIZE)
-	{
-		free(savebuffer);
-		save_p = NULL;
-		I_Error("Savegame buffer overrun");
-	}
+	length = savebuffer.pos;
 
 	// Allocate space for compressed save: one byte fewer than for the
 	// uncompressed data to ensure that the compression is worthwhile.
@@ -85,15 +80,16 @@ void SV_SendSaveGame(INT32 node, boolean resending)
 	if (!compressedsave)
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
+		free(savebuffer.buf);
 		return;
 	}
 
 	// Attempt to compress it.
-	if((compressedlen = lzf_compress(savebuffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1)))
+	if((compressedlen = lzf_compress(savebuffer.buf + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1)))
 	{
 		// Compressing succeeded; send compressed data
 
-		free(savebuffer);
+		free(savebuffer.buf);
 
 		// State that we're compressed.
 		buffertosend = compressedsave;
@@ -107,12 +103,12 @@ void SV_SendSaveGame(INT32 node, boolean resending)
 		free(compressedsave);
 
 		// State that we're not compressed
-		buffertosend = savebuffer;
-		WRITEUINT32(savebuffer, 0);
+		buffertosend = savebuffer.buf;
+		savebuffer.pos = 0;
+		P_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;
@@ -125,8 +121,7 @@ static consvar_t cv_dumpconsistency = CVAR_INIT ("dumpconsistency", "Off", CV_SA
 
 void SV_SavedGame(void)
 {
-	size_t length;
-	UINT8 *savebuffer;
+	save_t savebuffer;
 	char tmpsave[256];
 
 	if (!cv_dumpconsistency.value)
@@ -135,29 +130,22 @@ void SV_SavedGame(void)
 	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
 
 	// first save it in a malloced buffer
-	save_p = savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
-	if (!save_p)
+	savebuffer.size = SAVEGAMESIZE;
+	savebuffer.buf = (UINT8 *)malloc(savebuffer.size);
+	if (!savebuffer.buf)
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
 		return;
 	}
+	savebuffer.pos = 0;
 
-	P_SaveNetGame(false);
-
-	length = save_p - savebuffer;
-	if (length > SAVEGAMESIZE)
-	{
-		free(savebuffer);
-		save_p = NULL;
-		I_Error("Savegame buffer overrun");
-	}
+	P_SaveNetGame(&savebuffer, false);
 
 	// then save it!
-	if (!FIL_WriteFile(tmpsave, savebuffer, length))
+	if (!FIL_WriteFile(tmpsave, savebuffer.buf, savebuffer.pos))
 		CONS_Printf(M_GetText("Didn't save %s for netgame"), tmpsave);
 
-	free(savebuffer);
-	save_p = NULL;
+	free(savebuffer.pos);
 }
 
 #undef  TMPSAVENAME
@@ -167,33 +155,34 @@ void SV_SavedGame(void)
 
 void CL_LoadReceivedSavegame(boolean reloading)
 {
-	UINT8 *savebuffer = NULL;
-	size_t length, decompressedlen;
+	save_t savebuffer;
+	size_t decompressedlen;
 	char tmpsave[256];
 
 	FreeFileNeeded();
 
 	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
 
-	length = FIL_ReadFile(tmpsave, &savebuffer);
+	savebuffer.size = FIL_ReadFile(tmpsave, &savebuffer.buf);
+	savebuffer.pos = 0;
 
-	CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length));
-	if (!length)
+	CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(savebuffer.size));
+	if (!savebuffer.size)
 	{
 		I_Error("Can't read savegame sent");
 		return;
 	}
 
-	save_p = savebuffer;
-
 	// Decompress saved game if necessary.
-	decompressedlen = READUINT32(save_p);
+	decompressedlen = P_ReadUINT32(&savebuffer);
 	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;
+		lzf_decompress(savebuffer.buf + sizeof(UINT32), savebuffer.size - sizeof(UINT32), decompressedbuffer, decompressedlen);
+		Z_Free(savebuffer.buf);
+		savebuffer.buf = decompressedbuffer;
+		savebuffer.size = decompressedlen;
+		savebuffer.pos = 0;
 	}
 
 	paused = false;
@@ -203,7 +192,7 @@ void CL_LoadReceivedSavegame(boolean reloading)
 	automapactive = false;
 
 	// load a base level
-	if (P_LoadNetGame(reloading))
+	if (P_LoadNetGame(&savebuffer, reloading))
 	{
 		const UINT8 actnum = mapheaderinfo[gamemap-1]->actnum;
 		CONS_Printf(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap));
@@ -219,8 +208,7 @@ void CL_LoadReceivedSavegame(boolean reloading)
 	}
 
 	// done
-	Z_Free(savebuffer);
-	save_p = NULL;
+	Z_Free(savebuffer.buf);
 	if (unlink(tmpsave) == -1)
 		CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
 	consistancy[gametic%BACKUPTICS] = Consistancy();
diff --git a/src/netcode/http-mserv.c b/src/netcode/http-mserv.c
index b8c662a51e215965470310d758331236359378b7..0fe1e9934265c301c78e77c9650ea952c61cc68d 100644
--- a/src/netcode/http-mserv.c
+++ b/src/netcode/http-mserv.c
@@ -63,7 +63,11 @@ consvar_t cv_masterserver_token = CVAR_INIT
 
 static int hms_started;
 
+static boolean hms_args_checked;
+#ifndef NO_IPV6
 static boolean hms_allow_ipv6;
+#endif
+static boolean hms_allow_ipv4;
 
 static char *hms_api;
 #ifdef HAVE_THREADS
@@ -134,6 +138,18 @@ HMS_on_read (char *s, size_t _1, size_t n, void *userdata)
 	return n;
 }
 
+static void HMS_check_args_once(void)
+{
+	if (hms_args_checked)
+		return;
+
+#ifndef NO_IPV6
+	hms_allow_ipv6 = !M_CheckParm("-noipv6");
+#endif
+	hms_allow_ipv4 = !M_CheckParm("-noipv4");
+	hms_args_checked = true;
+}
+
 FUNCDEBUG static struct HMS_buffer *
 HMS_connect (int proto, const char *format, ...)
 {
@@ -152,7 +168,6 @@ HMS_connect (int proto, const char *format, ...)
 
 	if (! hms_started)
 	{
-		hms_allow_ipv6 = !M_CheckParm("-noipv6");
 		if (curl_global_init(CURL_GLOBAL_ALL) != 0)
 		{
 			Contact_error();
@@ -225,20 +240,27 @@ HMS_connect (int proto, const char *format, ...)
 		curl_easy_setopt(curl, CURLOPT_STDERR, logstream);
 	}
 
-	if (M_CheckParm("-bindaddr") && M_IsNextParm())
-	{
-		curl_easy_setopt(curl, CURLOPT_INTERFACE, M_GetNextParm());
-	}
-
 	curl_easy_setopt(curl, CURLOPT_URL, url);
 	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
 
 #ifndef NO_IPV6
-	if (proto == PROTO_V6)
+	if (proto == PROTO_V6 || (proto == PROTO_ANY && !hms_allow_ipv4))
+	{
 		curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
-	if (proto == PROTO_V4)
+		if (M_CheckParm("-bindaddr6") && M_IsNextParm())
+		{
+			curl_easy_setopt(curl, CURLOPT_INTERFACE, M_GetNextParm());
+		}
+	}
+	if (proto == PROTO_V4 || (proto == PROTO_ANY && !hms_allow_ipv6))
 #endif
+	{
 		curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+		if (M_CheckParm("-bindaddr") && M_IsNextParm())
+		{
+			curl_easy_setopt(curl, CURLOPT_INTERFACE, M_GetNextParm());
+		}
+	}
 
 	curl_easy_setopt(curl, CURLOPT_TIMEOUT, cv_masterserver_timeout.value);
 	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HMS_on_read);
@@ -326,6 +348,8 @@ HMS_fetch_rooms (int joining, int query_id)
 
 	(void)query_id;
 
+	HMS_check_args_once();
+
 	hms = HMS_connect(PROTO_ANY, "rooms");
 
 	if (! hms)
@@ -420,18 +444,15 @@ int
 HMS_register (void)
 {
 	struct HMS_buffer *hms;
-	int ok;
+	int ok = 0;
 
 	char post[256];
 
 	char *title;
 
-	hms = HMS_connect(PROTO_V4, "rooms/%d/register", ms_RoomId);
-
-	if (! hms)
-		return 0;
+	HMS_check_args_once();
 
-	title = curl_easy_escape(hms->curl, cv_servername.string, 0);
+	title = curl_easy_escape(NULL, cv_servername.string, 0);
 
 	snprintf(post, sizeof post,
 			"port=%d&"
@@ -447,22 +468,30 @@ HMS_register (void)
 
 	curl_free(title);
 
-	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
+	if (hms_allow_ipv4)
+	{
+		hms = HMS_connect(PROTO_V4, "rooms/%d/register", cv_masterserver_room_id.value);
 
-	ok = HMS_do(hms);
+		if (! hms)
+			return 0;
 
-	if (ok)
-	{
-		hms_server_token = strdup(strtok(hms->buffer, "\n"));
-	}
+		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
 
-	HMS_end(hms);
+		ok = HMS_do(hms);
+
+		if (ok)
+		{
+			hms_server_token = strdup(strtok(hms->buffer, "\n"));
+		}
+
+		HMS_end(hms);
+	}
 
 #ifndef NO_IPV6
 	if (!hms_allow_ipv6)
 		return ok;
 
-	hms = HMS_connect(PROTO_V6, "rooms/%d/register", ms_RoomId);
+	hms = HMS_connect(PROTO_V6, "rooms/%d/register", cv_masterserver_room_id.value);
 
 	if (! hms)
 		return 0;
@@ -486,20 +515,25 @@ int
 HMS_unlist (void)
 {
 	struct HMS_buffer *hms;
-	int ok;
+	int ok = 0;
 
-	hms = HMS_connect(PROTO_V4, "servers/%s/unlist", hms_server_token);
+	HMS_check_args_once();
 
-	if (! hms)
-		return 0;
+	if (hms_server_token && hms_allow_ipv4)
+	{
+		hms = HMS_connect(PROTO_V4, "servers/%s/unlist", hms_server_token);
 
-	curl_easy_setopt(hms->curl, CURLOPT_POST, 1);
-	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDSIZE, 0);
+		if (! hms)
+			return 0;
 
-	ok = HMS_do(hms);
-	HMS_end(hms);
+		curl_easy_setopt(hms->curl, CURLOPT_POST, 1);
+		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDSIZE, 0);
 
-	free(hms_server_token);
+		ok = HMS_do(hms);
+		HMS_end(hms);
+
+		free(hms_server_token);
+	}
 
 #ifndef NO_IPV6
 	if (hms_server_token_ipv6 && hms_allow_ipv6)
@@ -526,18 +560,14 @@ int
 HMS_update (void)
 {
 	struct HMS_buffer *hms;
-	int ok;
+	int ok = 0;
 
 	char post[256];
 
 	char *title;
 
-	hms = HMS_connect(PROTO_V4, "servers/%s/update", hms_server_token);
-
-	if (! hms)
-		return 0;
-
-	title = curl_easy_escape(hms->curl, cv_servername.string, 0);
+	HMS_check_args_once();
+	title = curl_easy_escape(NULL, cv_servername.string, 0);
 
 	snprintf(post, sizeof post,
 			"title=%s",
@@ -546,10 +576,18 @@ HMS_update (void)
 
 	curl_free(title);
 
-	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
+	if (hms_server_token && hms_allow_ipv4)
+	{
+		hms = HMS_connect(PROTO_V4, "servers/%s/update", hms_server_token);
 
-	ok = HMS_do(hms);
-	HMS_end(hms);
+		if (! hms)
+			return 0;
+
+		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
+
+		ok = HMS_do(hms);
+		HMS_end(hms);
+	}
 
 #ifndef NO_IPV6
 	if (hms_server_token_ipv6 && hms_allow_ipv6)
@@ -577,6 +615,8 @@ HMS_list_servers (void)
 	char *list;
 	char *p;
 
+	HMS_check_args_once();
+
 	hms = HMS_connect(PROTO_ANY, "servers");
 
 	if (! hms)
@@ -622,6 +662,8 @@ HMS_fetch_servers (msg_server_t *list, int room_number, int query_id)
 
 	int i;
 
+	HMS_check_args_once();
+
 	(void)query_id;
 
 	if (room_number > 0)
@@ -733,6 +775,8 @@ HMS_compare_mod_version (char *buffer, size_t buffer_size)
 	char *version;
 	char *version_name;
 
+	HMS_check_args_once();
+
 	hms = HMS_connect(PROTO_ANY, "versions/%d", MODID);
 
 	if (! hms)
diff --git a/src/netcode/i_net.h b/src/netcode/i_net.h
index 09b842296c313cdd43623cfecd1d8da64534397b..11da77ce52b0b88035d2058372f2b1fe4ea638ee 100644
--- a/src/netcode/i_net.h
+++ b/src/netcode/i_net.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -32,7 +32,6 @@
 #define INETPACKETLENGTH 1024
 
 extern INT16 hardware_MAXPACKETLENGTH;
-extern INT32 net_bandwidth; // in byte/s
 
 #if defined(_MSC_VER)
 #pragma pack(1)
@@ -40,39 +39,11 @@ extern INT32 net_bandwidth; // in byte/s
 
 typedef struct
 {
-	/// Supposed to be DOOMCOM_ID
-	INT32 id;
-
-	/// SRB2 executes an INT32 to execute commands.
-	INT16 intnum;
-	/// Communication between SRB2 and the driver.
-	/// Is CMD_SEND or CMD_GET.
-	INT16 command;
 	/// Is dest for send, set by get (-1 = no packet).
 	INT16 remotenode;
-
 	/// Number of bytes in doomdata to be sent
 	INT16 datalength;
 
-	/// Info common to all nodes.
-	/// Console is always node 0.
-	INT16 numnodes;
-	/// Flag: 1 = no duplication, 2-5 = dup for slow nets.
-	INT16 ticdup;
-	/// Flag: 1 = send a backup tic in every packet.
-	INT16 extratics;
-	/// kind of game
-	INT16 gametype;
-	/// Flag: -1 = new game, 0-5 = load savegame
-	INT16 savegame;
-	/// currect map
-	INT16 map;
-
-	/// Info specific to this node.
-	INT16 consoleplayer;
-	/// Number of "slots": the highest player number in use plus one.
-	INT16 numslots;
-
 	/// The packet data to be sent.
 	char data[MAXPACKETLENGTH];
 } ATTRPACK doomcom_t;
@@ -81,24 +52,28 @@ typedef struct
 #pragma pack()
 #endif
 
+/** \brief Number of connected nodes.
+*/
+extern INT16 numnetnodes;
+
+/** \brief Number of "slots": the highest player number in use plus one.
+*/
+extern INT16 numslots;
+
+/** \brief Flag: 1 = send a backup tic in every packet.
+*/
+extern INT16 extratics;
+
 extern doomcom_t *doomcom;
 
 /**	\brief return packet in doomcom struct
 */
 extern boolean (*I_NetGet)(void);
 
-/**	\brief ask to driver if there is data waiting
-*/
-extern boolean (*I_NetCanGet)(void);
-
 /**	\brief send packet within doomcom struct
 */
 extern void (*I_NetSend)(void);
 
-/**	\brief ask to driver if all is ok to send data now
-*/
-extern boolean (*I_NetCanSend)(void);
-
 /**	\brief	close a connection
 
 	\param	nodenum	node to be closed
diff --git a/src/netcode/i_tcp.c b/src/netcode/i_tcp.c
index 0148c485ab1fd5fd29cd4ca0bc6effca93ff69f1..eb4957e0466ccb0c8f8030a680895309ece8fb9d 100644
--- a/src/netcode/i_tcp.c
+++ b/src/netcode/i_tcp.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -37,6 +37,7 @@
 #endif
 
 #include "../doomdef.h"
+#include "../z_zone.h"
 
 #ifdef USE_WINSOCK1
 	#include <winsock.h>
@@ -87,6 +88,10 @@
 	#undef EHOSTUNREACH
 	#endif
 	#define EHOSTUNREACH WSAEHOSTUNREACH
+	#ifdef ENETUNREACH
+	#undef ENETUNREACH
+	#endif
+	#define ENETUNREACH WSAENETUNREACH
 	#ifndef IOC_VENDOR
 	#define IOC_VENDOR 0x18000000
 	#endif
@@ -114,20 +119,12 @@ typedef union
 } mysockaddr_t;
 
 	#ifdef HAVE_MINIUPNPC
-	#ifdef MINIUPNP_STATICLIB
-		#include "miniwget.h"
-		#include "miniupnpc.h"
-		#include "upnpcommands.h"
-	#else
-		#include "miniupnpc/miniwget.h"
-		#include "miniupnpc/miniupnpc.h"
-		#include "miniupnpc/upnpcommands.h"
-	#endif
-		static boolean UPNP_support = true;
+	#include "miniupnpc/miniwget.h"
+	#include "miniupnpc/miniupnpc.h"
+	#include "miniupnpc/upnpcommands.h"
+	static boolean UPNP_support = true;
 	#endif // HAVE_MINIUPNC
 
-#define MAXBANS 100
-
 #include "../i_system.h"
 #include "i_net.h"
 #include "d_net.h"
@@ -146,7 +143,6 @@ typedef union
 #endif
 
 #include "i_addrinfo.h"
-#define SELECTTEST
 #define DEFAULTPORT "5029"
 
 #ifdef USE_WINSOCK
@@ -175,8 +171,8 @@ 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 mysockaddr_t *banned;
+static UINT8 *bannedmask;
 
 static size_t numbans = 0;
 static boolean SOCK_bannednode[MAXNETNODES+1]; /// \note do we really need the +1?
@@ -323,7 +319,11 @@ init_upnpc_once(struct upnpdata *upnpuserdata)
 		I_OutputMsg(M_GetText("Found UPnP device:\n desc: %s\n st: %s\n"),
 		           dev->descURL, dev->st);
 
+#if (MINIUPNPC_API_VERSION >= 18)
+		UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr), NULL, 0);
+#else
 		UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr));
+#endif
 		I_OutputMsg(M_GetText("Local LAN IP address: %s\n"), lanaddr);
 		descXML = miniwget(dev->descURL, &descXMLsize, scope_id, &status_code);
 		if (descXML)
@@ -386,6 +386,7 @@ static const char *SOCK_AddrToStr(mysockaddr_t *sk)
 	int v6 = 0;
 #endif
 	void *addr;
+	int e = 0; // save error code so it can't be modified later code and avoid calling WSAGetLastError() more then once
 
 	if(sk->any.sa_family == AF_INET)
 		addr = &sk->ip4.sin_addr;
@@ -399,7 +400,10 @@ static const char *SOCK_AddrToStr(mysockaddr_t *sk)
 	if(addr == NULL)
 		sprintf(s, "No address");
 	else if(inet_ntop(sk->any.sa_family, addr, &s[v6], sizeof (s) - v6) == NULL)
-		sprintf(s, "Unknown family type, error #%u", errno);
+	{
+		e = errno;
+		sprintf(s, "Unknown family type, error #%u: %s", e, strerror(e));
+	}
 #ifdef HAVE_IPV6
 	else if(sk->any.sa_family == AF_INET6)
 	{
@@ -456,6 +460,8 @@ static boolean SOCK_cmpipv6(mysockaddr_t *a, mysockaddr_t *b, UINT8 mask)
 {
 	UINT8 bitmask;
 	I_Assert(mask <= 128);
+	if (mask == 0)
+		mask = 128;
 	if (memcmp(&a->ip6.sin6_addr.s6_addr, &b->ip6.sin6_addr.s6_addr, mask / 8) != 0)
 		return false;
 	if (mask % 8 == 0)
@@ -630,56 +636,6 @@ static boolean SOCK_Get(void)
 	return false;
 }
 
-// check if we can send (do not go over the buffer)
-
-static fd_set masterset;
-
-#ifdef SELECTTEST
-static boolean FD_CPY(fd_set *src, fd_set *dst, SOCKET_TYPE *fd, size_t len)
-{
-	boolean testset = false;
-	FD_ZERO(dst);
-	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
-		{
-			FD_SET(fd[i], dst);
-			testset = true;
-		}
-	}
-	return testset;
-}
-
-static boolean SOCK_CanSend(void)
-{
-	struct timeval timeval_for_select = {0, 0};
-	fd_set tset;
-	int wselect;
-
-	if(!FD_CPY(&masterset, &tset, mysockets, mysocketses))
-		return false;
-	wselect = select(255, NULL, &tset, NULL, &timeval_for_select);
-	if (wselect >= 1)
-		return true;
-	return false;
-}
-
-static boolean SOCK_CanGet(void)
-{
-	struct timeval timeval_for_select = {0, 0};
-	fd_set tset;
-	int rselect;
-
-	if(!FD_CPY(&masterset, &tset, mysockets, mysocketses))
-		return false;
-	rselect = select(255, &tset, NULL, NULL, &timeval_for_select);
-	if (rselect >= 1)
-		return true;
-	return false;
-}
-#endif
-
 static inline ssize_t SOCK_SendToAddr(SOCKET_TYPE socket, mysockaddr_t *sockaddr)
 {
 	socklen_t d4 = (socklen_t)sizeof(struct sockaddr_in);
@@ -687,7 +643,6 @@ static inline ssize_t SOCK_SendToAddr(SOCKET_TYPE socket, mysockaddr_t *sockaddr
 	socklen_t d6 = (socklen_t)sizeof(struct sockaddr_in6);
 #endif
 	socklen_t d, da = (socklen_t)sizeof(mysockaddr_t);
-	ssize_t status;
 
 	switch (sockaddr->any.sa_family)
 	{
@@ -698,17 +653,15 @@ static inline ssize_t SOCK_SendToAddr(SOCKET_TYPE socket, mysockaddr_t *sockaddr
 		default:       d = da; break;
 	}
 
-	status = sendto(socket, (char *)&doomcom->data, doomcom->datalength, 0, &sockaddr->any, d);
-	if (status == -1)
-	{
-		CONS_Alert(CONS_WARNING, "Unable to send packet to %s: %s\n", SOCK_AddrToStr(sockaddr), strerror(errno));
-	}
-	return status;
+	return sendto(socket, (char *)&doomcom->data, doomcom->datalength, 0, &sockaddr->any, d);
 }
 
+#define ALLOWEDERROR(x) ((x) == ECONNREFUSED || (x) == EWOULDBLOCK || (x) == EHOSTUNREACH || (x) == ENETUNREACH || (x) == EADDRNOTAVAIL)
+
 static void SOCK_Send(void)
 {
 	ssize_t c = ERRSOCKET;
+	int e = 0; // save error code so it can't be modified later code and avoid calling WSAGetLastError() more then once
 
 	if (!nodeconnected[doomcom->remotenode])
 		return;
@@ -720,34 +673,53 @@ static void SOCK_Send(void)
 			for (size_t j = 0; j < broadcastaddresses; j++)
 			{
 				if (myfamily[i] == broadcastaddress[j].any.sa_family)
-					SOCK_SendToAddr(mysockets[i], &broadcastaddress[j]);
+				{
+					c = SOCK_SendToAddr(mysockets[i], &broadcastaddress[j]);
+					if (c == ERRSOCKET)
+					{
+						e = errno;
+						if (!ALLOWEDERROR(e))
+							break;
+					}
+				}
 			}
 		}
-		return;
 	}
 	else if (nodesocket[doomcom->remotenode] == (SOCKET_TYPE)ERRSOCKET)
 	{
 		for (size_t i = 0; i < mysocketses; i++)
 		{
 			if (myfamily[i] == clientaddress[doomcom->remotenode].any.sa_family)
-				SOCK_SendToAddr(mysockets[i], &clientaddress[doomcom->remotenode]);
+			{
+				c = SOCK_SendToAddr(mysockets[i], &clientaddress[doomcom->remotenode]);
+				if (c == ERRSOCKET)
+				{
+					e = errno;
+					if (!ALLOWEDERROR(e))
+						break;
+				}
+			}
 		}
-		return;
 	}
 	else
 	{
 		c = SOCK_SendToAddr(nodesocket[doomcom->remotenode], &clientaddress[doomcom->remotenode]);
+		if (c == ERRSOCKET)
+		{
+			e = errno;
+		}
 	}
 
-	if (c == ERRSOCKET)
+	if (c == ERRSOCKET && e != 0) // 0 means no socket for the address family was found
 	{
-		int e = errno; // save error code so it can't be modified later
-		if (e != ECONNREFUSED && e != EWOULDBLOCK && e != EHOSTUNREACH)
-			I_Error("SOCK_Send, error sending to node %d (%s) #%u: %s", doomcom->remotenode,
+		if (!ALLOWEDERROR(e))
+			I_Error("SOCK_Send, error sending to node %d (%s) #%u, %s", doomcom->remotenode,
 				SOCK_GetNodeAddress(doomcom->remotenode), e, strerror(e));
 	}
 }
 
+#undef ALLOWEDERROR
+
 static void SOCK_FreeNodenum(INT32 numnode)
 {
 	// can't disconnect from self :)
@@ -772,6 +744,8 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 {
 	SOCKET_TYPE s = socket(family, SOCK_DGRAM, IPPROTO_UDP);
 	int opt;
+	int rc;
+	int e = 0; // save error code so it can't be modified later code and avoid calling WSAGetLastError() more then once
 	socklen_t opts;
 #ifdef FIONBIO
 	unsigned long trueval = true;
@@ -786,12 +760,17 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 #ifdef USE_WINSOCK2
 		DWORD dwBytesReturned = 0;
 		BOOL bfalse = FALSE;
-		WSAIoctl(s, SIO_UDP_CONNRESET, &bfalse, sizeof(bfalse),
+		rc = WSAIoctl(s, SIO_UDP_CONNRESET, &bfalse, sizeof(bfalse),
 		         NULL, 0, &dwBytesReturned, NULL, NULL);
 #else
 		unsigned long falseval = false;
-		ioctl(s, SIO_UDP_CONNRESET, &falseval);
+		rc = ioctl(s, SIO_UDP_CONNRESET, &falseval);
 #endif
+		if (rc == -1)
+		{
+			e = errno;
+			I_OutputMsg("SIO_UDP_CONNRESET failed: #%u, %s\n", e, strerror(e));
+		}
 	}
 #endif
 
@@ -804,14 +783,22 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 		{
 			opt = true;
 			opts = (socklen_t)sizeof(opt);
-			setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, opts);
+			rc = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, opts);
+			if (rc <= -1)
+			{
+				e = errno;
+				I_OutputMsg("setting SO_REUSEADDR failed: #%u, %s\n", e, strerror(e));
+			}
 		}
 		// make it broadcastable
 		opt = true;
 		opts = (socklen_t)sizeof(opt);
-		if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, (char *)&opt, opts))
+		rc = setsockopt(s, SOL_SOCKET, SO_BROADCAST, (char *)&opt, opts);
+		if (rc <= -1)
 		{
+			e = errno;
 			CONS_Alert(CONS_WARNING, M_GetText("Could not get broadcast rights\n")); // I do not care anymore
+			I_OutputMsg("setting SO_BROADCAST failed: #%u, %s\n", e, strerror(e));
 		}
 	}
 #ifdef HAVE_IPV6
@@ -821,24 +808,34 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 		{
 			opt = true;
 			opts = (socklen_t)sizeof(opt);
-			setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, opts);
+			rc = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, opts);
+			if (rc <= -1)
+			{
+				e = errno;
+				I_OutputMsg("setting SO_REUSEADDR failed: #%u, %s\n", e, strerror(e));
+			}
 		}
 #ifdef IPV6_V6ONLY
 		// make it IPv6 ony
 		opt = true;
 		opts = (socklen_t)sizeof(opt);
-		if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&opt, opts))
+		rc = setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&opt, opts);
+		if (rc <= -1)
 		{
+			e = errno;
 			CONS_Alert(CONS_WARNING, M_GetText("Could not limit IPv6 bind\n")); // I do not care anymore
+			I_OutputMsg("setting IPV6_V6ONLY failed: #%u, %s\n", e, strerror(e));
 		}
 #endif
 	}
 #endif
 
-	if (bind(s, addr, addrlen) == ERRSOCKET)
+	rc = bind(s, addr, addrlen);
+	if (rc == ERRSOCKET)
 	{
+		e = errno;
 		close(s);
-		I_OutputMsg("Binding failed\n");
+		I_OutputMsg("Binding failed: #%u, %s\n", e, strerror(e));
 		return (SOCKET_TYPE)ERRSOCKET;
 	}
 
@@ -852,9 +849,18 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 
 			inet_pton(AF_INET6, IPV6_MULTICAST_ADDRESS, &maddr.ipv6mr_multiaddr);
 			maddr.ipv6mr_interface = 0;
-			if (setsockopt(s, IPPROTO_IPV6, IPV6_JOIN_GROUP, (const char *)&maddr, sizeof(maddr)) != 0)
+			rc = setsockopt(s, IPPROTO_IPV6, IPV6_JOIN_GROUP, (const char *)&maddr, sizeof(maddr));
+			if (rc <= -1)
 			{
+				e = errno;
 				CONS_Alert(CONS_WARNING, M_GetText("Could not register multicast address\n"));
+				if (e == ENODEV)
+				{
+					close(s);
+					I_OutputMsg("Binding failed: no IPv6 device\n");
+					return (SOCKET_TYPE)ERRSOCKET;
+				}
+				I_OutputMsg("setting IPV6_JOIN_GROUP failed: #%u, %s \n", e, strerror(e));
 			}
 		}
 	}
@@ -862,33 +868,56 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 
 #ifdef FIONBIO
 	// make it non blocking
-	opt = true;
-	if (ioctl(s, FIONBIO, &trueval) != 0)
+	rc = ioctl(s, FIONBIO, &trueval);
+	if (rc == -1)
 	{
+		e = errno;
 		close(s);
-		I_OutputMsg("Seting FIOBIO on failed\n");
+		I_OutputMsg("FIOBIO failed: #%u, %s\n", e, strerror(e));
 		return (SOCKET_TYPE)ERRSOCKET;
 	}
 #endif
 
+	opt = 0;
 	opts = (socklen_t)sizeof(opt);
-	getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char *)&opt, &opts);
+	rc = getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char *)&opt, &opts);
+	if (rc <= -1)
+	{
+		e = errno;
+		I_OutputMsg("getting SO_RCVBUF failed: #%u, %s\n", e, strerror(e));
+	}
 	CONS_Printf(M_GetText("Network system buffer: %dKb\n"), opt>>10);
 
 	if (opt < 64<<10) // 64k
 	{
 		opt = 64<<10;
 		opts = (socklen_t)sizeof(opt);
-		setsockopt(s, SOL_SOCKET, SO_RCVBUF, (char *)&opt, opts);
-		getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char *)&opt, &opts);
-		if (opt < 64<<10)
+		rc = setsockopt(s, SOL_SOCKET, SO_RCVBUF, (char *)&opt, opts);
+		if (rc <= -1)
+		{
+			e = errno;
+			I_OutputMsg("setting SO_RCVBUF failed: #%u, %s\n", e, strerror(e));
+		}
+		opt = 0;
+		rc = getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char *)&opt, &opts);
+		if (rc <= -1)
+		{
+			e = errno;
+			I_OutputMsg("getting SO_RCVBUF failed: #%u, %s\n", e, strerror(e));
+		}
+		if (opt <= 64<<10)
 			CONS_Alert(CONS_WARNING, M_GetText("Can't set buffer length to 64k, file transfer will be bad\n"));
 		else
 			CONS_Printf(M_GetText("Network system buffer set to: %dKb\n"), opt>>10);
 	}
 
-	if (getsockname(s, &straddr.any, &len) == -1)
+	rc = getsockname(s, &straddr.any, &len);
+	if (rc != 0)
+	{
+		e = errno;
 		CONS_Alert(CONS_WARNING, M_GetText("Failed to get port number\n"));
+		I_OutputMsg("getsockname failed: #%u, %s\n", e, strerror(e));
+	}
 	else
 	{
 		if (family == AF_INET)
@@ -910,6 +939,7 @@ static boolean UDP_Socket(void)
 #ifdef HAVE_IPV6
 	const INT32 b_ipv6 = !M_CheckParm("-noipv6");
 #endif
+	const INT32 b_ipv4 = !M_CheckParm("-noipv4");
 	const char *serv;
 
 
@@ -917,7 +947,6 @@ static boolean UDP_Socket(void)
 		mysockets[s] = ERRSOCKET;
 	for (s = 0; s < MAXNETNODES+1; s++)
 		nodesocket[s] = ERRSOCKET;
-	FD_ZERO(&masterset);
 	s = 0;
 
 	memset(&hints, 0x00, sizeof (hints));
@@ -926,16 +955,45 @@ static boolean UDP_Socket(void)
 	hints.ai_socktype = SOCK_DGRAM;
 	hints.ai_protocol = IPPROTO_UDP;
 
+#ifdef HAVE_IPV6
+	if (!b_ipv6)
+		I_OutputMsg("Disabling IPv6 support at runtime\n");
+#else
+	I_OutputMsg("Compiled without IPv6 support\n");
+#endif
+
 	if (serverrunning)
 		serv = serverport_name;
 	else
 		serv = clientport_name;
 
-	if (M_CheckParm("-bindaddr"))
+	if (b_ipv4)
 	{
-		while (M_IsNextParm())
+		if (M_CheckParm("-bindaddr"))
 		{
-			gaie = I_getaddrinfo(M_GetNextParm(), serv, &hints, &ai);
+			while (M_IsNextParm())
+			{
+				gaie = I_getaddrinfo(M_GetNextParm(), serv, &hints, &ai);
+				if (gaie == 0)
+				{
+					runp = ai;
+					while (runp != NULL && s < MAXNETNODES+1)
+					{
+						mysockets[s] = UDP_Bind(runp->ai_family, runp->ai_addr, (socklen_t)runp->ai_addrlen);
+						if (mysockets[s] != (SOCKET_TYPE)ERRSOCKET)
+						{
+							myfamily[s] = hints.ai_family;
+							s++;
+						}
+						runp = runp->ai_next;
+					}
+					I_freeaddrinfo(ai);
+				}
+			}
+		}
+		else
+		{
+			gaie = I_getaddrinfo("0.0.0.0", serv, &hints, &ai);
 			if (gaie == 0)
 			{
 				runp = ai;
@@ -944,9 +1002,15 @@ static boolean UDP_Socket(void)
 					mysockets[s] = UDP_Bind(runp->ai_family, runp->ai_addr, (socklen_t)runp->ai_addrlen);
 					if (mysockets[s] != (SOCKET_TYPE)ERRSOCKET)
 					{
-						FD_SET(mysockets[s], &masterset);
 						myfamily[s] = hints.ai_family;
 						s++;
+#ifdef HAVE_MINIUPNPC
+						if (UPNP_support)
+						{
+							I_UPnP_rem(serverport_name, "UDP");
+							I_UPnP_add(NULL, serverport_name, "UDP");
+						}
+#endif
 					}
 					runp = runp->ai_next;
 				}
@@ -954,33 +1018,6 @@ static boolean UDP_Socket(void)
 			}
 		}
 	}
-	else
-	{
-		gaie = I_getaddrinfo("0.0.0.0", serv, &hints, &ai);
-		if (gaie == 0)
-		{
-			runp = ai;
-			while (runp != NULL && s < MAXNETNODES+1)
-			{
-				mysockets[s] = UDP_Bind(runp->ai_family, runp->ai_addr, (socklen_t)runp->ai_addrlen);
-				if (mysockets[s] != (SOCKET_TYPE)ERRSOCKET)
-				{
-					FD_SET(mysockets[s], &masterset);
-					myfamily[s] = hints.ai_family;
-					s++;
-#ifdef HAVE_MINIUPNPC
-					if (UPNP_support)
-					{
-						I_UPnP_rem(serverport_name, "UDP");
-						I_UPnP_add(NULL, serverport_name, "UDP");
-					}
-#endif
-				}
-				runp = runp->ai_next;
-			}
-			I_freeaddrinfo(ai);
-		}
-	}
 #ifdef HAVE_IPV6
 	if (b_ipv6)
 	{
@@ -998,7 +1035,6 @@ static boolean UDP_Socket(void)
 						mysockets[s] = UDP_Bind(runp->ai_family, runp->ai_addr, (socklen_t)runp->ai_addrlen);
 						if (mysockets[s] != (SOCKET_TYPE)ERRSOCKET)
 						{
-							FD_SET(mysockets[s], &masterset);
 							myfamily[s] = hints.ai_family;
 							s++;
 						}
@@ -1019,7 +1055,6 @@ static boolean UDP_Socket(void)
 					mysockets[s] = UDP_Bind(runp->ai_family, runp->ai_addr, (socklen_t)runp->ai_addrlen);
 					if (mysockets[s] != (SOCKET_TYPE)ERRSOCKET)
 					{
-						FD_SET(mysockets[s], &masterset);
 						myfamily[s] = hints.ai_family;
 						s++;
 					}
@@ -1047,11 +1082,14 @@ static boolean UDP_Socket(void)
 
 	s = 0;
 
-	// setup broadcast adress to BROADCASTADDR entry
-	broadcastaddress[s].any.sa_family = AF_INET;
-	broadcastaddress[s].ip4.sin_port = htons(atoi(DEFAULTPORT));
-	broadcastaddress[s].ip4.sin_addr.s_addr = htonl(INADDR_BROADCAST);
-	s++;
+	if (b_ipv4)
+	{
+		// setup broadcast adress to BROADCASTADDR entry
+		broadcastaddress[s].any.sa_family = AF_INET;
+		broadcastaddress[s].ip4.sin_port = htons(atoi(DEFAULTPORT));
+		broadcastaddress[s].ip4.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+		s++;
+	}
 
 #ifdef HAVE_IPV6
 	if (b_ipv6)
@@ -1067,7 +1105,7 @@ static boolean UDP_Socket(void)
 
 	broadcastaddresses = s;
 
-	doomcom->extratics = 1; // internet is very high ping
+	extratics = 1; // internet is very high ping
 
 	return true;
 }
@@ -1145,16 +1183,13 @@ boolean I_InitTcpDriver(void)
 
 static void SOCK_CloseSocket(void)
 {
-	for (size_t i=0; i < MAXNETNODES+1; i++)
+	for (size_t i=0; i < mysocketses; i++)
 	{
-		if (mysockets[i] != (SOCKET_TYPE)ERRSOCKET
-		 && FD_ISSET(mysockets[i], &masterset))
-		{
-			FD_CLR(mysockets[i], &masterset);
+		if (mysockets[i] != (SOCKET_TYPE)ERRSOCKET)
 			close(mysockets[i]);
-		}
 		mysockets[i] = ERRSOCKET;
 	}
+	mysocketses = 0;
 }
 
 void I_ShutdownTcpDriver(void)
@@ -1183,7 +1218,7 @@ static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port)
 	DEBFILE(va("Creating new node: %s@%s\n", address, port));
 
 	memset (&hints, 0x00, sizeof (hints));
-	hints.ai_flags = 0;
+	hints.ai_flags = AI_ADDRCONFIG;
 	hints.ai_family = AF_UNSPEC;
 	hints.ai_socktype = SOCK_DGRAM;
 	hints.ai_protocol = IPPROTO_UDP;
@@ -1206,18 +1241,14 @@ static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port)
 		// test ip address of server
 		for (i = 0; i < mysocketses; ++i)
 		{
-			/* sendto tests that there is a network to this
-				address */
-			if (runp->ai_addr->sa_family == myfamily[i] &&
-					sendto(mysockets[i], NULL, 0, 0,
-						runp->ai_addr, runp->ai_addrlen) == 0)
+			if (runp->ai_addr->sa_family == myfamily[i])
 			{
 				memcpy(&clientaddress[newnode], runp->ai_addr, runp->ai_addrlen);
 				break;
 			}
 		}
 
-		if (i < mysocketses)
+		if (i >= mysocketses)
 			runp = runp->ai_next;
 		else
 			break;
@@ -1240,12 +1271,6 @@ static boolean SOCK_OpenSocket(void)
 	I_NetFreeNodenum = SOCK_FreeNodenum;
 	I_NetMakeNodewPort = SOCK_NetMakeNodewPort;
 
-#ifdef SELECTTEST
-	// seem like not work with libsocket : (
-	I_NetCanSend = SOCK_CanSend;
-	I_NetCanGet = SOCK_CanGet;
-#endif
-
 	// build the socket but close it first
 	SOCK_CloseSocket();
 	return UDP_Socket();
@@ -1255,9 +1280,9 @@ static boolean SOCK_Ban(INT32 node)
 {
 	if (node > MAXNETNODES)
 		return false;
-	if (numbans == MAXBANS)
-		return false;
 
+	banned = Z_Realloc(banned, sizeof(*banned) * (numbans+1), PU_STATIC, NULL);
+	bannedmask = Z_Realloc(bannedmask, sizeof(*bannedmask) * (numbans+1), PU_STATIC, NULL);
 	M_Memcpy(&banned[numbans], &clientaddress[node], sizeof (mysockaddr_t));
 	if (banned[numbans].any.sa_family == AF_INET)
 	{
@@ -1268,7 +1293,7 @@ static boolean SOCK_Ban(INT32 node)
 	else if (banned[numbans].any.sa_family == AF_INET6)
 	{
 		banned[numbans].ip6.sin6_port = 0;
-		bannedmask[numbans] = 128;
+		bannedmask[numbans] = 64;
 	}
 #endif
 	numbans++;
@@ -1280,7 +1305,7 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask)
 	struct my_addrinfo *ai, *runp, hints;
 	int gaie;
 
-	if (numbans == MAXBANS || !address)
+	if (!address)
 		return false;
 
 	memset(&hints, 0x00, sizeof(hints));
@@ -1295,15 +1320,17 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask)
 
 	runp = ai;
 
-	while(runp != NULL && numbans != MAXBANS)
+	while(runp != NULL)
 	{
+		banned = Z_Realloc(banned, sizeof(*banned) * (numbans+1), PU_STATIC, NULL);
+		bannedmask = Z_Realloc(bannedmask, sizeof(*bannedmask) * (numbans+1), PU_STATIC, NULL);
 		memcpy(&banned[numbans], runp->ai_addr, runp->ai_addrlen);
 
 		if (mask)
 			bannedmask[numbans] = (UINT8)atoi(mask);
 #ifdef HAVE_IPV6
 		else if (runp->ai_family == AF_INET6)
-			bannedmask[numbans] = 128;
+			bannedmask[numbans] = 64;
 #endif
 		else
 			bannedmask[numbans] = 32;
@@ -1312,7 +1339,7 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask)
 			bannedmask[numbans] = 32;
 #ifdef HAVE_IPV6
 		else if (bannedmask[numbans] > 128 && runp->ai_family == AF_INET6)
-			bannedmask[numbans] = 128;
+			bannedmask[numbans] = 64;
 #endif
 		numbans++;
 		runp = runp->ai_next;
@@ -1326,6 +1353,10 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask)
 static void SOCK_ClearBans(void)
 {
 	numbans = 0;
+	Z_Free(banned);
+	banned = NULL;
+	Z_Free(bannedmask);
+	bannedmask = NULL;
 }
 
 boolean I_InitTcpNetwork(void)
@@ -1362,25 +1393,24 @@ boolean I_InitTcpNetwork(void)
 		// in-game.
 		// Since Boris has implemented join in-game, there is no actual need for specifying a
 		// particular number here.
-		// FIXME: for dedicated server, numnodes needs to be set to 0 upon start
+		// FIXME: for dedicated server, numnetnodes needs to be set to 0 upon start
 		if (dedicated)
-			doomcom->numnodes = 0;
+			numnetnodes = 0;
 /*		else if (M_IsNextParm())
-			doomcom->numnodes = (INT16)atoi(M_GetNextParm());*/
+			numnetnodes = (INT16)atoi(M_GetNextParm());*/
 		else
-			doomcom->numnodes = 1;
+			numnetnodes = 1;
 
-		if (doomcom->numnodes < 0)
-			doomcom->numnodes = 0;
-		if (doomcom->numnodes > MAXNETNODES)
-			doomcom->numnodes = MAXNETNODES;
+		if (numnetnodes < 0)
+			numnetnodes = 0;
+		if (numnetnodes > MAXNETNODES)
+			numnetnodes = MAXNETNODES;
 
 		// server
 		servernode = 0;
 		// FIXME:
 		// ??? and now ?
 		// server on a big modem ??? 4*isdn
-		net_bandwidth = 16000;
 		hardware_MAXPACKETLENGTH = INETPACKETLENGTH;
 
 		ret = true;
@@ -1409,7 +1439,6 @@ boolean I_InitTcpNetwork(void)
 			// so we're on a LAN
 			COM_BufAddText("connect any\n");
 
-			net_bandwidth = 800000;
 			hardware_MAXPACKETLENGTH = MAXPACKETLENGTH;
 		}
 	}
diff --git a/src/netcode/mserv.c b/src/netcode/mserv.c
index 3acacd24c164471057ca3ffa7a6225f8ba95d2f3..f17db4b6e32e59a08a7ac5c9c3a44e867c0ffff2 100644
--- a/src/netcode/mserv.c
+++ b/src/netcode/mserv.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 // Copyright (C) 2020-2023 by James R.
 //
 // This program is free software distributed under the
@@ -50,9 +50,12 @@ static void Command_Listserv_f(void);
 
 #endif/*MASTERSERVER*/
 
+static boolean ServerName_CanChange (const char*);
+
 static void Update_parameters (void);
 
 static void MasterServer_OnChange(void);
+static void RoomId_OnChange(void);
 
 static CV_PossibleValue_t masterserver_update_rate_cons_t[] = {
 	{2,  "MIN"},
@@ -61,11 +64,13 @@ static CV_PossibleValue_t masterserver_update_rate_cons_t[] = {
 };
 
 consvar_t cv_masterserver = CVAR_INIT ("masterserver", "https://ds.ms.srb2.org/MS/0", CV_SAVE|CV_CALL, NULL, MasterServer_OnChange);
-consvar_t cv_servername = CVAR_INIT ("servername", "SRB2 server", CV_SAVE|CV_NETVAR|CV_CALL|CV_NOINIT|CV_ALLOWLUA, NULL, Update_parameters);
+consvar_t cv_servername = CVAR_INIT_WITH_CALLBACKS ("servername", "SRB2 server", CV_SAVE|CV_NETVAR|CV_CALL|CV_NOINIT|CV_ALLOWLUA, NULL, Update_parameters, ServerName_CanChange);
 
 consvar_t cv_masterserver_update_rate = CVAR_INIT ("masterserver_update_rate", "15", CV_SAVE|CV_CALL|CV_NOINIT, masterserver_update_rate_cons_t, Update_parameters);
+CV_PossibleValue_t cv_masterserver_room_values[] = {{-1, "MIN"}, {999999999, "MAX"}, {0, NULL}};
+consvar_t cv_masterserver_room_id = CVAR_INIT ("masterserver_room_id", "-1", CV_CALL, cv_masterserver_room_values, RoomId_OnChange);
 
-INT16 ms_RoomId = -1;
+static INT16 ms_RoomId = -1;
 
 #if defined (MASTERSERVER) && defined (HAVE_THREADS)
 int           ms_QueryId;
@@ -90,6 +95,7 @@ void AddMServCommands(void)
 {
 	CV_RegisterVar(&cv_masterserver);
 	CV_RegisterVar(&cv_masterserver_update_rate);
+	CV_RegisterVar(&cv_masterserver_room_id);
 	CV_RegisterVar(&cv_masterserver_timeout);
 	CV_RegisterVar(&cv_masterserver_debug);
 	CV_RegisterVar(&cv_masterserver_token);
@@ -444,7 +450,7 @@ void UnregisterServer(void)
 static boolean
 Online (void)
 {
-	return ( serverrunning && ms_RoomId > 0 );
+	return ( serverrunning && cv_masterserver_room_id.value > 0 );
 }
 
 static inline void SendPingToMasterServer(void)
@@ -497,6 +503,15 @@ Set_api (const char *api)
 
 #endif/*MASTERSERVER*/
 
+static boolean ServerName_CanChange(const char* newvalue)
+{
+	if (strlen(newvalue) < MAXSERVERNAME)
+		return true;
+
+	CONS_Alert(CONS_NOTICE, "The server name must be shorter than %d characters\n", MAXSERVERNAME);
+	return false;
+}
+
 static void
 Update_parameters (void)
 {
@@ -523,6 +538,17 @@ Update_parameters (void)
 #endif/*MASTERSERVER*/
 }
 
+static void RoomId_OnChange(void)
+{
+	if (ms_RoomId != cv_masterserver_room_id.value)
+	{
+		UnregisterServer();
+		ms_RoomId = cv_masterserver_room_id.value;
+		if (Online())
+			RegisterServer();
+	}
+}
+
 static void MasterServer_OnChange(void)
 {
 #ifdef MASTERSERVER
diff --git a/src/netcode/mserv.h b/src/netcode/mserv.h
index 0bc8c8e6b878a415887e2a9613188d3ceac30956..ed3c9b27b8f6d9f52b1c8cc4e2640b5501653dff 100644
--- a/src/netcode/mserv.h
+++ b/src/netcode/mserv.h
@@ -66,15 +66,11 @@ typedef struct
 
 extern consvar_t cv_masterserver, cv_servername;
 extern consvar_t cv_masterserver_update_rate;
+extern consvar_t cv_masterserver_room_id;
 extern consvar_t cv_masterserver_timeout;
 extern consvar_t cv_masterserver_debug;
 extern consvar_t cv_masterserver_token;
 
-// < 0 to not connect (usually -1) (offline mode)
-// == 0 to show all rooms, not a valid hosting room
-// anything else is whatever room the MS assigns to that number (online mode)
-extern INT16 ms_RoomId;
-
 #ifdef HAVE_THREADS
 extern int           ms_QueryId;
 extern I_mutex       ms_QueryId_mutex;
diff --git a/src/netcode/net_command.c b/src/netcode/net_command.c
index 2b3abfd0238a3246fedc086afc007a85c993b425..d319e336f1bb52647cd242d3f4d1dc131e8bf902 100644
--- a/src/netcode/net_command.c
+++ b/src/netcode/net_command.c
@@ -269,10 +269,10 @@ void PT_TextCmd(SINT8 node, INT32 netconsole)
 		}
 
 		// 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
+		// numslots+1 "+1" since numslots can change within this time and sent time
 		j = software_MAXPACKETLENGTH
 			- (netbuffer->u.textcmd[0]+2+BASESERVERTICSSIZE
-			+ (doomcom->numslots+1)*sizeof(ticcmd_t));
+			+ (numslots+1)*sizeof(ticcmd_t));
 
 		// search a tic that have enougth space in the ticcmd
 		while ((textcmd = D_GetExistingTextcmd(tic, netconsole)),
diff --git a/src/netcode/server_connection.c b/src/netcode/server_connection.c
index f4601e4acac08a187b982f8e286e9e53d46b2396..4dfcf400da2a9dbafb7fa8a9164de72986fe4cf3 100644
--- a/src/netcode/server_connection.c
+++ b/src/netcode/server_connection.c
@@ -245,7 +245,7 @@ static boolean SV_SendServerConfig(INT32 node)
 	netbuffer->packettype = PT_SERVERCFG;
 
 	netbuffer->u.servercfg.serverplayer = (UINT8)serverplayer;
-	netbuffer->u.servercfg.totalslotnum = (UINT8)(doomcom->numslots);
+	netbuffer->u.servercfg.totalslotnum = (UINT8)numslots;
 	netbuffer->u.servercfg.gametic = (tic_t)LONG(gametic);
 	netbuffer->u.servercfg.clientnode = (UINT8)node;
 	netbuffer->u.servercfg.gamestate = (UINT8)gamestate;
diff --git a/src/netcode/tic_command.c b/src/netcode/tic_command.c
index 7721bc3f1b1149790ce4de35ba1f1a7aa69f00c5..66f6bb4252bb5a040700380ec73cb1c03786660b 100644
--- a/src/netcode/tic_command.c
+++ b/src/netcode/tic_command.c
@@ -244,7 +244,7 @@ void PT_ServerTics(SINT8 node, INT32 netconsole)
 	if (realstart <= neededtic && realend > neededtic)
 	{
 		UINT8 *pak = (UINT8 *)&packet->cmds;
-		UINT8 *txtpak = (UINT8 *)&packet->cmds[packet->numslots * packet->numtics];
+		UINT8 *txtpak = (UINT8 *)&packet->cmds[numslots * packet->numtics];
 
 		for (tic_t i = realstart; i < realend; i++)
 		{
@@ -253,7 +253,7 @@ void PT_ServerTics(SINT8 node, INT32 netconsole)
 
 			// copy the tics
 			pak = G_ScpyTiccmd(netcmds[i%BACKUPTICS], pak,
-				packet->numslots*sizeof (ticcmd_t));
+				numslots*sizeof (ticcmd_t));
 
 			CL_CopyNetCommandsFromServerPacket(i, &txtpak);
 		}
@@ -320,7 +320,7 @@ static tic_t SV_CalculateNumTicsForPacket(SINT8 nodenum, tic_t firsttic, tic_t l
 
 	for (tic_t tic = firsttic; tic < lasttic; tic++)
 	{
-		size += sizeof (ticcmd_t) * doomcom->numslots;
+		size += sizeof (ticcmd_t) * numslots;
 		size += TotalTextCmdPerTic(tic);
 
 		if (size > software_MAXPACKETLENGTH)
@@ -337,7 +337,7 @@ static tic_t SV_CalculateNumTicsForPacket(SINT8 nodenum, tic_t firsttic, tic_t l
 				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);
+							sizeu1(size), numslots, nodenum);
 				else
 				{
 					lasttic++; // send it anyway!
@@ -394,20 +394,20 @@ void SV_SendTics(void)
 			netbuffer->packettype = PT_SERVERTICS;
 			netbuffer->u.serverpak.starttic = realfirsttic;
 			netbuffer->u.serverpak.numtics = (UINT8)(lasttictosend - realfirsttic);
-			netbuffer->u.serverpak.numslots = (UINT8)SHORT(doomcom->numslots);
+			netbuffer->u.serverpak.numslots = (UINT8)SHORT(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));
+				bufpos = G_DcpyTiccmd(bufpos, netcmds[i%BACKUPTICS], 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;
+			if (lasttictosend-extratics > realfirsttic)
+				node->supposedtic = lasttictosend-extratics;
 			else
 				node->supposedtic = lasttictosend;
 			node->supposedtic = max(node->supposedtic, node->tic);
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 0c02263f5ac2e9fcd22a30a7c1a5a22206bc1e5e..a810176b05a1002113b27f1f4e5d4b62e0ff9265 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -54,276 +54,6 @@ static dirtype_t diags[] =
 	DI_NORTHWEST, DI_NORTHEAST, DI_SOUTHWEST, DI_SOUTHEAST
 };
 
-//Real Prototypes to A_*
-void A_Fall(mobj_t *actor);
-void A_Look(mobj_t *actor);
-void A_Chase(mobj_t *actor);
-void A_FaceStabChase(mobj_t *actor);
-void A_FaceStabRev(mobj_t *actor);
-void A_FaceStabHurl(mobj_t *actor);
-void A_FaceStabMiss(mobj_t *actor);
-void A_StatueBurst(mobj_t *actor);
-void A_JetJawRoam(mobj_t *actor);
-void A_JetJawChomp(mobj_t *actor);
-void A_PointyThink(mobj_t *actor);
-void A_CheckBuddy(mobj_t *actor);
-void A_HoodFire(mobj_t *actor);
-void A_HoodThink(mobj_t *actor);
-void A_HoodFall(mobj_t *actor);
-void A_ArrowBonks(mobj_t *actor);
-void A_SnailerThink(mobj_t *actor);
-void A_SharpChase(mobj_t *actor);
-void A_SharpSpin(mobj_t *actor);
-void A_SharpDecel(mobj_t *actor);
-void A_CrushstaceanWalk(mobj_t *actor);
-void A_CrushstaceanPunch(mobj_t *actor);
-void A_CrushclawAim(mobj_t *actor);
-void A_CrushclawLaunch(mobj_t *actor);
-void A_VultureVtol(mobj_t *actor);
-void A_VultureCheck(mobj_t *actor);
-void A_VultureHover(mobj_t *actor);
-void A_VultureBlast(mobj_t *actor);
-void A_VultureFly(mobj_t *actor);
-void A_SkimChase(mobj_t *actor);
-void A_FaceTarget(mobj_t *actor);
-void A_FaceTracer(mobj_t *actor);
-void A_LobShot(mobj_t *actor);
-void A_FireShot(mobj_t *actor);
-void A_SuperFireShot(mobj_t *actor);
-void A_BossFireShot(mobj_t *actor);
-void A_Boss7FireMissiles(mobj_t *actor);
-void A_Boss1Laser(mobj_t *actor);
-void A_FocusTarget(mobj_t *actor);
-void A_Boss4Reverse(mobj_t *actor);
-void A_Boss4SpeedUp(mobj_t *actor);
-void A_Boss4Raise(mobj_t *actor);
-void A_SkullAttack(mobj_t *actor);
-void A_BossZoom(mobj_t *actor);
-void A_BossScream(mobj_t *actor);
-void A_Scream(mobj_t *actor);
-void A_Pain(mobj_t *actor);
-void A_1upThinker(mobj_t *actor);
-void A_MonitorPop(mobj_t *actor);
-void A_GoldMonitorPop(mobj_t *actor);
-void A_GoldMonitorRestore(mobj_t *actor);
-void A_GoldMonitorSparkle(mobj_t *actor);
-void A_Explode(mobj_t *actor);
-void A_BossDeath(mobj_t *actor);
-void A_SetShadowScale(mobj_t *actor);
-void A_ShadowScream(mobj_t *actor);
-void A_CustomPower(mobj_t *actor);
-void A_GiveWeapon(mobj_t *actor);
-void A_RingBox(mobj_t *actor);
-void A_Invincibility(mobj_t *actor);
-void A_SuperSneakers(mobj_t *actor);
-void A_AwardScore(mobj_t *actor);
-void A_ExtraLife(mobj_t *actor);
-void A_GiveShield(mobj_t *actor);
-void A_GravityBox(mobj_t *actor);
-void A_ScoreRise(mobj_t *actor);
-void A_BunnyHop(mobj_t *actor);
-void A_BubbleSpawn(mobj_t *actor);
-void A_FanBubbleSpawn(mobj_t *actor);
-void A_BubbleRise(mobj_t *actor);
-void A_BubbleCheck(mobj_t *actor);
-void A_AttractChase(mobj_t *actor);
-void A_DropMine(mobj_t *actor);
-void A_FishJump(mobj_t *actor);
-void A_ThrownRing(mobj_t *actor);
-void A_SetSolidSteam(mobj_t *actor);
-void A_UnsetSolidSteam(mobj_t *actor);
-void A_SignSpin(mobj_t *actor);
-void A_SignPlayer(mobj_t *actor);
-void A_OverlayThink(mobj_t *actor);
-void A_JetChase(mobj_t *actor);
-void A_JetbThink(mobj_t *actor);
-void A_JetgShoot(mobj_t *actor);
-void A_JetgThink(mobj_t *actor);
-void A_ShootBullet(mobj_t *actor);
-void A_MinusDigging(mobj_t *actor);
-void A_MinusPopup(mobj_t *actor);
-void A_MinusCheck(mobj_t *actor);
-void A_ChickenCheck(mobj_t *actor);
-void A_MouseThink(mobj_t *actor);
-void A_DetonChase(mobj_t *actor);
-void A_CapeChase(mobj_t *actor);
-void A_RotateSpikeBall(mobj_t *actor);
-void A_SlingAppear(mobj_t *actor);
-void A_UnidusBall(mobj_t *actor);
-void A_RockSpawn(mobj_t *actor);
-void A_SetFuse(mobj_t *actor);
-void A_CrawlaCommanderThink(mobj_t *actor);
-void A_RingExplode(mobj_t *actor);
-void A_OldRingExplode(mobj_t *actor);
-void A_MixUp(mobj_t *actor);
-void A_RecyclePowers(mobj_t *actor);
-void A_Boss2TakeDamage(mobj_t *actor);
-void A_Boss7Chase(mobj_t *actor);
-void A_GoopSplat(mobj_t *actor);
-void A_Boss2PogoSFX(mobj_t *actor);
-void A_Boss2PogoTarget(mobj_t *actor);
-void A_EggmanBox(mobj_t *actor);
-void A_TurretFire(mobj_t *actor);
-void A_SuperTurretFire(mobj_t *actor);
-void A_TurretStop(mobj_t *actor);
-void A_SparkFollow(mobj_t *actor);
-void A_BuzzFly(mobj_t *actor);
-void A_GuardChase(mobj_t *actor);
-void A_EggShield(mobj_t *actor);
-void A_SetReactionTime(mobj_t *actor);
-void A_Boss1Spikeballs(mobj_t *actor);
-void A_Boss3TakeDamage(mobj_t *actor);
-void A_Boss3Path(mobj_t *actor);
-void A_Boss3ShockThink(mobj_t *actor);
-void A_Shockwave(mobj_t *actor);
-void A_LinedefExecute(mobj_t *actor);
-void A_LinedefExecuteFromArg(mobj_t *actor);
-void A_PlaySeeSound(mobj_t *actor);
-void A_PlayAttackSound(mobj_t *actor);
-void A_PlayActiveSound(mobj_t *actor);
-void A_SmokeTrailer(mobj_t *actor);
-void A_SpawnObjectAbsolute(mobj_t *actor);
-void A_SpawnObjectRelative(mobj_t *actor);
-void A_ChangeAngleRelative(mobj_t *actor);
-void A_ChangeAngleAbsolute(mobj_t *actor);
-void A_RollAngle(mobj_t *actor);
-void A_ChangeRollAngleRelative(mobj_t *actor);
-void A_ChangeRollAngleAbsolute(mobj_t *actor);
-void A_PlaySound(mobj_t *actor);
-void A_FindTarget(mobj_t *actor);
-void A_FindTracer(mobj_t *actor);
-void A_SetTics(mobj_t *actor);
-void A_SetRandomTics(mobj_t *actor);
-void A_ChangeColorRelative(mobj_t *actor);
-void A_ChangeColorAbsolute(mobj_t *actor);
-void A_Dye(mobj_t *actor);
-void A_SetTranslation(mobj_t *actor);
-void A_MoveRelative(mobj_t *actor);
-void A_MoveAbsolute(mobj_t *actor);
-void A_Thrust(mobj_t *actor);
-void A_ZThrust(mobj_t *actor);
-void A_SetTargetsTarget(mobj_t *actor);
-void A_SetObjectFlags(mobj_t *actor);
-void A_SetObjectFlags2(mobj_t *actor);
-void A_RandomState(mobj_t *actor);
-void A_RandomStateRange(mobj_t *actor);
-void A_StateRangeByAngle(mobj_t *actor);
-void A_StateRangeByParameter(mobj_t *actor);
-void A_DualAction(mobj_t *actor);
-void A_RemoteAction(mobj_t *actor);
-void A_ToggleFlameJet(mobj_t *actor);
-void A_OrbitNights(mobj_t *actor);
-void A_GhostMe(mobj_t *actor);
-void A_SetObjectState(mobj_t *actor);
-void A_SetObjectTypeState(mobj_t *actor);
-void A_KnockBack(mobj_t *actor);
-void A_PushAway(mobj_t *actor);
-void A_RingDrain(mobj_t *actor);
-void A_SplitShot(mobj_t *actor);
-void A_MissileSplit(mobj_t *actor);
-void A_MultiShot(mobj_t *actor);
-void A_InstaLoop(mobj_t *actor);
-void A_Custom3DRotate(mobj_t *actor);
-void A_SearchForPlayers(mobj_t *actor);
-void A_CheckRandom(mobj_t *actor);
-void A_CheckTargetRings(mobj_t *actor);
-void A_CheckRings(mobj_t *actor);
-void A_CheckTotalRings(mobj_t *actor);
-void A_CheckHealth(mobj_t *actor);
-void A_CheckRange(mobj_t *actor);
-void A_CheckHeight(mobj_t *actor);
-void A_CheckTrueRange(mobj_t *actor);
-void A_CheckThingCount(mobj_t *actor);
-void A_CheckAmbush(mobj_t *actor);
-void A_CheckCustomValue(mobj_t *actor);
-void A_CheckCusValMemo(mobj_t *actor);
-void A_SetCustomValue(mobj_t *actor);
-void A_UseCusValMemo(mobj_t *actor);
-void A_RelayCustomValue(mobj_t *actor);
-void A_CusValAction(mobj_t *actor);
-void A_ForceStop(mobj_t *actor);
-void A_ForceWin(mobj_t *actor);
-void A_SpikeRetract(mobj_t *actor);
-void A_InfoState(mobj_t *actor);
-void A_Repeat(mobj_t *actor);
-void A_SetScale(mobj_t *actor);
-void A_RemoteDamage(mobj_t *actor);
-void A_HomingChase(mobj_t *actor);
-void A_TrapShot(mobj_t *actor);
-void A_Boss1Chase(mobj_t *actor);
-void A_Boss2Chase(mobj_t *actor);
-void A_Boss2Pogo(mobj_t *actor);
-void A_BossJetFume(mobj_t *actor);
-void A_VileTarget(mobj_t *actor);
-void A_VileAttack(mobj_t *actor);
-void A_VileFire(mobj_t *actor);
-void A_BrakChase(mobj_t *actor);
-void A_BrakFireShot(mobj_t *actor);
-void A_BrakLobShot(mobj_t *actor);
-void A_NapalmScatter(mobj_t *actor);
-void A_SpawnFreshCopy(mobj_t *actor);
-void A_FlickySpawn(mobj_t *actor);
-void A_FlickyCenter(mobj_t *actor);
-void A_FlickyAim(mobj_t *actor);
-void A_FlickyFly(mobj_t *actor);
-void A_FlickySoar(mobj_t *actor);
-void A_FlickyCoast(mobj_t *actor);
-void A_FlickyHop(mobj_t *actor);
-void A_FlickyFlounder(mobj_t *actor);
-void A_FlickyCheck(mobj_t *actor);
-void A_FlickyHeightCheck(mobj_t *actor);
-void A_FlickyFlutter(mobj_t *actor);
-void A_FlameParticle(mobj_t *actor);
-void A_FadeOverlay(mobj_t *actor);
-void A_Boss5Jump(mobj_t *actor);
-void A_LightBeamReset(mobj_t *actor);
-void A_MineExplode(mobj_t *actor);
-void A_MineRange(mobj_t *actor);
-void A_ConnectToGround(mobj_t *actor);
-void A_SpawnParticleRelative(mobj_t *actor);
-void A_MultiShotDist(mobj_t *actor);
-void A_WhoCaresIfYourSonIsABee(mobj_t *actor);
-void A_ParentTriesToSleep(mobj_t *actor);
-void A_CryingToMomma(mobj_t *actor);
-void A_CheckFlags2(mobj_t *actor);
-void A_Boss5FindWaypoint(mobj_t *actor);
-void A_DoNPCSkid(mobj_t *actor);
-void A_DoNPCPain(mobj_t *actor);
-void A_PrepareRepeat(mobj_t *actor);
-void A_Boss5ExtraRepeat(mobj_t *actor);
-void A_Boss5Calm(mobj_t *actor);
-void A_Boss5CheckOnGround(mobj_t *actor);
-void A_Boss5CheckFalling(mobj_t *actor);
-void A_Boss5PinchShot(mobj_t *actor);
-void A_Boss5MakeItRain(mobj_t *actor);
-void A_Boss5MakeJunk(mobj_t *actor);
-void A_LookForBetter(mobj_t *actor);
-void A_Boss5BombExplode(mobj_t *actor);
-void A_DustDevilThink(mobj_t *actor);
-void A_TNTExplode(mobj_t *actor);
-void A_DebrisRandom(mobj_t *actor);
-void A_TrainCameo(mobj_t *actor);
-void A_TrainCameo2(mobj_t *actor);
-void A_CanarivoreGas(mobj_t *actor);
-void A_KillSegments(mobj_t *actor);
-void A_SnapperSpawn(mobj_t *actor);
-void A_SnapperThinker(mobj_t *actor);
-void A_SaloonDoorSpawn(mobj_t *actor);
-void A_MinecartSparkThink(mobj_t *actor);
-void A_ModuloToState(mobj_t *actor);
-void A_LavafallRocks(mobj_t *actor);
-void A_LavafallLava(mobj_t *actor);
-void A_FallingLavaCheck(mobj_t *actor);
-void A_FireShrink(mobj_t *actor);
-void A_SpawnPterabytes(mobj_t *actor);
-void A_PterabyteHover(mobj_t *actor);
-void A_RolloutSpawn(mobj_t *actor);
-void A_RolloutRock(mobj_t *actor);
-void A_DragonbomberSpawn(mobj_t *actor);
-void A_DragonWing(mobj_t *actor);
-void A_DragonSegment(mobj_t *actor);
-void A_ChangeHeight(mobj_t *actor);
-
 //for p_enemy.c
 
 //
@@ -4014,7 +3744,7 @@ static void P_DoBossVictory(mobj_t *mo)
 	// scan the remaining thinkers to see if all bosses are dead
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -4865,7 +4595,7 @@ void A_AttractChase(mobj_t *actor)
 	else
 		actor->flags2 &= ~MF2_DONTDRAW;
 
-	// Turn flingrings back into regular rings if attracted.
+	// Turn rings into flingrings if shield is lost or out of range
 	if (actor->tracer && actor->tracer->player
 		&& !(actor->tracer->player->powers[pw_shield] & SH_PROTECTELECTRIC) && actor->info->reactiontime && actor->type != (mobjtype_t)actor->info->reactiontime)
 	{
@@ -4897,8 +4627,9 @@ void A_AttractChase(mobj_t *actor)
 	// If a FlingRing gets attracted by a shield, change it into a normal ring.
 	if (actor->type == (mobjtype_t)actor->info->reactiontime)
 	{
-		P_SpawnMobj(actor->x, actor->y, actor->z, actor->info->painchance);
-		P_RemoveMobj(actor);
+		actor->type = mobjinfo[actor->type].painchance; // Become the regular version of the fling object.
+		actor->flags = mobjinfo[actor->type].flags;		// Reset actor flags.
+		P_SetMobjState(actor, actor->info->spawnstate); // Go to regular object's spawn state.
 		return;
 	}
 
@@ -5851,7 +5582,7 @@ void A_MinusDigging(mobj_t *actor)
 
 		minus = actor;
 
-		P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_MinusCarry);
+		P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_MinusCarry, minus);
 	}
 	else
 	{
@@ -6718,7 +6449,7 @@ void A_RingExplode(mobj_t *actor)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -9025,7 +8756,7 @@ void A_FindTarget(mobj_t *actor)
 	// scan the thinkers
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -9089,7 +8820,7 @@ void A_FindTracer(mobj_t *actor)
 	// scan the thinkers
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -9767,7 +9498,7 @@ void A_RemoteAction(mobj_t *actor)
 		// scan the thinkers
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo2 = (mobj_t *)th;
@@ -10030,7 +9761,7 @@ void A_SetObjectTypeState(mobj_t *actor)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -10660,7 +10391,7 @@ void A_CheckThingCount(mobj_t *actor)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -13995,7 +13726,7 @@ void A_DustDevilThink(mobj_t *actor)
 
 	dustdevil = actor;
 
-	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_DustDevilLaunch);
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_DustDevilLaunch, dustdevil);
 
 	//Whirlwind sound effect.
 	if (leveltime % 70 == 0)
@@ -14111,7 +13842,7 @@ void A_TNTExplode(mobj_t *actor)
 
 	barrel = actor;
 
-	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_TNTExplode);
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_TNTExplode, barrel);
 
 	// cause a quake -- P_StartQuake does not exist yet
 	epicenter.x = actor->x;
@@ -14664,7 +14395,7 @@ void A_LavafallLava(mobj_t *actor)
 	if (LUA_CallAction(A_LAVAFALLLAVA, actor))
 		return;
 
-	if ((40 - actor->fuse) % (2*(actor->scale >> FRACBITS)))
+	if ((40 - actor->fuse) % max(2*(actor->scale >> FRACBITS), 1)) // avoid crashes if actor->scale < FRACUNIT
 		return;
 
 	// Don't spawn lava unless a player is nearby.
@@ -14857,12 +14588,18 @@ void A_RolloutRock(mobj_t *actor)
 
 	if (!actor->tracer || P_MobjWasRemoved(actor->tracer) || !actor->tracer->health)
 		actor->flags |= MF_PUSHABLE;
+	else if (actor->tracer->eflags & MFE_VERTICALFLIP)
+	{
+		actor->flags2 |= MF2_OBJECTFLIP;
+		actor->eflags |= MFE_VERTICALFLIP;
+	}
 	else
 	{
-		actor->flags2 = (actor->flags2 & ~MF2_OBJECTFLIP) | (actor->tracer->flags2 & MF2_OBJECTFLIP);
-		actor->eflags = (actor->eflags & ~MFE_VERTICALFLIP) | (actor->tracer->eflags & MFE_VERTICALFLIP);
+		actor->flags2 &= ~MF2_OBJECTFLIP;
+		actor->eflags &= ~MFE_VERTICALFLIP;
 	}
 
+
 	actor->friction = FRACUNIT; // turns out riding on solids sucks, so let's just make it easier on ourselves
 
 	if (actor->eflags & MFE_JUSTHITFLOOR)
diff --git a/src/p_inter.c b/src/p_inter.c
index 6596ec48ac5ab212340654372511d81f039c5718..c9d0d39647914cc16c2c2a893e9e8a0e1c980812 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -101,7 +101,7 @@ void P_ClearStarPost(INT32 postnum)
 	// scan the thinkers
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -130,7 +130,7 @@ void P_ResetStarposts(void)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		post = (mobj_t *)th;
@@ -836,7 +836,8 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				{
 					clientGamedata->collected[special->health-1] = true;
 					M_UpdateUnlockablesAndExtraEmblems(clientGamedata);
-					G_SaveGameData(clientGamedata);
+					if (!prevCollected) // don't thrash the disk and wreak performance.
+						G_SaveGameData(clientGamedata);
 				}
 
 				if (netgame)
@@ -1002,7 +1003,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 						// scan the thinkers to find the corresponding anchorpoint
 						for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 						{
-							if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+							if (th->removing)
 								continue;
 
 							mo2 = (mobj_t *)th;
@@ -1096,7 +1097,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				// scan the remaining thinkers
 				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 				{
-					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+					if (th->removing)
 						continue;
 
 					mo2 = (mobj_t *)th;
@@ -1146,7 +1147,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				// in from the paraloop. Isn't this just so efficient?
 				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 				{
-					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+					if (th->removing)
 						continue;
 
 					mo2 = (mobj_t *)th;
@@ -1365,7 +1366,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSEXTRATIME:
-			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE || (G_IsSpecialStage(gamemap) && !(maptol & TOL_NIGHTS))))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 			{
@@ -1377,7 +1378,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			else
 			{
 				for (i = 0; i < MAXPLAYERS; i++)
-					if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
+					if (playeringame[i] && (player->powers[pw_carry] == CR_NIGHTSMODE || (G_IsSpecialStage(gamemap) && !(maptol & TOL_NIGHTS))))
 					{
 						players[i].nightstime += special->info->speed;
 						players[i].startedtime += special->info->speed;
@@ -1521,7 +1522,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				// scan the remaining thinkers to find koopa
 				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 				{
-					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+					if (th->removing)
 						continue;
 
 					mo2 = (mobj_t *)th;
@@ -2019,7 +2020,7 @@ void P_TouchStarPost(mobj_t *post, player_t *player, boolean snaptopost)
 
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo2 = (mobj_t *)th;
@@ -2869,7 +2870,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 				// scan the thinkers to make sure all the old pinch dummies are gone on death
 				for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 				{
-					if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+					if (th->removing)
 						continue;
 
 					mo = (mobj_t *)th;
@@ -3225,8 +3226,8 @@ static boolean P_TagDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, IN
 		return false;
 
 	// Ignore IT players shooting each other, unless friendlyfire is on.
-	if ((player->pflags & PF_TAGIT && !((cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE) || (damagetype & DMG_CANHURTSELF)) &&
-		source && source->player && source->player->pflags & PF_TAGIT)))
+	if ((player->pflags & PF_TAGIT && source && source->player && !(((cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)) || ((damagetype & DMG_CANHURTSELF) && source->player == player)) &&
+		source->player->pflags & PF_TAGIT)))
 	{
 		if (inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK))
 		{
@@ -3241,7 +3242,8 @@ static boolean P_TagDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, IN
 
 	// Don't allow players on the same team to hurt one another,
 	// unless cv_friendlyfire is on.
-	if (!(cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE) || (damagetype & DMG_CANHURTSELF)) && (player->pflags & PF_TAGIT) == (source->player->pflags & PF_TAGIT))
+	if (source && source->player && !((cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)) || ((damagetype & DMG_CANHURTSELF) && source->player == player)) && 
+		(player->pflags & PF_TAGIT) == (source->player->pflags & PF_TAGIT))
 	{
 		if (inflictor->type == MT_LHRT && !(player->powers[pw_shield] & SH_NOSTACK))
 		{
@@ -3343,7 +3345,7 @@ static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *sou
 	// Tag handling
 	if (G_TagGametype())
 		return P_TagDamage(target, inflictor, source, damage, damagetype);
-	else if (damagetype & DMG_CANHURTSELF)
+	else if ((damagetype & DMG_CANHURTSELF) && source && source->player && source->player == player)
 		return true;
 	else if (G_GametypeHasTeams()) // CTF + Team Match
 	{
@@ -3412,18 +3414,20 @@ static void P_KillPlayer(player_t *player, mobj_t *source, INT32 damage)
 	if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
 	{
 		P_PlayerFlagBurst(player, false);
-		if (source && source->player)
+		if (source && source->player && source->player != player) // Don't score points against yourself
 		{
 			// Award no points when players shoot each other when cv_friendlyfire is on.
 			if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
 				P_AddPlayerScore(source->player, 25);
 		}
 	}
-	if (source && source->player && !player->powers[pw_super]) //don't score points against super players
+	if (source && source->player && source->player != player && !player->powers[pw_super]) //don't score points against super players or yourself
 	{
 		// Award no points when players shoot each other when cv_friendlyfire is on.
 		if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
+		{
 			P_AddPlayerScore(source->player, 100);
+		}
 	}
 
 	// If the player was super, tell them he/she ain't so super nomore.
@@ -3537,14 +3541,14 @@ static void P_ShieldDamage(player_t *player, mobj_t *inflictor, mobj_t *source,
 	if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
 	{
 		P_PlayerFlagBurst(player, false);
-		if (source && source->player)
+		if (source && source->player && source->player != player) // Don't score points against yourself
 		{
 			// Award no points when players shoot each other when cv_friendlyfire is on.
 			if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
 				P_AddPlayerScore(source->player, 25);
 		}
 	}
-	if (source && source->player && !player->powers[pw_super]) //don't score points against super players
+	if (source && source->player && source->player != player && !player->powers[pw_super]) //don't score points against super players or yourself
 	{
 		// Award no points when players shoot each other when cv_friendlyfire is on.
 		if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
@@ -3561,7 +3565,7 @@ static void P_RingDamage(player_t *player, mobj_t *inflictor, mobj_t *source, IN
 	if (damagetype == DMG_SPIKE) // spikes
 		S_StartSound(player->mo, sfx_spkdth);
 
-	if (source && source->player && !player->powers[pw_super]) //don't score points against super players
+	if (source && source->player && source->player != player && !player->powers[pw_super]) //don't score points against super players
 	{
 		// Award no points when players shoot each other when cv_friendlyfire is on.
 		if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
@@ -3571,7 +3575,7 @@ static void P_RingDamage(player_t *player, mobj_t *inflictor, mobj_t *source, IN
 	if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
 	{
 		P_PlayerFlagBurst(player, false);
-		if (source && source->player)
+		if (source && source->player && source->player != player) // Don't score points against yourself
 		{
 			// Award no points when players shoot each other when cv_friendlyfire is on.
 			if (!G_GametypeHasTeams() || !(source->player->ctfteam == player->ctfteam && source != player->mo))
@@ -3640,7 +3644,7 @@ void P_SpecialStageDamage(player_t *player, mobj_t *inflictor, mobj_t *source)
 		if (player->nightstime > 5*TICRATE)
 			player->nightstime -= 5*TICRATE;
 		else
-			player->nightstime = 0;
+			player->nightstime = 1;
 	}
 
 	P_DoPlayerPain(player, inflictor, source);
@@ -3786,6 +3790,8 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 
 		if (player->powers[pw_carry] == CR_NIGHTSMODE) // NiGHTS damage handling
 		{
+			if (player->powers[pw_flashing])
+				return false;
 			if (!force)
 			{
 				if (source == target)
@@ -3803,6 +3809,10 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 
 		if (G_IsSpecialStage(gamemap) && !(damagetype & DMG_DEATHMASK))
 		{
+			if (player->powers[pw_flashing])
+				return false;
+			if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
+				return true;
 			P_SpecialStageDamage(player, inflictor, source);
 			return true;
 		}
@@ -3878,7 +3888,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 		// To reduce griefing potential, don't allow players to be killed
 		// by friendly fire. Spilling their rings and other items is enough.
 		else if (!force && G_GametypeHasTeams()
-			&& source && source->player && (source->player->ctfteam == player->ctfteam)
+			&& source && source->player && source->player != player && (source->player->ctfteam == player->ctfteam)
 			&& (cv_friendlyfire.value || (gametyperules & GTR_FRIENDLYFIRE)))
 		{
 			damage = 0;
diff --git a/src/p_local.h b/src/p_local.h
index 0cb47362b11371629eddb7ed2fc0e05baf0ed648..814f5b1a3d6a618c2c05c69a0855ef5e444a7aa1 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -39,11 +39,6 @@
 // Convenience macro to fix issue with collision along bottom/left edges of blockmap -Red
 #define BMBOUNDFIX(xl, xh, yl, yh) {if (xl > xh) xl = 0; if (yl > yh) yl = 0;}
 
-// MAXRADIUS is for precalculated sector block boxes
-// the spider demon is larger,
-// but we do not have any moving sectors nearby
-#define MAXRADIUS (32*FRACUNIT)
-
 // max Z move up or down without jumping
 // above this, a height difference is considered as a 'dropoff'
 #define MAXSTEPMOVE (24*FRACUNIT)
@@ -207,7 +202,7 @@ mobj_t *P_LookForEnemies(player_t *player, boolean nonenemies, boolean bullet);
 void P_NukeEnemies(mobj_t *inflictor, mobj_t *source, fixed_t radius);
 void P_Earthquake(mobj_t *inflictor, mobj_t *source, fixed_t radius);
 boolean P_HomingAttack(mobj_t *source, mobj_t *enemy); /// \todo doesn't belong in p_user
-boolean P_SuperReady(player_t *player, boolean transform);
+boolean P_SuperReady(player_t *player);
 void P_DoJump(player_t *player, boolean soundandstate, boolean allowflip);
 void P_DoSpinDashDust(player_t *player);
 #define P_AnalogMove(player) (P_ControlStyle(player) == CS_LMAOGALOG)
@@ -298,7 +293,6 @@ void P_RecalcPrecipInSector(sector_t *sector);
 void P_PrecipitationEffects(void);
 
 void P_RemoveMobj(mobj_t *th);
-boolean P_MobjWasRemoved(mobj_t *th);
 void P_RemoveSavegameMobj(mobj_t *th);
 boolean P_SetMobjState(mobj_t *mobj, statenum_t state);
 void P_RunShields(void);
@@ -310,6 +304,12 @@ boolean P_CheckSkyHit(mobj_t *mo, line_t *line);
 void P_PushableThinker(mobj_t *mobj);
 void P_SceneryThinker(mobj_t *mobj);
 
+// This does not need to be added to Lua.
+// To test it in Lua, check mobj.valid
+FUNCINLINE static ATTRINLINE boolean P_MobjWasRemoved(mobj_t *mobj)
+{
+	return mobj == NULL || mobj->thinker.function.acp1 != (actionf_p1)P_MobjThinker;
+}
 
 fixed_t P_MobjFloorZ(sector_t *sector, sector_t *boundsec, fixed_t x, fixed_t y, fixed_t radius, line_t *line, boolean lowest, boolean perfect);
 fixed_t P_MobjCeilingZ(sector_t *sector, sector_t *boundsec, fixed_t x, fixed_t y, fixed_t radius, line_t *line, boolean lowest, boolean perfect);
diff --git a/src/p_map.c b/src/p_map.c
index e7828c351bfe7b6837c5cc488df997dcfd24b2bc..4a4dffc695ac97f3c70ebb68e22d1bea7b238409 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -36,6 +36,9 @@
 
 #include "m_perfstats.h" // ps_checkposition_calls
 
+// Formerly called MAXRADIUS
+#define MAXTRYMOVE (32*FRACUNIT)
+
 fixed_t tmbbox[4];
 mobj_t *tmthing;
 static INT32 tmflags;
@@ -389,7 +392,6 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 	{
 		INT32 pflags;
 		UINT8 secondjump;
-		boolean washoming;
 
 		if (spring->flags & MF_ENEMY) // Spring shells
 			P_SetTarget(&spring->target, object);
@@ -421,7 +423,7 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 		{
 			boolean wasSpindashing = object->player->dashspeed > 0 && (object->player->charability2 == CA2_SPINDASH);
 
-			pflags = object->player->pflags & (PF_STARTJUMP | PF_JUMPED | PF_NOJUMPDAMAGE | PF_SPINNING | PF_THOKKED | PF_BOUNCING); // I still need these.
+			pflags = object->player->pflags & (PF_STARTJUMP | PF_JUMPED | PF_NOJUMPDAMAGE | PF_SPINNING | PF_BOUNCING); // I still need these.
 
 			if (wasSpindashing) // Ensure we're in the rolling state, and not spindash.
 				P_SetMobjState(object, S_PLAY_ROLL);
@@ -433,7 +435,6 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 			}
 		}
 		secondjump = object->player->secondjump;
-		washoming = object->player->homing;
 		P_ResetPlayer(object->player);
 
 		if (spring->info->painchance == 1) // For all those ancient, SOC'd abilities.
@@ -445,8 +446,6 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 		{
 			object->player->pflags |= (pflags &~ PF_STARTJUMP);
 			object->player->secondjump = secondjump;
-			if (washoming)
-				object->player->pflags &= ~PF_THOKKED;
 		}
 		else if (!vertispeed)
 		{
@@ -1027,7 +1026,6 @@ static unsigned PIT_DoCheckThing(mobj_t *thing)
 		if ((thing->flags & MF_PUSHABLE) // not carrying a player
 			&& (tmthing->player->powers[pw_carry] == CR_NONE) // player is not already riding something
 			&& !(tmthing->player->powers[pw_ignorelatch] & (1<<15))
-			&& ((tmthing->eflags & MFE_VERTICALFLIP) == (thing->eflags & MFE_VERTICALFLIP))
 			&& (P_MobjFlip(tmthing)*tmthing->momz <= 0)
 			&& ((!(tmthing->eflags & MFE_VERTICALFLIP) && abs(thing->z + thing->height - tmthing->z) < (thing->height>>2))
 				|| (tmthing->eflags & MFE_VERTICALFLIP && abs(tmthing->z + tmthing->height - thing->z) < (thing->height>>2))))
@@ -1041,6 +1039,7 @@ static unsigned PIT_DoCheckThing(mobj_t *thing)
 			P_SetTarget(&tmthing->tracer, thing);
 			if (!P_IsObjectOnGround(thing))
 				thing->momz += tmthing->momz;
+
 			return CHECKTHING_COLLIDE;
 		}
 	}
@@ -1262,8 +1261,9 @@ static unsigned PIT_DoCheckThing(mobj_t *thing)
 
 		if (tmthing->type != MT_SHELL && tmthing->target && tmthing->target->type == thing->type)
 		{
-			// Don't hit same species as originator.
-			if (thing == tmthing->target)
+			// Don't hit yourself, and if a player, don't hit bots
+			if (thing == tmthing->target
+				|| (thing->player && tmthing->target->player && (thing->player->bot == BOT_2PAI || thing->player->bot == BOT_2PHUMAN)))
 				return CHECKTHING_IGNORE;
 
 			if (thing->type != MT_PLAYER)
@@ -1468,13 +1468,13 @@ static unsigned PIT_DoCheckThing(mobj_t *thing)
 	}
 
 	// check for special pickup
-	if (thing->flags & MF_SPECIAL)
+	if (thing->flags & MF_SPECIAL && (tmthing->player || (tmthing->flags & MF_PUSHABLE))) // MF_PUSHABLE added for steam jets
 	{
 		P_TouchSpecialThing(thing, tmthing, true); // can remove thing
 		return CHECKTHING_COLLIDE;
 	}
 	// check again for special pickup
-	if (tmthing->flags & MF_SPECIAL)
+	if (tmthing->flags & MF_SPECIAL && (thing->player || (thing->flags & MF_PUSHABLE))) // MF_PUSHABLE added for steam jets
 	{
 		P_TouchSpecialThing(tmthing, thing, true); // can remove thing
 		return CHECKTHING_COLLIDE;
@@ -2168,15 +2168,10 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 		}
 	}
 
-	// The bounding box is extended by MAXRADIUS
-	// because mobj_ts are grouped into mapblocks
-	// based on their origin point, and can overlap
-	// into adjacent blocks by up to MAXRADIUS units.
-
-	xl = (unsigned)(tmbbox[BOXLEFT] - bmaporgx - MAXRADIUS)>>MAPBLOCKSHIFT;
-	xh = (unsigned)(tmbbox[BOXRIGHT] - bmaporgx + MAXRADIUS)>>MAPBLOCKSHIFT;
-	yl = (unsigned)(tmbbox[BOXBOTTOM] - bmaporgy - MAXRADIUS)>>MAPBLOCKSHIFT;
-	yh = (unsigned)(tmbbox[BOXTOP] - bmaporgy + MAXRADIUS)>>MAPBLOCKSHIFT;
+	xl = (unsigned)(tmbbox[BOXLEFT] - bmaporgx)>>MAPBLOCKSHIFT;
+	xh = (unsigned)(tmbbox[BOXRIGHT] - bmaporgx)>>MAPBLOCKSHIFT;
+	yl = (unsigned)(tmbbox[BOXBOTTOM] - bmaporgy)>>MAPBLOCKSHIFT;
+	yh = (unsigned)(tmbbox[BOXTOP] - bmaporgy)>>MAPBLOCKSHIFT;
 
 	BMBOUNDFIX(xl, xh, yl, yh);
 
@@ -2267,7 +2262,7 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 		for (bx = xl; bx <= xh; bx++)
 			for (by = yl; by <= yh; by++)
 			{
-				if (!P_BlockThingsIterator(bx, by, PIT_CheckThing))
+				if (!P_BlockThingsIterator(bx, by, PIT_CheckThing, tmthing))
 					blockval = false;
 				else
 					tmhitthing = tmfloorthing;
@@ -2396,11 +2391,6 @@ boolean P_CheckCameraPosition(fixed_t x, fixed_t y, camera_t *thiscam)
 		}
 	}
 
-	// The bounding box is extended by MAXRADIUS
-	// because mobj_ts are grouped into mapblocks
-	// based on their origin point, and can overlap
-	// into adjacent blocks by up to MAXRADIUS units.
-
 	xl = (unsigned)(tmbbox[BOXLEFT] - bmaporgx)>>MAPBLOCKSHIFT;
 	xh = (unsigned)(tmbbox[BOXRIGHT] - bmaporgx)>>MAPBLOCKSHIFT;
 	yl = (unsigned)(tmbbox[BOXBOTTOM] - bmaporgy)>>MAPBLOCKSHIFT;
@@ -2505,6 +2495,9 @@ boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam)
 
 	floatok = false;
 
+	if (dedicated) // this crashes so don't even try it
+		return false;
+
 	if (twodlevel
 		|| (thiscam == &camera && players[displayplayer].mo && (players[displayplayer].mo->flags2 & MF2_TWOD))
 		|| (thiscam == &camera2 && players[secondarydisplayplayer].mo && (players[secondarydisplayplayer].mo->flags2 & MF2_TWOD)))
@@ -2528,16 +2521,16 @@ boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam)
 		}
 
 		do {
-			if (x-tryx > MAXRADIUS)
-				tryx += MAXRADIUS;
-			else if (x-tryx < -MAXRADIUS)
-				tryx -= MAXRADIUS;
+			if (x-tryx > MAXTRYMOVE)
+				tryx += MAXTRYMOVE;
+			else if (x-tryx < -MAXTRYMOVE)
+				tryx -= MAXTRYMOVE;
 			else
 				tryx = x;
-			if (y-tryy > MAXRADIUS)
-				tryy += MAXRADIUS;
-			else if (y-tryy < -MAXRADIUS)
-				tryy -= MAXRADIUS;
+			if (y-tryy > MAXTRYMOVE)
+				tryy += MAXTRYMOVE;
+			else if (y-tryy < -MAXTRYMOVE)
+				tryy -= MAXTRYMOVE;
 			else
 				tryy = y;
 
@@ -2683,7 +2676,7 @@ increment_move
 	floatok = false;
 
 	// This makes sure that there are no freezes from computing extremely small movements.
-	// Originally was MAXRADIUS/2, but that can cause some bad inconsistencies for small players.
+	// Originally was MAXTRYMOVE/2, but that can cause some bad inconsistencies for small players.
 	radius = max(radius, thing->scale);
 
 	// And we also have to prevent Big Large (tm) movements, as those can skip too far
@@ -2872,10 +2865,10 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 	{
 		INT32 xl, xh, yl, yh;
 
-		yh = (unsigned)(thing->y + MAXRADIUS - bmaporgy)>>MAPBLOCKSHIFT;
-		yl = (unsigned)(thing->y - MAXRADIUS - bmaporgy)>>MAPBLOCKSHIFT;
-		xh = (unsigned)(thing->x + MAXRADIUS - bmaporgx)>>MAPBLOCKSHIFT;
-		xl = (unsigned)(thing->x - MAXRADIUS - bmaporgx)>>MAPBLOCKSHIFT;
+		yh = (unsigned)(thing->y + thing->radius - bmaporgy)>>MAPBLOCKSHIFT;
+		yl = (unsigned)(thing->y - thing->radius - bmaporgy)>>MAPBLOCKSHIFT;
+		xh = (unsigned)(thing->x + thing->radius - bmaporgx)>>MAPBLOCKSHIFT;
+		xl = (unsigned)(thing->x - thing->radius - bmaporgx)>>MAPBLOCKSHIFT;
 
 		BMBOUNDFIX(xl, xh, yl, yh);
 
@@ -2883,7 +2876,7 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 		standx = x;
 		standy = y;
 
-		P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_PushableMoved);
+		P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_PushableMoved, stand);
 	}
 
 	// Link the thing into its new position
@@ -2947,16 +2940,16 @@ boolean P_SceneryTryMove(mobj_t *thing, fixed_t x, fixed_t y)
 	tryx = thing->x;
 	tryy = thing->y;
 	do {
-		if (x-tryx > MAXRADIUS)
-			tryx += MAXRADIUS;
-		else if (x-tryx < -MAXRADIUS)
-			tryx -= MAXRADIUS;
+		if (x-tryx > MAXTRYMOVE)
+			tryx += MAXTRYMOVE;
+		else if (x-tryx < -MAXTRYMOVE)
+			tryx -= MAXTRYMOVE;
 		else
 			tryx = x;
-		if (y-tryy > MAXRADIUS)
-			tryy += MAXRADIUS;
-		else if (y-tryy < -MAXRADIUS)
-			tryy -= MAXRADIUS;
+		if (y-tryy > MAXTRYMOVE)
+			tryy += MAXTRYMOVE;
+		else if (y-tryy < -MAXTRYMOVE)
+			tryy -= MAXTRYMOVE;
 		else
 			tryy = y;
 
@@ -3964,23 +3957,25 @@ papercollision:
 		mo->momy = tmymove;
 	}
 
+	const fixed_t tmradius = mo->radius > 8 ? mo->radius : 8;
+
 	do {
-		if (tmxmove > mo->radius) {
-			newx = mo->x + mo->radius;
-			tmxmove -= mo->radius;
-		} else if (tmxmove < -mo->radius) {
-			newx = mo->x - mo->radius;
-			tmxmove += mo->radius;
+		if (tmxmove > tmradius) {
+			newx = mo->x + tmradius;
+			tmxmove -= tmradius;
+		} else if (tmxmove < -tmradius) {
+			newx = mo->x - tmradius;
+			tmxmove += tmradius;
 		} else {
 			newx = mo->x + tmxmove;
 			tmxmove = 0;
 		}
-		if (tmymove > mo->radius) {
-			newy = mo->y + mo->radius;
-			tmymove -= mo->radius;
-		} else if (tmymove < -mo->radius) {
-			newy = mo->y - mo->radius;
-			tmymove += mo->radius;
+		if (tmymove > tmradius) {
+			newy = mo->y + tmradius;
+			tmymove -= tmradius;
+		} else if (tmymove < -tmradius) {
+			newy = mo->y - tmradius;
+			tmymove += tmradius;
 		} else {
 			newy = mo->y + tmymove;
 			tmymove = 0;
@@ -4213,7 +4208,8 @@ void P_RadiusAttack(mobj_t *spot, mobj_t *source, fixed_t damagedist, UINT8 dama
 	INT32 xl, xh, yl, yh;
 	fixed_t dist;
 
-	dist = FixedMul(damagedist, spot->scale) + MAXRADIUS;
+	dist = FixedMul(damagedist, spot->scale);
+
 	yh = (unsigned)(spot->y + dist - bmaporgy)>>MAPBLOCKSHIFT;
 	yl = (unsigned)(spot->y - dist - bmaporgy)>>MAPBLOCKSHIFT;
 	xh = (unsigned)(spot->x + dist - bmaporgx)>>MAPBLOCKSHIFT;
@@ -4227,7 +4223,7 @@ void P_RadiusAttack(mobj_t *spot, mobj_t *source, fixed_t damagedist, UINT8 dama
 	bombdamagetype = damagetype;
 	bombsightcheck = sightcheck;
 
-	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_RadiusAttack);
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_RadiusAttack, bombspot);
 }
 
 //
@@ -4383,15 +4379,15 @@ static boolean P_CheckSectorPolyObjects(sector_t *sector, boolean realcrush, boo
 			{
 				mobj_t *mo;
 				blocknode_t *block;
+				blocknode_t *next = NULL;
 
 				if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 					continue;
 
-				block = blocklinks[y * bmapwidth + x];
-
-				for (; block; block = block->mnext)
+				for (block = blocklinks[y * bmapwidth + x]; block != NULL; block = next)
 				{
 					mo = block->mobj;
+					next = block->mnext;
 
 					// Monster Iestyn: do we need to check if a mobj has already been checked? ...probably not I suspect
 					if (!P_MobjInsidePolyobj(po, mo))
diff --git a/src/p_maputl.c b/src/p_maputl.c
index 82b86471522ee3b2cbeebbedfb1e28c1c4438501..de239056a51524813b4466519035c631f3975a02 100644
--- a/src/p_maputl.c
+++ b/src/p_maputl.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -500,8 +500,24 @@ void P_LineOpening(line_t *linedef, mobj_t *mobj)
 			INT32 texnum = R_GetTextureNum(side->midtexture); // make sure the texture is actually valid
 
 			if (texnum) {
+				fixed_t scaley = abs(side->scaley_mid);
+				fixed_t offsetvalue = FixedDiv(side->rowoffset + side->offsety_mid, scaley);
+				fixed_t midopentop, midopenbottom;
+
+				if (linedef->flags & ML_NOSKEW)
+				{
+					// Use the sector's actual heights if the midtexture is not skewed
+					midopentop = min(front->ceilingheight, back->ceilingheight);
+					midopenbottom = max(front->floorheight, back->floorheight);
+				}
+				else
+				{
+					midopentop = opentop;
+					midopenbottom = openbottom;
+				}
+
 				// Get the midtexture's height
-				texheight = textures[texnum]->height << FRACBITS;
+				texheight = FixedDiv(textureheight[texnum], scaley);
 
 				// Set texbottom and textop to the Z coordinates of the texture's boundaries
 #if 0
@@ -509,26 +525,26 @@ void P_LineOpening(line_t *linedef, mobj_t *mobj)
 				// on non-solid polyobjects should NEVER happen in the future
 				if (linedef->polyobj && (linedef->polyobj->flags & POF_TESTHEIGHT)) {
 					if (linedef->flags & ML_WRAPMIDTEX && !side->repeatcnt) { // "infinite" repeat
-						texbottom = back->floorheight + side->rowoffset + side->offsety_mid;
-						textop = back->ceilingheight + side->rowoffset + side->offsety_mid;
+						texbottom = back->floorheight + offsetvalue;
+						textop = back->ceilingheight + offsetvalue;
 					} else if (linedef->flags & ML_MIDTEX) {
-						texbottom = back->floorheight + side->rowoffset + side->offsety_mid;
+						texbottom = back->floorheight + offsetvalue;
 						textop = texbottom + texheight*(side->repeatcnt+1);
 					} else {
-						textop = back->ceilingheight + side->rowoffset + side->offsety_mid;
+						textop = back->ceilingheight + offsetvalue;
 						texbottom = textop - texheight*(side->repeatcnt+1);
 					}
 				} else
 #endif
 				{
 					if (linedef->flags & ML_WRAPMIDTEX && !side->repeatcnt) { // "infinite" repeat
-						texbottom = openbottom + side->rowoffset + side->offsety_mid;
-						textop = opentop + side->rowoffset + side->offsety_mid;
+						texbottom = midopenbottom + offsetvalue;
+						textop = midopentop + offsetvalue;
 					} else if (linedef->flags & ML_MIDPEG) {
-						texbottom = openbottom + side->rowoffset + side->offsety_mid;
+						texbottom = midopenbottom + offsetvalue;
 						textop = texbottom + texheight*(side->repeatcnt+1);
 					} else {
-						textop = opentop + side->rowoffset + side->offsety_mid;
+						textop = midopentop + offsetvalue;
 						texbottom = textop - texheight*(side->repeatcnt+1);
 					}
 				}
@@ -539,11 +555,21 @@ void P_LineOpening(line_t *linedef, mobj_t *mobj)
 				delta2 = abs(thingtop - texmid);
 
 				if (delta1 > delta2) { // Below
-					if (opentop > texbottom)
+					if (opentop > texbottom) {
 						opentop = texbottom;
+						if (linedef->flags & ML_NOSKEW)
+							opentopslope = NULL; // Object is not actually on a slope
+						else
+							opentopslope = linedef->midtexslope;
+					}
 				} else { // Above
-					if (openbottom < textop)
+					if (openbottom < textop) {
 						openbottom = textop;
+						if (linedef->flags & ML_NOSKEW)
+							openbottomslope = NULL; // Object is not actually on a slope
+						else
+							openbottomslope = linedef->midtexslope;
+					}
 				}
 			}
 		}
@@ -1024,35 +1050,39 @@ boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean (*func)(line_t *))
 //
 // P_BlockThingsIterator
 //
-boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean (*func)(mobj_t *))
+boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean (*func)(mobj_t *), mobj_t *thing)
 {
-	mobj_t *mobj;
-	blocknode_t *block;
+	blocknode_t *block, *next = NULL;
+
+	boolean checkthing = false;
+	if (thing)
+		checkthing = true;
 
 	if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 		return true;
 
 	// Check interaction with the objects in the blockmap.
-	for (block = blocklinks[y*bmapwidth + x]; block; block = block->mnext)
+	for (block = blocklinks[y*bmapwidth + x]; block != NULL; block = next)
 	{
-		mobj = block->mobj;
+		next = block->mnext; // We want to note our reference to mnext here!
 
-		if (!func(mobj))
+		if (!func(block->mobj))
 			return false;
-		if (P_MobjWasRemoved(tmthing)) // func just broke blockmap chain, cannot continue.
+
+		if (checkthing && P_MobjWasRemoved(thing)) // func just popped our tmthing, cannot continue.
 			return true;
 	}
 
 	return true;
 }
 
-boolean P_DoBlockThingsIterate(int x1, int y1, int x2, int y2, boolean (*func)(mobj_t *))
+boolean P_DoBlockThingsIterate(int x1, int y1, int x2, int y2, boolean (*func)(mobj_t *), mobj_t *thing)
 {
 	boolean status = true;
 
 	for (INT32 bx = x1; bx <= x2; bx++)
 		for (INT32 by = y1; by <= y2; by++)
-			if (!P_BlockThingsIterator(bx, by, func))
+			if (!P_BlockThingsIterator(bx, by, func, thing))
 				status = false;
 
 	return status;
@@ -1404,7 +1434,7 @@ static boolean P_TraverseIntercepts(traverser_t func, fixed_t maxfrac)
 
 		if (dist > maxfrac)
 			return true; // Checked everything in range.
-
+		
 		if (!func(in))
 			return false; // Don't bother going farther.
 
@@ -1425,14 +1455,14 @@ boolean P_PathTraverse(fixed_t px1, fixed_t py1, fixed_t px2, fixed_t py2,
 	INT32 flags, traverser_t trav)
 {
 	fixed_t xt1, yt1, xt2, yt2;
-	fixed_t xstep, ystep, partial, xintercept, yintercept;
+	fixed_t xstep, ystep, partialx, partialy, xintercept, yintercept;
 	INT32 mapx, mapy, mapxstep, mapystep, count;
 
 	earlyout = flags & PT_EARLYOUT;
 
 	validcount++;
 	intercept_p = intercepts;
-
+	
 	if (((px1 - bmaporgx) & (MAPBLOCKSIZE-1)) == 0)
 		px1 += FRACUNIT; // Don't side exactly on a line.
 
@@ -1444,56 +1474,82 @@ boolean P_PathTraverse(fixed_t px1, fixed_t py1, fixed_t px2, fixed_t py2,
 	trace.dx = px2 - px1;
 	trace.dy = py2 - py1;
 
-	px1 -= bmaporgx;
-	py1 -= bmaporgy;
-	xt1 = (unsigned)px1>>MAPBLOCKSHIFT;
-	yt1 = (unsigned)py1>>MAPBLOCKSHIFT;
+	xt1 = px1>>MAPBLOCKSHIFT;
+	yt1 = py1>>MAPBLOCKSHIFT;
+	px1 = (unsigned)(px1 - bmaporgx);
+	py1 = (unsigned)(py1 - bmaporgy);
 
-	px2 -= bmaporgx;
-	py2 -= bmaporgy;
-	xt2 = (unsigned)px2>>MAPBLOCKSHIFT;
-	yt2 = (unsigned)py2>>MAPBLOCKSHIFT;
+	xt2 = px2>>MAPBLOCKSHIFT;
+	yt2 = py2>>MAPBLOCKSHIFT;
+	px2 = (unsigned)(px2 - bmaporgx);
+	py2 = (unsigned)(py2 - bmaporgy);
 
 	if (xt2 > xt1)
 	{
 		mapxstep = 1;
-		partial = FRACUNIT - ((px1>>MAPBTOFRAC) & FRACMASK);
+		partialx = FRACUNIT - (((unsigned)px1>>MAPBTOFRAC) & FRACMASK);
 		ystep = FixedDiv(py2 - py1, abs(px2 - px1));
 	}
 	else if (xt2 < xt1)
 	{
 		mapxstep = -1;
-		partial = (px1>>MAPBTOFRAC) & FRACMASK;
+		partialx = ((unsigned)px1>>MAPBTOFRAC) & FRACMASK;
 		ystep = FixedDiv(py2 - py1, abs(px2 - px1));
 	}
 	else
 	{
 		mapxstep = 0;
-		partial = FRACUNIT;
+		partialx = FRACUNIT;
 		ystep = 256*FRACUNIT;
 	}
 
-	yintercept = (py1>>MAPBTOFRAC) + FixedMul(partial, ystep);
+	yintercept = ((unsigned)py1>>MAPBTOFRAC) + FixedMul(partialx, ystep);
 
 	if (yt2 > yt1)
 	{
 		mapystep = 1;
-		partial = FRACUNIT - ((py1>>MAPBTOFRAC) & FRACMASK);
+		partialy = FRACUNIT - (((unsigned)py1>>MAPBTOFRAC) & FRACMASK);
 		xstep = FixedDiv(px2 - px1, abs(py2 - py1));
 	}
 	else if (yt2 < yt1)
 	{
 		mapystep = -1;
-		partial = (py1>>MAPBTOFRAC) & FRACMASK;
+		partialy = ((unsigned)py1>>MAPBTOFRAC) & FRACMASK;
 		xstep = FixedDiv(px2 - px1, abs(py2 - py1));
 	}
 	else
 	{
 		mapystep = 0;
-		partial = FRACUNIT;
+		partialy = FRACUNIT;
 		xstep = 256*FRACUNIT;
 	}
-	xintercept = (px1>>MAPBTOFRAC) + FixedMul(partial, xstep);
+	xintercept = ((unsigned)px1>>MAPBTOFRAC) + FixedMul(partialy, xstep);
+
+	// [RH] Fix for traces that pass only through blockmap corners. In that case,
+	// xintercept and yintercept can both be set ahead of mapx and mapy, so the
+	// for loop would never advance anywhere.
+
+	if (abs(xstep) == 1 && abs(ystep) == 1)
+	{
+		if (ystep < 0)
+		{
+			partialx = FRACUNIT - partialx;
+		}
+		if (xstep < 0)
+		{
+			partialy = FRACUNIT - partialy;
+		}
+		if (partialx == partialy)
+		{
+			xintercept = xt1;
+			yintercept = yt1;
+		}
+	}
+
+	xt1 = (unsigned)px1>>MAPBLOCKSHIFT;
+	yt1 = (unsigned)py1>>MAPBLOCKSHIFT;
+	xt2 = (unsigned)px2>>MAPBLOCKSHIFT;
+	yt2 = (unsigned)py2>>MAPBLOCKSHIFT;
 
 	// Step through map blocks.
 	// Count is present to prevent a round off error
@@ -1508,23 +1564,67 @@ boolean P_PathTraverse(fixed_t px1, fixed_t py1, fixed_t px2, fixed_t py2,
 				return false; // early out
 
 		if (flags & PT_ADDTHINGS)
-			if (!P_BlockThingsIterator(mapx, mapy, PIT_AddThingIntercepts))
+			if (!P_BlockThingsIterator(mapx, mapy, PIT_AddThingIntercepts, NULL))
 				return false; // early out
 
-		if (mapx == xt2 && mapy == yt2)
+		// both coordinates reached the end, so end the traversing.
+		if ((mapxstep | mapystep) == 0)
 			break;
 
-		if ((yintercept >> FRACBITS) == mapy)
+		// [RH] Handle corner cases properly instead of pretending they don't exist.
+		switch ((((yintercept >> FRACBITS) == mapy) << 1) | ((xintercept >> FRACBITS) == mapx))
 		{
-			yintercept += ystep;
-			mapx += mapxstep;
-		}
-		else if ((xintercept >> FRACBITS) == mapx)
-		{
-			xintercept += xstep;
-			mapy += mapystep;
+			case 0: // neither xintercept nor yintercept match!
+				count = 64; // Stop traversing, because somebody screwed up.
+				break;
+
+			case 1: // xintercept matches
+				xintercept += xstep;
+				mapy += mapystep;
+				if (mapy == yt2)
+					mapystep = 0;
+				break;
+
+			case 2: // yintercept matches
+				yintercept += ystep;
+				mapx += mapxstep;
+				if (mapx == xt2)
+					mapxstep = 0;
+				break;
+
+			case 3: // xintercept and yintercept both match
+				// The trace is exiting a block through its corner. Not only does the block
+				// being entered need to be checked (which will happen when this loop
+				// continues), but the other two blocks adjacent to the corner also need to
+				// be checked.
+				if (flags & PT_ADDLINES)
+				{
+					if (!P_BlockLinesIterator(mapx + mapxstep, mapy, PIT_AddLineIntercepts))
+						return false; // early out
+					if (!P_BlockLinesIterator(mapx, mapy + mapystep, PIT_AddLineIntercepts))
+						return false; // early out
+				}
+
+				if (flags & PT_ADDTHINGS)
+				{
+					if (!P_BlockThingsIterator(mapx + mapxstep, mapy, PIT_AddThingIntercepts, NULL))
+						return false; // early out
+					if (!P_BlockThingsIterator(mapx, mapy + mapystep, PIT_AddThingIntercepts, NULL))
+						return false; // early out
+				}
+
+				xintercept += xstep;
+				yintercept += ystep;
+				mapx += mapxstep;
+				mapy += mapystep;
+				if (mapx == xt2)
+					mapxstep = 0;
+				if (mapy == yt2)
+					mapystep = 0;
+				break;
 		}
 	}
+	
 	// Go through the sorted list
 	return P_TraverseIntercepts(trav, FRACUNIT);
 }
diff --git a/src/p_maputl.h b/src/p_maputl.h
index e894c08a2461caf82397c569549e5bd3213ae3b7..11cc90c59099c6a0c28cfa7db171c126993512f2 100644
--- a/src/p_maputl.h
+++ b/src/p_maputl.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -61,7 +61,7 @@ extern ffloor_t *openfloorrover, *openceilingrover;
 void P_LineOpening(line_t *plinedef, mobj_t *mobj);
 
 boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean(*func)(line_t *));
-boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean(*func)(mobj_t *));
+boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean(*func)(mobj_t *), mobj_t *thing);
 
 void P_ClearBlockNodes(void);
 
@@ -94,7 +94,7 @@ typedef struct bthingit_s
 bthingit_t *P_NewBlockThingsIterator(int x1, int y1, int x2, int y2);
 mobj_t *P_BlockThingsIteratorNext(bthingit_t *it, boolean centeronly);
 void P_FreeBlockThingsIterator(bthingit_t *it);
-boolean P_DoBlockThingsIterate(int x1, int y1, int x2, int y2, boolean (*func)(mobj_t *));
+boolean P_DoBlockThingsIterate(int x1, int y1, int x2, int y2, boolean (*func)(mobj_t *), mobj_t *thing);
 
 #define PT_ADDLINES     1
 #define PT_ADDTHINGS    2
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 78915e939bdc8ad18843b655ac89ec0f155d3c71..f81c725c84dc7f58d04b762c5cbcfd6322a7951c 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -40,7 +40,7 @@
 #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);
+consvar_t cv_movebob = CVAR_INIT ("movebob", "0.25", CV_FLOAT|CV_SAVE, CV_BobSpeed, NULL);
 
 actioncache_t actioncachehead;
 
@@ -91,17 +91,21 @@ static void P_SetupStateAnimation(mobj_t *mobj, state_t *st)
 	if (mobj->sprite == SPR_PLAY && mobj->skin)
 	{
 		spritedef_t *spritedef = P_GetSkinSpritedef(mobj->skin, mobj->sprite2);
-		animlength = (INT32)(spritedef->numframes);
+		animlength = (INT32)(spritedef->numframes) - 1;
 	}
 	else
 		animlength = st->var1;
 
 	if (!(st->frame & FF_ANIMATE))
+	{
+		mobj->anim_duration = 0;
 		return;
+	}
 
 	if (animlength <= 0 || st->var2 == 0)
 	{
 		mobj->frame &= ~FF_ANIMATE;
+		mobj->anim_duration = 0;
 		return; // Crash/stupidity prevention
 	}
 
@@ -175,27 +179,6 @@ static void P_CycleMobjState(mobj_t *mobj)
 	}
 }
 
-//
-// P_CycleMobjState for players.
-//
-static void P_CyclePlayerMobjState(mobj_t *mobj)
-{
-	// state animations
-	P_CycleStateAnimation(mobj);
-
-	// cycle through states,
-	// calling action functions at transitions
-	if (mobj->tics != -1)
-	{
-		mobj->tics--;
-
-		// you can cycle through multiple states in a tic
-		if (!mobj->tics && mobj->state)
-			if (!P_SetMobjState(mobj, mobj->state->nextstate))
-				return; // freed itself
-	}
-}
-
 static panim_t GetPlayerAnimationFromState(player_t *player, statenum_t state)
 {
 	switch(P_GetCanonicalPlayerState(player, state))
@@ -338,8 +321,10 @@ static boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
 		mobj->state = st;
 		mobj->tics = st->tics;
 
-		// Adjust the player's animation speed to match their velocity.
-		if (player->panim == PA_EDGE && (player->charflags & SF_FASTEDGE))
+		// Adjust the player's animation speed
+		if (state == S_PLAY_WAIT && (player->charflags & SF_FASTWAIT))
+			mobj->tics = 5;
+		else if (player->panim == PA_EDGE && (player->charflags & SF_FASTEDGE))
 			mobj->tics = 2;
 		else if (!(disableSpeedAdjust || player->charflags & SF_NOSPEEDADJUST))
 		{
@@ -763,7 +748,7 @@ void P_EmeraldManager(void)
 
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 
 		mo = (mobj_t *)think;
@@ -1383,6 +1368,7 @@ fixed_t P_GetMobjGravity(mobj_t *mo)
 				case MT_WATERDROP:
 				case MT_CYBRAKDEMON:
 					gravityadd >>= 1;
+					break;
 				default:
 					break;
 			}
@@ -3452,7 +3438,7 @@ void P_DestroyRobots(void)
 
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 
 		mo = (mobj_t *)think;
@@ -3779,7 +3765,7 @@ static void P_PlayerMobjThinker(mobj_t *mobj)
 
 	// always do the gravity bit now, that's simpler
 	// BUT CheckPosition only if wasn't done before.
-	if (!(mobj->eflags & MFE_ONGROUND) || mobj->momz
+	if (mobj->momz
 		|| ((mobj->eflags & MFE_VERTICALFLIP) && mobj->z + mobj->height != mobj->ceilingz)
 		|| (!(mobj->eflags & MFE_VERTICALFLIP) && mobj->z != mobj->floorz)
 		|| P_IsObjectInGoop(mobj))
@@ -3792,22 +3778,11 @@ static void P_PlayerMobjThinker(mobj_t *mobj)
 	}
 	else
 	{
-#if 0 // i don't know why this is here, it's causing a few undesired state glitches, and disabling it doesn't appear to negatively affect the game, but i don't want it gone permanently just in case some obscure bug crops up
-		if (!(mobj->player->powers[pw_carry] == CR_NIGHTSMODE)) // used for drilling
-			mobj->player->pflags &= ~PF_STARTJUMP;
-		mobj->player->pflags &= ~(PF_JUMPED|PF_NOJUMPDAMAGE);
-		if (mobj->player->secondjump || mobj->player->powers[pw_tailsfly])
-		{
-			mobj->player->secondjump = 0;
-			mobj->player->powers[pw_tailsfly] = 0;
-			P_SetMobjState(mobj, S_PLAY_WALK);
-		}
-#endif
 		mobj->eflags &= ~MFE_JUSTHITFLOOR;
 	}
 
 animonly:
-	P_CyclePlayerMobjState(mobj);
+	P_CycleMobjState(mobj);
 }
 
 static void CalculatePrecipFloor(precipmobj_t *mobj)
@@ -4262,7 +4237,7 @@ static void P_Boss3Thinker(mobj_t *mobj)
 			// this can happen if the boss was hurt earlier than expected
 			for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 			{
-				if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				if (th->removing)
 					continue;
 
 				mo2 = (mobj_t *)th;
@@ -5351,7 +5326,7 @@ static void P_Boss9Thinker(mobj_t *mobj)
 		// Build a hoop linked list of 'em!
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo2 = (mobj_t *)th;
@@ -6053,7 +6028,7 @@ mobj_t *P_GetClosestAxis(mobj_t *source)
 	// scan the thinkers to find the closest axis point
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -6659,8 +6634,21 @@ static boolean P_ShieldLook(mobj_t *thing, shieldtype_t shield)
 	thing->flags |= MF_NOCLIPHEIGHT;
 	thing->eflags = (thing->eflags & ~MFE_VERTICALFLIP)|(thing->target->eflags & MFE_VERTICALFLIP);
 
-	P_SetScale(thing, FixedMul(thing->target->scale, thing->target->player->shieldscale), true);
-	thing->old_scale = FixedMul(thing->target->old_scale, thing->target->player->shieldscale);
+	//Set the shield's scale based on shieldscale, hide it if we're too small!
+	fixed_t scale = FixedMul(thing->target->scale, thing->target->player->shieldscale);
+	if (scale < 1) {
+		P_SetScale(thing, thing->target->scale, true);
+		thing->old_scale = thing->target->old_scale;
+
+		thing->flags2 |= (MF2_DONTDRAW|MF2_JUSTATTACKED); //Hide and indicate we're hidden
+	} else {
+		P_SetScale(thing, scale, true);
+		thing->old_scale = FixedMul(thing->target->old_scale, thing->target->player->shieldscale);
+
+		//Only unhide if we were hidden by the above code
+		if (thing->flags2 & MF2_JUSTATTACKED)
+			thing->flags2 &= ~(MF2_DONTDRAW|MF2_JUSTATTACKED);
+	}
 
 #define NewMH(mobj)   mobj->height // Ugly mobj-height and player-height defines, for the sake of prettier code
 #define NewPH(player) P_GetPlayerHeight(player)
@@ -6785,6 +6773,12 @@ void P_RunOverlays(void)
 		else
 			zoffs = 0;
 
+		// hide the overlay as well if we're part of a hidden shield
+		if ((mo->target->flags2 & (MF2_JUSTATTACKED|MF2_DONTDRAW)) == (MF2_JUSTATTACKED|MF2_DONTDRAW))
+			mo->flags2 |= (MF2_DONTDRAW|MF2_JUSTATTACKED);
+		else if (mo->flags2 & MF2_JUSTATTACKED)
+			mo->flags2 &= ~(MF2_DONTDRAW|MF2_JUSTATTACKED);
+
 		P_UnsetThingPosition(mo);
 		mo->x = mo->target->x;
 		mo->y = mo->target->y;
@@ -7017,14 +7011,6 @@ static void P_MobjScaleThink(mobj_t *mobj)
 		mobj->z -= (mobj->height - oldheight)/2;
 	else if (correctionType == 2)
 		mobj->z -= mobj->height - oldheight;
-
-	if (mobj->scale == mobj->destscale)
-		/// \todo Lua hook for "reached destscale"?
-		switch (mobj->type)
-		{
-		default:
-			break;
-		}
 }
 
 static void P_MaceSceneryThink(mobj_t *mobj)
@@ -7313,7 +7299,7 @@ static void P_RosySceneryThink(mobj_t *mobj)
 		player = &players[i];
 	}
 
-	if (stat == S_ROSY_JUMP || stat == S_ROSY_PAIN)
+	if (stat == S_ROSY_JUMP || stat == S_ROSY_FALL || stat == S_ROSY_PAIN)
 	{
 		if (P_IsObjectOnGround(mobj))
 		{
@@ -7324,16 +7310,16 @@ static void P_RosySceneryThink(mobj_t *mobj)
 				stat = S_ROSY_WALK;
 			P_SetMobjState(mobj, stat);
 		}
-		else if (P_MobjFlip(mobj)*mobj->momz < 0)
-			mobj->frame = mobj->state->frame + mobj->state->var1;
+		else if (P_MobjFlip(mobj)*mobj->momz < 0 && stat == S_ROSY_JUMP)
+			P_SetMobjState(mobj, S_ROSY_FALL);
 	}
 
 	if (!player)
 	{
-		if ((stat < S_ROSY_IDLE1 || stat > S_ROSY_IDLE4) && stat != S_ROSY_JUMP)
+		if (stat != S_ROSY_IDLE && stat != S_ROSY_JUMP && stat != S_ROSY_FALL)
 		{
 			mobj->momx = mobj->momy = 0;
-			P_SetMobjState(mobj, S_ROSY_IDLE1);
+			P_SetMobjState(mobj, S_ROSY_IDLE);
 		}
 	}
 	else
@@ -7347,13 +7333,11 @@ static void P_RosySceneryThink(mobj_t *mobj)
 
 		switch (stat)
 		{
-		case S_ROSY_IDLE1:
-		case S_ROSY_IDLE2:
-		case S_ROSY_IDLE3:
-		case S_ROSY_IDLE4:
+		case S_ROSY_IDLE:
 			dojump = true;
 			break;
 		case S_ROSY_JUMP:
+		case S_ROSY_FALL:
 		case S_ROSY_PAIN:
 			// handled above
 			break;
@@ -7379,8 +7363,7 @@ static void P_RosySceneryThink(mobj_t *mobj)
 				max = pdist;
 				if ((--mobj->extravalue1) <= 0)
 				{
-					if (++mobj->frame > mobj->state->frame + mobj->state->var1)
-						mobj->frame = mobj->state->frame;
+					P_SetMobjState(mobj, S_ROSY_WALK);
 					if (mom > 12*mobj->scale)
 						mobj->extravalue1 = 2;
 					else if (mom > 6*mobj->scale)
@@ -8056,6 +8039,10 @@ static boolean P_MobjBossThink(mobj_t *mobj)
 		case MT_METALSONIC_BATTLE:
 			P_Boss9Thinker(mobj);
 			break;
+		case MT_OLDK:
+			if (mobj->health <= 0)
+				mobj->momz -= (2*FRACUNIT)/3;
+			break;
 		default: // Generic SOC-made boss
 			if (mobj->flags2 & MF2_SKULLFLY)
 				P_SpawnGhostMobj(mobj);
@@ -9332,12 +9319,12 @@ static void P_PointPushThink(mobj_t *mobj)
 	radius = mobj->spawnpoint->args[0] << FRACBITS;
 
 	pushmobj = mobj;
-	xl = (unsigned)(mobj->x - radius - bmaporgx - MAXRADIUS)>>MAPBLOCKSHIFT;
-	xh = (unsigned)(mobj->x + radius - bmaporgx + MAXRADIUS)>>MAPBLOCKSHIFT;
-	yl = (unsigned)(mobj->y - radius - bmaporgy - MAXRADIUS)>>MAPBLOCKSHIFT;
-	yh = (unsigned)(mobj->y + radius - bmaporgy + MAXRADIUS)>>MAPBLOCKSHIFT;
+	xl = (unsigned)(mobj->x - radius - bmaporgx)>>MAPBLOCKSHIFT;
+	xh = (unsigned)(mobj->x + radius - bmaporgx)>>MAPBLOCKSHIFT;
+	yl = (unsigned)(mobj->y - radius - bmaporgy)>>MAPBLOCKSHIFT;
+	yh = (unsigned)(mobj->y + radius - bmaporgy)>>MAPBLOCKSHIFT;
 
-	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_PushThing);
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_PushThing, pushmobj);
 }
 
 static boolean P_MobjRegularThink(mobj_t *mobj)
@@ -10142,6 +10129,7 @@ static boolean P_FuseThink(mobj_t *mobj)
 //
 void P_MobjThinker(mobj_t *mobj)
 {
+	boolean ispushable;
 	I_Assert(mobj != NULL);
 	I_Assert(!P_MobjWasRemoved(mobj));
 
@@ -10167,8 +10155,10 @@ void P_MobjThinker(mobj_t *mobj)
 
 	tmfloorthing = tmhitthing = NULL;
 
+	ispushable = mobj->flags & MF_PUSHABLE || (mobj->info->flags & MF_PUSHABLE && mobj->fuse);
+
 	// Sector flag MSF_TRIGGERLINE_MOBJ allows ANY mobj to trigger a linedef exec
-	P_CheckMobjTrigger(mobj, false);
+	P_CheckMobjTrigger(mobj, ispushable);
 
 	if (mobj->scale != mobj->destscale)
 		P_MobjScaleThink(mobj); // Slowly scale up/down to reach your destscale.
@@ -10212,7 +10202,7 @@ void P_MobjThinker(mobj_t *mobj)
 
 	// if it's pushable, or if it would be pushable other than temporary disablement, use the
 	// separate thinker
-	if (mobj->flags & MF_PUSHABLE || (mobj->info->flags & MF_PUSHABLE && mobj->fuse))
+	if (ispushable)
 	{
 		if (!P_MobjPushableThink(mobj))
 			return;
@@ -10321,10 +10311,7 @@ void P_MobjThinker(mobj_t *mobj)
 	}
 
 	// Can end up here if a player dies.
-	if (mobj->player)
-		P_CyclePlayerMobjState(mobj);
-	else
-		P_CycleMobjState(mobj);
+	P_CycleMobjState(mobj);
 
 	if (P_MobjWasRemoved(mobj))
 		return;
@@ -10339,6 +10326,7 @@ void P_MobjThinker(mobj_t *mobj)
 		case MT_GRENADEPICKUP:
 			if (mobj->health == 0) // Fading tile
 			{
+				// TODO: Maybe use mobj->alpha instead of messing with frame flags
 				INT32 value = mobj->info->damage/10;
 				value = mobj->fuse/value;
 				value = 10-value;
@@ -10390,8 +10378,6 @@ void P_PushableThinker(mobj_t *mobj)
 	I_Assert(mobj != NULL);
 	I_Assert(!P_MobjWasRemoved(mobj));
 
-	P_CheckMobjTrigger(mobj, true);
-
 	// it has to be pushable RIGHT NOW for this part to happen
 	if (mobj->flags & MF_PUSHABLE && !(mobj->momx || mobj->momy))
 		P_TryMove(mobj, mobj->x, mobj->y, true);
@@ -10613,6 +10599,19 @@ static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
 	}
 }
 
+static INT32 P_SetupNPC(mobj_t *mobj, const char *name)
+{
+	INT32 skinnum = R_SkinAvailable(name);
+
+	if (skinnum != -1)
+	{
+		mobj->skin = skins[skinnum];
+		mobj->color = skins[skinnum]->prefcolor;
+	}
+
+	return skinnum;
+}
+
 //
 // P_SpawnMobj
 //
@@ -10684,6 +10683,7 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type, ...)
 
 	// Sprite rendering
 	mobj->blendmode = AST_TRANSLUCENT;
+	mobj->alpha = FRACUNIT;
 	mobj->spritexscale = mobj->spriteyscale = mobj->scale;
 	mobj->spritexoffset = mobj->spriteyoffset = 0;
 	mobj->floorspriteslope = NULL;
@@ -10744,7 +10744,8 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type, ...)
 		// when spawning MT_PLAYER, set mobj->player before calling MobjSpawn hook to prevent P_RemoveMobj from succeeding on player mobj.
 		va_start(args, type);
 		mobj->player = va_arg(args, player_t *);
-		mobj->player->mo = mobj;
+		if (mobj->player)
+			mobj->player->mo = mobj;
 		va_end(args);
 	}
 
@@ -10964,17 +10965,14 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type, ...)
 				nummaprings++;
 			break;
 		case MT_METALSONIC_RACE:
-			mobj->skin = skins[5];
-			/* FALLTHRU */
 		case MT_METALSONIC_BATTLE:
-			mobj->color = skins[5]->prefcolor;
-			sc = 5;
+			sc = P_SetupNPC(mobj, "metalsonic");
 			break;
 		case MT_FANG:
-			sc = 4;
+			sc = P_SetupNPC(mobj, "fang");
 			break;
 		case MT_ROSY:
-			sc = 3;
+			sc = P_SetupNPC(mobj, "amy");
 			break;
 		case MT_CORK:
 			mobj->flags2 |= MF2_SUPERFIRE;
@@ -11004,13 +11002,6 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type, ...)
 				mcsolid->angle = mobj->angle + ANGLE_90;
 			}
 			break;
-		case MT_TORCHFLOWER:
-			{
-				mobj_t *fire = P_SpawnMobjFromMobj(mobj, 0, 0, 46*FRACUNIT, MT_FLAME);
-				if (!P_MobjWasRemoved(fire))
-					P_SetTarget(&mobj->target, fire);
-				break;
-			}
 		case MT_PYREFLY:
 			mobj->extravalue1 = (FixedHypot(mobj->x, mobj->y)/FRACUNIT) % 360;
 			mobj->extravalue2 = 0;
@@ -11175,17 +11166,16 @@ tic_t itemrespawntime[ITEMQUESIZE];
 size_t iquehead, iquetail;
 
 #ifdef PARANOIA
-#define SCRAMBLE_REMOVED // Force debug build to crash when Removed mobj is accessed
+#define SCRAMBLE_REMOVED // Force debug build to crash when a removed mobj is accessed
 #endif
 void P_RemoveMobj(mobj_t *mobj)
 {
 	I_Assert(mobj != NULL);
-	if (P_MobjWasRemoved(mobj))
-		return; // something already removing this mobj.
+	if (P_MobjWasRemoved(mobj) || mobj->thinker.removing)
+		return; // Something already removed or is removing this mobj.
 
-	mobj->thinker.function.acp1 = (actionf_p1)P_RemoveThinkerDelayed; // shh. no recursing.
+	mobj->thinker.removing = true; // Set earlier to avoid recursion.
 	LUA_HookMobj(mobj, MOBJ_HOOK(MobjRemoved));
-	mobj->thinker.function.acp1 = (actionf_p1)P_MobjThinker; // needed for P_UnsetThingPosition, etc. to work.
 
 	// Rings only, please!
 	if (mobj->spawnpoint &&
@@ -11283,15 +11273,6 @@ void P_RemoveMobj(mobj_t *mobj)
 #endif
 }
 
-// This does not need to be added to Lua.
-// To test it in Lua, check mobj.valid
-boolean P_MobjWasRemoved(mobj_t *mobj)
-{
-	if (mobj && mobj->thinker.function.acp1 == (actionf_p1)P_MobjThinker)
-		return false;
-	return true;
-}
-
 void P_RemovePrecipMobj(precipmobj_t *mobj)
 {
 	// unlink from sector and block lists
@@ -12855,7 +12836,7 @@ static boolean P_MapAlreadyHasStarPost(mobj_t *mobj)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -13004,6 +12985,13 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 			}
 		}
 		break;
+	case MT_TORCHFLOWER:
+		{
+			mobj_t *fire = P_SpawnMobjFromMobj(mobj, 0, 0, 46*FRACUNIT, MT_FLAME);
+			if (!P_MobjWasRemoved(fire))
+				P_SetTarget(&mobj->target, fire);
+			break;
+		}
 	case MT_CANDLE:
 	case MT_CANDLEPRICKET:
 		if (mthing->args[0])
@@ -13142,6 +13130,32 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 			banner->angle = mobjangle + ANGLE_90;
 	}
 	break;
+	case MT_SSZTREE:
+	{ // Spawn the branches
+		INT32 i;
+		mobj_t *branch;
+		for (i = 0; i < 5; i++)
+		{
+			branch = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_SSZTREE_BRANCH);
+			if (P_MobjWasRemoved(branch))
+				continue;
+			branch->angle = mobj->angle + FixedAngle(i*72*FRACUNIT);
+		}
+	}
+	break;
+	case MT_SSZTREE2:
+	{ // Spawn the branches
+		INT32 i;
+		mobj_t *branch;
+		for (i = 0; i < 5; i++)
+		{
+			branch = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_SSZTREE2_BRANCH);
+			if (P_MobjWasRemoved(branch))
+				continue;
+			branch->angle = mobj->angle + FixedAngle(i*72*FRACUNIT);
+		}
+	}
+	break;
 	case MT_HHZTREE_TOP:
 	{ // Spawn the branches
 		angle_t mobjangle = FixedAngle(mthing->angle << FRACBITS) & (ANGLE_90 - 1);
@@ -14286,7 +14300,8 @@ mobj_t *P_SpawnMobjFromMobj(mobj_t *mobj, fixed_t xofs, fixed_t yofs, fixed_t zo
 	yofs = FixedMul(yofs, mobj->scale);
 	zofs = FixedMul(zofs, mobj->scale);
 
-	newmobj = P_SpawnMobj(mobj->x + xofs, mobj->y + yofs, mobj->z + zofs, type);
+	newmobj = P_SpawnMobj(mobj->x + xofs, mobj->y + yofs, mobj->z + zofs, type, NULL);
+
 	if (!newmobj)
 		return NULL;
 
diff --git a/src/p_mobj.h b/src/p_mobj.h
index 2f013a2f30fac7f7d117cb190fcbc5bd1db17e54..4a842766999d205eb46f15250034e523236302d5 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -313,6 +313,7 @@ typedef struct mobj_s
 
 	UINT32 renderflags; // render flags
 	INT32 blendmode; // blend mode
+	fixed_t alpha; // alpha
 	fixed_t spritexscale, spriteyscale;
 	fixed_t spritexoffset, spriteyoffset;
 	fixed_t old_spritexscale, old_spriteyscale, old_spritexscale2, old_spriteyscale2;
@@ -456,6 +457,7 @@ typedef struct precipmobj_s
 
 	UINT32 renderflags; // render flags
 	INT32 blendmode; // blend mode
+	fixed_t alpha; // alpha
 	fixed_t spritexscale, spriteyscale;
 	fixed_t spritexoffset, spriteyoffset;
 	fixed_t old_spritexscale, old_spriteyscale, old_spritexscale2, old_spriteyscale2;
@@ -537,6 +539,8 @@ boolean P_SceneryZMovement(mobj_t *mo);
 void P_PlayerZMovement(mobj_t *mo);
 void P_EmeraldManager(void);
 
+mobj_t *P_FindNewPosition(UINT32 oldposition);
+
 extern INT32 modulothing;
 
 #define MAXHUNTEMERALDS 64
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index e779956e8d92092bf41a97db8efa5a7e7fd1d973..3d1a38d36ff9d9b8f7324df0b18029e35519870d 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2006      by James Haley
-// Copyright (C) 2006-2023 by Sonic Team Junior.
+// Copyright (C) 2006-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -878,15 +878,15 @@ static void Polyobj_carryThings(polyobj_t *po, fixed_t dx, fixed_t dy)
 		{
 			mobj_t *mo;
 			blocknode_t *block;
+			blocknode_t *next = NULL;
 
 			if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 				continue;
 
-			block = blocklinks[y * bmapwidth + x];
-
-			for (; block; block = block->mnext)
+			for (block = blocklinks[y * bmapwidth + x]; block != NULL; block = next)
 			{
 				mo = block->mobj;
+				next = block->mnext;
 
 				if (mo->lastlook == pomovecount)
 					continue;
@@ -927,11 +927,11 @@ static INT32 Polyobj_clipThings(polyobj_t *po, line_t *line)
 	if (!(po->flags & POF_SOLID))
 		return hitflags;
 
-	// adjust linedef bounding box to blockmap, extend by MAXRADIUS
-	linebox[BOXLEFT]   = (unsigned)(line->bbox[BOXLEFT]   - bmaporgx - MAXRADIUS) >> MAPBLOCKSHIFT;
-	linebox[BOXRIGHT]  = (unsigned)(line->bbox[BOXRIGHT]  - bmaporgx + MAXRADIUS) >> MAPBLOCKSHIFT;
-	linebox[BOXBOTTOM] = (unsigned)(line->bbox[BOXBOTTOM] - bmaporgy - MAXRADIUS) >> MAPBLOCKSHIFT;
-	linebox[BOXTOP]    = (unsigned)(line->bbox[BOXTOP]    - bmaporgy + MAXRADIUS) >> MAPBLOCKSHIFT;
+	// adjust linedef bounding box to blockmap
+	linebox[BOXLEFT]   = (unsigned)(line->bbox[BOXLEFT]   - bmaporgx) >> MAPBLOCKSHIFT;
+	linebox[BOXRIGHT]  = (unsigned)(line->bbox[BOXRIGHT]  - bmaporgx) >> MAPBLOCKSHIFT;
+	linebox[BOXBOTTOM] = (unsigned)(line->bbox[BOXBOTTOM] - bmaporgy) >> MAPBLOCKSHIFT;
+	linebox[BOXTOP]    = (unsigned)(line->bbox[BOXTOP]    - bmaporgy) >> MAPBLOCKSHIFT;
 
 	// check all mobj blockmap cells the line contacts
 	for (y = linebox[BOXBOTTOM]; y <= linebox[BOXTOP]; ++y)
@@ -942,9 +942,11 @@ static INT32 Polyobj_clipThings(polyobj_t *po, line_t *line)
 			{
 				mobj_t *mo = NULL;
 				blocknode_t *block = blocklinks[y * bmapwidth + x];
+				blocknode_t *next = NULL;
 
-				for (; block; block = block->mnext)
+				for (; block != NULL; block = next)
 				{
+					next = block->mnext;
 					mo = block->mobj;
 
 					// Don't scroll objects that aren't affected by gravity
@@ -1115,15 +1117,15 @@ static void Polyobj_rotateThings(polyobj_t *po, vector2_t origin, angle_t delta,
 		{
 			mobj_t *mo;
 			blocknode_t *block;
+			blocknode_t *next = NULL;
 
 			if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 				continue;
 
-			block = blocklinks[y * bmapwidth + x];
-
-			for (; block; block = block->mnext)
+			for (block = blocklinks[y * bmapwidth + x]; block != NULL; block = next)
 			{
 				mo = block->mobj;
+				next = block->mnext;
 
 				if (mo->lastlook == pomovecount)
 					continue;
@@ -1316,7 +1318,7 @@ void Polyobj_InitLevel(void)
 	// the mobj_t pointers on a queue for use below.
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo = (mobj_t *)th;
diff --git a/src/p_pspr.h b/src/p_pspr.h
index 5fb6767633398d1e304001123dcda541599b1aba..5e1c1e05e8772c01d53871eca52d3b939fc66413 100644
--- a/src/p_pspr.h
+++ b/src/p_pspr.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -39,7 +39,7 @@
 #define FF_FRAMEMASK 0xff
 
 /// \brief Frame flags - SPR2: Super sprite2
-#define FF_SPR2SUPER 0x80
+#define FF_SPR2SUPER SPR2F_SUPER //TODO: 2.3: remove this backwards compat hack
 /// \brief Frame flags - SPR2: A change of state at the end of Sprite2 animation
 #define FF_SPR2ENDSTATE 0x100
 /// \brief Frame flags - SPR2: 50% of starting in middle of Sprite2 animation
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 5e4d6d0760441e6bc94c6815824b8b7e1ab38c80..0a49c680ac73cc77805c6a405c9e1df7f9b1d1e3 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -35,9 +35,249 @@
 #include "p_polyobj.h"
 #include "lua_script.h"
 #include "p_slopes.h"
+#include "hu_stuff.h"
 
 savedata_t savedata;
-UINT8 *save_p;
+
+#define ALLOC_SIZE(p, z) \
+	if ((p)->pos + (z) > (p)->size) \
+	{ \
+		while ((p)->pos + (z) > (p)->size) \
+			(p)->size <<= 1; \
+		(p)->buf = realloc((p)->buf, (p)->size); \
+	}
+
+void P_WriteUINT8(save_t *p, UINT8 v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteSINT8(save_t *p, SINT8 v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteUINT16(save_t *p, UINT16 v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteINT16(save_t *p, INT16 v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteUINT32(save_t *p, UINT32 v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteINT32(save_t *p, INT32 v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteChar(save_t *p, char v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteFixed(save_t *p, fixed_t v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteAngle(save_t *p, angle_t v)
+{
+	ALLOC_SIZE(p, sizeof(v));
+	memcpy(&p->buf[p->pos], &v, sizeof(v));
+	p->pos += sizeof(v);
+}
+
+void P_WriteStringN(save_t *p, char const *s, size_t n)
+{
+	size_t i;
+
+	for (i = 0; i < n && s[i] != '\0'; i++)
+		P_WriteChar(p, s[i]);
+
+	if (i < n)
+		P_WriteChar(p, '\0');
+}
+
+void P_WriteStringL(save_t *p, char const *s, size_t n)
+{
+	size_t i;
+
+	for (i = 0; i < n - 1 && s[i] != '\0'; i++)
+		P_WriteChar(p, s[i]);
+
+	P_WriteChar(p, '\0');
+}
+
+void P_WriteString(save_t *p, char const *s)
+{
+	size_t i;
+
+	for (i = 0; s[i] != '\0'; i++)
+		P_WriteChar(p, s[i]);
+
+	P_WriteChar(p, '\0');
+}
+
+void P_WriteMem(save_t *p, void const *s, size_t n)
+{
+	ALLOC_SIZE(p, n);
+	memcpy(&p->buf[p->pos], s, n);
+	p->pos += n;
+}
+
+void P_SkipStringN(save_t *p, size_t n)
+{
+	size_t i;
+	for (i = 0; p->pos < p->size && i < n && P_ReadChar(p) != '\0'; i++);
+}
+
+void P_SkipStringL(save_t *p, size_t n)
+{
+	P_SkipStringN(p, n);
+}
+
+void P_SkipString(save_t *p)
+{
+	P_SkipStringN(p, SIZE_MAX);
+}
+
+UINT8 P_ReadUINT8(save_t *p)
+{
+	UINT8 v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+SINT8 P_ReadSINT8(save_t *p)
+{
+	SINT8 v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+UINT16 P_ReadUINT16(save_t *p)
+{
+	UINT16 v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+INT16 P_ReadINT16(save_t *p)
+{
+	INT16 v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+UINT32 P_ReadUINT32(save_t *p)
+{
+	UINT32 v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+INT32 P_ReadINT32(save_t *p)
+{
+	INT32 v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+char P_ReadChar(save_t *p)
+{
+	char v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+fixed_t P_ReadFixed(save_t *p)
+{
+	fixed_t v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+angle_t P_ReadAngle(save_t *p)
+{
+	angle_t v;
+	if (p->pos + sizeof(v) > p->size)
+		return 0;
+	memcpy(&v, &p->buf[p->pos], sizeof(v));
+	p->pos += sizeof(v);
+	return v;
+}
+
+void P_ReadStringN(save_t *p, char *s, size_t n)
+{
+	size_t i;
+	for (i = 0; p->pos < p->size && i < n && (s[i] = P_ReadChar(p)) != '\0'; i++);
+	s[i] = '\0';
+}
+
+void P_ReadStringL(save_t *p, char *s, size_t n)
+{
+	P_ReadStringN(p, s, n - 1);
+}
+
+void P_ReadString(save_t *p, char *s)
+{
+	P_ReadStringN(p, s, SIZE_MAX);
+}
+
+void P_ReadMem(save_t *p, void *s, size_t n)
+{
+	if (p->pos + n > p->size)
+		return;
+	memcpy(s, &p->buf[p->pos], n);
+	p->pos += n;
+}
 
 // Block UINT32s to attempt to ensure that the correct data is
 // being sent and received
@@ -62,7 +302,19 @@ typedef enum
 	DRONE      = 0x80,
 } player_saveflags;
 
-static inline void P_ArchivePlayer(void)
+static inline UINT32 SavePlayer(const player_t *player)
+{
+	if (player) return (UINT32)(player - players);
+	return 0xFFFFFFFF;
+}
+
+static inline player_t *LoadPlayer(UINT32 player)
+{
+	if (player >= MAXPLAYERS) return NULL;
+	return &players[player];
+}
+
+static inline void P_ArchivePlayer(save_t *save_p)
 {
 	const player_t *player = &players[consoleplayer];
 	SINT8 pllives = player->lives;
@@ -72,32 +324,32 @@ static inline void P_ArchivePlayer(void)
 #ifdef NEWSKINSAVES
 	// Write a specific value into the old skininfo location.
 	// If we read something other than this, it's an older save file that used skin numbers.
-	WRITEUINT16(save_p, NEWSKINSAVES);
+	P_WriteUINT16(save_p, NEWSKINSAVES);
 #endif
 
 	// Write skin names, so that loading skins in different orders
 	// doesn't change who the save file is for!
-	WRITESTRINGN(save_p, skins[player->skin]->name, SKINNAMESIZE);
+	P_WriteStringN(save_p, skins[player->skin]->name, SKINNAMESIZE);
 
 	if (botskin != 0)
 	{
-		WRITESTRINGN(save_p, skins[botskin-1]->name, SKINNAMESIZE);
+		P_WriteStringN(save_p, skins[botskin-1]->name, SKINNAMESIZE);
 	}
 	else
 	{
-		WRITESTRINGN(save_p, "\0", SKINNAMESIZE);
+		P_WriteStringN(save_p, "\0", SKINNAMESIZE);
 	}
 
-	WRITEUINT8(save_p, numgameovers);
-	WRITESINT8(save_p, pllives);
-	WRITEUINT32(save_p, player->score);
-	WRITEINT32(save_p, player->continues);
+	P_WriteUINT8(save_p, numgameovers);
+	P_WriteSINT8(save_p, pllives);
+	P_WriteUINT32(save_p, player->score);
+	P_WriteINT32(save_p, player->continues);
 }
 
-static inline void P_UnArchivePlayer(void)
+static inline void P_UnArchivePlayer(save_t *save_p)
 {
 #ifdef NEWSKINSAVES
-	INT16 backwardsCompat = READUINT16(save_p);
+	INT16 backwardsCompat = P_ReadUINT16(save_p);
 
 	if (backwardsCompat != NEWSKINSAVES)
 	{
@@ -111,30 +363,30 @@ static inline void P_UnArchivePlayer(void)
 		char ourSkinName[SKINNAMESIZE+1];
 		char botSkinName[SKINNAMESIZE+1];
 
-		READSTRINGN(save_p, ourSkinName, SKINNAMESIZE);
+		P_ReadStringN(save_p, ourSkinName, SKINNAMESIZE);
 		savedata.skin = R_SkinAvailable(ourSkinName);
 
-		READSTRINGN(save_p, botSkinName, SKINNAMESIZE);
+		P_ReadStringN(save_p, botSkinName, SKINNAMESIZE);
 		savedata.botskin = R_SkinAvailable(botSkinName) + 1;
 	}
 
-	savedata.numgameovers = READUINT8(save_p);
-	savedata.lives = READSINT8(save_p);
-	savedata.score = READUINT32(save_p);
-	savedata.continues = READINT32(save_p);
+	savedata.numgameovers = P_ReadUINT8(save_p);
+	savedata.lives = P_ReadSINT8(save_p);
+	savedata.score = P_ReadUINT32(save_p);
+	savedata.continues = P_ReadINT32(save_p);
 }
 
-static void P_NetArchivePlayers(void)
+static void P_NetArchivePlayers(save_t *save_p)
 {
 	INT32 i, j;
 	UINT16 flags;
 //	size_t q;
 
-	WRITEUINT32(save_p, ARCHIVEBLOCK_PLAYERS);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_PLAYERS);
 
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		WRITESINT8(save_p, (SINT8)adminplayers[i]);
+		P_WriteSINT8(save_p, (SINT8)adminplayers[i]);
 
 		if (!playeringame[i])
 			continue;
@@ -143,142 +395,143 @@ static void P_NetArchivePlayers(void)
 
 		// no longer send ticcmds
 
-		WRITESTRINGN(save_p, player_names[i], MAXPLAYERNAME);
-		WRITEINT16(save_p, players[i].angleturn);
-		WRITEINT16(save_p, players[i].oldrelangleturn);
-		WRITEANGLE(save_p, players[i].aiming);
-		WRITEANGLE(save_p, players[i].drawangle);
-		WRITEANGLE(save_p, players[i].viewrollangle);
-		WRITEANGLE(save_p, players[i].awayviewaiming);
-		WRITEINT32(save_p, players[i].awayviewtics);
-		WRITEINT16(save_p, players[i].rings);
-		WRITEINT16(save_p, players[i].spheres);
-
-		WRITESINT8(save_p, players[i].pity);
-		WRITEINT32(save_p, players[i].currentweapon);
-		WRITEINT32(save_p, players[i].ringweapons);
-
-		WRITEUINT16(save_p, players[i].ammoremoval);
-		WRITEUINT32(save_p, players[i].ammoremovaltimer);
-		WRITEINT32(save_p, players[i].ammoremovaltimer);
+		P_WriteStringN(save_p, player_names[i], MAXPLAYERNAME);
+		P_WriteINT16(save_p, players[i].angleturn);
+		P_WriteINT16(save_p, players[i].oldrelangleturn);
+		P_WriteAngle(save_p, players[i].aiming);
+		P_WriteAngle(save_p, players[i].drawangle);
+		P_WriteAngle(save_p, players[i].viewrollangle);
+		P_WriteAngle(save_p, players[i].awayviewaiming);
+		P_WriteINT32(save_p, players[i].awayviewtics);
+		P_WriteINT16(save_p, players[i].rings);
+		P_WriteINT16(save_p, players[i].spheres);
+
+		P_WriteSINT8(save_p, players[i].pity);
+		P_WriteINT32(save_p, players[i].currentweapon);
+		P_WriteINT32(save_p, players[i].ringweapons);
+
+		P_WriteUINT16(save_p, players[i].ammoremoval);
+		P_WriteUINT32(save_p, players[i].ammoremovaltimer);
+		P_WriteINT32(save_p, players[i].ammoremovaltimer);
 
 		for (j = 0; j < NUMPOWERS; j++)
-			WRITEUINT16(save_p, players[i].powers[j]);
-
-		WRITEUINT8(save_p, players[i].playerstate);
-		WRITEUINT32(save_p, players[i].pflags);
-		WRITEUINT8(save_p, players[i].panim);
-		WRITEUINT8(save_p, players[i].stronganim);
-		WRITEUINT8(save_p, players[i].spectator);
-		WRITEUINT8(save_p, players[i].muted);
-
-		WRITEUINT16(save_p, players[i].flashpal);
-		WRITEUINT16(save_p, players[i].flashcount);
-
-		WRITEUINT16(save_p, players[i].skincolor);
-		WRITEINT32(save_p, players[i].skin);
-		WRITEUINT32(save_p, players[i].availabilities);
-		WRITEUINT32(save_p, players[i].score);
-		WRITEUINT32(save_p, players[i].recordscore);
-		WRITEFIXED(save_p, players[i].dashspeed);
-		WRITESINT8(save_p, players[i].lives);
-		WRITESINT8(save_p, players[i].continues);
-		WRITESINT8(save_p, players[i].xtralife);
-		WRITEUINT8(save_p, players[i].gotcontinue);
-		WRITEFIXED(save_p, players[i].speed);
-		WRITEUINT8(save_p, players[i].secondjump);
-		WRITEUINT8(save_p, players[i].fly1);
-		WRITEUINT8(save_p, players[i].scoreadd);
-		WRITEUINT32(save_p, players[i].glidetime);
-		WRITEUINT8(save_p, players[i].climbing);
-		WRITEINT32(save_p, players[i].deadtimer);
-		WRITEUINT32(save_p, players[i].exiting);
-		WRITEUINT8(save_p, players[i].homing);
-		WRITEUINT32(save_p, players[i].dashmode);
-		WRITEUINT32(save_p, players[i].skidtime);
+			P_WriteUINT16(save_p, players[i].powers[j]);
+
+		P_WriteUINT8(save_p, players[i].playerstate);
+		P_WriteUINT32(save_p, players[i].pflags);
+		P_WriteUINT8(save_p, players[i].panim);
+		P_WriteUINT8(save_p, players[i].stronganim);
+		P_WriteUINT8(save_p, players[i].spectator);
+		P_WriteUINT8(save_p, players[i].muted);
+
+		P_WriteUINT16(save_p, players[i].flashpal);
+		P_WriteUINT16(save_p, players[i].flashcount);
+
+		P_WriteUINT16(save_p, players[i].skincolor);
+		P_WriteINT32(save_p, players[i].skin);
+		P_WriteUINT32(save_p, players[i].availabilities);
+		P_WriteUINT32(save_p, players[i].score);
+		P_WriteUINT32(save_p, players[i].recordscore);
+		P_WriteFixed(save_p, players[i].dashspeed);
+		P_WriteSINT8(save_p, players[i].lives);
+		P_WriteSINT8(save_p, players[i].continues);
+		P_WriteSINT8(save_p, players[i].xtralife);
+		P_WriteUINT8(save_p, players[i].gotcontinue);
+		P_WriteFixed(save_p, players[i].speed);
+		P_WriteUINT8(save_p, players[i].secondjump);
+		P_WriteUINT8(save_p, players[i].fly1);
+		P_WriteUINT8(save_p, players[i].scoreadd);
+		P_WriteUINT32(save_p, players[i].glidetime);
+		P_WriteUINT8(save_p, players[i].climbing);
+		P_WriteINT32(save_p, players[i].deadtimer);
+		P_WriteUINT32(save_p, players[i].exiting);
+		P_WriteUINT8(save_p, players[i].homing);
+		P_WriteUINT32(save_p, players[i].dashmode);
+		P_WriteUINT32(save_p, players[i].skidtime);
 
 		//////////
 		// Bots //
 		//////////
-		WRITEUINT8(save_p, players[i].bot);
-		WRITEUINT8(save_p, players[i].botmem.lastForward);
-		WRITEUINT8(save_p, players[i].botmem.lastBlocked);
-		WRITEUINT8(save_p, players[i].botmem.catchup_tics);
-		WRITEUINT8(save_p, players[i].botmem.thinkstate);
-		WRITEUINT8(save_p, players[i].removing);
+		P_WriteUINT8(save_p, players[i].bot);
+		P_WriteUINT32(save_p, SavePlayer(players[i].botleader));
+		P_WriteUINT8(save_p, players[i].botmem.lastForward);
+		P_WriteUINT8(save_p, players[i].botmem.lastBlocked);
+		P_WriteUINT8(save_p, players[i].botmem.catchup_tics);
+		P_WriteUINT8(save_p, players[i].botmem.thinkstate);
+		P_WriteUINT8(save_p, players[i].removing);
 
-		WRITEUINT8(save_p, players[i].blocked);
-		WRITEUINT16(save_p, players[i].lastbuttons);
+		P_WriteUINT8(save_p, players[i].blocked);
+		P_WriteUINT16(save_p, players[i].lastbuttons);
 
 		////////////////////////////
 		// Conveyor Belt Movement //
 		////////////////////////////
-		WRITEFIXED(save_p, players[i].cmomx); // Conveyor momx
-		WRITEFIXED(save_p, players[i].cmomy); // Conveyor momy
-		WRITEFIXED(save_p, players[i].rmomx); // "Real" momx (momx - cmomx)
-		WRITEFIXED(save_p, players[i].rmomy); // "Real" momy (momy - cmomy)
+		P_WriteFixed(save_p, players[i].cmomx); // Conveyor momx
+		P_WriteFixed(save_p, players[i].cmomy); // Conveyor momy
+		P_WriteFixed(save_p, players[i].rmomx); // "Real" momx (momx - cmomx)
+		P_WriteFixed(save_p, players[i].rmomy); // "Real" momy (momy - cmomy)
 
 		/////////////////////
 		// Race Mode Stuff //
 		/////////////////////
-		WRITEINT16(save_p, players[i].numboxes);
-		WRITEINT16(save_p, players[i].totalring);
-		WRITEUINT32(save_p, players[i].realtime);
-		WRITEUINT8(save_p, players[i].laps);
+		P_WriteINT16(save_p, players[i].numboxes);
+		P_WriteINT16(save_p, players[i].totalring);
+		P_WriteUINT32(save_p, players[i].realtime);
+		P_WriteUINT8(save_p, players[i].laps);
 
 		////////////////////
 		// CTF Mode Stuff //
 		////////////////////
-		WRITEINT32(save_p, players[i].ctfteam);
-		WRITEUINT16(save_p, players[i].gotflag);
-
-		WRITEINT32(save_p, players[i].weapondelay);
-		WRITEINT32(save_p, players[i].tossdelay);
-
-		WRITEUINT32(save_p, players[i].starposttime);
-		WRITEINT16(save_p, players[i].starpostx);
-		WRITEINT16(save_p, players[i].starposty);
-		WRITEINT16(save_p, players[i].starpostz);
-		WRITEINT32(save_p, players[i].starpostnum);
-		WRITEANGLE(save_p, players[i].starpostangle);
-		WRITEFIXED(save_p, players[i].starpostscale);
-
-		WRITEANGLE(save_p, players[i].angle_pos);
-		WRITEANGLE(save_p, players[i].old_angle_pos);
-
-		WRITEINT32(save_p, players[i].flyangle);
-		WRITEUINT32(save_p, players[i].drilltimer);
-		WRITEINT32(save_p, players[i].linkcount);
-		WRITEUINT32(save_p, players[i].linktimer);
-		WRITEINT32(save_p, players[i].anotherflyangle);
-		WRITEUINT32(save_p, players[i].nightstime);
-		WRITEUINT32(save_p, players[i].bumpertime);
-		WRITEINT32(save_p, players[i].drillmeter);
-		WRITEUINT8(save_p, players[i].drilldelay);
-		WRITEUINT8(save_p, players[i].bonustime);
-		WRITEFIXED(save_p, players[i].oldscale);
-		WRITEUINT8(save_p, players[i].mare);
-		WRITEUINT8(save_p, players[i].marelap);
-		WRITEUINT8(save_p, players[i].marebonuslap);
-		WRITEUINT32(save_p, players[i].marebegunat);
-		WRITEUINT32(save_p, players[i].lastmaretime);
-		WRITEUINT32(save_p, players[i].startedtime);
-		WRITEUINT32(save_p, players[i].finishedtime);
-		WRITEUINT32(save_p, players[i].lapbegunat);
-		WRITEUINT32(save_p, players[i].lapstartedtime);
-		WRITEINT16(save_p, players[i].finishedspheres);
-		WRITEINT16(save_p, players[i].finishedrings);
-		WRITEUINT32(save_p, players[i].marescore);
-		WRITEUINT32(save_p, players[i].lastmarescore);
-		WRITEUINT32(save_p, players[i].totalmarescore);
-		WRITEUINT8(save_p, players[i].lastmare);
-		WRITEUINT8(save_p, players[i].lastmarelap);
-		WRITEUINT8(save_p, players[i].lastmarebonuslap);
-		WRITEUINT8(save_p, players[i].totalmarelap);
-		WRITEUINT8(save_p, players[i].totalmarebonuslap);
-		WRITEINT32(save_p, players[i].maxlink);
-		WRITEUINT8(save_p, players[i].texttimer);
-		WRITEUINT8(save_p, players[i].textvar);
+		P_WriteINT32(save_p, players[i].ctfteam);
+		P_WriteUINT16(save_p, players[i].gotflag);
+
+		P_WriteINT32(save_p, players[i].weapondelay);
+		P_WriteINT32(save_p, players[i].tossdelay);
+
+		P_WriteUINT32(save_p, players[i].starposttime);
+		P_WriteINT16(save_p, players[i].starpostx);
+		P_WriteINT16(save_p, players[i].starposty);
+		P_WriteINT16(save_p, players[i].starpostz);
+		P_WriteINT32(save_p, players[i].starpostnum);
+		P_WriteAngle(save_p, players[i].starpostangle);
+		P_WriteFixed(save_p, players[i].starpostscale);
+
+		P_WriteAngle(save_p, players[i].angle_pos);
+		P_WriteAngle(save_p, players[i].old_angle_pos);
+
+		P_WriteINT32(save_p, players[i].flyangle);
+		P_WriteUINT32(save_p, players[i].drilltimer);
+		P_WriteINT32(save_p, players[i].linkcount);
+		P_WriteUINT32(save_p, players[i].linktimer);
+		P_WriteINT32(save_p, players[i].anotherflyangle);
+		P_WriteUINT32(save_p, players[i].nightstime);
+		P_WriteUINT32(save_p, players[i].bumpertime);
+		P_WriteINT32(save_p, players[i].drillmeter);
+		P_WriteUINT8(save_p, players[i].drilldelay);
+		P_WriteUINT8(save_p, players[i].bonustime);
+		P_WriteFixed(save_p, players[i].oldscale);
+		P_WriteUINT8(save_p, players[i].mare);
+		P_WriteUINT8(save_p, players[i].marelap);
+		P_WriteUINT8(save_p, players[i].marebonuslap);
+		P_WriteUINT32(save_p, players[i].marebegunat);
+		P_WriteUINT32(save_p, players[i].lastmaretime);
+		P_WriteUINT32(save_p, players[i].startedtime);
+		P_WriteUINT32(save_p, players[i].finishedtime);
+		P_WriteUINT32(save_p, players[i].lapbegunat);
+		P_WriteUINT32(save_p, players[i].lapstartedtime);
+		P_WriteINT16(save_p, players[i].finishedspheres);
+		P_WriteINT16(save_p, players[i].finishedrings);
+		P_WriteUINT32(save_p, players[i].marescore);
+		P_WriteUINT32(save_p, players[i].lastmarescore);
+		P_WriteUINT32(save_p, players[i].totalmarescore);
+		P_WriteUINT8(save_p, players[i].lastmare);
+		P_WriteUINT8(save_p, players[i].lastmarelap);
+		P_WriteUINT8(save_p, players[i].lastmarebonuslap);
+		P_WriteUINT8(save_p, players[i].totalmarelap);
+		P_WriteUINT8(save_p, players[i].totalmarebonuslap);
+		P_WriteINT32(save_p, players[i].maxlink);
+		P_WriteUINT8(save_p, players[i].texttimer);
+		P_WriteUINT8(save_p, players[i].textvar);
 
 		if (players[i].capsule)
 			flags |= CAPSULE;
@@ -298,73 +551,73 @@ static void P_NetArchivePlayers(void)
 		if (players[i].drone)
 			flags |= DRONE;
 
-		WRITEINT16(save_p, players[i].lastsidehit);
-		WRITEINT16(save_p, players[i].lastlinehit);
+		P_WriteINT16(save_p, players[i].lastsidehit);
+		P_WriteINT16(save_p, players[i].lastlinehit);
 
-		WRITEUINT32(save_p, players[i].losstime);
+		P_WriteUINT32(save_p, players[i].losstime);
 
-		WRITEUINT8(save_p, players[i].timeshit);
+		P_WriteUINT8(save_p, players[i].timeshit);
 
-		WRITEINT32(save_p, players[i].onconveyor);
+		P_WriteINT32(save_p, players[i].onconveyor);
 
-		WRITEUINT32(save_p, players[i].jointime);
-		WRITEUINT32(save_p, players[i].quittime);
+		P_WriteUINT32(save_p, players[i].jointime);
+		P_WriteUINT32(save_p, players[i].quittime);
 
-		WRITEUINT16(save_p, flags);
+		P_WriteUINT16(save_p, flags);
 
 		if (flags & CAPSULE)
-			WRITEUINT32(save_p, players[i].capsule->mobjnum);
+			P_WriteUINT32(save_p, players[i].capsule->mobjnum);
 
 		if (flags & FIRSTAXIS)
-			WRITEUINT32(save_p, players[i].axis1->mobjnum);
+			P_WriteUINT32(save_p, players[i].axis1->mobjnum);
 
 		if (flags & SECONDAXIS)
-			WRITEUINT32(save_p, players[i].axis2->mobjnum);
+			P_WriteUINT32(save_p, players[i].axis2->mobjnum);
 
 		if (flags & AWAYVIEW)
-			WRITEUINT32(save_p, players[i].awayviewmobj->mobjnum);
+			P_WriteUINT32(save_p, players[i].awayviewmobj->mobjnum);
 
 		if (flags & FOLLOW)
-			WRITEUINT32(save_p, players[i].followmobj->mobjnum);
+			P_WriteUINT32(save_p, players[i].followmobj->mobjnum);
 
 		if (flags & DRONE)
-			WRITEUINT32(save_p, players[i].drone->mobjnum);
-
-		WRITEFIXED(save_p, players[i].camerascale);
-		WRITEFIXED(save_p, players[i].shieldscale);
-
-		WRITEUINT8(save_p, players[i].charability);
-		WRITEUINT8(save_p, players[i].charability2);
-		WRITEUINT32(save_p, players[i].charflags);
-		WRITEUINT32(save_p, (UINT32)players[i].thokitem);
-		WRITEUINT32(save_p, (UINT32)players[i].spinitem);
-		WRITEUINT32(save_p, (UINT32)players[i].revitem);
-		WRITEUINT32(save_p, (UINT32)players[i].followitem);
-		WRITEFIXED(save_p, players[i].actionspd);
-		WRITEFIXED(save_p, players[i].mindash);
-		WRITEFIXED(save_p, players[i].maxdash);
-		WRITEFIXED(save_p, players[i].normalspeed);
-		WRITEFIXED(save_p, players[i].runspeed);
-		WRITEUINT8(save_p, players[i].thrustfactor);
-		WRITEUINT8(save_p, players[i].accelstart);
-		WRITEUINT8(save_p, players[i].acceleration);
-		WRITEFIXED(save_p, players[i].jumpfactor);
-		WRITEFIXED(save_p, players[i].height);
-		WRITEFIXED(save_p, players[i].spinheight);
+			P_WriteUINT32(save_p, players[i].drone->mobjnum);
+
+		P_WriteFixed(save_p, players[i].camerascale);
+		P_WriteFixed(save_p, players[i].shieldscale);
+
+		P_WriteUINT8(save_p, players[i].charability);
+		P_WriteUINT8(save_p, players[i].charability2);
+		P_WriteUINT32(save_p, players[i].charflags);
+		P_WriteUINT32(save_p, (UINT32)players[i].thokitem);
+		P_WriteUINT32(save_p, (UINT32)players[i].spinitem);
+		P_WriteUINT32(save_p, (UINT32)players[i].revitem);
+		P_WriteUINT32(save_p, (UINT32)players[i].followitem);
+		P_WriteFixed(save_p, players[i].actionspd);
+		P_WriteFixed(save_p, players[i].mindash);
+		P_WriteFixed(save_p, players[i].maxdash);
+		P_WriteFixed(save_p, players[i].normalspeed);
+		P_WriteFixed(save_p, players[i].runspeed);
+		P_WriteUINT8(save_p, players[i].thrustfactor);
+		P_WriteUINT8(save_p, players[i].accelstart);
+		P_WriteUINT8(save_p, players[i].acceleration);
+		P_WriteFixed(save_p, players[i].jumpfactor);
+		P_WriteFixed(save_p, players[i].height);
+		P_WriteFixed(save_p, players[i].spinheight);
 	}
 }
 
-static void P_NetUnArchivePlayers(void)
+static void P_NetUnArchivePlayers(save_t *save_p)
 {
 	INT32 i, j;
 	UINT16 flags;
 
-	if (READUINT32(save_p) != ARCHIVEBLOCK_PLAYERS)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_PLAYERS)
 		I_Error("Bad $$$.sav at archive block Players");
 
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		adminplayers[i] = (INT32)READSINT8(save_p);
+		adminplayers[i] = (INT32)P_ReadSINT8(save_p);
 
 		// Do NOT memset player struct to 0
 		// other areas may initialize data elsewhere
@@ -374,198 +627,199 @@ static void P_NetUnArchivePlayers(void)
 
 		// NOTE: sending tics should (hopefully) no longer be necessary
 
-		READSTRINGN(save_p, player_names[i], MAXPLAYERNAME);
-		players[i].angleturn = READINT16(save_p);
-		players[i].oldrelangleturn = READINT16(save_p);
-		players[i].aiming = READANGLE(save_p);
-		players[i].drawangle = READANGLE(save_p);
-		players[i].viewrollangle = READANGLE(save_p);
-		players[i].awayviewaiming = READANGLE(save_p);
-		players[i].awayviewtics = READINT32(save_p);
-		players[i].rings = READINT16(save_p);
-		players[i].spheres = READINT16(save_p);
-
-		players[i].pity = READSINT8(save_p);
-		players[i].currentweapon = READINT32(save_p);
-		players[i].ringweapons = READINT32(save_p);
-
-		players[i].ammoremoval = READUINT16(save_p);
-		players[i].ammoremovaltimer = READUINT32(save_p);
-		players[i].ammoremovalweapon = READINT32(save_p);
+		P_ReadStringN(save_p, player_names[i], MAXPLAYERNAME);
+		players[i].angleturn = P_ReadINT16(save_p);
+		players[i].oldrelangleturn = P_ReadINT16(save_p);
+		players[i].aiming = P_ReadAngle(save_p);
+		players[i].drawangle = P_ReadAngle(save_p);
+		players[i].viewrollangle = P_ReadAngle(save_p);
+		players[i].awayviewaiming = P_ReadAngle(save_p);
+		players[i].awayviewtics = P_ReadINT32(save_p);
+		players[i].rings = P_ReadINT16(save_p);
+		players[i].spheres = P_ReadINT16(save_p);
+
+		players[i].pity = P_ReadSINT8(save_p);
+		players[i].currentweapon = P_ReadINT32(save_p);
+		players[i].ringweapons = P_ReadINT32(save_p);
+
+		players[i].ammoremoval = P_ReadUINT16(save_p);
+		players[i].ammoremovaltimer = P_ReadUINT32(save_p);
+		players[i].ammoremovalweapon = P_ReadINT32(save_p);
 
 		for (j = 0; j < NUMPOWERS; j++)
-			players[i].powers[j] = READUINT16(save_p);
-
-		players[i].playerstate = READUINT8(save_p);
-		players[i].pflags = READUINT32(save_p);
-		players[i].panim = READUINT8(save_p);
-		players[i].stronganim = READUINT8(save_p);
-		players[i].spectator = READUINT8(save_p);
-		players[i].muted = READUINT8(save_p);
-
-		players[i].flashpal = READUINT16(save_p);
-		players[i].flashcount = READUINT16(save_p);
-
-		players[i].skincolor = READUINT16(save_p);
-		players[i].skin = READINT32(save_p);
-		players[i].availabilities = READUINT32(save_p);
-		players[i].score = READUINT32(save_p);
-		players[i].recordscore = READUINT32(save_p);
-		players[i].dashspeed = READFIXED(save_p); // dashing speed
-		players[i].lives = READSINT8(save_p);
-		players[i].continues = READSINT8(save_p); // continues that player has acquired
-		players[i].xtralife = READSINT8(save_p); // Ring Extra Life counter
-		players[i].gotcontinue = READUINT8(save_p); // got continue from stage
-		players[i].speed = READFIXED(save_p); // Player's speed (distance formula of MOMX and MOMY values)
-		players[i].secondjump = READUINT8(save_p);
-		players[i].fly1 = READUINT8(save_p); // Tails flying
-		players[i].scoreadd = READUINT8(save_p); // Used for multiple enemy attack bonus
-		players[i].glidetime = READUINT32(save_p); // Glide counter for thrust
-		players[i].climbing = READUINT8(save_p); // Climbing on the wall
-		players[i].deadtimer = READINT32(save_p); // End game if game over lasts too long
-		players[i].exiting = READUINT32(save_p); // Exitlevel timer
-		players[i].homing = READUINT8(save_p); // Are you homing?
-		players[i].dashmode = READUINT32(save_p); // counter for dashmode ability
-		players[i].skidtime = READUINT32(save_p); // Skid timer
+			players[i].powers[j] = P_ReadUINT16(save_p);
+
+		players[i].playerstate = P_ReadUINT8(save_p);
+		players[i].pflags = P_ReadUINT32(save_p);
+		players[i].panim = P_ReadUINT8(save_p);
+		players[i].stronganim = P_ReadUINT8(save_p);
+		players[i].spectator = P_ReadUINT8(save_p);
+		players[i].muted = P_ReadUINT8(save_p);
+
+		players[i].flashpal = P_ReadUINT16(save_p);
+		players[i].flashcount = P_ReadUINT16(save_p);
+
+		players[i].skincolor = P_ReadUINT16(save_p);
+		players[i].skin = P_ReadINT32(save_p);
+		players[i].availabilities = P_ReadUINT32(save_p);
+		players[i].score = P_ReadUINT32(save_p);
+		players[i].recordscore = P_ReadUINT32(save_p);
+		players[i].dashspeed = P_ReadFixed(save_p); // dashing speed
+		players[i].lives = P_ReadSINT8(save_p);
+		players[i].continues = P_ReadSINT8(save_p); // continues that player has acquired
+		players[i].xtralife = P_ReadSINT8(save_p); // Ring Extra Life counter
+		players[i].gotcontinue = P_ReadUINT8(save_p); // got continue from stage
+		players[i].speed = P_ReadFixed(save_p); // Player's speed (distance formula of MOMX and MOMY values)
+		players[i].secondjump = P_ReadUINT8(save_p);
+		players[i].fly1 = P_ReadUINT8(save_p); // Tails flying
+		players[i].scoreadd = P_ReadUINT8(save_p); // Used for multiple enemy attack bonus
+		players[i].glidetime = P_ReadUINT32(save_p); // Glide counter for thrust
+		players[i].climbing = P_ReadUINT8(save_p); // Climbing on the wall
+		players[i].deadtimer = P_ReadINT32(save_p); // End game if game over lasts too long
+		players[i].exiting = P_ReadUINT32(save_p); // Exitlevel timer
+		players[i].homing = P_ReadUINT8(save_p); // Are you homing?
+		players[i].dashmode = P_ReadUINT32(save_p); // counter for dashmode ability
+		players[i].skidtime = P_ReadUINT32(save_p); // Skid timer
 
 		//////////
 		// Bots //
 		//////////
-		players[i].bot = READUINT8(save_p);
+		players[i].bot = P_ReadUINT8(save_p);
+		players[i].botleader = LoadPlayer(P_ReadUINT32(save_p));
 
-		players[i].botmem.lastForward = READUINT8(save_p);
-		players[i].botmem.lastBlocked = READUINT8(save_p);
-		players[i].botmem.catchup_tics = READUINT8(save_p);
-		players[i].botmem.thinkstate = READUINT8(save_p);
-		players[i].removing = READUINT8(save_p);
+		players[i].botmem.lastForward = P_ReadUINT8(save_p);
+		players[i].botmem.lastBlocked = P_ReadUINT8(save_p);
+		players[i].botmem.catchup_tics = P_ReadUINT8(save_p);
+		players[i].botmem.thinkstate = P_ReadUINT8(save_p);
+		players[i].removing = P_ReadUINT8(save_p);
 
-		players[i].blocked = READUINT8(save_p);
-		players[i].lastbuttons = READUINT16(save_p);
+		players[i].blocked = P_ReadUINT8(save_p);
+		players[i].lastbuttons = P_ReadUINT16(save_p);
 
 		////////////////////////////
 		// Conveyor Belt Movement //
 		////////////////////////////
-		players[i].cmomx = READFIXED(save_p); // Conveyor momx
-		players[i].cmomy = READFIXED(save_p); // Conveyor momy
-		players[i].rmomx = READFIXED(save_p); // "Real" momx (momx - cmomx)
-		players[i].rmomy = READFIXED(save_p); // "Real" momy (momy - cmomy)
+		players[i].cmomx = P_ReadFixed(save_p); // Conveyor momx
+		players[i].cmomy = P_ReadFixed(save_p); // Conveyor momy
+		players[i].rmomx = P_ReadFixed(save_p); // "Real" momx (momx - cmomx)
+		players[i].rmomy = P_ReadFixed(save_p); // "Real" momy (momy - cmomy)
 
 		/////////////////////
 		// Race Mode Stuff //
 		/////////////////////
-		players[i].numboxes = READINT16(save_p); // Number of item boxes obtained for Race Mode
-		players[i].totalring = READINT16(save_p); // Total number of rings obtained for Race Mode
-		players[i].realtime = READUINT32(save_p); // integer replacement for leveltime
-		players[i].laps = READUINT8(save_p); // Number of laps (optional)
+		players[i].numboxes = P_ReadINT16(save_p); // Number of item boxes obtained for Race Mode
+		players[i].totalring = P_ReadINT16(save_p); // Total number of rings obtained for Race Mode
+		players[i].realtime = P_ReadUINT32(save_p); // integer replacement for leveltime
+		players[i].laps = P_ReadUINT8(save_p); // Number of laps (optional)
 
 		////////////////////
 		// CTF Mode Stuff //
 		////////////////////
-		players[i].ctfteam = READINT32(save_p); // 1 == Red, 2 == Blue
-		players[i].gotflag = READUINT16(save_p); // 1 == Red, 2 == Blue Do you have the flag?
-
-		players[i].weapondelay = READINT32(save_p);
-		players[i].tossdelay = READINT32(save_p);
-
-		players[i].starposttime = READUINT32(save_p);
-		players[i].starpostx = READINT16(save_p);
-		players[i].starposty = READINT16(save_p);
-		players[i].starpostz = READINT16(save_p);
-		players[i].starpostnum = READINT32(save_p);
-		players[i].starpostangle = READANGLE(save_p);
-		players[i].starpostscale = READFIXED(save_p);
-
-		players[i].angle_pos = READANGLE(save_p);
-		players[i].old_angle_pos = READANGLE(save_p);
-
-		players[i].flyangle = READINT32(save_p);
-		players[i].drilltimer = READUINT32(save_p);
-		players[i].linkcount = READINT32(save_p);
-		players[i].linktimer = READUINT32(save_p);
-		players[i].anotherflyangle = READINT32(save_p);
-		players[i].nightstime = READUINT32(save_p);
-		players[i].bumpertime = READUINT32(save_p);
-		players[i].drillmeter = READINT32(save_p);
-		players[i].drilldelay = READUINT8(save_p);
-		players[i].bonustime = (boolean)READUINT8(save_p);
-		players[i].oldscale = READFIXED(save_p);
-		players[i].mare = READUINT8(save_p);
-		players[i].marelap = READUINT8(save_p);
-		players[i].marebonuslap = READUINT8(save_p);
-		players[i].marebegunat = READUINT32(save_p);
-		players[i].lastmaretime = READUINT32(save_p);
-		players[i].startedtime = READUINT32(save_p);
-		players[i].finishedtime = READUINT32(save_p);
-		players[i].lapbegunat = READUINT32(save_p);
-		players[i].lapstartedtime = READUINT32(save_p);
-		players[i].finishedspheres = READINT16(save_p);
-		players[i].finishedrings = READINT16(save_p);
-		players[i].marescore = READUINT32(save_p);
-		players[i].lastmarescore = READUINT32(save_p);
-		players[i].totalmarescore = READUINT32(save_p);
-		players[i].lastmare = READUINT8(save_p);
-		players[i].lastmarelap = READUINT8(save_p);
-		players[i].lastmarebonuslap = READUINT8(save_p);
-		players[i].totalmarelap = READUINT8(save_p);
-		players[i].totalmarebonuslap = READUINT8(save_p);
-		players[i].maxlink = READINT32(save_p);
-		players[i].texttimer = READUINT8(save_p);
-		players[i].textvar = READUINT8(save_p);
-
-		players[i].lastsidehit = READINT16(save_p);
-		players[i].lastlinehit = READINT16(save_p);
-
-		players[i].losstime = READUINT32(save_p);
-
-		players[i].timeshit = READUINT8(save_p);
-
-		players[i].onconveyor = READINT32(save_p);
-
-		players[i].jointime = READUINT32(save_p);
-		players[i].quittime = READUINT32(save_p);
-
-		flags = READUINT16(save_p);
+		players[i].ctfteam = P_ReadINT32(save_p); // 1 == Red, 2 == Blue
+		players[i].gotflag = P_ReadUINT16(save_p); // 1 == Red, 2 == Blue Do you have the flag?
+
+		players[i].weapondelay = P_ReadINT32(save_p);
+		players[i].tossdelay = P_ReadINT32(save_p);
+
+		players[i].starposttime = P_ReadUINT32(save_p);
+		players[i].starpostx = P_ReadINT16(save_p);
+		players[i].starposty = P_ReadINT16(save_p);
+		players[i].starpostz = P_ReadINT16(save_p);
+		players[i].starpostnum = P_ReadINT32(save_p);
+		players[i].starpostangle = P_ReadAngle(save_p);
+		players[i].starpostscale = P_ReadFixed(save_p);
+
+		players[i].angle_pos = P_ReadAngle(save_p);
+		players[i].old_angle_pos = P_ReadAngle(save_p);
+
+		players[i].flyangle = P_ReadINT32(save_p);
+		players[i].drilltimer = P_ReadUINT32(save_p);
+		players[i].linkcount = P_ReadINT32(save_p);
+		players[i].linktimer = P_ReadUINT32(save_p);
+		players[i].anotherflyangle = P_ReadINT32(save_p);
+		players[i].nightstime = P_ReadUINT32(save_p);
+		players[i].bumpertime = P_ReadUINT32(save_p);
+		players[i].drillmeter = P_ReadINT32(save_p);
+		players[i].drilldelay = P_ReadUINT8(save_p);
+		players[i].bonustime = (boolean)P_ReadUINT8(save_p);
+		players[i].oldscale = P_ReadFixed(save_p);
+		players[i].mare = P_ReadUINT8(save_p);
+		players[i].marelap = P_ReadUINT8(save_p);
+		players[i].marebonuslap = P_ReadUINT8(save_p);
+		players[i].marebegunat = P_ReadUINT32(save_p);
+		players[i].lastmaretime = P_ReadUINT32(save_p);
+		players[i].startedtime = P_ReadUINT32(save_p);
+		players[i].finishedtime = P_ReadUINT32(save_p);
+		players[i].lapbegunat = P_ReadUINT32(save_p);
+		players[i].lapstartedtime = P_ReadUINT32(save_p);
+		players[i].finishedspheres = P_ReadINT16(save_p);
+		players[i].finishedrings = P_ReadINT16(save_p);
+		players[i].marescore = P_ReadUINT32(save_p);
+		players[i].lastmarescore = P_ReadUINT32(save_p);
+		players[i].totalmarescore = P_ReadUINT32(save_p);
+		players[i].lastmare = P_ReadUINT8(save_p);
+		players[i].lastmarelap = P_ReadUINT8(save_p);
+		players[i].lastmarebonuslap = P_ReadUINT8(save_p);
+		players[i].totalmarelap = P_ReadUINT8(save_p);
+		players[i].totalmarebonuslap = P_ReadUINT8(save_p);
+		players[i].maxlink = P_ReadINT32(save_p);
+		players[i].texttimer = P_ReadUINT8(save_p);
+		players[i].textvar = P_ReadUINT8(save_p);
+
+		players[i].lastsidehit = P_ReadINT16(save_p);
+		players[i].lastlinehit = P_ReadINT16(save_p);
+
+		players[i].losstime = P_ReadUINT32(save_p);
+
+		players[i].timeshit = P_ReadUINT8(save_p);
+
+		players[i].onconveyor = P_ReadINT32(save_p);
+
+		players[i].jointime = P_ReadUINT32(save_p);
+		players[i].quittime = P_ReadUINT32(save_p);
+
+		flags = P_ReadUINT16(save_p);
 
 		if (flags & CAPSULE)
-			players[i].capsule = (mobj_t *)(size_t)READUINT32(save_p);
+			players[i].capsule = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 
 		if (flags & FIRSTAXIS)
-			players[i].axis1 = (mobj_t *)(size_t)READUINT32(save_p);
+			players[i].axis1 = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 
 		if (flags & SECONDAXIS)
-			players[i].axis2 = (mobj_t *)(size_t)READUINT32(save_p);
+			players[i].axis2 = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 
 		if (flags & AWAYVIEW)
-			players[i].awayviewmobj = (mobj_t *)(size_t)READUINT32(save_p);
+			players[i].awayviewmobj = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 
 		if (flags & FOLLOW)
-			players[i].followmobj = (mobj_t *)(size_t)READUINT32(save_p);
+			players[i].followmobj = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 
 		if (flags & DRONE)
-			players[i].drone = (mobj_t *)(size_t)READUINT32(save_p);
+			players[i].drone = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 
-		players[i].camerascale = READFIXED(save_p);
-		players[i].shieldscale = READFIXED(save_p);
+		players[i].camerascale = P_ReadFixed(save_p);
+		players[i].shieldscale = P_ReadFixed(save_p);
 
 		//SetPlayerSkinByNum(i, players[i].skin);
-		players[i].charability = READUINT8(save_p);
-		players[i].charability2 = READUINT8(save_p);
-		players[i].charflags = READUINT32(save_p);
-		players[i].thokitem = (mobjtype_t)READUINT32(save_p);
-		players[i].spinitem = (mobjtype_t)READUINT32(save_p);
-		players[i].revitem = (mobjtype_t)READUINT32(save_p);
-		players[i].followitem = (mobjtype_t)READUINT32(save_p);
-		players[i].actionspd = READFIXED(save_p);
-		players[i].mindash = READFIXED(save_p);
-		players[i].maxdash = READFIXED(save_p);
-		players[i].normalspeed = READFIXED(save_p);
-		players[i].runspeed = READFIXED(save_p);
-		players[i].thrustfactor = READUINT8(save_p);
-		players[i].accelstart = READUINT8(save_p);
-		players[i].acceleration = READUINT8(save_p);
-		players[i].jumpfactor = READFIXED(save_p);
-		players[i].height = READFIXED(save_p);
-		players[i].spinheight = READFIXED(save_p);
+		players[i].charability = P_ReadUINT8(save_p);
+		players[i].charability2 = P_ReadUINT8(save_p);
+		players[i].charflags = P_ReadUINT32(save_p);
+		players[i].thokitem = P_ReadUINT32(save_p);
+		players[i].spinitem = P_ReadUINT32(save_p);
+		players[i].revitem = P_ReadUINT32(save_p);
+		players[i].followitem = P_ReadUINT32(save_p);
+		players[i].actionspd = P_ReadFixed(save_p);
+		players[i].mindash = P_ReadFixed(save_p);
+		players[i].maxdash = P_ReadFixed(save_p);
+		players[i].normalspeed = P_ReadFixed(save_p);
+		players[i].runspeed = P_ReadFixed(save_p);
+		players[i].thrustfactor = P_ReadUINT8(save_p);
+		players[i].accelstart = P_ReadUINT8(save_p);
+		players[i].acceleration = P_ReadUINT8(save_p);
+		players[i].jumpfactor = P_ReadFixed(save_p);
+		players[i].height = P_ReadFixed(save_p);
+		players[i].spinheight = P_ReadFixed(save_p);
 
 		players[i].viewheight = 41*players[i].height/48; // scale cannot be factored in at this point
 	}
@@ -668,12 +922,12 @@ static void ClearNetColormaps(void)
 	net_colormaps = NULL;
 }
 
-static void P_NetArchiveColormaps(void)
+static void P_NetArchiveColormaps(save_t *save_p)
 {
 	// We save and then we clean up our colormap mess
 	extracolormap_t *exc, *exc_next;
 	UINT32 i = 0;
-	WRITEUINT32(save_p, num_net_colormaps); // save for safety
+	P_WriteUINT32(save_p, num_net_colormaps); // save for safety
 
 	for (exc = net_colormaps; i < num_net_colormaps; i++, exc = exc_next)
 	{
@@ -682,15 +936,15 @@ static void P_NetArchiveColormaps(void)
 		if (!exc)
 			exc = R_CreateDefaultColormap(false);
 
-		WRITEUINT8(save_p, exc->fadestart);
-		WRITEUINT8(save_p, exc->fadeend);
-		WRITEUINT8(save_p, exc->flags);
+		P_WriteUINT8(save_p, exc->fadestart);
+		P_WriteUINT8(save_p, exc->fadeend);
+		P_WriteUINT8(save_p, exc->flags);
 
-		WRITEINT32(save_p, exc->rgba);
-		WRITEINT32(save_p, exc->fadergba);
+		P_WriteINT32(save_p, exc->rgba);
+		P_WriteINT32(save_p, exc->fadergba);
 
 #ifdef EXTRACOLORMAPLUMPS
-		WRITESTRINGN(save_p, exc->lumpname, 9);
+		P_WriteStringN(save_p, exc->lumpname, 9);
 #endif
 
 		exc_next = exc->next;
@@ -702,7 +956,7 @@ static void P_NetArchiveColormaps(void)
 	net_colormaps = NULL;
 }
 
-static void P_NetUnArchiveColormaps(void)
+static void P_NetUnArchiveColormaps(save_t *save_p)
 {
 	// When we reach this point, we already populated our list with
 	// dummy colormaps. Now that we are loading the color data,
@@ -710,7 +964,7 @@ static void P_NetUnArchiveColormaps(void)
 	extracolormap_t *exc, *existing_exc, *exc_next = NULL;
 	UINT32 i = 0;
 
-	num_net_colormaps = READUINT32(save_p);
+	num_net_colormaps = P_ReadUINT32(save_p);
 
 	for (exc = net_colormaps; i < num_net_colormaps; i++, exc = exc_next)
 	{
@@ -720,15 +974,15 @@ static void P_NetUnArchiveColormaps(void)
 		char lumpname[9];
 #endif
 
-		fadestart = READUINT8(save_p);
-		fadeend = READUINT8(save_p);
-		flags = READUINT8(save_p);
+		fadestart = P_ReadUINT8(save_p);
+		fadeend = P_ReadUINT8(save_p);
+		flags = P_ReadUINT8(save_p);
 
-		rgba = READINT32(save_p);
-		fadergba = READINT32(save_p);
+		rgba = P_ReadINT32(save_p);
+		fadergba = P_ReadINT32(save_p);
 
 #ifdef EXTRACOLORMAPLUMPS
-		READSTRINGN(save_p, lumpname, 9);
+		P_ReadStringN(save_p, lumpname, 9);
 
 		if (lumpname[0])
 		{
@@ -804,29 +1058,29 @@ static void P_NetUnArchiveColormaps(void)
 	net_colormaps = NULL;
 }
 
-static void P_NetArchiveWaypoints(void)
+static void P_NetArchiveWaypoints(save_t *save_p)
 {
 	INT32 i, j;
 
 	for (i = 0; i < NUMWAYPOINTSEQUENCES; i++)
 	{
-		WRITEUINT16(save_p, numwaypoints[i]);
+		P_WriteUINT16(save_p, numwaypoints[i]);
 		for (j = 0; j < numwaypoints[i]; j++)
-			WRITEUINT32(save_p, waypoints[i][j] ? waypoints[i][j]->mobjnum : 0);
+			P_WriteUINT32(save_p, waypoints[i][j] ? waypoints[i][j]->mobjnum : 0);
 	}
 }
 
-static void P_NetUnArchiveWaypoints(void)
+static void P_NetUnArchiveWaypoints(save_t *save_p)
 {
 	INT32 i, j;
 	UINT32 mobjnum;
 
 	for (i = 0; i < NUMWAYPOINTSEQUENCES; i++)
 	{
-		numwaypoints[i] = READUINT16(save_p);
+		numwaypoints[i] = P_ReadUINT16(save_p);
 		for (j = 0; j < numwaypoints[i]; j++)
 		{
-			mobjnum = READUINT32(save_p);
+			mobjnum = P_ReadUINT32(save_p);
 			waypoints[i][j] = (mobjnum == 0) ? NULL : P_FindNewPosition(mobjnum);
 		}
 	}
@@ -919,7 +1173,11 @@ enum
 	LD_SDMIDLIGHT    = 1<<19,
 	LD_SDBOTLIGHT    = 1<<20,
 	LD_SDREPEATCNT   = 1<<21,
-	LD_SDFLAGS       = 1<<22
+	LD_SDFLAGS       = 1<<22,
+	LD_SDLIGHTABS    = 1<<23,
+	LD_SDTOPLIGHTABS = 1<<24,
+	LD_SDMIDLIGHTABS = 1<<25,
+	LD_SDBOTLIGHTABS = 1<<26
 };
 
 static boolean P_AreArgsEqual(const line_t *li, const line_t *spawnli)
@@ -969,7 +1227,7 @@ static boolean CheckFFloorDiff(const sector_t *ss)
 
 // Special case: save the stats of all modified ffloors along with their ffloor "number"s
 // we don't bother with ffloors that haven't changed, that would just add to savegame even more than is really needed
-static void ArchiveFFloors(const sector_t *ss)
+static void ArchiveFFloors(save_t *save_p, const sector_t *ss)
 {
 	size_t j = 0; // ss->ffloors is saved as ffloor #0, ss->ffloors->next is #1, etc
 	ffloor_t *rover;
@@ -984,19 +1242,19 @@ static void ArchiveFFloors(const sector_t *ss)
 
 		if (fflr_diff)
 		{
-			WRITEUINT16(save_p, j); // save ffloor "number"
-			WRITEUINT8(save_p, fflr_diff);
+			P_WriteUINT16(save_p, j); // save ffloor "number"
+			P_WriteUINT8(save_p, fflr_diff);
 			if (fflr_diff & FD_FLAGS)
-				WRITEUINT32(save_p, rover->fofflags);
+				P_WriteUINT32(save_p, rover->fofflags);
 			if (fflr_diff & FD_ALPHA)
-				WRITEINT16(save_p, rover->alpha);
+				P_WriteINT16(save_p, rover->alpha);
 		}
 		j++;
 	}
-	WRITEUINT16(save_p, 0xffff);
+	P_WriteUINT16(save_p, 0xffff);
 }
 
-static void UnArchiveFFloors(const sector_t *ss)
+static void UnArchiveFFloors(save_t *save_p, const sector_t *ss)
 {
 	UINT16 j = 0; // number of current ffloor in loop
 	UINT16 fflr_i; // saved ffloor "number" of next modified ffloor
@@ -1007,7 +1265,7 @@ static void UnArchiveFFloors(const sector_t *ss)
 	if (!rover) // it is assumed sectors[i].ffloors actually exists, but just in case...
 		I_Error("Sector does not have any ffloors!");
 
-	fflr_i = READUINT16(save_p); // get first modified ffloor's number ready
+	fflr_i = P_ReadUINT16(save_p); // get first modified ffloor's number ready
 	for (;;) // for some reason the usual for (rover = x; ...) thing doesn't work here?
 	{
 		if (fflr_i == 0xffff) // end of modified ffloors list, let's stop already
@@ -1022,21 +1280,21 @@ static void UnArchiveFFloors(const sector_t *ss)
 			continue;
 		}
 
-		fflr_diff = READUINT8(save_p);
+		fflr_diff = P_ReadUINT8(save_p);
 
 		if (fflr_diff & FD_FLAGS)
-			rover->fofflags = READUINT32(save_p);
+			rover->fofflags = P_ReadUINT32(save_p);
 		if (fflr_diff & FD_ALPHA)
-			rover->alpha = READINT16(save_p);
+			rover->alpha = P_ReadINT16(save_p);
 
-		fflr_i = READUINT16(save_p); // get next ffloor "number" ready
+		fflr_i = P_ReadUINT16(save_p); // get next ffloor "number" ready
 
 		j++;
 		rover = rover->next;
 	}
 }
 
-static void ArchiveSectors(void)
+static void ArchiveSectors(save_t *save_p)
 {
 	size_t i, j;
 	const sector_t *ss = sectors;
@@ -1130,102 +1388,102 @@ static void ArchiveSectors(void)
 
 		if (diff)
 		{
-			WRITEUINT32(save_p, i);
-			WRITEUINT8(save_p, diff);
+			P_WriteUINT32(save_p, i);
+			P_WriteUINT8(save_p, diff);
 			if (diff & SD_DIFF2)
-				WRITEUINT8(save_p, diff2);
+				P_WriteUINT8(save_p, diff2);
 			if (diff2 & SD_DIFF3)
-				WRITEUINT8(save_p, diff3);
+				P_WriteUINT8(save_p, diff3);
 			if (diff3 & SD_DIFF4)
-				WRITEUINT8(save_p, diff4);
+				P_WriteUINT8(save_p, diff4);
 			if (diff4 & SD_DIFF5)
-				WRITEUINT8(save_p, diff5);
+				P_WriteUINT8(save_p, diff5);
 			if (diff & SD_FLOORHT)
-				WRITEFIXED(save_p, ss->floorheight);
+				P_WriteFixed(save_p, ss->floorheight);
 			if (diff & SD_CEILHT)
-				WRITEFIXED(save_p, ss->ceilingheight);
+				P_WriteFixed(save_p, ss->ceilingheight);
 			if (diff & SD_FLOORPIC)
-				WRITEMEM(save_p, levelflats[ss->floorpic].name, 8);
+				P_WriteMem(save_p, levelflats[ss->floorpic].name, 8);
 			if (diff & SD_CEILPIC)
-				WRITEMEM(save_p, levelflats[ss->ceilingpic].name, 8);
+				P_WriteMem(save_p, levelflats[ss->ceilingpic].name, 8);
 			if (diff & SD_LIGHT)
-				WRITEINT16(save_p, ss->lightlevel);
+				P_WriteINT16(save_p, ss->lightlevel);
 			if (diff & SD_SPECIAL)
-				WRITEINT16(save_p, ss->special);
+				P_WriteINT16(save_p, ss->special);
 			if (diff2 & SD_FXOFFS)
-				WRITEFIXED(save_p, ss->floorxoffset);
+				P_WriteFixed(save_p, ss->floorxoffset);
 			if (diff2 & SD_FYOFFS)
-				WRITEFIXED(save_p, ss->flooryoffset);
+				P_WriteFixed(save_p, ss->flooryoffset);
 			if (diff2 & SD_CXOFFS)
-				WRITEFIXED(save_p, ss->ceilingxoffset);
+				P_WriteFixed(save_p, ss->ceilingxoffset);
 			if (diff2 & SD_CYOFFS)
-				WRITEFIXED(save_p, ss->ceilingyoffset);
+				P_WriteFixed(save_p, ss->ceilingyoffset);
 			if (diff2 & SD_FLOORANG)
-				WRITEANGLE(save_p, ss->floorangle);
+				P_WriteAngle(save_p, ss->floorangle);
 			if (diff2 & SD_CEILANG)
-				WRITEANGLE(save_p, ss->ceilingangle);
+				P_WriteAngle(save_p, ss->ceilingangle);
 			if (diff2 & SD_TAG)
 			{
-				WRITEUINT32(save_p, ss->tags.count);
+				P_WriteUINT32(save_p, ss->tags.count);
 				for (j = 0; j < ss->tags.count; j++)
-					WRITEINT16(save_p, ss->tags.tags[j]);
+					P_WriteINT16(save_p, ss->tags.tags[j]);
 			}
 
 			if (diff3 & SD_COLORMAP)
-				WRITEUINT32(save_p, CheckAddNetColormapToList(ss->extra_colormap));
+				P_WriteUINT32(save_p, CheckAddNetColormapToList(ss->extra_colormap));
 					// returns existing index if already added, or appends to net_colormaps and returns new index
 			if (diff3 & SD_CRUMBLESTATE)
-				WRITEINT32(save_p, ss->crumblestate);
+				P_WriteINT32(save_p, ss->crumblestate);
 			if (diff3 & SD_FLOORLIGHT)
 			{
-				WRITEINT16(save_p, ss->floorlightlevel);
-				WRITEUINT8(save_p, ss->floorlightabsolute);
+				P_WriteINT16(save_p, ss->floorlightlevel);
+				P_WriteUINT8(save_p, ss->floorlightabsolute);
 			}
 			if (diff3 & SD_CEILLIGHT)
 			{
-				WRITEINT16(save_p, ss->ceilinglightlevel);
-				WRITEUINT8(save_p, ss->ceilinglightabsolute);
+				P_WriteINT16(save_p, ss->ceilinglightlevel);
+				P_WriteUINT8(save_p, ss->ceilinglightabsolute);
 			}
 			if (diff3 & SD_FLAG)
-				WRITEUINT32(save_p, ss->flags);
+				P_WriteUINT32(save_p, ss->flags);
 			if (diff3 & SD_SPECIALFLAG)
-				WRITEUINT32(save_p, ss->specialflags);
+				P_WriteUINT32(save_p, ss->specialflags);
 			if (diff4 & SD_DAMAGETYPE)
-				WRITEUINT8(save_p, ss->damagetype);
+				P_WriteUINT8(save_p, ss->damagetype);
 			if (diff4 & SD_TRIGGERTAG)
-				WRITEINT16(save_p, ss->triggertag);
+				P_WriteINT16(save_p, ss->triggertag);
 			if (diff4 & SD_TRIGGERER)
-				WRITEUINT8(save_p, ss->triggerer);
+				P_WriteUINT8(save_p, ss->triggerer);
 			if (diff4 & SD_FXSCALE)
-				WRITEFIXED(save_p, ss->floorxscale);
+				P_WriteFixed(save_p, ss->floorxscale);
 			if (diff4 & SD_FYSCALE)
-				WRITEFIXED(save_p, ss->flooryscale);
+				P_WriteFixed(save_p, ss->flooryscale);
 			if (diff4 & SD_CXSCALE)
-				WRITEFIXED(save_p, ss->ceilingxscale);
+				P_WriteFixed(save_p, ss->ceilingxscale);
 			if (diff4 & SD_CYSCALE)
-				WRITEFIXED(save_p, ss->ceilingyscale);
+				P_WriteFixed(save_p, ss->ceilingyscale);
 			if (diff5 & SD_GRAVITY)
-				WRITEFIXED(save_p, ss->gravity);
+				P_WriteFixed(save_p, ss->gravity);
 			if (diff5 & SD_FLOORPORTAL)
-				WRITEUINT32(save_p, ss->portal_floor);
+				P_WriteUINT32(save_p, ss->portal_floor);
 			if (diff5 & SD_CEILPORTAL)
-				WRITEUINT32(save_p, ss->portal_ceiling);
+				P_WriteUINT32(save_p, ss->portal_ceiling);
 			if (diff & SD_FFLOORS)
-				ArchiveFFloors(ss);
+				ArchiveFFloors(save_p, ss);
 		}
 	}
 
-	WRITEUINT32(save_p, 0xffffffff);
+	P_WriteUINT32(save_p, 0xffffffff);
 }
 
-static void UnArchiveSectors(void)
+static void UnArchiveSectors(save_t *save_p)
 {
 	UINT32 i;
 	UINT16 j;
 	UINT8 diff, diff2, diff3, diff4, diff5;
 	for (;;)
 	{
-		i = READUINT32(save_p);
+		i = P_ReadUINT32(save_p);
 
 		if (i == 0xffffffff)
 			break;
@@ -1233,58 +1491,58 @@ static void UnArchiveSectors(void)
 		if (i > numsectors)
 			I_Error("Invalid sector number %u from server (expected end at %s)", i, sizeu1(numsectors));
 
-		diff = READUINT8(save_p);
+		diff = P_ReadUINT8(save_p);
 		if (diff & SD_DIFF2)
-			diff2 = READUINT8(save_p);
+			diff2 = P_ReadUINT8(save_p);
 		else
 			diff2 = 0;
 		if (diff2 & SD_DIFF3)
-			diff3 = READUINT8(save_p);
+			diff3 = P_ReadUINT8(save_p);
 		else
 			diff3 = 0;
 		if (diff3 & SD_DIFF4)
-			diff4 = READUINT8(save_p);
+			diff4 = P_ReadUINT8(save_p);
 		else
 			diff4 = 0;
 		if (diff4 & SD_DIFF5)
-			diff5 = READUINT8(save_p);
+			diff5 = P_ReadUINT8(save_p);
 		else
 			diff5 = 0;
 
 		if (diff & SD_FLOORHT)
-			sectors[i].floorheight = READFIXED(save_p);
+			sectors[i].floorheight = P_ReadFixed(save_p);
 		if (diff & SD_CEILHT)
-			sectors[i].ceilingheight = READFIXED(save_p);
+			sectors[i].ceilingheight = P_ReadFixed(save_p);
 		if (diff & SD_FLOORPIC)
 		{
-			sectors[i].floorpic = P_AddLevelFlatRuntime((char *)save_p);
-			save_p += 8;
+			sectors[i].floorpic = P_AddLevelFlatRuntime((char *)&save_p->buf[save_p->pos]);
+			save_p->pos += 8;
 		}
 		if (diff & SD_CEILPIC)
 		{
-			sectors[i].ceilingpic = P_AddLevelFlatRuntime((char *)save_p);
-			save_p += 8;
+			sectors[i].ceilingpic = P_AddLevelFlatRuntime((char *)&save_p->buf[save_p->pos]);
+			save_p->pos += 8;
 		}
 		if (diff & SD_LIGHT)
-			sectors[i].lightlevel = READINT16(save_p);
+			sectors[i].lightlevel = P_ReadINT16(save_p);
 		if (diff & SD_SPECIAL)
-			sectors[i].special = READINT16(save_p);
+			sectors[i].special = P_ReadINT16(save_p);
 
 		if (diff2 & SD_FXOFFS)
-			sectors[i].floorxoffset = READFIXED(save_p);
+			sectors[i].floorxoffset = P_ReadFixed(save_p);
 		if (diff2 & SD_FYOFFS)
-			sectors[i].flooryoffset = READFIXED(save_p);
+			sectors[i].flooryoffset = P_ReadFixed(save_p);
 		if (diff2 & SD_CXOFFS)
-			sectors[i].ceilingxoffset = READFIXED(save_p);
+			sectors[i].ceilingxoffset = P_ReadFixed(save_p);
 		if (diff2 & SD_CYOFFS)
-			sectors[i].ceilingyoffset = READFIXED(save_p);
+			sectors[i].ceilingyoffset = P_ReadFixed(save_p);
 		if (diff2 & SD_FLOORANG)
-			sectors[i].floorangle  = READANGLE(save_p);
+			sectors[i].floorangle  = P_ReadAngle(save_p);
 		if (diff2 & SD_CEILANG)
-			sectors[i].ceilingangle = READANGLE(save_p);
+			sectors[i].ceilingangle = P_ReadAngle(save_p);
 		if (diff2 & SD_TAG)
 		{
-			size_t ncount = READUINT32(save_p);
+			size_t ncount = P_ReadUINT32(save_p);
 
 			// Remove entries from global lists.
 			for (j = 0; j < sectors[i].tags.count; j++)
@@ -1298,7 +1556,7 @@ static void UnArchiveSectors(void)
 			}
 
 			for (j = 0; j < ncount; j++)
-				sectors[i].tags.tags[j] = READINT16(save_p);
+				sectors[i].tags.tags[j] = P_ReadINT16(save_p);
 
 			// Add new entries.
 			for (j = 0; j < sectors[i].tags.count; j++)
@@ -1307,49 +1565,49 @@ static void UnArchiveSectors(void)
 
 
 		if (diff3 & SD_COLORMAP)
-			sectors[i].extra_colormap = GetNetColormapFromList(READUINT32(save_p));
+			sectors[i].extra_colormap = GetNetColormapFromList(P_ReadUINT32(save_p));
 		if (diff3 & SD_CRUMBLESTATE)
-			sectors[i].crumblestate = READINT32(save_p);
+			sectors[i].crumblestate = P_ReadINT32(save_p);
 		if (diff3 & SD_FLOORLIGHT)
 		{
-			sectors[i].floorlightlevel = READINT16(save_p);
-			sectors[i].floorlightabsolute = READUINT8(save_p);
+			sectors[i].floorlightlevel = P_ReadINT16(save_p);
+			sectors[i].floorlightabsolute = P_ReadUINT8(save_p);
 		}
 		if (diff3 & SD_CEILLIGHT)
 		{
-			sectors[i].ceilinglightlevel = READINT16(save_p);
-			sectors[i].ceilinglightabsolute = READUINT8(save_p);
+			sectors[i].ceilinglightlevel = P_ReadINT16(save_p);
+			sectors[i].ceilinglightabsolute = P_ReadUINT8(save_p);
 		}
 		if (diff3 & SD_FLAG)
 		{
-			sectors[i].flags = READUINT32(save_p);
+			sectors[i].flags = P_ReadUINT32(save_p);
 			CheckForReverseGravity |= (sectors[i].flags & MSF_GRAVITYFLIP);
 		}
 		if (diff3 & SD_SPECIALFLAG)
-			sectors[i].specialflags = READUINT32(save_p);
+			sectors[i].specialflags = P_ReadUINT32(save_p);
 		if (diff4 & SD_DAMAGETYPE)
-			sectors[i].damagetype = READUINT8(save_p);
+			sectors[i].damagetype = P_ReadUINT8(save_p);
 		if (diff4 & SD_TRIGGERTAG)
-			sectors[i].triggertag = READINT16(save_p);
+			sectors[i].triggertag = P_ReadINT16(save_p);
 		if (diff4 & SD_TRIGGERER)
-			sectors[i].triggerer = READUINT8(save_p);
+			sectors[i].triggerer = P_ReadUINT8(save_p);
 		if (diff4 & SD_FXSCALE)
-			sectors[i].floorxscale = READFIXED(save_p);
+			sectors[i].floorxscale = P_ReadFixed(save_p);
 		if (diff4 & SD_FYSCALE)
-			sectors[i].flooryscale = READFIXED(save_p);
+			sectors[i].flooryscale = P_ReadFixed(save_p);
 		if (diff4 & SD_CXSCALE)
-			sectors[i].ceilingxscale = READFIXED(save_p);
+			sectors[i].ceilingxscale = P_ReadFixed(save_p);
 		if (diff4 & SD_CYSCALE)
-			sectors[i].ceilingyscale = READFIXED(save_p);
+			sectors[i].ceilingyscale = P_ReadFixed(save_p);
 		if (diff5 & SD_GRAVITY)
-			sectors[i].gravity = READFIXED(save_p);
+			sectors[i].gravity = P_ReadFixed(save_p);
 		if (diff5 & SD_FLOORPORTAL)
-			sectors[i].portal_floor = READUINT32(save_p);
+			sectors[i].portal_floor = P_ReadUINT32(save_p);
 		if (diff5 & SD_CEILPORTAL)
-			sectors[i].portal_ceiling = READUINT32(save_p);
+			sectors[i].portal_ceiling = P_ReadUINT32(save_p);
 
 		if (diff & SD_FFLOORS)
-			UnArchiveFFloors(&sectors[i]);
+			UnArchiveFFloors(save_p, &sectors[i]);
 	}
 }
 
@@ -1393,52 +1651,84 @@ static UINT32 GetSideDiff(const side_t *si, const side_t *spawnsi)
 		diff |= LD_SDBOTSCALEY;
 	if (si->repeatcnt != spawnsi->repeatcnt)
 		diff |= LD_SDREPEATCNT;
+	if (si->light != spawnsi->light)
+		diff |= LD_SDLIGHT;
+	if (si->light_top != spawnsi->light_top)
+		diff |= LD_SDTOPLIGHT;
+	if (si->light_mid != spawnsi->light_mid)
+		diff |= LD_SDMIDLIGHT;
+	if (si->light_bottom != spawnsi->light_bottom)
+		diff |= LD_SDBOTLIGHT;
+	if (si->lightabsolute != spawnsi->lightabsolute)
+		diff |= LD_SDLIGHTABS;
+	if (si->lightabsolute_top != spawnsi->lightabsolute_top)
+		diff |= LD_SDTOPLIGHTABS;
+	if (si->lightabsolute_mid != spawnsi->lightabsolute_mid)
+		diff |= LD_SDMIDLIGHTABS;
+	if (si->lightabsolute_bottom != spawnsi->lightabsolute_bottom)
+		diff |= LD_SDBOTLIGHTABS;
 	return diff;
 }
 
-static void ArchiveSide(const side_t *si, UINT32 diff)
+static void ArchiveSide(save_t *save_p, const side_t *si, UINT32 diff)
 {
-	WRITEUINT32(save_p, diff);
+	P_WriteUINT32(save_p, diff);
 
 	if (diff & LD_SDTEXOFFX)
-		WRITEFIXED(save_p, si->textureoffset);
+		P_WriteFixed(save_p, si->textureoffset);
 	if (diff & LD_SDTEXOFFY)
-		WRITEFIXED(save_p, si->rowoffset);
+		P_WriteFixed(save_p, si->rowoffset);
 	if (diff & LD_SDTOPTEX)
-		WRITEINT32(save_p, si->toptexture);
+		P_WriteINT32(save_p, si->toptexture);
 	if (diff & LD_SDBOTTEX)
-		WRITEINT32(save_p, si->bottomtexture);
+		P_WriteINT32(save_p, si->bottomtexture);
 	if (diff & LD_SDMIDTEX)
-		WRITEINT32(save_p, si->midtexture);
+		P_WriteINT32(save_p, si->midtexture);
 	if (diff & LD_SDTOPOFFX)
-		WRITEFIXED(save_p, si->offsetx_top);
+		P_WriteFixed(save_p, si->offsetx_top);
 	if (diff & LD_SDMIDOFFX)
-		WRITEFIXED(save_p, si->offsetx_mid);
+		P_WriteFixed(save_p, si->offsetx_mid);
 	if (diff & LD_SDBOTOFFX)
-		WRITEFIXED(save_p, si->offsetx_bottom);
+		P_WriteFixed(save_p, si->offsetx_bottom);
 	if (diff & LD_SDTOPOFFY)
-		WRITEFIXED(save_p, si->offsety_top);
+		P_WriteFixed(save_p, si->offsety_top);
 	if (diff & LD_SDMIDOFFY)
-		WRITEFIXED(save_p, si->offsety_mid);
+		P_WriteFixed(save_p, si->offsety_mid);
 	if (diff & LD_SDBOTOFFY)
-		WRITEFIXED(save_p, si->offsety_bottom);
+		P_WriteFixed(save_p, si->offsety_bottom);
 	if (diff & LD_SDTOPSCALEX)
-		WRITEFIXED(save_p, si->scalex_top);
+		P_WriteFixed(save_p, si->scalex_top);
 	if (diff & LD_SDMIDSCALEX)
-		WRITEFIXED(save_p, si->scalex_mid);
+		P_WriteFixed(save_p, si->scalex_mid);
 	if (diff & LD_SDBOTSCALEX)
-		WRITEFIXED(save_p, si->scalex_bottom);
+		P_WriteFixed(save_p, si->scalex_bottom);
 	if (diff & LD_SDTOPSCALEY)
-		WRITEFIXED(save_p, si->scaley_top);
+		P_WriteFixed(save_p, si->scaley_top);
 	if (diff & LD_SDMIDSCALEY)
-		WRITEFIXED(save_p, si->scaley_mid);
+		P_WriteFixed(save_p, si->scaley_mid);
 	if (diff & LD_SDBOTSCALEY)
-		WRITEFIXED(save_p, si->scaley_bottom);
+		P_WriteFixed(save_p, si->scaley_bottom);
 	if (diff & LD_SDREPEATCNT)
-		WRITEINT16(save_p, si->repeatcnt);
-}
-
-static void ArchiveLines(void)
+		P_WriteINT16(save_p, si->repeatcnt);
+	if (diff & LD_SDLIGHT)
+		P_WriteINT16(save_p, si->light);
+	if (diff & LD_SDTOPLIGHT)
+		P_WriteINT16(save_p, si->light_top);
+	if (diff & LD_SDMIDLIGHT)
+		P_WriteINT16(save_p, si->light_mid);
+	if (diff & LD_SDBOTLIGHT)
+		P_WriteINT16(save_p, si->light_bottom);
+	if (diff & LD_SDLIGHTABS)
+		P_WriteUINT8(save_p, si->lightabsolute);
+	if (diff & LD_SDTOPLIGHTABS)
+		P_WriteUINT8(save_p, si->lightabsolute_top);
+	if (diff & LD_SDMIDLIGHTABS)
+		P_WriteUINT8(save_p, si->lightabsolute_mid);
+	if (diff & LD_SDBOTLIGHTABS)
+		P_WriteUINT8(save_p, si->lightabsolute_bottom);
+}
+
+static void ArchiveLines(save_t *save_p)
 {
 	size_t i;
 	const line_t *li = lines;
@@ -1488,21 +1778,21 @@ static void ArchiveLines(void)
 
 		if (diff)
 		{
-			WRITEUINT32(save_p, i);
-			WRITEUINT8(save_p, diff);
+			P_WriteUINT32(save_p, i);
+			P_WriteUINT8(save_p, diff);
 			if (diff & LD_DIFF2)
-				WRITEUINT8(save_p, diff2);
+				P_WriteUINT8(save_p, diff2);
 			if (diff & LD_FLAG)
-				WRITEINT16(save_p, li->flags);
+				P_WriteINT16(save_p, li->flags);
 			if (diff & LD_SPECIAL)
-				WRITEINT16(save_p, li->special);
+				P_WriteINT16(save_p, li->special);
 			if (diff & LD_CLLCOUNT)
-				WRITEINT16(save_p, li->callcount);
+				P_WriteINT16(save_p, li->callcount);
 			if (diff & LD_ARGS)
 			{
 				UINT8 j;
 				for (j = 0; j < NUMLINEARGS; j++)
-					WRITEINT32(save_p, li->args[j]);
+					P_WriteINT32(save_p, li->args[j]);
 			}
 			if (diff & LD_STRINGARGS)
 			{
@@ -1513,72 +1803,88 @@ static void ArchiveLines(void)
 
 					if (!li->stringargs[j])
 					{
-						WRITEINT32(save_p, 0);
+						P_WriteINT32(save_p, 0);
 						continue;
 					}
 
 					len = strlen(li->stringargs[j]);
-					WRITEINT32(save_p, len);
+					P_WriteINT32(save_p, len);
 					for (k = 0; k < len; k++)
-						WRITECHAR(save_p, li->stringargs[j][k]);
+						P_WriteChar(save_p, li->stringargs[j][k]);
 				}
 			}
 			if (diff & LD_SIDE1)
-				ArchiveSide(&sides[li->sidenum[0]], side1diff);
+				ArchiveSide(save_p, &sides[li->sidenum[0]], side1diff);
 			if (diff & LD_SIDE2)
-				ArchiveSide(&sides[li->sidenum[1]], side2diff);
+				ArchiveSide(save_p, &sides[li->sidenum[1]], side2diff);
 			if (diff2 & LD_EXECUTORDELAY)
-				WRITEINT32(save_p, li->executordelay);
+				P_WriteINT32(save_p, li->executordelay);
 			if (diff2 & LD_TRANSFPORTAL)
-				WRITEUINT32(save_p, li->secportal);
+				P_WriteUINT32(save_p, li->secportal);
 		}
 	}
-	WRITEUINT32(save_p, 0xffffffff);
+	P_WriteUINT32(save_p, 0xffffffff);
 }
 
-static void UnArchiveSide(side_t *si)
+static void UnArchiveSide(save_t *save_p, side_t *si)
 {
-	UINT32 diff = READUINT32(save_p);
+	UINT32 diff = P_ReadUINT32(save_p);
 
 	if (diff & LD_SDTEXOFFX)
-		si->textureoffset = READFIXED(save_p);
+		si->textureoffset = P_ReadFixed(save_p);
 	if (diff & LD_SDTEXOFFY)
-		si->rowoffset = READFIXED(save_p);
+		si->rowoffset = P_ReadFixed(save_p);
 	if (diff & LD_SDTOPTEX)
-		si->toptexture = READINT32(save_p);
+		si->toptexture = P_ReadINT32(save_p);
 	if (diff & LD_SDBOTTEX)
-		si->bottomtexture = READINT32(save_p);
+		si->bottomtexture = P_ReadINT32(save_p);
 	if (diff & LD_SDMIDTEX)
-		si->midtexture = READINT32(save_p);
+		si->midtexture = P_ReadINT32(save_p);
 	if (diff & LD_SDTOPOFFX)
-		si->offsetx_top = READFIXED(save_p);
+		si->offsetx_top = P_ReadFixed(save_p);
 	if (diff & LD_SDMIDOFFX)
-		si->offsetx_mid = READFIXED(save_p);
+		si->offsetx_mid = P_ReadFixed(save_p);
 	if (diff & LD_SDBOTOFFX)
-		si->offsetx_bottom = READFIXED(save_p);
+		si->offsetx_bottom = P_ReadFixed(save_p);
 	if (diff & LD_SDTOPOFFY)
-		si->offsety_top = READFIXED(save_p);
+		si->offsety_top = P_ReadFixed(save_p);
 	if (diff & LD_SDMIDOFFY)
-		si->offsety_mid = READFIXED(save_p);
+		si->offsety_mid = P_ReadFixed(save_p);
 	if (diff & LD_SDBOTOFFY)
-		si->offsety_bottom = READFIXED(save_p);
+		si->offsety_bottom = P_ReadFixed(save_p);
 	if (diff & LD_SDTOPSCALEX)
-		si->scalex_top = READFIXED(save_p);
+		si->scalex_top = P_ReadFixed(save_p);
 	if (diff & LD_SDMIDSCALEX)
-		si->scalex_mid = READFIXED(save_p);
+		si->scalex_mid = P_ReadFixed(save_p);
 	if (diff & LD_SDBOTSCALEX)
-		si->scalex_bottom = READFIXED(save_p);
+		si->scalex_bottom = P_ReadFixed(save_p);
 	if (diff & LD_SDTOPSCALEY)
-		si->scaley_top = READFIXED(save_p);
+		si->scaley_top = P_ReadFixed(save_p);
 	if (diff & LD_SDMIDSCALEY)
-		si->scaley_mid = READFIXED(save_p);
+		si->scaley_mid = P_ReadFixed(save_p);
 	if (diff & LD_SDBOTSCALEY)
-		si->scaley_bottom = READFIXED(save_p);
+		si->scaley_bottom = P_ReadFixed(save_p);
 	if (diff & LD_SDREPEATCNT)
-		si->repeatcnt = READINT16(save_p);
-}
-
-static void UnArchiveLines(void)
+		si->repeatcnt = P_ReadINT16(save_p);
+	if (diff & LD_SDLIGHT)
+		si->light = P_ReadINT16(save_p);
+	if (diff & LD_SDTOPLIGHT)
+		si->light_top = P_ReadINT16(save_p);
+	if (diff & LD_SDMIDLIGHT)
+		si->light_mid = P_ReadINT16(save_p);
+	if (diff & LD_SDBOTLIGHT)
+		si->light_bottom = P_ReadINT16(save_p);
+	if (diff & LD_SDLIGHTABS)
+		si->lightabsolute = P_ReadUINT8(save_p);
+	if (diff & LD_SDTOPLIGHTABS)
+		si->lightabsolute_top = P_ReadUINT8(save_p);
+	if (diff & LD_SDMIDLIGHTABS)
+		si->lightabsolute_mid = P_ReadUINT8(save_p);
+	if (diff & LD_SDBOTLIGHTABS)
+		si->lightabsolute_bottom = P_ReadUINT8(save_p);
+}
+
+static void UnArchiveLines(save_t *save_p)
 {
 	UINT32 i;
 	line_t *li;
@@ -1586,38 +1892,38 @@ static void UnArchiveLines(void)
 
 	for (;;)
 	{
-		i = READUINT32(save_p);
+		i = P_ReadUINT32(save_p);
 
 		if (i == 0xffffffff)
 			break;
 		if (i > numlines)
 			I_Error("Invalid line number %u from server", i);
 
-		diff = READUINT8(save_p);
+		diff = P_ReadUINT8(save_p);
 		if (diff & LD_DIFF2)
-			diff2 = READUINT8(save_p);
+			diff2 = P_ReadUINT8(save_p);
 		else
 			diff2 = 0;
 		li = &lines[i];
 
 		if (diff & LD_FLAG)
-			li->flags = READINT16(save_p);
+			li->flags = P_ReadINT16(save_p);
 		if (diff & LD_SPECIAL)
-			li->special = READINT16(save_p);
+			li->special = P_ReadINT16(save_p);
 		if (diff & LD_CLLCOUNT)
-			li->callcount = READINT16(save_p);
+			li->callcount = P_ReadINT16(save_p);
 		if (diff & LD_ARGS)
 		{
 			UINT8 j;
 			for (j = 0; j < NUMLINEARGS; j++)
-				li->args[j] = READINT32(save_p);
+				li->args[j] = P_ReadINT32(save_p);
 		}
 		if (diff & LD_STRINGARGS)
 		{
 			UINT8 j;
 			for (j = 0; j < NUMLINESTRINGARGS; j++)
 			{
-				size_t len = READINT32(save_p);
+				size_t len = P_ReadINT32(save_p);
 				size_t k;
 
 				if (!len)
@@ -1629,38 +1935,38 @@ static void UnArchiveLines(void)
 
 				li->stringargs[j] = Z_Realloc(li->stringargs[j], len + 1, PU_LEVEL, NULL);
 				for (k = 0; k < len; k++)
-					li->stringargs[j][k] = READCHAR(save_p);
+					li->stringargs[j][k] = P_ReadChar(save_p);
 				li->stringargs[j][len] = '\0';
 			}
 		}
 		if (diff & LD_SIDE1)
-			UnArchiveSide(&sides[li->sidenum[0]]);
+			UnArchiveSide(save_p, &sides[li->sidenum[0]]);
 		if (diff & LD_SIDE2)
-			UnArchiveSide(&sides[li->sidenum[1]]);
+			UnArchiveSide(save_p, &sides[li->sidenum[1]]);
 		if (diff2 & LD_EXECUTORDELAY)
-			li->executordelay = READINT32(save_p);
+			li->executordelay = P_ReadINT32(save_p);
 		if (diff2 & LD_TRANSFPORTAL)
-			li->secportal = READUINT32(save_p);
+			li->secportal = P_ReadUINT32(save_p);
 	}
 }
 
-static void P_NetArchiveWorld(void)
+static void P_NetArchiveWorld(save_t *save_p)
 {
 	// initialize colormap vars because paranoia
 	ClearNetColormaps();
 
-	WRITEUINT32(save_p, ARCHIVEBLOCK_WORLD);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_WORLD);
 
-	ArchiveSectors();
-	ArchiveLines();
+	ArchiveSectors(save_p);
+	ArchiveLines(save_p);
 	R_ClearTextureNumCache(false);
 }
 
-static void P_NetUnArchiveWorld(void)
+static void P_NetUnArchiveWorld(save_t *save_p)
 {
 	UINT16 i;
 
-	if (READUINT32(save_p) != ARCHIVEBLOCK_WORLD)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_WORLD)
 		I_Error("Bad $$$.sav at archive block World");
 
 	// initialize colormap vars because paranoia
@@ -1674,8 +1980,8 @@ static void P_NetUnArchiveWorld(void)
 			num_ffloors++;
 	}
 
-	UnArchiveSectors();
-	UnArchiveLines();
+	UnArchiveSectors(save_p);
+	UnArchiveLines(save_p);
 }
 
 //
@@ -1746,7 +2052,8 @@ typedef enum
 	MD2_DISPOFFSET          = 1<<23,
 	MD2_DRAWONLYFORPLAYER   = 1<<24,
 	MD2_DONTDRAWFORVIEWMOBJ = 1<<25,
-	MD2_TRANSLATION         = 1<<26
+	MD2_TRANSLATION         = 1<<26,
+	MD2_ALPHA               = 1<<27
 } mobj_diff2_t;
 
 typedef enum
@@ -1813,19 +2120,13 @@ static UINT32 SaveLine(const line_t *line)
 	return 0xFFFFFFFF;
 }
 
-static inline UINT32 SavePlayer(const player_t *player)
-{
-	if (player) return (UINT32)(player - players);
-	return 0xFFFFFFFF;
-}
-
 static UINT32 SaveSlope(const pslope_t *slope)
 {
 	if (slope) return (UINT32)(slope->id);
 	return 0xFFFFFFFF;
 }
 
-static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
+static void SaveMobjThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const mobj_t *mobj = (const mobj_t *)th;
 	UINT32 diff;
@@ -1989,6 +2290,8 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		diff2 |= MD2_DONTDRAWFORVIEWMOBJ;
 	if (mobj->dispoffset != mobj->info->dispoffset)
 		diff2 |= MD2_DISPOFFSET;
+	if (mobj->alpha != FRACUNIT)
+		diff2 |= MD2_ALPHA;
 
 	if (diff2 != 0)
 		diff |= MD_MORE;
@@ -1997,28 +2300,28 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 	if (mobj->type == MT_HOOPCENTER)
 		diff = MD_SPAWNPOINT;
 
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, diff);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, diff);
 	if (diff & MD_MORE)
-		WRITEUINT32(save_p, diff2);
+		P_WriteUINT32(save_p, diff2);
 
 	// save pointer, at load time we will search this pointer to reinitilize pointers
-	WRITEUINT32(save_p, (size_t)mobj);
+	P_WriteUINT32(save_p, (size_t)mobj);
 
-	WRITEFIXED(save_p, mobj->z); // Force this so 3dfloor problems don't arise.
-	WRITEFIXED(save_p, mobj->floorz);
-	WRITEFIXED(save_p, mobj->ceilingz);
+	P_WriteFixed(save_p, mobj->z); // Force this so 3dfloor problems don't arise.
+	P_WriteFixed(save_p, mobj->floorz);
+	P_WriteFixed(save_p, mobj->ceilingz);
 
 	if (diff2 & MD2_FLOORROVER)
 	{
-		WRITEUINT32(save_p, SaveSector(mobj->floorrover->target));
-		WRITEUINT16(save_p, P_GetFFloorID(mobj->floorrover));
+		P_WriteUINT32(save_p, SaveSector(mobj->floorrover->target));
+		P_WriteUINT16(save_p, P_GetFFloorID(mobj->floorrover));
 	}
 
 	if (diff2 & MD2_CEILINGROVER)
 	{
-		WRITEUINT32(save_p, SaveSector(mobj->ceilingrover->target));
-		WRITEUINT16(save_p, P_GetFFloorID(mobj->ceilingrover));
+		P_WriteUINT32(save_p, SaveSector(mobj->ceilingrover->target));
+		P_WriteUINT16(save_p, P_GetFFloorID(mobj->ceilingrover));
 	}
 
 	if (diff & MD_SPAWNPOINT)
@@ -2027,647 +2330,649 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 
 		for (z = 0; z < nummapthings; z++)
 			if (&mapthings[z] == mobj->spawnpoint)
-				WRITEUINT16(save_p, z);
+				P_WriteUINT16(save_p, z);
 		if (mobj->type == MT_HOOPCENTER)
 			return;
 	}
 
 	if (diff & MD_TYPE)
-		WRITEUINT32(save_p, mobj->type);
+		P_WriteUINT32(save_p, mobj->type);
 	if (diff & MD_POS)
 	{
-		WRITEFIXED(save_p, mobj->x);
-		WRITEFIXED(save_p, mobj->y);
-		WRITEANGLE(save_p, mobj->angle);
-		WRITEANGLE(save_p, mobj->pitch);
-		WRITEANGLE(save_p, mobj->roll);
+		P_WriteFixed(save_p, mobj->x);
+		P_WriteFixed(save_p, mobj->y);
+		P_WriteAngle(save_p, mobj->angle);
+		P_WriteAngle(save_p, mobj->pitch);
+		P_WriteAngle(save_p, mobj->roll);
 	}
 	if (diff & MD_MOM)
 	{
-		WRITEFIXED(save_p, mobj->momx);
-		WRITEFIXED(save_p, mobj->momy);
-		WRITEFIXED(save_p, mobj->momz);
-		WRITEFIXED(save_p, mobj->pmomz);
+		P_WriteFixed(save_p, mobj->momx);
+		P_WriteFixed(save_p, mobj->momy);
+		P_WriteFixed(save_p, mobj->momz);
+		P_WriteFixed(save_p, mobj->pmomz);
 	}
 	if (diff & MD_RADIUS)
-		WRITEFIXED(save_p, mobj->radius);
+		P_WriteFixed(save_p, mobj->radius);
 	if (diff & MD_HEIGHT)
-		WRITEFIXED(save_p, mobj->height);
+		P_WriteFixed(save_p, mobj->height);
 	if (diff & MD_FLAGS)
-		WRITEUINT32(save_p, mobj->flags);
+		P_WriteUINT32(save_p, mobj->flags);
 	if (diff & MD_FLAGS2)
-		WRITEUINT32(save_p, mobj->flags2);
+		P_WriteUINT32(save_p, mobj->flags2);
 	if (diff & MD_HEALTH)
-		WRITEINT32(save_p, mobj->health);
+		P_WriteINT32(save_p, mobj->health);
 	if (diff & MD_RTIME)
-		WRITEINT32(save_p, mobj->reactiontime);
+		P_WriteINT32(save_p, mobj->reactiontime);
 	if (diff & MD_STATE)
-		WRITEUINT16(save_p, mobj->state-states);
+		P_WriteUINT16(save_p, mobj->state-states);
 	if (diff & MD_TICS)
-		WRITEINT32(save_p, mobj->tics);
+		P_WriteINT32(save_p, mobj->tics);
 	if (diff & MD_SPRITE) {
-		WRITEUINT16(save_p, mobj->sprite);
+		P_WriteUINT16(save_p, mobj->sprite);
 		if (mobj->sprite == SPR_PLAY)
-			WRITEUINT16(save_p, mobj->sprite2);
+			P_WriteUINT16(save_p, mobj->sprite2);
 	}
 	if (diff & MD_FRAME)
 	{
-		WRITEUINT32(save_p, mobj->frame);
-		WRITEUINT16(save_p, mobj->anim_duration);
+		P_WriteUINT32(save_p, mobj->frame);
+		P_WriteUINT16(save_p, mobj->anim_duration);
 	}
 	if (diff & MD_EFLAGS)
-		WRITEUINT16(save_p, mobj->eflags);
+		P_WriteUINT16(save_p, mobj->eflags);
 	if (diff & MD_PLAYER)
-		WRITEUINT8(save_p, mobj->player-players);
+		P_WriteUINT8(save_p, mobj->player-players);
 	if (diff & MD_MOVEDIR)
-		WRITEANGLE(save_p, mobj->movedir);
+		P_WriteAngle(save_p, mobj->movedir);
 	if (diff & MD_MOVECOUNT)
-		WRITEINT32(save_p, mobj->movecount);
+		P_WriteINT32(save_p, mobj->movecount);
 	if (diff & MD_THRESHOLD)
-		WRITEINT32(save_p, mobj->threshold);
+		P_WriteINT32(save_p, mobj->threshold);
 	if (diff & MD_LASTLOOK)
-		WRITEINT32(save_p, mobj->lastlook);
+		P_WriteINT32(save_p, mobj->lastlook);
 	if (diff & MD_TARGET)
-		WRITEUINT32(save_p, mobj->target->mobjnum);
+		P_WriteUINT32(save_p, mobj->target->mobjnum);
 	if (diff & MD_TRACER)
-		WRITEUINT32(save_p, mobj->tracer->mobjnum);
+		P_WriteUINT32(save_p, mobj->tracer->mobjnum);
 	if (diff & MD_FRICTION)
-		WRITEFIXED(save_p, mobj->friction);
+		P_WriteFixed(save_p, mobj->friction);
 	if (diff & MD_MOVEFACTOR)
-		WRITEFIXED(save_p, mobj->movefactor);
+		P_WriteFixed(save_p, mobj->movefactor);
 	if (diff & MD_FUSE)
-		WRITEINT32(save_p, mobj->fuse);
+		P_WriteINT32(save_p, mobj->fuse);
 	if (diff & MD_WATERTOP)
-		WRITEFIXED(save_p, mobj->watertop);
+		P_WriteFixed(save_p, mobj->watertop);
 	if (diff & MD_WATERBOTTOM)
-		WRITEFIXED(save_p, mobj->waterbottom);
+		P_WriteFixed(save_p, mobj->waterbottom);
 	if (diff & MD_SCALE)
-		WRITEFIXED(save_p, mobj->scale);
+		P_WriteFixed(save_p, mobj->scale);
 	if (diff & MD_DSCALE)
-		WRITEFIXED(save_p, mobj->destscale);
+		P_WriteFixed(save_p, mobj->destscale);
 	if (diff2 & MD2_SCALESPEED)
-		WRITEFIXED(save_p, mobj->scalespeed);
+		P_WriteFixed(save_p, mobj->scalespeed);
 	if (diff2 & MD2_CUSVAL)
-		WRITEINT32(save_p, mobj->cusval);
+		P_WriteINT32(save_p, mobj->cusval);
 	if (diff2 & MD2_CVMEM)
-		WRITEINT32(save_p, mobj->cvmem);
+		P_WriteINT32(save_p, mobj->cvmem);
 	if (diff2 & MD2_SKIN)
-		WRITEUINT8(save_p, (UINT8)(((skin_t *)mobj->skin)->skinnum));
+		P_WriteUINT8(save_p, (UINT8)(((skin_t *)mobj->skin)->skinnum));
 	if (diff2 & MD2_COLOR)
-		WRITEUINT16(save_p, mobj->color);
+		P_WriteUINT16(save_p, mobj->color);
 	if (diff2 & MD2_EXTVAL1)
-		WRITEINT32(save_p, mobj->extravalue1);
+		P_WriteINT32(save_p, mobj->extravalue1);
 	if (diff2 & MD2_EXTVAL2)
-		WRITEINT32(save_p, mobj->extravalue2);
+		P_WriteINT32(save_p, mobj->extravalue2);
 	if (diff2 & MD2_HNEXT)
-		WRITEUINT32(save_p, mobj->hnext->mobjnum);
+		P_WriteUINT32(save_p, mobj->hnext->mobjnum);
 	if (diff2 & MD2_HPREV)
-		WRITEUINT32(save_p, mobj->hprev->mobjnum);
+		P_WriteUINT32(save_p, mobj->hprev->mobjnum);
 	if (diff2 & MD2_SLOPE)
-		WRITEUINT16(save_p, mobj->standingslope->id);
+		P_WriteUINT16(save_p, mobj->standingslope->id);
 	if (diff2 & MD2_COLORIZED)
-		WRITEUINT8(save_p, mobj->colorized);
+		P_WriteUINT8(save_p, mobj->colorized);
 	if (diff2 & MD2_MIRRORED)
-		WRITEUINT8(save_p, mobj->mirrored);
+		P_WriteUINT8(save_p, mobj->mirrored);
 	if (diff2 & MD2_SPRITEROLL)
-		WRITEANGLE(save_p, mobj->spriteroll);
+		P_WriteAngle(save_p, mobj->spriteroll);
 	if (diff2 & MD2_SHADOWSCALE)
-		WRITEFIXED(save_p, mobj->shadowscale);
+		P_WriteFixed(save_p, mobj->shadowscale);
 	if (diff2 & MD2_RENDERFLAGS)
-		WRITEUINT32(save_p, mobj->renderflags);
+		P_WriteUINT32(save_p, mobj->renderflags);
 	if (diff2 & MD2_BLENDMODE)
-		WRITEINT32(save_p, mobj->blendmode);
+		P_WriteINT32(save_p, mobj->blendmode);
 	if (diff2 & MD2_SPRITEXSCALE)
-		WRITEFIXED(save_p, mobj->spritexscale);
+		P_WriteFixed(save_p, mobj->spritexscale);
 	if (diff2 & MD2_SPRITEYSCALE)
-		WRITEFIXED(save_p, mobj->spriteyscale);
+		P_WriteFixed(save_p, mobj->spriteyscale);
 	if (diff2 & MD2_SPRITEXOFFSET)
-		WRITEFIXED(save_p, mobj->spritexoffset);
+		P_WriteFixed(save_p, mobj->spritexoffset);
 	if (diff2 & MD2_SPRITEYOFFSET)
-		WRITEFIXED(save_p, mobj->spriteyoffset);
+		P_WriteFixed(save_p, mobj->spriteyoffset);
 	if (diff2 & MD2_FLOORSPRITESLOPE)
 	{
 		pslope_t *slope = mobj->floorspriteslope;
 
-		WRITEFIXED(save_p, slope->zdelta);
-		WRITEANGLE(save_p, slope->zangle);
-		WRITEANGLE(save_p, slope->xydirection);
+		P_WriteFixed(save_p, slope->zdelta);
+		P_WriteAngle(save_p, slope->zangle);
+		P_WriteAngle(save_p, slope->xydirection);
 
-		WRITEFIXED(save_p, slope->o.x);
-		WRITEFIXED(save_p, slope->o.y);
-		WRITEFIXED(save_p, slope->o.z);
+		P_WriteFixed(save_p, slope->o.x);
+		P_WriteFixed(save_p, slope->o.y);
+		P_WriteFixed(save_p, slope->o.z);
 
-		WRITEFIXED(save_p, slope->d.x);
-		WRITEFIXED(save_p, slope->d.y);
+		P_WriteFixed(save_p, slope->d.x);
+		P_WriteFixed(save_p, slope->d.y);
 
-		WRITEFIXED(save_p, slope->normal.x);
-		WRITEFIXED(save_p, slope->normal.y);
-		WRITEFIXED(save_p, slope->normal.z);
+		P_WriteFixed(save_p, slope->normal.x);
+		P_WriteFixed(save_p, slope->normal.y);
+		P_WriteFixed(save_p, slope->normal.z);
 	}
 	if (diff2 & MD2_DRAWONLYFORPLAYER)
-		WRITEUINT8(save_p, mobj->drawonlyforplayer-players);
+		P_WriteUINT8(save_p, mobj->drawonlyforplayer-players);
 	if (diff2 & MD2_DONTDRAWFORVIEWMOBJ)
-		WRITEUINT32(save_p, mobj->dontdrawforviewmobj->mobjnum);
+		P_WriteUINT32(save_p, mobj->dontdrawforviewmobj->mobjnum);
 	if (diff2 & MD2_DISPOFFSET)
-		WRITEINT32(save_p, mobj->dispoffset);
+		P_WriteINT32(save_p, mobj->dispoffset);
 	if (diff2 & MD2_TRANSLATION)
-		WRITEUINT16(save_p, mobj->translation);
+		P_WriteUINT16(save_p, mobj->translation);
+	if (diff2 & MD2_ALPHA)
+		P_WriteFixed(save_p, mobj->alpha);
 
-	WRITEUINT32(save_p, mobj->mobjnum);
+	P_WriteUINT32(save_p, mobj->mobjnum);
 }
 
-static void SaveNoEnemiesThinker(const thinker_t *th, const UINT8 type)
+static void SaveNoEnemiesThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const noenemies_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
 }
 
-static void SaveBounceCheeseThinker(const thinker_t *th, const UINT8 type)
+static void SaveBounceCheeseThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const bouncecheese_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEFIXED(save_p, ht->speed);
-	WRITEFIXED(save_p, ht->distance);
-	WRITEFIXED(save_p, ht->floorwasheight);
-	WRITEFIXED(save_p, ht->ceilingwasheight);
-	WRITECHAR(save_p, ht->low);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteFixed(save_p, ht->distance);
+	P_WriteFixed(save_p, ht->floorwasheight);
+	P_WriteFixed(save_p, ht->ceilingwasheight);
+	P_WriteChar(save_p, ht->low);
 }
 
-static void SaveContinuousFallThinker(const thinker_t *th, const UINT8 type)
+static void SaveContinuousFallThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const continuousfall_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEFIXED(save_p, ht->speed);
-	WRITEINT32(save_p, ht->direction);
-	WRITEFIXED(save_p, ht->floorstartheight);
-	WRITEFIXED(save_p, ht->ceilingstartheight);
-	WRITEFIXED(save_p, ht->destheight);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteFixed(save_p, ht->floorstartheight);
+	P_WriteFixed(save_p, ht->ceilingstartheight);
+	P_WriteFixed(save_p, ht->destheight);
 }
 
-static void SaveMarioBlockThinker(const thinker_t *th, const UINT8 type)
+static void SaveMarioBlockThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const mariothink_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEFIXED(save_p, ht->speed);
-	WRITEINT32(save_p, ht->direction);
-	WRITEFIXED(save_p, ht->floorstartheight);
-	WRITEFIXED(save_p, ht->ceilingstartheight);
-	WRITEINT16(save_p, ht->tag);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteFixed(save_p, ht->floorstartheight);
+	P_WriteFixed(save_p, ht->ceilingstartheight);
+	P_WriteINT16(save_p, ht->tag);
 }
 
-static void SaveMarioCheckThinker(const thinker_t *th, const UINT8 type)
+static void SaveMarioCheckThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const mariocheck_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-	WRITEUINT32(save_p, SaveSector(ht->sector));
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
 }
 
-static void SaveThwompThinker(const thinker_t *th, const UINT8 type)
+static void SaveThwompThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const thwomp_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEFIXED(save_p, ht->crushspeed);
-	WRITEFIXED(save_p, ht->retractspeed);
-	WRITEINT32(save_p, ht->direction);
-	WRITEFIXED(save_p, ht->floorstartheight);
-	WRITEFIXED(save_p, ht->ceilingstartheight);
-	WRITEINT32(save_p, ht->delay);
-	WRITEINT16(save_p, ht->tag);
-	WRITEUINT16(save_p, ht->sound);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteFixed(save_p, ht->crushspeed);
+	P_WriteFixed(save_p, ht->retractspeed);
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteFixed(save_p, ht->floorstartheight);
+	P_WriteFixed(save_p, ht->ceilingstartheight);
+	P_WriteINT32(save_p, ht->delay);
+	P_WriteINT16(save_p, ht->tag);
+	P_WriteUINT16(save_p, ht->sound);
 }
 
-static void SaveFloatThinker(const thinker_t *th, const UINT8 type)
+static void SaveFloatThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const floatthink_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT16(save_p, ht->tag);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT16(save_p, ht->tag);
 }
 
-static void SaveEachTimeThinker(const thinker_t *th, const UINT8 type)
+static void SaveEachTimeThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const eachtime_t *ht  = (const void *)th;
 	size_t i;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		WRITECHAR(save_p, ht->playersInArea[i]);
+		P_WriteChar(save_p, ht->playersInArea[i]);
 	}
-	WRITECHAR(save_p, ht->triggerOnExit);
+	P_WriteChar(save_p, ht->triggerOnExit);
 }
 
-static void SaveRaiseThinker(const thinker_t *th, const UINT8 type)
+static void SaveRaiseThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const raise_t *ht  = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT16(save_p, ht->tag);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEFIXED(save_p, ht->ceilingbottom);
-	WRITEFIXED(save_p, ht->ceilingtop);
-	WRITEFIXED(save_p, ht->basespeed);
-	WRITEFIXED(save_p, ht->extraspeed);
-	WRITEUINT8(save_p, ht->shaketimer);
-	WRITEUINT8(save_p, ht->flags);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT16(save_p, ht->tag);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteFixed(save_p, ht->ceilingbottom);
+	P_WriteFixed(save_p, ht->ceilingtop);
+	P_WriteFixed(save_p, ht->basespeed);
+	P_WriteFixed(save_p, ht->extraspeed);
+	P_WriteUINT8(save_p, ht->shaketimer);
+	P_WriteUINT8(save_p, ht->flags);
 }
 
-static void SaveCeilingThinker(const thinker_t *th, const UINT8 type)
+static void SaveCeilingThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const ceiling_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT8(save_p, ht->type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEFIXED(save_p, ht->bottomheight);
-	WRITEFIXED(save_p, ht->topheight);
-	WRITEFIXED(save_p, ht->speed);
-	WRITEFIXED(save_p, ht->delay);
-	WRITEFIXED(save_p, ht->delaytimer);
-	WRITEUINT8(save_p, ht->crush);
-	WRITEINT32(save_p, ht->texture);
-	WRITEINT32(save_p, ht->direction);
-	WRITEINT16(save_p, ht->tag);
-	WRITEFIXED(save_p, ht->origspeed);
-	WRITEFIXED(save_p, ht->sourceline);
-}
-
-static void SaveFloormoveThinker(const thinker_t *th, const UINT8 type)
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT8(save_p, ht->type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteFixed(save_p, ht->bottomheight);
+	P_WriteFixed(save_p, ht->topheight);
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteFixed(save_p, ht->delay);
+	P_WriteFixed(save_p, ht->delaytimer);
+	P_WriteUINT8(save_p, ht->crush);
+	P_WriteINT32(save_p, ht->texture);
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteINT16(save_p, ht->tag);
+	P_WriteFixed(save_p, ht->origspeed);
+	P_WriteFixed(save_p, ht->sourceline);
+}
+
+static void SaveFloormoveThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const floormove_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT8(save_p, ht->type);
-	WRITEUINT8(save_p, ht->crush);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT32(save_p, ht->direction);
-	WRITEINT32(save_p, ht->texture);
-	WRITEFIXED(save_p, ht->floordestheight);
-	WRITEFIXED(save_p, ht->speed);
-	WRITEFIXED(save_p, ht->origspeed);
-	WRITEFIXED(save_p, ht->delay);
-	WRITEFIXED(save_p, ht->delaytimer);
-	WRITEINT16(save_p, ht->tag);
-	WRITEFIXED(save_p, ht->sourceline);
-}
-
-static void SaveLightflashThinker(const thinker_t *th, const UINT8 type)
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT8(save_p, ht->type);
+	P_WriteUINT8(save_p, ht->crush);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteINT32(save_p, ht->texture);
+	P_WriteFixed(save_p, ht->floordestheight);
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteFixed(save_p, ht->origspeed);
+	P_WriteFixed(save_p, ht->delay);
+	P_WriteFixed(save_p, ht->delaytimer);
+	P_WriteINT16(save_p, ht->tag);
+	P_WriteFixed(save_p, ht->sourceline);
+}
+
+static void SaveLightflashThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const lightflash_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT32(save_p, ht->maxlight);
-	WRITEINT32(save_p, ht->minlight);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT32(save_p, ht->maxlight);
+	P_WriteINT32(save_p, ht->minlight);
 }
 
-static void SaveStrobeThinker(const thinker_t *th, const UINT8 type)
+static void SaveStrobeThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const strobe_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT32(save_p, ht->count);
-	WRITEINT16(save_p, ht->minlight);
-	WRITEINT16(save_p, ht->maxlight);
-	WRITEINT32(save_p, ht->darktime);
-	WRITEINT32(save_p, ht->brighttime);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT32(save_p, ht->count);
+	P_WriteINT16(save_p, ht->minlight);
+	P_WriteINT16(save_p, ht->maxlight);
+	P_WriteINT32(save_p, ht->darktime);
+	P_WriteINT32(save_p, ht->brighttime);
 }
 
-static void SaveGlowThinker(const thinker_t *th, const UINT8 type)
+static void SaveGlowThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const glow_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT16(save_p, ht->minlight);
-	WRITEINT16(save_p, ht->maxlight);
-	WRITEINT16(save_p, ht->direction);
-	WRITEINT16(save_p, ht->speed);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT16(save_p, ht->minlight);
+	P_WriteINT16(save_p, ht->maxlight);
+	P_WriteINT16(save_p, ht->direction);
+	P_WriteINT16(save_p, ht->speed);
 }
 
-static inline void SaveFireflickerThinker(const thinker_t *th, const UINT8 type)
+static inline void SaveFireflickerThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const fireflicker_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT32(save_p, ht->count);
-	WRITEINT32(save_p, ht->resetcount);
-	WRITEINT16(save_p, ht->maxlight);
-	WRITEINT16(save_p, ht->minlight);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT32(save_p, ht->count);
+	P_WriteINT32(save_p, ht->resetcount);
+	P_WriteINT16(save_p, ht->maxlight);
+	P_WriteINT16(save_p, ht->minlight);
 }
 
-static void SaveElevatorThinker(const thinker_t *th, const UINT8 type)
+static void SaveElevatorThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const elevator_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT8(save_p, ht->type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEUINT32(save_p, SaveSector(ht->actionsector));
-	WRITEINT32(save_p, ht->direction);
-	WRITEFIXED(save_p, ht->floordestheight);
-	WRITEFIXED(save_p, ht->ceilingdestheight);
-	WRITEFIXED(save_p, ht->speed);
-	WRITEFIXED(save_p, ht->origspeed);
-	WRITEFIXED(save_p, ht->low);
-	WRITEFIXED(save_p, ht->high);
-	WRITEFIXED(save_p, ht->distance);
-	WRITEFIXED(save_p, ht->delay);
-	WRITEFIXED(save_p, ht->delaytimer);
-	WRITEFIXED(save_p, ht->floorwasheight);
-	WRITEFIXED(save_p, ht->ceilingwasheight);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-}
-
-static void SaveCrumbleThinker(const thinker_t *th, const UINT8 type)
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT8(save_p, ht->type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteUINT32(save_p, SaveSector(ht->actionsector));
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteFixed(save_p, ht->floordestheight);
+	P_WriteFixed(save_p, ht->ceilingdestheight);
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteFixed(save_p, ht->origspeed);
+	P_WriteFixed(save_p, ht->low);
+	P_WriteFixed(save_p, ht->high);
+	P_WriteFixed(save_p, ht->distance);
+	P_WriteFixed(save_p, ht->delay);
+	P_WriteFixed(save_p, ht->delaytimer);
+	P_WriteFixed(save_p, ht->floorwasheight);
+	P_WriteFixed(save_p, ht->ceilingwasheight);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+}
+
+static void SaveCrumbleThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const crumble_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEUINT32(save_p, SaveSector(ht->actionsector));
-	WRITEUINT32(save_p, SavePlayer(ht->player)); // was dummy
-	WRITEINT32(save_p, ht->direction);
-	WRITEINT32(save_p, ht->origalpha);
-	WRITEINT32(save_p, ht->timer);
-	WRITEFIXED(save_p, ht->speed);
-	WRITEFIXED(save_p, ht->floorwasheight);
-	WRITEFIXED(save_p, ht->ceilingwasheight);
-	WRITEUINT8(save_p, ht->flags);
-}
-
-static inline void SaveScrollThinker(const thinker_t *th, const UINT8 type)
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteUINT32(save_p, SaveSector(ht->actionsector));
+	P_WriteUINT32(save_p, SavePlayer(ht->player)); // was dummy
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteINT32(save_p, ht->origalpha);
+	P_WriteINT32(save_p, ht->timer);
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteFixed(save_p, ht->floorwasheight);
+	P_WriteFixed(save_p, ht->ceilingwasheight);
+	P_WriteUINT8(save_p, ht->flags);
+}
+
+static inline void SaveScrollThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const scroll_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEFIXED(save_p, ht->dx);
-	WRITEFIXED(save_p, ht->dy);
-	WRITEINT32(save_p, ht->affectee);
-	WRITEINT32(save_p, ht->control);
-	WRITEFIXED(save_p, ht->last_height);
-	WRITEFIXED(save_p, ht->vdx);
-	WRITEFIXED(save_p, ht->vdy);
-	WRITEINT32(save_p, ht->accel);
-	WRITEINT32(save_p, ht->exclusive);
-	WRITEUINT8(save_p, ht->type);
+	P_WriteUINT8(save_p, type);
+	P_WriteFixed(save_p, ht->dx);
+	P_WriteFixed(save_p, ht->dy);
+	P_WriteINT32(save_p, ht->affectee);
+	P_WriteINT32(save_p, ht->control);
+	P_WriteFixed(save_p, ht->last_height);
+	P_WriteFixed(save_p, ht->vdx);
+	P_WriteFixed(save_p, ht->vdy);
+	P_WriteINT32(save_p, ht->accel);
+	P_WriteINT32(save_p, ht->exclusive);
+	P_WriteUINT8(save_p, ht->type);
 }
 
-static inline void SaveFrictionThinker(const thinker_t *th, const UINT8 type)
+static inline void SaveFrictionThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const friction_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->friction);
-	WRITEINT32(save_p, ht->movefactor);
-	WRITEINT32(save_p, ht->affectee);
-	WRITEINT32(save_p, ht->referrer);
-	WRITEUINT8(save_p, ht->roverfriction);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->friction);
+	P_WriteINT32(save_p, ht->movefactor);
+	P_WriteINT32(save_p, ht->affectee);
+	P_WriteINT32(save_p, ht->referrer);
+	P_WriteUINT8(save_p, ht->roverfriction);
 }
 
-static inline void SavePusherThinker(const thinker_t *th, const UINT8 type)
+static inline void SavePusherThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const pusher_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT8(save_p, ht->type);
-	WRITEFIXED(save_p, ht->x_mag);
-	WRITEFIXED(save_p, ht->y_mag);
-	WRITEFIXED(save_p, ht->z_mag);
-	WRITEINT32(save_p, ht->affectee);
-	WRITEUINT8(save_p, ht->roverpusher);
-	WRITEINT32(save_p, ht->referrer);
-	WRITEINT32(save_p, ht->exclusive);
-	WRITEINT32(save_p, ht->slider);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT8(save_p, ht->type);
+	P_WriteFixed(save_p, ht->x_mag);
+	P_WriteFixed(save_p, ht->y_mag);
+	P_WriteFixed(save_p, ht->z_mag);
+	P_WriteINT32(save_p, ht->affectee);
+	P_WriteUINT8(save_p, ht->roverpusher);
+	P_WriteINT32(save_p, ht->referrer);
+	P_WriteINT32(save_p, ht->exclusive);
+	P_WriteINT32(save_p, ht->slider);
 }
 
-static void SaveLaserThinker(const thinker_t *th, const UINT8 type)
+static void SaveLaserThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const laserthink_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT16(save_p, ht->tag);
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-	WRITEUINT8(save_p, ht->nobosses);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT16(save_p, ht->tag);
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteUINT8(save_p, ht->nobosses);
 }
 
-static void SaveLightlevelThinker(const thinker_t *th, const UINT8 type)
+static void SaveLightlevelThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const lightlevel_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT16(save_p, ht->sourcelevel);
-	WRITEINT16(save_p, ht->destlevel);
-	WRITEFIXED(save_p, ht->fixedcurlevel);
-	WRITEFIXED(save_p, ht->fixedpertic);
-	WRITEINT32(save_p, ht->timer);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT16(save_p, ht->sourcelevel);
+	P_WriteINT16(save_p, ht->destlevel);
+	P_WriteFixed(save_p, ht->fixedcurlevel);
+	P_WriteFixed(save_p, ht->fixedpertic);
+	P_WriteINT32(save_p, ht->timer);
 }
 
-static void SaveExecutorThinker(const thinker_t *th, const UINT8 type)
+static void SaveExecutorThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const executor_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveLine(ht->line));
-	WRITEUINT32(save_p, SaveMobjnum(ht->caller));
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEINT32(save_p, ht->timer);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveLine(ht->line));
+	P_WriteUINT32(save_p, SaveMobjnum(ht->caller));
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteINT32(save_p, ht->timer);
 }
 
-static void SaveDisappearThinker(const thinker_t *th, const UINT8 type)
+static void SaveDisappearThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const disappear_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, ht->appeartime);
-	WRITEUINT32(save_p, ht->disappeartime);
-	WRITEUINT32(save_p, ht->offset);
-	WRITEUINT32(save_p, ht->timer);
-	WRITEINT32(save_p, ht->affectee);
-	WRITEINT32(save_p, ht->sourceline);
-	WRITEINT32(save_p, ht->exists);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, ht->appeartime);
+	P_WriteUINT32(save_p, ht->disappeartime);
+	P_WriteUINT32(save_p, ht->offset);
+	P_WriteUINT32(save_p, ht->timer);
+	P_WriteINT32(save_p, ht->affectee);
+	P_WriteINT32(save_p, ht->sourceline);
+	P_WriteINT32(save_p, ht->exists);
 }
 
-static void SaveFadeThinker(const thinker_t *th, const UINT8 type)
+static void SaveFadeThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const fade_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, CheckAddNetColormapToList(ht->dest_exc));
-	WRITEUINT32(save_p, ht->sectornum);
-	WRITEUINT32(save_p, ht->ffloornum);
-	WRITEINT32(save_p, ht->alpha);
-	WRITEINT16(save_p, ht->sourcevalue);
-	WRITEINT16(save_p, ht->destvalue);
-	WRITEINT16(save_p, ht->destlightlevel);
-	WRITEINT16(save_p, ht->speed);
-	WRITEUINT8(save_p, (UINT8)ht->ticbased);
-	WRITEINT32(save_p, ht->timer);
-	WRITEUINT8(save_p, ht->doexists);
-	WRITEUINT8(save_p, ht->dotranslucent);
-	WRITEUINT8(save_p, ht->dolighting);
-	WRITEUINT8(save_p, ht->docolormap);
-	WRITEUINT8(save_p, ht->docollision);
-	WRITEUINT8(save_p, ht->doghostfade);
-	WRITEUINT8(save_p, ht->exactalpha);
-}
-
-static void SaveFadeColormapThinker(const thinker_t *th, const UINT8 type)
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, CheckAddNetColormapToList(ht->dest_exc));
+	P_WriteUINT32(save_p, ht->sectornum);
+	P_WriteUINT32(save_p, ht->ffloornum);
+	P_WriteINT32(save_p, ht->alpha);
+	P_WriteINT16(save_p, ht->sourcevalue);
+	P_WriteINT16(save_p, ht->destvalue);
+	P_WriteINT16(save_p, ht->destlightlevel);
+	P_WriteINT16(save_p, ht->speed);
+	P_WriteUINT8(save_p, (UINT8)ht->ticbased);
+	P_WriteINT32(save_p, ht->timer);
+	P_WriteUINT8(save_p, ht->doexists);
+	P_WriteUINT8(save_p, ht->dotranslucent);
+	P_WriteUINT8(save_p, ht->dolighting);
+	P_WriteUINT8(save_p, ht->docolormap);
+	P_WriteUINT8(save_p, ht->docollision);
+	P_WriteUINT8(save_p, ht->doghostfade);
+	P_WriteUINT8(save_p, ht->exactalpha);
+}
+
+static void SaveFadeColormapThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const fadecolormap_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSector(ht->sector));
-	WRITEUINT32(save_p, CheckAddNetColormapToList(ht->source_exc));
-	WRITEUINT32(save_p, CheckAddNetColormapToList(ht->dest_exc));
-	WRITEUINT8(save_p, (UINT8)ht->ticbased);
-	WRITEINT32(save_p, ht->duration);
-	WRITEINT32(save_p, ht->timer);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSector(ht->sector));
+	P_WriteUINT32(save_p, CheckAddNetColormapToList(ht->source_exc));
+	P_WriteUINT32(save_p, CheckAddNetColormapToList(ht->dest_exc));
+	P_WriteUINT8(save_p, (UINT8)ht->ticbased);
+	P_WriteINT32(save_p, ht->duration);
+	P_WriteINT32(save_p, ht->timer);
 }
 
-static void SavePlaneDisplaceThinker(const thinker_t *th, const UINT8 type)
+static void SavePlaneDisplaceThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const planedisplace_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->affectee);
-	WRITEINT32(save_p, ht->control);
-	WRITEFIXED(save_p, ht->last_height);
-	WRITEFIXED(save_p, ht->speed);
-	WRITEUINT8(save_p, ht->type);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->affectee);
+	P_WriteINT32(save_p, ht->control);
+	P_WriteFixed(save_p, ht->last_height);
+	P_WriteFixed(save_p, ht->speed);
+	P_WriteUINT8(save_p, ht->type);
 }
 
-static inline void SaveDynamicLineSlopeThinker(const thinker_t *th, const UINT8 type)
+static inline void SaveDynamicLineSlopeThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const dynlineplanethink_t* ht = (const void*)th;
 
-	WRITEUINT8(save_p, type);
-	WRITEUINT8(save_p, ht->type);
-	WRITEUINT32(save_p, SaveSlope(ht->slope));
-	WRITEUINT32(save_p, SaveLine(ht->sourceline));
-	WRITEFIXED(save_p, ht->extent);
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT8(save_p, ht->type);
+	P_WriteUINT32(save_p, SaveSlope(ht->slope));
+	P_WriteUINT32(save_p, SaveLine(ht->sourceline));
+	P_WriteFixed(save_p, ht->extent);
 }
 
-static inline void SaveDynamicVertexSlopeThinker(const thinker_t *th, const UINT8 type)
+static inline void SaveDynamicVertexSlopeThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	size_t i;
 	const dynvertexplanethink_t* ht = (const void*)th;
 
-	WRITEUINT8(save_p, type);
-	WRITEUINT32(save_p, SaveSlope(ht->slope));
+	P_WriteUINT8(save_p, type);
+	P_WriteUINT32(save_p, SaveSlope(ht->slope));
 	for (i = 0; i < 3; i++)
-		WRITEUINT32(save_p, SaveSector(ht->secs[i]));
-	WRITEMEM(save_p, ht->vex, sizeof(ht->vex));
-	WRITEMEM(save_p, ht->origsecheights, sizeof(ht->origsecheights));
-	WRITEMEM(save_p, ht->origvecheights, sizeof(ht->origvecheights));
-	WRITEUINT8(save_p, ht->relative);
+		P_WriteUINT32(save_p, SaveSector(ht->secs[i]));
+	P_WriteMem(save_p, ht->vex, sizeof(ht->vex));
+	P_WriteMem(save_p, ht->origsecheights, sizeof(ht->origsecheights));
+	P_WriteMem(save_p, ht->origvecheights, sizeof(ht->origvecheights));
+	P_WriteUINT8(save_p, ht->relative);
 }
 
-static inline void SavePolyrotatetThinker(const thinker_t *th, const UINT8 type)
+static inline void SavePolyrotatetThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const polyrotate_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEINT32(save_p, ht->speed);
-	WRITEINT32(save_p, ht->distance);
-	WRITEUINT8(save_p, ht->turnobjs);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteINT32(save_p, ht->speed);
+	P_WriteINT32(save_p, ht->distance);
+	P_WriteUINT8(save_p, ht->turnobjs);
 }
 
-static void SavePolymoveThinker(const thinker_t *th, const UINT8 type)
+static void SavePolymoveThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const polymove_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEINT32(save_p, ht->speed);
-	WRITEFIXED(save_p, ht->momx);
-	WRITEFIXED(save_p, ht->momy);
-	WRITEINT32(save_p, ht->distance);
-	WRITEANGLE(save_p, ht->angle);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteINT32(save_p, ht->speed);
+	P_WriteFixed(save_p, ht->momx);
+	P_WriteFixed(save_p, ht->momy);
+	P_WriteINT32(save_p, ht->distance);
+	P_WriteAngle(save_p, ht->angle);
 }
 
-static void SavePolywaypointThinker(const thinker_t *th, UINT8 type)
+static void SavePolywaypointThinker(save_t *save_p, const thinker_t *th, UINT8 type)
 {
 	const polywaypoint_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEINT32(save_p, ht->speed);
-	WRITEINT32(save_p, ht->sequence);
-	WRITEINT32(save_p, ht->pointnum);
-	WRITEINT32(save_p, ht->direction);
-	WRITEUINT8(save_p, ht->returnbehavior);
-	WRITEUINT8(save_p, ht->continuous);
-	WRITEUINT8(save_p, ht->stophere);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteINT32(save_p, ht->speed);
+	P_WriteINT32(save_p, ht->sequence);
+	P_WriteINT32(save_p, ht->pointnum);
+	P_WriteINT32(save_p, ht->direction);
+	P_WriteUINT8(save_p, ht->returnbehavior);
+	P_WriteUINT8(save_p, ht->continuous);
+	P_WriteUINT8(save_p, ht->stophere);
 }
 
-static void SavePolyslidedoorThinker(const thinker_t *th, const UINT8 type)
+static void SavePolyslidedoorThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const polyslidedoor_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEINT32(save_p, ht->delay);
-	WRITEINT32(save_p, ht->delayCount);
-	WRITEINT32(save_p, ht->initSpeed);
-	WRITEINT32(save_p, ht->speed);
-	WRITEINT32(save_p, ht->initDistance);
-	WRITEINT32(save_p, ht->distance);
-	WRITEUINT32(save_p, ht->initAngle);
-	WRITEUINT32(save_p, ht->angle);
-	WRITEUINT32(save_p, ht->revAngle);
-	WRITEFIXED(save_p, ht->momx);
-	WRITEFIXED(save_p, ht->momy);
-	WRITEUINT8(save_p, ht->closing);
-}
-
-static void SavePolyswingdoorThinker(const thinker_t *th, const UINT8 type)
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteINT32(save_p, ht->delay);
+	P_WriteINT32(save_p, ht->delayCount);
+	P_WriteINT32(save_p, ht->initSpeed);
+	P_WriteINT32(save_p, ht->speed);
+	P_WriteINT32(save_p, ht->initDistance);
+	P_WriteINT32(save_p, ht->distance);
+	P_WriteUINT32(save_p, ht->initAngle);
+	P_WriteUINT32(save_p, ht->angle);
+	P_WriteUINT32(save_p, ht->revAngle);
+	P_WriteFixed(save_p, ht->momx);
+	P_WriteFixed(save_p, ht->momy);
+	P_WriteUINT8(save_p, ht->closing);
+}
+
+static void SavePolyswingdoorThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const polyswingdoor_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEINT32(save_p, ht->delay);
-	WRITEINT32(save_p, ht->delayCount);
-	WRITEINT32(save_p, ht->initSpeed);
-	WRITEINT32(save_p, ht->speed);
-	WRITEINT32(save_p, ht->initDistance);
-	WRITEINT32(save_p, ht->distance);
-	WRITEUINT8(save_p, ht->closing);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteINT32(save_p, ht->delay);
+	P_WriteINT32(save_p, ht->delayCount);
+	P_WriteINT32(save_p, ht->initSpeed);
+	P_WriteINT32(save_p, ht->speed);
+	P_WriteINT32(save_p, ht->initDistance);
+	P_WriteINT32(save_p, ht->distance);
+	P_WriteUINT8(save_p, ht->closing);
 }
 
-static void SavePolydisplaceThinker(const thinker_t *th, const UINT8 type)
+static void SavePolydisplaceThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const polydisplace_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEUINT32(save_p, SaveSector(ht->controlSector));
-	WRITEFIXED(save_p, ht->dx);
-	WRITEFIXED(save_p, ht->dy);
-	WRITEFIXED(save_p, ht->oldHeights);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteUINT32(save_p, SaveSector(ht->controlSector));
+	P_WriteFixed(save_p, ht->dx);
+	P_WriteFixed(save_p, ht->dy);
+	P_WriteFixed(save_p, ht->oldHeights);
 }
 
-static void SavePolyrotdisplaceThinker(const thinker_t *th, const UINT8 type)
+static void SavePolyrotdisplaceThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const polyrotdisplace_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEUINT32(save_p, SaveSector(ht->controlSector));
-	WRITEFIXED(save_p, ht->rotscale);
-	WRITEUINT8(save_p, ht->turnobjs);
-	WRITEFIXED(save_p, ht->oldHeights);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteUINT32(save_p, SaveSector(ht->controlSector));
+	P_WriteFixed(save_p, ht->rotscale);
+	P_WriteUINT8(save_p, ht->turnobjs);
+	P_WriteFixed(save_p, ht->oldHeights);
 }
 
-static void SavePolyfadeThinker(const thinker_t *th, const UINT8 type)
+static void SavePolyfadeThinker(save_t *save_p, const thinker_t *th, const UINT8 type)
 {
 	const polyfade_t *ht = (const void *)th;
-	WRITEUINT8(save_p, type);
-	WRITEINT32(save_p, ht->polyObjNum);
-	WRITEINT32(save_p, ht->sourcevalue);
-	WRITEINT32(save_p, ht->destvalue);
-	WRITEUINT8(save_p, (UINT8)ht->docollision);
-	WRITEUINT8(save_p, (UINT8)ht->doghostfade);
-	WRITEUINT8(save_p, (UINT8)ht->ticbased);
-	WRITEINT32(save_p, ht->duration);
-	WRITEINT32(save_p, ht->timer);
+	P_WriteUINT8(save_p, type);
+	P_WriteINT32(save_p, ht->polyObjNum);
+	P_WriteINT32(save_p, ht->sourcevalue);
+	P_WriteINT32(save_p, ht->destvalue);
+	P_WriteUINT8(save_p, (UINT8)ht->docollision);
+	P_WriteUINT8(save_p, (UINT8)ht->doghostfade);
+	P_WriteUINT8(save_p, (UINT8)ht->ticbased);
+	P_WriteINT32(save_p, ht->duration);
+	P_WriteINT32(save_p, ht->timer);
 }
 
-static void P_NetArchiveThinkers(void)
+static void P_NetArchiveThinkers(save_t *save_p)
 {
 	const thinker_t *th;
 	UINT32 i;
 
-	WRITEUINT32(save_p, ARCHIVEBLOCK_THINKERS);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_THINKERS);
 
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
@@ -2675,13 +2980,12 @@ static void P_NetArchiveThinkers(void)
 		// save off the current thinkers
 		for (th = thlist[i].next; th != &thlist[i]; th = th->next)
 		{
-			if (!(th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed
-			 || th->function.acp1 == (actionf_p1)P_NullPrecipThinker))
+			if (!(th->removing || th->function.acp1 == (actionf_p1)P_NullPrecipThinker))
 				numsaved++;
 
 			if (th->function.acp1 == (actionf_p1)P_MobjThinker)
 			{
-				SaveMobjThinker(th, tc_mobj);
+				SaveMobjThinker(save_p, th, tc_mobj);
 				continue;
 			}
 	#ifdef PARANOIA
@@ -2689,213 +2993,213 @@ static void P_NetArchiveThinkers(void)
 	#endif
 			else if (th->function.acp1 == (actionf_p1)T_MoveCeiling)
 			{
-				SaveCeilingThinker(th, tc_ceiling);
+				SaveCeilingThinker(save_p, th, tc_ceiling);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_CrushCeiling)
 			{
-				SaveCeilingThinker(th, tc_crushceiling);
+				SaveCeilingThinker(save_p, th, tc_crushceiling);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_MoveFloor)
 			{
-				SaveFloormoveThinker(th, tc_floor);
+				SaveFloormoveThinker(save_p, th, tc_floor);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_LightningFlash)
 			{
-				SaveLightflashThinker(th, tc_flash);
+				SaveLightflashThinker(save_p, th, tc_flash);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_StrobeFlash)
 			{
-				SaveStrobeThinker(th, tc_strobe);
+				SaveStrobeThinker(save_p, th, tc_strobe);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_Glow)
 			{
-				SaveGlowThinker(th, tc_glow);
+				SaveGlowThinker(save_p, th, tc_glow);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_FireFlicker)
 			{
-				SaveFireflickerThinker(th, tc_fireflicker);
+				SaveFireflickerThinker(save_p, th, tc_fireflicker);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_MoveElevator)
 			{
-				SaveElevatorThinker(th, tc_elevator);
+				SaveElevatorThinker(save_p, th, tc_elevator);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_ContinuousFalling)
 			{
-				SaveContinuousFallThinker(th, tc_continuousfalling);
+				SaveContinuousFallThinker(save_p, th, tc_continuousfalling);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_ThwompSector)
 			{
-				SaveThwompThinker(th, tc_thwomp);
+				SaveThwompThinker(save_p, th, tc_thwomp);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_NoEnemiesSector)
 			{
-				SaveNoEnemiesThinker(th, tc_noenemies);
+				SaveNoEnemiesThinker(save_p, th, tc_noenemies);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_EachTimeThinker)
 			{
-				SaveEachTimeThinker(th, tc_eachtime);
+				SaveEachTimeThinker(save_p, th, tc_eachtime);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_RaiseSector)
 			{
-				SaveRaiseThinker(th, tc_raisesector);
+				SaveRaiseThinker(save_p, th, tc_raisesector);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_CameraScanner)
 			{
-				SaveElevatorThinker(th, tc_camerascanner);
+				SaveElevatorThinker(save_p, th, tc_camerascanner);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_Scroll)
 			{
-				SaveScrollThinker(th, tc_scroll);
+				SaveScrollThinker(save_p, th, tc_scroll);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_Friction)
 			{
-				SaveFrictionThinker(th, tc_friction);
+				SaveFrictionThinker(save_p, th, tc_friction);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_Pusher)
 			{
-				SavePusherThinker(th, tc_pusher);
+				SavePusherThinker(save_p, th, tc_pusher);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_BounceCheese)
 			{
-				SaveBounceCheeseThinker(th, tc_bouncecheese);
+				SaveBounceCheeseThinker(save_p, th, tc_bouncecheese);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_StartCrumble)
 			{
-				SaveCrumbleThinker(th, tc_startcrumble);
+				SaveCrumbleThinker(save_p, th, tc_startcrumble);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_MarioBlock)
 			{
-				SaveMarioBlockThinker(th, tc_marioblock);
+				SaveMarioBlockThinker(save_p, th, tc_marioblock);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_MarioBlockChecker)
 			{
-				SaveMarioCheckThinker(th, tc_marioblockchecker);
+				SaveMarioCheckThinker(save_p, th, tc_marioblockchecker);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_FloatSector)
 			{
-				SaveFloatThinker(th, tc_floatsector);
+				SaveFloatThinker(save_p, th, tc_floatsector);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_LaserFlash)
 			{
-				SaveLaserThinker(th, tc_laserflash);
+				SaveLaserThinker(save_p, th, tc_laserflash);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_LightFade)
 			{
-				SaveLightlevelThinker(th, tc_lightfade);
+				SaveLightlevelThinker(save_p, th, tc_lightfade);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_ExecutorDelay)
 			{
-				SaveExecutorThinker(th, tc_executor);
+				SaveExecutorThinker(save_p, th, tc_executor);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_Disappear)
 			{
-				SaveDisappearThinker(th, tc_disappear);
+				SaveDisappearThinker(save_p, th, tc_disappear);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_Fade)
 			{
-				SaveFadeThinker(th, tc_fade);
+				SaveFadeThinker(save_p, th, tc_fade);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_FadeColormap)
 			{
-				SaveFadeColormapThinker(th, tc_fadecolormap);
+				SaveFadeColormapThinker(save_p, th, tc_fadecolormap);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PlaneDisplace)
 			{
-				SavePlaneDisplaceThinker(th, tc_planedisplace);
+				SavePlaneDisplaceThinker(save_p, th, tc_planedisplace);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyObjRotate)
 			{
-				SavePolyrotatetThinker(th, tc_polyrotate);
+				SavePolyrotatetThinker(save_p, th, tc_polyrotate);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyObjMove)
 			{
-				SavePolymoveThinker(th, tc_polymove);
+				SavePolymoveThinker(save_p, th, tc_polymove);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyObjWaypoint)
 			{
-				SavePolywaypointThinker(th, tc_polywaypoint);
+				SavePolywaypointThinker(save_p, th, tc_polywaypoint);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyDoorSlide)
 			{
-				SavePolyslidedoorThinker(th, tc_polyslidedoor);
+				SavePolyslidedoorThinker(save_p, th, tc_polyslidedoor);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyDoorSwing)
 			{
-				SavePolyswingdoorThinker(th, tc_polyswingdoor);
+				SavePolyswingdoorThinker(save_p, th, tc_polyswingdoor);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyObjFlag)
 			{
-				SavePolymoveThinker(th, tc_polyflag);
+				SavePolymoveThinker(save_p, th, tc_polyflag);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyObjDisplace)
 			{
-				SavePolydisplaceThinker(th, tc_polydisplace);
+				SavePolydisplaceThinker(save_p, th, tc_polydisplace);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyObjRotDisplace)
 			{
-				SavePolyrotdisplaceThinker(th, tc_polyrotdisplace);
+				SavePolyrotdisplaceThinker(save_p, th, tc_polyrotdisplace);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_PolyObjFade)
 			{
-				SavePolyfadeThinker(th, tc_polyfade);
+				SavePolyfadeThinker(save_p, th, tc_polyfade);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_DynamicSlopeLine)
 			{
-				SaveDynamicLineSlopeThinker(th, tc_dynslopeline);
+				SaveDynamicLineSlopeThinker(save_p, th, tc_dynslopeline);
 				continue;
 			}
 			else if (th->function.acp1 == (actionf_p1)T_DynamicSlopeVert)
 			{
-				SaveDynamicVertexSlopeThinker(th, tc_dynslopevert);
+				SaveDynamicVertexSlopeThinker(save_p, th, tc_dynslopevert);
 				continue;
 			}
 #ifdef PARANOIA
 			else
-				I_Assert(th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed); // wait garbage collection
+				I_Assert(th->removing); // wait garbage collection
 #endif
 		}
 
 		CONS_Debug(DBG_NETPLAY, "%u thinkers saved in list %d\n", numsaved, i);
 
-		WRITEUINT8(save_p, tc_end);
+		P_WriteUINT8(save_p, tc_end);
 	}
 }
 
@@ -2910,7 +3214,7 @@ mobj_t *P_FindNewPosition(UINT32 oldposition)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mobj = (mobj_t *)th;
@@ -2941,12 +3245,6 @@ static line_t *LoadLine(UINT32 line)
 	return &lines[line];
 }
 
-static inline player_t *LoadPlayer(UINT32 player)
-{
-	if (player >= MAXPLAYERS) return NULL;
-	return &players[player];
-}
-
 static inline pslope_t *LoadSlope(UINT32 slopeid)
 {
 	pslope_t *p = slopelist;
@@ -2959,7 +3257,7 @@ static inline pslope_t *LoadSlope(UINT32 slopeid)
 	return NULL;
 }
 
-static thinker_t* LoadMobjThinker(actionf_p1 thinker)
+static thinker_t* LoadMobjThinker(save_t *save_p, actionf_p1 thinker)
 {
 	thinker_t *next;
 	mobj_t *mobj;
@@ -2969,35 +3267,35 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	fixed_t z, floorz, ceilingz;
 	ffloor_t *floorrover = NULL, *ceilingrover = NULL;
 
-	diff = READUINT32(save_p);
+	diff = P_ReadUINT32(save_p);
 	if (diff & MD_MORE)
-		diff2 = READUINT32(save_p);
+		diff2 = P_ReadUINT32(save_p);
 	else
 		diff2 = 0;
 
-	next = (void *)(size_t)READUINT32(save_p);
+	next = (void *)(size_t)P_ReadUINT32(save_p);
 
-	z = READFIXED(save_p); // Force this so 3dfloor problems don't arise.
-	floorz = READFIXED(save_p);
-	ceilingz = READFIXED(save_p);
+	z = P_ReadFixed(save_p); // Force this so 3dfloor problems don't arise.
+	floorz = P_ReadFixed(save_p);
+	ceilingz = P_ReadFixed(save_p);
 
 	if (diff2 & MD2_FLOORROVER)
 	{
-		sector_t *sec = LoadSector(READUINT32(save_p));
-		UINT16 id = READUINT16(save_p);
+		sector_t *sec = LoadSector(P_ReadUINT32(save_p));
+		UINT16 id = P_ReadUINT16(save_p);
 		floorrover = P_GetFFloorByID(sec, id);
 	}
 
 	if (diff2 & MD2_CEILINGROVER)
 	{
-		sector_t *sec = LoadSector(READUINT32(save_p));
-		UINT16 id = READUINT16(save_p);
+		sector_t *sec = LoadSector(P_ReadUINT32(save_p));
+		UINT16 id = P_ReadUINT16(save_p);
 		ceilingrover = P_GetFFloorByID(sec, id);
 	}
 
 	if (diff & MD_SPAWNPOINT)
 	{
-		UINT16 spawnpointnum = READUINT16(save_p);
+		UINT16 spawnpointnum = P_ReadUINT16(save_p);
 
 		if (mapthings[spawnpointnum].type == 1713) // NiGHTS Hoop special case
 		{
@@ -3023,7 +3321,7 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	mobj->ceilingrover = ceilingrover;
 
 	if (diff & MD_TYPE)
-		mobj->type = READUINT32(save_p);
+		mobj->type = P_ReadUINT32(save_p);
 	else
 	{
 		for (i = 0; i < NUMMOBJTYPES; i++)
@@ -3042,11 +3340,11 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	mobj->info = &mobjinfo[mobj->type];
 	if (diff & MD_POS)
 	{
-		mobj->x = READFIXED(save_p);
-		mobj->y = READFIXED(save_p);
-		mobj->angle = READANGLE(save_p);
-		mobj->pitch = READANGLE(save_p);
-		mobj->roll = READANGLE(save_p);
+		mobj->x = P_ReadFixed(save_p);
+		mobj->y = P_ReadFixed(save_p);
+		mobj->angle = P_ReadAngle(save_p);
+		mobj->pitch = P_ReadAngle(save_p);
+		mobj->roll = P_ReadAngle(save_p);
 	}
 	else
 	{
@@ -3058,47 +3356,47 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	}
 	if (diff & MD_MOM)
 	{
-		mobj->momx = READFIXED(save_p);
-		mobj->momy = READFIXED(save_p);
-		mobj->momz = READFIXED(save_p);
-		mobj->pmomz = READFIXED(save_p);
+		mobj->momx = P_ReadFixed(save_p);
+		mobj->momy = P_ReadFixed(save_p);
+		mobj->momz = P_ReadFixed(save_p);
+		mobj->pmomz = P_ReadFixed(save_p);
 	} // otherwise they're zero, and the memset took care of it
 
 	if (diff & MD_RADIUS)
-		mobj->radius = READFIXED(save_p);
+		mobj->radius = P_ReadFixed(save_p);
 	else
 		mobj->radius = mobj->info->radius;
 	if (diff & MD_HEIGHT)
-		mobj->height = READFIXED(save_p);
+		mobj->height = P_ReadFixed(save_p);
 	else
 		mobj->height = mobj->info->height;
 	if (diff & MD_FLAGS)
-		mobj->flags = READUINT32(save_p);
+		mobj->flags = P_ReadUINT32(save_p);
 	else
 		mobj->flags = mobj->info->flags;
 	if (diff & MD_FLAGS2)
-		mobj->flags2 = READUINT32(save_p);
+		mobj->flags2 = P_ReadUINT32(save_p);
 	if (diff & MD_HEALTH)
-		mobj->health = READINT32(save_p);
+		mobj->health = P_ReadINT32(save_p);
 	else
 		mobj->health = mobj->info->spawnhealth;
 	if (diff & MD_RTIME)
-		mobj->reactiontime = READINT32(save_p);
+		mobj->reactiontime = P_ReadINT32(save_p);
 	else
 		mobj->reactiontime = mobj->info->reactiontime;
 
 	if (diff & MD_STATE)
-		mobj->state = &states[READUINT16(save_p)];
+		mobj->state = &states[P_ReadUINT16(save_p)];
 	else
 		mobj->state = &states[mobj->info->spawnstate];
 	if (diff & MD_TICS)
-		mobj->tics = READINT32(save_p);
+		mobj->tics = P_ReadINT32(save_p);
 	else
 		mobj->tics = mobj->state->tics;
 	if (diff & MD_SPRITE) {
-		mobj->sprite = READUINT16(save_p);
+		mobj->sprite = P_ReadUINT16(save_p);
 		if (mobj->sprite == SPR_PLAY)
-			mobj->sprite2 = READUINT16(save_p);
+			mobj->sprite2 = P_ReadUINT16(save_p);
 	}
 	else {
 		mobj->sprite = mobj->state->sprite;
@@ -3107,8 +3405,8 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	}
 	if (diff & MD_FRAME)
 	{
-		mobj->frame = READUINT32(save_p);
-		mobj->anim_duration = READUINT16(save_p);
+		mobj->frame = P_ReadUINT32(save_p);
+		mobj->anim_duration = P_ReadUINT16(save_p);
 	}
 	else
 	{
@@ -3116,128 +3414,132 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 		mobj->anim_duration = (UINT16)mobj->state->var2;
 	}
 	if (diff & MD_EFLAGS)
-		mobj->eflags = READUINT16(save_p);
+		mobj->eflags = P_ReadUINT16(save_p);
 	if (diff & MD_PLAYER)
 	{
-		i = READUINT8(save_p);
+		i = P_ReadUINT8(save_p);
 		mobj->player = &players[i];
 		mobj->player->mo = mobj;
 	}
 	if (diff & MD_MOVEDIR)
-		mobj->movedir = READANGLE(save_p);
+		mobj->movedir = P_ReadAngle(save_p);
 	if (diff & MD_MOVECOUNT)
-		mobj->movecount = READINT32(save_p);
+		mobj->movecount = P_ReadINT32(save_p);
 	if (diff & MD_THRESHOLD)
-		mobj->threshold = READINT32(save_p);
+		mobj->threshold = P_ReadINT32(save_p);
 	if (diff & MD_LASTLOOK)
-		mobj->lastlook = READINT32(save_p);
+		mobj->lastlook = P_ReadINT32(save_p);
 	else
 		mobj->lastlook = -1;
 	if (diff & MD_TARGET)
-		mobj->target = (mobj_t *)(size_t)READUINT32(save_p);
+		mobj->target = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 	if (diff & MD_TRACER)
-		mobj->tracer = (mobj_t *)(size_t)READUINT32(save_p);
+		mobj->tracer = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 	if (diff & MD_FRICTION)
-		mobj->friction = READFIXED(save_p);
+		mobj->friction = P_ReadFixed(save_p);
 	else
 		mobj->friction = ORIG_FRICTION;
 	if (diff & MD_MOVEFACTOR)
-		mobj->movefactor = READFIXED(save_p);
+		mobj->movefactor = P_ReadFixed(save_p);
 	else
 		mobj->movefactor = FRACUNIT;
 	if (diff & MD_FUSE)
-		mobj->fuse = READINT32(save_p);
+		mobj->fuse = P_ReadINT32(save_p);
 	if (diff & MD_WATERTOP)
-		mobj->watertop = READFIXED(save_p);
+		mobj->watertop = P_ReadFixed(save_p);
 	if (diff & MD_WATERBOTTOM)
-		mobj->waterbottom = READFIXED(save_p);
+		mobj->waterbottom = P_ReadFixed(save_p);
 	if (diff & MD_SCALE)
-		mobj->scale = READFIXED(save_p);
+		mobj->scale = P_ReadFixed(save_p);
 	else
 		mobj->scale = FRACUNIT;
 	if (diff & MD_DSCALE)
-		mobj->destscale = READFIXED(save_p);
+		mobj->destscale = P_ReadFixed(save_p);
 	else
 		mobj->destscale = mobj->scale;
 	if (diff2 & MD2_SCALESPEED)
-		mobj->scalespeed = READFIXED(save_p);
+		mobj->scalespeed = P_ReadFixed(save_p);
 	else
 		mobj->scalespeed = FRACUNIT/12;
 	if (diff2 & MD2_CUSVAL)
-		mobj->cusval = READINT32(save_p);
+		mobj->cusval = P_ReadINT32(save_p);
 	if (diff2 & MD2_CVMEM)
-		mobj->cvmem = READINT32(save_p);
+		mobj->cvmem = P_ReadINT32(save_p);
 	if (diff2 & MD2_SKIN)
-		mobj->skin = skins[READUINT8(save_p)];
+		mobj->skin = skins[P_ReadUINT8(save_p)];
 	if (diff2 & MD2_COLOR)
-		mobj->color = READUINT16(save_p);
+		mobj->color = P_ReadUINT16(save_p);
 	if (diff2 & MD2_EXTVAL1)
-		mobj->extravalue1 = READINT32(save_p);
+		mobj->extravalue1 = P_ReadINT32(save_p);
 	if (diff2 & MD2_EXTVAL2)
-		mobj->extravalue2 = READINT32(save_p);
+		mobj->extravalue2 = P_ReadINT32(save_p);
 	if (diff2 & MD2_HNEXT)
-		mobj->hnext = (mobj_t *)(size_t)READUINT32(save_p);
+		mobj->hnext = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 	if (diff2 & MD2_HPREV)
-		mobj->hprev = (mobj_t *)(size_t)READUINT32(save_p);
+		mobj->hprev = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 	if (diff2 & MD2_SLOPE)
-		mobj->standingslope = P_SlopeById(READUINT16(save_p));
+		mobj->standingslope = P_SlopeById(P_ReadUINT16(save_p));
 	if (diff2 & MD2_COLORIZED)
-		mobj->colorized = READUINT8(save_p);
+		mobj->colorized = P_ReadUINT8(save_p);
 	if (diff2 & MD2_MIRRORED)
-		mobj->mirrored = READUINT8(save_p);
+		mobj->mirrored = P_ReadUINT8(save_p);
 	if (diff2 & MD2_SPRITEROLL)
-		mobj->spriteroll = READANGLE(save_p);
+		mobj->spriteroll = P_ReadAngle(save_p);
 	if (diff2 & MD2_SHADOWSCALE)
-		mobj->shadowscale = READFIXED(save_p);
+		mobj->shadowscale = P_ReadFixed(save_p);
 	if (diff2 & MD2_RENDERFLAGS)
-		mobj->renderflags = READUINT32(save_p);
+		mobj->renderflags = P_ReadUINT32(save_p);
 	if (diff2 & MD2_BLENDMODE)
-		mobj->blendmode = READINT32(save_p);
+		mobj->blendmode = P_ReadINT32(save_p);
 	else
 		mobj->blendmode = AST_TRANSLUCENT;
 	if (diff2 & MD2_SPRITEXSCALE)
-		mobj->spritexscale = READFIXED(save_p);
+		mobj->spritexscale = P_ReadFixed(save_p);
 	else
 		mobj->spritexscale = FRACUNIT;
 	if (diff2 & MD2_SPRITEYSCALE)
-		mobj->spriteyscale = READFIXED(save_p);
+		mobj->spriteyscale = P_ReadFixed(save_p);
 	else
 		mobj->spriteyscale = FRACUNIT;
 	if (diff2 & MD2_SPRITEXOFFSET)
-		mobj->spritexoffset = READFIXED(save_p);
+		mobj->spritexoffset = P_ReadFixed(save_p);
 	if (diff2 & MD2_SPRITEYOFFSET)
-		mobj->spriteyoffset = READFIXED(save_p);
+		mobj->spriteyoffset = P_ReadFixed(save_p);
 	if (diff2 & MD2_FLOORSPRITESLOPE)
 	{
 		pslope_t *slope = (pslope_t *)P_CreateFloorSpriteSlope(mobj);
 
-		slope->zdelta = READFIXED(save_p);
-		slope->zangle = READANGLE(save_p);
-		slope->xydirection = READANGLE(save_p);
+		slope->zdelta = P_ReadFixed(save_p);
+		slope->zangle = P_ReadAngle(save_p);
+		slope->xydirection = P_ReadAngle(save_p);
 
-		slope->o.x = READFIXED(save_p);
-		slope->o.y = READFIXED(save_p);
-		slope->o.z = READFIXED(save_p);
+		slope->o.x = P_ReadFixed(save_p);
+		slope->o.y = P_ReadFixed(save_p);
+		slope->o.z = P_ReadFixed(save_p);
 
-		slope->d.x = READFIXED(save_p);
-		slope->d.y = READFIXED(save_p);
+		slope->d.x = P_ReadFixed(save_p);
+		slope->d.y = P_ReadFixed(save_p);
 
-		slope->normal.x = READFIXED(save_p);
-		slope->normal.y = READFIXED(save_p);
-		slope->normal.z = READFIXED(save_p);
+		slope->normal.x = P_ReadFixed(save_p);
+		slope->normal.y = P_ReadFixed(save_p);
+		slope->normal.z = P_ReadFixed(save_p);
 
 		slope->moved = true;
 	}
 	if (diff2 & MD2_DRAWONLYFORPLAYER)
-		mobj->drawonlyforplayer = &players[READUINT8(save_p)];
+		mobj->drawonlyforplayer = &players[P_ReadUINT8(save_p)];
 	if (diff2 & MD2_DONTDRAWFORVIEWMOBJ)
-		mobj->dontdrawforviewmobj = (mobj_t *)(size_t)READUINT32(save_p);
+		mobj->dontdrawforviewmobj = (mobj_t *)(size_t)P_ReadUINT32(save_p);
 	if (diff2 & MD2_DISPOFFSET)
-		mobj->dispoffset = READINT32(save_p);
+		mobj->dispoffset = P_ReadINT32(save_p);
 	else
 		mobj->dispoffset = mobj->info->dispoffset;
 	if (diff2 & MD2_TRANSLATION)
-		mobj->translation = READUINT16(save_p);
+		mobj->translation = P_ReadUINT16(save_p);
+	if (diff2 & MD2_ALPHA)
+		mobj->alpha = P_ReadFixed(save_p);
+	else
+		mobj->alpha = FRACUNIT;
 
 	if (diff & MD_REDFLAG)
 	{
@@ -3253,7 +3555,7 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	// set sprev, snext, bprev, bnext, subsector
 	P_SetThingPosition(mobj);
 
-	mobj->mobjnum = READUINT32(save_p);
+	mobj->mobjnum = P_ReadUINT32(save_p);
 
 	if (mobj->player)
 	{
@@ -3282,25 +3584,25 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	return &mobj->thinker;
 }
 
-static thinker_t* LoadNoEnemiesThinker(actionf_p1 thinker)
+static thinker_t* LoadNoEnemiesThinker(save_t *save_p, actionf_p1 thinker)
 {
 	noenemies_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sourceline = LoadLine(READUINT32(save_p));
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
 	return &ht->thinker;
 }
 
-static thinker_t* LoadBounceCheeseThinker(actionf_p1 thinker)
+static thinker_t* LoadBounceCheeseThinker(save_t *save_p, actionf_p1 thinker)
 {
 	bouncecheese_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sourceline = LoadLine(READUINT32(save_p));
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->speed = READFIXED(save_p);
-	ht->distance = READFIXED(save_p);
-	ht->floorwasheight = READFIXED(save_p);
-	ht->ceilingwasheight = READFIXED(save_p);
-	ht->low = READCHAR(save_p);
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->speed = P_ReadFixed(save_p);
+	ht->distance = P_ReadFixed(save_p);
+	ht->floorwasheight = P_ReadFixed(save_p);
+	ht->ceilingwasheight = P_ReadFixed(save_p);
+	ht->low = P_ReadChar(save_p);
 
 	if (ht->sector)
 		ht->sector->ceilingdata = ht;
@@ -3308,16 +3610,16 @@ static thinker_t* LoadBounceCheeseThinker(actionf_p1 thinker)
 	return &ht->thinker;
 }
 
-static thinker_t* LoadContinuousFallThinker(actionf_p1 thinker)
+static thinker_t* LoadContinuousFallThinker(save_t *save_p, actionf_p1 thinker)
 {
 	continuousfall_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->speed = READFIXED(save_p);
-	ht->direction = READINT32(save_p);
-	ht->floorstartheight = READFIXED(save_p);
-	ht->ceilingstartheight = READFIXED(save_p);
-	ht->destheight = READFIXED(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->speed = P_ReadFixed(save_p);
+	ht->direction = P_ReadINT32(save_p);
+	ht->floorstartheight = P_ReadFixed(save_p);
+	ht->ceilingstartheight = P_ReadFixed(save_p);
+	ht->destheight = P_ReadFixed(save_p);
 
 	if (ht->sector)
 	{
@@ -3328,16 +3630,16 @@ static thinker_t* LoadContinuousFallThinker(actionf_p1 thinker)
 	return &ht->thinker;
 }
 
-static thinker_t* LoadMarioBlockThinker(actionf_p1 thinker)
+static thinker_t* LoadMarioBlockThinker(save_t *save_p, actionf_p1 thinker)
 {
 	mariothink_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->speed = READFIXED(save_p);
-	ht->direction = READINT32(save_p);
-	ht->floorstartheight = READFIXED(save_p);
-	ht->ceilingstartheight = READFIXED(save_p);
-	ht->tag = READINT16(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->speed = P_ReadFixed(save_p);
+	ht->direction = P_ReadINT32(save_p);
+	ht->floorstartheight = P_ReadFixed(save_p);
+	ht->ceilingstartheight = P_ReadFixed(save_p);
+	ht->tag = P_ReadINT16(save_p);
 
 	if (ht->sector)
 	{
@@ -3348,29 +3650,29 @@ static thinker_t* LoadMarioBlockThinker(actionf_p1 thinker)
 	return &ht->thinker;
 }
 
-static thinker_t* LoadMarioCheckThinker(actionf_p1 thinker)
+static thinker_t* LoadMarioCheckThinker(save_t *save_p, actionf_p1 thinker)
 {
 	mariocheck_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sourceline = LoadLine(READUINT32(save_p));
-	ht->sector = LoadSector(READUINT32(save_p));
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
 	return &ht->thinker;
 }
 
-static thinker_t* LoadThwompThinker(actionf_p1 thinker)
+static thinker_t* LoadThwompThinker(save_t *save_p, actionf_p1 thinker)
 {
 	thwomp_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sourceline = LoadLine(READUINT32(save_p));
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->crushspeed = READFIXED(save_p);
-	ht->retractspeed = READFIXED(save_p);
-	ht->direction = READINT32(save_p);
-	ht->floorstartheight = READFIXED(save_p);
-	ht->ceilingstartheight = READFIXED(save_p);
-	ht->delay = READINT32(save_p);
-	ht->tag = READINT16(save_p);
-	ht->sound = READUINT16(save_p);
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->crushspeed = P_ReadFixed(save_p);
+	ht->retractspeed = P_ReadFixed(save_p);
+	ht->direction = P_ReadINT32(save_p);
+	ht->floorstartheight = P_ReadFixed(save_p);
+	ht->ceilingstartheight = P_ReadFixed(save_p);
+	ht->delay = P_ReadINT32(save_p);
+	ht->tag = P_ReadINT16(save_p);
+	ht->sound = P_ReadUINT16(save_p);
 
 	if (ht->sector)
 	{
@@ -3381,163 +3683,163 @@ static thinker_t* LoadThwompThinker(actionf_p1 thinker)
 	return &ht->thinker;
 }
 
-static thinker_t* LoadFloatThinker(actionf_p1 thinker)
+static thinker_t* LoadFloatThinker(save_t *save_p, actionf_p1 thinker)
 {
 	floatthink_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sourceline = LoadLine(READUINT32(save_p));
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->tag = READINT16(save_p);
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->tag = P_ReadINT16(save_p);
 	return &ht->thinker;
 }
 
-static thinker_t* LoadEachTimeThinker(actionf_p1 thinker)
+static thinker_t* LoadEachTimeThinker(save_t *save_p, actionf_p1 thinker)
 {
 	size_t i;
 	eachtime_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sourceline = LoadLine(READUINT32(save_p));
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		ht->playersInArea[i] = READCHAR(save_p);
+		ht->playersInArea[i] = P_ReadChar(save_p);
 	}
-	ht->triggerOnExit = READCHAR(save_p);
+	ht->triggerOnExit = P_ReadChar(save_p);
 	return &ht->thinker;
 }
 
-static thinker_t* LoadRaiseThinker(actionf_p1 thinker)
+static thinker_t* LoadRaiseThinker(save_t *save_p, actionf_p1 thinker)
 {
 	raise_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->tag = READINT16(save_p);
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->ceilingbottom = READFIXED(save_p);
-	ht->ceilingtop = READFIXED(save_p);
-	ht->basespeed = READFIXED(save_p);
-	ht->extraspeed = READFIXED(save_p);
-	ht->shaketimer = READUINT8(save_p);
-	ht->flags = READUINT8(save_p);
+	ht->tag = P_ReadINT16(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->ceilingbottom = P_ReadFixed(save_p);
+	ht->ceilingtop = P_ReadFixed(save_p);
+	ht->basespeed = P_ReadFixed(save_p);
+	ht->extraspeed = P_ReadFixed(save_p);
+	ht->shaketimer = P_ReadUINT8(save_p);
+	ht->flags = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static thinker_t* LoadCeilingThinker(actionf_p1 thinker)
+static thinker_t* LoadCeilingThinker(save_t *save_p, actionf_p1 thinker)
 {
 	ceiling_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->type = READUINT8(save_p);
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->bottomheight = READFIXED(save_p);
-	ht->topheight = READFIXED(save_p);
-	ht->speed = READFIXED(save_p);
-	ht->delay = READFIXED(save_p);
-	ht->delaytimer = READFIXED(save_p);
-	ht->crush = READUINT8(save_p);
-	ht->texture = READINT32(save_p);
-	ht->direction = READINT32(save_p);
-	ht->tag = READINT16(save_p);
-	ht->origspeed = READFIXED(save_p);
-	ht->sourceline = READFIXED(save_p);
+	ht->type = P_ReadUINT8(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->bottomheight = P_ReadFixed(save_p);
+	ht->topheight = P_ReadFixed(save_p);
+	ht->speed = P_ReadFixed(save_p);
+	ht->delay = P_ReadFixed(save_p);
+	ht->delaytimer = P_ReadFixed(save_p);
+	ht->crush = P_ReadUINT8(save_p);
+	ht->texture = P_ReadINT32(save_p);
+	ht->direction = P_ReadINT32(save_p);
+	ht->tag = P_ReadINT16(save_p);
+	ht->origspeed = P_ReadFixed(save_p);
+	ht->sourceline = P_ReadFixed(save_p);
 	if (ht->sector)
 		ht->sector->ceilingdata = ht;
 	return &ht->thinker;
 }
 
-static thinker_t* LoadFloormoveThinker(actionf_p1 thinker)
+static thinker_t* LoadFloormoveThinker(save_t *save_p, actionf_p1 thinker)
 {
 	floormove_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->type = READUINT8(save_p);
-	ht->crush = READUINT8(save_p);
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->direction = READINT32(save_p);
-	ht->texture = READINT32(save_p);
-	ht->floordestheight = READFIXED(save_p);
-	ht->speed = READFIXED(save_p);
-	ht->origspeed = READFIXED(save_p);
-	ht->delay = READFIXED(save_p);
-	ht->delaytimer = READFIXED(save_p);
-	ht->tag = READINT16(save_p);
-	ht->sourceline = READFIXED(save_p);
+	ht->type = P_ReadUINT8(save_p);
+	ht->crush = P_ReadUINT8(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->direction = P_ReadINT32(save_p);
+	ht->texture = P_ReadINT32(save_p);
+	ht->floordestheight = P_ReadFixed(save_p);
+	ht->speed = P_ReadFixed(save_p);
+	ht->origspeed = P_ReadFixed(save_p);
+	ht->delay = P_ReadFixed(save_p);
+	ht->delaytimer = P_ReadFixed(save_p);
+	ht->tag = P_ReadINT16(save_p);
+	ht->sourceline = P_ReadFixed(save_p);
 	if (ht->sector)
 		ht->sector->floordata = ht;
 	return &ht->thinker;
 }
 
-static thinker_t* LoadLightflashThinker(actionf_p1 thinker)
+static thinker_t* LoadLightflashThinker(save_t *save_p, actionf_p1 thinker)
 {
 	lightflash_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->maxlight = READINT32(save_p);
-	ht->minlight = READINT32(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->maxlight = P_ReadINT32(save_p);
+	ht->minlight = P_ReadINT32(save_p);
 	if (ht->sector)
 		ht->sector->lightingdata = ht;
 	return &ht->thinker;
 }
 
-static thinker_t* LoadStrobeThinker(actionf_p1 thinker)
+static thinker_t* LoadStrobeThinker(save_t *save_p, actionf_p1 thinker)
 {
 	strobe_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->count = READINT32(save_p);
-	ht->minlight = READINT16(save_p);
-	ht->maxlight = READINT16(save_p);
-	ht->darktime = READINT32(save_p);
-	ht->brighttime = READINT32(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->count = P_ReadINT32(save_p);
+	ht->minlight = P_ReadINT16(save_p);
+	ht->maxlight = P_ReadINT16(save_p);
+	ht->darktime = P_ReadINT32(save_p);
+	ht->brighttime = P_ReadINT32(save_p);
 	if (ht->sector)
 		ht->sector->lightingdata = ht;
 	return &ht->thinker;
 }
 
-static thinker_t* LoadGlowThinker(actionf_p1 thinker)
+static thinker_t* LoadGlowThinker(save_t *save_p, actionf_p1 thinker)
 {
 	glow_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->minlight = READINT16(save_p);
-	ht->maxlight = READINT16(save_p);
-	ht->direction = READINT16(save_p);
-	ht->speed = READINT16(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->minlight = P_ReadINT16(save_p);
+	ht->maxlight = P_ReadINT16(save_p);
+	ht->direction = P_ReadINT16(save_p);
+	ht->speed = P_ReadINT16(save_p);
 	if (ht->sector)
 		ht->sector->lightingdata = ht;
 	return &ht->thinker;
 }
 
-static thinker_t* LoadFireflickerThinker(actionf_p1 thinker)
+static thinker_t* LoadFireflickerThinker(save_t *save_p, actionf_p1 thinker)
 {
 	fireflicker_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->count = READINT32(save_p);
-	ht->resetcount = READINT32(save_p);
-	ht->maxlight = READINT16(save_p);
-	ht->minlight = READINT16(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->count = P_ReadINT32(save_p);
+	ht->resetcount = P_ReadINT32(save_p);
+	ht->maxlight = P_ReadINT16(save_p);
+	ht->minlight = P_ReadINT16(save_p);
 	if (ht->sector)
 		ht->sector->lightingdata = ht;
 	return &ht->thinker;
 }
 
-static thinker_t* LoadElevatorThinker(actionf_p1 thinker, boolean setplanedata)
+static thinker_t* LoadElevatorThinker(save_t *save_p, actionf_p1 thinker, boolean setplanedata)
 {
 	elevator_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->type = READUINT8(save_p);
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->actionsector = LoadSector(READUINT32(save_p));
-	ht->direction = READINT32(save_p);
-	ht->floordestheight = READFIXED(save_p);
-	ht->ceilingdestheight = READFIXED(save_p);
-	ht->speed = READFIXED(save_p);
-	ht->origspeed = READFIXED(save_p);
-	ht->low = READFIXED(save_p);
-	ht->high = READFIXED(save_p);
-	ht->distance = READFIXED(save_p);
-	ht->delay = READFIXED(save_p);
-	ht->delaytimer = READFIXED(save_p);
-	ht->floorwasheight = READFIXED(save_p);
-	ht->ceilingwasheight = READFIXED(save_p);
-	ht->sourceline = LoadLine(READUINT32(save_p));
+	ht->type = P_ReadUINT8(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->actionsector = LoadSector(P_ReadUINT32(save_p));
+	ht->direction = P_ReadINT32(save_p);
+	ht->floordestheight = P_ReadFixed(save_p);
+	ht->ceilingdestheight = P_ReadFixed(save_p);
+	ht->speed = P_ReadFixed(save_p);
+	ht->origspeed = P_ReadFixed(save_p);
+	ht->low = P_ReadFixed(save_p);
+	ht->high = P_ReadFixed(save_p);
+	ht->distance = P_ReadFixed(save_p);
+	ht->delay = P_ReadFixed(save_p);
+	ht->delaytimer = P_ReadFixed(save_p);
+	ht->floorwasheight = P_ReadFixed(save_p);
+	ht->ceilingwasheight = P_ReadFixed(save_p);
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
 
 	if (ht->sector && setplanedata)
 	{
@@ -3548,21 +3850,21 @@ static thinker_t* LoadElevatorThinker(actionf_p1 thinker, boolean setplanedata)
 	return &ht->thinker;
 }
 
-static thinker_t* LoadCrumbleThinker(actionf_p1 thinker)
+static thinker_t* LoadCrumbleThinker(save_t *save_p, actionf_p1 thinker)
 {
 	crumble_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sourceline = LoadLine(READUINT32(save_p));
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->actionsector = LoadSector(READUINT32(save_p));
-	ht->player = LoadPlayer(READUINT32(save_p));
-	ht->direction = READINT32(save_p);
-	ht->origalpha = READINT32(save_p);
-	ht->timer = READINT32(save_p);
-	ht->speed = READFIXED(save_p);
-	ht->floorwasheight = READFIXED(save_p);
-	ht->ceilingwasheight = READFIXED(save_p);
-	ht->flags = READUINT8(save_p);
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->actionsector = LoadSector(P_ReadUINT32(save_p));
+	ht->player = LoadPlayer(P_ReadUINT32(save_p));
+	ht->direction = P_ReadINT32(save_p);
+	ht->origalpha = P_ReadINT32(save_p);
+	ht->timer = P_ReadINT32(save_p);
+	ht->speed = P_ReadFixed(save_p);
+	ht->floorwasheight = P_ReadFixed(save_p);
+	ht->ceilingwasheight = P_ReadFixed(save_p);
+	ht->flags = P_ReadUINT8(save_p);
 
 	if (ht->sector)
 		ht->sector->floordata = ht;
@@ -3570,123 +3872,123 @@ static thinker_t* LoadCrumbleThinker(actionf_p1 thinker)
 	return &ht->thinker;
 }
 
-static thinker_t* LoadScrollThinker(actionf_p1 thinker)
+static thinker_t* LoadScrollThinker(save_t *save_p, actionf_p1 thinker)
 {
 	scroll_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->dx = READFIXED(save_p);
-	ht->dy = READFIXED(save_p);
-	ht->affectee = READINT32(save_p);
-	ht->control = READINT32(save_p);
-	ht->last_height = READFIXED(save_p);
-	ht->vdx = READFIXED(save_p);
-	ht->vdy = READFIXED(save_p);
-	ht->accel = READINT32(save_p);
-	ht->exclusive = READINT32(save_p);
-	ht->type = READUINT8(save_p);
+	ht->dx = P_ReadFixed(save_p);
+	ht->dy = P_ReadFixed(save_p);
+	ht->affectee = P_ReadINT32(save_p);
+	ht->control = P_ReadINT32(save_p);
+	ht->last_height = P_ReadFixed(save_p);
+	ht->vdx = P_ReadFixed(save_p);
+	ht->vdy = P_ReadFixed(save_p);
+	ht->accel = P_ReadINT32(save_p);
+	ht->exclusive = P_ReadINT32(save_p);
+	ht->type = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadFrictionThinker(actionf_p1 thinker)
+static inline thinker_t* LoadFrictionThinker(save_t *save_p, actionf_p1 thinker)
 {
 	friction_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->friction = READINT32(save_p);
-	ht->movefactor = READINT32(save_p);
-	ht->affectee = READINT32(save_p);
-	ht->referrer = READINT32(save_p);
-	ht->roverfriction = READUINT8(save_p);
+	ht->friction = P_ReadINT32(save_p);
+	ht->movefactor = P_ReadINT32(save_p);
+	ht->affectee = P_ReadINT32(save_p);
+	ht->referrer = P_ReadINT32(save_p);
+	ht->roverfriction = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static thinker_t* LoadPusherThinker(actionf_p1 thinker)
+static thinker_t* LoadPusherThinker(save_t *save_p, actionf_p1 thinker)
 {
 	pusher_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->type = READUINT8(save_p);
-	ht->x_mag = READFIXED(save_p);
-	ht->y_mag = READFIXED(save_p);
-	ht->z_mag = READFIXED(save_p);
-	ht->affectee = READINT32(save_p);
-	ht->roverpusher = READUINT8(save_p);
-	ht->referrer = READINT32(save_p);
-	ht->exclusive = READINT32(save_p);
-	ht->slider = READINT32(save_p);
+	ht->type = P_ReadUINT8(save_p);
+	ht->x_mag = P_ReadFixed(save_p);
+	ht->y_mag = P_ReadFixed(save_p);
+	ht->z_mag = P_ReadFixed(save_p);
+	ht->affectee = P_ReadINT32(save_p);
+	ht->roverpusher = P_ReadUINT8(save_p);
+	ht->referrer = P_ReadINT32(save_p);
+	ht->exclusive = P_ReadINT32(save_p);
+	ht->slider = P_ReadINT32(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadLaserThinker(actionf_p1 thinker)
+static inline thinker_t* LoadLaserThinker(save_t *save_p, actionf_p1 thinker)
 {
 	laserthink_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->tag = READINT16(save_p);
-	ht->sourceline = LoadLine(READUINT32(save_p));
-	ht->nobosses = READUINT8(save_p);
+	ht->tag = P_ReadINT16(save_p);
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
+	ht->nobosses = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadLightlevelThinker(actionf_p1 thinker)
+static inline thinker_t* LoadLightlevelThinker(save_t *save_p, actionf_p1 thinker)
 {
 	lightlevel_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->sourcelevel = READINT16(save_p);
-	ht->destlevel = READINT16(save_p);
-	ht->fixedcurlevel = READFIXED(save_p);
-	ht->fixedpertic = READFIXED(save_p);
-	ht->timer = READINT32(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->sourcelevel = P_ReadINT16(save_p);
+	ht->destlevel = P_ReadINT16(save_p);
+	ht->fixedcurlevel = P_ReadFixed(save_p);
+	ht->fixedpertic = P_ReadFixed(save_p);
+	ht->timer = P_ReadINT32(save_p);
 	if (ht->sector)
 		ht->sector->lightingdata = ht;
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadExecutorThinker(actionf_p1 thinker)
+static inline thinker_t* LoadExecutorThinker(save_t *save_p, actionf_p1 thinker)
 {
 	executor_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->line = LoadLine(READUINT32(save_p));
-	ht->caller = LoadMobj(READUINT32(save_p));
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->timer = READINT32(save_p);
+	ht->line = LoadLine(P_ReadUINT32(save_p));
+	ht->caller = LoadMobj(P_ReadUINT32(save_p));
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->timer = P_ReadINT32(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadDisappearThinker(actionf_p1 thinker)
+static inline thinker_t* LoadDisappearThinker(save_t *save_p, actionf_p1 thinker)
 {
 	disappear_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->appeartime = READUINT32(save_p);
-	ht->disappeartime = READUINT32(save_p);
-	ht->offset = READUINT32(save_p);
-	ht->timer = READUINT32(save_p);
-	ht->affectee = READINT32(save_p);
-	ht->sourceline = READINT32(save_p);
-	ht->exists = READINT32(save_p);
+	ht->appeartime = P_ReadUINT32(save_p);
+	ht->disappeartime = P_ReadUINT32(save_p);
+	ht->offset = P_ReadUINT32(save_p);
+	ht->timer = P_ReadUINT32(save_p);
+	ht->affectee = P_ReadINT32(save_p);
+	ht->sourceline = P_ReadINT32(save_p);
+	ht->exists = P_ReadINT32(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadFadeThinker(actionf_p1 thinker)
+static inline thinker_t* LoadFadeThinker(save_t *save_p, actionf_p1 thinker)
 {
 	sector_t *ss;
 	fade_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->dest_exc = GetNetColormapFromList(READUINT32(save_p));
-	ht->sectornum = READUINT32(save_p);
-	ht->ffloornum = READUINT32(save_p);
-	ht->alpha = READINT32(save_p);
-	ht->sourcevalue = READINT16(save_p);
-	ht->destvalue = READINT16(save_p);
-	ht->destlightlevel = READINT16(save_p);
-	ht->speed = READINT16(save_p);
-	ht->ticbased = (boolean)READUINT8(save_p);
-	ht->timer = READINT32(save_p);
-	ht->doexists = READUINT8(save_p);
-	ht->dotranslucent = READUINT8(save_p);
-	ht->dolighting = READUINT8(save_p);
-	ht->docolormap = READUINT8(save_p);
-	ht->docollision = READUINT8(save_p);
-	ht->doghostfade = READUINT8(save_p);
-	ht->exactalpha = READUINT8(save_p);
+	ht->dest_exc = GetNetColormapFromList(P_ReadUINT32(save_p));
+	ht->sectornum = P_ReadUINT32(save_p);
+	ht->ffloornum = P_ReadUINT32(save_p);
+	ht->alpha = P_ReadINT32(save_p);
+	ht->sourcevalue = P_ReadINT16(save_p);
+	ht->destvalue = P_ReadINT16(save_p);
+	ht->destlightlevel = P_ReadINT16(save_p);
+	ht->speed = P_ReadINT16(save_p);
+	ht->ticbased = (boolean)P_ReadUINT8(save_p);
+	ht->timer = P_ReadINT32(save_p);
+	ht->doexists = P_ReadUINT8(save_p);
+	ht->dotranslucent = P_ReadUINT8(save_p);
+	ht->dolighting = P_ReadUINT8(save_p);
+	ht->docolormap = P_ReadUINT8(save_p);
+	ht->docollision = P_ReadUINT8(save_p);
+	ht->doghostfade = P_ReadUINT8(save_p);
+	ht->exactalpha = P_ReadUINT8(save_p);
 
 	ss = LoadSector(ht->sectornum);
 	if (ss)
@@ -3707,176 +4009,176 @@ static inline thinker_t* LoadFadeThinker(actionf_p1 thinker)
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadFadeColormapThinker(actionf_p1 thinker)
+static inline thinker_t* LoadFadeColormapThinker(save_t *save_p, actionf_p1 thinker)
 {
 	fadecolormap_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->sector = LoadSector(READUINT32(save_p));
-	ht->source_exc = GetNetColormapFromList(READUINT32(save_p));
-	ht->dest_exc = GetNetColormapFromList(READUINT32(save_p));
-	ht->ticbased = (boolean)READUINT8(save_p);
-	ht->duration = READINT32(save_p);
-	ht->timer = READINT32(save_p);
+	ht->sector = LoadSector(P_ReadUINT32(save_p));
+	ht->source_exc = GetNetColormapFromList(P_ReadUINT32(save_p));
+	ht->dest_exc = GetNetColormapFromList(P_ReadUINT32(save_p));
+	ht->ticbased = (boolean)P_ReadUINT8(save_p);
+	ht->duration = P_ReadINT32(save_p);
+	ht->timer = P_ReadINT32(save_p);
 	if (ht->sector)
 		ht->sector->fadecolormapdata = ht;
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadPlaneDisplaceThinker(actionf_p1 thinker)
+static inline thinker_t* LoadPlaneDisplaceThinker(save_t *save_p, actionf_p1 thinker)
 {
 	planedisplace_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
 
-	ht->affectee = READINT32(save_p);
-	ht->control = READINT32(save_p);
-	ht->last_height = READFIXED(save_p);
-	ht->speed = READFIXED(save_p);
-	ht->type = READUINT8(save_p);
+	ht->affectee = P_ReadINT32(save_p);
+	ht->control = P_ReadINT32(save_p);
+	ht->last_height = P_ReadFixed(save_p);
+	ht->speed = P_ReadFixed(save_p);
+	ht->type = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadDynamicLineSlopeThinker(actionf_p1 thinker)
+static inline thinker_t* LoadDynamicLineSlopeThinker(save_t *save_p, actionf_p1 thinker)
 {
 	dynlineplanethink_t* ht = Z_Malloc(sizeof(*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
 
-	ht->type = READUINT8(save_p);
-	ht->slope = LoadSlope(READUINT32(save_p));
-	ht->sourceline = LoadLine(READUINT32(save_p));
-	ht->extent = READFIXED(save_p);
+	ht->type = P_ReadUINT8(save_p);
+	ht->slope = LoadSlope(P_ReadUINT32(save_p));
+	ht->sourceline = LoadLine(P_ReadUINT32(save_p));
+	ht->extent = P_ReadFixed(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadDynamicVertexSlopeThinker(actionf_p1 thinker)
+static inline thinker_t* LoadDynamicVertexSlopeThinker(save_t *save_p, actionf_p1 thinker)
 {
 	size_t i;
 	dynvertexplanethink_t* ht = Z_Malloc(sizeof(*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
 
-	ht->slope = LoadSlope(READUINT32(save_p));
+	ht->slope = LoadSlope(P_ReadUINT32(save_p));
 	for (i = 0; i < 3; i++)
-		ht->secs[i] = LoadSector(READUINT32(save_p));
-	READMEM(save_p, ht->vex, sizeof(ht->vex));
-	READMEM(save_p, ht->origsecheights, sizeof(ht->origsecheights));
-	READMEM(save_p, ht->origvecheights, sizeof(ht->origvecheights));
-	ht->relative = READUINT8(save_p);
+		ht->secs[i] = LoadSector(P_ReadUINT32(save_p));
+	P_ReadMem(save_p, ht->vex, sizeof(ht->vex));
+	P_ReadMem(save_p, ht->origsecheights, sizeof(ht->origsecheights));
+	P_ReadMem(save_p, ht->origvecheights, sizeof(ht->origvecheights));
+	ht->relative = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadPolyrotatetThinker(actionf_p1 thinker)
+static inline thinker_t* LoadPolyrotatetThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polyrotate_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->speed = READINT32(save_p);
-	ht->distance = READINT32(save_p);
-	ht->turnobjs = READUINT8(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->speed = P_ReadINT32(save_p);
+	ht->distance = P_ReadINT32(save_p);
+	ht->turnobjs = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static thinker_t* LoadPolymoveThinker(actionf_p1 thinker)
+static thinker_t* LoadPolymoveThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polymove_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->speed = READINT32(save_p);
-	ht->momx = READFIXED(save_p);
-	ht->momy = READFIXED(save_p);
-	ht->distance = READINT32(save_p);
-	ht->angle = READANGLE(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->speed = P_ReadINT32(save_p);
+	ht->momx = P_ReadFixed(save_p);
+	ht->momy = P_ReadFixed(save_p);
+	ht->distance = P_ReadINT32(save_p);
+	ht->angle = P_ReadAngle(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadPolywaypointThinker(actionf_p1 thinker)
+static inline thinker_t* LoadPolywaypointThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polywaypoint_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->speed = READINT32(save_p);
-	ht->sequence = READINT32(save_p);
-	ht->pointnum = READINT32(save_p);
-	ht->direction = READINT32(save_p);
-	ht->returnbehavior = READUINT8(save_p);
-	ht->continuous = READUINT8(save_p);
-	ht->stophere = READUINT8(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->speed = P_ReadINT32(save_p);
+	ht->sequence = P_ReadINT32(save_p);
+	ht->pointnum = P_ReadINT32(save_p);
+	ht->direction = P_ReadINT32(save_p);
+	ht->returnbehavior = P_ReadUINT8(save_p);
+	ht->continuous = P_ReadUINT8(save_p);
+	ht->stophere = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadPolyslidedoorThinker(actionf_p1 thinker)
+static inline thinker_t* LoadPolyslidedoorThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polyslidedoor_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->delay = READINT32(save_p);
-	ht->delayCount = READINT32(save_p);
-	ht->initSpeed = READINT32(save_p);
-	ht->speed = READINT32(save_p);
-	ht->initDistance = READINT32(save_p);
-	ht->distance = READINT32(save_p);
-	ht->initAngle = READUINT32(save_p);
-	ht->angle = READUINT32(save_p);
-	ht->revAngle = READUINT32(save_p);
-	ht->momx = READFIXED(save_p);
-	ht->momy = READFIXED(save_p);
-	ht->closing = READUINT8(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->delay = P_ReadINT32(save_p);
+	ht->delayCount = P_ReadINT32(save_p);
+	ht->initSpeed = P_ReadINT32(save_p);
+	ht->speed = P_ReadINT32(save_p);
+	ht->initDistance = P_ReadINT32(save_p);
+	ht->distance = P_ReadINT32(save_p);
+	ht->initAngle = P_ReadUINT32(save_p);
+	ht->angle = P_ReadUINT32(save_p);
+	ht->revAngle = P_ReadUINT32(save_p);
+	ht->momx = P_ReadFixed(save_p);
+	ht->momy = P_ReadFixed(save_p);
+	ht->closing = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadPolyswingdoorThinker(actionf_p1 thinker)
+static inline thinker_t* LoadPolyswingdoorThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polyswingdoor_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->delay = READINT32(save_p);
-	ht->delayCount = READINT32(save_p);
-	ht->initSpeed = READINT32(save_p);
-	ht->speed = READINT32(save_p);
-	ht->initDistance = READINT32(save_p);
-	ht->distance = READINT32(save_p);
-	ht->closing = READUINT8(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->delay = P_ReadINT32(save_p);
+	ht->delayCount = P_ReadINT32(save_p);
+	ht->initSpeed = P_ReadINT32(save_p);
+	ht->speed = P_ReadINT32(save_p);
+	ht->initDistance = P_ReadINT32(save_p);
+	ht->distance = P_ReadINT32(save_p);
+	ht->closing = P_ReadUINT8(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadPolydisplaceThinker(actionf_p1 thinker)
+static inline thinker_t* LoadPolydisplaceThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polydisplace_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->controlSector = LoadSector(READUINT32(save_p));
-	ht->dx = READFIXED(save_p);
-	ht->dy = READFIXED(save_p);
-	ht->oldHeights = READFIXED(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->controlSector = LoadSector(P_ReadUINT32(save_p));
+	ht->dx = P_ReadFixed(save_p);
+	ht->dy = P_ReadFixed(save_p);
+	ht->oldHeights = P_ReadFixed(save_p);
 	return &ht->thinker;
 }
 
-static inline thinker_t* LoadPolyrotdisplaceThinker(actionf_p1 thinker)
+static inline thinker_t* LoadPolyrotdisplaceThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polyrotdisplace_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->controlSector = LoadSector(READUINT32(save_p));
-	ht->rotscale = READFIXED(save_p);
-	ht->turnobjs = READUINT8(save_p);
-	ht->oldHeights = READFIXED(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->controlSector = LoadSector(P_ReadUINT32(save_p));
+	ht->rotscale = P_ReadFixed(save_p);
+	ht->turnobjs = P_ReadUINT8(save_p);
+	ht->oldHeights = P_ReadFixed(save_p);
 	return &ht->thinker;
 }
 
-static thinker_t* LoadPolyfadeThinker(actionf_p1 thinker)
+static thinker_t* LoadPolyfadeThinker(save_t *save_p, actionf_p1 thinker)
 {
 	polyfade_t *ht = Z_Malloc(sizeof (*ht), PU_LEVSPEC, NULL);
 	ht->thinker.function.acp1 = thinker;
-	ht->polyObjNum = READINT32(save_p);
-	ht->sourcevalue = READINT32(save_p);
-	ht->destvalue = READINT32(save_p);
-	ht->docollision = (boolean)READUINT8(save_p);
-	ht->doghostfade = (boolean)READUINT8(save_p);
-	ht->ticbased = (boolean)READUINT8(save_p);
-	ht->duration = READINT32(save_p);
-	ht->timer = READINT32(save_p);
+	ht->polyObjNum = P_ReadINT32(save_p);
+	ht->sourcevalue = P_ReadINT32(save_p);
+	ht->destvalue = P_ReadINT32(save_p);
+	ht->docollision = (boolean)P_ReadUINT8(save_p);
+	ht->doghostfade = (boolean)P_ReadUINT8(save_p);
+	ht->ticbased = (boolean)P_ReadUINT8(save_p);
+	ht->duration = P_ReadINT32(save_p);
+	ht->timer = P_ReadINT32(save_p);
 	return &ht->thinker;
 }
 
-static void P_NetUnArchiveThinkers(void)
+static void P_NetUnArchiveThinkers(save_t *save_p)
 {
 	thinker_t *currentthinker;
 	thinker_t *next;
@@ -3885,7 +4187,7 @@ static void P_NetUnArchiveThinkers(void)
 	UINT32 i;
 	UINT32 numloaded = 0;
 
-	if (READUINT32(save_p) != ARCHIVEBLOCK_THINKERS)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_THINKERS)
 		I_Error("Bad $$$.sav at archive block Thinkers");
 
 	// remove all the current thinkers
@@ -3923,7 +4225,7 @@ static void P_NetUnArchiveThinkers(void)
 		for (;;)
 		{
 			thinker_t* th = NULL;
-			tclass = READUINT8(save_p);
+			tclass = P_ReadUINT8(save_p);
 
 			if (tclass == tc_end)
 				break; // leave the saved thinker reading loop
@@ -3932,167 +4234,167 @@ static void P_NetUnArchiveThinkers(void)
 			switch (tclass)
 			{
 				case tc_mobj:
-					th = LoadMobjThinker((actionf_p1)P_MobjThinker);
+					th = LoadMobjThinker(save_p, (actionf_p1)P_MobjThinker);
 					break;
 
 				case tc_ceiling:
-					th = LoadCeilingThinker((actionf_p1)T_MoveCeiling);
+					th = LoadCeilingThinker(save_p, (actionf_p1)T_MoveCeiling);
 					break;
 
 				case tc_crushceiling:
-					th = LoadCeilingThinker((actionf_p1)T_CrushCeiling);
+					th = LoadCeilingThinker(save_p, (actionf_p1)T_CrushCeiling);
 					break;
 
 				case tc_floor:
-					th = LoadFloormoveThinker((actionf_p1)T_MoveFloor);
+					th = LoadFloormoveThinker(save_p, (actionf_p1)T_MoveFloor);
 					break;
 
 				case tc_flash:
-					th = LoadLightflashThinker((actionf_p1)T_LightningFlash);
+					th = LoadLightflashThinker(save_p, (actionf_p1)T_LightningFlash);
 					break;
 
 				case tc_strobe:
-					th = LoadStrobeThinker((actionf_p1)T_StrobeFlash);
+					th = LoadStrobeThinker(save_p, (actionf_p1)T_StrobeFlash);
 					break;
 
 				case tc_glow:
-					th = LoadGlowThinker((actionf_p1)T_Glow);
+					th = LoadGlowThinker(save_p, (actionf_p1)T_Glow);
 					break;
 
 				case tc_fireflicker:
-					th = LoadFireflickerThinker((actionf_p1)T_FireFlicker);
+					th = LoadFireflickerThinker(save_p, (actionf_p1)T_FireFlicker);
 					break;
 
 				case tc_elevator:
-					th = LoadElevatorThinker((actionf_p1)T_MoveElevator, true);
+					th = LoadElevatorThinker(save_p, (actionf_p1)T_MoveElevator, true);
 					break;
 
 				case tc_continuousfalling:
-					th = LoadContinuousFallThinker((actionf_p1)T_ContinuousFalling);
+					th = LoadContinuousFallThinker(save_p, (actionf_p1)T_ContinuousFalling);
 					break;
 
 				case tc_thwomp:
-					th = LoadThwompThinker((actionf_p1)T_ThwompSector);
+					th = LoadThwompThinker(save_p, (actionf_p1)T_ThwompSector);
 					break;
 
 				case tc_noenemies:
-					th = LoadNoEnemiesThinker((actionf_p1)T_NoEnemiesSector);
+					th = LoadNoEnemiesThinker(save_p, (actionf_p1)T_NoEnemiesSector);
 					break;
 
 				case tc_eachtime:
-					th = LoadEachTimeThinker((actionf_p1)T_EachTimeThinker);
+					th = LoadEachTimeThinker(save_p, (actionf_p1)T_EachTimeThinker);
 					break;
 
 				case tc_raisesector:
-					th = LoadRaiseThinker((actionf_p1)T_RaiseSector);
+					th = LoadRaiseThinker(save_p, (actionf_p1)T_RaiseSector);
 					break;
 
 				case tc_camerascanner:
-					th = LoadElevatorThinker((actionf_p1)T_CameraScanner, false);
+					th = LoadElevatorThinker(save_p, (actionf_p1)T_CameraScanner, false);
 					break;
 
 				case tc_bouncecheese:
-					th = LoadBounceCheeseThinker((actionf_p1)T_BounceCheese);
+					th = LoadBounceCheeseThinker(save_p, (actionf_p1)T_BounceCheese);
 					break;
 
 				case tc_startcrumble:
-					th = LoadCrumbleThinker((actionf_p1)T_StartCrumble);
+					th = LoadCrumbleThinker(save_p, (actionf_p1)T_StartCrumble);
 					break;
 
 				case tc_marioblock:
-					th = LoadMarioBlockThinker((actionf_p1)T_MarioBlock);
+					th = LoadMarioBlockThinker(save_p, (actionf_p1)T_MarioBlock);
 					break;
 
 				case tc_marioblockchecker:
-					th = LoadMarioCheckThinker((actionf_p1)T_MarioBlockChecker);
+					th = LoadMarioCheckThinker(save_p, (actionf_p1)T_MarioBlockChecker);
 					break;
 
 				case tc_floatsector:
-					th = LoadFloatThinker((actionf_p1)T_FloatSector);
+					th = LoadFloatThinker(save_p, (actionf_p1)T_FloatSector);
 					break;
 
 				case tc_laserflash:
-					th = LoadLaserThinker((actionf_p1)T_LaserFlash);
+					th = LoadLaserThinker(save_p, (actionf_p1)T_LaserFlash);
 					break;
 
 				case tc_lightfade:
-					th = LoadLightlevelThinker((actionf_p1)T_LightFade);
+					th = LoadLightlevelThinker(save_p, (actionf_p1)T_LightFade);
 					break;
 
 				case tc_executor:
-					th = LoadExecutorThinker((actionf_p1)T_ExecutorDelay);
+					th = LoadExecutorThinker(save_p, (actionf_p1)T_ExecutorDelay);
 					restoreNum = true;
 					break;
 
 				case tc_disappear:
-					th = LoadDisappearThinker((actionf_p1)T_Disappear);
+					th = LoadDisappearThinker(save_p, (actionf_p1)T_Disappear);
 					break;
 
 				case tc_fade:
-					th = LoadFadeThinker((actionf_p1)T_Fade);
+					th = LoadFadeThinker(save_p, (actionf_p1)T_Fade);
 					break;
 
 				case tc_fadecolormap:
-					th = LoadFadeColormapThinker((actionf_p1)T_FadeColormap);
+					th = LoadFadeColormapThinker(save_p, (actionf_p1)T_FadeColormap);
 					break;
 
 				case tc_planedisplace:
-					th = LoadPlaneDisplaceThinker((actionf_p1)T_PlaneDisplace);
+					th = LoadPlaneDisplaceThinker(save_p, (actionf_p1)T_PlaneDisplace);
 					break;
 				case tc_polyrotate:
-					th = LoadPolyrotatetThinker((actionf_p1)T_PolyObjRotate);
+					th = LoadPolyrotatetThinker(save_p, (actionf_p1)T_PolyObjRotate);
 					break;
 
 				case tc_polymove:
-					th = LoadPolymoveThinker((actionf_p1)T_PolyObjMove);
+					th = LoadPolymoveThinker(save_p, (actionf_p1)T_PolyObjMove);
 					break;
 
 				case tc_polywaypoint:
-					th = LoadPolywaypointThinker((actionf_p1)T_PolyObjWaypoint);
+					th = LoadPolywaypointThinker(save_p, (actionf_p1)T_PolyObjWaypoint);
 					break;
 
 				case tc_polyslidedoor:
-					th = LoadPolyslidedoorThinker((actionf_p1)T_PolyDoorSlide);
+					th = LoadPolyslidedoorThinker(save_p, (actionf_p1)T_PolyDoorSlide);
 					break;
 
 				case tc_polyswingdoor:
-					th = LoadPolyswingdoorThinker((actionf_p1)T_PolyDoorSwing);
+					th = LoadPolyswingdoorThinker(save_p, (actionf_p1)T_PolyDoorSwing);
 					break;
 
 				case tc_polyflag:
-					th = LoadPolymoveThinker((actionf_p1)T_PolyObjFlag);
+					th = LoadPolymoveThinker(save_p, (actionf_p1)T_PolyObjFlag);
 					break;
 
 				case tc_polydisplace:
-					th = LoadPolydisplaceThinker((actionf_p1)T_PolyObjDisplace);
+					th = LoadPolydisplaceThinker(save_p, (actionf_p1)T_PolyObjDisplace);
 					break;
 
 				case tc_polyrotdisplace:
-					th = LoadPolyrotdisplaceThinker((actionf_p1)T_PolyObjRotDisplace);
+					th = LoadPolyrotdisplaceThinker(save_p, (actionf_p1)T_PolyObjRotDisplace);
 					break;
 
 				case tc_polyfade:
-					th = LoadPolyfadeThinker((actionf_p1)T_PolyObjFade);
+					th = LoadPolyfadeThinker(save_p, (actionf_p1)T_PolyObjFade);
 					break;
 
 				case tc_dynslopeline:
-					th = LoadDynamicLineSlopeThinker((actionf_p1)T_DynamicSlopeLine);
+					th = LoadDynamicLineSlopeThinker(save_p, (actionf_p1)T_DynamicSlopeLine);
 					break;
 
 				case tc_dynslopevert:
-					th = LoadDynamicVertexSlopeThinker((actionf_p1)T_DynamicSlopeVert);
+					th = LoadDynamicVertexSlopeThinker(save_p, (actionf_p1)T_DynamicSlopeVert);
 					break;
 
 				case tc_scroll:
-					th = LoadScrollThinker((actionf_p1)T_Scroll);
+					th = LoadScrollThinker(save_p, (actionf_p1)T_Scroll);
 					break;
 
 				case tc_friction:
-					th = LoadFrictionThinker((actionf_p1)T_Friction);
+					th = LoadFrictionThinker(save_p, (actionf_p1)T_Friction);
 					break;
 
 				case tc_pusher:
-					th = LoadPusherThinker((actionf_p1)T_Pusher);
+					th = LoadPusherThinker(save_p, (actionf_p1)T_Pusher);
 					break;
 
 				default:
@@ -4132,29 +4434,29 @@ static void P_NetUnArchiveThinkers(void)
 #define PD_FLAGS  0x01
 #define PD_TRANS   0x02
 
-static inline void P_ArchivePolyObj(polyobj_t *po)
+static inline void P_ArchivePolyObj(save_t *save_p, polyobj_t *po)
 {
 	UINT8 diff = 0;
-	WRITEINT32(save_p, po->id);
-	WRITEANGLE(save_p, po->angle);
+	P_WriteINT32(save_p, po->id);
+	P_WriteAngle(save_p, po->angle);
 
-	WRITEFIXED(save_p, po->spawnSpot.x);
-	WRITEFIXED(save_p, po->spawnSpot.y);
+	P_WriteFixed(save_p, po->spawnSpot.x);
+	P_WriteFixed(save_p, po->spawnSpot.y);
 
 	if (po->flags != po->spawnflags)
 		diff |= PD_FLAGS;
 	if (po->translucency != po->spawntrans)
 		diff |= PD_TRANS;
 
-	WRITEUINT8(save_p, diff);
+	P_WriteUINT8(save_p, diff);
 
 	if (diff & PD_FLAGS)
-		WRITEINT32(save_p, po->flags);
+		P_WriteINT32(save_p, po->flags);
 	if (diff & PD_TRANS)
-		WRITEINT32(save_p, po->translucency);
+		P_WriteINT32(save_p, po->translucency);
 }
 
-static inline void P_UnArchivePolyObj(polyobj_t *po)
+static inline void P_UnArchivePolyObj(save_t *save_p, polyobj_t *po)
 {
 	INT32 id;
 	UINT32 angle;
@@ -4166,19 +4468,19 @@ static inline void P_UnArchivePolyObj(polyobj_t *po)
 	// when they first start to run.
 	po->thinker = NULL;
 
-	id = READINT32(save_p);
+	id = P_ReadINT32(save_p);
 
-	angle = READANGLE(save_p);
+	angle = P_ReadAngle(save_p);
 
-	x = READFIXED(save_p);
-	y = READFIXED(save_p);
+	x = P_ReadFixed(save_p);
+	y = P_ReadFixed(save_p);
 
-	diff = READUINT8(save_p);
+	diff = P_ReadUINT8(save_p);
 
 	if (diff & PD_FLAGS)
-		po->flags = READINT32(save_p);
+		po->flags = P_ReadINT32(save_p);
 	if (diff & PD_TRANS)
-		po->translucency = READINT32(save_p);
+		po->translucency = P_ReadINT32(save_p);
 
 	// if the object is bad or isn't in the id hash, we can do nothing more
 	// with it, so return now
@@ -4189,33 +4491,33 @@ static inline void P_UnArchivePolyObj(polyobj_t *po)
 	Polyobj_MoveOnLoad(po, angle, x, y);
 }
 
-static inline void P_ArchivePolyObjects(void)
+static inline void P_ArchivePolyObjects(save_t *save_p)
 {
 	INT32 i;
 
-	WRITEUINT32(save_p, ARCHIVEBLOCK_POBJS);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_POBJS);
 
 	// save number of polyobjects
-	WRITEINT32(save_p, numPolyObjects);
+	P_WriteINT32(save_p, numPolyObjects);
 
 	for (i = 0; i < numPolyObjects; ++i)
-		P_ArchivePolyObj(&PolyObjects[i]);
+		P_ArchivePolyObj(save_p, &PolyObjects[i]);
 }
 
-static inline void P_UnArchivePolyObjects(void)
+static inline void P_UnArchivePolyObjects(save_t *save_p)
 {
 	INT32 i, numSavedPolys;
 
-	if (READUINT32(save_p) != ARCHIVEBLOCK_POBJS)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_POBJS)
 		I_Error("Bad $$$.sav at archive block Pobjs");
 
-	numSavedPolys = READINT32(save_p);
+	numSavedPolys = P_ReadINT32(save_p);
 
 	if (numSavedPolys != numPolyObjects)
 		I_Error("P_UnArchivePolyObjects: polyobj count inconsistency\n");
 
 	for (i = 0; i < numSavedPolys; ++i)
-		P_UnArchivePolyObj(&PolyObjects[i]);
+		P_UnArchivePolyObj(save_p, &PolyObjects[i]);
 }
 
 static inline void P_FinishMobjs(void)
@@ -4227,7 +4529,7 @@ static inline void P_FinishMobjs(void)
 	for (currentthinker = thlist[THINK_MOBJ].next; currentthinker != &thlist[THINK_MOBJ];
 		currentthinker = currentthinker->next)
 	{
-		if (currentthinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (currentthinker->removing)
 			continue;
 
 		mobj = (mobj_t *)currentthinker;
@@ -4245,7 +4547,7 @@ static void P_RelinkPointers(void)
 	for (currentthinker = thlist[THINK_MOBJ].next; currentthinker != &thlist[THINK_MOBJ];
 		currentthinker = currentthinker->next)
 	{
-		if (currentthinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (currentthinker->removing)
 			continue;
 
 		mobj = (mobj_t *)currentthinker;
@@ -4333,11 +4635,11 @@ static void P_RelinkPointers(void)
 	}
 }
 
-static inline void P_NetArchiveSpecials(void)
+static inline void P_NetArchiveSpecials(save_t *save_p)
 {
 	size_t i, z;
 
-	WRITEUINT32(save_p, ARCHIVEBLOCK_SPECIALS);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_SPECIALS);
 
 	// itemrespawn queue for deathmatch
 	i = iquetail;
@@ -4347,53 +4649,54 @@ static inline void P_NetArchiveSpecials(void)
 		{
 			if (&mapthings[z] == itemrespawnque[i])
 			{
-				WRITEUINT32(save_p, z);
+				P_WriteUINT32(save_p, z);
 				break;
 			}
 		}
-		WRITEUINT32(save_p, itemrespawntime[i]);
+		P_WriteUINT32(save_p, itemrespawntime[i]);
 		i = (i + 1) & (ITEMQUESIZE-1);
 	}
 
 	// end delimiter
-	WRITEUINT32(save_p, 0xffffffff);
+	P_WriteUINT32(save_p, 0xffffffff);
 
 	// Sky number
-	WRITEINT32(save_p, globallevelskynum);
+	P_WriteINT32(save_p, globallevelskynum);
 
 	// Current global weather type
-	WRITEUINT8(save_p, globalweather);
+	P_WriteUINT8(save_p, globalweather);
 
 	if (metalplayback) // Is metal sonic running?
 	{
-		WRITEUINT8(save_p, 0x01);
-		G_SaveMetal(&save_p);
+		UINT8 *p = &save_p->buf[save_p->pos+1];
+		P_WriteUINT8(save_p, 0x01);
+		G_SaveMetal(&p);
 	}
 	else
-		WRITEUINT8(save_p, 0x00);
+		P_WriteUINT8(save_p, 0x00);
 }
 
-static void P_NetUnArchiveSpecials(void)
+static void P_NetUnArchiveSpecials(save_t *save_p)
 {
 	size_t i;
 	INT32 j;
 
-	if (READUINT32(save_p) != ARCHIVEBLOCK_SPECIALS)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_SPECIALS)
 		I_Error("Bad $$$.sav at archive block Specials");
 
 	// BP: added save itemrespawn queue for deathmatch
 	iquetail = iquehead = 0;
-	while ((i = READUINT32(save_p)) != 0xffffffff)
+	while ((i = P_ReadUINT32(save_p)) != 0xffffffff)
 	{
 		itemrespawnque[iquehead] = &mapthings[i];
-		itemrespawntime[iquehead++] = READINT32(save_p);
+		itemrespawntime[iquehead++] = P_ReadINT32(save_p);
 	}
 
-	j = READINT32(save_p);
+	j = P_ReadINT32(save_p);
 	if (j != globallevelskynum)
 		P_SetupLevelSky(j, true);
 
-	globalweather = READUINT8(save_p);
+	globalweather = P_ReadUINT8(save_p);
 
 	if (globalweather)
 	{
@@ -4408,14 +4711,17 @@ static void P_NetUnArchiveSpecials(void)
 			P_SwitchWeather(globalweather);
 	}
 
-	if (READUINT8(save_p) == 0x01) // metal sonic
-		G_LoadMetal(&save_p);
+	if (P_ReadUINT8(save_p) == 0x01) // metal sonic
+	{
+		UINT8 *p = &save_p->buf[save_p->pos];
+		G_LoadMetal(&p);
+	}
 }
 
 // =======================================================================
 //          Misc
 // =======================================================================
-static inline void P_ArchiveMisc(INT16 mapnum)
+static inline void P_ArchiveMisc(save_t *save_p, INT16 mapnum)
 {
 	//lastmapsaved = mapnum;
 	lastmaploaded = mapnum;
@@ -4423,16 +4729,16 @@ static inline void P_ArchiveMisc(INT16 mapnum)
 	if (gamecomplete)
 		mapnum |= 8192;
 
-	WRITEINT16(save_p, mapnum);
-	WRITEUINT16(save_p, emeralds+357);
-	WRITESTRINGN(save_p, timeattackfolder, sizeof(timeattackfolder));
+	P_WriteINT16(save_p, mapnum);
+	P_WriteUINT16(save_p, emeralds+357);
+	P_WriteStringN(save_p, timeattackfolder, sizeof(timeattackfolder));
 }
 
-static inline void P_UnArchiveSPGame(INT16 mapoverride)
+static inline void P_UnArchiveSPGame(save_t *save_p, INT16 mapoverride)
 {
 	char testname[sizeof(timeattackfolder)];
 
-	gamemap = READINT16(save_p);
+	gamemap = P_ReadINT16(save_p);
 
 	if (mapoverride != 0)
 	{
@@ -4453,9 +4759,9 @@ static inline void P_UnArchiveSPGame(INT16 mapoverride)
 	tokenlist = 0;
 	token = 0;
 
-	savedata.emeralds = READUINT16(save_p)-357;
+	savedata.emeralds = P_ReadUINT16(save_p)-357;
 
-	READSTRINGN(save_p, testname, sizeof(testname));
+	P_ReadStringN(save_p, testname, sizeof(testname));
 
 	if (strcmp(testname, timeattackfolder))
 	{
@@ -4469,100 +4775,106 @@ static inline void P_UnArchiveSPGame(INT16 mapoverride)
 	playeringame[consoleplayer] = true;
 }
 
-static void P_NetArchiveMisc(boolean resending)
+static void P_NetArchiveMisc(save_t *save_p, boolean resending)
 {
 	INT32 i;
 
-	WRITEUINT32(save_p, ARCHIVEBLOCK_MISC);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_MISC);
 
 	if (resending)
-		WRITEUINT32(save_p, gametic);
-	WRITEINT16(save_p, gamemap);
+		P_WriteUINT32(save_p, gametic);
+	P_WriteINT16(save_p, gamemap);
 
 	if (gamestate != GS_LEVEL)
-		WRITEINT16(save_p, GS_WAITINGPLAYERS); // nice hack to put people back into waitingplayers
+		P_WriteINT16(save_p, GS_WAITINGPLAYERS); // nice hack to put people back into waitingplayers
 	else
-		WRITEINT16(save_p, gamestate);
-	WRITEINT16(save_p, gametype);
+		P_WriteINT16(save_p, gamestate);
+	P_WriteINT16(save_p, gametype);
 
 	{
 		UINT32 pig = 0;
 		for (i = 0; i < MAXPLAYERS; i++)
 			pig |= (playeringame[i] != 0)<<i;
-		WRITEUINT32(save_p, pig);
+		P_WriteUINT32(save_p, pig);
 	}
 
-	WRITEUINT32(save_p, P_GetRandSeed());
+	P_WriteUINT32(save_p, P_GetRandSeed());
 
-	WRITEUINT32(save_p, tokenlist);
+	P_WriteUINT32(save_p, tokenlist);
 
-	WRITEUINT32(save_p, leveltime);
-	WRITEUINT32(save_p, ssspheres);
-	WRITEINT16(save_p, lastmap);
-	WRITEUINT16(save_p, bossdisabled);
+	P_WriteUINT32(save_p, leveltime);
+	P_WriteUINT32(save_p, ssspheres);
+	P_WriteINT16(save_p, lastmap);
+	P_WriteUINT16(save_p, bossdisabled);
 
-	WRITEUINT16(save_p, emeralds);
+	P_WriteUINT16(save_p, emeralds);
 	{
 		UINT8 globools = 0;
 		if (stagefailed)
 			globools |= 1;
 		if (stoppedclock)
 			globools |= (1<<1);
-		WRITEUINT8(save_p, globools);
+		P_WriteUINT8(save_p, globools);
 	}
 
-	WRITEUINT32(save_p, token);
-	WRITEINT32(save_p, sstimer);
-	WRITEUINT32(save_p, bluescore);
-	WRITEUINT32(save_p, redscore);
+	P_WriteUINT32(save_p, token);
+	P_WriteINT32(save_p, sstimer);
+	P_WriteUINT32(save_p, bluescore);
+	P_WriteUINT32(save_p, redscore);
 
-	WRITEUINT16(save_p, skincolor_redteam);
-	WRITEUINT16(save_p, skincolor_blueteam);
-	WRITEUINT16(save_p, skincolor_redring);
-	WRITEUINT16(save_p, skincolor_bluering);
+	P_WriteUINT16(save_p, skincolor_redteam);
+	P_WriteUINT16(save_p, skincolor_blueteam);
+	P_WriteUINT16(save_p, skincolor_redring);
+	P_WriteUINT16(save_p, skincolor_bluering);
 
-	WRITEINT32(save_p, modulothing);
+	P_WriteINT32(save_p, modulothing);
 
-	WRITEINT16(save_p, autobalance);
-	WRITEINT16(save_p, teamscramble);
+	P_WriteINT16(save_p, autobalance);
+	P_WriteINT16(save_p, teamscramble);
 
 	for (i = 0; i < MAXPLAYERS; i++)
-		WRITEINT16(save_p, scrambleplayers[i]);
+		P_WriteINT16(save_p, scrambleplayers[i]);
 
 	for (i = 0; i < MAXPLAYERS; i++)
-		WRITEINT16(save_p, scrambleteams[i]);
+		P_WriteINT16(save_p, scrambleteams[i]);
 
-	WRITEINT16(save_p, scrambletotal);
-	WRITEINT16(save_p, scramblecount);
+	P_WriteINT16(save_p, scrambletotal);
+	P_WriteINT16(save_p, scramblecount);
 
-	WRITEUINT32(save_p, countdown);
-	WRITEUINT32(save_p, countdown2);
+	P_WriteUINT32(save_p, countdown);
+	P_WriteUINT32(save_p, countdown2);
 
-	WRITEFIXED(save_p, gravity);
+	P_WriteFixed(save_p, gravity);
 
-	WRITEUINT32(save_p, countdowntimer);
-	WRITEUINT8(save_p, countdowntimeup);
+	P_WriteUINT32(save_p, countdowntimer);
+	P_WriteUINT8(save_p, countdowntimeup);
 
-	WRITEUINT32(save_p, hidetime);
+	P_WriteUINT32(save_p, hidetime);
 
 	// Is it paused?
 	if (paused)
-		WRITEUINT8(save_p, 0x2f);
+		P_WriteUINT8(save_p, 0x2f);
 	else
-		WRITEUINT8(save_p, 0x2e);
+		P_WriteUINT8(save_p, 0x2e);
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		P_WriteUINT8(save_p, spam_tokens[i]);
+		P_WriteUINT32(save_p, spam_tics[i]);
+	}
 }
 
-static inline boolean P_NetUnArchiveMisc(boolean reloading)
+static inline boolean P_NetUnArchiveMisc(save_t *save_p, boolean reloading)
 {
 	INT32 i;
 
-	if (READUINT32(save_p) != ARCHIVEBLOCK_MISC)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_MISC)
 		I_Error("Bad $$$.sav at archive block Misc");
 
 	if (reloading)
-		gametic = READUINT32(save_p);
+		gametic = P_ReadUINT32(save_p);
 
-	gamemap = READINT16(save_p);
+	gamemap = P_ReadINT16(save_p);
 
 	// gamemap changed; we assume that its map header is always valid,
 	// so make it so
@@ -4573,12 +4885,12 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 	// normally sets this flag
 	mapmusflags |= MUSIC_RELOADRESET;
 
-	G_SetGamestate(READINT16(save_p));
+	G_SetGamestate(P_ReadINT16(save_p));
 
-	gametype = READINT16(save_p);
+	gametype = P_ReadINT16(save_p);
 
 	{
-		UINT32 pig = READUINT32(save_p);
+		UINT32 pig = P_ReadUINT32(save_p);
 		for (i = 0; i < MAXPLAYERS; i++)
 		{
 			playeringame[i] = (pig & (1<<i)) != 0;
@@ -4586,9 +4898,9 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 		}
 	}
 
-	P_SetRandSeed(READUINT32(save_p));
+	P_SetRandSeed(P_ReadUINT32(save_p));
 
-	tokenlist = READUINT32(save_p);
+	tokenlist = P_ReadUINT32(save_p);
 
 	if (!P_LoadLevel(true, reloading))
 	{
@@ -4597,83 +4909,89 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 	}
 
 	// get the time
-	leveltime = READUINT32(save_p);
-	ssspheres = READUINT32(save_p);
-	lastmap = READINT16(save_p);
-	bossdisabled = READUINT16(save_p);
+	leveltime = P_ReadUINT32(save_p);
+	ssspheres = P_ReadUINT32(save_p);
+	lastmap = P_ReadINT16(save_p);
+	bossdisabled = P_ReadUINT16(save_p);
 
-	emeralds = READUINT16(save_p);
+	emeralds = P_ReadUINT16(save_p);
 	{
-		UINT8 globools = READUINT8(save_p);
+		UINT8 globools = P_ReadUINT8(save_p);
 		stagefailed = !!(globools & 1);
 		stoppedclock = !!(globools & (1<<1));
 	}
 
-	token = READUINT32(save_p);
-	sstimer = READINT32(save_p);
-	bluescore = READUINT32(save_p);
-	redscore = READUINT32(save_p);
+	token = P_ReadUINT32(save_p);
+	sstimer = P_ReadINT32(save_p);
+	bluescore = P_ReadUINT32(save_p);
+	redscore = P_ReadUINT32(save_p);
 
-	skincolor_redteam = READUINT16(save_p);
-	skincolor_blueteam = READUINT16(save_p);
-	skincolor_redring = READUINT16(save_p);
-	skincolor_bluering = READUINT16(save_p);
+	skincolor_redteam = P_ReadUINT16(save_p);
+	skincolor_blueteam = P_ReadUINT16(save_p);
+	skincolor_redring = P_ReadUINT16(save_p);
+	skincolor_bluering = P_ReadUINT16(save_p);
 
-	modulothing = READINT32(save_p);
+	modulothing = P_ReadINT32(save_p);
 
-	autobalance = READINT16(save_p);
-	teamscramble = READINT16(save_p);
+	autobalance = P_ReadINT16(save_p);
+	teamscramble = P_ReadINT16(save_p);
 
 	for (i = 0; i < MAXPLAYERS; i++)
-		scrambleplayers[i] = READINT16(save_p);
+		scrambleplayers[i] = P_ReadINT16(save_p);
 
 	for (i = 0; i < MAXPLAYERS; i++)
-		scrambleteams[i] = READINT16(save_p);
+		scrambleteams[i] = P_ReadINT16(save_p);
 
-	scrambletotal = READINT16(save_p);
-	scramblecount = READINT16(save_p);
+	scrambletotal = P_ReadINT16(save_p);
+	scramblecount = P_ReadINT16(save_p);
 
-	countdown = READUINT32(save_p);
-	countdown2 = READUINT32(save_p);
+	countdown = P_ReadUINT32(save_p);
+	countdown2 = P_ReadUINT32(save_p);
 
-	gravity = READFIXED(save_p);
+	gravity = P_ReadFixed(save_p);
 
-	countdowntimer = (tic_t)READUINT32(save_p);
-	countdowntimeup = (boolean)READUINT8(save_p);
+	countdowntimer = (tic_t)P_ReadUINT32(save_p);
+	countdowntimeup = (boolean)P_ReadUINT8(save_p);
 
-	hidetime = READUINT32(save_p);
+	hidetime = P_ReadUINT32(save_p);
 
 	// Is it paused?
-	if (READUINT8(save_p) == 0x2f)
+	if (P_ReadUINT8(save_p) == 0x2f)
 		paused = true;
 
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		spam_tokens[i] = P_ReadUINT8(save_p);
+		spam_tics[i] = P_ReadUINT32(save_p);
+	}
+
 	return true;
 }
 
-static inline void P_NetArchiveEmblems(void)
+static inline void P_NetArchiveEmblems(save_t *save_p)
 {
 	gamedata_t *data = serverGamedata;
 	INT32 i, j;
 	UINT8 btemp;
 	INT32 curmare;
 
-	WRITEUINT32(save_p, ARCHIVEBLOCK_EMBLEMS);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_EMBLEMS);
 
 	// These should be synchronized before savegame loading by the wad files being the same anyway,
 	// but just in case, for now, we'll leave them here for testing. It would be very bad if they mismatch.
-	WRITEUINT8(save_p, (UINT8)savemoddata);
-	WRITEINT32(save_p, numemblems);
-	WRITEINT32(save_p, numextraemblems);
+	P_WriteUINT8(save_p, (UINT8)savemoddata);
+	P_WriteINT32(save_p, numemblems);
+	P_WriteINT32(save_p, numextraemblems);
 
 	// The rest of this is lifted straight from G_SaveGameData in g_game.c
 	// TODO: Optimize this to only send information about emblems, unlocks, etc. which actually exist
 	//       There is no need to go all the way up to MAXEMBLEMS when wads are guaranteed to be the same.
 
-	WRITEUINT32(save_p, data->totalplaytime);
+	P_WriteUINT32(save_p, data->totalplaytime);
 
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
-		WRITEUINT8(save_p, (data->mapvisited[i] & MV_MAX));
+		P_WriteUINT8(save_p, (data->mapvisited[i] & MV_MAX));
 
 	// To save space, use one bit per collected/achieved/unlocked flag
 	for (i = 0; i < MAXEMBLEMS;)
@@ -4681,7 +4999,7 @@ static inline void P_NetArchiveEmblems(void)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
 			btemp |= (data->collected[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		P_WriteUINT8(save_p, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXEXTRAEMBLEMS;)
@@ -4689,7 +5007,7 @@ static inline void P_NetArchiveEmblems(void)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
 			btemp |= (data->extraCollected[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		P_WriteUINT8(save_p, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXUNLOCKABLES;)
@@ -4697,7 +5015,7 @@ static inline void P_NetArchiveEmblems(void)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
 			btemp |= (data->unlocked[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		P_WriteUINT8(save_p, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXCONDITIONSETS;)
@@ -4705,28 +5023,28 @@ static inline void P_NetArchiveEmblems(void)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
 			btemp |= (data->achieved[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		P_WriteUINT8(save_p, btemp);
 		i += j;
 	}
 
-	WRITEUINT32(save_p, data->timesBeaten);
-	WRITEUINT32(save_p, data->timesBeatenWithEmeralds);
-	WRITEUINT32(save_p, data->timesBeatenUltimate);
+	P_WriteUINT32(save_p, data->timesBeaten);
+	P_WriteUINT32(save_p, data->timesBeatenWithEmeralds);
+	P_WriteUINT32(save_p, data->timesBeatenUltimate);
 
 	// Main records
 	for (i = 0; i < NUMMAPS; i++)
 	{
 		if (data->mainrecords[i])
 		{
-			WRITEUINT32(save_p, data->mainrecords[i]->score);
-			WRITEUINT32(save_p, data->mainrecords[i]->time);
-			WRITEUINT16(save_p, data->mainrecords[i]->rings);
+			P_WriteUINT32(save_p, data->mainrecords[i]->score);
+			P_WriteUINT32(save_p, data->mainrecords[i]->time);
+			P_WriteUINT16(save_p, data->mainrecords[i]->rings);
 		}
 		else
 		{
-			WRITEUINT32(save_p, 0);
-			WRITEUINT32(save_p, 0);
-			WRITEUINT16(save_p, 0);
+			P_WriteUINT32(save_p, 0);
+			P_WriteUINT32(save_p, 0);
+			P_WriteUINT16(save_p, 0);
 		}
 	}
 
@@ -4735,43 +5053,43 @@ static inline void P_NetArchiveEmblems(void)
 	{
 		if (!data->nightsrecords[i] || !data->nightsrecords[i]->nummares)
 		{
-			WRITEUINT8(save_p, 0);
+			P_WriteUINT8(save_p, 0);
 			continue;
 		}
 
-		WRITEUINT8(save_p, data->nightsrecords[i]->nummares);
+		P_WriteUINT8(save_p, data->nightsrecords[i]->nummares);
 
 		for (curmare = 0; curmare < (data->nightsrecords[i]->nummares + 1); ++curmare)
 		{
-			WRITEUINT32(save_p, data->nightsrecords[i]->score[curmare]);
-			WRITEUINT8(save_p, data->nightsrecords[i]->grade[curmare]);
-			WRITEUINT32(save_p, data->nightsrecords[i]->time[curmare]);
+			P_WriteUINT32(save_p, data->nightsrecords[i]->score[curmare]);
+			P_WriteUINT8(save_p, data->nightsrecords[i]->grade[curmare]);
+			P_WriteUINT32(save_p, data->nightsrecords[i]->time[curmare]);
 		}
 	}
 
 	// Mid-map stuff
-	WRITEUINT32(save_p, unlocktriggers);
+	P_WriteUINT32(save_p, unlocktriggers);
 
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
 		if (!ntemprecords[i].nummares)
 		{
-			WRITEUINT8(save_p, 0);
+			P_WriteUINT8(save_p, 0);
 			continue;
 		}
 
-		WRITEUINT8(save_p, ntemprecords[i].nummares);
+		P_WriteUINT8(save_p, ntemprecords[i].nummares);
 
 		for (curmare = 0; curmare < (ntemprecords[i].nummares + 1); ++curmare)
 		{
-			WRITEUINT32(save_p, ntemprecords[i].score[curmare]);
-			WRITEUINT8(save_p, ntemprecords[i].grade[curmare]);
-			WRITEUINT32(save_p, ntemprecords[i].time[curmare]);
+			P_WriteUINT32(save_p, ntemprecords[i].score[curmare]);
+			P_WriteUINT8(save_p, ntemprecords[i].grade[curmare]);
+			P_WriteUINT32(save_p, ntemprecords[i].time[curmare]);
 		}
 	}
 }
 
-static inline void P_NetUnArchiveEmblems(void)
+static inline void P_NetUnArchiveEmblems(save_t *save_p)
 {
 	gamedata_t *data = serverGamedata;
 	INT32 i, j;
@@ -4782,14 +5100,14 @@ static inline void P_NetUnArchiveEmblems(void)
 	UINT8 recmares;
 	INT32 curmare;
 
-	if (READUINT32(save_p) != ARCHIVEBLOCK_EMBLEMS)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_EMBLEMS)
 		I_Error("Bad $$$.sav at archive block Emblems");
 
-	savemoddata = (boolean)READUINT8(save_p); // this one is actually necessary because savemoddata stays false otherwise for some reason.
+	savemoddata = (boolean)P_ReadUINT8(save_p); // this one is actually necessary because savemoddata stays false otherwise for some reason.
 
-	if (numemblems != READINT32(save_p))
+	if (numemblems != P_ReadINT32(save_p))
 		I_Error("Bad $$$.sav dearchiving Emblems (numemblems mismatch)");
-	if (numextraemblems != READINT32(save_p))
+	if (numextraemblems != P_ReadINT32(save_p))
 		I_Error("Bad $$$.sav dearchiving Emblems (numextraemblems mismatch)");
 
 	// This shouldn't happen, but if something really fucked up happens and you transfer
@@ -4804,53 +5122,53 @@ static inline void P_NetUnArchiveEmblems(void)
 	// TODO: Optimize this to only read information about emblems, unlocks, etc. which actually exist
 	//       There is no need to go all the way up to MAXEMBLEMS when wads are guaranteed to be the same.
 
-	data->totalplaytime = READUINT32(save_p);
+	data->totalplaytime = P_ReadUINT32(save_p);
 
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
-		if ((data->mapvisited[i] = READUINT8(save_p)) > MV_MAX)
+		if ((data->mapvisited[i] = P_ReadUINT8(save_p)) > MV_MAX)
 			I_Error("Bad $$$.sav dearchiving Emblems (invalid visit flags)");
 
 	// To save space, use one bit per collected/achieved/unlocked flag
 	for (i = 0; i < MAXEMBLEMS;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(save_p);
 		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
 			data->collected[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 	for (i = 0; i < MAXEXTRAEMBLEMS;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(save_p);
 		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
 			data->extraCollected[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 	for (i = 0; i < MAXUNLOCKABLES;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(save_p);
 		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
 			data->unlocked[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 	for (i = 0; i < MAXCONDITIONSETS;)
 	{
-		rtemp = READUINT8(save_p);
+		rtemp = P_ReadUINT8(save_p);
 		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
 			data->achieved[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 
-	data->timesBeaten = READUINT32(save_p);
-	data->timesBeatenWithEmeralds = READUINT32(save_p);
-	data->timesBeatenUltimate = READUINT32(save_p);
+	data->timesBeaten = P_ReadUINT32(save_p);
+	data->timesBeatenWithEmeralds = P_ReadUINT32(save_p);
+	data->timesBeatenUltimate = P_ReadUINT32(save_p);
 
 	// Main records
 	for (i = 0; i < NUMMAPS; ++i)
 	{
-		recscore = READUINT32(save_p);
-		rectime  = (tic_t)READUINT32(save_p);
-		recrings = READUINT16(save_p);
+		recscore = P_ReadUINT32(save_p);
+		rectime  = (tic_t)P_ReadUINT32(save_p);
+		recrings = P_ReadUINT16(save_p);
 
 		if (recrings > 10000 || recscore > MAXSCORE)
 			I_Error("Bad $$$.sav dearchiving Emblems (invalid score)");
@@ -4867,16 +5185,16 @@ static inline void P_NetUnArchiveEmblems(void)
 	// Nights records
 	for (i = 0; i < NUMMAPS; ++i)
 	{
-		if ((recmares = READUINT8(save_p)) == 0)
+		if ((recmares = P_ReadUINT8(save_p)) == 0)
 			continue;
 
 		G_AllocNightsRecordData((INT16)i, data);
 
 		for (curmare = 0; curmare < (recmares+1); ++curmare)
 		{
-			data->nightsrecords[i]->score[curmare] = READUINT32(save_p);
-			data->nightsrecords[i]->grade[curmare] = READUINT8(save_p);
-			data->nightsrecords[i]->time[curmare] = (tic_t)READUINT32(save_p);
+			data->nightsrecords[i]->score[curmare] = P_ReadUINT32(save_p);
+			data->nightsrecords[i]->grade[curmare] = P_ReadUINT8(save_p);
+			data->nightsrecords[i]->time[curmare] = (tic_t)P_ReadUINT32(save_p);
 
 			if (data->nightsrecords[i]->grade[curmare] > GRADE_S)
 			{
@@ -4888,18 +5206,18 @@ static inline void P_NetUnArchiveEmblems(void)
 	}
 
 	// Mid-map stuff
-	unlocktriggers = READUINT32(save_p);
+	unlocktriggers = P_ReadUINT32(save_p);
 
 	for (i = 0; i < MAXPLAYERS; ++i)
 	{
-		if ((recmares = READUINT8(save_p)) == 0)
+		if ((recmares = P_ReadUINT8(save_p)) == 0)
 			continue;
 
 		for (curmare = 0; curmare < (recmares+1); ++curmare)
 		{
-			ntemprecords[i].score[curmare] = READUINT32(save_p);
-			ntemprecords[i].grade[curmare] = READUINT8(save_p);
-			ntemprecords[i].time[curmare] = (tic_t)READUINT32(save_p);
+			ntemprecords[i].score[curmare] = P_ReadUINT32(save_p);
+			ntemprecords[i].grade[curmare] = P_ReadUINT8(save_p);
+			ntemprecords[i].time[curmare] = (tic_t)P_ReadUINT32(save_p);
 
 			if (ntemprecords[i].grade[curmare] > GRADE_S)
 			{
@@ -4911,37 +5229,37 @@ static inline void P_NetUnArchiveEmblems(void)
 	}
 }
 
-static void P_NetArchiveSectorPortals(void)
+static void P_NetArchiveSectorPortals(save_t *save_p)
 {
-	WRITEUINT32(save_p, ARCHIVEBLOCK_SECPORTALS);
+	P_WriteUINT32(save_p, ARCHIVEBLOCK_SECPORTALS);
 
-	WRITEUINT32(save_p, secportalcount);
+	P_WriteUINT32(save_p, secportalcount);
 
 	for (size_t i = 0; i < secportalcount; i++)
 	{
 		UINT8 type = secportals[i].type;
 
-		WRITEUINT8(save_p, type);
-		WRITEFIXED(save_p, secportals[i].origin.x);
-		WRITEFIXED(save_p, secportals[i].origin.y);
+		P_WriteUINT8(save_p, type);
+		P_WriteUINT8(save_p, secportals[i].ceiling ? 1 : 0);
+		P_WriteUINT32(save_p, SaveSector(secportals[i].target));
 
 		switch (type)
 		{
 		case SECPORTAL_LINE:
-			WRITEUINT32(save_p, SaveLine(secportals[i].line.start));
-			WRITEUINT32(save_p, SaveLine(secportals[i].line.dest));
+			P_WriteUINT32(save_p, SaveLine(secportals[i].line.start));
+			P_WriteUINT32(save_p, SaveLine(secportals[i].line.dest));
 			break;
 		case SECPORTAL_PLANE:
 		case SECPORTAL_HORIZON:
 		case SECPORTAL_FLOOR:
 		case SECPORTAL_CEILING:
-			WRITEUINT32(save_p, SaveSector(secportals[i].sector));
+			P_WriteUINT32(save_p, SaveSector(secportals[i].sector));
 			break;
 		case SECPORTAL_OBJECT:
-			if (secportals[i].mobj && !P_MobjWasRemoved(secportals[i].mobj))
-				SaveMobjnum(secportals[i].mobj);
+			if (!P_MobjWasRemoved(secportals[i].mobj))
+				P_WriteUINT32(save_p, SaveMobjnum(secportals[i].mobj));
 			else
-				WRITEUINT32(save_p, 0);
+				P_WriteUINT32(save_p, 0);
 			break;
 		default:
 			break;
@@ -4949,15 +5267,15 @@ static void P_NetArchiveSectorPortals(void)
 	}
 }
 
-static void P_NetUnArchiveSectorPortals(void)
+static void P_NetUnArchiveSectorPortals(save_t *save_p)
 {
-	if (READUINT32(save_p) != ARCHIVEBLOCK_SECPORTALS)
+	if (P_ReadUINT32(save_p) != ARCHIVEBLOCK_SECPORTALS)
 		I_Error("Bad $$$.sav at archive block Secportals");
 
 	Z_Free(secportals);
 	P_InitSectorPortals();
 
-	UINT32 count = READUINT32(save_p);
+	UINT32 count = P_ReadUINT32(save_p);
 
 	for (UINT32 i = 0; i < count; i++)
 	{
@@ -4965,24 +5283,24 @@ static void P_NetUnArchiveSectorPortals(void)
 
 		sectorportal_t *secportal = &secportals[id];
 
-		secportal->type = READUINT8(save_p);
-		secportal->origin.x = READFIXED(save_p);
-		secportal->origin.y = READFIXED(save_p);
+		secportal->type = P_ReadUINT8(save_p);
+		secportal->ceiling = (P_ReadUINT8(save_p) != 0) ? true : false;
+		secportal->target = LoadSector(P_ReadUINT32(save_p));
 
 		switch (secportal->type)
 		{
 		case SECPORTAL_LINE:
-			secportal->line.start = LoadLine(READUINT32(save_p));
-			secportal->line.dest = LoadLine(READUINT32(save_p));
+			secportal->line.start = LoadLine(P_ReadUINT32(save_p));
+			secportal->line.dest = LoadLine(P_ReadUINT32(save_p));
 			break;
 		case SECPORTAL_PLANE:
 		case SECPORTAL_HORIZON:
 		case SECPORTAL_FLOOR:
 		case SECPORTAL_CEILING:
-			secportal->sector = LoadSector(READUINT32(save_p));
+			secportal->sector = LoadSector(P_ReadUINT32(save_p));
 			break;
 		case SECPORTAL_OBJECT:
-			id = READUINT32(save_p);
+			id = P_ReadUINT32(save_p);
 			secportal->mobj = (id == 0) ? NULL : P_FindNewPosition(id);
 			break;
 		default:
@@ -4991,7 +5309,7 @@ static void P_NetUnArchiveSectorPortals(void)
 	}
 }
 
-static inline void P_ArchiveLuabanksAndConsistency(void)
+static inline void P_ArchiveLuabanksAndConsistency(save_t *save_p)
 {
 	UINT8 i, banksinuse = NUM_LUABANKS;
 
@@ -5000,30 +5318,30 @@ static inline void P_ArchiveLuabanksAndConsistency(void)
 
 	if (banksinuse)
 	{
-		WRITEUINT8(save_p, 0xb7); // luabanks marker
-		WRITEUINT8(save_p, banksinuse);
+		P_WriteUINT8(save_p, 0xb7); // luabanks marker
+		P_WriteUINT8(save_p, banksinuse);
 		for (i = 0; i < banksinuse; i++)
-			WRITEINT32(save_p, luabanks[i]);
+			P_WriteINT32(save_p, luabanks[i]);
 	}
 
-	WRITEUINT8(save_p, 0x1d); // consistency marker
+	P_WriteUINT8(save_p, 0x1d); // consistency marker
 }
 
-static inline boolean P_UnArchiveLuabanksAndConsistency(void)
+static inline boolean P_UnArchiveLuabanksAndConsistency(save_t *save_p)
 {
-	switch (READUINT8(save_p))
+	switch (P_ReadUINT8(save_p))
 	{
 		case 0xb7: // luabanks marker
 			{
-				UINT8 i, banksinuse = READUINT8(save_p);
+				UINT8 i, banksinuse = P_ReadUINT8(save_p);
 				if (banksinuse > NUM_LUABANKS)
 				{
 					CONS_Alert(CONS_ERROR, M_GetText("Corrupt Luabanks! (Too many banks in use)\n"));
 					return false;
 				}
 				for (i = 0; i < banksinuse; i++)
-					luabanks[i] = READINT32(save_p);
-				if (READUINT8(save_p) != 0x1d) // consistency marker
+					luabanks[i] = P_ReadINT32(save_p);
+				if (P_ReadUINT8(save_p) != 0x1d) // consistency marker
 				{
 					CONS_Alert(CONS_ERROR, M_GetText("Corrupt Luabanks! (Failed consistency check)\n"));
 					return false;
@@ -5039,27 +5357,27 @@ static inline boolean P_UnArchiveLuabanksAndConsistency(void)
 	return true;
 }
 
-void P_SaveGame(INT16 mapnum)
+void P_SaveGame(save_t *save_p, INT16 mapnum)
 {
-	P_ArchiveMisc(mapnum);
-	P_ArchivePlayer();
-	P_ArchiveLuabanksAndConsistency();
+	P_ArchiveMisc(save_p, mapnum);
+	P_ArchivePlayer(save_p);
+	P_ArchiveLuabanksAndConsistency(save_p);
 }
 
-void P_SaveNetGame(boolean resending)
+void P_SaveNetGame(save_t *save_p, boolean resending)
 {
 	thinker_t *th;
 	mobj_t *mobj;
 	INT32 i = 1; // don't start from 0, it'd be confused with a blank pointer otherwise
 
-	CV_SaveNetVars(&save_p);
-	P_NetArchiveMisc(resending);
-	P_NetArchiveEmblems();
+	CV_SaveNetVars(save_p);
+	P_NetArchiveMisc(save_p, resending);
+	P_NetArchiveEmblems(save_p);
 
 	// Assign the mobjnumber for pointer tracking
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mobj = (mobj_t *)th;
@@ -5068,32 +5386,32 @@ void P_SaveNetGame(boolean resending)
 		mobj->mobjnum = i++;
 	}
 
-	P_NetArchivePlayers();
+	P_NetArchivePlayers(save_p);
 	if (gamestate == GS_LEVEL)
 	{
-		P_NetArchiveWorld();
-		P_ArchivePolyObjects();
-		P_NetArchiveThinkers();
-		P_NetArchiveSpecials();
-		P_NetArchiveColormaps();
-		P_NetArchiveWaypoints();
-		P_NetArchiveSectorPortals();
+		P_NetArchiveWorld(save_p);
+		P_ArchivePolyObjects(save_p);
+		P_NetArchiveThinkers(save_p);
+		P_NetArchiveSpecials(save_p);
+		P_NetArchiveColormaps(save_p);
+		P_NetArchiveWaypoints(save_p);
+		P_NetArchiveSectorPortals(save_p);
 	}
-	LUA_Archive();
+	LUA_Archive(save_p);
 
-	P_ArchiveLuabanksAndConsistency();
+	P_ArchiveLuabanksAndConsistency(save_p);
 }
 
-boolean P_LoadGame(INT16 mapoverride)
+boolean P_LoadGame(save_t *save_p, INT16 mapoverride)
 {
 	if (gamestate == GS_INTERMISSION)
 		Y_EndIntermission();
 	G_SetGamestate(GS_NULL); // should be changed in P_UnArchiveMisc
 
-	P_UnArchiveSPGame(mapoverride);
-	P_UnArchivePlayer();
+	P_UnArchiveSPGame(save_p, mapoverride);
+	P_UnArchivePlayer(save_p);
 
-	if (!P_UnArchiveLuabanksAndConsistency())
+	if (!P_UnArchiveLuabanksAndConsistency(save_p))
 		return false;
 
 	// Only do this after confirming savegame is ok
@@ -5103,26 +5421,26 @@ boolean P_LoadGame(INT16 mapoverride)
 	return true;
 }
 
-boolean P_LoadNetGame(boolean reloading)
+boolean P_LoadNetGame(save_t *save_p, boolean reloading)
 {
-	CV_LoadNetVars(&save_p);
-	if (!P_NetUnArchiveMisc(reloading))
+	CV_LoadNetVars(save_p);
+	if (!P_NetUnArchiveMisc(save_p, reloading))
 		return false;
-	P_NetUnArchiveEmblems();
-	P_NetUnArchivePlayers();
+	P_NetUnArchiveEmblems(save_p);
+	P_NetUnArchivePlayers(save_p);
 	if (gamestate == GS_LEVEL)
 	{
-		P_NetUnArchiveWorld();
-		P_UnArchivePolyObjects();
-		P_NetUnArchiveThinkers();
-		P_NetUnArchiveSpecials();
-		P_NetUnArchiveColormaps();
-		P_NetUnArchiveWaypoints();
-		P_NetUnArchiveSectorPortals();
+		P_NetUnArchiveWorld(save_p);
+		P_UnArchivePolyObjects(save_p);
+		P_NetUnArchiveThinkers(save_p);
+		P_NetUnArchiveSpecials(save_p);
+		P_NetUnArchiveColormaps(save_p);
+		P_NetUnArchiveWaypoints(save_p);
+		P_NetUnArchiveSectorPortals(save_p);
 		P_RelinkPointers();
 		P_FinishMobjs();
 	}
-	LUA_UnArchive();
+	LUA_UnArchive(save_p);
 
 	// This is stupid and hacky, but maybe it'll work!
 	P_SetRandSeed(P_GetInitSeed());
@@ -5133,5 +5451,5 @@ boolean P_LoadNetGame(boolean reloading)
 	// precipitation when loading a netgame save. Instead, precip has to be spawned here.
 	// This is done in P_NetUnArchiveSpecials now.
 
-	return P_UnArchiveLuabanksAndConsistency();
+	return P_UnArchiveLuabanksAndConsistency(save_p);
 }
diff --git a/src/p_saveg.h b/src/p_saveg.h
index 545008e7efc6af656fe94864a4ebcf8ea28e5f27..05e82190aeb27bb8aa62f5bf4a2b2c8b889759fb 100644
--- a/src/p_saveg.h
+++ b/src/p_saveg.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,17 +18,24 @@
 #pragma interface
 #endif
 
+#include "tables.h"
+
 #define NEWSKINSAVES (INT16_MAX) // TODO: 2.3: Delete (Purely for backwards compatibility)
 
 // Persistent storage/archiving.
 // These are the load / save game routines.
 
-void P_SaveGame(INT16 mapnum);
-void P_SaveNetGame(boolean resending);
-boolean P_LoadGame(INT16 mapoverride);
-boolean P_LoadNetGame(boolean reloading);
+typedef struct
+{
+	unsigned char *buf;
+	size_t size;
+	size_t pos;
+} save_t;
 
-mobj_t *P_FindNewPosition(UINT32 oldposition);
+void P_SaveGame(save_t *save_p, INT16 mapnum);
+void P_SaveNetGame(save_t *save_p, boolean resending);
+boolean P_LoadGame(save_t *save_p, INT16 mapoverride);
+boolean P_LoadNetGame(save_t *save_p, boolean reloading);
 
 typedef struct
 {
@@ -42,6 +49,37 @@ typedef struct
 } savedata_t;
 
 extern savedata_t savedata;
-extern UINT8 *save_p;
+
+void P_WriteUINT8(save_t *p, UINT8 v);
+void P_WriteSINT8(save_t *p, SINT8 v);
+void P_WriteUINT16(save_t *p, UINT16 v);
+void P_WriteINT16(save_t *p, INT16 v);
+void P_WriteUINT32(save_t *p, UINT32 v);
+void P_WriteINT32(save_t *p, INT32 v);
+void P_WriteChar(save_t *p, char v);
+void P_WriteFixed(save_t *p, fixed_t v);
+void P_WriteAngle(save_t *p, angle_t v);
+void P_WriteStringN(save_t *p, char const *s, size_t n);
+void P_WriteStringL(save_t *p, char const *s, size_t n);
+void P_WriteString(save_t *p, char const *s);
+void P_WriteMem(save_t *p, void const *s, size_t n);
+
+void P_SkipStringN(save_t *p, size_t n);
+void P_SkipStringL(save_t *p, size_t n);
+void P_SkipString(save_t *p);
+
+UINT8 P_ReadUINT8(save_t *p);
+SINT8 P_ReadSINT8(save_t *p);
+UINT16 P_ReadUINT16(save_t *p);
+INT16 P_ReadINT16(save_t *p);
+UINT32 P_ReadUINT32(save_t *p);
+INT32 P_ReadINT32(save_t *p);
+char P_ReadChar(save_t *p);
+fixed_t P_ReadFixed(save_t *p);
+angle_t P_ReadAngle(save_t *p);
+void P_ReadStringN(save_t *p, char *s, size_t n);
+void P_ReadStringL(save_t *p, char *s, size_t n);
+void P_ReadString(save_t *p, char *s);
+void P_ReadMem(save_t *p, void *s, size_t n);
 
 #endif
diff --git a/src/p_setup.c b/src/p_setup.c
index 41487d702f265c7e5164c61be8d0b8f8c059aa38..247587e11e63d813bf57410e29d276ac5b4125e3 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -530,7 +530,7 @@ UINT32 P_GetScoreForGradeOverall(INT16 map, UINT8 grade)
 void P_AddNiGHTSTimes(INT16 i, char *gtext)
 {
 	char *spos = gtext;
-	
+
 	for (UINT8 n = 0; n < 8; n++)
 	{
 		if (spos != NULL)
@@ -609,15 +609,15 @@ Ploadflat (levelflat_t *levelflat, const char *flatname, boolean resize)
 	levelflat->type = LEVELFLAT_TEXTURE;
 
 	// Look for a flat
-	int texturenum = R_CheckFlatNumForName(levelflat->name);
+	int texturenum = R_CheckTextureNumForName(levelflat->name, TEXTURETYPE_FLAT);
 	if (texturenum < 0)
 	{
 		// If we can't find a flat, try looking for a texture!
-		texturenum = R_CheckTextureNumForName(levelflat->name);
+		texturenum = R_CheckTextureNumForName(levelflat->name, TEXTURETYPE_TEXTURE);
 		if (texturenum < 0)
 		{
 			// Use "not found" texture
-			texturenum = R_CheckTextureNumForName("REDWALL");
+			texturenum = R_CheckTextureNumForName("REDWALL", TEXTURETYPE_TEXTURE);
 
 			// Give up?
 			if (texturenum < 0)
@@ -692,7 +692,7 @@ void P_ReloadRings(void)
 	// scan the thinkers to find rings/spheres/hoops to unset
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo = (mobj_t *)th;
@@ -750,7 +750,7 @@ void P_SwitchSpheresBonusMode(boolean bonustime)
 	// scan the thinkers to find spheres to switch
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo = (mobj_t *)th;
@@ -831,13 +831,15 @@ void P_ScanThings(INT16 mapnum, INT16 wadnum, INT16 lumpnum)
 
 static void P_SpawnEmeraldHunt(void)
 {
-	INT32 emer[3], num[MAXHUNTEMERALDS], i, randomkey;
+	INT32 emer[3], num[MAXHUNTEMERALDS], i, amount, randomkey;
 	fixed_t x, y, z;
 
 	for (i = 0; i < numhuntemeralds; i++)
 		num[i] = i;
 
-	for (i = 0; i < 3; i++)
+	amount = min(numhuntemeralds, 3);
+
+	for (i = 0; i < amount; i++)
 	{
 		// generate random index, shuffle afterwards
 		randomkey = P_RandomKey(numhuntemeralds--);
@@ -1113,6 +1115,8 @@ static void P_InitializeLinedef(line_t *ld)
 	ld->callcount = 0;
 	ld->secportal = UINT32_MAX;
 
+	ld->midtexslope = NULL;
+
 	// cph 2006/09/30 - fix sidedef errors right away.
 	// cph 2002/07/20 - these errors are fatal if not fixed, so apply them
 	for (j = 0; j < 2; j++)
@@ -1363,6 +1367,9 @@ static void P_LoadSidedefs(UINT8 *data)
 		sd->scalex_top = sd->scalex_mid = sd->scalex_bottom = FRACUNIT;
 		sd->scaley_top = sd->scaley_mid = sd->scaley_bottom = FRACUNIT;
 
+		sd->light = sd->light_top = sd->light_mid = sd->light_bottom = 0;
+		sd->lightabsolute = sd->lightabsolute_top = sd->lightabsolute_mid = sd->lightabsolute_bottom = false;
+
 		P_SetSidedefSector(i, (UINT16)SHORT(msd->sector));
 
 		// Special info stored in texture fields!
@@ -1550,11 +1557,41 @@ static void P_LoadThings(UINT8 *data)
 }
 
 // Stores positions for relevant map data spread through a TEXTMAP.
-UINT32 mapthingsPos[UINT16_MAX];
-UINT32 linesPos[UINT16_MAX];
-UINT32 sidesPos[UINT16_MAX];
-UINT32 vertexesPos[UINT16_MAX];
-UINT32 sectorsPos[UINT16_MAX];
+typedef struct textmap_block_s
+{
+	UINT32 *pos;
+	size_t capacity;
+} textmap_block_t;
+
+static textmap_block_t mapthingBlocks;
+static textmap_block_t linedefBlocks;
+static textmap_block_t sidedefBlocks;
+static textmap_block_t vertexBlocks;
+static textmap_block_t sectorBlocks;
+
+static void TextmapStorePos(textmap_block_t *blocks, size_t *count)
+{
+	size_t locCount = (*count) + 1;
+
+	if (blocks->pos == NULL)
+	{
+		// Initial capacity (half of the former one.)
+		blocks->capacity = UINT16_MAX / 2;
+
+		Z_Calloc(sizeof(blocks->pos) * blocks->capacity, PU_LEVEL, &blocks->pos);
+	}
+	else if (locCount >= blocks->capacity)
+	{
+		// If we hit the list's capacity, make space for 1024 more blocks
+		blocks->capacity += 1024;
+
+		Z_Realloc(blocks->pos, sizeof(blocks->pos) * blocks->capacity, PU_LEVEL, &blocks->pos);
+	}
+
+	blocks->pos[locCount - 1] = M_TokenizerGetEndPos();
+
+	(*count) = locCount;
+}
 
 // Determine total amount of map data in TEXTMAP.
 static boolean TextmapCount(size_t size)
@@ -1598,15 +1635,15 @@ static boolean TextmapCount(size_t size)
 			brackets++;
 		// Check for valid fields.
 		else if (fastcmp(tkn, "thing"))
-			mapthingsPos[nummapthings++] = M_TokenizerGetEndPos();
+			TextmapStorePos(&mapthingBlocks, &nummapthings);
 		else if (fastcmp(tkn, "linedef"))
-			linesPos[numlines++] = M_TokenizerGetEndPos();
+			TextmapStorePos(&linedefBlocks, &numlines);
 		else if (fastcmp(tkn, "sidedef"))
-			sidesPos[numsides++] = M_TokenizerGetEndPos();
+			TextmapStorePos(&sidedefBlocks, &numsides);
 		else if (fastcmp(tkn, "vertex"))
-			vertexesPos[numvertexes++] = M_TokenizerGetEndPos();
+			TextmapStorePos(&vertexBlocks, &numvertexes);
 		else if (fastcmp(tkn, "sector"))
-			sectorsPos[numsectors++] = M_TokenizerGetEndPos();
+			TextmapStorePos(&sectorBlocks, &numsectors);
 		else
 			CONS_Alert(CONS_NOTICE, "Unknown field '%s'.\n", tkn);
 	}
@@ -1943,6 +1980,22 @@ static void ParseTextmapSidedefParameter(UINT32 i, const char *param, const char
 		P_SetSidedefSector(i, atol(val));
 	else if (fastcmp(param, "repeatcnt"))
 		sides[i].repeatcnt = atol(val);
+	else if (fastcmp(param, "light"))
+		sides[i].light = atol(val);
+	else if (fastcmp(param, "light_top"))
+		sides[i].light_top = atol(val);
+	else if (fastcmp(param, "light_mid"))
+		sides[i].light_mid = atol(val);
+	else if (fastcmp(param, "light_bottom"))
+		sides[i].light_bottom = atol(val);
+	else if (fastcmp(param, "lightabsolute") && fastcmp("true", val))
+		sides[i].lightabsolute = true;
+	else if (fastcmp(param, "lightabsolute_top") && fastcmp("true", val))
+		sides[i].lightabsolute_top = true;
+	else if (fastcmp(param, "lightabsolute_mid") && fastcmp("true", val))
+		sides[i].lightabsolute_mid = true;
+	else if (fastcmp(param, "lightabsolute_bottom") && fastcmp("true", val))
+		sides[i].lightabsolute_bottom = true;
 }
 
 static void ParseTextmapLinedefParameter(UINT32 i, const char *param, const char *val)
@@ -2670,6 +2723,22 @@ static void P_WriteTextmap(void)
 			fprintf(f, "texturemiddle = \"%.*s\";\n", 8, textures[wsides[i].midtexture]->name);
 		if (wsides[i].repeatcnt != 0)
 			fprintf(f, "repeatcnt = %d;\n", wsides[i].repeatcnt);
+		if (wsides[i].light != 0)
+			fprintf(f, "light = %d;\n", wsides[i].light);
+		if (wsides[i].light_top != 0)
+			fprintf(f, "light_top = %d;\n", wsides[i].light_top);
+		if (wsides[i].light_mid != 0)
+			fprintf(f, "light_mid = %d;\n", wsides[i].light_mid);
+		if (wsides[i].light_bottom != 0)
+			fprintf(f, "light_bottom = %d;\n", wsides[i].light_bottom);
+		if (wsides[i].lightabsolute)
+			fprintf(f, "lightabsolute = true;\n");
+		if (wsides[i].lightabsolute_top)
+			fprintf(f, "lightabsolute_top = true;\n");
+		if (wsides[i].lightabsolute_mid)
+			fprintf(f, "lightabsolute_mid = true;\n");
+		if (wsides[i].lightabsolute_bottom)
+			fprintf(f, "lightabsolute_bottom = true;\n");
 		fprintf(f, "}\n");
 		fprintf(f, "\n");
 	}
@@ -2926,7 +2995,7 @@ static void P_LoadTextmap(void)
 	side_t     *sd;
 	mapthing_t *mt;
 
-	CONS_Alert(CONS_NOTICE, "UDMF support is still a work-in-progress; its specs and features are prone to change until it is fully implemented.\n");
+	//CONS_Alert(CONS_NOTICE, "UDMF support is still a work-in-progress; its specs and features are prone to change until it is fully implemented.\n");
 
 	/// Given the UDMF specs, some fields are given a default value.
 	/// If an element's field has a default value set, it is omitted
@@ -2940,7 +3009,7 @@ static void P_LoadTextmap(void)
 		vt->floorzset = vt->ceilingzset = false;
 		vt->floorz = vt->ceilingz = 0;
 
-		TextmapParse(vertexesPos[i], i, ParseTextmapVertexParameter);
+		TextmapParse(vertexBlocks.pos[i], i, ParseTextmapVertexParameter);
 
 		if (vt->x == INT32_MAX)
 			I_Error("P_LoadTextmap: vertex %s has no x value set!\n", sizeu1(i));
@@ -2997,7 +3066,7 @@ static void P_LoadTextmap(void)
 		textmap_planefloor.defined = 0;
 		textmap_planeceiling.defined = 0;
 
-		TextmapParse(sectorsPos[i], i, ParseTextmapSectorParameter);
+		TextmapParse(sectorBlocks.pos[i], i, ParseTextmapSectorParameter);
 
 		P_InitializeSector(sc);
 		if (textmap_colormap.used)
@@ -3006,7 +3075,7 @@ static void P_LoadTextmap(void)
 			// TODO: remove this limitation in a backwards-compatible way (UDMF versioning?)
 			UINT8 lightalpha = (textmap_colormap.lightalpha * 102) / 10;
 			UINT8 fadealpha = (textmap_colormap.fadealpha * 102) / 10;
-			
+
 			INT32 rgba = P_ColorToRGBA(textmap_colormap.lightcolor, lightalpha);
 			INT32 fadergba = P_ColorToRGBA(textmap_colormap.fadecolor, fadealpha);
 			sc->extra_colormap = sc->spawn_extra_colormap = R_CreateColormap(rgba, fadergba, textmap_colormap.fadestart, textmap_colormap.fadeend, textmap_colormap.flags);
@@ -3046,7 +3115,7 @@ static void P_LoadTextmap(void)
 		ld->sidenum[0] = NO_SIDEDEF;
 		ld->sidenum[1] = NO_SIDEDEF;
 
-		TextmapParse(linesPos[i], i, ParseTextmapLinedefParameter);
+		TextmapParse(linedefBlocks.pos[i], i, ParseTextmapLinedefParameter);
 
 		if (!ld->v1)
 			I_Error("P_LoadTextmap: linedef %s has no v1 value set!\n", sizeu1(i));
@@ -3072,8 +3141,10 @@ static void P_LoadTextmap(void)
 		sd->bottomtexture = R_TextureNumForName("-");
 		sd->sector = NULL;
 		sd->repeatcnt = 0;
+		sd->light = sd->light_top = sd->light_mid = sd->light_bottom = 0;
+		sd->lightabsolute = sd->lightabsolute_top = sd->lightabsolute_mid = sd->lightabsolute_bottom = false;
 
-		TextmapParse(sidesPos[i], i, ParseTextmapSidedefParameter);
+		TextmapParse(sidedefBlocks.pos[i], i, ParseTextmapSidedefParameter);
 
 		if (!sd->sector)
 			I_Error("P_LoadTextmap: sidedef %s has no sector value set!\n", sizeu1(i));
@@ -3097,7 +3168,7 @@ static void P_LoadTextmap(void)
 		memset(mt->stringargs, 0x00, NUMMAPTHINGSTRINGARGS*sizeof(*mt->stringargs));
 		mt->mobj = NULL;
 
-		TextmapParse(mapthingsPos[i], i, ParseTextmapThingParameter);
+		TextmapParse(mapthingBlocks.pos[i], i, ParseTextmapThingParameter);
 	}
 }
 
@@ -3415,13 +3486,13 @@ typedef enum {
 } nodetype_t;
 
 // Find out the BSP format.
-static nodetype_t P_GetNodetype(const virtres_t *virt, UINT8 **nodedata)
+static nodetype_t P_GetNodetype(const virtres_t *virt, UINT8 **nodedata, char signature[4 + 1])
 {
 	boolean supported[NUMNODETYPES] = {0};
 	nodetype_t nodetype = NT_UNSUPPORTED;
-	char signature[4 + 1];
 
 	*nodedata = NULL;
+	signature[0] = signature[4] = '\0';
 
 	if (udmf)
 	{
@@ -3430,7 +3501,7 @@ static nodetype_t P_GetNodetype(const virtres_t *virt, UINT8 **nodedata)
 		if (virtznodes && virtznodes->size)
 		{
 			*nodedata = virtznodes->data;
-			supported[NT_XGLN] = supported[NT_XGL3] = true;
+			supported[NT_XGLN] = supported[NT_XGL2] = supported[NT_XGL3] = true;
 		}
 	}
 	else
@@ -3452,9 +3523,9 @@ static nodetype_t P_GetNodetype(const virtres_t *virt, UINT8 **nodedata)
 			virtssectors = vres_Find(virt, "SSECTORS");
 
 			if (virtssectors && virtssectors->size)
-			{ // Possibly GL nodes: NODES ignored, SSECTORS takes precedence as nodes lump, (It is confusing yeah) and has a signature.
+			{ // Possibly GL nodes: NODES ignored, SSECTORS takes precedence as nodes lump (it is confusing, yeah), and has a signature.
 				*nodedata = virtssectors->data;
-				supported[NT_XGLN] = supported[NT_ZGLN] = supported[NT_XGL3] = true;
+				supported[NT_XGLN] = supported[NT_ZGLN] = supported[NT_XGL2] = supported[NT_XGL3] = true;
 			}
 			else
 			{ // Possibly ZDoom extended nodes: SSECTORS is empty, NODES has a signature.
@@ -3474,19 +3545,42 @@ static nodetype_t P_GetNodetype(const virtres_t *virt, UINT8 **nodedata)
 	}
 
 	M_Memcpy(signature, *nodedata, 4);
-	signature[4] = '\0';
 	(*nodedata) += 4;
 
-	if (!strcmp(signature, "XNOD"))
-		nodetype = NT_XNOD;
-	else if (!strcmp(signature, "ZNOD"))
-		nodetype = NT_ZNOD;
-	else if (!strcmp(signature, "XGLN"))
-		nodetype = NT_XGLN;
-	else if (!strcmp(signature, "ZGLN"))
-		nodetype = NT_ZGLN;
-	else if (!strcmp(signature, "XGL3"))
-		nodetype = NT_XGL3;
+	// Identify node format from its starting signature.
+	if (memcmp(&signature[1], "NOD", 3) == 0) // ZDoom extended nodes
+	{
+		if (signature[0] == 'X')
+		{
+			nodetype = NT_XNOD; // Uncompressed
+		}
+		else if (signature[0] == 'Z')
+		{
+			nodetype = NT_ZNOD; // Compressed
+		}
+	}
+	else if (memcmp(&signature[1], "GL", 2) == 0) // GL nodes
+	{
+		switch (signature[0])
+		{
+		case 'X': // Uncompressed
+			switch (signature[3])
+			{
+			case 'N': nodetype = NT_XGLN; break; // GL nodes
+			case '2': nodetype = NT_XGL2; break; // Version 2 GL nodes
+			case '3': nodetype = NT_XGL3; break; // Version 3 GL nodes
+			}
+			break;
+		case 'Z': // Compressed
+			switch (signature[3])
+			{
+			case 'N': nodetype = NT_ZGLN; break; // GL nodes (compressed)
+			case '2': nodetype = NT_ZGL2; break; // Version 2 GL nodes (compressed)
+			case '3': nodetype = NT_ZGL3; break; // Version 3 GL nodes (compressed)
+			}
+			break;
+		}
+	}
 
 	return supported[nodetype] ? nodetype : NT_UNSUPPORTED;
 }
@@ -3498,7 +3592,6 @@ static boolean P_LoadExtraVertices(UINT8 **data)
 	UINT32 xtrvrtx = READUINT32((*data));
 	line_t* ld = lines;
 	vertex_t *oldpos = vertexes;
-	ssize_t offset;
 	size_t i;
 
 	if (numvertexes != origvrtx) // If native vertex count doesn't match node original vertex count, bail out (broken data?).
@@ -3513,12 +3606,11 @@ static boolean P_LoadExtraVertices(UINT8 **data)
 	// If extra vertexes were generated, reallocate the vertex array and fix the pointers.
 	numvertexes += xtrvrtx;
 	vertexes = Z_Realloc(vertexes, numvertexes*sizeof(*vertexes), PU_LEVEL, NULL);
-	offset = (size_t)(vertexes - oldpos);
 
 	for (i = 0, ld = lines; i < numlines; i++, ld++)
 	{
-		ld->v1 += offset;
-		ld->v2 += offset;
+		ld->v1 = &vertexes[ld->v1 - oldpos];
+		ld->v2 = &vertexes[ld->v2 - oldpos];
 	}
 
 	// Read extra vertex data.
@@ -3556,6 +3648,7 @@ static boolean P_LoadExtendedSubsectorsAndSegs(UINT8 **data, nodetype_t nodetype
 		switch (nodetype)
 		{
 		case NT_XGLN:
+		case NT_XGL2:
 		case NT_XGL3:
 			for (m = 0; m < (size_t)subsectors[i].numlines; m++, k++)
 			{
@@ -3567,7 +3660,7 @@ static boolean P_LoadExtendedSubsectorsAndSegs(UINT8 **data, nodetype_t nodetype
 
 				READUINT32((*data)); // partner, can be ignored by software renderer
 
-				if (nodetype == NT_XGL3)
+				if (nodetype != NT_XGLN)
 				{
 					UINT32 linenum = READUINT32((*data));
 					if (linenum != 0xFFFFFFFF && linenum >= numlines)
@@ -3678,8 +3771,9 @@ static void P_LoadExtendedNodes(UINT8 **data, nodetype_t nodetype)
 
 static void P_LoadMapBSP(const virtres_t *virt)
 {
+	char signature[4 + 1];
 	UINT8 *nodedata = NULL;
-	nodetype_t nodetype = P_GetNodetype(virt, &nodedata);
+	nodetype_t nodetype = P_GetNodetype(virt, &nodedata, signature);
 
 	switch (nodetype)
 	{
@@ -3711,6 +3805,7 @@ static void P_LoadMapBSP(const virtres_t *virt)
 	}
 	case NT_XNOD:
 	case NT_XGLN:
+	case NT_XGL2:
 	case NT_XGL3:
 		if (!P_LoadExtraVertices(&nodedata))
 			return;
@@ -3719,10 +3814,13 @@ static void P_LoadMapBSP(const virtres_t *virt)
 		P_LoadExtendedNodes(&nodedata, nodetype);
 		break;
 	default:
-		CONS_Alert(CONS_WARNING, "Unsupported BSP format detected.\n");
-		return;
+		if (isprint(signature[0]) && isprint(signature[1]) && isprint(signature[2]) && isprint(signature[3]))
+		{
+			I_Error("Unsupported BSP format '%s' detected!\n", signature);
+			return;
+		}
+		I_Error("Unknown BSP format detected!\n");
 	}
-	return;
 }
 
 // Split from P_LoadBlockMap for convenience
@@ -7235,7 +7333,7 @@ void P_RespawnThings(void)
 
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 		P_RemoveMobj((mobj_t *)think);
 	}
@@ -7271,11 +7369,11 @@ static void P_RunLevelScript(const char *scriptname)
 			return;
 		}
 
-		COM_BufInsertText(W_CacheLumpNum(lumpnum, PU_CACHE));
+		COM_BufInsertTextEx(W_CacheLumpNum(lumpnum, PU_CACHE), COM_LUA);
 	}
 	else
 	{
-		COM_BufAddText(va("exec %s\n", scriptname));
+		COM_ExecFile(scriptname, COM_LUA, false);
 	}
 	COM_BufExecute(); // Run it!
 }
@@ -7547,20 +7645,20 @@ static void P_InitCamera(void)
 			CV_SetValue(&cv_analog[1], 0);
 
 		displayplayer = consoleplayer; // Start with your OWN view, please!
-	}
 
-	if (twodlevel)
-	{
-		CV_SetValue(&cv_analog[0], false);
-		CV_SetValue(&cv_analog[1], false);
-	}
-	else
-	{
-		if (cv_useranalog[0].value)
-			CV_SetValue(&cv_analog[0], true);
+		if (twodlevel)
+		{
+			CV_SetValue(&cv_analog[0], false);
+			CV_SetValue(&cv_analog[1], false);
+		}
+		else
+		{
+			if (cv_useranalog[0].value)
+				CV_SetValue(&cv_analog[0], true);
 
-		if ((splitscreen && cv_useranalog[1].value) || botingame)
-			CV_SetValue(&cv_analog[1], true);
+			if ((splitscreen && cv_useranalog[1].value) || botingame)
+				CV_SetValue(&cv_analog[1], true);
+		}
 	}
 }
 
@@ -7755,6 +7853,10 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	maptol = mapheaderinfo[gamemap-1]->typeoflevel;
 	gametyperules = gametypedefaultrules[gametype];
 
+	// clear the target on map change, since the object will be invalidated
+	P_SetTarget(&ticcmd_ztargetfocus[0], NULL);
+	P_SetTarget(&ticcmd_ztargetfocus[1], NULL);
+
 	CON_Drawer(); // let the user know what we are going to do
 	I_FinishUpdate(); // page flip or blit buffer
 
@@ -7904,6 +8006,9 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	// Free GPU textures before freeing patches.
 	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
 		HWR_ClearAllTextures();
+
+	// Delete light table textures
+	HWR_ClearLightTables();
 #endif
 
 	Patch_FreeTag(PU_PATCH_LOWPRIORITY);
diff --git a/src/p_setup.h b/src/p_setup.h
index da38d4c08b152f87fab59517706c3ccbc7544339..6b0218854362085c0ae77220d984b567096e2d93 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_slopes.c b/src/p_slopes.c
index e75d36edefe8d8484cb0c9bebb4d02d5f1b1e174..12d5afd02fb96d7f19965e6f445de1e5b122a6d9 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2009      by Stephen McGranahan.
-// Copyright (C) 2015-2023 by Sonic Team Junior.
+// Copyright (C) 2015-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -28,6 +28,8 @@
 pslope_t *slopelist = NULL;
 UINT16 slopecount = 0;
 
+static void P_UpdateMidtextureSlopesForSector(sector_t *sector);
+
 // Calculate line normal
 void P_CalculateSlopeNormal(pslope_t *slope)
 {
@@ -180,7 +182,7 @@ void T_DynamicSlopeLine (dynlineplanethink_t* th)
 	pslope_t* slope = th->slope;
 	line_t* srcline = th->sourceline;
 
-	fixed_t zdelta;
+	fixed_t zdelta, oldoz = slope->o.z;
 
 	switch(th->type) {
 	case DP_FRONTFLOOR:
@@ -207,11 +209,14 @@ void T_DynamicSlopeLine (dynlineplanethink_t* th)
 		return;
 	}
 
-	if (slope->zdelta != FixedDiv(zdelta, th->extent)) {
+	if (slope->zdelta != FixedDiv(zdelta, th->extent) || oldoz != slope->o.z) {
 		slope->zdelta = FixedDiv(zdelta, th->extent);
 		slope->zangle = R_PointToAngle2(0, 0, th->extent, -zdelta);
 		slope->moved = true;
 		P_CalculateSlopeNormal(slope);
+		P_UpdateMidtextureSlopesForSector(srcline->frontsector);
+		if (srcline->backsector)
+			P_UpdateMidtextureSlopesForSector(srcline->backsector);
 	}
 }
 
@@ -232,6 +237,12 @@ void T_DynamicSlopeVert (dynvertexplanethink_t* th)
 	}
 
 	ReconfigureViaVertexes(th->slope, th->vex[0], th->vex[1], th->vex[2]);
+
+	for (i = 0; i < 3; i++)
+	{
+		if (th->secs[i])
+			P_UpdateMidtextureSlopesForSector(th->secs[i]);
+	}
 }
 
 static inline void P_AddDynLineSlopeThinker (pslope_t* slope, dynplanetype_t type, line_t* sourceline, fixed_t extent)
@@ -758,6 +769,111 @@ pslope_t *P_MakeSlopeViaEquationConstants(const double a, const double b, const
 	return ret;
 }
 
+static pslope_t *P_GetReferenceSlopeForMidtexture(line_t *line)
+{
+	if (line->flags & ML_MIDPEG)
+	{
+		// Line has ML_MIDPEG, so use the floor slope
+		fixed_t frontheight = P_GetSectorFloorZAt(line->frontsector, line->v1->x, line->v1->y);
+		fixed_t backheight = P_GetSectorFloorZAt(line->backsector, line->v1->x, line->v1->y);
+
+		if (frontheight > backheight)
+		{
+			return line->frontsector->f_slope;
+		}
+		else
+		{
+			return line->backsector->f_slope;
+		}
+	}
+	else
+	{
+		// Line does not have ML_MIDPEG, so use the ceiling slope
+		fixed_t frontheight = P_GetSectorCeilingZAt(line->frontsector, line->v1->x, line->v1->y);
+		fixed_t backheight = P_GetSectorCeilingZAt(line->backsector, line->v1->x, line->v1->y);
+
+		if (frontheight < backheight)
+		{
+			return line->frontsector->c_slope;
+		}
+		else
+		{
+			return line->backsector->c_slope;
+		}
+	}
+
+	return NULL;
+}
+
+// Updates a slope for a solid midtexture based on the slope of the sector it's in.
+static void P_UpdateSolidMidtextureSlope(line_t *line, pslope_t *ref)
+{
+	pslope_t *slope = line->midtexslope;
+
+	if (ref == NULL)
+		return;
+
+	// Set origin
+	vector3_t origin;
+	origin.x = line->v1->x;
+	origin.y = line->v1->y;
+	origin.z = P_GetSlopeZAt(ref, origin.x, origin.y);
+	FV3_Copy(&slope->o, &origin);
+
+	// Get where the line ends
+	vector3_t point;
+	point.x = line->v2->x;
+	point.y = line->v2->y;
+	point.z = P_GetSlopeZAt(ref, point.x, point.y);
+
+	// Get length of the line
+	fixed_t extent = R_PointToDist2(0, 0, line->dx, line->dy);
+
+	// Precalculate variables
+	slope->zdelta = FixedDiv(origin.z - point.z, extent);
+	slope->zangle = R_PointToAngle2(0, origin.z, extent, point.z);
+	slope->xydirection = line->angle;
+
+	// Precalculate the direction
+	vector2_t dir;
+	dir.x = FixedMul(FINECOSINE(slope->zangle >> ANGLETOFINESHIFT), FINECOSINE((slope->xydirection+ANGLE_180) >> ANGLETOFINESHIFT));
+	dir.y = FixedMul(FINECOSINE(slope->zangle >> ANGLETOFINESHIFT), FINESINE((slope->xydirection+ANGLE_180) >> ANGLETOFINESHIFT));
+	FV2_Copy(&slope->d, &dir);
+
+	P_CalculateSlopeNormal(slope);
+
+	// Calling P_CalculateSlopeVectors is not necessary.
+	slope->moved = true;
+}
+
+// Goes through every line in the sector and updates the midtexture slope if it is present
+static void P_UpdateMidtextureSlopesForSector(sector_t *sector)
+{
+	for (size_t i = 0; i < sector->linecount; i++)
+	{
+		if (sector->lines[i]->midtexslope != NULL)
+			P_UpdateSolidMidtextureSlope(sector->lines[i], P_GetReferenceSlopeForMidtexture(sector->lines[i]));
+	}
+}
+
+// Creates a solid midtexture slope for the line if possible
+static void P_CreateSolidMidtextureSlope(line_t *line)
+{
+	if (line->backsector == NULL) // Ignore single-sided lines (of course)
+		return;
+
+	if ((line->flags & ML_MIDSOLID) == 0) // Ignore if the midtexture is not solid
+		return;
+
+	pslope_t *ref = P_GetReferenceSlopeForMidtexture(line);
+	if (ref)
+	{
+		line->midtexslope = Slope_Add(ref->flags & SL_NOPHYSICS);
+
+		P_UpdateSolidMidtextureSlope(line, ref);
+	}
+}
+
 /// Initializes and reads the slopes from the map data.
 void P_SpawnSlopes(const boolean fromsave) {
 	size_t i;
@@ -785,14 +901,21 @@ void P_SpawnSlopes(const boolean fromsave) {
 
 	/// Copies slopes from tagged sectors via line specials.
 	/// \note Doesn't actually copy, but instead they share the same pointers.
+	// Also, creates midtexture slopes.
 	for (i = 0; i < numlines; i++)
-		switch (lines[i].special)
+	{
+		line_t *line = &lines[i];
+
+		switch (line->special)
 		{
 			case 720:
-				P_CopySectorSlope(&lines[i]);
+				P_CopySectorSlope(line);
 			default:
 				break;
 		}
+
+		P_CreateSolidMidtextureSlope(line);
+	}
 }
 
 /// Initializes slopes.
@@ -810,10 +933,10 @@ void P_InitSlopes(void)
 // Returns the height of the sloped plane at (x, y) as a fixed_t
 fixed_t P_GetSlopeZAt(const pslope_t *slope, fixed_t x, fixed_t y)
 {
-	fixed_t dist = FixedMul(x - slope->o.x, slope->d.x) +
-	               FixedMul(y - slope->o.y, slope->d.y);
+	fixed_t dist = FixedMul(x - slope->o.x, slope->d.x) / 2 +
+	               FixedMul(y - slope->o.y, slope->d.y) / 2;
 
-	return slope->o.z + FixedMul(dist, slope->zdelta);
+	return slope->o.z + FixedMul(dist, slope->zdelta) * 2;
 }
 
 // Like P_GetSlopeZAt but falls back to z if slope is NULL
@@ -923,15 +1046,37 @@ fixed_t P_GetWallTransferMomZ(mobj_t *mo, pslope_t *slope)
 {
 	vector3_t slopemom, axis;
 	angle_t ang;
+	angle_t advanceAng = ANG15;
+	const boolean upwards = (slope->zangle < ANGLE_180);
 
 	if (slope->flags & SL_NOPHYSICS)
 		return 0;
 
 	// If there's physics, time for launching.
 	// Doesn't kill the vertical momentum as much as P_SlopeLaunch does.
-	ang = slope->zangle + ANG15*((slope->zangle > 0) ? 1 : -1);
-	if (ang > ANGLE_90 && ang < ANGLE_180)
-		ang = ((slope->zangle > 0) ? ANGLE_90 : InvAngle(ANGLE_90)); // hard cap of directly upwards
+	ang = slope->zangle;
+
+	// for the time being, let's pretend the slope inclines upwards only
+	if (!upwards)
+	{
+		ang += ANGLE_180;
+	}
+
+	// angles past 90 degrees need to shrink to get closer to 90 degrees
+	if (ang > ANGLE_90)
+	{
+		advanceAng = InvAngle(advanceAng);
+	}
+
+	// now we set the actual final angle
+	if ((ang > ANGLE_90) != (ang + advanceAng > ANGLE_90)) // does advancing the angle push it past directly upwards?
+	{
+		ang = (upwards ? ANGLE_90 : InvAngle(ANGLE_90)); // hard cap of directly upwards
+	}
+	else
+	{
+		ang = slope->zangle + advanceAng;
+	}
 
 	slopemom.x = mo->momx;
 	slopemom.y = mo->momy;
diff --git a/src/p_slopes.h b/src/p_slopes.h
index fdc07f67e257ba05eef35fc481c3be3a0ebdfd7c..39b7ba56dbf2119e730b145e90d76bb18e9d7e70 100644
--- a/src/p_slopes.h
+++ b/src/p_slopes.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2009      by Stephen McGranahan.
-// Copyright (C) 2015-2023 by Sonic Team Junior.
+// Copyright (C) 2015-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_spec.c b/src/p_spec.c
index e9a4aa8df9fff59eb90d40022f8cfe76e161ec04..394de0167de8b517e396752ea8a374d63a6143cc 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -135,22 +135,24 @@ void P_ParseAnimationDefintion(SINT8 istexture);
 
 static boolean P_FindTextureForAnimation(anim_t *anim, animdef_t *animdef)
 {
-	if (R_CheckTextureNumForName(animdef->startname) == -1)
+	INT32 start = R_CheckTextureNumForName(animdef->startname, TEXTURETYPE_TEXTURE);
+	if (start == -1)
 		return false;
 
-	anim->picnum = R_TextureNumForName(animdef->endname);
-	anim->basepic = R_TextureNumForName(animdef->startname);
+	anim->basepic = start;
+	anim->picnum = R_CheckTextureNumForName(animdef->endname, TEXTURETYPE_TEXTURE);
 
 	return true;
 }
 
 static boolean P_FindFlatForAnimation(anim_t *anim, animdef_t *animdef)
 {
-	if (R_CheckFlatNumForName(animdef->startname) == -1)
+	INT32 start = R_CheckTextureNumForName(animdef->startname, TEXTURETYPE_FLAT);
+	if (start == -1)
 		return false;
 
-	anim->picnum = R_CheckFlatNumForName(animdef->endname);
-	anim->basepic = R_CheckFlatNumForName(animdef->startname);
+	anim->basepic = start;
+	anim->picnum = R_CheckTextureNumForName(animdef->endname, TEXTURETYPE_FLAT);
 
 	return true;
 }
@@ -2405,11 +2407,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					y = line->args[3] << FRACBITS;
 					z = line->args[4] << FRACBITS;
 
-					P_UnsetThingPosition(mo);
-					mo->x += x;
-					mo->y += y;
-					mo->z += z;
-					P_SetThingPosition(mo);
+					P_SetOrigin(mo, mo->x + x, mo->y + y, mo->z + z);
 
 					if (mo->player)
 					{
@@ -2417,6 +2415,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 							P_SetOrigin(bot, bot->x + x, bot->y + y, bot->z + z);
 						if (splitscreen && mo->player == &players[secondarydisplayplayer] && camera2.chase)
 						{
+							camera2.reset = true;
 							camera2.x += x;
 							camera2.y += y;
 							camera2.z += z;
@@ -2424,6 +2423,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						}
 						else if (camera.chase && mo->player == &players[displayplayer])
 						{
+							camera.reset = true;
 							camera.x += x;
 							camera.y += y;
 							camera.z += z;
@@ -2548,7 +2548,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					char *text = Z_Malloc(len + 1, PU_CACHE, NULL);
 					memcpy(text, lump, len);
 					text[len] = '\0';
-					COM_BufInsertText(text);
+					COM_BufInsertTextEx(text, COM_LUA);
 					Z_Free(text);
 				}
 			}
@@ -3644,7 +3644,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					if (mo2->type != MT_EGGTRAP)
 						continue;
 
-					if (mo2->thinker.function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+					if (mo2->thinker.removing)
 						continue;
 
 					P_KillMobj(mo2, NULL, mo, 0);
@@ -3684,7 +3684,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 466: // Set level failure state
 			{
-				if (line->args[1])
+				if (line->args[0])
 				{
 					stagefailed = false;
 					CONS_Debug(DBG_GAMELOGIC, "Stage can be completed successfully!\n");
@@ -3856,7 +3856,7 @@ void P_SetupSignExit(player_t *player)
 	// spin all signposts in the level then.
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 
 		thing = (mobj_t *)think;
@@ -3894,7 +3894,7 @@ boolean P_IsFlagAtBase(mobjtype_t flag)
 
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 
 		mo = (mobj_t *)think;
@@ -4397,7 +4397,7 @@ static void P_ProcessEggCapsule(player_t *player, sector_t *sector)
 	// The chimps are my friends.. heeheeheheehehee..... - LouisJM
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 		mo2 = (mobj_t *)th;
 		if (mo2->type != MT_EGGTRAP)
@@ -6238,7 +6238,7 @@ static void P_DoPortalCopyFromLine(sector_t *dest_sector, int plane_type, int ta
 	}
 }
 
-static sectorportal_t *P_SectorGetPortalOrCreate(sector_t *sector, UINT32 *num, UINT32 *result)
+static sectorportal_t *P_SectorGetPortalOrCreate(sector_t *sector, UINT32 *num, UINT32 *result, boolean ceiling)
 {
 	sectorportal_t *secportal = NULL;
 
@@ -6246,8 +6246,8 @@ static sectorportal_t *P_SectorGetPortalOrCreate(sector_t *sector, UINT32 *num,
 	{
 		*num = P_NewSectorPortal();
 		secportal = &secportals[*num];
-		secportal->origin.x = sector->soundorg.x;
-		secportal->origin.y = sector->soundorg.y;
+		secportal->target = sector;
+		secportal->ceiling = ceiling;
 		*result = *num;
 	}
 	else
@@ -6261,12 +6261,12 @@ static sectorportal_t *P_SectorGetPortalOrCreate(sector_t *sector, UINT32 *num,
 
 static sectorportal_t *P_SectorGetFloorPortalOrCreate(sector_t *sector, UINT32 *result)
 {
-	return P_SectorGetPortalOrCreate(sector, &sector->portal_floor, result);
+	return P_SectorGetPortalOrCreate(sector, &sector->portal_floor, result, false);
 }
 
 static sectorportal_t *P_SectorGetCeilingPortalOrCreate(sector_t *sector, UINT32 *result)
 {
-	return P_SectorGetPortalOrCreate(sector, &sector->portal_ceiling, result);
+	return P_SectorGetPortalOrCreate(sector, &sector->portal_ceiling, result, true);
 }
 
 static void P_CopySectorPortalToLines(UINT32 portal_num, int sector_tag)
diff --git a/src/p_spec.h b/src/p_spec.h
index b9c3a2ca3c2055481b54cd2d2fce3217df244fe7..ba08781b636cdb0042368c5ff90b5fa0db8c193c 100644
--- a/src/p_spec.h
+++ b/src/p_spec.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_tick.c b/src/p_tick.c
index 6d7d4fd969fb70964e4e0cf6663d26bd6d81e8a7..db8688484ac3dbcf56fafc293af6bc995190e0f9 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -162,7 +162,7 @@ void Command_CountMobjs_f(void)
 
 			for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 			{
-				if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				if (th->removing)
 					continue;
 
 				if (((mobj_t *)th)->type == i)
@@ -182,7 +182,7 @@ void Command_CountMobjs_f(void)
 
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			if (((mobj_t *)th)->type == i)
@@ -348,6 +348,7 @@ void P_RemoveThinkerDelayed(thinker_t *thinker)
 void P_RemoveThinker(thinker_t *thinker)
 {
 	LUA_InvalidateUserdata(thinker);
+	thinker->removing = true;
 	thinker->function.acp1 = (actionf_p1)P_RemoveThinkerDelayed;
 }
 
@@ -564,6 +565,12 @@ void P_DoTeamscrambling(void)
 		CV_SetValue(&cv_teamscramble, 0);
 }
 
+
+//
+// P_DoSpecialStageStuff()
+//
+// For old-style (non-NiGHTS) special stages
+//
 static inline void P_DoSpecialStageStuff(void)
 {
 	boolean stillalive = false;
@@ -601,7 +608,15 @@ static inline void P_DoSpecialStageStuff(void)
 				if (--players[i].nightstime > 6)
 				{
 					if (P_IsLocalPlayer(&players[i]) && oldnightstime > 10*TICRATE && players[i].nightstime <= 10*TICRATE)
-						S_ChangeMusicInternal("_drown", false);
+					{
+						if (mapheaderinfo[gamemap-1]->levelflags & LF_MIXNIGHTSCOUNTDOWN)
+						{
+							S_FadeMusic(0, 10*MUSICRATE);
+							S_StartSound(NULL, sfx_timeup); // that creepy "out of time" music from NiGHTS.
+						}
+						else
+							S_ChangeMusicInternal("_drown", false);
+					}
 					stillalive = true;
 				}
 				else if (!players[i].exiting)
diff --git a/src/p_user.c b/src/p_user.c
index 97ab599c96d16b544c7279fb01b40df04a322825..ad757d1e56edb9a2e4d1e593658c28d73c26c862 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -436,7 +436,7 @@ UINT8 P_FindLowestMare(void)
 	// to find the egg capsule with the lowest mare
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -489,7 +489,7 @@ boolean P_TransferToNextMare(player_t *player)
 	// to find the closest axis point
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -540,7 +540,7 @@ static mobj_t *P_FindAxis(INT32 mare, INT32 axisnum)
 	// to find the closest axis point
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -575,7 +575,7 @@ static mobj_t *P_FindAxisTransfer(INT32 mare, INT32 axisnum, mobjtype_t type)
 	// to find the closest axis point
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -616,7 +616,7 @@ void P_TransferToAxis(player_t *player, INT32 axisnum)
 	// to find the closest axis point
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -670,7 +670,7 @@ static void P_DeNightserizePlayer(player_t *player)
 	player->powers[pw_carry] = CR_NIGHTSFALL;
 
 	player->powers[pw_underwater] = 0;
-	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_SHIELDDOWN|PF_STARTDASH|PF_GLIDING|PF_STARTJUMP|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SPINNING|PF_DRILLING|PF_TRANSFERTOCLOSEST);
+	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_STARTDASH|PF_GLIDING|PF_STARTJUMP|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SPINNING|PF_DRILLING|PF_TRANSFERTOCLOSEST);
 	player->secondjump = 0;
 	player->homing = 0;
 	player->climbing = 0;
@@ -717,7 +717,7 @@ static void P_DeNightserizePlayer(player_t *player)
 	// Check to see if the player should be killed.
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -803,7 +803,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	if (mapheaderinfo[gamemap-1]->nightstimer[newmare] > 0)
 		nighttime = mapheaderinfo[gamemap-1]->nightstimer[newmare];
 
-	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_SHIELDDOWN|PF_STARTDASH|PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SHIELDABILITY|PF_SPINNING|PF_DRILLING);
+	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_STARTDASH|PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SHIELDABILITY|PF_SPINNING|PF_DRILLING);
 	player->homing = 0;
 	player->mo->fuse = 0;
 	player->speed = 0;
@@ -1004,6 +1004,9 @@ boolean P_IsPlayerInNightsTransformationState(player_t *player)
 //
 boolean P_PlayerInPain(player_t *player)
 {
+	if (P_MobjWasRemoved(player->mo))
+		return false;
+
 	// no silly, sliding isn't pain
 	if (!(player->pflags & PF_SLIDING) && player->mo->state == &states[player->mo->info->painstate] && player->powers[pw_flashing])
 		return true;
@@ -1117,6 +1120,13 @@ void P_ResetPlayer(player_t *player)
 		if (player->mo->tracer && !P_MobjWasRemoved(player->mo->tracer))
 		{
 			player->mo->tracer->flags |= MF_PUSHABLE;
+
+			// goose the mom a little bit to trigger gravity to process for a tic
+			if (player->mo->tracer->eflags & MFE_VERTICALFLIP)
+				player->mo->tracer->momz -= 1;
+			else
+				player->mo->tracer->momz += 1;
+
 			P_SetTarget(&player->mo->tracer->tracer, NULL);
 		}
 		P_SetTarget(&player->mo->tracer, NULL);
@@ -1380,6 +1390,8 @@ void P_DoSuperTransformation(player_t *player, boolean giverings)
 	if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOSSMUSIC) && P_IsLocalPlayer(player))
 		P_PlayJingle(player, JT_SUPER);
 
+	S_StartSound(NULL, sfx_supert); //let all players hear it -mattw_cfi
+
 	player->mo->momx = player->mo->momy = player->mo->momz = player->cmomx = player->cmomy = player->rmomx = player->rmomy = 0;
 
 	// Transformation animation
@@ -1396,11 +1408,8 @@ void P_DoSuperTransformation(player_t *player, boolean giverings)
 		player->powers[pw_sneakers] = 0;
 	}
 
-	if (G_CoopGametype())
-		S_StartSound(player->mo, sfx_supert); //only hear it near yourself in co-op
-	else
+	if (!G_CoopGametype())
 	{
-		S_StartSound(NULL, sfx_supert); //let all players hear it -mattw_cfi
 		HU_SetCEchoFlags(0);
 		HU_SetCEchoDuration(5);
 		HU_DoCEcho(va("%s\\is now super.\\\\\\\\", player_names[player-players]));
@@ -1435,7 +1444,7 @@ void P_DoSuperDetransformation(player_t *player)
 	if (!G_CoopGametype())
 		player->powers[pw_flashing] = flashingtics-1;
 
-	if (player->mo->sprite2 & SPR2F_SUPER)
+	if (player->mo->sprite2 & FF_SPR2SUPER)
 		P_SetMobjState(player->mo, player->mo->state-states);
 
 	// Inform the netgame that the champion has fallen in the heat of battle.
@@ -1917,7 +1926,7 @@ void P_SpawnShieldOrb(player_t *player)
 	// blaze through the thinkers to see if an orb already exists!
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		shieldobj = (mobj_t *)th;
@@ -2099,6 +2108,7 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 
 	ghost->renderflags = mobj->renderflags;
 	ghost->blendmode = mobj->blendmode;
+	ghost->alpha = mobj->alpha;
 
 	ghost->spritexscale = mobj->spritexscale;
 	ghost->spriteyscale = mobj->spriteyscale;
@@ -2400,7 +2410,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 		if (dorollstuff)
 		{
 			if ((player->charability2 == CA2_SPINDASH) && !((player->pflags & (PF_SPINNING|PF_THOKKED)) == PF_THOKKED) && !(player->charability == CA_THOK && player->secondjump)
-			&& (player->cmd.buttons & BT_SPIN) && (FixedHypot(player->mo->momx, player->mo->momy) > (5*player->mo->scale)))
+			&& (player->cmd.buttons & BT_SPIN) && (FixedHypot(player->mo->momx, player->mo->momy) >= (5*player->mo->scale)))
 				player->pflags = (player->pflags|PF_SPINNING) & ~PF_THOKKED;
 			else if (!(player->pflags & PF_STARTDASH))
 				player->pflags &= ~PF_SPINNING;
@@ -3328,6 +3338,7 @@ static void P_DoPlayerHeadSigns(player_t *player)
 					sign->frame = 2|FF_FULLBRIGHT;
 			}
 		}
+	}
 
 	if (!P_MobjWasRemoved(sign) && splitscreen) // Hide the sign from yourself in splitscreen - In single-screen, it wouldn't get spawned if it shouldn't be visible
 	{
@@ -3365,7 +3376,6 @@ static void P_DoPlayerHeadSigns(player_t *player)
 		}
 #endif
 	}
-	}
 }
 
 //
@@ -4143,7 +4153,7 @@ static void P_DoTeeter(player_t *player)
 			teeteryl = teeteryh = player->mo->y;
 			couldteeter = false;
 			solidteeter = teeter;
-			if (!P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_CheckSolidsTeeter))
+			if (!P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_CheckSolidsTeeter, tmthing))
 				goto teeterdone; // we've found something that stops us teetering at all
 teeterdone:
 			teeter = solidteeter;
@@ -4228,7 +4238,6 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd)
 	if (player->pflags & PF_ATTACKDOWN || player->climbing || (G_TagGametype() && !(player->pflags & PF_TAGIT)))
 		return;
 
-	// Fire a fireball if we have the Fire Flower powerup!
 	if (((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER) && !(player->weapondelay))
 	{
 		player->pflags |= PF_ATTACKDOWN;
@@ -4240,7 +4249,6 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd)
 		return;
 	}
 
-	// No ringslinging outside of ringslinger!
 	if (!G_RingSlingerGametype() || player->weapondelay)
 		return;
 
@@ -4419,7 +4427,34 @@ static void P_DoSuperStuff(player_t *player)
 		// If you're super and not Sonic, de-superize!
 		if (!(ALL7EMERALDS(emeralds) && player->charflags & SF_SUPER))
 		{
-			P_DoSuperDetransformation(player);
+			player->powers[pw_super] = 0;
+			P_SetMobjState(player->mo, S_PLAY_STND);
+			if (P_IsLocalPlayer(player))
+			{
+				music_stack_noposition = true; // HACK: Do not reposition next music
+				music_stack_fadeout = MUSICRATE/2; // HACK: Fade out current music
+			}
+			P_RestoreMusic(player);
+			P_SpawnShieldOrb(player);
+
+			// Restore color
+			if ((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER)
+			{
+				player->mo->color = SKINCOLOR_WHITE;
+				G_GhostAddColor(GHC_FIREFLOWER);
+			}
+			else
+			{
+				player->mo->color = P_GetPlayerColor(player);
+				G_GhostAddColor(GHC_NORMAL);
+			}
+
+			if (!G_CoopGametype())
+			{
+				HU_SetCEchoFlags(0);
+				HU_SetCEchoDuration(5);
+				HU_DoCEcho(va("%s\\is no longer super.\\\\\\\\", player_names[player-players]));
+			}
 			return;
 		}
 
@@ -4446,37 +4481,28 @@ static void P_DoSuperStuff(player_t *player)
 
 		// Ran out of rings while super!
 		if (player->rings <= 0 || player->exiting)
+		{
 			P_DoSuperDetransformation(player);
+		}
 	}
 }
 
 //
 // P_SuperReady
 //
-// Returns true if player is ready to transform or detransform
+// Returns true if player is ready to turn super, duh
 //
-boolean P_SuperReady(player_t *player, boolean transform)
+boolean P_SuperReady(player_t *player)
 {
-	if (!transform &&
-	(player->powers[pw_super] < TICRATE*3/2
-	|| !G_CoopGametype())) // No turning back in competitive!
-		return false;
-	else if (transform
-	&& (player->powers[pw_super]
-	|| !ALL7EMERALDS(emeralds)
-	|| !(player->rings >= 50)))
-		return false;
-
-	if (player->mo
+	if (!player->powers[pw_super]
+	&& !player->powers[pw_invulnerability]
 	&& !player->powers[pw_tailsfly]
-	&& !player->powers[pw_carry]
 	&& (player->charflags & SF_SUPER)
-	&& !P_PlayerInPain(player)
-	&& !player->climbing
-	&& !(player->pflags & (PF_JUMPSTASIS|PF_THOKKED|PF_STARTDASH|PF_GLIDING|PF_SLIDING|PF_SHIELDABILITY))
-	&& ((player->pflags & PF_JUMPED) || (P_IsObjectOnGround(player->mo) && (player->panim == PA_IDLE || player->panim == PA_EDGE
-	|| player->panim == PA_WALK || player->panim == PA_RUN || (player->charflags & SF_DASHMODE && player->panim == PA_DASH))))
-	&& !(maptol & TOL_NIGHTS))
+	&& (player->pflags & PF_JUMPED)
+	&& !(player->powers[pw_shield] & SH_NOSTACK)
+	&& !(maptol & TOL_NIGHTS)
+	&& ALL7EMERALDS(emeralds)
+	&& (player->rings >= 50))
 		return true;
 
 	return false;
@@ -4574,6 +4600,13 @@ void P_DoJump(player_t *player, boolean soundandstate, boolean allowflip)
 					player->mo->momz += player->mo->tracer->momz;
 				if (!P_IsObjectOnGround(player->mo->tracer))
 					P_SetObjectMomZ(player->mo->tracer, -9*FRACUNIT, true);
+
+				// goose the mom a little bit to trigger gravity to process for a tic
+				if (player->mo->tracer->eflags & MFE_VERTICALFLIP)
+					player->mo->tracer->momz -= 1;
+				else
+					player->mo->tracer->momz += 1;
+
 				player->mo->tracer->flags |= MF_PUSHABLE;
 				P_SetTarget(&player->mo->tracer->tracer, NULL);
 			}
@@ -4749,7 +4782,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 				 // Revving
 				else if ((cmd->buttons & BT_SPIN) && (player->pflags & PF_STARTDASH))
 				{
-					if (player->speed > 5*player->mo->scale)
+					if (player->speed >= 5*player->mo->scale)
 					{
 						player->pflags &= ~PF_STARTDASH;
 						P_SetMobjState(player->mo, S_PLAY_ROLL);
@@ -4789,9 +4822,8 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 					if (!player->spectator)
 						S_StartSound(player->mo, sfx_spin);
 				}
-				else
 				// Catapult the player from a spindash rev!
-				if (onground && !(player->pflags & PF_SPINDOWN) && (player->pflags & PF_STARTDASH) && (player->pflags & PF_SPINNING))
+				else if (onground && !(player->pflags & PF_SPINDOWN) && (player->pflags & PF_STARTDASH) && (player->pflags & PF_SPINNING))
 				{
 					player->pflags &= ~PF_STARTDASH;
 					if (player->powers[pw_carry] == CR_BRAKGOOP)
@@ -5123,7 +5155,7 @@ void P_Telekinesis(player_t *player, fixed_t thrust, fixed_t range)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -5175,7 +5207,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 {
 	mobj_t *lockonshield = NULL;
 
-	if ((player->powers[pw_shield] & SH_NOSTACK) && !player->powers[pw_super] && !(player->pflags & PF_SHIELDDOWN)
+	if ((player->powers[pw_shield] & SH_NOSTACK) && !player->powers[pw_super] && !(player->pflags & PF_SPINDOWN)
 		&& ((!(player->pflags & PF_THOKKED) || (((player->powers[pw_shield] & SH_NOSTACK) == SH_BUBBLEWRAP || (player->powers[pw_shield] & SH_NOSTACK) == SH_ATTRACT) && player->secondjump == UINT8_MAX) ))) // thokked is optional if you're bubblewrapped / 3dblasted
 	{
 		if ((player->powers[pw_shield] & SH_NOSTACK) == SH_ATTRACT && !(player->charflags & SF_NOSHIELDABILITY))
@@ -5205,7 +5237,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 				}
 			}
 		}
-		if ((!(player->charflags & SF_NOSHIELDABILITY)) && (cmd->buttons & BT_SHIELD && !LUA_HookPlayer(player, HOOK(ShieldSpecial)))) // Shield button effects
+		if ((!(player->charflags & SF_NOSHIELDABILITY)) && (cmd->buttons & BT_SPIN && !LUA_HookPlayer(player, HOOK(ShieldSpecial)))) // Spin button effects
 		{
 			// Force stop
 			if ((player->powers[pw_shield] & ~(SH_FORCEHP|SH_STACK)) == SH_FORCE)
@@ -5294,7 +5326,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 //
 // Handles player jumping
 //
-static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd, boolean spinshieldhack)
+static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 {
 	mobj_t *lockonthok = NULL, *visual = NULL;
 
@@ -5327,13 +5359,12 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd, boolean spinshieldhac
 			;
 		else if (P_PlayerShieldThink(player, cmd, lockonthok, visual))
 			;
-		else if (cmd->buttons & BT_SPIN)
+		else if ((cmd->buttons & BT_SPIN))
 		{
-			if (spinshieldhack && !(player->pflags & PF_SPINDOWN) && P_SuperReady(player, true)
-			&& !player->powers[pw_invulnerability] && !(player->powers[pw_shield] & SH_NOSTACK)) // These two checks are no longer in P_SuperReady
+			if (!(player->pflags & PF_SPINDOWN) && P_SuperReady(player))
 			{
-				// If you're using two-button play, can turn Super and aren't already,
-				// and you don't have a shield, then turn Super!
+				// If you can turn super and aren't already,
+				// and you don't have a shield, do it!
 				P_DoSuperTransformation(player, false);
 			}
 			else if (!LUA_HookPlayer(player, HOOK(JumpSpinSpecial)))
@@ -5436,6 +5467,12 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd, boolean spinshieldhac
 		}
 		else if (player->pflags & PF_SLIDING || ((gametyperules & GTR_TEAMFLAGS) && player->gotflag) || player->pflags & PF_SHIELDABILITY)
 			;
+		/*else if (P_SuperReady(player))
+		{
+			// If you can turn super and aren't already,
+			// and you don't have a shield, do it!
+			P_DoSuperTransformation(player, false);
+		}*/
 		else if (player->pflags & PF_JUMPED)
 		{
 			if (!LUA_HookPlayer(player, HOOK(AbilitySpecial)))
@@ -6490,7 +6527,7 @@ static void P_NightsTransferPoints(player_t *player, fixed_t xspeed, fixed_t rad
 		// Find next waypoint
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo2 = (mobj_t *)th;
@@ -6526,7 +6563,7 @@ static void P_NightsTransferPoints(player_t *player, fixed_t xspeed, fixed_t rad
 		{
 			for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 			{
-				if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				if (th->removing)
 					continue;
 
 				mo2 = (mobj_t *)th;
@@ -6555,7 +6592,7 @@ static void P_NightsTransferPoints(player_t *player, fixed_t xspeed, fixed_t rad
 		{
 			for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 			{
-				if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				if (th->removing)
 					continue;
 
 				mo2 = (mobj_t *)th;
@@ -7283,7 +7320,7 @@ static void P_NiGHTSMovement(player_t *player)
 		// to find the closest axis point
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo2 = (mobj_t *)th;
@@ -8083,7 +8120,6 @@ void P_MovePlayer(player_t *player)
 {
 	ticcmd_t *cmd;
 	INT32 i;
-	boolean spinshieldhack = false; // Hack: Is Spin and Shield bound to the same button (pressed on the same tic)?
 
 	fixed_t runspd;
 
@@ -8182,7 +8218,7 @@ void P_MovePlayer(player_t *player)
 
 			for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 			{
-				if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				if (th->removing)
 					continue;
 
 				mo2 = (mobj_t *)th;
@@ -8705,13 +8741,10 @@ void P_MovePlayer(player_t *player)
 	&& !(player->mo->eflags & (MFE_UNDERWATER|MFE_TOUCHWATER)))
 		P_ElementalFire(player, false);
 
-	if ((cmd->buttons & (BT_SPIN|BT_SHIELD)) == (BT_SPIN|BT_SHIELD) && !(player->pflags & (PF_SPINDOWN|PF_SHIELDDOWN)))
-		spinshieldhack = true; // Spin and Shield is bound to the same button (pressed on the same tic), so enable two-button play (Jump and Spin+Shield)
-
 	P_DoSpinAbility(player, cmd);
 
 	// jumping
-	P_DoJumpStuff(player, cmd, spinshieldhack);
+	P_DoJumpStuff(player, cmd);
 
 	// If you're not spinning, you'd better not be spindashing!
 	if (!(player->pflags & PF_SPINNING) && player->powers[pw_carry] != CR_NIGHTSMODE)
@@ -8799,25 +8832,10 @@ void P_MovePlayer(player_t *player)
 			P_PlayerFlagBurst(player, true);
 	}
 
-	// Check for fire and shield buttons
+	// check for fire
 	if (!player->exiting)
-	{
 		P_DoFiring(player, cmd);
 
-		// Shield button behavior
-		// Check P_PlayerShieldThink for actual shields!
-		if ((cmd->buttons & BT_SHIELD) && !(player->pflags & PF_SHIELDDOWN) && !spinshieldhack)
-		{
-			// Transform into super if we can!
-			if (P_SuperReady(player, true))
-				P_DoSuperTransformation(player, false);
-
-			// Detransform from super if we can!
-			else if (P_SuperReady(player, false))
-				P_DoSuperDetransformation(player);
-		}
-	}
-
 	{
 		boolean atspinheight = false;
 		fixed_t oldheight = player->mo->height;
@@ -8835,6 +8853,8 @@ void P_MovePlayer(player_t *player)
 			player->mo->height = P_GetPlayerSpinHeight(player);
 			atspinheight = true;
 		}
+		else if (player->powers[pw_carry] == CR_PLAYER || player->powers[pw_carry] == CR_PTERABYTE) // You're slightly shorter while being carried
+			player->mo->height = FixedDiv(P_GetPlayerHeight(player), FixedDiv(14*FRACUNIT,10*FRACUNIT));
 		else
 			player->mo->height = P_GetPlayerHeight(player);
 
@@ -9165,7 +9185,7 @@ void P_NukeEnemies(mobj_t *inflictor, mobj_t *source, fixed_t radius)
 
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 
 		mo = (mobj_t *)think;
@@ -9263,7 +9283,7 @@ mobj_t *P_LookForFocusTarget(player_t *player, mobj_t *exclude, SINT8 direction,
 
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 
 		mo = (mobj_t *)think;
@@ -9382,7 +9402,7 @@ mobj_t *P_LookForEnemies(player_t *player, boolean nonenemies, boolean bullet)
 
 	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
 	{
-		if (think->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (think->removing)
 			continue;
 
 		mo = (mobj_t *)think;
@@ -9528,7 +9548,7 @@ void P_FindEmerald(void)
 	// to find all emeralds
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -9836,7 +9856,7 @@ static CV_PossibleValue_t campos_cons_t[] = { {INT32_MIN, "MIN"}, {INT32_MAX, "M
 consvar_t cv_cam_dist = CVAR_INIT ("cam_curdist", "160", CV_FLOAT|CV_ALLOWLUA, campos_cons_t, NULL);
 consvar_t cv_cam_height = CVAR_INIT ("cam_curheight", "25", CV_FLOAT|CV_ALLOWLUA, campos_cons_t, NULL);
 consvar_t cv_cam_still = CVAR_INIT ("cam_still", "Off", CV_ALLOWLUA, CV_OnOff, NULL);
-consvar_t cv_cam_speed = CVAR_INIT ("cam_speed", "0.3", CV_FLOAT|CV_SAVE|CV_ALLOWLUA, CV_CamSpeed, NULL);
+consvar_t cv_cam_speed = CVAR_INIT ("cam_speed", "0.4", CV_FLOAT|CV_SAVE|CV_ALLOWLUA, CV_CamSpeed, NULL);
 consvar_t cv_cam_rotate = CVAR_INIT ("cam_rotate", "0", CV_CALL|CV_NOINIT|CV_ALLOWLUA, CV_CamRotate, CV_CamRotate_OnChange);
 consvar_t cv_cam_rotspeed = CVAR_INIT ("cam_rotspeed", "10", CV_SAVE|CV_ALLOWLUA, rotation_cons_t, NULL);
 consvar_t cv_cam_turnmultiplier = CVAR_INIT ("cam_turnmultiplier", "0.75", CV_FLOAT|CV_SAVE|CV_ALLOWLUA, multiplier_cons_t, NULL);
@@ -9845,7 +9865,7 @@ consvar_t cv_cam_adjust = CVAR_INIT ("cam_adjust", "On", CV_SAVE|CV_ALLOWLUA, CV
 consvar_t cv_cam2_dist = CVAR_INIT ("cam2_curdist", "160", CV_FLOAT|CV_ALLOWLUA, campos_cons_t, NULL);
 consvar_t cv_cam2_height = CVAR_INIT ("cam2_curheight", "25", CV_FLOAT|CV_ALLOWLUA, campos_cons_t, NULL);
 consvar_t cv_cam2_still = CVAR_INIT ("cam2_still", "Off", CV_ALLOWLUA, CV_OnOff, NULL);
-consvar_t cv_cam2_speed = CVAR_INIT ("cam2_speed", "0.3", CV_FLOAT|CV_SAVE|CV_ALLOWLUA, CV_CamSpeed, NULL);
+consvar_t cv_cam2_speed = CVAR_INIT ("cam2_speed", "0.4", CV_FLOAT|CV_SAVE|CV_ALLOWLUA, CV_CamSpeed, NULL);
 consvar_t cv_cam2_rotate = CVAR_INIT ("cam2_rotate", "0", CV_CALL|CV_NOINIT|CV_ALLOWLUA, CV_CamRotate, CV_CamRotate2_OnChange);
 consvar_t cv_cam2_rotspeed = CVAR_INIT ("cam2_rotspeed", "10", CV_SAVE|CV_ALLOWLUA, rotation_cons_t, NULL);
 consvar_t cv_cam2_turnmultiplier = CVAR_INIT ("cam2_turnmultiplier", "0.75", CV_FLOAT|CV_SAVE|CV_ALLOWLUA, multiplier_cons_t, NULL);
@@ -10927,7 +10947,7 @@ static mobj_t *P_GetAxis(INT32 num)
 
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mobj = (mobj_t *)th;
@@ -11516,6 +11536,18 @@ void P_DoTailsOverlay(player_t *player, mobj_t *tails)
 }
 
 // Metal Sonic's jet fume
+//
+// The follow object's state is set to its spawn state when deactivated.
+// When the player is on a moving animation, the follow object goes to its see state.
+// When dash mode is entered, the follow object switches to its melee state.
+//
+// If MF2_FRET is set, the jet fume will flash during dash mode.
+// MF2_AMBUSH can be used to enable Metal Sonic's skidding animation.
+// MF2_JUSTATTACKED will enable the color cycling.
+// If the follow item is MT_METALJETFUME, the above two effects are automatically applied.
+//
+// MF2_STRONGBOX is internally used to track if the jet fume is in its deactivated state or not.
+// MF2_BOSSNOTRAP is internally used to instantly reset the jet fume's scale to its intended scale.
 void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 {
 	static const UINT8 FUME_SKINCOLORS[] =
@@ -11542,15 +11574,28 @@ void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	fixed_t heightoffset = ((mo->eflags & MFE_VERTICALFLIP) ? mo->height - (P_GetPlayerHeight(player) >> 1) : (P_GetPlayerHeight(player) >> 1));
 	panim_t panim = player->panim;
 	tic_t dashmode = min(player->dashmode, DASHMODE_MAX);
+	boolean ismetaljetfume = fume->type == MT_METALJETFUME;
+	boolean notmoving = panim != PA_WALK && panim != PA_RUN && panim != PA_DASH;
 	boolean underwater = mo->eflags & MFE_UNDERWATER;
 	statenum_t stat = fume->state-states;
 	boolean resetinterp = false;
 
-	if (panim != PA_WALK && panim != PA_RUN && panim != PA_DASH) // turn invisible when not in a coherent movement state
+	if (notmoving) // deactivate when not in a coherent movement state
 	{
-		if (stat != fume->info->spawnstate)
+		if ((fume->flags2 & MF2_STRONGBOX) == 0)
+		{
 			P_SetMobjState(fume, fume->info->spawnstate);
-		return;
+			fume->flags2 |= MF2_STRONGBOX;
+		}
+		if (P_MobjWasRemoved(fume) || ismetaljetfume)
+			return;
+	}
+
+	// Rotate on skid animation if follow item is MT_METALJETFUME, or if MF2_AMBUSH is set
+	if (player->mo->sprite2 == SPR2_SKID)
+	{
+		if ((ismetaljetfume && (player->charflags & SF_JETFUME)) || (fume->flags2 & MF2_AMBUSH))
+			angle += ANGLE_90;
 	}
 
 	if (underwater) // No fume underwater; spawn bubbles instead!
@@ -11588,54 +11633,82 @@ void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 
 		if (panim == PA_WALK)
 		{
-			if (stat != fume->info->spawnstate)
+			if ((fume->flags2 & MF2_STRONGBOX) == 0)
 			{
-				fume->threshold = 0;
 				P_SetMobjState(fume, fume->info->spawnstate);
+				fume->threshold = 0;
+				fume->flags2 &= ~MF2_STRONGBOX;
 			}
-			return;
+			if (P_MobjWasRemoved(fume) || ismetaljetfume)
+				return;
 		}
 	}
 
-	if (stat == fume->info->spawnstate) // If currently inivisble, activate!
+	// If currently deactivated, activate!
+	if (!notmoving && !underwater && (fume->flags2 & MF2_STRONGBOX))
 	{
 		P_SetMobjState(fume, (stat = fume->info->seestate));
+		if (P_MobjWasRemoved(fume))
+			return;
 		P_SetScale(fume, mo->scale, false);
+		fume->flags2 &= ~MF2_STRONGBOX;
 		resetinterp = true;
 	}
 
-	if (dashmode > DASHMODE_THRESHOLD && stat != fume->info->seestate) // If in dashmode, grow really big and flash
+	// If in dash mode, grow really big
+	if (dashmode > DASHMODE_THRESHOLD && stat != fume->info->meleestate)
 	{
 		fume->destscale = mo->scale;
-		fume->flags2 ^= MF2_DONTDRAW;
 		fume->flags2 |= mo->flags2 & MF2_DONTDRAW;
+
+		// Flash if follow item is MT_METALJETFUME, or if MF2_FRET is set
+		if (ismetaljetfume || (fume->flags2 & MF2_FRET))
+			fume->flags2 ^= MF2_DONTDRAW;
 	}
 	else // Otherwise, pick a size and color depending on speed and proximity to dashmode
 	{
-		if (dashmode == DASHMODE_THRESHOLD && dashmode > (tic_t)fume->movecount) // If just about to enter dashmode, play the startup animation again
+		// If just about to enter dash mode, play the startup animation again
+		if (dashmode == DASHMODE_THRESHOLD && dashmode > (tic_t)fume->movecount)
 		{
-			P_SetMobjState(fume, (stat = fume->info->seestate));
+			P_SetMobjState(fume, fume->info->meleestate);
+			if (P_MobjWasRemoved(fume))
+				return;
 			P_SetScale(fume, 2*mo->scale, true);
 		}
+
 		fume->flags2 = (fume->flags2 & ~MF2_DONTDRAW) | (mo->flags2 & MF2_DONTDRAW);
 		fume->destscale = (mo->scale + FixedDiv(player->speed, player->normalspeed)) / (underwater ? 6 : 3);
-		fume->color = FUME_SKINCOLORS[(dashmode * sizeof(FUME_SKINCOLORS)) / (DASHMODE_MAX + 1)];
+
+		// Do color cycling if follow item is MT_METALJETFUME, or if MF2_JUSTATTACKED is set
+		if (ismetaljetfume || (fume->flags2 & MF2_JUSTATTACKED))
+			fume->color = FUME_SKINCOLORS[(dashmode * sizeof(FUME_SKINCOLORS)) / (DASHMODE_MAX + 1)];
 
 		if (underwater)
 		{
-			fume->frame = (fume->frame & FF_FRAMEMASK) | FF_ANIMATE | (P_RandomRange(0, 9) * FF_TRANS10);
+			fume->frame = (fume->frame & ~FF_TRANSMASK) | (P_RandomRange(0, 9) << FF_TRANSSHIFT);
 			fume->threshold = 1;
 		}
 		else if (fume->threshold)
 		{
-			fume->frame = (fume->frame & FF_FRAMEMASK) | fume->state->frame;
+			fume->frame = (fume->frame & FF_FRAMEMASK) | (fume->state->frame & ~FF_FRAMEMASK);
 			fume->threshold = 0;
 		}
 	}
 
-	fume->movecount = dashmode; // keeps track of previous dashmode value so we know whether Metal is entering or leaving it
-	fume->flags2 = (fume->flags2 & ~MF2_OBJECTFLIP) | (mo->flags2 & MF2_OBJECTFLIP); // Make sure to flip in reverse gravity!
-	fume->eflags = (fume->eflags & ~MFE_VERTICALFLIP) | (mo->eflags & MFE_VERTICALFLIP); // Make sure to flip in reverse gravity!
+	// keeps track of previous dash mode value so we know whether Metal is entering or leaving it
+	fume->movecount = dashmode;
+
+	// Make sure to flip in reverse gravity!
+	fume->flags2 = (fume->flags2 & ~MF2_OBJECTFLIP) | (mo->flags2 & MF2_OBJECTFLIP);
+	fume->eflags = (fume->eflags & ~MFE_VERTICALFLIP) | (mo->eflags & MFE_VERTICALFLIP);
+
+	// Set the appropriate scale at spawn
+	// This is... strange, but I had to choose a flag that a follow object would not ordinarily use.
+	if ((fume->flags2 & MF2_BOSSNOTRAP) == 0)
+	{
+		P_SetScale(fume, fume->destscale, true);
+		fume->flags2 |= MF2_BOSSNOTRAP;
+	}
 
 	// Finally, set its position
 	dist = -mo->radius - FixedMul(fume->info->radius, fume->destscale - mo->scale/3);
@@ -11645,9 +11718,10 @@ void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	fume->y = mo->y + P_ReturnThrustY(fume, angle, dist);
 	fume->z = mo->z + heightoffset - (fume->height >> 1);
 	P_SetThingPosition(fume);
-	if (resetinterp) R_ResetMobjInterpolationState(fume);
+	if (resetinterp)
+		R_ResetMobjInterpolationState(fume);
 
-	// If dashmode is high enough, spawn a trail
+	// If dash mode is high enough, spawn a trail
 	if (player->normalspeed >= skins[player->skin]->normalspeed*2)
 	{
 		mobj_t *ghost = P_SpawnGhostMobj(fume);
@@ -11661,6 +11735,8 @@ void P_DoFollowMobj(player_t *player, mobj_t *followmobj)
 {
 	if (LUA_HookFollowMobj(player, followmobj) || P_MobjWasRemoved(followmobj))
 		{;}
+	else if (player->charflags & SF_JETFUME)
+		P_DoMetalJetFume(player, followmobj);
 	else
 	{
 		switch (followmobj->type)
@@ -12009,7 +12085,7 @@ void P_PlayerThink(player_t *player)
 
 		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+			if (th->removing)
 				continue;
 
 			mo2 = (mobj_t *)th;
@@ -12069,7 +12145,7 @@ void P_PlayerThink(player_t *player)
 				ticmiss++;
 
 			P_DoRopeHang(player);
-			P_DoJumpStuff(player, &player->cmd, false); // P_DoRopeHang would set PF_SPINDOWN, so no spinshieldhack here
+			P_DoJumpStuff(player, &player->cmd);
 		}
 		else //if (player->powers[pw_carry] == CR_ZOOMTUBE)
 		{
@@ -12150,6 +12226,10 @@ void P_PlayerThink(player_t *player)
 				case CR_DUSTDEVIL:
 					player->drawangle += ANG20;
 					break;
+				case CR_FAN:
+					if (player->pflags & PF_ANALOGMODE) // Don't impact drawangle in any special way when on a fan
+						player->drawangle = player->mo->angle;
+					break;
 				/* -- in case we wanted to have the camera freely movable during zoom tubes
 				case CR_ZOOMTUBE:*/
 				case CR_ROPEHANG:
@@ -12338,12 +12418,6 @@ void P_PlayerThink(player_t *player)
 			player->pflags &= ~PF_SPINDOWN;
 	}
 
-	// Check for Shield button
-	if (cmd->buttons & BT_SHIELD)
-		player->pflags |= PF_SHIELDDOWN;
-	else
-		player->pflags &= ~PF_SHIELDDOWN;
-
 	// IF PLAYER NOT HERE THEN FLASH END IF
 	if (player->quittime && player->powers[pw_flashing] < flashingtics - 1
 	&& !(G_TagGametype() && !(player->pflags & PF_TAGIT)) && !player->gotflag)
@@ -12847,9 +12921,9 @@ void P_PlayerAfterThink(player_t *player)
 				else
 				{
 					if (tails->player)
-						P_TryMove(player->mo, tails->x + P_ReturnThrustX(tails, tails->player->drawangle, 4*FRACUNIT), tails->y + P_ReturnThrustY(tails, tails->player->drawangle, 4*FRACUNIT), true);
+						P_TryMove(player->mo, tails->x + P_ReturnThrustX(tails, tails->player->drawangle, 4*tails->scale), tails->y + P_ReturnThrustY(tails, tails->player->drawangle, 4*tails->scale), true);
 					else
-						P_TryMove(player->mo, tails->x + P_ReturnThrustX(tails, tails->angle, 4*FRACUNIT), tails->y + P_ReturnThrustY(tails, tails->angle, 4*FRACUNIT), true);
+						P_TryMove(player->mo, tails->x + P_ReturnThrustX(tails, tails->angle, 4*tails->scale), tails->y + P_ReturnThrustY(tails, tails->angle, 4*tails->scale), true);
 					player->mo->momx = tails->momx;
 					player->mo->momy = tails->momy;
 					player->mo->momz = tails->momz;
@@ -12863,7 +12937,7 @@ void P_PlayerAfterThink(player_t *player)
 						P_SetPlayerAngle(player, player->mo->angle);
 				}
 
-				if (P_AproxDistance(player->mo->x - tails->x, player->mo->y - tails->y) > player->mo->radius)
+				if (P_AproxDistance(player->mo->x - tails->x, player->mo->y - tails->y) > tails->radius)
 					player->powers[pw_carry] = CR_NONE;
 
 				if (player->powers[pw_carry] == CR_PLAYER)
@@ -13084,7 +13158,7 @@ void P_PlayerAfterThink(player_t *player)
 				player->mo->momy = ptera->momy;
 				player->mo->momz = ptera->momz;
 
-				if (P_AproxDistance(player->mo->x - ptera->x - ptera->watertop, player->mo->y - ptera->y - ptera->waterbottom) > player->mo->radius)
+				if (P_AproxDistance(player->mo->x - ptera->x - ptera->watertop, player->mo->y - ptera->y - ptera->waterbottom) > ptera->radius)
 					goto dropoff;
 
 				ptera->watertop >>= 1;
@@ -13161,7 +13235,8 @@ void P_PlayerAfterThink(player_t *player)
 						player->followmobj->colorized = true;
 						break;
 					default:
-						player->followmobj->flags2 |= MF2_LINKDRAW;
+						if ((player->charflags & SF_JETFUME) == 0)
+							player->followmobj->flags2 |= MF2_LINKDRAW;
 						break;
 				}
 			}
diff --git a/src/r_bbox.c b/src/r_bbox.c
index 8ccad2bb58186098810f299405fc0f4d23fa9b5a..93fa2dca28c4a1f923c69ff4f44958c7e583310c 100644
--- a/src/r_bbox.c
+++ b/src/r_bbox.c
@@ -275,7 +275,7 @@ boolean R_ThingBoundingBoxVisible(mobj_t *thing)
 	switch (thing->type)
 	{
 		default:
-			// First person / awayviewmobj -- rendering a bbox 
+			// First person / awayviewmobj -- rendering a bbox
 			// too close to the viewpoint causes anomalies
 			// and these are exactly on the viewpoint!
 			if (thing != r_viewmobj)
diff --git a/src/r_bsp.c b/src/r_bsp.c
index d606d7a274b322e932cdd9778ea9d780748c7394..ad27b9bf8162a39bef6a5ddfa75c3e681c5632ca 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -230,6 +230,18 @@ static INT32 R_DoorClosed(void)
 	&& (backsector->floorheight <= frontsector->floorheight || curline->sidedef->bottomtexture);
 }
 
+static UINT8 R_FloorLightLevel(sector_t *sector, INT16 base_lightlevel)
+{
+	return max(0, min(255, sector->floorlightlevel +
+		((sector->floorlightabsolute) ? 0 : base_lightlevel)));
+}
+
+static UINT8 R_CeilingLightLevel(sector_t *sector, INT16 base_lightlevel)
+{
+	return max(0, min(255, sector->ceilinglightlevel +
+		((sector->ceilinglightabsolute) ? 0 : base_lightlevel)));
+}
+
 //
 // If player's view height is underneath fake floor, lower the
 // drawn ceiling to be just under the floor height, and replace
@@ -312,11 +324,11 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 			tempsec->lightlevel = s->lightlevel;
 
 			if (floorlightlevel)
-				*floorlightlevel = s->floorlightsec == -1 ? (s->floorlightabsolute ? s->floorlightlevel : max(0, min(255, s->lightlevel + s->floorlightlevel)))
+			*floorlightlevel = s->floorlightsec == -1 ? (s->floorlightabsolute ? s->floorlightlevel : max(0, min(255, s->lightlevel + s->floorlightlevel)))
 					: sectors[s->floorlightsec].lightlevel;
 
 			if (ceilinglightlevel)
-				*ceilinglightlevel = s->ceilinglightsec == -1 ? (s->ceilinglightabsolute ? s->ceilinglightlevel : max(0, min(255, s->lightlevel + s->ceilinglightlevel)))
+			*ceilinglightlevel = s->ceilinglightsec == -1 ? (s->ceilinglightabsolute ? s->ceilinglightlevel : max(0, min(255, s->lightlevel + s->ceilinglightlevel)))
 					: sectors[s->ceilinglightsec].lightlevel;
 		}
 		else if (heightsec != -1 && viewz >= sectors[heightsec].ceilingheight
@@ -356,11 +368,11 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 			tempsec->lightlevel = s->lightlevel;
 
 			if (floorlightlevel)
-				*floorlightlevel = s->floorlightsec == -1 ? (s->floorlightabsolute ? s->floorlightlevel : max(0, min(255, s->lightlevel + s->floorlightlevel)))
+			*floorlightlevel = s->floorlightsec == -1 ? (s->floorlightabsolute ? s->floorlightlevel : max(0, min(255, s->lightlevel + s->floorlightlevel)))
 					: sectors[s->floorlightsec].lightlevel;
 
 			if (ceilinglightlevel)
-				*ceilinglightlevel = s->ceilinglightsec == -1 ? (s->ceilinglightabsolute ? s->ceilinglightlevel : max(0, min(255, s->lightlevel + s->ceilinglightlevel)))
+			*ceilinglightlevel = s->ceilinglightsec == -1 ? (s->ceilinglightabsolute ? s->ceilinglightlevel : max(0, min(255, s->lightlevel + s->ceilinglightlevel)))
 					: sectors[s->ceilinglightsec].lightlevel;
 		}
 		sec = tempsec;
@@ -968,11 +980,10 @@ static void R_Subsector(size_t num)
 				&& ((viewz < heightcheck && (rover->fofflags & FOF_BOTHPLANES || !(rover->fofflags & FOF_INVERTPLANES)))
 				|| (viewz > heightcheck && (rover->fofflags & FOF_BOTHPLANES || rover->fofflags & FOF_INVERTPLANES))))
 			{
-				light = R_GetPlaneLight(frontsector, planecenterz,
-					viewz < heightcheck);
+				light = R_GetPlaneLight(frontsector, planecenterz, viewz < heightcheck);
 
 				ffloor[numffloors].plane = R_FindPlane(rover->master->frontsector, *rover->bottomheight, *rover->bottompic,
-					*frontsector->lightlist[light].lightlevel, *rover->bottomxoffs, *rover->bottomyoffs,
+					R_FloorLightLevel(rover->master->frontsector, *frontsector->lightlist[light].lightlevel), *rover->bottomxoffs, *rover->bottomyoffs,
 					*rover->bottomxscale, *rover->bottomyscale, *rover->bottomangle,
 					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->b_slope, NULL);
 
@@ -1002,7 +1013,7 @@ static void R_Subsector(size_t num)
 				light = R_GetPlaneLight(frontsector, planecenterz, viewz < heightcheck);
 
 				ffloor[numffloors].plane = R_FindPlane(rover->master->frontsector, *rover->topheight, *rover->toppic,
-					*frontsector->lightlist[light].lightlevel, *rover->topxoffs, *rover->topyoffs,
+					R_CeilingLightLevel(rover->master->frontsector, *frontsector->lightlist[light].lightlevel), *rover->topxoffs, *rover->topyoffs,
 					*rover->topxscale, *rover->topyscale, *rover->topangle,
 					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->t_slope, NULL);
 
diff --git a/src/r_data.c b/src/r_data.c
index 75ec0556d46f1edff4d2c473b595a40e835426ac..32faa07a8fb40370008218f2c4fa9ec3a4d3737b 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -426,9 +426,6 @@ void R_ClearColormaps(void)
 {
 	// Purged by PU_LEVEL, just overwrite the pointer
 	extra_colormaps = R_CreateDefaultColormap(true);
-#ifdef HWRENDER
-	HWR_ClearLightTables();
-#endif
 }
 
 //
@@ -849,6 +846,15 @@ void R_GenerateLightTable(extracolormap_t *extra_colormap, boolean uselookup)
 	}
 }
 
+void R_UpdateLightTable(extracolormap_t *extra_colormap, boolean uselookup)
+{
+	R_GenerateLightTable(extra_colormap, uselookup);
+
+#ifdef HWRENDER
+	extra_colormap->gl_lighttable.needs_update = true;
+#endif
+}
+
 extracolormap_t *R_CreateColormapFromLinedef(char *p1, char *p2, char *p3)
 {
 	// default values
diff --git a/src/r_data.h b/src/r_data.h
index 9340ed284b6524e54774cfa43ed6c24e3e92afe3..64abf6d83637ed8f54c7449f621ac52eb6aa9113 100644
--- a/src/r_data.h
+++ b/src/r_data.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -90,6 +90,7 @@ typedef enum
 } textmapcolormapflags_t;
 
 void R_GenerateLightTable(extracolormap_t *extra_colormap, boolean uselookup);
+void R_UpdateLightTable(extracolormap_t *extra_colormap, boolean uselookup);
 lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap);
 extracolormap_t * R_CreateColormapFromLinedef(char *p1, char *p2, char *p3);
 extracolormap_t* R_CreateColormap(INT32 rgba, INT32 fadergba, UINT8 fadestart, UINT8 fadeend, UINT8 flags);
diff --git a/src/r_defs.h b/src/r_defs.h
index da4dd2d70e6049479eacd24c51af11b4a995507b..eac3e2d1f38752f9b3b2002f1a7ab3e3fe15975e 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -60,6 +60,8 @@ typedef UINT8 lighttable_t;
 #define CMF_FADEFULLBRIGHTSPRITES  1
 #define CMF_FOG 4
 
+#define TEXTURE_255_IS_TRANSPARENT
+
 // ExtraColormap type. Use for extra_colormaps from now on.
 typedef struct extracolormap_s
 {
@@ -74,8 +76,11 @@ typedef struct extracolormap_s
 	lighttable_t *colormap;
 
 #ifdef HWRENDER
-	// The id of the hardware lighttable. Zero means it does not exist yet.
-	UINT32 gl_lighttable_id;
+	struct {
+		UINT32 id; // The id of the hardware lighttable. Zero means it does not exist yet.
+		RGBA_t *data; // The texture data of the hardware lighttable.
+		boolean needs_update; // If the colormap changed recently or not.
+	} gl_lighttable;
 #endif
 
 #ifdef EXTRACOLORMAPLUMPS
@@ -240,9 +245,8 @@ typedef struct sectorportal_s
 		struct sector_s *sector;
 		struct mobj_s *mobj;
 	};
-	struct {
-		fixed_t x, y;
-	} origin;
+	struct sector_s *target;
+	boolean ceiling;
 } sectorportal_t;
 
 typedef struct ffloor_s
@@ -358,7 +362,7 @@ typedef struct pslope_s
 
 	double dzdelta;
 
-	boolean moved : 1;
+	boolean moved;
 
 	UINT8 flags; // Slope options
 } pslope_t;
@@ -612,6 +616,8 @@ typedef struct line_s
 	INT16 callcount; // no. of calls left before triggering, for the "X calls" linedef specials, defaults to 0
 
 	UINT32 secportal; // transferred sector portal
+
+	struct pslope_s *midtexslope;
 } line_t;
 
 typedef struct
@@ -629,6 +635,11 @@ typedef struct
 	fixed_t scalex_top, scalex_mid, scalex_bottom;
 	fixed_t scaley_top, scaley_mid, scaley_bottom;
 
+	// per-wall lighting for UDMF
+	// TODO: implement per-texture lighting
+	INT16 light, light_top, light_mid, light_bottom;
+	boolean lightabsolute, lightabsolute_top, lightabsolute_mid, lightabsolute_bottom;
+
 	// Texture indices.
 	// We do not maintain names here.
 	INT32 toptexture, bottomtexture, midtexture;
diff --git a/src/r_draw.c b/src/r_draw.c
index 86f7e488c8502516cc4f36bb04b7a516385fc5ed..906398ec4972e53565cdcfa04deb51557956d39c 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -19,7 +19,6 @@
 #include "doomstat.h"
 #include "r_local.h"
 #include "r_translation.h"
-#include "st_stuff.h" // need ST_HEIGHT
 #include "i_video.h"
 #include "v_video.h"
 #include "m_misc.h"
@@ -74,7 +73,7 @@ UINT8 *dc_transmap; // one of the translucency tables
 UINT8 *dc_translation;
 
 struct r_lightlist_s *dc_lightlist = NULL;
-INT32 dc_numlights = 0, dc_maxlights, dc_texheight;
+INT32 dc_numlights = 0, dc_maxlights, dc_texheight, dc_postlength;
 
 // =========================================================================
 //                      SPAN DRAWING CODE STUFF
diff --git a/src/r_draw.h b/src/r_draw.h
index 1a828312a7a922353a3ff5b969908dc61dcf0ee6..d5c5b4e25d36ba375d365a2d13f0ba5fab19e3f3 100644
--- a/src/r_draw.h
+++ b/src/r_draw.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -41,8 +41,7 @@ extern UINT8 *dc_translation;
 extern struct r_lightlist_s *dc_lightlist;
 extern INT32 dc_numlights, dc_maxlights;
 
-//Fix TUTIFRUTI
-extern INT32 dc_texheight;
+extern INT32 dc_texheight, dc_postlength;
 
 // -----------------------
 // SPAN DRAWING CODE STUFF
@@ -154,8 +153,12 @@ void R_VideoErase(size_t ofs, INT32 count);
 // -----------------
 
 void R_DrawColumn_8(void);
+void R_DrawColumnClamped_8(void);
+void R_Draw2sMultiPatchColumn_8(void);
 void R_DrawShadeColumn_8(void);
 void R_DrawTranslucentColumn_8(void);
+void R_DrawTranslucentColumnClamped_8(void);
+void R_Draw2sMultiPatchTranslucentColumn_8(void);
 void R_DrawDropShadowColumn_8(void);
 void R_DrawTranslatedColumn_8(void);
 void R_DrawTranslatedTranslucentColumn_8(void);
@@ -170,6 +173,7 @@ void R_DrawTiltedTranslucentSpan_8(void);
 void R_DrawSplat_8(void);
 void R_DrawTranslucentSplat_8(void);
 void R_DrawTiltedSplat_8(void);
+void R_DrawTiltedTranslucentSplat_8(void);
 
 void R_DrawFloorSprite_8(void);
 void R_DrawTranslucentFloorSprite_8(void);
@@ -191,6 +195,7 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void);
 void R_DrawSplat_NPO2_8(void);
 void R_DrawTranslucentSplat_NPO2_8(void);
 void R_DrawTiltedSplat_NPO2_8(void);
+void R_DrawTiltedTranslucentSplat_NPO2_8(void);
 
 void R_DrawFloorSprite_NPO2_8(void);
 void R_DrawTranslucentFloorSprite_NPO2_8(void);
diff --git a/src/r_draw8.c b/src/r_draw8.c
index 99fb71e289343034bc6327314da0e64b0d687dab..c720f454e078d1f7857227eae01caa866d56d136 100644
--- a/src/r_draw8.c
+++ b/src/r_draw8.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -100,6 +100,281 @@ void R_DrawColumn_8(void)
 	}
 }
 
+/**	\brief The R_DrawColumnClamped_8 function
+	Same as R_DrawColumn_8, but prevents artifacts from showing up (caused by fixed-point imprecisions)
+*/
+void R_DrawColumnClamped_8(void)
+{
+	INT32 count;
+	UINT8 *dest;
+	fixed_t frac;
+	fixed_t fracstep;
+
+	count = dc_yh - dc_yl;
+
+	if (count < 0) // Zero length, column does not exceed a pixel.
+		return;
+
+#ifdef RANGECHECK
+	if ((unsigned)dc_x >= (unsigned)vid.width || dc_yl < 0 || dc_yh >= vid.height)
+		return;
+#endif
+
+	// Framebuffer destination address.
+	dest = &topleft[dc_yl*vid.width + dc_x];
+
+	count++;
+
+	// Determine scaling, which is the only mapping to be done.
+	fracstep = dc_iscale;
+	frac = dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep);
+
+	// Inner loop that does the actual texture mapping, e.g. a DDA-like scaling.
+	// This is as fast as it gets.
+	{
+		const UINT8 *source = dc_source;
+		const lighttable_t *colormap = dc_colormap;
+		INT32 heightmask = dc_texheight-1;
+		INT32 idx;
+		if (dc_texheight & heightmask)   // not a power of 2 -- killough
+		{
+			heightmask++;
+			heightmask <<= FRACBITS;
+
+			if (frac < 0)
+				while ((frac += heightmask) <  0);
+			else
+				while (frac >= heightmask)
+					frac -= heightmask;
+
+			do
+			{
+				// Re-map color indices from wall texture column
+				//  using a lighting/special effects LUT.
+				// heightmask is the Tutti-Frutti fix
+				idx = frac>>FRACBITS;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = colormap[source[idx]];
+				dest += vid.width;
+
+				// Avoid overflow.
+				if (fracstep > 0x7FFFFFFF - frac)
+					frac += fracstep - heightmask;
+				else
+					frac += fracstep;
+
+				while (frac >= heightmask)
+					frac -= heightmask;
+			} while (--count);
+		}
+		else
+		{
+			while ((count -= 2) >= 0) // texture height is a power of 2
+			{
+				idx = (frac>>FRACBITS) & heightmask;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = colormap[source[idx]];
+				dest += vid.width;
+				frac += fracstep;
+				idx = (frac>>FRACBITS) & heightmask;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = colormap[source[idx]];
+				dest += vid.width;
+				frac += fracstep;
+			}
+			if (count & 1)
+			{
+				idx = (frac>>FRACBITS) & heightmask;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = colormap[source[idx]];
+			}
+		}
+	}
+}
+
+void R_Draw2sMultiPatchColumn_8(void)
+{
+	INT32 count;
+	register UINT8 *dest;
+	register fixed_t frac;
+	fixed_t fracstep;
+
+	count = dc_yh - dc_yl;
+
+	if (count < 0) // Zero length, column does not exceed a pixel.
+		return;
+
+#ifdef RANGECHECK
+	if ((unsigned)dc_x >= (unsigned)vid.width || dc_yl < 0 || dc_yh >= vid.height)
+		return;
+#endif
+
+	// Framebuffer destination address.
+	dest = &topleft[dc_yl*vid.width + dc_x];
+
+	count++;
+
+	// Determine scaling, which is the only mapping to be done.
+	fracstep = dc_iscale;
+	frac = dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep);
+
+	// Inner loop that does the actual texture mapping, e.g. a DDA-like scaling.
+	// This is as fast as it gets.
+	{
+		register const UINT8 *source = dc_source;
+		register const lighttable_t *colormap = dc_colormap;
+		register INT32 heightmask = dc_texheight-1;
+		register UINT8 val;
+		if (dc_texheight & heightmask)   // not a power of 2 -- killough
+		{
+			heightmask++;
+			heightmask <<= FRACBITS;
+
+			if (frac < 0)
+				while ((frac += heightmask) <  0);
+			else
+				while (frac >= heightmask)
+					frac -= heightmask;
+
+			do
+			{
+				// Re-map color indices from wall texture column
+				//  using a lighting/special effects LUT.
+				// heightmask is the Tutti-Frutti fix
+				val = source[frac>>FRACBITS];
+
+				if (val != TRANSPARENTPIXEL)
+					*dest = colormap[val];
+
+				dest += vid.width;
+
+				// Avoid overflow.
+				if (fracstep > 0x7FFFFFFF - frac)
+					frac += fracstep - heightmask;
+				else
+					frac += fracstep;
+
+				while (frac >= heightmask)
+					frac -= heightmask;
+			} while (--count);
+		}
+		else
+		{
+			while ((count -= 2) >= 0) // texture height is a power of 2
+			{
+				val = source[(frac>>FRACBITS) & heightmask];
+				if (val != TRANSPARENTPIXEL)
+					*dest = colormap[val];
+				dest += vid.width;
+				frac += fracstep;
+				val = source[(frac>>FRACBITS) & heightmask];
+				if (val != TRANSPARENTPIXEL)
+					*dest = colormap[val];
+				dest += vid.width;
+				frac += fracstep;
+			}
+			if (count & 1)
+			{
+				val = source[(frac>>FRACBITS) & heightmask];
+				if (val != TRANSPARENTPIXEL)
+					*dest = colormap[val];
+			}
+		}
+	}
+}
+
+void R_Draw2sMultiPatchTranslucentColumn_8(void)
+{
+	INT32 count;
+	register UINT8 *dest;
+	register fixed_t frac;
+	fixed_t fracstep;
+
+	count = dc_yh - dc_yl;
+
+	if (count < 0) // Zero length, column does not exceed a pixel.
+		return;
+
+#ifdef RANGECHECK
+	if ((unsigned)dc_x >= (unsigned)vid.width || dc_yl < 0 || dc_yh >= vid.height)
+		return;
+#endif
+
+	// Framebuffer destination address.
+	dest = &topleft[dc_yl*vid.width + dc_x];
+
+	count++;
+
+	// Determine scaling, which is the only mapping to be done.
+	fracstep = dc_iscale;
+	frac = dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep);
+
+	// Inner loop that does the actual texture mapping, e.g. a DDA-like scaling.
+	// This is as fast as it gets.
+	{
+		register const UINT8 *source = dc_source;
+		register const UINT8 *transmap = dc_transmap;
+		register const lighttable_t *colormap = dc_colormap;
+		register INT32 heightmask = dc_texheight-1;
+		register UINT8 val;
+		if (dc_texheight & heightmask)   // not a power of 2 -- killough
+		{
+			heightmask++;
+			heightmask <<= FRACBITS;
+
+			if (frac < 0)
+				while ((frac += heightmask) <  0);
+			else
+				while (frac >= heightmask)
+					frac -= heightmask;
+
+			do
+			{
+				// Re-map color indices from wall texture column
+				//  using a lighting/special effects LUT.
+				// heightmask is the Tutti-Frutti fix
+				val = source[frac>>FRACBITS];
+
+				if (val != TRANSPARENTPIXEL)
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
+
+				dest += vid.width;
+
+				// Avoid overflow.
+				if (fracstep > 0x7FFFFFFF - frac)
+					frac += fracstep - heightmask;
+				else
+					frac += fracstep;
+
+				while (frac >= heightmask)
+					frac -= heightmask;
+			} while (--count);
+		}
+		else
+		{
+			while ((count -= 2) >= 0) // texture height is a power of 2
+			{
+				val = source[(frac>>FRACBITS) & heightmask];
+				if (val != TRANSPARENTPIXEL)
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
+				dest += vid.width;
+				frac += fracstep;
+				val = source[(frac>>FRACBITS) & heightmask];
+				if (val != TRANSPARENTPIXEL)
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
+				dest += vid.width;
+				frac += fracstep;
+			}
+			if (count & 1)
+			{
+				val = source[(frac>>FRACBITS) & heightmask];
+				if (val != TRANSPARENTPIXEL)
+					*dest = *(transmap + (colormap[val]<<8) + (*dest));
+			}
+		}
+	}
+}
+
 /**	\brief The R_DrawShadeColumn_8 function
 	Experiment to make software go faster. Taken from the Boom source
 */
@@ -212,6 +487,90 @@ void R_DrawTranslucentColumn_8(void)
 	}
 }
 
+/**	\brief The R_DrawTranslucentColumnClamped_8 function
+	Same as R_DrawTranslucentColumn_8, but prevents artifacts from showing up (caused by fixed-point imprecisions)
+*/
+void R_DrawTranslucentColumnClamped_8(void)
+{
+	INT32 count;
+	UINT8 *dest;
+	fixed_t frac, fracstep;
+
+	count = dc_yh - dc_yl + 1;
+
+	if (count <= 0) // Zero length, column does not exceed a pixel.
+		return;
+
+#ifdef RANGECHECK
+	if ((unsigned)dc_x >= (unsigned)vid.width || dc_yl < 0 || dc_yh >= vid.height)
+		I_Error("R_DrawTranslucentColumnClamped_8: %d to %d at %d", dc_yl, dc_yh, dc_x);
+#endif
+
+	dest = &topleft[dc_yl*vid.width + dc_x];
+
+	// Looks familiar.
+	fracstep = dc_iscale;
+	frac = dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep);
+
+	// Inner loop that does the actual texture mapping, e.g. a DDA-like scaling.
+	// This is as fast as it gets.
+	{
+		const UINT8 *source = dc_source;
+		const UINT8 *transmap = dc_transmap;
+		const lighttable_t *colormap = dc_colormap;
+		INT32 heightmask = dc_texheight - 1;
+		INT32 idx;
+		if (dc_texheight & heightmask)
+		{
+			heightmask++;
+			heightmask <<= FRACBITS;
+
+			if (frac < 0)
+				while ((frac += heightmask) < 0)
+					;
+			else
+				while (frac >= heightmask)
+					frac -= heightmask;
+
+			do
+			{
+				// Re-map color indices from wall texture column
+				// using a lighting/special effects LUT.
+				// heightmask is the Tutti-Frutti fix
+				idx = frac>>FRACBITS;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = *(transmap + (colormap[source[idx]]<<8) + (*dest));
+				dest += vid.width;
+				if ((frac += fracstep) >= heightmask)
+					frac -= heightmask;
+			}
+			while (--count);
+		}
+		else
+		{
+			while ((count -= 2) >= 0) // texture height is a power of 2
+			{
+				idx = (frac>>FRACBITS)&heightmask;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = *(transmap + (colormap[source[idx]]<<8) + (*dest));
+				dest += vid.width;
+				frac += fracstep;
+				idx = (frac>>FRACBITS)&heightmask;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = *(transmap + (colormap[source[idx]]<<8) + (*dest));
+				dest += vid.width;
+				frac += fracstep;
+			}
+			if (count & 1)
+			{
+				idx = (frac>>FRACBITS)&heightmask;
+				if (idx >= 0 && idx < dc_postlength)
+					*dest = *(transmap + (colormap[source[idx]]<<8) + (*dest));
+			}
+		}
+	}
+}
+
 // Hack: A cut-down copy of R_DrawTranslucentColumn_8 that does not read texture
 // data since something about calculating the texture reading address for drop shadows is broken.
 // dc_texturemid and dc_iscale get wrong values for drop shadows, however those are not strictly
@@ -942,6 +1301,136 @@ void R_DrawTiltedSplat_8(void)
 #endif
 }
 
+void R_DrawTiltedTranslucentSplat_8(void)
+{
+	// x1, x2 = ds_x1, ds_x2
+	int width = ds_x2 - ds_x1;
+	double iz, uz, vz;
+	UINT32 u, v;
+	int i;
+
+	UINT8 *source;
+	UINT8 *colormap;
+	UINT8 *dest;
+
+	UINT8 val;
+
+	double startz, startu, startv;
+	double izstep, uzstep, vzstep;
+	double endz, endu, endv;
+	UINT32 stepu, stepv;
+
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
+
+	R_CalcSlopeLight();
+
+	dest = &topleft[ds_y*vid.width + ds_x1];
+	source = ds_source;
+	//colormap = ds_colormap;
+
+#if 0	// The "perfect" reference version of this routine. Pretty slow.
+		// Use it only to see how things are supposed to look.
+	i = 0;
+	do
+	{
+		double z = 1.f/iz;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
+
+		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+
+		val = source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)];
+		if (val != TRANSPARENTPIXEL)
+			*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+
+		dest++;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
+	} while (--width >= 0);
+#else
+	startz = 1.f/iz;
+	startu = uz*startz;
+	startv = vz*startz;
+
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
+	//x1 = 0;
+	width++;
+
+	while (width >= SPANSIZE)
+	{
+		iz += izstep;
+		uz += uzstep;
+		vz += vzstep;
+
+		endz = 1.f/iz;
+		endu = uz*endz;
+		endv = vz*endz;
+		stepu = (INT64)((endu - startu) * INVSPAN);
+		stepv = (INT64)((endv - startv) * INVSPAN);
+		u = (INT64)(startu);
+		v = (INT64)(startv);
+
+		for (i = SPANSIZE-1; i >= 0; i--)
+		{
+			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+			val = source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)];
+			if (val != TRANSPARENTPIXEL)
+				*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+			dest++;
+			u += stepu;
+			v += stepv;
+		}
+		startu = endu;
+		startv = endv;
+		width -= SPANSIZE;
+	}
+	if (width > 0)
+	{
+		if (width == 1)
+		{
+			u = (INT64)(startu);
+			v = (INT64)(startv);
+			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+			val = source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)];
+			if (val != TRANSPARENTPIXEL)
+				*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+		}
+		else
+		{
+			double left = width;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
+
+			endz = 1.f/iz;
+			endu = uz*endz;
+			endv = vz*endz;
+			left = 1.f/left;
+			stepu = (INT64)((endu - startu) * left);
+			stepv = (INT64)((endv - startv) * left);
+			u = (INT64)(startu);
+			v = (INT64)(startv);
+
+			for (; width != 0; width--)
+			{
+				colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+				val = source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)];
+				if (val != TRANSPARENTPIXEL)
+					*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+				dest++;
+				u += stepu;
+				v += stepv;
+			}
+		}
+	}
+#endif
+}
+
 /**	\brief The R_DrawSplat_8 function
 	Just like R_DrawSpan_8, but skips transparent pixels.
 */
diff --git a/src/r_draw8_npo2.c b/src/r_draw8_npo2.c
index 1ed3a8fa406ac8cb7cef2c95729a3a4de611faf0..7fbb40d84956d342f1d675876f7dbcc9b2cd539b 100644
--- a/src/r_draw8_npo2.c
+++ b/src/r_draw8_npo2.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -666,6 +666,204 @@ void R_DrawTiltedSplat_NPO2_8(void)
 #endif
 }
 
+void R_DrawTiltedTranslucentSplat_NPO2_8(void)
+{
+	// x1, x2 = ds_x1, ds_x2
+	int width = ds_x2 - ds_x1;
+	double iz, uz, vz;
+	UINT32 u, v;
+	int i;
+
+	UINT8 *source;
+	UINT8 *colormap;
+	UINT8 *dest;
+
+	UINT8 val;
+
+	double startz, startu, startv;
+	double izstep, uzstep, vzstep;
+	double endz, endu, endv;
+	UINT32 stepu, stepv;
+
+	struct libdivide_u32_t x_divider = libdivide_u32_gen(ds_flatwidth);
+	struct libdivide_u32_t y_divider = libdivide_u32_gen(ds_flatheight);
+
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
+
+	R_CalcSlopeLight();
+
+	dest = &topleft[ds_y*vid.width + ds_x1];
+	source = ds_source;
+	//colormap = ds_colormap;
+
+#if 0	// The "perfect" reference version of this routine. Pretty slow.
+		// Use it only to see how things are supposed to look.
+	i = 0;
+	do
+	{
+		double z = 1.f/iz;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
+
+		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+
+		// Lactozilla: Non-powers-of-two
+		{
+			fixed_t x = (((fixed_t)u) >> FRACBITS);
+			fixed_t y = (((fixed_t)v) >> FRACBITS);
+
+			// Carefully align all of my Friends.
+			if (x < 0)
+				x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+			else
+				x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
+			if (y < 0)
+				y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+			else
+				y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
+
+			val = source[((y * ds_flatwidth) + x)];
+		}
+
+		if (val != TRANSPARENTPIXEL)
+			*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+
+		dest++;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
+	} while (--width >= 0);
+#else
+	startz = 1.f/iz;
+	startu = uz*startz;
+	startv = vz*startz;
+
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
+	//x1 = 0;
+	width++;
+
+	while (width >= SPANSIZE)
+	{
+		iz += izstep;
+		uz += uzstep;
+		vz += vzstep;
+
+		endz = 1.f/iz;
+		endu = uz*endz;
+		endv = vz*endz;
+		stepu = (INT64)((endu - startu) * INVSPAN);
+		stepv = (INT64)((endv - startv) * INVSPAN);
+		u = (INT64)(startu);
+		v = (INT64)(startv);
+
+		for (i = SPANSIZE-1; i >= 0; i--)
+		{
+			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+			// Lactozilla: Non-powers-of-two
+			{
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
+
+				// Carefully align all of my Friends.
+				if (x < 0)
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
+				if (y < 0)
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
+
+				val = source[((y * ds_flatwidth) + x)];
+			}
+			if (val != TRANSPARENTPIXEL)
+				*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+			dest++;
+			u += stepu;
+			v += stepv;
+		}
+		startu = endu;
+		startv = endv;
+		width -= SPANSIZE;
+	}
+	if (width > 0)
+	{
+		if (width == 1)
+		{
+			u = (INT64)(startu);
+			v = (INT64)(startv);
+			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+			// Lactozilla: Non-powers-of-two
+			{
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
+
+				// Carefully align all of my Friends.
+				if (x < 0)
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
+				if (y < 0)
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
+
+				val = source[((y * ds_flatwidth) + x)];
+			}
+			if (val != TRANSPARENTPIXEL)
+				*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+		}
+		else
+		{
+			double left = width;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
+
+			endz = 1.f/iz;
+			endu = uz*endz;
+			endv = vz*endz;
+			left = 1.f/left;
+			stepu = (INT64)((endu - startu) * left);
+			stepv = (INT64)((endv - startv) * left);
+			u = (INT64)(startu);
+			v = (INT64)(startv);
+
+			for (; width != 0; width--)
+			{
+				colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
+				// Lactozilla: Non-powers-of-two
+				{
+					fixed_t x = (((fixed_t)u) >> FRACBITS);
+					fixed_t y = (((fixed_t)v) >> FRACBITS);
+
+					// Carefully align all of my Friends.
+					if (x < 0)
+						x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+					else
+						x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
+					if (y < 0)
+						y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+					else
+						y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
+
+					val = source[((y * ds_flatwidth) + x)];
+				}
+				if (val != TRANSPARENTPIXEL)
+					*dest = *(ds_transmap + (colormap[val] << 8) + *dest);
+				dest++;
+				u += stepu;
+				v += stepv;
+			}
+		}
+	}
+#endif
+}
+
 /**	\brief The R_DrawSplat_NPO2_8 function
 	Just like R_DrawSpan_NPO2_8, but skips transparent pixels.
 */
diff --git a/src/r_fps.c b/src/r_fps.c
index 0773f228d6c8b6fdf266aff91f4f25e07080fee1..71f6e59a7c465690f4acbc8c48522bb31fe8e225 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 1999-2000 by Jess Haas, Nicolas Kalkhof, Colin Phipps, Florian Schulze, Andrey Budko (prboom)
-// Copyright (C) 1999-2019 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -120,6 +120,19 @@ static vector3_t *R_LerpVector3(const vector3_t *from, const vector3_t *to, fixe
 	return out;
 }
 
+static double R_LerpDouble(double from, double to, double frac)
+{
+	return from + (frac * (to - from));
+}
+
+static dvector3_t *R_LerpDVector3(const dvector3_t *from, const dvector3_t *to, double frac, dvector3_t *out)
+{
+	DVector3_Subtract(to, from, out);
+	DVector3_Multiply(out, frac, out);
+	DVector3_Add(from, out, out);
+	return out;
+}
+
 // recalc necessary stuff for mouseaiming
 // slopes are already calculated for the full possible view (which is 4*viewheight).
 // 18/08/18: (No it's actually 16*viewheight, thanks Jimita for finding this out)
@@ -497,6 +510,14 @@ void R_CreateInterpolator_DynSlope(thinker_t *thinker, pslope_t *slope)
 	FV2_Copy(&interp->dynslope.bakd, &slope->d);
 
 	interp->dynslope.oldzdelta = interp->dynslope.bakzdelta = slope->zdelta;
+
+	DVector3_Copy(&interp->dynslope.oldorigin, &slope->dorigin);
+	DVector3_Copy(&interp->dynslope.bakorigin, &slope->dorigin);
+
+	DVector3_Copy(&interp->dynslope.oldnormdir, &slope->dnormdir);
+	DVector3_Copy(&interp->dynslope.baknormdir, &slope->dnormdir);
+
+	interp->dynslope.olddzdelta = interp->dynslope.bakdzdelta = slope->dzdelta;
 }
 
 void R_InitializeLevelInterpolators(void)
@@ -561,6 +582,21 @@ static void UpdateLevelInterpolatorState(levelinterpolator_t *interp)
 		FV3_Copy(&interp->dynslope.bako, &interp->dynslope.slope->o);
 		FV2_Copy(&interp->dynslope.bakd, &interp->dynslope.slope->d);
 		interp->dynslope.bakzdelta = interp->dynslope.slope->zdelta;
+
+		DVector3_Copy(&interp->dynslope.oldorigin, &interp->dynslope.bakorigin);
+		DVector3_Copy(&interp->dynslope.oldnormdir, &interp->dynslope.baknormdir);
+		interp->dynslope.olddzdelta = interp->dynslope.bakdzdelta;
+
+		if (interp->dynslope.slope->moved)
+		{
+			P_CalculateSlopeVectors(interp->dynslope.slope);
+
+			interp->dynslope.slope->moved = false;
+		}
+
+		DVector3_Copy(&interp->dynslope.bakorigin, &interp->dynslope.slope->dorigin);
+		DVector3_Copy(&interp->dynslope.baknormdir, &interp->dynslope.slope->dnormdir);
+		interp->dynslope.bakdzdelta = interp->dynslope.slope->dzdelta;
 		break;
 	}
 }
@@ -646,7 +682,13 @@ void R_ApplyLevelInterpolators(fixed_t frac)
 			R_LerpVector3(&interp->dynslope.oldo, &interp->dynslope.bako, frac, &interp->dynslope.slope->o);
 			R_LerpVector2(&interp->dynslope.oldd, &interp->dynslope.bakd, frac, &interp->dynslope.slope->d);
 			interp->dynslope.slope->zdelta = R_LerpFixed(interp->dynslope.oldzdelta, interp->dynslope.bakzdelta, frac);
-			interp->dynslope.slope->moved = true;
+			if (rendermode == render_soft)
+			{
+				double dfrac = FixedToDouble(frac);
+				R_LerpDVector3(&interp->dynslope.oldorigin, &interp->dynslope.bakorigin, dfrac, &interp->dynslope.slope->dorigin);
+				R_LerpDVector3(&interp->dynslope.oldnormdir, &interp->dynslope.baknormdir, dfrac, &interp->dynslope.slope->dnormdir);
+				interp->dynslope.slope->dzdelta = R_LerpDouble(interp->dynslope.olddzdelta, interp->dynslope.bakdzdelta, dfrac);
+			}
 			break;
 		}
 	}
@@ -704,6 +746,10 @@ void R_RestoreLevelInterpolators(void)
 			FV3_Copy(&interp->dynslope.slope->o, &interp->dynslope.bako);
 			FV2_Copy(&interp->dynslope.slope->d, &interp->dynslope.bakd);
 			interp->dynslope.slope->zdelta = interp->dynslope.bakzdelta;
+
+			DVector3_Copy(&interp->dynslope.slope->dorigin, &interp->dynslope.bakorigin);
+			DVector3_Copy(&interp->dynslope.slope->dnormdir, &interp->dynslope.baknormdir);
+			interp->dynslope.slope->dzdelta = interp->dynslope.bakdzdelta;
 			break;
 		}
 	}
diff --git a/src/r_fps.h b/src/r_fps.h
index cd40b0a9a572b23b97bb9b2cc493ea6a34a243a7..ce64d19be442b2cc9bd2627b0787cb76538a6aa5 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 1999-2000 by Jess Haas, Nicolas Kalkhof, Colin Phipps, Florian Schulze, Andrey Budko (prboom)
-// Copyright (C) 1999-2019 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -115,6 +115,9 @@ typedef struct levelinterpolator_s {
 			vector3_t oldo, bako;
 			vector2_t oldd, bakd;
 			fixed_t oldzdelta, bakzdelta;
+			dvector3_t oldorigin, bakorigin;
+			dvector3_t oldnormdir, baknormdir;
+			double olddzdelta, bakdzdelta;
 		} dynslope;
 	};
 } levelinterpolator_t;
diff --git a/src/r_main.c b/src/r_main.c
index 4624392944a931b4178f84038de2982f9ff371cb..3de5cb1514cfa01aed8567364d749499a58a9f11 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -161,7 +161,7 @@ consvar_t cv_translucency = CVAR_INIT ("translucency", "On", CV_SAVE, CV_OnOff,
 consvar_t cv_drawdist = CVAR_INIT ("drawdist", "Infinite", CV_SAVE, drawdist_cons_t, NULL);
 consvar_t cv_drawdist_nights = CVAR_INIT ("drawdist_nights", "2048", CV_SAVE, drawdist_cons_t, NULL);
 consvar_t cv_drawdist_precip = CVAR_INIT ("drawdist_precip", "1024", CV_SAVE, drawdist_precip_cons_t, NULL);
-consvar_t cv_fov = CVAR_INIT ("fov", "90", CV_SAVE|CV_FLOAT|CV_CALL, fov_cons_t, Fov_OnChange);
+consvar_t cv_fov = CVAR_INIT ("fov", "100", CV_SAVE|CV_FLOAT|CV_CALL, fov_cons_t, Fov_OnChange);
 consvar_t cv_fovchange = CVAR_INIT ("fovchange", "Off", CV_SAVE, CV_OnOff, NULL);
 consvar_t cv_maxportals = CVAR_INIT ("maxportals", "2", CV_SAVE, maxportals_cons_t, NULL);
 
@@ -390,22 +390,22 @@ fixed_t R_PointToDist(fixed_t x, fixed_t y)
 	return R_PointToDist2(viewx, viewy, x, y);
 }
 
-line_t *R_GetFFloorLine(const seg_t *seg, const ffloor_t *pfloor)
+line_t *R_GetFFloorLine(const line_t *line, const ffloor_t *pfloor, const sector_t *sector)
 {
 	if (pfloor->master->flags & ML_TFERLINE)
 	{
-		size_t linenum = seg->linedef - pfloor->target->lines[0];
+		size_t linenum = min((size_t)(line - sector->lines[0]), pfloor->master->frontsector->linecount);
 		return pfloor->master->frontsector->lines[0] + linenum;
 	}
 	else
 		return pfloor->master;
 }
 
-side_t *R_GetFFloorSide(const seg_t *seg, const ffloor_t *pfloor)
+side_t *R_GetFFloorSide(const line_t *line, const ffloor_t *pfloor, const sector_t *sector)
 {
 	if (pfloor->master->flags & ML_TFERLINE)
 	{
-		line_t *newline = R_GetFFloorLine(seg, pfloor);
+		line_t *newline = R_GetFFloorLine(line, pfloor, sector);
 		return &sides[newline->sidenum[0]];
 	}
 	else
@@ -1095,7 +1095,7 @@ void R_SetupFrame(player_t *player)
 	camera_t *thiscam;
 	boolean chasecam = R_ViewpointHasChasecam(player);
 	boolean ispaused = paused || P_AutoPause();
-	
+
 	if (splitscreen && player == &players[secondarydisplayplayer] && player != &players[consoleplayer])
 		thiscam = &camera2;
 	else
@@ -1152,8 +1152,14 @@ void R_SetupFrame(player_t *player)
 
 		if (quake.epicenter) {
 			// Calculate 3D distance from epicenter, using the camera.
-			fixed_t xydist = R_PointToDist2(thiscam->x, thiscam->y, quake.epicenter->x, quake.epicenter->y);
-			fixed_t dist = R_PointToDist2(0, thiscam->z, xydist, quake.epicenter->z);
+			fixed_t xydist, dist;
+			if (P_MobjWasRemoved(r_viewmobj)) {
+				xydist = R_PointToDist2(thiscam->x, thiscam->y, quake.epicenter->x, quake.epicenter->y);
+				dist = R_PointToDist2(0, thiscam->z, xydist, quake.epicenter->z);
+			} else {
+				xydist = R_PointToDist2(r_viewmobj->x, r_viewmobj->y, quake.epicenter->x, quake.epicenter->y);
+				dist = R_PointToDist2(0, r_viewmobj->z, xydist, quake.epicenter->z);
+			}
 
 			// More effect closer to epicenter, outside of radius = no effect
 			if (!quake.radius || dist > quake.radius)
@@ -1369,7 +1375,7 @@ boolean R_ViewpointHasChasecam(player_t *player)
 		chasecam = true; // force chasecam on
 	else if (player->spectator) // no spectator chasecam
 		chasecam = false; // force chasecam off
-		
+
 	if (chasecam && !thiscam->chase)
 	{
 		P_ResetCamera(player, thiscam);
@@ -1380,7 +1386,7 @@ boolean R_ViewpointHasChasecam(player_t *player)
 		P_ResetCamera(player, thiscam);
 		thiscam->chase = false;
 	}
-	
+
 	if (isplayer2)
 	{
 		R_SetViewContext(VIEWCONTEXT_PLAYER2);
@@ -1428,6 +1434,9 @@ static void R_PortalFrame(portal_t *portal)
 	viewsin = FINESINE(viewangle>>ANGLETOFINESHIFT);
 	viewcos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
 
+	if (!P_MobjWasRemoved(portal->viewmobj))
+		r_viewmobj = portal->viewmobj;
+
 	portalclipstart = portal->start;
 	portalclipend = portal->end;
 
diff --git a/src/r_main.h b/src/r_main.h
index aedfeb6ad0f2ca2a0caaba771f38e44ad29e1142..af7086e834a9e902da7f32e4413e64433c89bd0a 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -86,8 +86,8 @@ subsector_t *R_PointInSubsectorOrNull(fixed_t x, fixed_t y);
 
 boolean R_DoCulling(line_t *cullheight, line_t *viewcullheight, fixed_t vz, fixed_t bottomh, fixed_t toph);
 
-line_t *R_GetFFloorLine(const seg_t *seg, const ffloor_t *pfloor);
-side_t *R_GetFFloorSide(const seg_t *seg, const ffloor_t *pfloor);
+line_t *R_GetFFloorLine(const line_t *line, const ffloor_t *pfloor, const sector_t *sector);
+side_t *R_GetFFloorSide(const line_t *line, const ffloor_t *pfloor, const sector_t *sector);
 
 // Render stats
 
diff --git a/src/r_patch.c b/src/r_patch.c
index a6a9dc2ebcc0651c26f6faaede8bf7535f607c74..19e66404f1da18e82f5ec4076e5b61b83184feea 100644
--- a/src/r_patch.c
+++ b/src/r_patch.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2023 by Lactozilla.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_patch.h b/src/r_patch.h
index f1a018c035a322a2a57f926e161fe97b66be5c2f..ff76254e84a5e204b75e3b2a1d83c69532ee4a31 100644
--- a/src/r_patch.h
+++ b/src/r_patch.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2024 by Lactozilla.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_patchrotation.c b/src/r_patchrotation.c
index b9106984970f728e83e567627daa4c1b9c478011..7c4f6257eeb029ccb3a0fa30b9a44222fe1a7958 100644
--- a/src/r_patchrotation.c
+++ b/src/r_patchrotation.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2023 by Lactozilla.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -54,12 +54,26 @@ INT32 R_GetRollAngle(angle_t rollangle)
 patch_t *Patch_GetRotated(patch_t *patch, INT32 angle, boolean flip)
 {
 	rotsprite_t *rotsprite = patch->rotated;
-	if (rotsprite == NULL || angle < 1 || angle >= ROTANGLES)
+	if (angle < 1 || angle >= ROTANGLES)
 		return NULL;
 
+	if (rotsprite == NULL)
+	{
+		rotsprite = RotatedPatch_Create(ROTANGLES);
+		patch->rotated = rotsprite;
+	}
+
 	if (flip)
 		angle += rotsprite->angles;
 
+	if (rotsprite->patches[angle] == NULL)
+	{
+		INT32 xpivot = 0, ypivot = 0;
+		xpivot = patch->leftoffset;
+		ypivot = patch->topoffset;
+		RotatedPatch_DoRotation(rotsprite, patch, angle, xpivot, ypivot, flip);
+	}
+
 	return rotsprite->patches[angle];
 }
 
diff --git a/src/r_patchrotation.h b/src/r_patchrotation.h
index a239ac5fa4dfb4220e3aeda3faebe2781e58b7ff..184d7ac13b5f7987cd63976d127165836e59b5b4 100644
--- a/src/r_patchrotation.h
+++ b/src/r_patchrotation.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020-2023 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2023 by Lactozilla.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_picformats.c b/src/r_picformats.c
index d71657021b6e2cc91efe504b49286f459461095d..a45c143b01698de2abbb48f534f543a3a68d845e 100644
--- a/src/r_picformats.c
+++ b/src/r_picformats.c
@@ -2,8 +2,8 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 2005-2009 by Andrey "entryway" Budko.
-// Copyright (C) 2018-2023 by Jaime "Lactozilla" Passos.
-// Copyright (C) 2019-2023 by Sonic Team Junior.
+// Copyright (C) 2018-2024 by Lactozilla.
+// Copyright (C) 2019-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_picformats.h b/src/r_picformats.h
index 098f927a5619d74ca9813d37658d5bfb75bc8f04..123dda976c204787505fb1a90bad859fe39b836b 100644
--- a/src/r_picformats.h
+++ b/src/r_picformats.h
@@ -1,8 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
-// Copyright (C) 2018-2023 by Jaime "Lactozilla" Passos.
-// Copyright (C) 2019-2023 by Sonic Team Junior.
+// Copyright (C) 2018-2024 by Lactozilla.
+// Copyright (C) 2019-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_plane.c b/src/r_plane.c
index 33e3aac1349faebb317d47c1f30ecec5d94a55ce..053cfe921fb16fc94c4bc14755dcf4305f3911dd 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -83,7 +83,7 @@ static fixed_t planeheight;
 fixed_t yslopetab[MAXVIDHEIGHT*16];
 fixed_t *yslope;
 
-static fixed_t xoffs, yoffs;
+static INT64 xoffs, yoffs;
 static dvector3_t slope_origin, slope_u, slope_v;
 static dvector3_t slope_lightu, slope_lightv;
 
@@ -182,8 +182,8 @@ static void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 
 		R_CalculatePlaneRipple(currentplane->viewangle + currentplane->plangle);
 
-		ds_xfrac += planeripple.xfrac;
-		ds_yfrac += planeripple.yfrac;
+		ds_xfrac += FixedMul(planeripple.xfrac, currentplane->xscale);
+		ds_yfrac += FixedMul(planeripple.yfrac, currentplane->yscale);
 		ds_bgofs >>= FRACBITS;
 
 		if ((y + ds_bgofs) >= viewheight)
@@ -376,19 +376,25 @@ visplane_t *R_FindPlane(sector_t *sector, fixed_t height, INT32 picnum, INT32 li
 	visplane_t *check;
 	unsigned hash;
 
+	float offset_xd = FixedToFloat(xoff) / FixedToFloat(xscale ? xscale : 1);
+	float offset_yd = FixedToFloat(yoff) / FixedToFloat(yscale ? yscale : 1);
+
+	INT64 offset_x = offset_xd * FRACUNIT;
+	INT64 offset_y = offset_yd * FRACUNIT;
+
 	if (!slope) // Don't mess with this right now if a slope is involved
 	{
-		xoff += FixedMul(viewx, xscale);
-		yoff -= FixedMul(viewy, yscale);
+		offset_x += viewx;
+		offset_y -= viewy;
 
 		if (plangle != 0)
 		{
 			// Add the view offset, rotated by the plane angle.
 			float ang = ANG2RAD(plangle);
-			float x = FixedToFloat(xoff);
-			float y = FixedToFloat(yoff);
-			xoff = FloatToFixed(x * cos(ang) + y * sin(ang));
-			yoff = FloatToFixed(-x * sin(ang) + y * cos(ang));
+			float x = offset_x / (float)FRACUNIT;
+			float y = offset_y / (float)FRACUNIT;
+			offset_x = (x * cos(ang) + y * sin(ang)) * FRACUNIT;
+			offset_y = (-x * sin(ang) + y * cos(ang)) * FRACUNIT;
 		}
 	}
 
@@ -399,16 +405,19 @@ visplane_t *R_FindPlane(sector_t *sector, fixed_t height, INT32 picnum, INT32 li
 			float ang = ANG2RAD(polyobj->angle);
 			float x = FixedToFloat(polyobj->centerPt.x);
 			float y = FixedToFloat(polyobj->centerPt.y);
-			xoff -= FloatToFixed(x * cos(ang) + y * sin(ang));
-			yoff -= FloatToFixed(x * sin(ang) - y * cos(ang));
+			offset_x -= (x * cos(ang) + y * sin(ang)) * FRACUNIT;
+			offset_y -= (x * sin(ang) - y * cos(ang)) * FRACUNIT;
 		}
 		else
 		{
-			xoff -= polyobj->centerPt.x;
-			yoff += polyobj->centerPt.y;
+			offset_x -= polyobj->centerPt.x;
+			offset_y += polyobj->centerPt.y;
 		}
 	}
 
+	offset_x = ((INT64)offset_x * xscale) / FRACUNIT;
+	offset_y = ((INT64)offset_y * yscale) / FRACUNIT;
+
 	// This appears to fix the Nimbus Ruins sky bug.
 	if (picnum == skyflatnum && pfloor)
 	{
@@ -423,7 +432,7 @@ visplane_t *R_FindPlane(sector_t *sector, fixed_t height, INT32 picnum, INT32 li
 		{
 			if (height == check->height && picnum == check->picnum
 				&& lightlevel == check->lightlevel
-				&& xoff == check->xoffs && yoff == check->yoffs
+				&& offset_x == check->xoffs && offset_y == check->yoffs
 				&& xscale == check->xscale && yscale == check->yscale
 				&& planecolormap == check->extra_colormap
 				&& check->viewx == viewx && check->viewy == viewy && check->viewz == viewz
@@ -449,8 +458,8 @@ visplane_t *R_FindPlane(sector_t *sector, fixed_t height, INT32 picnum, INT32 li
 	check->lightlevel = lightlevel;
 	check->minx = vid.width;
 	check->maxx = -1;
-	check->xoffs = xoff;
-	check->yoffs = yoff;
+	check->xoffs = offset_x;
+	check->yoffs = offset_y;
 	check->xscale = xscale;
 	check->yscale = yscale;
 	check->extra_colormap = planecolormap;
@@ -658,13 +667,13 @@ static void R_DrawSkyPlane(visplane_t *pl)
 }
 
 // Returns the height of the sloped plane at (x, y) as a double
-static double R_GetSlopeZAt(const pslope_t *slope, fixed_t x, fixed_t y)
+static double R_GetSlopeZAt(const pslope_t *slope, INT64 x, INT64 y)
 {
 	// If you want to reimplement this using just the equation constants, use this instead:
 	// (d + a*x + b*y) * -(1.0 / c)
 
-	double px = FixedToDouble(x) - slope->dorigin.x;
-	double py = FixedToDouble(y) - slope->dorigin.y;
+	double px = (x / (double)FRACUNIT) - slope->dorigin.x;
+	double py = (y / (double)FRACUNIT) - slope->dorigin.y;
 
 	double dist = (px * slope->dnormdir.x) + (py * slope->dnormdir.y);
 
@@ -672,10 +681,10 @@ static double R_GetSlopeZAt(const pslope_t *slope, fixed_t x, fixed_t y)
 }
 
 // Sets the texture origin vector of the sloped plane.
-static void R_SetSlopePlaneOrigin(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xoff, fixed_t yoff, fixed_t angle)
+static void R_SetSlopePlaneOrigin(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, INT64 xoff, INT64 yoff, fixed_t angle)
 {
-	INT64 vx = (INT64)xpos + (INT64)xoff;
-	INT64 vy = (INT64)ypos - (INT64)yoff;
+	INT64 vx = (INT64)xpos + xoff;
+	INT64 vy = (INT64)ypos - yoff;
 
 	float vxf = vx / (float)FRACUNIT;
 	float vyf = vy / (float)FRACUNIT;
@@ -702,7 +711,7 @@ void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos,
 		slope->moved = false;
 	}
 
-	R_SetSlopePlaneOrigin(slope, xpos, ypos, zpos, xoff, yoff, angle);
+	R_SetSlopePlaneOrigin(slope, xpos, ypos, zpos, (INT64)xoff, (INT64)yoff, angle);
 	height = R_GetSlopeZAt(slope, xpos, ypos);
 	zeroheight = height - FixedToDouble(zpos);
 
@@ -735,7 +744,7 @@ void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos,
 }
 
 // This function calculates all of the vectors necessary for drawing a sloped and scaled plane.
-void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xs, fixed_t ys, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle)
+void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xs, fixed_t ys, INT64 xoff, INT64 yoff, angle_t angle, angle_t plangle)
 {
 	double height, z_at_xy;
 	float ang;
@@ -836,9 +845,11 @@ static void CalcSlopePlaneVectors(visplane_t *pl, fixed_t xoff, fixed_t yoff)
 {
 	if (!ds_fog && (pl->xscale != FRACUNIT || pl->yscale != FRACUNIT))
 	{
+		float offset_x = FixedToFloat(xoff) / FixedToFloat(pl->xscale ? pl->xscale : 1);
+		float offset_y = FixedToFloat(yoff) / FixedToFloat(pl->yscale ? pl->yscale : 1);
 		R_SetScaledSlopePlane(pl->slope, pl->viewx, pl->viewy, pl->viewz,
 			FixedDiv(FRACUNIT, pl->xscale), FixedDiv(FRACUNIT, pl->yscale),
-			FixedDiv(xoff, pl->xscale), FixedDiv(yoff, pl->yscale), pl->viewangle, pl->plangle);
+			(INT64)(offset_x * FRACUNIT), (INT64)(offset_y * FRACUNIT), pl->viewangle, pl->plangle);
 	}
 	else
 		R_SetSlopePlane(pl->slope, pl->viewx, pl->viewy, pl->viewz, xoff, yoff, pl->viewangle, pl->plangle);
@@ -910,7 +921,7 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 		if (pl->polyobj->translucency == 0 || (pl->extra_colormap && (pl->extra_colormap->flags & CMF_FOG)))
 			light = (pl->lightlevel >> LIGHTSEGSHIFT);
-		else
+		else // TODO: 2.3: Make transparent polyobject planes always use light level
 			light = LIGHTLEVELS-1;
 	}
 	else
@@ -952,7 +963,7 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 				if ((spanfunctype == SPANDRAWFUNC_SPLAT) || (pl->extra_colormap && (pl->extra_colormap->flags & CMF_FOG)))
 					light = (pl->lightlevel >> LIGHTSEGSHIFT);
-				else
+				else // TODO: 2.3: Make transparent FOF planes use light level instead of always being fullbright
 					light = LIGHTLEVELS-1;
 			}
 			else if (pl->ffloor->fofflags & FOF_FOG)
@@ -1079,6 +1090,9 @@ void R_DrawSinglePlane(visplane_t *pl)
 			case SPANDRAWFUNC_SPLAT:
 				spanfunctype = SPANDRAWFUNC_TILTEDSPLAT;
 				break;
+			case SPANDRAWFUNC_TRANSSPLAT:
+				spanfunctype = SPANDRAWFUNC_TILTEDTRANSSPLAT;
+				break;
 			case SPANDRAWFUNC_SOLID:
 				spanfunctype = SPANDRAWFUNC_TILTEDSOLID;
 				break;
diff --git a/src/r_plane.h b/src/r_plane.h
index 69620f25e07cadefee8242f23ad3a26a57ffbc9d..cd947750119b8446e0683bd4d62ae7b88fa9c1a7 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -48,7 +48,7 @@ typedef struct visplane_s
 	UINT16 padbottomstart, bottom[MAXVIDWIDTH], padbottomend;
 	INT32 high, low; // R_PlaneBounds should set these.
 
-	fixed_t xoffs, yoffs; // Scrolling flats.
+	INT64 xoffs, yoffs; // Scrolling flats.
 	fixed_t xscale, yscale;
 
 	sector_t *sector;
@@ -85,7 +85,7 @@ void R_DrawSinglePlane(visplane_t *pl);
 
 // Calculates the slope vectors needed for tilted span drawing.
 void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle);
-void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xs, fixed_t ys, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle);
+void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xs, fixed_t ys, INT64 xoff, INT64 yoff, angle_t angle, angle_t plangle);
 
 typedef struct planemgr_s
 {
diff --git a/src/r_portal.c b/src/r_portal.c
index 4d042cae38574bd54165d5b5850e8e18c5f31f33..957d574b2e9ebc3505a459adfee51c08270066ac 100644
--- a/src/r_portal.c
+++ b/src/r_portal.c
@@ -101,7 +101,7 @@ void Portal_ClipApply (const portal_t* portal)
 
 static portal_t* Portal_Add (const INT16 x1, const INT16 x2)
 {
-	portal_t *portal		= Z_Malloc(sizeof(portal_t), PU_LEVEL, NULL);
+	portal_t *portal		= Z_Calloc(sizeof(portal_t), PU_LEVEL, NULL);
 	INT16 *ceilingclipsave	= Z_Malloc(sizeof(INT16)*(x2-x1 + 1), PU_LEVEL, NULL);
 	INT16 *floorclipsave	= Z_Malloc(sizeof(INT16)*(x2-x1 + 1), PU_LEVEL, NULL);
 	fixed_t *frontscalesave	= Z_Malloc(sizeof(fixed_t)*(x2-x1 + 1), PU_LEVEL, NULL);
@@ -117,7 +117,7 @@ static portal_t* Portal_Add (const INT16 x1, const INT16 x2)
 		portal_cap->next = portal;
 		portal_cap = portal;
 	}
-	portal->next = NULL;
+	portal->clipline = -1;
 
 	// Store clipping values so they can be restored once the portal is rendered.
 	portal->ceilingclip	= ceilingclipsave;
@@ -142,11 +142,9 @@ void Portal_Remove (portal_t* portal)
 	Z_Free(portal);
 }
 
-static void Portal_GetViewpointForLine(portal_t *portal, line_t *start, line_t *dest)
+static void Portal_GetViewpointForLine(portal_t *portal, line_t *start, line_t *dest, angle_t dangle)
 {
 	// Offset the portal view by the linedef centers
-	angle_t dangle = R_PointToAngle2(0,0,dest->dx,dest->dy) - R_PointToAngle2(start->dx,start->dy,0,0);
-
 	fixed_t disttopoint;
 	angle_t angtopoint;
 
@@ -168,7 +166,6 @@ static void Portal_GetViewpointForLine(portal_t *portal, line_t *start, line_t *
 
 	portal->viewx = dest_c.x + FixedMul(FINECOSINE(angtopoint>>ANGLETOFINESHIFT), disttopoint);
 	portal->viewy = dest_c.y + FixedMul(FINESINE(angtopoint>>ANGLETOFINESHIFT), disttopoint);
-	portal->viewz = viewz + dest->frontsector->floorheight - start->frontsector->floorheight;
 	portal->viewangle = viewangle + dangle;
 }
 
@@ -189,12 +186,13 @@ void Portal_Add2Lines (const INT32 line1, const INT32 line2, const INT32 x1, con
 	line_t* start	= &lines[line1];
 	line_t* dest	= &lines[line2];
 
-	Portal_GetViewpointForLine(portal, start, dest);
+	angle_t dangle = R_PointToAngle2(0,0,dest->dx,dest->dy) - R_PointToAngle2(start->dx,start->dy,0,0);
+
+	Portal_GetViewpointForLine(portal, start, dest, dangle);
+
+	portal->viewz = viewz + dest->frontsector->floorheight - start->frontsector->floorheight;
 
 	portal->clipline = line2;
-	portal->is_skybox = false;
-	portal->is_horizon = false;
-	portal->horizon_sector = NULL;
 
 	Portal_ClipRange(portal);
 
@@ -317,10 +315,7 @@ static boolean Portal_AddSkybox (const visplane_t* plane)
 
 	Portal_ClipVisplane(plane, portal);
 
-	portal->clipline = -1;
 	portal->is_skybox = true;
-	portal->is_horizon = false;
-	portal->horizon_sector = NULL;
 
 	Portal_GetViewpointForSkybox(portal);
 
@@ -332,14 +327,28 @@ static void Portal_GetViewpointForSecPortal(portal_t *portal, sectorportal_t *se
 	fixed_t x, y, z;
 	angle_t angle;
 
+	sector_t *target = secportal->target;
+
+	fixed_t target_x = target->soundorg.x;
+	fixed_t target_y = target->soundorg.y;
+	fixed_t target_z;
+
+	if (secportal->ceiling)
+		target_z = P_GetSectorCeilingZAt(target, target_x, target_y);
+	else
+		target_z = P_GetSectorFloorZAt(target, target_x, target_y);
+
 	switch (secportal->type)
 	{
 	case SECPORTAL_LINE:
-		Portal_GetViewpointForLine(portal, secportal->line.start, secportal->line.dest);
+		angle = secportal->line.dest->angle - secportal->line.start->angle;
+		Portal_GetViewpointForLine(portal, secportal->line.start, secportal->line.dest, angle);
+		portal->viewz = viewz; // Apparently it just works like that. Not going to question it.
 		return;
 	case SECPORTAL_OBJECT:
-		if (!secportal->mobj || P_MobjWasRemoved(secportal->mobj))
+		if (P_MobjWasRemoved(secportal->mobj))
 			return;
+		portal->viewmobj = secportal->mobj;
 		x = secportal->mobj->x;
 		y = secportal->mobj->y;
 		z = secportal->mobj->z;
@@ -373,8 +382,9 @@ static void Portal_GetViewpointForSecPortal(portal_t *portal, sectorportal_t *se
 		return;
 	}
 
-	fixed_t refx = secportal->origin.x - viewx;
-	fixed_t refy = secportal->origin.y - viewy;
+	fixed_t refx = target_x - viewx;
+	fixed_t refy = target_y - viewy;
+	fixed_t refz = target_z - viewz;
 
 	// Rotate the X/Y to match the target angle
 	if (angle != 0)
@@ -387,7 +397,7 @@ static void Portal_GetViewpointForSecPortal(portal_t *portal, sectorportal_t *se
 
 	portal->viewx = x - refx;
 	portal->viewy = y - refy;
-	portal->viewz = z + viewz;
+	portal->viewz = z - refz;
 	portal->viewangle = angle + viewangle;
 }
 
@@ -413,11 +423,6 @@ static boolean Portal_AddSectorPortal (const visplane_t* plane)
 
 	Portal_ClipVisplane(plane, portal);
 
-	portal->clipline = -1;
-	portal->is_horizon = false;
-	portal->is_skybox = false;
-	portal->horizon_sector = NULL;
-
 	Portal_GetViewpointForSecPortal(portal, secportal);
 
 	return true;
@@ -425,7 +430,7 @@ static boolean Portal_AddSectorPortal (const visplane_t* plane)
 
 /** Creates a transferred sector portal.
  */
-void Portal_AddTransferred (UINT32 secportalnum, const INT32 x1, const INT32 x2)
+void Portal_AddTransferred (const UINT32 secportalnum, const INT32 x1, const INT32 x2)
 {
 	if (secportalnum >= secportalcount)
 		return;
@@ -435,9 +440,6 @@ void Portal_AddTransferred (UINT32 secportalnum, const INT32 x1, const INT32 x2)
 		return;
 
 	portal_t* portal = Portal_Add(x1, x2);
-	portal->is_skybox = false;
-	portal->is_horizon = false;
-	portal->horizon_sector = NULL;
 
 	if (secportal->type == SECPORTAL_SKYBOX)
 		Portal_GetViewpointForSkybox(portal);
diff --git a/src/r_portal.h b/src/r_portal.h
index 2485e45a71afb5bc189525f140527c5bb03e2a21..5190885b7189c38b1b7b1b7b13f0519f1d455b0c 100644
--- a/src/r_portal.h
+++ b/src/r_portal.h
@@ -36,6 +36,8 @@ typedef struct portal_s
 
 	boolean is_skybox;
 
+	mobj_t *viewmobj;
+
 	UINT8 pass;			/**< Keeps track of the portal's recursion depth. */
 	INT32 clipline;		/**< Optional clipline for line-based portals. */
 
@@ -58,7 +60,7 @@ extern INT32 portalclipstart, portalclipend;
 void Portal_InitList		(void);
 void Portal_Remove			(portal_t* portal);
 void Portal_Add2Lines		(const INT32 line1, const INT32 line2, const INT32 x1, const INT32 x2);
-void Portal_AddTransferred	(UINT32 secportalnum, const INT32 x1, const INT32 x2);
+void Portal_AddTransferred	(const UINT32 secportalnum, const INT32 x1, const INT32 x2);
 
 void Portal_ClipRange (portal_t* portal);
 void Portal_ClipApply (const portal_t* portal);
diff --git a/src/r_segs.c b/src/r_segs.c
index e07ced86a7c6002c7bd127c1e2867bdae5386419..4939d1fc8924af906c2aeff2f260e16fc06d8937 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -94,6 +94,124 @@ transnum_t R_GetLinedefTransTable(fixed_t alpha)
 	return (20*(FRACUNIT - alpha - 1) + FRACUNIT) >> (FRACBITS+1);
 }
 
+static UINT8 R_SideLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light +
+		((side->lightabsolute) ? 0 : base_lightlevel)));
+}
+
+/* TODO: implement per-texture lighting
+static UINT8 R_TopLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light_top +
+		((side->lightabsolute_top) ? 0 : R_SideLightLevel(side, base_lightlevel))));
+}
+
+static UINT8 R_MidLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light_mid +
+		((side->lightabsolute_mid) ? 0 : R_SideLightLevel(side, base_lightlevel))));
+}
+
+static UINT8 R_BottomLightLevel(side_t *side, INT16 base_lightlevel)
+{
+	return max(0, min(255, side->light_bottom +
+		((side->lightabsolute_bottom) ? 0 : R_SideLightLevel(side, base_lightlevel))));
+}
+*/
+
+// If we have a multi-patch texture on a 2sided wall (rare) then we draw
+//  it using R_DrawColumn, else we draw it using R_DrawMaskedColumn, this
+//  way we don't have to store extra post_t info with each column for
+//  multi-patch textures. They are not normally needed as multi-patch
+//  textures don't have holes in it. At least not for now.
+static void R_Render2sidedMultiPatchColumn(column_t *column, unsigned lengthcol)
+{
+	INT32 topscreen, bottomscreen;
+
+	post_t *post = &column->posts[0];
+	if (!post->length)
+		return;
+
+	topscreen = sprtopscreen;
+	bottomscreen = topscreen + spryscale * lengthcol;
+
+	dc_yl = (sprtopscreen+FRACUNIT-1)>>FRACBITS;
+	dc_yh = (bottomscreen-1)>>FRACBITS;
+
+	if (windowtop != INT32_MAX && windowbottom != INT32_MAX)
+	{
+		dc_yl = ((windowtop + FRACUNIT)>>FRACBITS);
+		dc_yh = (windowbottom - 1)>>FRACBITS;
+	}
+
+	if (dc_yh >= mfloorclip[dc_x])
+		dc_yh =  mfloorclip[dc_x] - 1;
+	if (dc_yl <= mceilingclip[dc_x])
+		dc_yl =  mceilingclip[dc_x] + 1;
+
+	if (dc_yl >= vid.height || dc_yh < 0)
+		return;
+
+	if (dc_yl <= dc_yh && dc_yh < vid.height && dc_yh > 0)
+	{
+		dc_source = column->pixels + post->data_offset;
+		dc_postlength = post->length;
+
+		if (colfunc == colfuncs[BASEDRAWFUNC])
+			(colfuncs[COLDRAWFUNC_TWOSMULTIPATCH])();
+		else if (colfunc == colfuncs[COLDRAWFUNC_FUZZY])
+			(colfuncs[COLDRAWFUNC_TWOSMULTIPATCHTRANS])();
+		else
+			colfunc();
+	}
+}
+
+static void R_RenderFlipped2sidedMultiPatchColumn(column_t *column, unsigned lengthcol)
+{
+	INT32 topscreen, bottomscreen;
+
+	void (*localcolfunc)(void);
+
+	post_t *post = &column->posts[0];
+	if (!post->length)
+		return;
+
+	topscreen = sprtopscreen;
+	bottomscreen = topscreen + spryscale * lengthcol;
+
+	dc_yl = (sprtopscreen+FRACUNIT-1)>>FRACBITS;
+	dc_yh = (bottomscreen-1)>>FRACBITS;
+
+	if (windowtop != INT32_MAX && windowbottom != INT32_MAX)
+	{
+		dc_yl = ((windowtop + FRACUNIT)>>FRACBITS);
+		dc_yh = (windowbottom - 1)>>FRACBITS;
+	}
+
+	if (dc_yh >= mfloorclip[dc_x])
+		dc_yh =  mfloorclip[dc_x] - 1;
+	if (dc_yl <= mceilingclip[dc_x])
+		dc_yl =  mceilingclip[dc_x] + 1;
+
+	if (dc_yl >= vid.height || dc_yh < 0)
+		return;
+
+	if (dc_yl <= dc_yh && dc_yh < vid.height && dc_yh > 0)
+	{
+		dc_postlength = post->length;
+
+		if (colfunc == colfuncs[BASEDRAWFUNC])
+			localcolfunc = colfuncs[COLDRAWFUNC_TWOSMULTIPATCH];
+		else if (colfunc == colfuncs[COLDRAWFUNC_FUZZY])
+			localcolfunc = colfuncs[COLDRAWFUNC_TWOSMULTIPATCHTRANS];
+		else
+			localcolfunc = colfunc;
+
+		R_DrawFlippedPost(column->pixels + post->data_offset, post->length, localcolfunc);
+	}
+}
+
 void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 {
 	size_t pindex;
@@ -181,7 +299,16 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 	// Texture must be cached
 	R_CheckTextureCache(texnum);
 
-	if (vertflip) // vertically flipped?
+	// handle case where multipatch texture is drawn on a 2sided wall, multi-patch textures
+	// are not stored per-column with post info in SRB2
+	if (!textures[texnum]->transparency)
+	{
+		if (vertflip) // vertically flipped?
+			colfunc_2s = R_RenderFlipped2sidedMultiPatchColumn;
+		else
+			colfunc_2s = R_Render2sidedMultiPatchColumn;
+	}
+	else if (vertflip) // vertically flipped?
 		colfunc_2s = R_DrawFlippedMaskedColumn;
 	else
 		colfunc_2s = R_DrawMaskedColumn; // render the usual 2sided single-patch packed texture
@@ -223,7 +350,7 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 			if ((colfunc != colfuncs[COLDRAWFUNC_FUZZY])
 				|| (rlight->flags & FOF_FOG)
 				|| (rlight->extra_colormap && (rlight->extra_colormap->flags & CMF_FOG)))
-				lightnum = (rlight->lightlevel >> LIGHTSEGSHIFT);
+				lightnum = R_SideLightLevel(curline->sidedef, rlight->lightlevel) >> LIGHTSEGSHIFT;
 			else
 				lightnum = LIGHTLEVELS - 1;
 
@@ -241,7 +368,7 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 	{
 		if ((colfunc != colfuncs[COLDRAWFUNC_FUZZY])
 			|| (frontsector->extra_colormap && (frontsector->extra_colormap->flags & CMF_FOG)))
-			lightnum = (frontsector->lightlevel >> LIGHTSEGSHIFT);
+			lightnum = R_SideLightLevel(curline->sidedef, frontsector->lightlevel) >> LIGHTSEGSHIFT;
 		else
 			lightnum = LIGHTLEVELS - 1;
 
@@ -442,26 +569,75 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 }
 
 // Loop through R_DrawMaskedColumn calls
+static fixed_t repeatscroll = 0;
+
 static void R_DrawRepeatMaskedColumn(column_t *col, unsigned lengthcol)
 {
-	while (sprtopscreen < sprbotscreen) {
+	fixed_t topscreen = sprtopscreen;
+	fixed_t bottomscreen = sprbotscreen;
+
+	fixed_t texheight = dc_texheight*spryscale;
+
+	fixed_t scroll = -repeatscroll;
+	if (scroll < 0)
+	{
+		scroll = -FixedMul((abs(scroll) % (dc_texheight*FRACUNIT)), spryscale);
+		bottomscreen += texheight; // Draw an extra time
+	}
+	else if (scroll)
+	{
+		scroll = FixedMul(scroll % (dc_texheight*FRACUNIT), spryscale);
+		topscreen -= texheight; // Draw an extra time
+	}
+
+	while (topscreen < bottomscreen)
+	{
+		sprtopscreen = topscreen + scroll;
+
 		R_DrawMaskedColumn(col, lengthcol);
+
 		if ((INT64)sprtopscreen + (INT64)dc_texheight*spryscale > (INT64)INT32_MAX) // prevent overflow
-			sprtopscreen = INT32_MAX;
-		else
-			sprtopscreen += dc_texheight*spryscale;
+			break;
+
+		topscreen += texheight;
 	}
+
+	sprtopscreen = sprbotscreen;
 }
 
 static void R_DrawRepeatFlippedMaskedColumn(column_t *col, unsigned lengthcol)
 {
-	while (sprtopscreen < sprbotscreen) {
+	fixed_t topscreen = sprtopscreen;
+	fixed_t bottomscreen = sprbotscreen;
+
+	fixed_t texheight = dc_texheight*spryscale;
+
+	fixed_t scroll = -repeatscroll;
+	if (scroll < 0)
+	{
+		scroll = -FixedMul((abs(scroll) % (dc_texheight*FRACUNIT)), spryscale);
+		bottomscreen += texheight; // Draw an extra time
+	}
+	else if (scroll)
+	{
+		scroll = FixedMul(scroll % (dc_texheight*FRACUNIT), spryscale);
+		topscreen -= texheight; // Draw an extra time
+	}
+
+	while (topscreen < bottomscreen)
+	{
+		sprtopscreen = topscreen + scroll;
+		sprbotscreen = bottomscreen + scroll;
+
 		R_DrawFlippedMaskedColumn(col, lengthcol);
+
 		if ((INT64)sprtopscreen + (INT64)dc_texheight*spryscale > (INT64)INT32_MAX) // prevent overflow
-			sprtopscreen = INT32_MAX;
-		else
-			sprtopscreen += dc_texheight*spryscale;
+			break;
+
+		topscreen += texheight;
 	}
+
+	sprtopscreen = sprbotscreen;
 }
 
 // Returns true if a fake floor is translucent.
@@ -497,7 +673,6 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	sector_t        tempsec;
 	INT32             templight;
 	INT32             i, p;
-	fixed_t         offsetvalue;
 	lightlist_t     *light;
 	r_lightlist_t   *rlight;
 	INT32           range;
@@ -515,6 +690,8 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	fixed_t wall_scalex, wall_scaley;
 	UINT8 vertflip;
 	unsigned lengthcol;
+	boolean fog = false;
+	boolean fuzzy = false;
 
 	void (*colfunc_2s) (column_t *, unsigned);
 
@@ -526,13 +703,11 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	curline = ds->curline;
 	backsector = pfloor->target;
 	frontsector = curline->frontsector == pfloor->target ? curline->backsector : curline->frontsector;
-	sidedef = R_GetFFloorSide(curline, pfloor);
-
-	colfunc = colfuncs[BASEDRAWFUNC];
+	sidedef = R_GetFFloorSide(curline->linedef, pfloor, pfloor->target);
 
 	if (pfloor->master->flags & ML_TFERLINE)
 	{
-		line_t *newline = R_GetFFloorLine(curline, pfloor);
+		line_t *newline = R_GetFFloorLine(curline->linedef, pfloor, pfloor->target);
 		do_texture_skew = newline->flags & ML_SKEWTD;
 		dont_peg_bottom = newline->flags & ML_DONTPEGBOTTOM;
 	}
@@ -547,7 +722,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 
 	if (pfloor->fofflags & FOF_TRANSLUCENT)
 	{
-		boolean fuzzy = true;
+		fuzzy = true;
 
 		// Hacked up support for alpha value in software mode Tails 09-24-2002
 		// ...unhacked by toaster 04-01-2021, re-hacked a little by sphere 19-11-2021
@@ -560,17 +735,14 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 			else if (!(dc_transmap = R_GetTranslucencyTable(trans)) || trans == 0)
 				fuzzy = false; // Opaque
 		}
-
-		if (fuzzy)
-			colfunc = colfuncs[COLDRAWFUNC_FUZZY];
 	}
 	else if (pfloor->fofflags & FOF_FOG)
+	{
 		colfunc = colfuncs[COLDRAWFUNC_FOG];
+		fog = true;
+	}
 
 	range = max(ds->x2-ds->x1, 1);
-	//SoM: Moved these up here so they are available for my lightlist calculations
-	rw_scalestep = ds->scalestep;
-	spryscale = ds->scale1 + (x1 - ds->x1)*rw_scalestep;
 
 	dc_numlights = 0;
 	if (frontsector->numlights)
@@ -652,9 +824,9 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 
 			// Check if the current light effects the colormap/lightlevel
 			if (pfloor->fofflags & FOF_FOG)
-				rlight->lightnum = (pfloor->master->frontsector->lightlevel >> LIGHTSEGSHIFT);
+				rlight->lightnum = R_SideLightLevel(curline->sidedef, pfloor->master->frontsector->lightlevel) >> LIGHTSEGSHIFT;
 			else
-				rlight->lightnum = (rlight->lightlevel >> LIGHTSEGSHIFT);
+				rlight->lightnum = R_SideLightLevel(curline->sidedef, rlight->lightlevel) >> LIGHTSEGSHIFT;
 
 			if (pfloor->fofflags & FOF_FOG || rlight->flags & FOF_FOG || (rlight->extra_colormap && (rlight->extra_colormap->flags & CMF_FOG)))
 				;
@@ -672,18 +844,17 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	{
 		// Get correct light level!
 		if ((frontsector->extra_colormap && (frontsector->extra_colormap->flags & CMF_FOG)))
-			lightnum = (frontsector->lightlevel >> LIGHTSEGSHIFT);
-		else if (pfloor->fofflags & FOF_FOG)
-			lightnum = (pfloor->master->frontsector->lightlevel >> LIGHTSEGSHIFT);
-		else if (colfunc == colfuncs[COLDRAWFUNC_FUZZY])
+			lightnum = R_SideLightLevel(curline->sidedef, frontsector->lightlevel) >> LIGHTSEGSHIFT;
+		else if (fog)
+			lightnum = R_SideLightLevel(curline->sidedef, pfloor->master->frontsector->lightlevel) >> LIGHTSEGSHIFT;
+		else if (fuzzy)
 			lightnum = LIGHTLEVELS-1;
 		else
-			lightnum = R_FakeFlat(frontsector, &tempsec, &templight, &templight, false)
-				->lightlevel >> LIGHTSEGSHIFT;
+			lightnum = R_SideLightLevel(curline->sidedef, R_FakeFlat(frontsector, &tempsec, &templight, &templight, false)->lightlevel) >> LIGHTSEGSHIFT;
 
 		if (pfloor->fofflags & FOF_FOG || (frontsector->extra_colormap && (frontsector->extra_colormap->flags & CMF_FOG)));
 			else if (curline->v1->y == curline->v2->y)
-		lightnum--;
+			lightnum--;
 		else if (curline->v1->x == curline->v2->x)
 			lightnum++;
 
@@ -703,6 +874,14 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 		vertflip = !vertflip;
 	}
 
+	//SoM: Moved these up here so they are available for my lightlist calculations
+	// Lactozilla: Moved them back down
+	// This uses floating point math now, because the fixed-point imprecisions
+	// become more severe the bigger the texture is scaled.
+	double dwall_scaley = FixedToDouble(wall_scaley);
+	double scalestep = FixedToDouble(ds->scalestep) / dwall_scaley;
+	double yscale = (FixedToDouble(ds->scale1) + (x1 - ds->x1)*scalestep) / dwall_scaley;
+
 	thicksidecol = ffloortexturecolumn;
 
 	wall_offsetx = ds->offsetx + sidedef->offsetx_mid;
@@ -734,7 +913,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	else
 		dc_texturemid = FixedMul(*pfloor->topheight - viewz, wall_scaley);
 
-	offsetvalue = sidedef->rowoffset + sidedef->offsety_mid;
+	repeatscroll = sidedef->rowoffset + sidedef->offsety_mid;
 
 	if (dont_peg_bottom)
 	{
@@ -744,7 +923,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 			dc_texturemid = FixedMul(left_bottom, wall_scaley);
 		}
 		else
-			offsetvalue -= FixedMul(*pfloor->topheight - *pfloor->bottomheight, wall_scaley);
+			repeatscroll -= FixedMul(*pfloor->topheight - *pfloor->bottomheight, wall_scaley);
 	}
 
 	if (skewslope)
@@ -753,12 +932,21 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 		ffloortextureslide = FixedMul(R_GetSlopeTextureSlide(skewslope, lineangle), wall_scaley);
 	}
 
-	dc_texturemid += offsetvalue;
+	dc_texturemid += repeatscroll;
 
 	// Texture must be cached
 	R_CheckTextureCache(texnum);
 
-	if (vertflip) // vertically flipped?
+	// handle case where multipatch texture is drawn on a 2sided wall, multi-patch textures
+	// are not stored per-column with post info in SRB2
+	if (!textures[texnum]->transparency)
+	{
+		if (vertflip) // vertically flipped?
+			colfunc_2s = R_RenderFlipped2sidedMultiPatchColumn;
+		else
+			colfunc_2s = R_Render2sidedMultiPatchColumn;
+	}
+	else if (vertflip) // vertically flipped?
 		colfunc_2s = R_DrawRepeatFlippedMaskedColumn;
 	else
 		colfunc_2s = R_DrawRepeatMaskedColumn;
@@ -824,15 +1012,41 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 						rlight->botheight += rlight->botheightstep;
 				}
 			}
-			spryscale += rw_scalestep;
+			yscale += scalestep;
 			continue;
 		}
 
-		dc_iscale = FixedMul(0xffffffffu / (unsigned)spryscale, wall_scaley);
-
 		// Get data for the column
 		col = R_GetColumn(texnum, ((thicksidecol[dc_x] + wall_offsetx) >> FRACBITS));
 
+		spryscale = DoubleToFixed(yscale);
+
+		// Eh. I tried fixing the scaling artifacts but it still wasn't perfect.
+		// So this checks if the column has gaps in it or not, and if it does, uses a version of the column drawers
+		// that prevents the artifacts from being visible.
+		// Note that if rendering fog then none of this matters because there's no texture mapping to be done
+		if (!fog)
+		{
+			dc_iscale = 0xffffffffu / (unsigned)spryscale;
+
+			// Column has a single post and it matches the texture height, use regular column drawers
+			if (col->num_posts == 1 && col->posts[0].topdelta == 0 && col->posts[0].length == (unsigned)dc_texheight)
+			{
+				if (fuzzy)
+					colfunc = colfuncs[COLDRAWFUNC_FUZZY];
+				else
+					colfunc = colfuncs[BASEDRAWFUNC];
+			}
+			else
+			{
+				// Otherwise use column drawers with extra checks
+				if (fuzzy)
+					colfunc = colfuncs[COLDRAWFUNC_CLAMPEDTRANS];
+				else
+					colfunc = colfuncs[COLDRAWFUNC_CLAMPED];
+			}
+		}
+
 		// SoM: New code does not rely on R_DrawColumnShadowed_8 which
 		// will (hopefully) put less strain on the stack.
 		if (dc_numlights)
@@ -947,7 +1161,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 			if (windowtop < windowbottom)
 				colfunc_2s (col, lengthcol);
 
-			spryscale += rw_scalestep;
+			yscale += scalestep;
 			continue;
 		}
 
@@ -966,7 +1180,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 
 		// draw the texture
 		colfunc_2s (col, lengthcol);
-		spryscale += rw_scalestep;
+		yscale += scalestep;
 	}
 	colfunc = colfuncs[BASEDRAWFUNC];
 
@@ -1274,7 +1488,7 @@ static void R_RenderSegLoop (void)
 			for (i = 0; i < dc_numlights; i++)
 			{
 				INT32 lightnum;
-				lightnum = (dc_lightlist[i].lightlevel >> LIGHTSEGSHIFT);
+				lightnum = R_SideLightLevel(curline->sidedef, dc_lightlist[i].lightlevel) >> LIGHTSEGSHIFT;
 
 				if (dc_lightlist[i].extra_colormap)
 					;
@@ -2461,7 +2675,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		//  use different light tables
 		//  for horizontal / vertical / diagonal
 		// OPTIMIZE: get rid of LIGHTSEGSHIFT globally
-		lightnum = (frontsector->lightlevel >> LIGHTSEGSHIFT);
+		lightnum = R_SideLightLevel(curline->sidedef, frontsector->lightlevel) >> LIGHTSEGSHIFT;
 
 		if (curline->v1->y == curline->v2->y)
 			lightnum--;
diff --git a/src/r_skins.c b/src/r_skins.c
index 7ae01afecb90c0d2668a0ad3227894eb2461f614..47ead32637c869af7ea44cb668a43dfc6bac8631 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2025 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -44,10 +44,9 @@ UINT16 P_GetStateSprite2(state_t *state)
 	else
 	{
 		// Transform the state frame into an animation ID
-		UINT32 stateframe = state->frame & FF_FRAMEMASK;
-		UINT16 spr2 = stateframe & ~FF_SPR2SUPER;
+		UINT16 spr2 = state->frame & FF_FRAMEMASK;
 
-		if (stateframe & FF_SPR2SUPER)
+		if (state->frame & SPR2F_SUPER)
 			spr2 |= SPR2F_SUPER;
 
 		return spr2;
@@ -71,7 +70,7 @@ boolean P_IsStateSprite2Super(state_t *state)
 		if (state->sprite2 & SPR2F_SUPER)
 			return true;
 	}
-	else if (state->frame & FF_SPR2SUPER)
+	else if (state->frame & SPR2F_SUPER)
 		return true;
 
 	return false;
@@ -82,10 +81,19 @@ UINT16 P_ApplySuperFlagToSprite2(UINT16 spr2, mobj_t *mobj)
 {
 	if (mobj->player)
 	{
-		if (mobj->player->charflags & SF_NOSUPERSPRITES || (mobj->player->powers[pw_carry] == CR_NIGHTSMODE && (mobj->player->charflags & SF_NONIGHTSSUPER)))
+		boolean is_nights = mobj->player->powers[pw_carry] == CR_NIGHTSMODE;
+
+		if (mobj->player->charflags & SF_NOSUPERSPRITES || (is_nights && (mobj->player->charflags & SF_NONIGHTSSUPER)))
 			spr2 &= ~SPR2F_SUPER;
-		else if (mobj->player->powers[pw_super] || (mobj->player->powers[pw_carry] == CR_NIGHTSMODE && (mobj->player->charflags & SF_SUPER)))
+		else if (mobj->player->powers[pw_super] || (is_nights && (mobj->player->charflags & SF_SUPER)))
 			spr2 |= SPR2F_SUPER;
+
+		// Special case for transforming when you are NiGHTS.
+		// Do NOT apply the super sprites in this situation, even if they exist.
+		if (is_nights && mobj->state >= &states[S_PLAY_NIGHTS_TRANS1] && mobj->state <= &states[S_PLAY_NIGHTS_TRANS6])
+		{
+			spr2 &= ~SPR2F_SUPER;
+		}
 	}
 
 	if (spr2 & SPR2F_SUPER)
@@ -638,6 +646,14 @@ static void R_LoadSkinSprites(UINT16 wadnum, UINT16 *lump, UINT16 *lastlump, ski
 
 	if (skin->sprites[0].numframes == 0)
 		CONS_Alert(CONS_ERROR, M_GetText("No frames found for sprite SPR2_%s\n"), spr2names[0]);
+
+	// TODO: 2.3: Delete
+	memcpy(&skin->sprites_compat[start_spr2],
+		&skin->sprites[start_spr2],
+		sizeof(spritedef_t) * (free_spr2 - start_spr2));
+	memcpy(&skin->sprites_compat[start_spr2 + NUMPLAYERSPRITES],
+		&skin->super.sprites[start_spr2],
+		sizeof(spritedef_t) * (free_spr2 - start_spr2));
 }
 
 // returns whether found appropriate property
@@ -724,6 +740,8 @@ static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
 	GETFLAG(MACHINE)
 	GETFLAG(DASHMODE)
 	GETFLAG(FASTEDGE)
+	GETFLAG(FASTWAIT)
+	GETFLAG(JETFUME)
 	GETFLAG(MULTIABILITY)
 	GETFLAG(NONIGHTSROTATION)
 	GETFLAG(NONIGHTSSUPER)
diff --git a/src/r_skins.h b/src/r_skins.h
index db6a1ef63ee9e218652a7f84ccaaa3dd13f9cd84..7df0d584bd88a09b25a6e8a31b2da351a1a06db5 100644
--- a/src/r_skins.h
+++ b/src/r_skins.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -92,6 +92,9 @@ typedef struct
 
 	hashentry_int32_int32_t *defaulttocustomstate; // e.g. S_PLAY_WALK => S_SKIN_BIGTHECAT_WALK
 	hashentry_int32_int32_t *customtodefaultstate; // e.g. S_SKIN_BIGTHECAT_WALK => S_PLAY_WALK
+
+	// TODO: 2.3: Delete
+	spritedef_t sprites_compat[NUMPLAYERSPRITES * 2];
 } skin_t;
 
 /// Externs
diff --git a/src/r_sky.c b/src/r_sky.c
index 1b2c43d12b770bffba413bbbae8a84139aeb041f..ad7262c6e168d9a469e5189c41cb1c2b45438576 100644
--- a/src/r_sky.c
+++ b/src/r_sky.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_splats.c b/src/r_splats.c
index 027ccd720aed2902b7c096040548b95e3d3b369d..4b4de8275132bfe8bb92d807281c6514fe712777 100644
--- a/src/r_splats.c
+++ b/src/r_splats.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -53,7 +53,7 @@ static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32
 
 			if (dir == 0)
 			{
-				for (;;)
+				for (; count > 0; count--)
 				{
 					rastertab[y1].maxx = xs;
 					rastertab[y1].tx2 = xe;
@@ -62,13 +62,11 @@ static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32
 					xs += dx0;
 					xe += dx1;
 					y1++;
-
-					if (count-- < 1) break;
 				}
 			}
 			else
 			{
-				for (;;)
+				for (; count > 0; count--)
 				{
 					rastertab[y1].maxx = xs;
 					rastertab[y1].tx2 = tc;
@@ -77,8 +75,6 @@ static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32
 					xs += dx0;
 					xe += dx1;
 					y1++;
-
-					if (count-- < 1) break;
 				}
 			}
 		}
@@ -95,7 +91,7 @@ static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32
 
 			if (dir == 0)
 			{
-				for (;;)
+				for (; count > 0; count--)
 				{
 					rastertab[y2].minx = xs;
 					rastertab[y2].tx1 = xe;
@@ -104,13 +100,11 @@ static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32
 					xs += dx0;
 					xe += dx1;
 					y2++;
-
-					if (count-- < 1) break;
 				}
 			}
 			else
 			{
-				for (;;)
+				for (; count > 0; count--)
 				{
 					rastertab[y2].minx = xs;
 					rastertab[y2].tx1 = tc;
@@ -119,8 +113,6 @@ static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32
 					xs += dx0;
 					xe += dx1;
 					y2++;
-
-					if (count-- < 1) break;
 				}
 			}
 		}
@@ -381,7 +373,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 
 	if (pSplat->slope)
 	{
-		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_SetScaledSlopePlane(pSplat->slope, vis->viewpoint.x, vis->viewpoint.y, vis->viewpoint.z, (INT64)pSplat->xscale, (INT64)pSplat->yscale, -pSplat->verts[0].x, pSplat->verts[0].y, vis->viewpoint.angle, pSplat->angle);
 	}
 	else if (!ds_solidcolor)
 	{
diff --git a/src/r_textures.c b/src/r_textures.c
index 59cc114139c5abb24df0f47fa0dddd0e70ec8d72..4dc6bbae40ea7f722ed040eab779ed1ce62ae467 100644
--- a/src/r_textures.c
+++ b/src/r_textures.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -147,9 +147,9 @@ static void R_DrawFlippedColumnInCache(column_t *column, UINT8 *cache, texpatch_
 
 		if (count > 0)
 		{
-			for (; dest < cache + position + count; --source, is_opaque++)
+			for (; dest < cache + position + count; --source, dest++, is_opaque++)
 			{
-				*dest++ = *source;
+				*dest = *source;
 				*is_opaque = true;
 			}
 		}
@@ -295,7 +295,6 @@ UINT8 *R_GenerateTexture(size_t texnum)
 		UINT16 lumpnum = patch->lump;
 		UINT8 *pdata;
 		softwarepatch_t *realpatch;
-		boolean holey = false;
 
 #ifndef NO_PNG_LUMPS
 		UINT8 header[PNG_HEADER_SIZE];
@@ -310,9 +309,11 @@ UINT8 *R_GenerateTexture(size_t texnum)
 		pdata = W_CacheLumpNumPwad(wadnum, lumpnum, PU_CACHE);
 		realpatch = (softwarepatch_t *)pdata;
 
+		texture->transparency = false;
+
 		// Check the patch for holes.
 		if (texture->width > SHORT(realpatch->width) || texture->height > SHORT(realpatch->height))
-			holey = true;
+			texture->transparency = true;
 		else
 		{
 			UINT8 *colofs = (UINT8 *)realpatch->columnofs;
@@ -332,12 +333,12 @@ UINT8 *R_GenerateTexture(size_t texnum)
 					col = (doompost_t *)((UINT8 *)col + col->length + 4);
 				}
 				if (y < texture->height)
-					holey = true; // this texture is HOLEy! D:
+					texture->transparency = true; // this texture is HOLEy! D:
 			}
 		}
 
 		// If the patch uses transparency, we have to save it this way.
-		if (holey)
+		if (texture->transparency)
 		{
 			texture->flip = patch->flip;
 
@@ -378,6 +379,15 @@ UINT8 *R_GenerateTexture(size_t texnum)
 	temp_columns = Z_Calloc(sizeof(column_t) * texture->width, PU_STATIC, NULL);
 	temp_block = Z_Calloc(total_pixels, PU_STATIC, NULL);
 
+#ifdef TEXTURE_255_IS_TRANSPARENT
+	texture->transparency = false;
+
+	// Transparency hack
+	memset(temp_block, TRANSPARENTPIXEL, total_pixels);
+#else
+	texture->transparency = true;
+#endif
+
 	for (x = 0; x < texture->width; x++)
 	{
 		column_t *column = &temp_columns[x];
@@ -474,13 +484,27 @@ UINT8 *R_GenerateTexture(size_t texnum)
 	// Now write the columns
 	column_posts = Z_Calloc(sizeof(unsigned) * texture->width, PU_STATIC, NULL);
 
+#ifdef TEXTURE_255_IS_TRANSPARENT
+	total_posts = texture->width;
+	temp_posts = Z_Realloc(temp_posts, sizeof(post_t) * total_posts, PU_CACHE, NULL);
+#endif
+
 	for (x = 0; x < texture->width; x++)
 	{
 		post_t *post = NULL;
-		boolean was_opaque = false;
 
 		column_t *column = &temp_columns[x];
 
+#ifdef TEXTURE_255_IS_TRANSPARENT
+		post = &temp_posts[x];
+		post->topdelta = 0;
+		post->length = texture->height;
+		post->data_offset = 0;
+		column_posts[x] = x;
+		column->num_posts = 1;
+#else
+		boolean was_opaque = false;
+
 		column_posts[x] = (unsigned)-1;
 
 		for (INT32 y = 0; y < texture->height; y++)
@@ -510,6 +534,7 @@ UINT8 *R_GenerateTexture(size_t texnum)
 
 			post->length++;
 		}
+#endif
 	}
 
 	blocksize = (sizeof(column_t) * texture->width) + (sizeof(post_t) * total_posts) + (sizeof(UINT8) * total_pixels);
@@ -821,8 +846,8 @@ Rloadflats (INT32 i, INT32 w)
 			UINT8 *flatlump = W_CacheLumpNumPwad(wadnum, lumpnum, PU_CACHE);
 			if (Picture_PNGDimensions((UINT8 *)flatlump, &texw, &texh, NULL, NULL, lumplength))
 			{
-				width = (INT16)width;
-				height = (INT16)height;
+				width = (INT16)texw;
+				height = (INT16)texh;
 			}
 			else
 			{
@@ -900,7 +925,7 @@ Rloadtextures (INT32 i, INT32 w)
 
 		// printf("\"%s\" (wad: %u, lump: %u) is a single patch, dimensions %d x %d\n",W_CheckNameForNumPwad(wadnum,lumpnum),wadnum,lumpnum,width,height);
 
-		R_AddSinglePatchTexture(i, wadnum, lumpnum, width, height, TEXTURETYPE_SINGLEPATCH);
+		R_AddSinglePatchTexture(i, wadnum, lumpnum, width, height, TEXTURETYPE_TEXTURE);
 
 		i++;
 	}
@@ -1068,6 +1093,98 @@ void R_LoadTexturesPwad(UINT16 wadnum)
 	R_FinishLoadingTextures(newtextures);
 }
 
+static lumpnum_t W_GetTexPatchLumpNum(const char *name)
+{
+	// Flats as a texture patch crashes horribly, and flats
+	// can share the same name as textures.
+
+	// But even if they worked, we want to prioritize:
+	// Patches -> Textures -> anything else
+
+	enum
+	{
+		USE_PATCHES,
+		USE_TEXTURES,
+		USE__MAX,
+	};
+
+	lumpnum_t lump = LUMPERROR;
+	INT32 lump_type_it;
+
+
+	for (lump_type_it = 0; lump_type_it < USE__MAX; lump_type_it++)
+	{
+		INT32 i;
+
+		for (i = numwadfiles - 1; i >= 0; i--) // Scan wad files backwards so patched lumps take precedent
+		{
+			lumpnum_t start = LUMPERROR;
+			lumpnum_t end = LUMPERROR;
+
+			switch (wadfiles[i]->type)
+			{
+				case RET_WAD:
+					if (lump_type_it == USE_PATCHES)
+					{
+						if ((start = W_CheckNumForMarkerStartPwad("P_START", (UINT16)i, 0)) == INT16_MAX)
+							continue;
+						else if ((end = W_CheckNumForNamePwad("P_END", (UINT16)i, start)) == INT16_MAX)
+							continue;
+					}
+					else if (lump_type_it == USE_TEXTURES)
+					{
+						if ((start = W_CheckNumForMarkerStartPwad("TX_START", (UINT16)i, 0)) == INT16_MAX)
+							continue;
+						else if ((end = W_CheckNumForNamePwad("TX_END", (UINT16)i, start)) == INT16_MAX)
+							continue;
+					}
+					break;
+				case RET_PK3:
+				case RET_FOLDER:
+					if (lump_type_it == USE_PATCHES)
+					{
+						if ((start = W_CheckNumForFolderStartPK3("Patches/", i, 0)) == INT16_MAX)
+							continue;
+						if ((end = W_CheckNumForFolderEndPK3("Patches/", i, start)) == INT16_MAX)
+							continue;
+					}
+					else if (lump_type_it == USE_TEXTURES)
+					{
+						if ((start = W_CheckNumForFolderStartPK3("Textures/", i, 0)) == INT16_MAX)
+							continue;
+						if ((end = W_CheckNumForFolderEndPK3("Textures/", i, start)) == INT16_MAX)
+							continue;
+					}
+					break;
+				default:
+					continue;
+			}
+
+			// Now find lump with specified name in that range.
+			lump = W_CheckNumForNamePwad(name, (UINT16)i, start);
+			if (lump < end)
+			{
+				lump += (i<<16); // found it, in our constraints
+				break;
+			}
+			lump = LUMPERROR;
+		}
+
+		if (lump != LUMPERROR)
+		{
+			break;
+		}
+	}
+
+	if (lump == LUMPERROR)
+	{
+		// Use whatever else you can find.
+		return W_CheckNumForPatchName(name);
+	}
+
+	return lump;
+}
+
 static texpatch_t *R_ParsePatch(boolean actuallyLoadPatch)
 {
 	char *texturesToken;
@@ -1240,13 +1357,13 @@ static texpatch_t *R_ParsePatch(boolean actuallyLoadPatch)
 	if (actuallyLoadPatch == true)
 	{
 		// Check lump exists
-		patchLumpNum = W_GetNumForName(patchName);
+		patchLumpNum = W_GetTexPatchLumpNum(patchName);
 		// If so, allocate memory for texpatch_t and fill 'er up
 		resultPatch = (texpatch_t *)Z_Malloc(sizeof(texpatch_t),PU_STATIC,NULL);
 		resultPatch->originx = patchXPos;
 		resultPatch->originy = patchYPos;
-		resultPatch->lump = patchLumpNum & 65535;
-		resultPatch->wad = patchLumpNum>>16;
+		resultPatch->lump = LUMPNUM(patchLumpNum);
+		resultPatch->wad = WADFILENUM(patchLumpNum);
 		resultPatch->flip = flip;
 		resultPatch->alpha = alpha;
 		resultPatch->style = style;
@@ -1377,7 +1494,7 @@ static texture_t *R_ParseTexture(boolean actuallyLoadTexture)
 			resultTexture->hash = quickncasehash(newTextureName, 8);
 			resultTexture->width = newTextureWidth;
 			resultTexture->height = newTextureHeight;
-			resultTexture->type = TEXTURETYPE_COMPOSITE;
+			resultTexture->type = TEXTURETYPE_TEXTURE;
 		}
 		Z_Free(texturesToken);
 		texturesToken = M_GetToken(NULL);
@@ -1563,7 +1680,7 @@ static void AddTextureToCache(const char *name, UINT32 hash, INT32 id, UINT8 typ
 //
 // Check whether texture is available. Filter out NoTexture indicator.
 //
-INT32 R_CheckTextureNumForName(const char *name)
+INT32 R_CheckTextureNumForName(const char *name, UINT8 type)
 {
 	INT32 i;
 	UINT32 hash;
@@ -1575,14 +1692,14 @@ INT32 R_CheckTextureNumForName(const char *name)
 	hash = quickncasehash(name, 8);
 
 	for (i = 0; i < tidcachelen; i++)
-		if (tidcache[i].hash == hash && !strncasecmp(tidcache[i].name, name, 8))
+		if (tidcache[i].type == type && tidcache[i].hash == hash && !strncasecmp(tidcache[i].name, name, 8))
 			return tidcache[i].id;
 
 	// Need to parse the list backwards, so textures loaded more recently are used in lieu of ones loaded earlier
 	for (i = numtextures - 1; i >= 0; i--)
-		if (textures[i]->hash == hash && !strncasecmp(textures[i]->name, name, 8))
+		if (textures[i]->type == type && textures[i]->hash == hash && !strncasecmp(textures[i]->name, name, 8))
 		{
-			AddTextureToCache(name, hash, i, textures[i]->type);
+			AddTextureToCache(name, hash, i, type);
 			return i;
 		}
 
@@ -1599,7 +1716,7 @@ const char *R_CheckTextureNameForNum(INT32 num)
 {
 	if (num > 0 && num < numtextures)
 		return textures[num]->name;
-	
+
 	return "-";
 }
 
@@ -1621,47 +1738,29 @@ const char *R_TextureNameForNum(INT32 num)
 //
 // R_TextureNumForName
 //
-// Calls R_CheckTextureNumForName, aborts with error message.
+// Calls R_CheckTextureNumForName. Returns REDWALL if not found.
 //
 INT32 R_TextureNumForName(const char *name)
 {
-	const INT32 i = R_CheckTextureNumForName(name);
+	INT32 i = R_CheckTextureNumForName(name, TEXTURETYPE_TEXTURE);
 
+	// Didn't find it, so look for a flat
+	if (i == -1)
+	{
+		i = R_CheckTextureNumForName(name, TEXTURETYPE_FLAT);
+	}
+
+	// Still didn't find it, so return REDWALL
 	if (i == -1)
 	{
 		static INT32 redwall = -2;
 		CONS_Debug(DBG_SETUP, "WARNING: R_TextureNumForName: %.8s not found\n", name);
 		if (redwall == -2)
-			redwall = R_CheckTextureNumForName("REDWALL");
+			redwall = R_CheckTextureNumForName("REDWALL", TEXTURETYPE_TEXTURE);
 		if (redwall != -1)
 			return redwall;
 		return 1;
 	}
-	return i;
-}
-
-// Like R_CheckTextureNumForName, but only looks in the flat namespace specifically.
-INT32 R_CheckFlatNumForName(const char *name)
-{
-	INT32 i;
-	UINT32 hash;
 
-	// "NoTexture" marker.
-	if (name[0] == '-')
-		return 0;
-
-	hash = quickncasehash(name, 8);
-
-	for (i = 0; i < tidcachelen; i++)
-		if (tidcache[i].type == TEXTURETYPE_FLAT && tidcache[i].hash == hash && !strncasecmp(tidcache[i].name, name, 8))
-			return tidcache[i].id;
-
-	for (i = numtextures - 1; i >= 0; i--)
-		if (textures[i]->hash == hash && !strncasecmp(textures[i]->name, name, 8) && textures[i]->type == TEXTURETYPE_FLAT)
-		{
-			AddTextureToCache(name, hash, i, TEXTURETYPE_FLAT);
-			return i;
-		}
-
-	return -1;
+	return i;
 }
diff --git a/src/r_textures.h b/src/r_textures.h
index eb68ec09f21d4fe8d847b048ac12ba9bd3a1817d..e6985556b65176058416c9c6ebb1aa5551d6e086 100644
--- a/src/r_textures.h
+++ b/src/r_textures.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -40,8 +40,7 @@ typedef struct
 enum
 {
 	TEXTURETYPE_UNKNOWN,
-	TEXTURETYPE_SINGLEPATCH,
-	TEXTURETYPE_COMPOSITE,
+	TEXTURETYPE_TEXTURE,
 	TEXTURETYPE_FLAT
 };
 
@@ -54,6 +53,7 @@ typedef struct
 	char name[8];
 	UINT32 hash;
 	UINT8 type; // TEXTURETYPE_*
+	boolean transparency;
 	INT16 width, height;
 	UINT8 flip; // 1 = flipx, 2 = flipy, 3 = both
 	void *flat; // The texture, as a flat.
@@ -99,8 +99,7 @@ void R_SetFlatVars(size_t length);
 
 // Returns the texture number for the texture name.
 INT32 R_TextureNumForName(const char *name);
-INT32 R_CheckTextureNumForName(const char *name);
-INT32 R_CheckFlatNumForName(const char *name);
+INT32 R_CheckTextureNumForName(const char *name, UINT8 type);
 
 // Returns the texture name for the texture number (in case you ever needed it)
 const char *R_CheckTextureNameForNum(INT32 num);
diff --git a/src/r_things.c b/src/r_things.c
index 004e0a6c292d95c4d323422db4681f53f5e276e4..2c0529e47a572b8fcd5b7966fcb7652e13a6b03b 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -136,6 +136,12 @@ static void R_InstallSpriteLump(UINT16 wad,            // graphics patch
 {
 	char cn = R_Frame2Char(frame), cr = R_Rotation2Char(rotation); // for debugging
 
+	char framedescription[256];
+	if (cn != '\xFF')
+		sprintf(framedescription, "%s frame %d (%c)", spritename, frame, cn);
+	else
+		sprintf(framedescription, "%s frame %d", spritename, frame);
+
 	INT32 r;
 	lumpnum_t lumppat = (wad << 16) + lump;
 
@@ -153,9 +159,9 @@ static void R_InstallSpriteLump(UINT16 wad,            // graphics patch
 	{
 		// the lump should be used for all rotations
 		if (sprtemp[frame].rotate == SRF_SINGLE)
-			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has multiple rot = 0 lump\n", spritename, frame, cn);
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has multiple rot = 0 lump\n", framedescription);
 		else if (sprtemp[frame].rotate != SRF_NONE) // Let's bundle 1-8/16 and L/R rotations into one debug message.
-			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has rotations and a rot = 0 lump\n", spritename, frame, cn);
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has rotations and a rot = 0 lump\n", framedescription);
 
 		sprtemp[frame].rotate = SRF_SINGLE;
 		for (r = 0; r < 16; r++)
@@ -175,15 +181,15 @@ static void R_InstallSpriteLump(UINT16 wad,            // graphics patch
 		if (sprtemp[frame].rotate == SRF_NONE)
 			sprtemp[frame].rotate = SRF_SINGLE;
 		else if (sprtemp[frame].rotate == SRF_SINGLE)
-			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has L/R rotations and a rot = 0 lump\n", spritename, frame, cn);
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has L/R rotations and a rot = 0 lump\n", framedescription);
 		else if (sprtemp[frame].rotate == SRF_3D)
-			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has both L/R and 1-8 rotations\n", spritename, frame, cn);
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has both L/R and 1-8 rotations\n", framedescription);
 		else if (sprtemp[frame].rotate == SRF_3DGE)
-			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has both L/R and 1-G rotations\n", spritename, frame, cn);
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has both L/R and 1-G rotations\n", framedescription);
 		else if ((sprtemp[frame].rotate & SRF_LEFT) && (rotation == ROT_L))
-			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has multiple L rotations\n", spritename, frame, cn);
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has multiple L rotations\n", framedescription);
 		else if ((sprtemp[frame].rotate & SRF_RIGHT) && (rotation == ROT_R))
-			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has multiple R rotations\n", spritename, frame, cn);
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has multiple R rotations\n", framedescription);
 
 		sprtemp[frame].rotate |= ((rotation == ROT_R) ? SRF_RIGHT : SRF_LEFT);
 		if ((sprtemp[frame].rotate & SRF_2D) == SRF_2D)
@@ -210,9 +216,9 @@ static void R_InstallSpriteLump(UINT16 wad,            // graphics patch
 	if (sprtemp[frame].rotate == SRF_NONE)
 		sprtemp[frame].rotate = SRF_SINGLE;
 	else if (sprtemp[frame].rotate == SRF_SINGLE)
-		CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has 1-8/G rotations and a rot = 0 lump\n", spritename, frame, cn);
+		CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has 1-8/G rotations and a rot = 0 lump\n", framedescription);
 	else if (sprtemp[frame].rotate & SRF_2D)
-		CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s frame %d (%c) has both L/R and 1-8/G rotations\n", spritename, frame, cn);
+		CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s has both L/R and 1-8/G rotations\n", framedescription);
 
 	// make 0 based
 	rotation--;
@@ -232,7 +238,12 @@ static void R_InstallSpriteLump(UINT16 wad,            // graphics patch
 	}
 
 	if (sprtemp[frame].lumppat[rotation] != LUMPERROR)
-		CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s: %d_%c (%c%c) has two lumps mapped to it\n", spritename, frame, cr, cn, cr);
+	{
+		if (cn != '\xFF')
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s: %d_%c (%c%c) has two lumps mapped to it\n", spritename, frame, cr, cn, cr);
+		else
+			CONS_Debug(DBG_SETUP, "R_InitSprites: Sprite %s: %d_%c has two lumps mapped to it\n", spritename, frame, cr);
+	}
 
 	// lumppat & lumpid are the same for original Doom, but different
 	// when using sprites in pwad : the lumppat points the new graphics
@@ -268,6 +279,14 @@ static boolean GetFramesAndRotationsFromShortLumpName(
 		*ret_rotation2 = R_Char2Rotation(name[7]);
 		if (*ret_frame2 >= 64 || *ret_rotation2 == 255)
 			return false;
+
+		// TRNSLATE is a valid but extremely unlikely sprite name:
+		// * The sprite name is "TRNS"
+		// * The frame is L, rotation A; mirrored to frame T, rotation E
+		// In the very unfortunate event that TRNSLATE is found between sprite lumps,
+		// this name check prevents it from being added as a sprite, when it actually isn't.
+		if (memcmp(name, "TRNSLATE", 8) == 0)
+			return false;
 	}
 	else
 	{
@@ -356,7 +375,7 @@ static void MirrorMissingRotations(void)
 	{
 		spriteframe_t *frame = &sprtemp[framenum];
 
-		if (frame->rotate == SRF_NONE || !(frame->rotate & SRF_3DMASK))
+		if (frame->rotate == SRF_NONE || !(frame->rotate & (SRF_3DMASK | SRF_2D)))
 			continue;
 
 		UINT8 numrotations = frame->rotate == SRF_3D ? 8 : 16;
@@ -368,7 +387,7 @@ static void MirrorMissingRotations(void)
 
 			UINT8 baserotation = GetOppositeRotation(rotation, frame->rotate);
 			UINT32 lumpnum = frame->lumppat[baserotation - 1];
-			R_InstallSpriteLump(WADFILENUM(lumpnum), LUMPNUM(lumpnum), frame->lumpid[baserotation], framenum, rotation, 1);
+			R_InstallSpriteLump(WADFILENUM(lumpnum), LUMPNUM(lumpnum), frame->lumpid[baserotation - 1], framenum, rotation, 1);
 		}
 	}
 }
@@ -380,14 +399,17 @@ static void CheckFrame(const char *sprname)
 	{
 		spriteframe_t *spriteframe = &sprtemp[frame];
 
+		char framedescription[256];
+		if (frame < 64)
+			sprintf(framedescription, "%s frame %d (%c)", sprname, frame, R_Frame2Char(frame));
+		else
+			sprintf(framedescription, "%s frame %d", sprname, frame);
+
 		switch (spriteframe->rotate)
 		{
 		case SRF_NONE:
 			// no rotations were found for that frame at all
-			if (frame < 64)
-				I_Error("R_AddSingleSpriteDef: No patches found for %s frame %d (%c)", sprname, frame, R_Frame2Char(frame));
-			else
-				I_Error("R_AddSingleSpriteDef: No patches found for %s frame %d", sprname, frame);
+			I_Error("R_AddSingleSpriteDef: No patches found for %s", framedescription);
 			break;
 
 		case SRF_SINGLE:
@@ -397,8 +419,8 @@ static void CheckFrame(const char *sprname)
 		case SRF_2D: // both Left and Right rotations
 			// we test to see whether the left and right slots are present
 			if ((spriteframe->lumppat[2] == LUMPERROR) || (spriteframe->lumppat[6] == LUMPERROR))
-				I_Error("R_AddSingleSpriteDef: Sprite %s frame %d (%c) is missing rotations (L-R mode)",
-				sprname, frame, R_Frame2Char(frame));
+				I_Error("R_AddSingleSpriteDef: Sprite %s is missing rotations (L-R mode)",
+				framedescription);
 			break;
 
 		default:
@@ -410,8 +432,8 @@ static void CheckFrame(const char *sprname)
 					// we test the patch lump, or the id lump whatever
 					// if it was not loaded the two are LUMPERROR
 					if (spriteframe->lumppat[rotation] == LUMPERROR)
-						I_Error("R_AddSingleSpriteDef: Sprite %s frame %d (%c) is missing rotations (1-%c mode)",
-								sprname, frame, R_Frame2Char(frame), ((spriteframe->rotate & SRF_3DGE) ? 'G' : '8'));
+						I_Error("R_AddSingleSpriteDef: Sprite %s is missing rotations (1-%c mode)",
+								framedescription, ((spriteframe->rotate & SRF_3DGE) ? 'G' : '8'));
 				}
 			}
 			break;
@@ -665,7 +687,6 @@ static void AddLongSpriteDefs(UINT16 wadnum, size_t *ptr_spritesadded, size_t *p
 		char *sprname = W_GetLumpFolderNamePK3(wadnum, lumpnum);
 		strupr(sprname);
 		sprnum = R_GetSpriteNumByName(sprname);
-		Z_Free(sprname);
 
 		if (sprnum != NUMSPRITES && R_AddSingleSpriteDef(sprname, &sprites[sprnum], wadnum, folderstart, folderend, true))
 		{
@@ -676,6 +697,8 @@ static void AddLongSpriteDefs(UINT16 wadnum, size_t *ptr_spritesadded, size_t *p
 #endif
 		}
 
+		Z_Free(sprname);
+
 		lumpnum = folderend;
 	}
 
@@ -824,8 +847,10 @@ void R_DrawMaskedColumn(column_t *column, unsigned lengthcol)
 	{
 		post_t *post = &column->posts[i];
 
+		dc_postlength = post->length;
+
 		INT32 topscreen = sprtopscreen + spryscale*post->topdelta;
-		INT32 bottomscreen = topscreen + spryscale*post->length;
+		INT32 bottomscreen = topscreen + spryscale*dc_postlength;
 
 		dc_yl = (topscreen+FRACUNIT-1)>>FRACBITS;
 		dc_yh = (bottomscreen-1)>>FRACBITS;
@@ -894,10 +919,12 @@ void R_DrawFlippedMaskedColumn(column_t *column, unsigned lengthcol)
 		if (!post->length)
 			continue;
 
-		topdelta = lengthcol-post->length-post->topdelta;
+		dc_postlength = post->length;
+
+		topdelta = lengthcol-dc_postlength-post->topdelta;
 		topscreen = sprtopscreen + spryscale*topdelta;
-		bottomscreen = sprbotscreen == INT32_MAX ? topscreen + spryscale*post->length
-		                                      : sprbotscreen + spryscale*post->length;
+		bottomscreen = sprbotscreen == INT32_MAX ? topscreen + spryscale*dc_postlength
+		                                      : sprbotscreen + spryscale*dc_postlength;
 
 		dc_yl = (topscreen+FRACUNIT-1)>>FRACBITS;
 		dc_yh = (bottomscreen-1)>>FRACBITS;
@@ -941,7 +968,9 @@ UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 trans
 	if (color != SKINCOLOR_NONE)
 	{
 		// New colormap stuff for skins Tails 06-07-2002
-		if (mobj->colorized)
+		if ((mobj->state-states == S_METALSONIC_DASH || mobj->state-states == S_METALSONIC_BOUNCE) && (((leveltime/2) & 1))) // Metal boss doing attack
+			skinnum = TC_DASHMODE;
+		else if (mobj->colorized)
 			skinnum = TC_RAINBOW;
 		else if (mobj->player && mobj->player->dashmode >= DASHMODE_THRESHOLD
 			&& (mobj->player->charflags & SF_DASHMODE)
@@ -977,6 +1006,12 @@ UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 trans
 	return NULL;
 }
 
+// Based off of R_GetLinedefTransTable
+transnum_t R_GetThingTransTable(fixed_t alpha, transnum_t transmap)
+{
+	return (20*(FRACUNIT - ((alpha * (10 - transmap))/10) - 1) + FRACUNIT) >> (FRACBITS+1);
+}
+
 //
 // R_DrawVisSprite
 //  mfloorclip and mceilingclip should also be set.
@@ -1482,6 +1517,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	floordiff = abs((isflipped ? interp.height : 0) + interp.z - groundz);
 
 	trans = floordiff / (100*FRACUNIT) + 3;
+	trans = R_GetThingTransTable(thing->alpha, trans);
 	if (trans >= 9) return;
 
 	scalemul = FixedMul(FRACUNIT - floordiff/640, scale);
@@ -1755,6 +1791,10 @@ static void R_ProjectSprite(mobj_t *thing)
 	}
 
 	this_scale = interp.scale;
+
+	if (this_scale < 1)
+		return;
+
 	radius = interp.radius; // For drop shadows
 	height = interp.height; // Ditto
 
@@ -2169,6 +2209,11 @@ static void R_ProjectSprite(mobj_t *thing)
 	else
 		trans = 0;
 
+	if ((oldthing->flags2 & MF2_LINKDRAW) && oldthing->tracer)
+		trans = R_GetThingTransTable(oldthing->tracer->alpha, trans);
+	else
+		trans = R_GetThingTransTable(oldthing->alpha, trans);
+
 	// Check if this sprite needs to be rendered like a shadow
 	shadowdraw = (!!(thing->renderflags & RF_SHADOWDRAW) && !(papersprite || splat));
 	shadoweffects = (thing->renderflags & RF_SHADOWEFFECTS);
@@ -3629,6 +3674,7 @@ boolean R_ThingVisible (mobj_t *thing)
 		(thing->sprite == SPR_NULL) || // Don't draw null-sprites
 		(thing->flags2 & MF2_DONTDRAW) || // Don't draw MF2_LINKDRAW objects
 		(thing->drawonlyforplayer && thing->drawonlyforplayer != viewplayer) || // Don't draw other players' personal objects
+		(!R_BlendLevelVisible(thing->blendmode, R_GetThingTransTable(thing->alpha, 0))) ||
 		(!P_MobjWasRemoved(r_viewmobj) && (
 		  (r_viewmobj == thing) || // Don't draw first-person players or awayviewmobj objects
 		  (r_viewmobj->player && r_viewmobj->player->followmobj == thing) || // Don't draw first-person players' followmobj
diff --git a/src/r_things.h b/src/r_things.h
index 3daf55c4235bc753aeb5f1c897856e6d9303338c..ae21d2404bc1793c569194f22ed0a8e815c89e46 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -93,6 +93,7 @@ boolean R_ThingIsFullDark (mobj_t *thing);
 boolean R_ThingIsFlashing (mobj_t *thing);
 
 UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 translation);
+transnum_t R_GetThingTransTable(fixed_t alpha, transnum_t transmap);
 
 void R_ThingOffsetOverlay (mobj_t *thing, fixed_t *outx, fixed_t *outy);
 
diff --git a/src/r_translation.c b/src/r_translation.c
index a53b30e9f6fc6196b72df18b3f6063534bda3219..53bb0e6a0fd12fa1291d697db462b647d8909503 100644
--- a/src/r_translation.c
+++ b/src/r_translation.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2006 by Randy Heit.
-// Copyright (C) 2023 by Sonic Team Junior.
+// Copyright (C) 2023-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_translation.h b/src/r_translation.h
index 1eb40233d12070708d9bda26bfa8b53792235352..9cae1a6470d71ae76647a4c6f70b821d0f615d6a 100644
--- a/src/r_translation.h
+++ b/src/r_translation.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2006 by Randy Heit.
-// Copyright (C) 2023 by Sonic Team Junior.
+// Copyright (C) 2023-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/s_sound.c b/src/s_sound.c
index aa049899ff602cf6a1f7af4c287d8f3673bf5597..0329c289ee104e56cf1c156683690d65455aeed5 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -2256,9 +2256,9 @@ void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32
 		return;
 
 	strncpy(newmusic, mmusic, sizeof(newmusic)-1);
+	newmusic[6] = 0;
 	if (LUA_HookMusicChange(music_name, &hook_param))
 		return;
-	newmusic[6] = 0;
 
 	// No Music (empty string)
 	if (newmusic[0] == 0)
@@ -2494,11 +2494,11 @@ static void Command_Tunes_f(void)
 		track = (UINT16)atoi(COM_Argv(2))-1;
 
 	strncpy(mapmusname, tunearg, 7);
+	mapmusname[6] = 0;
 
 	if (argc > 4)
 		position = (UINT32)atoi(COM_Argv(4));
 
-	mapmusname[6] = 0;
 	mapmusflags = (track & MUSIC_TRACKMASK);
 	mapmusposition = position;
 
diff --git a/src/s_sound.h b/src/s_sound.h
index d64778fc37db6fffdf8027ad7aa3cb4f338a6a33..589060c8c2a45ce25deeee80db521a4673f39dc2 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/screen.c b/src/screen.c
index 9a82a15618a4045d651d10aae38a8e963b6711a5..dc77a9a93a6395becb4ac9177625ae7f00c74cad 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -112,6 +112,10 @@ void SCR_SetDrawFuncs(void)
 		colfuncs[COLDRAWFUNC_SHADE] = R_DrawShadeColumn_8;
 		colfuncs[COLDRAWFUNC_SHADOWED] = R_DrawColumnShadowed_8;
 		colfuncs[COLDRAWFUNC_TRANSTRANS] = R_DrawTranslatedTranslucentColumn_8;
+		colfuncs[COLDRAWFUNC_CLAMPED] = R_DrawColumnClamped_8;
+		colfuncs[COLDRAWFUNC_CLAMPEDTRANS] = R_DrawTranslucentColumnClamped_8;
+		colfuncs[COLDRAWFUNC_TWOSMULTIPATCH] = R_Draw2sMultiPatchColumn_8;
+		colfuncs[COLDRAWFUNC_TWOSMULTIPATCHTRANS] = R_Draw2sMultiPatchTranslucentColumn_8;
 		colfuncs[COLDRAWFUNC_FOG] = R_DrawFogColumn_8;
 
 		spanfuncs[SPANDRAWFUNC_TRANS] = R_DrawTranslucentSpan_8;
@@ -120,6 +124,7 @@ void SCR_SetDrawFuncs(void)
 		spanfuncs[SPANDRAWFUNC_SPLAT] = R_DrawSplat_8;
 		spanfuncs[SPANDRAWFUNC_TRANSSPLAT] = R_DrawTranslucentSplat_8;
 		spanfuncs[SPANDRAWFUNC_TILTEDSPLAT] = R_DrawTiltedSplat_8;
+		spanfuncs[SPANDRAWFUNC_TILTEDTRANSSPLAT] = R_DrawTiltedTranslucentSplat_8;
 		spanfuncs[SPANDRAWFUNC_SPRITE] = R_DrawFloorSprite_8;
 		spanfuncs[SPANDRAWFUNC_TRANSSPRITE] = R_DrawTranslucentFloorSprite_8;
 		spanfuncs[SPANDRAWFUNC_TILTEDSPRITE] = R_DrawTiltedFloorSprite_8;
@@ -142,6 +147,7 @@ void SCR_SetDrawFuncs(void)
 		spanfuncs_npo2[SPANDRAWFUNC_SPLAT] = R_DrawSplat_NPO2_8;
 		spanfuncs_npo2[SPANDRAWFUNC_TRANSSPLAT] = R_DrawTranslucentSplat_NPO2_8;
 		spanfuncs_npo2[SPANDRAWFUNC_TILTEDSPLAT] = R_DrawTiltedSplat_NPO2_8;
+		spanfuncs_npo2[SPANDRAWFUNC_TILTEDTRANSSPLAT] = R_DrawTiltedTranslucentSplat_NPO2_8;
 		spanfuncs_npo2[SPANDRAWFUNC_SPRITE] = R_DrawFloorSprite_NPO2_8;
 		spanfuncs_npo2[SPANDRAWFUNC_TRANSSPRITE] = R_DrawTranslucentFloorSprite_NPO2_8;
 		spanfuncs_npo2[SPANDRAWFUNC_TILTEDSPRITE] = R_DrawTiltedFloorSprite_NPO2_8;
@@ -489,11 +495,9 @@ void SCR_ClosedCaptions(void)
 			basey -= 42;
 		else if (splitscreen)
 			basey -= 8;
-		else if ((modeattacking == ATTACKING_NIGHTS)
-		|| (!(maptol & TOL_NIGHTS)
-		&& LUA_HudEnabled(hud_powerups)
+		else if (LUA_HudEnabled(hud_powerups)
 		&& ((cv_powerupdisplay.value == 2) // "Always"
-		 || (cv_powerupdisplay.value == 1 && !camera.chase)))) // "First-person only"
+		 || (cv_powerupdisplay.value == 1 && !camera.chase))) // "First-person only"
 			basey -= 16;
 	}
 
diff --git a/src/screen.h b/src/screen.h
index 8b952e553a18c30f9cbdaf323bdbc01aa11f0fe4..3ce593a52e8595e7412eb9c20c29c91ba9c057bb 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -30,10 +30,6 @@
 #define NUMSCREENS 5
 #endif
 
-// Size of statusbar.
-#define ST_HEIGHT 32
-#define ST_WIDTH 320
-
 // used now as a maximum video mode size for extra vesa modes.
 
 // we try to re-allocate a minimum of buffers for stability of the memory,
@@ -97,6 +93,10 @@ enum
 	COLDRAWFUNC_SHADE,
 	COLDRAWFUNC_SHADOWED,
 	COLDRAWFUNC_TRANSTRANS,
+	COLDRAWFUNC_CLAMPED,
+	COLDRAWFUNC_CLAMPEDTRANS,
+	COLDRAWFUNC_TWOSMULTIPATCH,
+	COLDRAWFUNC_TWOSMULTIPATCHTRANS,
 	COLDRAWFUNC_FOG,
 
 	COLDRAWFUNC_MAX
@@ -115,6 +115,7 @@ enum
 	SPANDRAWFUNC_SPLAT,
 	SPANDRAWFUNC_TRANSSPLAT,
 	SPANDRAWFUNC_TILTEDSPLAT,
+	SPANDRAWFUNC_TILTEDTRANSSPLAT,
 
 	SPANDRAWFUNC_SPRITE,
 	SPANDRAWFUNC_TRANSSPRITE,
diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt
index 5aca04d120c39287eec5c2097aa6375e52eb27fb..8950846ee4f43cf794bd6d2505bbf039e6c71d2f 100644
--- a/src/sdl/CMakeLists.txt
+++ b/src/sdl/CMakeLists.txt
@@ -1,7 +1,6 @@
 # Declare SDL2 interface sources
 
 target_sources(SRB2SDL2 PRIVATE
-	mixer_sound.c
 	ogl_sdl.c
 	i_threads.c
 	i_net.c
@@ -13,6 +12,164 @@ target_sources(SRB2SDL2 PRIVATE
 	hwsym_sdl.c
 )
 
+# Compatibility flag with later versions of GCC
+# We should really fix our code to not need this
+if (NOT SRB2_CONFIG_FORCE_NO_MS_BITFIELDS)
+	if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+		check_cxx_compiler_flag("-mno-ms-bitfields" HAS_NO_MS_BITFIELDS)
+		if(HAS_NO_MS_BITFIELDS)
+			target_compile_options(SRB2SDL2 PRIVATE -mno-ms-bitfields)
+		endif()
+	endif()
+endif()
+
+# Yes we know we use insecure CRT functions...
+if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
+    target_compile_definitions(SRB2SDL2 PRIVATE -D_CRT_SECURE_NO_WARNINGS)
+endif()
+
+# Compiler warnings configuration
+target_compile_options(SRB2SDL2 PRIVATE
+    # Using generator expressions to handle per-language compile options
+
+    # C, GNU
+    # This is a direct translation from versions.mk
+    $<$<AND:$<COMPILE_LANGUAGE:C>,$<C_COMPILER_ID:GNU>>:
+        -Wall
+        -Wno-trigraphs
+        -W # Was controlled by RELAXWARNINGS
+        -Wfloat-equal
+        -Wundef
+        -Wpointer-arith
+        -Wbad-function-cast
+        -Wcast-qual
+        -Wcast-align # Was controlled by NOCASTALIGNWARN
+        -Wwrite-strings
+        -Wsign-compare
+        -Wmissing-prototypes
+        -Wmissing-declarations
+        -Wmissing-noreturn
+        -Winline
+        -Wformat-y2k
+        -Wformat-security
+
+        $<$<VERSION_LESS:$<C_COMPILER_VERSION>,2.9.5>:
+            -Wno-div-by-zero
+            -Wendif-labels
+            -Wdisabled-optimization
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,4.0.0>:
+            -Wold-style-definition
+            -Wmissing-field-initializers
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,4.1.0>:
+            -Wshadow
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,4.3.0>:
+            -funit-at-a-time
+            -Wlogical-op
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,4.5.0>:
+            -Wlogical-op
+            -Wno-error=array-bounds
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,4.6.0>:
+            -Wno-suggest-attribute=noreturn
+            -Wno-error=suggest-attribute=noreturn
+            -Werror=vla
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,5.4.0>:
+            -Wno-logical-op
+            -Wno-error=logical-op
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,6.1.0>:
+            -Wno-tautological-compare
+            -Wno-error=tautological-compare
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,7.1.0>:
+            -Wno-error=format-overflow=2
+            -Wimplicit-fallthrough=4
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,8.1.0>:
+            -Wno-error=format-overflow
+            -Wno-error=stringop-truncation
+            -Wno-error=stringop-overflow
+            -Wno-format-overflow
+            -Wno-stringop-truncation
+            -Wno-stringop-overflow
+            -Wno-error=multistatement-macros
+        >
+
+        $<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,9.1.0>:
+            -Wno-error=address-of-packed-member
+        >
+    >
+
+    # C, Clang and Apple Clang
+    $<$<AND:$<COMPILE_LANGUAGE:C>,$<OR:$<C_COMPILER_ID:AppleClang>,$<C_COMPILER_ID:Clang>>>:
+        -Wall
+        -Wno-absolute-value
+        -Wno-trigraphs
+        -Wno-error=non-literal-null-conversion
+        -Wno-error=constant-conversion
+        -Wno-unused-but-set-variable
+        -Wno-error=unused-but-set-variable
+        -Wno-shorten-64-to-32
+    >
+
+    # C, MSVC
+    $<$<AND:$<COMPILE_LANGUAGE:C>,$<C_COMPILER_ID:MSVC>>:
+        # All warnings at and before Visual Studio 2019 RTM
+        # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warnings-by-compiler-version?view=msvc-170
+        /Wv:19.20.27004.0
+    >
+
+    # C++, GNU, Clang and Apple Clang
+    $<$<AND:$<COMPILE_LANGUAGE:CXX>,$<OR:$<C_COMPILER_ID:GNU>,$<C_COMPILER_ID:AppleClang>,$<C_COMPILER_ID:Clang>>>:
+        -Wall
+        -Wno-unused-function
+        -Wno-unused-but-set-variable
+        -Wno-unused-private-field
+    >
+
+    # C++, MSVC
+    $<$<AND:$<COMPILE_LANGUAGE:CXX>,$<C_COMPILER_ID:MSVC>>:
+        /Wv:19.20.27004.0
+    >
+
+    # GNU
+    $<$<C_COMPILER_ID:GNU>:
+        -fmax-errors=5
+    >
+)
+if(SRB2_CONFIG_ERRORMODE)
+    target_compile_options(SRB2SDL2 PRIVATE
+        $<$<OR:$<C_COMPILER_ID:GNU>,$<C_COMPILER_ID:AppleClang>,$<C_COMPILER_ID:Clang>>:
+            -Werror
+        >
+
+        $<$<C_COMPILER_ID:MSVC>:
+            /WX
+        >
+    )
+endif()
+
+# Link warnings configuration
+target_link_options(SRB2SDL2 PRIVATE
+    $<$<C_COMPILER_ID:GNU>:
+        # -Wl,--as-needed   - Was controlled by NOLDWARNING
+    >
+)
+
 if("${CMAKE_SYSTEM_NAME}" MATCHES Windows)
 	target_sources(SRB2SDL2 PRIVATE
 		../win32/win_dbg.c
@@ -62,12 +219,6 @@ if("${CMAKE_SYSTEM_NAME}" MATCHES Windows)
 	target_link_libraries(SRB2SDL2 PRIVATE SDL2::SDL2main)
 endif()
 
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}" AND NOT "${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}")
-	target_link_libraries(SRB2SDL2 PRIVATE SDL2::SDL2-static SDL2_mixer::SDL2_mixer-static)
-else()
-	target_link_libraries(SRB2SDL2 PRIVATE SDL2::SDL2 SDL2_mixer::SDL2_mixer)
-endif()
-
 if("${CMAKE_SYSTEM_NAME}" MATCHES Linux)
 	target_link_libraries(SRB2SDL2 PRIVATE m rt)
 endif()
@@ -81,8 +232,84 @@ if("${CMAKE_SYSTEM_NAME}" MATCHES Windows)
 	)
 endif()
 
-target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_MIXER -DSOUND=SOUND_MIXER)
+find_package(SDL2 CONFIG REQUIRED)
+find_package(SDL2 REQUIRED)
+if(TARGET SDL2main)
+	target_link_libraries(SRB2SDL2 PRIVATE SDL2main)
+endif()
 target_compile_definitions(SRB2SDL2 PRIVATE -DDIRECTFULLSCREEN -DHAVE_SDL)
+target_include_directories(SRB2SDL2 PRIVATE ${SDL2_INCLUDE_DIR})
+
+find_package(SDL2_mixer_ext CONFIG QUIET)
+find_package(SDL2_mixer_ext QUIET)
+if(TARGET SDL2_mixer_ext::SDL2_mixer_ext OR TARGET SDL2_mixer_ext::SDL2_mixer_ext_Static OR TARGET SDL2_mixer_ext OR TARGET SDL2_mixer_ext_Static)
+	if(NOT TARGET SDL2_mixer_ext::SDL2_mixer_ext_Static AND TARGET SDL2_mixer_ext_Static)
+	add_library(SDL2_mixer_ext::SDL2_mixer_ext_Static ALIAS SDL2_mixer_ext_Static)
+	endif()
+	if(NOT TARGET SDL2_mixer_ext::SDL2_mixer_ext AND TARGET SDL2_mixer_ext)
+	add_library(SDL2_mixer_ext::SDL2_mixer_ext ALIAS SDL2_mixer_ext)
+	endif()
+	if(TARGET SDL2_mixer_ext::SDL2_mixer_ext OR TARGET SDL2_mixer_ext::SDL2_mixer_ext_Static)
+		if(TARGET SDL2_mixer_ext::SDL2_mixer_ext_Static)
+			target_link_libraries(SRB2SDL2 PRIVATE SDL2_mixer_ext::SDL2_mixer_ext_Static)
+		else()
+			target_link_libraries(SRB2SDL2 PRIVATE SDL2_mixer_ext::SDL2_mixer_ext)
+		endif()
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_MIXER -DHAVE_MIXERX -DSOUND=SOUND_MIXER)
+	endif()
+endif()
+
+if(TARGET SDL2_mixer_ext::SDL2_mixer_ext OR TARGET SDL2_mixer_ext::SDL2_mixer_ext_Static)
+	message(STATUS "SDL2_mixer_ext found, skipping SDL2_mixer")
+else()
+	message(STATUS "SDL2_mixer_ext not found, going to try SDL2_mixer")
+	find_package(SDL2_mixer CONFIG QUIET)
+	find_package(SDL2_mixer QUIET)
+	if(TARGET SDL2_mixer::SDL2_mixer OR TARGET SDL2_mixer::SDL2_mixer-static OR TARGET SDL2_mixer OR TARGET SDL2_mixer_Static)
+		if(NOT TARGET SDL2_mixer::SDL2_mixer-static AND TARGET SDL2_mixer_Static)
+		add_library(SDL2_mixer::SDL2_mixer-static ALIAS SDL2_mixer_Static)
+		endif()
+		if(NOT TARGET SDL2_mixer::SDL2_mixer AND TARGET SDL2_mixer)
+		add_library(SDL2_mixer::SDL2_mixer ALIAS SDL2_mixer)
+		endif()
+		if(TARGET SDL2_mixer::SDL2_mixer OR TARGET SDL2_mixer::SDL2_mixer-static)
+			if(TARGET SDL2_mixer::SDL2_mixer-static)
+				target_link_libraries(SRB2SDL2 PRIVATE SDL2_mixer::SDL2_mixer-static)
+			else()
+				target_link_libraries(SRB2SDL2 PRIVATE SDL2_mixer::SDL2_mixer)
+			endif()
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_MIXER -DSOUND=SOUND_MIXER)
+		endif()
+	endif()
+endif()
+
+if(TARGET SDL2_mixer_ext::SDL2_mixer_ext OR TARGET SDL2_mixer_ext::SDL2_mixer_ext_Static OR TARGET SDL2_mixer::SDL2_mixer OR TARGET SDL2_mixer::SDL2_mixer-static)
+	target_sources(SRB2SDL2 PRIVATE mixer_sound.c)
+	target_link_libraries(SRB2SDL2 PRIVATE SDL2::SDL2)
+else()
+	target_sources(SRB2SDL2 PRIVATE sdl_sound.c)
+	target_link_libraries(SRB2SDL2 PRIVATE SDL2::SDL2)
+endif()
+
+if(TARGET SDL2::SDL2)
+	message(STATUS "SDL2 Found")
+else()
+	message(STATUS "no SDL2 Found")
+endif()
+
+if(TARGET SDL2::SDL2main)
+	message(STATUS "SDL2main Found")
+else()
+	message(STATUS "No SDL2main Found")
+endif()
+
+if(TARGET SDL2_mixer_ext::SDL2_mixer_ext OR TARGET SDL2_mixer_ext::SDL2_mixer_ext_Static)
+	message(STATUS "SDL2_mixer_ext Found")
+elseif(TARGET SDL2_mixer::SDL2_mixer OR TARGET SDL2_mixer::SDL2_mixer-static)
+	message(STATUS "SDL2_mixer found")
+else()
+	message(STATUS "no SDL2_mixer_ext or SDL2_mixer Found")
+endif()
 
 #### Installation ####
 if("${CMAKE_SYSTEM_NAME}" MATCHES Darwin)
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index 86ffa7082ac3210321a06c3b477ba3b6239947b9..2a25f33031adee58358a423e0251937995e5471c 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -447,6 +447,7 @@
     <ClCompile Include="..\blua\lauxlib.c" />
     <ClCompile Include="..\blua\lbaselib.c" />
     <ClCompile Include="..\blua\lcode.c" />
+    <ClCompile Include="..\blua\ldblib.c" />
     <ClCompile Include="..\blua\ldebug.c" />
     <ClCompile Include="..\blua\ldo.c" />
     <ClCompile Include="..\blua\ldump.c" />
@@ -512,6 +513,7 @@
     <ClCompile Include="..\lua_hudlib_drawlist.c" />
     <ClCompile Include="..\lua_infolib.c" />
     <ClCompile Include="..\lua_inputlib.c" />
+	<ClCompile Include="..\lua_interceptlib.c" />
     <ClCompile Include="..\lua_maplib.c" />
     <ClCompile Include="..\lua_mathlib.c" />
     <ClCompile Include="..\lua_mobjlib.c" />
@@ -641,4 +643,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index d2f9d018f2b3ef44e59def85634aa46ef24acd95..8daaa7773f091d5dfbf6bac965b58160380bb50c 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -604,6 +604,9 @@
     <ClCompile Include="..\blua\ldebug.c">
       <Filter>BLUA</Filter>
     </ClCompile>
+    <ClCompile Include="..\blua\ldblib.c">
+      <Filter>BLUA</Filter>
+    </ClCompile>
     <ClCompile Include="..\blua\ldo.c">
       <Filter>BLUA</Filter>
     </ClCompile>
@@ -780,6 +783,9 @@
     </ClCompile>
     <ClCompile Include="..\lua_inputlib.c">
       <Filter>LUA</Filter>
+    </ClCompile>
+	<ClCompile Include="..\lua_interceptlib.c">
+      <Filter>LUA</Filter>
     </ClCompile>
     <ClCompile Include="..\lua_maplib.c">
       <Filter>LUA</Filter>
@@ -1125,4 +1131,4 @@
       <Filter>SDLApp</Filter>
     </Image>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/sdl/hwsym_sdl.c b/src/sdl/hwsym_sdl.c
index ca87fcc7951758e9a5de7ca1a405f88b1f202204..f561266017dd9fcf20b467b3c5e9323be7f5dbe2 100644
--- a/src/sdl/hwsym_sdl.c
+++ b/src/sdl/hwsym_sdl.c
@@ -112,6 +112,7 @@ void *hwSym(const char *funcName,void *handle)
 
 	GETFUNC(SetPaletteLookup);
 	GETFUNC(CreateLightTable);
+	GETFUNC(UpdateLightTable);
 	GETFUNC(ClearLightTables);
 	GETFUNC(SetScreenPalette);
 
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 9f0fa0250a664dfe020d649ac65ccb347a63a3c8..b2396e269b8d7cc456ee9f525aeda7991835cd4e 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -5,7 +5,7 @@
 //
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Portions Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 2014-2023 by Sonic Team Junior.
+// Copyright (C) 2014-2025 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
@@ -78,10 +78,11 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #include "SDL_cpuinfo.h"
 #define HAVE_SDLCPUINFO
 
-#if defined (__unix__) || defined(__APPLE__) || (defined (UNIXCOMMON) && !defined (__HAIKU__))
-#if defined (__linux__)
-#include <sys/vfs.h>
+#if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
+#if defined (__linux__) || defined (__HAIKU__)
+#include <sys/statvfs.h>
 #else
+#include <sys/statvfs.h>
 #include <sys/param.h>
 #include <sys/mount.h>
 /*For meminfo*/
@@ -94,7 +95,7 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #endif
 #endif
 
-#if defined (__linux__) || (defined (UNIXCOMMON) && !defined (__HAIKU__))
+#if defined (__linux__) || defined (UNIXCOMMON)
 #ifndef NOTERMIOS
 #include <termios.h>
 #include <sys/ioctl.h> // ioctl
@@ -109,8 +110,10 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #if defined (__unix__) || (defined (UNIXCOMMON) && !defined (__APPLE__))
 #include <errno.h>
 #include <sys/wait.h>
+#ifndef __HAIKU__ // haiku's crash dialog is just objectively better
 #define NEWSIGNALHANDLER
 #endif
+#endif
 
 #ifndef NOMUMBLE
 #ifdef __linux__ // need -lrt
@@ -271,75 +274,86 @@ SDL_bool framebuffer = SDL_FALSE;
 UINT8 keyboard_started = false;
 
 #ifdef UNIXBACKTRACE
-#define STDERR_WRITE(string) if (fd != -1) I_OutputMsg("%s", string)
-#define CRASHLOG_WRITE(string) if (fd != -1) junk = write(fd, string, strlen(string))
-#define CRASHLOG_STDERR_WRITE(string) \
-	if (fd != -1)\
-		junk = write(fd, string, strlen(string));\
-	I_OutputMsg("%s", string)
+
+static void bt_write_file(int fd, const char *string) {
+	ssize_t written = 0;
+	ssize_t sourcelen = strlen(string);
+
+	while (fd != -1 && (written != -1 && errno != EINTR) && written < sourcelen)
+		written = write(fd, string, sourcelen);
+}
+
+static void bt_write_stderr(const char *string) {
+	bt_write_file(STDERR_FILENO, string);
+}
+
+static void bt_write_all(int fd, const char *string) {
+	bt_write_file(fd, string);
+	bt_write_file(STDERR_FILENO, string);
+}
 
 static void write_backtrace(INT32 signal)
 {
 	int fd = -1;
-#ifndef NOEXECINFO
-	size_t size;
-#endif
 	time_t rawtime;
 	struct tm timeinfo;
-	ssize_t junk;
 
 	enum { BT_SIZE = 1024, STR_SIZE = 32 };
 #ifndef NOEXECINFO
-	void *array[BT_SIZE];
+	void *funcptrs[BT_SIZE];
+	size_t bt_size;
 #endif
 	char timestr[STR_SIZE];
 
-	const char *error = "An error occurred within SRB2! Send this stack trace to someone who can help!\n";
-	const char *error2 = "(Or find crash-log.txt in your SRB2 directory.)\n"; // Shown only to stderr.
+	const char *filename = va("%s" PATHSEP "%s", srb2home, "crash-log.txt");
 
-	fd = open(va("%s" PATHSEP "%s", srb2home, "crash-log.txt"), O_CREAT|O_APPEND|O_RDWR, S_IRUSR|S_IWUSR);
+	fd = open(filename, O_CREAT|O_APPEND|O_RDWR, S_IRUSR|S_IWUSR);
 
-	if (fd == -1)
-		I_OutputMsg("\nWARNING: Couldn't open crash log for writing! Make sure your permissions are correct. Please save the below report!\n");
+	if (fd == -1) // File handle error
+		bt_write_stderr("\nWARNING: Couldn't open crash log for writing! Make sure your permissions are correct. Please save the below report!\n");
 
 	// Get the current time as a string.
 	time(&rawtime);
 	localtime_r(&rawtime, &timeinfo);
 	strftime(timestr, STR_SIZE, "%a, %d %b %Y %T %z", &timeinfo);
 
-	CRASHLOG_WRITE("------------------------\n"); // Nice looking seperator
+	bt_write_file(fd, "------------------------\n"); // Nice looking seperator
+
+	bt_write_all(fd, "\n"); // Newline to look nice for both outputs.
+	bt_write_all(fd, "An error occurred within SRB2! Send this stack trace to someone who can help!\n");
 
-	CRASHLOG_STDERR_WRITE("\n"); // Newline to look nice for both outputs.
-	CRASHLOG_STDERR_WRITE(error); // "Oops, SRB2 crashed" message
-	STDERR_WRITE(error2); // Tell the user where the crash log is.
+	if (fd != -1) // If the crash log exists,
+		bt_write_stderr("(Or find crash-log.txt in your SRB2 directory.)\n"); // tell the user where the crash log is.
 
 	// Tell the log when we crashed.
-	CRASHLOG_WRITE("Time of crash: ");
-	CRASHLOG_WRITE(timestr);
-	CRASHLOG_WRITE("\n");
+	bt_write_file(fd, "Time of crash: ");
+	bt_write_file(fd, timestr);
+	bt_write_file(fd, "\n");
 
 	// Give the crash log the cause and a nice 'Backtrace:' thing
 	// The signal is given to the user when the parent process sees we crashed.
-	CRASHLOG_WRITE("Cause: ");
-	CRASHLOG_WRITE(strsignal(signal));
-	CRASHLOG_WRITE("\n"); // Newline for the signal name
+	bt_write_file(fd, "Cause: ");
+	bt_write_file(fd, strsignal(signal));
+	bt_write_file(fd, "\n"); // Newline for the signal name
 
-#ifndef NOEXECINFO
-	CRASHLOG_STDERR_WRITE("\nBacktrace:\n");
+#ifdef NOEXECINFO
+	bt_write_all(fd, "\nNo Backtrace support\n");
+#else
+	bt_write_all(fd, "\nBacktrace:\n");
 
 	// Flood the output and log with the backtrace
-	size = backtrace(array, BT_SIZE);
-	backtrace_symbols_fd(array, size, fd);
-	backtrace_symbols_fd(array, size, STDERR_FILENO);
+	bt_size = backtrace(funcptrs, BT_SIZE);
+	backtrace_symbols_fd(funcptrs, bt_size, fd);
+	backtrace_symbols_fd(funcptrs, bt_size, STDERR_FILENO);
 #endif
 
-	CRASHLOG_WRITE("\n"); // Write another newline to the log so it looks nice :)
-	(void)junk;
-	close(fd);
+	bt_write_file(fd, "\n"); // Write another newline to the log so it looks nice :)
+	if (fd != -1) {
+		fsync(fd); // reaaaaally make sure we got that data written.
+		close(fd);
+	}
 }
-#undef STDERR_WRITE
-#undef CRASHLOG_WRITE
-#undef CRASHLOG_STDERR_WRITE
+
 #endif // UNIXBACKTRACE
 
 static void I_ReportSignal(int num, int coredumped)
@@ -466,10 +480,9 @@ typedef struct
 
 feild_t tty_con;
 
-// when printing general stuff to stdout stderr (Sys_Printf)
-//   we need to disable the tty console stuff
-// this increments so we can recursively disable
-static INT32 ttycon_hide = 0;
+// lock to prevent clearing partial lines, since not everything
+// printed ends on a newline.
+static boolean ttycon_ateol = true;
 // some key codes that the terminal may be using
 // TTimo NOTE: I'm not sure how relevant this is
 static INT32 tty_erase;
@@ -497,63 +510,31 @@ static inline void tty_FlushIn(void)
 // TTimo NOTE: it seems on some terminals just sending '\b' is not enough
 //   so for now, in any case we send "\b \b" .. yeah well ..
 //   (there may be a way to find out if '\b' alone would work though)
+// Hanicef NOTE: using \b this way is unreliable because of terminal state,
+//   it's better to use \r to reset the cursor to the beginning of the
+//   line and clear from there.
 static void tty_Back(void)
 {
-	char key;
-	ssize_t d;
-	key = '\b';
-	d = write(STDOUT_FILENO, &key, 1);
-	key = ' ';
-	d = write(STDOUT_FILENO, &key, 1);
-	key = '\b';
-	d = write(STDOUT_FILENO, &key, 1);
-	(void)d;
-}
-
-static void tty_Clear(void)
-{
-	size_t i;
+	write(STDOUT_FILENO, "\r", 1);
 	if (tty_con.cursor>0)
 	{
-		for (i=0; i<tty_con.cursor; i++)
-		{
-			tty_Back();
-		}
-	}
-
-}
-
-// clear the display of the line currently edited
-// bring cursor back to beginning of line
-static inline void tty_Hide(void)
-{
-	//I_Assert(consolevent);
-	if (ttycon_hide)
-	{
-		ttycon_hide++;
-		return;
+		write(STDOUT_FILENO, tty_con.buffer, tty_con.cursor);
 	}
-	tty_Clear();
-	ttycon_hide++;
+	write(STDOUT_FILENO, " \b", 2);
 }
 
-// show the current line
-// FIXME TTimo need to position the cursor if needed??
-static inline void tty_Show(void)
+static void tty_Clear(void)
 {
 	size_t i;
-	ssize_t d;
-	//I_Assert(consolevent);
-	I_Assert(ttycon_hide>0);
-	ttycon_hide--;
-	if (ttycon_hide == 0 && tty_con.cursor)
+	write(STDOUT_FILENO, "\r", 1);
+	if (tty_con.cursor>0)
 	{
 		for (i=0; i<tty_con.cursor; i++)
 		{
-			d = write(STDOUT_FILENO, tty_con.buffer+i, 1);
+			write(STDOUT_FILENO, " ", 1);
 		}
+		write(STDOUT_FILENO, "\r", 1);
 	}
-	(void)d;
 }
 
 // never exit without calling this, or your terminal will be left in a pretty bad state
@@ -661,6 +642,11 @@ void I_GetConsoleEvents(void)
 				tty_con.cursor = 0;
 				ev.key = KEY_ENTER;
 			}
+			else if (key == 0x4) // ^D, aka EOF
+			{
+				// shut down, most unix programs behave this way
+				I_Quit();
+			}
 			else continue;
 		}
 		else if (tty_con.cursor < sizeof (tty_con.buffer))
@@ -868,9 +854,16 @@ static void I_RegisterChildSignals(void)
 void I_OutputMsg(const char *fmt, ...)
 {
 	size_t len;
-	char txt[8192];
+	char *txt;
 	va_list  argptr;
 
+	va_start(argptr,fmt);
+	len = vsnprintf(NULL, 0, fmt, argptr);
+	va_end(argptr);
+	if (len == 0)
+		return;
+
+	txt = malloc(len+1);
 	va_start(argptr,fmt);
 	vsprintf(txt, fmt, argptr);
 	va_end(argptr);
@@ -904,7 +897,10 @@ void I_OutputMsg(const char *fmt, ...)
 		DWORD bytesWritten;
 
 		if (co == INVALID_HANDLE_VALUE)
+		{
+			free(txt);
 			return;
+		}
 
 		if (GetFileType(co) == FILE_TYPE_CHAR && GetConsoleMode(co, &bytesWritten))
 		{
@@ -920,11 +916,16 @@ void I_OutputMsg(const char *fmt, ...)
 			if (oldLength > 0)
 			{
 				LPVOID blank = malloc(oldLength);
-				if (!blank) return;
+				if (!blank)
+				{
+					free(txt);
+					return;
+				}
 				memset(blank, ' ', oldLength); // Blank out.
 				oldLines = malloc(oldLength*sizeof(TCHAR));
 				if (!oldLines)
 				{
+					free(txt);
 					free(blank);
 					return;
 				}
@@ -959,18 +960,20 @@ void I_OutputMsg(const char *fmt, ...)
 	}
 #else
 #ifdef HAVE_TERMIOS
-	if (consolevent)
+	if (consolevent && ttycon_ateol)
 	{
-		tty_Hide();
+		tty_Clear();
+		ttycon_ateol = false;
 	}
 #endif
 
 	if (!framebuffer)
 		fprintf(stderr, "%s", txt);
 #ifdef HAVE_TERMIOS
-	if (consolevent)
+	if (consolevent && txt[len-1] == '\n')
 	{
-		tty_Show();
+		write(STDOUT_FILENO, tty_con.buffer, tty_con.cursor);
+		ttycon_ateol = true;
 	}
 #endif
 
@@ -979,6 +982,7 @@ void I_OutputMsg(const char *fmt, ...)
 		fflush(stderr);
 
 #endif
+	free(txt);
 }
 
 //
@@ -2296,16 +2300,25 @@ void I_Sleep(UINT32 ms)
 
 void I_SleepDuration(precise_t duration)
 {
-#if defined(__linux__) || defined(__FreeBSD__)
+#if defined(__linux__) || defined(__FreeBSD__) || defined(__HAIKU__)
 	UINT64 precision = I_GetPrecisePrecision();
-	struct timespec ts = {
-		.tv_sec = duration / precision,
-		.tv_nsec = duration * 1000000000 / precision % 1000000000,
-	};
-	int status;
-	do status = clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts);
-	while (status == EINTR);
-#else
+	precise_t dest = I_GetPreciseTime() + duration;
+	precise_t slack = (precision / 5000); // 0.2 ms slack
+	if (duration > slack)
+	{
+		duration -= slack;
+		struct timespec ts = {
+			.tv_sec = duration / precision,
+			.tv_nsec = duration * 1000000000 / precision % 1000000000,
+		};
+		int status;
+		do status = clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts);
+		while (status == EINTR);
+	}
+
+	// busy-wait the rest
+	while (((INT64)dest - (INT64)I_GetPreciseTime()) > 0);
+#elif defined (MIN_SLEEP_DURATION_MS)
 	UINT64 precision = I_GetPrecisePrecision();
 	INT32 sleepvalue = cv_sleep.value;
 	UINT64 delaygranularity;
@@ -2743,18 +2756,13 @@ void I_ShutdownSystem(void)
 void I_GetDiskFreeSpace(INT64 *freespace)
 {
 #if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
-#if defined (SOLARIS) || defined (__HAIKU__)
-	*freespace = INT32_MAX;
-	return;
-#else // Both Linux and BSD have this, apparently.
-	struct statfs stfs;
-	if (statfs(srb2home, &stfs) == -1)
+	struct statvfs stfs;
+	if (statvfs(srb2home, &stfs) == -1)
 	{
 		*freespace = INT32_MAX;
 		return;
 	}
 	*freespace = stfs.f_bavail * stfs.f_bsize;
-#endif
 #elif defined (_WIN32)
 	static p_GetDiskFreeSpaceExA pfnGetDiskFreeSpaceEx = NULL;
 	static boolean testwin95 = false;
@@ -2976,7 +2984,7 @@ static void pathonly(char *s)
 */
 static const char *searchWad(const char *searchDir)
 {
-	static char tempsw[256] = "";
+	static char tempsw[MAX_WADPATH] = "";
 	filestatus_t fstemp;
 
 	strcpy(tempsw, WADKEYWORD1);
@@ -2992,8 +3000,8 @@ static const char *searchWad(const char *searchDir)
 
 #define CHECKWADPATH(ret) \
 do { \
-	I_OutputMsg(",%s", returnWadPath); \
-	if (isWadPathOk(returnWadPath)) \
+	I_OutputMsg(",%s", ret); \
+	if (isWadPathOk(ret)) \
 		return ret; \
 } while (0)
 
@@ -3022,7 +3030,9 @@ static const char *locateWad(void)
 #ifndef NOCWD
 	// examine current dir
 	strcpy(returnWadPath, ".");
-	CHECKWADPATH(NULL);
+	I_OutputMsg(",%s", returnWadPath);
+	if (isWadPathOk(returnWadPath))
+		return NULL;
 #endif
 
 #ifdef __APPLE__
@@ -3039,9 +3049,16 @@ static const char *locateWad(void)
 
 #ifndef NOHOME
 	// find in $HOME
-	I_OutputMsg(",HOME");
+	I_OutputMsg(",HOME/" DEFAULTDIR);
 	if ((envstr = I_GetEnv("HOME")) != NULL)
-		SEARCHWAD(envstr);
+	{
+		char *tmp = malloc(strlen(envstr) + 1 + sizeof(DEFAULTDIR));
+		strcpy(tmp, envstr);
+		strcat(tmp, "/");
+		strcat(tmp, DEFAULTDIR);
+		CHECKWADPATH(tmp);
+		free(tmp);
+	}
 #endif
 
 	// search paths
@@ -3067,8 +3084,10 @@ const char *I_LocateWad(void)
 	{
 		// change to the directory where we found srb2.pk3
 #if defined (_WIN32)
+		waddir = _fullpath(NULL, waddir, MAX_PATH);
 		SetCurrentDirectoryA(waddir);
 #else
+		waddir = realpath(waddir, NULL);
 		if (chdir(waddir) == -1)
 			I_OutputMsg("Couldn't change working directory\n");
 #endif
@@ -3275,4 +3294,18 @@ const char *I_GetSysName(void)
 	return SDL_GetPlatform();
 }
 
+
+void I_SetTextInputMode(boolean active)
+{
+	if (active)
+		SDL_StartTextInput();
+	else
+		SDL_StopTextInput();
+}
+
+boolean I_GetTextInputMode(void)
+{
+	return SDL_IsTextInputActive();
+}
+
 #endif
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 76fe172fce0e27f46ceb491be5a43f1b3e72f770..a1ff94480ecbfbfe37fa5337831c8245790cf556 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -87,7 +87,7 @@
 #endif
 
 // maximum number of windowed modes (see windowedModes[][])
-#define MAXWINMODES (18)
+#define MAXWINMODES (21)
 
 /**	\brief
 */
@@ -157,7 +157,9 @@ static INT32 windowedModes[MAXWINMODES][2] =
 	{1920,1080}, // 1.66
 	{1680,1050}, // 1.60,5.25
 	{1600,1200}, // 1.33
+	{1600,1000}, // 1.60,5.00
 	{1600, 900}, // 1.66
+	{1536, 864}, // 1.66,4.80
 	{1366, 768}, // 1.66
 	{1440, 900}, // 1.60,4.50
 	{1280,1024}, // 1.33?
@@ -166,6 +168,7 @@ static INT32 windowedModes[MAXWINMODES][2] =
 	{1280, 720}, // 1.66
 	{1152, 864}, // 1.33,3.60
 	{1024, 768}, // 1.33,3.20
+	{ 960, 600}, // 1.60,3.00
 	{ 800, 600}, // 1.33,2.50
 	{ 640, 480}, // 1.33,2.00
 	{ 640, 400}, // 1.60,2.00
@@ -382,6 +385,8 @@ static INT32 Impl_SDL_Scancode_To_Keycode(SDL_Scancode code)
 
 static boolean ShouldIgnoreMouse(void)
 {
+	if (cv_alwaysgrabmouse.value)
+		return false;
 	if (menuactive)
 		return !M_MouseNeeded();
 	if (paused || con_destlines || chat_on)
@@ -1704,7 +1709,7 @@ static void Impl_VideoSetupBuffer(void)
 	vid.direct = NULL;
 	if (vid.buffer)
 		free(vid.buffer);
-	vid.buffer = calloc(vid.rowbytes*vid.height, NUMSCREENS);
+	vid.buffer = calloc(NUMSCREENS, vid.rowbytes*vid.height);
 	if (!vid.buffer)
 	{
 		I_Error("%s", M_GetText("Not enough memory for video buffer\n"));
@@ -1852,6 +1857,9 @@ void I_StartupGraphics(void)
 	if (mousegrabok && !disable_mouse)
 		SDLdoGrabMouse();
 
+	// disable text input right off the bat, since we don't need it at the start.
+	I_SetTextInputMode(textinputmodeenabledbylua);
+
 	graphics_started = true;
 }
 
@@ -1899,6 +1907,7 @@ void VID_StartupOpenGL(void)
 
 		HWD.pfnSetPaletteLookup = hwSym("SetPaletteLookup",NULL);
 		HWD.pfnCreateLightTable = hwSym("CreateLightTable",NULL);
+		HWD.pfnUpdateLightTable = hwSym("UpdateLightTable",NULL);
 		HWD.pfnClearLightTables = hwSym("ClearLightTables",NULL);
 		HWD.pfnSetScreenPalette = hwSym("SetScreenPalette",NULL);
 
@@ -1943,8 +1952,6 @@ void I_ShutdownGraphics(void)
 	I_OutputMsg("shut down\n");
 
 #ifdef HWRENDER
-	if (GLUhandle)
-		hwClose(GLUhandle);
 	if (sdlglcontext)
 	{
 		SDL_GL_DeleteContext(sdlglcontext);
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 8f2ca340879d5236822c534b2f9da57774a5356e..0f9983ee1d10d84b20ade3dfa8b208ade4d353ee 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -135,7 +135,7 @@ static void Midiplayer_Onchange(void)
 	if (Mix_GetMidiPlayer() != cv_midiplayer.value)
 	{
 		if (Mix_SetMidiPlayer(cv_midiplayer.value)) // <> 0 means error
-			CONS_Alert(CONS_ERROR, "Midi player error: %s", Mix_GetError());
+			CONS_Alert(CONS_ERROR, "Midi player error: %s\n", Mix_GetError());
 		else
 			restart = true;
 	}
@@ -143,7 +143,7 @@ static void Midiplayer_Onchange(void)
 	if (!Mix_GetSoundFonts() || stricmp(Mix_GetSoundFonts(), cv_midisoundfontpath.string))
 	{
 		if (!Mix_SetSoundFonts(cv_midisoundfontpath.string)) // == 0 means error
-			CONS_Alert(CONS_ERROR, "Sound font error: %s", Mix_GetError());
+			CONS_Alert(CONS_ERROR, "Sound font error: %s\n", Mix_GetError());
 		else
 			restart = true;
 	}
@@ -190,7 +190,7 @@ static void MidiSoundfontPath_Onchange(void)
 		if (proceed)
 		{
 			if (!Mix_SetSoundFonts(cv_midisoundfontpath.string))
-				CONS_Alert(CONS_ERROR, "Sound font error: %s", Mix_GetError());
+				CONS_Alert(CONS_ERROR, "Sound font error: %s\n", Mix_GetError());
 			else
 				S_StartEx(true);
 		}
@@ -199,7 +199,18 @@ static void MidiSoundfontPath_Onchange(void)
 
 // make sure that s_sound.c does not already verify these
 // which happens when: defined(HAVE_MIXERX) && !defined(HAVE_MIXER)
-static CV_PossibleValue_t midiplayer_cons_t[] = {{MIDI_OPNMIDI, "OPNMIDI"}, {MIDI_Fluidsynth, "Fluidsynth"}, {MIDI_Timidity, "Timidity"}, {MIDI_Native, "Native"}, {0, NULL}};
+static CV_PossibleValue_t midiplayer_cons_t[] = {
+	{MIDI_ADLMIDI,    "ADLMIDI"},
+	{MIDI_OPNMIDI,    "OPNMIDI"},
+	{MIDI_Timidity,   "Timidity"},
+	{MIDI_Fluidsynth, "Fluidsynth"},
+#if SDL_MIXER_VERSION_ATLEAST(2,6,0)
+	{MIDI_EDMIDI,     "EDMIDI"},
+#endif
+	{MIDI_Native,     "Native"},
+	{MIDI_ANY,        "Any"},
+	{0,               NULL}
+};
 consvar_t cv_midiplayer = CVAR_INIT ("midiplayer", "OPNMIDI" /*MIDI_OPNMIDI*/, CV_CALL|CV_NOINIT|CV_SAVE, midiplayer_cons_t, Midiplayer_Onchange);
 consvar_t cv_midisoundfontpath = CVAR_INIT ("midisoundfont", "sf2/8bitsf.SF2", CV_CALL|CV_NOINIT|CV_SAVE, NULL, MidiSoundfontPath_Onchange);
 consvar_t cv_miditimiditypath = CVAR_INIT ("midisoundbank", "./timidity", CV_SAVE, NULL, NULL);
diff --git a/src/sdl/ogl_sdl.c b/src/sdl/ogl_sdl.c
index e7347547e224b43f2fcdf66e527c8adc2681e243..c78b43ec4097cafad8420ceb7497e2883220a67e 100644
--- a/src/sdl/ogl_sdl.c
+++ b/src/sdl/ogl_sdl.c
@@ -70,18 +70,10 @@ PFNglGetString pglGetString;
 /**	\brief SDL video display surface
 */
 INT32 oglflags = 0;
-void *GLUhandle = NULL;
 SDL_GLContext sdlglcontext = 0;
 
 void *GetGLFunc(const char *proc)
 {
-	if (strncmp(proc, "glu", 3) == 0)
-	{
-		if (GLUhandle)
-			return hwSym(proc, GLUhandle);
-		else
-			return NULL;
-	}
 	return SDL_GL_GetProcAddress(proc);
 }
 
@@ -89,7 +81,6 @@ boolean LoadGL(void)
 {
 #ifndef STATIC_OPENGL
 	const char *OGLLibname = NULL;
-	const char *GLULibname = NULL;
 
 	if (M_CheckParm("-OGLlib") && M_IsNextParm())
 		OGLLibname = M_GetNextParm();
@@ -102,43 +93,6 @@ boolean LoadGL(void)
 			CONS_Printf("If you know what is the OpenGL library's name, use -OGLlib\n");
 		return 0;
 	}
-
-#if 0
-	GLULibname = "/proc/self/exe";
-#elif defined (_WIN32)
-	GLULibname = "GLU32.DLL";
-#elif defined (__MACH__)
-	GLULibname = "/System/Library/Frameworks/OpenGL.framework/Libraries/libGLU.dylib";
-#elif defined (macintos)
-	GLULibname = "OpenGLLibrary";
-#elif defined (__unix__)
-	GLULibname = "libGLU.so.1";
-#elif defined (__HAIKU__)
-	GLULibname = "libGLU.so";
-#else
-	GLULibname = NULL;
-#endif
-
-	if (M_CheckParm("-GLUlib") && M_IsNextParm())
-		GLULibname = M_GetNextParm();
-
-	if (GLULibname)
-	{
-		GLUhandle = hwOpen(GLULibname);
-		if (GLUhandle)
-			return SetupGLfunc();
-		else
-		{
-			CONS_Alert(CONS_ERROR, "Could not load GLU Library: %s\n", GLULibname);
-			if (!M_CheckParm ("-GLUlib"))
-				CONS_Alert(CONS_ERROR, "If you know what is the GLU library's name, use -GLUlib\n");
-		}
-	}
-	else
-	{
-		CONS_Alert(CONS_ERROR, "Could not load GLU Library\n");
-		CONS_Alert(CONS_ERROR, "If you know what is the GLU library's name, use -GLUlib\n");
-	}
 #endif
 	return SetupGLfunc();
 }
@@ -155,6 +109,7 @@ boolean OglSdlSurface(INT32 w, INT32 h)
 {
 	INT32 cbpp = cv_scr_depth.value < 16 ? 16 : cv_scr_depth.value;
 	static boolean first_init = false;
+	static int majorGL = 0, minorGL = 0;
 
 	oglflags = 0;
 
@@ -189,6 +144,12 @@ boolean OglSdlSurface(INT32 w, INT32 h)
 	else
 		maximumAnisotropy = 1;
 
+	if (sscanf((const char*)gl_version, "%d.%d", &majorGL, &minorGL)
+		&& (!(majorGL == 1 && minorGL <= 3)))
+		supportMipMap = true;
+	else
+		supportMipMap = false;
+
 	SetupGLFunc4();
 
 	glanisotropicmode_cons_t[1].value = maximumAnisotropy;
diff --git a/src/sdl/ogl_sdl.h b/src/sdl/ogl_sdl.h
index bd1d699ff44db27a4599bfb81d4f65e34ac7f858..87df5e5e6115334dd8705658d70f9f6e5b6706f8 100644
--- a/src/sdl/ogl_sdl.h
+++ b/src/sdl/ogl_sdl.h
@@ -19,8 +19,6 @@
 
 #include "../v_video.h"
 
-extern void *GLUhandle;
-
 boolean OglSdlSurface(INT32 w, INT32 h);
 
 void OglSdlFinishUpdate(boolean vidwait);
diff --git a/src/snake.c b/src/snake.c
index 2349d5fdbfb6d66af7d4bc55df03b553052153c8..1276daafd0ae83f738a9cea0becefbc252097f20 100644
--- a/src/snake.c
+++ b/src/snake.c
@@ -23,7 +23,7 @@
 #define SPEED 5
 
 #define NUM_BLOCKS_X 20
-#define NUM_BLOCKS_Y 10
+#define NUM_BLOCKS_Y 8
 #define BLOCK_SIZE 12
 #define BORDER_SIZE 12
 
@@ -32,7 +32,7 @@
 
 #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 BOTTOM_Y (BASEVIDHEIGHT - 76)
 #define TOP_Y (BOTTOM_Y - MAP_HEIGHT - BORDER_SIZE * 2 + 1)
 
 enum bonustype_s {
@@ -582,7 +582,7 @@ boolean Snake_JoyGrabber(void *opaque, event_t *ev)
 {
 	snake_t *snake = opaque;
 
-	if (ev->type == ev_joystick  && ev->key == 0)
+	if (snake != NULL && ev->type == ev_joystick  && ev->key == 0)
 	{
 		snake->joyevents[snake->joyeventcount] = ev;
 		snake->joyeventcount++;
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 7df6f8848dd80c2cb0eb5839fd0389b7c86a3b8b..4fdacd51ada1871c4a711f91749e0d9b39872796 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1042,6 +1042,9 @@ static void ST_drawInput(void)
 
 	INT32 x = hudinfo[HUD_INPUT].x, y = hudinfo[HUD_INPUT].y;
 
+	if (hu_showscores)
+		return;
+
 	if (stplyr->powers[pw_carry] == CR_NIGHTSMODE)
 		y += 8;
 	else if (modeattacking || !LUA_HudEnabled(hud_lives))
@@ -1173,10 +1176,8 @@ static void ST_drawInput(void)
 	V_DrawFill(x+16+(xoffs), y+(yoffs)-offs, 10, 10, col);\
 	V_DrawCharacter(x+16+1+(xoffs), y+1+(yoffs)-offs, hudinfo[HUD_INPUT].f|symb, false)
 
-	drawbutt( 4,-3, BT_JUMP,   'J' );
-	drawbutt(15,-3, BT_SPIN,   'S' );
-	drawbutt(26,-3, BT_SHIELD, '\0'); // Instead of a wide 'J' or 'S', we'll draw a thin "SH" for Shield
-	V_DrawThinString(x+16+26, y+2+(-3)-offs, hudinfo[HUD_LIVES].f, "SH");
+	drawbutt( 4,-3, BT_JUMP, 'J');
+	drawbutt(15,-3, BT_SPIN, 'S');
 
 	V_DrawFill(x+16+4, y+8, 21, 10, hudinfo[HUD_INPUT].f|20); // sundial backing
 	if (stplyr->mo)
@@ -1255,6 +1256,8 @@ static void ST_drawInput(void)
 		V_DrawThinString(x, y, hudinfo[HUD_INPUT].f|((leveltime & 4) ? V_YELLOWMAP : V_REDMAP), "BAD DEMO!!");
 }
 
+static boolean lt_active = false;
+
 static patch_t *lt_patches[3];
 static INT32 lt_scroll = 0;
 static INT32 lt_mom = 0;
@@ -1269,19 +1272,19 @@ tic_t lt_exitticker = 0, lt_endtime = 0;
 //
 static void ST_cacheLevelTitle(void)
 {
-#define SETPATCH(default, warning, custom, idx) \
+#define SETPATCH(def, warning, custom, idx) \
 { \
 	lumpnum_t patlumpnum = LUMPERROR; \
 	if (mapheaderinfo[gamemap-1]->custom[0] != '\0') \
 	{ \
-		patlumpnum = W_CheckNumForName(mapheaderinfo[gamemap-1]->custom); \
+		patlumpnum = W_CheckNumForPatchName(mapheaderinfo[gamemap-1]->custom); \
 		if (patlumpnum != LUMPERROR) \
 			lt_patches[idx] = (patch_t *)W_CachePatchNum(patlumpnum, PU_HUDGFX); \
 	} \
 	if (patlumpnum == LUMPERROR) \
 	{ \
 		if (!(mapheaderinfo[gamemap-1]->levelflags & LF_WARNINGTITLE)) \
-			lt_patches[idx] = (patch_t *)W_CachePatchName(default, PU_HUDGFX); \
+			lt_patches[idx] = (patch_t *)W_CachePatchName(def, PU_HUDGFX); \
 		else \
 			lt_patches[idx] = (patch_t *)W_CachePatchName(warning, PU_HUDGFX); \
 	} \
@@ -1303,6 +1306,7 @@ void ST_startTitleCard(void)
 	ST_cacheLevelTitle();
 
 	// initialize HUD variables
+	lt_active = true;
 	lt_ticker = lt_exitticker = lt_lasttic = 0;
 	lt_endtime = 2*TICRATE + (10*NEWTICRATERATIO);
 	lt_scroll = BASEVIDWIDTH * FRACUNIT;
@@ -1311,21 +1315,11 @@ void ST_startTitleCard(void)
 }
 
 //
-// What happens before drawing the title card.
-// Which is just setting the HUD translucency.
+// Stops the title card.
 //
-void ST_preDrawTitleCard(void)
+void ST_stopTitleCard(void)
 {
-	if (!G_IsTitleCardAvailable())
-		return;
-
-	if (lt_ticker >= (lt_endtime + TICRATE))
-		return;
-
-	if (!lt_exitticker)
-		st_translucency = 0;
-	else
-		st_translucency = max(0, min((INT32)lt_exitticker-4, cv_translucenthud.value));
+	lt_active = false;
 }
 
 //
@@ -1334,47 +1328,43 @@ void ST_preDrawTitleCard(void)
 //
 void ST_runTitleCard(void)
 {
-	boolean run = !(paused || P_AutoPause());
-
-	if (!G_IsTitleCardAvailable())
-		return;
-
-	if (lt_ticker >= (lt_endtime + TICRATE))
+	if (!lt_active || ((paused || P_AutoPause()) && lt_ticker >= PRELEVELTIME))
 		return;
 
-	if (run || (lt_ticker < PRELEVELTIME))
+	if (!lt_exitticker)
 	{
-		// tick
-		lt_ticker++;
-		if (lt_ticker >= lt_endtime)
-			lt_exitticker++;
+		if (abs(lt_scroll) > FRACUNIT)
+			lt_scroll -= (lt_scroll>>2);
+		else
+			lt_scroll = 0;
 
-		// scroll to screen (level title)
-		if (!lt_exitticker)
-		{
-			if (abs(lt_scroll) > FRACUNIT)
-				lt_scroll -= (lt_scroll>>2);
-			else
-				lt_scroll = 0;
-		}
-		// scroll away from screen (level title)
+		if (abs(lt_zigzag) > FRACUNIT)
+			lt_zigzag -= (lt_zigzag>>2);
 		else
+			lt_zigzag = 0;
+	}
+	else
+	{
+		lt_mom -= FRACUNIT*6;
+
+		if (lt_scroll > -BASEVIDWIDTH * FRACUNIT * 2)
 		{
-			lt_mom -= FRACUNIT*6;
 			lt_scroll += lt_mom;
 		}
 
-		// scroll to screen (zigzag)
-		if (!lt_exitticker)
+		if (lt_zigzag > -(lt_patches[1]->width)*FRACUNIT)
 		{
-			if (abs(lt_zigzag) > FRACUNIT)
-				lt_zigzag -= (lt_zigzag>>2);
-			else
-				lt_zigzag = 0;
-		}
-		// scroll away from screen (zigzag)
-		else
 			lt_zigzag += lt_mom;
+		}
+	}
+
+	lt_ticker++;
+	lt_exitticker = max((signed)lt_ticker - (signed)lt_endtime, 0);
+
+	if (lt_ticker >= (lt_endtime + TICRATE))
+	{
+		lt_active = false;
+		return;
 	}
 }
 
@@ -1401,9 +1391,6 @@ void ST_drawTitleCard(void)
 
 	colormap = R_GetTranslationColormap(TC_DEFAULT, colornum, GTC_CACHE);
 
-	if (!G_IsTitleCardAvailable())
-		return;
-
 	if (!LUA_HudEnabled(hud_stagetitle))
 		goto luahook;
 
@@ -1484,13 +1471,14 @@ void ST_preLevelTitleCardDrawer(void)
 //
 void ST_drawWipeTitleCard(void)
 {
+	if (!lt_active)
+		return;
+
 	stplyr = &players[consoleplayer];
-	ST_preDrawTitleCard();
 	ST_drawTitleCard();
 	if (splitscreen)
 	{
 		stplyr = &players[secondarydisplayplayer];
-		ST_preDrawTitleCard();
 		ST_drawTitleCard();
 	}
 }
@@ -2078,24 +2066,25 @@ static void ST_drawNiGHTSHUD(void)
 	if (!stplyr->exiting && !oldspecialstage && LUA_HudEnabled(hud_nightsscore))
 		ST_DrawNightsOverlayNum(304<<FRACBITS, 14<<FRACBITS, FRACUNIT, V_PERPLAYER|V_SNAPTOTOP|V_SNAPTORIGHT, stplyr->marescore, nightsnum, SKINCOLOR_AZURE);
 
-	// TODO give this its own section for Lua
+	// TODO: give this its own section for Lua
+	// TODO: on multi-mare maps, show time & grade for each completed mare
 	if (!stplyr->exiting && LUA_HudEnabled(hud_nightsscore))
 	{
 		if (modeattacking == ATTACKING_NIGHTS)
 		{
 			INT32 maretime = max(stplyr->realtime - stplyr->marebegunat, 0);
 
-#define VFLAGS V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_PERPLAYER|V_HUDTRANS
-			V_DrawScaledPatch(BASEVIDWIDTH-22, BASEVIDHEIGHT-20, VFLAGS, W_CachePatchName("NGRTIMER", PU_HUDGFX));
-			V_DrawPaddedTallNum(BASEVIDWIDTH-22, BASEVIDHEIGHT-20, VFLAGS, G_TicsToCentiseconds(maretime), 2);
-			V_DrawScaledPatch(BASEVIDWIDTH-46, BASEVIDHEIGHT-20, VFLAGS, sboperiod);
+#define VFLAGS V_SNAPTOTOP|V_SNAPTORIGHT|V_PERPLAYER|V_HUDTRANS
+			V_DrawScaledPatch(BASEVIDWIDTH-16, 40, VFLAGS, W_CachePatchName("NGRTIMER", PU_HUDGFX));
+			V_DrawPaddedTallNum(BASEVIDWIDTH-16, 40, VFLAGS, G_TicsToCentiseconds(maretime), 2);
+			V_DrawScaledPatch(BASEVIDWIDTH-40, 40, VFLAGS, sboperiod);
 			if (maretime < 60*TICRATE)
-				V_DrawTallNum(BASEVIDWIDTH-46, BASEVIDHEIGHT-20, VFLAGS, G_TicsToSeconds(maretime));
+				V_DrawTallNum(BASEVIDWIDTH-40, 40, VFLAGS, G_TicsToSeconds(maretime));
 			else
 			{
-				V_DrawPaddedTallNum(BASEVIDWIDTH-46, BASEVIDHEIGHT-20, VFLAGS, G_TicsToSeconds(maretime), 2);
-				V_DrawScaledPatch(BASEVIDWIDTH-70, BASEVIDHEIGHT-20, VFLAGS, sbocolon);
-				V_DrawTallNum(BASEVIDWIDTH-70, BASEVIDHEIGHT-20, VFLAGS, G_TicsToMinutes(maretime, true));
+				V_DrawPaddedTallNum(BASEVIDWIDTH-40, 40, VFLAGS, G_TicsToSeconds(maretime), 2);
+				V_DrawScaledPatch(BASEVIDWIDTH-64, 40, VFLAGS, sbocolon);
+				V_DrawTallNum(BASEVIDWIDTH-64, 40, VFLAGS, G_TicsToMinutes(maretime, true));
 			}
 #undef VFLAGS
 		}
@@ -2665,7 +2654,7 @@ static boolean ST_doItemFinderIconsAndSound(void)
 	// Scan thinkers to find emblem mobj with these ids
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+		if (th->removing)
 			continue;
 
 		mo2 = (mobj_t *)th;
@@ -2701,24 +2690,14 @@ static boolean ST_doItemFinderIconsAndSound(void)
 	return true;
 }
 
+static boolean drawstagetitle = false;
+
 //
 // Draw the status bar overlay, customisable: the user chooses which
 // kind of information to overlay
 //
 static void ST_overlayDrawer(void)
 {
-	// Decide whether to draw the stage title or not
-	boolean stagetitle = false;
-
-	// Check for a valid level title
-	// If the HUD is enabled
-	// And, if Lua is running, if the HUD library has the stage title enabled
-	if (G_IsTitleCardAvailable() && *mapheaderinfo[gamemap-1]->lvlttl != '\0' && !(hu_showscores && (netgame || multiplayer)))
-	{
-		stagetitle = true;
-		ST_preDrawTitleCard();
-	}
-
 	// hu_showscores = auto hide score/time/rings when tab rankings are shown
 	if (!(hu_showscores && (netgame || multiplayer)))
 	{
@@ -2841,7 +2820,7 @@ static void ST_overlayDrawer(void)
 		}
 		else if (cv_powerupdisplay.value == 2 && LUA_HudEnabled(hud_powerups))
 			ST_drawPowerupHUD();  // same as it ever was...
-		
+
 	}
 	else if (!(netgame || multiplayer) && cv_powerupdisplay.value == 2 && LUA_HudEnabled(hud_powerups))
 		ST_drawPowerupHUD(); // same as it ever was...
@@ -2858,7 +2837,7 @@ static void ST_overlayDrawer(void)
 	}
 
 	// draw level title Tails
-	if (stagetitle && (!WipeInAction) && (!WipeStageTitle))
+	if (drawstagetitle && !WipeInAction && !WipeStageTitle)
 		ST_drawTitleCard();
 
 	if (!hu_showscores && (netgame || multiplayer) && LUA_HudEnabled(hud_textspectator))
@@ -2929,7 +2908,22 @@ void ST_Drawer(void)
 		}
 	}
 
-	st_translucency = cv_translucenthud.value;
+	// Decide whether to draw the stage title or not
+	if (lt_active)
+	{
+		drawstagetitle = !(hu_showscores && (netgame || multiplayer));
+
+		if (!lt_exitticker)
+			st_translucency = 0;
+		else
+			st_translucency = max(0, min((INT32)lt_exitticker-4, cv_translucenthud.value));
+	}
+	else
+	{
+		st_translucency = cv_translucenthud.value;
+
+		drawstagetitle = false;
+	}
 
 	if (st_overlay)
 	{
diff --git a/src/st_stuff.h b/src/st_stuff.h
index 6f6bac1fa47fd5b4b71a850383138ad9821456ee..a2fb8a0ed7328f27de43f750e32a3c9c7f390eae 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -49,9 +49,9 @@ void ST_doPaletteStuff(void);
 
 // title card
 void ST_startTitleCard(void);
+void ST_stopTitleCard(void);
 void ST_runTitleCard(void);
 void ST_drawTitleCard(void);
-void ST_preDrawTitleCard(void);
 void ST_preLevelTitleCardDrawer(void);
 void ST_drawWipeTitleCard(void);
 
diff --git a/src/string.c b/src/string.c
index b24e12e6e36fec3078aaae76bee7a765f9c30c8f..79573283e10bee24ef70a394899c6f5585f67521 100644
--- a/src/string.c
+++ b/src/string.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2006      by Graue.
-// Copyright (C) 2006-2023 by Sonic Team Junior.
+// Copyright (C) 2006-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -83,7 +83,7 @@ char *xstrtok(char *line, const char *delims)
 		return NULL;
 
 	p = saveline; // save start of this token
-	
+
 	saveline += strcspn(saveline, delims); // get the number of non-delims characters, go past delimiter
 
 	if(*saveline != '\0') // trash the delim if necessary
@@ -91,4 +91,3 @@ char *xstrtok(char *line, const char *delims)
 
 	return p;
 }
-
diff --git a/src/tables.c b/src/tables.c
index d0fb428ba8909653cc7f1d340caa9590f1373391..8e9075be259be149d1bb68686201973ccad70ae1 100644
--- a/src/tables.c
+++ b/src/tables.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 // Copyright (C) 2009 by Stephen McGranahan.
 //
 // This program is free software distributed under the
diff --git a/src/tables.h b/src/tables.h
index 65d7f72433177f77d711c5e4b5809ed2f9d6e2b8..3c0dd2a81ff97a6ccc382716bf136a897b1c4e75 100644
--- a/src/tables.h
+++ b/src/tables.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
deleted file mode 100644
index 28c4ce492f2d8b4e7590e3ac10f9ce6a95d98a0f..0000000000000000000000000000000000000000
--- a/src/tests/CMakeLists.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-target_sources(srb2tests PRIVATE
-	boolcompat.cpp
-)
diff --git a/src/tests/boolcompat.cpp b/src/tests/boolcompat.cpp
deleted file mode 100644
index fee40cd36f2bce34217a875024d6b41fe71adbd1..0000000000000000000000000000000000000000
--- a/src/tests/boolcompat.cpp
+++ /dev/null
@@ -1,8 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "../doomtype.h"
-
-TEST_CASE("C++ bool is convertible to doomtype.h boolean") {
-	REQUIRE(static_cast<boolean>(true) == 1);
-	REQUIRE(static_cast<boolean>(false) == 0);
-}
diff --git a/src/v_video.c b/src/v_video.c
index 3beb435b58428491e5b62f3ce48ec35535c25f60..39a1001d199952077fc783875b4d104c819a8fcd 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1137,7 +1137,7 @@ void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c)
 	}
 #endif
 
-	
+
 
 	if (splitscreen && (c & V_PERPLAYER))
 	{
@@ -1953,7 +1953,7 @@ char *V_FontWordWrap(INT32 x, INT32 w, INT32 option, fixed_t scale, const char *
 	INT32 spacewidth = font.spacewidth, charwidth = 0;
 
 	slen = strlen(string);
-	
+
 	if (w == 0)
 		w = BASEVIDWIDTH;
 	w -= x;
@@ -2131,7 +2131,7 @@ void V_DrawAlignedFontStringAtFixed(fixed_t x, fixed_t y, INT32 option, fixed_t
 				lx = x - (V_FontStringWidth(line, option, font)*pscale);
 				break;
 		}
-		
+
 		V_DrawFontStringAtFixed(lx, ly, option, pscale, vscale, line, font);
 
 		ly += FixedMul(((option & V_RETURN8) ? 8 : font.linespacing)<<FRACBITS, vscale);
@@ -2422,7 +2422,7 @@ INT32 V_FontStringWidth(const char *string, INT32 option, fontdef_t font)
 			if (wline < w) wline = w;
 			w = 0;
 			continue;
-		}	
+		}
 		if (string[i] & 0x80)
 			continue;
 
@@ -2455,7 +2455,7 @@ INT32 V_FontStringHeight(const char *string, INT32 option, fontdef_t font)
 			{
 				result += (option & V_RETURN8) ? 8 : font.linespacing;
 				h = 0;
-			}	
+			}
 			continue;
 		}
 
diff --git a/src/v_video.h b/src/v_video.h
index d37762466147ae843c8f0fba5218f4e8af3f6462..bdab23e8ad6cbd0c5868525da6f91500742e7e0f 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/version.h b/src/version.h
index 8d8f8978e7ea3cf593247a73a9460ed139193c10..54cf92bf1ab3ea66f6d7d267df292ea03f532965 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,4 +1,4 @@
-#define SRB2VERSION "2.2.14"/* this must be the first line, for cmake !! */
+#define SRB2VERSION "2.2.16"/* 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 55
+#define MODVERSION 57
 
 // Define this as a prerelease version suffix (pre#, RC#)
 #define BETAVERSION "nightly"
diff --git a/src/w_wad.c b/src/w_wad.c
index 78d26f9056c16e818d0384d1f91f2237d8b8024b..50ba622a9b32f538b18d50dcdf29fbfd457f7355 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -65,6 +65,7 @@
 #include "i_video.h" // rendermode
 #include "md5.h"
 #include "lua_script.h"
+#include "lua_hook.h"
 #ifdef SCANTHINGS
 #include "p_setup.h" // P_ScanThings
 #endif
@@ -98,6 +99,7 @@ typedef struct lumpnum_cache_s
 {
 	char lumpname[32];
 	lumpnum_t lumpnum;
+	UINT32 hash;
 } lumpnum_cache_t;
 
 static lumpnum_cache_t lumpnumcache[LUMPNUMCACHESIZE];
@@ -306,12 +308,10 @@ static void W_LoadDehackedLumps(UINT16 wadnum, boolean mainfile)
   * \param resblock resulting MD5 checksum
   * \return 0 if MD5 checksum was made, and is at resblock, 1 if error was found
   */
+
+#ifndef NOMD5
 static INT32 W_MakeFileMD5(const char *filename, void *resblock)
 {
-#ifdef NOMD5
-	(void)filename;
-	memset(resblock, 0x00, 16);
-#else
 	FILE *fhandle;
 
 	if ((fhandle = fopen(filename, "rb")) != NULL)
@@ -328,9 +328,9 @@ static INT32 W_MakeFileMD5(const char *filename, void *resblock)
 		fclose(fhandle);
 		return 0;
 	}
-#endif
 	return 1;
 }
+#endif
 
 // Invalidates the cache of lump numbers. Call this whenever a wad is added.
 static void W_InvalidateLumpnumCache(void)
@@ -1010,6 +1010,10 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		break;
 	}
 
+	lua_lumploading++;
+	LUA_HookVoid(HOOK(AddonLoaded));
+	lua_lumploading--;
+
 	W_InvalidateLumpnumCache();
 	return wadfile->numlumps;
 }
@@ -1170,6 +1174,11 @@ UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
 	W_ReadFileShaders(wadfile);
 	W_LoadTrnslateLumps(numwadfiles - 1);
 	W_LoadDehackedLumpsPK3(numwadfiles - 1, mainfile);
+
+	lua_lumploading++;
+	LUA_HookVoid(HOOK(AddonLoaded));
+	lua_lumploading--;
+
 	W_InvalidateLumpnumCache();
 
 	return wadfile->numlumps;
@@ -1328,10 +1337,10 @@ UINT16 W_CheckNumForFolderStartPK3(const char *name, UINT16 wad, UINT16 startlum
 			/* SLADE is special and puts a single directory entry. Skip that. */
 			if (strlen(lump_p->fullname) == name_length)
 				i++;
-			break;
+			return i;
 		}
 	}
-	return i;
+	return INT16_MAX;
 }
 
 // In a PK3 type of resource file, it looks for the next lumpinfo entry that doesn't share the specified pathfile.
@@ -1475,6 +1484,63 @@ UINT16 W_CheckNumForFullNamePK3(const char *name, UINT16 wad, UINT16 startlump)
 	return INT16_MAX;
 }
 
+static lumpnum_t CheckLumpInCache(const char *name, boolean longname)
+{
+	if (longname)
+	{
+		UINT32 hash = quickncasehash(name, 32);
+
+		// Loop backwards so that we check most recent entries first
+		for (INT32 i = lumpnumcacheindex + LUMPNUMCACHESIZE; i > lumpnumcacheindex; i--)
+		{
+			if (lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].hash == hash
+				&& stricmp(lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].lumpname, name) == 0)
+			{
+				lumpnumcacheindex = i & (LUMPNUMCACHESIZE - 1);
+				return lumpnumcache[lumpnumcacheindex].lumpnum;
+			}
+		}
+	}
+	else
+	{
+		UINT32 hash = quickncasehash(name, 8);
+
+		// Loop backwards so that we check most recent entries first
+		for (INT32 i = lumpnumcacheindex + LUMPNUMCACHESIZE; i > lumpnumcacheindex; i--)
+		{
+			if (lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].hash == hash
+				&& lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].lumpname[8] == '\0'
+				&& strnicmp(lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].lumpname, name, 8) == 0)
+			{
+				lumpnumcacheindex = i & (LUMPNUMCACHESIZE - 1);
+				return lumpnumcache[lumpnumcacheindex].lumpnum;
+			}
+		}
+	}
+
+	return LUMPERROR;
+}
+
+static void AddLumpToCache(lumpnum_t lumpnum, const char *name, boolean longname)
+{
+	if (longname && strlen(name) >= 32)
+		return;
+
+	lumpnumcacheindex = (lumpnumcacheindex + 1) & (LUMPNUMCACHESIZE - 1);
+	memset(lumpnumcache[lumpnumcacheindex].lumpname, '\0', 32);
+	if (longname)
+	{
+		strlcpy(lumpnumcache[lumpnumcacheindex].lumpname, name, 32);
+		lumpnumcache[lumpnumcacheindex].hash = quickncasehash(name, 32);
+	}
+	else
+	{
+		strncpy(lumpnumcache[lumpnumcacheindex].lumpname, name, 8);
+		lumpnumcache[lumpnumcacheindex].hash = quickncasehash(name, 8);
+	}
+	lumpnumcache[lumpnumcacheindex].lumpnum = lumpnum;
+}
+
 //
 // W_CheckNumForName
 // Returns LUMPERROR if name not found.
@@ -1487,17 +1553,10 @@ lumpnum_t W_CheckNumForName(const char *name)
 	if (!*name) // some doofus gave us an empty string?
 		return LUMPERROR;
 
-	// Check the lumpnumcache first. Loop backwards so that we check
-	// most recent entries first
-	for (i = lumpnumcacheindex + LUMPNUMCACHESIZE; i > lumpnumcacheindex; i--)
-	{
-		if (!lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].lumpname[8]
-			&& strncmp(lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].lumpname, name, 8) == 0)
-		{
-			lumpnumcacheindex = i & (LUMPNUMCACHESIZE - 1);
-			return lumpnumcache[lumpnumcacheindex].lumpnum;
-		}
-	}
+	// Check the lumpnumcache first.
+	lumpnum_t cachenum = CheckLumpInCache(name, false);
+	if (cachenum != LUMPERROR)
+		return cachenum;
 
 	// scan wad files backwards so patch lump files take precedence
 	for (i = numwadfiles - 1; i >= 0; i--)
@@ -1511,12 +1570,11 @@ lumpnum_t W_CheckNumForName(const char *name)
 	else
 	{
 		// Update the cache.
-		lumpnumcacheindex = (lumpnumcacheindex + 1) & (LUMPNUMCACHESIZE - 1);
-		memset(lumpnumcache[lumpnumcacheindex].lumpname, '\0', 32);
-		strncpy(lumpnumcache[lumpnumcacheindex].lumpname, name, 8);
-		lumpnumcache[lumpnumcacheindex].lumpnum = (i<<16)+check;
+		lumpnum_t lumpnum = (i << 16) + check;
+
+		AddLumpToCache(lumpnum, name, false);
 
-		return lumpnumcache[lumpnumcacheindex].lumpnum;
+		return lumpnum;
 	}
 }
 
@@ -1534,16 +1592,10 @@ lumpnum_t W_CheckNumForLongName(const char *name)
 	if (!*name) // some doofus gave us an empty string?
 		return LUMPERROR;
 
-	// Check the lumpnumcache first. Loop backwards so that we check
-	// most recent entries first
-	for (i = lumpnumcacheindex + LUMPNUMCACHESIZE; i > lumpnumcacheindex; i--)
-	{
-		if (strcmp(lumpnumcache[i & (LUMPNUMCACHESIZE - 1)].lumpname, name) == 0)
-		{
-			lumpnumcacheindex = i & (LUMPNUMCACHESIZE - 1);
-			return lumpnumcache[lumpnumcacheindex].lumpnum;
-		}
-	}
+	// Check the lumpnumcache first.
+	lumpnum_t cachenum = CheckLumpInCache(name, true);
+	if (cachenum != LUMPERROR)
+		return cachenum;
 
 	// scan wad files backwards so patch lump files take precedence
 	for (i = numwadfiles - 1; i >= 0; i--)
@@ -1556,16 +1608,12 @@ lumpnum_t W_CheckNumForLongName(const char *name)
 	if (check == INT16_MAX) return LUMPERROR;
 	else
 	{
-		if (strlen(name) < 32)
-		{
-			// Update the cache.
-			lumpnumcacheindex = (lumpnumcacheindex + 1) & (LUMPNUMCACHESIZE - 1);
-			memset(lumpnumcache[lumpnumcacheindex].lumpname, '\0', 32);
-			strlcpy(lumpnumcache[lumpnumcacheindex].lumpname, name, 32);
-			lumpnumcache[lumpnumcacheindex].lumpnum = (i << 16) + check;
-		}
+		// Update the cache.
+		lumpnum_t lumpnum = (i << 16) + check;
+
+		AddLumpToCache(lumpnum, name, true);
 
-		return (i << 16) + check;
+		return lumpnum;
 	}
 }
 
@@ -1647,6 +1695,166 @@ lumpnum_t W_GetNumForLongName(const char *name)
 	return i;
 }
 
+//
+// Same as W_CheckNumForNamePwad, but handles namespaces.
+//
+static UINT16 W_CheckNumForPatchNamePwad(const char *name, UINT16 wad, boolean longname)
+{
+	UINT16 i, start = INT16_MAX, end = INT16_MAX;
+	static char uname[8 + 1] = { 0 };
+	UINT32 hash = 0;
+	lumpinfo_t *lump_p;
+
+	if (!TestValidLump(wad,0))
+		return INT16_MAX;
+
+	if (!longname)
+	{
+		strlcpy(uname, name, sizeof uname);
+		strupr(uname);
+		hash = quickncasehash(uname, 8);
+	}
+
+	// SRB2 doesn't have a specific namespace for graphics, which means someone can do weird things
+	// like placing graphics inside a namespace it doesn't make sense for them to be in, like Sounds/ or SOC/
+	// So for now, this checks for lumps OUTSIDE of the flats namespace.
+	// When this situation changes, change the loops below to check for lumps INSIDE the namespaces to look in.
+	// TODO: cache namespace lump IDs
+	if (W_FileHasFolders(wadfiles[wad]))
+	{
+		start = W_CheckNumForFolderStartPK3("Flats/", wad, 0);
+		end = W_CheckNumForFolderEndPK3("Flats/", wad, start);
+
+		// if the start and end is the same, the folder is empty
+		if (end <= start)
+		{
+			start = INT16_MAX;
+			end = INT16_MAX;
+		}
+	}
+	else
+	{
+		start = W_CheckNumForMarkerStartPwad("F_START", wad, 0);
+		end = W_CheckNumForNamePwad("F_END", wad, start);
+		if (end != INT16_MAX)
+			end++;
+	}
+
+	lump_p = wadfiles[wad]->lumpinfo;
+
+	if (start == INT16_MAX)
+		start = wadfiles[wad]->numlumps;
+
+	for (i = 0; i < start; i++, lump_p++)
+	{
+		if ((!longname && lump_p->hash == hash && !strncmp(lump_p->name, uname, sizeof(uname) - 1))
+		|| (longname && stricmp(lump_p->longname, name) == 0))
+			return i;
+	}
+
+	if (end != INT16_MAX && start < end)
+	{
+		lump_p = wadfiles[wad]->lumpinfo + end;
+
+		for (i = end; i < wadfiles[wad]->numlumps; i++, lump_p++)
+		{
+			if ((!longname && lump_p->hash == hash && !strncmp(lump_p->name, uname, sizeof(uname) - 1))
+			|| (longname && stricmp(lump_p->longname, name) == 0))
+				return i;
+		}
+	}
+
+	// not found.
+	return INT16_MAX;
+}
+
+//
+// W_CheckNumForPatchNameInternal
+// Gets a lump number out of a patch name. Returns LUMPERROR if name not found.
+//
+static lumpnum_t W_CheckNumForPatchNameInternal(const char *name, boolean longname)
+{
+	INT32 i;
+	lumpnum_t check = INT16_MAX;
+
+	if (!*name) // some doofus gave us an empty string?
+		return LUMPERROR;
+
+	// Check the lumpnumcache first.
+	lumpnum_t cachenum = CheckLumpInCache(name, longname);
+	if (cachenum != LUMPERROR)
+		return cachenum;
+
+	// scan wad files backwards so patch lump files take precedence
+	for (i = numwadfiles - 1; i >= 0; i--)
+	{
+		check = W_CheckNumForPatchNamePwad(name,(UINT16)i,longname);
+		if (check != INT16_MAX)
+			break; //found it
+	}
+
+	if (check == INT16_MAX) return LUMPERROR;
+	else
+	{
+		// Update the cache.
+		lumpnum_t lumpnum = (i << 16) + check;
+
+		AddLumpToCache(lumpnum, name, longname);
+
+		return lumpnum;
+	}
+}
+
+//
+// W_CheckNumForPatchName
+// Wrapper for W_CheckNumForPatchNameInternal(name, false). Returns LUMPERROR if name not found.
+//
+lumpnum_t W_CheckNumForPatchName(const char *name)
+{
+	return W_CheckNumForPatchNameInternal(name, false);
+}
+
+//
+// Like W_CheckNumForPatchName, but can find entries with long names.
+// Wrapper for W_CheckNumForPatchNameInternal(name, true). Returns LUMPERROR if name not found.
+//
+lumpnum_t W_CheckNumForLongPatchName(const char *name)
+{
+	return W_CheckNumForPatchNameInternal(name, true);
+}
+
+//
+// W_GetNumForPatchName
+//
+// Calls W_CheckNumForPatchName, but bombs out if not found.
+//
+lumpnum_t W_GetNumForPatchName(const char *name)
+{
+	lumpnum_t i;
+
+	i = W_CheckNumForPatchName(name);
+
+	if (i == LUMPERROR)
+		I_Error("W_CheckNumForPatchName: %s not found!\n", name);
+
+	return i;
+}
+
+//
+// Like W_GetNumForPatchName, but can find entries with long names
+//
+lumpnum_t W_GetNumForLongPatchName(const char *name)
+{
+	lumpnum_t i;
+
+	i = W_CheckNumForLongPatchName(name);
+
+	if (i == LUMPERROR)
+		I_Error("W_GetNumForLongPatchName: %s not found!\n", name);
+
+	return i;
+}
+
 //
 // W_CheckNumForNameInBlock
 // Checks only in blocks from blockstart lump to blockend lump
@@ -2291,10 +2499,10 @@ void *W_CachePatchName(const char *name, INT32 tag)
 {
 	lumpnum_t num;
 
-	num = W_CheckNumForName(name);
+	num = W_CheckNumForPatchName(name);
 
 	if (num == LUMPERROR)
-		return W_CachePatchNum(W_GetNumForName("MISSING"), tag);
+		return W_CachePatchNum(W_GetNumForPatchName("MISSING"), tag);
 	return W_CachePatchNum(num, tag);
 }
 
@@ -2302,10 +2510,10 @@ void *W_CachePatchLongName(const char *name, INT32 tag)
 {
 	lumpnum_t num;
 
-	num = W_CheckNumForLongName(name);
+	num = W_CheckNumForLongPatchName(name);
 
 	if (num == LUMPERROR)
-		return W_CachePatchNum(W_GetNumForLongName("MISSING"), tag);
+		return W_CachePatchNum(W_GetNumForLongPatchName("MISSING"), tag);
 	return W_CachePatchNum(num, tag);
 }
 #ifndef NOMD5
@@ -2450,7 +2658,8 @@ static lumpchecklist_t folderblacklist[] =
 {
 	{"Lua/", 4},
 	{"SOC/", 4},
-	{"Sprites/",  8},
+	{"Sprites/", 8},
+	{"LongSprites/", 12},
 	{"Textures/", 9},
 	{"Patches/", 8},
 	{"Flats/", 6},
diff --git a/src/w_wad.h b/src/w_wad.h
index 80e0e32fd585faaddcaf24dd8167e3f694d388f2..84aafa3a40e2e48f662c723d5a5854138806b5b0 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -193,6 +193,12 @@ lumpnum_t W_CheckNumForName(const char *name);
 lumpnum_t W_CheckNumForLongName(const char *name);
 lumpnum_t W_GetNumForName(const char *name); // like W_CheckNumForName but I_Error on LUMPERROR
 lumpnum_t W_GetNumForLongName(const char *name);
+
+lumpnum_t W_CheckNumForPatchName(const char *name);
+lumpnum_t W_CheckNumForLongPatchName(const char *name);
+lumpnum_t W_GetNumForPatchName(const char *name); // like W_CheckNumForPatchName but I_Error on LUMPERROR
+lumpnum_t W_GetNumForLongPatchName(const char *name);
+
 lumpnum_t W_CheckNumForNameInBlock(const char *name, const char *blockstart, const char *blockend);
 UINT8 W_LumpExists(const char *name); // Lua uses this.
 
diff --git a/src/win32/Srb2win.rc b/src/win32/Srb2win.rc
index 46114f8717cc0a7396a7355084c25a2e704ac075..9fd8ea574b4866c9a060375926ec11a39372fd8f 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,14,0
- PRODUCTVERSION 2,2,14,0
+ FILEVERSION 2,2,16,0
+ PRODUCTVERSION 2,2,16,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -98,7 +98,7 @@ BEGIN
             VALUE "FileDescription", "Sonic Robo Blast 2\0"
             VALUE "FileVersion", VERSIONSTRING_RC
             VALUE "InternalName", "srb2\0"
-            VALUE "LegalCopyright", "Copyright 1998-2023 by Sonic Team Junior\0"
+            VALUE "LegalCopyright", "Copyright 1998-2025 by Sonic Team Junior\0"
             VALUE "LegalTrademarks", "Sonic the Hedgehog and related characters are trademarks of Sega.\0"
             VALUE "OriginalFilename", "srb2win.exe\0"
             VALUE "PrivateBuild", "\0"
diff --git a/src/y_inter.c b/src/y_inter.c
index 2add8645b0888ea460419b0165abeca702d6e29b..d7e644567eb14030e1bbb569434e1db460d8b56c 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2004-2023 by Sonic Team Junior.
+// Copyright (C) 2004-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -109,6 +109,7 @@ typedef union
 		UINT16 *color[MAXPLAYERS]; // Winner's color #
 		boolean spectator[MAXPLAYERS]; // Spectator list
 		UINT8 *character[MAXPLAYERS]; // Winner's character #
+		INT32 ctfteam[MAXPLAYERS]; // Winner's ctfteam #
 		INT32 num[MAXPLAYERS]; // Winner's player #
 		char *name[MAXPLAYERS]; // Winner's name
 		patch_t *result; // RESULT
@@ -579,9 +580,9 @@ void Y_IntermissionDrawer(void)
 		{
 			if (LUA_HudEnabled(hud_intermissiontitletext))
 			{
-				const char *ringtext = "\x82" "get 50 rings, then";
-				const char *tut1text = "\x82" "press " "\x80" "shield";
-				const char *tut2text = "\x82" "to transform";
+				const char *ringtext = "\x82" "50 rings, no shield";
+				const char *tut1text = "\x82" "press " "\x80" "spin";
+				const char *tut2text = "\x82" "mid-" "\x80" "jump";
 				ttheight = 8;
 				V_DrawLevelTitle(data.spec.passedx1 + xoffset1, ttheight, 0, data.spec.passed1);
 				ttheight += V_LevelNameHeight(data.spec.passed3) + 2;
@@ -849,7 +850,7 @@ void Y_IntermissionDrawer(void)
 			{
 				UINT8 *colormap = R_GetTranslationColormap(*data.match.character[i], *data.match.color[i], GTC_CACHE);
 
-				if (*data.match.color[i] == SKINCOLOR_RED) //red
+				if (data.match.ctfteam[i] == 1) //red
 				{
 					if (redplayers++ > 9)
 						continue;
@@ -857,7 +858,7 @@ void Y_IntermissionDrawer(void)
 					y = (redplayers * 16) + 32;
 					V_DrawCenteredString(x+6, y, 0, va("%d", redplayers));
 				}
-				else if (*data.match.color[i] == SKINCOLOR_BLUE) //blue
+				else if (data.match.ctfteam[i] == 2) //blue
 				{
 					if (blueplayers++ > 9)
 						continue;
@@ -1129,7 +1130,7 @@ void Y_Ticker(void)
 			else if (mapheaderinfo[gamemap-1]->musintername[0] && S_MusicExists(mapheaderinfo[gamemap-1]->musintername, !midi_disabled, !digital_disabled))
 				S_ChangeMusicInternal(mapheaderinfo[gamemap-1]->musintername, false); // don't loop it
 			else
-				S_ChangeMusicInternal("_clear", false); // don't loop it
+				S_ChangeMusicInternal(stagefailed ? "CHFAIL" : "CHPASS", false); // don't loop it
 			tallydonetic = -1;
 		}
 
@@ -1622,6 +1623,7 @@ static void Y_CalculateMatchWinners(void)
 	boolean completed[MAXPLAYERS];
 
 	// Initialize variables
+	memset(data.match.ctfteam, 0, sizeof (data.match.ctfteam));
 	memset(data.match.scores, 0, sizeof (data.match.scores));
 	memset(data.match.color, 0, sizeof (data.match.color));
 	memset(data.match.character, 0, sizeof (data.match.character));
@@ -1642,8 +1644,15 @@ static void Y_CalculateMatchWinners(void)
 
 			if (players[i].score >= data.match.scores[data.match.numplayers] && completed[i] == false)
 			{
+				data.match.ctfteam[data.match.numplayers] = players[i].ctfteam;
 				data.match.scores[data.match.numplayers] = players[i].score;
 				data.match.color[data.match.numplayers] = &players[i].skincolor;
+				if (data.match.ctfteam[data.match.numplayers] == 1) // red team
+					data.match.color[data.match.numplayers] = &skincolor_redteam;
+
+				if (data.match.ctfteam[data.match.numplayers] == 2) // blue team
+					data.match.color[data.match.numplayers] = &skincolor_blueteam;
+
 				data.match.character[data.match.numplayers] = &players[i].skin;
 				data.match.name[data.match.numplayers] = player_names[i];
 				data.match.spectator[data.match.numplayers] = players[i].spectator;
@@ -2065,7 +2074,7 @@ static void Y_AwardCoopBonuses(void)
 				(bonuses_list[bonusnum][j])(&players[i], &localbonuses[j]);
 			else
 				Y_SetNullBonus(&players[i], &localbonuses[j]);
-			
+
 			players[i].score += localbonuses[j].points;
 			if (players[i].score > MAXSCORE)
 				players[i].score = MAXSCORE;
diff --git a/src/z_zone.c b/src/z_zone.c
index 5750f8ae016becd9c9548b690b0e7fd5b32ec7cb..0852fabaeac03f3751d33063cb14f63105678537 100644
--- a/src/z_zone.c
+++ b/src/z_zone.c
@@ -671,6 +671,7 @@ static void Command_Memfree_f(void)
 		CONS_Printf(M_GetText("Cached textures        : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRCACHE)>>10));
 		CONS_Printf(M_GetText("Texture colormaps      : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPATCHCOLMIPMAP)>>10));
 		CONS_Printf(M_GetText("Model textures         : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRMODELTEXTURE)>>10));
+		CONS_Printf(M_GetText("Light table textures   : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRLIGHTTABLEDATA)>>10));
 		CONS_Printf(M_GetText("Plane polygons         : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPLANE)>>10));
 		CONS_Printf(M_GetText("All GPU textures       : %7d KB\n"), HWR_GetTextureUsed()>>10);
 	}
diff --git a/src/z_zone.h b/src/z_zone.h
index ce7af4a159555e3a6c2be83e8d0eadcf8a7a69eb..56c540c3bb31a4a84e5f2403519455f3830583ee 100644
--- a/src/z_zone.h
+++ b/src/z_zone.h
@@ -55,6 +55,7 @@ enum
 	PU_HWRPATCHINFO          = 21, // Hardware GLPatch_t struct for OpenGL texture cache
 	PU_HWRPATCHCOLMIPMAP     = 22, // Hardware GLMipmap_t struct colormap variation of patch
 	PU_HWRMODELTEXTURE       = 23, // Hardware model texture
+	PU_HWRLIGHTTABLEDATA     = 24, // Hardware light table data
 
 	PU_HWRCACHE              = 48, // static until unlocked
 	PU_CACHE                 = 49, // static until unlocked
diff --git a/thirdparty/00-SDL-Mixer-X-2.6.0-DISABLE_INSTALL.patch b/thirdparty/00-SDL-Mixer-X-2.6.0-DISABLE_INSTALL.patch
new file mode 100644
index 0000000000000000000000000000000000000000..0e7f55f1b282300bbdb6d21a8abd67373fd45828
--- /dev/null
+++ b/thirdparty/00-SDL-Mixer-X-2.6.0-DISABLE_INSTALL.patch
@@ -0,0 +1,26 @@
+--- a/CMakeLists.txt	2023-11-22 22:42:53.000000000 -0500
++++ b/CMakeLists.txt	2024-02-15 18:46:57.852076200 -0500
+@@ -45,6 +45,7 @@
+ 
+     option(SDL_MIXER_X_STATIC   "Build static library of SDL Mixer X" ${SDL_MIXER_X_STATIC_ENABLED_BY_DEFAULT})
+     option(SDL_MIXER_X_SHARED   "Build shared library of SDL Mixer X" ${SDL_MIXER_X_SHARED_ENABLED_BY_DEFAULT})
++     option(SDL_MIXER_X_DISABLE_INSTALL   "Disable install of SDL Mixer X" OFF)
+ else()
+     set(SDL_MIXER_X_STATIC ON)
+     set(SDL_MIXER_X_SHARED OFF)
+@@ -475,6 +476,7 @@
+     add_subdirectory(examples)
+ endif()
+ 
++if(NOT SDL_MIXER_X_DISABLE_INSTALL)
+ if(SDL_MIXER_X_STATIC AND NOT BUILD_AS_VB6_BINDING)
+     install(TARGETS SDL2_mixer_ext_Static
+             EXPORT SDL2MixerExtStaticTargets
+@@ -500,6 +502,7 @@
+             NAMESPACE SDL2_mixer_ext::
+             DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/SDL2_mixer_ext")
+ endif()
++endif()
+ 
+ 
+ if(BUILD_AS_VB6_BINDING)
diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt
index 19aa22c9b7a17613910dbdc9f28717e16e604986..5114cbe2c81432173409cce5a113ea9305b5b23b 100644
--- a/thirdparty/CMakeLists.txt
+++ b/thirdparty/CMakeLists.txt
@@ -1,23 +1,2 @@
-macro(export)
-endmacro()
 
-if(SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES)
-	set(SRB2_INTERNAL_LIBRARY_TYPE SHARED)
-	set(NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES OFF)
-else()
-	set(SRB2_INTERNAL_LIBRARY_TYPE STATIC)
-	set(NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES ON)
-endif()
-
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	include("cpm-sdl2.cmake")
-	include("cpm-sdl2-mixer.cmake")
-	include("cpm-zlib.cmake")
-	include("cpm-png.cmake")
-	include("cpm-curl.cmake")
-	include("cpm-openmpt.cmake")
-endif()
-
-if("${SRB2_CONFIG_USE_GME}")
-	include("cpm-libgme.cmake")
-endif()
+include("Ccache.cmake")
diff --git a/thirdparty/Ccache.cmake b/thirdparty/Ccache.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..6ca0d9fa0615d8cf091992ed84bec7b4fef09144
--- /dev/null
+++ b/thirdparty/Ccache.cmake
@@ -0,0 +1,15 @@
+# Enable CCache
+# (Set USE_CCACHE=ON to use, CCACHE_OPTIONS for options)
+if("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL Windows)
+	option(USE_CCACHE "Enable ccache support" OFF)
+
+	if(USE_CCACHE)
+		find_program(CCACHE_TOOL_PATH ccache)
+		if(CCACHE_TOOL_PATH)
+			set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_TOOL_PATH} CACHE STRING "" FORCE)
+			set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_TOOL_PATH} CACHE STRING "" FORCE)
+		else()
+			message(WARNING "USE_CCACHE was set but ccache is not found (set CCACHE_TOOL_PATH)")
+		endif()
+	endif()
+endif()
\ No newline at end of file
diff --git a/thirdparty/cpm-libgme.cmake b/thirdparty/cpm-libgme.cmake
deleted file mode 100644
index f15bc3b31cb23ed7663ed950c14c1d6a0dc36567..0000000000000000000000000000000000000000
--- a/thirdparty/cpm-libgme.cmake
+++ /dev/null
@@ -1,16 +0,0 @@
-CPMAddPackage(
-	NAME libgme
-	VERSION 0.6.3
-	URL "https://bitbucket.org/mpyne/game-music-emu/get/e76bdc0cb916e79aa540290e6edd0c445879d3ba.zip"
-	EXCLUDE_FROM_ALL ON
-	OPTIONS
-		"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-		"ENABLE_UBSAN OFF"
-		"GME_YM2612_EMU MAME"
-)
-
-if(libgme_ADDED)
-	target_compile_features(gme PRIVATE cxx_std_11)
-	# libgme's CMakeLists.txt already links this
-	#target_link_libraries(gme PRIVATE ZLIB::ZLIB)
-endif()
diff --git a/thirdparty/cpm-png.cmake b/thirdparty/cpm-png.cmake
deleted file mode 100644
index f16ac037b0cc9c077f3ef315b67b430e68cf9b40..0000000000000000000000000000000000000000
--- a/thirdparty/cpm-png.cmake
+++ /dev/null
@@ -1,69 +0,0 @@
-CPMAddPackage(
-	NAME png
-	VERSION 1.6.38
-	URL "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.38.zip"
-	# png cmake build is broken on msys/mingw32
-	DOWNLOAD_ONLY YES
-)
-
-if(png_ADDED)
-	# Since png's cmake build is broken, we're going to create a target manually
-	set(
-		PNG_SOURCES
-		png.h
-		pngconf.h
-		pngpriv.h
-		pngdebug.h
-		pnginfo.h
-		pngstruct.h
-		png.c
-		pngerror.c
-		pngget.c
-		pngmem.c
-		pngpread.c
-		pngread.c
-		pngrio.c
-		pngrtran.c
-		pngrutil.c
-		pngset.c
-		pngtrans.c
-		pngwio.c
-		pngwrite.c
-		pngwtran.c
-		pngwutil.c
-	)
-	list(TRANSFORM PNG_SOURCES PREPEND "${png_SOURCE_DIR}/")
-
-	add_custom_command(
-		OUTPUT "${png_BINARY_DIR}/include/png.h" "${png_BINARY_DIR}/include/pngconf.h"
-		COMMAND ${CMAKE_COMMAND} -E copy "${png_SOURCE_DIR}/png.h" "${png_SOURCE_DIR}/pngconf.h" "${png_BINARY_DIR}/include"
-		DEPENDS "${png_SOURCE_DIR}/png.h" "${png_SOURCE_DIR}/pngconf.h"
-		VERBATIM
-	)
-	add_custom_command(
-		OUTPUT "${png_BINARY_DIR}/include/pnglibconf.h"
-		COMMAND ${CMAKE_COMMAND} -E copy "${png_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt" "${png_BINARY_DIR}/include/pnglibconf.h"
-		DEPENDS "${png_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt"
-		VERBATIM
-	)
-	list(
-		APPEND PNG_SOURCES
-		"${png_BINARY_DIR}/include/png.h"
-		"${png_BINARY_DIR}/include/pngconf.h"
-		"${png_BINARY_DIR}/include/pnglibconf.h"
-	)
-	add_library(png "${SRB2_INTERNAL_LIBRARY_TYPE}" ${PNG_SOURCES})
-
-	# Disable ARM NEON since having it automatic breaks libpng external build on clang for some reason
-	target_compile_definitions(png PRIVATE -DPNG_ARM_NEON_OPT=0)
-
-	# The png includes need to be available to consumers
-	target_include_directories(png PUBLIC "${png_BINARY_DIR}/include")
-
-	# ... and these also need to be present only for png build
-	target_include_directories(png PRIVATE "${ZLIB_SOURCE_DIR}")
-	target_include_directories(png PRIVATE "${ZLIB_BINARY_DIR}")
-	target_include_directories(png PRIVATE "${png_BINARY_DIR}")
-	target_link_libraries(png PRIVATE ZLIB::ZLIB)
-	add_library(PNG::PNG ALIAS png)
-endif()
diff --git a/thirdparty/cpm-sdl2-mixer.cmake b/thirdparty/cpm-sdl2-mixer.cmake
deleted file mode 100644
index b7dfeae0d3eab254651f0c5e6e69e7af0626012e..0000000000000000000000000000000000000000
--- a/thirdparty/cpm-sdl2-mixer.cmake
+++ /dev/null
@@ -1,22 +0,0 @@
-CPMAddPackage(
-	NAME SDL2_mixer
-	VERSION 2.6.2
-	URL "https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.6.2.zip"
-	EXCLUDE_FROM_ALL ON
-	OPTIONS
-		"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-		"SDL2MIXER_INSTALL OFF"
-		"SDL2MIXER_DEPS_SHARED OFF"
-		"SDL2MIXER_SAMPLES OFF"
-		"SDL2MIXER_VENDORED ON"
-		"SDL2MIXER_FLAC ON"
-		"SDL2MIXER_FLAC_LIBFLAC OFF"
-		"SDL2MIXER_FLAC_DRFLAC ON"
-		"SDL2MIXER_MOD OFF"
-		"SDL2MIXER_MP3 ON"
-		"SDL2MIXER_MP3_DRMP3 ON"
-		"SDL2MIXER_MIDI ON"
-		"SDL2MIXER_OPUS OFF"
-		"SDL2MIXER_VORBIS STB"
-		"SDL2MIXER_WAVE ON"
-)
diff --git a/thirdparty/cpm-sdl2.cmake b/thirdparty/cpm-sdl2.cmake
deleted file mode 100644
index 0bb404e4c90fc6e9e5ff79314f0703a1aa4ae9a6..0000000000000000000000000000000000000000
--- a/thirdparty/cpm-sdl2.cmake
+++ /dev/null
@@ -1,28 +0,0 @@
-set(
-	internal_sdl2_options
-
-		"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-		"SDL_SHARED ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-		"SDL_STATIC ${NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-		"SDL_TEST OFF"
-		"SDL2_DISABLE_INSTALL ON"
-)
-
-if(${CMAKE_SYSTEM} MATCHES Windows)
-	list(APPEND internal_sdl2_options "SDL2_DISABLE_SDL2MAIN OFF")
-endif()
-if(${CMAKE_SYSTEM} MATCHES Darwin)
-	list(APPEND internal_sdl2_options "SDL2_DISABLE_SDL2MAIN OFF")
-endif()
-if(${CMAKE_SYSTEM} MATCHES Linux)
-	list(APPEND internal_sdl2_options "SDL2_DISABLE_SDL2MAIN ON")
-endif()
-
-CPMAddPackage(
-	NAME SDL2
-	VERSION 2.28.5
-	GITHUB_REPOSITORY "libsdl-org/SDL"
-	GIT_TAG release-2.28.5
-	EXCLUDE_FROM_ALL ON
-	OPTIONS ${internal_sdl2_options}
-)
diff --git a/thirdparty/cpm-zlib.cmake b/thirdparty/cpm-zlib.cmake
deleted file mode 100644
index f0a2c33aee8c483d82b4ab26e1d0be2143fd9e16..0000000000000000000000000000000000000000
--- a/thirdparty/cpm-zlib.cmake
+++ /dev/null
@@ -1,18 +0,0 @@
-CPMAddPackage(
-	NAME zlib
-	VERSION 1.3.1
-	GITHUB_REPOSITORY "madler/zlib"
-	GIT_TAG v1.3.1
-	EXCLUDE_FROM_ALL
-	OPTIONS
-		"ZLIB_BUILD_EXAMPLES OFF"
-)
-
-if(zlib_ADDED)
-	if(SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES)
-		add_library(ZLIB::ZLIB ALIAS zlib)
-	endif()
-	if(NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES)
-		add_library(ZLIB::ZLIB ALIAS zlibstatic)
-	endif()
-endif()
diff --git a/thirdparty/cpm-curl.cmake b/thirdparty/curl.cmake
similarity index 55%
rename from thirdparty/cpm-curl.cmake
rename to thirdparty/curl.cmake
index 3d8c6e61d46dee6f06f4e6d69beb09d1605f6584..7b6c3a299f951139fe18e0c25d18074d0deae300 100644
--- a/thirdparty/cpm-curl.cmake
+++ b/thirdparty/curl.cmake
@@ -1,8 +1,16 @@
+if(TARGET CURL::libcurl)
+    return()
+endif()
+
+message(STATUS "Third-party: creating target 'CURL::libcurl'")
+
+set(CURL_ENABLE_EXPORT_TARGET OFF CACHE BOOL "" FORCE)
+
 set(
 	internal_curl_options
 
 	"BUILD_CURL_EXE OFF"
-	"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+	"BUILD_SHARED_LIBS OFF"
 	"CURL_DISABLE_TESTS ON"
 	"HTTP_ONLY ON"
 	"CURL_DISABLE_CRYPTO_AUTH ON"
@@ -26,10 +34,24 @@ if(${CMAKE_SYSTEM} MATCHES Linux)
 	list(APPEND internal_curl_options "CURL_USE_OPENSSL ON")
 endif()
 
-CPMAddPackage(
-	NAME curl
-	VERSION 7.86.0
-	URL "https://github.com/curl/curl/archive/refs/tags/curl-7_86_0.zip"
-	EXCLUDE_FROM_ALL ON
-	OPTIONS ${internal_curl_options}
-)
+include(FetchContent)
+
+if (CURL_USE_THIRDPARTY)
+	FetchContent_Declare(
+		curl
+		VERSION 7.88.1
+		GITHUB_REPOSITORY "curl/curl"
+		GIT_TAG curl-7_88_1
+		EXCLUDE_FROM_ALL ON
+		OPTIONS ${internal_curl_options}
+	)
+else()
+	FetchContent_Declare(
+		curl
+		SOURCE_DIR "${CMAKE_SOURCE_DIR}/thirdparty/curl/"
+		EXCLUDE_FROM_ALL ON
+		OPTIONS ${internal_curl_options}
+	)
+endif()
+
+FetchContent_MakeAvailable(curl)
diff --git a/thirdparty/libapng.cmake b/thirdparty/libapng.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..f4bc50ff1d301b58bf91c44bfe40986c5c987654
--- /dev/null
+++ b/thirdparty/libapng.cmake
@@ -0,0 +1,38 @@
+if(TARGET libapng_static)
+    return()
+endif()
+
+message(STATUS "Third-party: creating target 'libapng_static'")
+
+set(PNG_SHARED OFF CACHE BOOL "" FORCE)
+set(PNG_BUILD_ZLIB ON CACHE BOOL "" FORCE)
+
+include(FetchContent)
+
+	FetchContent_Declare(
+		libapng-local
+		SOURCE_DIR "${CMAKE_SOURCE_DIR}/thirdparty/libapng/"
+		EXCLUDE_FROM_ALL ON
+		DOWNLOAD_ONLY YES
+		OPTION
+			"PNG_SHARED OFF"
+			"PNG_EXECUTABLES OFF"
+			"PNG_TESTS OFF"
+			"PNG_BUILD_ZLIB ON"
+	)
+
+FetchContent_MakeAvailable(libapng-local)
+
+#add_custom_command(
+#OUTPUT ${libapng-local_BINARY_DIR}/CMakeLists.txt
+#COMMAND ${CMAKE_COMMAND}
+#  -Din_file:FILEPATH=${libapng-locall_SOURCE_DIR}/CMakeLists.txt
+#  -Dpatch_file:FILEPATH=${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/00-libapng-ZLIB.patch
+#  -Dout_file:FILEPATH=${libapng-local_BINARY_DIR}/CMakeLists.txt
+#  -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/PatchFile.cmake
+#DEPENDS ${libapng-local_SOURCE_DIR}/CMakeLists.txt
+#)
+
+set(libapng_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/libapng" "${libapng_BINARY_DIR}" CACHE PATH "" FORCE)
+
+set(libapng_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/libapng/png.h" "${CMAKE_CURRENT_SOURCE_DIR}/libapng/pngconf.h" CACHE PATH "" FORCE)
diff --git a/thirdparty/cpm-openmpt.cmake b/thirdparty/libopenmpt.cmake
similarity index 82%
rename from thirdparty/cpm-openmpt.cmake
rename to thirdparty/libopenmpt.cmake
index 01f7ff75f64d915799e432f2923a2c7e8c344466..256b4c406b99b73bf96e9488316b1093515f65c9 100644
--- a/thirdparty/cpm-openmpt.cmake
+++ b/thirdparty/libopenmpt.cmake
@@ -1,13 +1,33 @@
-CPMAddPackage(
-	NAME openmpt
-	VERSION 0.4.30
-	URL "https://github.com/OpenMPT/openmpt/archive/refs/tags/libopenmpt-0.4.30.zip"
-	DOWNLOAD_ONLY ON
-)
+if(TARGET libopenmpt)
+    #return()
+endif()
+
+message(STATUS "Third-party: creating target 'libopenmpt'")
+
+
+include(FetchContent)
+
+if (libopenmpt_USE_THIRDPARTY)
+	FetchContent_Declare(
+		libopenmpt-local
+		GITHUB_REPOSITORY openmpt
+		GIT_TAG libopenmpt-0.4.38
+		version 0.4.38
+		EXCLUDE_FROM_ALL ON
+	)
+else()
+	FetchContent_Declare(
+		libopenmpt-local
+		SOURCE_DIR "${CMAKE_SOURCE_DIR}/thirdparty/libopenmpt/"
+		EXCLUDE_FROM_ALL ON
+	)
+endif()
+
+FetchContent_MakeAvailable(libopenmpt)
 
-if(openmpt_ADDED)
+if(aaaa-libopenmpt-local_ADDED)
 	set(
-		openmpt_SOURCES
+		libopenmpt-local_SOURCES
 
 		# minimp3
 		# -DMPT_WITH_MINIMP3
@@ -264,26 +284,26 @@ if(openmpt_ADDED)
 		libopenmpt/libopenmpt_impl.cpp
 		libopenmpt/libopenmpt_ext_impl.cpp
 	)
-	list(TRANSFORM openmpt_SOURCES PREPEND "${openmpt_SOURCE_DIR}/")
+	list(TRANSFORM libopenmpt-local_SOURCES PREPEND "${libopenmpt-local_SOURCE_DIR}/")
 
 	# -DLIBOPENMPT_BUILD
 	configure_file("openmpt_svn_version.h" "svn_version.h")
-	add_library(openmpt "${SRB2_INTERNAL_LIBRARY_TYPE}" ${openmpt_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/svn_version.h)
+	add_library(libopenmpt STATIC ${libopenmpt-local_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/svn_version.h)
 	if("${CMAKE_C_COMPILER_ID}" STREQUAL GNU OR "${CMAKE_C_COMPILER_ID}" STREQUAL Clang OR "${CMAKE_C_COMPILER_ID}" STREQUAL AppleClang)
-		target_compile_options(openmpt PRIVATE "-g0")
+		target_compile_options(libopenmpt PRIVATE "-g0")
 	endif()
 	if("${CMAKE_SYSTEM_NAME}" STREQUAL Windows AND "${CMAKE_C_COMPILER_ID}" STREQUAL MSVC)
-		target_link_libraries(openmpt PRIVATE Rpcrt4)
+		target_link_libraries(libopenmpt PRIVATE Rpcrt4)
 	endif()
-	target_compile_features(openmpt PRIVATE cxx_std_11)
-	target_compile_definitions(openmpt PRIVATE -DLIBOPENMPT_BUILD)
+	target_compile_features(libopenmpt PRIVATE cxx_std_11)
+	target_compile_definitions(libopenmpt PRIVATE -DLIBOPENMPT_BUILD)
 
-	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/common")
-	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/src")
-	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/include")
-	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}")
-	target_include_directories(openmpt PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
+	target_include_directories(libopenmpt PRIVATE "${libopenmpt-local_SOURCE_DIR}/common")
+	target_include_directories(libopenmpt PRIVATE "${libopenmpt-local_SOURCE_DIR}/src")
+	target_include_directories(libopenmpt PRIVATE "${libopenmpt-local_SOURCE_DIR}/include")
+	target_include_directories(libopenmpt PRIVATE "${libopenmpt-local_SOURCE_DIR}")
+	target_include_directories(libopenmpt PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
 
 	# I wish this wasn't necessary, but it is
-	target_include_directories(openmpt PUBLIC "${openmpt_SOURCE_DIR}")
+	target_include_directories(libopenmpt PUBLIC "${libopenmpt-local_SOURCE_DIR}")
 endif()
diff --git a/thirdparty/libpng-1.6.40-apng.patch b/thirdparty/libpng-1.6.40-apng.patch
new file mode 100644
index 0000000000000000000000000000000000000000..c5b870004ad2f7a82df6bbbc061c24cc8f5a5393
--- /dev/null
+++ b/thirdparty/libpng-1.6.40-apng.patch
@@ -0,0 +1,1728 @@
+diff -Naru libpng-1.6.40.org/png.h libpng-1.6.40/png.h
+--- libpng-1.6.40.org/png.h	2023-09-10 11:12:23.044481879 +0900
++++ libpng-1.6.40/png.h	2023-09-10 11:08:58.964075833 +0900
+@@ -330,6 +330,10 @@
+ #   include "pnglibconf.h"
+ #endif
+ 
++#define PNG_APNG_SUPPORTED
++#define PNG_READ_APNG_SUPPORTED
++#define PNG_WRITE_APNG_SUPPORTED
++
+ #ifndef PNG_VERSION_INFO_ONLY
+ /* Machine specific configuration. */
+ #  include "pngconf.h"
+@@ -425,6 +429,17 @@
+  * See pngconf.h for base types that vary by machine/system
+  */
+ 
++#ifdef PNG_APNG_SUPPORTED
++/* dispose_op flags from inside fcTL */
++#define PNG_DISPOSE_OP_NONE        0x00U
++#define PNG_DISPOSE_OP_BACKGROUND  0x01U
++#define PNG_DISPOSE_OP_PREVIOUS    0x02U
++
++/* blend_op flags from inside fcTL */
++#define PNG_BLEND_OP_SOURCE        0x00U
++#define PNG_BLEND_OP_OVER          0x01U
++#endif /* PNG_APNG_SUPPORTED */
++
+ /* This triggers a compiler error in png.c, if png.c and png.h
+  * do not agree upon the version number.
+  */
+@@ -746,6 +761,10 @@
+ #define PNG_INFO_sCAL 0x4000U  /* ESR, 1.0.6 */
+ #define PNG_INFO_IDAT 0x8000U  /* ESR, 1.0.6 */
+ #define PNG_INFO_eXIf 0x10000U /* GR-P, 1.6.31 */
++#ifdef PNG_APNG_SUPPORTED
++#define PNG_INFO_acTL 0x20000U
++#define PNG_INFO_fcTL 0x40000U
++#endif
+ 
+ /* This is used for the transformation routines, as some of them
+  * change these values for the row.  It also should enable using
+@@ -783,6 +802,10 @@
+ #ifdef PNG_PROGRESSIVE_READ_SUPPORTED
+ typedef PNG_CALLBACK(void, *png_progressive_info_ptr, (png_structp, png_infop));
+ typedef PNG_CALLBACK(void, *png_progressive_end_ptr, (png_structp, png_infop));
++#ifdef PNG_APNG_SUPPORTED
++typedef PNG_CALLBACK(void, *png_progressive_frame_ptr, (png_structp,
++    png_uint_32));
++#endif
+ 
+ /* The following callback receives png_uint_32 row_number, int pass for the
+  * png_bytep data of the row.  When transforming an interlaced image the
+@@ -3226,6 +3249,74 @@
+ /*******************************************************************************
+  *  END OF HARDWARE AND SOFTWARE OPTIONS
+  ******************************************************************************/
++#ifdef PNG_APNG_SUPPORTED
++PNG_EXPORT(250, png_uint_32, png_get_acTL, (png_structp png_ptr,
++   png_infop info_ptr, png_uint_32 *num_frames, png_uint_32 *num_plays));
++
++PNG_EXPORT(251, png_uint_32, png_set_acTL, (png_structp png_ptr,
++   png_infop info_ptr, png_uint_32 num_frames, png_uint_32 num_plays));
++
++PNG_EXPORT(252, png_uint_32, png_get_num_frames, (png_structp png_ptr,
++   png_infop info_ptr));
++
++PNG_EXPORT(253, png_uint_32, png_get_num_plays, (png_structp png_ptr,
++   png_infop info_ptr));
++
++PNG_EXPORT(254, png_uint_32, png_get_next_frame_fcTL,
++   (png_structp png_ptr, png_infop info_ptr, png_uint_32 *width,
++   png_uint_32 *height, png_uint_32 *x_offset, png_uint_32 *y_offset,
++   png_uint_16 *delay_num, png_uint_16 *delay_den, png_byte *dispose_op,
++   png_byte *blend_op));
++
++PNG_EXPORT(255, png_uint_32, png_set_next_frame_fcTL,
++   (png_structp png_ptr, png_infop info_ptr, png_uint_32 width,
++   png_uint_32 height, png_uint_32 x_offset, png_uint_32 y_offset,
++   png_uint_16 delay_num, png_uint_16 delay_den, png_byte dispose_op,
++   png_byte blend_op));
++
++PNG_EXPORT(256, png_uint_32, png_get_next_frame_width,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(257, png_uint_32, png_get_next_frame_height,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(258, png_uint_32, png_get_next_frame_x_offset,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(259, png_uint_32, png_get_next_frame_y_offset,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(260, png_uint_16, png_get_next_frame_delay_num,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(261, png_uint_16, png_get_next_frame_delay_den,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(262, png_byte, png_get_next_frame_dispose_op,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(263, png_byte, png_get_next_frame_blend_op,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(264, png_byte, png_get_first_frame_is_hidden,
++   (png_structp png_ptr, png_infop info_ptr));
++PNG_EXPORT(265, png_uint_32, png_set_first_frame_is_hidden,
++   (png_structp png_ptr, png_infop info_ptr, png_byte is_hidden));
++
++#ifdef PNG_READ_APNG_SUPPORTED
++PNG_EXPORT(266, void, png_read_frame_head, (png_structp png_ptr,
++   png_infop info_ptr));
++#ifdef PNG_PROGRESSIVE_READ_SUPPORTED
++PNG_EXPORT(267, void, png_set_progressive_frame_fn, (png_structp png_ptr,
++   png_progressive_frame_ptr frame_info_fn,
++   png_progressive_frame_ptr frame_end_fn));
++#endif /* PNG_PROGRESSIVE_READ_SUPPORTED */
++#endif /* PNG_READ_APNG_SUPPORTED */
++
++#ifdef PNG_WRITE_APNG_SUPPORTED
++PNG_EXPORT(268, void, png_write_frame_head, (png_structp png_ptr,
++   png_infop info_ptr, png_bytepp row_pointers,
++   png_uint_32 width, png_uint_32 height,
++   png_uint_32 x_offset, png_uint_32 y_offset,
++   png_uint_16 delay_num, png_uint_16 delay_den, png_byte dispose_op,
++   png_byte blend_op));
++
++PNG_EXPORT(269, void, png_write_frame_tail, (png_structp png_ptr,
++   png_infop info_ptr));
++#endif /* PNG_WRITE_APNG_SUPPORTED */
++#endif /* PNG_APNG_SUPPORTED */
+ 
+ /* Maintainer: Put new public prototypes here ^, in libpng.3, in project
+  * defs, and in scripts/symbols.def.
+@@ -3235,7 +3326,11 @@
+  * one to use is one more than this.)
+  */
+ #ifdef PNG_EXPORT_LAST_ORDINAL
++#ifdef PNG_APNG_SUPPORTED
++  PNG_EXPORT_LAST_ORDINAL(269);
++#else
+   PNG_EXPORT_LAST_ORDINAL(249);
++#endif /* PNG_APNG_SUPPORTED */
+ #endif
+ 
+ #ifdef __cplusplus
+diff -Naru libpng-1.6.40.org/pngget.c libpng-1.6.40/pngget.c
+--- libpng-1.6.40.org/pngget.c	2023-09-10 11:09:32.954139030 +0900
++++ libpng-1.6.40/pngget.c	2023-09-10 11:08:58.922075757 +0900
+@@ -1257,4 +1257,166 @@
+ #  endif
+ #endif
+ 
++#ifdef PNG_APNG_SUPPORTED
++png_uint_32 PNGAPI
++png_get_acTL(png_structp png_ptr, png_infop info_ptr,
++             png_uint_32 *num_frames, png_uint_32 *num_plays)
++{
++    png_debug1(1, "in %s retrieval function", "acTL");
++
++    if (png_ptr != NULL && info_ptr != NULL &&
++        (info_ptr->valid & PNG_INFO_acTL) &&
++        num_frames != NULL && num_plays != NULL)
++    {
++        *num_frames = info_ptr->num_frames;
++        *num_plays = info_ptr->num_plays;
++        return (1);
++    }
++
++    return (0);
++}
++
++png_uint_32 PNGAPI
++png_get_num_frames(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_num_frames()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->num_frames);
++    return (0);
++}
++
++png_uint_32 PNGAPI
++png_get_num_plays(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_num_plays()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->num_plays);
++    return (0);
++}
++
++png_uint_32 PNGAPI
++png_get_next_frame_fcTL(png_structp png_ptr, png_infop info_ptr,
++             png_uint_32 *width, png_uint_32 *height,
++             png_uint_32 *x_offset, png_uint_32 *y_offset,
++             png_uint_16 *delay_num, png_uint_16 *delay_den,
++             png_byte *dispose_op, png_byte *blend_op)
++{
++    png_debug1(1, "in %s retrieval function", "fcTL");
++
++    if (png_ptr != NULL && info_ptr != NULL &&
++        (info_ptr->valid & PNG_INFO_fcTL) &&
++        width != NULL && height != NULL &&
++        x_offset != NULL && y_offset != NULL &&
++        delay_num != NULL && delay_den != NULL &&
++        dispose_op != NULL && blend_op != NULL)
++    {
++        *width = info_ptr->next_frame_width;
++        *height = info_ptr->next_frame_height;
++        *x_offset = info_ptr->next_frame_x_offset;
++        *y_offset = info_ptr->next_frame_y_offset;
++        *delay_num = info_ptr->next_frame_delay_num;
++        *delay_den = info_ptr->next_frame_delay_den;
++        *dispose_op = info_ptr->next_frame_dispose_op;
++        *blend_op = info_ptr->next_frame_blend_op;
++        return (1);
++    }
++
++    return (0);
++}
++
++png_uint_32 PNGAPI
++png_get_next_frame_width(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_width()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_width);
++    return (0);
++}
++
++png_uint_32 PNGAPI
++png_get_next_frame_height(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_height()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_height);
++    return (0);
++}
++
++png_uint_32 PNGAPI
++png_get_next_frame_x_offset(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_x_offset()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_x_offset);
++    return (0);
++}
++
++png_uint_32 PNGAPI
++png_get_next_frame_y_offset(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_y_offset()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_y_offset);
++    return (0);
++}
++
++png_uint_16 PNGAPI
++png_get_next_frame_delay_num(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_delay_num()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_delay_num);
++    return (0);
++}
++
++png_uint_16 PNGAPI
++png_get_next_frame_delay_den(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_delay_den()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_delay_den);
++    return (0);
++}
++
++png_byte PNGAPI
++png_get_next_frame_dispose_op(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_dispose_op()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_dispose_op);
++    return (0);
++}
++
++png_byte PNGAPI
++png_get_next_frame_blend_op(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_get_next_frame_blend_op()");
++
++    if (png_ptr != NULL && info_ptr != NULL)
++        return (info_ptr->next_frame_blend_op);
++    return (0);
++}
++
++png_byte PNGAPI
++png_get_first_frame_is_hidden(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_first_frame_is_hidden()");
++
++    if (png_ptr != NULL)
++       return (png_byte)(png_ptr->apng_flags & PNG_FIRST_FRAME_HIDDEN);
++
++    PNG_UNUSED(info_ptr)
++
++    return 0;
++}
++#endif /* PNG_APNG_SUPPORTED */
+ #endif /* READ || WRITE */
+diff -Naru libpng-1.6.40.org/pnginfo.h libpng-1.6.40/pnginfo.h
+--- libpng-1.6.40.org/pnginfo.h	2022-11-24 08:37:51.507052183 +0900
++++ libpng-1.6.40/pnginfo.h	2023-09-10 11:08:58.922075757 +0900
+@@ -263,5 +263,18 @@
+    png_bytepp row_pointers;        /* the image bits */
+ #endif
+ 
++#ifdef PNG_APNG_SUPPORTED
++   png_uint_32 num_frames; /* including default image */
++   png_uint_32 num_plays;
++   png_uint_32 next_frame_width;
++   png_uint_32 next_frame_height;
++   png_uint_32 next_frame_x_offset;
++   png_uint_32 next_frame_y_offset;
++   png_uint_16 next_frame_delay_num;
++   png_uint_16 next_frame_delay_den;
++   png_byte next_frame_dispose_op;
++   png_byte next_frame_blend_op;
++#endif
++
+ };
+ #endif /* PNGINFO_H */
+diff -Naru libpng-1.6.40.org/pngpread.c libpng-1.6.40/pngpread.c
+--- libpng-1.6.40.org/pngpread.c	2022-11-24 08:37:51.507052183 +0900
++++ libpng-1.6.40/pngpread.c	2023-09-10 11:08:58.922075757 +0900
+@@ -195,6 +195,106 @@
+ 
+    chunk_name = png_ptr->chunk_name;
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++   if (png_ptr->num_frames_read > 0 &&
++       png_ptr->num_frames_read < info_ptr->num_frames)
++   {
++      if (chunk_name == png_IDAT)
++      {
++         /* Discard trailing IDATs for the first frame */
++         if (png_ptr->mode & PNG_HAVE_fcTL || png_ptr->num_frames_read > 1)
++            png_error(png_ptr, "out of place IDAT");
++
++         if (png_ptr->push_length + 4 > png_ptr->buffer_size)
++         {
++            png_push_save_buffer(png_ptr);
++            return;
++         }
++
++         png_ptr->mode &= ~PNG_HAVE_CHUNK_HEADER;
++         return;
++      }
++      else if (chunk_name == png_fdAT)
++      {
++         if (png_ptr->buffer_size < 4)
++         {
++            png_push_save_buffer(png_ptr);
++            return;
++         }
++
++         png_ensure_sequence_number(png_ptr, 4);
++
++         if (!(png_ptr->mode & PNG_HAVE_fcTL))
++         {
++            /* Discard trailing fdATs for frames other than the first */
++            if (png_ptr->num_frames_read < 2)
++               png_error(png_ptr, "out of place fdAT");
++
++            if (png_ptr->push_length + 4 > png_ptr->buffer_size)
++            {
++               png_push_save_buffer(png_ptr);
++               return;
++            }
++
++            png_ptr->mode &= ~PNG_HAVE_CHUNK_HEADER;
++            return;
++         }
++
++         else
++         {
++            /* frame data follows */
++            png_ptr->idat_size = png_ptr->push_length - 4;
++            png_ptr->mode |= PNG_HAVE_IDAT;
++            png_ptr->process_mode = PNG_READ_IDAT_MODE;
++
++            return;
++         }
++      }
++
++      else if (chunk_name == png_fcTL)
++      {
++         if (png_ptr->push_length + 4 > png_ptr->buffer_size)
++         {
++            png_push_save_buffer(png_ptr);
++            return;
++         }
++
++         png_read_reset(png_ptr);
++         png_ptr->mode &= ~PNG_HAVE_fcTL;
++
++         png_handle_fcTL(png_ptr, info_ptr, png_ptr->push_length);
++
++         if (!(png_ptr->mode & PNG_HAVE_fcTL))
++            png_error(png_ptr, "missing required fcTL chunk");
++
++         png_read_reinit(png_ptr, info_ptr);
++         png_progressive_read_reset(png_ptr);
++
++         if (png_ptr->frame_info_fn != NULL)
++            (*(png_ptr->frame_info_fn))(png_ptr, png_ptr->num_frames_read);
++
++         png_ptr->mode &= ~PNG_HAVE_CHUNK_HEADER;
++
++         return;
++      }
++
++      else
++      {
++         if (png_ptr->push_length + 4 > png_ptr->buffer_size)
++         {
++            png_push_save_buffer(png_ptr);
++            return;
++         }
++         png_warning(png_ptr, "Skipped (ignored) a chunk "
++                              "between APNG chunks");
++         png_ptr->mode &= ~PNG_HAVE_CHUNK_HEADER;
++         return;
++      }
++
++      return;
++   }
++#endif /* PNG_READ_APNG_SUPPORTED */
++
+    if (chunk_name == png_IDAT)
+    {
+       if ((png_ptr->mode & PNG_AFTER_IDAT) != 0)
+@@ -261,6 +361,9 @@
+ 
+    else if (chunk_name == png_IDAT)
+    {
++#ifdef PNG_READ_APNG_SUPPORTED
++      png_have_info(png_ptr, info_ptr);
++#endif
+       png_ptr->idat_size = png_ptr->push_length;
+       png_ptr->process_mode = PNG_READ_IDAT_MODE;
+       png_push_have_info(png_ptr, info_ptr);
+@@ -406,6 +509,30 @@
+       png_handle_iTXt(png_ptr, info_ptr, png_ptr->push_length);
+    }
+ #endif
++#ifdef PNG_READ_APNG_SUPPORTED
++   else if (chunk_name == png_acTL)
++   {
++      if (png_ptr->push_length + 4 > png_ptr->buffer_size)
++      {
++         png_push_save_buffer(png_ptr);
++         return;
++      }
++
++      png_handle_acTL(png_ptr, info_ptr, png_ptr->push_length);
++   }
++
++   else if (chunk_name == png_fcTL)
++   {
++      if (png_ptr->push_length + 4 > png_ptr->buffer_size)
++      {
++         png_push_save_buffer(png_ptr);
++         return;
++      }
++
++      png_handle_fcTL(png_ptr, info_ptr, png_ptr->push_length);
++   }
++
++#endif /* PNG_READ_APNG_SUPPORTED */
+ 
+    else
+    {
+@@ -539,7 +666,11 @@
+       png_byte chunk_tag[4];
+ 
+       /* TODO: this code can be commoned up with the same code in push_read */
++#ifdef PNG_READ_APNG_SUPPORTED
++      PNG_PUSH_SAVE_BUFFER_IF_LT(12)
++#else
+       PNG_PUSH_SAVE_BUFFER_IF_LT(8)
++#endif
+       png_push_fill_buffer(png_ptr, chunk_length, 4);
+       png_ptr->push_length = png_get_uint_31(png_ptr, chunk_length);
+       png_reset_crc(png_ptr);
+@@ -547,17 +678,64 @@
+       png_ptr->chunk_name = PNG_CHUNK_FROM_STRING(chunk_tag);
+       png_ptr->mode |= PNG_HAVE_CHUNK_HEADER;
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++      if (png_ptr->chunk_name != png_fdAT && png_ptr->num_frames_read > 0)
++      {
++          if (png_ptr->flags & PNG_FLAG_ZSTREAM_ENDED)
++          {
++              png_ptr->process_mode = PNG_READ_CHUNK_MODE;
++              if (png_ptr->frame_end_fn != NULL)
++                 (*(png_ptr->frame_end_fn))(png_ptr, png_ptr->num_frames_read);
++              png_ptr->num_frames_read++;
++              return;
++          }
++          else
++          {
++              if (png_ptr->chunk_name == png_IEND)
++                  png_error(png_ptr, "Not enough image data");
++              if (png_ptr->push_length + 4 > png_ptr->buffer_size)
++              {
++                 png_push_save_buffer(png_ptr);
++                 return;
++              }
++              png_warning(png_ptr, "Skipping (ignoring) a chunk between "
++                                   "APNG chunks");
++              png_crc_finish(png_ptr, png_ptr->push_length);
++              png_ptr->mode &= ~PNG_HAVE_CHUNK_HEADER;
++              return;
++          }
++      }
++      else
++#endif
++#ifdef PNG_READ_APNG_SUPPORTED
++      if (png_ptr->chunk_name != png_IDAT && png_ptr->num_frames_read == 0)
++#else
+       if (png_ptr->chunk_name != png_IDAT)
++#endif
+       {
+          png_ptr->process_mode = PNG_READ_CHUNK_MODE;
+ 
+          if ((png_ptr->flags & PNG_FLAG_ZSTREAM_ENDED) == 0)
+             png_error(png_ptr, "Not enough compressed data");
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++         if (png_ptr->frame_end_fn != NULL)
++            (*(png_ptr->frame_end_fn))(png_ptr, png_ptr->num_frames_read);
++         png_ptr->num_frames_read++;
++#endif
++
+          return;
+       }
+ 
+       png_ptr->idat_size = png_ptr->push_length;
++
++#ifdef PNG_READ_APNG_SUPPORTED
++      if (png_ptr->num_frames_read > 0)
++      {
++         png_ensure_sequence_number(png_ptr, 4);
++         png_ptr->idat_size -= 4;
++      }
++#endif
+    }
+ 
+    if (png_ptr->idat_size != 0 && png_ptr->save_buffer_size != 0)
+@@ -631,6 +809,15 @@
+    if (!(buffer_length > 0) || buffer == NULL)
+       png_error(png_ptr, "No IDAT data (internal error)");
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++   /* If the app is not APNG-aware, decode only the first frame */
++   if (!(png_ptr->apng_flags & PNG_APNG_APP) && png_ptr->num_frames_read > 0)
++   {
++     png_ptr->flags |= PNG_FLAG_ZSTREAM_ENDED;
++     return;
++   }
++#endif
++
+    /* This routine must process all the data it has been given
+     * before returning, calling the row callback as required to
+     * handle the uncompressed results.
+@@ -1085,6 +1272,18 @@
+    png_set_read_fn(png_ptr, progressive_ptr, png_push_fill_buffer);
+ }
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++void PNGAPI
++png_set_progressive_frame_fn(png_structp png_ptr,
++   png_progressive_frame_ptr frame_info_fn,
++   png_progressive_frame_ptr frame_end_fn)
++{
++   png_ptr->frame_info_fn = frame_info_fn;
++   png_ptr->frame_end_fn = frame_end_fn;
++   png_ptr->apng_flags |= PNG_APNG_APP;
++}
++#endif
++
+ png_voidp PNGAPI
+ png_get_progressive_ptr(png_const_structrp png_ptr)
+ {
+diff -Naru libpng-1.6.40.org/pngpriv.h libpng-1.6.40/pngpriv.h
+--- libpng-1.6.40.org/pngpriv.h	2023-09-10 11:09:32.954139030 +0900
++++ libpng-1.6.40/pngpriv.h	2023-09-10 11:08:58.923075759 +0900
+@@ -628,6 +628,10 @@
+ #define PNG_HAVE_CHUNK_AFTER_IDAT 0x2000U /* Have another chunk after IDAT */
+ #define PNG_WROTE_eXIf            0x4000U
+ #define PNG_IS_READ_STRUCT        0x8000U /* Else is a write struct */
++#ifdef PNG_APNG_SUPPORTED
++#define PNG_HAVE_acTL            0x10000U
++#define PNG_HAVE_fcTL            0x20000U
++#endif
+ 
+ /* Flags for the transformations the PNG library does on the image data */
+ #define PNG_BGR                 0x0001U
+@@ -864,6 +868,16 @@
+ #define png_tRNS PNG_U32(116,  82,  78,  83)
+ #define png_zTXt PNG_U32(122,  84,  88, 116)
+ 
++#ifdef PNG_APNG_SUPPORTED
++#define png_acTL PNG_U32( 97,  99,  84,  76)
++#define png_fcTL PNG_U32(102,  99,  84,  76)
++#define png_fdAT PNG_U32(102, 100,  65,  84)
++
++/* For png_struct.apng_flags: */
++#define PNG_FIRST_FRAME_HIDDEN       0x0001U
++#define PNG_APNG_APP                 0x0002U
++#endif
++
+ /* The following will work on (signed char*) strings, whereas the get_uint_32
+  * macro will fail on top-bit-set values because of the sign extension.
+  */
+@@ -1635,6 +1649,47 @@
+     */
+ #endif
+ 
++#ifdef PNG_APNG_SUPPORTED
++PNG_INTERNAL_FUNCTION(void,png_ensure_fcTL_is_valid,(png_structp png_ptr,
++   png_uint_32 width, png_uint_32 height,
++   png_uint_32 x_offset, png_uint_32 y_offset,
++   png_uint_16 delay_num, png_uint_16 delay_den,
++   png_byte dispose_op, png_byte blend_op), PNG_EMPTY);
++
++#ifdef PNG_READ_APNG_SUPPORTED
++PNG_INTERNAL_FUNCTION(void,png_handle_acTL,(png_structp png_ptr, png_infop info_ptr,
++   png_uint_32 length),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_handle_fcTL,(png_structp png_ptr, png_infop info_ptr,
++   png_uint_32 length),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_handle_fdAT,(png_structp png_ptr, png_infop info_ptr,
++   png_uint_32 length),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_have_info,(png_structp png_ptr, png_infop info_ptr),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_ensure_sequence_number,(png_structp png_ptr,
++   png_uint_32 length),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_read_reset,(png_structp png_ptr),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_read_reinit,(png_structp png_ptr,
++   png_infop info_ptr),PNG_EMPTY);
++#ifdef PNG_PROGRESSIVE_READ_SUPPORTED
++PNG_INTERNAL_FUNCTION(void,png_progressive_read_reset,(png_structp png_ptr),PNG_EMPTY);
++#endif /* PNG_PROGRESSIVE_READ_SUPPORTED */
++#endif /* PNG_READ_APNG_SUPPORTED */
++
++#ifdef PNG_WRITE_APNG_SUPPORTED
++PNG_INTERNAL_FUNCTION(void,png_write_acTL,(png_structp png_ptr,
++   png_uint_32 num_frames, png_uint_32 num_plays),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_write_fcTL,(png_structp png_ptr,
++   png_uint_32 width, png_uint_32 height,
++   png_uint_32 x_offset, png_uint_32 y_offset,
++   png_uint_16 delay_num, png_uint_16 delay_den,
++   png_byte dispose_op, png_byte blend_op),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_write_fdAT,(png_structp png_ptr,
++   png_const_bytep data, png_size_t length),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_write_reset,(png_structp png_ptr),PNG_EMPTY);
++PNG_INTERNAL_FUNCTION(void,png_write_reinit,(png_structp png_ptr,
++   png_infop info_ptr, png_uint_32 width, png_uint_32 height),PNG_EMPTY);
++#endif /* PNG_WRITE_APNG_SUPPORTED */
++#endif /* PNG_APNG_SUPPORTED */
++
+ /* Added at libpng version 1.4.0 */
+ #ifdef PNG_COLORSPACE_SUPPORTED
+ /* These internal functions are for maintaining the colorspace structure within
+diff -Naru libpng-1.6.40.org/pngread.c libpng-1.6.40/pngread.c
+--- libpng-1.6.40.org/pngread.c	2022-11-24 08:37:51.508052181 +0900
++++ libpng-1.6.40/pngread.c	2023-09-10 11:08:58.923075759 +0900
+@@ -161,6 +161,9 @@
+ 
+       else if (chunk_name == png_IDAT)
+       {
++#ifdef PNG_READ_APNG_SUPPORTED
++         png_have_info(png_ptr, info_ptr);
++#endif
+          png_ptr->idat_size = length;
+          break;
+       }
+@@ -255,6 +258,17 @@
+          png_handle_iTXt(png_ptr, info_ptr, length);
+ #endif
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++      else if (chunk_name == png_acTL)
++         png_handle_acTL(png_ptr, info_ptr, length);
++
++      else if (chunk_name == png_fcTL)
++         png_handle_fcTL(png_ptr, info_ptr, length);
++
++      else if (chunk_name == png_fdAT)
++         png_handle_fdAT(png_ptr, info_ptr, length);
++#endif
++
+       else
+          png_handle_unknown(png_ptr, info_ptr, length,
+              PNG_HANDLE_CHUNK_AS_DEFAULT);
+@@ -262,6 +276,72 @@
+ }
+ #endif /* SEQUENTIAL_READ */
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++void PNGAPI
++png_read_frame_head(png_structp png_ptr, png_infop info_ptr)
++{
++    png_byte have_chunk_after_DAT; /* after IDAT or after fdAT */
++
++    png_debug(0, "Reading frame head");
++
++    if (!(png_ptr->mode & PNG_HAVE_acTL))
++        png_error(png_ptr, "attempt to png_read_frame_head() but "
++                           "no acTL present");
++
++    /* do nothing for the main IDAT */
++    if (png_ptr->num_frames_read == 0)
++        return;
++
++    png_read_reset(png_ptr);
++    png_ptr->flags &= ~PNG_FLAG_ROW_INIT;
++    png_ptr->mode &= ~PNG_HAVE_fcTL;
++
++    have_chunk_after_DAT = 0;
++    for (;;)
++    {
++        png_uint_32 length = png_read_chunk_header(png_ptr);
++
++        if (png_ptr->chunk_name == png_IDAT)
++        {
++            /* discard trailing IDATs for the first frame */
++            if (have_chunk_after_DAT || png_ptr->num_frames_read > 1)
++                png_error(png_ptr, "png_read_frame_head(): out of place IDAT");
++            png_crc_finish(png_ptr, length);
++        }
++
++        else if (png_ptr->chunk_name == png_fcTL)
++        {
++            png_handle_fcTL(png_ptr, info_ptr, length);
++            have_chunk_after_DAT = 1;
++        }
++
++        else if (png_ptr->chunk_name == png_fdAT)
++        {
++            png_ensure_sequence_number(png_ptr, length);
++
++            /* discard trailing fdATs for frames other than the first */
++            if (!have_chunk_after_DAT && png_ptr->num_frames_read > 1)
++                png_crc_finish(png_ptr, length - 4);
++            else if(png_ptr->mode & PNG_HAVE_fcTL)
++            {
++                png_ptr->idat_size = length - 4;
++                png_ptr->mode |= PNG_HAVE_IDAT;
++
++                break;
++            }
++            else
++                png_error(png_ptr, "png_read_frame_head(): out of place fdAT");
++        }
++        else
++        {
++            png_warning(png_ptr, "Skipped (ignored) a chunk "
++                                 "between APNG chunks");
++            png_crc_finish(png_ptr, length);
++        }
++    }
++}
++#endif /* PNG_READ_APNG_SUPPORTED */
++
+ /* Optional call to update the users info_ptr structure */
+ void PNGAPI
+ png_read_update_info(png_structrp png_ptr, png_inforp info_ptr)
+diff -Naru libpng-1.6.40.org/pngrutil.c libpng-1.6.40/pngrutil.c
+--- libpng-1.6.40.org/pngrutil.c	2022-11-24 08:37:51.510052177 +0900
++++ libpng-1.6.40/pngrutil.c	2023-09-10 11:08:58.923075759 +0900
+@@ -864,6 +864,11 @@
+    filter_type = buf[11];
+    interlace_type = buf[12];
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++   png_ptr->first_frame_width = width;
++   png_ptr->first_frame_height = height;
++#endif
++
+    /* Set internal variables */
+    png_ptr->width = width;
+    png_ptr->height = height;
+@@ -2858,6 +2863,179 @@
+ }
+ #endif
+ 
++#ifdef PNG_READ_APNG_SUPPORTED
++void /* PRIVATE */
++png_handle_acTL(png_structp png_ptr, png_infop info_ptr, png_uint_32 length)
++{
++    png_byte data[8];
++    png_uint_32 num_frames;
++    png_uint_32 num_plays;
++    png_uint_32 didSet;
++
++    png_debug(1, "in png_handle_acTL");
++
++    if (!(png_ptr->mode & PNG_HAVE_IHDR))
++    {
++        png_error(png_ptr, "Missing IHDR before acTL");
++    }
++    else if (png_ptr->mode & PNG_HAVE_IDAT)
++    {
++        png_warning(png_ptr, "Invalid acTL after IDAT skipped");
++        png_crc_finish(png_ptr, length);
++        return;
++    }
++    else if (png_ptr->mode & PNG_HAVE_acTL)
++    {
++        png_warning(png_ptr, "Duplicate acTL skipped");
++        png_crc_finish(png_ptr, length);
++        return;
++    }
++    else if (length != 8)
++    {
++        png_warning(png_ptr, "acTL with invalid length skipped");
++        png_crc_finish(png_ptr, length);
++        return;
++    }
++
++    png_crc_read(png_ptr, data, 8);
++    png_crc_finish(png_ptr, 0);
++
++    num_frames = png_get_uint_31(png_ptr, data);
++    num_plays = png_get_uint_31(png_ptr, data + 4);
++
++    /* the set function will do error checking on num_frames */
++    didSet = png_set_acTL(png_ptr, info_ptr, num_frames, num_plays);
++    if(didSet)
++        png_ptr->mode |= PNG_HAVE_acTL;
++}
++
++void /* PRIVATE */
++png_handle_fcTL(png_structp png_ptr, png_infop info_ptr, png_uint_32 length)
++{
++    png_byte data[22];
++    png_uint_32 width;
++    png_uint_32 height;
++    png_uint_32 x_offset;
++    png_uint_32 y_offset;
++    png_uint_16 delay_num;
++    png_uint_16 delay_den;
++    png_byte dispose_op;
++    png_byte blend_op;
++
++    png_debug(1, "in png_handle_fcTL");
++
++    png_ensure_sequence_number(png_ptr, length);
++
++    if (!(png_ptr->mode & PNG_HAVE_IHDR))
++    {
++        png_error(png_ptr, "Missing IHDR before fcTL");
++    }
++    else if (png_ptr->mode & PNG_HAVE_IDAT)
++    {
++        /* for any frames other then the first this message may be misleading,
++        * but correct. PNG_HAVE_IDAT is unset before the frame head is read
++        * i can't think of a better message */
++        png_warning(png_ptr, "Invalid fcTL after IDAT skipped");
++        png_crc_finish(png_ptr, length-4);
++        return;
++    }
++    else if (png_ptr->mode & PNG_HAVE_fcTL)
++    {
++        png_warning(png_ptr, "Duplicate fcTL within one frame skipped");
++        png_crc_finish(png_ptr, length-4);
++        return;
++    }
++    else if (length != 26)
++    {
++        png_warning(png_ptr, "fcTL with invalid length skipped");
++        png_crc_finish(png_ptr, length-4);
++        return;
++    }
++
++    png_crc_read(png_ptr, data, 22);
++    png_crc_finish(png_ptr, 0);
++
++    width = png_get_uint_31(png_ptr, data);
++    height = png_get_uint_31(png_ptr, data + 4);
++    x_offset = png_get_uint_31(png_ptr, data + 8);
++    y_offset = png_get_uint_31(png_ptr, data + 12);
++    delay_num = png_get_uint_16(data + 16);
++    delay_den = png_get_uint_16(data + 18);
++    dispose_op = data[20];
++    blend_op = data[21];
++
++    if (png_ptr->num_frames_read == 0 && (x_offset != 0 || y_offset != 0))
++    {
++        png_warning(png_ptr, "fcTL for the first frame must have zero offset");
++        return;
++    }
++
++    if (info_ptr != NULL)
++    {
++        if (png_ptr->num_frames_read == 0 &&
++            (width != info_ptr->width || height != info_ptr->height))
++        {
++            png_warning(png_ptr, "size in first frame's fcTL must match "
++                               "the size in IHDR");
++            return;
++        }
++
++        /* The set function will do more error checking */
++        png_set_next_frame_fcTL(png_ptr, info_ptr, width, height,
++                                x_offset, y_offset, delay_num, delay_den,
++                                dispose_op, blend_op);
++
++        png_read_reinit(png_ptr, info_ptr);
++
++        png_ptr->mode |= PNG_HAVE_fcTL;
++    }
++}
++
++void /* PRIVATE */
++png_have_info(png_structp png_ptr, png_infop info_ptr)
++{
++    if((info_ptr->valid & PNG_INFO_acTL) && !(info_ptr->valid & PNG_INFO_fcTL))
++    {
++        png_ptr->apng_flags |= PNG_FIRST_FRAME_HIDDEN;
++        info_ptr->num_frames++;
++    }
++}
++
++void /* PRIVATE */
++png_handle_fdAT(png_structp png_ptr, png_infop info_ptr, png_uint_32 length)
++{
++    png_ensure_sequence_number(png_ptr, length);
++
++    /* This function is only called from png_read_end(), png_read_info(),
++    * and png_push_read_chunk() which means that:
++    * - the user doesn't want to read this frame
++    * - or this is an out-of-place fdAT
++    * in either case it is safe to ignore the chunk with a warning */
++    png_warning(png_ptr, "ignoring fdAT chunk");
++    png_crc_finish(png_ptr, length - 4);
++    PNG_UNUSED(info_ptr)
++}
++
++void /* PRIVATE */
++png_ensure_sequence_number(png_structp png_ptr, png_uint_32 length)
++{
++    png_byte data[4];
++    png_uint_32 sequence_number;
++
++    if (length < 4)
++        png_error(png_ptr, "invalid fcTL or fdAT chunk found");
++
++    png_crc_read(png_ptr, data, 4);
++    sequence_number = png_get_uint_31(png_ptr, data);
++
++    if (sequence_number != png_ptr->next_seq_num)
++        png_error(png_ptr, "fcTL or fdAT chunk with out-of-order sequence "
++                           "number found");
++
++    png_ptr->next_seq_num++;
++}
++#endif /* PNG_READ_APNG_SUPPORTED */
++
+ #ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
+ /* Utility function for png_handle_unknown; set up png_ptr::unknown_chunk */
+ static int
+@@ -4166,7 +4344,38 @@
+       {
+          uInt avail_in;
+          png_bytep buffer;
++#ifdef PNG_READ_APNG_SUPPORTED
++         png_uint_32 bytes_to_skip = 0;
++
++         while (png_ptr->idat_size == 0 || bytes_to_skip != 0)
++         {
++            png_crc_finish(png_ptr, bytes_to_skip);
++            bytes_to_skip = 0;
+ 
++            png_ptr->idat_size = png_read_chunk_header(png_ptr);
++            if (png_ptr->num_frames_read == 0)
++            {
++               if (png_ptr->chunk_name != png_IDAT)
++                  png_error(png_ptr, "Not enough image data");
++            }
++            else
++            {
++               if (png_ptr->chunk_name == png_IEND)
++                  png_error(png_ptr, "Not enough image data");
++               if (png_ptr->chunk_name != png_fdAT)
++               {
++                  png_warning(png_ptr, "Skipped (ignored) a chunk "
++                                       "between APNG chunks");
++                  bytes_to_skip = png_ptr->idat_size;
++                  continue;
++               }
++
++               png_ensure_sequence_number(png_ptr, png_ptr->idat_size);
++
++               png_ptr->idat_size -= 4;
++            }
++         }
++#else
+          while (png_ptr->idat_size == 0)
+          {
+             png_crc_finish(png_ptr, 0);
+@@ -4178,7 +4387,7 @@
+             if (png_ptr->chunk_name != png_IDAT)
+                png_error(png_ptr, "Not enough image data");
+          }
+-
++#endif /* PNG_READ_APNG_SUPPORTED */
+          avail_in = png_ptr->IDAT_read_size;
+ 
+          if (avail_in > png_ptr->idat_size)
+@@ -4241,6 +4450,9 @@
+ 
+          png_ptr->mode |= PNG_AFTER_IDAT;
+          png_ptr->flags |= PNG_FLAG_ZSTREAM_ENDED;
++#ifdef PNG_READ_APNG_SUPPORTED
++         png_ptr->num_frames_read++;
++#endif
+ 
+          if (png_ptr->zstream.avail_in > 0 || png_ptr->idat_size > 0)
+             png_chunk_benign_error(png_ptr, "Extra compressed data");
+@@ -4678,4 +4890,80 @@
+ 
+    png_ptr->flags |= PNG_FLAG_ROW_INIT;
+ }
++
++#ifdef PNG_READ_APNG_SUPPORTED
++/* This function is to be called after the main IDAT set has been read and
++ * before a new IDAT is read. It resets some parts of png_ptr
++ * to make them usable by the read functions again */
++void /* PRIVATE */
++png_read_reset(png_structp png_ptr)
++{
++    png_ptr->mode &= ~PNG_HAVE_IDAT;
++    png_ptr->mode &= ~PNG_AFTER_IDAT;
++    png_ptr->row_number = 0;
++    png_ptr->pass = 0;
++}
++
++void /* PRIVATE */
++png_read_reinit(png_structp png_ptr, png_infop info_ptr)
++{
++    png_ptr->width = info_ptr->next_frame_width;
++    png_ptr->height = info_ptr->next_frame_height;
++    png_ptr->rowbytes = PNG_ROWBYTES(png_ptr->pixel_depth,png_ptr->width);
++    png_ptr->info_rowbytes = PNG_ROWBYTES(info_ptr->pixel_depth,
++        png_ptr->width);
++    if (png_ptr->prev_row)
++        memset(png_ptr->prev_row, 0, png_ptr->rowbytes + 1);
++}
++
++#ifdef PNG_PROGRESSIVE_READ_SUPPORTED
++/* same as png_read_reset() but for the progressive reader */
++void /* PRIVATE */
++png_progressive_read_reset(png_structp png_ptr)
++{
++#ifdef PNG_READ_INTERLACING_SUPPORTED
++   /* Arrays to facilitate easy interlacing - use pass (0 - 6) as index */
++
++   /* Start of interlace block */
++    const int png_pass_start[] = {0, 4, 0, 2, 0, 1, 0};
++
++    /* Offset to next interlace block */
++    const int png_pass_inc[] = {8, 8, 4, 4, 2, 2, 1};
++
++    /* Start of interlace block in the y direction */
++    const int png_pass_ystart[] = {0, 0, 4, 0, 2, 0, 1};
++
++    /* Offset to next interlace block in the y direction */
++    const int png_pass_yinc[] = {8, 8, 8, 4, 4, 2, 2};
++
++    if (png_ptr->interlaced)
++    {
++        if (!(png_ptr->transformations & PNG_INTERLACE))
++            png_ptr->num_rows = (png_ptr->height + png_pass_yinc[0] - 1 -
++                                png_pass_ystart[0]) / png_pass_yinc[0];
++        else
++            png_ptr->num_rows = png_ptr->height;
++
++        png_ptr->iwidth = (png_ptr->width +
++                           png_pass_inc[png_ptr->pass] - 1 -
++                           png_pass_start[png_ptr->pass]) /
++                           png_pass_inc[png_ptr->pass];
++    }
++    else
++#endif /* PNG_READ_INTERLACING_SUPPORTED */
++    {
++        png_ptr->num_rows = png_ptr->height;
++        png_ptr->iwidth = png_ptr->width;
++    }
++    png_ptr->flags &= ~PNG_FLAG_ZSTREAM_ENDED;
++    if (inflateReset(&(png_ptr->zstream)) != Z_OK)
++        png_error(png_ptr, "inflateReset failed");
++    png_ptr->zstream.avail_in = 0;
++    png_ptr->zstream.next_in = 0;
++    png_ptr->zstream.next_out = png_ptr->row_buf;
++    png_ptr->zstream.avail_out = (uInt)PNG_ROWBYTES(png_ptr->pixel_depth,
++        png_ptr->iwidth) + 1;
++}
++#endif /* PNG_PROGRESSIVE_READ_SUPPORTED */
++#endif /* PNG_READ_APNG_SUPPORTED */
+ #endif /* READ */
+diff -Naru libpng-1.6.40.org/pngset.c libpng-1.6.40/pngset.c
+--- libpng-1.6.40.org/pngset.c	2023-09-10 11:09:32.954139030 +0900
++++ libpng-1.6.40/pngset.c	2023-09-10 11:08:58.932075775 +0900
+@@ -280,6 +280,11 @@
+    info_ptr->pixel_depth = (png_byte)(info_ptr->channels * info_ptr->bit_depth);
+ 
+    info_ptr->rowbytes = PNG_ROWBYTES(info_ptr->pixel_depth, width);
++
++#ifdef PNG_APNG_SUPPORTED
++   /* for non-animated png. this may be overwritten from an acTL chunk later */
++   info_ptr->num_frames = 1;
++#endif
+ }
+ 
+ #ifdef PNG_oFFs_SUPPORTED
+@@ -1149,6 +1154,147 @@
+ }
+ #endif /* sPLT */
+ 
++#ifdef PNG_APNG_SUPPORTED
++png_uint_32 PNGAPI
++png_set_acTL(png_structp png_ptr, png_infop info_ptr,
++    png_uint_32 num_frames, png_uint_32 num_plays)
++{
++    png_debug1(1, "in %s storage function", "acTL");
++
++    if (png_ptr == NULL || info_ptr == NULL)
++    {
++        png_warning(png_ptr,
++                    "Call to png_set_acTL() with NULL png_ptr "
++                    "or info_ptr ignored");
++        return (0);
++    }
++    if (num_frames == 0)
++    {
++        png_warning(png_ptr,
++                    "Ignoring attempt to set acTL with num_frames zero");
++        return (0);
++    }
++    if (num_frames > PNG_UINT_31_MAX)
++    {
++        png_warning(png_ptr,
++                    "Ignoring attempt to set acTL with num_frames > 2^31-1");
++        return (0);
++    }
++    if (num_plays > PNG_UINT_31_MAX)
++    {
++        png_warning(png_ptr,
++                    "Ignoring attempt to set acTL with num_plays "
++                    "> 2^31-1");
++        return (0);
++    }
++
++    info_ptr->num_frames = num_frames;
++    info_ptr->num_plays = num_plays;
++
++    info_ptr->valid |= PNG_INFO_acTL;
++
++    return (1);
++}
++
++/* delay_num and delay_den can hold any 16-bit values including zero */
++png_uint_32 PNGAPI
++png_set_next_frame_fcTL(png_structp png_ptr, png_infop info_ptr,
++    png_uint_32 width, png_uint_32 height,
++    png_uint_32 x_offset, png_uint_32 y_offset,
++    png_uint_16 delay_num, png_uint_16 delay_den,
++    png_byte dispose_op, png_byte blend_op)
++{
++    png_debug1(1, "in %s storage function", "fcTL");
++
++    if (png_ptr == NULL || info_ptr == NULL)
++    {
++        png_warning(png_ptr,
++                    "Call to png_set_fcTL() with NULL png_ptr or info_ptr "
++                    "ignored");
++        return (0);
++    }
++
++    png_ensure_fcTL_is_valid(png_ptr, width, height, x_offset, y_offset,
++                             delay_num, delay_den, dispose_op, blend_op);
++
++    if (blend_op == PNG_BLEND_OP_OVER)
++    {
++        if (!(png_ptr->color_type & PNG_COLOR_MASK_ALPHA) &&
++            !(png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)))
++        {
++          png_warning(png_ptr, "PNG_BLEND_OP_OVER is meaningless "
++                               "and wasteful for opaque images, ignored");
++          blend_op = PNG_BLEND_OP_SOURCE;
++        }
++    }
++
++    info_ptr->next_frame_width = width;
++    info_ptr->next_frame_height = height;
++    info_ptr->next_frame_x_offset = x_offset;
++    info_ptr->next_frame_y_offset = y_offset;
++    info_ptr->next_frame_delay_num = delay_num;
++    info_ptr->next_frame_delay_den = delay_den;
++    info_ptr->next_frame_dispose_op = dispose_op;
++    info_ptr->next_frame_blend_op = blend_op;
++
++    info_ptr->valid |= PNG_INFO_fcTL;
++
++    return (1);
++}
++
++void /* PRIVATE */
++png_ensure_fcTL_is_valid(png_structp png_ptr,
++    png_uint_32 width, png_uint_32 height,
++    png_uint_32 x_offset, png_uint_32 y_offset,
++    png_uint_16 delay_num, png_uint_16 delay_den,
++    png_byte dispose_op, png_byte blend_op)
++{
++    if (width == 0 || width > PNG_UINT_31_MAX)
++        png_error(png_ptr, "invalid width in fcTL (> 2^31-1)");
++    if (height == 0 || height > PNG_UINT_31_MAX)
++        png_error(png_ptr, "invalid height in fcTL (> 2^31-1)");
++    if (x_offset > PNG_UINT_31_MAX)
++        png_error(png_ptr, "invalid x_offset in fcTL (> 2^31-1)");
++    if (y_offset > PNG_UINT_31_MAX)
++        png_error(png_ptr, "invalid y_offset in fcTL (> 2^31-1)");
++    if (width + x_offset > png_ptr->first_frame_width ||
++        height + y_offset > png_ptr->first_frame_height)
++        png_error(png_ptr, "dimensions of a frame are greater than"
++                           "the ones in IHDR");
++
++    if (dispose_op != PNG_DISPOSE_OP_NONE &&
++        dispose_op != PNG_DISPOSE_OP_BACKGROUND &&
++        dispose_op != PNG_DISPOSE_OP_PREVIOUS)
++        png_error(png_ptr, "invalid dispose_op in fcTL");
++
++    if (blend_op != PNG_BLEND_OP_SOURCE &&
++        blend_op != PNG_BLEND_OP_OVER)
++        png_error(png_ptr, "invalid blend_op in fcTL");
++
++    PNG_UNUSED(delay_num)
++    PNG_UNUSED(delay_den)
++}
++
++png_uint_32 PNGAPI
++png_set_first_frame_is_hidden(png_structp png_ptr, png_infop info_ptr,
++                              png_byte is_hidden)
++{
++    png_debug(1, "in png_first_frame_is_hidden()");
++
++    if (png_ptr == NULL)
++        return 0;
++
++    if (is_hidden)
++        png_ptr->apng_flags |= PNG_FIRST_FRAME_HIDDEN;
++    else
++        png_ptr->apng_flags &= ~PNG_FIRST_FRAME_HIDDEN;
++
++    PNG_UNUSED(info_ptr)
++
++    return 1;
++}
++#endif /* PNG_APNG_SUPPORTED */
++
+ #ifdef PNG_STORE_UNKNOWN_CHUNKS_SUPPORTED
+ static png_byte
+ check_location(png_const_structrp png_ptr, int location)
+diff -Naru libpng-1.6.40.org/pngstruct.h libpng-1.6.40/pngstruct.h
+--- libpng-1.6.40.org/pngstruct.h	2022-11-24 08:37:51.510052177 +0900
++++ libpng-1.6.40/pngstruct.h	2023-09-10 11:08:58.924075760 +0900
+@@ -399,6 +399,27 @@
+    png_byte filter_type;
+ #endif
+ 
++#ifdef PNG_APNG_SUPPORTED
++   png_uint_32 apng_flags;
++   png_uint_32 next_seq_num;         /* next fcTL/fdAT chunk sequence number */
++   png_uint_32 first_frame_width;
++   png_uint_32 first_frame_height;
++
++#ifdef PNG_READ_APNG_SUPPORTED
++   png_uint_32 num_frames_read;      /* incremented after all image data of */
++                                     /* a frame is read */
++#ifdef PNG_PROGRESSIVE_READ_SUPPORTED
++   png_progressive_frame_ptr frame_info_fn; /* frame info read callback */
++   png_progressive_frame_ptr frame_end_fn;  /* frame data read callback */
++#endif
++#endif
++
++#ifdef PNG_WRITE_APNG_SUPPORTED
++   png_uint_32 num_frames_to_write;
++   png_uint_32 num_frames_written;
++#endif
++#endif /* PNG_APNG_SUPPORTED */
++
+ /* New members added in libpng-1.2.0 */
+ 
+ /* New members added in libpng-1.0.2 but first enabled by default in 1.2.0 */
+diff -Naru libpng-1.6.40.org/pngtest.c libpng-1.6.40/pngtest.c
+--- libpng-1.6.40.org/pngtest.c	2023-09-10 11:12:23.044481879 +0900
++++ libpng-1.6.40/pngtest.c	2023-09-10 11:08:58.924075760 +0900
+@@ -875,6 +875,10 @@
+    volatile int num_passes;
+    int pass;
+    int bit_depth, color_type;
++#ifdef PNG_APNG_SUPPORTED
++   png_uint_32 num_frames;
++   png_uint_32 num_plays;
++#endif
+ 
+    row_buf = NULL;
+    error_parameters.file_name = inname;
+@@ -1383,6 +1387,22 @@
+       }
+    }
+ #endif
++
++#ifdef PNG_APNG_SUPPORTED
++   if (png_get_valid(read_ptr, read_info_ptr, PNG_INFO_acTL))
++   {
++      if (png_get_acTL(read_ptr, read_info_ptr, &num_frames, &num_plays))
++      {
++         png_byte is_hidden;
++         pngtest_debug2("Handling acTL chunks (frames %ld, plays %ld)",
++                    num_frames, num_plays);
++         png_set_acTL(write_ptr, write_info_ptr, num_frames, num_plays);
++         is_hidden = png_get_first_frame_is_hidden(read_ptr, read_info_ptr);
++         png_set_first_frame_is_hidden(write_ptr, write_info_ptr, is_hidden);
++      }
++   }
++#endif
++
+ #ifdef PNG_WRITE_UNKNOWN_CHUNKS_SUPPORTED
+    {
+       png_unknown_chunkp unknowns;
+@@ -1463,6 +1483,110 @@
+    t_misc += (t_stop - t_start);
+    t_start = t_stop;
+ #endif
++#ifdef PNG_APNG_SUPPORTED
++   if (png_get_valid(read_ptr, read_info_ptr, PNG_INFO_acTL))
++   {
++      png_uint_32 frame;
++      for (frame = 0; frame < num_frames; frame++)
++      {
++         png_uint_32 frame_width;
++         png_uint_32 frame_height;
++         png_uint_32 x_offset;
++         png_uint_32 y_offset;
++         png_uint_16 delay_num;
++         png_uint_16 delay_den;
++         png_byte dispose_op;
++         png_byte blend_op;
++         png_read_frame_head(read_ptr, read_info_ptr);
++         if (png_get_valid(read_ptr, read_info_ptr, PNG_INFO_fcTL))
++         {
++            png_get_next_frame_fcTL(read_ptr, read_info_ptr,
++                                    &frame_width, &frame_height,
++                                    &x_offset, &y_offset,
++                                    &delay_num, &delay_den,
++                                    &dispose_op, &blend_op);
++         }
++         else
++         {
++            frame_width = width;
++            frame_height = height;
++            x_offset = 0;
++            y_offset = 0;
++            delay_num = 1;
++            delay_den = 1;
++            dispose_op = PNG_DISPOSE_OP_NONE;
++            blend_op = PNG_BLEND_OP_SOURCE;
++         }
++#ifdef PNG_WRITE_APNG_SUPPORTED
++         png_write_frame_head(write_ptr, write_info_ptr, (png_bytepp)&row_buf,
++                              frame_width, frame_height,
++                              x_offset, y_offset,
++                              delay_num, delay_den,
++                              dispose_op, blend_op);
++#endif
++         for (pass = 0; pass < num_passes; pass++)
++         {
++#           ifdef calc_pass_height
++               png_uint_32 pass_height;
++
++               if (num_passes == 7) /* interlaced */
++               {
++                  if (PNG_PASS_COLS(frame_width, pass) > 0)
++                     pass_height = PNG_PASS_ROWS(frame_height, pass);
++
++                  else
++                     pass_height = 0;
++               }
++
++               else /* not interlaced */
++                  pass_height = frame_height;
++#           else
++#              define pass_height frame_height
++#           endif
++
++            pngtest_debug1("Writing row data for pass %d", pass);
++            for (y = 0; y < pass_height; y++)
++            {
++#ifndef SINGLE_ROWBUF_ALLOC
++               pngtest_debug2("Allocating row buffer (pass %d, y = %u)...", pass, y);
++
++               row_buf = (png_bytep)png_malloc(read_ptr,
++                  png_get_rowbytes(read_ptr, read_info_ptr));
++
++               pngtest_debug2("\t0x%08lx (%lu bytes)", (unsigned long)row_buf,
++                  (unsigned long)png_get_rowbytes(read_ptr, read_info_ptr));
++
++#endif /* !SINGLE_ROWBUF_ALLOC */
++               png_read_rows(read_ptr, (png_bytepp)&row_buf, NULL, 1);
++
++#ifdef PNG_WRITE_SUPPORTED
++#ifdef PNGTEST_TIMING
++               t_stop = (float)clock();
++               t_decode += (t_stop - t_start);
++               t_start = t_stop;
++#endif
++               png_write_rows(write_ptr, (png_bytepp)&row_buf, 1);
++#ifdef PNGTEST_TIMING
++               t_stop = (float)clock();
++               t_encode += (t_stop - t_start);
++               t_start = t_stop;
++#endif
++#endif /* PNG_WRITE_SUPPORTED */
++
++#ifndef SINGLE_ROWBUF_ALLOC
++               pngtest_debug2("Freeing row buffer (pass %d, y = %u)", pass, y);
++               png_free(read_ptr, row_buf);
++               row_buf = NULL;
++#endif /* !SINGLE_ROWBUF_ALLOC */
++            }
++         }
++#ifdef PNG_WRITE_APNG_SUPPORTED
++         png_write_frame_tail(write_ptr, write_info_ptr);
++#endif
++      }
++   }
++   else
++#endif
+    for (pass = 0; pass < num_passes; pass++)
+    {
+ #     ifdef calc_pass_height
+diff -Naru libpng-1.6.40.org/pngwrite.c libpng-1.6.40/pngwrite.c
+--- libpng-1.6.40.org/pngwrite.c	2023-09-10 11:09:32.955139032 +0900
++++ libpng-1.6.40/pngwrite.c	2023-09-10 11:08:58.924075760 +0900
+@@ -128,6 +128,10 @@
+        * the application continues writing the PNG.  So check the 'invalid'
+        * flag here too.
+        */
++#ifdef PNG_WRITE_APNG_SUPPORTED
++      if (info_ptr->valid & PNG_INFO_acTL)
++         png_write_acTL(png_ptr, info_ptr->num_frames, info_ptr->num_plays);
++#endif
+ #ifdef PNG_GAMMA_SUPPORTED
+ #  ifdef PNG_WRITE_gAMA_SUPPORTED
+       if ((info_ptr->colorspace.flags & PNG_COLORSPACE_INVALID) == 0 &&
+@@ -373,6 +377,11 @@
+       png_benign_error(png_ptr, "Wrote palette index exceeding num_palette");
+ #endif
+ 
++#ifdef PNG_WRITE_APNG_SUPPORTED
++   if (png_ptr->num_frames_written != png_ptr->num_frames_to_write)
++      png_error(png_ptr, "Not enough frames written");
++#endif
++
+    /* See if user wants us to write information chunks */
+    if (info_ptr != NULL)
+    {
+@@ -1475,6 +1484,43 @@
+ }
+ #endif
+ 
++#ifdef PNG_WRITE_APNG_SUPPORTED
++void PNGAPI
++png_write_frame_head(png_structp png_ptr, png_infop info_ptr,
++    png_bytepp row_pointers, png_uint_32 width, png_uint_32 height,
++    png_uint_32 x_offset, png_uint_32 y_offset,
++    png_uint_16 delay_num, png_uint_16 delay_den, png_byte dispose_op,
++    png_byte blend_op)
++{
++    png_debug(1, "in png_write_frame_head");
++
++    /* there is a chance this has been set after png_write_info was called,
++    * so it would be set but not written. is there a way to be sure? */
++    if (!(info_ptr->valid & PNG_INFO_acTL))
++        png_error(png_ptr, "png_write_frame_head(): acTL not set");
++
++    png_write_reset(png_ptr);
++
++    png_write_reinit(png_ptr, info_ptr, width, height);
++
++    if ( !(png_ptr->num_frames_written == 0 &&
++           (png_ptr->apng_flags & PNG_FIRST_FRAME_HIDDEN) ) )
++        png_write_fcTL(png_ptr, width, height, x_offset, y_offset,
++                       delay_num, delay_den, dispose_op, blend_op);
++
++    PNG_UNUSED(row_pointers)
++}
++
++void PNGAPI
++png_write_frame_tail(png_structp png_ptr, png_infop info_ptr)
++{
++    png_debug(1, "in png_write_frame_tail");
++
++    png_ptr->num_frames_written++;
++
++    PNG_UNUSED(info_ptr)
++}
++#endif /* PNG_WRITE_APNG_SUPPORTED */
+ 
+ #ifdef PNG_SIMPLIFIED_WRITE_SUPPORTED
+ /* Initialize the write structure - general purpose utility. */
+diff -Naru libpng-1.6.40.org/pngwutil.c libpng-1.6.40/pngwutil.c
+--- libpng-1.6.40.org/pngwutil.c	2022-11-24 08:37:51.511052176 +0900
++++ libpng-1.6.40/pngwutil.c	2023-09-10 11:08:58.952075811 +0900
+@@ -821,6 +821,11 @@
+    /* Write the chunk */
+    png_write_complete_chunk(png_ptr, png_IHDR, buf, 13);
+ 
++#ifdef PNG_WRITE_APNG_SUPPORTED
++   png_ptr->first_frame_width = width;
++   png_ptr->first_frame_height = height;
++#endif
++
+    if ((png_ptr->do_filter) == PNG_NO_FILTERS)
+    {
+       if (png_ptr->color_type == PNG_COLOR_TYPE_PALETTE ||
+@@ -1002,8 +1007,17 @@
+                optimize_cmf(data, png_image_size(png_ptr));
+ #endif
+ 
+-         if (size > 0)
+-            png_write_complete_chunk(png_ptr, png_IDAT, data, size);
++            if (size > 0)
++#ifdef PNG_WRITE_APNG_SUPPORTED
++            {
++               if (png_ptr->num_frames_written == 0)
++#endif
++               png_write_complete_chunk(png_ptr, png_IDAT, data, size);
++#ifdef PNG_WRITE_APNG_SUPPORTED
++               else
++                  png_write_fdAT(png_ptr, data, size);
++            }
++#endif /* PNG_WRITE_APNG_SUPPORTED */
+          png_ptr->mode |= PNG_HAVE_IDAT;
+ 
+          png_ptr->zstream.next_out = data;
+@@ -1050,7 +1064,17 @@
+ #endif
+ 
+          if (size > 0)
++#ifdef PNG_WRITE_APNG_SUPPORTED
++         {
++            if (png_ptr->num_frames_written == 0)
++#endif
+             png_write_complete_chunk(png_ptr, png_IDAT, data, size);
++#ifdef PNG_WRITE_APNG_SUPPORTED
++            else
++               png_write_fdAT(png_ptr, data, size);
++         }
++#endif /* PNG_WRITE_APNG_SUPPORTED */
++
+          png_ptr->zstream.avail_out = 0;
+          png_ptr->zstream.next_out = NULL;
+          png_ptr->mode |= PNG_HAVE_IDAT | PNG_AFTER_IDAT;
+@@ -1885,6 +1909,82 @@
+ }
+ #endif
+ 
++#ifdef PNG_WRITE_APNG_SUPPORTED
++void /* PRIVATE */
++png_write_acTL(png_structp png_ptr,
++    png_uint_32 num_frames, png_uint_32 num_plays)
++{
++    png_byte buf[8];
++
++    png_debug(1, "in png_write_acTL");
++
++    png_ptr->num_frames_to_write = num_frames;
++
++    if (png_ptr->apng_flags & PNG_FIRST_FRAME_HIDDEN)
++        num_frames--;
++
++    png_save_uint_32(buf, num_frames);
++    png_save_uint_32(buf + 4, num_plays);
++
++    png_write_complete_chunk(png_ptr, png_acTL, buf, (png_size_t)8);
++}
++
++void /* PRIVATE */
++png_write_fcTL(png_structp png_ptr, png_uint_32 width, png_uint_32 height,
++    png_uint_32 x_offset, png_uint_32 y_offset,
++    png_uint_16 delay_num, png_uint_16 delay_den, png_byte dispose_op,
++    png_byte blend_op)
++{
++    png_byte buf[26];
++
++    png_debug(1, "in png_write_fcTL");
++
++    if (png_ptr->num_frames_written == 0 && (x_offset != 0 || y_offset != 0))
++        png_error(png_ptr, "x and/or y offset for the first frame aren't 0");
++    if (png_ptr->num_frames_written == 0 &&
++        (width != png_ptr->first_frame_width ||
++         height != png_ptr->first_frame_height))
++        png_error(png_ptr, "width and/or height in the first frame's fcTL "
++                           "don't match the ones in IHDR");
++
++    /* more error checking */
++    png_ensure_fcTL_is_valid(png_ptr, width, height, x_offset, y_offset,
++                             delay_num, delay_den, dispose_op, blend_op);
++
++    png_save_uint_32(buf, png_ptr->next_seq_num);
++    png_save_uint_32(buf + 4, width);
++    png_save_uint_32(buf + 8, height);
++    png_save_uint_32(buf + 12, x_offset);
++    png_save_uint_32(buf + 16, y_offset);
++    png_save_uint_16(buf + 20, delay_num);
++    png_save_uint_16(buf + 22, delay_den);
++    buf[24] = dispose_op;
++    buf[25] = blend_op;
++
++    png_write_complete_chunk(png_ptr, png_fcTL, buf, (png_size_t)26);
++
++    png_ptr->next_seq_num++;
++}
++
++void /* PRIVATE */
++png_write_fdAT(png_structp png_ptr,
++    png_const_bytep data, png_size_t length)
++{
++    png_byte buf[4];
++
++    png_write_chunk_header(png_ptr, png_fdAT, (png_uint_32)(4 + length));
++
++    png_save_uint_32(buf, png_ptr->next_seq_num);
++    png_write_chunk_data(png_ptr, buf, 4);
++
++    png_write_chunk_data(png_ptr, data, length);
++
++    png_write_chunk_end(png_ptr);
++
++    png_ptr->next_seq_num++;
++}
++#endif /* PNG_WRITE_APNG_SUPPORTED */
++
+ /* Initializes the row writing capability of libpng */
+ void /* PRIVATE */
+ png_write_start_row(png_structrp png_ptr)
+@@ -2778,4 +2878,39 @@
+    }
+ #endif /* WRITE_FLUSH */
+ }
++
++#ifdef PNG_WRITE_APNG_SUPPORTED
++void /* PRIVATE */
++png_write_reset(png_structp png_ptr)
++{
++    png_ptr->row_number = 0;
++    png_ptr->pass = 0;
++    png_ptr->mode &= ~PNG_HAVE_IDAT;
++}
++
++void /* PRIVATE */
++png_write_reinit(png_structp png_ptr, png_infop info_ptr,
++                 png_uint_32 width, png_uint_32 height)
++{
++    if (png_ptr->num_frames_written == 0 &&
++        (width != png_ptr->first_frame_width ||
++         height != png_ptr->first_frame_height))
++        png_error(png_ptr, "width and/or height in the first frame's fcTL "
++                           "don't match the ones in IHDR");
++    if (width > png_ptr->first_frame_width ||
++        height > png_ptr->first_frame_height)
++        png_error(png_ptr, "width and/or height for a frame greater than"
++                           "the ones in IHDR");
++
++    png_set_IHDR(png_ptr, info_ptr, width, height,
++                 info_ptr->bit_depth, info_ptr->color_type,
++                 info_ptr->interlace_type, info_ptr->compression_type,
++                 info_ptr->filter_type);
++
++    png_ptr->width = width;
++    png_ptr->height = height;
++    png_ptr->rowbytes = PNG_ROWBYTES(png_ptr->pixel_depth, width);
++    png_ptr->usr_width = png_ptr->width;
++}
++#endif /* PNG_WRITE_APNG_SUPPORTED */
+ #endif /* WRITE */
+diff -Naru libpng-1.6.40.org/scripts/symbols.def libpng-1.6.40/scripts/symbols.def
+--- libpng-1.6.40.org/scripts/symbols.def	2022-11-24 08:37:51.515052168 +0900
++++ libpng-1.6.40/scripts/symbols.def	2023-09-10 11:08:58.925075762 +0900
+@@ -253,3 +253,23 @@
+  png_set_eXIf @247
+  png_get_eXIf_1 @248
+  png_set_eXIf_1 @249
++ png_get_acTL @250
++ png_set_acTL @251
++ png_get_num_frames @252
++ png_get_num_plays @253
++ png_get_next_frame_fcTL @254
++ png_set_next_frame_fcTL @255
++ png_get_next_frame_width @256
++ png_get_next_frame_height @257
++ png_get_next_frame_x_offset @258
++ png_get_next_frame_y_offset @259
++ png_get_next_frame_delay_num @260
++ png_get_next_frame_delay_den @261
++ png_get_next_frame_dispose_op @262
++ png_get_next_frame_blend_op @263
++ png_get_first_frame_is_hidden @264
++ png_set_first_frame_is_hidden @265
++ png_read_frame_head @266
++ png_set_progressive_frame_fn @267
++ png_write_frame_head @268
++ png_write_frame_tail @269
diff --git a/thirdparty/openmpt_svn_version.h b/thirdparty/openmpt_svn_version.h
deleted file mode 100644
index a45ed9f22449e7a4164bd2a7881c5ae52f805087..0000000000000000000000000000000000000000
--- a/thirdparty/openmpt_svn_version.h
+++ /dev/null
@@ -1,10 +0,0 @@
-
-#pragma once
-#define OPENMPT_VERSION_SVNVERSION "17963"
-#define OPENMPT_VERSION_REVISION 17963
-#define OPENMPT_VERSION_DIRTY 0
-#define OPENMPT_VERSION_MIXEDREVISIONS 0
-#define OPENMPT_VERSION_URL "https://source.openmpt.org/svn/openmpt/tags/libopenmpt-0.4.32"
-#define OPENMPT_VERSION_DATE "2022-09-25T14:19:05.052596Z"
-#define OPENMPT_VERSION_IS_PACKAGE 1
-
diff --git a/thirdparty/sdl2-mixer-ext.cmake b/thirdparty/sdl2-mixer-ext.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..1998a7bd9217c2bdfd9da5f1b5de544e48cd6c5d
--- /dev/null
+++ b/thirdparty/sdl2-mixer-ext.cmake
@@ -0,0 +1,38 @@
+if(TARGET SDL2_mixer_ext_Static)
+    return()
+endif()
+
+message(STATUS "Third-party: creating target 'SDL2_mixer_ext::SDL2_mixer_ext'")
+
+set(SDL_MIXER_X_SHARED ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES} CACHE BOOL "" FORCE)
+set(SDL_MIXER_X_STATIC ${NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES} CACHE BOOL "" FORCE)
+set(SDL_MIXER_X_DISABLE_INSTALL ON CACHE BOOL "" FORCE)
+set(USE_SYSTEM_SDL2 ON CACHE BOOL "" FORCE)
+set(SDL2_INCLUDE_PATH ${SDL2_INCLUDE_DIR} CACHE PATH "" FORCE)
+set(USE_XMP OFF CACHE PATH "" FORCE)
+
+set(
+	internal_SDL2_mixer_ext_options
+		"SDL_MIXER_X_SHARED ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+		"SDL_MIXER_X_STATIC ${NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+		"SDL_MIXER_X_DISABLE_INSTALL ON"
+		"USE_SYSTEM_SDL2 ON"
+		"USE_XMP OFF"
+)
+
+if(${CMAKE_SYSTEM} MATCHES Windows)
+	#list(APPEND internal_SDL2_mixer_ext_options "DOWNLOAD_AUDIO_CODECS_DEPENDENCY ON")
+	#set(DOWNLOAD_AUDIO_CODECS_DEPENDENCY ON CACHE BOOL "" FORCE)
+endif()
+
+include(FetchContent)
+
+
+FetchContent_Declare(
+	SDL2_mixer_ext
+	OPTIONS ${internal_SDL2_mixer_ext_options}
+	GIT_TAG "2.6.0-1"
+	GIT_REPOSITORY "https://github.com/STJr/SDL-Mixer-X.git"
+)
+
+FetchContent_MakeAvailable(SDL2_mixer_ext)
diff --git a/thirdparty/sdl2.cmake b/thirdparty/sdl2.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..16fbd7563b2f1c57a12a9202c3e89a69a2f9c191
--- /dev/null
+++ b/thirdparty/sdl2.cmake
@@ -0,0 +1,61 @@
+if(TARGET SDL2-static)
+    return()
+endif()
+
+message(STATUS "Third-party: creating target 'SDL2::SDL2'")
+
+set(SDL_STATIC ON CACHE BOOL "" FORCE)
+set(SDL_SHARED OFF CACHE BOOL "" FORCE)
+set(SDL_TEST OFF CACHE BOOL "" FORCE)
+set(SDL2_DISABLE_INSTALL OFF CACHE BOOL "" FORCE)
+
+set(
+	internal_sdl2_options
+
+		"SDL_STATIC ON"
+		"SDL_SHARED OFF"
+		"SDL_TEST OFF"
+		"SDL2_DISABLE_INSTALL OFF"
+)
+
+if(${CMAKE_SYSTEM} MATCHES Windows)
+	list(APPEND internal_sdl2_options "SDL2_DISABLE_SDL2MAIN OFF")
+	option(SDL2_DISABLE_SDL2MAIN   "Disable building/installation of SDL2main" OFF)
+	set(SDL2_DISABLE_SDL2MAIN OFF CACHE BOOL "" FORCE)
+endif()
+if(${CMAKE_SYSTEM} MATCHES Darwin)
+	list(APPEND internal_sdl2_options "SDL2_DISABLE_SDL2MAIN OFF")
+	option(SDL2_DISABLE_SDL2MAIN   "Disable building/installation of SDL2main" OFF)
+	set(SDL2_DISABLE_SDL2MAIN OFF CACHE BOOL "" FORCE)
+endif()
+if(${CMAKE_SYSTEM} MATCHES Linux)
+	list(APPEND internal_sdl2_options "SDL2_DISABLE_SDL2MAIN ON")
+	option(SDL2_DISABLE_SDL2MAIN   "Disable building/installation of SDL2main" ON)
+	set(SDL2_DISABLE_SDL2MAIN ON CACHE BOOL "" FORCE)
+endif()
+
+include(FetchContent)
+
+if (SDL2_USE_THIRDPARTY)
+	FetchContent_Declare(
+		SDL2
+		VERSION 2.30.0
+		GITHUB_REPOSITORY "libsdl-org/SDL"
+		GIT_TAG release-2.30.0
+		OPTIONS ${internal_sdl2_options}
+		OVERRIDE_FIND_PACKAGE
+	)
+else()
+	FetchContent_Declare(
+		SDL2
+		SOURCE_DIR "${CMAKE_SOURCE_DIR}/thirdparty/SDL2/"
+		OPTIONS ${internal_sdl2_options}
+		OVERRIDE_FIND_PACKAGE
+	)
+endif()
+
+FetchContent_MakeAvailable(SDL2)
+
+set(SDL2_INCLUDE_DIR "${SDL2_BINARY_DIR}/include" CACHE PATH "" FORCE)
+set(SDL2_LIBRARY "${SDL2_BINARY_DIR}/SDL2-staticd.lib" CACHE PATH "" FORCE)
+set(SDL2_DIR ${SDL2_BINARY_DIR} CACHE PATH "" FORCE)
diff --git a/thirdparty/vcpkg-overlays/.placeholder b/thirdparty/vcpkg-overlays/.placeholder
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/thirdparty/zlib.cmake b/thirdparty/zlib.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..50c567dda9e664632c709fc374276c401ee59203
--- /dev/null
+++ b/thirdparty/zlib.cmake
@@ -0,0 +1,35 @@
+if(TARGET ZLIB::ZLIB)
+    return()
+endif()
+
+message(STATUS "Third-party: creating target 'ZLIB::ZLIB'")
+
+set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
+
+include(FetchContent)
+
+if (zlib_USE_THIRDPARTY)
+	FetchContent_Declare(
+		ZLIB
+		GITHUB_REPOSITORY "madler/zlib"
+		GIT_TAG v1.3.1
+		OVERRIDE_FIND_PACKAGE
+		OPTIONS
+			"ZLIB_BUILD_EXAMPLES OFF"
+	)
+else()
+	FetchContent_Declare(
+		ZLIB
+		SOURCE_DIR "${CMAKE_SOURCE_DIR}/thirdparty/zlib/"
+		OVERRIDE_FIND_PACKAGE
+		OPTIONS
+			"ZLIB_BUILD_EXAMPLES OFF"
+	)
+endif()
+
+FetchContent_MakeAvailable(ZLIB)
+
+
+add_library(ZLIB::ZLIB ALIAS zlibstatic)
+
+set(ZLIB_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/zlib" "${zlib_BINARY_DIR}" CACHE PATH "" FORCE)
diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json
new file mode 100644
index 0000000000000000000000000000000000000000..547114800f3088a704ec397b482c4bfed86cea3d
--- /dev/null
+++ b/vcpkg-configuration.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json",
+  "overlay-ports": [
+    "./thirdparty/vcpkg-overlays"
+  ]
+}
diff --git a/vcpkg.json b/vcpkg.json
new file mode 100644
index 0000000000000000000000000000000000000000..3b79ec83fbbb5db9e7f7f898108a189f4e8541a7
--- /dev/null
+++ b/vcpkg.json
@@ -0,0 +1,68 @@
+{
+  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
+  "name": "srb2",
+  "version": "1.0.0",
+  "builtin-baseline": "d5ec528843d29e3a52d745a64b469f810b2cedbf",
+  "dependencies": [
+    {
+      "name": "curl",
+      "platform": "!wasm32"
+    },
+    "libgme",
+    {
+      "name":"libopenmpt",
+      "platform": "!wasm32"
+    },
+    "libpng",
+    "miniupnpc",
+    {
+      "name": "sdl2",
+      "default-features": false,
+      "features": [
+        {
+          "name": "wayland",
+          "platform": "linux"
+        },
+        {
+          "name": "x11",
+          "platform": "!windows"
+        }
+      ],
+      "platform": "!wasm32",
+      "version>=": "2.30.6#2"
+    },
+    {
+      "name": "sdl2-mixer-ext",
+      "features": [
+        {
+          "name":"fluidsynth",
+          "platform": "!static"
+        },
+        {
+          "name":"libflac",
+          "platform": "!wasm32"
+        },
+        "libgme",
+        "libmodplug",
+        {
+          "name":"libvorbis",
+          "platform": "!wasm32"
+        },
+        "libxmp",
+        {
+          "name":"mpg123",
+          "platform": "!wasm32"
+        },
+        {
+          "name":"opusfile",
+          "platform": "!wasm32"
+        },
+        "pxtone",
+        "timidity"
+      ],
+      "platform": "!wasm32",
+      "version>=": "2.6.0#0"
+    },
+    "zlib"
+  ]
+}