diff --git a/.gitignore b/.gitignore
index 922fac4aaf187f58ab2145c69641397c4ff81dc5..3090417dd6b00b8796d2743675301615e488707d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@ Win32_LIB_ASM_Release
 *.db
 *.opendb
 /.vs
+/debian
+/assets/debian
diff --git a/.travis.yml b/.travis.yml
index 15a3c844c1879a8accde781002dd4429d0d24cd5..6d2e8cddfc1d55810123c5da2a083197dc2cec9f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,20 @@
+# Travis-CI Config
+#
+# You may use the Deployer to upload packages and builds to external servers.
+# See deployer/travis/deployer_defaults.sh for environment variables to configure.
+
 language: c
 sudo: required
 dist: trusty
 
 matrix:
     include:
+################################
+# Test Buildbots
+# Deployer does not operate on these. See Deployer Buildbots, below.
+# These bots are disabled when a deployment is triggered by 'deployer' branch name AND DPL_TERMINATE_TESTS=1.
+# These bots remain enabled when a deployment is triggered by release tag.
+################################
         - os: linux
           addons:
             apt:
@@ -16,6 +27,7 @@ matrix:
               - gcc-4.4
           compiler: gcc-4.4
           env: GCC44=1
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #gcc-4.4 (Ubuntu/Linaro 4.4.7-8ubuntu1) 4.4.7
         - os: linux
           addons:
@@ -29,6 +41,7 @@ matrix:
               - gcc-4.6
           compiler: gcc-4.6
           env: GCC46=1
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #gcc-4.6 (Ubuntu/Linaro 4.6.4-6ubuntu2) 4.6.4
         - os: linux
           addons:
@@ -42,10 +55,12 @@ matrix:
               - gcc-4.7
           compiler: gcc-4.7
           env: GCC47=1
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #gcc-4.7
         - os: linux
           compiler: gcc
           env: GCC48=1
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
         - os: linux
           addons:
@@ -61,6 +76,7 @@ matrix:
               - gcc-4.8
           compiler: gcc-4.8
           env: GCC48=1
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
         - os: linux
           addons:
@@ -76,6 +92,7 @@ matrix:
               - gcc-7
           compiler: gcc-7
           env: WFLAGS="-Wno-tautological-compare -Wno-error=implicit-fallthrough -Wno-implicit-fallthrough" GCC72=1
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #gcc-7 (Ubuntu 7.2.0-1ubuntu1~14.04) 7.2.0 20170802
         - os: linux
           addons:
@@ -90,10 +107,12 @@ matrix:
               - p7zip-full
               - gcc-8
           compiler: gcc-8
-          env: WFLAGS="-Wno-tautological-compare -Wno-error=implicit-fallthrough -Wno-implicit-fallthrough -Wno-error=format-overflow" GCC81=1
+          env: WFLAGS="-Wno-tautological-compare -Wno-error=implicit-fallthrough -Wno-implicit-fallthrough -Wno-error=format-overflow -Wno-error=format-truncation" GCC81=1
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #gcc-8 (Ubuntu 7.2.0-1ubuntu1~14.04) 8.1.0
         - os: linux
           compiler: clang
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #clang version 3.5.0 (tags/RELEASE_350/final)
         - os: linux
           addons:
@@ -108,6 +127,7 @@ matrix:
               - p7zip-full
               - clang-3.5
           compiler: clang-3.5
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #Ubuntu clang version 3.5.0-4ubuntu2~trusty2 (tags/RELEASE_350/final) (based on LLVM 3.5.0)
         - os: linux
           addons:
@@ -123,6 +143,7 @@ matrix:
               - p7zip-full
               - clang-3.6
           compiler: clang-3.6
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #Ubuntu clang version 3.6.2-svn240577-1~exp1 (branches/release_36) (based on LLVM 3.6.2)
         - os: linux
           addons:
@@ -138,6 +159,7 @@ matrix:
               - p7zip-full
               - clang-3.7
           compiler: clang-3.7
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #Ubuntu clang version 3.7.1-svn253571-1~exp1 (branches/release_37) (based on LLVM 3.7.1)
         - os: linux
           addons:
@@ -153,6 +175,7 @@ matrix:
               - p7zip-full
               - clang-3.8
           compiler: clang-3.8
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #clang version 3.8.1-svn271127-1~exp1 (branches/release_38)
         - os: linux
           addons:
@@ -168,6 +191,7 @@ matrix:
               - p7zip-full
               - clang-3.9
           compiler: clang-3.9
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #clang version 3.9.X
 #        - os: linux
 #          addons:
@@ -183,6 +207,7 @@ matrix:
 #              - p7zip-full
 #              - clang-4.0
 #          compiler: clang-4.0
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #clang version 4.0.X
 #        - os: linux
 #          addons:
@@ -198,34 +223,325 @@ matrix:
 #              - p7zip-full
 #              - clang-5.0
 #          compiler: clang-5.0
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #clang version 5.0.X
 #        - os: osx
 #          osx_image: beta-xcode6.1
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn)
 #        - os: osx
 #          osx_image: beta-xcode6.2
 #          compiler: gcc
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #Apple LLVM version 6.0 (clang-600.0.57) (based on LLVM 3.5svn)
 ##        - os: osx
 ##          osx_image: beta-xcode6.3
+##          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 ##          #I think xcode.6.3 VM is broken, it does not boot
 #        - os: osx
 #          osx_image: xcode6.4
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn)
 #        - os: osx
 #          osx_image: xcode7
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #Apple LLVM version 7.0.0 (clang-700.0.72)
 #        - os: osx
 #          osx_image: xcode7.1
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #Apple LLVM version 7.0.0 (clang-700.1.76)
 #        - os: osx
 #          osx_image: xcode7.2
+#          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
 #          #Apple LLVM version 7.0.2 (clang-700.1.81)
 #        - os: osx
 #          osx_image: xcode7.3
+#          #Apple LLVM version 7.3.0 (clang-703.0.31)
+#        - os: osx
+#          osx_image: xcode7.3
 #          #Apple LLVM version 7.3.0 (clang-703.0.31)
         - os: osx
+          if: env(DPL_ENABLED) != "1" OR env(DPL_TERMINATE_TESTS) != "1" OR NOT branch =~ /^.*deployer.*$/
           #Default: macOS 10.13 and Xcode 9.4.1
+
+
+################################
+# Deployer Buildbots - OSX
+################################
+        - os: osx
+          if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+              AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+              AND env(DPL_TERMINATE_MAIN) != "1"
+          env:
+          - _DPL_JOB_ENABLED=1
+          - _DPL_JOB_NAME=osx
+          - _DPL_FTP_TARGET=1
+          - _DPL_PACKAGE_BINARY=1
+          #Apple LLVM version 7.3.0 (clang-703.0.31)
+
+
+################################
+# Deployer Buildbots - Linux assets
+# Set DPL_TERMINATE_ASSETS to disable all of these
+# List Ubuntu LTS next, newest to oldest
+# Then list non-LTS, newest to oldest
+################################
+        - os: linux
+          addons:
+            apt:
+              packages:
+              - libsdl2-mixer-dev
+              - libpng-dev
+              - libgl1-mesa-dev
+              - libgme-dev
+              - p7zip-full
+              - gcc-4.8
+          compiler: gcc-4.8
+          dist: xenial
+          if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+              AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+              AND env(DPL_TERMINATE_ASSETS) != "1"
+          env:
+          - _DPL_JOB_ENABLED=1
+          - _DPL_JOB_NAME=bionic-asset
+          - _DPL_DPUT_TARGET=1
+          - _DPL_PACKAGE_SOURCE=1
+          - _DPL_PACKAGE_MAIN=0
+          - _DPL_PACKAGE_ASSET=1
+          - PACKAGE_DISTRO=bionic
+          #- PACKAGE_SUBVERSION=~18.04bionic
+          #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+
+        ################################
+        # The below asset bots produce packages that occupy too much space.
+        # It would be nice if the asset files were not included in the source package itself,
+        # so these can deploy to each Ubuntu target without manual intervention.
+        #
+        # Currently, to get around Launchpad's space limitation,
+        # copy the packages from *one* bot and the space usage is not increased.
+        ################################
+        # - os: linux
+        #   addons:
+        #     apt:
+        #       packages:
+        #       - libsdl2-mixer-dev
+        #       - libpng-dev
+        #       - libgl1-mesa-dev
+        #       - libgme-dev
+        #       - p7zip-full
+        #       - gcc-4.8
+        #   compiler: gcc-4.8
+        #   dist: trusty
+        #   if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+        #       AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+        #       AND env(DPL_TERMINATE_ASSETS) != "1"
+        #   env:
+        #   - _DPL_JOB_ENABLED=1
+        #   - _DPL_JOB_NAME=trusty-asset
+        #   - _DPL_DPUT_TARGET=1
+        #   - _DPL_PACKAGE_SOURCE=1
+        #   - _DPL_PACKAGE_MAIN=0
+        #   - _DPL_PACKAGE_ASSET=1
+        #   - PACKAGE_DISTRO=trusty
+        #   #- PACKAGE_SUBVERSION=~14.04trusty
+        #   #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+        # - os: linux
+        #   addons:
+        #     apt:
+        #       packages:
+        #       - libsdl2-mixer-dev
+        #       - libpng-dev
+        #       - libgl1-mesa-dev
+        #       - libgme-dev
+        #       - p7zip-full
+        #       - gcc-4.8
+        #   compiler: gcc-4.8
+        #   dist: xenial
+        #   if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+        #       AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+        #       AND env(DPL_TERMINATE_ASSETS) != "1"
+        #   env:
+        #   - _DPL_JOB_ENABLED=1
+        #   - _DPL_JOB_NAME=disco-asset
+        #   - _DPL_DPUT_TARGET=1
+        #   - _DPL_PACKAGE_SOURCE=1
+        #   - _DPL_PACKAGE_MAIN=0
+        #   - _DPL_PACKAGE_ASSET=1
+        #   - PACKAGE_DISTRO=disco
+        #   #- PACKAGE_SUBVERSION=~19.04disco
+        #   #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+        # - os: linux
+        #   addons:
+        #     apt:
+        #       packages:
+        #       - libsdl2-mixer-dev
+        #       - libpng-dev
+        #       - libgl1-mesa-dev
+        #       - libgme-dev
+        #       - p7zip-full
+        #       - gcc-4.8
+        #   compiler: gcc-4.8
+        #   dist: xenial
+        #   if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+        #       AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+        #       AND env(DPL_TERMINATE_ASSETS) != "1"
+        #   env:
+        #   - _DPL_JOB_ENABLED=1
+        #   - _DPL_JOB_NAME=cosmic-asset
+        #   - _DPL_DPUT_TARGET=1
+        #   - _DPL_PACKAGE_SOURCE=1
+        #   - _DPL_PACKAGE_MAIN=0
+        #   - _DPL_PACKAGE_ASSET=1
+        #   - PACKAGE_DISTRO=cosmic
+        #   #- PACKAGE_SUBVERSION=~18.10cosmic
+        #   #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+        # - os: linux
+        #   addons:
+        #     apt:
+        #       packages:
+        #       - libsdl2-mixer-dev
+        #       - libpng-dev
+        #       - libgl1-mesa-dev
+        #       - libgme-dev
+        #       - p7zip-full
+        #       - gcc-4.8
+        #   compiler: gcc-4.8
+        #   dist: xenial
+        #   if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+        #       AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+        #       AND env(DPL_TERMINATE_ASSETS) != "1"
+        #   env:
+        #   - _DPL_JOB_ENABLED=1
+        #   - _DPL_JOB_NAME=xenial-asset
+        #   - _DPL_DPUT_TARGET=1
+        #   - _DPL_PACKAGE_SOURCE=1
+        #   - _DPL_PACKAGE_MAIN=0
+        #   - _DPL_PACKAGE_ASSET=1
+        #   - PACKAGE_DISTRO=xenial
+        #   #- PACKAGE_SUBVERSION=~16.04xenial
+        #   #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+
+
+################################
+# Deployer Buildbots - Linux binaries
+# List Ubuntu LTS, newest to oldest
+# Then list non-LTS, newest to oldest
+################################
+        - os: linux
+          addons:
+            apt:
+              packages:
+              - libsdl2-mixer-dev
+              - libpng-dev
+              - libgl1-mesa-dev
+              - libgme-dev
+              - p7zip-full
+              - gcc-4.8
+          compiler: gcc-4.8
+          dist: xenial
+          if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+              AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+              AND env(DPL_TERMINATE_MAIN) != "1"
+          env:
+          - _DPL_JOB_ENABLED=1
+          - _DPL_JOB_NAME=bionic
+          - _DPL_DPUT_TARGET=1
+          - _DPL_PACKAGE_SOURCE=1
+          - PACKAGE_DISTRO=bionic
+          - PACKAGE_SUBVERSION=~18.04bionic
+          #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+        - os: linux
+          addons:
+            apt:
+              packages:
+              - libsdl2-mixer-dev
+              - libpng-dev
+              - libgl1-mesa-dev
+              - libgme-dev
+              - p7zip-full
+              - gcc-4.8
+          compiler: gcc-4.8
+          dist: trusty
+          if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+              AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+              AND env(DPL_TERMINATE_MAIN) != "1"
+          env:
+          - _DPL_JOB_ENABLED=1
+          - _DPL_JOB_NAME=trusty
+          - _DPL_DPUT_TARGET=1
+          - _DPL_PACKAGE_SOURCE=1
+          - PACKAGE_DISTRO=trusty
+          - PACKAGE_SUBVERSION=~14.04trusty
+          #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+        - os: linux
+          addons:
+            apt:
+              packages:
+              - libsdl2-mixer-dev
+              - libpng-dev
+              - libgl1-mesa-dev
+              - libgme-dev
+              - p7zip-full
+              - gcc-4.8
+          compiler: gcc-4.8
+          dist: xenial
+          if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+              AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+              AND env(DPL_TERMINATE_MAIN) != "1"
+          env:
+          - _DPL_JOB_ENABLED=1
+          - _DPL_JOB_NAME=disco
+          - _DPL_DPUT_TARGET=1
+          - _DPL_PACKAGE_SOURCE=1
+          - PACKAGE_DISTRO=disco
+          - PACKAGE_SUBVERSION=~19.04disco
+          #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+        - os: linux
+          addons:
+            apt:
+              packages:
+              - libsdl2-mixer-dev
+              - libpng-dev
+              - libgl1-mesa-dev
+              - libgme-dev
+              - p7zip-full
+              - gcc-4.8
+          compiler: gcc-4.8
+          dist: xenial
+          if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+              AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+              AND env(DPL_TERMINATE_MAIN) != "1"
+          env:
+          - _DPL_JOB_ENABLED=1
+          - _DPL_JOB_NAME=cosmic
+          - _DPL_DPUT_TARGET=1
+          - _DPL_PACKAGE_SOURCE=1
+          - PACKAGE_DISTRO=cosmic
+          - PACKAGE_SUBVERSION=~18.10cosmic
+          #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
+        - os: linux
+          addons:
+            apt:
+              packages:
+              - libsdl2-mixer-dev
+              - libpng-dev
+              - libgl1-mesa-dev
+              - libgme-dev
+              - p7zip-full
+              - gcc-4.8
+          compiler: gcc-4.8
+          dist: xenial
+          if: env(DPL_ENABLED) = "1" AND (env(_DPL_JOB_ENABLED) = "1" OR env(DPL_JOB_ENABLE_ALL) = "1")
+              AND (branch =~ /^.*deployer.*$/ OR (tag IS present AND env(DPL_TAG_ENABLED) = "1"))
+              AND env(DPL_TERMINATE_MAIN) != "1"
+          env:
+          - _DPL_JOB_ENABLED=1
+          - _DPL_JOB_NAME=xenial
+          - _DPL_DPUT_TARGET=1
+          - _DPL_PACKAGE_SOURCE=1
+          - PACKAGE_DISTRO=xenial
+          - PACKAGE_SUBVERSION=~16.04xenial
+          #gcc-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.1) 4.8.5
     allow_failures:
       - compiler: clang-3.5
       - compiler: clang-3.6
@@ -235,12 +551,14 @@ matrix:
       - compiler: clang-4.0
       - compiler: clang-5.0
 
+
 cache:
   apt: true
   ccache: true
   directories:
   - $HOME/srb2_cache
 
+
 addons:
   apt:
     packages:
@@ -248,6 +566,7 @@ addons:
     - libpng-dev
     - libgl1-mesa-dev
     - libgme-dev
+    - zlib1g-dev
     - p7zip-full
   homebrew:
     taps:
@@ -260,18 +579,115 @@ addons:
     update: true
 
 
+
+before_install:
+  # Initialize Deployer defaults
+  - . ./deployer/travis/deployer_defaults.sh
+
+  # Initialize Deployer; check if Deployer is enabled
+  # This needs to be run in the current shell so that $__DPL_ACTIVE is set for this session
+  - . ./deployer/travis/deployer.sh
+
+  # Also check if we should now terminate -- see `deployer.sh` for conditions.
+  # This should never happen on non-release buildbots when Deployer is not triggered.
+  - if [[ "$__DPL_TRY_TERMINATE_EARLY" == "1" ]]; then
+      if [[ "$__DPL_ACTIVE" != "1" ]]; then
+        echo "Exiting early because this job is not deploying.";
+        exit;
+      fi;
+    fi
+
+  # If we're triggered by release tag, force ASSET_FILES_OPTIONAL_GET=1
+  - if [[ "$__DPL_TAG_ELIGIBLE" = "1" ]]; then
+      ASSET_FILES_OPTIONAL_GET=1;
+    fi;
+
+
+install:
+  # Install OS X library dependencies via Homebrew
+  # Do this differently for release buildbots:
+  #     * `brew install --build-bottle` builds libraries for x86_64's lowest common denominator CPU, core2
+  #     * `sdl2_mixer` requires options from the formula tap https://github.com/mazmazz/homebrew-srb2
+  #     * `brew postinstall` runs post-install scripts after building a bottle
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
+      if [[ "$__DPL_ACTIVE" == "1" ]]; then
+        brew install --build-bottle sdl2 game-music-emu;
+        brew install --build-bottle mazmazz/srb2/sdl2_mixer --with-flac --with-mpg123;
+        brew postinstall sdl2 game-music-emu mazmazz/srb2/sdl2_mixer;
+      fi;
+    fi
+  - mkdir -p $HOME/srb2_cache
+
+
 before_script:
-  - wget --verbose --server-response -c http://rosenthalcastle.org/srb2/SRB2-v2115-assets-2.7z -O $HOME/srb2_cache/SRB2-v2115-assets-2.7z
-  - 7z x $HOME/srb2_cache/SRB2-v2115-assets-2.7z -oassets
+  # OLDPWD is root repo folder
+  - OLDPWD=$PWD
+  - __ASSET_DIRECTORY="$OLDPWD/assets/installer"
+  - mkdir -p "$__ASSET_DIRECTORY"
+  - cd "$HOME/srb2_cache"
+
+  # Get stat command so we know what the cached archive date is.
+  # stat is different for OSX
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
+      STATCMD="stat -f %m";
+    else
+      STATCMD="stat -c %y";
+    fi
+
+  # Get asset files (required for MD5)
+  # See `deployer_defaults.sh` for asset download path
+  - if [[ "$ASSET_ARCHIVE_PATH" != "" ]]; then
+      if [ -f "$(basename $ASSET_ARCHIVE_PATH)" ]; then
+        echo "$(basename $ASSET_ARCHIVE_PATH) cache date -- $($STATCMD $(basename $ASSET_ARCHIVE_PATH))";
+      fi;
+      wget --verbose --server-response -N "$ASSET_ARCHIVE_PATH";
+      7z x "$(basename $ASSET_ARCHIVE_PATH)" -o"$__ASSET_DIRECTORY" -aos;
+    fi;
+
+  # Get optional files too
+  - if [[ "$__DPL_ACTIVE" == "1" ]] && [[ "$ASSET_FILES_OPTIONAL_GET" == "1" ]] && [[ "$ASSET_ARCHIVE_OPTIONAL_PATH" != "" ]]; then
+      if [ -f "$(basename $ASSET_ARCHIVE_OPTIONAL_PATH)" ]; then
+        echo "$(basename $ASSET_ARCHIVE_OPTIONAL_PATH) cache date -- $($STATCMD $(basename $ASSET_ARCHIVE_OPTIONAL_PATH))";
+      fi;
+      wget --verbose --server-response -N "$ASSET_ARCHIVE_OPTIONAL_PATH";
+      7z x "$(basename $ASSET_ARCHIVE_OPTIONAL_PATH)" -o"$__ASSET_DIRECTORY" -aos;
+    fi;
+
+  # Go back to root repo folder
+  - cd "$OLDPWD"
+
+  # Prepare CMake asset lists
+  - SRB2_ASSET_HASHED=$(echo ${ASSET_FILES_HASHED// /\;})
+  - SRB2_ASSET_DOCS=$(echo ${ASSET_FILES_DOCS// /\;})
+  - SRB2_ASSET_DIRECTORY="$__ASSET_DIRECTORY"
+
+  # Prepare CMake
   - mkdir build
   - cd build
+  - mkdir package
   - export CFLAGS="-Wall -W -Werror $WFLAGS"
   - export CCACHE_COMPRESS=true
-  - cmake .. -DCMAKE_BUILD_TYPE=Release
+  # If OS X, set -march=core2 to build compatible binaries with old Macs
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
+      export CFLAGS="${CFLAGS} -march=core2";
+    fi;
+  - cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$PWD/bin -DCPACK_PACKAGE_DIRECTORY=$PWD/package
+      -DSRB2_ASSET_HASHED="${SRB2_ASSET_HASHED}" -DSRB2_ASSET_DOCS="${SRB2_ASSET_DOCS}"
+      -DSRB2_ASSET_DIRECTORY="${SRB2_ASSET_DIRECTORY}"
+      -DCPACK_PACKAGE_DESCRIPTION_SUMMARY="${PROGRAM_NAME}"
+      -DCPACK_PACKAGE_VENDOR="${PROGRAM_VENDOR}"
+      -DSRB2_SDL2_EXE_NAME="${PROGRAM_FILENAME}"
 
-before_install:
-  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then curl -O -L https://www.libsdl.org/release/SDL2-2.0.6.dmg; hdiutil attach SDL2-2.0.6.dmg; sudo cp -a /Volumes/SDL2/SDL2.framework /Library/Frameworks/; fi
-  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then curl -O -L https://www.libsdl.org/projects/SDL_mixer/release/SDL2_mixer-2.0.1.dmg; hdiutil attach SDL2_mixer-2.0.1.dmg; sudo cp -a /Volumes/SDL2_mixer/SDL2_mixer.framework /Library/Frameworks/; fi
-  - mkdir -p $HOME/srb2_cache
+script:
+  # Build our Makefile from Cmake!
+  - if [[ "$__DPL_ACTIVE" == "1" ]]; then
+      . ../deployer/travis/deployer_build.sh;
+    else
+      make -k;
+    fi;
 
-script: make -k
+after_success:
+  # Run the upload scripts
+  # These do nothing if Deployer is not triggered
+  - . ../deployer/travis/deployer_ftp.sh
+  - . ../deployer/travis/deployer_dput.sh
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0a5507b924c38620bb9845e332e1af0b72b4972c..b702218592579c74176c96e7cf8ea761c9bc911f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,8 @@
 cmake_minimum_required(VERSION 3.0)
+# DO NOT CHANGE THIS SRB2 STRING! Some variable names depend on this string.
+# Version change is fine.
 project(SRB2
-	VERSION 2.1.23
+	VERSION 2.1.24
 	LANGUAGES C)
 
 if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR})
@@ -92,8 +94,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
 set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
 
 # Set EXE names so the assets CMakeLists can refer to its target
-set(SRB2_SDL2_EXE_NAME srb2)
-set(SRB2_WIN_EXE_NAME srb2dd)
+set(SRB2_SDL2_EXE_NAME srb2 CACHE STRING "Executable binary output name")
+set(SRB2_WIN_EXE_NAME srb2dd CACHE STRING "Executable binary output name for DirectDraw build")
 
 include_directories(${CMAKE_CURRENT_BINARY_DIR}/src)
 
@@ -122,8 +124,8 @@ if(${CMAKE_SYSTEM} MATCHES "Darwin")
 	set(CPACK_GENERATOR "DragNDrop")
 endif()
 
-set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Sonic Robo Blast 2")
-set(CPACK_PACKAGE_VENDOR "Sonic Team Jr.")
+set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Sonic Robo Blast 2" CACHE STRING "Program name for display purposes")
+set(CPACK_PACKAGE_VENDOR "Sonic Team Jr." CACHE STRING "Vendor name for display purposes")
 #set(CPACK_PACKAGE_DESCRIPTION_FILE )
 set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
 set(CPACK_PACKAGE_VERSION_MAJOR ${SRB2_VERSION_MAJOR})
diff --git a/appveyor.yml b/appveyor.yml
index f0f843fbb29dee5becc5d83404d91b557031107e..98da61dbf987652a76e4666eb41604c5e72bf6cb 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.1.23.{branch}-{build}
+version: 2.1.24.{branch}-{build}
 os: MinGW
 
 environment:
diff --git a/assets/.gitignore b/assets/.gitignore
index 9ed61ca1ad2a899cddaa9311c3c0425e54cc68c5..d6e46a75b0f0d3aa04d5c319d41c456ef202bc54 100644
--- a/assets/.gitignore
+++ b/assets/.gitignore
@@ -1,5 +1,10 @@
-*
-*.*
+*.srb
+*.pk3
+*.dta
+*.wad
+*.txt
 !README.txt
 !LICENSE.txt
-!LICENSE-3RD-PARTY.txt
\ No newline at end of file
+!LICENSE-3RD-PARTY.txt
+!CMakeLists.txt
+!debian-template/*
diff --git a/assets/CMakeLists.txt b/assets/CMakeLists.txt
index 6edb3df130de0d7dd28c4d6d8431ea425a3fd227..68ff0fdf9fdd64134989b8a2a16fd7c74ff589a5 100644
--- a/assets/CMakeLists.txt
+++ b/assets/CMakeLists.txt
@@ -1,40 +1,58 @@
 ## Assets Target Configuration ##
 
-# MD5 generation
-set(SRB2_ASSET_ALL
-	${CMAKE_CURRENT_SOURCE_DIR}/srb2.srb
-	${CMAKE_CURRENT_SOURCE_DIR}/player.dta
-	${CMAKE_CURRENT_SOURCE_DIR}/rings.dta
-	${CMAKE_CURRENT_SOURCE_DIR}/zones.dta
-	${CMAKE_CURRENT_SOURCE_DIR}/patch.dta
-	${CMAKE_CURRENT_SOURCE_DIR}/music.dta
-	${CMAKE_CURRENT_SOURCE_DIR}/README.txt
-	${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt
-	${CMAKE_CURRENT_SOURCE_DIR}/LICENSE-3RD-PARTY.txt
-)
+# For prepending the current source path, later
+FUNCTION(PREPEND var prefix)
+   SET(listVar "")
+   FOREACH(f ${ARGN})
+      LIST(APPEND listVar "${prefix}/${f}")
+   ENDFOREACH(f)
+   SET(${var} "${listVar}" PARENT_SCOPE)
+ENDFUNCTION(PREPEND)
+
+set(SRB2_ASSET_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/installer"
+	CACHE STRING "Path to directory that contains all asset files for the installer.")
 
 set(SRB2_ASSET_HASHED
-	srb2.srb
-	player.dta
-	rings.dta
-	zones.dta
-	patch.dta
+"srb2.srb;\
+player.dta;\
+rings.dta;\
+zones.dta;\
+patch.dta"
+	CACHE STRING "Asset filenames to apply MD5 checks. No spaces between entries!"
+)
+
+set(SRB2_ASSET_DOCS
+"README.txt;\
+LICENSE.txt;\
+LICENSE-3RD-PARTY.txt"
+	CACHE STRING "Documentation filenames. In OS X, these are packaged separately from other assets. No spaces between entries!"
 )
 
+PREPEND(SRB2_ASSET_DOCS ${SRB2_ASSET_DIRECTORY} ${SRB2_ASSET_DOCS})
+
 foreach(SRB2_ASSET ${SRB2_ASSET_HASHED})
-	file(MD5 ${CMAKE_CURRENT_SOURCE_DIR}/${SRB2_ASSET} "SRB2_ASSET_${SRB2_ASSET}_HASH")
+	file(MD5 ${SRB2_ASSET_DIRECTORY}/${SRB2_ASSET} "SRB2_ASSET_${SRB2_ASSET}_HASH")
 	set(SRB2_ASSET_${SRB2_ASSET}_HASH ${SRB2_ASSET_${SRB2_ASSET}_HASH} PARENT_SCOPE)
 endforeach()
 
 # Installation
 
-if(CLANG)
+if(${CMAKE_SYSTEM} MATCHES Darwin)
 	get_target_property(outname SRB2SDL2 OUTPUT_NAME)
-	install(FILES ${SRB2_ASSET_ALL}
+	install(DIRECTORY "${SRB2_ASSET_DIRECTORY}/"
 		DESTINATION "${outname}.app/Contents/Resources"
 	)
+	install(FILES ${SRB2_ASSET_DOCS}
+		DESTINATION .
+		OPTIONAL
+	)
 else()
-	install(FILES ${SRB2_ASSET_ALL}
+	install(DIRECTORY "${SRB2_ASSET_DIRECTORY}/"
 		DESTINATION .
 	)
+	# Docs are assumed to be located in SRB2_ASSET_DIRECTORY, so don't install again
+	#install(FILES ${SRB2_ASSET_DOCS}
+	#	DESTINATION .
+	#	OPTIONAL
+	#)
 endif()
diff --git a/assets/debian/README.Debian b/assets/debian-template/README.Debian
similarity index 59%
rename from assets/debian/README.Debian
rename to assets/debian-template/README.Debian
index 68c952a4e8d0fa8dca227994afcb8d3bbd3cc38b..f3fe90030427b38888e2edb73312702580b52fef 100644
--- a/assets/debian/README.Debian
+++ b/assets/debian-template/README.Debian
@@ -12,9 +12,39 @@ with apt-key add. Thanks!
  -- Callum Dickinson <gcfreak_ag20@hotmail.com>  Fri, 26 Nov 2010 18:25:31 +1300
 
 
+---------------
+
+
+Templating
+
+Note that you MUST run [repo-root]/debian_template.sh before running debuild
+on these scripts! debian_template.sh fills these template files with working values.
+
+You should also set PACKAGE_NAME_EMAIL="John Doe <jdoe@example.com>" to match
+the identity of the key you will use to sign the package.
+
+
+Building for Launchpad PPA
+
+Run this step first:
+
+    1. source [repo-root]/debian_template.sh
+       * Initializes defaults for the package variables and fills in templates.
+
+Use these steps to prepare building a source package for Launchpad:
+
+    1. cd [repo-root]/assets/
+    2. debuild -T clean-all (optional; if you already have asset files, this clears them)
+
+Build the source package:
+
+    1. debuild -T build (this downloads the asset files from srb2.org if necessary)
+    2. debuild -S (builds the source package for Launchpad, including the asset files)
+
+
 Signing for Launchpad PPA
 
-First, follow the above instructions to generate a GnuPG key with your identity. You will need
+First, follow Callum's instructions to generate a GnuPG key with your identity. You will need
 to publish the fingerprint of that key to Ubuntu's key server.
 
     https://help.ubuntu.com/community/GnuPrivacyGuardHowto#Uploading_the_key_to_Ubuntu_keyserver
@@ -26,22 +56,18 @@ upload signed source packages and publish them onto your PPA.
 IF YOU UPLOAD A PACKAGE and Launchpad does NOT send you a confirmation or rejection email, that
 means your key is not set up correctly with your Launchpad account.
 
+Finally, if your packages have not already been signed, follow these steps:
 
-Building for Launchpad PPA
+    1. cd ..
+       * Packages are located in the parent folder of where debuild was called
+    2. debsign "srb2-data_[version]_source.changes"
+       * You may need to specify -k [key-fingerprint]
 
-Use these steps to prepare building a source package for Launchpad:
-
-    1. Highly recommend copying the assets/ folder to outside your repo folder, or else the asset
-       files may be included in the main source package, when you build that.
-    2. cd [wherever-your-assets-folder-is]/assets/
-    3. debuild -T clean (optional, if you already have asset files)
-
-Building the source package is a two-step process:
 
-    1. debuild -T build (this downloads the asset files from srb2.org if necessary)
-    2. debuild -S (builds the source package for Launchpad, including the asset files)
+Uploading for Launchpad PPA
 
-Then follow the instructions at <https://help.launchpad.net/Packaging/PPA/Uploading> to upload
+Follow the instructions at <https://help.launchpad.net/Packaging/PPA/Uploading> to upload
 to your PPA and have Launchpad build your binary deb packages.
 
+
  -- Marco Zafra <marco.a.zafra@gmail.com>  Mon, 26 Nov 2018 21:13:00 -0500
diff --git a/assets/debian/README.source b/assets/debian-template/README.source
similarity index 100%
rename from assets/debian/README.source
rename to assets/debian-template/README.source
diff --git a/assets/debian-template/changelog b/assets/debian-template/changelog
new file mode 100644
index 0000000000000000000000000000000000000000..64562e2a3140dbe67052291a8a7c2fab0a85f25e
--- /dev/null
+++ b/assets/debian-template/changelog
@@ -0,0 +1,5 @@
+${PACKAGE_NAME}-data (${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION}) ${PACKAGE_DISTRO}; urgency=${PACKAGE_URGENCY}
+
+  * ${PROGRAM_NAME} v${PROGRAM_VERSION} asset data
+
+ -- ${PACKAGE_NAME_EMAIL}  ${__PACKAGE_DATETIME}
diff --git a/assets/debian/compat b/assets/debian-template/compat
similarity index 100%
rename from assets/debian/compat
rename to assets/debian-template/compat
diff --git a/assets/debian/control b/assets/debian-template/control
similarity index 84%
rename from assets/debian/control
rename to assets/debian-template/control
index 22d9643eedc665ced1dad165a878beabe98edb42..ae5c0ce67f7286bcc0b8f058f584747e6b1a6380 100644
--- a/assets/debian/control
+++ b/assets/debian-template/control
@@ -1,15 +1,15 @@
 # SRB2-data Debian package control file.
 
-Source: srb2-data
+Source: ${PACKAGE_NAME}-data
 Section: games
 Priority: extra
-Maintainer: Sonic Team Junior <stjr@srb2.org>
+Maintainer: ${PACKAGE_GROUP_NAME_EMAIL}
 Build-Depends: debhelper (>= 7.0.50~),
  wget
 Standards-Version: 3.8.4
-Homepage: http://www.srb2.org
+Homepage: ${PACKAGE_WEBSITE}
 
-Package: srb2-data
+Package: ${PACKAGE_NAME}-data
 Architecture: all
 Description: A cross-platform 3D Sonic fangame
  Sonic Robo Blast 2 is a 3D open-source Sonic the Hedgehog
diff --git a/debian/copyright b/assets/debian-template/copyright
similarity index 57%
rename from debian/copyright
rename to assets/debian-template/copyright
index 97d606b0fb67b73eaae1858f43652006edfd91b6..cc47c453bf2b0ccc6db34c57a03dfe1ce2fc75a7 100644
--- a/debian/copyright
+++ b/assets/debian-template/copyright
@@ -1,18 +1,18 @@
 This work was packaged for Debian by:
 
-    Marco Zafra <marco.a.zafra@gmail.com>  Mon, 26 Nov 2018 14:31:00 -0500
+    ${PACKAGE_NAME_EMAIL}  ${__PACKAGE_DATETIME}
 
 It was downloaded from:
 
-    <http://srb2.org>
+    ${PACKAGE_WEBSITE}
 
 Upstream Author(s):
 
-    Sonic Team Junior <stjr@srb2.org>
+    ${PACKAGE_GROUP_NAME_EMAIL}
 
 Copyright:
 
-    Copyright (C) 1998-2018 Sonic Team Junior
+    Copyright (C) 1998-2018 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 Sonic Team Junior <stjr@srb2.org>
+    Copyright (C) 2010-2018 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/assets/debian/rules b/assets/debian-template/rules
old mode 100755
new mode 100644
similarity index 70%
rename from assets/debian/rules
rename to assets/debian-template/rules
index a34a3393f261cbf019e4ddd06d8e38db884f45c2..c2d19922d406ea891324d8f066d40b4fee1c193a
--- a/assets/debian/rules
+++ b/assets/debian-template/rules
@@ -23,6 +23,16 @@
 #
 #############################################################################
 
+#############################################################################
+#
+# !!!!!!!!!! DEPLOYER NOTE !!!!!!!!!!
+#
+# Variables to be templated are curly-braced ${PACKAGE_INSTALL_PATH}
+# Variables used by the rules script are parenthese'd $(DATADIR)
+# See [repo-root]/debian_template.sh
+#
+#############################################################################
+
 # Uncomment this to turn on verbose mode.
 #export DH_VERBOSE=1
 
@@ -37,30 +47,32 @@ RM	:= rm -rf
 DIR	:= $(shell pwd)
 
 PACKAGE := $(shell cat $(DIR)/debian/control | grep 'Package:' | sed -e 's/Package: //g')
-DATAFILES := srb2.srb zones.dta player.dta rings.dta music.dta patch.dta README.txt LICENSE.txt LICENSE-3RD-PARTY.txt
+ARCHIVEPATH := ${ASSET_ARCHIVE_PATH}
+ARCHIVEOPTIONALPATH := ${ASSET_ARCHIVE_OPTIONAL_PATH}
+GETOPTIONALFILES := ${ASSET_FILES_OPTIONAL_GET}
 
-DATADIR	:= usr/games/SRB2
+DATADIR	:= $(shell echo "${PACKAGE_INSTALL_PATH}" | sed -e 's/^\///')
 RESOURCEDIR := .
+STAGINGDIR := $(RESOURCEDIR)/installer
 WGET	:= wget -P $(RESOURCEDIR) -c -nc
 
 build:
 	$(MKDIR) $(DIR)/debian/tmp/$(DATADIR)
 	> $(DIR)/debian/source/include-binaries
-	# This will need to be updated every time SRB2 official version is
 	# Copy data files to their install locations, and add data files to include-binaries
-	for file in $(DATAFILES); do \
-		if [ ! -f $(RESOURCEDIR)/$$file ]; then \
-			$(WGET) http://alam.srb2.org/SRB2/2.1.21-Final/Resources/$$file; \
-		fi; \
-		if [ -f $(RESOURCEDIR)/$$file ]; then \
-			$(INSTALL) $(RESOURCEDIR)/$$file $(DIR)/debian/tmp/$(DATADIR)/$$file; \
-			echo $(RESOURCEDIR)/$$file >> $(DIR)/debian/source/include-binaries; \
+	if [ ! -d $(STAGINGDIR) ]; then \
+		mkdir -p "$(STAGINGDIR)"; \
+		$(WGET) $(ARCHIVEPATH); \
+		7z x "$(RESOURCEDIR)/$(shell basename $(ARCHIVEPATH))" -aos; \
+		if [ "$(GETOPTIONALFILES)" = "1" ]; then \
+			$(WGET) $(ARCHIVEOPTIONALPATH); \
+			7z x "$(RESOURCEDIR)/$(shell basename $(ARCHIVEOPTIONALPATH))" -aos; \
 		fi; \
-		if [ ! -f $(DIR)/debian/tmp/$(DATADIR)/$$file ]; then \
-			echo $(DIR)/debian/tmp/$(DATADIR)/$$file not found and could not be downloaded!; \
-			return 1; \
-		fi; \
-	done
+	fi
+	# Install asset directory and add asset file to include-binaries
+	cp -vr "$(STAGINGDIR)/." "$(DIR)/debian/tmp/$(DATADIR)"
+	find "$(STAGINGDIR)" >> $(DIR)/debian/source/include-binaries
+
 
 binary-indep:
 	# Generate install folder file
diff --git a/assets/debian/source/format b/assets/debian-template/source/format
similarity index 100%
rename from assets/debian/source/format
rename to assets/debian-template/source/format
diff --git a/assets/debian/source/options b/assets/debian-template/source/options
similarity index 100%
rename from assets/debian/source/options
rename to assets/debian-template/source/options
diff --git a/assets/debian/changelog b/assets/debian/changelog
deleted file mode 100644
index f3a92e1cdff72845820bcc8d2ef2cc2a119cb0d5..0000000000000000000000000000000000000000
--- a/assets/debian/changelog
+++ /dev/null
@@ -1,19 +0,0 @@
-srb2-data (2.1.21~7) trusty; urgency=high
-
-  * Updated for SRB2 v2.1.21
-
- -- Marco Zafra <marco.a.zafra@gmail.com>  Mon, 26 Nov 2018 14:31:00 -0500
-
-
-srb2-data (2.1.14~1) unstable; urgency=low
-
-  * Updated for SRB2 v2.1.14
-
- -- Alam Arias <alam+debian@srb2.org>  Sat, 6 Jan 2016 11:00:00 -0500
-
-
-srb2-data (2.0.6-2) maverick; urgency=high
-
-  * Initial proper release..
-
- -- Callum Dickinson <gcfreak_ag20@hotmail.com>  Sat,  29 Jan 2011 01:18:42 +1300
diff --git a/debian/README.Debian b/debian-template/README.Debian
similarity index 62%
rename from debian/README.Debian
rename to debian-template/README.Debian
index 4b724816e2beee374009a34282c0061b85b2db0d..3aa52787ea82d786d8e3fcfbc4166fbf62ce982f 100644
--- a/debian/README.Debian
+++ b/debian-template/README.Debian
@@ -10,10 +10,38 @@ and give them to your users to install with apt-key add. Thanks!
 
  -- Callum Dickinson <gcfreak_ag20@hotmail.com>  Fri, 26 Nov 2010 18:25:31 +1300
 
+---------------
+
+
+Templating
+
+Note that you MUST run [repo-root]/debian_template.sh before running debuild
+on these scripts! debian_template.sh fills these template files with working values.
+
+You should also set PACKAGE_NAME_EMAIL="John Doe <jdoe@example.com>" to match
+the identity of the key you will use to sign the package.
+
+
+Building for Launchpad PPA
+
+Use these steps to prepare building a source package for Launchpad:
+
+    1. cd [repo-root]
+    2. git reset --hard; git clean -fd; git clean -fx;
+       * Resets your repo folder to a committed state and removes untracked files
+       * If you built srb2-data in the assets/ folder, MAKE SURE THAT FOLDER DOES NOT HAVE ASSETS,
+         OR THEY MAY BE INCLUDED IN THE MAIN SOURCE PACKAGE!
+
+Build the source package:
+
+    1. source [repo-root]/debian_template.sh
+       * Initializes defaults for the package variables and fills in templates.
+    2. debuild -S (builds the source package for Launchpad)
+
 
 Signing for Launchpad PPA
 
-First, follow the above instructions to generate a GnuPG key with your identity. You will need
+First, follow Callum's instructions to generate a GnuPG key with your identity. You will need
 to publish the fingerprint of that key to Ubuntu's key server.
 
     https://help.ubuntu.com/community/GnuPrivacyGuardHowto#Uploading_the_key_to_Ubuntu_keyserver
@@ -25,22 +53,18 @@ upload signed source packages and publish them onto your PPA.
 IF YOU UPLOAD A PACKAGE and Launchpad does NOT send you a confirmation or rejection email, that
 means your key is not set up correctly with your Launchpad account.
 
+Finally, if your packages have not already been signed, follow these steps:
 
-Building for Launchpad PPA
+    1. cd ..
+       * Packages are located in the parent folder of where debuild was called
+    2. debsign "srb2_[version]_source.changes"
+       * You may need to specify -k [key-fingerprint]
 
-Use these steps to prepare building a source package for Launchpad:
 
-    1. cd [srb2repo]
-    2. git reset --hard; git clean -fd; git clean -fx;
-       * Resets your repo folder to a committed state and removes untracked files
-       * If you built srb2-data in the assets/ folder, MAKE SURE THAT FOLDER DOES NOT HAVE ASSETS,
-         OR THEY MAY BE INCLUDED IN THE MAIN SOURCE PACKAGE!
+Uploading for Launchpad PPA
 
-Building the source package takes just one step:
-
-    1. debuild -S (builds the source package for Launchpad)
-
-Then follow the instructions at <https://help.launchpad.net/Packaging/PPA/Uploading> to upload
+Follow the instructions at <https://help.launchpad.net/Packaging/PPA/Uploading> to upload
 to your PPA and have Launchpad build your binary deb packages.
 
+
  -- Marco Zafra <marco.a.zafra@gmail.com>  Mon, 26 Nov 2018 21:13:00 -0500
diff --git a/debian/README.source b/debian-template/README.source
similarity index 100%
rename from debian/README.source
rename to debian-template/README.source
diff --git a/debian-template/changelog b/debian-template/changelog
new file mode 100644
index 0000000000000000000000000000000000000000..fb08908cdfa720a114f2d8d01cfe6ca2993bdf87
--- /dev/null
+++ b/debian-template/changelog
@@ -0,0 +1,5 @@
+${PACKAGE_NAME} (${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION}) ${PACKAGE_DISTRO}; urgency=${PACKAGE_URGENCY}
+
+  * ${PROGRAM_NAME} v${PROGRAM_VERSION} program build
+
+ -- ${PACKAGE_NAME_EMAIL}  ${__PACKAGE_DATETIME}
diff --git a/debian/compat b/debian-template/compat
similarity index 100%
rename from debian/compat
rename to debian-template/compat
diff --git a/debian/control b/debian-template/control
similarity index 65%
rename from debian/control
rename to debian-template/control
index 0f2d8062bac4d1acc93d50c3f078fb16c43f987a..e1348d704e3187dfc0e70a7165d467a2c6354492 100644
--- a/debian/control
+++ b/debian-template/control
@@ -1,24 +1,30 @@
 # SRB2 Debian package control file.
 
-Source: srb2
+Source: ${PACKAGE_NAME}
 Section: games
 Priority: extra
-Maintainer: Sonic Team Junior <stjr@srb2.org>
+Maintainer: ${PACKAGE_GROUP_NAME_EMAIL}
 Build-Depends: debhelper (>= 7.0.50~),
  libsdl2-dev,
  libsdl2-mixer-dev,
- libpng12-dev (>= 1.2.7) | libpng-dev,
+ libpng-dev | libpng16-dev | libpng12-dev (>= 1.2.7),
  zlib1g-dev,
  libgme-dev,
  libglu1-dev | libglu-dev,
  libosmesa6-dev | libgl-dev,
  nasm [i386]
 Standards-Version: 3.8.4
-Homepage: http://www.srb2.org
+Homepage: ${PACKAGE_WEBSITE}
 
-Package: srb2
+Package: ${PACKAGE_NAME}
 Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, srb2-data (>= 2.1.15), srb2-data (<= 2.1.23)
+Depends: ${SHLIBS_DEPENDS}, ${MISC_DEPENDS},
+ ${PACKAGE_NAME}-data (>> ${PACKAGE_ASSET_MINVERSION}), ${PACKAGE_NAME}-data (<< ${PACKAGE_ASSET_MAXVERSION}),
+ libsdl2-2.0-0,
+ libsdl2-mixer-2.0-0,
+ zlib1g,
+ libgme0,
+ libpng | libpng16-16 | libpng12-0
 Description: A cross-platform 3D Sonic fangame
  Sonic Robo Blast 2 is a 3D open-source Sonic the Hedgehog
  fangame built using a modified version of the Doom Legacy
@@ -28,10 +34,10 @@ Description: A cross-platform 3D Sonic fangame
  and quite a lot of the fun that the original Sonic games provided.
 
 
-Package: srb2-dbg
+Package: ${PACKAGE_NAME}-dbg
 Architecture: any
-# FIXME: should be Depends: ${shlibs:Depends}, ${misc:Depends}, srb2-data (= 2.1.14), srb2 but dh_shlibdeps is being an asshat
-Depends: libc6, ${misc:Depends}, srb2-data (>= 2.1.15), srb2-data (<= 2.1.23), srb2
+# FIXME: should be Depends: ${SHLIBS_DEPENDS}, ${MISC_DEPENDS}, srb2-data (= 2.1.14), srb2 but dh_shlibdeps is being an asshat
+Depends: libc6, ${MISC_DEPENDS}, ${PACKAGE_NAME}-data (>> ${PACKAGE_ASSET_MINVERSION}), ${PACKAGE_NAME}-data (<< ${PACKAGE_ASSET_MAXVERSION}), ${PACKAGE_NAME}
 Description: A cross-platform 3D Sonic fangame
  Sonic Robo Blast 2 is a 3D open-source Sonic the Hedgehog
  fangame built using a modified version of the Doom Legacy
diff --git a/assets/debian/copyright b/debian-template/copyright
similarity index 57%
rename from assets/debian/copyright
rename to debian-template/copyright
index 97d606b0fb67b73eaae1858f43652006edfd91b6..cc47c453bf2b0ccc6db34c57a03dfe1ce2fc75a7 100644
--- a/assets/debian/copyright
+++ b/debian-template/copyright
@@ -1,18 +1,18 @@
 This work was packaged for Debian by:
 
-    Marco Zafra <marco.a.zafra@gmail.com>  Mon, 26 Nov 2018 14:31:00 -0500
+    ${PACKAGE_NAME_EMAIL}  ${__PACKAGE_DATETIME}
 
 It was downloaded from:
 
-    <http://srb2.org>
+    ${PACKAGE_WEBSITE}
 
 Upstream Author(s):
 
-    Sonic Team Junior <stjr@srb2.org>
+    ${PACKAGE_GROUP_NAME_EMAIL}
 
 Copyright:
 
-    Copyright (C) 1998-2018 Sonic Team Junior
+    Copyright (C) 1998-2018 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 Sonic Team Junior <stjr@srb2.org>
+    Copyright (C) 2010-2018 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/debian/docs b/debian-template/docs
similarity index 100%
rename from debian/docs
rename to debian-template/docs
diff --git a/debian/rules b/debian-template/rules
old mode 100755
new mode 100644
similarity index 87%
rename from debian/rules
rename to debian-template/rules
index ff80d50bf2f2f881937f42d607d370b31b85e668..0a77624cb490639564b0212abc902dbfdda88be5
--- a/debian/rules
+++ b/debian-template/rules
@@ -23,6 +23,16 @@
 #
 #############################################################################
 
+#############################################################################
+#
+# !!!!!!!!!! DEPLOYER NOTE !!!!!!!!!!
+#
+# Variables to be templated are curly-braced ${PACKAGE_INSTALL_PATH}
+# Variables used by the rules script are parenthese'd $(PKGDIR)
+# See [repo-root]/debian_template.sh
+#
+#############################################################################
+
 # Uncomment this to turn on verbose mode.
 #export DH_VERBOSE=1
 
@@ -50,16 +60,16 @@ DIR	:= $(shell pwd)
 
 # FIXME: hate hate hate head/tail hack :(
 CONTROLF = $(DIR)/debian/control
-PACKAGE  = srb2
-DBGPKG   = $(PACKAGE)-dbg
-TITLE	= Sonic Robo Blast 2
+PACKAGE  = ${PACKAGE_NAME}
+DBGPKG   = ${PACKAGE}-dbg
+TITLE	= ${PROGRAM_NAME}
 SECTION = Games/Action
-EXENAME = srb2
+EXENAME = ${PROGRAM_FILENAME}
 DBGNAME	= debug/$(EXENAME)
 
-PKGDIR	= usr/games/SRB2
+PKGDIR	= $(shell echo "${PACKAGE_INSTALL_PATH}" | sed -e 's/^\///')
 DBGDIR	= usr/lib/debug/$(PKGDIR)
-LINKDIR = usr/games
+LINKDIR = $(shell echo "${PACKAGE_LINK_PATH}" | sed -e 's/^\///')
 PIXMAPS_DIR = usr/share/pixmaps
 DESKTOP_DIR = usr/share/applications
 PREFIX	= $(shell test "$(CROSS_COMPILE_BUILD)" != "$(CROSS_COMPILE_HOST)" && echo "PREFIX=$(CROSS_COMPILE_HOST)")
@@ -102,8 +112,8 @@ binary-arch:
 	$(INSTALL) $(BINDIR)/$(EXENAME) $(DIR)/debian/tmp/$(PKGDIR)/$(PACKAGE)
 	$(INSTALL) $(BINDIR)/$(DBGNAME) $(DIR)/debian/tmp/$(DBGDIR)/$(PACKAGE)
 	# Install desktop file and banner image
-	$(INSTALL) $(DIR)/srb2.png $(DIR)/debian/tmp/usr/share/pixmaps
-	$(INSTALL) $(DIR)/debian/srb2.desktop $(DIR)/debian/tmp/usr/share/applications
+	$(INSTALL) $(DIR)/srb2.png $(DIR)/debian/tmp/usr/share/pixmaps/${PROGRAM_FILENAME}.png
+	$(INSTALL) $(DIR)/debian/srb2.desktop $(DIR)/debian/tmp/usr/share/applications/${PROGRAM_FILENAME}.desktop
 	# add compiled binaries to include-binaries
 	echo $(BINDIR)/$(EXENAME) >> $(DIR)/debian/source/include-binaries
 	echo $(BINDIR)/$(EXENAME) >> $(DIR)/debian/source/include-binaries
diff --git a/debian/source/format b/debian-template/source/format
similarity index 100%
rename from debian/source/format
rename to debian-template/source/format
diff --git a/debian/source/options b/debian-template/source/options
similarity index 81%
rename from debian/source/options
rename to debian-template/source/options
index 841c65a6f05e48766e4f9c6519222c25f9ecf7be..1ef771ddf4a618e4594d324addc38a0f3e401412 100644
--- a/debian/source/options
+++ b/debian-template/source/options
@@ -2,7 +2,7 @@ tar-ignore = "assets/*.srb"
 tar-ignore = "assets/*.pk3"
 tar-ignore = "assets/*.dta"
 tar-ignore = "assets/*.wad"
-tar-ignore = "assets/debian/srb2-data/*"
+tar-ignore = "assets/debian/${PACKAGE_NAME}-data/*"
 tar-ignore = "assets/debian/tmp/*"
 tar-ignore = "*.obj"
 tar-ignore = "*.dep"
diff --git a/debian-template/srb2.desktop b/debian-template/srb2.desktop
new file mode 100644
index 0000000000000000000000000000000000000000..07c7906e05c5b0b1690c5be817140ad5b628e341
--- /dev/null
+++ b/debian-template/srb2.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Name=${PROGRAM_NAME}
+Comment=${PROGRAM_DESCRIPTION}
+Encoding=UTF-8
+Exec=${PACKAGE_INSTALL_PATH}/${PROGRAM_FILENAME}
+Icon=/usr/share/pixmaps/${PROGRAM_FILENAME}.png
+Terminal=false
+Type=Application
+StartupNotify=false
+Categories=Application;Game;
diff --git a/debian/changelog b/debian/changelog
deleted file mode 100644
index b06a78e2ba6fa055a57a2c397995b69e47b2a6d9..0000000000000000000000000000000000000000
--- a/debian/changelog
+++ /dev/null
@@ -1,12 +0,0 @@
-srb2 (2.1.23~9) trusty; urgency=high
-
-  * SRB2 v2.1.23 release
-
- -- Marco Zafra <marco.a.zafra@gmail.com>  Mon, 27 Nov 2018 16:45:00 -0500
-
-
-srb2 (2.0.6-5) maverick; urgency=high
-
-  * Initial proper release..
-
- -- Callum Dickinson <gcfreak_ag20@hotmail.com>  Sat, 29 Jan 2011 01:18:42 +1300
diff --git a/debian/srb2.desktop b/debian/srb2.desktop
deleted file mode 100644
index 3a1cac9f68e5dfb84a97ee4f3151af31ea64036a..0000000000000000000000000000000000000000
--- a/debian/srb2.desktop
+++ /dev/null
@@ -1,10 +0,0 @@
-[Desktop Entry]
-Name=Sonic Robo Blast 2
-Comment=A free 3D Sonic the Hedgehog fangame closely inspired by the original Sonic games on the Sega Genesis.
-Encoding=UTF-8
-Exec=/usr/games/SRB2/srb2
-Icon=/usr/share/pixmaps/srb2.png
-Terminal=false
-Type=Application
-StartupNotify=false
-Categories=Application;Game;
diff --git a/debian_template.sh b/debian_template.sh
new file mode 100644
index 0000000000000000000000000000000000000000..c1af3c19f0a3a66e46389b6a0ec67d9b43c11cfb
--- /dev/null
+++ b/debian_template.sh
@@ -0,0 +1,166 @@
+#!/bin/bash
+
+# Deployer for Travis-CI
+# Debian package templating
+#
+# Call this script BEFORE running debuild!
+# source ./debian_template.sh [clean] [main/asset]
+#
+# Before running this script,
+# you should also set PACKAGE_NAME_EMAIL="John Doe <jdoe@example.com>" to match
+# the identity of the key you will use to sign the package.
+#
+
+# Get script's actual path
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+# Recursive function for directory crawling
+# $1 = Directory root to crawl
+# $2 = Code to eval on file
+# $3 = Code to eval on directory
+# Exposes $dirtails, $dirlevel, and $dirtailname
+dirlevel=0 # initialize
+dirtails=()
+
+# Utility function to make dira/dirb/dirc string
+makedirtailname () {
+	dirtailname=""
+	for tail in $dirtails; do
+		if [[ "$dirtailname" == "" ]]; then
+			dirtailname="/$tail";
+		else
+			dirtailname="$dirtailname/$tail";
+		fi;
+	done;
+}
+
+evaldirectory () {
+	if [ -d "$1" ]; then
+		# Set contextual variables
+		# dirtails is an array of directory basenames after the crawl root
+		if (( $dirlevel > 0 )); then
+			dirtails+=( "$(basename $1)" );
+		else
+			dirtails=();
+		fi;
+		dirlevel=$((dirlevel+1));
+
+		# Generate directory path after the crawl root
+		makedirtailname;
+
+		# Eval our directory with the latest contextual info
+		# Don't eval on root
+		if (( $dirlevel > 1 )) && [[ "$3" != "" ]]; then
+			eval "$3";
+		fi;
+
+		# Iterate entries
+		for name in $1/*; do
+			if [ -d "$name" ]; then
+				# Name is a directory, but don't eval yet
+				# Recurse so our vars are updated
+				evaldirectory "$name" "$2" "$3";
+
+				# Decrement our directory level and remove a dirtail
+				unset 'dirtails[ ${#dirtails[@]}-1 ]';
+				dirlevel=$((dirlevel-1));
+				makedirtailname;
+			else
+				# Name is a file
+				if [ -f "$name" ] && [[ "$2" != "" ]]; then
+					eval "$2";
+				fi;
+			fi;
+		done;
+
+		# Reset our variables; we're done iterating
+		if (( $dirlevel == 1 )); then
+			dirlevel=0;
+		fi;
+	fi;
+}
+
+#
+# Initialize package parameter defaults
+#
+if [[ "$__DEBIAN_PARAMETERS_INITIALIZED" != "1" ]]; then
+	. ${DIR}/deployer/travis/deployer_defaults.sh;
+fi;
+
+# Clean up after ourselves; we only expect to run this script once
+# during buildboting
+__DEBIAN_PARAMETERS_INITIALIZED=0
+
+# for envsubst
+export __PACKAGE_DATETIME="$(date '+%a, %d %b %Y %H:%M:%S %z')"
+export __PACKAGE_DATETIME_DIGIT="$(date -u '+%Y%m%d%H%M%S')"
+
+if [[ "$PACKAGE_REVISION" == "" ]]; then
+	PACKAGE_REVISION="-$__PACKAGE_DATETIME_DIGIT";
+	__PACKAGE_REVISION_BY_DATE=1;
+	export PACKAGE_REVISION=${PACKAGE_REVISION}; # for envsubst
+fi;
+
+#
+# Clean the old debian/ directories
+#
+if [[ "$1" == "clean" ]]; then
+	toclean=$2;
+else
+	toclean=$1;
+fi;
+
+if [[ "$toclean" == "" ]] || [[ "$toclean" == "main" ]]; then
+	echo "Cleaning main package scripts";
+	if [[ ! -f ${DIR}/debian ]]; then
+		rm -rf ${DIR}/debian;
+	fi;
+fi;
+if [[ "$toclean" == "" ]] || [[ "$toclean" == "asset" ]]; then
+	echo "Cleaning asset package scripts";
+	if [[ ! -f ${DIR}/assets/debian ]]; then
+		rm -rf ${DIR}/assets/debian;
+	fi;
+fi;
+
+#
+# Make new templates
+#
+if [[ "$1" != "clean" ]]; then
+	totemplate=$1;
+
+	# HACK: ${shlibs:Depends} in the templates make the templating fail
+	# So just define replacemment variables
+	export SHLIBS_DEPENDS=${SHLIBS_DEPENDS};
+	export MISC_DEPENDS=${MISC_DEPENDS};
+	export DEBFILEVAR='$$file'; # used in assets/debian/rules
+
+	# Package parameters are exported for envsubst in deployer_defaults.sh
+
+	if [[ "$totemplate" == "" ]] || [[ "$totemplate" == "main" ]]; then
+		echo "Generating main package scripts";
+		fromroot=${DIR}/debian-template;
+		toroot=${DIR}/debian;
+		mkdir ${toroot};
+
+		evaldirectory ${fromroot} \
+			"cat \$name | envsubst > ${toroot}\${dirtailname}/\$( basename \$name )" \
+			"mkdir \"${toroot}\${dirtailname}\"";
+	fi;
+
+	if [[ "$totemplate" == "" ]] || [[ "$totemplate" == "asset" ]]; then
+		echo "Generating asset package scripts";
+		fromroot=${DIR}/assets/debian-template;
+		toroot=${DIR}/assets/debian;
+		mkdir ${toroot};
+
+		# Root dir to crawl; file eval; directory eval
+		evaldirectory ${fromroot} \
+			"cat \$name | envsubst > ${toroot}\${dirtailname}/\$( basename \$name )" \
+			"mkdir \"${toroot}\${dirtailname}\"";
+	fi;
+fi;
+
+if [[ "$__DPL_ACTIVE" != "1" ]] && [[ "$__PACKAGE_REVISION_BY_DATE" == "1" ]]; then
+	unset PACKAGE_REVISION; # so we can reset the date on subsequent runs
+fi;
diff --git a/deployer/travis/deployer.sh b/deployer/travis/deployer.sh
new file mode 100644
index 0000000000000000000000000000000000000000..c88155d217956def89f50fe5673b56a9b5fb35c6
--- /dev/null
+++ b/deployer/travis/deployer.sh
@@ -0,0 +1,157 @@
+#!/bin/bash
+
+# Deployer for Travis-CI
+# Initialization
+#
+# Performs validity checks to ensure that Deployer is allowed to run
+# e.g., is an FTP hostname specified? Are we whitelisted by OSNAMES and BRANCHES?
+#
+# Set these environment variables in your Travis-CI settings, where they are stored securely.
+# See other shell scripts for more options.
+#
+# DPL_ENABLED = 1                       (leave blank to disable)
+# DPL_TAG_ENABLED = 1                   (run Deployer on all tags)
+# DPL_JOB_ENABLE_ALL = 1                (run Deployer on all jobs; leave blank to act on specific jobs, see below)
+# DPL_JOBNAMES = name1,name2            (whitelist of job names to allow uploading; leave blank to upload from all jobs)
+# DPL_OSNAMES = osx                     (whitelist of OS names to allow uploading; leave blank to upload from all OSes)
+# DPL_BRANCHES = master,branch1,branch2 (whitelist of branches to upload; leave blank to upload all branches)
+#
+# To enable Deployer on specific jobs, set _DPL_JOB_ENABLED=1 for that job. Example:
+# - matrix:
+#   - os: osx
+#     env:
+#     - _DPL_JOB_ENABLED=1
+#
+# DO NOT set __DPL_ACTIVE, because that would bypass these validity checks.
+
+# Validate Deployer state
+if [[ "$DPL_ENABLED" == "1" ]] && [[ "$TRAVIS_PULL_REQUEST" == "false" ]]; then
+    # Test for base eligibility:
+    # Are we in a deployer branch? Or
+    # Are we in a release tag AND DPL_TAG_ENABLED=1?
+    if [[ $TRAVIS_BRANCH == *"deployer"* ]]; then
+        __DPL_BASE_ELIGIBLE=1;
+        __DPL_TERMINATE_EARLY_ELIGIBLE=1;
+    fi;
+
+    if [[ "$TRAVIS_TAG" != "" ]] && [[ "$DPL_TAG_ENABLED" == "1" ]]; then
+        __DPL_BASE_ELIGIBLE=1;
+        __DPL_TAG_ELIGIBLE=1;
+        __DPL_TERMINATE_EARLY_ELIGIBLE=1;
+    fi;
+
+    # Logging message for trigger word
+    if [[ "$__DPL_TAG_ELIGIBLE" != "1" ]] && [[ "$DPL_TRIGGER" != "" ]]; then
+        echo "Testing for trigger $DPL_TRIGGER, commit message: $TRAVIS_COMMIT_MESSAGE";
+        echo "[${DPL_TRIGGER}]";
+        echo "[${DPL_TRIGGER}-${_DPL_JOB_NAME}]";
+        echo "[${DPL_TRIGGER}-${TRAVIS_OS_NAME}]";
+    fi;
+
+    #
+    # Search for the trigger word
+    # Force enable if release tags are eligible
+    #
+    if [[ "$__DPL_TAG_ELIGIBLE" == "1" ]] || [[ "$DPL_TRIGGER" == "" ]] \
+    || [[ $TRAVIS_COMMIT_MESSAGE == *"[$DPL_TRIGGER]"* ]] \
+    || [[ $TRAVIS_COMMIT_MESSAGE == *"[${DPL_TRIGGER}-${_DPL_JOB_NAME}]"* ]] \
+    || [[ $TRAVIS_COMMIT_MESSAGE == *"[${DPL_TRIGGER}-${TRAVIS_OS_NAME}]"* ]]; then
+        #
+        # Whitelist by branch name
+        # Force enable if release tags are eligible
+        #
+        if [[ "$__DPL_TAG_ELIGIBLE" == "1" ]] || [[ "$DPL_BRANCHES" == "" ]] || [[ $DPL_BRANCHES == *"$TRAVIS_BRANCH"* ]]; then
+            # Set this so we only early-terminate builds when we are specifically deploying
+            # Trigger string and branch are encompassing conditions; the rest are job-specific
+            # This check only matters for deployer branches and when DPL_TERMINATE_TESTS=1,
+            # because we're filtering non-deployer jobs.
+            #
+            # __DPL_TRY_TERMINATE_EARLY is invalidated in .travis.yml if __DPL_ACTIVE=1
+            if [[ "$__DPL_TERMINATE_EARLY_ELIGIBLE" == "1" ]] && [[ "$DPL_TERMINATE_TESTS" == "1" ]]; then
+                __DPL_TRY_TERMINATE_EARLY=1;
+            fi;
+
+            #
+            # Is the job enabled for deployment?
+            #
+            if [[ "$DPL_JOB_ENABLE_ALL" == "1" ]] || [[ "$_DPL_JOB_ENABLED" == "1" ]]; then
+                #
+                # Whitelist by job names
+                #
+                if [[ "$DPL_JOBNAMES" == "" ]] || [[ "$_DPL_JOB_NAME" == "" ]] || [[ $DPL_JOBNAMES == *"$_DPL_JOB_NAME"* ]]; then
+                    #
+                    # Whitelist by OS names
+                    #
+                    if [[ "$DPL_OSNAMES" == "" ]] || [[ $DPL_OSNAMES == *"$TRAVIS_OS_NAME"* ]]; then
+                        # Base Deployer is eligible for becoming active
+
+                        # Are we building for Linux?
+                        if [[ "$_DPL_PACKAGE_BINARY" == "1" ]] || [[ "$_DPL_PACKAGE_SOURCE" == "1" ]]; then
+                            if [[ "$_DPL_PACKAGE_MAIN" == "1" ]] || [[ "$_DPL_PACKAGE_ASSET" == "1" ]]; then
+                                if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
+                                    __DPL_DEBIAN_ACTIVE=1;
+                                fi;
+                            fi;
+                        fi;
+
+                        # Now check for deployment targets
+                        if [[ "$_DPL_FTP_TARGET" == "1" ]] && [[ "$DPL_FTP_HOSTNAME" != "" ]]; then
+                            if [[ "$TRAVIS_OS_HOST" == "linux" ]] && [[ "$DPL_FTP_PROTOCOL" == "ftp" ]]; then
+                                echo "Non-secure FTP will not work on Linux Travis-CI jobs!";
+                                echo "Try SFTP or another target. Details:";
+                                echo "https://blog.travis-ci.com/2018-07-23-the-tale-of-ftp-at-travis-ci";
+                            else
+                                if [[ "$__DPL_DEBIAN_ACTIVE" == "1" ]] || [[ "$_DPL_PACKAGE_BINARY" == "1" ]] || [[ "$_DPL_BINARY" == "1" ]]; then
+                                    echo "Deployer FTP target is enabled";
+                                    __DPL_FTP_ACTIVE=1;
+                                else
+                                    echo "Deployer FTP target cannot be enabled: You must specify _DPL_PACKAGE_BINARY=1,";
+                                    echo "and/or _DPL_BINARY=1 in your job's environment variables.";
+                                fi;
+                            fi;
+                        fi;
+
+                        if [[ "$_DPL_DPUT_TARGET" == "1" ]] && [[ "$__DPL_DEBIAN_ACTIVE" == "1" ]] \
+                        && [[ "$DPL_DPUT_INCOMING" != "" ]]; then
+                            if [[ "$DPL_DPUT_METHOD" == "ftp" ]]; then
+                                echo "DPUT will not work with non-secure FTP on Linux Travis-CI jobs!";
+                                echo "Try SFTP or another method for DPUT. Details:";
+                                echo "https://blog.travis-ci.com/2018-07-23-the-tale-of-ftp-at-travis-ci";
+                            else
+                                echo "Deployer DPUT target is enabled";
+                                __DPL_DPUT_ACTIVE=1;
+                            fi;
+                        fi;
+
+                        # If any deployment targets are active, then so is the Deployer at large
+                        if [[ "$__DPL_FTP_ACTIVE" == "1" ]] || [[ "$__DPL_DPUT_ACTIVE" == "1" ]]; then
+                            __DPL_ACTIVE=1;
+                        fi;
+                    fi;
+                fi;
+            fi;
+        fi;
+    else
+        if [[ "$DPL_TRIGGER" != "" ]]; then
+            echo "Testing for global trigger [$DPL_TRIGGER, commit message: $TRAVIS_COMMIT_MESSAGE";
+        fi;
+        if [[ "$DPL_TRIGGER" != "" ]] && [[ $TRAVIS_COMMIT_MESSAGE == *"[$DPL_TRIGGER"* ]]; then
+            if [[ "$__DPL_TAG_ELIGIBLE" == "1" ]] || [[ "$DPL_BRANCHES" == "" ]] || [[ $DPL_BRANCHES == *"$TRAVIS_BRANCH"* ]]; then
+                # This check only matters for deployer branches and when DPL_TERMINATE_TESTS=1,
+                # because we're filtering non-deployer jobs.
+                if [[ "$__DPL_TERMINATE_EARLY_ELIGIBLE" == "1" ]] && [[ "$DPL_TERMINATE_TESTS" == "1" ]]; then
+                    # Assume that some job received the trigger, so mark this for early termination
+                    __DPL_TRY_TERMINATE_EARLY=1;
+                fi;
+            fi;
+        fi;
+    fi;
+fi;
+
+if [[ "$__DPL_TRY_TERMINATE_EARLY" == "1" ]] && [[ "$__DPL_ACTIVE" != "1" ]]; then
+    echo "Deployer is active in another job";
+fi;
+
+if [[ "$__DPL_TRY_TERMINATE_EARLY" != "1" ]] && [[ "$__DPL_ACTIVE" != "1" ]]; then
+    echo "Deployer is not active";
+fi;
diff --git a/deployer/travis/deployer_build.sh b/deployer/travis/deployer_build.sh
new file mode 100644
index 0000000000000000000000000000000000000000..3817f025de4151d573222d7f980f9e1e0d758702
--- /dev/null
+++ b/deployer/travis/deployer_build.sh
@@ -0,0 +1,190 @@
+#!/bin/bash
+
+# Deployer for Travis-CI
+# Build Script
+#
+# Builds the required targets depending on which sub-modules are enabled
+
+if [[ "$__DPL_FTP_ACTIVE" == "1" ]] || [[ "$__DPL_DPUT_ACTIVE" == "1" ]]; then
+	if [[ "$__DPL_DEBIAN_ACTIVE" == "1" ]]; then
+		echo "Building Debian package(s)"
+
+		sudo apt-get install devscripts debhelper fakeroot secure-delete expect;
+
+		# Build source packages first, since they zip up the entire source folder,
+		# binaries and all
+		if [[ "$_DPL_PACKAGE_MAIN" == "1" ]]; then
+			. ../debian_template.sh main;
+			OLDPWD=$PWD; # [repo]/build
+			cd ..; # repo root
+
+			if [[ "$_DPL_PACKAGE_SOURCE" == "1" ]]; then
+				echo "Building main source Debian package";
+				expect <(cat <<EOD
+spawn debuild -S -us -uc;
+expect "continue anyway? (y/n)"
+send "y\r"
+interact
+EOD
+);
+			fi;
+
+			if [[ "$_DPL_PACKAGE_BINARY" == "1" ]]; then
+				echo "Building main binary Debian package";
+				expect <(cat <<EOD
+spawn debuild -us -uc;
+expect "continue anyway? (y/n)"
+send "y\r"
+interact
+EOD
+);
+			fi;
+
+			cd $OLDPWD;
+		fi;
+
+		# Also an asset package
+		if [[ "$_DPL_PACKAGE_ASSET" == "1" ]]; then
+			. ../debian_template.sh asset;
+			OLDPWD=$PWD; # [repo]/build
+			cd ../assets;
+
+			# make sure the asset files exist, download them if they don't
+			#echo "Checking asset files for asset Debian package";
+			#debuild -T build;
+
+			if [[ "$_DPL_PACKAGE_SOURCE" == "1" ]]; then
+				echo "Building asset source Debian package";
+				expect <(cat <<EOD
+spawn debuild -S -us -uc;
+expect "continue anyway? (y/n)"
+send "y\r"
+interact
+EOD
+);
+			fi;
+
+			if [[ "$_DPL_PACKAGE_BINARY" == "1" ]]; then
+				echo "Building asset binary Debian package";
+				expect <(cat <<EOD
+spawn debuild -us -uc;
+expect "continue anyway? (y/n)"
+send "y\r"
+interact
+EOD
+);
+			fi;
+
+			cd $OLDPWD;
+		fi;
+
+		# Now sign our packages
+		if [[ "$DPL_PGP_KEY_PRIVATE" != "" ]] && [[ "$DPL_PGP_KEY_PASSPHRASE" != "" ]]; then
+			# Get the key to sign
+			# Do this AFTER debuild so that we can specify the passphrase in command line
+			echo "$DPL_PGP_KEY_PRIVATE" | base64 --decode > key.asc;
+			echo "$DPL_PGP_KEY_PASSPHRASE" > phrase.txt;
+			gpg --import key.asc;
+
+			if [[ "$_DPL_PACKAGE_MAIN" == "1" ]]; then
+				echo "Signing main package(s)";
+
+				PACKAGEFILENAME=${PACKAGE_NAME}_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				PACKAGEDBGFILENAME=${PACKAGE_NAME}-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGENIGHTLYFILENAME=${PACKAGE_NAME}-nightly_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGENIGHTLYDBGFILENAME=${PACKAGE_NAME}-nightly-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGEPATCHFILENAME=${PACKAGE_NAME}-patch_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGEPATCHDBGFILENAME=${PACKAGE_NAME}-patch-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGEPATCHNIGHTLYFILENAME=${PACKAGE_NAME}-patch-nightly_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGEPATCHNIGHTLYDBGFILENAME=${PACKAGE_NAME}-patch-nightly-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+
+				PACKAGEFILENAMES=(
+					$PACKAGEFILENAME
+					$PACKAGEDBGFILENAME
+					#$PACKAGENIGHTLYFILENAME
+					#$PACKAGENIGHTLYDBGFILENAME
+					#$PACKAGEPATCHFILENAME
+					#$PACKAGEPATCHDBGFILENAME
+					#$PACKAGEPATCHNIGHTLYFILENAME
+					#$PACKAGEPATCHNIGHTLYDBGFILENAME
+				);
+
+				# Main packages are in parent of root repo folder
+				OLDPWD=$PWD; # [repo]/build
+				cd ../..; # parent of repo root
+
+				for n in ${PACKAGEFILENAMES}; do
+					for f in ./$n*.changes; do
+						debsign --no-re-sign -p"gpg --passphrase-file $OLDPWD/phrase.txt --batch" "$f";
+					done;
+				done;
+
+				cd $OLDPWD;
+			fi;
+
+			if [[ "$_DPL_PACKAGE_ASSET" == "1" ]]; then
+				echo "Signing asset package(s)";
+
+				PACKAGEFILENAME=${PACKAGE_NAME}-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGENIGHTLYFILENAME=${PACKAGE_NAME}-nightly-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGEPATCHFILENAME=${PACKAGE_NAME}-patch-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+				#PACKAGEPATCHNIGHTLYFILENAME=${PACKAGE_NAME}-patch-nightly-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+
+				PACKAGEFILENAMES=(
+					$PACKAGEFILENAME
+					#$PACKAGENIGHTLYFILENAME
+					#$PACKAGEPATCHFILENAME
+					#$PACKAGEPATCHNIGHTLYFILENAME
+				)
+
+				# Asset packages are in root repo folder
+				OLDPWD=$PWD; # [repo]/build
+				cd ..; # repo root
+
+				for n in ${PACKAGEFILENAMES}; do
+					for f in ./$n*.changes; do
+						debsign --no-re-sign -p"gpg --passphrase-file $OLDPWD/phrase.txt --batch" "$f";
+					done;
+				done;
+
+				cd $OLDPWD;
+			fi;
+
+			# Delete the keys :eyes:
+			srm key.asc;
+			srm phrase.txt;
+		fi;
+	fi;
+
+	# all other OSes
+	if [[ "$TRAVIS_OS_NAME" != "linux" ]]; then
+		#
+		# Check for binary building
+		#
+		if [[ "$_DPL_BINARY" == "1" ]]; then
+			echo "Building a Binary";
+			make -k;
+		fi;
+
+		#
+		# Check for package building
+		#
+		if [[ "$_DPL_PACKAGE_BINARY" == "1" ]]; then
+			echo "Building a Package";
+
+			# Make an OSX package; superuser is required for library bundling
+			#
+			# HACK: OSX packaging can't write libraries to .app package unless we're superuser
+			# because the original library files don't have WRITE permission
+			# Bug may be sidestepped by using CHMOD_BUNDLE_ITEMS=TRUE
+			# But I don't know where this is set. Not `cmake -D...` because this var is ignored.
+			# https://cmake.org/Bug/view.php?id=9284
+			if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
+				sudo make -k package;
+			else
+				# Some day, when Windows is supported, we'll just make a standard package
+				make -k package;
+			fi;
+		fi;
+	fi;
+fi;
diff --git a/deployer/travis/deployer_defaults.sh b/deployer/travis/deployer_defaults.sh
new file mode 100644
index 0000000000000000000000000000000000000000..bccb7409ac2750564d3a75433e12ff747ad21ae7
--- /dev/null
+++ b/deployer/travis/deployer_defaults.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+# Deployer for Travis-CI
+# Default Variables
+#
+# Here are all of the user-set variables used by Deployer.
+# See the "Cross-platform deployment" page on SRB2 Wiki for documentation.
+
+# Core Parameters
+: ${DPL_ENABLED}                # Enable Deployer behavior; must be set for any deployment activity
+: ${DPL_TAG_ENABLED}            # Trigger Deployer for all tag releases
+: ${DPL_JOB_ENABLE_ALL}         # Enable all jobs for deployment
+: ${DPL_TERMINATE_TESTS}        # Terminate all build test jobs (used in .travis.yml)
+: ${DPL_TRIGGER}                # Use a [word] in the commit message to trigger Deployer
+: ${DPL_JOBNAMES}               # Trigger Deployer by job name
+: ${DPL_OSNAMES}                # Trigger Deployer by OS name (osx,linux)
+: ${DPL_BRANCHES}               # Trigger Deployer by git branch name
+
+# Job Parameters
+: ${_DPL_JOB_ENABLED}           # Enable Deployer for this specific job. DPL_ENABLED must be set too.
+: ${_DPL_JOB_NAME}              # Identifier for the job, used for logging and trigger word matching
+: ${_DPL_FTP_TARGET}            # Deploy to FTP
+: ${_DPL_DPUT_TARGET}           # Deploy to DPUT
+: ${_DPL_PACKAGE_SOURCE}        # Build packages into a Source distribution. Linux only.
+: ${_DPL_PACKAGE_BINARY}        # Build packages into a Binary distribution.
+: ${_DPL_PACKAGE_MAIN:=1}       # Build main installation package. Linux only; OS X assumes this.
+: ${_DPL_PACKAGE_ASSET}         # Build asset installation package. Linux only.
+
+# Asset File Parameters
+: ${ASSET_ARCHIVE_PATH:=https://github.com/mazmazz/SRB2/releases/download/SRB2_assets/SRB2-v2122-assets.7z}
+: ${ASSET_ARCHIVE_OPTIONAL_PATH:=https://github.com/mazmazz/SRB2/releases/download/SRB2_assets/SRB2-v2122-optional-assets.7z}
+: ${ASSET_FILES_HASHED:=srb2.srb zones.dta player.dta rings.dta patch.dta}
+: ${ASSET_FILES_DOCS:=README.txt LICENSE.txt LICENSE-3RD-PARTY.txt}
+: ${ASSET_FILES_OPTIONAL_GET:=0}
+
+# FTP Parameters
+: ${DPL_FTP_PROTOCOL}
+: ${DPL_FTP_USER}
+: ${DPL_FTP_PASS}
+: ${DPL_FTP_HOSTNAME}
+: ${DPL_FTP_PORT}
+: ${DPL_FTP_PATH}
+
+# DPUT Parameters
+: ${DPL_DPUT_DOMAIN:=ppa.launchpad.net}
+: ${DPL_DPUT_METHOD:=sftp}
+: ${DPL_DPUT_INCOMING}
+: ${DPL_DPUT_LOGIN:=anonymous}
+: ${DPL_SSH_KEY_PRIVATE}        # Base64-encoded private key file. Used to sign repository uploads
+: ${DPL_SSH_KEY_PASSPHRASE}     # Decodes the private key file.
+
+# Package Parameters
+: ${PACKAGE_NAME:=srb2}
+: ${PACKAGE_VERSION:=2.1.23}
+: ${PACKAGE_SUBVERSION}         # Highly recommended to set this to reflect the distro series target (e.g., ~18.04bionic)
+: ${PACKAGE_REVISION}           # Defaults to UTC timestamp
+: ${PACKAGE_INSTALL_PATH:=/usr/games/SRB2}
+: ${PACKAGE_LINK_PATH:=/usr/games}
+: ${PACKAGE_DISTRO:=trusty}
+: ${PACKAGE_URGENCY:=high}
+: ${PACKAGE_NAME_EMAIL:=Sonic Team Junior <stjr@srb2.org>}
+: ${PACKAGE_GROUP_NAME_EMAIL:=Sonic Team Junior <stjr@srb2.org>}
+: ${PACKAGE_WEBSITE:=<http://www.srb2.org>}
+
+: ${PACKAGE_ASSET_MINVERSION:=2.1.21}  # Number this the version BEFORE the actual required version, because we do a > check
+: ${PACKAGE_ASSET_MAXVERSION:=2.1.24}  # Number this the version AFTER the actual required version, because we do a < check
+
+: ${PROGRAM_NAME:=Sonic Robo Blast 2}
+: ${PROGRAM_VENDOR:=Sonic Team Junior}
+: ${PROGRAM_VERSION:=2.1.23}
+: ${PROGRAM_DESCRIPTION:=A free 3D Sonic the Hedgehog fangame closely inspired by the original Sonic games on the Sega Genesis.}
+: ${PROGRAM_FILENAME:=srb2}
+
+: ${DPL_PGP_KEY_PRIVATE}        # Base64-encoded private key file. Used to sign Debian packages
+: ${DPL_PGP_KEY_PASSPHRASE}     # Decodes the private key file.
+
+# Export Asset and Package Parameters for envsubst templating
+
+export ASSET_ARCHIVE_PATH="${ASSET_ARCHIVE_PATH}"
+export ASSET_ARCHIVE_OPTIONAL_PATH="${ASSET_ARCHIVE_OPTIONAL_PATH}"
+export ASSET_FILES_HASHED="${ASSET_FILES_HASHED}"
+export ASSET_FILES_DOCS="${ASSET_FILES_DOCS}"
+export ASSET_FILES_OPTIONAL_GET="${ASSET_FILES_OPTIONAL_GET}"
+
+export PACKAGE_NAME="${PACKAGE_NAME}"
+export PACKAGE_VERSION="${PACKAGE_VERSION}"
+export PACKAGE_SUBVERSION="${PACKAGE_SUBVERSION}" # in case we have this
+export PACKAGE_REVISION="${PACKAGE_REVISION}"
+export PACKAGE_ASSET_MINVERSION="${PACKAGE_ASSET_MINVERSION}"
+export PACKAGE_ASSET_MAXVERSION="${PACKAGE_ASSET_MAXVERSION}"
+export PACKAGE_INSTALL_PATH="${PACKAGE_INSTALL_PATH}"
+export PACKAGE_LINK_PATH="${PACKAGE_LINK_PATH}"
+export PACKAGE_DISTRO="${PACKAGE_DISTRO}"
+export PACKAGE_URGENCY="${PACKAGE_URGENCY}"
+export PACKAGE_NAME_EMAIL="${PACKAGE_NAME_EMAIL}"
+export PACKAGE_GROUP_NAME_EMAIL="${PACKAGE_GROUP_NAME_EMAIL}"
+export PACKAGE_WEBSITE="${PACKAGE_WEBSITE}"
+
+export PROGRAM_NAME="${PROGRAM_NAME}"
+export PROGRAM_VERSION="${PROGRAM_VERSION}"
+export PROGRAM_DESCRIPTION="${PROGRAM_DESCRIPTION}"
+export PROGRAM_FILENAME="${PROGRAM_FILENAME}"
+
+# This file is called in debian_template.sh, so mark our completion so we don't run it again
+__DEBIAN_PARAMETERS_INITIALIZED=1
diff --git a/deployer/travis/deployer_dput.sh b/deployer/travis/deployer_dput.sh
new file mode 100644
index 0000000000000000000000000000000000000000..863a928cdf01b7a141144f01be2f9b0b664ccf66
--- /dev/null
+++ b/deployer/travis/deployer_dput.sh
@@ -0,0 +1,133 @@
+#!/bin/bash
+
+# Deployer for Travis-CI
+# DPUT uploader (e.g., Launchpad PPA)
+#
+
+if [[ "$__DPL_DPUT_ACTIVE" == "1" ]]; then
+    # Install APT dependencies
+    # paramiko required for ssh
+    sudo apt-get install python-paramiko expect dput; # python-pip
+    #pip install paramiko;
+
+    # Output the DPUT config
+    # Dput only works if you're using secure FTP, so that's what we default to.
+    cat > "./dput.cf" << EOM
+[deployer]
+fqdn = ${DPL_DPUT_DOMAIN}
+method = ${DPL_DPUT_METHOD}
+incoming = ${DPL_DPUT_INCOMING}
+login = ${DPL_DPUT_LOGIN}
+allow_unsigned_uploads = 0
+EOM
+
+    # Output SSH config
+    # Don't let SSH prompt us for untrusted hosts
+    cat >> "./ssh_config" << EOM
+
+Host *
+    StrictHostKeyChecking no
+    UserKnownHostsFile=/dev/null
+    PubKeyAuthentication yes
+    IdentityFile ${PWD}/key.private
+    IdentitiesOnly yes
+EOM
+    sudo sh -c "cat < ${PWD}/ssh_config >> /etc/ssh/ssh_config";
+
+    # Get the private key
+    echo "$DPL_SSH_KEY_PRIVATE" | base64 --decode > key.private;
+    chmod 700 ./key.private;
+
+    if [[ "$_DPL_PACKAGE_MAIN" == "1" ]]; then
+        PACKAGEFILENAME=${PACKAGE_NAME}_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+        PACKAGEDBGFILENAME=${PACKAGE_NAME}-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+		#PACKAGENIGHTLYFILENAME=${PACKAGE_NAME}-nightly_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+		#PACKAGENIGHTLYDBGFILENAME=${PACKAGE_NAME}-nightly-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+        #PACKAGEPATCHFILENAME=${PACKAGE_NAME}-patch_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+		#PACKAGEPATCHDBGFILENAME=${PACKAGE_NAME}-patch-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+        #PACKAGEPATCHNIGHTLYFILENAME=${PACKAGE_NAME}-patch-nightly_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+		#PACKAGEPATCHNIGHTLYDBGFILENAME=${PACKAGE_NAME}-patch-nightly-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+
+        PACKAGEFILENAMES=(
+            $PACKAGEFILENAME
+            $PACKAGEDBGFILENAME
+            #$PACKAGENIGHTLYFILENAME
+            #$PACKAGENIGHTLYDBGFILENAME
+            #$PACKAGEPATCHFILENAME
+            #$PACKAGEPATCHDBGFILENAME
+            #$PACKAGEPATCHNIGHTLYFILENAME
+            #$PACKAGEPATCHNIGHTLYDBGFILENAME
+        );
+
+        # Main packages are in parent of root repo folder
+        OLDPWD=$PWD; # [repo]/build
+        cd ../..;
+
+        # Enter passphrase if required
+        for n in ${PACKAGEFILENAMES}; do
+            for f in $n*.changes; do
+                # Binary builds also generate source builds, so exclude the source
+                # builds if desired
+                if [[ "$_DPL_PACKAGE_SOURCE" != "1" ]]; then
+                    if [[ "$f" == *"_source"* ]] || [[ "$f" == *".tar.xz"* ]]; then
+                        continue;
+                    fi;
+                fi;
+
+                expect <(cat <<EOD
+spawn dput -c "${OLDPWD}/dput.cf" deployer "$f";
+expect "Enter passphrase for key"
+send "${DPL_SSH_KEY_PASSPHRASE}\r"
+interact
+EOD
+);
+            done;
+        done;
+
+        # Go back to [repo]/build folder
+        cd $OLDPWD;
+    fi;
+
+    if [[ "$_DPL_PACKAGE_ASSET" == "1" ]]; then
+        PACKAGEFILENAME=${PACKAGE_NAME}-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+        #PACKAGENIGHTLYFILENAME=${PACKAGE_NAME}-nightly-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+        #PACKAGEPATCHFILENAME=${PACKAGE_NAME}-patch-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+        #PACKAGEPATCHNIGHTLYFILENAME=${PACKAGE_NAME}-patch-nightly-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+
+        PACKAGEFILENAMES=(
+            $PACKAGEFILENAME
+            #$PACKAGENIGHTLYFILENAME
+            #$PACKAGEPATCHFILENAME
+            #$PACKAGEPATCHNIGHTLYFILENAME
+        )
+
+        # Asset packages are in root repo folder
+        OLDPWD=$PWD; # [repo]/build
+        cd ..;
+
+        # Enter passphrase if required
+        for n in ${PACKAGEFILENAMES}; do
+            for f in $n*.changes; do
+                # Binary builds also generate source builds, so exclude the source
+                # builds if desired
+                if [[ "$_DPL_PACKAGE_SOURCE" != "1" ]]; then
+                    if [[ "$f" == *"_source"* ]] || [[ "$f" == *".tar.xz"* ]]; then
+                        continue;
+                    fi;
+                fi;
+                expect <(cat <<EOD
+spawn dput -c "${OLDPWD}/dput.cf" deployer "$f";
+expect "Enter passphrase for key"
+send "${DPL_SSH_KEY_PASSPHRASE}\r"
+interact
+EOD
+);
+            done;
+        done;
+
+        # Go back to [repo]/build folder
+        cd $OLDPWD;
+    fi;
+
+    srm ./key.private;
+fi;
diff --git a/deployer/travis/deployer_ftp.sh b/deployer/travis/deployer_ftp.sh
new file mode 100644
index 0000000000000000000000000000000000000000..1f6bd08b597d3c65c545b340de63df2b82208824
--- /dev/null
+++ b/deployer/travis/deployer_ftp.sh
@@ -0,0 +1,137 @@
+#!/bin/bash
+
+# Deployer for Travis-CI
+# FTP Uploader
+#
+# Package files are uploaded to, e.g., ftp://username:password@example.com:21/path/to/upload/STJr/SRB2/master/460873812-151.1
+# With file `commit.txt` and folder(s) `bin` and `package`
+#
+# Set these environment variables in your Travis-CI settings, where they are stored securely.
+# See other shell scripts for more options.
+#
+# DPL_FTP_PROTOCOL = ftp                    (ftp or sftp or ftps or however your FTP URI begins)
+# DPL_FTP_USER = username
+# DPL_FTP_PASS = password
+# DPL_FTP_HOSTNAME = example.com
+# DPL_FTP_PORT = 21
+# DPL_FTP_PATH = path/to/upload             (do not add trailing slash)
+
+if [[ "$__DPL_FTP_ACTIVE" == "1" ]]; then
+	if [[ "$TRAVIS_JOB_NAME" != "" ]]; then
+		JOBNAME=$TRAVIS_JOB_NAME;
+	else
+		if [[ "$_DPL_JOB_NAME" != "" ]]; then
+			JOBNAME=$_DPL_JOB_NAME;
+		else
+			JOBNAME=$TRAVIS_OS_NAME;
+		fi;
+	fi;
+
+	# Generate commit.txt file
+	echo "Travis-CI Build $TRAVIS_OS_NAME - $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH - $TRAVIS_JOB_NUMBER - $JOBNAME" > "commit.txt";
+	echo "Job ID $TRAVIS_JOB_ID" >> "commit.txt";
+	echo "" >> "commit.txt";
+	echo "Commit $TRAVIS_COMMIT" >> "commit.txt";
+	echo "$TRAVIS_COMMIT_MESSAGE" >> "commit.txt";
+	echo "" >> "commit.txt";
+
+	# Initialize FTP parameters
+	if [[ "$DPL_FTP_PORT" == "" ]]; then
+		DPL_FTP_PORT=21;
+	fi;
+	if [[ "$DPL_FTP_PROTOCOL" == "" ]]; then
+		DPL_FTP_PROTOCOL=ftp;
+	fi;
+	__DPL_FTP_LOCATION=$DPL_FTP_PROTOCOL://$DPL_FTP_HOSTNAME:$DPL_FTP_PORT/$DPL_FTP_PATH/$TRAVIS_REPO_SLUG/$TRAVIS_BRANCH/$TRAVIS_JOB_ID-$TRAVIS_JOB_NUMBER-$JOBNAME;
+
+	# Upload to FTP!
+	echo "Uploading to FTP...";
+	curl --ftp-create-dirs -T "commit.txt" -u $DPL_FTP_USER:$DPL_FTP_PASS "$__DPL_FTP_LOCATION/commit.txt";
+
+	if [[ "$__DPL_DEBIAN_ACTIVE" == "1" ]]; then
+		if [[ "$_DPL_PACKAGE_MAIN" == "1" ]]; then
+			PACKAGEFILENAME=${PACKAGE_NAME}_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			PACKAGEDBGFILENAME=${PACKAGE_NAME}-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGENIGHTLYFILENAME=${PACKAGE_NAME}-nightly_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGENIGHTLYDBGFILENAME=${PACKAGE_NAME}-nightly-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGEPATCHFILENAME=${PACKAGE_NAME}-patch_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGEPATCHDBGFILENAME=${PACKAGE_NAME}-patch-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGEPATCHNIGHTLYFILENAME=${PACKAGE_NAME}-patch-nightly_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGEPATCHNIGHTLYDBGFILENAME=${PACKAGE_NAME}-patch-nightly-dbg_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+
+			PACKAGEFILENAMES=(
+				$PACKAGEFILENAME
+				$PACKAGEDBGFILENAME
+				#$PACKAGENIGHTLYFILENAME
+				#$PACKAGENIGHTLYDBGFILENAME
+				#$PACKAGEPATCHFILENAME
+				#$PACKAGEPATCHDBGFILENAME
+				#$PACKAGEPATCHNIGHTLYFILENAME
+				#$PACKAGEPATCHNIGHTLYDBGFILENAME
+			);
+
+			# Main packages are in parent of root repo folder
+			OLDPWD=$PWD; # [repo]/build
+			cd ../..;
+
+			for n in ${PACKAGEFILENAMES}; do
+				for f in ./$n*; do
+					# Binary builds also generate source builds, so exclude the source
+					# builds if desired
+					if [[ "$_DPL_PACKAGE_SOURCE" != "1" ]]; then
+						if [[ "$f" == *"_source"* ]] || [[ "$f" == *".tar.xz"* ]]; then
+							continue;
+						fi;
+					fi;
+					curl --ftp-create-dirs -T "$f" -u $DPL_FTP_USER:$DPL_FTP_PASS  "$__DPL_FTP_LOCATION/package/main/$f";
+				done;
+			done;
+
+			# Go back to [repo]/build folder
+			cd $OLDPWD;
+		fi;
+
+		if [[ "$_DPL_PACKAGE_ASSET" == "1" ]]; then
+			PACKAGEFILENAME=${PACKAGE_NAME}-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGENIGHTLYFILENAME=${PACKAGE_NAME}-nightly-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGEPATCHFILENAME=${PACKAGE_NAME}-patch-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+			#PACKAGEPATCHNIGHTLYFILENAME=${PACKAGE_NAME}-patch-nightly-data_${PACKAGE_VERSION}${PACKAGE_SUBVERSION}${PACKAGE_REVISION};
+
+			PACKAGEFILENAMES=(
+				$PACKAGEFILENAME
+				#$PACKAGENIGHTLYFILENAME
+				#$PACKAGEPATCHFILENAME
+				#$PACKAGEPATCHNIGHTLYFILENAME
+			)
+
+			# Asset packages are in root repo folder
+			OLDPWD=$PWD; # [repo]/build
+			cd ..;
+
+			for n in ${PACKAGEFILENAMES}; do
+				for f in ./$n*; do
+					# Binary builds also generate source builds, so exclude the source
+					# builds if desired
+					if [[ "$_DPL_PACKAGE_SOURCE" != "1" ]]; then
+						if [[ "$f" == *"_source"* ]] || [[ "$f" == *".tar.xz"* ]]; then
+							continue;
+						fi;
+					fi;
+					curl --ftp-create-dirs -T "$f" -u $DPL_FTP_USER:$DPL_FTP_PASS  "$__DPL_FTP_LOCATION/package/asset/$f";
+				done;
+			done;
+
+			# Go back to [repo]/build folder
+			cd $OLDPWD;
+		fi;
+	else
+		if [[ "$_DPL_BINARY" == "1" ]]; then
+			find bin -type f -exec curl -u $DPL_FTP_USER:$DPL_FTP_PASS --ftp-create-dirs -T {} $__DPL_FTP_LOCATION/{} \;;
+		fi;
+
+		if [[ "$_DPL_PACKAGE_BINARY" == "1" ]]; then
+			sudo rm -r package/_CPack_Packages
+			find package -type f -exec curl -u $DPL_FTP_USER:$DPL_FTP_PASS --ftp-create-dirs -T {} $__DPL_FTP_LOCATION/{} \;;
+		fi;
+	fi;
+fi
diff --git a/libs/libgme.props b/libs/libgme.props
new file mode 100644
index 0000000000000000000000000000000000000000..209f6b9a88219d178e1d4ab066c94dd18539df25
--- /dev/null
+++ b/libs/libgme.props
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ImportGroup Label="PropertySheets" />
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup Condition="'$(Platform)' == 'Win32' OR '$(Platform)' == 'x64'">
+    <IncludePath>$(SolutionDir)libs\gme\include;$(IncludePath)</IncludePath>
+    <LibraryPath Condition="'$(Platform)' == 'Win32'">$(SolutionDir)libs\gme\win32;$(LibraryPath)</LibraryPath>
+    <LibraryPath Condition="'$(Platform)' == 'x64'">$(SolutionDir)libs\gme\win64;$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Platform)' == 'Win32' OR '$(Platform)' == 'x64'">
+    <Link>
+      <AdditionalDependencies>libgme.dll.a;%(AdditionalDependencies)</AdditionalDependencies>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup />
+</Project>
\ No newline at end of file
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a6fab34ff621af052079ac5c8acb145f7ab8f65e..9e319d100eea7a864d301599cd28785328ab93b3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -123,6 +123,7 @@ set(SRB2_CORE_RENDER_SOURCES
 	r_sky.c
 	r_splats.c
 	r_things.c
+	r_portal.c
 
 	r_bsp.h
 	r_data.h
@@ -136,6 +137,7 @@ set(SRB2_CORE_RENDER_SOURCES
 	r_splats.h
 	r_state.h
 	r_things.h
+	r_portal.h
 )
 
 set(SRB2_CORE_GAME_SOURCES
diff --git a/src/Makefile b/src/Makefile
index 5407a4d5ea9dee14be42ff8cda8f30d4240c4274..b8d91fcccdfb364044a3501e2cce0230a68a5406 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -455,6 +455,7 @@ OBJS:=$(i_main_o) \
 		$(OBJDIR)/r_sky.o    \
 		$(OBJDIR)/r_splats.o \
 		$(OBJDIR)/r_things.o \
+		$(OBJDIR)/r_portal.o \
 		$(OBJDIR)/screen.o   \
 		$(OBJDIR)/v_video.o  \
 		$(OBJDIR)/s_sound.o  \
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index c43cfe82a96196ce3ae58ee3720bab4f610a23fd..dd9fa8423b21897fc8894d37bfd4e59486be171f 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -1266,21 +1266,24 @@ UINT8 *HWR_GetScreenshot(void)
 	return buf;
 }
 
-boolean HWR_Screenshot(const char *lbmname)
+boolean HWR_Screenshot(const char *pathname)
 {
 	boolean ret;
 	UINT8 *buf = malloc(vid.width * vid.height * 3 * sizeof (*buf));
 
 	if (!buf)
+	{
+		CONS_Debug(DBG_RENDER, "HWR_Screenshot: Failed to allocate memory\n");
 		return false;
+	}
 
 	// returns 24bit 888 RGB
 	HWD.pfnReadRect(0, 0, vid.width, vid.height, vid.width * 3, (void *)buf);
 
 #ifdef USE_PNG
-	ret = M_SavePNG(lbmname, buf, vid.width, vid.height, false);
+	ret = M_SavePNG(pathname, buf, vid.width, vid.height, NULL);
 #else
-	ret = saveTGA(lbmname, buf, vid.width, vid.height);
+	ret = saveTGA(pathname, buf, vid.width, vid.height);
 #endif
 	free(buf);
 	return ret;
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index b1811eb4500558549bd11ec59edca619ae2f548f..c79452bb56f4e65a99b7588728b9fb8747639e0a 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -6194,7 +6194,7 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	}
 
 	// note: sets viewangle, viewx, viewy, viewz
-	R_SetupFrame(player, false); // This can stay false because it is only used to set viewsky in r_main.c, which isn't used here
+	R_SetupFrame(player);
 
 	// copy view cam position for local use
 	dup_viewx = viewx;
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index ced8f837da26f899bbf612e01e96eff235fbe580..fdfc1d25722712d7f11740492d54483639d21bf1 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -39,8 +39,6 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player);
 void HWR_RenderPlayerView(INT32 viewnumber, player_t *player);
 void HWR_DrawViewBorder(INT32 clearlines);
 void HWR_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatlumpnum);
-UINT8 *HWR_GetScreenshot(void);
-boolean HWR_Screenshot(const char *lbmname);
 void HWR_InitTextureMapping(void);
 void HWR_SetViewSize(void);
 void HWR_DrawPatch(GLPatch_t *gpatch, INT32 x, INT32 y, INT32 option);
@@ -54,6 +52,9 @@ void HWR_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 color);
 void HWR_DrawConsoleFill(INT32 x, INT32 y, INT32 w, INT32 h, UINT32 color, INT32 options);	// Lat: separate flags from color since color needs to be an uint to work right.
 void HWR_DrawPic(INT32 x,INT32 y,lumpnum_t lumpnum);
 
+UINT8 *HWR_GetScreenshot(void);
+boolean HWR_Screenshot(const char *pathname);
+
 void HWR_AddCommands(void);
 void HWR_CorrectSWTricks(void);
 void transform(float *cx, float *cy, float *cz);
diff --git a/src/info.h b/src/info.h
index 3e4243bdb43eff29f4976ba0cdbf707dc662262e..13abfa5f60249d91afd05bc0fba7b35376f41a44 100644
--- a/src/info.h
+++ b/src/info.h
@@ -267,7 +267,7 @@ void A_SaloonDoorSpawn();
 void A_MinecartSparkThink();
 
 // ratio of states to sprites to mobj types is roughly 6 : 1 : 1
-#define NUMMOBJFREESLOTS 256
+#define NUMMOBJFREESLOTS 512
 #define NUMSPRITEFREESLOTS NUMMOBJFREESLOTS
 #define NUMSTATEFREESLOTS (NUMMOBJFREESLOTS*8)
 
diff --git a/src/m_misc.c b/src/m_misc.c
index c967548c65b54560ddf3efbc1d40bae731df0062..621d705f9084131b2af359655a60b5a8f13ac809 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -30,6 +30,7 @@
 #include "g_game.h"
 #include "m_misc.h"
 #include "hu_stuff.h"
+#include "st_stuff.h"
 #include "v_video.h"
 #include "z_zone.h"
 #include "g_input.h"
@@ -609,6 +610,23 @@ void M_SaveConfig(const char *filename)
 	fclose(f);
 }
 
+// ==========================================================================
+//                              SCREENSHOTS
+// ==========================================================================
+static UINT8 screenshot_palette[768];
+static void M_CreateScreenShotPalette(void)
+{
+	size_t i, j;
+	for (i = 0, j = 0; i < 768; i += 3, j++)
+	{
+		RGBA_t locpal = ((cv_screenshot_colorprofile.value)
+		? pLocalPalette[(max(st_palette,0)*256)+j]
+		: pMasterPalette[(max(st_palette,0)*256)+j]);
+		screenshot_palette[i] = locpal.s.red;
+		screenshot_palette[i+1] = locpal.s.green;
+		screenshot_palette[i+2] = locpal.s.blue;
+	}
+}
 
 #if NUMSCREENS > 2
 static const char *Newsnapshotfile(const char *pathname, const char *ext)
@@ -677,25 +695,20 @@ static void PNG_warn(png_structp PNG, png_const_charp pngtext)
 	CONS_Debug(DBG_RENDER, "libpng warning at %p: %s", PNG, pngtext);
 }
 
-static void M_PNGhdr(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png_uint_32 width, PNG_CONST png_uint_32 height, const boolean palette)
+static void M_PNGhdr(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png_uint_32 width, PNG_CONST png_uint_32 height, PNG_CONST png_byte *palette)
 {
 	const png_byte png_interlace = PNG_INTERLACE_NONE; //PNG_INTERLACE_ADAM7
 	if (palette)
 	{
 		png_colorp png_PLTE = png_malloc(png_ptr, sizeof(png_color)*256); //palette
+		const png_byte *pal = palette;
 		png_uint_16 i;
-
-		RGBA_t *pal = ((cv_screenshot_colorprofile.value)
-		? pLocalPalette
-		: pMasterPalette);
-
 		for (i = 0; i < 256; i++)
 		{
-			png_PLTE[i].red   = pal[i].s.red;
-			png_PLTE[i].green = pal[i].s.green;
-			png_PLTE[i].blue  = pal[i].s.blue;
+			png_PLTE[i].red   = *pal; pal++;
+			png_PLTE[i].green = *pal; pal++;
+			png_PLTE[i].blue  = *pal; pal++;
 		}
-
 		png_set_IHDR(png_ptr, png_info_ptr, width, height, 8, PNG_COLOR_TYPE_PALETTE,
 		 png_interlace, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
 		png_write_info_before_PLTE(png_ptr, png_info_ptr);
@@ -968,7 +981,7 @@ static void M_PNGfix_acTL(png_structp png_ptr, png_infop png_info_ptr,
 #endif
 }
 
-static boolean M_SetupaPNG(png_const_charp filename, boolean palette)
+static boolean M_SetupaPNG(png_const_charp filename, png_bytep pal)
 {
 	apng_FILE = fopen(filename,"wb+"); // + mode for reading
 	if (!apng_FILE)
@@ -1020,7 +1033,7 @@ static boolean M_SetupaPNG(png_const_charp filename, boolean palette)
 	png_set_compression_strategy(apng_ptr, cv_zlib_strategya.value);
 	png_set_compression_window_bits(apng_ptr, cv_zlib_window_bitsa.value);
 
-	M_PNGhdr(apng_ptr, apng_info_ptr, vid.width, vid.height, palette);
+	M_PNGhdr(apng_ptr, apng_info_ptr, vid.width, vid.height, pal);
 
 	M_PNGText(apng_ptr, apng_info_ptr, true);
 
@@ -1044,6 +1057,7 @@ static boolean M_SetupaPNG(png_const_charp filename, boolean palette)
 static inline moviemode_t M_StartMovieAPNG(const char *pathname)
 {
 #ifdef USE_APNG
+	UINT8 *palette;
 	const char *freename = NULL;
 	boolean ret = false;
 
@@ -1059,10 +1073,8 @@ static inline moviemode_t M_StartMovieAPNG(const char *pathname)
 		return MM_OFF;
 	}
 
-	if (rendermode == render_soft)
-		ret = M_SetupaPNG(va(pandf,pathname,freename), true);
-	else
-		ret = M_SetupaPNG(va(pandf,pathname,freename), false);
+	if (rendermode == render_soft) M_CreateScreenShotPalette();
+	ret = M_SetupaPNG(va(pandf,pathname,freename), (palette = screenshot_palette));
 
 	if (!ret)
 	{
@@ -1265,13 +1277,14 @@ void M_StopMovie(void)
   * \param data     The image data.
   * \param width    Width of the picture.
   * \param height   Height of the picture.
-  * \param palette  Palette of image data
+  * \param palette  Palette of image data.
   *  \note if palette is NULL, BGR888 format
   */
-boolean M_SavePNG(const char *filename, void *data, int width, int height, const boolean palette)
+boolean M_SavePNG(const char *filename, void *data, int width, int height, const UINT8 *palette)
 {
 	png_structp png_ptr;
 	png_infop png_info_ptr;
+	PNG_CONST png_byte *PLTE = (const png_byte *)palette;
 #ifdef PNG_SETJMP_SUPPORTED
 #ifdef USE_FAR_KEYWORD
 	jmp_buf jmpbuf;
@@ -1286,8 +1299,7 @@ boolean M_SavePNG(const char *filename, void *data, int width, int height, const
 		return false;
 	}
 
-	png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL,
-	 PNG_error, PNG_warn);
+	png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, PNG_error, PNG_warn);
 	if (!png_ptr)
 	{
 		CONS_Debug(DBG_RENDER, "M_SavePNG: Error on initialize libpng\n");
@@ -1334,7 +1346,7 @@ boolean M_SavePNG(const char *filename, void *data, int width, int height, const
 	png_set_compression_strategy(png_ptr, cv_zlib_strategy.value);
 	png_set_compression_window_bits(png_ptr, cv_zlib_window_bits.value);
 
-	M_PNGhdr(png_ptr, png_info_ptr, width, height, palette);
+	M_PNGhdr(png_ptr, png_info_ptr, width, height, PLTE);
 
 	M_PNGText(png_ptr, png_info_ptr, false);
 
@@ -1381,7 +1393,7 @@ typedef struct
   * \param palette  Palette of image data
   */
 #if NUMSCREENS > 2
-static boolean WritePCXfile(const char *filename, const UINT8 *data, int width, int height)
+static boolean WritePCXfile(const char *filename, const UINT8 *data, int width, int height, const UINT8 *pal)
 {
 	int i;
 	size_t length;
@@ -1425,15 +1437,11 @@ static boolean WritePCXfile(const char *filename, const UINT8 *data, int width,
 
 	// write color table
 	{
-		RGBA_t *pal = ((cv_screenshot_colorprofile.value)
-		? pLocalPalette
-		: pMasterPalette);
-
 		for (i = 0; i < 256; i++)
 		{
-			*pack++ = pal[i].s.red;
-			*pack++ = pal[i].s.green;
-			*pack++ = pal[i].s.blue;
+			*pack++ = *pal; pal++;
+			*pack++ = *pal; pal++;
+			*pack++ = *pal; pal++;
 		}
 	}
 
@@ -1453,9 +1461,8 @@ void M_ScreenShot(void)
 }
 
 /** Takes a screenshot.
-  * The screenshot is saved as "srb2xxxx.pcx" (or "srb2xxxx.tga" in hardware
-  * rendermode) where xxxx is the lowest four-digit number for which a file
-  * does not already exist.
+  * The screenshot is saved as "srb2xxxx.png" where xxxx is the lowest
+  * four-digit number for which a file does not already exist.
   *
   * \sa HWR_ScreenShot
   */
@@ -1469,6 +1476,10 @@ void M_DoScreenShot(void)
 	// Don't take multiple screenshots, obviously
 	takescreenshot = false;
 
+	// how does one take a screenshot without a render system?
+	if (rendermode == render_none)
+		return;
+
 	if (cv_screenshot_option.value == 0)
 		pathname = usehome ? srb2home : srb2path;
 	else if (cv_screenshot_option.value == 1)
@@ -1479,16 +1490,13 @@ void M_DoScreenShot(void)
 		pathname = cv_screenshot_folder.string;
 
 #ifdef USE_PNG
-	if (rendermode != render_none)
-		freename = Newsnapshotfile(pathname,"png");
+	freename = Newsnapshotfile(pathname,"png");
 #else
 	if (rendermode == render_soft)
 		freename = Newsnapshotfile(pathname,"pcx");
-	else if (rendermode != render_none)
+	else if (rendermode == render_opengl)
 		freename = Newsnapshotfile(pathname,"tga");
 #endif
-	else
-		I_Error("Can't take a screenshot without a render system");
 
 	if (rendermode == render_soft)
 	{
@@ -1502,16 +1510,16 @@ void M_DoScreenShot(void)
 
 	// save the pcx file
 #ifdef HWRENDER
-	if (rendermode != render_soft)
+	if (rendermode == render_opengl)
 		ret = HWR_Screenshot(va(pandf,pathname,freename));
 	else
 #endif
-	if (rendermode != render_none)
 	{
+		M_CreateScreenShotPalette();
 #ifdef USE_PNG
-		ret = M_SavePNG(va(pandf,pathname,freename), linear, vid.width, vid.height, true);
+		ret = M_SavePNG(va(pandf,pathname,freename), linear, vid.width, vid.height, screenshot_palette);
 #else
-		ret = WritePCXfile(va(pandf,pathname,freename), linear, vid.width, vid.height);
+		ret = WritePCXfile(va(pandf,pathname,freename), linear, vid.width, vid.height, screenshot_palette);
 #endif
 	}
 
@@ -1519,14 +1527,14 @@ failure:
 	if (ret)
 	{
 		if (moviemode != MM_SCREENSHOT)
-			CONS_Printf(M_GetText("screen shot %s saved in %s\n"), freename, pathname);
+			CONS_Printf(M_GetText("Screen shot %s saved in %s\n"), freename, pathname);
 	}
 	else
 	{
 		if (freename)
-			CONS_Printf(M_GetText("Couldn't create screen shot %s in %s\n"), freename, pathname);
+			CONS_Alert(CONS_ERROR, M_GetText("Couldn't create screen shot %s in %s\n"), freename, pathname);
 		else
-			CONS_Printf(M_GetText("Couldn't create screen shot (all 10000 slots used!) in %s\n"), pathname);
+			CONS_Alert(CONS_ERROR, M_GetText("Couldn't create screen shot in %s (all 10000 slots used!)\n"), pathname);
 
 		if (moviemode == MM_SCREENSHOT)
 			M_StopMovie();
diff --git a/src/m_misc.h b/src/m_misc.h
index 28d9cd5a508919ed80c857cb5a9b83245bcc7bdf..6ac92dbcc085324c5c5bcba48c8f22fd2824c85f 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -58,7 +58,7 @@ void FIL_ForceExtension(char *path, const char *extension);
 boolean FIL_CheckExtension(const char *in);
 
 #ifdef HAVE_PNG
-boolean M_SavePNG(const char *filename, void *data, int width, int height, const boolean palette);
+boolean M_SavePNG(const char *filename, void *data, int width, int height, const UINT8 *palette);
 #endif
 
 extern boolean takescreenshot;
diff --git a/src/p_setup.c b/src/p_setup.c
index 0602865a9dfe876573dbf74f90dc00dac0f2cd46..af4f1f9dd6989322f85704951b79248400b2a4a6 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -3147,7 +3147,6 @@ boolean P_SetupLevel(boolean skipprecip)
 		savedata.lives = 0;
 	}
 
-	skyVisible = skyVisible1 = skyVisible2 = true; // assume the skybox is visible on level load.
 	if (loadprecip) // uglier hack
 	{ // to make a newly loaded level start on the second frame.
 		INT32 buf = gametic % BACKUPTICS;
diff --git a/src/p_spec.c b/src/p_spec.c
index f8aefb9c8d871d97556951cd5970c5bde69fb138..e47b5cc036edf8a44657666479ff13ac612bfc09 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -7746,22 +7746,11 @@ static void P_SpawnScrollers(void)
 
 	for (i = 0; i < numlines; i++, l++)
 	{
-		fixed_t dx = l->dx; // direction and speed of scrolling
-		fixed_t dy = l->dy;
+		fixed_t dx = l->dx >> SCROLL_SHIFT; // direction and speed of scrolling
+		fixed_t dy = l->dy >> SCROLL_SHIFT;
 		INT32 control = -1, accel = 0; // no control sector or acceleration
 		INT32 special = l->special;
 
-		// If front texture X offset provided, override the amount with it.
-		if (sides[l->sidenum[0]].textureoffset != 0)
-		{
-			fixed_t len = sides[l->sidenum[0]].textureoffset;
-			fixed_t h = FixedHypot(dx, dy);
-			dx = FixedMul(FixedDiv(dx, h), len);
-			dy = FixedMul(FixedDiv(dy, h), len);
-		}
-		dx = dx >> SCROLL_SHIFT;
-		dy = dy >> SCROLL_SHIFT;
-
 		// These types are same as the ones they get set to except that the
 		// first side's sector's heights cause scrolling when they change, and
 		// this linedef controls the direction and speed of the scrolling. The
@@ -7796,14 +7785,8 @@ static void P_SpawnScrollers(void)
 
 			case 513: // scroll effect ceiling
 			case 533: // scroll and carry objects on ceiling
-				if (l->tag == 0)
-					Add_Scroller(sc_ceiling, -dx, dy, control, l->frontsector - sectors, accel, l->flags & ML_NOCLIMB);
-				else
-				{
-					for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
-						Add_Scroller(sc_ceiling, -dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
-				}
-
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Scroller(sc_ceiling, -dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				if (special != 533)
 					break;
 				/* FALLTHRU */
@@ -7811,26 +7794,14 @@ static void P_SpawnScrollers(void)
 			case 523:	// carry objects on ceiling
 				dx = FixedMul(dx, CARRYFACTOR);
 				dy = FixedMul(dy, CARRYFACTOR);
-
-				if (l->tag == 0)
-					Add_Scroller(sc_carry_ceiling, dx, dy, control, l->frontsector - sectors, accel, l->flags & ML_NOCLIMB);
-				else
-				{
-					for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
-						Add_Scroller(sc_carry_ceiling, dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
-				}
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Scroller(sc_carry_ceiling, dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				break;
 
 			case 510: // scroll effect floor
 			case 530: // scroll and carry objects on floor
-				if (l->tag == 0)
-					Add_Scroller(sc_floor, -dx, dy, control, l->frontsector - sectors, accel, l->flags & ML_NOCLIMB);
-				else
-				{
-					for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
-						Add_Scroller(sc_floor, -dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
-				}
-
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Scroller(sc_floor, -dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				if (special != 530)
 					break;
 				/* FALLTHRU */
@@ -7838,14 +7809,8 @@ static void P_SpawnScrollers(void)
 			case 520:	// carry objects on floor
 				dx = FixedMul(dx, CARRYFACTOR);
 				dy = FixedMul(dy, CARRYFACTOR);
-
-				if (l->tag == 0)
-					Add_Scroller(sc_carry, dx, dy, control, l->frontsector - sectors, accel, l->flags & ML_NOCLIMB);
-				else
-				{
-					for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
-						Add_Scroller(sc_carry, dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
-				}
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Scroller(sc_carry, dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				break;
 
 			// scroll wall according to linedef
@@ -9208,77 +9173,43 @@ static void P_SpawnPushers(void)
 	line_t *l = lines;
 	register INT32 s;
 	mobj_t *thing;
-	pushertype_e pushertype;
-	fixed_t dx, dy;
 
 	for (i = 0; i < numlines; i++, l++)
-	{
 		switch (l->special)
 		{
-		case 541: // wind
-			pushertype = p_wind;
-			break;
-
-		case 544: // current
-			pushertype = p_current;
-			break;
-		case 547: // push/pull
-			if (l->tag == 0)
-			{
-				s = l->frontsector - sectors;
-				if ((thing = P_GetPushThing(s)) != NULL) // No MT_P* means no effect
-					Add_Pusher(p_push, l->dx, l->dy, thing, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
-			}
-			else
+			case 541: // wind
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Pusher(p_wind, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
+				break;
+			case 544: // current
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Pusher(p_current, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
+				break;
+			case 547: // push/pull
 				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
 				{
-					if ((thing = P_GetPushThing(s)) != NULL)  // No MT_P* means no effect
+					thing = P_GetPushThing(s);
+					if (thing) // No MT_P* means no effect
 						Add_Pusher(p_push, l->dx, l->dy, thing, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
 				}
-
-			continue;
-
-		case 545: // current up
-			pushertype = p_upcurrent;
-			break;
-
-		case 546: // current down
-			pushertype = p_downcurrent;
-			break;
-
-		case 542: // wind up
-			pushertype = p_upwind;
-			break;
-
-		case 543: // wind down
-			pushertype = p_downwind;
-			break;
-
-		default:
-			continue;
-		}
-
-		dx = l->dx;
-		dy = l->dy;
-
-		// Obtain versor and scale it up according to texture offset, if provided; line length is ignored in this case.
-
-		if (sides[l->sidenum[0]].textureoffset != 0)
-		{
-			fixed_t len = sides[l->sidenum[0]].textureoffset;
-			fixed_t h = FixedHypot(dx, dy);
-			dx = FixedMul(FixedDiv(dx, h), len);
-			dy = FixedMul(FixedDiv(dy, h), len);
-		}
-
-		if (l->tag == 0)
-			Add_Pusher(pushertype, dx, dy, NULL, l->frontsector - sectors, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
-		else
-		{
-			for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
-				Add_Pusher(pushertype, dx, dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
+				break;
+			case 545: // current up
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Pusher(p_upcurrent, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
+				break;
+			case 546: // current down
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Pusher(p_downcurrent, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
+				break;
+			case 542: // wind up
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Pusher(p_upwind, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
+				break;
+			case 543: // wind down
+				for (s = -1; (s = P_FindSectorFromLineTag(l, s)) >= 0 ;)
+					Add_Pusher(p_downwind, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
+				break;
 		}
-	}
 }
 
 static void P_SearchForDisableLinedefs(void)
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 22abaeb88e8783e4b67c8efdcd695e3a004d9095..d521d9f4d4943a6ea5e341738a20e2100dadb91b 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -15,6 +15,7 @@
 #include "g_game.h"
 #include "r_local.h"
 #include "r_state.h"
+#include "r_portal.h" // Add seg portals
 
 #include "r_splats.h"
 #include "p_local.h" // camera
@@ -26,11 +27,11 @@ side_t *sidedef;
 line_t *linedef;
 sector_t *frontsector;
 sector_t *backsector;
-boolean portalline; // is curline a portal seg?
 
 // very ugly realloc() of drawsegs at run-time, I upped it to 512
 // instead of 256.. and someone managed to send me a level with
 // 896 drawsegs! So too bad here's a limit removal a-la-Boom
+drawseg_t *curdrawsegs = NULL; /**< This is used to handle multiple lists for masked drawsegs. */
 drawseg_t *drawsegs = NULL;
 drawseg_t *ds_p = NULL;
 
@@ -459,7 +460,7 @@ static void R_AddLine(seg_t *line)
 				line2 = P_FindSpecialLineFromTag(40, line->linedef->tag, line2);
 			if (line2 >= 0) // found it!
 			{
-				R_AddPortal(line->linedef-lines, line2, x1, x2); // Remember the lines for later rendering
+				Portal_Add2Lines(line->linedef-lines, line2, x1, x2); // Remember the lines for later rendering
 				//return; // Don't fill in that space now!
 				goto clipsolid;
 			}
@@ -1377,13 +1378,5 @@ void R_RenderBSPNode(INT32 bspnum)
 		bspnum = bsp->children[side^1];
 	}
 
-	// PORTAL CULLING
-	if (portalcullsector) {
-		sector_t *sect = subsectors[bspnum & ~NF_SUBSECTOR].sector;
-		if (sect != portalcullsector)
-			return;
-		portalcullsector = NULL;
-	}
-
 	R_Subsector(bspnum == -1 ? 0 : bspnum & ~NF_SUBSECTOR);
 }
diff --git a/src/r_bsp.h b/src/r_bsp.h
index e3662e2e6ad23f6c25efed2ceb44f861a2dd15eb..825be6064b078ba0f5c0966b1d726c72e2546c07 100644
--- a/src/r_bsp.h
+++ b/src/r_bsp.h
@@ -29,6 +29,7 @@ extern boolean portalline; // is curline a portal seg?
 
 extern INT32 checkcoord[12][4];
 
+extern drawseg_t *curdrawsegs;
 extern drawseg_t *drawsegs;
 extern drawseg_t *ds_p;
 extern INT32 doorclosed;
@@ -38,7 +39,6 @@ void R_ClearClipSegs(void);
 void R_PortalClearClipSegs(INT32 start, INT32 end);
 void R_ClearDrawSegs(void);
 void R_RenderBSPNode(INT32 bspnum);
-void R_AddPortal(INT32 line1, INT32 line2, INT32 x1, INT32 x2);
 
 #ifdef POLYOBJECTS
 void R_SortPolyObjects(subsector_t *sub);
diff --git a/src/r_main.c b/src/r_main.c
index 23dc39d62f5c31753ee240a237d89d581035df02..273d13a56bb26165b8749f557898ae201009cf63 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -30,6 +30,7 @@
 #include "p_spec.h" // skyboxmo
 #include "z_zone.h"
 #include "m_random.h" // quake camera shake
+#include "r_portal.h"
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -65,37 +66,9 @@ size_t loopcount;
 fixed_t viewx, viewy, viewz;
 angle_t viewangle, aimingangle;
 fixed_t viewcos, viewsin;
-boolean viewsky, skyVisible;
-boolean skyVisible1, skyVisible2; // saved values of skyVisible for P1 and P2, for splitscreen
 sector_t *viewsector;
 player_t *viewplayer;
 
-// PORTALS!
-// You can thank and/or curse JTE for these.
-UINT8 portalrender;
-sector_t *portalcullsector;
-typedef struct portal_pair
-{
-	INT32 line1;
-	INT32 line2;
-	UINT8 pass;
-	struct portal_pair *next;
-
-	fixed_t viewx;
-	fixed_t viewy;
-	fixed_t viewz;
-	angle_t viewangle;
-
-	INT32 start;
-	INT32 end;
-	INT16 *ceilingclip;
-	INT16 *floorclip;
-	fixed_t *frontscale;
-} portal_pair;
-portal_pair *portal_base, *portal_cap;
-line_t *portalclipline;
-INT32 portalclipstart, portalclipend;
-
 //
 // precalculated math tables
 //
@@ -764,7 +737,7 @@ static void R_SetupFreelook(void)
 
 #undef AIMINGTODY
 
-void R_SetupFrame(player_t *player, boolean skybox)
+void R_SetupFrame(player_t *player)
 {
 	camera_t *thiscam;
 	boolean chasecam = false;
@@ -794,7 +767,6 @@ void R_SetupFrame(player_t *player, boolean skybox)
 	else if (!chasecam)
 		thiscam->chase = false;
 
-	viewsky = !skybox;
 	if (player->awayviewtics)
 	{
 		// cut-away view stuff
@@ -883,7 +855,6 @@ void R_SkyboxFrame(player_t *player)
 		thiscam = &camera;
 
 	// cut-away view stuff
-	viewsky = true;
 	viewmobj = skyboxmo[0];
 #ifdef PARANOIA
 	if (!viewmobj)
@@ -1010,17 +981,8 @@ void R_SkyboxFrame(player_t *player)
 	R_SetupFreelook();
 }
 
-#define ANGLED_PORTALS
-
-static void R_PortalFrame(line_t *start, line_t *dest, portal_pair *portal)
+static void R_PortalFrame(portal_t *portal)
 {
-	vertex_t dest_c, start_c;
-#ifdef ANGLED_PORTALS
-	// delta angle
-	angle_t dangle = R_PointToAngle2(0,0,dest->dx,dest->dy) - R_PointToAngle2(start->dx,start->dy,0,0);
-#endif
-
-	//R_SetupFrame(player, false);
 	viewx = portal->viewx;
 	viewy = portal->viewy;
 	viewz = portal->viewz;
@@ -1029,94 +991,35 @@ static void R_PortalFrame(line_t *start, line_t *dest, portal_pair *portal)
 	viewsin = FINESINE(viewangle>>ANGLETOFINESHIFT);
 	viewcos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
 
-	portalcullsector = dest->frontsector;
-	viewsector = dest->frontsector;
-	portalclipline = dest;
 	portalclipstart = portal->start;
 	portalclipend = portal->end;
 
-	// Offset the portal view by the linedef centers
-
-	// looking glass center
-	start_c.x = (start->v1->x + start->v2->x) / 2;
-	start_c.y = (start->v1->y + start->v2->y) / 2;
-
-	// other side center
-	dest_c.x = (dest->v1->x + dest->v2->x) / 2;
-	dest_c.y = (dest->v1->y + dest->v2->y) / 2;
-
-	// Heights!
-	viewz += dest->frontsector->floorheight - start->frontsector->floorheight;
-
-	// calculate the difference in position and rotation!
-#ifdef ANGLED_PORTALS
-	if (dangle == 0)
-#endif
-	{ // the entrance goes straight opposite the exit, so we just need to mess with the offset.
-		viewx += dest_c.x - start_c.x;
-		viewy += dest_c.y - start_c.y;
-		return;
+	if (portal->clipline != -1)
+	{
+		portalclipline = &lines[portal->clipline];
+		viewsector = portalclipline->frontsector;
 	}
-
-#ifdef ANGLED_PORTALS
-	viewangle += dangle;
-	viewsin = FINESINE(viewangle>>ANGLETOFINESHIFT);
-	viewcos = FINECOSINE(viewangle>>ANGLETOFINESHIFT);
-	//CONS_Printf("dangle == %u\n", AngleFixed(dangle)>>FRACBITS);
-
-	// ????
+	else
 	{
-		fixed_t disttopoint;
-		angle_t angtopoint;
-
-		disttopoint = R_PointToDist2(start_c.x, start_c.y, viewx, viewy);
-		angtopoint = R_PointToAngle2(start_c.x, start_c.y, viewx, viewy);
-		angtopoint += dangle;
-
-		viewx = dest_c.x+FixedMul(FINECOSINE(angtopoint>>ANGLETOFINESHIFT), disttopoint);
-		viewy = dest_c.y+FixedMul(FINESINE(angtopoint>>ANGLETOFINESHIFT), disttopoint);
+		portalclipline = NULL;
+		viewsector = R_PointInSubsector(viewx, viewy)->sector;
 	}
-#endif
 }
 
-void R_AddPortal(INT32 line1, INT32 line2, INT32 x1, INT32 x2)
+static void Mask_Pre (maskcount_t* m)
 {
-	portal_pair *portal = Z_Malloc(sizeof(portal_pair), PU_LEVEL, NULL);
-	INT16 *ceilingclipsave = Z_Malloc(sizeof(INT16)*(x2-x1), PU_LEVEL, NULL);
-	INT16 *floorclipsave = Z_Malloc(sizeof(INT16)*(x2-x1), PU_LEVEL, NULL);
-	fixed_t *frontscalesave = Z_Malloc(sizeof(fixed_t)*(x2-x1), PU_LEVEL, NULL);
-
-	portal->line1 = line1;
-	portal->line2 = line2;
-	portal->pass = portalrender+1;
-	portal->next = NULL;
-
-	R_PortalStoreClipValues(x1, x2, ceilingclipsave, floorclipsave, frontscalesave);
-
-	portal->ceilingclip = ceilingclipsave;
-	portal->floorclip = floorclipsave;
-	portal->frontscale = frontscalesave;
-
-	portal->start = x1;
-	portal->end = x2;
-
-	portalline = true; // this tells R_StoreWallRange that curline is a portal seg
-
-	portal->viewx = viewx;
-	portal->viewy = viewy;
-	portal->viewz = viewz;
-	portal->viewangle = viewangle;
+	m->drawsegs[0] = ds_p - drawsegs;
+	m->vissprites[0] = visspritecount;
+	m->viewx = viewx;
+	m->viewy = viewy;
+	m->viewz = viewz;
+	m->viewsector = viewsector;
+}
 
-	if (!portal_base)
-	{
-		portal_base = portal;
-		portal_cap = portal;
-	}
-	else
-	{
-		portal_cap->next = portal;
-		portal_cap = portal;
-	}
+static void Mask_Post (maskcount_t* m)
+{
+	m->drawsegs[1] = ds_p - drawsegs;
+	m->vissprites[1] = visspritecount;
 }
 
 // ================
@@ -1131,8 +1034,8 @@ void R_AddPortal(INT32 line1, INT32 line2, INT32 x1, INT32 x2)
 
 void R_RenderPlayerView(player_t *player)
 {
-	portal_pair *portal;
-	const boolean skybox = (skyboxmo[0] && cv_skybox.value);
+	UINT8			nummasks	= 1;
+	maskcount_t*	masks		= malloc(sizeof(maskcount_t));
 
 	if (cv_homremoval.value && player == &players[displayplayer]) // if this is display player 1
 	{
@@ -1142,38 +1045,7 @@ void R_RenderPlayerView(player_t *player)
 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 32+(timeinmap&15));
 	}
 
-	// load previous saved value of skyVisible for the player
-	if (splitscreen && player == &players[secondarydisplayplayer])
-		skyVisible = skyVisible2;
-	else
-		skyVisible = skyVisible1;
-
-	portalrender = 0;
-	portal_base = portal_cap = NULL;
-
-	if (skybox && skyVisible)
-	{
-		R_SkyboxFrame(player);
-
-		R_ClearClipSegs();
-		R_ClearDrawSegs();
-		R_ClearPlanes();
-		R_ClearSprites();
-#ifdef FLOORSPLATS
-		R_ClearVisibleFloorSplats();
-#endif
-
-		R_RenderBSPNode((INT32)numnodes - 1);
-		R_ClipSprites();
-		R_DrawPlanes();
-#ifdef FLOORSPLATS
-		R_DrawVisibleFloorSplats();
-#endif
-		R_DrawMasked();
-	}
-
-	R_SetupFrame(player, skybox);
-	skyVisible = false;
+	R_SetupFrame(player);
 	framecount++;
 	validcount++;
 
@@ -1185,19 +1057,21 @@ void R_RenderPlayerView(player_t *player)
 #ifdef FLOORSPLATS
 	R_ClearVisibleFloorSplats();
 #endif
+	Portal_InitList();
 
 	// check for new console commands.
 	NetUpdate();
 
 	// The head node is the last node output.
 
+	Mask_Pre(&masks[nummasks - 1]);
+	curdrawsegs = ds_p;
 //profile stuff ---------------------------------------------------------
 #ifdef TIMING
 	mytotal = 0;
 	ProfZeroTimer();
 #endif
 	R_RenderBSPNode((INT32)numnodes - 1);
-	R_ClipSprites();
 #ifdef TIMING
 	RDMSR(0x10, &mycount);
 	mytotal += mycount; // 64bit add
@@ -1205,54 +1079,66 @@ void R_RenderPlayerView(player_t *player)
 	CONS_Debug(DBG_RENDER, "RenderBSPNode: 0x%d %d\n", *((INT32 *)&mytotal + 1), (INT32)mytotal);
 #endif
 //profile stuff ---------------------------------------------------------
+	Mask_Post(&masks[nummasks - 1]);
+
+	R_ClipSprites(drawsegs, NULL);
+
+
+	// Add skybox portals caused by sky visplanes.
+	if (cv_skybox.value && skyboxmo[0])
+		Portal_AddSkyboxPortals();
 
-	// PORTAL RENDERING
-	for(portal = portal_base; portal; portal = portal_base)
+	// Portal rendering. Hijacks the BSP traversal.
+	if (portal_base)
 	{
-		// render the portal
-		CONS_Debug(DBG_RENDER, "Rendering portal from line %d to %d\n", portal->line1, portal->line2);
-		portalrender = portal->pass;
+		portal_t *portal;
+
+		for(portal = portal_base; portal; portal = portal_base)
+		{
+			portalrender = portal->pass; // Recursiveness depth.
+
+			R_ClearFFloorClips();
+
+			// Apply the viewpoint stored for the portal.
+			R_PortalFrame(portal);
 
-		R_PortalFrame(&lines[portal->line1], &lines[portal->line2], portal);
+			// Hack in the clipsegs to delimit the starting
+			// clipping for sprites and possibly other similar
+			// future items.
+			R_PortalClearClipSegs(portal->start, portal->end);
 
-		R_PortalClearClipSegs(portal->start, portal->end);
+			// Hack in the top/bottom clip values for the window
+			// that were previously stored.
+			Portal_ClipApply(portal);
 
-		R_PortalRestoreClipValues(portal->start, portal->end, portal->ceilingclip, portal->floorclip, portal->frontscale);
+			validcount++;
 
-		validcount++;
+			masks = realloc(masks, (++nummasks)*sizeof(maskcount_t));
 
-		R_RenderBSPNode((INT32)numnodes - 1);
-		R_ClipSprites();
-		//R_DrawPlanes();
-		//R_DrawMasked();
+			Mask_Pre(&masks[nummasks - 1]);
+			curdrawsegs = ds_p;
 
-		// okay done. free it.
-		portalcullsector = NULL; // Just in case...
-		portal_base = portal->next;
-		Z_Free(portal->ceilingclip);
-		Z_Free(portal->floorclip);
-		Z_Free(portal->frontscale);
-		Z_Free(portal);
+			// Render the BSP from the new viewpoint, and clip
+			// any sprites with the new clipsegs and window.
+			R_RenderBSPNode((INT32)numnodes - 1);
+			Mask_Post(&masks[nummasks - 1]);
+
+			R_ClipSprites(ds_p - (masks[nummasks - 1].drawsegs[1] - masks[nummasks - 1].drawsegs[0]), portal);
+
+			Portal_Remove(portal);
+		}
 	}
-	// END PORTAL RENDERING
 
 	R_DrawPlanes();
 #ifdef FLOORSPLATS
 	R_DrawVisibleFloorSplats();
 #endif
+
 	// draw mid texture and sprite
 	// And now 3D floors/sides!
-	R_DrawMasked();
+	R_DrawMasked(masks, nummasks);
 
-	// Check for new console commands.
-	NetUpdate();
-
-	// save value to skyVisible1 or skyVisible2
-	// this is so that P1 can't affect whether P2 can see a skybox or not, or vice versa
-	if (splitscreen && player == &players[secondarydisplayplayer])
-		skyVisible2 = skyVisible;
-	else
-		skyVisible1 = skyVisible;
+	free(masks);
 }
 
 // =========================================================================
diff --git a/src/r_main.h b/src/r_main.h
index 6ae5aa221667569b4b59987b37c4002219958f0c..1d82a01b961bbcd91952abd8a8191fcc4f5dd310 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -94,7 +94,7 @@ void R_ExecuteSetViewSize(void);
 
 void R_SkyboxFrame(player_t *player);
 
-void R_SetupFrame(player_t *player, boolean skybox);
+void R_SetupFrame(player_t *player);
 // Called by G_Drawer.
 void R_RenderPlayerView(player_t *player);
 
diff --git a/src/r_plane.c b/src/r_plane.c
index 0ef4c2c05bd9f579af6c5269f91cad68f3c68230..2f6f97240a0418f3608d1901a8406b80716cf10f 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -23,6 +23,8 @@
 #include "r_state.h"
 #include "r_splats.h" // faB(21jan):testing
 #include "r_sky.h"
+#include "r_portal.h"
+
 #include "v_video.h"
 #include "w_wad.h"
 #include "z_zone.h"
@@ -43,9 +45,8 @@
 //#define QUINCUNX
 
 //SoM: 3/23/2000: Use Boom visplane hashing.
-#define MAXVISPLANES 512
 
-static visplane_t *visplanes[MAXVISPLANES];
+visplane_t *visplanes[MAXVISPLANES];
 static visplane_t *freetail;
 static visplane_t **freehead = &freetail;
 
@@ -112,50 +113,6 @@ void R_InitPlanes(void)
 	// FIXME: unused
 }
 
-// R_PortalStoreClipValues
-// Saves clipping values for later. -Red
-void R_PortalStoreClipValues(INT32 start, INT32 end, INT16 *ceil, INT16 *floor, fixed_t *scale)
-{
-	INT32 i;
-	for (i = 0; i < end-start; i++)
-	{
-		*ceil = ceilingclip[start+i];
-		ceil++;
-		*floor = floorclip[start+i];
-		floor++;
-		*scale = frontscale[start+i];
-		scale++;
-	}
-}
-
-// R_PortalRestoreClipValues
-// Inverse of the above. Restores the old value!
-void R_PortalRestoreClipValues(INT32 start, INT32 end, INT16 *ceil, INT16 *floor, fixed_t *scale)
-{
-	INT32 i;
-	for (i = 0; i < end-start; i++)
-	{
-		ceilingclip[start+i] = *ceil;
-		ceil++;
-		floorclip[start+i] = *floor;
-		floor++;
-		frontscale[start+i] = *scale;
-		scale++;
-	}
-
-	// HACKS FOLLOW
-	for (i = 0; i < start; i++)
-	{
-		floorclip[i] = -1;
-		ceilingclip[i] = (INT16)viewheight;
-	}
-	for (i = end; i < vid.width; i++)
-	{
-		floorclip[i] = -1;
-		ceilingclip[i] = (INT16)viewheight;
-	}
-}
-
 //
 // R_MapPlane
 //
@@ -348,6 +305,23 @@ void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 #endif
 }
 
+void R_ClearFFloorClips (void)
+{
+	INT32 i, p;
+
+	// opening / clipping determination
+	for (i = 0; i < viewwidth; i++)
+	{
+		for (p = 0; p < MAXFFLOORS; p++)
+		{
+			ffloor[p].f_clip[i] = (INT16)viewheight;
+			ffloor[p].c_clip[i] = -1;
+		}
+	}
+
+	numffloors = 0;
+}
+
 //
 // R_ClearPlanes
 // At begining of frame.
@@ -370,8 +344,6 @@ void R_ClearPlanes(void)
 		}
 	}
 
-	numffloors = 0;
-
 	for (i = 0; i < MAXVISPLANES; i++)
 	for (*freehead = visplanes[i], visplanes[i] = NULL;
 		freehead && *freehead ;)
@@ -723,16 +695,6 @@ static void R_DrawSkyPlane(visplane_t *pl)
 	INT32 x;
 	INT32 angle;
 
-	// If we're not supposed to draw the sky (e.g. for skyboxes), don't do anything!
-	// This probably utterly ruins sky rendering for FOFs and polyobjects, unfortunately
-	if (!viewsky)
-	{
-		// Mark that the sky was visible here for next tic
-		// (note: this is a hack and it sometimes can cause HOMs to appear for a tic IIRC)
-		skyVisible = true;
-		return;
-	}
-
 	// Reset column drawer function (note: couldn't we just call walldrawerfunc directly?)
 	// (that is, unless we'll need to switch drawers in future for some reason)
 	wallcolfunc = walldrawerfunc;
diff --git a/src/r_plane.h b/src/r_plane.h
index 6e6a6d49d0522a6aa6b0d9facd35754f5744af14..238fde1827846f0be8278f6ee6a78e21aaf2c52b 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -18,6 +18,8 @@
 #include "r_data.h"
 #include "p_polyobj.h"
 
+#define MAXVISPLANES 512
+
 //
 // Now what is a visplane, anyway?
 // Simple: kinda floor/ceiling polygon optimised for SRB2 rendering.
@@ -53,6 +55,7 @@ typedef struct visplane_s
 #endif
 } visplane_t;
 
+extern visplane_t *visplanes[MAXVISPLANES];
 extern visplane_t *floorplane;
 extern visplane_t *ceilingplane;
 
@@ -72,9 +75,8 @@ extern fixed_t *yslope;
 extern lighttable_t **planezlight;
 
 void R_InitPlanes(void);
-void R_PortalStoreClipValues(INT32 start, INT32 end, INT16 *ceil, INT16 *floor, fixed_t *scale);
-void R_PortalRestoreClipValues(INT32 start, INT32 end, INT16 *ceil, INT16 *floor, fixed_t *scale);
 void R_ClearPlanes(void);
+void R_ClearFFloorClips (void);
 
 void R_MapPlane(INT32 y, INT32 x1, INT32 x2);
 void R_MakeSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2);
@@ -122,4 +124,6 @@ typedef struct planemgr_s
 
 extern visffloor_t ffloor[MAXFFLOORS];
 extern INT32 numffloors;
+
+void Portal_AddSkyboxPortals (void);
 #endif
diff --git a/src/r_portal.c b/src/r_portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..ea24cd91c49b420df663e847cdbebb651a3f36db
--- /dev/null
+++ b/src/r_portal.c
@@ -0,0 +1,333 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2018 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  r_portal.c
+/// \brief Software renderer portals.
+
+#include "r_portal.h"
+#include "r_plane.h"
+#include "r_main.h"
+#include "doomstat.h"
+#include "p_spec.h" // Skybox viewpoints
+#include "z_zone.h"
+#include "r_things.h"
+#include "r_sky.h"
+
+UINT8 portalrender;			/**< When rendering a portal, it establishes the depth of the current BSP traversal. */
+
+// Linked list for portals.
+portal_t *portal_base, *portal_cap;
+
+line_t *portalclipline;
+INT32 portalclipstart, portalclipend;
+
+boolean portalline; // is curline a portal seg?
+
+void Portal_InitList (void)
+{
+	portalrender = 0;
+	portal_base = portal_cap = NULL;
+}
+
+/** Store the clipping window for a portal in its given range.
+ *
+ * The window is copied from the current window at the time
+ * the function is called, so it is useful for converting one-sided
+ * lines into portals.
+ */
+void Portal_ClipRange (portal_t* portal)
+{
+	INT32 start	= portal->start;
+	INT32 end	= portal->end;
+	INT16 *ceil		= portal->ceilingclip;
+	INT16 *floor	= portal->floorclip;
+	fixed_t *scale	= portal->frontscale;
+
+	INT32 i;
+	for (i = 0; i < end-start; i++)
+	{
+		*ceil = ceilingclip[start+i];
+		ceil++;
+		*floor = floorclip[start+i];
+		floor++;
+		*scale = frontscale[start+i];
+		scale++;
+	}
+}
+
+/** Apply the clipping window from a portal.
+ */
+void Portal_ClipApply (const portal_t* portal)
+{
+	INT32 i;
+	INT32 start	= portal->start;
+	INT32 end	= portal->end;
+	INT16 *ceil		= portal->ceilingclip;
+	INT16 *floor	= portal->floorclip;
+	fixed_t *scale	= portal->frontscale;
+
+	for (i = 0; i < end-start; i++)
+	{
+		ceilingclip[start+i] = *ceil;
+		ceil++;
+		floorclip[start+i] = *floor;
+		floor++;
+		frontscale[start+i] = *scale;
+		scale++;
+	}
+
+	// HACKS FOLLOW
+	for (i = 0; i < start; i++)
+	{
+		floorclip[i] = -1;
+		ceilingclip[i] = (INT16)viewheight;
+	}
+	for (i = end; i < vid.width; i++)
+	{
+		floorclip[i] = -1;
+		ceilingclip[i] = (INT16)viewheight;
+	}
+}
+
+static portal_t* Portal_Add (const INT16 x1, const INT16 x2)
+{
+	portal_t *portal		= Z_Malloc(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);
+
+	// Linked list.
+	if (!portal_base)
+	{
+		portal_base	= portal;
+		portal_cap	= portal;
+	}
+	else
+	{
+		portal_cap->next = portal;
+		portal_cap = portal;
+	}
+	portal->next = NULL;
+
+	// Store clipping values so they can be restored once the portal is rendered.
+	portal->ceilingclip	= ceilingclipsave;
+	portal->floorclip	= floorclipsave;
+	portal->frontscale	= frontscalesave;
+	portal->start	= x1;
+	portal->end		= x2;
+
+	// Increase recursion level.
+	portal->pass = portalrender+1;
+
+	return portal;
+}
+
+void Portal_Remove (portal_t* portal)
+{
+	portal_base = portal->next;
+	Z_Free(portal->ceilingclip);
+	Z_Free(portal->floorclip);
+	Z_Free(portal->frontscale);
+	Z_Free(portal);
+}
+
+/** Creates a portal out of two lines and a determined screen range.
+ *
+ * line1 determines the entrance, and line2 the exit.
+ * x1 and x2 determine the screen's column bounds.
+
+ * The view's offset from the entry line center is obtained,
+ * and then rotated&translated to the exit line's center.
+ * When the portal renders, it will create the illusion of
+ * the two lines being seamed together.
+ */
+void Portal_Add2Lines (const INT32 line1, const INT32 line2, const INT32 x1, const INT32 x2)
+{
+	portal_t* portal = Portal_Add(x1, x2);
+
+	// Offset the portal view by the linedef centers
+	line_t* start	= &lines[line1];
+	line_t* dest	= &lines[line2];
+
+	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;
+
+	vertex_t dest_c, start_c;
+
+	// looking glass center
+	start_c.x = (start->v1->x + start->v2->x) / 2;
+	start_c.y = (start->v1->y + start->v2->y) / 2;
+
+	// other side center
+	dest_c.x = (dest->v1->x + dest->v2->x) / 2;
+	dest_c.y = (dest->v1->y + dest->v2->y) / 2;
+
+	disttopoint = R_PointToDist2(start_c.x, start_c.y, viewx, viewy);
+	angtopoint = R_PointToAngle2(start_c.x, start_c.y, viewx, viewy);
+	angtopoint += dangle;
+
+	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;
+
+	portal->clipline = line2;
+
+	Portal_ClipRange(portal);
+
+	portalline = true; // this tells R_StoreWallRange that curline is a portal seg
+}
+
+/** Store the clipping window for a portal using a visplane.
+ *
+ * Since visplanes top/bottom windows work in an identical way,
+ * it can just be copied almost directly.
+ */
+static void Portal_ClipVisplane (const visplane_t* plane, portal_t* portal)
+{
+	INT16 start	= portal->start;
+	INT16 end	= portal->end;
+	INT32 i;
+
+	for (i = 0; i < end - start; i++)
+	{
+		// Invalid column.
+		if (plane->top[i + start] == 65535)
+		{
+			portal->ceilingclip[i] = -1;
+			portal->floorclip[i] = -1;
+			continue;
+		}
+		portal->ceilingclip[i] = plane->top[i + start] - 1;
+		portal->floorclip[i] = plane->bottom[i + start] + 1;
+		portal->frontscale[i] = INT32_MAX;
+	}
+}
+
+extern INT32 viewwidth;
+
+static boolean TrimVisplaneBounds (const visplane_t* plane, INT16* start, INT16* end)
+{
+	*start = plane->minx;
+	*end = plane->maxx + 1;
+
+	// Visplanes have 1-px pads on their sides (extra columns).
+	// Trim them, else it may render out of bounds.
+	if (*end > viewwidth)
+		*end = viewwidth;
+
+	if (!(*start < *end))
+		return true;
+
+
+	/** Trims a visplane's horizontal gap to match its render area.
+	 *
+	 * Visplanes' minx/maxx may sometimes exceed the area they're
+	 * covering. This merely adjusts the boundaries to the next
+	 * valid area.
+	 */
+
+	while (plane->bottom[*start] == 0 && plane->top[*start] == 65535 && *start < *end)
+	{
+		(*start)++;
+	}
+
+
+	while (plane->bottom[*end - 1] == 0 && plane->top[*start] == 65535 && *end > *start)
+	{
+		(*end)--;
+	}
+
+	return false;
+}
+
+/** Creates a skybox portal out of a visplane.
+ *
+ * Applies the necessary offsets and rotation to give
+ * a depth illusion to the skybox.
+ */
+void Portal_AddSkybox (const visplane_t* plane)
+{
+	INT16 start, end;
+	mapheader_t *mh;
+	portal_t* portal;
+
+	if (TrimVisplaneBounds(plane, &start, &end))
+		return;
+
+	portal = Portal_Add(start, end);
+
+	Portal_ClipVisplane(plane, portal);
+
+	portal->viewx = skyboxmo[0]->x;
+	portal->viewy = skyboxmo[0]->y;
+	portal->viewz = skyboxmo[0]->z;
+	portal->viewangle = viewangle + skyboxmo[0]->angle;
+
+	mh = mapheaderinfo[gamemap-1];
+
+	// If a relative viewpoint exists, offset the viewpoint.
+	if (skyboxmo[1])
+	{
+		fixed_t x = 0, y = 0;
+		angle_t ang = skyboxmo[0]->angle>>ANGLETOFINESHIFT;
+
+		if (mh->skybox_scalex > 0)
+			x = (viewx - skyboxmo[1]->x) / mh->skybox_scalex;
+		else if (mh->skybox_scalex < 0)
+			x = (viewx - skyboxmo[1]->x) * -mh->skybox_scalex;
+
+		if (mh->skybox_scaley > 0)
+			y = (viewy - skyboxmo[1]->y) / mh->skybox_scaley;
+		else if (mh->skybox_scaley < 0)
+			y = (viewy - skyboxmo[1]->y) * -mh->skybox_scaley;
+
+		// Apply transform to account for the skybox viewport angle.
+		portal->viewx += FixedMul(x,FINECOSINE(ang)) - FixedMul(y,  FINESINE(ang));
+		portal->viewy += FixedMul(x,  FINESINE(ang)) + FixedMul(y,FINECOSINE(ang));
+	}
+
+	if (mh->skybox_scalez > 0)
+		portal->viewz += viewz / mh->skybox_scalez;
+	else if (mh->skybox_scalez < 0)
+		portal->viewz += viewz * -mh->skybox_scalez;
+
+	portal->clipline = -1;
+}
+
+/** Creates portals for the currently existing sky visplanes.
+ * The visplanes are also removed and cleared from the list.
+ */
+void Portal_AddSkyboxPortals (void)
+{
+	visplane_t *pl;
+	INT32 i;
+	UINT16 count = 0;
+
+	for (i = 0; i < MAXVISPLANES; i++, pl++)
+	{
+		for (pl = visplanes[i]; pl; pl = pl->next)
+		{
+			if (pl->picnum == skyflatnum)
+			{
+				Portal_AddSkybox(pl);
+
+				pl->minx = 0;
+				pl->maxx = -1;
+
+				count++;
+			}
+		}
+	}
+
+	CONS_Debug(DBG_RENDER, "Skybox portals: %d\n", count);
+}
diff --git a/src/r_portal.h b/src/r_portal.h
new file mode 100644
index 0000000000000000000000000000000000000000..e8f9119e84a6eddc41e2bf651cf1ac408772bc2d
--- /dev/null
+++ b/src/r_portal.h
@@ -0,0 +1,59 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2018 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  r_portal.h
+/// \brief Software renderer portal struct, functions, linked list extern.
+
+#ifndef __R_PORTAL__
+#define __R_PORTAL__
+
+#include "r_data.h"
+#include "r_plane.h" // visplanes
+
+/** Portal structure for the software renderer.
+ */
+typedef struct portal_s
+{
+	struct portal_s *next;
+
+	// Viewport.
+	fixed_t viewx;
+	fixed_t viewy;
+	fixed_t viewz;
+	angle_t viewangle;
+
+	UINT8 pass;			/**< Keeps track of the portal's recursion depth. */
+	INT32 clipline;		/**< Optional clipline for line-based portals. */
+
+	// Clipping information.
+	INT32 start;		/**< First horizontal pixel coordinate to draw at. */
+	INT32 end;			/**< Last horizontal pixel coordinate to draw at. */
+	INT16 *ceilingclip; /**< Temporary screen top clipping array. */
+	INT16 *floorclip;	/**< Temporary screen bottom clipping array. */
+	fixed_t *frontscale;/**< Temporary screen bottom clipping array. */
+} portal_t;
+
+extern portal_t* portal_base;
+extern portal_t* portal_cap;
+extern UINT8 portalrender;
+
+extern line_t *portalclipline;
+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_AddSkybox	(const visplane_t* plane);
+
+void Portal_ClipRange (portal_t* portal);
+void Portal_ClipApply (const portal_t* portal);
+
+void Portal_AddSkyboxPortals (void);
+#endif
diff --git a/src/r_segs.c b/src/r_segs.c
index 03c5fb6e53fd0cc370e441bdcd0ea5c953c14823..6eb81ce7a4a64a78a3cda0765bd64149fc0eb280 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -15,6 +15,7 @@
 #include "r_local.h"
 #include "r_sky.h"
 
+#include "r_portal.h"
 #include "r_splats.h"
 
 #include "w_wad.h"
@@ -1737,6 +1738,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 	if (ds_p == drawsegs+maxdrawsegs)
 	{
+		size_t curpos = curdrawsegs - drawsegs;
 		size_t pos = ds_p - drawsegs;
 		size_t newmax = maxdrawsegs ? maxdrawsegs*2 : 128;
 		if (firstseg)
@@ -1744,6 +1746,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		drawsegs = Z_Realloc(drawsegs, newmax*sizeof (*drawsegs), PU_STATIC, NULL);
 		ds_p = drawsegs + pos;
 		maxdrawsegs = newmax;
+		curdrawsegs = drawsegs + curpos;
 		if (firstseg)
 			firstseg = drawsegs + (size_t)firstseg;
 	}
diff --git a/src/r_state.h b/src/r_state.h
index 9c8ce51d6841fd487b97e10a31be682c9893fcc2..da9425bdf0405160810e7102a4d53ce1241eb881 100644
--- a/src/r_state.h
+++ b/src/r_state.h
@@ -80,14 +80,8 @@ extern side_t *sides;
 //
 extern fixed_t viewx, viewy, viewz;
 extern angle_t viewangle, aimingangle;
-extern boolean viewsky, skyVisible;
-extern boolean skyVisible1, skyVisible2; // saved values of skyVisible for P1 and P2, for splitscreen
 extern sector_t *viewsector;
 extern player_t *viewplayer;
-extern UINT8 portalrender;
-extern sector_t *portalcullsector;
-extern line_t *portalclipline;
-extern INT32 portalclipstart, portalclipend;
 
 extern consvar_t cv_allowmlook;
 extern consvar_t cv_maxportals;
diff --git a/src/r_things.c b/src/r_things.c
index e8d679b5397704fb3a5c95d9e805ae2c43247e4d..f5482683fd8c312a6dbce63c92c721ef55687521 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -24,6 +24,7 @@
 #include "i_video.h" // rendermode
 #include "r_things.h"
 #include "r_plane.h"
+#include "r_portal.h"
 #include "p_tick.h"
 #include "p_local.h"
 #include "p_slopes.h"
@@ -444,7 +445,7 @@ void R_AddSpriteDefs(UINT16 wadnum)
 //
 // GAME FUNCTIONS
 //
-static UINT32 visspritecount;
+UINT32 visspritecount;
 static UINT32 clippedvissprites;
 static vissprite_t *visspritechunks[MAXVISSPRITES >> VISSPRITECHUNKBITS] = {NULL};
 
@@ -1249,7 +1250,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	}
 
 	// PORTAL SPRITE CLIPPING
-	if (portalrender)
+	if (portalrender && portalclipline)
 	{
 		if (x2 < portalclipstart || x1 > portalclipend)
 			return;
@@ -1351,8 +1352,8 @@ static void R_ProjectSprite(mobj_t *thing)
 	{
 		if (vis->x1 < portalclipstart)
 			vis->x1 = portalclipstart;
-		if (vis->x2 > portalclipend)
-			vis->x2 = portalclipend;
+		if (vis->x2 >= portalclipend)
+			vis->x2 = portalclipend-1;
 	}
 
 	vis->xscale = xscale; //SoM: 4/17/2000
@@ -1517,7 +1518,7 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 		return;
 
 	// PORTAL SPRITE CLIPPING
-	if (portalrender)
+	if (portalrender && portalclipline)
 	{
 		if (x2 < portalclipstart || x1 > portalclipend)
 			return;
@@ -1569,8 +1570,8 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 	{
 		if (vis->x1 < portalclipstart)
 			vis->x1 = portalclipstart;
-		if (vis->x2 > portalclipend)
-			vis->x2 = portalclipend;
+		if (vis->x2 >= portalclipend)
+			vis->x2 = portalclipend-1;
 	}
 
 	vis->xscale = xscale; //SoM: 4/17/2000
@@ -1696,9 +1697,7 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel)
 //
 // R_SortVisSprites
 //
-static vissprite_t vsprsortedhead;
-
-void R_SortVisSprites(void)
+static void R_SortVisSprites(vissprite_t* vsprsortedhead, UINT32 start, UINT32 end)
 {
 	UINT32       i, linkedvissprites = 0;
 	vissprite_t *ds, *dsprev, *dsnext, *dsfirst;
@@ -1707,20 +1706,17 @@ void R_SortVisSprites(void)
 	fixed_t      bestscale;
 	INT32        bestdispoffset;
 
-	if (!visspritecount)
-		return;
-
 	unsorted.next = unsorted.prev = &unsorted;
 
-	dsfirst = R_GetVisSprite(0);
+	dsfirst = R_GetVisSprite(start);
 
 	// The first's prev and last's next will be set to
 	// nonsense, but are fixed in a moment
-	for (i = 0, dsnext = dsfirst, ds = NULL; i < visspritecount; i++)
+	for (i = start, dsnext = dsfirst, ds = NULL; i < end; i++)
 	{
 		dsprev = ds;
 		ds = dsnext;
-		if (i < visspritecount - 1) dsnext = R_GetVisSprite(i + 1);
+		if (i < end - 1) dsnext = R_GetVisSprite(i + 1);
 
 		ds->next = dsnext;
 		ds->prev = dsprev;
@@ -1798,8 +1794,8 @@ void R_SortVisSprites(void)
 	}
 
 	// pull the vissprites out by scale
-	vsprsortedhead.next = vsprsortedhead.prev = &vsprsortedhead;
-	for (i = 0; i < visspritecount-linkedvissprites; i++)
+	vsprsortedhead->next = vsprsortedhead->prev = vsprsortedhead;
+	for (i = start; i < end-linkedvissprites; i++)
 	{
 		bestscale = bestdispoffset = INT32_MAX;
 		for (ds = unsorted.next; ds != &unsorted; ds = ds->next)
@@ -1824,10 +1820,10 @@ void R_SortVisSprites(void)
 		}
 		best->next->prev = best->prev;
 		best->prev->next = best->next;
-		best->next = &vsprsortedhead;
-		best->prev = vsprsortedhead.prev;
-		vsprsortedhead.prev->next = best;
-		vsprsortedhead.prev = best;
+		best->next = vsprsortedhead;
+		best->prev = vsprsortedhead->prev;
+		vsprsortedhead->prev->next = best;
+		vsprsortedhead->prev = best;
 	}
 }
 
@@ -1837,28 +1833,28 @@ void R_SortVisSprites(void)
 static drawnode_t *R_CreateDrawNode(drawnode_t *link);
 
 static drawnode_t nodebankhead;
-static drawnode_t nodehead;
 
-static void R_CreateDrawNodes(void)
+static void R_CreateDrawNodes(maskcount_t* mask, drawnode_t* head, boolean tempskip)
 {
 	drawnode_t *entry;
 	drawseg_t *ds;
 	INT32 i, p, best, x1, x2;
 	fixed_t bestdelta, delta;
 	vissprite_t *rover;
+	static vissprite_t vsprsortedhead;
 	drawnode_t *r2;
 	visplane_t *plane;
 	INT32 sintersect;
 	fixed_t scale = 0;
 
 	// Add the 3D floors, thicksides, and masked textures...
-	for (ds = ds_p; ds-- > drawsegs ;)
+	for (ds = drawsegs + mask->drawsegs[1]; ds-- > drawsegs + mask->drawsegs[0];)
 	{
 		if (ds->numthicksides)
 		{
 			for (i = 0; i < ds->numthicksides; i++)
 			{
-				entry = R_CreateDrawNode(&nodehead);
+				entry = R_CreateDrawNode(head);
 				entry->thickseg = ds;
 				entry->ffloor = ds->thicksides[i];
 			}
@@ -1873,7 +1869,7 @@ static void R_CreateDrawNodes(void)
 				;
 			else {
 				// Put it in!
-				entry = R_CreateDrawNode(&nodehead);
+				entry = R_CreateDrawNode(head);
 				entry->plane = plane;
 				entry->seg = ds;
 			}
@@ -1882,7 +1878,7 @@ static void R_CreateDrawNodes(void)
 #endif
 		if (ds->maskedtexturecol)
 		{
-			entry = R_CreateDrawNode(&nodehead);
+			entry = R_CreateDrawNode(head);
 			entry->seg = ds;
 		}
 		if (ds->numffloorplanes)
@@ -1913,7 +1909,7 @@ static void R_CreateDrawNodes(void)
 				}
 				if (best != -1)
 				{
-					entry = R_CreateDrawNode(&nodehead);
+					entry = R_CreateDrawNode(head);
 					entry->plane = ds->ffloorplanes[best];
 					entry->seg = ds;
 					ds->ffloorplanes[best] = NULL;
@@ -1924,6 +1920,9 @@ static void R_CreateDrawNodes(void)
 		}
 	}
 
+	if (tempskip)
+		return;
+
 #ifdef POLYOBJECTS_PLANES
 	// find all the remaining polyobject planes and add them on the end of the list
 	// probably this is a terrible idea if we wanted them to be sorted properly
@@ -1940,17 +1939,19 @@ static void R_CreateDrawNodes(void)
 			PolyObjects[i].visplane = NULL;
 			continue;
 		}
-		entry = R_CreateDrawNode(&nodehead);
+		entry = R_CreateDrawNode(head);
 		entry->plane = plane;
 		// note: no seg is set, for what should be obvious reasons
 		PolyObjects[i].visplane = NULL;
 	}
 #endif
 
-	if (visspritecount == 0)
+	// No vissprites in this mask?
+	if (mask->vissprites[1] - mask->vissprites[0] == 0)
 		return;
 
-	R_SortVisSprites();
+	R_SortVisSprites(&vsprsortedhead, mask->vissprites[0], mask->vissprites[1]);
+
 	for (rover = vsprsortedhead.prev; rover != &vsprsortedhead; rover = rover->prev)
 	{
 		if (rover->szt > vid.height || rover->sz < 0)
@@ -1958,7 +1959,7 @@ static void R_CreateDrawNodes(void)
 
 		sintersect = (rover->x1 + rover->x2) / 2;
 
-		for (r2 = nodehead.next; r2 != &nodehead; r2 = r2->next)
+		for (r2 = head->next; r2 != head; r2 = r2->next)
 		{
 			if (r2->plane)
 			{
@@ -2097,9 +2098,9 @@ static void R_CreateDrawNodes(void)
 				}
 			}
 		}
-		if (r2 == &nodehead)
+		if (r2 == head)
 		{
-			entry = R_CreateDrawNode(&nodehead);
+			entry = R_CreateDrawNode(head);
 			entry->sprite = rover;
 		}
 	}
@@ -2141,25 +2142,24 @@ static void R_DoneWithNode(drawnode_t *node)
 	(node->prev = &nodebankhead)->next = node;
 }
 
-static void R_ClearDrawNodes(void)
+static void R_ClearDrawNodes(drawnode_t* head)
 {
 	drawnode_t *rover;
 	drawnode_t *next;
 
-	for (rover = nodehead.next; rover != &nodehead ;)
+	for (rover = head->next; rover != head;)
 	{
 		next = rover->next;
 		R_DoneWithNode(rover);
 		rover = next;
 	}
 
-	nodehead.next = nodehead.prev = &nodehead;
+	head->next = head->prev = head;
 }
 
 void R_InitDrawNodes(void)
 {
 	nodebankhead.next = nodebankhead.prev = &nodebankhead;
-	nodehead.next = nodehead.prev = &nodehead;
 }
 
 //
@@ -2185,7 +2185,7 @@ static void R_DrawPrecipitationSprite(vissprite_t *spr)
 
 // R_ClipSprites
 // Clips vissprites without drawing, so that portals can work. -Red
-void R_ClipSprites(void)
+void R_ClipSprites(drawseg_t* dsstart, portal_t* portal)
 {
 	vissprite_t *spr;
 	for (; clippedvissprites < visspritecount; clippedvissprites++)
@@ -2211,7 +2211,7 @@ void R_ClipSprites(void)
 		// and buggy, by going past LEFT end of array:
 
 		//    for (ds = ds_p-1; ds >= drawsegs; ds--)    old buggy code
-		for (ds = ds_p; ds-- > drawsegs ;)
+		for (ds = ds_p; ds-- > dsstart;)
 		{
 			// determine if the drawseg obscures the sprite
 			if (ds->x1 > spr->x2 ||
@@ -2223,33 +2223,36 @@ void R_ClipSprites(void)
 				continue;
 			}
 
-			if (ds->portalpass > 0 && ds->portalpass <= portalrender)
-				continue; // is a portal
+			if (ds->portalpass != 66)
+			{
+				if (ds->portalpass > 0 && ds->portalpass <= portalrender)
+					continue; // is a portal
 
-			r1 = ds->x1 < spr->x1 ? spr->x1 : ds->x1;
-			r2 = ds->x2 > spr->x2 ? spr->x2 : ds->x2;
+				if (ds->scale1 > ds->scale2)
+				{
+					lowscale = ds->scale2;
+					scale = ds->scale1;
+				}
+				else
+				{
+					lowscale = ds->scale1;
+					scale = ds->scale2;
+				}
 
-			if (ds->scale1 > ds->scale2)
-			{
-				lowscale = ds->scale2;
-				scale = ds->scale1;
-			}
-			else
-			{
-				lowscale = ds->scale1;
-				scale = ds->scale2;
+				if (scale < spr->sortscale ||
+					(lowscale < spr->sortscale &&
+					 !R_PointOnSegSide (spr->gx, spr->gy, ds->curline)))
+				{
+					// masked mid texture?
+					/*if (ds->maskedtexturecol)
+						R_RenderMaskedSegRange (ds, r1, r2);*/
+					// seg is behind sprite
+					continue;
+				}
 			}
 
-			if (scale < spr->sortscale ||
-			    (lowscale < spr->sortscale &&
-			     !R_PointOnSegSide (spr->gx, spr->gy, ds->curline)))
-			{
-				// masked mid texture?
-				/*if (ds->maskedtexturecol)
-					R_RenderMaskedSegRange (ds, r1, r2);*/
-				// seg is behind sprite
-				continue;
-			}
+			r1 = ds->x1 < spr->x1 ? spr->x1 : ds->x1;
+			r2 = ds->x2 > spr->x2 ? spr->x2 : ds->x2;
 
 			// clip this piece of the sprite
 			silhouette = ds->silhouette;
@@ -2367,20 +2370,29 @@ void R_ClipSprites(void)
 				//Fab : 26-04-98: was -1, now clips against console bottom
 				spr->cliptop[x] = (INT16)con_clipviewtop;
 		}
+
+		if (portal)
+		{
+			for (x = spr->x1; x <= spr->x2; x++)
+			{
+				if (spr->clipbot[x] > portal->floorclip[x - portal->start])
+					spr->clipbot[x] = portal->floorclip[x - portal->start];
+				if (spr->cliptop[x] < portal->ceilingclip[x - portal->start])
+					spr->cliptop[x] = portal->ceilingclip[x - portal->start];
+			}
+		}
 	}
 }
 
 //
 // R_DrawMasked
 //
-void R_DrawMasked(void)
+static void R_DrawMaskedList (drawnode_t* head)
 {
 	drawnode_t *r2;
 	drawnode_t *next;
 
-	R_CreateDrawNodes();
-
-	for (r2 = nodehead.next; r2 != &nodehead; r2 = r2->next)
+	for (r2 = head->next; r2 != head; r2 = r2->next)
 	{
 		if (r2->plane)
 		{
@@ -2432,7 +2444,38 @@ void R_DrawMasked(void)
 			r2 = next;
 		}
 	}
-	R_ClearDrawNodes();
+}
+
+void R_DrawMasked(maskcount_t* masks, UINT8 nummasks)
+{
+	drawnode_t heads[nummasks];	/**< Drawnode lists; as many as number of views/portals. */
+	INT8 i;
+
+	for (i = 0; i < nummasks; i++)
+	{
+		heads[i].next = heads[i].prev = &heads[i];
+
+		viewx = masks[i].viewx;
+		viewy = masks[i].viewy;
+		viewz = masks[i].viewz;
+		viewsector = masks[i].viewsector;
+
+		R_CreateDrawNodes(&masks[i], &heads[i], false);
+	}
+
+	//for (i = 0; i < nummasks; i++)
+	//	CONS_Printf("Mask no.%d:\ndrawsegs: %d\n vissprites: %d\n\n", i, masks[i].drawsegs[1] - masks[i].drawsegs[0], masks[i].vissprites[1] - masks[i].vissprites[0]);
+
+	for (; nummasks > 0; nummasks--)
+	{
+		viewx = masks[nummasks - 1].viewx;
+		viewy = masks[nummasks - 1].viewy;
+		viewz = masks[nummasks - 1].viewz;
+		viewsector = masks[nummasks - 1].viewsector;
+
+		R_DrawMaskedList(&heads[nummasks - 1]);
+		R_ClearDrawNodes(&heads[nummasks - 1]);
+	}
 }
 
 // ==========================================================================
diff --git a/src/r_things.h b/src/r_things.h
index 1003103ca94e80a9b9362d767e3ffab194c7ad1a..d287df8328293374265873a9d5e1602a3181f60f 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -16,6 +16,7 @@
 
 #include "sounds.h"
 #include "r_plane.h"
+#include "r_portal.h"
 
 // "Left" and "Right" character symbols for additional rotation functionality
 #define ROT_L ('L' - '0')
@@ -45,7 +46,6 @@ extern fixed_t windowbottom;
 
 void R_DrawMaskedColumn(column_t *column);
 void R_DrawFlippedMaskedColumn(column_t *column, INT32 texheight);
-void R_SortVisSprites(void);
 
 //faB: find sprites in wadfile, replace existing, add new ones
 //     (only sprites from namelist are added or replaced)
@@ -55,8 +55,21 @@ void R_AddSpriteDefs(UINT16 wadnum);
 void R_AddSprites(sector_t *sec, INT32 lightlevel);
 void R_InitSprites(void);
 void R_ClearSprites(void);
-void R_ClipSprites(void);
-void R_DrawMasked(void);
+void R_ClipSprites(drawseg_t* dsstart, portal_t* portal);
+
+/** Used to count the amount of masked elements
+ * per portal to later group them in separate
+ * drawnode lists.
+ */
+typedef struct
+{
+	size_t drawsegs[2];
+	size_t vissprites[2];
+	fixed_t viewx, viewy, viewz;			/**< View z stored at the time of the BSP traversal for the view/portal. Masked sorting/drawing needs it. */
+	sector_t* viewsector;
+} maskcount_t;
+
+void R_DrawMasked(maskcount_t* masks, UINT8 nummasks);
 
 // -----------
 // SKINS STUFF
@@ -207,6 +220,7 @@ typedef struct drawnode_s
 
 extern INT32 numskins;
 extern skin_t skins[MAXSKINS];
+extern UINT32 visspritecount;
 
 void SetPlayerSkin(INT32 playernum,const char *skinname);
 void SetPlayerSkinByNum(INT32 playernum,INT32 skinnum); // Tails 03-16-2002
diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt
index 7f8f052bab673069732a7b01bee833cb3348eb55..de2157055839476d8599db462d22e2d7cdeb1b20 100644
--- a/src/sdl/CMakeLists.txt
+++ b/src/sdl/CMakeLists.txt
@@ -153,7 +153,7 @@ if(${SDL2_FOUND})
 			${ZLIB_LIBRARIES}
 			${OPENGL_LIBRARIES}
 		)
-		set_target_properties(SRB2SDL2 PROPERTIES OUTPUT_NAME "Sonic Robo Blast 2")
+		set_target_properties(SRB2SDL2 PROPERTIES OUTPUT_NAME "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
 	else()
 		target_link_libraries(SRB2SDL2 PRIVATE
 			${SDL2_LIBRARIES}
@@ -337,10 +337,19 @@ if(${SDL2_FOUND})
 
 
 	# Mac bundle fixup
+	# HACK: THIS IS IMPORTANT! See the escaped \${CMAKE_INSTALL_PREFIX}? This
+	# makes it so that var is evaluated LATER during cpack, not right now!
+	# This fixes the quirk where the bundled libraries don't land in the final package
+	# https://cmake.org/pipermail/cmake/2011-March/043532.html
+	#
+	# HOWEVER: ${CPACK_PACKAGE_DESCRIPTION_SUMMARY} is NOT escaped, because that var
+	# is only available to us at this step. Read the link: ${CMAKE_INSTALL_PREFIX} at
+	# this current step points to the CMAKE build folder, NOT the folder that CPACK uses.
+	# Therefore, it makes sense to escape that var, but not the other.
 	if(${CMAKE_SYSTEM} MATCHES Darwin)
 		install(CODE "
 			include(BundleUtilities)
-			fixup_bundle(\"${CMAKE_INSTALL_PREFIX}/Sonic Robo Blast 2.app\"
+			fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/${CPACK_PACKAGE_DESCRIPTION_SUMMARY}.app\"
 				\"\"
 				/Library/Frameworks
 			)"
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index e43772179562d95f87de7cf5d3b1734840288e4d..ee5df4dcb1f3cfa706b579f0111e02c9c0b78985 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -93,6 +93,7 @@
     <Import Project="..\..\libs\libpng.props" />
     <Import Project="..\..\libs\SDL2.props" />
     <Import Project="..\..\libs\SDL_mixer.props" />
+    <Import Project="..\..\libs\libgme.props" />
     <Import Project="Srb2SDL.props" />
   </ImportGroup>
   <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
diff --git a/src/sdl/Srb2SDL.props b/src/sdl/Srb2SDL.props
index 260f81eed79d762e0f079810d30ea0e0d35d4dcb..75839a5b2cb9c4efaf589ffa98845f1fd56aaf76 100644
--- a/src/sdl/Srb2SDL.props
+++ b/src/sdl/Srb2SDL.props
@@ -5,7 +5,10 @@
   <PropertyGroup />
   <ItemDefinitionGroup>
     <ClCompile>
-      <PreprocessorDefinitions>USE_WGL_SWAP;DIRECTFULLSCREEN;HAVE_SDL;HWRENDER;HW3SOUND;HAVE_FILTER;HAVE_MIXER;SDLMAIN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <!-- x86/x64 defines: has specific libraries that ARM does not -->
+      <PreprocessorDefinitions Condition="'$(Platform)' == 'Win32' OR '$(Platform)' == 'x64'">HAVE_ZLIB;HAVE_LIBGME;USE_WGL_SWAP;DIRECTFULLSCREEN;HAVE_SDL;HWRENDER;HW3SOUND;HAVE_FILTER;HAVE_MIXER;SDLMAIN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <!-- ARM defines -->
+      <PreprocessorDefinitions Condition="'$(Platform)' != 'Win32' AND '$(Platform)' != 'x64'">USE_WGL_SWAP;DIRECTFULLSCREEN;HAVE_SDL;HWRENDER;HW3SOUND;HAVE_FILTER;HAVE_MIXER;SDLMAIN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
   </ItemDefinitionGroup>
   <ItemGroup />
diff --git a/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj b/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
index a8ecbf7f85984eaeb63a1b3254f001da6c0d0f32..878db3d8de37b4a8cfb19a8a95dcfaa99559ceda 100644
--- a/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
+++ b/src/sdl/macosx/Srb2mac.xcodeproj/project.pbxproj
@@ -1219,7 +1219,7 @@
 		C01FCF4B08A954540054247B /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.1.23;
+				CURRENT_PROJECT_VERSION = 2.1.24;
 				GCC_PREPROCESSOR_DEFINITIONS = (
 					"$(inherited)",
 					NORMALSRB2,
@@ -1231,7 +1231,7 @@
 		C01FCF4C08A954540054247B /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				CURRENT_PROJECT_VERSION = 2.1.23;
+				CURRENT_PROJECT_VERSION = 2.1.24;
 				GCC_ENABLE_FIX_AND_CONTINUE = NO;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
 				GCC_PREPROCESSOR_DEFINITIONS = (
diff --git a/src/sounds.h b/src/sounds.h
index 489e2882695c0377d481526b34096325d6a0a53a..47530912133953fafcd166198a5166ab5bbb192a 100644
--- a/src/sounds.h
+++ b/src/sounds.h
@@ -42,7 +42,7 @@ typedef enum
 } skinsound_t;
 
 // free sfx for S_AddSoundFx()
-#define NUMSFXFREESLOTS 800 // Matches SOC Editor.
+#define NUMSFXFREESLOTS 1600 // Matches SOC Editor.
 #define NUMSKINSFXSLOTS (MAXSKINS*NUMSKINSOUNDS)
 
 //
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 9ad04b5ce4c1b17b6728728a9d0d0f00ef59dac8..4509ed849fa0dbd09abb64f84c507ab047c5db97 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -190,7 +190,7 @@ void ST_Ticker(void)
 }
 
 // 0 is default, any others are special palettes.
-static INT32 st_palette = 0;
+INT32 st_palette = 0;
 
 void ST_doPaletteStuff(void)
 {
diff --git a/src/st_stuff.h b/src/st_stuff.h
index e1d8a8a92aae61045d602c9b37cee2b2db9e4337..aca4e60d2949aacc618d27a93c6b6fc3d92b291e 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -58,6 +58,7 @@ boolean ST_SameTeam(player_t *a, player_t *b);
 //--------------------
 
 extern boolean st_overlay; // sb overlay on or off when fullscreen
+extern INT32 st_palette; // 0 is default, any others are special palettes.
 
 extern lumpnum_t st_borderpatchnum;
 // patches, also used in intermission
diff --git a/src/win32/Srb2win-vc10.vcxproj b/src/win32/Srb2win-vc10.vcxproj
index 774ce5cbe8c7560c592f2b6b97e3db7864221347..acab2507a37d995112b6402e7c83ac0d6f0dd57a 100644
--- a/src/win32/Srb2win-vc10.vcxproj
+++ b/src/win32/Srb2win-vc10.vcxproj
@@ -91,6 +91,7 @@
     <Import Project="..\..\libs\FMOD.props" />
     <Import Project="..\..\libs\zlib.props" />
     <Import Project="..\..\libs\libpng.props" />
+    <Import Project="..\..\libs\libgme.props" />
     <Import Project="SRB2Win.props" />
   </ImportGroup>
   <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
diff --git a/src/win32/Srb2win.props b/src/win32/Srb2win.props
index 44a30d50d4f71b3d8942a5730fc0300baa0a9f7c..fa152f0c97aa6894c289eadb388cb7b33fb153d2 100644
--- a/src/win32/Srb2win.props
+++ b/src/win32/Srb2win.props
@@ -5,7 +5,10 @@
   <PropertyGroup />
   <ItemDefinitionGroup>
     <ClCompile>
-      <PreprocessorDefinitions>_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <!-- x86/x64 defines: has specific libraries that ARM does not -->
+      <PreprocessorDefinitions Condition="'$(Platform)' == 'Win32' OR '$(Platform)' == 'x64'">HAVE_ZLIB;HAVE_LIBGME;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <!-- ARM defines -->
+      <PreprocessorDefinitions Condition="'$(Platform)' != 'Win32' AND '$(Platform)' != 'x64'">_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
     <Link />
     <Link>