diff --git a/.travis.yml b/.travis.yml
index b6f8a7aa7a1ff2053d5f58b7ff8729916a4b232b..6d2e8cddfc1d55810123c5da2a083197dc2cec9f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -107,7 +107,7 @@ 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
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e7c4de6167d786e044dd407a64076cd2f36f0463..7995034d326d617022b896cd205199d46a631b69 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,7 @@ 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 1.0.4
+	VERSION 1.1.0
 	LANGUAGES C)
 
 if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR})
diff --git a/appveyor.yml b/appveyor.yml
index 10b658918392895b7b4f9626dfbabf34e54d3e6d..3d46cf6de40a08e7e0325c9bc98e7ad0c344dcd0 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.0.4.{branch}-{build}
+version: 1.1.0.{branch}-{build}
 os: MinGW
 
 environment:
@@ -29,7 +29,7 @@ environment:
  ##############################
  DPL_ENABLED: 0
  DPL_TAG_ENABLED: 0
- DPL_INSTALLER_NAME: srb2kart-v104
+ DPL_INSTALLER_NAME: srb2kart-v110
  # Asset handling is barebones vs. Travis Deployer. We operate on 7z only.
  # Include the README files and the OpenGL batch in the main and patch archives.
  # The x86/x64 archives contain the DLL binaries.
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e93777cbd4bc7a2d14cf2b2f273bef18be61a3a2..2f97c173c64c2851f9273df6b4ff8b112461c706 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -401,7 +401,11 @@ if(${SRB2_CONFIG_HWRENDER})
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_light.c
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_main.c
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2.c
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2load.c
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md3load.c
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_model.c
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_trick.c
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/u_list.c
 	)
 
 	set (SRB2_HWRENDER_HEADERS
@@ -415,6 +419,10 @@ if(${SRB2_CONFIG_HWRENDER})
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_light.h
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_main.h
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2.h
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2load.h
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md3load.h
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_model.h
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/u_list.h
 	)
 
 	set(SRB2_R_OPENGL_SOURCES
diff --git a/src/Makefile b/src/Makefile
index b84a06a49148a4bc203a92bf21d1f8604bf1e2f0..214c2bf7a6a47c288d49b34a4dcbd2637c56a6d5 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -282,7 +282,8 @@ ifndef DC
 endif
 	OPTS+=-DHWRENDER
 	OBJS+=$(OBJDIR)/hw_bsp.o $(OBJDIR)/hw_draw.o $(OBJDIR)/hw_light.o \
-		 $(OBJDIR)/hw_main.o $(OBJDIR)/hw_clip.o $(OBJDIR)/hw_md2.o $(OBJDIR)/hw_cache.o $(OBJDIR)/hw_trick.o
+		 $(OBJDIR)/hw_main.o $(OBJDIR)/hw_clip.o $(OBJDIR)/hw_md2.o $(OBJDIR)/hw_cache.o $(OBJDIR)/hw_trick.o \
+		 $(OBJDIR)/hw_md2load.o $(OBJDIR)/hw_md3load.o $(OBJDIR)/hw_model.o $(OBJDIR)/u_list.o
 endif
 
 ifdef NOHS
@@ -741,16 +742,18 @@ ifdef MINGW
 $(OBJDIR)/r_opengl.o: hardware/r_opengl/r_opengl.c hardware/r_opengl/r_opengl.h \
  doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
  command.h hardware/hw_data.h hardware/hw_glide.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h am_map.h \
- d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
+ hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
+ hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
+ am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
  p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
 	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
 else
 $(OBJDIR)/r_opengl.o: hardware/r_opengl/r_opengl.c hardware/r_opengl/r_opengl.h \
  doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
  command.h hardware/hw_data.h hardware/hw_glide.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h am_map.h \
- d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
+ hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
+ hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
+ am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
  p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
 	$(CC) $(CFLAGS) $(WFLAGS) -I/usr/X11R6/include -c $< -o $@
 endif
@@ -902,24 +905,27 @@ ifndef NOHW
 $(OBJDIR)/r_opengl.o: hardware/r_opengl/r_opengl.c hardware/r_opengl/r_opengl.h \
  doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
  command.h hardware/hw_data.h hardware/hw_glide.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h am_map.h \
- d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
+ hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
+ hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
+ am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
  p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
 	$(CC) $(CFLAGS) $(WFLAGS) -D_WINDOWS -mwindows -c $< -o $@
 
 $(OBJDIR)/ogl_win.o: hardware/r_opengl/ogl_win.c hardware/r_opengl/r_opengl.h \
  doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
  command.h hardware/hw_data.h hardware/hw_glide.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h am_map.h \
- d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
+ hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
+ hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
+ am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
  p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
 	$(CC) $(CFLAGS) $(WFLAGS) -D_WINDOWS -mwindows -c $< -o $@
 
 $(OBJDIR)/r_minigl.o: hardware/r_minigl/r_minigl.c hardware/r_opengl/r_opengl.h \
  doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
  command.h hardware/hw_data.h hardware/hw_glide.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h am_map.h \
- d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
+ hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
+ hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
+ am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
  p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
 	$(CC) $(CFLAGS) $(WFLAGS) -D_WINDOWS -mwindows -c $< -o $@
 endif
diff --git a/src/Makefile.cfg b/src/Makefile.cfg
index a0398154a5c14a80b0e408c1413e4f48597cac74..8402e349f1a3dd6654fbb8a4114912de867814d0 100644
--- a/src/Makefile.cfg
+++ b/src/Makefile.cfg
@@ -222,6 +222,7 @@ endif
 ifdef GCC71
  WFLAGS+=-Wno-error=implicit-fallthrough
  WFLAGS+=-Wno-implicit-fallthrough
+ WFLAGS+=-Wno-error=format-truncation
 endif
 ifdef GCC80
  WFLAGS+=-Wno-error=format-overflow
diff --git a/src/android/i_sound.c b/src/android/i_sound.c
index 2bb304424af8b8dafe3dfc9df46eef877ef4cc89..b5a1c364618fbde5b8142eb74f7541ff2ecd18be 100644
--- a/src/android/i_sound.c
+++ b/src/android/i_sound.c
@@ -96,6 +96,37 @@ boolean I_SetSongSpeed(float speed)
         return false;
 }
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+        return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+        (void)position;
+        return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+        return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -140,3 +171,44 @@ void I_SetMusicVolume(INT32 volume)
 {
         (void)volume;
 }
+
+/// ------------------------
+//  MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+        return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+        (void)ms;
+        return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/b_bot.c b/src/b_bot.c
index b84db288c4f354f3c962df16511c29597d697510..c16976b0732f2063fe297b68885e3be86def6e7c 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -271,13 +271,7 @@ void B_RespawnBot(INT32 playernum)
 	player->powers[pw_nocontrol] = sonic->player->powers[pw_nocontrol];
 
 	P_TeleportMove(tails, x, y, z);
-	if (player->charability == CA_FLY)
-	{
-		P_SetPlayerMobjState(tails, S_KART_STND1); // SRB2kart - was S_PLAY_ABL1
-		tails->player->powers[pw_tailsfly] = (UINT16)-1;
-	}
-	else
-		P_SetPlayerMobjState(tails, S_KART_STND1); // SRB2kart - was S_PLAY_FALL1
+	P_SetPlayerMobjState(tails, S_KART_STND1); // SRB2kart - was S_PLAY_FALL1
 	P_SetScale(tails, sonic->scale);
 	tails->destscale = sonic->destscale;
 }
diff --git a/src/command.c b/src/command.c
index a5d45bc1e4c7c2b8626eff7647f6b2c02e6d2649..3eebe32d1754f94bc6813d6bdd796c683000972d 100644
--- a/src/command.c
+++ b/src/command.c
@@ -50,6 +50,7 @@ static void COM_Exec_f(void);
 static void COM_Wait_f(void);
 static void COM_Help_f(void);
 static void COM_Toggle_f(void);
+static void COM_Add_f(void);
 
 static void CV_EnforceExecVersion(void);
 static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr);
@@ -156,6 +157,20 @@ void COM_BufInsertText(const char *ptext)
 	}
 }
 
+/** Progress the wait timer and flush waiting console commands when ready.
+  */
+void
+COM_BufTicker(void)
+{
+	if (com_wait)
+	{
+		com_wait--;
+		return;
+	}
+
+	COM_BufExecute();
+}
+
 /** Flushes (executes) console commands in the buffer.
   */
 void COM_BufExecute(void)
@@ -165,12 +180,6 @@ void COM_BufExecute(void)
 	char line[1024] = "";
 	INT32 quotes;
 
-	if (com_wait)
-	{
-		com_wait--;
-		return;
-	}
-
 	while (com_text.cursize)
 	{
 		// find a '\n' or; line break
@@ -291,6 +300,7 @@ void COM_Init(void)
 	COM_AddCommand("wait", COM_Wait_f);
 	COM_AddCommand("help", COM_Help_f);
 	COM_AddCommand("toggle", COM_Toggle_f);
+	COM_AddCommand("add", COM_Add_f);
 	RegisterNetXCmd(XD_NETVAR, Got_NetVar);
 }
 
@@ -537,10 +547,41 @@ static void COM_ExecuteString(char *ptext)
 			{
 				CONS_Alert(CONS_WARNING, M_GetText("Alias recursion cycle detected!\n"));
 				recursion = 0;
-				return;
 			}
-			recursion++;
-			COM_BufInsertText(a->value);
+			else
+			{
+				char buf[1024];
+				char *write = buf, *read = a->value, *seek = read;
+
+				while ((seek = strchr(seek, '$')) != NULL)
+				{
+					memcpy(write, read, seek-read);
+					write += seek-read;
+
+					seek++;
+
+					if (*seek >= '1' && *seek <= '9')
+					{
+						if (com_argc > (size_t)(*seek - '0'))
+						{
+							memcpy(write, com_argv[*seek - '0'], strlen(com_argv[*seek - '0']));
+							write += strlen(com_argv[*seek - '0']);
+						}
+						seek++;
+					}
+					else
+					{
+						*write = '$';
+						write++;
+					}
+
+					read = seek;
+				}
+				WRITESTRING(write, read);
+
+				recursion++;
+				COM_BufInsertText(buf);
+			}
 			return;
 		}
 	}
@@ -563,8 +604,6 @@ static void COM_ExecuteString(char *ptext)
 static void COM_Alias_f(void)
 {
 	cmdalias_t *a;
-	char cmd[1024];
-	size_t i, c;
 
 	if (COM_Argc() < 3)
 	{
@@ -577,19 +616,9 @@ static void COM_Alias_f(void)
 	com_alias = a;
 
 	a->name = Z_StrDup(COM_Argv(1));
-
-	// copy the rest of the command line
-	cmd[0] = 0; // start out with a null string
-	c = COM_Argc();
-	for (i = 2; i < c; i++)
-	{
-		strcat(cmd, COM_Argv(i));
-		if (i != c)
-			strcat(cmd, " ");
-	}
-	strcat(cmd, "\n");
-
-	a->value = Z_StrDup(cmd);
+	// Just use arg 2 if it's the only other argument, in case the alias is wrapped in quotes (backward compat, or multiple commands in one string).
+	// Otherwise pull the whole string and seek to the end of the alias name. The strctr is in case the alias is quoted.
+	a->value = Z_StrDup(COM_Argc() == 3 ? COM_Argv(2) : (strchr(COM_Args() + strlen(a->name), ' ') + 1));
 }
 
 /** Prints a line of text to the console.
@@ -855,6 +884,27 @@ static void COM_Toggle_f(void)
 	CV_AddValue(cvar, +1);
 }
 
+/** Command variant of CV_AddValue
+  */
+static void COM_Add_f(void)
+{
+	consvar_t *cvar;
+
+	if (COM_Argc() != 3)
+	{
+		CONS_Printf(M_GetText("Add <cvar_name> <value>: Add to the value of a cvar. Negative values work too!\n"));
+		return;
+	}
+	cvar = CV_FindVar(COM_Argv(1));
+	if (!cvar)
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("%s is not a cvar\n"), COM_Argv(1));
+		return;
+	}
+
+	CV_AddValue(cvar, atoi(COM_Argv(2)));
+}
+
 // =========================================================================
 //                      VARIABLE SIZE BUFFERS
 // =========================================================================
@@ -1356,7 +1406,7 @@ static void Got_NetVar(UINT8 **p, INT32 playernum)
 	Setvalue(cvar, svalue, stealth);
 }
 
-void CV_SaveNetVars(UINT8 **p)
+void CV_SaveNetVars(UINT8 **p, boolean isdemorecording)
 {
 	consvar_t *cvar;
 	UINT8 *count_p = *p;
@@ -1366,10 +1416,32 @@ void CV_SaveNetVars(UINT8 **p)
 	// the client will reset all netvars to default before loading
 	WRITEUINT16(*p, 0x0000);
 	for (cvar = consvar_vars; cvar; cvar = cvar->next)
-		if ((cvar->flags & CV_NETVAR) && !CV_IsSetToDefault(cvar))
+		if (((cvar->flags & CV_NETVAR) && !CV_IsSetToDefault(cvar)) || (isdemorecording && cvar->netid == cv_numlaps.netid))
 		{
 			WRITEUINT16(*p, cvar->netid);
-			WRITESTRING(*p, cvar->string);
+
+			// UGLY HACK: Save proper lap count in net replays
+			if (isdemorecording && cvar->netid == cv_numlaps.netid)
+			{
+				if (cv_basenumlaps.value &&
+					(!(mapheaderinfo[gamemap - 1]->levelflags & LF_SECTIONRACE)
+					|| (mapheaderinfo[gamemap - 1]->numlaps > cv_basenumlaps.value))
+				)
+				{
+					WRITESTRING(*p, cv_basenumlaps.string);
+				}
+				else
+				{
+					char buf[9];
+					sprintf(buf, "%d", mapheaderinfo[gamemap - 1]->numlaps);
+					WRITESTRING(*p, buf);
+				}
+			}
+			else
+			{
+				WRITESTRING(*p, cvar->string);
+			}
+
 			WRITEUINT8(*p, false);
 			++count;
 		}
diff --git a/src/command.h b/src/command.h
index f9d177e2eb914a417933b164b46c252d5a3d5e7f..6b5d513ef1421b956ea92336409857332dc9412b 100644
--- a/src/command.h
+++ b/src/command.h
@@ -45,6 +45,9 @@ void COM_ImmedExecute(const char *ptext);
 // Execute commands in buffer, flush them
 void COM_BufExecute(void);
 
+// As above; and progress the wait timer.
+void COM_BufTicker(void);
+
 // setup command buffer, at game tartup
 void COM_Init(void);
 
@@ -161,7 +164,7 @@ void CV_AddValue(consvar_t *var, INT32 increment);
 void CV_SaveVariables(FILE *f);
 
 // load/save gamesate (load and save option and for network join in game)
-void CV_SaveNetVars(UINT8 **p);
+void CV_SaveNetVars(UINT8 **p, boolean isdemorecording);
 void CV_LoadNetVars(UINT8 **p);
 
 // reset cheat netvars after cheats is deactivated
diff --git a/src/config.h.in b/src/config.h.in
index bd7e7861990dcae1fceab09546a666654b430516..f3f8339f854cbf7bdb300c0560e348f0ce733d84 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -37,7 +37,7 @@
  * Last updated 2015 / 05 / 03 - SRB2 v2.1.15 - srb2.srb
  * Last updated 2018 / 12 / 23 - SRB2 v2.1.22 - patch.dta
  * Last updated 2019 / 01 / 18 - Kart v1.0.2 - Main assets
- * Last updated 2019 / 03 / 11 - Kart v1.0.4 - patch.kart
+ * Last updated 2019 / 05 / 06 - Kart v1.1.0 - patch.kart
  */
 
 // Base SRB2 hashes
@@ -52,7 +52,7 @@
 #define ASSET_HASH_CHARS_KART    "e2c428347dde52858a3dacd29fc5b964"
 #define ASSET_HASH_MAPS_KART     "1335cd064656aedca359cfbb5233ac4a"
 #ifdef USE_PATCH_KART
-#define ASSET_HASH_PATCH_KART    "b5f48e1abccfa47a5745199182e2fef4"
+#define ASSET_HASH_PATCH_KART    "7093231f2c3c1cca1a909a708be85d9a"
 #endif
 
 #endif
diff --git a/src/console.c b/src/console.c
index 44bb03a3de1507484379dd97d07fae2d7cb6f06e..28958089b32cf88f6fe2139b4465ca29266589d4 100644
--- a/src/console.c
+++ b/src/console.c
@@ -96,6 +96,7 @@ static size_t input_len; // length of current line, used to bound cursor and suc
 // protos.
 static void CON_InputInit(void);
 static void CON_RecalcSize(void);
+static void CON_ChangeHeight(void);
 
 static void CONS_hudlines_Change(void);
 static void CONS_backcolor_Change(void);
@@ -467,6 +468,12 @@ static void CON_RecalcSize(void)
 		con_destlines = vid.height;
 	}
 
+	if (con_destlines > 0) // Resize console if already open
+	{
+		CON_ChangeHeight();
+		con_curlines = con_destlines;
+	}
+
 	// check for change of video width
 	if (conw == con_width)
 		return; // didn't change
@@ -516,6 +523,20 @@ static void CON_RecalcSize(void)
 	Z_Free(tmp_buffer);
 }
 
+static void CON_ChangeHeight(void)
+{
+	INT32 minheight = 20 * con_scalefactor;	// 20 = 8+8+4
+
+	// toggle console in
+	con_destlines = (cons_height.value*vid.height)/100;
+	if (con_destlines < minheight)
+		con_destlines = minheight;
+	else if (con_destlines > vid.height)
+		con_destlines = vid.height;
+
+	con_destlines &= ~0x3; // multiple of text row height
+}
+
 // Handles Console moves in/out of screen (per frame)
 //
 static void CON_MoveConsole(void)
@@ -620,16 +641,7 @@ void CON_Ticker(void)
 			CON_ClearHUD();
 		}
 		else
-		{
-			// toggle console in
-			con_destlines = (cons_height.value*vid.height)/100;
-			if (con_destlines < minheight)
-				con_destlines = minheight;
-			else if (con_destlines > vid.height)
-				con_destlines = vid.height;
-
-			con_destlines &= ~0x3; // multiple of text row height
-		}
+			CON_ChangeHeight();
 	}
 
 	// console movement
@@ -1149,6 +1161,7 @@ static void CON_Print(char *msg)
 {
 	size_t l;
 	INT32 controlchars = 0; // for color changing
+	char color = '\x80';  // keep color across lines
 
 	if (msg == NULL)
 		return;
@@ -1174,7 +1187,7 @@ static void CON_Print(char *msg)
 		{
 			if (*msg & 0x80)
 			{
-				con_line[con_cx++] = *(msg++);
+				color = con_line[con_cx++] = *(msg++);
 				controlchars++;
 				continue;
 			}
@@ -1182,12 +1195,14 @@ static void CON_Print(char *msg)
 			{
 				con_cy--;
 				CON_Linefeed();
+				color = '\x80';
 				controlchars = 0;
 			}
 			else if (*msg == '\n') // linefeed
 			{
 				CON_Linefeed();
-				controlchars = 0;
+				con_line[con_cx++] = color;
+				controlchars = 1;
 			}
 			else if (*msg == ' ') // space
 			{
@@ -1195,7 +1210,8 @@ static void CON_Print(char *msg)
 				if (con_cx - controlchars >= con_width-11)
 				{
 					CON_Linefeed();
-					controlchars = 0;
+					con_line[con_cx++] = color;
+					controlchars = 1;
 				}
 			}
 			else if (*msg == '\t')
@@ -1210,7 +1226,8 @@ static void CON_Print(char *msg)
 				if (con_cx - controlchars >= con_width-11)
 				{
 					CON_Linefeed();
-					controlchars = 0;
+					con_line[con_cx++] = color;
+					controlchars = 1;
 				}
 			}
 			msg++;
@@ -1227,7 +1244,8 @@ static void CON_Print(char *msg)
 		if ((con_cx - controlchars) + l > con_width-11)
 		{
 			CON_Linefeed();
-			controlchars = 0;
+			con_line[con_cx++] = color;
+			controlchars = 1;
 		}
 
 		// a word at a time
@@ -1582,8 +1600,7 @@ static void CON_DrawConsole(void)
 	i = con_cy - con_scrollup;
 
 	// skip the last empty line due to the cursor being at the start of a new line
-	if (!con_scrollup && !con_cx)
-		i--;
+	i--;
 
 	i -= (con_curlines - minheight) / charheight;
 
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 7d98a7f1cab54058c1c7096d7b8db814de1e4428..c7e24e821f0106c12f9e3834983568fdeb40b5d5 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -21,6 +21,7 @@
 #include "i_system.h"
 #include "i_video.h"
 #include "d_net.h"
+#include "d_netfil.h" // fileneedednum
 #include "d_main.h"
 #include "d_event.h"
 #include "g_game.h"
@@ -91,12 +92,10 @@ tic_t jointimeout = (3*TICRATE);
 static boolean sendingsavegame[MAXNETNODES]; // Are we sending the savegame?
 static tic_t freezetimeout[MAXNETNODES]; // Until when can this node freeze the server before getting a timeout?
 
-#ifdef NEWPING
 UINT16 pingmeasurecount = 1;
 UINT32 realpingtable[MAXPLAYERS]; //the base table of ping where an average will be sent to everyone.
 UINT32 playerpingtable[MAXPLAYERS]; //table of player latency values.
 tic_t servermaxping = 800;			// server's max ping. Defaults to 800
-#endif
 SINT8 nodetoplayer[MAXNETNODES];
 SINT8 nodetoplayer2[MAXNETNODES]; // say the numplayer for this node if any (splitscreen)
 SINT8 nodetoplayer3[MAXNETNODES]; // say the numplayer for this node if any (splitscreen == 2)
@@ -140,6 +139,7 @@ static UINT8 localtextcmd3[MAXTEXTCMD]; // splitscreen == 2
 static UINT8 localtextcmd4[MAXTEXTCMD]; // splitscreen == 3
 static tic_t neededtic;
 SINT8 servernode = 0; // the number of the server node
+char connectedservername[MAXSERVERNAME];
 /// \brief do we accept new players?
 /// \todo WORK!
 boolean acceptnewnode = true;
@@ -585,21 +585,7 @@ static inline void resynch_write_player(resynch_pak *rsp, const size_t i)
 	rsp->kartspeed = (UINT8)players[i].kartspeed;
 	rsp->kartweight = (UINT8)players[i].kartweight;
 	//
-	rsp->normalspeed = (fixed_t)LONG(players[i].normalspeed);
-	rsp->runspeed = (fixed_t)LONG(players[i].runspeed);
-	rsp->thrustfactor = players[i].thrustfactor;
-	rsp->accelstart = players[i].accelstart;
-	rsp->acceleration = players[i].acceleration;
-	rsp->charability = players[i].charability;
-	rsp->charability2 = players[i].charability2;
 	rsp->charflags = (UINT32)LONG(players[i].charflags);
-	rsp->thokitem = (UINT32)LONG(players[i].thokitem); //mobjtype_t
-	rsp->spinitem = (UINT32)LONG(players[i].spinitem); //mobjtype_t
-	rsp->revitem = (UINT32)LONG(players[i].revitem); //mobjtype_t
-	rsp->actionspd = (fixed_t)LONG(players[i].actionspd);
-	rsp->mindash = (fixed_t)LONG(players[i].mindash);
-	rsp->maxdash = (fixed_t)LONG(players[i].maxdash);
-	rsp->jumpfactor = (fixed_t)LONG(players[i].jumpfactor);
 
 	rsp->speed = (fixed_t)LONG(players[i].speed);
 	rsp->jumping = players[i].jumping;
@@ -722,21 +708,7 @@ static void resynch_read_player(resynch_pak *rsp)
 	players[i].kartspeed = (UINT8)rsp->kartspeed;
 	players[i].kartweight = (UINT8)rsp->kartweight;
 
-	players[i].normalspeed = (fixed_t)LONG(rsp->normalspeed);
-	players[i].runspeed = (fixed_t)LONG(rsp->runspeed);
-	players[i].thrustfactor = rsp->thrustfactor;
-	players[i].accelstart = rsp->accelstart;
-	players[i].acceleration = rsp->acceleration;
-	players[i].charability = rsp->charability;
-	players[i].charability2 = rsp->charability2;
 	players[i].charflags = (UINT32)LONG(rsp->charflags);
-	players[i].thokitem = (UINT32)LONG(rsp->thokitem); //mobjtype_t
-	players[i].spinitem = (UINT32)LONG(rsp->spinitem); //mobjtype_t
-	players[i].revitem = (UINT32)LONG(rsp->revitem); //mobjtype_t
-	players[i].actionspd = (fixed_t)LONG(rsp->actionspd);
-	players[i].mindash = (fixed_t)LONG(rsp->mindash);
-	players[i].maxdash = (fixed_t)LONG(rsp->maxdash);
-	players[i].jumpfactor = (fixed_t)LONG(rsp->jumpfactor);
 
 	players[i].speed = (fixed_t)LONG(rsp->speed);
 	players[i].jumping = rsp->jumping;
@@ -1129,6 +1101,7 @@ typedef enum
 #endif
 	CL_CONNECTED,
 	CL_ABORTED,
+	CL_ASKFULLFILELIST,
 	CL_ASKDOWNLOADFILES,
 	CL_WAITDOWNLOADFILESRESPONSE,
 	CL_CHALLENGE
@@ -1139,6 +1112,7 @@ static void GetPackets(void);
 static cl_mode_t cl_mode = CL_SEARCHING;
 static boolean cl_needsdownload = false;
 
+static UINT16 cl_lastcheckedfilecount = 0;
 static UINT8 cl_challengenum = 0;
 static UINT8 cl_challengequestion[MD5_LEN+1];
 static char cl_challengepassword[65];
@@ -1256,6 +1230,9 @@ static inline void CL_DrawConnectionStatus(void)
 					cltext = M_GetText("Waiting to download game state...");
 				break;
 #endif
+			case CL_ASKFULLFILELIST:
+				cltext = M_GetText("This server has a LOT of files!");
+				break;
 			case CL_ASKJOIN:
 			case CL_WAITJOINRESPONSE:
 				cltext = M_GetText("Requesting to join...");
@@ -1315,6 +1292,14 @@ static inline void CL_DrawConnectionStatus(void)
 }
 #endif
 
+static boolean CL_AskFileList(INT32 firstfile)
+{
+	netbuffer->packettype = PT_TELLFILESNEEDED;
+	netbuffer->u.filesneedednum = firstfile;
+
+	return HSendPacket(servernode, true, 0, sizeof (INT32));
+}
+
 /** Sends a special packet to declare how many players in local
   * Used only in arbitratrenetstart()
   * Sends a PT_CLIENTJOIN packet to the server
@@ -1433,7 +1418,7 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 
 	netbuffer->u.serverinfo.actnum = 0; //mapheaderinfo[gamemap-1]->actnum
 
-	p = PutFileNeeded();
+	p = PutFileNeeded(0);
 
 	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
 }
@@ -1443,8 +1428,14 @@ static void SV_SendPlayerInfo(INT32 node)
 	UINT8 i;
 	netbuffer->packettype = PT_PLAYERINFO;
 
-	for (i = 0; i < MAXPLAYERS; i++)
+	for (i = 0; i < MSCOMPAT_MAXPLAYERS; i++)
 	{
+		if (i >= MAXPLAYERS)
+		{
+			netbuffer->u.playerinfo[i].node = 255;
+			continue;
+		}
+
 		if (!playeringame[i])
 		{
 			netbuffer->u.playerinfo[i].node = 255; // This slot is empty.
@@ -1492,7 +1483,7 @@ static void SV_SendPlayerInfo(INT32 node)
 			netbuffer->u.playerinfo[i].data |= 0x80;
 	}
 
-	HSendPacket(node, false, 0, sizeof(plrinfo) * MAXPLAYERS);
+	HSendPacket(node, false, 0, sizeof(plrinfo) * MSCOMPAT_MAXPLAYERS);
 }
 
 /** Sends a PT_SERVERCFG packet
@@ -1542,7 +1533,7 @@ static boolean SV_SendServerConfig(INT32 node)
 	op = p = netbuffer->u.servercfg.varlengthinputs;
 
 	CV_SavePlayerNames(&p);
-	CV_SaveNetVars(&p);
+	CV_SaveNetVars(&p, false);
 	{
 		const size_t len = sizeof (serverconfig_pak) + (size_t)(p - op);
 
@@ -1721,8 +1712,8 @@ static void CL_LoadReceivedSavegame(void)
 	}
 
 	paused = false;
-	demoplayback = false;
-	titledemo = false;
+	demo.playback = false;
+	demo.title = false;
 	automapactive = false;
 
 	// load a base level
@@ -1904,6 +1895,66 @@ void CL_UpdateServerList(boolean internetsearch, INT32 room)
 
 #endif // ifndef NONET
 
+static boolean CL_FinishedFileList(void)
+{
+	INT32 i;
+	CONS_Printf(M_GetText("Checking files...\n"));
+	i = CL_CheckFiles();
+	if (i == 3) // too many files
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have too many WAD files loaded\n"
+			"to add ones the server is using.\n"
+			"Please restart SRB2Kart before connecting.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 2) // cannot join for some reason
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have WAD files loaded or have\n"
+			"modified the game in some way, and\n"
+			"your file list does not match\n"
+			"the server's file list.\n"
+			"Please restart SRB2Kart before connecting.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 1)
+		cl_mode = CL_ASKJOIN;
+	else
+	{
+		// must download something
+		// can we, though?
+		if (!CL_CheckDownloadable()) // nope!
+		{
+			D_QuitNetGame();
+			CL_Reset();
+			D_StartTitle();
+			M_StartMessage(M_GetText(
+				"You cannot connect to this server\n"
+				"because you cannot download the files\n"
+				"that you are missing from the server.\n\n"
+				"See the console or log file for\n"
+				"more details.\n\n"
+				"Press ESC\n"
+			), NULL, MM_NOTHING);
+			return false;
+		}
+
+		cl_mode = CL_ASKDOWNLOADFILES;
+	}
+	return true;
+}
+
 /** Called by CL_ServerConnectionTicker
   *
   * \param viams ???
@@ -1947,66 +1998,16 @@ static boolean CL_ServerConnectionSearchTicker(boolean viams, tic_t *asksent)
 
 		if (client)
 		{
-			D_ParseFileneeded(serverlist[i].info.fileneedednum,
-				serverlist[i].info.fileneeded);
-			CONS_Printf(M_GetText("Checking files...\n"));
-			i = CL_CheckFiles();
-			if (i == 3) // too many files
+			D_ParseFileneeded(serverlist[i].info.fileneedednum, serverlist[i].info.fileneeded, 0);
+			if (serverlist[i].info.kartvars & SV_LOTSOFADDONS)
 			{
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"You have too many WAD files loaded\n"
-					"to add ones the server is using.\n"
-					"Please restart SRB2Kart before connecting.\n\n"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
-			}
-			else if (i == 2) // cannot join for some reason
-			{
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"You have WAD files loaded or have\n"
-					"modified the game in some way, and\n"
-					"your file list does not match\n"
-					"the server's file list.\n"
-					"Please restart SRB2Kart before connecting.\n\n"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
+				cl_mode = CL_ASKFULLFILELIST;
+				cl_lastcheckedfilecount = 0;
+				return true;
 			}
-			else if (i == 1)
-				cl_mode = CL_ASKJOIN;
-			else
-			{
-				// must download something
-				// can we, though?
-				if (!CL_CheckDownloadable()) // nope!
-				{
-					D_QuitNetGame();
-					CL_Reset();
-					D_StartTitle();
-					M_StartMessage(M_GetText(
-						"You cannot connect to this server\n"
-						"because you cannot download the files\n"
-						"that you are missing from the server.\n\n"
-						"See the console or log file for\n"
-						"more details.\n\n"
-						"Press ESC\n"
-					), NULL, MM_NOTHING);
-					return false;
-				}
 
-				cl_mode = CL_ASKDOWNLOADFILES;
-
-				// no problem if can't send packet, we will retry later
-				//if (CL_SendRequestFile())
-				//	cl_mode = CL_DOWNLOADFILES;
-			}
+			if (!CL_FinishedFileList())
+				return false;
 		}
 		else
 			cl_mode = CL_ASKJOIN; // files need not be checked for the server.
@@ -2057,6 +2058,22 @@ static boolean CL_ServerConnectionTicker(boolean viams, const char *tmpsave, tic
 				return false;
 			break;
 
+		case CL_ASKFULLFILELIST:
+			if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
+			{
+				if (!CL_FinishedFileList())
+					return false;
+			}
+			else if (fileneedednum != cl_lastcheckedfilecount || *asksent + NEWTICRATE < I_GetTime())
+			{
+				if (CL_AskFileList(fileneedednum))
+				{
+					cl_lastcheckedfilecount = fileneedednum;
+					*asksent = I_GetTime();
+				}
+			}
+			break;
+
 		case CL_DOWNLOADFILES:
 			waitmore = false;
 			for (i = 0; i < fileneedednum; i++)
@@ -2298,17 +2315,14 @@ static void CL_ConnectToServer(boolean viams)
 
 	if (i != -1)
 	{
-		INT32 j;
+		UINT8 num = serverlist[i].info.gametype;
 		const char *gametypestr = NULL;
+
+		strncpy(connectedservername, serverlist[i].info.servername, MAXSERVERNAME);
+
 		CONS_Printf(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername);
-		for (j = 0; gametype_cons_t[j].strvalue; j++)
-		{
-			if (gametype_cons_t[j].value == serverlist[i].info.gametype)
-			{
-				gametypestr = gametype_cons_t[j].strvalue;
-				break;
-			}
-		}
+		if (num < NUMGAMETYPES)
+			gametypestr = Gametype_Names[num];
 		if (gametypestr)
 			CONS_Printf(M_GetText("Gametype: %s\n"), gametypestr);
 		CONS_Printf(M_GetText("Version: %d.%d.%u\n"), serverlist[i].info.version/100,
@@ -2345,7 +2359,7 @@ static void CL_ConnectToServer(boolean viams)
 #endif
 	DEBFILE(va("Synchronisation Finished\n"));
 
-	displayplayer = consoleplayer;
+	displayplayers[0] = consoleplayer;
 }
 
 #ifndef NONET
@@ -2517,7 +2531,7 @@ static void Command_connect(void)
 		return;
 	}
 
-	if (Playing() || titledemo)
+	if (Playing() || demo.title)
 	{
 		CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n"));
 		return;
@@ -2609,14 +2623,16 @@ void CL_ClearPlayer(INT32 playernum)
 //
 // Removes a player from the current game
 //
-static void CL_RemovePlayer(INT32 playernum, INT32 reason)
+void CL_RemovePlayer(INT32 playernum, INT32 reason)
 {
 	// Sanity check: exceptional cases (i.e. c-fails) can cause multiple
 	// kick commands to be issued for the same player.
 	if (!playeringame[playernum])
 		return;
 
-	if (server && !demoplayback)
+	demo_extradata[playernum] |= DXD_PLAYSTATE;
+
+	if (server && !demo.playback)
 	{
 		INT32 node = playernode[playernum];
 		//playerpernode[node] = 0; // It'd be better to remove them all at once, but ghosting happened, so continue to let CL_RemovePlayer do it one-by-one
@@ -2691,8 +2707,8 @@ static void CL_RemovePlayer(INT32 playernum, INT32 reason)
 		RemoveAdminPlayer(playernum); // don't stay admin after you're gone
 	}
 
-	if (playernum == displayplayer)
-		displayplayer = consoleplayer; // don't look through someone's view who isn't there
+	if (playernum == displayplayers[0] && !demo.playback)
+		displayplayers[0] = consoleplayer; // don't look through someone's view who isn't there
 
 #ifdef HAVE_BLUA
 	LUA_InvalidatePlayer(&players[playernum]);
@@ -2712,7 +2728,7 @@ void CL_Reset(void)
 		G_StopMetalRecording();
 	if (metalplayback)
 		G_StopMetalDemo();
-	if (demorecording)
+	if (demo.recording)
 		G_CheckDemoStatus();
 
 	// reset client/server code
@@ -3164,12 +3180,10 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			HU_AddChatText(va("\x82*%s has been kicked (Go away)", player_names[pnum]), false);
 			kickreason = KR_KICK;
 			break;
-#ifdef NEWPING
 		case KICK_MSG_PING_HIGH:
 			HU_AddChatText(va("\x82*%s left the game (Broke ping limit)", player_names[pnum]), false);
 			kickreason = KR_PINGLIMIT;
 			break;
-#endif
 		case KICK_MSG_CON_FAIL:
 			HU_AddChatText(va("\x82*%s left the game (Synch Failure)", player_names[pnum]), false);
 			kickreason = KR_SYNCH;
@@ -3242,10 +3256,8 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 		D_StartTitle();
 		if (msg == KICK_MSG_CON_FAIL)
 			M_StartMessage(M_GetText("Server closed connection\n(Synch failure)\nPress ESC\n"), NULL, MM_NOTHING);
-#ifdef NEWPING
 		else if (msg == KICK_MSG_PING_HIGH)
 			M_StartMessage(M_GetText("Server closed connection\n(Broke ping limit)\nPress ESC\n"), NULL, MM_NOTHING);
-#endif
 		else if (msg == KICK_MSG_BANNED)
 			M_StartMessage(M_GetText("You have been banned by the server\n\nPress ESC\n"), NULL, MM_NOTHING);
 		else if (msg == KICK_MSG_CUSTOM_KICK)
@@ -3289,15 +3301,15 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}};
 consvar_t cv_netticbuffer = {"netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
-consvar_t cv_allownewplayer = {"allowjoin", "On", CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL	};
+consvar_t cv_allownewplayer = {"allowjoin", "On", CV_SAVE|CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL	};
 #ifdef VANILLAJOINNEXTROUND
-consvar_t cv_joinnextround = {"joinnextround", "Off", CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL}; /// \todo not done
+consvar_t cv_joinnextround = {"joinnextround", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL}; /// \todo not done
 #endif
 static CV_PossibleValue_t maxplayers_cons_t[] = {{2, "MIN"}, {MAXPLAYERS, "MAX"}, {0, NULL}};
 consvar_t cv_maxplayers = {"maxplayers", "8", CV_SAVE, maxplayers_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 static CV_PossibleValue_t resynchattempts_cons_t[] = {{0, "MIN"}, {20, "MAX"}, {0, NULL}};
 consvar_t cv_resynchattempts = {"resynchattempts", "5", CV_SAVE, resynchattempts_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL	};
-consvar_t cv_blamecfail = {"blamecfail", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL	};
+consvar_t cv_blamecfail = {"blamecfail", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL	};
 
 // max file size to send to a player (in kilobytes)
 static CV_PossibleValue_t maxsend_cons_t[] = {{0, "MIN"}, {51200, "MAX"}, {0, NULL}};
@@ -3340,12 +3352,6 @@ void D_ClientServerInit(void)
 	RegisterNetXCmd(XD_ADDPLAYER, Got_AddPlayer);
 	RegisterNetXCmd(XD_REMOVEPLAYER, Got_RemovePlayer);
 #ifndef NONET
-	CV_RegisterVar(&cv_allownewplayer);
-#ifdef VANILLAJOINNEXTROUND
-	CV_RegisterVar(&cv_joinnextround);
-#endif
-	CV_RegisterVar(&cv_showjoinaddress);
-	CV_RegisterVar(&cv_blamecfail);
 #ifdef DUMPCONSISTENCY
 	CV_RegisterVar(&cv_dumpconsistency);
 #endif
@@ -3512,6 +3518,7 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 {
 	INT16 node, newplayernum;
 	UINT8 splitscreenplayer = 0;
+	UINT8 i;
 
 	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
 	{
@@ -3545,34 +3552,19 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 	if (node == mynode)
 	{
 		playernode[newplayernum] = 0; // for information only
+
 		if (splitscreenplayer)
 		{
-			if (splitscreenplayer == 1)
-			{
-				secondarydisplayplayer = newplayernum;
-				DEBFILE("spawning my brother\n");
-				if (botingame)
-					players[newplayernum].bot = 1;
-				// Same goes for player 2 when relevant
-			}
-			else if (splitscreenplayer == 2)
-			{
-				thirddisplayplayer = newplayernum;
-				DEBFILE("spawning my sister\n");
-			}
-			else if (splitscreenplayer == 3)
-			{
-				fourthdisplayplayer = newplayernum;
-				DEBFILE("spawning my trusty pet dog\n");
-			}
+			displayplayers[splitscreenplayer] = newplayernum;
+			DEBFILE(va("spawning one of my sister number %d\n", splitscreenplayer));
+			if (splitscreenplayer == 1 && botingame)
+				players[newplayernum].bot = 1;
 		}
 		else
 		{
 			consoleplayer = newplayernum;
-			displayplayer = newplayernum;
-			secondarydisplayplayer = newplayernum;
-			thirddisplayplayer = newplayernum;
-			fourthdisplayplayer = newplayernum;
+			for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
+				displayplayers[i] = newplayernum;
 			DEBFILE("spawning me\n");
 		}
 		D_SendPlayerConfig();
@@ -3738,7 +3730,7 @@ boolean Playing(void)
 
 boolean SV_SpawnServer(void)
 {
-	if (demoplayback)
+	if (demo.playback)
 		G_StopDemo(); // reset engine parameter
 	if (metalplayback)
 		G_StopMetalDemo();
@@ -4046,6 +4038,39 @@ static void HandlePacketFromAwayNode(SINT8 node)
 #endif
 			break;
 
+		case PT_TELLFILESNEEDED:
+			if (server && serverrunning)
+			{
+				UINT8 *p;
+				INT32 firstfile = netbuffer->u.filesneedednum;
+
+				netbuffer->packettype = PT_MOREFILESNEEDED;
+				netbuffer->u.filesneededcfg.first = firstfile;
+				netbuffer->u.filesneededcfg.more = 0;
+
+				p = PutFileNeeded(firstfile);
+
+				HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
+			}
+			else // Shouldn't get this if you aren't the server...?
+				Net_CloseConnection(node);
+			break;
+
+		case PT_MOREFILESNEEDED:
+			if (server && serverrunning)
+			{ // But wait I thought I'm the server?
+				Net_CloseConnection(node);
+				break;
+			}
+			SERVERONLY
+			if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
+			{
+				D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
+				if (!netbuffer->u.filesneededcfg.more)
+					cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
+			}
+			break;
+
 		case PT_ASKINFO:
 			if (server && serverrunning)
 			{
@@ -4717,7 +4742,6 @@ FILESTAMP
 			resynch_local_inprogress = true;
 			CL_AcknowledgeResynch(&netbuffer->u.resynchpak);
 			break;
-#ifdef NEWPING
 		case PT_PING:
 			// Only accept PT_PING from the server.
 			if (node != servernode)
@@ -4747,7 +4771,6 @@ FILESTAMP
 			}
 
 			break;
-#endif
 		case PT_SERVERCFG:
 			break;
 		case PT_FILEFRAGMENT:
@@ -5319,16 +5342,16 @@ void TryRunTics(tic_t realtics)
 
 	if (realtics >= 1)
 	{
-		COM_BufExecute();
+		COM_BufTicker();
 		if (mapchangepending)
 			D_MapChange(-1, 0, encoremode, false, 2, false, fromlevelselect); // finish the map change
 	}
 
 	NetUpdate();
 
-	if (demoplayback)
+	if (demo.playback)
 	{
-		neededtic = gametic + (realtics * cv_playbackspeed.value);
+		neededtic = gametic + realtics * (gamestate == GS_LEVEL ? cv_playbackspeed.value : 1);
 		// start a game after a demo
 		maketic += realtics;
 		firstticstosend = maketic;
@@ -5385,7 +5408,6 @@ void TryRunTics(tic_t realtics)
 	}
 }
 
-#ifdef NEWPING
 
 /* 	Ping Update except better:
 	We call this once per second and check for people's pings. If their ping happens to be too high, we increment some timer and kick them out.
@@ -5469,11 +5491,9 @@ static inline void PingUpdate(void)
 
 	pingmeasurecount = 1; //Reset count
 }
-#endif
 
 static tic_t gametime = 0;
 
-#ifdef NEWPING
 static void UpdatePingTable(void)
 {
 	INT32 i;
@@ -5488,7 +5508,6 @@ static void UpdatePingTable(void)
 		pingmeasurecount++;
 	}
 }
-#endif
 
 // Handle timeouts to prevent definitive freezes from happenning
 static void HandleNodeTimeouts(void)
@@ -5513,9 +5532,7 @@ void NetKeepAlive(void)
 	if (realtics <= 0) // nothing new to update
 		return;
 
-#ifdef NEWPING
 	UpdatePingTable();
-#endif
 
 	if (server)
 		CL_SendClientKeepAlive();
@@ -5562,9 +5579,7 @@ void NetUpdate(void)
 
 	gametime = nowtime;
 
-#ifdef NEWPING
 	UpdatePingTable();
-#endif
 
 	if (client)
 		maketic = neededtic;
@@ -5589,7 +5604,7 @@ FILESTAMP
 	}
 	else
 	{
-		if (!demoplayback)
+		if (!demo.playback)
 		{
 			INT32 counts;
 
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index f3a9011eb9d1e9d696f0ba1ddb17178309bb87d5..66d9e73ea4aa46f046b21f46c31f2b6f80a99df7 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -93,9 +93,11 @@ typedef enum
 	PT_NODETIMEOUT,   // Packet sent to self if the connection times out.
 	PT_RESYNCHING,    // Packet sent to resync players.
 	                  // Blocks game advance until synched.
-#ifdef NEWPING
+
+	PT_TELLFILESNEEDED, // Client, to server: "what other files do I need starting from this number?"
+	PT_MOREFILESNEEDED, // Server, to client: "you need these (+ more on top of those)"
+
 	PT_PING,          // Packet sent to tell clients the other client's latency to server.
-#endif
 	NUMPACKETTYPE
 } packettype_t;
 
@@ -224,21 +226,7 @@ typedef struct
 	UINT8 kartspeed;
 	UINT8 kartweight;
 	//
-	fixed_t normalspeed;
-	fixed_t runspeed;
-	UINT8 thrustfactor;
-	UINT8 accelstart;
-	UINT8 acceleration;
-	UINT8 charability;
-	UINT8 charability2;
 	UINT32 charflags;
-	UINT32 thokitem; // mobjtype_t
-	UINT32 spinitem; // mobjtype_t
-	UINT32 revitem; // mobjtype_t
-	fixed_t actionspd;
-	fixed_t mindash;
-	fixed_t maxdash;
-	fixed_t jumpfactor;
 
 	fixed_t speed;
 	UINT8 jumping;
@@ -371,6 +359,7 @@ typedef struct
 } ATTRPACK joinchallenge_pak;
 
 #define SV_SPEEDMASK 0x03
+#define SV_LOTSOFADDONS 0x20
 #define SV_DEDICATED 0x40
 #define SV_PASSWORD 0x80
 
@@ -441,6 +430,14 @@ typedef struct
 	UINT8 ctfteam;
 } ATTRPACK plrconfig;
 
+typedef struct
+{
+	INT32 first;
+	UINT8 num;
+	UINT8 more;
+	UINT8 files[MAXFILENEEDED]; // is filled with writexxx (byteptr.h)
+} ATTRPACK filesneededconfig_pak;
+
 //
 // Network packet data
 //
@@ -471,11 +468,11 @@ typedef struct
 		serverrefuse_pak serverrefuse;      //       65025 bytes (somehow I feel like those values are garbage...)
 		askinfo_pak askinfo;                //          61 bytes
 		msaskinfo_pak msaskinfo;            //          22 bytes
-		plrinfo playerinfo[MAXPLAYERS];     //         576 bytes(?)
+		plrinfo playerinfo[MSCOMPAT_MAXPLAYERS];//         576 bytes(?)
 		plrconfig playerconfig[MAXPLAYERS]; // (up to) 528 bytes(?)
-#ifdef NEWPING
+		INT32 filesneedednum;               //           4 bytes
+		filesneededconfig_pak filesneededcfg; //       ??? bytes
 		UINT32 pingtable[MAXPLAYERS+1];     //          68 bytes
-#endif
 	} u; // This is needed to pack diff packet types data together
 } ATTRPACK doomdata_t;
 
@@ -509,9 +506,7 @@ extern consvar_t cv_playbackspeed;
 #define KICK_MSG_PLAYER_QUIT 3
 #define KICK_MSG_TIMEOUT     4
 #define KICK_MSG_BANNED      5
-#ifdef NEWPING
 #define KICK_MSG_PING_HIGH   6
-#endif
 #define KICK_MSG_CUSTOM_KICK 7
 #define KICK_MSG_CUSTOM_BAN  8
 
@@ -532,16 +527,15 @@ extern boolean dedicated; // For dedicated server
 extern UINT16 software_MAXPACKETLENGTH;
 extern boolean acceptnewnode;
 extern SINT8 servernode;
+extern char connectedservername[MAXSERVERNAME];
 
 void Command_Ping_f(void);
 extern tic_t connectiontimeout;
 extern tic_t jointimeout;
-#ifdef NEWPING
 extern UINT16 pingmeasurecount;
 extern UINT32 realpingtable[MAXPLAYERS];
 extern UINT32 playerpingtable[MAXPLAYERS];
 extern tic_t servermaxping;
-#endif
 
 extern consvar_t
 #ifdef VANILLAJOINNEXTROUND
@@ -573,6 +567,7 @@ void CL_AddSplitscreenPlayer(void);
 void CL_RemoveSplitscreenPlayer(UINT8 p);
 void CL_Reset(void);
 void CL_ClearPlayer(INT32 playernum);
+void CL_RemovePlayer(INT32 playernum, INT32 reason);
 void CL_UpdateServerList(boolean internetsearch, INT32 room);
 boolean CL_Responder(event_t *ev);
 // Is there a game running
diff --git a/src/d_main.c b/src/d_main.c
index 82f3721a5b2724915be2dc45463889e1475afd27..467976c1753c140947b290e57ccae99515383660 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -74,7 +74,7 @@ int	snprintf(char *str, size_t n, const char *fmt, ...);
 #include "m_cond.h" // condition initialization
 #include "fastcmp.h"
 #include "keys.h"
-#include "filesrch.h" // refreshdirmenu, mainwadstally
+#include "filesrch.h" // refreshdirmenu
 
 #ifdef CMAKECONFIG
 #include "config.h"
@@ -118,14 +118,8 @@ boolean devparm = false; // started game with -devparm
 boolean singletics = false; // timedemo
 boolean lastdraw = false;
 
-postimg_t postimgtype = postimg_none;
-INT32 postimgparam;
-postimg_t postimgtype2 = postimg_none;
-INT32 postimgparam2;
-postimg_t postimgtype3 = postimg_none;
-INT32 postimgparam3;
-postimg_t postimgtype4 = postimg_none;
-INT32 postimgparam4;
+postimg_t postimgtype[MAXSPLITSCREENPLAYERS];
+INT32 postimgparam[MAXSPLITSCREENPLAYERS];
 
 // These variables are only true if
 // whether the respective sound system is disabled
@@ -248,6 +242,12 @@ void D_ProcessEvents(void)
 				continue;
 		}
 
+		if (demo.savemode == DSM_TITLEENTRY)
+		{
+			if (G_DemoTitleResponder(ev))
+				continue;
+		}
+
 		// Menu input
 		if (M_Responder(ev))
 			continue; // menu ate the event
@@ -274,6 +274,7 @@ static void D_Display(void)
 	boolean forcerefresh = false;
 	static boolean wipe = false;
 	INT32 wipedefindex = 0;
+	UINT8 i;
 
 	if (dedicated)
 		return;
@@ -422,110 +423,78 @@ static void D_Display(void)
 		// draw the view directly
 		if (cv_renderview.value && !automapactive)
 		{
-			if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
+			for (i = 0; i <= splitscreen; i++)
 			{
-				viewwindowy = 0;
-				viewwindowx = 0;
-
-				topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
-				objectsdrawn = 0;
-#ifdef HWRENDER
-				if (rendermode != render_soft)
-					HWR_RenderPlayerView(0, &players[displayplayer]);
-				else
-#endif
-				if (rendermode != render_none)
-					R_RenderPlayerView(&players[displayplayer]);
-			}
-
-			// render the second screen
-			if (splitscreen && players[secondarydisplayplayer].mo)
-			{
-#ifdef HWRENDER
-				if (rendermode != render_soft)
-					HWR_RenderPlayerView(1, &players[secondarydisplayplayer]);
-				else
-#endif
-				if (rendermode != render_none)
+				if (players[displayplayers[i]].mo || players[displayplayers[i]].playerstate == PST_DEAD)
 				{
-					if (splitscreen > 1)
+					if (i == 0) // Initialize for P1
 					{
-						viewwindowx = viewwidth;
 						viewwindowy = 0;
-					}
-					else
-					{
 						viewwindowx = 0;
-						viewwindowy = viewheight;
-					}
-
-					M_Memcpy(ylookup, ylookup2, viewheight*sizeof (ylookup[0]));
 
-					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
+						topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
+						objectsdrawn = 0;
+					}
 
-					R_RenderPlayerView(&players[secondarydisplayplayer]);
+					viewssnum = i;
 
-					viewwindowy = 0;
-					M_Memcpy(ylookup, ylookup1, viewheight*sizeof (ylookup[0]));
-				}
-			}
-
-			// render the third screen
-			if (splitscreen > 1 && players[thirddisplayplayer].mo)
-			{
 #ifdef HWRENDER
-				if (rendermode != render_soft)
-					HWR_RenderPlayerView(2, &players[thirddisplayplayer]);
-				else
+					if (rendermode != render_soft)
+						HWR_RenderPlayerView(i, &players[displayplayers[i]]);
+					else
 #endif
-				if (rendermode != render_none)
-				{
-					viewwindowx = 0;
-					viewwindowy = viewheight;
-					M_Memcpy(ylookup, ylookup3, viewheight*sizeof (ylookup[0]));
-
-					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
-
-					R_RenderPlayerView(&players[thirddisplayplayer]);
-
-					viewwindowy = 0;
-					M_Memcpy(ylookup, ylookup1, viewheight*sizeof (ylookup[0]));
+					if (rendermode != render_none)
+					{
+						if (i > 0) // Splitscreen-specific
+						{
+							switch (i) 
+							{
+								case 1:
+									if (splitscreen > 1)
+									{
+										viewwindowx = viewwidth;
+										viewwindowy = 0;
+									}
+									else
+									{
+										viewwindowx = 0;
+										viewwindowy = viewheight;
+									}
+									M_Memcpy(ylookup, ylookup2, viewheight*sizeof (ylookup[0]));
+									break;
+								case 2:
+									viewwindowx = 0;
+									viewwindowy = viewheight;
+									M_Memcpy(ylookup, ylookup3, viewheight*sizeof (ylookup[0]));
+									break;
+								case 3:
+									viewwindowx = viewwidth;
+									viewwindowy = viewheight;
+									M_Memcpy(ylookup, ylookup4, viewheight*sizeof (ylookup[0]));
+								default:
+									break;
+							}
+
+							
+							topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
+						}
+
+						R_RenderPlayerView(&players[displayplayers[i]]);
+
+						if (i > 0)
+							M_Memcpy(ylookup, ylookup1, viewheight*sizeof (ylookup[0]));
+					}
 				}
 			}
 
-			if (splitscreen > 2 && players[fourthdisplayplayer].mo) // render the fourth screen
+			if (rendermode == render_soft)
 			{
-#ifdef HWRENDER
-				if (rendermode != render_soft)
-					HWR_RenderPlayerView(3, &players[fourthdisplayplayer]);
-				else
-#endif
-				if (rendermode != render_none)
+				for (i = 0; i <= splitscreen; i++)
 				{
-					viewwindowx = viewwidth;
-					viewwindowy = viewheight;
-					M_Memcpy(ylookup, ylookup4, viewheight*sizeof (ylookup[0]));
-
-					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
-
-					R_RenderPlayerView(&players[fourthdisplayplayer]);
-
-					viewwindowy = 0;
-					M_Memcpy(ylookup, ylookup1, viewheight*sizeof (ylookup[0]));
+					if (postimgtype[i])
+						V_DoPostProcessor(i, postimgtype[i], postimgparam[i]);
 				}
 			}
-
-			if (rendermode == render_soft)
-			{
-				if (postimgtype)
-					V_DoPostProcessor(0, postimgtype, postimgparam);
-				if (postimgtype2)
-					V_DoPostProcessor(1, postimgtype2, postimgparam2);
-				if (postimgtype3)
-					V_DoPostProcessor(2, postimgtype3, postimgparam3);
-				if (postimgtype4)
-					V_DoPostProcessor(3, postimgtype4, postimgparam4);
-			}
 		}
 
 		if (lastdraw)
@@ -550,7 +519,7 @@ static void D_Display(void)
 	wipegamestate = gamestate;
 
 	// draw pause pic
-	if (paused && cv_showhud.value)
+	if (paused && cv_showhud.value && !demo.playback)
 	{
 		INT32 py;
 		patch_t *patch;
@@ -562,6 +531,9 @@ static void D_Display(void)
 		V_DrawScaledPatch(viewwindowx + (BASEVIDWIDTH - SHORT(patch->width))/2, py, 0, patch);
 	}
 
+	if (demo.rewinding)
+		V_DrawFadeScreen(TC_RAINBOW, (leveltime & 0x20) ? SKINCOLOR_PASTEL : SKINCOLOR_MOONSLAM);
+
 	// vid size change is now finished if it was on...
 	vid.recalc = 0;
 
@@ -619,6 +591,9 @@ static void D_Display(void)
 			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-10, V_YELLOWMAP, s);
 		}
 
+		if (cv_shittyscreen.value)
+			V_DrawVhsEffect(cv_shittyscreen.value == 2);
+
 		I_FinishUpdate(); // page flip or blit buffer
 	}
 }
@@ -636,9 +611,6 @@ void D_SRB2Loop(void)
 	if (dedicated)
 		server = true;
 
-	if (M_CheckParm("-voodoo")) // 256x256 Texture Limiter
-		COM_BufAddText("gr_voodoocompatibility on\n");
-
 	// Pushing of + parameters is now done back in D_SRB2Main, not here.
 
 	CONS_Printf("I_StartupKeyboard()...\n");
@@ -735,7 +707,7 @@ void D_SRB2Loop(void)
 				M_DoScreenShot();
 		}
 
-		// consoleplayer -> displayplayer (hear sounds from viewpoint)
+		// consoleplayer -> displayplayers (hear sounds from viewpoint)
 		S_UpdateSounds(); // move positional sounds
 
 		// check for media change, loop music..
@@ -814,13 +786,13 @@ void D_StartTitle(void)
 	maptol = 0;
 
 	gameaction = ga_nothing;
-	displayplayer = consoleplayer = 0;
+	memset(displayplayers, 0, sizeof(displayplayers));
+	consoleplayer = 0;
 	//demosequence = -1;
 	gametype = GT_RACE; // SRB2kart
 	paused = false;
 	advancedemo = false;
 	F_StartTitleScreen();
-	CON_ToggleOff();
 
 	// Reset the palette -- SRB2Kart: actually never mind let's do this in the middle of every fade
 	/*if (rendermode != render_none)
@@ -1217,6 +1189,10 @@ void D_SRB2Main(void)
 	// Setup default unlockable conditions
 	M_SetupDefaultConditionSets();
 
+	// Setup character tables
+	// Have to be done here before files are loaded
+	M_InitCharacterTables();
+
 	// load wad, including the main wad file
 	CONS_Printf("W_InitMultipleFiles(): Adding IWAD and main PWADs.\n");
 	if (!W_InitMultipleFiles(startupwadfiles, false))
@@ -1257,8 +1233,6 @@ void D_SRB2Main(void)
 
 #endif //ifndef DEVELOP
 
-	mainwadstally = packetsizetally;
-
 	//
 	// search for maps
 	//
@@ -1513,7 +1487,7 @@ void D_SRB2Main(void)
 
 		if (M_CheckParm("-playdemo"))
 		{
-			singledemo = true; // quit after one demo
+			demo.quitafterplaying = true; // quit after one demo
 			G_DeferedPlayDemo(tmp);
 		}
 		else
@@ -1540,6 +1514,8 @@ void D_SRB2Main(void)
 		// as having been modified for the first game.
 		M_PushSpecialParameters(); // push all "+" parameter at the command buffer
 
+		strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);
+
 		if (M_CheckParm("-gametype") && M_IsNextParm())
 		{
 			// from Command_Map_f
@@ -1547,13 +1523,9 @@ void D_SRB2Main(void)
 			INT16 newgametype = -1;
 			const char *sgametype = M_GetNextParm();
 
-			for (j = 0; gametype_cons_t[j].strvalue; j++)
-				if (!strcasecmp(gametype_cons_t[j].strvalue, sgametype))
-				{
-					newgametype = (INT16)gametype_cons_t[j].value;
-					break;
-				}
-			if (!gametype_cons_t[j].strvalue) // reached end of the list with no match
+			newgametype = G_GetGametypeByName(sgametype);
+
+			if (newgametype == -1) // reached end of the list with no match
 			{
 				j = atoi(sgametype); // assume they gave us a gametype number, which is okay too
 				if (j >= 0 && j < NUMGAMETYPES)
@@ -1608,13 +1580,13 @@ void D_SRB2Main(void)
 	}
 	else if (M_CheckParm("-skipintro"))
 	{
-		CON_ToggleOff();
-		CON_ClearHUD();
 		F_StartTitleScreen();
 	}
 	else
 		F_StartIntro(); // Tails 03-03-2002
 
+	CON_ToggleOff();
+
 	if (dedicated && server)
 	{
 		pagename = "TITLESKY";
diff --git a/src/d_net.c b/src/d_net.c
index 9f71996738a967a3c642eef41bb336c410e3ff3a..94b11d518a5e7f99f661b8cfb5a29ec6b440c752 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -185,22 +185,10 @@ typedef struct
 	UINT8 nextacknum;
 
 	UINT8 flags;
-#ifndef NEWPING
-	// jacobson tcp timeout evaluation algorithm (Karn variation)
-	fixed_t ping;
-	fixed_t varping;
-	INT32 timeout; // computed with ping and varping
-#endif
 } node_t;
 
 static node_t nodes[MAXNETNODES];
-#ifndef NEWPING
-#define PINGDEFAULT ((200*TICRATE*FRACUNIT)/1000)
-#define VARPINGDEFAULT ((50*TICRATE*FRACUNIT)/1000)
-#define TIMEOUT(p,v) (p+4*v+FRACUNIT/2)>>FRACBITS;
-#else
-#define NODETIMEOUT 14 //What the above boiled down to...
-#endif
+#define NODETIMEOUT 14
 
 #ifndef NONET
 // return <0 if a < b (mod 256)
@@ -320,19 +308,7 @@ static UINT8 GetAcktosend(INT32 node)
 static void RemoveAck(INT32 i)
 {
 	INT32 node = ackpak[i].destinationnode;
-#ifndef NEWPING
-	fixed_t trueping = (I_GetTime() - ackpak[i].senttime)<<FRACBITS;
-	if (ackpak[i].resentnum)
-	{
-		// +FRACUNIT/2 for round
-		nodes[node].ping = (nodes[node].ping*7 + trueping)/8;
-		nodes[node].varping = (nodes[node].varping*7 + abs(nodes[node].ping-trueping))/8;
-		nodes[node].timeout = TIMEOUT(nodes[node].ping,nodes[node].varping);
-	}
-	DEBFILE(va("Remove ack %d trueping %d ping %f var %f timeout %d\n",ackpak[i].acknum,trueping>>FRACBITS,(double)FIXED_TO_FLOAT(nodes[node].ping),(double)FIXED_TO_FLOAT(nodes[node].varping),nodes[node].timeout));
-#else
 	DEBFILE(va("Remove ack %d\n",ackpak[i].acknum));
-#endif
 	ackpak[i].acknum = 0;
 	if (nodes[node].flags & NF_CLOSE)
 		Net_CloseConnection(node);
@@ -519,11 +495,7 @@ void Net_AckTicker(void)
 	{
 		const INT32 nodei = ackpak[i].destinationnode;
 		node_t *node = &nodes[nodei];
-#ifdef NEWPING
 		if (ackpak[i].acknum && ackpak[i].senttime + NODETIMEOUT < I_GetTime())
-#else
-		if (ackpak[i].acknum && ackpak[i].senttime + node->timeout < I_GetTime())
-#endif
 		{
 			if (ackpak[i].resentnum > 10 && (node->flags & NF_CLOSE))
 			{
@@ -534,13 +506,8 @@ void Net_AckTicker(void)
 				ackpak[i].acknum = 0;
 				continue;
 			}
-#ifdef NEWPING
 			DEBFILE(va("Resend ack %d, %u<%d at %u\n", ackpak[i].acknum, ackpak[i].senttime,
 				NODETIMEOUT, I_GetTime()));
-#else
-			DEBFILE(va("Resend ack %d, %u<%d at %u\n", ackpak[i].acknum, ackpak[i].senttime,
-				node->timeout, I_GetTime()));
-#endif
 			M_Memcpy(netbuffer, ackpak[i].pak.raw, ackpak[i].length);
 			ackpak[i].senttime = I_GetTime();
 			ackpak[i].resentnum++;
@@ -658,11 +625,6 @@ void Net_WaitAllAckReceived(UINT32 timeout)
 static void InitNode(node_t *node)
 {
 	node->acktosend_head = node->acktosend_tail = 0;
-#ifndef NEWPING
-	node->ping = PINGDEFAULT;
-	node->varping = VARPINGDEFAULT;
-	node->timeout = TIMEOUT(node->ping, node->varping);
-#endif
 	node->firstacktosend = 0;
 	node->nextacknum = 1;
 	node->remotefirstack = 0;
@@ -854,9 +816,7 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"CLIENTJOIN",
 	"NODETIMEOUT",
 	"RESYNCHING",
-#ifdef NEWPING
 	"PING"
-#endif
 };
 
 static void DebugPrintpacket(const char *header)
@@ -1410,30 +1370,73 @@ boolean D_CheckNetGame(void)
 	return ret;
 }
 
+struct pingcell
+{
+	INT32 num;
+	INT32 ms;
+};
+
+static int pingcellcmp(const void *va, const void *vb)
+{
+	const struct pingcell *a, *b;
+	a = va;
+	b = vb;
+	return ( a->ms - b->ms );
+}
+
+/*
+New ping command formatted nicely to present ping in
+ascending order. And with equally spaced columns.
+The caller's ping is presented at the bottom too, for
+convenience.
+*/
+
 void Command_Ping_f(void)
 {
-#ifndef NEWPING
-	if(server)
+	struct pingcell pingv[MAXPLAYERS];
+	INT32           pingc;
+
+	int name_width = 0;
+	int   ms_width = 0;
+
+	int n;
+	INT32 i;
+
+	pingc = 0;
+	for (i = 1; i < MAXPLAYERS; ++i)
+		if (playeringame[i])
 	{
-#endif
-		INT32 i;
-		for (i = 0; i < MAXPLAYERS;i++)
-		{
-#ifndef NEWPING
-			const INT32 node = playernode[i];
-			if (playeringame[i] && node != 0)
-				CONS_Printf(M_GetText("%.2d : %s\n %d tics, %d ms.\n"), i, player_names[i],
-				GetLag(node), G_TicsToMilliseconds(GetLag(node)));
-#else
-			if (playeringame[i] && i != 0)
-				CONS_Printf(M_GetText("%.2d : %s\n %d ms\n"), i, player_names[i], playerpingtable[i]);
-#endif
-		}
-#ifndef NEWPING
+		n = strlen(player_names[i]);
+		if (n > name_width)
+			name_width = n;
+
+		n = playerpingtable[i];
+		if (n > ms_width)
+			ms_width = n;
+
+		pingv[pingc].num = i;
+		pingv[pingc].ms  = playerpingtable[i];
+		pingc++;
+	}
+
+	     if (ms_width < 10)  ms_width = 1;
+	else if (ms_width < 100) ms_width = 2;
+	else                     ms_width = 3;
+
+	qsort(pingv, pingc, sizeof (struct pingcell), &pingcellcmp);
+
+	for (i = 0; i < pingc; ++i)
+	{
+		CONS_Printf("%02d : %-*s %*d ms\n",
+				pingv[i].num,
+				name_width, player_names[pingv[i].num],
+				ms_width,   pingv[i].ms);
+	}
+
+	if (!server && playeringame[consoleplayer])
+	{
+		CONS_Printf("\nYour ping is %d ms\n", playerpingtable[consoleplayer]);
 	}
-	else
-		CONS_Printf(M_GetText("Only the server can use this.\n"));
-#endif
 }
 
 void D_CloseConnection(void)
diff --git a/src/d_net.h b/src/d_net.h
index 8e518e404b61678e2f4655cb69e1596e37df17fd..eb657eec1d5ecf54872961494a53cfa7a9b5d146 100644
--- a/src/d_net.h
+++ b/src/d_net.h
@@ -21,7 +21,6 @@
 // Max computers in a game
 #define MAXNETNODES (MAXPLAYERS+4)
 #define BROADCASTADDR MAXNETNODES
-#define MAXSPLITSCREENPLAYERS 4 // Max number of players on a single computer
 #define NETSPLITSCREEN // Kart's splitscreen netgame feature
 
 #define STATLENGTH (TICRATE*2)
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index fe855dbff84efedef567aadd5cc9f1f599ac2d3c..b91c6bdbf026152862af8793824e20434c5510a9 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -129,6 +129,9 @@ static void Command_StopMovie_f(void);
 static void Command_Map_f(void);
 static void Command_ResetCamera_f(void);
 
+static void Command_View_f (void);
+static void Command_SetViews_f(void);
+
 static void Command_Addfile(void);
 static void Command_ListWADS_f(void);
 #ifdef DELFILE
@@ -423,7 +426,7 @@ consvar_t cv_numlaps = {"numlaps", "3", CV_NETVAR|CV_CALL|CV_NOINIT, numlaps_con
 static CV_PossibleValue_t basenumlaps_cons_t[] = {{1, "MIN"}, {50, "MAX"}, {0, "Map default"}, {0, NULL}};
 consvar_t cv_basenumlaps = {"basenumlaps", "Map default", CV_NETVAR|CV_CALL|CV_CHEAT, basenumlaps_cons_t, BaseNumLaps_OnChange, 0, NULL, NULL, 0, 0, NULL};
 
-consvar_t cv_forceskin = {"forceskin", "-1", CV_NETVAR|CV_CALL|CV_CHEAT, NULL, ForceSkin_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_forceskin = {"forceskin", "Off", CV_NETVAR|CV_CALL|CV_CHEAT, Forceskin_cons_t, ForceSkin_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_downloading = {"downloading", "On", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_allowexitlevel = {"allowexitlevel", "No", CV_NETVAR, CV_YesNo, NULL, 0, NULL, NULL, 0, 0, NULL};
 
@@ -434,7 +437,6 @@ static CV_PossibleValue_t nettimeout_cons_t[] = {{TICRATE/7, "MIN"}, {60*TICRATE
 consvar_t cv_nettimeout = {"nettimeout", "105", CV_CALL|CV_SAVE, nettimeout_cons_t, NetTimeout_OnChange, 0, NULL, NULL, 0, 0, NULL};
 //static CV_PossibleValue_t jointimeout_cons_t[] = {{5*TICRATE, "MIN"}, {60*TICRATE, "MAX"}, {0, NULL}};
 consvar_t cv_jointimeout = {"jointimeout", "105", CV_CALL|CV_SAVE, nettimeout_cons_t, JoinTimeout_OnChange, 0, NULL, NULL, 0, 0, NULL};
-#ifdef NEWPING
 static CV_PossibleValue_t maxping_cons_t[] = {{0, "MIN"}, {1000, "MAX"}, {0, NULL}};
 consvar_t cv_maxping = {"maxping", "800", CV_SAVE, maxping_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
@@ -445,7 +447,6 @@ consvar_t cv_pingtimeout = {"pingtimeout", "10", CV_SAVE, pingtimeout_cons_t, NU
 static CV_PossibleValue_t showping_cons_t[] = {{0, "Off"}, {1, "Always"}, {2, "Warning"}, {0, NULL}};
 consvar_t cv_showping = {"showping", "Always", CV_SAVE, showping_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
-#endif
 // Intermission time Tails 04-19-2002
 static CV_PossibleValue_t inttime_cons_t[] = {{0, "MIN"}, {3600, "MAX"}, {0, NULL}};
 consvar_t cv_inttime = {"inttime", "20", CV_NETVAR, inttime_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -514,6 +515,24 @@ const char *netxcmdnames[MAXNETXCMD - 1] =
   */
 void D_RegisterServerCommands(void)
 {
+	INT32 i;
+	Forceskin_cons_t[0].value = -1;
+	Forceskin_cons_t[0].strvalue = "Off";
+
+	for (i = 0; i < NUMGAMETYPES; i++)
+	{
+		gametype_cons_t[i].value = i;
+		gametype_cons_t[i].strvalue = Gametype_Names[i];
+	}
+	gametype_cons_t[NUMGAMETYPES].value = 0;
+	gametype_cons_t[NUMGAMETYPES].strvalue = NULL;
+
+	// Set the values to 0/NULL, it will be overwritten later when a skin is assigned to the slot.
+	for (i = 1; i < MAXSKINS; i++)
+	{
+		Forceskin_cons_t[i].value = 0;
+		Forceskin_cons_t[i].strvalue = NULL;
+	}
 	RegisterNetXCmd(XD_NAMEANDCOLOR, Got_NameAndColor);
 	RegisterNetXCmd(XD_WEAPONPREF, Got_WeaponPref);
 	RegisterNetXCmd(XD_MAP, Got_Mapcmd);
@@ -667,6 +686,14 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_maxsend);
 	CV_RegisterVar(&cv_noticedownload);
 	CV_RegisterVar(&cv_downloadspeed);
+#ifndef NONET
+	CV_RegisterVar(&cv_allownewplayer);
+#ifdef VANILLAJOINNEXTROUND
+	CV_RegisterVar(&cv_joinnextround);
+#endif
+	CV_RegisterVar(&cv_showjoinaddress);
+	CV_RegisterVar(&cv_blamecfail);
+#endif
 
 	COM_AddCommand("ping", Command_Ping_f);
 	CV_RegisterVar(&cv_nettimeout);
@@ -674,11 +701,9 @@ void D_RegisterServerCommands(void)
 
 	CV_RegisterVar(&cv_skipmapcheck);
 	CV_RegisterVar(&cv_sleep);
-#ifdef NEWPING
 	CV_RegisterVar(&cv_maxping);
 	CV_RegisterVar(&cv_pingtimeout);
 	CV_RegisterVar(&cv_showping);
-#endif
 
 #ifdef SEENAMES
 	 CV_RegisterVar(&cv_allowseenames);
@@ -726,6 +751,13 @@ void D_RegisterClientCommands(void)
 
 	COM_AddCommand("resetcamera", Command_ResetCamera_f);
 
+	COM_AddCommand("view", Command_View_f);
+	COM_AddCommand("view2", Command_View_f);
+	COM_AddCommand("view3", Command_View_f);
+	COM_AddCommand("view4", Command_View_f);
+
+	COM_AddCommand("setviews", Command_SetViews_f);
+
 	COM_AddCommand("setcontrol", Command_Setcontrol_f);
 	COM_AddCommand("setcontrol2", Command_Setcontrol2_f);
 	COM_AddCommand("setcontrol3", Command_Setcontrol3_f);
@@ -800,6 +832,9 @@ void D_RegisterClientCommands(void)
 
 	COM_AddCommand("displayplayer", Command_Displayplayer_f);
 
+	CV_RegisterVar(&cv_recordmultiplayerdemos);
+	CV_RegisterVar(&cv_netdemosyncquality);
+
 	// FIXME: not to be here.. but needs be done for config loading
 	CV_RegisterVar(&cv_usegamma);
 
@@ -853,6 +888,10 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_driftaxis2);
 	CV_RegisterVar(&cv_driftaxis3);
 	CV_RegisterVar(&cv_driftaxis4);
+	CV_RegisterVar(&cv_deadzone);
+	CV_RegisterVar(&cv_deadzone2);
+	CV_RegisterVar(&cv_deadzone3);
+	CV_RegisterVar(&cv_deadzone4);
 
 	// filesrch.c
 	CV_RegisterVar(&cv_addons_option);
@@ -919,6 +958,8 @@ void D_RegisterClientCommands(void)
 	// screen.c
 	CV_RegisterVar(&cv_fullscreen);
 	CV_RegisterVar(&cv_renderview);
+	CV_RegisterVar(&cv_vhseffect);
+	CV_RegisterVar(&cv_shittyscreen);
 	CV_RegisterVar(&cv_scr_depth);
 	CV_RegisterVar(&cv_scr_width);
 	CV_RegisterVar(&cv_scr_height);
@@ -1130,11 +1171,11 @@ static void CleanupPlayerName(INT32 playernum, const char *newname)
 	// spaces may have been removed
 	if (playernum == consoleplayer)
 		CV_StealthSet(&cv_playername, tmpname);
-	else if (playernum == secondarydisplayplayer || (!netgame && playernum == 1))
+	else if (playernum == displayplayers[1] || (!netgame && playernum == 1))
 		CV_StealthSet(&cv_playername2, tmpname);
-	else if (playernum == thirddisplayplayer || (!netgame && playernum == 2))
+	else if (playernum == displayplayers[2] || (!netgame && playernum == 2))
 		CV_StealthSet(&cv_playername3, tmpname);
-	else if (playernum == fourthdisplayplayer || (!netgame && playernum == 3))
+	else if (playernum == displayplayers[3] || (!netgame && playernum == 3))
 		CV_StealthSet(&cv_playername4, tmpname);
 	else I_Assert(((void)"CleanupPlayerName used on non-local player", 0));
 
@@ -1166,6 +1207,7 @@ static void SetPlayerName(INT32 playernum, char *newname)
 				HU_AddChatText(va("\x82*%s renamed to %s", player_names[playernum], newname), false);
 
 			strcpy(player_names[playernum], newname);
+			demo_extradata[playernum] |= DXD_NAME;
 		}
 	}
 	else
@@ -1241,11 +1283,11 @@ static void ForceAllSkins(INT32 forcedskin)
 		{
 			if (i == consoleplayer)
 				CV_StealthSet(&cv_skin, skins[forcedskin].name);
-			else if (i == secondarydisplayplayer)
+			else if (i == displayplayers[1])
 				CV_StealthSet(&cv_skin2, skins[forcedskin].name);
-			else if (i == thirddisplayplayer)
+			else if (i == displayplayers[2])
 				CV_StealthSet(&cv_skin3, skins[forcedskin].name);
-			else if (i == fourthdisplayplayer)
+			else if (i == displayplayers[3])
 				CV_StealthSet(&cv_skin4, skins[forcedskin].name);
 		}
 	}
@@ -1380,8 +1422,8 @@ static void SendNameAndColor2(void)
 	if (splitscreen < 1 && !botingame)
 		return; // can happen if skin2/color2/name2 changed
 
-	if (secondarydisplayplayer != consoleplayer)
-		secondplaya = secondarydisplayplayer;
+	if (displayplayers[1] != consoleplayer)
+		secondplaya = displayplayers[1];
 	else if (!netgame) // HACK
 		secondplaya = 1;
 
@@ -1430,7 +1472,7 @@ static void SendNameAndColor2(void)
 		CleanupPlayerName(secondplaya, cv_playername2.zstring);
 		strcpy(player_names[secondplaya], cv_playername2.zstring);
 
-		// don't use secondarydisplayplayer: the second player must be 1
+		// don't use displayplayers[1]: the second player must be 1
 		players[secondplaya].skincolor = cv_playercolor2.value;
 		if (players[secondplaya].mo)
 			players[secondplaya].mo->color = players[secondplaya].skincolor;
@@ -1469,14 +1511,14 @@ static void SendNameAndColor2(void)
 	snac2pending++;
 
 	// Don't change name if muted
-	if (cv_mute.value && !(server || IsPlayerAdmin(secondarydisplayplayer)))
-		CV_StealthSet(&cv_playername2, player_names[secondarydisplayplayer]);
+	if (cv_mute.value && !(server || IsPlayerAdmin(displayplayers[1])))
+		CV_StealthSet(&cv_playername2, player_names[displayplayers[1]]);
 	else // Cleanup name if changing it
-		CleanupPlayerName(secondarydisplayplayer, cv_playername2.zstring);
+		CleanupPlayerName(displayplayers[1], cv_playername2.zstring);
 
 	// Don't change skin if the server doesn't want you to.
-	if (!CanChangeSkin(secondarydisplayplayer))
-		CV_StealthSet(&cv_skin2, skins[players[secondarydisplayplayer].skin].name);
+	if (!CanChangeSkin(displayplayers[1]))
+		CV_StealthSet(&cv_skin2, skins[players[displayplayers[1]].skin].name);
 
 	// check if player has the skin loaded (cv_skin2 may have
 	// the name of a skin that was available in the previous game)
@@ -1503,8 +1545,8 @@ static void SendNameAndColor3(void)
 	if (splitscreen < 2)
 		return; // can happen if skin3/color3/name3 changed
 
-	if (thirddisplayplayer != consoleplayer)
-		thirdplaya = thirddisplayplayer;
+	if (displayplayers[2] != consoleplayer)
+		thirdplaya = displayplayers[2];
 	else if (!netgame) // HACK
 		thirdplaya = 2;
 
@@ -1545,7 +1587,7 @@ static void SendNameAndColor3(void)
 		CleanupPlayerName(thirdplaya, cv_playername3.zstring);
 		strcpy(player_names[thirdplaya], cv_playername3.zstring);
 
-		// don't use thirddisplayplayer: the third player must be 2
+		// don't use displayplayers[2]: the third player must be 2
 		players[thirdplaya].skincolor = cv_playercolor3.value;
 		if (players[thirdplaya].mo)
 			players[thirdplaya].mo->color = players[thirdplaya].skincolor;
@@ -1584,14 +1626,14 @@ static void SendNameAndColor3(void)
 	snac3pending++;
 
 	// Don't change name if muted
-	if (cv_mute.value && !(server || IsPlayerAdmin(thirddisplayplayer)))
-		CV_StealthSet(&cv_playername3, player_names[thirddisplayplayer]);
+	if (cv_mute.value && !(server || IsPlayerAdmin(displayplayers[2])))
+		CV_StealthSet(&cv_playername3, player_names[displayplayers[2]]);
 	else // Cleanup name if changing it
-		CleanupPlayerName(thirddisplayplayer, cv_playername3.zstring);
+		CleanupPlayerName(displayplayers[2], cv_playername3.zstring);
 
 	// Don't change skin if the server doesn't want you to.
-	if (!CanChangeSkin(thirddisplayplayer))
-		CV_StealthSet(&cv_skin3, skins[players[thirddisplayplayer].skin].name);
+	if (!CanChangeSkin(displayplayers[2]))
+		CV_StealthSet(&cv_skin3, skins[players[displayplayers[2]].skin].name);
 
 	// check if player has the skin loaded (cv_skin3 may have
 	// the name of a skin that was available in the previous game)
@@ -1618,8 +1660,8 @@ static void SendNameAndColor4(void)
 	if (splitscreen < 3)
 		return; // can happen if skin4/color4/name4 changed
 
-	if (fourthdisplayplayer != consoleplayer)
-		fourthplaya = fourthdisplayplayer;
+	if (displayplayers[3] != consoleplayer)
+		fourthplaya = displayplayers[3];
 	else if (!netgame) // HACK
 		fourthplaya = 3;
 
@@ -1668,7 +1710,7 @@ static void SendNameAndColor4(void)
 		CleanupPlayerName(fourthplaya, cv_playername4.zstring);
 		strcpy(player_names[fourthplaya], cv_playername4.zstring);
 
-		// don't use fourthdisplayplayer: the second player must be 4
+		// don't use displayplayers[3]: the second player must be 4
 		players[fourthplaya].skincolor = cv_playercolor4.value;
 		if (players[fourthplaya].mo)
 			players[fourthplaya].mo->color = players[fourthplaya].skincolor;
@@ -1707,14 +1749,14 @@ static void SendNameAndColor4(void)
 	snac4pending++;
 
 	// Don't change name if muted
-	if (cv_mute.value && !(server || IsPlayerAdmin(fourthdisplayplayer)))
-		CV_StealthSet(&cv_playername4, player_names[fourthdisplayplayer]);
+	if (cv_mute.value && !(server || IsPlayerAdmin(displayplayers[3])))
+		CV_StealthSet(&cv_playername4, player_names[displayplayers[3]]);
 	else // Cleanup name if changing it
-		CleanupPlayerName(fourthdisplayplayer, cv_playername4.zstring);
+		CleanupPlayerName(displayplayers[3], cv_playername4.zstring);
 
 	// Don't change skin if the server doesn't want you to.
-	if (!CanChangeSkin(fourthdisplayplayer))
-		CV_StealthSet(&cv_skin4, skins[players[fourthdisplayplayer].skin].name);
+	if (!CanChangeSkin(displayplayers[3]))
+		CV_StealthSet(&cv_skin4, skins[players[displayplayers[3]].skin].name);
 
 	// check if player has the skin loaded (cv_skin4 may have
 	// the name of a skin that was available in the previous game)
@@ -1744,12 +1786,12 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 #endif
 
 	if (playernum == consoleplayer)
-		snacpending--;
-	else if (playernum == secondarydisplayplayer)
+		snacpending--; // TODO: make snacpending an array instead of 4 separate vars?
+	else if (playernum == displayplayers[1])
 		snac2pending--;
-	else if (playernum == thirddisplayplayer)
+	else if (playernum == displayplayers[2])
 		snac3pending--;
-	else if (playernum == fourthdisplayplayer)
+	else if (playernum == displayplayers[3])
 		snac4pending--;
 
 #ifdef PARANOIA
@@ -1769,10 +1811,11 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 	p->skincolor = color % MAXSKINCOLORS;
 	if (p->mo)
 		p->mo->color = (UINT8)p->skincolor;
+	demo_extradata[playernum] |= DXD_COLOR;
 
 	// normal player colors
-	if (server && (p != &players[consoleplayer] && p != &players[secondarydisplayplayer]
-		&& p != &players[thirddisplayplayer] && p != &players[fourthdisplayplayer]))
+	if (server && (p != &players[consoleplayer] && p != &players[displayplayers[1]]
+		&& p != &players[displayplayers[2]] && p != &players[displayplayers[3]]))
 	{
 		boolean kick = false;
 
@@ -1809,11 +1852,11 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 
 		if (playernum == consoleplayer)
 			CV_StealthSet(&cv_skin, skins[forcedskin].name);
-		else if (playernum == secondarydisplayplayer)
+		else if (playernum == displayplayers[1])
 			CV_StealthSet(&cv_skin2, skins[forcedskin].name);
-		else if (playernum == thirddisplayplayer)
+		else if (playernum == displayplayers[2])
 			CV_StealthSet(&cv_skin3, skins[forcedskin].name);
-		else if (playernum == fourthdisplayplayer)
+		else if (playernum == displayplayers[3])
 			CV_StealthSet(&cv_skin4, skins[forcedskin].name);
 	}
 	else
@@ -1900,7 +1943,198 @@ void D_SendPlayerConfig(void)
 // Only works for displayplayer, sorry!
 static void Command_ResetCamera_f(void)
 {
-	P_ResetCamera(&players[displayplayer], &camera);
+	P_ResetCamera(&players[displayplayers[0]], &camera[0]);
+}
+
+/* Consider replacing nametonum with this */
+static INT32 LookupPlayer(const char *s)
+{
+	INT32 playernum;
+
+	if (*s == '0')/* clever way to bypass atoi */
+		return 0;
+
+	if (( playernum = atoi(s) ))
+	{
+		playernum = max(min(playernum, MAXPLAYERS-1), 0);/* not out of range */
+		return playernum;
+	}
+
+	for (playernum = 0; playernum < MAXPLAYERS; ++playernum)
+	{
+		/* Match name case-insensitively: fully, or partially the start. */
+		if (playeringame[playernum])
+			if (strnicmp(player_names[playernum], s, strlen(s)) == 0)
+		{
+			return playernum;
+		}
+	}
+	return -1;
+}
+
+static INT32 FindPlayerByPlace(INT32 place)
+{
+	INT32 playernum;
+	for (playernum = 0; playernum < MAXPLAYERS; ++playernum)
+		if (playeringame[playernum])
+	{
+		if (players[playernum].kartstuff[k_position] == place)
+		{
+			return playernum;
+		}
+	}
+	return -1;
+}
+
+//
+// GetViewablePlayerPlaceRange
+// Return in first and last, that player available to view, sorted by placement
+// in the race.
+//
+static void GetViewablePlayerPlaceRange(INT32 *first, INT32 *last)
+{
+	INT32 i;
+	INT32 place;
+
+	(*first) = MAXPLAYERS;
+	(*last) = 0;
+
+	for (i = 0; i < MAXPLAYERS; ++i)
+		if (G_CouldView(i))
+	{
+		place = players[i].kartstuff[k_position];
+		if (place < (*first))
+			(*first) = place;
+		if (place > (*last))
+			(*last) = place;
+	}
+}
+
+#define PRINTVIEWPOINT( pre,suf ) \
+	CONS_Printf(pre"viewing \x84(%d) \x83%s\x80"suf".\n",\
+			(*displayplayerp), player_names[(*displayplayerp)]);
+static void Command_View_f(void)
+{
+	INT32 *displayplayerp;
+	INT32 olddisplayplayer;
+	int viewnum;
+	const char *playerparam;
+	INT32 placenum;
+	INT32 playernum;
+	INT32 firstplace, lastplace;
+	char c;
+	/* easy peasy */
+	c = COM_Argv(0)[strlen(COM_Argv(0))-1];/* may be digit */
+	switch (c)
+	{
+		case '2': viewnum = 2; break;
+		case '3': viewnum = 3; break;
+		case '4': viewnum = 4; break;
+		default:  viewnum = 1;
+	}
+
+	if (viewnum > 1 && !( multiplayer && demo.playback ))
+	{
+		CONS_Alert(CONS_NOTICE,
+				"You must be viewing a multiplayer replay to use this.\n");
+		return;
+	}
+
+	displayplayerp = &displayplayers[viewnum-1];
+
+	if (COM_Argc() > 1)/* switch to player */
+	{
+		playerparam = COM_Argv(1);
+		if (playerparam[0] == '#')/* search by placement */
+		{
+			placenum = atoi(&playerparam[1]);
+			playernum = FindPlayerByPlace(placenum);
+			if (playernum == -1 || !G_CouldView(playernum))
+			{
+				GetViewablePlayerPlaceRange(&firstplace, &lastplace);
+				if (playernum == -1)
+				{
+					CONS_Alert(CONS_WARNING, "There is no player in that place! ");
+				}
+				else
+				{
+					CONS_Alert(CONS_WARNING,
+							"That player cannot be viewed currently! "
+							"The first player that you can view is \x82#%d\x80; ",
+							firstplace);
+				}
+				CONS_Printf("Last place is \x82#%d\x80.\n", lastplace);
+				return;
+			}
+		}
+		else
+		{
+			if (( playernum = LookupPlayer(COM_Argv(1)) ) == -1)
+			{
+				CONS_Alert(CONS_WARNING, "There is no player by that name!\n");
+				return;
+			}
+			if (!playeringame[playernum])
+			{
+				CONS_Alert(CONS_WARNING, "There is no player using that slot!\n");
+				return;
+			}
+		}
+
+		olddisplayplayer = (*displayplayerp);
+		G_ResetView(viewnum, playernum, false);
+
+		/* The player we wanted was corrected to who it already was. */
+		if ((*displayplayerp) == olddisplayplayer)
+			return;
+
+		if ((*displayplayerp) != playernum)/* differ parameter */
+		{
+			/* skipped some */
+			CONS_Alert(CONS_NOTICE, "That player cannot be viewed currently.\n");
+			PRINTVIEWPOINT ("Now "," instead")
+		}
+		else
+			PRINTVIEWPOINT ("Now ",)
+	}
+	else/* print current view */
+	{
+		if (splitscreen < viewnum-1)/* We can't see those guys! */
+			return;
+		PRINTVIEWPOINT ("Currently ",)
+	}
+}
+#undef PRINTVIEWPOINT
+
+static void Command_SetViews_f(void)
+{
+	UINT8 splits;
+	UINT8 newsplits;
+
+	if (!( demo.playback && multiplayer ))
+	{
+		CONS_Alert(CONS_NOTICE,
+				"You must be viewing a multiplayer replay to use this.\n");
+		return;
+	}
+
+	if (COM_Argc() != 2)
+	{
+		CONS_Printf("setviews <views>: set the number of split screens\n");
+		return;
+	}
+
+	splits = splitscreen+1;
+
+	newsplits = atoi(COM_Argv(1));
+	newsplits = min(max(newsplits, 1), 4);
+	if (newsplits > splits)
+		G_AdjustView(newsplits, 0, true);
+	else
+	{
+		splitscreen = newsplits-1;
+		R_ExecuteSetViewSize();
+	}
 }
 
 // ========================================================================
@@ -1913,9 +2147,14 @@ static void Command_Playdemo_f(void)
 {
 	char name[256];
 
-	if (COM_Argc() != 2)
+	if (COM_Argc() < 2)
 	{
-		CONS_Printf(M_GetText("playdemo <demoname>: playback a demo\n"));
+		CONS_Printf("playdemo <demoname> [-addfiles / -force]:\n");
+		CONS_Printf(M_GetText(
+					"Play back a demo file. The full path from your Kart directory must be given.\n\n"
+
+					"* With \"-addfiles\", any required files are added from a list contained within the demo file.\n"
+					"* With \"-force\", the demo is played even if the necessary files have not been added.\n"));
 		return;
 	}
 
@@ -1926,7 +2165,7 @@ static void Command_Playdemo_f(void)
 	}
 
 	// disconnect from server here?
-	if (demoplayback)
+	if (demo.playback)
 		G_StopDemo();
 	if (metalplayback)
 		G_StopMetalDemo();
@@ -1937,6 +2176,9 @@ static void Command_Playdemo_f(void)
 
 	CONS_Printf(M_GetText("Playing back demo '%s'.\n"), name);
 
+	demo.loadfiles = strcmp(COM_Argv(2), "-addfiles") == 0;
+	demo.ignorefiles = strcmp(COM_Argv(2), "-force") == 0;
+
 	// Internal if no extension, external if one exists
 	// If external, convert the file name to a path in SRB2's home directory
 	if (FIL_CheckExtension(name))
@@ -1962,7 +2204,7 @@ static void Command_Timedemo_f(void)
 	}
 
 	// disconnect from server here?
-	if (demoplayback)
+	if (demo.playback)
 		G_StopDemo();
 	if (metalplayback)
 		G_StopMetalDemo();
@@ -2086,7 +2328,7 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean r
 			{
 				//CL_AddSplitscreenPlayer();
 				botingame = true;
-				secondarydisplayplayer = 1;
+				displayplayers[1] = 1;
 				playeringame[1] = true;
 				players[1].bot = 1;
 				SendNameAndColor2();
@@ -2138,12 +2380,8 @@ void D_ModifyClientVote(SINT8 voted, UINT8 splitplayer)
 	char *p = buf;
 	UINT8 player = consoleplayer;
 
-	if (splitplayer == 1)
-		player = secondarydisplayplayer;
-	else if (splitplayer == 2)
-		player = thirddisplayplayer;
-	else if (splitplayer == 3)
-		player = fourthdisplayplayer;
+	if (splitplayer > 0)
+		player = displayplayers[splitplayer];
 
 	WRITESINT8(p, voted);
 	WRITEUINT8(p, player);
@@ -2203,13 +2441,15 @@ static void Command_Map_f(void)
 {
 	const char *mapname;
 	size_t i;
-	INT32 j, newmapnum;
-	boolean newresetplayers;
+	INT32 newmapnum;
+	boolean newresetplayers, newencoremode;
 	INT32 newgametype = gametype;
 
-	// max length of command: map map03 -gametype coop -noresetplayers -force
-	//                         1    2       3       4         5           6
+	// max length of command: map map03 -gametype race -noresetplayers -force -encore
+	//                         1    2       3       4         5           6      7
 	// = 8 arg max
+	// i don't know whether this is intrinsic to the system or just someone being weird but
+	// "noresetplayers" is pretty useless for kart if it turns out this is too close to the limit
 	if (COM_Argc() < 2 || COM_Argc() > 8)
 	{
 		CONS_Printf(M_GetText("map <mapname> [-gametype <type> [-force]: warp to map\n"));
@@ -2269,27 +2509,28 @@ static void Command_Map_f(void)
 			return;
 		}
 
-		for (j = 0; gametype_cons_t[j].strvalue; j++)
-			if (!strcasecmp(gametype_cons_t[j].strvalue, COM_Argv(i+1)))
-			{
-				// Don't do any variable setting here. Wait until you get your
-				// map packet first to avoid sending the same info twice!
-				newgametype = gametype_cons_t[j].value;
-				break;
-			}
+		newgametype = G_GetGametypeByName(COM_Argv(i+1));
+		if (newgametype == -1) // reached end of the list with no match
+		{
+			INT32 j = atoi(COM_Argv(i+1)); // assume they gave us a gametype number, which is okay too
+			if (j >= 0 && j < NUMGAMETYPES)
+				newgametype = (INT16)j;
+		}
+	}
+
+	// new encoremode value
+	// use cvar by default
 
-		if (!gametype_cons_t[j].strvalue) // reached end of the list with no match
+	newencoremode = (boolean)cv_kartencore.value;
+
+	if (COM_CheckParm("-encore"))
+	{
+		if (!M_SecretUnlocked(SECRET_ENCORE) && !newencoremode)
 		{
-			// assume they gave us a gametype number, which is okay too
-			for (j = 0; gametype_cons_t[j].strvalue != NULL; j++)
-			{
-				if (atoi(COM_Argv(i+1)) == gametype_cons_t[j].value)
-				{
-					newgametype = gametype_cons_t[j].value;
-					break;
-				}
-			}
+			CONS_Alert(CONS_NOTICE, M_GetText("You haven't unlocked Encore Mode yet!\n"));
+			return;
 		}
+		newencoremode = !newencoremode;
 	}
 
 	if (!(i = COM_CheckParm("-force")) && newgametype == gametype) // SRB2Kart
@@ -2300,15 +2541,20 @@ static void Command_Map_f(void)
 		; // The player wants us to trek on anyway.  Do so.
 	// G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer
 	// Alternatively, bail if the map header is completely missing anyway.
-	else
+	else if (!mapheaderinfo[newmapnum-1]
+	 || !(mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype)))
 	{
-		if (!mapheaderinfo[newmapnum-1]
-		 || !(mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype)))
+		char gametypestring[32] = "Single Player";
+
+		if (multiplayer)
 		{
-			CONS_Alert(CONS_WARNING, M_GetText("%s doesn't support %s mode!\n(Use -force to override)\n"), mapname,
-				(multiplayer ? gametype_cons_t[newgametype].strvalue : "Single Player"));
-			return;
+			if (newgametype >= 0 && newgametype < NUMGAMETYPES
+			&& Gametype_Names[newgametype])
+				strcpy(gametypestring, Gametype_Names[newgametype]);
 		}
+
+		CONS_Alert(CONS_WARNING, M_GetText("%s doesn't support %s mode!\n(Use -force to override)\n"), mapname, gametypestring);
+		return;
 	}
 
 	// Prevent warping to locked levels
@@ -2322,7 +2568,7 @@ static void Command_Map_f(void)
 	}
 
 	fromlevelselect = false;
-	D_MapChange(newmapnum, newgametype, (boolean)cv_kartencore.value, newresetplayers, 0, false, false);
+	D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, false);
 }
 
 /** Receives a map command and changes the map.
@@ -2393,10 +2639,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 		CON_LogMessage(M_GetText("Speeding off to level...\n"));
 	}
 
-	CON_ToggleOff();
-	CON_ClearHUD();
-
-	if (demoplayback && !timingdemo)
+	if (demo.playback && !demo.timing)
 		precache = false;
 
 	if (resetplayer)
@@ -2413,17 +2656,19 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	LUAh_MapChange(mapnumber);
 #endif*/
 
+	demo.savemode = (cv_recordmultiplayerdemos.value == 2) ? DSM_WILLAUTOSAVE : DSM_NOTSAVING;
+	demo.savebutton = 0;
 	G_InitNew(pencoremode, mapname, resetplayer, skipprecutscene);
-	if (demoplayback && !timingdemo)
+	if (demo.playback && !demo.timing)
 		precache = true;
-	if (timingdemo)
+	if (demo.timing)
 		G_DoneLevelLoad();
 
 	if (metalrecording)
 		G_BeginMetal();
-	if (demorecording) // Okay, level loaded, character spawned and skinned,
+	if (demo.recording) // Okay, level loaded, character spawned and skinned,
 		G_BeginRecording(); // I AM NOW READY TO RECORD.
-	demo_start = true;
+	demo.deferstart = true;
 }
 
 static void Command_Pause(void)
@@ -2473,13 +2718,13 @@ static void Got_Pause(UINT8 **cp, INT32 playernum)
 		return;
 	}
 
-	if (modeattacking)
+	if (modeattacking && !demo.playback)
 		return;
 
 	paused = READUINT8(*cp);
 	dedicatedpause = READUINT8(*cp);
 
-	if (!demoplayback)
+	if (!demo.playback)
 	{
 		if (netgame)
 		{
@@ -2566,6 +2811,7 @@ static void Got_Respawn(UINT8 **cp, INT32 playernum)
 
 	if (players[respawnplayer].mo)
 		P_DamageMobj(players[respawnplayer].mo, NULL, NULL, 10000);
+	demo_extradata[playernum] |= DXD_RESPAWN;
 }
 
 /** Deals with an ::XD_RANDOMSEED message in a netgame.
@@ -2787,11 +3033,11 @@ static void Command_Teamchange2_f(void)
 		return;
 	}
 
-	if (players[secondarydisplayplayer].spectator)
-		error = !(NetPacket.packet.newteam || (players[secondarydisplayplayer].pflags & PF_WANTSTOJOIN));
+	if (players[displayplayers[1]].spectator)
+		error = !(NetPacket.packet.newteam || (players[displayplayers[1]].pflags & PF_WANTSTOJOIN));
 	else if (G_GametypeHasTeams())
-		error = (NetPacket.packet.newteam == (unsigned)players[secondarydisplayplayer].ctfteam);
-	else if (G_GametypeHasSpectators() && !players[secondarydisplayplayer].spectator)
+		error = (NetPacket.packet.newteam == (unsigned)players[displayplayers[1]].ctfteam);
+	else if (G_GametypeHasSpectators() && !players[displayplayers[1]].spectator)
 		error = (NetPacket.packet.newteam == 3);
 #ifdef PARANOIA
 	else
@@ -2878,11 +3124,11 @@ static void Command_Teamchange3_f(void)
 		return;
 	}
 
-	if (players[thirddisplayplayer].spectator)
-		error = !(NetPacket.packet.newteam || (players[thirddisplayplayer].pflags & PF_WANTSTOJOIN));
+	if (players[displayplayers[2]].spectator)
+		error = !(NetPacket.packet.newteam || (players[displayplayers[2]].pflags & PF_WANTSTOJOIN));
 	else if (G_GametypeHasTeams())
-		error = (NetPacket.packet.newteam == (unsigned)players[thirddisplayplayer].ctfteam);
-	else if (G_GametypeHasSpectators() && !players[thirddisplayplayer].spectator)
+		error = (NetPacket.packet.newteam == (unsigned)players[displayplayers[2]].ctfteam);
+	else if (G_GametypeHasSpectators() && !players[displayplayers[2]].spectator)
 		error = (NetPacket.packet.newteam == 3);
 #ifdef PARANOIA
 	else
@@ -2969,11 +3215,11 @@ static void Command_Teamchange4_f(void)
 		return;
 	}
 
-	if (players[fourthdisplayplayer].spectator)
-		error = !(NetPacket.packet.newteam || (players[fourthdisplayplayer].pflags & PF_WANTSTOJOIN));
+	if (players[displayplayers[3]].spectator)
+		error = !(NetPacket.packet.newteam || (players[displayplayers[3]].pflags & PF_WANTSTOJOIN));
 	else if (G_GametypeHasTeams())
-		error = (NetPacket.packet.newteam == (unsigned)players[fourthdisplayplayer].ctfteam);
-	else if (G_GametypeHasSpectators() && !players[fourthdisplayplayer].spectator)
+		error = (NetPacket.packet.newteam == (unsigned)players[displayplayers[3]].ctfteam);
+	else if (G_GametypeHasSpectators() && !players[displayplayers[3]].spectator)
 		error = (NetPacket.packet.newteam == 3);
 #ifdef PARANOIA
 	else
@@ -3370,8 +3616,8 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
 		HU_AddChatText(va("\x82*%s became a spectator.", player_names[playernum]), false); // "entered the game" text was moved to P_SpectatorJoinGame
 
 	//reset view if you are changed, or viewing someone who was changed.
-	if (playernum == consoleplayer || displayplayer == playernum)
-		displayplayer = consoleplayer;
+	if (playernum == consoleplayer || displayplayers[0] == playernum)
+		displayplayers[0] = consoleplayer;
 
 	if (G_GametypeHasTeams())
 	{
@@ -3379,11 +3625,11 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
 		{
 			if (playernum == consoleplayer) //CTF and Team Match colors.
 				CV_SetValue(&cv_playercolor, NetPacket.packet.newteam + 5);
-			else if (playernum == secondarydisplayplayer)
+			else if (playernum == displayplayers[1])
 				CV_SetValue(&cv_playercolor2, NetPacket.packet.newteam + 5);
-			else if (playernum == thirddisplayplayer)
+			else if (playernum == displayplayers[2])
 				CV_SetValue(&cv_playercolor3, NetPacket.packet.newteam + 5);
-			else if (playernum == fourthdisplayplayer)
+			else if (playernum == displayplayers[3])
 				CV_SetValue(&cv_playercolor4, NetPacket.packet.newteam + 5);
 		}
 	}
@@ -3391,6 +3637,8 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
 	if (gamestate != GS_LEVEL)
 		return;
 
+	demo_extradata[playernum] |= DXD_PLAYSTATE;
+
 	// Clear player score and rings if a spectator.
 	if (players[playernum].spectator)
 	{
@@ -4045,14 +4293,6 @@ static void Command_Addfile(void)
 		if (*p == '\\' || *p == '/' || *p == ':')
 			break;
 	++p;
-	// check total packet size and no of files currently loaded
-	// See W_LoadWadFile in w_wad.c
-	if ((numwadfiles >= MAX_WADFILES)
-	|| ((packetsizetally + nameonlylength(fn) + 22) > MAXFILENEEDED*sizeof(UINT8)))
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
-		return;
-	}
 
 	WRITESTRINGN(buf_p,p,240);
 
@@ -4167,8 +4407,7 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 	}
 
 	// See W_LoadWadFile in w_wad.c
-	if ((numwadfiles >= MAX_WADFILES)
-	|| ((packetsizetally + nameonlylength(filename) + 22) > MAXFILENEEDED*sizeof(UINT8)))
+	if (numwadfiles >= MAX_WADFILES)
 		toomany = true;
 	else
 		ncs = findfile(filename,md5sum,true);
@@ -4368,12 +4607,22 @@ static void Command_ModDetails_f(void)
 //
 static void Command_ShowGametype_f(void)
 {
+	const char *gametypestr = NULL;
+
 	if (!(netgame || multiplayer)) // print "Single player" instead of "Race"
 	{
 		CONS_Printf(M_GetText("Current gametype is %s\n"), "Single Player");
 		return;
 	}
-	CONS_Printf(M_GetText("Current gametype is %s\n"), gametype_cons_t[gametype].strvalue);
+
+	// get name string for current gametype
+	if (gametype >= 0 && gametype < NUMGAMETYPES)
+		gametypestr = Gametype_Names[gametype];
+
+	if (gametypestr)
+		CONS_Printf(M_GetText("Current gametype is %s\n"), gametypestr);
+	else // string for current gametype was not found above (should never happen)
+		CONS_Printf(M_GetText("Unknown gametype set (%d)\n"), gametype);
 }
 
 /** Plays the intro.
@@ -4449,7 +4698,7 @@ static void PointLimit_OnChange(void)
 
 static void NumLaps_OnChange(void)
 {
-	if (!G_RaceGametype() || (modeattacking || demoplayback))
+	if (!G_RaceGametype() || (modeattacking || demo.playback))
 		return;
 
 	if (server && Playing()
@@ -4517,9 +4766,18 @@ static void TimeLimit_OnChange(void)
   */
 void D_GameTypeChanged(INT32 lastgametype)
 {
-	if (multiplayer)
-		CONS_Printf(M_GetText("Gametype was changed from %s to %s\n"), gametype_cons_t[lastgametype].strvalue, gametype_cons_t[gametype].strvalue);
+	if (netgame)
+	{
+		const char *oldgt = NULL, *newgt = NULL;
 
+		if (lastgametype >= 0 && lastgametype < NUMGAMETYPES)
+			oldgt = Gametype_Names[lastgametype];
+		if (gametype >= 0 && lastgametype < NUMGAMETYPES)
+			newgt = Gametype_Names[gametype];
+
+		if (oldgt && newgt)
+			CONS_Printf(M_GetText("Gametype was changed from %s to %s\n"), oldgt, newgt);
+	}
 	// Only do the following as the server, not as remote admin.
 	// There will always be a server, and this only needs to be done once.
 	if (server && (multiplayer || netgame))
@@ -4889,7 +5147,7 @@ static void Command_ExitLevel_f(void)
 		CONS_Printf(M_GetText("This only works in a netgame.\n"));
 	else if (!(server || (IsPlayerAdmin(consoleplayer))))
 		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
-	else if (gamestate != GS_LEVEL || demoplayback)
+	else if (gamestate != GS_LEVEL || demo.playback)
 		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
 	else
 		SendNetXCmd(XD_EXITLEVEL, NULL, 0);
@@ -4987,13 +5245,13 @@ static void Got_PickVotecmd(UINT8 **cp, INT32 playernum)
 	Y_SetupVoteFinish(pick, level);
 }
 
-/** Prints the number of the displayplayer.
+/** Prints the number of displayplayers[0].
   *
   * \todo Possibly remove this; it was useful for debugging at one point.
   */
 static void Command_Displayplayer_f(void)
 {
-	CONS_Printf(M_GetText("Displayplayer is %d\n"), displayplayer);
+	CONS_Printf(M_GetText("Displayplayer is %d\n"), displayplayers[0]);
 }
 
 /** Quits a game and returns to the title screen.
@@ -5152,27 +5410,11 @@ static void Command_Archivetest_f(void)
 
 /** Makes a change to ::cv_forceskin take effect immediately.
   *
-  * \todo Move the enforcement code out of SendNameAndColor() so this hack
-  *       isn't needed.
   * \sa Command_SetForcedSkin_f, cv_forceskin, forcedskin
   * \author Graue <graue@oceanbase.org>
   */
 static void ForceSkin_OnChange(void)
 {
-	if ((server || IsPlayerAdmin(consoleplayer)) && (cv_forceskin.value < -1 || cv_forceskin.value >= numskins))
-	{
-		if (cv_forceskin.value == -2)
-			CV_SetValue(&cv_forceskin, numskins-1);
-		else
-		{
-			// hack because I can't restrict this and still allow added skins to be used with forceskin.
-			if (!menuactive)
-				CONS_Printf(M_GetText("Valid skin numbers are 0 to %d (-1 disables)\n"), numskins - 1);
-			CV_SetValue(&cv_forceskin, -1);
-		}
-		return;
-	}
-
 	// NOT in SP, silly!
 	if (!(netgame || multiplayer))
 		return;
@@ -5181,7 +5423,7 @@ static void ForceSkin_OnChange(void)
 		CONS_Printf("The server has lifted the forced skin restrictions.\n");
 	else
 	{
-		CONS_Printf("The server is restricting all players to skin \"%s\".\n",skins[cv_forceskin.value].name);
+		CONS_Printf("The server is restricting all players to skin \"%s\".\n",cv_forceskin.string);
 		ForceAllSkins(cv_forceskin.value);
 	}
 }
@@ -5204,7 +5446,7 @@ static void Name2_OnChange(void)
 	if (cv_mute.value) //Secondary player can't be admin.
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
-		CV_StealthSet(&cv_playername2, player_names[secondarydisplayplayer]);
+		CV_StealthSet(&cv_playername2, player_names[displayplayers[1]]);
 	}
 	else
 		SendNameAndColor2();
@@ -5215,7 +5457,7 @@ static void Name3_OnChange(void)
 	if (cv_mute.value) //Third player can't be admin.
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
-		CV_StealthSet(&cv_playername3, player_names[thirddisplayplayer]);
+		CV_StealthSet(&cv_playername3, player_names[displayplayers[2]]);
 	}
 	else
 		SendNameAndColor3();
@@ -5226,7 +5468,7 @@ static void Name4_OnChange(void)
 	if (cv_mute.value) //Secondary player can't be admin.
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
-		CV_StealthSet(&cv_playername4, player_names[fourthdisplayplayer]);
+		CV_StealthSet(&cv_playername4, player_names[displayplayers[3]]);
 	}
 	else
 		SendNameAndColor4();
@@ -5267,12 +5509,12 @@ static void Skin2_OnChange(void)
 	if (!Playing() || !splitscreen)
 		return; // do whatever you want
 
-	if (CanChangeSkin(secondarydisplayplayer) && !P_PlayerMoving(secondarydisplayplayer))
+	if (CanChangeSkin(displayplayers[1]) && !P_PlayerMoving(displayplayers[1]))
 		SendNameAndColor2();
 	else
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
-		CV_StealthSet(&cv_skin2, skins[players[secondarydisplayplayer].skin].name);
+		CV_StealthSet(&cv_skin2, skins[players[displayplayers[1]].skin].name);
 	}
 }
 
@@ -5281,12 +5523,12 @@ static void Skin3_OnChange(void)
 	if (!Playing() || splitscreen < 2)
 		return; // do whatever you want
 
-	if (CanChangeSkin(thirddisplayplayer) && !P_PlayerMoving(thirddisplayplayer))
+	if (CanChangeSkin(displayplayers[2]) && !P_PlayerMoving(displayplayers[2]))
 		SendNameAndColor3();
 	else
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
-		CV_StealthSet(&cv_skin3, skins[players[thirddisplayplayer].skin].name);
+		CV_StealthSet(&cv_skin3, skins[players[displayplayers[2]].skin].name);
 	}
 }
 
@@ -5295,12 +5537,12 @@ static void Skin4_OnChange(void)
 	if (!Playing() || splitscreen < 3)
 		return; // do whatever you want
 
-	if (CanChangeSkin(fourthdisplayplayer) && !P_PlayerMoving(fourthdisplayplayer))
+	if (CanChangeSkin(displayplayers[3]) && !P_PlayerMoving(displayplayers[3]))
 		SendNameAndColor4();
 	else
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
-		CV_StealthSet(&cv_skin4, skins[players[fourthdisplayplayer].skin].name);
+		CV_StealthSet(&cv_skin4, skins[players[displayplayers[3]].skin].name);
 	}
 }
 
@@ -5341,7 +5583,7 @@ static void Color2_OnChange(void)
 	if (!Playing() || !splitscreen)
 		return; // do whatever you want
 
-	if (!P_PlayerMoving(secondarydisplayplayer))
+	if (!P_PlayerMoving(displayplayers[1]))
 	{
 		// Color change menu scrolling fix is no longer necessary
 		SendNameAndColor2();
@@ -5349,7 +5591,7 @@ static void Color2_OnChange(void)
 	else
 	{
 		CV_StealthSetValue(&cv_playercolor2,
-			players[secondarydisplayplayer].skincolor);
+			players[displayplayers[1]].skincolor);
 	}
 }
 
@@ -5358,7 +5600,7 @@ static void Color3_OnChange(void)
 	if (!Playing() || splitscreen < 2)
 		return; // do whatever you want
 
-	if (!P_PlayerMoving(thirddisplayplayer))
+	if (!P_PlayerMoving(displayplayers[2]))
 	{
 		// Color change menu scrolling fix is no longer necessary
 		SendNameAndColor3();
@@ -5366,7 +5608,7 @@ static void Color3_OnChange(void)
 	else
 	{
 		CV_StealthSetValue(&cv_playercolor3,
-			players[thirddisplayplayer].skincolor);
+			players[displayplayers[2]].skincolor);
 	}
 }
 
@@ -5375,7 +5617,7 @@ static void Color4_OnChange(void)
 	if (!Playing() || splitscreen < 3)
 		return; // do whatever you want
 
-	if (!P_PlayerMoving(fourthdisplayplayer))
+	if (!P_PlayerMoving(displayplayers[3]))
 	{
 		// Color change menu scrolling fix is no longer necessary
 		SendNameAndColor4();
@@ -5383,7 +5625,7 @@ static void Color4_OnChange(void)
 	else
 	{
 		CV_StealthSetValue(&cv_playercolor4,
-			players[fourthdisplayplayer].skincolor);
+			players[displayplayers[3]].skincolor);
 	}
 }
 
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 166c5e008b2a1fb5c670202abf3573a553355b67..e6c327abf414c6beaefcd6cfb867d6d0872c030c 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -143,11 +143,9 @@ extern consvar_t cv_ringslinger, cv_soundtest;
 
 extern consvar_t cv_specialrings, cv_powerstones, cv_matchboxes, cv_competitionboxes;
 
-#ifdef NEWPING
 extern consvar_t cv_maxping;
 extern consvar_t cv_pingtimeout;
 extern consvar_t cv_showping;
-#endif
 
 extern consvar_t cv_skipmapcheck;
 
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 99a05840389a9ee49cb8760be1aeecde695422ac..76b66836b22bb0dd089c86e42eca5d7b9e97c162 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -107,19 +107,40 @@ INT32 lastfilenum = -1;
   * Used to have size limiting built in - now handed via W_LoadWadFile in w_wad.c
   *
   */
-UINT8 *PutFileNeeded(void)
+UINT8 *PutFileNeeded(UINT16 firstfile)
 {
-	size_t i, count = 0;
-	UINT8 *p = netbuffer->u.serverinfo.fileneeded;
+	size_t i;
+	UINT8 count = 0;
+	UINT8 *p_start = netbuffer->packettype == PT_MOREFILESNEEDED ? netbuffer->u.filesneededcfg.files : netbuffer->u.serverinfo.fileneeded;
+	UINT8 *p = p_start;
 	char wadfilename[MAX_WADPATH] = "";
 	UINT8 filestatus;
 
-	for (i = 0; i < numwadfiles; i++)
+	for (i = mainwads; i < numwadfiles; i++)
 	{
 		// If it has only music/sound lumps, don't put it in the list
 		if (!wadfiles[i]->important)
 			continue;
 
+		if (firstfile)
+		{ // Skip files until we reach the first file.
+			firstfile--;
+			continue;
+		}
+
+		nameonly(strcpy(wadfilename, wadfiles[i]->filename));
+
+		// Look below at the WRITE macros to understand what these numbers mean.
+		if (p + 1 + 4 + min(strlen(wadfilename) + 1, MAX_WADPATH) + 16 > p_start + MAXFILENEEDED)
+		{
+			// Too many files to send all at once
+			if (netbuffer->packettype == PT_MOREFILESNEEDED)
+				netbuffer->u.filesneededcfg.more = 1;
+			else
+				netbuffer->u.serverinfo.kartvars |= SV_LOTSOFADDONS;
+			break;
+		}
+
 		filestatus = 1; // Importance - not really used any more, holds 1 by default for backwards compat with MS
 
 		// Store in the upper four bits
@@ -134,30 +155,32 @@ UINT8 *PutFileNeeded(void)
 
 		count++;
 		WRITEUINT32(p, wadfiles[i]->filesize);
-		nameonly(strcpy(wadfilename, wadfiles[i]->filename));
 		WRITESTRINGN(p, wadfilename, MAX_WADPATH);
 		WRITEMEM(p, wadfiles[i]->md5sum, 16);
 	}
-	netbuffer->u.serverinfo.fileneedednum = (UINT8)count;
+	if (netbuffer->packettype == PT_MOREFILESNEEDED)
+		netbuffer->u.filesneededcfg.num = count;
+	else
+		netbuffer->u.serverinfo.fileneedednum = count;
 
 	return p;
 }
 
 /** Parses the serverinfo packet and fills the fileneeded table on client
   *
-  * \param fileneedednum_parm The number of files needed to join the server
+  * \param fileneedednum_parm The number of files (sent in this page) needed to join the server
   * \param fileneededstr The memory block containing the list of needed files
-  *
+  * \param firstfile The first file index to read from
   */
-void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr)
+void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile)
 {
 	INT32 i;
 	UINT8 *p;
 	UINT8 filestatus;
 
-	fileneedednum = fileneedednum_parm;
+	fileneedednum = firstfile + fileneedednum_parm;
 	p = (UINT8 *)fileneededstr;
-	for (i = 0; i < fileneedednum; i++)
+	for (i = firstfile; i < fileneedednum; i++)
 	{
 		fileneeded[i].status = FS_NOTFOUND; // We haven't even started looking for the file yet
 		filestatus = READUINT8(p); // The first byte is the file status
@@ -338,7 +361,8 @@ INT32 CL_CheckFiles(void)
 	// the first is the iwad (the main wad file)
 	// we don't care if it's called srb2.srb or srb2.wad.
 	// Never download the IWAD, just assume it's there and identical
-	fileneeded[0].status = FS_OPEN;
+	// ...No! Why were we sending the base wads to begin with??
+	//fileneeded[0].status = FS_OPEN;
 
 	// Modified game handling -- check for an identical file list
 	// must be identical in files loaded AND in order
@@ -346,7 +370,7 @@ INT32 CL_CheckFiles(void)
 	if (modifiedgame)
 	{
 		CONS_Debug(DBG_NETPLAY, "game is modified; only doing basic checks\n");
-		for (i = 1, j = 1; i < fileneedednum || j < numwadfiles;)
+		for (i = 0, j = mainwads; i < fileneedednum || j < numwadfiles;)
 		{
 			if (j < numwadfiles && !wadfiles[j]->important)
 			{
@@ -373,15 +397,12 @@ INT32 CL_CheckFiles(void)
 		return 1;
 	}
 
-	// See W_LoadWadFile in w_wad.c
-	packetsize = packetsizetally;
-
-	for (i = 1; i < fileneedednum; i++)
+	for (i = 0; i < fileneedednum; i++)
 	{
 		CONS_Debug(DBG_NETPLAY, "searching for '%s' ", fileneeded[i].filename);
 
 		// Check in already loaded files
-		for (j = 1; wadfiles[j]; j++)
+		for (j = mainwads; wadfiles[j]; j++)
 		{
 			nameonly(strcpy(wadfilename, wadfiles[j]->filename));
 			if (!stricmp(wadfilename, fileneeded[i].filename) &&
@@ -397,8 +418,7 @@ INT32 CL_CheckFiles(void)
 
 		packetsize += nameonlylength(fileneeded[i].filename) + 22;
 
-		if ((numwadfiles+filestoget >= MAX_WADFILES)
-		|| (packetsize > MAXFILENEEDED*sizeof(UINT8)))
+		if (mainwads+filestoget >= MAX_WADFILES)
 			return 3;
 
 		filestoget++;
diff --git a/src/d_netfil.h b/src/d_netfil.h
index 3d7c2ed599dafb0b5eb4c772403b42a771d8b236..2f0333311e5cfd31e8143a631476e8226e3098dd 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -53,8 +53,8 @@ extern char downloaddir[512];
 extern INT32 lastfilenum;
 #endif
 
-UINT8 *PutFileNeeded(void);
-void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr);
+UINT8 *PutFileNeeded(UINT16 firstfile);
+void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile);
 void CL_PrepareDownloadSaveGame(const char *tmpsave);
 
 INT32 CL_CheckFiles(void);
diff --git a/src/d_player.h b/src/d_player.h
index decd96552e3363bf2c0fa5dbf513f83265b93854..114674ff5a3f84e1d6d6ac9be9539e4684ef853d 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -35,33 +35,6 @@ typedef enum
 	SF_HIRES = 1, // Draw the sprite 2x as small?
 } skinflags_t;
 
-//Primary and secondary skin abilities
-typedef enum
-{
-	CA_NONE=0,
-	CA_THOK,
-	CA_FLY,
-	CA_GLIDEANDCLIMB,
-	CA_HOMINGTHOK,
-	CA_SWIM,
-	CA_DOUBLEJUMP,
-	CA_FLOAT,
-	CA_SLOWFALL,
-	CA_TELEKINESIS,
-	CA_FALLSWITCH,
-	CA_JUMPBOOST,
-	CA_AIRDRILL,
-	CA_JUMPTHOK
-} charability_t;
-
-//Secondary skin abilities
-typedef enum
-{
-	CA2_NONE=0,
-	CA2_SPINDASH,
-	CA2_MULTIABILITY
-} charability2_t;
-
 //
 // Player states.
 //
@@ -300,6 +273,7 @@ typedef enum
 	k_boostpower,		// Base boost value, for offroad
 	k_speedboost,		// Boost value smoothing for max speed
 	k_accelboost,		// Boost value smoothing for acceleration
+	k_boostangle,		// angle set when not spun out OR boosted to determine what direction you should keep going at if you're spun out and boosted.
 	k_boostcam,			// Camera push forward on boost
 	k_destboostcam,		// Ditto
 	k_timeovercam,		// Camera timer for leaving behind or not
@@ -442,29 +416,8 @@ typedef struct player_s
 	UINT8 kartweight; // Kart weight stat between 1 and 9
 	//
 
-	fixed_t normalspeed; // Normal ground
-	fixed_t runspeed; // Speed you break into the run animation
-	UINT8 thrustfactor; // Thrust = thrustfactor * acceleration
-	UINT8 accelstart; // Starting acceleration if speed = 0.
-	UINT8 acceleration; // Acceleration
-
-	// See charability_t and charability2_t for more information.
-	UINT8 charability; // Ability definition
-	UINT8 charability2; // Secondary ability definition
-
 	UINT32 charflags; // Extra abilities/settings for skins (combinable stuff)
 	                 // See SF_ flags
-
-	mobjtype_t thokitem; // Object # to spawn for the thok
-	mobjtype_t spinitem; // Object # to spawn for spindash/spinning
-	mobjtype_t revitem; // Object # to spawn for spindash/spinning
-
-	fixed_t actionspd; // Speed of thok/glide/fly
-	fixed_t mindash; // Minimum spindash speed
-	fixed_t maxdash; // Maximum spindash speed
-
-	fixed_t jumpfactor; // How high can the player jump?
-
 	SINT8 lives;
 	SINT8 continues; // continues that player has acquired
 
diff --git a/src/dehacked.c b/src/dehacked.c
index 1c88fe8328ba35a46efce087b884a697ef967dd6..b311a860cfc5cac4cfe0ceed9aed873197151f45 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -1216,6 +1216,13 @@ static void readlevelheader(MYFILE *f, INT32 num)
 #endif
 			else if (fastcmp(word, "MUSICTRACK"))
 				mapheaderinfo[num-1]->mustrack = ((UINT16)i - 1);
+			else if (fastcmp(word, "MUSICPOS"))
+				mapheaderinfo[num-1]->muspos = (UINT32)get_number(word2);
+			else if (fastcmp(word, "MUSICINTERFADEOUT"))
+				mapheaderinfo[num-1]->musinterfadeout = (UINT32)get_number(word2);
+			else if (fastcmp(word, "MUSICINTER"))
+				deh_strlcpy(mapheaderinfo[num-1]->musintername, word2,
+					sizeof(mapheaderinfo[num-1]->musintername), va("Level header %d: intermission music", num));
 			else if (fastcmp(word, "FORCECHARACTER"))
 			{
 				strlcpy(mapheaderinfo[num-1]->forcecharacter, word2, SKINNAMESIZE+1);
@@ -1539,6 +1546,11 @@ static void readcutscenescene(MYFILE *f, INT32 num, INT32 scenenum)
 				DEH_WriteUndoline(word, va("%u", cutscenes[num]->scene[scenenum].musswitchflags), UNDO_NONE);
 				cutscenes[num]->scene[scenenum].musswitchflags = ((UINT16)i) & MUSIC_TRACKMASK;
 			}
+			else if (fastcmp(word, "MUSICPOS"))
+			{
+				DEH_WriteUndoline(word, va("%u", cutscenes[num]->scene[scenenum].musswitchposition), UNDO_NONE);
+				cutscenes[num]->scene[scenenum].musswitchposition = (UINT32)get_number(word2);
+			}
 			else if (fastcmp(word, "MUSICLOOP"))
 			{
 				DEH_WriteUndoline(word, va("%u", cutscenes[num]->scene[scenenum].musicloop), UNDO_NONE);
@@ -2142,11 +2154,12 @@ static boolean GoodDataFileName(const char *s)
 	p = s + strlen(s) - strlen(tail);
 	if (p <= s) return false; // too short
 	if (!fasticmp(p, tail)) return false; // doesn't end in .dat
-#ifdef DELFILE
-	if (fasticmp(s, "gamedata.dat") && !disableundo) return false;
-#else
-	if (fasticmp(s, "gamedata.dat")) return false;
-#endif
+
+	if (fasticmp(s, "gamedata.dat")) return false; // Vanilla SRB2 gamedata
+	if (fasticmp(s, "main.dat")) return false; // Vanilla SRB2 time attack replay folder
+	if (fasticmp(s, "kartdata.dat")) return false; // SRB2Kart gamedata
+	if (fasticmp(s, "kart.dat")) return false; // SRB2Kart time attack replay folder
+	if (fasticmp(s, "online.dat")) return false; // SRB2Kart online replay folder
 
 	return true;
 }
@@ -8135,29 +8148,35 @@ static const char *const ML_LIST[16] = {
 
 // This DOES differ from r_draw's Color_Names, unfortunately.
 // Also includes Super colors
-static const char *COLOR_ENUMS[] = {					// Rejigged for Kart.
+static const char *COLOR_ENUMS[] = { // Rejigged for Kart.
 	"NONE",			// SKINCOLOR_NONE
 	"WHITE",		// SKINCOLOR_WHITE
 	"SILVER",		// SKINCOLOR_SILVER
 	"GREY",			// SKINCOLOR_GREY
 	"NICKEL",		// SKINCOLOR_NICKEL
 	"BLACK",		// SKINCOLOR_BLACK
+	"SKUNK",		// SKINCOLOR_SKUNK
 	"FAIRY",		// SKINCOLOR_FAIRY
 	"POPCORN",		// SKINCOLOR_POPCORN
+	"ARTICHOKE",	// SKINCOLOR_ARTICHOKE
+	"PIGEON",		// SKINCOLOR_PIGEON
 	"SEPIA",		// SKINCOLOR_SEPIA
 	"BEIGE",		// SKINCOLOR_BEIGE
+	"WALNUT",		// SKINCOLOR_WALNUT
 	"BROWN",		// SKINCOLOR_BROWN
 	"LEATHER",		// SKINCOLOR_LEATHER
 	"SALMON",		// SKINCOLOR_SALMON
 	"PINK",			// SKINCOLOR_PINK
 	"ROSE",			// SKINCOLOR_ROSE
 	"BRICK",		// SKINCOLOR_BRICK
+	"CINNAMON",		// SKINCOLOR_CINNAMON
 	"RUBY",			// SKINCOLOR_RUBY
 	"RASPBERRY",	// SKINCOLOR_RASPBERRY
 	"CHERRY",		// SKINCOLOR_CHERRY
 	"RED",			// SKINCOLOR_RED
 	"CRIMSON",		// SKINCOLOR_CRIMSON
 	"MAROON",		// SKINCOLOR_MAROON
+	"LEMONADE",		// SKINCOLOR_LEMONADE
 	"FLAME",		// SKINCOLOR_FLAME
 	"SCARLET",		// SKINCOLOR_SCARLET
 	"KETCHUP",		// SKINCOLOR_KETCHUP
@@ -8176,8 +8195,10 @@ static const char *COLOR_ENUMS[] = {					// Rejigged for Kart.
 	"ROYAL",		// SKINCOLOR_ROYAL
 	"BRONZE",		// SKINCOLOR_BRONZE
 	"COPPER",		// SKINCOLOR_COPPER
+	"QUARRY",		// SKINCOLOR_QUARRY
 	"YELLOW",		// SKINCOLOR_YELLOW
 	"MUSTARD",		// SKINCOLOR_MUSTARD
+	"CROCODILE",	// SKINCOLOR_CROCODILE
 	"OLIVE",		// SKINCOLOR_OLIVE
 	"VOMIT",		// SKINCOLOR_VOMIT
 	"GARDEN",		// SKINCOLOR_GARDEN
@@ -8197,6 +8218,7 @@ static const char *COLOR_ENUMS[] = {					// Rejigged for Kart.
 	"PLAGUE",		// SKINCOLOR_PLAGUE
 	"ALGAE",		// SKINCOLOR_ALGAE
 	"CARIBBEAN",	// SKINCOLOR_CARIBBEAN
+	"AZURE",		// SKINCOLOR_AZURE
 	"AQUA",			// SKINCOLOR_AQUA
 	"TEAL",			// SKINCOLOR_TEAL
 	"CYAN",			// SKINCOLOR_CYAN
@@ -8206,7 +8228,9 @@ static const char *COLOR_ENUMS[] = {					// Rejigged for Kart.
 	"PLATINUM",		// SKINCOLOR_PLATINUM
 	"SLATE",		// SKINCOLOR_SLATE
 	"STEEL",		// SKINCOLOR_STEEL
+	"THUNDER",		// SKINCOLOR_THUNDER
 	"RUST",			// SKINCOLOR_RUST
+	"WRISTWATCH",	// SKINCOLOR_WRISTWATCH
 	"JET",			// SKINCOLOR_JET
 	"SAPPHIRE",		// SKINCOLOR_SAPPHIRE
 	"PERIWINKLE",	// SKINCOLOR_PERIWINKLE
@@ -8227,10 +8251,6 @@ static const char *COLOR_ENUMS[] = {					// Rejigged for Kart.
 	"POMEGRANATE",	// SKINCOLOR_POMEGRANATE
 	"LILAC",		// SKINCOLOR_LILAC
 
-
-
-
-
 	// Special super colors
 	// Super Sonic Yellow
 	"SUPER1",		// SKINCOLOR_SUPER1
@@ -8371,6 +8391,7 @@ static const char *const KARTSTUFF_LIST[] = {
 	"BOOSTPOWER",
 	"SPEEDBOOST",
 	"ACCELBOOST",
+	"BOOSTANGLE",
 	"BOOSTCAM",
 	"DESTBOOSTCAM",
 	"TIMEOVERCAM",
@@ -8490,6 +8511,7 @@ struct {
 
 	// doomdef.h constants
 	{"TICRATE",TICRATE},
+	{"MUSICRATE",MUSICRATE},
 	{"RING_DIST",RING_DIST},
 	{"PUSHACCEL",PUSHACCEL},
 	{"MODID",MODID}, // I don't know, I just thought it would be cool for a wad to potentially know what mod it was loaded into.
@@ -8642,27 +8664,6 @@ struct {
 	// Character flags (skinflags_t)
 	{"SF_HIRES",SF_HIRES},
 
-	// Character abilities!
-	// Primary
-	{"CA_NONE",CA_NONE}, // now slot 0!
-	{"CA_THOK",CA_THOK},
-	{"CA_FLY",CA_FLY},
-	{"CA_GLIDEANDCLIMB",CA_GLIDEANDCLIMB},
-	{"CA_HOMINGTHOK",CA_HOMINGTHOK},
-	{"CA_DOUBLEJUMP",CA_DOUBLEJUMP},
-	{"CA_FLOAT",CA_FLOAT},
-	{"CA_SLOWFALL",CA_SLOWFALL},
-	{"CA_SWIM",CA_SWIM},
-	{"CA_TELEKINESIS",CA_TELEKINESIS},
-	{"CA_FALLSWITCH",CA_FALLSWITCH},
-	{"CA_JUMPBOOST",CA_JUMPBOOST},
-	{"CA_AIRDRILL",CA_AIRDRILL},
-	{"CA_JUMPTHOK",CA_JUMPTHOK},
-	// Secondary
-	{"CA2_NONE",CA2_NONE}, // now slot 0!
-	{"CA2_SPINDASH",CA2_SPINDASH},
-	{"CA2_MULTIABILITY",CA2_MULTIABILITY},
-
 	// Sound flags
 	{"SF_TOTALLYSINGLE",SF_TOTALLYSINGLE},
 	{"SF_NOMULTIPLESOUND",SF_NOMULTIPLESOUND},
@@ -9799,7 +9800,7 @@ static inline int lib_getenum(lua_State *L)
 
 	// DYNAMIC variables too!!
 	// Try not to add anything that would break netgames or timeattack replays here.
-	// You know, like consoleplayer, displayplayer, secondarydisplayplayer, or gametime.
+	// You know, like consoleplayer, displayplayers, or gametime.
 	if (fastcmp(word,"gamemap")) {
 		lua_pushinteger(L, gamemap);
 		return 1;
@@ -9872,8 +9873,11 @@ static inline int lib_getenum(lua_State *L)
 	} else if (fastcmp(word,"mapmusflags")) {
 		lua_pushinteger(L, mapmusflags);
 		return 1;
+	} else if (fastcmp(word,"mapmusposition")) {
+		lua_pushinteger(L, mapmusposition);
+		return 1;
 	} else if (fastcmp(word,"server")) {
-		if ((!multiplayer || !netgame) && !playeringame[serverplayer])
+		if ((!multiplayer || !(netgame || demo.playback)) && !playeringame[serverplayer])
 			return 0;
 		LUA_PushUserdata(L, &players[serverplayer], META_PLAYER);
 		return 1;
@@ -9925,6 +9929,9 @@ static inline int lib_getenum(lua_State *L)
 	} else if (fastcmp(word,"mapobjectscale")) {
 		lua_pushinteger(L, mapobjectscale);
 		return 1;
+	} else if (fastcmp(word,"numlaps")) {
+		lua_pushinteger(L, cv_numlaps.value);
+		return 1;
 	}
 	return 0;
 }
diff --git a/src/djgppdos/i_sound.c b/src/djgppdos/i_sound.c
index 52c90aac2d2450ede35eb9773e8b9a222e661783..88b5986221496295ae86e81015db75df94bd2a07 100644
--- a/src/djgppdos/i_sound.c
+++ b/src/djgppdos/i_sound.c
@@ -438,6 +438,37 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+// MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+    (void)position;
+    return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+    return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -545,3 +576,44 @@ int I_QrySongPlaying(int handle)
 	return (midi_pos==-1);
 }
 #endif
+
+/// ------------------------
+// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/doomdef.h b/src/doomdef.h
index 6664ff51a41de52d0a7be5d064f42865e527d3fa..24b52e8d2e815f2da1e34dcee89f068aeaa62184 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -149,14 +149,18 @@ extern FILE *logstream;
 // most interface strings are ignored in development mode.
 // we use comprevision and compbranch instead.
 #else
-#define VERSION    100 // Game version
-#define SUBVERSION 4 // more precise version number
-#define VERSIONSTRING "v1.0.4"
-#define VERSIONSTRINGW L"v1.0.4"
-// Hey! If you change this, add 1 to the MODVERSION below!
-// Otherwise we can't force updates!
+#define VERSION    110 // Game version
+#define SUBVERSION 0 // more precise version number
+#define VERSIONSTRING "v1.1"
+#define VERSIONSTRINGW L"v1.1"
+// Hey! If you change this, add 1 to the MODVERSION below! Otherwise we can't force updates!
+// And change CMakeLists.txt, for CMake users!
+// AND appveyor.yml, for the build bots!
 #endif
 
+// Maintain compatibility with 1.0.x record attack replays?
+#define DEMO_COMPAT_100
+
 // Does this version require an added patch file?
 // Comment or uncomment this as necessary.
 //#define USE_PATCH_DTA
@@ -221,7 +225,7 @@ extern FILE *logstream;
 // it's only for detection of the version the player is using so the MS can alert them of an update.
 // Only set it higher, not lower, obviously.
 // Note that we use this to help keep internal testing in check; this is why v2.1.0 is not version "1".
-#define MODVERSION 4
+#define MODVERSION 5
 
 // Filter consvars by version
 // To version config.cfg, MAJOREXECVERSION is set equal to MODVERSION automatically.
@@ -248,6 +252,9 @@ extern FILE *logstream;
 #define PLAYERSMASK (MAXPLAYERS-1)
 #define MAXPLAYERNAME 21
 
+// Master Server compatibility ONLY
+#define MSCOMPAT_MAXPLAYERS (32)
+
 typedef enum
 {
 	SKINCOLOR_NONE = 0,
@@ -256,22 +263,28 @@ typedef enum
 	SKINCOLOR_GREY,
 	SKINCOLOR_NICKEL,
 	SKINCOLOR_BLACK,
+	SKINCOLOR_SKUNK,
 	SKINCOLOR_FAIRY,
 	SKINCOLOR_POPCORN,
+	SKINCOLOR_ARTICHOKE,
+	SKINCOLOR_PIGEON,
 	SKINCOLOR_SEPIA,
 	SKINCOLOR_BEIGE,
+	SKINCOLOR_WALNUT,
 	SKINCOLOR_BROWN,
 	SKINCOLOR_LEATHER,
 	SKINCOLOR_SALMON,
 	SKINCOLOR_PINK,
 	SKINCOLOR_ROSE,
 	SKINCOLOR_BRICK,
+	SKINCOLOR_CINNAMON,
 	SKINCOLOR_RUBY,
 	SKINCOLOR_RASPBERRY,
 	SKINCOLOR_CHERRY,
 	SKINCOLOR_RED,
 	SKINCOLOR_CRIMSON,
 	SKINCOLOR_MAROON,
+	SKINCOLOR_LEMONADE,
 	SKINCOLOR_FLAME,
 	SKINCOLOR_SCARLET,
 	SKINCOLOR_KETCHUP,
@@ -290,8 +303,10 @@ typedef enum
 	SKINCOLOR_ROYAL,
 	SKINCOLOR_BRONZE,
 	SKINCOLOR_COPPER,
+	SKINCOLOR_QUARRY,
 	SKINCOLOR_YELLOW,
 	SKINCOLOR_MUSTARD,
+	SKINCOLOR_CROCODILE,
 	SKINCOLOR_OLIVE,
 	SKINCOLOR_VOMIT,
 	SKINCOLOR_GARDEN,
@@ -311,6 +326,7 @@ typedef enum
 	SKINCOLOR_PLAGUE,
 	SKINCOLOR_ALGAE,
 	SKINCOLOR_CARIBBEAN,
+	SKINCOLOR_AZURE,
 	SKINCOLOR_AQUA,
 	SKINCOLOR_TEAL,
 	SKINCOLOR_CYAN,
@@ -320,7 +336,9 @@ typedef enum
 	SKINCOLOR_PLATINUM,
 	SKINCOLOR_SLATE,
 	SKINCOLOR_STEEL,
+	SKINCOLOR_THUNDER,
 	SKINCOLOR_RUST,
+	SKINCOLOR_WRISTWATCH,
 	SKINCOLOR_JET,
 	SKINCOLOR_SAPPHIRE, // sweet mother, i cannot weave - slender aphrodite has overcome me with longing for a girl
 	SKINCOLOR_PERIWINKLE,
@@ -418,6 +436,8 @@ typedef enum
 #define NEWTICRATERATIO 1 // try 4 for 140 fps :)
 #define NEWTICRATE (TICRATE*NEWTICRATERATIO)
 
+#define MUSICRATE 1000 // sound timing is calculated by milliseconds
+
 #define RING_DIST 1280*FRACUNIT // how close you need to be to a ring to attract it
 
 #define PUSHACCEL (2*FRACUNIT) // Acceleration for MF2_SLIDEPUSH items.
@@ -601,9 +621,6 @@ extern const char *compdate, *comptime, *comprevision, *compbranch;
 ///	Polyobject fake flat code
 #define POLYOBJECTS_PLANES
 
-///	Improved way of dealing with ping values and a ping limit.
-#define NEWPING
-
 ///	See name of player in your crosshair
 #define SEENAMES
 
diff --git a/src/doomstat.h b/src/doomstat.h
index 834b3a7cf12d058c61b7b9381392f1d3bacb5423..1f855da27b9b62d003216160a498f7119935cf39 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -33,8 +33,10 @@
 extern INT16 gamemap;
 extern char mapmusname[7];
 extern UINT16 mapmusflags;
+extern UINT32 mapmusposition;
 #define MUSIC_TRACKMASK   0x0FFF // ----************
 #define MUSIC_RELOADRESET 0x8000 // *---------------
+#define MUSIC_FORCERESET  0x4000 // -*--------------
 // Use other bits if necessary.
 
 extern INT16 maptol;
@@ -77,7 +79,10 @@ extern boolean addedtogame; // true after the server has added you
 extern boolean multiplayer;
 
 extern INT16 gametype;
+
+#define MAXSPLITSCREENPLAYERS 4 // Max number of players on a single computer
 extern UINT8 splitscreen;
+
 extern boolean circuitmap; // Does this level have 'circuit mode'?
 extern boolean fromlevelselect;
 extern boolean forceresetplayers, deferencoremode;
@@ -106,14 +111,8 @@ extern UINT8 window_notinfocus; // are we in focus? (backend independant -- hand
 extern boolean nodrawers;
 extern boolean noblit;
 extern boolean lastdraw;
-extern postimg_t postimgtype;
-extern INT32 postimgparam;
-extern postimg_t postimgtype2;
-extern INT32 postimgparam2;
-extern postimg_t postimgtype3;
-extern INT32 postimgparam3;
-extern postimg_t postimgtype4;
-extern INT32 postimgparam4;
+extern postimg_t postimgtype[MAXSPLITSCREENPLAYERS];
+extern INT32 postimgparam[MAXSPLITSCREENPLAYERS];
 
 extern INT32 viewwindowx, viewwindowy;
 extern INT32 viewwidth, scaledviewwidth;
@@ -122,10 +121,7 @@ extern boolean gamedataloaded;
 
 // Player taking events, and displaying.
 extern INT32 consoleplayer;
-extern INT32 displayplayer;
-extern INT32 secondarydisplayplayer; // for splitscreen
-extern INT32 thirddisplayplayer;
-extern INT32 fourthdisplayplayer;
+extern INT32 displayplayers[MAXSPLITSCREENPLAYERS];
 
 // Maps of special importance
 extern INT16 spstage_start;
@@ -156,6 +152,7 @@ typedef struct
 
 	char   musswitch[7];
 	UINT16 musswitchflags;
+	UINT32 musswitchposition;
 
 	UINT8 fadecolor; // Color number for fade, 0 means don't do the first fade
 	UINT8 fadeinid;  // ID of the first fade, to a color -- ignored if fadecolor is 0
@@ -227,6 +224,7 @@ typedef struct
 	INT16 nextlevel;       ///< Map number of next level, or 1100-1102 to end.
 	char musname[7];       ///< Music track to play. "" for no music.
 	UINT16 mustrack;       ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore.
+	UINT32 muspos;    ///< Music position to jump to.
 	char forcecharacter[17];  ///< (SKINNAMESIZE+1) Skin to switch to or "" to disable.
 	UINT8 weather;         ///< 0 = sunny day, 1 = storm, 2 = snow, 3 = rain, 4 = blank, 5 = thunder w/o rain, 6 = rain w/o lightning, 7 = heat wave.
 	INT16 skynum;          ///< Sky number to use.
@@ -260,6 +258,10 @@ typedef struct
 	//boolean automap;    ///< Displays a level's white map outline in modified games
 	fixed_t mobj_scale; ///< Replacement for TOL_ERZ3
 
+	// Music stuff.
+	UINT32 musinterfadeout;  ///< Fade out level music on intermission screen in milliseconds
+	char musintername[7];    ///< Intermission screen music.
+
 	// Lua stuff.
 	// (This is not ifdeffed so the map header structure can stay identical, just in case.)
 	UINT8 numCustomOptions;     ///< Internal. For Lua custom value support.
@@ -330,7 +332,10 @@ enum GameType // SRB2Kart
 	GT_HIDEANDSEEK,
 	GT_CTF
 };
-// If you alter this list, update gametype_cons_t in m_menu.c
+// If you alter this list, update dehacked.c, and Gametype_Names in g_game.c
+
+// String names for gametypes
+extern const char *Gametype_Names[NUMGAMETYPES];
 
 extern tic_t totalplaytime;
 extern UINT32 matchesplayed;
@@ -543,9 +548,7 @@ extern consvar_t cv_forceskin; // force clients to use the server's skin
 extern consvar_t cv_downloading; // allow clients to downloading WADs.
 extern consvar_t cv_nettimeout; // SRB2Kart: Advanced server options menu
 extern consvar_t cv_jointimeout;
-#ifdef NEWPING
 extern consvar_t cv_maxping;
-#endif
 extern ticcmd_t netcmds[BACKUPTICS][MAXPLAYERS];
 extern INT32 serverplayer;
 extern INT32 adminplayers[MAXPLAYERS];
diff --git a/src/doomtype.h b/src/doomtype.h
index 5d6434845a7013422f9a7eebf661f7fce641c57b..67491abb326bade92341f06af59b4d6c747a1bc9 100644
--- a/src/doomtype.h
+++ b/src/doomtype.h
@@ -366,16 +366,18 @@ size_t strlcpy(char *dst, const char *src, size_t siz);
 
 /* Miscellaneous types that don't fit anywhere else (Can this be changed?) */
 
+typedef struct
+{
+	UINT8 red;
+	UINT8 green;
+	UINT8 blue;
+	UINT8 alpha;
+} byteColor_t;
+
 union FColorRGBA
 {
 	UINT32 rgba;
-	struct
-	{
-		UINT8 red;
-		UINT8 green;
-		UINT8 blue;
-		UINT8 alpha;
-	} s;
+	byteColor_t s;
 } ATTRPACK;
 typedef union FColorRGBA RGBA_t;
 
diff --git a/src/dummy/i_sound.c b/src/dummy/i_sound.c
index 7275bb1ae5d9345e035aa775b602b5fb3bf4f8a1..f09158e013639c8a506058a0eaa7b1259d1285c6 100644
--- a/src/dummy/i_sound.c
+++ b/src/dummy/i_sound.c
@@ -95,6 +95,37 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+    (void)position;
+    return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+    return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -142,4 +173,45 @@ boolean I_SetSongTrack(int track)
 {
 	(void)track;
 	return false;
-}
\ No newline at end of file
+}
+
+/// ------------------------
+//  MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/f_finale.c b/src/f_finale.c
index 02d2eed5996bcee4985c939a5407fb81f520b4cd..2bf5c7438432d221c6263ef1994af370924032a3 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -238,7 +238,6 @@ void F_StartIntro(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
-	CON_ClearHUD();
 	F_NewCutscene(introtext[0]);
 
 	intro_scenenum = 0;
@@ -439,6 +438,7 @@ static const char *credits[] = {
 	"",
 	"\1Support Programming",
 	"Colette \"fickleheart\" Bordelon",
+	"James R.",
 	"\"Lat\'\"",
 	"\"Monster Iestyn\"",
 	"\"Shuffle\"",
@@ -460,9 +460,13 @@ static const char *credits[] = {
 	"\"ZarroTsu\"",
 	"",
 	"\1External Artists",
+	"\"1-Up Mason\"",
+	"\"Chengi\"",
 	"\"Chrispy\"",
 	"\"DirkTheHusky\"",
+	"\"LJSTAR\"",
 	"\"MotorRoach\"",
+	"\"Mr. McScrewup\"",
 	"\"Nev3r\"",
 	"\"Ritz\"",
 	"\"Rob\"",
@@ -471,6 +475,7 @@ static const char *credits[] = {
 	"\"Spherallic\"",
 	"\"VAdaPEGA\"",
 	"\"Virt\"",
+	"\"Voltrix\"",
 	"\"zxyspku\"",
 	"",
 	"\1Sound Design",
@@ -497,13 +502,18 @@ static const char *credits[] = {
 	"\"DrTapeworm\"",
 	"Paul \"Boinciel\" Clempson",
 	"Sherman \"CoatRack\" DesJardins",
+	"Colette \"fickleheart\" Bordelon",
 	"Vivian \"toaster\" Grannell",
 	"James \"SeventhSentinel\" Hall",
 	"\"Lat\'\"",
+	"\"MK\"",
+	"\"Ninferno\"",
 	"Sean \"Sryder\" Ryder",
 	"\"Ryuspark\"",
 	"\"Simsmagic\"",
 	"\"SP47\"",
+	"\"TG\"",
+	"\"Victor Rush Turbo\"",
 	"\"ZarroTsu\"",
 	"",
 	"\1Testing",
@@ -556,7 +566,7 @@ static struct {
 	// This Tyler52 gag is troublesome
 	// Alignment should be ((spaces+1 * 100) + (headers+1 * 38) + (lines * 15))
 	// Current max image spacing: (200*17)
-	{112, (15*100)+(17*38)+(72*15), "TYLER52", SKINCOLOR_NONE},
+	{112, (15*100)+(17*38)+(86*15), "TYLER52", SKINCOLOR_NONE},
 	{0, 0, NULL, SKINCOLOR_NONE}
 };
 
@@ -580,10 +590,10 @@ void F_StartCredits(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
-	CON_ClearHUD();
 	S_StopMusic();
 
 	S_ChangeMusicInternal("credit", false);
+	S_ShowMusicCredit();
 
 	finalecount = 0;
 	animtimer = 0;
@@ -773,7 +783,6 @@ void F_StartGameEvaluation(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
-	CON_ClearHUD();
 
 	finalecount = 0;
 }
@@ -883,7 +892,6 @@ void F_StartGameEnd(void)
 	gameaction = ga_nothing;
 	paused = false;
 	CON_ToggleOff();
-	CON_ClearHUD();
 	S_StopMusic();
 
 	// In case menus are still up?!!
@@ -927,9 +935,9 @@ void F_StartTitleScreen(void)
 	// IWAD dependent stuff.
 
 	// music is started in the ticker
-	if (!fromtitledemo) // SRB2Kart: Don't reset music if the right track is already playing
+	if (!demo.fromtitle) // SRB2Kart: Don't reset music if the right track is already playing
 		S_StopMusic();
-	fromtitledemo = false;
+	demo.fromtitle = false;
 
 	animtimer = 0;
 
@@ -1042,11 +1050,29 @@ void F_TitleScreenTicker(boolean run)
 	// is it time?
 	if (!(--demoIdleLeft))
 	{
+		//static boolean use_netreplay = false;
+
 		char dname[9];
 		lumpnum_t l;
 		const char *mapname;
 		UINT8 numstaff;
 
+		//@TODO uncomment this when this goes into vanilla
+		/*if ((use_netreplay = !use_netreplay))*/
+		{
+			numstaff = 1;
+			while ((l = W_CheckNumForName(va("TDEMO%03u", numstaff))) != LUMPERROR)
+				numstaff++;
+			numstaff--;
+
+			if (numstaff)
+			{
+				numstaff = M_RandomKey(numstaff)+1;
+				snprintf(dname, 9, "TDEMO%03u", numstaff);
+				goto loadreplay;
+			}
+		}
+
 		// prevent console spam if failed
 		demoIdleLeft = demoIdleTime;
 
@@ -1097,7 +1123,10 @@ void F_TitleScreenTicker(boolean run)
 			return;
 		}*/
 
-		titledemo = fromtitledemo = true;
+loadreplay:
+		demo.title = demo.fromtitle = true;
+		demo.ignorefiles = true;
+		demo.loadfiles = false;
 		G_DoPlayDemo(dname);
 	}
 }
@@ -1177,7 +1206,6 @@ void F_StartContinue(void)
 	keypressed = false;
 	paused = false;
 	CON_ToggleOff();
-	CON_ClearHUD();
 
 	// In case menus are still up?!!
 	M_ClearMenus(true);
@@ -1297,9 +1325,10 @@ static void F_AdvanceToNextScene(void)
 	picypos = cutscenes[cutnum]->scene[scenenum].ycoord[picnum];
 
 	if (cutscenes[cutnum]->scene[scenenum].musswitch[0])
-		S_ChangeMusic(cutscenes[cutnum]->scene[scenenum].musswitch,
+		S_ChangeMusicEx(cutscenes[cutnum]->scene[scenenum].musswitch,
 			cutscenes[cutnum]->scene[scenenum].musswitchflags,
-			cutscenes[cutnum]->scene[scenenum].musicloop);
+			cutscenes[cutnum]->scene[scenenum].musicloop,
+			cutscenes[cutnum]->scene[scenenum].musswitchposition, 0, 0);
 
 	// Fade to the next
 	dofadenow = true;
@@ -1348,8 +1377,6 @@ void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean reset
 
 	F_NewCutscene(cutscenes[cutscenenum]->scene[0].text);
 
-	CON_ClearHUD();
-
 	cutsceneover = false;
 	runningprecutscene = precutscene;
 	precutresetplayer = resetplayer;
@@ -1370,9 +1397,10 @@ void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean reset
 	stoptimer = 0;
 
 	if (cutscenes[cutnum]->scene[0].musswitch[0])
-		S_ChangeMusic(cutscenes[cutnum]->scene[0].musswitch,
+		S_ChangeMusicEx(cutscenes[cutnum]->scene[0].musswitch,
 			cutscenes[cutnum]->scene[0].musswitchflags,
-			cutscenes[cutnum]->scene[0].musicloop);
+			cutscenes[cutnum]->scene[0].musicloop,
+			cutscenes[cutnum]->scene[scenenum].musswitchposition, 0, 0);
 	else
 		S_StopMusic();
 }
diff --git a/src/filesrch.c b/src/filesrch.c
index 0276e1c90c92b00488814768f5503db9fee038f6..d132e9fb4d96da24be00da8435dcd3eb31cf5fc8 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -341,8 +341,6 @@ size_t dir_on[menudepth];
 UINT8 refreshdirmenu = 0;
 char *refreshdirname = NULL;
 
-size_t packetsizetally = 0;
-size_t mainwadstally = 0;
 
 #if defined (_XBOX) && defined (_MSC_VER)
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum,
@@ -368,9 +366,10 @@ void searchfilemenu(char *tempname)
 	return;
 }
 
-boolean preparefilemenu(boolean samedepth)
+boolean preparefilemenu(boolean samedepth, boolean replayhut)
 {
 	(void)samedepth;
+	(void)replayhut;
 	return false;
 }
 
@@ -437,9 +436,10 @@ void searchfilemenu(char *tempname)
 	return;
 }
 
-boolean preparefilemenu(boolean samedepth)
+boolean preparefilemenu(boolean samedepth, boolean replayhut)
 {
 	(void)samedepth;
+	(void)replayhut;
 	return false;
 }
 
@@ -710,7 +710,7 @@ void searchfilemenu(char *tempname)
 	}
 }
 
-boolean preparefilemenu(boolean samedepth)
+boolean preparefilemenu(boolean samedepth, boolean replayhut)
 {
 	DIR *dirhandle;
 	struct dirent *dent;
@@ -759,9 +759,13 @@ boolean preparefilemenu(boolean samedepth)
 		{
 			if (!S_ISDIR(fsstat.st_mode)) // file
 			{
-				if (!cv_addons_showall.value)
+				size_t len = strlen(dent->d_name)+1;
+				if (replayhut)
+				{
+					if (strcasecmp(".lmp", dent->d_name+len-5)) continue; // Not a replay
+				}
+				else if (!cv_addons_showall.value)
 				{
-					size_t len = strlen(dent->d_name)+1;
 					UINT8 ext;
 					for (ext = 0; ext < NUM_EXT_TABLE; ext++)
 						if (!strcasecmp(exttable[ext]+1, dent->d_name+len-(exttable[ext][0]))) break; // extension comparison
@@ -829,40 +833,49 @@ boolean preparefilemenu(boolean samedepth)
 			if (!S_ISDIR(fsstat.st_mode)) // file
 			{
 				if (!((numfolders+pos) < sizecoredirmenu)) continue; // crash prevention
-				for (; ext < NUM_EXT_TABLE; ext++)
-					if (!strcasecmp(exttable[ext]+1, dent->d_name+len-(exttable[ext][0]))) break; // extension comparison
-				if (ext == NUM_EXT_TABLE && !cv_addons_showall.value) continue; // not an addfile-able (or exec-able) file
-				ext += EXT_START; // moving to be appropriate position
 
-				if (ext >= EXT_LOADSTART)
+				if (replayhut)
+				{
+					if (strcasecmp(".lmp", dent->d_name+len-5)) continue; // Not a replay
+					ext = EXT_TXT; // This isn't used anywhere but better safe than sorry for messing with this...
+				}
+				else
 				{
-					size_t i;
-					for (i = 0; i < numwadfiles; i++)
+					for (; ext < NUM_EXT_TABLE; ext++)
+						if (!strcasecmp(exttable[ext]+1, dent->d_name+len-(exttable[ext][0]))) break; // extension comparison
+					if (ext == NUM_EXT_TABLE && !cv_addons_showall.value) continue; // not an addfile-able (or exec-able) file
+					ext += EXT_START; // moving to be appropriate position
+
+					if (ext >= EXT_LOADSTART)
 					{
-						if (!filenamebuf[i][0])
+						size_t i;
+						for (i = 0; i < numwadfiles; i++)
 						{
-							strncpy(filenamebuf[i], wadfiles[i]->filename, MAX_WADPATH);
-							filenamebuf[i][MAX_WADPATH - 1] = '\0';
-							nameonly(filenamebuf[i]);
+							if (!filenamebuf[i][0])
+							{
+								strncpy(filenamebuf[i], wadfiles[i]->filename, MAX_WADPATH);
+								filenamebuf[i][MAX_WADPATH - 1] = '\0';
+								nameonly(filenamebuf[i]);
+							}
+
+							if (strcmp(dent->d_name, filenamebuf[i]))
+								continue;
+							if (cv_addons_md5.value && !checkfilemd5(menupath, wadfiles[i]->md5sum))
+								continue;
+
+							ext |= EXT_LOADED;
 						}
-
-						if (strcmp(dent->d_name, filenamebuf[i]))
-							continue;
-						if (cv_addons_md5.value && !checkfilemd5(menupath, wadfiles[i]->md5sum))
-							continue;
-
-						ext |= EXT_LOADED;
 					}
-				}
-				else if (ext == EXT_TXT)
-				{
-					if (!strcmp(dent->d_name, "log.txt") || !strcmp(dent->d_name, "errorlog.txt"))
+					else if (ext == EXT_TXT)
+					{
+						if (!strcmp(dent->d_name, "log.txt") || !strcmp(dent->d_name, "errorlog.txt"))
+							ext |= EXT_LOADED;
+					}
+
+					if (!strcmp(dent->d_name, configfile))
 						ext |= EXT_LOADED;
 				}
 
-				if (!strcmp(dent->d_name, configfile))
-					ext |= EXT_LOADED;
-
 				folder = 0;
 			}
 			else // directory
@@ -881,6 +894,8 @@ boolean preparefilemenu(boolean samedepth)
 				strcpy(temp+len, PATHSEP);
 				coredirmenu[folderpos++] = temp;
 			}
+			else if (replayhut) // Reverse-alphabetical on just the files; acts as a fake "most recent first" with the current filename format
+				coredirmenu[sizecoredirmenu - 1 - pos++] = temp;
 			else
 				coredirmenu[numfolders + pos++] = temp;
 		}
diff --git a/src/filesrch.h b/src/filesrch.h
index 01a52848219db85f3189f52742ea48da0a5e3516..4cb92b2381d18f15a4bf3707c18f8f7db6149e1a 100644
--- a/src/filesrch.h
+++ b/src/filesrch.h
@@ -42,9 +42,6 @@ extern size_t dir_on[menudepth];
 extern UINT8 refreshdirmenu;
 extern char *refreshdirname;
 
-extern size_t packetsizetally;
-extern size_t mainwadstally;
-
 typedef enum
 {
 	EXT_FOLDER = 0,
@@ -94,6 +91,6 @@ typedef enum
 
 void closefilemenu(boolean validsize);
 void searchfilemenu(char *tempname);
-boolean preparefilemenu(boolean samedepth);
+boolean preparefilemenu(boolean samedepth, boolean replayhut);
 
 #endif // __FILESRCH_H__
diff --git a/src/g_game.c b/src/g_game.c
index ad25c8ce990ee02c4160dc825fbc6eb1689547bf..b8a1a3bfada7eff1cb00495414a03ec59a3308d2 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -14,6 +14,7 @@
 #include "doomdef.h"
 #include "console.h"
 #include "d_main.h"
+#include "d_clisrv.h"
 #include "d_player.h"
 #include "f_finale.h"
 #include "filesrch.h" // for refreshdirmenu
@@ -77,6 +78,7 @@ static void G_DoStartVote(void);
 
 char   mapmusname[7]; // Music name
 UINT16 mapmusflags; // Track and reset bit
+UINT32 mapmusposition; // Position to jump to
 
 INT16 gamemap = 1;
 INT16 maptol;
@@ -100,7 +102,6 @@ UINT8  numDemos      = 0; //3; -- i'm FED UP of losing my skincolour to a broken
 UINT32 demoDelayTime = 15*TICRATE;
 UINT32 demoIdleTime  = 3*TICRATE;
 
-boolean timingdemo; // if true, exit with report on completion
 boolean nodrawers; // for comparative timing purposes
 boolean noblit; // for comparative timing purposes
 static tic_t demostarttime; // for comparative timing purposes
@@ -112,10 +113,7 @@ boolean addedtogame;
 player_t players[MAXPLAYERS];
 
 INT32 consoleplayer; // player taking events and displaying
-INT32 displayplayer; // view being displayed
-INT32 secondarydisplayplayer; // for splitscreen
-INT32 thirddisplayplayer;
-INT32 fourthdisplayplayer;
+INT32 displayplayers[MAXSPLITSCREENPLAYERS]; // view being displayed
 
 tic_t gametic;
 tic_t levelstarttic; // gametic at level start
@@ -286,20 +284,16 @@ UINT32 timesBeaten;
 UINT32 timesBeatenWithEmeralds;
 //UINT32 timesBeatenUltimate;
 
-static char demoname[64];
-boolean demorecording;
-boolean demoplayback;
-boolean titledemo; // Title Screen demo can be cancelled by any key
-boolean fromtitledemo; // SRB2Kart: Don't stop the music
+//@TODO put these all in a struct for namespacing purposes?
+static char demoname[128];
 static UINT8 *demobuffer = NULL;
-static UINT8 *demo_p, *demotime_p;
+static UINT8 *demo_p, *demotime_p, *demoinfo_p;
 static UINT8 *demoend;
 static UINT8 demoflags;
-static UINT16 demoversion;
-boolean singledemo; // quit after playing a demo from cmdline
-boolean demo_start; // don't start playing demo right away
 static boolean demosynced = true; // console warning message
 
+struct demovars_s demo;
+
 boolean metalrecording; // recording as metal sonic
 mobj_t *metalplayback;
 static UINT8 *metalbuffer = NULL;
@@ -316,10 +310,15 @@ static struct {
 	// EZT_SCALE
 	fixed_t scale, lastscale;
 
+	// EZT_KART
+	INT32 kartitem, kartamount, kartbumpers;
+
+	UINT8 desyncframes; // Don't try to resync unless we've been off for two frames, to monkeypatch a few trouble spots
+
 	// EZT_HIT
 	UINT16 hits;
 	mobj_t **hitlist;
-} ghostext;
+} ghostext[MAXPLAYERS];
 
 // Your naming conventions are stupid and useless.
 // There is no conflict here.
@@ -329,6 +328,12 @@ boolean precache = true; // if true, load all graphics at start
 
 INT16 prevmap, nextmap;
 
+static CV_PossibleValue_t recordmultiplayerdemos_cons_t[] = {{0, "Disabled"}, {1, "Manual Save"}, {2, "Auto Save"}, {0, NULL}};
+consvar_t cv_recordmultiplayerdemos = {"netdemo_record", "Manual Save", CV_SAVE, recordmultiplayerdemos_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
+static CV_PossibleValue_t netdemosyncquality_cons_t[] = {{1, "MIN"}, {35, "MAX"}, {0, NULL}};
+consvar_t cv_netdemosyncquality = {"netdemo_syncquality", "1", CV_SAVE, netdemosyncquality_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 static UINT8 *savebuffer;
 
 // Analog Control
@@ -406,6 +411,8 @@ static CV_PossibleValue_t joyaxis_cons_t[] = {{0, "None"},
 #endif
 #endif
 
+static CV_PossibleValue_t deadzone_cons_t[] = {{0, "MIN"}, {FRACUNIT, "MAX"}, {0, NULL}};
+
 // don't mind me putting these here, I was lazy to figure out where else I could put those without blowing up the compiler.
 
 // it automatically becomes compact with 20+ players, but if you like it, I guess you can turn that on!
@@ -476,6 +483,7 @@ consvar_t cv_aimaxis = {"joyaxis_aim", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL,
 consvar_t cv_lookaxis = {"joyaxis_look", "None", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_fireaxis = {"joyaxis_fire", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_driftaxis = {"joyaxis_drift", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_deadzone = {"joy_deadzone", "0.5", CV_FLOAT|CV_SAVE, deadzone_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 consvar_t cv_turnaxis2 = {"joyaxis2_turn", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_moveaxis2 = {"joyaxis2_move", "None", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -484,6 +492,7 @@ consvar_t cv_aimaxis2 = {"joyaxis2_aim", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL
 consvar_t cv_lookaxis2 = {"joyaxis2_look", "None", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_fireaxis2 = {"joyaxis2_fire", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_driftaxis2 = {"joyaxis2_drift", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_deadzone2 = {"joy2_deadzone", "0.5", CV_FLOAT|CV_SAVE, deadzone_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 consvar_t cv_turnaxis3 = {"joyaxis3_turn", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_moveaxis3 = {"joyaxis3_move", "None", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -492,6 +501,7 @@ consvar_t cv_aimaxis3 = {"joyaxis3_aim", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL
 consvar_t cv_lookaxis3 = {"joyaxis3_look", "None", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_fireaxis3 = {"joyaxis3_fire", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_driftaxis3 = {"joyaxis3_drift", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_deadzone3 = {"joy3_deadzone", "0.5", CV_FLOAT|CV_SAVE, deadzone_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 consvar_t cv_turnaxis4 = {"joyaxis4_turn", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_moveaxis4 = {"joyaxis4_move", "None", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -500,6 +510,7 @@ consvar_t cv_aimaxis4 = {"joyaxis4_aim", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL
 consvar_t cv_lookaxis4 = {"joyaxis4_look", "None", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_fireaxis4 = {"joyaxis4_fire", "Z-Axis", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_driftaxis4 = {"joyaxis4_drift", "Z-Rudder", CV_SAVE, joyaxis_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_deadzone4 = {"joy4_deadzone", "0.5", CV_FLOAT|CV_SAVE, deadzone_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 
 #if MAXPLAYERS > 16
@@ -925,8 +936,8 @@ static INT32 Joy1Axis(axis_input_e axissel)
 		retaxis = +JOYAXISRANGE;
 	if (!Joystick.bGamepadStyle && axissel < AXISDEAD)
 	{
-		const INT32 jdeadzone = JOYAXISRANGE/4;
-		if (-jdeadzone < retaxis && retaxis < jdeadzone)
+		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_deadzone.value) >> FRACBITS;
+		if (abs(retaxis) <= jdeadzone)
 			return 0;
 	}
 	if (flp) retaxis = -retaxis; //flip it around
@@ -1006,7 +1017,7 @@ static INT32 Joy2Axis(axis_input_e axissel)
 		retaxis = +JOYAXISRANGE;
 	if (!Joystick2.bGamepadStyle && axissel < AXISDEAD)
 	{
-		const INT32 jdeadzone = JOYAXISRANGE/4;
+		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_deadzone2.value) >> FRACBITS;
 		if (-jdeadzone < retaxis && retaxis < jdeadzone)
 			return 0;
 	}
@@ -1087,7 +1098,7 @@ static INT32 Joy3Axis(axis_input_e axissel)
 		retaxis = +JOYAXISRANGE;
 	if (!Joystick3.bGamepadStyle && axissel < AXISDEAD)
 	{
-		const INT32 jdeadzone = JOYAXISRANGE/4;
+		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_deadzone3.value) >> FRACBITS;
 		if (-jdeadzone < retaxis && retaxis < jdeadzone)
 			return 0;
 	}
@@ -1167,7 +1178,7 @@ static INT32 Joy4Axis(axis_input_e axissel)
 		retaxis = +JOYAXISRANGE;
 	if (!Joystick4.bGamepadStyle && axissel < AXISDEAD)
 	{
-		const INT32 jdeadzone = JOYAXISRANGE/4;
+		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_deadzone4.value) >> FRACBITS;
 		if (-jdeadzone < retaxis && retaxis < jdeadzone)
 			return 0;
 	}
@@ -1213,9 +1224,9 @@ INT32 JoyAxis(axis_input_e axissel, UINT8 p)
 //
 // set secondaryplayer true to build player 2's ticcmd in splitscreen mode
 //
-INT32 localaiming, localaiming2, localaiming3, localaiming4;
-angle_t localangle, localangle2, localangle3, localangle4;
-boolean camspin, camspin2, camspin3, camspin4;
+INT32 localaiming[MAXSPLITSCREENPLAYERS];
+angle_t localangle[MAXSPLITSCREENPLAYERS];
+boolean camspin[MAXSPLITSCREENPLAYERS];
 
 static fixed_t forwardmove[2] = {25<<FRACBITS>>16, 50<<FRACBITS>>16};
 static fixed_t sidemove[2] = {2<<FRACBITS>>16, 4<<FRACBITS>>16};
@@ -1232,51 +1243,40 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	camera_t *thiscam;
 	angle_t lang;
 
-	static INT32 turnheld, turnheld2, turnheld3, turnheld4; // for accelerative turning
-	static boolean keyboard_look, keyboard_look2, keyboard_look3, keyboard_look4; // true if lookup/down using keyboard
-	static boolean resetdown, resetdown2, resetdown3, resetdown4; // don't cam reset every frame
+	static INT32 turnheld[MAXSPLITSCREENPLAYERS]; // for accelerative turning
+	static boolean keyboard_look[MAXSPLITSCREENPLAYERS]; // true if lookup/down using keyboard
+	static boolean resetdown[MAXSPLITSCREENPLAYERS]; // don't cam reset every frame
+
+	if (demo.playback) return;
+
+	if (ssplayer == 1)
+		player = &players[consoleplayer];
+	else
+		player = &players[displayplayers[ssplayer-1]];
+
+	if (ssplayer == 2)
+		thiscam = (player->bot == 2 ? &camera[0] : &camera[ssplayer-1]);
+	else
+		thiscam = &camera[ssplayer-1];
+	lang = localangle[ssplayer-1];
+	laim = localaiming[ssplayer-1];
+	th = turnheld[ssplayer-1];
+	kbl = keyboard_look[ssplayer-1];
+	rd = resetdown[ssplayer-1];
 
 	switch (ssplayer)
 	{
 		case 2:
-			player = &players[secondarydisplayplayer];
-			thiscam = (player->bot == 2 ? &camera : &camera2);
-			lang = localangle2;
-			laim = localaiming2;
-			th = turnheld2;
-			kbl = keyboard_look2;
-			rd = resetdown2;
 			G_CopyTiccmd(cmd, I_BaseTiccmd2(), 1);
 			break;
 		case 3:
-			player = &players[thirddisplayplayer];
-			thiscam = &camera3;
-			lang = localangle3;
-			laim = localaiming3;
-			th = turnheld3;
-			kbl = keyboard_look3;
-			rd = resetdown3;
 			G_CopyTiccmd(cmd, I_BaseTiccmd3(), 1);
 			break;
 		case 4:
-			player = &players[fourthdisplayplayer];
-			thiscam = &camera4;
-			lang = localangle4;
-			laim = localaiming4;
-			th = turnheld4;
-			kbl = keyboard_look4;
-			rd = resetdown4;
 			G_CopyTiccmd(cmd, I_BaseTiccmd4(), 1);
 			break;
 		case 1:
 		default:
-			player = &players[consoleplayer];
-			thiscam = &camera;
-			lang = localangle;
-			laim = localaiming;
-			th = turnheld;
-			kbl = keyboard_look;
-			rd = resetdown;
 			G_CopyTiccmd(cmd, I_BaseTiccmd(), 1); // empty, or external driver
 			break;
 	}
@@ -1558,8 +1558,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	if (((player->mo && player->speed > 0) // Moving
 		|| (leveltime > starttime && (cmd->buttons & BT_ACCELERATE && cmd->buttons & BT_BRAKE)) // Rubber-burn turn
 		|| (player->kartstuff[k_respawn]) // Respawning
-		|| (player->spectator || objectplacing)) // Not a physical player
-		&& !(player->kartstuff[k_spinouttimer] && player->kartstuff[k_sneakertimer])) // Spinning and boosting cancels out turning
+		|| (player->spectator || objectplacing))) // Not a physical player
 		lang += (cmd->angleturn<<16);
 
 	cmd->angleturn = (INT16)(lang >> 16);
@@ -1567,42 +1566,12 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 	if (!hu_stopped)
 	{
-		switch (ssplayer)
-		{
-		case 2:
-			localangle2 = lang;
-			localaiming2 = laim;
-			keyboard_look2 = kbl;
-			turnheld2 = th;
-			resetdown2 = rd;
-			camspin2 = InputDown(gc_lookback, ssplayer);
-			break;
-		case 3:
-			localangle3 = lang;
-			localaiming3 = laim;
-			keyboard_look3 = kbl;
-			turnheld3 = th;
-			resetdown3 = rd;
-			camspin3 = InputDown(gc_lookback, ssplayer);
-			break;
-		case 4:
-			localangle4 = lang;
-			localaiming4 = laim;
-			keyboard_look4 = kbl;
-			turnheld4 = th;
-			resetdown4 = rd;
-			camspin4 = InputDown(gc_lookback, ssplayer);
-			break;
-		case 1:
-		default:
-			localangle = lang;
-			localaiming = laim;
-			keyboard_look = kbl;
-			turnheld = th;
-			resetdown = rd;
-			camspin = InputDown(gc_lookback, ssplayer);
-			break;
-		}
+		localangle[ssplayer-1] = lang;
+		localaiming[ssplayer-1] = laim;
+		keyboard_look[ssplayer-1] = kbl;
+		turnheld[ssplayer-1] = th;
+		resetdown[ssplayer-1] = rd;
+		camspin[ssplayer-1] = InputDown(gc_lookback, ssplayer);
 	}
 
 	/* 	Lua: Allow this hook to overwrite ticcmd.
@@ -1622,8 +1591,8 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 	//Reset away view if a command is given.
 	if ((cmd->forwardmove || cmd->sidemove || cmd->buttons)
-		&& displayplayer != consoleplayer && ssplayer == 1)
-		displayplayer = consoleplayer;
+		&& displayplayers[0] != consoleplayer && ssplayer == 1)
+		displayplayers[0] = consoleplayer;
 
 }
 
@@ -1775,27 +1744,24 @@ void G_DoLoadLevel(boolean resetplayer)
 	if (!resetplayer)
 		P_FindEmerald();
 
-	displayplayer = consoleplayer; // view the guy you are playing
-	if (!splitscreen && !botingame)
-		secondarydisplayplayer = consoleplayer;
-	if (splitscreen < 2)
-		thirddisplayplayer = consoleplayer;
-	if (splitscreen < 3)
-		fourthdisplayplayer = consoleplayer;
+	displayplayers[0] = consoleplayer; // view the guy you are playing
+
+	for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
+	{
+		if (i > 0 && !(i == 1 && botingame) && splitscreen < i)
+			displayplayers[i] = consoleplayer;
+	}
 
 	gameaction = ga_nothing;
 #ifdef PARANOIA
 	Z_CheckHeap(-2);
 #endif
 
-	if (camera.chase)
-		P_ResetCamera(&players[displayplayer], &camera);
-	if (camera2.chase && splitscreen)
-		P_ResetCamera(&players[secondarydisplayplayer], &camera2);
-	if (camera3.chase && splitscreen > 1)
-		P_ResetCamera(&players[thirddisplayplayer], &camera3);
-	if (camera4.chase && splitscreen > 2)
-		P_ResetCamera(&players[fourthdisplayplayer], &camera4);
+	for (i = 0; i <= splitscreen; i++)
+	{
+		if (camera[i].chase)
+			P_ResetCamera(&players[displayplayers[i]], &camera[i]);
+	}
 
 	// clear cmd building stuff
 	memset(gamekeydown, 0, sizeof (gamekeydown));
@@ -1824,8 +1790,8 @@ static INT32 spectatedelay, spectatedelay2, spectatedelay3, spectatedelay4 = 0;
 boolean G_Responder(event_t *ev)
 {
 	// any other key pops up menu if in demos
-	if (gameaction == ga_nothing && !singledemo &&
-		((demoplayback && !modeattacking && !titledemo) || gamestate == GS_TITLESCREEN))
+	if (gameaction == ga_nothing && !demo.quitafterplaying &&
+		((demo.playback && !modeattacking && !demo.title && !multiplayer) || gamestate == GS_TITLESCREEN))
 	{
 		if (ev->type == ev_keydown && ev->data1 != 301)
 		{
@@ -1834,7 +1800,7 @@ boolean G_Responder(event_t *ev)
 		}
 		return false;
 	}
-	else if (demoplayback && titledemo)
+	else if (demo.playback && demo.title)
 	{
 		// Title demo uses intro responder
 		if (F_IntroResponder(ev))
@@ -1904,71 +1870,71 @@ boolean G_Responder(event_t *ev)
 	if (gamestate == GS_LEVEL && ev->type == ev_keydown
 		&& (ev->data1 == KEY_F12 || ev->data1 == gamecontrol[gc_viewpoint][0] || ev->data1 == gamecontrol[gc_viewpoint][1]))
 	{
-		if (splitscreen || !netgame)
-			displayplayer = consoleplayer;
+		if (!demo.playback && (splitscreen || !netgame))
+			displayplayers[0] = consoleplayer;
 		else
 		{
-			UINT8 i = 0; // spy mode
-			for (i = 0; i < MAXPLAYERS; i++)
-			{
-				displayplayer++;
-				if (displayplayer == MAXPLAYERS)
-					displayplayer = 0;
+			G_AdjustView(1, 1, true);
 
-				if (displayplayer == consoleplayer)
-					break; // End loop
+			// change statusbar also if playing back demo
+			if (demo.quitafterplaying)
+				ST_changeDemoView();
 
-				if (!playeringame[displayplayer])
-					continue;
+			return true;
+		}
+	}
 
-				if (players[displayplayer].spectator)
-					continue;
+	if (gamestate == GS_LEVEL && ev->type == ev_keydown && multiplayer && demo.playback)
+	{
+		if (ev->data1 == gamecontrolbis[gc_viewpoint][0] || ev->data1 == gamecontrolbis[gc_viewpoint][1])
+		{
+			G_AdjustView(2, 1, true);
 
-				// SRB2Kart: Only go through players who are actually playing
-				if (players[displayplayer].exiting)
-					continue;
+			return true;
+		}
+		else if (ev->data1 == gamecontrol3[gc_viewpoint][0] || ev->data1 == gamecontrol3[gc_viewpoint][1])
+		{
+			G_AdjustView(3, 1, true);
 
-				if (players[displayplayer].pflags & PF_TIMEOVER)
-					continue;
+			return true;
+		}
+		else if (ev->data1 == gamecontrol4[gc_viewpoint][0] || ev->data1 == gamecontrol4[gc_viewpoint][1])
+		{
+			G_AdjustView(4, 1, true);
 
-				// I don't know if we want this actually, but I'll humor the suggestion anyway
-				if (G_BattleGametype())
-				{
-					if (players[displayplayer].kartstuff[k_bumper] <= 0)
-						continue;
-				}
+			return true;
+		}
 
-				// SRB2Kart: we have no team-based modes, YET...
-				/*if (G_GametypeHasTeams())
-				{
-					if (players[consoleplayer].ctfteam
-					 && players[displayplayer].ctfteam != players[consoleplayer].ctfteam)
-						continue;
-				}
-				else if (gametype == GT_HIDEANDSEEK)
-				{
-					if (players[consoleplayer].pflags & PF_TAGIT)
-						continue;
-				}
-				// Other Tag-based gametypes?
-				else if (G_TagGametype())
-				{
-					if (!players[consoleplayer].spectator
-					 && (players[consoleplayer].pflags & PF_TAGIT) != (players[displayplayer].pflags & PF_TAGIT))
-						continue;
-				}
-				else if (G_GametypeHasSpectators() && G_BattleGametype())
-				{
-					if (!players[consoleplayer].spectator)
-						continue;
-				}*/
+		// Allow pausing
+		if (
+			ev->data1 == gamecontrol[gc_pause][0]
+			|| ev->data1 == gamecontrol[gc_pause][1]
+			|| ev->data1 == KEY_PAUSE
+		)
+		{
+			paused = !paused;
 
-				break;
+			if (demo.rewinding)
+			{
+				G_ConfirmRewind(leveltime);
+				paused = true;
+				S_PauseAudio();
 			}
+			else if (paused)
+				S_PauseAudio();
+			else
+				S_ResumeAudio();
 
-			// change statusbar also if playing back demo
-			if (singledemo)
-				ST_changeDemoView();
+			return true;
+		}
+
+		// Anything else opens the menu if not already open, except for a few keys...
+		if (!(
+			// Rankings
+			ev->data1 == gamecontrol[gc_scores][0] || ev->data1 == gamecontrol[gc_scores][1]
+		))
+		{
+			M_StartControlPanel();
 
 			return true;
 		}
@@ -2096,6 +2062,255 @@ boolean G_Responder(event_t *ev)
 	return false;
 }
 
+//
+// G_CouldView
+// Return whether a player could be viewed by any means.
+//
+boolean G_CouldView(INT32 playernum)
+{
+	player_t *player;
+
+	if (playernum < 0 || playernum > MAXPLAYERS-1)
+		return false;
+
+	if (!playeringame[playernum])
+		return false;
+
+	player = &players[playernum];
+
+	if (player->spectator)
+		return false;
+
+	// SRB2Kart: Only go through players who are actually playing
+	if (player->exiting)
+		return false;
+	if (( player->pflags & PF_TIMEOVER ))
+		return false;
+
+	// I don't know if we want this actually, but I'll humor the suggestion anyway
+	if (G_BattleGametype() && !demo.playback)
+	{
+		if (player->kartstuff[k_bumper] <= 0)
+			return false;
+	}
+
+	// SRB2Kart: we have no team-based modes, YET...
+	/*if (G_GametypeHasTeams())
+	{
+		if (players[consoleplayer].ctfteam
+		 && player->ctfteam != players[consoleplayer].ctfteam)
+			return false;
+	}
+	else if (gametype == GT_HIDEANDSEEK)
+	{
+		if (players[consoleplayer].pflags & PF_TAGIT)
+			return false;
+	}
+	// Other Tag-based gametypes?
+	else if (G_TagGametype())
+	{
+		if (!players[consoleplayer].spectator
+		 && (players[consoleplayer].pflags & PF_TAGIT) != (player->pflags & PF_TAGIT))
+			return false;
+	}
+	else if (G_GametypeHasSpectators() && G_BattleGametype())
+	{
+		if (!players[consoleplayer].spectator)
+			return false;
+	}*/
+
+	return true;
+}
+
+//
+// G_CanView
+// Return whether a player can be viewed on a particular view (splitscreen).
+//
+boolean G_CanView(INT32 playernum, UINT8 viewnum, boolean onlyactive)
+{
+	UINT8 splits;
+	UINT8 viewd;
+	INT32 *displayplayerp;
+
+	if (!(onlyactive ? G_CouldView(playernum) : (playeringame[playernum] && !players[playernum].spectator)))
+		return false;
+
+	splits = splitscreen+1;
+	if (viewnum > splits)
+		viewnum = splits;
+
+	for (viewd = 1; viewd < viewnum; ++viewd)
+	{
+		displayplayerp = (&displayplayers[viewd-1]);
+		if ((*displayplayerp) == playernum)
+			return false;
+	}
+	for (viewd = viewnum + 1; viewd <= splits; ++viewd)
+	{
+		displayplayerp = (&displayplayers[viewd-1]);
+		if ((*displayplayerp) == playernum)
+			return false;
+	}
+
+	return true;
+}
+
+//
+// G_FindView
+// Return the next player that can be viewed on a view, wraps forward.
+// An out of range startview is corrected.
+//
+INT32 G_FindView(INT32 startview, UINT8 viewnum, boolean onlyactive, boolean reverse)
+{
+	INT32 i, dir = reverse ? -1 : 1;
+	startview = min(max(startview, 0), MAXPLAYERS);
+	for (i = startview; i < MAXPLAYERS && i >= 0; i += dir)
+	{
+		if (G_CanView(i, viewnum, onlyactive))
+			return i;
+	}
+	for (i = (reverse ? MAXPLAYERS-1 : 0); i != startview; i += dir)
+	{
+		if (G_CanView(i, viewnum, onlyactive))
+			return i;
+	}
+	return -1;
+}
+
+INT32 G_CountPlayersPotentiallyViewable(boolean active)
+{
+	INT32 total = 0;
+	INT32 i;
+	for (i = 0; i < MAXPLAYERS; ++i)
+	{
+		if (active ? G_CouldView(i) : (playeringame[i] && !players[i].spectator))
+			total++;
+	}
+	return total;
+}
+
+//
+// G_ResetView
+// Correct a viewpoint to playernum or the next available, wraps forward.
+// Also promotes splitscreen up to available viewable players.
+// An out of range playernum is corrected.
+//
+void G_ResetView(UINT8 viewnum, INT32 playernum, boolean onlyactive)
+{
+	UINT8 splits;
+	UINT8 viewd;
+
+	INT32    *displayplayerp;
+	camera_t *camerap;
+
+	INT32 olddisplayplayer;
+	INT32 playersviewable;
+
+	splits = splitscreen+1;
+
+	/* Promote splits */
+	if (viewnum > splits)
+	{
+		playersviewable = G_CountPlayersPotentiallyViewable(onlyactive);
+		if (playersviewable < splits)/* do not demote */
+			return;
+
+		if (viewnum > playersviewable)
+			viewnum = playersviewable;
+		splitscreen = viewnum-1;
+
+		/* Prepare extra views for G_FindView to pass. */
+		for (viewd = splits+1; viewd < viewnum; ++viewd)
+		{
+			displayplayerp = (&displayplayers[viewd-1]);
+			(*displayplayerp) = INT32_MAX;
+		}
+
+		R_ExecuteSetViewSize();
+	}
+
+	displayplayerp = (&displayplayers[viewnum-1]);
+	olddisplayplayer = (*displayplayerp);
+
+	/* Check if anyone is available to view. */
+	if (( playernum = G_FindView(playernum, viewnum, onlyactive, playernum < olddisplayplayer) ) == -1)
+		return;
+
+	/* Focus our target view first so that we don't take its player. */
+	(*displayplayerp) = playernum;
+	if ((*displayplayerp) != olddisplayplayer)
+	{
+		camerap = &camera[viewnum-1];
+		P_ResetCamera(&players[(*displayplayerp)], camerap);
+	}
+
+	if (viewnum > splits)
+	{
+		for (viewd = splits+1; viewd < viewnum; ++viewd)
+		{
+			displayplayerp = (&displayplayers[viewd-1]);
+			camerap = &camera[viewd];
+
+			(*displayplayerp) = G_FindView(0, viewd, onlyactive, false);
+
+			P_ResetCamera(&players[(*displayplayerp)], camerap);
+		}
+	}
+
+	if (viewnum == 1 && demo.playback)
+		consoleplayer = displayplayers[0];
+}
+
+//
+// G_AdjustView
+// Increment a viewpoint by offset from the current player. A negative value
+// decrements.
+//
+void G_AdjustView(UINT8 viewnum, INT32 offset, boolean onlyactive)
+{
+	INT32 *displayplayerp, oldview;
+	displayplayerp = &displayplayers[viewnum-1];
+	oldview = (*displayplayerp);
+	G_ResetView(viewnum, ( (*displayplayerp) + offset ), onlyactive);
+
+	// If no other view could be found, go back to what we had.
+	if ((*displayplayerp) == -1)
+		(*displayplayerp) = oldview;
+}
+
+//
+// G_ResetViews
+// Ensures all viewpoints are valid
+// Also demotes splitscreen down to one player.
+//
+void G_ResetViews(void)
+{
+	UINT8 splits;
+	UINT8 viewd;
+
+	INT32 playersviewable;
+
+	splits = splitscreen+1;
+
+	playersviewable = G_CountPlayersPotentiallyViewable(false);
+	/* Demote splits */
+	if (playersviewable < splits)
+	{
+		splits = playersviewable;
+		splitscreen = max(splits-1, 0);
+		R_ExecuteSetViewSize();
+	}
+
+	/*
+	Consider installing a method to focus the last
+	view elsewhere if all players spectate?
+	*/
+	for (viewd = 1; viewd <= splits; ++viewd)
+	{
+		G_AdjustView(viewd, 0, false);
+	}
+}
+
 //
 // G_Ticker
 // Make ticcmd_ts for the players.
@@ -2145,6 +2360,7 @@ void G_Ticker(boolean run)
 
 	buf = gametic % BACKUPTICS;
 
+	if (!demo.playback)
 	// read/write demo and check turbo cheat
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
@@ -2152,15 +2368,18 @@ void G_Ticker(boolean run)
 
 		if (playeringame[i])
 		{
+			//@TODO all this throwdir stuff shouldn't be here! But it stays for now to maintain 1.0.4 compat...
+			// Remove for 1.1!
+
 			// SRB2kart
 			// Save the dir the player is holding
 			//  to allow items to be thrown forward or backward.
 			if (cmd->buttons & BT_FORWARD)
-					players[i].kartstuff[k_throwdir] = 1;
+				players[i].kartstuff[k_throwdir] = 1;
 			else if (cmd->buttons & BT_BACKWARD)
-					players[i].kartstuff[k_throwdir] = -1;
+				players[i].kartstuff[k_throwdir] = -1;
 			else
-					players[i].kartstuff[k_throwdir] = 0;
+				players[i].kartstuff[k_throwdir] = 0;
 
 			G_CopyTiccmd(cmd, &netcmds[buf][i], 1);
 
@@ -2173,7 +2392,7 @@ void G_Ticker(boolean run)
 	switch (gamestate)
 	{
 		case GS_LEVEL:
-			if (titledemo)
+			if (demo.title)
 				F_TitleDemoTicker();
 			P_Ticker(run); // tic the game
 			ST_Ticker();
@@ -2303,7 +2522,7 @@ static inline void G_PlayerFinishLevel(INT32 player)
 	// SRB2kart: Increment the "matches played" counter.
 	if (player == consoleplayer)
 	{
-		if (legitimateexit && !demoplayback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified)
+		if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified)
 		{
 			matchesplayed++;
 			if (M_UpdateUnlockablesAndExtraEmblems(true))
@@ -2325,25 +2544,12 @@ void G_PlayerReborn(INT32 player)
 	INT32 score, marescore;
 	INT32 lives;
 	INT32 continues;
-	UINT8 charability;
-	UINT8 charability2;
 	// SRB2kart
 	UINT8 kartspeed;
 	UINT8 kartweight;
 	//
-	fixed_t normalspeed;
-	fixed_t runspeed;
-	UINT8 thrustfactor;
-	UINT8 accelstart;
-	UINT8 acceleration;
 	INT32 charflags;
 	INT32 pflags;
-	UINT32 thokitem;
-	UINT32 spinitem;
-	UINT32 revitem;
-	fixed_t actionspd;
-	fixed_t mindash;
-	fixed_t maxdash;
 	INT32 ctfteam;
 	INT32 starposttime;
 	INT16 starpostx;
@@ -2351,7 +2557,6 @@ void G_PlayerReborn(INT32 player)
 	INT16 starpostz;
 	INT32 starpostnum;
 	INT32 starpostangle;
-	fixed_t jumpfactor;
 	INT32 exiting;
 	INT16 numboxes;
 	INT16 totalring;
@@ -2399,17 +2604,10 @@ void G_PlayerReborn(INT32 player)
 
 	skincolor = players[player].skincolor;
 	skin = players[player].skin;
-	charability = players[player].charability;
-	charability2 = players[player].charability2;
 	// SRB2kart
 	kartspeed = players[player].kartspeed;
 	kartweight = players[player].kartweight;
 	//
-	normalspeed = players[player].normalspeed;
-	runspeed = players[player].runspeed;
-	thrustfactor = players[player].thrustfactor;
-	accelstart = players[player].accelstart;
-	acceleration = players[player].acceleration;
 	charflags = players[player].charflags;
 
 	starposttime = players[player].starposttime;
@@ -2419,13 +2617,6 @@ void G_PlayerReborn(INT32 player)
 	starpostnum = players[player].starpostnum;
 	respawnflip = players[player].kartstuff[k_starpostflip];	//SRB2KART
 	starpostangle = players[player].starpostangle;
-	jumpfactor = players[player].jumpfactor;
-	thokitem = players[player].thokitem;
-	spinitem = players[player].spinitem;
-	revitem = players[player].revitem;
-	actionspd = players[player].actionspd;
-	mindash = players[player].mindash;
-	maxdash = players[player].maxdash;
 
 	mare = players[player].mare;
 	bot = players[player].bot;
@@ -2489,24 +2680,11 @@ void G_PlayerReborn(INT32 player)
 	// save player config truth reborn
 	p->skincolor = skincolor;
 	p->skin = skin;
-	p->charability = charability;
-	p->charability2 = charability2;
 	// SRB2kart
 	p->kartspeed = kartspeed;
 	p->kartweight = kartweight;
 	//
-	p->normalspeed = normalspeed;
-	p->runspeed = runspeed;
-	p->thrustfactor = thrustfactor;
-	p->accelstart = accelstart;
-	p->acceleration = acceleration;
 	p->charflags = charflags;
-	p->thokitem = thokitem;
-	p->spinitem = spinitem;
-	p->revitem = revitem;
-	p->actionspd = actionspd;
-	p->mindash = mindash;
-	p->maxdash = maxdash;
 
 	p->starposttime = starposttime;
 	p->starpostx = starpostx;
@@ -2514,7 +2692,6 @@ void G_PlayerReborn(INT32 player)
 	p->starpostz = starpostz;
 	p->starpostnum = starpostnum;
 	p->starpostangle = starpostangle;
-	p->jumpfactor = jumpfactor;
 	p->exiting = exiting;
 
 	p->numboxes = numboxes;
@@ -2558,7 +2735,8 @@ void G_PlayerReborn(INT32 player)
 		{
 			strncpy(mapmusname, mapheaderinfo[gamemap-1]->musname, 7);
 			mapmusname[6] = 0;
-			mapmusflags = mapheaderinfo[gamemap-1]->mustrack & MUSIC_TRACKMASK;
+			mapmusflags = (mapheaderinfo[gamemap-1]->mustrack & MUSIC_TRACKMASK);
+			mapmusposition = mapheaderinfo[gamemap-1]->muspos;
 			songcredit = true;
 		}
 	}
@@ -2591,22 +2769,22 @@ void G_PlayerReborn(INT32 player)
 		{
 			if (p == &players[consoleplayer])
 				CV_SetValue(&cv_playercolor, skincolor_redteam);
-			else if (p == &players[secondarydisplayplayer])
+			else if (p == &players[displayplayers[1]])
 				CV_SetValue(&cv_playercolor2, skincolor_redteam);
-			else if (p == &players[thirddisplayplayer])
+			else if (p == &players[displayplayers[2]])
 				CV_SetValue(&cv_playercolor3, skincolor_redteam);
-			else if (p == &players[fourthdisplayplayer])
+			else if (p == &players[displayplayers[3]])
 				CV_SetValue(&cv_playercolor4, skincolor_redteam);
 		}
 		else if (p->ctfteam == 2 && p->skincolor != skincolor_blueteam)
 		{
 			if (p == &players[consoleplayer])
 				CV_SetValue(&cv_playercolor, skincolor_blueteam);
-			else if (p == &players[secondarydisplayplayer])
+			else if (p == &players[displayplayers[1]])
 				CV_SetValue(&cv_playercolor2, skincolor_blueteam);
-			else if (p == &players[thirddisplayplayer])
+			else if (p == &players[displayplayers[2]])
 				CV_SetValue(&cv_playercolor3, skincolor_blueteam);
-			else if (p == &players[fourthdisplayplayer])
+			else if (p == &players[displayplayers[3]])
 				CV_SetValue(&cv_playercolor4, skincolor_blueteam);
 		}
 	}*/
@@ -2711,18 +2889,18 @@ void G_SpawnPlayer(INT32 playernum, boolean starpost)
 		if (nummapthings)
 		{
 			if (playernum == consoleplayer
-				|| (splitscreen && playernum == secondarydisplayplayer)
-				|| (splitscreen > 1 && playernum == thirddisplayplayer)
-				|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+				|| (splitscreen && playernum == displayplayers[1])
+				|| (splitscreen > 1 && playernum == displayplayers[2])
+				|| (splitscreen > 2 && playernum == displayplayers[3]))
 				CONS_Alert(CONS_ERROR, M_GetText("No player spawns found, spawning at the first mapthing!\n"));
 			spawnpoint = &mapthings[0];
 		}
 		else
 		{
 			if (playernum == consoleplayer
-			|| (splitscreen && playernum == secondarydisplayplayer)
-			|| (splitscreen > 1 && playernum == thirddisplayplayer)
-			|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+			|| (splitscreen && playernum == displayplayers[1])
+			|| (splitscreen > 1 && playernum == displayplayers[2])
+			|| (splitscreen > 2 && playernum == displayplayers[3]))
 				CONS_Alert(CONS_ERROR, M_GetText("No player spawns found, spawning at the origin!\n"));
 			//P_MovePlayerToSpawn handles this fine if the spawnpoint is NULL.
 		}
@@ -2742,9 +2920,9 @@ mapthing_t *G_FindCTFStart(INT32 playernum)
 	if (!numredctfstarts && !numbluectfstarts) //why even bother, eh?
 	{
 		if (playernum == consoleplayer
-			|| (splitscreen && playernum == secondarydisplayplayer)
-			|| (splitscreen > 1 && playernum == thirddisplayplayer)
-			|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+			|| (splitscreen && playernum == displayplayers[1])
+			|| (splitscreen > 1 && playernum == displayplayers[2])
+			|| (splitscreen > 2 && playernum == displayplayers[3]))
 			CONS_Alert(CONS_WARNING, M_GetText("No CTF starts in this map!\n"));
 		return NULL;
 	}
@@ -2754,9 +2932,9 @@ mapthing_t *G_FindCTFStart(INT32 playernum)
 		if (!numredctfstarts)
 		{
 			if (playernum == consoleplayer
-				|| (splitscreen && playernum == secondarydisplayplayer)
-				|| (splitscreen > 1 && playernum == thirddisplayplayer)
-				|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+				|| (splitscreen && playernum == displayplayers[1])
+				|| (splitscreen > 1 && playernum == displayplayers[2])
+				|| (splitscreen > 2 && playernum == displayplayers[3]))
 				CONS_Alert(CONS_WARNING, M_GetText("No Red Team starts in this map!\n"));
 			return NULL;
 		}
@@ -2769,9 +2947,9 @@ mapthing_t *G_FindCTFStart(INT32 playernum)
 		}
 
 		if (playernum == consoleplayer
-			|| (splitscreen && playernum == secondarydisplayplayer)
-			|| (splitscreen > 1 && playernum == thirddisplayplayer)
-			|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+			|| (splitscreen && playernum == displayplayers[1])
+			|| (splitscreen > 1 && playernum == displayplayers[2])
+			|| (splitscreen > 2 && playernum == displayplayers[3]))
 			CONS_Alert(CONS_WARNING, M_GetText("Could not spawn at any Red Team starts!\n"));
 		return NULL;
 	}
@@ -2780,9 +2958,9 @@ mapthing_t *G_FindCTFStart(INT32 playernum)
 		if (!numbluectfstarts)
 		{
 			if (playernum == consoleplayer
-				|| (splitscreen && playernum == secondarydisplayplayer)
-				|| (splitscreen > 1 && playernum == thirddisplayplayer)
-				|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+				|| (splitscreen && playernum == displayplayers[1])
+				|| (splitscreen > 1 && playernum == displayplayers[2])
+				|| (splitscreen > 2 && playernum == displayplayers[3]))
 				CONS_Alert(CONS_WARNING, M_GetText("No Blue Team starts in this map!\n"));
 			return NULL;
 		}
@@ -2794,9 +2972,9 @@ mapthing_t *G_FindCTFStart(INT32 playernum)
 				return bluectfstarts[i];
 		}
 		if (playernum == consoleplayer
-			|| (splitscreen && playernum == secondarydisplayplayer)
-			|| (splitscreen > 1 && playernum == thirddisplayplayer)
-			|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+			|| (splitscreen && playernum == displayplayers[1])
+			|| (splitscreen > 1 && playernum == displayplayers[2])
+			|| (splitscreen > 2 && playernum == displayplayers[3]))
 			CONS_Alert(CONS_WARNING, M_GetText("Could not spawn at any Blue Team starts!\n"));
 		return NULL;
 	}
@@ -2817,17 +2995,17 @@ mapthing_t *G_FindMatchStart(INT32 playernum)
 				return deathmatchstarts[i];
 		}
 		if (playernum == consoleplayer
-			|| (splitscreen && playernum == secondarydisplayplayer)
-			|| (splitscreen > 1 && playernum == thirddisplayplayer)
-			|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+			|| (splitscreen && playernum == displayplayers[1])
+			|| (splitscreen > 1 && playernum == displayplayers[2])
+			|| (splitscreen > 2 && playernum == displayplayers[3]))
 			CONS_Alert(CONS_WARNING, M_GetText("Could not spawn at any Deathmatch starts!\n"));
 		return NULL;
 	}
 
 	if (playernum == consoleplayer
-		|| (splitscreen && playernum == secondarydisplayplayer)
-		|| (splitscreen > 1 && playernum == thirddisplayplayer)
-		|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+		|| (splitscreen && playernum == displayplayers[1])
+		|| (splitscreen > 1 && playernum == displayplayers[2])
+		|| (splitscreen > 2 && playernum == displayplayers[3]))
 		CONS_Alert(CONS_WARNING, M_GetText("No Deathmatch starts in this map!\n"));
 	return NULL;
 }
@@ -2893,17 +3071,17 @@ mapthing_t *G_FindRaceStart(INT32 playernum)
 		//return playerstarts[0];
 
 		if (playernum == consoleplayer
-			|| (splitscreen && playernum == secondarydisplayplayer)
-			|| (splitscreen > 1 && playernum == thirddisplayplayer)
-			|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+			|| (splitscreen && playernum == displayplayers[1])
+			|| (splitscreen > 1 && playernum == displayplayers[2])
+			|| (splitscreen > 2 && playernum == displayplayers[3]))
 			CONS_Alert(CONS_WARNING, M_GetText("Could not spawn at any Race starts!\n"));
 		return NULL;
 	}
 
 	if (playernum == consoleplayer
-		|| (splitscreen && playernum == secondarydisplayplayer)
-		|| (splitscreen > 1 && playernum == thirddisplayplayer)
-		|| (splitscreen > 2 && playernum == fourthdisplayplayer))
+		|| (splitscreen && playernum == displayplayers[1])
+		|| (splitscreen > 1 && playernum == displayplayers[2])
+		|| (splitscreen > 2 && playernum == displayplayers[3]))
 		CONS_Alert(CONS_WARNING, M_GetText("No Race starts in this map!\n"));
 	return NULL;
 }
@@ -2995,14 +3173,11 @@ void G_DoReborn(INT32 playernum)
 			if (player->starpostnum) // SRB2kart
 				starpost = true;
 
-			if (camera.chase)
-				P_ResetCamera(&players[displayplayer], &camera);
-			if (camera2.chase && splitscreen > 0)
-				P_ResetCamera(&players[secondarydisplayplayer], &camera2);
-			if (camera3.chase && splitscreen > 1)
-				P_ResetCamera(&players[thirddisplayplayer], &camera3);
-			if (camera4.chase && splitscreen > 2)
-				P_ResetCamera(&players[fourthdisplayplayer], &camera4);
+			for (i = 0; i <= splitscreen; i++)
+			{
+				if (camera[i].chase)
+					P_ResetCamera(&players[displayplayers[i]], &camera[i]);
+			}
 
 			// clear cmd building stuff
 			memset(gamekeydown, 0, sizeof (gamekeydown));
@@ -3024,8 +3199,8 @@ void G_DoReborn(INT32 playernum)
 
 			if (botingame)
 			{ // Bots respawn next to their master.
-				players[secondarydisplayplayer].playerstate = PST_REBORN;
-				G_SpawnPlayer(secondarydisplayplayer, false);
+				players[displayplayers[1]].playerstate = PST_REBORN;
+				G_SpawnPlayer(displayplayers[1], false);
 			}
 		}
 		else
@@ -3064,6 +3239,8 @@ void G_AddPlayer(INT32 playernum)
 
 	p->jointime = 0;
 	p->playerstate = PST_REBORN;
+
+	demo_extradata[playernum] |= DXD_PLAYSTATE|DXD_COLOR|DXD_NAME|DXD_SKIN; // Set everything
 }
 
 void G_ExitLevel(void)
@@ -3086,9 +3263,41 @@ void G_ExitLevel(void)
 
 		// Remove CEcho text on round end.
 		HU_ClearCEcho();
+
+		// Don't save demos immediately here! Let standings write first
 	}
 }
 
+// See also the enum GameType in doomstat.h
+const char *Gametype_Names[NUMGAMETYPES] =
+{
+	"Race", // GT_RACE
+	"Battle" // GT_MATCH
+
+	/*"Co-op", // GT_COOP
+	"Competition", // GT_COMPETITION
+	"Team Match", // GT_TEAMMATCH
+	"Tag", // GT_TAG
+	"Hide and Seek", // GT_HIDEANDSEEK
+	"CTF" // GT_CTF*/
+};
+
+//
+// G_GetGametypeByName
+//
+// Returns the number for the given gametype name string, or -1 if not valid.
+//
+INT32 G_GetGametypeByName(const char *gametypestr)
+{
+	INT32 i;
+
+	for (i = 0; i < NUMGAMETYPES; i++)
+		if (!stricmp(gametypestr, Gametype_Names[i]))
+			return i;
+
+	return -1; // unknown gametype
+}
+
 //
 // G_IsSpecialStage
 //
@@ -3150,7 +3359,7 @@ boolean G_GametypeHasSpectators(void)
 #if 0
 	return (gametype != GT_COOP && gametype != GT_COMPETITION && gametype != GT_RACE);
 #else
-	return (netgame); //true
+	return (netgame || (multiplayer && demo.playback)); //true
 #endif
 }
 
@@ -3476,7 +3685,7 @@ static void G_DoCompleted(void)
 		}
 
 	// play some generic music if there's no win/cool/lose music going on (for exitlevel commands)
-	if (G_RaceGametype() && j == splitscreen+1 && (cv_inttime.value > 0))
+	if (G_RaceGametype() && ((multiplayer && demo.playback) || j == splitscreen+1) && (cv_inttime.value > 0))
 		S_ChangeMusicInternal("racent", true);
 
 	if (automapactive)
@@ -3486,6 +3695,8 @@ static void G_DoCompleted(void)
 
 	prevmap = (INT16)(gamemap-1);
 
+	if (demo.playback) goto demointermission;
+
 	// go to next level
 	// nextmap is 0-based, unlike gamemap
 	if (nextmapoverride != 0)
@@ -3588,12 +3799,15 @@ static void G_DoCompleted(void)
 			nextmap = G_RandMap(G_TOLFlag(gametype), prevmap, false, 0, false, NULL);
 	}
 
+
 	// We are committed to this map now.
 	// We may as well allocate its header if it doesn't exist
 	// (That is, if it's a real map)
 	if (nextmap < NUMMAPS && !mapheaderinfo[nextmap])
 		P_AllocMapHeader(nextmap);
 
+demointermission:
+
 	if (skipstats && !modeattacking) // Don't skip stats if we're in record attack
 		G_AfterIntermission();
 	else
@@ -3608,6 +3822,20 @@ void G_AfterIntermission(void)
 	HU_ClearCEcho();
 	//G_NextLevel();
 
+	if (demo.playback)
+	{
+		G_StopDemo();
+
+		if (demo.inreplayhut)
+			M_ReplayHut(0);
+		else
+			D_StartTitle();
+
+		return;
+	}
+	else if (demo.recording && (modeattacking || demo.savemode != DSM_NOTSAVING))
+		G_SaveDemo();
+
 	if (modeattacking) // End the run.
 	{
 		M_EndModeAttackRun();
@@ -3752,6 +3980,9 @@ static void G_DoContinued(void)
 // when something new is added.
 void G_EndGame(void)
 {
+	if (demo.recording && (modeattacking || demo.savemode != DSM_NOTSAVING))
+		G_SaveDemo();
+
 	// Only do evaluation and credits in coop games.
 	if (gametype == GT_COOP)
 	{
@@ -4119,7 +4350,7 @@ static void M_ForceLoadGameResponse(INT32 ch)
 	//set cursaveslot to -1 so nothing gets saved.
 	cursaveslot = -1;
 
-	displayplayer = consoleplayer;
+	displayplayers[0] = consoleplayer;
 	multiplayer = false;
 	splitscreen = 0;
 	SplitScreen_OnChange(); // not needed?
@@ -4183,7 +4414,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 	}
 	save_p += VERSIONSIZE;
 
-	if (demoplayback) // reset game engine
+	if (demo.playback) // reset game engine
 		G_StopDemo();
 
 //	paused = false;
@@ -4209,7 +4440,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 
 //	gameaction = ga_nothing;
 //	G_SetGamestate(GS_LEVEL);
-	displayplayer = consoleplayer;
+	displayplayers[0] = consoleplayer;
 	multiplayer = false;
 	splitscreen = 0;
 	SplitScreen_OnChange(); // not needed?
@@ -4274,7 +4505,7 @@ void G_SaveGame(UINT32 savegameslot)
 //
 // G_DeferedInitNew
 // Can be called by the startup code or the menu task,
-// consoleplayer, displayplayer, playeringame[] should be set.
+// consoleplayer, displayplayers[], playeringame[] should be set.
 //
 void G_DeferedInitNew(boolean pencoremode, const char *mapname, INT32 pickedchar, UINT8 ssplayers, boolean FLS)
 {
@@ -4282,7 +4513,7 @@ void G_DeferedInitNew(boolean pencoremode, const char *mapname, INT32 pickedchar
 	UINT8 color = 0;
 	paused = false;
 
-	if (demoplayback)
+	if (demo.playback)
 		COM_BufAddText("stopdemo\n");
 
 	while (ghosts)
@@ -4344,7 +4575,7 @@ void G_InitNew(UINT8 pencoremode, const char *mapname, boolean resetplayer, bool
 	legitimateexit = false; // SRB2Kart
 	comebackshowninfo = false;
 
-	if (!demoplayback && !netgame) // Netgame sets random seed elsewhere, demo playback sets seed just before us!
+	if (!demo.playback && !netgame) // Netgame sets random seed elsewhere, demo playback sets seed just before us!
 		P_SetRandSeed(M_RandomizedSeed()); // Use a more "Random" random seed
 
 	//SRB2Kart - Score is literally the only thing you SHOULDN'T reset at all times
@@ -4390,7 +4621,7 @@ void G_InitNew(UINT8 pencoremode, const char *mapname, boolean resetplayer, bool
 
 			players[i].marescore = 0;
 
-			if (resetplayer) // SRB2Kart
+			if (resetplayer && !(multiplayer && demo.playback)) // SRB2Kart
 			{
 				players[i].score = 0;
 			}
@@ -4499,7 +4730,7 @@ char *G_BuildMapTitle(INT32 mapnum)
 // DEMO RECORDING
 //
 
-#define DEMOVERSION 0x0001
+#define DEMOVERSION 0x0002
 #define DEMOHEADER  "\xF0" "KartReplay" "\x0F"
 
 #define DF_GHOST        0x01 // This demo contains ghost data too!
@@ -4507,6 +4738,16 @@ char *G_BuildMapTitle(INT32 mapnum)
 #define DF_NIGHTSATTACK 0x04 // This demo is from NiGHTS attack and contains its time left, score, and mares!
 #define DF_ATTACKMASK   0x06 // This demo is from ??? attack and contains ???
 #define DF_ATTACKSHIFT  1
+#define DF_ENCORE       0x40
+#define DF_MULTIPLAYER  0x80 // This demo was recorded in multiplayer mode!
+
+#ifdef DEMO_COMPAT_100
+#define DF_FILELIST     0x08 // This demo contains an extra files list
+#define DF_GAMETYPEMASK 0x30
+#define DF_GAMESHIFT    4
+#endif
+
+#define DEMO_SPECTATOR 0x40
 
 // For demos
 #define ZT_FWD     0x01
@@ -4515,9 +4756,20 @@ char *G_BuildMapTitle(INT32 mapnum)
 #define ZT_BUTTONS 0x08
 #define ZT_AIMING  0x10
 #define ZT_DRIFT   0x20
+#define ZT_LATENCY 0x40
 #define DEMOMARKER 0x80 // demoend
 
-static ticcmd_t oldcmd;
+UINT8 demo_extradata[MAXPLAYERS];
+UINT8 demo_writerng; // 0=no, 1=yes, 2=yes but on a timeout
+static ticcmd_t oldcmd[MAXPLAYERS];
+
+#define DW_END        0xFF // End of extradata block
+#define DW_RNG        0xFE // Check RNG seed!
+
+#define DW_EXTRASTUFF 0xFE // Numbers below this are reserved for writing player slot data
+
+// Below consts are only used for demo extrainfo sections
+#define DW_STANDING 0x00
 
 // For Metal Sonic and time attack ghosts
 #define GZT_XYZ    0x01
@@ -4539,8 +4791,9 @@ static ticcmd_t oldcmd;
 #define EZT_SCALE  0x10 // Changed size
 #define EZT_HIT    0x20 // Damaged a mobj
 #define EZT_SPRITE 0x40 // Changed sprite set completely out of PLAY (NiGHTS, SOCs, whatever)
+#define EZT_KART   0x80 // SRB2Kart: Changed current held item/quantity and bumpers for battle
 
-static mobj_t oldmetal, oldghost;
+static mobj_t oldmetal, oldghost[MAXPLAYERS];
 
 void G_SaveMetal(UINT8 **buffer)
 {
@@ -4578,98 +4831,347 @@ ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
 	return dest;
 }
 
-void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
+// Finds a skin with the closest stats if the expected skin doesn't exist.
+static INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight)
 {
-	UINT8 ziptic;
-	(void)playernum;
+	INT32 i, closest_skin = 0;
+	UINT8 closest_stats = UINT8_MAX, stat_diff;
 
-	if (!demo_p || !demo_start)
-		return;
-	ziptic = READUINT8(demo_p);
-
-	if (ziptic & ZT_FWD)
-		oldcmd.forwardmove = READSINT8(demo_p);
-	if (ziptic & ZT_SIDE)
-		oldcmd.sidemove = READSINT8(demo_p);
-	if (ziptic & ZT_ANGLE)
-		oldcmd.angleturn = READINT16(demo_p);
-	if (ziptic & ZT_BUTTONS)
-		oldcmd.buttons = (oldcmd.buttons & (BT_FORWARD|BT_BACKWARD)) | (READUINT16(demo_p) & ~(BT_FORWARD|BT_BACKWARD));
-	if (ziptic & ZT_AIMING)
-		oldcmd.aiming = READINT16(demo_p);
-	if (ziptic & ZT_DRIFT)
-		oldcmd.driftturn = READINT16(demo_p);
+	for (i = 0; i < numskins; i++)
+	{
+		stat_diff = abs(skins[i].kartspeed - kartspeed) + abs(skins[i].kartweight - kartweight);
+		if (stat_diff < closest_stats)
+		{
+			closest_stats = stat_diff;
+			closest_skin = i;
+		}
+	}
 
-	G_CopyTiccmd(cmd, &oldcmd, 1);
+	return closest_skin;
+}
 
-	// SRB2kart: Copy-pasted from ticcmd building, removes that crappy demo cam
-	if (((players[displayplayer].mo && players[displayplayer].speed > 0) // Moving
-		|| (leveltime > starttime && (cmd->buttons & BT_ACCELERATE && cmd->buttons & BT_BRAKE)) // Rubber-burn turn
-		|| (players[displayplayer].kartstuff[k_respawn]) // Respawning
-		|| (players[displayplayer].spectator || objectplacing)) // Not a physical player
-		&& !(players[displayplayer].kartstuff[k_spinouttimer] && players[displayplayer].kartstuff[k_sneakertimer])) // Spinning and boosting cancels out spinout
-		localangle += (cmd->angleturn<<16);
+static void FindClosestSkinForStats(UINT32 p, UINT8 kartspeed, UINT8 kartweight)
+{
+	INT32 closest_skin = GetSkinNumClosestToStats(kartspeed, kartweight);
 
-	if (!(demoflags & DF_GHOST) && *demo_p == DEMOMARKER)
-	{
-		// end of demo data stream
-		G_CheckDemoStatus();
-		return;
-	}
+	//CONS_Printf("Using %s instead...\n", skins[closest_skin].name);
+	SetPlayerSkinByNum(p, closest_skin);
 }
 
-void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
+void G_ReadDemoExtraData(void)
 {
-	char ziptic = 0;
-	UINT8 *ziptic_p;
-	(void)playernum;
+	INT32 p, extradata, i;
+	char name[17];
 
-	if (!demo_p)
+	memset(name, '\0', 17);
+
+	p = READUINT8(demo_p);
+
+	while (p < DW_EXTRASTUFF)
+	{
+		extradata = READUINT8(demo_p);
+
+		if (extradata & DXD_RESPAWN)
+		{
+			if (players[p].mo)
+				P_DamageMobj(players[p].mo, NULL, NULL, 10000); // Is this how this should work..?
+		}
+		if (extradata & DXD_SKIN)
+		{
+			UINT8 kartspeed, kartweight;
+
+			// Skin
+			M_Memcpy(name, demo_p, 16);
+			demo_p += 16;
+			SetPlayerSkin(p, name);
+
+			kartspeed = READUINT8(demo_p);
+			kartweight = READUINT8(demo_p);
+
+
+			if (stricmp(skins[players[p].skin].name, name) != 0)
+				FindClosestSkinForStats(p, kartspeed, kartweight);
+
+			players[p].kartspeed = kartspeed;
+			players[p].kartweight = kartweight;
+		}
+		if (extradata & DXD_COLOR)
+		{
+			// Color
+			M_Memcpy(name, demo_p, 16);
+			demo_p += 16;
+			for (i = 0; i < MAXSKINCOLORS; i++)
+				if (!stricmp(KartColor_Names[i], name))				// SRB2kart
+				{
+					players[p].skincolor = i;
+					if (players[p].mo)
+						players[p].mo->color = i;
+					break;
+				}
+		}
+		if (extradata & DXD_NAME)
+		{
+			// Name
+			M_Memcpy(player_names[p],demo_p,16);
+			demo_p += 16;
+		}
+		if (extradata & DXD_PLAYSTATE)
+		{
+			extradata = READUINT8(demo_p);
+
+			switch (extradata) {
+			case DXD_PST_PLAYING:
+				players[p].pflags |= PF_WANTSTOJOIN; // fuck you
+				break;
+
+			case DXD_PST_SPECTATING:
+				players[p].pflags &= ~PF_WANTSTOJOIN; // double-fuck you
+				if (!playeringame[p])
+				{
+					CL_ClearPlayer(p);
+					playeringame[p] = true;
+					G_AddPlayer(p);
+					players[p].spectator = true;
+
+					// There's likely an off-by-one error in timing recording or playback of joins. This hacks around it so I don't have to find out where that is. \o/
+					if (oldcmd[p].forwardmove)
+						P_RandomByte();
+				}
+				else
+				{
+					players[p].spectator = true;
+					if (players[p].mo)
+						P_DamageMobj(players[p].mo, NULL, NULL, 10000);
+					else
+						players[p].playerstate = PST_REBORN;
+				}
+				break;
+
+			case DXD_PST_LEFT:
+				CL_RemovePlayer(p, 0);
+				break;
+			}
+
+			G_ResetViews();
+
+			// maybe these are necessary?
+			if (G_BattleGametype())
+				K_CheckBumpers(); // SRB2Kart
+			else if (G_RaceGametype())
+				P_CheckRacers(); // also SRB2Kart
+		}
+
+
+		p = READUINT8(demo_p);
+	}
+
+	while (p != DW_END)
+	{
+		UINT32 rng;
+
+		switch (p)
+		{
+		case DW_RNG:
+			rng = READUINT32(demo_p);
+			if (P_GetRandSeed() != rng)
+			{
+				P_SetRandSeed(rng);
+
+				if (demosynced)
+					CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
+				demosynced = false;
+			}
+		}
+
+		p = READUINT8(demo_p);
+	}
+
+	if (!(demoflags & DF_GHOST) && *demo_p == DEMOMARKER)
+	{
+		// end of demo data stream
+		G_CheckDemoStatus();
+		return;
+	}
+}
+
+void G_WriteDemoExtraData(void)
+{
+	INT32 i;
+	char name[16];
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (demo_extradata[i])
+		{
+			WRITEUINT8(demo_p, i);
+			WRITEUINT8(demo_p, demo_extradata[i]);
+
+			//if (demo_extradata[i] & DXD_RESPAWN) has no extra data
+			if (demo_extradata[i] & DXD_SKIN)
+			{
+				// Skin
+				memset(name, 0, 16);
+				strncpy(name, skins[players[i].skin].name, 16);
+				M_Memcpy(demo_p,name,16);
+				demo_p += 16;
+
+				WRITEUINT8(demo_p, skins[players[i].skin].kartspeed);
+				WRITEUINT8(demo_p, skins[players[i].skin].kartweight);
+			}
+			if (demo_extradata[i] & DXD_COLOR)
+			{
+				// Color
+				memset(name, 0, 16);
+				strncpy(name, KartColor_Names[players[i].skincolor], 16);
+				M_Memcpy(demo_p,name,16);
+				demo_p += 16;
+			}
+			if (demo_extradata[i] & DXD_NAME)
+			{
+				// Name
+				memset(name, 0, 16);
+				strncpy(name, player_names[i], 16);
+				M_Memcpy(demo_p,name,16);
+				demo_p += 16;
+			}
+			if (demo_extradata[i] & DXD_PLAYSTATE)
+			{
+				demo_writerng = 1;
+				if (!playeringame[i])
+					WRITEUINT8(demo_p, DXD_PST_LEFT);
+				else if (
+					players[i].spectator &&
+					!(players[i].pflags & PF_WANTSTOJOIN) // <= fuck you specifically
+				)
+					WRITEUINT8(demo_p, DXD_PST_SPECTATING);
+				else
+					WRITEUINT8(demo_p, DXD_PST_PLAYING);
+			}
+		}
+
+		demo_extradata[i] = 0;
+	}
+
+	// May not be necessary, but might as well play it safe...
+	if ((leveltime & 255) == 128)
+		demo_writerng = 1;
+
+	{
+		static UINT8 timeout = 0;
+
+		if (timeout) timeout--;
+
+		if (demo_writerng == 1 || (demo_writerng == 2 && timeout == 0))
+		{
+			demo_writerng = 0;
+			timeout = 16;
+			WRITEUINT8(demo_p, DW_RNG);
+			WRITEUINT32(demo_p, P_GetRandSeed());
+		}
+	}
+
+	WRITEUINT8(demo_p, DW_END);
+}
+
+void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
+{
+	UINT8 ziptic;
+
+	if (!demo_p || !demo.deferstart)
+		return;
+	ziptic = READUINT8(demo_p);
+
+	if (ziptic & ZT_FWD)
+		oldcmd[playernum].forwardmove = READSINT8(demo_p);
+	if (ziptic & ZT_SIDE)
+		oldcmd[playernum].sidemove = READSINT8(demo_p);
+	if (ziptic & ZT_ANGLE)
+		oldcmd[playernum].angleturn = READINT16(demo_p);
+	if (ziptic & ZT_BUTTONS)
+		oldcmd[playernum].buttons = READUINT16(demo_p);
+	if (ziptic & ZT_AIMING)
+		oldcmd[playernum].aiming = READINT16(demo_p);
+	if (ziptic & ZT_DRIFT)
+		oldcmd[playernum].driftturn = READINT16(demo_p);
+	if (ziptic & ZT_LATENCY)
+		oldcmd[playernum].latency = READUINT8(demo_p);
+
+	G_CopyTiccmd(cmd, &oldcmd[playernum], 1);
+
+	// SRB2kart: Copy-pasted from ticcmd building, removes that crappy demo cam
+	if (((players[displayplayers[0]].mo && players[displayplayers[0]].speed > 0) // Moving
+		|| (leveltime > starttime && (cmd->buttons & BT_ACCELERATE && cmd->buttons & BT_BRAKE)) // Rubber-burn turn
+		|| (players[displayplayers[0]].kartstuff[k_respawn]) // Respawning
+		|| (players[displayplayers[0]].spectator || objectplacing)) // Not a physical player
+		&& !(players[displayplayers[0]].kartstuff[k_spinouttimer] && players[displayplayers[0]].kartstuff[k_sneakertimer])) // Spinning and boosting cancels out spinout
+		localangle[0] += (cmd->angleturn<<16);
+
+	if (!(demoflags & DF_GHOST) && *demo_p == DEMOMARKER)
+	{
+		// end of demo data stream
+		G_CheckDemoStatus();
+		return;
+	}
+}
+
+void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
+{
+	char ziptic = 0;
+	UINT8 *ziptic_p;
+
+	if (!demo_p)
 		return;
 	ziptic_p = demo_p++; // the ziptic, written at the end of this function
 
-	if (cmd->forwardmove != oldcmd.forwardmove)
+	if (cmd->forwardmove != oldcmd[playernum].forwardmove)
 	{
 		WRITEUINT8(demo_p,cmd->forwardmove);
-		oldcmd.forwardmove = cmd->forwardmove;
+		oldcmd[playernum].forwardmove = cmd->forwardmove;
 		ziptic |= ZT_FWD;
 	}
 
-	if (cmd->sidemove != oldcmd.sidemove)
+	if (cmd->sidemove != oldcmd[playernum].sidemove)
 	{
 		WRITEUINT8(demo_p,cmd->sidemove);
-		oldcmd.sidemove = cmd->sidemove;
+		oldcmd[playernum].sidemove = cmd->sidemove;
 		ziptic |= ZT_SIDE;
 	}
 
-	if (cmd->angleturn != oldcmd.angleturn)
+	if (cmd->angleturn != oldcmd[playernum].angleturn)
 	{
 		WRITEINT16(demo_p,cmd->angleturn);
-		oldcmd.angleturn = cmd->angleturn;
+		oldcmd[playernum].angleturn = cmd->angleturn;
 		ziptic |= ZT_ANGLE;
 	}
 
-	if (cmd->buttons != oldcmd.buttons)
+	if (cmd->buttons != oldcmd[playernum].buttons)
 	{
 		WRITEUINT16(demo_p,cmd->buttons);
-		oldcmd.buttons = cmd->buttons;
+		oldcmd[playernum].buttons = cmd->buttons;
 		ziptic |= ZT_BUTTONS;
 	}
 
-	if (cmd->aiming != oldcmd.aiming)
+	if (cmd->aiming != oldcmd[playernum].aiming)
 	{
 		WRITEINT16(demo_p,cmd->aiming);
-		oldcmd.aiming = cmd->aiming;
+		oldcmd[playernum].aiming = cmd->aiming;
 		ziptic |= ZT_AIMING;
 	}
 
-	if (cmd->driftturn != oldcmd.driftturn)
+	if (cmd->driftturn != oldcmd[playernum].driftturn)
 	{
 		WRITEINT16(demo_p,cmd->driftturn);
-		oldcmd.driftturn = cmd->driftturn;
+		oldcmd[playernum].driftturn = cmd->driftturn;
 		ziptic |= ZT_DRIFT;
 	}
 
+	if (cmd->latency != oldcmd[playernum].latency)
+	{
+		WRITEUINT8(demo_p,cmd->latency);
+		oldcmd[playernum].latency = cmd->latency;
+		ziptic |= ZT_LATENCY;
+	}
+
 	*ziptic_p = ziptic;
 
 	// attention here for the ticcmd size!
@@ -4681,71 +5183,93 @@ void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
 	}
 }
 
-void G_GhostAddThok(void)
+void G_GhostAddThok(INT32 playernum)
 {
-	if (!demorecording || !(demoflags & DF_GHOST))
+	if (!demo.recording || !(demoflags & DF_GHOST))
 		return;
-	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_THOK;
+	ghostext[playernum].flags = (ghostext[playernum].flags & ~EZT_THOKMASK) | EZT_THOK;
 }
 
-void G_GhostAddSpin(void)
+void G_GhostAddSpin(INT32 playernum)
 {
-	if (!demorecording || !(demoflags & DF_GHOST))
+	if (!demo.recording || !(demoflags & DF_GHOST))
 		return;
-	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_SPIN;
+	ghostext[playernum].flags = (ghostext[playernum].flags & ~EZT_THOKMASK) | EZT_SPIN;
 }
 
-void G_GhostAddRev(void)
+void G_GhostAddRev(INT32 playernum)
 {
-	if (!demorecording || !(demoflags & DF_GHOST))
+	if (!demo.recording || !(demoflags & DF_GHOST))
 		return;
-	ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_REV;
+	ghostext[playernum].flags = (ghostext[playernum].flags & ~EZT_THOKMASK) | EZT_REV;
 }
 
-void G_GhostAddFlip(void)
+void G_GhostAddFlip(INT32 playernum)
 {
-	if (!demorecording || !(demoflags & DF_GHOST))
+	if (!demo.recording || !(demoflags & DF_GHOST))
 		return;
-	ghostext.flags |= EZT_FLIP;
+	ghostext[playernum].flags |= EZT_FLIP;
 }
 
-void G_GhostAddColor(ghostcolor_t color)
+void G_GhostAddColor(INT32 playernum, ghostcolor_t color)
 {
-	if (!demorecording || !(demoflags & DF_GHOST))
+	if (!demo.recording || !(demoflags & DF_GHOST))
 		return;
-	if (ghostext.lastcolor == (UINT8)color)
+	if (ghostext[playernum].lastcolor == (UINT8)color)
 	{
-		ghostext.flags &= ~EZT_COLOR;
+		ghostext[playernum].flags &= ~EZT_COLOR;
 		return;
 	}
-	ghostext.flags |= EZT_COLOR;
-	ghostext.color = (UINT8)color;
+	ghostext[playernum].flags |= EZT_COLOR;
+	ghostext[playernum].color = (UINT8)color;
 }
 
-void G_GhostAddScale(fixed_t scale)
+void G_GhostAddScale(INT32 playernum, fixed_t scale)
 {
-	if (!demorecording || !(demoflags & DF_GHOST))
+	if (!demo.recording || !(demoflags & DF_GHOST))
 		return;
-	if (ghostext.lastscale == scale)
+	if (ghostext[playernum].lastscale == scale)
 	{
-		ghostext.flags &= ~EZT_SCALE;
+		ghostext[playernum].flags &= ~EZT_SCALE;
 		return;
 	}
-	ghostext.flags |= EZT_SCALE;
-	ghostext.scale = scale;
+	ghostext[playernum].flags |= EZT_SCALE;
+	ghostext[playernum].scale = scale;
 }
 
-void G_GhostAddHit(mobj_t *victim)
+void G_GhostAddHit(INT32 playernum, mobj_t *victim)
 {
-	if (!demorecording || !(demoflags & DF_GHOST))
+	if (!demo.recording || !(demoflags & DF_GHOST))
 		return;
-	ghostext.flags |= EZT_HIT;
-	ghostext.hits++;
-	ghostext.hitlist = Z_Realloc(ghostext.hitlist, ghostext.hits * sizeof(mobj_t *), PU_LEVEL, NULL);
-	ghostext.hitlist[ghostext.hits-1] = victim;
+	ghostext[playernum].flags |= EZT_HIT;
+	ghostext[playernum].hits++;
+	ghostext[playernum].hitlist = Z_Realloc(ghostext[playernum].hitlist, ghostext[playernum].hits * sizeof(mobj_t *), PU_LEVEL, NULL);
+	ghostext[playernum].hitlist[ghostext[playernum].hits-1] = victim;
+}
+
+void G_WriteAllGhostTics(void)
+{
+	INT32 i, counter = leveltime;
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i] || players[i].spectator)
+			continue;
+
+		if (!players[i].mo)
+			continue;
+
+		counter++;
+
+		if (counter % cv_netdemosyncquality.value != 0) // Only write 1 in this many ghost datas per tic to cut down on multiplayer replay size.
+			continue;
+
+		WRITEUINT8(demo_p, i);
+		G_WriteGhostTic(players[i].mo, i);
+	}
+	WRITEUINT8(demo_p, 0xFF);
 }
 
-void G_WriteGhostTic(mobj_t *ghost)
+void G_WriteGhostTic(mobj_t *ghost, INT32 playernum)
 {
 	char ziptic = 0;
 	UINT8 *ziptic_p;
@@ -4767,41 +5291,41 @@ void G_WriteGhostTic(mobj_t *ghost)
 
 	ziptic_p = demo_p++; // the ziptic, written at the end of this function
 
-	#define MAXMOM (0xFFFF<<8)
+	#define MAXMOM (0x7FFF<<8)
 
 	// GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic.
-	if (abs(ghost->x-oldghost.x) > MAXMOM
-	|| abs(ghost->y-oldghost.y) > MAXMOM
-	|| abs(ghost->z-oldghost.z) > MAXMOM
-	|| (leveltime & 255) == 1) // Hack to enable slightly nicer resyncing
-	{
-		oldghost.x = ghost->x;
-		oldghost.y = ghost->y;
-		oldghost.z = ghost->z;
+	if (abs(ghost->x-oldghost[playernum].x) > MAXMOM
+	|| abs(ghost->y-oldghost[playernum].y) > MAXMOM
+	|| abs(ghost->z-oldghost[playernum].z) > MAXMOM
+	|| ((UINT8)(leveltime & 255) > 0 && (UINT8)(leveltime & 255) <= (UINT8)cv_netdemosyncquality.value)) // Hack to enable slightly nicer resyncing
+	{
+		oldghost[playernum].x = ghost->x;
+		oldghost[playernum].y = ghost->y;
+		oldghost[playernum].z = ghost->z;
 		ziptic |= GZT_XYZ;
-		WRITEFIXED(demo_p,oldghost.x);
-		WRITEFIXED(demo_p,oldghost.y);
-		WRITEFIXED(demo_p,oldghost.z);
+		WRITEFIXED(demo_p,oldghost[playernum].x);
+		WRITEFIXED(demo_p,oldghost[playernum].y);
+		WRITEFIXED(demo_p,oldghost[playernum].z);
 	}
 	else
 	{
 		// For moving normally:
 		// Store one full byte of movement, plus one byte of fractional movement.
-		INT16 momx = (INT16)((ghost->x-oldghost.x + (1<<4))>>8);
-		INT16 momy = (INT16)((ghost->y-oldghost.y + (1<<4))>>8);
-		if (momx != oldghost.momx
-		|| momy != oldghost.momy)
+		INT16 momx = (INT16)((ghost->x-oldghost[playernum].x + (1<<4))>>8);
+		INT16 momy = (INT16)((ghost->y-oldghost[playernum].y + (1<<4))>>8);
+		if (momx != oldghost[playernum].momx
+		|| momy != oldghost[playernum].momy)
 		{
-			oldghost.momx = momx;
-			oldghost.momy = momy;
+			oldghost[playernum].momx = momx;
+			oldghost[playernum].momy = momy;
 			ziptic |= GZT_MOMXY;
 			WRITEINT16(demo_p,momx);
 			WRITEINT16(demo_p,momy);
 		}
-		momx = (INT16)((ghost->z-oldghost.z + (1<<4))>>8);
-		if (momx != oldghost.momz)
+		momx = (INT16)((ghost->z-oldghost[playernum].z + (1<<4))>>8);
+		if (momx != oldghost[playernum].momz)
 		{
-			oldghost.momz = momx;
+			oldghost[playernum].momz = momx;
 			ziptic |= GZT_MOMZ;
 			WRITEINT16(demo_p,momx);
 		}
@@ -4809,9 +5333,9 @@ void G_WriteGhostTic(mobj_t *ghost)
 		// This SHOULD set oldghost.x/y/z to match ghost->x/y/z
 		// but it keeps the fractional loss of one byte,
 		// so it will hopefully be made up for in future tics.
-		oldghost.x += oldghost.momx<<8;
-		oldghost.y += oldghost.momy<<8;
-		oldghost.z += oldghost.momz<<8;
+		oldghost[playernum].x += oldghost[playernum].momx<<8;
+		oldghost[playernum].y += oldghost[playernum].momy<<8;
+		oldghost[playernum].z += oldghost[playernum].momz<<8;
 	}
 
 	#undef MAXMOM
@@ -4819,56 +5343,72 @@ void G_WriteGhostTic(mobj_t *ghost)
 	// Only store the 8 most relevant bits of angle
 	// because exact values aren't too easy to discern to begin with when only 8 angles have different sprites
 	// and it does not affect this mode of movement at all anyway.
-	if (ghost->angle>>24 != oldghost.angle)
+	if (ghost->angle>>24 != oldghost[playernum].angle)
 	{
-		oldghost.angle = ghost->angle>>24;
+		oldghost[playernum].angle = ghost->angle>>24;
 		ziptic |= GZT_ANGLE;
-		WRITEUINT8(demo_p,oldghost.angle);
+		WRITEUINT8(demo_p,oldghost[playernum].angle);
 	}
 
 	// Store the sprite frame.
 	frame = ghost->frame & 0xFF;
-	if (frame != oldghost.frame)
+	if (frame != oldghost[playernum].frame)
 	{
-		oldghost.frame = frame;
+		oldghost[playernum].frame = frame;
 		ziptic |= GZT_SPRITE;
-		WRITEUINT8(demo_p,oldghost.frame);
+		WRITEUINT8(demo_p,oldghost[playernum].frame);
 	}
 
 	// Check for sprite set changes
 	sprite = ghost->sprite;
-	if (sprite != oldghost.sprite)
+	if (sprite != oldghost[playernum].sprite)
 	{
-		oldghost.sprite = sprite;
-		ghostext.flags |= EZT_SPRITE;
+		oldghost[playernum].sprite = sprite;
+		ghostext[playernum].flags |= EZT_SPRITE;
 	}
 
-	if (ghostext.flags)
+	if (ghost->player)
 	{
-		ziptic |= GZT_EXTRA;
+		if (
+			ghostext[playernum].kartitem != ghost->player->kartstuff[k_itemtype] ||
+			ghostext[playernum].kartamount != ghost->player->kartstuff[k_itemamount] ||
+			ghostext[playernum].kartbumpers != ghost->player->kartstuff[k_bumper]
+		)
+		{
+			ghostext[playernum].flags |= EZT_KART;
+			ghostext[playernum].kartitem = ghost->player->kartstuff[k_itemtype];
+			ghostext[playernum].kartamount = ghost->player->kartstuff[k_itemamount];
+			ghostext[playernum].kartbumpers = ghost->player->kartstuff[k_bumper];
+
+		}
+	}
+
+	if (ghostext[playernum].color == ghostext[playernum].lastcolor)
+		ghostext[playernum].flags &= ~EZT_COLOR;
+	if (ghostext[playernum].scale == ghostext[playernum].lastscale)
+		ghostext[playernum].flags &= ~EZT_SCALE;
 
-		if (ghostext.color == ghostext.lastcolor)
-			ghostext.flags &= ~EZT_COLOR;
-		if (ghostext.scale == ghostext.lastscale)
-			ghostext.flags &= ~EZT_SCALE;
+	if (ghostext[playernum].flags)
+	{
+		ziptic |= GZT_EXTRA;
+		WRITEUINT8(demo_p,ghostext[playernum].flags);
 
-		WRITEUINT8(demo_p,ghostext.flags);
-		if (ghostext.flags & EZT_COLOR)
+		if (ghostext[playernum].flags & EZT_COLOR)
 		{
-			WRITEUINT8(demo_p,ghostext.color);
-			ghostext.lastcolor = ghostext.color;
+			WRITEUINT8(demo_p,ghostext[playernum].color);
+			ghostext[playernum].lastcolor = ghostext[playernum].color;
 		}
-		if (ghostext.flags & EZT_SCALE)
+		if (ghostext[playernum].flags & EZT_SCALE)
 		{
-			WRITEFIXED(demo_p,ghostext.scale);
-			ghostext.lastscale = ghostext.scale;
+			WRITEFIXED(demo_p,ghostext[playernum].scale);
+			ghostext[playernum].lastscale = ghostext[playernum].scale;
 		}
-		if (ghostext.flags & EZT_HIT)
+		if (ghostext[playernum].flags & EZT_HIT)
 		{
-			WRITEUINT16(demo_p,ghostext.hits);
-			for (i = 0; i < ghostext.hits; i++)
+			WRITEUINT16(demo_p,ghostext[playernum].hits);
+			for (i = 0; i < ghostext[playernum].hits; i++)
 			{
-				mobj_t *mo = ghostext.hitlist[i];
+				mobj_t *mo = ghostext[playernum].hitlist[i];
 				WRITEUINT32(demo_p,UINT32_MAX); // reserved for some method of determining exactly which mobj this is. (mobjnum doesn't work here.)
 				WRITEUINT32(demo_p,mo->type);
 				WRITEUINT16(demo_p,(UINT16)mo->health);
@@ -4877,13 +5417,19 @@ void G_WriteGhostTic(mobj_t *ghost)
 				WRITEFIXED(demo_p,mo->z);
 				WRITEANGLE(demo_p,mo->angle);
 			}
-			Z_Free(ghostext.hitlist);
-			ghostext.hits = 0;
-			ghostext.hitlist = NULL;
+			Z_Free(ghostext[playernum].hitlist);
+			ghostext[playernum].hits = 0;
+			ghostext[playernum].hitlist = NULL;
 		}
-		if (ghostext.flags & EZT_SPRITE)
+		if (ghostext[playernum].flags & EZT_SPRITE)
 			WRITEUINT8(demo_p,sprite);
-		ghostext.flags = 0;
+		if (ghostext[playernum].flags & EZT_KART)
+		{
+			WRITEINT32(demo_p, ghostext[playernum].kartitem);
+			WRITEINT32(demo_p, ghostext[playernum].kartamount);
+			WRITEINT32(demo_p, ghostext[playernum].kartbumpers);
+		}
+		ghostext[playernum].flags = 0;
 	}
 
 	*ziptic_p = ziptic;
@@ -4897,9 +5443,27 @@ void G_WriteGhostTic(mobj_t *ghost)
 	}
 }
 
+void G_ConsAllGhostTics(void)
+{
+	UINT8 p = READUINT8(demo_p);
+
+	while (p != 0xFF)
+	{
+		G_ConsGhostTic(p);
+		p = READUINT8(demo_p);
+	}
+
+	if (*demo_p == DEMOMARKER)
+	{
+		// end of demo data stream
+		G_CheckDemoStatus();
+		return;
+	}
+}
+
 // Uses ghost data to do consistency checks on your position.
 // This fixes desynchronising demos when fighting eggman.
-void G_ConsGhostTic(void)
+void G_ConsGhostTic(INT32 playernum)
 {
 	UINT8 ziptic;
 	fixed_t px,py,pz,gx,gy,gz;
@@ -4907,34 +5471,34 @@ void G_ConsGhostTic(void)
 	fixed_t syncleeway;
 	boolean nightsfail = false;
 
-	if (!demo_p || !demo_start)
+	if (!demo_p || !demo.deferstart)
 		return;
 	if (!(demoflags & DF_GHOST))
 		return; // No ghost data to use.
 
-	testmo = players[0].mo;
+	testmo = players[playernum].mo;
 
 	// Grab ghost data.
 	ziptic = READUINT8(demo_p);
 	if (ziptic & GZT_XYZ)
 	{
-		oldghost.x = READFIXED(demo_p);
-		oldghost.y = READFIXED(demo_p);
-		oldghost.z = READFIXED(demo_p);
+		oldghost[playernum].x = READFIXED(demo_p);
+		oldghost[playernum].y = READFIXED(demo_p);
+		oldghost[playernum].z = READFIXED(demo_p);
 		syncleeway = 0;
 	}
 	else
 	{
 		if (ziptic & GZT_MOMXY)
 		{
-			oldghost.momx = READINT16(demo_p)<<8;
-			oldghost.momy = READINT16(demo_p)<<8;
+			oldghost[playernum].momx = READINT16(demo_p)<<8;
+			oldghost[playernum].momy = READINT16(demo_p)<<8;
 		}
 		if (ziptic & GZT_MOMZ)
-			oldghost.momz = READINT16(demo_p)<<8;
-		oldghost.x += oldghost.momx;
-		oldghost.y += oldghost.momy;
-		oldghost.z += oldghost.momz;
+			oldghost[playernum].momz = READINT16(demo_p)<<8;
+		oldghost[playernum].x += oldghost[playernum].momx;
+		oldghost[playernum].y += oldghost[playernum].momy;
+		oldghost[playernum].z += oldghost[playernum].momz;
 		syncleeway = FRACUNIT;
 	}
 	if (ziptic & GZT_ANGLE)
@@ -4942,7 +5506,7 @@ void G_ConsGhostTic(void)
 	if (ziptic & GZT_SPRITE)
 		demo_p++;
 	if(ziptic & GZT_NIGHTS) {
-		if (!testmo->player || !(testmo->player->pflags & PF_NIGHTSMODE) || !testmo->tracer)
+		if (!testmo || !testmo->player || !(testmo->player->pflags & PF_NIGHTSMODE) || !testmo->tracer)
 			nightsfail = true;
 		else
 			testmo = testmo->tracer;
@@ -4998,27 +5562,68 @@ void G_ConsGhostTic(void)
 		}
 		if (ziptic & EZT_SPRITE)
 			demo_p++;
+		if (ziptic & EZT_KART)
+		{
+			ghostext[playernum].kartitem = READINT32(demo_p);
+			ghostext[playernum].kartamount = READINT32(demo_p);
+			ghostext[playernum].kartbumpers = READINT32(demo_p);
+		}
 	}
 
-	// Re-synchronise
-	px = testmo->x;
-	py = testmo->y;
-	pz = testmo->z;
-	gx = oldghost.x;
-	gy = oldghost.y;
-	gz = oldghost.z;
-
-	if (nightsfail || abs(px-gx) > syncleeway || abs(py-gy) > syncleeway || abs(pz-gz) > syncleeway)
+	if (testmo)
 	{
-		if (demosynced)
-			CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
-		demosynced = false;
+		// Re-synchronise
+		px = testmo->x;
+		py = testmo->y;
+		pz = testmo->z;
+		gx = oldghost[playernum].x;
+		gy = oldghost[playernum].y;
+		gz = oldghost[playernum].z;
+
+		if (nightsfail || abs(px-gx) > syncleeway || abs(py-gy) > syncleeway || abs(pz-gz) > syncleeway)
+		{
+			ghostext[playernum].desyncframes++;
+
+			if (ghostext[playernum].desyncframes >= 2)
+			{
+				if (demosynced)
+					CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
+				demosynced = false;
+
+				P_UnsetThingPosition(testmo);
+				testmo->x = oldghost[playernum].x;
+				testmo->y = oldghost[playernum].y;
+				P_SetThingPosition(testmo);
+				testmo->z = oldghost[playernum].z;
+
+				if (abs(testmo->z - testmo->floorz) < 4*FRACUNIT)
+					testmo->z = testmo->floorz; // Sync players to the ground when they're likely supposed to be there...
+
+				ghostext[playernum].desyncframes = 2;
+			}
+		}
+		else
+			ghostext[playernum].desyncframes = 0;
+
+		if (
+#ifdef DEMO_COMPAT_100
+			demo.version != 0x0001 &&
+#endif
+			(
+				players[playernum].kartstuff[k_itemtype] != ghostext[playernum].kartitem ||
+				players[playernum].kartstuff[k_itemamount] != ghostext[playernum].kartamount ||
+				players[playernum].kartstuff[k_bumper] != ghostext[playernum].kartbumpers
+			)
+		)
+		{
+			if (demosynced)
+				CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n"));
+			demosynced = false;
 
-		P_UnsetThingPosition(testmo);
-		testmo->x = oldghost.x;
-		testmo->y = oldghost.y;
-		P_SetThingPosition(testmo);
-		testmo->z = oldghost.z;
+			players[playernum].kartstuff[k_itemtype] = ghostext[playernum].kartitem;
+			players[playernum].kartstuff[k_itemamount] = ghostext[playernum].kartamount;
+			players[playernum].kartstuff[k_bumper] = ghostext[playernum].kartbumpers;
+		}
 	}
 
 	if (*demo_p == DEMOMARKER)
@@ -5036,6 +5641,38 @@ void G_GhostTicker(void)
 	{
 		// Skip normal demo data.
 		UINT8 ziptic = READUINT8(g->p);
+
+#ifdef DEMO_COMPAT_100
+		if (g->version != 0x0001)
+		{
+#endif
+		while (ziptic != DW_END) // Get rid of extradata stuff
+		{
+			if (ziptic == 0) // Only support player 0 info for now
+			{
+				ziptic = READUINT8(g->p);
+				if (ziptic & DXD_SKIN)
+					g->p += 18; // We _could_ read this info, but it shouldn't change anything in record attack...
+				if (ziptic & DXD_COLOR)
+					g->p += 16; // Same tbh
+				if (ziptic & DXD_NAME)
+					g->p += 16; // yea
+				if (ziptic & DXD_PLAYSTATE && READUINT8(g->p) != DXD_PST_PLAYING)
+					I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this
+			}
+			else if (ziptic == DW_RNG)
+				g->p += 4; // RNG seed
+			else
+				I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this
+
+			ziptic = READUINT8(g->p);
+		}
+
+		ziptic = READUINT8(g->p); // Back to actual ziptic stuff
+#ifdef DEMO_COMPAT_100
+		}
+#endif
+
 		if (ziptic & ZT_FWD)
 			g->p++;
 		if (ziptic & ZT_SIDE)
@@ -5048,9 +5685,24 @@ void G_GhostTicker(void)
 			g->p += 2;
 		if (ziptic & ZT_DRIFT)
 			g->p += 2;
+		if (ziptic & ZT_LATENCY)
+			g->p += 1;
 
 		// Grab ghost data.
 		ziptic = READUINT8(g->p);
+
+#ifdef DEMO_COMPAT_100
+		if (g->version != 0x0001)
+		{
+#endif
+		if (ziptic == 0xFF)
+			goto skippedghosttic; // Didn't write ghost info this frame
+		else if (ziptic != 0)
+			I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this
+		ziptic = READUINT8(g->p);
+#ifdef DEMO_COMPAT_100
+		}
+#endif
 		if (ziptic & GZT_XYZ)
 		{
 			g->oldmo.x = READFIXED(g->p);
@@ -5187,8 +5839,21 @@ void G_GhostTicker(void)
 			}
 			if (ziptic & EZT_SPRITE)
 				g->mo->sprite = READUINT8(g->p);
+			if (ziptic & EZT_KART)
+				g->p += 12; // kartitem, kartamount, kartbumpers
+		}
+
+#ifdef DEMO_COMPAT_100
+		if (g->version != 0x0001)
+		{
+#endif
+		if (READUINT8(g->p) != 0xFF) // Make sure there isn't other ghost data here.
+			I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this
+#ifdef DEMO_COMPAT_100
 		}
+#endif
 
+skippedghosttic:
 		// Tick ghost colors (Super and Mario Invincibility flashing)
 		switch(g->color)
 		{
@@ -5218,53 +5883,228 @@ void G_GhostTicker(void)
 	}
 }
 
-void G_ReadMetalTic(mobj_t *metal)
-{
-	UINT8 ziptic;
-	UINT16 speed;
-	UINT8 statetype;
+// Demo rewinding functions
+typedef struct rewindinfo_s {
+	tic_t leveltime;
 
-	if (!metal_p)
-		return;
-	ziptic = READUINT8(metal_p);
+	struct {
+		boolean ingame;
+		player_t player;
+		mobj_t mobj;
+	} playerinfo[MAXPLAYERS];
 
-	// Read changes from the tic
-	if (ziptic & GZT_XYZ)
+	struct rewindinfo_s *prev;
+} rewindinfo_t;
+
+static tic_t currentrewindnum;
+static rewindinfo_t *rewindhead = NULL; // Reverse chronological order
+
+void G_InitDemoRewind(void)
+{
+	while (rewindhead)
 	{
-		P_TeleportMove(metal, READFIXED(metal_p), READFIXED(metal_p), READFIXED(metal_p));
-		oldmetal.x = metal->x;
-		oldmetal.y = metal->y;
-		oldmetal.z = metal->z;
+		rewindinfo_t *p = rewindhead->prev;
+		Z_Free(rewindhead);
+		rewindhead = p;
 	}
-	else
+
+	currentrewindnum = 0;
+}
+
+void G_StoreRewindInfo(void)
+{
+	static UINT8 timetolog = 8;
+	rewindinfo_t *info;
+	size_t i;
+
+	if (timetolog-- > 0)
+		return;
+	timetolog = 8;
+
+	info = Z_Calloc(sizeof(rewindinfo_t), PU_STATIC, NULL);
+
+	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		if (ziptic & GZT_MOMXY)
+		if (!playeringame[i] || players[i].spectator)
 		{
-			oldmetal.momx = READINT16(metal_p)<<8;
-			oldmetal.momy = READINT16(metal_p)<<8;
+			info->playerinfo[i].ingame = false;
+			continue;
 		}
-		if (ziptic & GZT_MOMZ)
-			oldmetal.momz = READINT16(metal_p)<<8;
-		oldmetal.x += oldmetal.momx;
-		oldmetal.y += oldmetal.momy;
-		oldmetal.z += oldmetal.momz;
+
+		info->playerinfo[i].ingame = true;
+		memcpy(&info->playerinfo[i].player, &players[i], sizeof(player_t));
+		if (players[i].mo)
+			memcpy(&info->playerinfo[i].mobj, players[i].mo, sizeof(mobj_t));
 	}
-	if (ziptic & GZT_ANGLE)
-		oldmetal.angle = READUINT8(metal_p)<<24;
-	if (ziptic & GZT_SPRITE)
-		metal_p++; // Currently unused. (Metal Sonic figures out what he's doing his own damn self.)
 
-	// Set movement, position, and angle
-	// oldmetal contains where you're supposed to be.
-	metal->momx = oldmetal.momx;
-	metal->momy = oldmetal.momy;
-	metal->momz = oldmetal.momz;
-	P_UnsetThingPosition(metal);
-	metal->x = oldmetal.x;
-	metal->y = oldmetal.y;
-	metal->z = oldmetal.z;
-	P_SetThingPosition(metal);
-	metal->angle = oldmetal.angle;
+	info->leveltime = leveltime;
+	info->prev = rewindhead;
+	rewindhead = info;
+}
+
+void G_PreviewRewind(tic_t previewtime)
+{
+	SINT8 i;
+	size_t j;
+	fixed_t tweenvalue = 0;
+	rewindinfo_t *info = rewindhead, *next_info = rewindhead;
+
+	if (!info)
+		return;
+
+	while (info->leveltime > previewtime && info->prev)
+	{
+		next_info = info;
+		info = info->prev;
+	}
+	if (info != next_info)
+		tweenvalue = FixedDiv(previewtime - info->leveltime, next_info->leveltime - info->leveltime);
+
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i] || players[i].spectator)
+		{
+			if (info->playerinfo[i].player.mo)
+			{
+				//@TODO spawn temp object to act as a player display
+			}
+
+			continue;
+		}
+
+		if (!info->playerinfo[i].ingame || !info->playerinfo[i].player.mo)
+		{
+			if (players[i].mo)
+				players[i].mo->flags2 |= MF2_DONTDRAW;
+
+			continue;
+		}
+
+		if (!players[i].mo)
+			continue; //@TODO spawn temp object to act as a player display
+
+		players[i].mo->flags2 &= ~MF2_DONTDRAW;
+
+		P_UnsetThingPosition(players[i].mo);
+#define TWEEN(pr) info->playerinfo[i].mobj.pr + FixedMul((INT32) (next_info->playerinfo[i].mobj.pr - info->playerinfo[i].mobj.pr), tweenvalue)
+		players[i].mo->x = TWEEN(x);
+		players[i].mo->y = TWEEN(y);
+		players[i].mo->z = TWEEN(z);
+		players[i].mo->angle = TWEEN(angle);
+#undef TWEEN
+		P_SetThingPosition(players[i].mo);
+
+		players[i].frameangle = info->playerinfo[i].player.frameangle + FixedMul((INT32) (next_info->playerinfo[i].player.frameangle - info->playerinfo[i].player.frameangle), tweenvalue);
+
+		players[i].mo->sprite = info->playerinfo[i].mobj.sprite;
+		players[i].mo->frame = info->playerinfo[i].mobj.frame;
+
+		players[i].realtime = info->playerinfo[i].player.realtime;
+		for (j = 0; j < NUMKARTSTUFF; j++)
+			players[i].kartstuff[j] = info->playerinfo[i].player.kartstuff[j];
+	}
+
+	for (i = splitscreen; i >= 0; i--)
+		P_ResetCamera(&players[displayplayers[i]], &camera[i]);
+}
+
+void G_ConfirmRewind(tic_t rewindtime)
+{
+	SINT8 i;
+	tic_t j;
+	boolean oldmenuactive = menuactive, oldsounddisabled = sound_disabled;
+
+	INT32 olddp1 = displayplayers[0], olddp2 = displayplayers[1], olddp3 = displayplayers[2], olddp4 = displayplayers[3];
+	UINT8 oldss = splitscreen;
+
+	menuactive = false; // Prevent loops
+
+	CV_StealthSetValue(&cv_renderview, 0);
+
+	if (rewindtime > starttime)
+	{
+		sound_disabled = true; // Prevent sound spam
+		demo.rewinding = true;
+	}
+	else
+		demo.rewinding = false;
+
+	G_DoPlayDemo(NULL); // Restart the current demo
+
+	for (j = 0; j < rewindtime && leveltime < rewindtime; j++)
+	{
+		//TryRunTics(1);
+		G_Ticker((j % NEWTICRATERATIO) == 0);
+	}
+
+	demo.rewinding = false;
+	menuactive = oldmenuactive; // Bring the menu back up
+	sound_disabled = oldsounddisabled; // Re-enable SFX
+
+	wipegamestate = gamestate; // No fading back in!
+
+	COM_BufInsertText("renderview on\n");
+
+	splitscreen = oldss;
+	displayplayers[0] = olddp1;
+	displayplayers[1] = olddp2;
+	displayplayers[2] = olddp3;
+	displayplayers[3] = olddp4;
+	R_ExecuteSetViewSize();
+	G_ResetViews();
+
+	for (i = splitscreen; i >= 0; i--)
+		P_ResetCamera(&players[displayplayers[i]], &camera[i]);
+}
+
+void G_ReadMetalTic(mobj_t *metal)
+{
+	UINT8 ziptic;
+	UINT16 speed;
+	UINT8 statetype;
+
+	if (!metal_p)
+		return;
+	ziptic = READUINT8(metal_p);
+
+	// Read changes from the tic
+	if (ziptic & GZT_XYZ)
+	{
+		P_TeleportMove(metal, READFIXED(metal_p), READFIXED(metal_p), READFIXED(metal_p));
+		oldmetal.x = metal->x;
+		oldmetal.y = metal->y;
+		oldmetal.z = metal->z;
+	}
+	else
+	{
+		if (ziptic & GZT_MOMXY)
+		{
+			oldmetal.momx = READINT16(metal_p)<<8;
+			oldmetal.momy = READINT16(metal_p)<<8;
+		}
+		if (ziptic & GZT_MOMZ)
+			oldmetal.momz = READINT16(metal_p)<<8;
+		oldmetal.x += oldmetal.momx;
+		oldmetal.y += oldmetal.momy;
+		oldmetal.z += oldmetal.momz;
+	}
+	if (ziptic & GZT_ANGLE)
+		oldmetal.angle = READUINT8(metal_p)<<24;
+	if (ziptic & GZT_SPRITE)
+		metal_p++; // Currently unused. (Metal Sonic figures out what he's doing his own damn self.)
+
+	// Set movement, position, and angle
+	// oldmetal contains where you're supposed to be.
+	metal->momx = oldmetal.momx;
+	metal->momy = oldmetal.momy;
+	metal->momz = oldmetal.momz;
+	P_UnsetThingPosition(metal);
+	metal->x = oldmetal.x;
+	metal->y = oldmetal.y;
+	metal->z = oldmetal.z;
+	P_SetThingPosition(metal);
+	metal->angle = oldmetal.angle;
 
 	if (ziptic & GZT_EXTRA)
 	{ // But wait, there's more!
@@ -5283,7 +6123,7 @@ void G_ReadMetalTic(mobj_t *metal)
 	speed = FixedDiv(P_AproxDistance(oldmetal.momx, oldmetal.momy), metal->scale)>>FRACBITS;
 
 	// Use speed to decide an appropriate state
-	if (speed > 28) // default skin runspeed
+	if (speed > 20) // default skin runspeed
 		statetype = 2;
 	else if (speed > 1) // stopspeed
 		statetype = 1;
@@ -5432,9 +6272,12 @@ void G_RecordDemo(const char *name)
 {
 	INT32 maxsize;
 
+	CONS_Printf("Recording demo %s.lmp\n", name);
+
 	strcpy(demoname, name);
 	strcat(demoname, ".lmp");
-	maxsize = 1024*1024;
+	//@TODO make a maxdemosize cvar
+	maxsize = 1024*1024*2;
 	if (M_CheckParm("-maxdemo") && M_IsNextParm())
 		maxsize = atoi(M_GetNextParm()) * 1024;
 //	if (demobuffer)
@@ -5443,7 +6286,7 @@ void G_RecordDemo(const char *name)
 	demobuffer = malloc(maxsize);
 	demoend = demobuffer + maxsize;
 
-	demorecording = true;
+	demo.recording = true;
 }
 
 void G_RecordMetal(void)
@@ -5460,16 +6303,23 @@ void G_RecordMetal(void)
 
 void G_BeginRecording(void)
 {
-	UINT8 i;
+	UINT8 i, p;
 	char name[16];
 	player_t *player = &players[consoleplayer];
 
+	char *filename;
+	UINT8 totalfiles;
+	UINT8 *m;
+
 	if (demo_p)
 		return;
 	memset(name,0,sizeof(name));
 
 	demo_p = demobuffer;
-	demoflags = DF_GHOST|(modeattacking<<DF_ATTACKSHIFT);
+	demoflags = DF_GHOST|(multiplayer ? DF_MULTIPLAYER : (modeattacking<<DF_ATTACKSHIFT));
+
+	if (encoremode)
+		demoflags |= DF_ENCORE;
 
 	// Setup header.
 	M_Memcpy(demo_p, DEMOHEADER, 12); demo_p += 12;
@@ -5477,6 +6327,10 @@ void G_BeginRecording(void)
 	WRITEUINT8(demo_p,SUBVERSION);
 	WRITEUINT16(demo_p,DEMOVERSION);
 
+	// Full replay title
+	demo_p += 64;
+	snprintf(demo.titlename, 64, "%s - %s", G_BuildMapTitle(gamemap), modeattacking ? "Record Attack" : connectedservername);
+
 	// demo checksum
 	demo_p += 16;
 
@@ -5485,7 +6339,26 @@ void G_BeginRecording(void)
 	WRITEINT16(demo_p,gamemap);
 	M_Memcpy(demo_p, mapmd5, 16); demo_p += 16;
 
-	WRITEUINT8(demo_p,demoflags);
+	WRITEUINT8(demo_p, demoflags);
+	WRITEUINT8(demo_p, gametype & 0xFF);
+
+	// file list
+	m = demo_p;/* file count */
+	demo_p += 1;
+
+	totalfiles = 0;
+	for (i = mainwads; ++i < numwadfiles; )
+		if (wadfiles[i]->important)
+	{
+		nameonly(( filename = va("%s", wadfiles[i]->filename) ));
+		WRITESTRINGN(demo_p, filename, 64);
+		WRITEMEM(demo_p, wadfiles[i]->md5sum, 16);
+
+		totalfiles++;
+	}
+
+	WRITEUINT8(m, totalfiles);
+
 	switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
 	{
 	case ATTACKING_NONE: // 0
@@ -5506,69 +6379,69 @@ void G_BeginRecording(void)
 
 	WRITEUINT32(demo_p,P_GetInitSeed());
 
-	// Name
-	for (i = 0; i < 16 && cv_playername.string[i]; i++)
-		name[i] = cv_playername.string[i];
-	for (; i < 16; i++)
-		name[i] = '\0';
-	M_Memcpy(demo_p,name,16);
-	demo_p += 16;
+	// Reserved for extrainfo location from start of file
+	demoinfo_p = demo_p;
+	WRITEUINT32(demo_p, 0);
 
-	// Skin
-	for (i = 0; i < 16 && cv_skin.string[i]; i++)
-		name[i] = cv_skin.string[i];
-	for (; i < 16; i++)
-		name[i] = '\0';
-	M_Memcpy(demo_p,name,16);
-	demo_p += 16;
+	// Save netvars
+	CV_SaveNetVars(&demo_p, true);
 
-	// Color
-	for (i = 0; i < 16 && cv_playercolor.string[i]; i++)
-		name[i] = cv_playercolor.string[i];
-	for (; i < 16; i++)
-		name[i] = '\0';
-	M_Memcpy(demo_p,name,16);
-	demo_p += 16;
+	// Now store some info for each in-game player
+	for (p = 0; p < MAXPLAYERS; p++) {
+		if (playeringame[p]) {
+			player = &players[p];
 
-	// Stats
-	WRITEUINT8(demo_p,player->charability);
-	WRITEUINT8(demo_p,player->charability2);
-	WRITEUINT8(demo_p,player->actionspd>>FRACBITS);
-	WRITEUINT8(demo_p,player->mindash>>FRACBITS);
-	WRITEUINT8(demo_p,player->maxdash>>FRACBITS);
-	// SRB2kart
-	WRITEUINT8(demo_p,player->kartspeed);
-	WRITEUINT8(demo_p,player->kartweight);
-	//
-	WRITEUINT8(demo_p,player->normalspeed>>FRACBITS);
-	WRITEUINT8(demo_p,player->runspeed>>FRACBITS);
-	WRITEUINT8(demo_p,player->thrustfactor);
-	WRITEUINT8(demo_p,player->accelstart);
-	WRITEUINT8(demo_p,player->acceleration);
+			WRITEUINT8(demo_p, p | (player->spectator ? DEMO_SPECTATOR : 0));
+
+			// Name
+			memset(name, 0, 16);
+			strncpy(name, player_names[p], 16);
+			M_Memcpy(demo_p,name,16);
+			demo_p += 16;
+
+			// Skin
+			memset(name, 0, 16);
+			strncpy(name, skins[player->skin].name, 16);
+			M_Memcpy(demo_p,name,16);
+			demo_p += 16;
+
+			// Color
+			memset(name, 0, 16);
+			strncpy(name, KartColor_Names[player->skincolor], 16);
+			M_Memcpy(demo_p,name,16);
+			demo_p += 16;
+
+			// Score, since Kart uses this to determine where you start on the map
+			WRITEUINT32(demo_p, player->score);
 
-	// Trying to convert it back to % causes demo desync due to precision loss.
-	// Don't do it.
-	WRITEFIXED(demo_p, player->jumpfactor);
+			// Kart speed and weight
+			WRITEUINT8(demo_p, skins[player->skin].kartspeed);
+			WRITEUINT8(demo_p, skins[player->skin].kartweight);
+		}
+	}
 
-	// Save netvar data (SONICCD, etc)
-	CV_SaveNetVars(&demo_p);
+	WRITEUINT8(demo_p, 0xFF); // Denote the end of the player listing
 
 	memset(&oldcmd,0,sizeof(oldcmd));
 	memset(&oldghost,0,sizeof(oldghost));
 	memset(&ghostext,0,sizeof(ghostext));
-	ghostext.lastcolor = ghostext.color = GHC_NORMAL;
-	ghostext.lastscale = ghostext.scale = FRACUNIT;
 
-	if (player->mo)
+	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		oldghost.x = player->mo->x;
-		oldghost.y = player->mo->y;
-		oldghost.z = player->mo->z;
-		oldghost.angle = player->mo->angle;
+		ghostext[i].lastcolor = ghostext[i].color = GHC_NORMAL;
+		ghostext[i].lastscale = ghostext[i].scale = FRACUNIT;
 
-		// preticker started us gravity flipped
-		if (player->mo->eflags & MFE_VERTICALFLIP)
-			ghostext.flags |= EZT_FLIP;
+		if (players[i].mo)
+		{
+			oldghost[i].x = players[i].mo->x;
+			oldghost[i].y = players[i].mo->y;
+			oldghost[i].z = players[i].mo->z;
+			oldghost[i].angle = players[i].mo->angle;
+
+			// preticker started us gravity flipped
+			if (players[i].mo->eflags & MFE_VERTICALFLIP)
+				ghostext[i].flags |= EZT_FLIP;
+		}
 	}
 }
 
@@ -5600,9 +6473,44 @@ void G_BeginMetal(void)
 	oldmetal.angle = mo->angle;
 }
 
+void G_WriteStanding(UINT8 ranking, char *name, INT32 skinnum, UINT8 color, UINT32 val)
+{
+	char temp[16];
+
+	if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
+	{
+		WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
+		*(UINT32 *)demoinfo_p = demo_p - demobuffer;
+	}
+
+	WRITEUINT8(demo_p, DW_STANDING);
+	WRITEUINT8(demo_p, ranking);
+
+	// Name
+	memset(temp, 0, 16);
+	strncpy(temp, name, 16);
+	M_Memcpy(demo_p,temp,16);
+	demo_p += 16;
+
+	// Skin
+	memset(temp, 0, 16);
+	strncpy(temp, skins[skinnum].name, 16);
+	M_Memcpy(demo_p,temp,16);
+	demo_p += 16;
+
+	// Color
+	memset(temp, 0, 16);
+	strncpy(temp, KartColor_Names[color], 16);
+	M_Memcpy(demo_p,temp,16);
+	demo_p += 16;
+
+	// Score/time/whatever
+	WRITEUINT32(demo_p, val);
+}
+
 void G_SetDemoTime(UINT32 ptime, UINT32 plap)
 {
-	if (!demorecording || !demotime_p)
+	if (!demo.recording || !demotime_p)
 		return;
 	if (demoflags & DF_RECORDATTACK)
 	{
@@ -5618,6 +6526,165 @@ void G_SetDemoTime(UINT32 ptime, UINT32 plap)
 	}*/
 }
 
+static void G_LoadDemoExtraFiles(UINT8 **pp)
+{
+	UINT8 totalfiles;
+	char filename[MAX_WADPATH];
+	UINT8 md5sum[16];
+	filestatus_t ncs;
+	boolean toomany = false;
+	boolean alreadyloaded;
+	UINT8 i, j;
+
+	totalfiles = READUINT8((*pp));
+	for (i = 0; i < totalfiles; ++i)
+	{
+		if (toomany)
+			SKIPSTRING((*pp));
+		else
+		{
+			strlcpy(filename, (char *)(*pp), sizeof filename);
+			SKIPSTRING((*pp));
+		}
+		READMEM((*pp), md5sum, 16);
+
+		if (!toomany)
+		{
+			alreadyloaded = false;
+
+			for (j = 0; j < numwadfiles; ++j)
+			{
+				if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0)
+				{
+					alreadyloaded = true;
+					break;
+				}
+			}
+
+			if (alreadyloaded)
+				continue;
+
+			if (numwadfiles >= MAX_WADFILES)
+				toomany = true;
+			else
+				ncs = findfile(filename, md5sum, false);
+
+			if (toomany)
+			{
+				CONS_Alert(CONS_WARNING, M_GetText("Too many files loaded to add anymore for demo playback\n"));
+				if (!CON_Ready())
+					M_StartMessage(M_GetText("There are too many files loaded to add this demo's add-ons.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
+			}
+			else if (ncs != FS_FOUND)
+			{
+				if (ncs == FS_NOTFOUND)
+					CONS_Alert(CONS_NOTICE, M_GetText("You do not have a copy of %s\n"), filename);
+				else if (ncs == FS_MD5SUMBAD)
+					CONS_Alert(CONS_NOTICE, M_GetText("Checksum mismatch on %s\n"), filename);
+				else
+					CONS_Alert(CONS_NOTICE, M_GetText("Unknown error finding file %s\n"), filename);
+
+				if (!CON_Ready())
+					M_StartMessage(M_GetText("There were errors trying to add this demo's add-ons. Check the console for more information.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
+			}
+			else
+			{
+				P_AddWadFile(filename);
+			}
+		}
+	}
+}
+
+static void G_SkipDemoExtraFiles(UINT8 **pp)
+{
+	UINT8 totalfiles;
+	UINT8 i;
+
+	totalfiles = READUINT8((*pp));
+	for (i = 0; i < totalfiles; ++i)
+	{
+		SKIPSTRING((*pp));// file name
+		(*pp) += 16;// md5
+	}
+}
+
+// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's.
+// Enabling quick prevents filesystem checks to see if needed files are available to load.
+static UINT8 G_CheckDemoExtraFiles(UINT8 **pp, boolean quick)
+{
+	UINT8 totalfiles, filesloaded, nmusfilecount;
+	char filename[MAX_WADPATH];
+	UINT8 md5sum[16];
+	boolean toomany = false;
+	boolean alreadyloaded;
+	UINT8 i, j;
+	UINT8 error = 0;
+
+	totalfiles = READUINT8((*pp));
+	filesloaded = 0;
+	for (i = 0; i < totalfiles; ++i)
+	{
+		if (toomany)
+			SKIPSTRING((*pp));
+		else
+		{
+			strlcpy(filename, (char *)(*pp), sizeof filename);
+			SKIPSTRING((*pp));
+		}
+		READMEM((*pp), md5sum, 16);
+
+		if (!toomany)
+		{
+			alreadyloaded = false;
+			nmusfilecount = 0;
+
+			for (j = 0; j < numwadfiles; ++j)
+			{
+				if (wadfiles[j]->important && j > mainwads)
+					nmusfilecount++;
+				else
+					continue;
+
+				if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0)
+				{
+					alreadyloaded = true;
+
+					if (i != nmusfilecount-1 && error < DFILE_ERROR_OUTOFORDER)
+						error |= DFILE_ERROR_OUTOFORDER;
+
+					break;
+				}
+			}
+
+			if (alreadyloaded)
+			{
+				filesloaded++;
+				continue;
+			}
+
+			if (numwadfiles >= MAX_WADFILES)
+				error = DFILE_ERROR_CANNOTLOAD;
+			else if (!quick && findfile(filename, md5sum, false) != FS_FOUND)
+				error = DFILE_ERROR_CANNOTLOAD;
+			else if (error < DFILE_ERROR_INCOMPLETEOUTOFORDER)
+				error |= DFILE_ERROR_NOTLOADED;
+		} else
+			error = DFILE_ERROR_CANNOTLOAD;
+	}
+
+	// Get final file count
+	nmusfilecount = 0;
+
+	for (j = 0; j < numwadfiles; ++j)
+		if (wadfiles[j]->important && j > mainwads)
+			nmusfilecount++;
+
+	if (!error && filesloaded < nmusfilecount)
+		error = DFILE_ERROR_EXTRAFILES;
+
+	return error;
+}
+
 // Returns bitfield:
 // 1 == new demo has lower time
 // 2 == new demo has higher score
@@ -5648,12 +6715,15 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	I_Assert(c == SUBVERSION);
 	s = READUINT16(p);
 	I_Assert(s == DEMOVERSION);
+	p += 64; // full demo title
 	p += 16; // demo checksum
 	I_Assert(!memcmp(p, "PLAY", 4));
 	p += 4; // PLAY
 	p += 2; // gamemap
 	p += 16; // map md5
 	flags = READUINT8(p); // demoflags
+	p++; // gametype
+	G_SkipDemoExtraFiles(&p);
 
 	aflags = flags & (DF_RECORDATTACK|DF_NIGHTSATTACK);
 	I_Assert(aflags);
@@ -5694,7 +6764,15 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	switch(oldversion) // demoversion
 	{
 	case DEMOVERSION: // latest always supported
+		p += 64; // full demo title
 		break;
+#ifdef DEMO_COMPAT_100
+	case 0x0001:
+		// Old replays gotta go :]
+		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' outdated version. It will be overwritten. Nyeheheh.\n"), oldname);
+		Z_Free(buffer);
+		return UINT8_MAX;
+#endif
 	// too old, cannot support.
 	default:
 		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
@@ -5711,6 +6789,8 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	p += 2; // gamemap
 	p += 16; // mapmd5
 	flags = READUINT8(p);
+	p++; // gametype
+	G_SkipDemoExtraFiles(&p);
 	if (!(flags & aflags))
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' not from same game mode. It will be overwritten.\n"), oldname);
@@ -5742,6 +6822,188 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	return c;
 }
 
+void G_LoadDemoInfo(menudemo_t *pdemo)
+{
+	UINT8 *infobuffer, *info_p, *extrainfo_p;
+	UINT8 version, subversion, pdemoflags;
+	UINT16 pdemoversion, count;
+
+	if (!FIL_ReadFile(pdemo->filepath, &infobuffer))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), pdemo->filepath);
+		pdemo->type = MD_INVALID;
+		sprintf(pdemo->title, "INVALID REPLAY");
+
+		return;
+	}
+
+	info_p = infobuffer;
+
+	if (memcmp(info_p, DEMOHEADER, 12))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemo->filepath);
+		pdemo->type = MD_INVALID;
+		sprintf(pdemo->title, "INVALID REPLAY");
+		Z_Free(infobuffer);
+		return;
+	}
+
+	pdemo->type = MD_LOADED;
+
+	info_p += 12; // DEMOHEADER
+
+	version = READUINT8(info_p);
+	subversion = READUINT8(info_p);
+	pdemoversion = READUINT16(info_p);
+
+	switch(pdemoversion)
+	{
+	case DEMOVERSION: // latest always supported
+		// demo title
+		M_Memcpy(pdemo->title, info_p, 64);
+		info_p += 64;
+
+		break;
+#ifdef DEMO_COMPAT_100
+	case 0x0001:
+		pdemo->type = MD_OUTDATED;
+		sprintf(pdemo->title, "Legacy Replay");
+		break;
+#endif
+	// too old, cannot support.
+	default:
+		CONS_Alert(CONS_ERROR, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemo->filepath);
+		pdemo->type = MD_INVALID;
+		sprintf(pdemo->title, "INVALID REPLAY");
+		Z_Free(infobuffer);
+		return;
+	}
+
+	if (version != VERSION || subversion != SUBVERSION)
+		pdemo->type = MD_OUTDATED;
+
+	info_p += 16; // demo checksum
+	if (memcmp(info_p, "PLAY", 4))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemo->filepath);
+		pdemo->type = MD_INVALID;
+		sprintf(pdemo->title, "INVALID REPLAY");
+		Z_Free(infobuffer);
+		return;
+	}
+	info_p += 4; // "PLAY"
+	pdemo->map = READINT16(info_p);
+	info_p += 16; // mapmd5
+
+	pdemoflags = READUINT8(info_p);
+
+	// temp?
+	if (!(pdemoflags & DF_MULTIPLAYER))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("%s is not a multiplayer replay and can't be listed on this menu fully yet.\n"), pdemo->filepath);
+		Z_Free(infobuffer);
+		return;
+	}
+#ifdef DEMO_COMPAT_100
+	else if (pdemoversion == 0x0001)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("%s is a legacy multiplayer replay and cannot be played.\n"), pdemo->filepath);
+		pdemo->type = MD_INVALID;
+		sprintf(pdemo->title, "INVALID REPLAY");
+		Z_Free(infobuffer);
+		return;
+	}
+#endif
+
+	pdemo->gametype = READUINT8(info_p);
+
+	pdemo->addonstatus = G_CheckDemoExtraFiles(&info_p, true);
+	info_p += 4; // RNG seed
+
+	extrainfo_p = infobuffer + READUINT32(info_p);
+
+	// Pared down version of CV_LoadNetVars to find the kart speed
+	pdemo->kartspeed = 1; // Default to normal speed
+	count = READUINT16(info_p);
+	while (count--)
+	{
+		UINT16 netid;
+		char *svalue;
+
+		netid = READUINT16(info_p);
+		svalue = (char *)info_p;
+		SKIPSTRING(info_p);
+		info_p++; // stealth
+
+		if (netid == cv_kartspeed.netid)
+		{
+			UINT8 j;
+			for (j = 0; kartspeed_cons_t[j].strvalue; j++)
+				if (!stricmp(kartspeed_cons_t[j].strvalue, svalue))
+					pdemo->kartspeed = kartspeed_cons_t[j].value;
+		}
+		else if (netid == cv_basenumlaps.netid && pdemo->gametype == GT_RACE)
+			pdemo->numlaps = atoi(svalue);
+	}
+
+	if (pdemoflags & DF_ENCORE)
+		pdemo->kartspeed |= DF_ENCORE;
+
+	/*// Temporary info until this is actually present in replays.
+	(void)extrainfo_p;
+	sprintf(pdemo->winnername, "transrights420");
+	pdemo->winnerskin = 1;
+	pdemo->winnercolor = SKINCOLOR_MOONSLAM;
+	pdemo->winnertime = 6666;*/
+
+	// Read standings!
+	count = 0;
+
+	while (READUINT8(extrainfo_p) == DW_STANDING) // Assume standings are always first in the extrainfo
+	{
+		INT32 i;
+		char temp[16];
+
+		pdemo->standings[count].ranking = READUINT8(extrainfo_p);
+
+		// Name
+		M_Memcpy(pdemo->standings[count].name, extrainfo_p, 16);
+		extrainfo_p += 16;
+
+		// Skin
+		M_Memcpy(temp,extrainfo_p,16);
+		extrainfo_p += 16;
+		pdemo->standings[count].skin = UINT8_MAX;
+		for (i = 0; i < numskins; i++)
+			if (stricmp(skins[i].name, temp) == 0)
+			{
+				pdemo->standings[count].skin = i;
+				break;
+			}
+
+		// Color
+		M_Memcpy(temp,extrainfo_p,16);
+		extrainfo_p += 16;
+		for (i = 0; i < MAXSKINCOLORS; i++)
+			if (!stricmp(KartColor_Names[i],temp))				// SRB2kart
+			{
+				pdemo->standings[count].color = i;
+				break;
+			}
+
+		// Score/time/whatever
+		pdemo->standings[count].timeorscore = READUINT32(extrainfo_p);
+
+		count++;
+
+		if (count >= MAXPLAYERS)
+			break; //@TODO still cycle through the rest of these if extra demo data is ever used
+	}
+
+	// I think that's everything we need?
+	Z_Free(infobuffer);
+}
+
 //
 // G_PlayDemo
 //
@@ -5749,7 +7011,7 @@ void G_DeferedPlayDemo(const char *name)
 {
 	COM_BufAddText("playdemo \"");
 	COM_BufAddText(name);
-	COM_BufAddText("\"\n");
+	COM_BufAddText("\" -addfiles\n");
 }
 
 //
@@ -5758,62 +7020,76 @@ void G_DeferedPlayDemo(const char *name)
 #define SKIPERRORS
 void G_DoPlayDemo(char *defdemoname)
 {
-	UINT8 i;
+	UINT8 i, p;
 	lumpnum_t l;
 	char skin[17],color[17],*n,*pdemoname;
-	UINT8 version,subversion,charability,charability2,kartspeed,kartweight,thrustfactor,accelstart,acceleration;
+	UINT8 version,subversion;
 	UINT32 randseed;
-	fixed_t actionspd,mindash,maxdash,normalspeed,runspeed,jumpfactor;
 	char msg[1024];
 #if defined(SKIPERRORS) && !defined(DEVELOP)
 	boolean skiperrors = false;
 #endif
+	boolean spectator;
+	UINT8 slots[MAXPLAYERS], kartspeed[MAXPLAYERS], kartweight[MAXPLAYERS], numslots = 0;
+
+	G_InitDemoRewind();
 
 	skin[16] = '\0';
 	color[16] = '\0';
 
-	n = defdemoname+strlen(defdemoname);
-	while (*n != '/' && *n != '\\' && n != defdemoname)
-		n--;
-	if (n != defdemoname)
-		n++;
-	pdemoname = ZZ_Alloc(strlen(n)+1);
-	strcpy(pdemoname,n);
-
-	// Internal if no extension, external if one exists
-	if (FIL_CheckExtension(defdemoname))
+	// No demo name means we're restarting the current demo
+	if (defdemoname == NULL)
 	{
-		//FIL_DefaultExtension(defdemoname, ".lmp");
-		if (!FIL_ReadFile(defdemoname, &demobuffer))
+		demo_p = demobuffer;
+		pdemoname = ZZ_Alloc(1); // Easier than adding checks for this everywhere it's freed
+	}
+	else
+	{
+		n = defdemoname+strlen(defdemoname);
+		while (*n != '/' && *n != '\\' && n != defdemoname)
+			n--;
+		if (n != defdemoname)
+			n++;
+		pdemoname = ZZ_Alloc(strlen(n)+1);
+		strcpy(pdemoname,n);
+
+		M_SetPlaybackMenuPointer();
+
+		// Internal if no extension, external if one exists
+		if (FIL_CheckExtension(defdemoname))
 		{
-			snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname);
+			//FIL_DefaultExtension(defdemoname, ".lmp");
+			if (!FIL_ReadFile(defdemoname, &demobuffer))
+			{
+				snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname);
+				CONS_Alert(CONS_ERROR, "%s", msg);
+				gameaction = ga_nothing;
+				M_StartMessage(msg, NULL, MM_NOTHING);
+				return;
+			}
+			demo_p = demobuffer;
+		}
+		// load demo resource from WAD
+		else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
+		{
+			snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
 			CONS_Alert(CONS_ERROR, "%s", msg);
 			gameaction = ga_nothing;
 			M_StartMessage(msg, NULL, MM_NOTHING);
 			return;
 		}
-		demo_p = demobuffer;
-	}
-	// load demo resource from WAD
-	else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
-	{
-		snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		gameaction = ga_nothing;
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		return;
-	}
-	else // it's an internal demo
-	{
-		demobuffer = demo_p = W_CacheLumpNum(l, PU_STATIC);
+		else // it's an internal demo
+		{
+			demobuffer = demo_p = W_CacheLumpNum(l, PU_STATIC);
 #if defined(SKIPERRORS) && !defined(DEVELOP)
-		skiperrors = true; // SRB2Kart: Don't print warnings for staff ghosts, since they'll inevitably happen when we make bugfixes/changes...
+			skiperrors = true; // SRB2Kart: Don't print warnings for staff ghosts, since they'll inevitably happen when we make bugfixes/changes...
 #endif
+		}
 	}
 
 	// read demo header
 	gameaction = ga_nothing;
-	demoplayback = true;
+	demo.playback = true;
 	if (memcmp(demo_p, DEMOHEADER, 12))
 	{
 		snprintf(msg, 1024, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemoname);
@@ -5821,19 +7097,27 @@ void G_DoPlayDemo(char *defdemoname)
 		M_StartMessage(msg, NULL, MM_NOTHING);
 		Z_Free(pdemoname);
 		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
+		demo.playback = false;
+		demo.title = false;
 		return;
 	}
 	demo_p += 12; // DEMOHEADER
 
 	version = READUINT8(demo_p);
 	subversion = READUINT8(demo_p);
-	demoversion = READUINT16(demo_p);
-	switch(demoversion)
+	demo.version = READUINT16(demo_p);
+	switch(demo.version)
 	{
 	case DEMOVERSION: // latest always supported
+		// demo title
+		M_Memcpy(demo.titlename, demo_p, 64);
+		demo_p += 64;
+
+		break;
+#ifdef DEMO_COMPAT_100
+	case 0x0001:
 		break;
+#endif
 	// too old, cannot support.
 	default:
 		snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname);
@@ -5841,8 +7125,8 @@ void G_DoPlayDemo(char *defdemoname)
 		M_StartMessage(msg, NULL, MM_NOTHING);
 		Z_Free(pdemoname);
 		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
+		demo.playback = false;
+		demo.title = false;
 		return;
 	}
 	demo_p += 16; // demo checksum
@@ -5853,8 +7137,8 @@ void G_DoPlayDemo(char *defdemoname)
 		M_StartMessage(msg, NULL, MM_NOTHING);
 		Z_Free(pdemoname);
 		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
+		demo.playback = false;
+		demo.title = false;
 		return;
 	}
 	demo_p += 4; // "PLAY"
@@ -5862,7 +7146,87 @@ void G_DoPlayDemo(char *defdemoname)
 	demo_p += 16; // mapmd5
 
 	demoflags = READUINT8(demo_p);
+#ifdef DEMO_COMPAT_100
+	if (demo.version == 0x0001)
+	{
+		if (demoflags & DF_MULTIPLAYER)
+		{
+			snprintf(msg, 1024, M_GetText("%s is an alpha multiplayer replay and cannot be played.\n"), pdemoname);
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			M_StartMessage(msg, NULL, MM_NOTHING);
+			Z_Free(pdemoname);
+			Z_Free(demobuffer);
+			demo.playback = false;
+			demo.title = false;
+			return;
+		}
+	}
+	else
+	{
+#endif
+	gametype = READUINT8(demo_p);
+
+	if (demo.title) // Titledemos should always play and ought to always be compatible with whatever wadlist is running.
+		G_SkipDemoExtraFiles(&demo_p);
+	else if (demo.loadfiles)
+		G_LoadDemoExtraFiles(&demo_p);
+	else if (demo.ignorefiles)
+		G_SkipDemoExtraFiles(&demo_p);
+	else
+	{
+		UINT8 error = G_CheckDemoExtraFiles(&demo_p, false);
+
+		if (error)
+		{
+			switch (error)
+			{
+			case DFILE_ERROR_NOTLOADED:
+				snprintf(msg, 1024,
+					M_GetText("Required files for this demo are not loaded.\n\nUse\n\"playdemo %s -addfiles\"\nto load them and play the demo.\n"),
+				pdemoname);
+				break;
+
+			case DFILE_ERROR_OUTOFORDER:
+				snprintf(msg, 1024,
+					M_GetText("Required files for this demo are loaded out of order.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
+				pdemoname);
+				break;
+
+			case DFILE_ERROR_INCOMPLETEOUTOFORDER:
+				snprintf(msg, 1024,
+					M_GetText("Required files for this demo are not loaded, and some are out of order.\n\nUse\n\"playdemo %s -addfiles\"\nto load needed files and play the demo.\n"),
+				pdemoname);
+				break;
+
+			case DFILE_ERROR_CANNOTLOAD:
+				snprintf(msg, 1024,
+					M_GetText("Required files for this demo cannot be loaded.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
+				pdemoname);
+				break;
+
+			case DFILE_ERROR_EXTRAFILES:
+				snprintf(msg, 1024,
+					M_GetText("You have additional files loaded beyond the demo's file list.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
+				pdemoname);
+				break;
+			}
+
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			if (!CON_Ready()) // In the console they'll just see the notice there! No point pulling them out.
+				M_StartMessage(msg, NULL, MM_NOTHING);
+			Z_Free(pdemoname);
+			Z_Free(demobuffer);
+			demo.playback = false;
+			demo.title = false;
+			return;
+		}
+	}
+#ifdef DEMO_COMPAT_100
+	}
+#endif
+
 	modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
+	multiplayer = !!(demoflags & DF_MULTIPLAYER);
 	CON_ToggleOff();
 
 	hu_demotime = UINT32_MAX;
@@ -5885,36 +7249,111 @@ void G_DoPlayDemo(char *defdemoname)
 		break;
 	}
 
-	// Random seed
-	randseed = READUINT32(demo_p);
+	// Random seed
+	randseed = READUINT32(demo_p);
+#ifdef DEMO_COMPAT_100
+	if (demo.version != 0x0001)
+#endif
+	demo_p += 4; // Extrainfo location
+
+#ifdef DEMO_COMPAT_100
+	if (demo.version == 0x0001)
+	{
+		// Player name
+		M_Memcpy(player_names[0],demo_p,16);
+		demo_p += 16;
+
+		// Skin
+		M_Memcpy(skin,demo_p,16);
+		demo_p += 16;
+
+		// Color
+		M_Memcpy(color,demo_p,16);
+		demo_p += 16;
+
+		demo_p += 5; // Backwards compat - some stats
+		// SRB2kart
+		kartspeed[0] = READUINT8(demo_p);
+		kartweight[0] = READUINT8(demo_p);
+		//
+		demo_p += 9; // Backwards compat - more stats
+
+		// Skin not loaded?
+		if (!SetPlayerSkin(0, skin))
+		{
+			snprintf(msg, 1024, M_GetText("%s features a character that is not currently loaded.\n"), pdemoname);
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			M_StartMessage(msg, NULL, MM_NOTHING);
+			Z_Free(pdemoname);
+			Z_Free(demobuffer);
+			demo.playback = false;
+			demo.title = false;
+			return;
+		}
+
+		// ...*map* not loaded?
+		if (!gamemap || (gamemap > NUMMAPS) || !mapheaderinfo[gamemap-1] || !(mapheaderinfo[gamemap-1]->menuflags & LF2_EXISTSHACK))
+		{
+			snprintf(msg, 1024, M_GetText("%s features a course that is not currently loaded.\n"), pdemoname);
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			M_StartMessage(msg, NULL, MM_NOTHING);
+			Z_Free(pdemoname);
+			Z_Free(demobuffer);
+			demo.playback = false;
+			demo.title = false;
+			return;
+		}
+
+		// Set color
+		for (i = 0; i < MAXSKINCOLORS; i++)
+			if (!stricmp(KartColor_Names[i],color))				// SRB2kart
+			{
+				players[0].skincolor = i;
+				break;
+			}
+
+		// net var data
+		CV_LoadNetVars(&demo_p);
+
+		// Sigh ... it's an empty demo.
+		if (*demo_p == DEMOMARKER)
+		{
+			snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			M_StartMessage(msg, NULL, MM_NOTHING);
+			Z_Free(pdemoname);
+			Z_Free(demobuffer);
+			demo.playback = false;
+			demo.title = false;
+			return;
+		}
+
+		Z_Free(pdemoname);
 
-	// Player name
-	M_Memcpy(player_names[0],demo_p,16);
-	demo_p += 16;
+		memset(&oldcmd,0,sizeof(oldcmd));
+		memset(&oldghost,0,sizeof(oldghost));
+		memset(&ghostext,0,sizeof(ghostext));
 
-	// Skin
-	M_Memcpy(skin,demo_p,16);
-	demo_p += 16;
+		CONS_Alert(CONS_WARNING, M_GetText("Demo version does not match game version. Desyncs may occur.\n"));
 
-	// Color
-	M_Memcpy(color,demo_p,16);
-	demo_p += 16;
+		// console warning messages
+#if defined(SKIPERRORS) && !defined(DEVELOP)
+		demosynced = (!skiperrors);
+#else
+		demosynced = true;
+#endif
 
-	charability = READUINT8(demo_p);
-	charability2 = READUINT8(demo_p);
-	actionspd = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	mindash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	maxdash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	// SRB2kart
-	kartspeed = READUINT8(demo_p);
-	kartweight = READUINT8(demo_p);
-	//
-	normalspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	runspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	thrustfactor = READUINT8(demo_p);
-	accelstart = READUINT8(demo_p);
-	acceleration = READUINT8(demo_p);
-	jumpfactor = READFIXED(demo_p);
+		// didn't start recording right away.
+		demo.deferstart = false;
+
+		consoleplayer = 0;
+		memset(displayplayers, 0, sizeof(displayplayers));
+		memset(playeringame, 0, sizeof(playeringame));
+		playeringame[0] = true;
+
+		goto post_compat;
+	}
+#endif
 
 	// net var data
 	CV_LoadNetVars(&demo_p);
@@ -5927,34 +7366,8 @@ void G_DoPlayDemo(char *defdemoname)
 		M_StartMessage(msg, NULL, MM_NOTHING);
 		Z_Free(pdemoname);
 		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
-		return;
-	}
-
-	// Skin not loaded?
-	if (!SetPlayerSkin(0, skin))
-	{
-		snprintf(msg, 1024, M_GetText("%s features a character that is not currently loaded.\n"), pdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		Z_Free(pdemoname);
-		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
-		return;
-	}
-
-	// ...*map* not loaded?
-	if (!gamemap || (gamemap > NUMMAPS) || !mapheaderinfo[gamemap-1] || !(mapheaderinfo[gamemap-1]->menuflags & LF2_EXISTSHACK))
-	{
-		snprintf(msg, 1024, M_GetText("%s features a course that is not currently loaded.\n"), pdemoname);
-		CONS_Alert(CONS_ERROR, "%s", msg);
-		M_StartMessage(msg, NULL, MM_NOTHING);
-		Z_Free(pdemoname);
-		Z_Free(demobuffer);
-		demoplayback = false;
-		titledemo = false;
+		demo.playback = false;
+		demo.title = false;
 		return;
 	}
 
@@ -5962,6 +7375,7 @@ void G_DoPlayDemo(char *defdemoname)
 
 	memset(&oldcmd,0,sizeof(oldcmd));
 	memset(&oldghost,0,sizeof(oldghost));
+	memset(&ghostext,0,sizeof(ghostext));
 
 #if defined(SKIPERRORS) && !defined(DEVELOP)
 	if ((VERSION != version || SUBVERSION != subversion) && !skiperrors)
@@ -5978,53 +7392,131 @@ void G_DoPlayDemo(char *defdemoname)
 #endif
 
 	// didn't start recording right away.
-	demo_start = false;
+	demo.deferstart = false;
 
 /*#ifdef HAVE_BLUA
 	LUAh_MapChange(gamemap);
 #endif*/
-	displayplayer = consoleplayer = 0;
+	displayplayers[0] = consoleplayer = 0;
 	memset(playeringame,0,sizeof(playeringame));
-	playeringame[0] = true;
-	P_SetRandSeed(randseed);
-	G_InitNew(false, G_BuildMapName(gamemap), true, true); // Doesn't matter whether you reset or not here, given changes to resetplayer.
 
-	// Set color
-	for (i = 0; i < MAXSKINCOLORS; i++)
-		if (!stricmp(KartColor_Names[i],color))				// SRB2kart
+	// Load players that were in-game when the map started
+	p = READUINT8(demo_p);
+
+	for (i = 1; i < MAXSPLITSCREENPLAYERS; i++)
+		displayplayers[i] = INT32_MAX;
+
+	while (p != 0xFF)
+	{
+		spectator = false;
+		if (p & DEMO_SPECTATOR)
 		{
-			players[0].skincolor = i;
-			break;
+			spectator = true;
+			p &= ~DEMO_SPECTATOR;
+
+			if (modeattacking)
+			{
+				snprintf(msg, 1024, M_GetText("%s is a record attack replay with spectators, and is thus invalid.\n"), pdemoname);
+				CONS_Alert(CONS_ERROR, "%s", msg);
+				M_StartMessage(msg, NULL, MM_NOTHING);
+				Z_Free(pdemoname);
+				Z_Free(demobuffer);
+				demo.playback = false;
+				demo.title = false;
+				return;
+			}
+		}
+		slots[numslots] = p; numslots++;
+
+		if (modeattacking && numslots > 1)
+		{
+			snprintf(msg, 1024, M_GetText("%s is a record attack replay with multiple players, and is thus invalid.\n"), pdemoname);
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			M_StartMessage(msg, NULL, MM_NOTHING);
+			Z_Free(pdemoname);
+			Z_Free(demobuffer);
+			demo.playback = false;
+			demo.title = false;
+			return;
 		}
-	//CV_StealthSetValue(&cv_playercolor, players[0].skincolor); -- as far as I can tell this is more trouble than it's worth
-	if (players[0].mo)
+
+		if (!playeringame[displayplayers[0]] || players[displayplayers[0]].spectator)
+			displayplayers[0] = consoleplayer = serverplayer = p;
+
+		playeringame[p] = true;
+		players[p].spectator = spectator;
+
+		// Name
+		M_Memcpy(player_names[p],demo_p,16);
+		demo_p += 16;
+
+		// Skin
+		M_Memcpy(skin,demo_p,16);
+		demo_p += 16;
+		SetPlayerSkin(p, skin);
+
+		// Color
+		M_Memcpy(color,demo_p,16);
+		demo_p += 16;
+		for (i = 0; i < MAXSKINCOLORS; i++)
+			if (!stricmp(KartColor_Names[i],color))				// SRB2kart
+			{
+				players[p].skincolor = i;
+				break;
+			}
+
+		// Score, since Kart uses this to determine where you start on the map
+		players[p].score = READUINT32(demo_p);
+
+		// Kart stats, temporarily
+		kartspeed[p] = READUINT8(demo_p);
+		kartweight[p] = READUINT8(demo_p);
+
+		if (stricmp(skins[players[p].skin].name, skin) != 0)
+			FindClosestSkinForStats(p, kartspeed[p], kartweight[p]);
+
+		// Look for the next player
+		p = READUINT8(demo_p);
+	}
+
+	splitscreen = 0;
+
+	if (demo.title)
 	{
-		players[0].mo->color = players[0].skincolor;
-		oldghost.x = players[0].mo->x;
-		oldghost.y = players[0].mo->y;
-		oldghost.z = players[0].mo->z;
+		splitscreen = M_RandomKey(6)-1;
+		splitscreen = min(min(3, numslots-1), splitscreen); // Bias toward 1p and 4p views
+
+		for (p = 0; p <= splitscreen; p++)
+			G_ResetView(p+1, slots[M_RandomKey(numslots)], false);
 	}
 
-	// Set saved attribute values
-	// No cheat checking here, because even if they ARE wrong...
-	// it would only break the replay if we clipped them.
-	players[0].charability = charability;
-	players[0].charability2 = charability2;
-	players[0].actionspd = actionspd;
-	players[0].mindash = mindash;
-	players[0].maxdash = maxdash;
-	// SRB2kart
-	players[0].kartspeed = kartspeed;
-	players[0].kartweight = kartweight;
-	//
-	players[0].normalspeed = normalspeed;
-	players[0].runspeed = runspeed;
-	players[0].thrustfactor = thrustfactor;
-	players[0].accelstart = accelstart;
-	players[0].acceleration = acceleration;
-	players[0].jumpfactor = jumpfactor;
+	R_ExecuteSetViewSize();
+
+#ifdef DEMO_COMPAT_100
+post_compat:
+#endif
+
+	P_SetRandSeed(randseed);
+	G_InitNew(demoflags & DF_ENCORE, G_BuildMapName(gamemap), true, true); // Doesn't matter whether you reset or not here, given changes to resetplayer.
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (players[i].mo)
+		{
+			players[i].mo->color = players[i].skincolor;
+			oldghost[i].x = players[i].mo->x;
+			oldghost[i].y = players[i].mo->y;
+			oldghost[i].z = players[i].mo->z;
+		}
+
+		// Set saved attribute values
+		// No cheat checking here, because even if they ARE wrong...
+		// it would only break the replay if we clipped them.
+		players[i].kartspeed = kartspeed[i];
+		players[i].kartweight = kartweight[i];
+	}
 
-	demo_start = true;
+	demo.deferstart = true;
 }
 #undef SKIPERRORS
 
@@ -6039,6 +7531,7 @@ void G_AddGhost(char *defdemoname)
 	mapthing_t *mthing;
 	UINT16 count, ghostversion;
 	skin_t *ghskin = &skins[0];
+	UINT8 kartspeed = UINT8_MAX, kartweight = UINT8_MAX;
 
 	name[16] = '\0';
 	skin[16] = '\0';
@@ -6081,14 +7574,22 @@ void G_AddGhost(char *defdemoname)
 		Z_Free(pdemoname);
 		Z_Free(buffer);
 		return;
-	} p += 12; // DEMOHEADER
+	}
+
+	p += 12; // DEMOHEADER
 	p++; // VERSION
 	p++; // SUBVERSION
+
 	ghostversion = READUINT16(p);
 	switch(ghostversion)
 	{
 	case DEMOVERSION: // latest always supported
+		p += 64; // title
+		break;
+#ifdef DEMO_COMPAT_100
+	case 0x0001:
 		break;
+#endif
 	// too old, cannot support.
 	default:
 		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname);
@@ -6096,6 +7597,7 @@ void G_AddGhost(char *defdemoname)
 		Z_Free(buffer);
 		return;
 	}
+
 	M_Memcpy(md5, p, 16); p += 16; // demo checksum
 	for (gh = ghosts; gh; gh = gh->next)
 		if (!memcmp(md5, gh->checksum, 16)) // another ghost in the game already has this checksum?
@@ -6105,16 +7607,21 @@ void G_AddGhost(char *defdemoname)
 			Z_Free(buffer);
 			return;
 		}
+
 	if (memcmp(p, "PLAY", 4))
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo format unacceptable.\n"), pdemoname);
 		Z_Free(pdemoname);
 		Z_Free(buffer);
 		return;
-	} p += 4; // "PLAY"
+	}
+
+	p += 4; // "PLAY"
 	p += 2; // gamemap
 	p += 16; // mapmd5 (possibly check for consistency?)
+
 	flags = READUINT8(p);
+
 	if (!(flags & DF_GHOST))
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: No ghost data in this demo.\n"), pdemoname);
@@ -6122,6 +7629,16 @@ void G_AddGhost(char *defdemoname)
 		Z_Free(buffer);
 		return;
 	}
+
+#ifdef DEMO_COMPAT_100
+	if (ghostversion != 0x0001)
+#endif
+		p++; // gametype
+
+#ifdef DEMO_COMPAT_100
+	if (ghostversion != 0x0001)
+#endif
+		G_SkipDemoExtraFiles(&p); // Don't wanna modify the file list for ghosts.
 	switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
 	{
 	case ATTACKING_NONE: // 0
@@ -6138,34 +7655,41 @@ void G_AddGhost(char *defdemoname)
 
 	p += 4; // random seed
 
-	// Player name (TODO: Display this somehow if it doesn't match cv_playername!)
-	M_Memcpy(name, p,16);
-	p += 16;
-
-	// Skin
-	M_Memcpy(skin, p,16);
-	p += 16;
-
-	// Color
-	M_Memcpy(color, p,16);
-	p += 16;
-
-	// Ghosts do not have a player structure to put this in.
-	p++; // charability
-	p++; // charability2
-	p++; // actionspd
-	p++; // mindash
-	p++; // maxdash
-	// SRB2kart
-	p++; // kartspeed
-	p++; // kartweight
-	//
-	p++; // normalspeed
-	p++; // runspeed
-	p++; // thrustfactor
-	p++; // accelstart
-	p++; // acceleration
-	p += 4; // jumpfactor
+#ifdef DEMO_COMPAT_100
+	if (ghostversion == 0x0001)
+	{
+		// Player name (TODO: Display this somehow if it doesn't match cv_playername!)
+		M_Memcpy(name, p,16);
+		p += 16;
+
+		// Skin
+		M_Memcpy(skin, p,16);
+		p += 16;
+
+		// Color
+		M_Memcpy(color, p,16);
+		p += 16;
+
+		// Ghosts do not have a player structure to put this in.
+		p++; // charability
+		p++; // charability2
+		p++; // actionspd
+		p++; // mindash
+		p++; // maxdash
+		// SRB2kart
+		p++; // kartspeed
+		p++; // kartweight
+		//
+		p++; // normalspeed
+		p++; // runspeed
+		p++; // thrustfactor
+		p++; // accelstart
+		p++; // acceleration
+		p += 4; // jumpfactor
+	}
+	else
+#endif
+	p += 4; // Extra data location reference
 
 	// net var data
 	count = READUINT16(p);
@@ -6184,6 +7708,46 @@ void G_AddGhost(char *defdemoname)
 		return;
 	}
 
+#ifdef DEMO_COMPAT_100
+	if (ghostversion != 0x0001)
+	{
+#endif
+	if (READUINT8(p) != 0)
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot.\n"), pdemoname);
+		Z_Free(pdemoname);
+		Z_Free(buffer);
+		return;
+	}
+
+	// Player name (TODO: Display this somehow if it doesn't match cv_playername!)
+	M_Memcpy(name, p, 16);
+	p += 16;
+
+	// Skin
+	M_Memcpy(skin, p, 16);
+	p += 16;
+
+	// Color
+	M_Memcpy(color, p, 16);
+	p += 16;
+
+	p += 4; // score
+
+	kartspeed = READUINT8(p);
+	kartweight = READUINT8(p);
+
+	if (READUINT8(p) != 0xFF)
+	{
+		CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot.\n"), pdemoname);
+		Z_Free(pdemoname);
+		Z_Free(buffer);
+		return;
+	}
+#ifdef DEMO_COMPAT_100
+	}
+#endif
+
 	for (i = 0; i < numskins; i++)
 		if (!stricmp(skins[i].name,skin))
 		{
@@ -6193,10 +7757,10 @@ void G_AddGhost(char *defdemoname)
 
 	if (i == numskins)
 	{
-		CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid character.\n"), pdemoname);
-		Z_Free(pdemoname);
-		Z_Free(buffer);
-		return;
+		if (kartspeed != UINT8_MAX && kartweight != UINT8_MAX)
+			ghskin = &skins[GetSkinNumClosestToStats(kartspeed, kartweight)];
+
+		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Invalid character. Falling back to %s.\n"), pdemoname, ghskin->name);
 	}
 
 	gh = Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL);
@@ -6273,30 +7837,56 @@ void G_UpdateStaffGhostName(lumpnum_t l)
 	if (memcmp(p, DEMOHEADER, 12))
 	{
 		goto fail;
-	} p += 12; // DEMOHEADER
+	}
+
+	p += 12; // DEMOHEADER
 	p++; // VERSION
 	p++; // SUBVERSION
+
 	ghostversion = READUINT16(p);
 	switch(ghostversion)
 	{
 	case DEMOVERSION: // latest always supported
+		p += 64; // full demo title
 		break;
+
+#ifdef DEMO_COMPAT_100
+	case 0x0001:
+		break;
+#endif
+
 	// too old, cannot support.
 	default:
 		goto fail;
 	}
+
 	p += 16; // demo checksum
+
 	if (memcmp(p, "PLAY", 4))
 	{
 		goto fail;
-	} p += 4; // "PLAY"
+	}
+
+	p += 4; // "PLAY"
 	p += 2; // gamemap
 	p += 16; // mapmd5 (possibly check for consistency?)
+
 	flags = READUINT8(p);
 	if (!(flags & DF_GHOST))
 	{
 		goto fail; // we don't NEED to do it here, but whatever
 	}
+
+#ifdef DEMO_COMPAT_100
+	if (ghostversion != 0x0001)
+#endif
+	p++; // Gametype
+
+#ifdef DEMO_COMPAT_100
+	if (ghostversion != 0x0001)
+#endif
+	G_SkipDemoExtraFiles(&p);
+
 	switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
 	{
 	case ATTACKING_NONE: // 0
@@ -6310,9 +7900,34 @@ void G_UpdateStaffGhostName(lumpnum_t l)
 	default: // 3
 		break;
 	}
+
 	p += 4; // random seed
 
-	// Player name
+
+#ifdef DEMO_COMPAT_100
+	if (ghostversion == 0x0001)
+	{
+		// Player name
+		M_Memcpy(dummystaffname, p,16);
+		dummystaffname[16] = '\0';
+		goto fail; // Not really a failure but whatever
+	}
+#endif
+
+	p += 4; // Extrainfo location marker
+
+	// Ehhhh don't need ghostversion here (?) so I'll reuse the var here
+	ghostversion = READUINT16(p);
+	while (ghostversion--)
+	{
+		p += 2;
+		SKIPSTRING(p);
+		p++; // stealth
+	}
+
+	// Assert first player is in and then read name
+	if (READUINT8(p) != 0)
+		goto fail;
 	M_Memcpy(dummystaffname, p,16);
 	dummystaffname[16] = '\0';
 
@@ -6335,7 +7950,7 @@ void G_TimeDemo(const char *name)
 	restorecv_vidwait = cv_vidwait.value;
 	if (cv_vidwait.value)
 		CV_Set(&cv_vidwait, "0");
-	timingdemo = true;
+	demo.timing = true;
 	singletics = true;
 	framecount = 0;
 	demostarttime = I_GetTime();
@@ -6383,6 +7998,10 @@ void G_DoPlayMetal(void)
 	{
 	case DEMOVERSION: // latest always supported
 		break;
+#ifdef DEMO_COMPAT_100
+	case 0x0001:
+		I_Error("You need to implement demo compat here, doofus! %s:%d", __FILE__, __LINE__);
+#endif
 	// too old, cannot support.
 	default:
 		CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, format version incompatible.\n"));
@@ -6465,13 +8084,16 @@ void G_StopDemo(void)
 {
 	Z_Free(demobuffer);
 	demobuffer = NULL;
-	demoplayback = false;
-	if (titledemo)
+	demo.playback = false;
+	if (demo.title)
 		modeattacking = false;
-	titledemo = false;
-	timingdemo = false;
+	demo.title = false;
+	demo.timing = false;
 	singletics = false;
 
+	CV_SetValue(&cv_playbackspeed, 1);
+	demo.rewinding = false;
+
 	if (gamestate == GS_LEVEL && rendermode != render_none)
 	{
 		V_SetPaletteLump("PLAYPAL"); // Reset the palette
@@ -6490,8 +8112,6 @@ void G_StopDemo(void)
 
 boolean G_CheckDemoStatus(void)
 {
-	boolean saved;
-
 	while (ghosts)
 	{
 		demoghost *next = ghosts->next;
@@ -6502,7 +8122,7 @@ boolean G_CheckDemoStatus(void)
 
 	// DO NOT end metal sonic demos here
 
-	if (timingdemo)
+	if (demo.timing)
 	{
 		INT32 demotime;
 		double f1, f2;
@@ -6510,7 +8130,7 @@ boolean G_CheckDemoStatus(void)
 		if (!demotime)
 			return true;
 		G_StopDemo();
-		timingdemo = false;
+		demo.timing = false;
 		f1 = (double)demotime;
 		f2 = (double)framecount*TICRATE;
 		CONS_Printf(M_GetText("timed %u gametics in %d realtics\n%f seconds, %f avg fps\n"), leveltime,demotime,f1/TICRATE,f2/f1);
@@ -6520,47 +8140,166 @@ boolean G_CheckDemoStatus(void)
 		return true;
 	}
 
-	if (demoplayback)
+	if (demo.playback)
 	{
-		if (singledemo)
+		if (demo.quitafterplaying)
 			I_Quit();
-		G_StopDemo();
 
-		if (modeattacking)
-			M_EndModeAttackRun();
+		if (multiplayer && !demo.title)
+			G_ExitLevel();
 		else
-			D_AdvanceDemo();
+		{
+			G_StopDemo();
+
+			if (modeattacking)
+				M_EndModeAttackRun();
+			else
+				D_AdvanceDemo();
+		}
 
 		return true;
 	}
 
-	if (demorecording)
+	if (demo.recording && (modeattacking || demo.savemode != DSM_NOTSAVING))
 	{
-		UINT8 *p = demobuffer+16; // checksum position
+		G_SaveDemo();
+		return true;
+	}
+	demo.recording = false;
+
+	return false;
+}
+
+void G_SaveDemo(void)
+{
+	UINT8 *p = demobuffer+16; // after version
+	UINT32 length;
 #ifdef NOMD5
-		UINT8 i;
+	UINT8 i;
+#endif
+
+	// Ensure extrainfo pointer is always available, even if no info is present.
+	if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
+	{
 		WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
-		for (i = 0; i < 16; i++, p++)
-			*p = P_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct.
+		*(UINT32 *)demoinfo_p = demo_p - demobuffer;
+	}
+	WRITEUINT8(demo_p, DW_END); // Mark end of demo extra data.
+
+	M_Memcpy(p, demo.titlename, 64); // Write demo title here
+	p += 64;
+
+	if (multiplayer)
+	{
+		// Change the demo's name to be a slug of the title
+		char demo_slug[128];
+		char *writepoint;
+		size_t i, strindex = 0;
+		boolean dash = true;
+
+		for (i = 0; demo.titlename[i] && i < 127; i++)
+		{
+			if ((demo.titlename[i] >= 'a' && demo.titlename[i] <= 'z') ||
+				(demo.titlename[i] >= '0' && demo.titlename[i] <= '9'))
+			{
+				demo_slug[strindex] = demo.titlename[i];
+				strindex++;
+				dash = false;
+			}
+			else if (demo.titlename[i] >= 'A' && demo.titlename[i] <= 'Z')
+			{
+				demo_slug[strindex] = demo.titlename[i] + 'a' - 'A';
+				strindex++;
+				dash = false;
+			}
+			else if (!dash)
+			{
+				demo_slug[strindex] = '-';
+				strindex++;
+				dash = true;
+			}
+		}
+
+		demo_slug[strindex] = 0;
+		if (dash) demo_slug[strindex-1] = 0;
+
+		writepoint = strstr(demoname, "-") + 1;
+		demo_slug[128 - (writepoint - demoname) - 4] = 0;
+		sprintf(writepoint, "%s.lmp", demo_slug);
+	}
+
+	length = *(UINT32 *)demoinfo_p;
+	WRITEUINT32(demoinfo_p, length);
+#ifdef NOMD5
+	for (i = 0; i < 16; i++, p++)
+		*p = M_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct.
 #else
-		WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker
-		md5_buffer((char *)p+16, demo_p - (p+16), p); // make a checksum of everything after the checksum in the file.
+	// Make a checksum of everything after the checksum in the file up to the end of the standard data. Extrainfo is freely modifiable.
+	md5_buffer((char *)p+16, (demobuffer + length) - (p+16), p);
 #endif
-		saved = FIL_WriteFile(va(pandf, srb2home, demoname), demobuffer, demo_p - demobuffer); // finally output the file.
-		free(demobuffer);
-		demorecording = false;
 
-		if (modeattacking != ATTACKING_RECORD)
+
+	if (FIL_WriteFile(va(pandf, srb2home, demoname), demobuffer, demo_p - demobuffer)) // finally output the file.
+		demo.savemode = DSM_SAVED;
+	free(demobuffer);
+	demo.recording = false;
+
+	if (modeattacking != ATTACKING_RECORD)
+	{
+		if (demo.savemode == DSM_SAVED)
+			CONS_Printf(M_GetText("Demo %s recorded\n"), demoname);
+		else
+			CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname);
+	}
+}
+
+boolean G_DemoTitleResponder(event_t *ev)
+{
+	size_t len;
+	INT32 ch;
+
+	if (ev->type != ev_keydown)
+		return false;
+
+	ch = (INT32)ev->data1;
+
+	// Only ESC and non-keyboard keys abort connection
+	if (ch == KEY_ESCAPE)
+	{
+		demo.savemode = (cv_recordmultiplayerdemos.value == 2) ? DSM_WILLAUTOSAVE : DSM_NOTSAVING;
+		return true;
+	}
+
+	if (ch == KEY_ENTER || ch >= KEY_MOUSE1)
+	{
+		demo.savemode = DSM_WILLSAVE;
+		return true;
+	}
+
+	if ((ch >= HU_FONTSTART && ch <= HU_FONTEND && hu_font[ch-HU_FONTSTART])
+	  || ch == ' ') // Allow spaces, of course
+	{
+		len = strlen(demo.titlename);
+		if (len < 64)
 		{
-			if (saved)
-				CONS_Printf(M_GetText("Demo %s recorded\n"), demoname);
-			else
-				CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname);
+			demo.titlename[len+1] = 0;
+			demo.titlename[len] = CON_ShiftChar(ch);
 		}
-		return true;
 	}
+	else if (ch == KEY_BACKSPACE)
+	{
+		if (shiftdown)
+			memset(demo.titlename, 0, sizeof(demo.titlename));
+		else
+		{
+			len = strlen(demo.titlename);
 
-	return false;
+			if (len > 0)
+				demo.titlename[len-1] = 0;
+		}
+	}
+
+	return true;
 }
 
 //
diff --git a/src/g_game.h b/src/g_game.h
index eea149c985db8d4eb9f2d048db4c9c44bcaa8bd5..a69f91421db3c18a67b0d0456f2d926cf5a16e6c 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -36,11 +36,61 @@ extern boolean playeringame[MAXPLAYERS];
 // ======================================
 
 // demoplaying back and demo recording
-extern boolean demoplayback, titledemo, fromtitledemo, demorecording, timingdemo;
+extern consvar_t cv_recordmultiplayerdemos, cv_netdemosyncquality;
+
+// Publicly-accessible demo vars
+struct demovars_s {
+	char titlename[65];
+	boolean recording, playback, timing;
+	UINT16 version; // Current file format of the demo being played
+	boolean title; // Title Screen demo can be cancelled by any key
+	boolean rewinding; // Rewind in progress
+
+	boolean loadfiles, ignorefiles; // Demo file loading options
+	boolean fromtitle; // SRB2Kart: Don't stop the music
+	boolean inreplayhut; // Go back to replayhut after demos
+	boolean quitafterplaying; // quit after playing a demo from cmdline
+	boolean deferstart; // don't start playing demo right away
+
+	tic_t savebutton; // Used to determine when the local player can choose to save the replay while the race is still going
+	enum {
+		DSM_NOTSAVING,
+		DSM_WILLAUTOSAVE,
+		DSM_TITLEENTRY,
+		DSM_WILLSAVE,
+		DSM_SAVED
+	} savemode;
+};
+
+extern struct demovars_s demo;
+
+typedef enum {
+	MD_NOTLOADED,
+	MD_LOADED,
+	MD_SUBDIR,
+	MD_OUTDATED,
+	MD_INVALID
+} menudemotype_e;
+
+typedef struct menudemo_s {
+	char filepath[256];
+	menudemotype_e type;
+
+	char title[65]; // Null-terminated for string prints
+	UINT16 map;
+	UINT8 addonstatus; // What do we need to do addon-wise to play this demo?
+	UINT8 gametype;
+	UINT8 kartspeed; // Add OR DF_ENCORE for encore mode, idk
+	UINT8 numlaps;
+
+	struct {
+		UINT8 ranking;
+		char name[17];
+		UINT8 skin, color;
+		UINT32 timeorscore;
+	} standings[MAXPLAYERS];
+} menudemo_t;
 
-// Quit after playing a demo from cmdline.
-extern boolean singledemo;
-extern boolean demo_start;
 
 extern mobj_t *metalplayback;
 
@@ -62,10 +112,10 @@ extern consvar_t cv_invertmouse/*, cv_alwaysfreelook, cv_chasefreelook, cv_mouse
 extern consvar_t cv_invertmouse2/*, cv_alwaysfreelook2, cv_chasefreelook2, cv_mousemove2*/;
 extern consvar_t cv_useranalog, cv_useranalog2, cv_useranalog3, cv_useranalog4;
 extern consvar_t cv_analog, cv_analog2, cv_analog3, cv_analog4;
-extern consvar_t cv_turnaxis,cv_moveaxis,cv_brakeaxis,cv_aimaxis,cv_lookaxis,cv_fireaxis,cv_driftaxis;
-extern consvar_t cv_turnaxis2,cv_moveaxis2,cv_brakeaxis2,cv_aimaxis2,cv_lookaxis2,cv_fireaxis2,cv_driftaxis2;
-extern consvar_t cv_turnaxis3,cv_moveaxis3,cv_brakeaxis3,cv_aimaxis3,cv_lookaxis3,cv_fireaxis3,cv_driftaxis3;
-extern consvar_t cv_turnaxis4,cv_moveaxis4,cv_brakeaxis4,cv_aimaxis4,cv_lookaxis4,cv_fireaxis4,cv_driftaxis4;
+extern consvar_t cv_turnaxis,cv_moveaxis,cv_brakeaxis,cv_aimaxis,cv_lookaxis,cv_fireaxis,cv_driftaxis,cv_deadzone;
+extern consvar_t cv_turnaxis2,cv_moveaxis2,cv_brakeaxis2,cv_aimaxis2,cv_lookaxis2,cv_fireaxis2,cv_driftaxis2,cv_deadzone2;
+extern consvar_t cv_turnaxis3,cv_moveaxis3,cv_brakeaxis3,cv_aimaxis3,cv_lookaxis3,cv_fireaxis3,cv_driftaxis3,cv_deadzone3;
+extern consvar_t cv_turnaxis4,cv_moveaxis4,cv_brakeaxis4,cv_aimaxis4,cv_lookaxis4,cv_fireaxis4,cv_driftaxis4,cv_deadzone4;
 extern consvar_t cv_ghost_besttime, cv_ghost_bestlap, cv_ghost_last, cv_ghost_guest, cv_ghost_staff;
 
 typedef enum
@@ -102,9 +152,9 @@ INT16 G_SoftwareClipAimingPitch(INT32 *aiming);
 boolean InputDown(INT32 gc, UINT8 p);
 INT32 JoyAxis(axis_input_e axissel, UINT8 p);
 
-extern angle_t localangle, localangle2, localangle3, localangle4;
-extern INT32 localaiming, localaiming2, localaiming3, localaiming4; // should be an angle_t but signed
-extern boolean camspin, camspin2, camspin3, camspin4; // SRB2Kart
+extern angle_t localangle[MAXSPLITSCREENPLAYERS];
+extern INT32 localaiming[MAXSPLITSCREENPLAYERS]; // should be an angle_t but signed
+extern boolean camspin[MAXSPLITSCREENPLAYERS]; // SRB2Kart
 
 //
 // GAME
@@ -128,6 +178,7 @@ void G_DeferedInitNew(boolean pencoremode, const char *mapname, INT32 pickedchar
 	UINT8 ssplayers, boolean FLS);
 void G_DoLoadLevel(boolean resetplayer);
 
+void G_LoadDemoInfo(menudemo_t *pdemo);
 void G_DeferedPlayDemo(const char *demo);
 
 // Can be called by the startup code or M_Responder, calls P_SetupLevel.
@@ -144,6 +195,7 @@ void G_BeginRecording(void);
 void G_BeginMetal(void);
 
 // Only called by shutdown code.
+void G_WriteStanding(UINT8 ranking, char *name, INT32 skinnum, UINT8 color, UINT32 val);
 void G_SetDemoTime(UINT32 ptime, UINT32 plap);
 UINT8 G_CmpDemoTime(char *oldname, char *newname);
 
@@ -155,19 +207,41 @@ typedef enum
 	GHC_INVINCIBLE
 } ghostcolor_t;
 
+extern UINT8 demo_extradata[MAXPLAYERS];
+extern UINT8 demo_writerng;
+#define DXD_RESPAWN 0x01 // "respawn" command in console
+#define DXD_SKIN 0x02 // skin changed
+#define DXD_NAME 0x04 // name changed
+#define DXD_COLOR 0x08 // color changed
+#define DXD_PLAYSTATE 0x10 // state changed between playing, spectating, or not in-game
+
+#define DXD_PST_PLAYING 0x01
+#define DXD_PST_SPECTATING 0x02
+#define DXD_PST_LEFT 0x03
+
 // Record/playback tics
+void G_ReadDemoExtraData(void);
+void G_WriteDemoExtraData(void);
 void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
 void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
-void G_GhostAddThok(void);
-void G_GhostAddSpin(void);
-void G_GhostAddRev(void);
-void G_GhostAddColor(ghostcolor_t color);
-void G_GhostAddFlip(void);
-void G_GhostAddScale(fixed_t scale);
-void G_GhostAddHit(mobj_t *victim);
-void G_WriteGhostTic(mobj_t *ghost);
-void G_ConsGhostTic(void);
+void G_GhostAddThok(INT32 playernum);
+void G_GhostAddSpin(INT32 playernum);
+void G_GhostAddRev(INT32 playernum);
+void G_GhostAddColor(INT32 playernum, ghostcolor_t color);
+void G_GhostAddFlip(INT32 playernum);
+void G_GhostAddScale(INT32 playernum, fixed_t scale);
+void G_GhostAddHit(INT32 playernum, mobj_t *victim);
+void G_WriteAllGhostTics(void);
+void G_WriteGhostTic(mobj_t *ghost, INT32 playernum);
+void G_ConsAllGhostTics(void);
+void G_ConsGhostTic(INT32 playernum);
 void G_GhostTicker(void);
+
+void G_InitDemoRewind(void);
+void G_StoreRewindInfo(void);
+void G_PreviewRewind(tic_t previewtime);
+void G_ConfirmRewind(tic_t rewindtime);
+
 void G_ReadMetalTic(mobj_t *metal);
 void G_WriteMetalTic(mobj_t *metal);
 void G_SaveMetal(UINT8 **buffer);
@@ -184,6 +258,13 @@ typedef struct demoghost {
 } demoghost;
 extern demoghost *ghosts;
 
+// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's.
+#define DFILE_ERROR_NOTLOADED            0x01 // Files are not loaded, but can be without a restart.
+#define DFILE_ERROR_OUTOFORDER           0x02 // Files are loaded, but out of order.
+#define DFILE_ERROR_INCOMPLETEOUTOFORDER 0x03 // Some files are loaded out of order, but others are not.
+#define DFILE_ERROR_CANNOTLOAD           0x04 // Files are missing and cannot be loaded.
+#define DFILE_ERROR_EXTRAFILES           0x05 // Extra files outside of the replay's file list are loaded.
+
 void G_DoPlayDemo(char *defdemoname);
 void G_TimeDemo(const char *name);
 void G_AddGhost(char *defdemoname);
@@ -194,7 +275,10 @@ void G_StopMetalDemo(void);
 ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(void);
 void G_StopDemo(void);
 boolean G_CheckDemoStatus(void);
+void G_SaveDemo(void);
+boolean G_DemoTitleResponder(event_t *ev);
 
+INT32 G_GetGametypeByName(const char *gametypestr);
 boolean G_IsSpecialStage(INT32 mapnum);
 boolean G_GametypeUsesLives(void);
 boolean G_GametypeHasTeams(void);
@@ -214,6 +298,16 @@ void G_EndGame(void); // moved from y_inter.c/h and renamed
 void G_Ticker(boolean run);
 boolean G_Responder(event_t *ev);
 
+boolean G_CouldView(INT32 playernum);
+boolean G_CanView(INT32 playernum, UINT8 viewnum, boolean onlyactive);
+
+INT32 G_FindView(INT32 startview, UINT8 viewnum, boolean onlyactive, boolean reverse);
+INT32 G_CountPlayersPotentiallyViewable(boolean active);
+
+void G_ResetViews(void);
+void G_ResetView(UINT8 viewnum, INT32 playernum, boolean onlyactive);
+void G_AdjustView(UINT8 viewnum, INT32 offset, boolean onlyactive);
+
 void G_AddPlayer(INT32 playernum);
 
 void G_SetExitGameFlag(void);
diff --git a/src/hardware/hw3sound.c b/src/hardware/hw3sound.c
index f7c6e1da025e1db33471bd8775bc6a8ff4c826a3..2594a5df9d161ed2e39a74e7897da481dcb6c8bc 100644
--- a/src/hardware/hw3sound.c
+++ b/src/hardware/hw3sound.c
@@ -296,7 +296,7 @@ static void HW3S_FillSourceParameters
 	data->max_distance = MAX_DISTANCE;
 	data->min_distance = MIN_DISTANCE;
 
-	if (origin && origin != players[displayplayer].mo)
+	if (origin && origin != players[displayplayers[0]].mo)
 	{
 		data->head_relative = false;
 
@@ -356,10 +356,10 @@ INT32 HW3S_I_StartSound(const void *origin_p, source3D_data_t *source_parm, chan
 	source3D_data_t source3d_data;
 	INT32             s_num = 0;
 	source_t        *source = NULL;
-	mobj_t *listenmobj = players[displayplayer].mo;
+	mobj_t *listenmobj = players[displayplayers[0]].mo; // TODO: Kart 4P does not support sounds properly here
 	mobj_t *listenmobj2 = NULL;
 
-	if (splitscreen) listenmobj2 = players[secondarydisplayplayer].mo;
+	if (splitscreen) listenmobj2 = players[displayplayers[1]].mo;
 
 	if (sound_disabled)
 		return -1;
@@ -876,12 +876,12 @@ static void HW3S_Update3DSource(source_t *src)
 
 void HW3S_UpdateSources(void)
 {
-	mobj_t *listener = players[displayplayer].mo;
+	mobj_t *listener = players[displayplayers[0]].mo;
 	mobj_t *listener2 = NULL;
 	source_t    *src;
 	INT32 audible, snum, volume, sep, pitch;
 
-	if (splitscreen) listener2 = players[secondarydisplayplayer].mo;
+	if (splitscreen) listener2 = players[displayplayers[1]].mo;
 
 	HW3S_UpdateListener2(listener2);
 	HW3S_UpdateListener(listener);
diff --git a/src/hardware/hw_cache.c b/src/hardware/hw_cache.c
index 78fc31afccceb1397b08bc40bc3a17e247ff0d89..d49531bdf3cfa53dcdf5c1eac8d3df8bd7db50a9 100644
--- a/src/hardware/hw_cache.c
+++ b/src/hardware/hw_cache.c
@@ -241,43 +241,6 @@ static void HWR_ResizeBlock(INT32 originalwidth, INT32 originalheight,
 		if (blockheight < 1)
 			I_Error("3D GenerateTexture : too small");
 	}
-	else if (cv_voodoocompatibility.value)
-	{
-		if (originalwidth > 256 || originalheight > 256)
-		{
-			blockwidth = 256;
-			while (originalwidth < blockwidth)
-				blockwidth >>= 1;
-			if (blockwidth < 1)
-				I_Error("3D GenerateTexture : too small");
-
-			blockheight = 256;
-			while (originalheight < blockheight)
-				blockheight >>= 1;
-			if (blockheight < 1)
-				I_Error("3D GenerateTexture : too small");
-		}
-		else
-		{
-			//size up to nearest power of 2
-			blockwidth = 1;
-			while (blockwidth < originalwidth)
-				blockwidth <<= 1;
-			// scale down the original graphics to fit in 256
-			if (blockwidth > 256)
-				blockwidth = 256;
-				//I_Error("3D GenerateTexture : too big");
-
-			//size up to nearest power of 2
-			blockheight = 1;
-			while (blockheight < originalheight)
-				blockheight <<= 1;
-			// scale down the original graphics to fit in 256
-			if (blockheight > 256)
-				blockheight = 255;
-				//I_Error("3D GenerateTexture : too big");
-		}
-	}
 	else
 	{
 		//size up to nearest power of 2
@@ -508,18 +471,6 @@ void HWR_MakePatch (const patch_t *patch, GLPatch_t *grPatch, GLMipmap_t *grMipm
 		newwidth = blockwidth;
 		newheight = blockheight;
 	}
-	else if (cv_voodoocompatibility.value) // Only scales down textures that exceed 256x256.
-	{
-		// no rounddown, do not size up patches, so they don't look 'scaled'
-		newwidth  = min(grPatch->width, blockwidth);
-		newheight = min(grPatch->height, blockheight);
-
-		if (newwidth > 256 || newheight > 256)
-		{
-			newwidth = blockwidth;
-			newheight = blockheight;
-		}
-	}
 	else
 	{
 		// no rounddown, do not size up patches, so they don't look 'scaled'
@@ -935,18 +886,6 @@ GLPatch_t *HWR_GetPic(lumpnum_t lumpnum)
 			newwidth = blockwidth;
 			newheight = blockheight;
 		}
-		else if (cv_voodoocompatibility.value) // Only scales down textures that exceed 256x256.
-		{
-			// no rounddown, do not size up patches, so they don't look 'scaled'
-			newwidth  = min(SHORT(pic->width),blockwidth);
-			newheight = min(SHORT(pic->height),blockheight);
-
-			if (newwidth > 256 || newheight > 256)
-			{
-				newwidth = blockwidth;
-				newheight = blockheight;
-			}
-		}
 		else
 		{
 			// no rounddown, do not size up patches, so they don't look 'scaled'
diff --git a/src/hardware/hw_clip.c b/src/hardware/hw_clip.c
index 2397ce089d72b8e9b5a9d499dcfcfebab02e6a47..a63527083125824fc5cbc521c040db5fa615670a 100644
--- a/src/hardware/hw_clip.c
+++ b/src/hardware/hw_clip.c
@@ -78,8 +78,8 @@
 #include "r_opengl/r_opengl.h"
 
 #ifdef HAVE_SPHEREFRUSTRUM
-static GLdouble viewMatrix[16];
-static GLdouble projMatrix[16];
+static GLfloat viewMatrix[16];
+static GLfloat projMatrix[16];
 float frustum[6][4];
 #endif
 
@@ -381,8 +381,8 @@ void gld_FrustrumSetup(void)
 	float t;
 	float clip[16];
 
-	pglGetDoublev(GL_PROJECTION_MATRIX, projMatrix);
-	pglGetDoublev(GL_MODELVIEW_MATRIX, viewMatrix);
+	pglGeFloatv(GL_PROJECTION_MATRIX, projMatrix);
+	pglGetFloatv(GL_MODELVIEW_MATRIX, viewMatrix);
 
 	clip[0]  = CALCMATRIX(0, 0, 1, 4, 2, 8, 3, 12);
 	clip[1]  = CALCMATRIX(0, 1, 1, 5, 2, 9, 3, 13);
diff --git a/src/hardware/hw_defs.h b/src/hardware/hw_defs.h
index ece627d3ce12ebac9c4c4c0f99a099f58653552c..b37850bb72457d53035d6d2583eb24ec94fd5538 100644
--- a/src/hardware/hw_defs.h
+++ b/src/hardware/hw_defs.h
@@ -101,15 +101,29 @@ typedef struct
 
 //Hurdler: Transform (coords + angles)
 //BP: transform order : scale(rotation_x(rotation_y(translation(v))))
+
+// Kart features
+#define USE_FTRANSFORM_ANGLEZ
+#define USE_FTRANSFORM_MIRROR
+
+// Vanilla features
+//#define USE_MODEL_NEXTFRAME
+
 typedef struct
 {
 	FLOAT       x,y,z;           // position
+#ifdef USE_FTRANSFORM_ANGLEZ
 	FLOAT       anglex,angley,anglez;   // aimingangle / viewangle
+#else
+	FLOAT       anglex,angley;   // aimingangle / viewangle
+#endif
 	FLOAT       scalex,scaley,scalez;
 	FLOAT       fovxangle, fovyangle;
 	UINT8       splitscreen;
 	boolean     flip;            // screenflip
+#ifdef USE_FTRANSFORM_MIRROR
 	boolean     mirror;          // SRB2Kart: Encore Mode
+#endif
 } FTransform;
 
 // Transformed vector, as passed to HWR API
@@ -152,7 +166,7 @@ enum EPolyFlags
 	                                    // When set, pass the color constant into the FSurfaceInfo -> FlatColor
 	PF_NoTexture        = 0x00002000,   // Use the small white texture
 	PF_Corona           = 0x00004000,   // Tell the rendrer we are drawing a corona
-	PF_MD2              = 0x00008000,   // Tell the rendrer we are drawing an MD2
+	PF_Unused           = 0x00008000,   // Unused
 	PF_RemoveYWrap      = 0x00010000,   // Force clamp texture on Y
 	PF_ForceWrapX       = 0x00020000,   // Force repeat texture on X
 	PF_ForceWrapY       = 0x00040000,   // Force repeat texture on Y
@@ -210,8 +224,6 @@ enum hwdsetspecialstate
 	HWD_SET_FOG_COLOR,
 	HWD_SET_FOG_DENSITY,
 	HWD_SET_FOV,
-	HWD_SET_POLYGON_SMOOTH,
-	HWD_SET_PALETTECOLOR,
 	HWD_SET_TEXTUREFILTERMODE,
 	HWD_SET_TEXTUREANISOTROPICMODE,
 	HWD_NUMSTATE
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index e2fa90eb035de94d0b5ad7d81de624ed0fbe4341..54bd9e78648f467abb1dcccb270d44a15f3613dd 100644
--- a/src/hardware/hw_drv.h
+++ b/src/hardware/hw_drv.h
@@ -58,20 +58,18 @@ EXPORT void HWRAPI(ClearMipMapCache) (void);
 EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value);
 
 //Hurdler: added for new development
-EXPORT void HWRAPI(DrawMD2) (INT32 *gl_cmd_buffer, md2_frame_t *frame, FTransform *pos, float scale);
-EXPORT void HWRAPI(DrawMD2i) (INT32 *gl_cmd_buffer, md2_frame_t *frame, INT32 duration, INT32 tics, md2_frame_t *nextframe, FTransform *pos, float scale, UINT8 flipped, UINT8 *color);
+EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, INT32 duration, INT32 tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 *color);
+EXPORT void HWRAPI(CreateModelVBOs) (model_t *model);
 EXPORT void HWRAPI(SetTransform) (FTransform *ptransform);
 EXPORT INT32 HWRAPI(GetTextureUsed) (void);
 EXPORT INT32 HWRAPI(GetRenderVersion) (void);
 
-#ifdef SHUFFLE
 #define SCREENVERTS 10
 EXPORT void HWRAPI(PostImgRedraw) (float points[SCREENVERTS][SCREENVERTS][2]);
-#endif
 EXPORT void HWRAPI(FlushScreenTextures) (void);
 EXPORT void HWRAPI(StartScreenWipe) (void);
 EXPORT void HWRAPI(EndScreenWipe) (void);
-EXPORT void HWRAPI(DoScreenWipe) (float alpha);
+EXPORT void HWRAPI(DoScreenWipe) (void);
 EXPORT void HWRAPI(DrawIntermissionBG) (void);
 EXPORT void HWRAPI(MakeScreenTexture) (void);
 EXPORT void HWRAPI(MakeScreenFinalTexture) (void);
@@ -96,8 +94,8 @@ struct hwdriver_s
 	GClipRect           pfnGClipRect;
 	ClearMipMapCache    pfnClearMipMapCache;
 	SetSpecialState     pfnSetSpecialState;//Hurdler: added for backward compatibility
-	DrawMD2             pfnDrawMD2;
-	DrawMD2i            pfnDrawMD2i;
+	DrawModel           pfnDrawModel;
+	CreateModelVBOs     pfnCreateModelVBOs;
 	SetTransform        pfnSetTransform;
 	GetTextureUsed      pfnGetTextureUsed;
 	GetRenderVersion    pfnGetRenderVersion;
@@ -107,9 +105,7 @@ struct hwdriver_s
 #ifndef HAVE_SDL
 	Shutdown            pfnShutdown;
 #endif
-#ifdef SHUFFLE
 	PostImgRedraw       pfnPostImgRedraw;
-#endif
 	FlushScreenTextures pfnFlushScreenTextures;
 	StartScreenWipe     pfnStartScreenWipe;
 	EndScreenWipe       pfnEndScreenWipe;
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 4fcef218af3f82ba6b723cf759ee862b204abfe5..6a08b1c79959d119a16f2202994b509dcde4a668 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -63,7 +63,7 @@ struct hwdriver_s hwdriver;
 // ==========================================================================
 
 
-static void HWR_AddSprites(sector_t *sec, UINT8 ssplayer);
+static void HWR_AddSprites(sector_t *sec);
 static void HWR_ProjectSprite(mobj_t *thing);
 #ifdef HWPRECIP
 static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing);
@@ -1319,7 +1319,7 @@ static void HWR_SplitWall(sector_t *sector, wallVert3D *wallVerts, INT32 texnum,
 
 // HWR_DrawSkyWalls
 // Draw walls into the depth buffer so that anything behind is culled properly
-static void HWR_DrawSkyWall(wallVert3D *wallVerts, FSurfaceInfo *Surf, fixed_t bottom, fixed_t top)
+static void HWR_DrawSkyWall(wallVert3D *wallVerts, FSurfaceInfo *Surf)
 {
 	HWD.pfnSetTexture(NULL);
 	// no texture
@@ -1327,9 +1327,6 @@ static void HWR_DrawSkyWall(wallVert3D *wallVerts, FSurfaceInfo *Surf, fixed_t b
 	wallVerts[0].t = wallVerts[1].t = 0;
 	wallVerts[0].s = wallVerts[3].s = 0;
 	wallVerts[2].s = wallVerts[1].s = 0;
-	// set top/bottom coords
-	wallVerts[2].y = wallVerts[3].y = FIXED_TO_FLOAT(top); // No real way to find the correct height of this
-	wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(bottom); // worldlow/bottom because it needs to cover up the lower thok barrier wall
 	HWR_ProjectWall(wallVerts, Surf, PF_Invisible|PF_Clip|PF_NoTexture, 255, NULL);
 	// PF_Invisible so it's not drawn into the colour buffer
 	// PF_NoTexture for no texture
@@ -1462,6 +1459,111 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 		worldlow  = gr_backsector->floorheight;
 #endif
 
+		// Sky culling
+		if (!gr_curline->polyseg) // Don't do it for polyobjects
+		{
+			// Sky Ceilings
+			wallVerts[3].y = wallVerts[2].y = FIXED_TO_FLOAT(INT32_MAX);
+
+			if (gr_frontsector->ceilingpic == skyflatnum)
+			{
+				if (gr_backsector->ceilingpic == skyflatnum)
+				{
+					// Both front and back sectors are sky, needs skywall from the frontsector's ceiling, but only if the
+					// backsector is lower
+					if ((worldhigh <= worldtop)
+#ifdef ESLOPE
+					&& (worldhighslope <= worldtopslope)
+#endif
+					)
+					{
+#ifdef ESLOPE
+						wallVerts[0].y = FIXED_TO_FLOAT(worldhigh);
+						wallVerts[1].y = FIXED_TO_FLOAT(worldhighslope);
+#else
+						wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(worldhigh);
+#endif
+						HWR_DrawSkyWall(wallVerts, &Surf);
+					}
+				}
+				else
+				{
+					// Only the frontsector is sky, just draw a skywall from the front ceiling
+#ifdef ESLOPE
+					wallVerts[0].y = FIXED_TO_FLOAT(worldtop);
+					wallVerts[1].y = FIXED_TO_FLOAT(worldtopslope);
+#else
+					wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(worldtop);
+#endif
+					HWR_DrawSkyWall(wallVerts, &Surf);
+				}
+			}
+			else if (gr_backsector->ceilingpic == skyflatnum)
+			{
+				// Only the backsector is sky, just draw a skywall from the front ceiling
+#ifdef ESLOPE
+				wallVerts[0].y = FIXED_TO_FLOAT(worldtop);
+				wallVerts[1].y = FIXED_TO_FLOAT(worldtopslope);
+#else
+				wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(worldtop);
+#endif
+				HWR_DrawSkyWall(wallVerts, &Surf);
+			}
+
+
+			// Sky Floors
+			wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(INT32_MIN);
+
+			if (gr_frontsector->floorpic == skyflatnum)
+			{
+				if (gr_backsector->floorpic == skyflatnum)
+				{
+					// Both front and back sectors are sky, needs skywall from the backsector's floor, but only if the
+					// it's higher, also needs to check for bottomtexture as the floors don't usually move down
+					// when both sides are sky floors
+					if ((worldlow >= worldbottom)
+#ifdef ESLOPE
+					&& (worldlowslope >= worldbottomslope)
+#endif
+					&& !(gr_sidedef->bottomtexture))
+					{
+#ifdef ESLOPE
+						wallVerts[3].y = FIXED_TO_FLOAT(worldlow);
+						wallVerts[2].y = FIXED_TO_FLOAT(worldlowslope);
+#else
+						wallVerts[3].y = wallVerts[2].y = FIXED_TO_FLOAT(worldlow);
+#endif
+
+						HWR_DrawSkyWall(wallVerts, &Surf);
+					}
+				}
+				else
+				{
+					// Only the backsector has sky, just draw a skywall from the back floor
+#ifdef ESLOPE
+					wallVerts[3].y = FIXED_TO_FLOAT(worldbottom);
+					wallVerts[2].y = FIXED_TO_FLOAT(worldbottomslope);
+#else
+					wallVerts[3].y = wallVerts[2].y = FIXED_TO_FLOAT(worldbottom);
+#endif
+
+					HWR_DrawSkyWall(wallVerts, &Surf);
+				}
+			}
+			else if ((gr_backsector->floorpic == skyflatnum) && !(gr_sidedef->bottomtexture))
+			{
+				// Only the backsector has sky, just draw a skywall from the back floor if there's no bottomtexture
+#ifdef ESLOPE
+				wallVerts[3].y = FIXED_TO_FLOAT(worldlow);
+				wallVerts[2].y = FIXED_TO_FLOAT(worldlowslope);
+#else
+				wallVerts[3].y = wallVerts[2].y = FIXED_TO_FLOAT(worldlow);
+#endif
+
+				HWR_DrawSkyWall(wallVerts, &Surf);
+			}
+		}
+
 		// hack to allow height changes in outdoor areas
 		// This is what gets rid of the upper textures if there should be sky
 		if (gr_frontsector->ceilingpic == skyflatnum &&
@@ -1914,85 +2016,6 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 				Surf.FlatColor.rgba = 0xffffffff;
 			}*/
 		}
-
-		// Isn't this just the most lovely mess
-		if (!gr_curline->polyseg) // Don't do it for polyobjects
-		{
-			if (gr_frontsector->ceilingpic == skyflatnum || gr_backsector->ceilingpic == skyflatnum)
-			{
-				fixed_t depthwallheight;
-
-				if (!gr_sidedef->toptexture || (gr_frontsector->ceilingpic == skyflatnum && gr_backsector->ceilingpic == skyflatnum)) // when both sectors are sky, the top texture isn't drawn
-					depthwallheight = gr_frontsector->ceilingheight < gr_backsector->ceilingheight ? gr_frontsector->ceilingheight : gr_backsector->ceilingheight;
-				else
-					depthwallheight = gr_frontsector->ceilingheight > gr_backsector->ceilingheight ? gr_frontsector->ceilingheight : gr_backsector->ceilingheight;
-
-				if (gr_frontsector->ceilingheight-gr_frontsector->floorheight <= 0) // current sector is a thok barrier
-				{
-					if (gr_backsector->ceilingheight-gr_backsector->floorheight <= 0) // behind sector is also a thok barrier
-					{
-						if (!gr_sidedef->bottomtexture) // Only extend further down if there's no texture
-							HWR_DrawSkyWall(wallVerts, &Surf, worldbottom < worldlow ? worldbottom : worldlow, INT32_MAX);
-						else
-							HWR_DrawSkyWall(wallVerts, &Surf, worldbottom > worldlow ? worldbottom : worldlow, INT32_MAX);
-					}
-					// behind sector is not a thok barrier
-					else if (gr_backsector->ceilingheight <= gr_frontsector->ceilingheight) // behind sector ceiling is lower or equal to current sector
-						HWR_DrawSkyWall(wallVerts, &Surf, depthwallheight, INT32_MAX);
-						// gr_front/backsector heights need to be used here because of the worldtop being set to worldhigh earlier on
-				}
-				else if (gr_backsector->ceilingheight-gr_backsector->floorheight <= 0) // behind sector is a thok barrier, current sector is not
-				{
-					if (gr_backsector->ceilingheight >= gr_frontsector->ceilingheight // thok barrier ceiling height is equal to or greater than current sector ceiling height
-						|| gr_backsector->floorheight <= gr_frontsector->floorheight // thok barrier ceiling height is equal to or less than current sector floor height
-						|| gr_backsector->ceilingpic != skyflatnum) // thok barrier is not a sky
-						HWR_DrawSkyWall(wallVerts, &Surf, depthwallheight, INT32_MAX);
-				}
-				else // neither sectors are thok barriers
-				{
-					if ((gr_backsector->ceilingheight < gr_frontsector->ceilingheight && !gr_sidedef->toptexture) // no top texture and sector behind is lower
-						|| gr_backsector->ceilingpic != skyflatnum) // behind sector is not a sky
-						HWR_DrawSkyWall(wallVerts, &Surf, depthwallheight, INT32_MAX);
-				}
-			}
-			// And now for sky floors!
-			if (gr_frontsector->floorpic == skyflatnum || gr_backsector->floorpic == skyflatnum)
-			{
-				fixed_t depthwallheight;
-
-				if (!gr_sidedef->bottomtexture)
-					depthwallheight = worldbottom > worldlow ? worldbottom : worldlow;
-				else
-					depthwallheight = worldbottom < worldlow ? worldbottom : worldlow;
-
-				if (gr_frontsector->ceilingheight-gr_frontsector->floorheight <= 0) // current sector is a thok barrier
-				{
-					if (gr_backsector->ceilingheight-gr_backsector->floorheight <= 0) // behind sector is also a thok barrier
-					{
-						if (!gr_sidedef->toptexture) // Only extend up if there's no texture
-							HWR_DrawSkyWall(wallVerts, &Surf, INT32_MIN, worldtop > worldhigh ? worldtop : worldhigh);
-						else
-							HWR_DrawSkyWall(wallVerts, &Surf, INT32_MIN, worldtop < worldhigh ? worldtop : worldhigh);
-					}
-					// behind sector is not a thok barrier
-					else if (gr_backsector->floorheight >= gr_frontsector->floorheight) // behind sector floor is greater or equal to current sector
-						HWR_DrawSkyWall(wallVerts, &Surf, INT32_MIN, depthwallheight);
-				}
-				else if (gr_backsector->ceilingheight-gr_backsector->floorheight <= 0) // behind sector is a thok barrier, current sector is not
-				{
-					if (gr_backsector->floorheight <= gr_frontsector->floorheight // thok barrier floor height is equal to or less than current sector floor height
-						|| gr_backsector->ceilingheight >= gr_frontsector->ceilingheight // thok barrier floor height is equal to or greater than current sector ceiling height
-						|| gr_backsector->floorpic != skyflatnum) // thok barrier is not a sky
-						HWR_DrawSkyWall(wallVerts, &Surf, INT32_MIN, depthwallheight);
-				}
-				else // neither sectors are thok barriers
-				{
-					if ((gr_backsector->floorheight > gr_frontsector->floorheight && !gr_sidedef->bottomtexture) // no bottom texture and sector behind is higher
-						|| gr_backsector->floorpic != skyflatnum) // behind sector is not a sky
-						HWR_DrawSkyWall(wallVerts, &Surf, INT32_MIN, depthwallheight);
-				}
-			}
-		}
 	}
 	else
 	{
@@ -2060,13 +2083,52 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 					HWR_ProjectWall(wallVerts, &Surf, PF_Masked, lightnum, colormap);
 			}
 		}
+		else
+		{
+#ifdef ESLOPE
+			//Set textures properly on single sided walls that are sloped
+			wallVerts[3].y = FIXED_TO_FLOAT(worldtop);
+			wallVerts[0].y = FIXED_TO_FLOAT(worldbottom);
+			wallVerts[2].y = FIXED_TO_FLOAT(worldtopslope);
+			wallVerts[1].y = FIXED_TO_FLOAT(worldbottomslope);
+#else
+			// set top/bottom coords
+			wallVerts[2].y = wallVerts[3].y = FIXED_TO_FLOAT(worldtop);
+			wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(worldbottom);
+#endif
+
+			// When there's no midtexture, draw a skywall to prevent rendering behind it
+			HWR_DrawSkyWall(wallVerts, &Surf);
+		}
+
 
+		// Single sided lines are simple for skywalls, just need to draw from the top or bottom of the sector if there's
+		// a sky flat
 		if (!gr_curline->polyseg)
 		{
 			if (gr_frontsector->ceilingpic == skyflatnum) // It's a single-sided line with sky for its sector
-				HWR_DrawSkyWall(wallVerts, &Surf, worldtop, INT32_MAX);
+			{
+				wallVerts[3].y = wallVerts[2].y = FIXED_TO_FLOAT(INT32_MAX);
+#ifdef ESLOPE
+				wallVerts[0].y = FIXED_TO_FLOAT(worldtop);
+				wallVerts[1].y = FIXED_TO_FLOAT(worldtopslope);
+#else
+				wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(worldtop);
+#endif
+				HWR_DrawSkyWall(wallVerts, &Surf);
+			}
 			if (gr_frontsector->floorpic == skyflatnum)
-				HWR_DrawSkyWall(wallVerts, &Surf, INT32_MIN, worldbottom);
+			{
+#ifdef ESLOPE
+				wallVerts[3].y = FIXED_TO_FLOAT(worldbottom);
+				wallVerts[2].y = FIXED_TO_FLOAT(worldbottomslope);
+#else
+				wallVerts[3].y = wallVerts[2].y = FIXED_TO_FLOAT(worldbottom);
+#endif
+				wallVerts[0].y = wallVerts[1].y = FIXED_TO_FLOAT(INT32_MIN);
+
+				HWR_DrawSkyWall(wallVerts, &Surf);
+			}
 		}
 	}
 
@@ -2381,7 +2443,7 @@ static void HWR_StoreWallRange(double startfrac, double endfrac)
 // e6y: Check whether the player can look beyond this line
 //
 #ifdef NEWCLIP
-boolean checkforemptylines = true;
+static boolean checkforemptylines = true;
 // Don't modify anything here, just check
 // Kalaron: Modified for sloped linedefs
 static boolean CheckClip(seg_t * seg, sector_t * afrontsector, sector_t * abacksector)
@@ -2421,62 +2483,47 @@ static boolean CheckClip(seg_t * seg, sector_t * afrontsector, sector_t * abacks
 		backc1 = backc2 = abacksector->ceilingheight;
 	}
 
-	// now check for closed sectors!
-	if (backc1 <= frontf1 && backc2 <= frontf2)
+	if (viewsector != abacksector && viewsector != afrontsector)
 	{
-		checkforemptylines = false;
-		if (!seg->sidedef->toptexture)
-			return false;
-
-		if (abacksector->ceilingpic == skyflatnum && afrontsector->ceilingpic == skyflatnum)
-			return false;
-
-		return true;
-	}
+		boolean mydoorclosed = false; // My door? Closed!? (doorclosed is actually otherwise unused in openGL)
 
-	if (backf1 >= frontc1 && backf2 >= frontc2)
-	{
-		checkforemptylines = false;
-		if (!seg->sidedef->bottomtexture)
-			return false;
-
-		// properly render skies (consider door "open" if both floors are sky):
-		if (abacksector->ceilingpic == skyflatnum && afrontsector->ceilingpic == skyflatnum)
-			return false;
-
-		return true;
-	}
-
-	if (backc1 <= backf1 && backc2 <= backf2)
-	{
-		checkforemptylines = false;
-		// preserve a kind of transparent door/lift special effect:
-		if (backc1 < frontc1 || backc2 < frontc2)
+		// If the sector behind the line blocks all kinds of view past it
+		// (back ceiling is lower than close floor, or back floor is higher than close ceiling)
+		if ((backc1 <= frontf1 && backc2 <= frontf2)
+			|| (backf1 >= frontc1 && backf2 >= frontc2))
 		{
-			if (!seg->sidedef->toptexture)
-				return false;
+			checkforemptylines = false;
+			return true;
 		}
-		if (backf1 > frontf1 || backf2 > frontf2)
+
+		// The door is closed if:
+		// backsector is 0 height or less and
+		// back ceiling is higher than close ceiling or we need to render a top texture and
+		// back floor is lower than close floor or we need to render a bottom texture and
+		// neither front or back sectors are using the sky ceiling
+		mydoorclosed = (backc1 <= backf1 && backc2 <= backf2
+		&& ((backc1 >= frontc1 && backc2 >= frontc2) || seg->sidedef->toptexture)
+		&& ((backf1 <= frontf1 && backf2 >= frontf2) || seg->sidedef->bottomtexture)
+		&& (abacksector->ceilingpic != skyflatnum || afrontsector->ceilingpic != skyflatnum));
+
+		if (mydoorclosed)
 		{
-			if (!seg->sidedef->bottomtexture)
-				return false;
+			checkforemptylines = false;
+			return true;
 		}
-		if (abacksector->ceilingpic == skyflatnum && afrontsector->ceilingpic == skyflatnum)
-			return false;
-
-		if (abacksector->floorpic == skyflatnum && afrontsector->floorpic == skyflatnum)
-			return false;
-
-		return true;
 	}
 
+	// Window.
+	// We know it's a window when the above isn't true and the back and front sectors don't match
 	if (backc1 != frontc1 || backc2 != frontc2
 		|| backf1 != frontf1 || backf2 != frontf2)
-		{
-			checkforemptylines = false;
-			return false;
-		}
+	{
+		checkforemptylines = false;
+		return false;
+	}
 
+	// In this case we just need to check whether there is actually a need to render any lines, so checkforempty lines
+	// stays true
 	return false;
 }
 #else
@@ -3389,7 +3436,7 @@ static void HWR_AddPolyObjectPlanes(void)
 //                  : Draw one or more line segments.
 // Notes            : Sets gr_cursectorlight to the light of the parent sector, to modulate wall textures
 // -----------------+
-static void HWR_Subsector(size_t num, UINT8 ssplayer)
+static void HWR_Subsector(size_t num)
 {
 	INT16 count;
 	seg_t *line;
@@ -3754,7 +3801,7 @@ static void HWR_Subsector(size_t num, UINT8 ssplayer)
 	{
 		// draw sprites first, coz they are clipped to the solidsegs of
 		// subsectors more 'in front'
-		HWR_AddSprites(gr_frontsector, ssplayer);
+		HWR_AddSprites(gr_frontsector);
 
 		//Hurdler: at this point validcount must be the same, but is not because
 		//         gr_frontsector doesn't point anymore to sub->sector due to
@@ -3806,7 +3853,7 @@ static boolean HWR_CheckHackBBox(fixed_t *bb)
 // BP: big hack for a test in lighning ref : 1249753487AB
 fixed_t *hwbbox;
 
-static void HWR_RenderBSPNode(INT32 bspnum, UINT8 ssplayer)
+static void HWR_RenderBSPNode(INT32 bspnum)
 {
 	/*//GZDoom code
 	if(bspnum == -1)
@@ -3846,12 +3893,12 @@ static void HWR_RenderBSPNode(INT32 bspnum, UINT8 ssplayer)
 		if (bspnum == -1)
 		{
 			//*(gr_drawsubsector_p++) = 0;
-			HWR_Subsector(0, ssplayer);
+			HWR_Subsector(0);
 		}
 		else
 		{
 			//*(gr_drawsubsector_p++) = bspnum&(~NF_SUBSECTOR);
-			HWR_Subsector(bspnum&(~NF_SUBSECTOR), ssplayer);
+			HWR_Subsector(bspnum&(~NF_SUBSECTOR));
 		}
 		return;
 	}
@@ -3863,14 +3910,14 @@ static void HWR_RenderBSPNode(INT32 bspnum, UINT8 ssplayer)
 	hwbbox = bsp->bbox[side];
 
 	// Recursively divide front space.
-	HWR_RenderBSPNode(bsp->children[side], ssplayer);
+	HWR_RenderBSPNode(bsp->children[side]);
 
 	// Possibly divide back space.
 	if (HWR_CheckBBox(bsp->bbox[side^1]))
 	{
 		// BP: big hack for a test in lighning ref : 1249753487AB
 		hwbbox = bsp->bbox[side^1];
-		HWR_RenderBSPNode(bsp->children[side^1], ssplayer);
+		HWR_RenderBSPNode(bsp->children[side^1]);
 	}
 }
 
@@ -4097,14 +4144,14 @@ static void HWR_DrawSpriteShadow(gr_vissprite_t *spr, GLPatch_t *gpatch, float t
 		angle_t shadowdir;
 
 		// Set direction
-		if (splitscreen && stplyr == &players[secondarydisplayplayer])
-			shadowdir = localangle2 + FixedAngle(cv_cam2_rotate.value);
-		else if (splitscreen > 1 && stplyr == &players[thirddisplayplayer])
-			shadowdir = localangle3 + FixedAngle(cv_cam3_rotate.value);
-		else if (splitscreen > 2 && stplyr == &players[fourthdisplayplayer])
-			shadowdir = localangle4 + FixedAngle(cv_cam4_rotate.value);
+		if (splitscreen && stplyr == &players[displayplayers[1]])
+			shadowdir = localangle[1] + FixedAngle(cv_cam2_rotate.value);
+		else if (splitscreen > 1 && stplyr == &players[displayplayers[2]])
+			shadowdir = localangle[2] + FixedAngle(cv_cam3_rotate.value);
+		else if (splitscreen > 2 && stplyr == &players[displayplayers[3]])
+			shadowdir = localangle[3] + FixedAngle(cv_cam4_rotate.value);
 		else
-			shadowdir = localangle + FixedAngle(cv_cam_rotate.value);
+			shadowdir = localangle[0] + FixedAngle(cv_cam_rotate.value);
 
 		// Find floorheight
 		floorheight = HWR_OpaqueFloorAtPos(
@@ -4259,10 +4306,45 @@ static void HWR_DrawSpriteShadow(gr_vissprite_t *spr, GLPatch_t *gpatch, float t
 	}
 }
 
+// This is expecting a pointer to an array containing 4 wallVerts for a sprite
+static void HWR_RotateSpritePolyToAim(gr_vissprite_t *spr, FOutVector *wallVerts)
+{
+	if (cv_grspritebillboarding.value
+		&& spr && spr->mobj && !(spr->mobj->frame & FF_PAPERSPRITE)
+		&& wallVerts)
+	{
+		float basey = FIXED_TO_FLOAT(spr->mobj->z);
+		float lowy = wallVerts[0].y;
+		if (P_MobjFlip(spr->mobj) == -1)
+		{
+			basey = FIXED_TO_FLOAT(spr->mobj->z + spr->mobj->height);
+		}
+		// Rotate sprites to fully billboard with the camera
+		// X, Y, AND Z need to be manipulated for the polys to rotate around the
+		// origin, because of how the origin setting works I believe that should
+		// be mobj->z or mobj->z + mobj->height
+		wallVerts[2].y = wallVerts[3].y = (spr->ty - basey) * gr_viewludsin + basey;
+		wallVerts[0].y = wallVerts[1].y = (lowy - basey) * gr_viewludsin + basey;
+		// translate back to be around 0 before translating back
+		wallVerts[3].x += ((spr->ty - basey) * gr_viewludcos) * gr_viewcos;
+		wallVerts[2].x += ((spr->ty - basey) * gr_viewludcos) * gr_viewcos;
+
+		wallVerts[0].x += ((lowy - basey) * gr_viewludcos) * gr_viewcos;
+		wallVerts[1].x += ((lowy - basey) * gr_viewludcos) * gr_viewcos;
+
+		wallVerts[3].z += ((spr->ty - basey) * gr_viewludcos) * gr_viewsin;
+		wallVerts[2].z += ((spr->ty - basey) * gr_viewludcos) * gr_viewsin;
+
+		wallVerts[0].z += ((lowy - basey) * gr_viewludcos) * gr_viewsin;
+		wallVerts[1].z += ((lowy - basey) * gr_viewludcos) * gr_viewsin;
+	}
+}
+
 static void HWR_SplitSprite(gr_vissprite_t *spr)
 {
 	float this_scale = 1.0f;
 	FOutVector wallVerts[4];
+	FOutVector baseWallVerts[4]; // This is what the verts should end up as
 	GLPatch_t *gpatch;
 	FSurfaceInfo Surf;
 	const boolean hires = (spr->mobj && spr->mobj->skin && ((skin_t *)spr->mobj->skin)->flags & SF_HIRES);
@@ -4275,11 +4357,13 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 	float realtop, realbot, top, bot;
 	float towtop, towbot, towmult;
 	float bheight;
+	float realheight, heightmult;
 	const sector_t *sector = spr->mobj->subsector->sector;
 	const lightlist_t *list = sector->lightlist;
 #ifdef ESLOPE
 	float endrealtop, endrealbot, endtop, endbot;
 	float endbheight;
+	float endrealheight;
 	fixed_t temp;
 	fixed_t v1x, v1y, v2x, v2y;
 #endif
@@ -4312,16 +4396,16 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 		HWR_DrawSpriteShadow(spr, gpatch, this_scale);
 	}
 
-	wallVerts[0].x = wallVerts[3].x = spr->x1;
-	wallVerts[2].x = wallVerts[1].x = spr->x2;
-	wallVerts[0].z = wallVerts[3].z = spr->z1;
-	wallVerts[1].z = wallVerts[2].z = spr->z2;
+	baseWallVerts[0].x = baseWallVerts[3].x = spr->x1;
+	baseWallVerts[2].x = baseWallVerts[1].x = spr->x2;
+	baseWallVerts[0].z = baseWallVerts[3].z = spr->z1;
+	baseWallVerts[1].z = baseWallVerts[2].z = spr->z2;
 
-	wallVerts[2].y = wallVerts[3].y = spr->ty;
+	baseWallVerts[2].y = baseWallVerts[3].y = spr->ty;
 	if (spr->mobj && fabsf(this_scale - 1.0f) > 1.0E-36f)
-		wallVerts[0].y = wallVerts[1].y = spr->ty - gpatch->height * this_scale;
+		baseWallVerts[0].y = baseWallVerts[1].y = spr->ty - gpatch->height * this_scale;
 	else
-		wallVerts[0].y = wallVerts[1].y = spr->ty - gpatch->height;
+		baseWallVerts[0].y = baseWallVerts[1].y = spr->ty - gpatch->height;
 
 	v1x = FLOAT_TO_FIXED(spr->x1);
 	v1y = FLOAT_TO_FIXED(spr->z1);
@@ -4330,44 +4414,56 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 
 	if (spr->flip)
 	{
-		wallVerts[0].sow = wallVerts[3].sow = gpatch->max_s;
-		wallVerts[2].sow = wallVerts[1].sow = 0;
-	}else{
-		wallVerts[0].sow = wallVerts[3].sow = 0;
-		wallVerts[2].sow = wallVerts[1].sow = gpatch->max_s;
+		baseWallVerts[0].sow = baseWallVerts[3].sow = gpatch->max_s;
+		baseWallVerts[2].sow = baseWallVerts[1].sow = 0;
+	}
+	else
+	{
+		baseWallVerts[0].sow = baseWallVerts[3].sow = 0;
+		baseWallVerts[2].sow = baseWallVerts[1].sow = gpatch->max_s;
 	}
 
 	// flip the texture coords (look familiar?)
 	if (spr->vflip)
 	{
-		wallVerts[3].tow = wallVerts[2].tow = gpatch->max_t;
-		wallVerts[0].tow = wallVerts[1].tow = 0;
-	}else{
-		wallVerts[3].tow = wallVerts[2].tow = 0;
-		wallVerts[0].tow = wallVerts[1].tow = gpatch->max_t;
+		baseWallVerts[3].tow = baseWallVerts[2].tow = gpatch->max_t;
+		baseWallVerts[0].tow = baseWallVerts[1].tow = 0;
+	}
+	else
+	{
+		baseWallVerts[3].tow = baseWallVerts[2].tow = 0;
+		baseWallVerts[0].tow = baseWallVerts[1].tow = gpatch->max_t;
 	}
 
 	// if it has a dispoffset, push it a little towards the camera
 	if (spr->dispoffset) {
 		float co = -gr_viewcos*(0.05f*spr->dispoffset);
 		float si = -gr_viewsin*(0.05f*spr->dispoffset);
-		wallVerts[0].z = wallVerts[3].z = wallVerts[0].z+si;
-		wallVerts[1].z = wallVerts[2].z = wallVerts[1].z+si;
-		wallVerts[0].x = wallVerts[3].x = wallVerts[0].x+co;
-		wallVerts[1].x = wallVerts[2].x = wallVerts[1].x+co;
+		baseWallVerts[0].z = baseWallVerts[3].z = baseWallVerts[0].z+si;
+		baseWallVerts[1].z = baseWallVerts[2].z = baseWallVerts[1].z+si;
+		baseWallVerts[0].x = baseWallVerts[3].x = baseWallVerts[0].x+co;
+		baseWallVerts[1].x = baseWallVerts[2].x = baseWallVerts[1].x+co;
 	}
 
-	realtop = top = wallVerts[3].y;
-	realbot = bot = wallVerts[0].y;
-	towtop = wallVerts[3].tow;
-	towbot = wallVerts[0].tow;
+	// Let dispoffset work first since this adjust each vertex
+	HWR_RotateSpritePolyToAim(spr, baseWallVerts);
+
+	realtop = top = baseWallVerts[3].y;
+	realbot = bot = baseWallVerts[0].y;
+	towtop = baseWallVerts[3].tow;
+	towbot = baseWallVerts[0].tow;
 	towmult = (towbot - towtop) / (top - bot);
 
 #ifdef ESLOPE
-	endrealtop = endtop = wallVerts[2].y;
-	endrealbot = endbot = wallVerts[1].y;
+	endrealtop = endtop = baseWallVerts[2].y;
+	endrealbot = endbot = baseWallVerts[1].y;
 #endif
 
+	// copy the contents of baseWallVerts into the drawn wallVerts array
+	// baseWallVerts is used to know the final shape to easily get the vertex
+	// co-ordinates
+	memcpy(wallVerts, baseWallVerts, sizeof(baseWallVerts));
+
 	if (!cv_translucency.value) // translucency disabled
 	{
 		Surf.FlatColor.s.alpha = 0xFF;
@@ -4494,12 +4590,55 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 		wallVerts[2].y = endtop;
 		wallVerts[0].y = bot;
 		wallVerts[1].y = endbot;
+
+		// The x and y only need to be adjusted in the case that it's not a papersprite
+		if (cv_grspritebillboarding.value
+			&& spr->mobj && !(spr->mobj->frame & FF_PAPERSPRITE))
+		{
+			// Get the x and z of the vertices so billboarding draws correctly
+			realheight = realbot - realtop;
+			endrealheight = endrealbot - endrealtop;
+			heightmult = (realtop - top) / realheight;
+			wallVerts[3].x = baseWallVerts[3].x + (baseWallVerts[3].x - baseWallVerts[0].x) * heightmult;
+			wallVerts[3].z = baseWallVerts[3].z + (baseWallVerts[3].z - baseWallVerts[0].z) * heightmult;
+
+			heightmult = (endrealtop - endtop) / endrealheight;
+			wallVerts[2].x = baseWallVerts[2].x + (baseWallVerts[2].x - baseWallVerts[1].x) * heightmult;
+			wallVerts[2].z = baseWallVerts[2].z + (baseWallVerts[2].z - baseWallVerts[1].z) * heightmult;
+
+			heightmult = (realtop - bot) / realheight;
+			wallVerts[0].x = baseWallVerts[3].x + (baseWallVerts[3].x - baseWallVerts[0].x) * heightmult;
+			wallVerts[0].z = baseWallVerts[3].z + (baseWallVerts[3].z - baseWallVerts[0].z) * heightmult;
+
+			heightmult = (endrealtop - endbot) / endrealheight;
+			wallVerts[1].x = baseWallVerts[2].x + (baseWallVerts[2].x - baseWallVerts[1].x) * heightmult;
+			wallVerts[1].z = baseWallVerts[2].z + (baseWallVerts[2].z - baseWallVerts[1].z) * heightmult;
+		}
 #else
 		wallVerts[3].tow = wallVerts[2].tow = towtop + ((realtop - top) * towmult);
 		wallVerts[0].tow = wallVerts[1].tow = towtop + ((realtop - bot) * towmult);
 
 		wallVerts[2].y = wallVerts[3].y = top;
 		wallVerts[0].y = wallVerts[1].y = bot;
+
+		// The x and y only need to be adjusted in the case that it's not a papersprite
+		if (cv_grspritebillboarding.value
+			&& spr->mobj && !(spr->mobj->frame & FF_PAPERSPRITE))
+		{
+			// Get the x and z of the vertices so billboarding draws correctly
+			realheight = realbot - realtop;
+			heightmult = (realtop - top) / realheight;
+			wallVerts[3].x = baseWallVerts[3].x + (baseWallVerts[3].x - baseWallVerts[0].x) * heightmult;
+			wallVerts[3].z = baseWallVerts[3].z + (baseWallVerts[3].z - baseWallVerts[0].z) * heightmult;
+			wallVerts[2].x = baseWallVerts[2].x + (baseWallVerts[2].x - baseWallVerts[1].x) * heightmult;
+			wallVerts[2].z = baseWallVerts[2].z + (baseWallVerts[2].z - baseWallVerts[1].z) * heightmult;
+
+			heightmult = (realtop - bot) / realheight;
+			wallVerts[0].x = baseWallVerts[3].x + (baseWallVerts[3].x - baseWallVerts[0].x) * heightmult;
+			wallVerts[0].z = baseWallVerts[3].z + (baseWallVerts[3].z - baseWallVerts[0].z) * heightmult;
+			wallVerts[1].x = baseWallVerts[2].x + (baseWallVerts[2].x - baseWallVerts[1].x) * heightmult;
+			wallVerts[1].z = baseWallVerts[2].z + (baseWallVerts[2].z - baseWallVerts[1].z) * heightmult;
+		}
 #endif
 
 		if (colormap)
@@ -4669,6 +4808,9 @@ static void HWR_DrawSprite(gr_vissprite_t *spr)
 		wallVerts[1].x = wallVerts[2].x = wallVerts[1].x+co;
 	}
 
+	// Let dispoffset work first since this adjust each vertex
+	HWR_RotateSpritePolyToAim(spr, wallVerts);
+
 	// This needs to be AFTER the shadows so that the regular sprites aren't drawn completely black.
 	// sprite lighting by modulating the RGB components
 	/// \todo coloured
@@ -4750,6 +4892,9 @@ static inline void HWR_DrawPrecipitationSprite(gr_vissprite_t *spr)
 	wallVerts[0].z = wallVerts[3].z = spr->z1;
 	wallVerts[1].z = wallVerts[2].z = spr->z2;
 
+	// Let dispoffset work first since this adjust each vertex
+	HWR_RotateSpritePolyToAim(spr, wallVerts);
+
 	wallVerts[0].sow = wallVerts[3].sow = 0;
 	wallVerts[2].sow = wallVerts[1].sow = gpatch->max_s;
 
@@ -5261,14 +5406,14 @@ static void HWR_DrawSprites(void)
 				if (spr->mobj && spr->mobj->skin && spr->mobj->sprite == SPR_PLAY)
 				{
 					// 8/1/19: Only don't display player models if no default SPR_PLAY is found.
-					if (!cv_grmd2.value || ((md2_playermodels[(skin_t*)spr->mobj->skin-skins].notfound || md2_playermodels[(skin_t*)spr->mobj->skin-skins].scale < 0.0f) && (md2_models[SPR_PLAY].notfound || md2_models[SPR_PLAY].scale < 0.0f)))
+					if (!cv_grmdls.value || ((md2_playermodels[(skin_t*)spr->mobj->skin-skins].notfound || md2_playermodels[(skin_t*)spr->mobj->skin-skins].scale < 0.0f) && ((!cv_grfallbackplayermodel.value) || md2_models[SPR_PLAY].notfound || md2_models[SPR_PLAY].scale < 0.0f)))
 						HWR_DrawSprite(spr);
 					else
 						HWR_DrawMD2(spr);
 				}
 				else
 				{
-					if (!cv_grmd2.value || md2_models[spr->mobj->sprite].notfound || md2_models[spr->mobj->sprite].scale < 0.0f)
+					if (!cv_grmdls.value || md2_models[spr->mobj->sprite].notfound || md2_models[spr->mobj->sprite].scale < 0.0f)
 						HWR_DrawSprite(spr);
 					else
 						HWR_DrawMD2(spr);
@@ -5283,7 +5428,7 @@ static void HWR_DrawSprites(void)
 // During BSP traversal, this adds sprites by sector.
 // --------------------------------------------------------------------------
 static UINT8 sectorlight;
-static void HWR_AddSprites(sector_t *sec, UINT8 ssplayer)
+static void HWR_AddSprites(sector_t *sec)
 {
 	mobj_t *thing;
 #ifdef HWPRECIP
@@ -5316,19 +5461,19 @@ static void HWR_AddSprites(sector_t *sec, UINT8 ssplayer)
 			if (splitscreen)
 			{
 				if (thing->eflags & MFE_DRAWONLYFORP1)
-					if (ssplayer != 1)
+					if (viewssnum != 0)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP2)
-					if (ssplayer != 2)
+					if (viewssnum != 1)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP3 && splitscreen > 1)
-					if (ssplayer != 3)
+					if (viewssnum != 2)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP4 && splitscreen > 2)
-					if (ssplayer != 4)
+					if (viewssnum != 3)
 						continue;
 			}
 
@@ -5351,19 +5496,19 @@ static void HWR_AddSprites(sector_t *sec, UINT8 ssplayer)
 			if (splitscreen)
 			{
 				if (thing->eflags & MFE_DRAWONLYFORP1)
-					if (ssplayer != 1)
+					if (viewssnum != 0)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP2)
-					if (ssplayer != 2)
+					if (viewssnum != 1)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP3 && splitscreen > 1)
-					if (ssplayer != 3)
+					if (viewssnum != 2)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP4 && splitscreen > 2)
-					if (ssplayer != 4)
+					if (viewssnum != 3)
 						continue;
 			}
 
@@ -5372,7 +5517,7 @@ static void HWR_AddSprites(sector_t *sec, UINT8 ssplayer)
 	}
 
 #ifdef HWPRECIP
-	// Someone seriously wants infinite draw distance for precipitation?
+	// No to infinite precipitation draw distance.
 	if ((limit_dist = (fixed_t)cv_drawdist_precip.value << FRACBITS))
 	{
 		for (precipthing = sec->preciplist; precipthing; precipthing = precipthing->snext)
@@ -5388,13 +5533,6 @@ static void HWR_AddSprites(sector_t *sec, UINT8 ssplayer)
 			HWR_ProjectPrecipitationSprite(precipthing);
 		}
 	}
-	else
-	{
-		// Draw everything in sector, no checks
-		for (precipthing = sec->preciplist; precipthing; precipthing = precipthing->snext)
-			if (!(precipthing->precipflags & PCF_INVISIBLE))
-				HWR_ProjectPrecipitationSprite(precipthing);
-	}
 #endif
 }
 
@@ -5435,7 +5573,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	tz = (tr_x * gr_viewcos) + (tr_y * gr_viewsin);
 
 	// thing is behind view plane?
-	if (tz < ZCLIP_PLANE && !papersprite && (!cv_grmd2.value || md2_models[thing->sprite].notfound == true)) //Yellow: Only MD2's dont disappear
+	if (tz < ZCLIP_PLANE && !papersprite && (!cv_grmdls.value || md2_models[thing->sprite].notfound == true)) //Yellow: Only MD2's dont disappear
 		return;
 
 	// The above can stay as it works for cutting sprites that are too close
@@ -5906,33 +6044,8 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 {
 	const float fpov = FIXED_TO_FLOAT(cv_fov.value+player->fovadd);
 	postimg_t *type;
-	UINT8 ssplayer = 0;
 
-	if (splitscreen)
-	{
-		if (player == &players[secondarydisplayplayer])
-		{
-			type = &postimgtype2;
-			ssplayer = 2;
-		}
-		else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		{
-			type = &postimgtype3;
-			ssplayer = 3;
-		}
-		else if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		{
-			type = &postimgtype4;
-			ssplayer = 4;
-		}
-		else
-		{
-			type = &postimgtype;
-			ssplayer = 1;
-		}
-	}
-	else
-		type = &postimgtype;
+	type = &postimgtype[viewnumber];
 
 	{
 		// do we really need to save player (is it not the same)?
@@ -6056,36 +6169,36 @@ if (0)
 
 	validcount++;
 
-	HWR_RenderBSPNode((INT32)numnodes-1, ssplayer);
+	HWR_RenderBSPNode((INT32)numnodes-1);
 
 #ifndef NEWCLIP
 	// Make a viewangle int so we can render things based on mouselook
 	if (player == &players[consoleplayer])
-		viewangle = localaiming;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		viewangle = localaiming2;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		viewangle = localaiming3;
-	else if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		viewangle = localaiming4;
+		viewangle = localaiming[0];
+	else if (splitscreen && player == &players[displayplayers[1]])
+		viewangle = localaiming[1];
+	else if (splitscreen > 1 && player == &players[displayplayers[2]])
+		viewangle = localaiming[2];
+	else if (splitscreen > 2 && player == &players[displayplayers[3]])
+		viewangle = localaiming[3];
 
 	// Handle stuff when you are looking farther up or down.
 	if ((aimingangle || cv_fov.value+player->fovadd > 90*FRACUNIT))
 	{
 		dup_viewangle += ANGLE_90;
 		HWR_ClearClipSegs();
-		HWR_RenderBSPNode((INT32)numnodes-1, ssplayer); //left
+		HWR_RenderBSPNode((INT32)numnodes-1); //left
 
 		dup_viewangle += ANGLE_90;
 		if (((INT32)aimingangle > ANGLE_45 || (INT32)aimingangle<-ANGLE_45))
 		{
 			HWR_ClearClipSegs();
-			HWR_RenderBSPNode((INT32)numnodes-1, ssplayer); //back
+			HWR_RenderBSPNode((INT32)numnodes-1); //back
 		}
 
 		dup_viewangle += ANGLE_90;
 		HWR_ClearClipSegs();
-		HWR_RenderBSPNode((INT32)numnodes-1, ssplayer); //right
+		HWR_RenderBSPNode((INT32)numnodes-1); //right
 
 		dup_viewangle += ANGLE_90;
 	}
@@ -6149,38 +6262,13 @@ if (0)
 void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 {
 	const float fpov = FIXED_TO_FLOAT(cv_fov.value+player->fovadd);
-	postimg_t *type;
-	UINT8 ssplayer = 0;
+	postimg_t *type = &postimgtype[viewnumber];
 
 	const boolean skybox = (skyboxmo[0] && cv_skybox.value); // True if there's a skybox object and skyboxes are on
 
 	FRGBAFloat ClearColor;
 
-	if (splitscreen)
-	{
-		if (player == &players[secondarydisplayplayer])
-		{
-			type = &postimgtype2;
-			ssplayer = 2;
-		}
-		else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		{
-			type = &postimgtype3;
-			ssplayer = 3;
-		}
-		else if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		{
-			type = &postimgtype4;
-			ssplayer = 4;
-		}
-		else
-		{
-			type = &postimgtype;
-			ssplayer = 1;
-		}
-	}
-	else
-		type = &postimgtype;
+	type = &postimgtype[viewnumber];
 
 	ClearColor.red = 0.0f;
 	ClearColor.green = 0.0f;
@@ -6315,36 +6403,36 @@ if (0)
 
 	validcount++;
 
-	HWR_RenderBSPNode((INT32)numnodes-1, ssplayer);
+	HWR_RenderBSPNode((INT32)numnodes-1);
 
 #ifndef NEWCLIP
 	// Make a viewangle int so we can render things based on mouselook
 	if (player == &players[consoleplayer])
-		viewangle = localaiming;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		viewangle = localaiming2;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		viewangle = localaiming3;
-	else if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		viewangle = localaiming4;
+		viewangle = localaiming[0];
+	else if (splitscreen && player == &players[displayplayers[1]])
+		viewangle = localaiming[1];
+	else if (splitscreen > 1 && player == &players[displayplayers[2]])
+		viewangle = localaiming[2];
+	else if (splitscreen > 2 && player == &players[displayplayers[3]])
+		viewangle = localaiming[3];
 
 	// Handle stuff when you are looking farther up or down.
 	if ((aimingangle || cv_fov.value+player->fovadd > 90*FRACUNIT))
 	{
 		dup_viewangle += ANGLE_90;
 		HWR_ClearClipSegs();
-		HWR_RenderBSPNode((INT32)numnodes-1, ssplayer); //left
+		HWR_RenderBSPNode((INT32)numnodes-1); //left
 
 		dup_viewangle += ANGLE_90;
 		if (((INT32)aimingangle > ANGLE_45 || (INT32)aimingangle<-ANGLE_45))
 		{
 			HWR_ClearClipSegs();
-			HWR_RenderBSPNode((INT32)numnodes-1, ssplayer); //back
+			HWR_RenderBSPNode((INT32)numnodes-1); //back
 		}
 
 		dup_viewangle += ANGLE_90;
 		HWR_ClearClipSegs();
-		HWR_RenderBSPNode((INT32)numnodes-1, ssplayer); //right
+		HWR_RenderBSPNode((INT32)numnodes-1); //right
 
 		dup_viewangle += ANGLE_90;
 	}
@@ -6795,11 +6883,6 @@ static void HWR_RenderWall(wallVert3D   *wallVerts, FSurfaceInfo *pSurf, FBITFIE
 #endif
 }
 
-void HWR_SetPaletteColor(INT32 palcolor)
-{
-	HWD.pfnSetSpecialState(HWD_SET_PALETTECOLOR, palcolor);
-}
-
 INT32 HWR_GetTextureUsed(void)
 {
 	return HWD.pfnGetTextureUsed();
@@ -6807,16 +6890,17 @@ INT32 HWR_GetTextureUsed(void)
 
 void HWR_DoPostProcessor(player_t *player)
 {
-	postimg_t *type;
+	postimg_t *type = &postimgtype[0];
+	UINT8 i;
 
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		type = &postimgtype4;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		type = &postimgtype3;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		type = &postimgtype2;
-	else
-		type = &postimgtype;
+	for (i = splitscreen; i > 0; i--)
+	{
+		if (player == &players[displayplayers[i]])
+		{
+			type = &postimgtype[i];
+			break;
+		}
+	}
 
 	// Armageddon Blast Flash!
 	// Could this even be considered postprocessor?
@@ -6850,7 +6934,6 @@ void HWR_DoPostProcessor(player_t *player)
 	if (splitscreen) // Not supported in splitscreen - someone want to add support?
 		return;
 
-#ifdef SHUFFLE
 	// Drunken vision! WooOOooo~
 	if (*type == postimg_water || *type == postimg_heat)
 	{
@@ -6893,7 +6976,6 @@ void HWR_DoPostProcessor(player_t *player)
 			HWD.pfnMakeScreenTexture();
 	}
 	// Flipping of the screen isn't done here anymore
-#endif // SHUFFLE
 }
 
 void HWR_StartScreenWipe(void)
@@ -6940,7 +7022,7 @@ void HWR_DoWipe(UINT8 wipenum, UINT8 scrnnum)
 
 	HWR_GetFadeMask(lumpnum);
 
-	HWD.pfnDoScreenWipe(HWRWipeCounter); // Still send in wipecounter since old stuff might not support multitexturing
+	HWD.pfnDoScreenWipe();
 
 	HWRWipeCounter += 0.05f; // increase opacity of end screen
 
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index 6978856ea3a3b2d716f8332dd54c310f6f3a51cd..7bc361d9507c5e2145e8de7c4d14653f250ff1f3 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -58,7 +58,6 @@ void HWR_AddCommands(void);
 void HWR_CorrectSWTricks(void);
 void transform(float *cx, float *cy, float *cz);
 FBITFIELD HWR_TranstableToAlpha(INT32 transtablenum, FSurfaceInfo *pSurf);
-void HWR_SetPaletteColor(INT32 palcolor);
 INT32 HWR_GetTextureUsed(void);
 void HWR_DoPostProcessor(player_t *player);
 void HWR_StartScreenWipe(void);
@@ -80,7 +79,8 @@ extern consvar_t cv_grstaticlighting;
 extern consvar_t cv_grcoronas;
 extern consvar_t cv_grcoronasize;
 #endif
-extern consvar_t cv_grmd2;
+extern consvar_t cv_grmdls;
+extern consvar_t cv_grfallbackplayermodel;
 extern consvar_t cv_grfog;
 extern consvar_t cv_grfogcolor;
 extern consvar_t cv_grfogdensity;
@@ -91,9 +91,9 @@ extern consvar_t cv_grgammablue;
 extern consvar_t cv_grfiltermode;
 extern consvar_t cv_granisotropicmode;
 extern consvar_t cv_grcorrecttricks;
-extern consvar_t cv_voodoocompatibility;
 extern consvar_t cv_grfovchange;
 extern consvar_t cv_grsolvetjoin;
+extern consvar_t cv_grspritebillboarding;
 
 extern float gr_viewwidth, gr_viewheight, gr_baseviewwindowx, gr_baseviewwindowy;
 
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index e50c13652b36d3a53495c5069678b47c8c27cf6e..d217f4094526682e14a7e0b2e3e6f67b4fb081cc 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -43,6 +43,7 @@
 #include "../r_draw.h"
 #include "../p_tick.h"
 #include "../k_kart.h" // colortranslations
+#include "hw_model.h"
 
 #include "hw_main.h"
 #include "../v_video.h"
@@ -75,172 +76,6 @@
 #include "errno.h"
 #endif
 
-#define NUMVERTEXNORMALS 162
-float avertexnormals[NUMVERTEXNORMALS][3] = {
-{-0.525731f, 0.000000f, 0.850651f},
-{-0.442863f, 0.238856f, 0.864188f},
-{-0.295242f, 0.000000f, 0.955423f},
-{-0.309017f, 0.500000f, 0.809017f},
-{-0.162460f, 0.262866f, 0.951056f},
-{0.000000f, 0.000000f, 1.000000f},
-{0.000000f, 0.850651f, 0.525731f},
-{-0.147621f, 0.716567f, 0.681718f},
-{0.147621f, 0.716567f, 0.681718f},
-{0.000000f, 0.525731f, 0.850651f},
-{0.309017f, 0.500000f, 0.809017f},
-{0.525731f, 0.000000f, 0.850651f},
-{0.295242f, 0.000000f, 0.955423f},
-{0.442863f, 0.238856f, 0.864188f},
-{0.162460f, 0.262866f, 0.951056f},
-{-0.681718f, 0.147621f, 0.716567f},
-{-0.809017f, 0.309017f, 0.500000f},
-{-0.587785f, 0.425325f, 0.688191f},
-{-0.850651f, 0.525731f, 0.000000f},
-{-0.864188f, 0.442863f, 0.238856f},
-{-0.716567f, 0.681718f, 0.147621f},
-{-0.688191f, 0.587785f, 0.425325f},
-{-0.500000f, 0.809017f, 0.309017f},
-{-0.238856f, 0.864188f, 0.442863f},
-{-0.425325f, 0.688191f, 0.587785f},
-{-0.716567f, 0.681718f, -0.147621f},
-{-0.500000f, 0.809017f, -0.309017f},
-{-0.525731f, 0.850651f, 0.000000f},
-{0.000000f, 0.850651f, -0.525731f},
-{-0.238856f, 0.864188f, -0.442863f},
-{0.000000f, 0.955423f, -0.295242f},
-{-0.262866f, 0.951056f, -0.162460f},
-{0.000000f, 1.000000f, 0.000000f},
-{0.000000f, 0.955423f, 0.295242f},
-{-0.262866f, 0.951056f, 0.162460f},
-{0.238856f, 0.864188f, 0.442863f},
-{0.262866f, 0.951056f, 0.162460f},
-{0.500000f, 0.809017f, 0.309017f},
-{0.238856f, 0.864188f, -0.442863f},
-{0.262866f, 0.951056f, -0.162460f},
-{0.500000f, 0.809017f, -0.309017f},
-{0.850651f, 0.525731f, 0.000000f},
-{0.716567f, 0.681718f, 0.147621f},
-{0.716567f, 0.681718f, -0.147621f},
-{0.525731f, 0.850651f, 0.000000f},
-{0.425325f, 0.688191f, 0.587785f},
-{0.864188f, 0.442863f, 0.238856f},
-{0.688191f, 0.587785f, 0.425325f},
-{0.809017f, 0.309017f, 0.500000f},
-{0.681718f, 0.147621f, 0.716567f},
-{0.587785f, 0.425325f, 0.688191f},
-{0.955423f, 0.295242f, 0.000000f},
-{1.000000f, 0.000000f, 0.000000f},
-{0.951056f, 0.162460f, 0.262866f},
-{0.850651f, -0.525731f, 0.000000f},
-{0.955423f, -0.295242f, 0.000000f},
-{0.864188f, -0.442863f, 0.238856f},
-{0.951056f, -0.162460f, 0.262866f},
-{0.809017f, -0.309017f, 0.500000f},
-{0.681718f, -0.147621f, 0.716567f},
-{0.850651f, 0.000000f, 0.525731f},
-{0.864188f, 0.442863f, -0.238856f},
-{0.809017f, 0.309017f, -0.500000f},
-{0.951056f, 0.162460f, -0.262866f},
-{0.525731f, 0.000000f, -0.850651f},
-{0.681718f, 0.147621f, -0.716567f},
-{0.681718f, -0.147621f, -0.716567f},
-{0.850651f, 0.000000f, -0.525731f},
-{0.809017f, -0.309017f, -0.500000f},
-{0.864188f, -0.442863f, -0.238856f},
-{0.951056f, -0.162460f, -0.262866f},
-{0.147621f, 0.716567f, -0.681718f},
-{0.309017f, 0.500000f, -0.809017f},
-{0.425325f, 0.688191f, -0.587785f},
-{0.442863f, 0.238856f, -0.864188f},
-{0.587785f, 0.425325f, -0.688191f},
-{0.688191f, 0.587785f, -0.425325f},
-{-0.147621f, 0.716567f, -0.681718f},
-{-0.309017f, 0.500000f, -0.809017f},
-{0.000000f, 0.525731f, -0.850651f},
-{-0.525731f, 0.000000f, -0.850651f},
-{-0.442863f, 0.238856f, -0.864188f},
-{-0.295242f, 0.000000f, -0.955423f},
-{-0.162460f, 0.262866f, -0.951056f},
-{0.000000f, 0.000000f, -1.000000f},
-{0.295242f, 0.000000f, -0.955423f},
-{0.162460f, 0.262866f, -0.951056f},
-{-0.442863f, -0.238856f, -0.864188f},
-{-0.309017f, -0.500000f, -0.809017f},
-{-0.162460f, -0.262866f, -0.951056f},
-{0.000000f, -0.850651f, -0.525731f},
-{-0.147621f, -0.716567f, -0.681718f},
-{0.147621f, -0.716567f, -0.681718f},
-{0.000000f, -0.525731f, -0.850651f},
-{0.309017f, -0.500000f, -0.809017f},
-{0.442863f, -0.238856f, -0.864188f},
-{0.162460f, -0.262866f, -0.951056f},
-{0.238856f, -0.864188f, -0.442863f},
-{0.500000f, -0.809017f, -0.309017f},
-{0.425325f, -0.688191f, -0.587785f},
-{0.716567f, -0.681718f, -0.147621f},
-{0.688191f, -0.587785f, -0.425325f},
-{0.587785f, -0.425325f, -0.688191f},
-{0.000000f, -0.955423f, -0.295242f},
-{0.000000f, -1.000000f, 0.000000f},
-{0.262866f, -0.951056f, -0.162460f},
-{0.000000f, -0.850651f, 0.525731f},
-{0.000000f, -0.955423f, 0.295242f},
-{0.238856f, -0.864188f, 0.442863f},
-{0.262866f, -0.951056f, 0.162460f},
-{0.500000f, -0.809017f, 0.309017f},
-{0.716567f, -0.681718f, 0.147621f},
-{0.525731f, -0.850651f, 0.000000f},
-{-0.238856f, -0.864188f, -0.442863f},
-{-0.500000f, -0.809017f, -0.309017f},
-{-0.262866f, -0.951056f, -0.162460f},
-{-0.850651f, -0.525731f, 0.000000f},
-{-0.716567f, -0.681718f, -0.147621f},
-{-0.716567f, -0.681718f, 0.147621f},
-{-0.525731f, -0.850651f, 0.000000f},
-{-0.500000f, -0.809017f, 0.309017f},
-{-0.238856f, -0.864188f, 0.442863f},
-{-0.262866f, -0.951056f, 0.162460f},
-{-0.864188f, -0.442863f, 0.238856f},
-{-0.809017f, -0.309017f, 0.500000f},
-{-0.688191f, -0.587785f, 0.425325f},
-{-0.681718f, -0.147621f, 0.716567f},
-{-0.442863f, -0.238856f, 0.864188f},
-{-0.587785f, -0.425325f, 0.688191f},
-{-0.309017f, -0.500000f, 0.809017f},
-{-0.147621f, -0.716567f, 0.681718f},
-{-0.425325f, -0.688191f, 0.587785f},
-{-0.162460f, -0.262866f, 0.951056f},
-{0.442863f, -0.238856f, 0.864188f},
-{0.162460f, -0.262866f, 0.951056f},
-{0.309017f, -0.500000f, 0.809017f},
-{0.147621f, -0.716567f, 0.681718f},
-{0.000000f, -0.525731f, 0.850651f},
-{0.425325f, -0.688191f, 0.587785f},
-{0.587785f, -0.425325f, 0.688191f},
-{0.688191f, -0.587785f, 0.425325f},
-{-0.955423f, 0.295242f, 0.000000f},
-{-0.951056f, 0.162460f, 0.262866f},
-{-1.000000f, 0.000000f, 0.000000f},
-{-0.850651f, 0.000000f, 0.525731f},
-{-0.955423f, -0.295242f, 0.000000f},
-{-0.951056f, -0.162460f, 0.262866f},
-{-0.864188f, 0.442863f, -0.238856f},
-{-0.951056f, 0.162460f, -0.262866f},
-{-0.809017f, 0.309017f, -0.500000f},
-{-0.864188f, -0.442863f, -0.238856f},
-{-0.951056f, -0.162460f, -0.262866f},
-{-0.809017f, -0.309017f, -0.500000f},
-{-0.681718f, 0.147621f, -0.716567f},
-{-0.681718f, -0.147621f, -0.716567f},
-{-0.850651f, 0.000000f, -0.525731f},
-{-0.688191f, 0.587785f, -0.425325f},
-{-0.587785f, 0.425325f, -0.688191f},
-{-0.425325f, 0.688191f, -0.587785f},
-{-0.425325f, -0.688191f, -0.587785f},
-{-0.587785f, -0.425325f, -0.688191f},
-{-0.688191f, -0.587785f, -0.425325f},
-};
-
 md2_t md2_models[NUMSPRITES];
 md2_t md2_playermodels[MAXSKINS];
 
@@ -248,198 +83,29 @@ md2_t md2_playermodels[MAXSKINS];
 /*
  * free model
  */
-static void md2_freeModel (md2_model_t *model)
+#if 0
+static void md2_freeModel (model_t *model)
 {
-	if (model)
-	{
-		if (model->skins)
-			free(model->skins);
-
-		if (model->texCoords)
-			free(model->texCoords);
-
-		if (model->triangles)
-			free(model->triangles);
-
-		if (model->frames)
-		{
-			size_t i;
-
-			for (i = 0; i < model->header.numFrames; i++)
-			{
-				if (model->frames[i].vertices)
-					free(model->frames[i].vertices);
-			}
-			free(model->frames);
-		}
-
-		if (model->glCommandBuffer)
-			free(model->glCommandBuffer);
-
-		free(model);
-	}
+	UnloadModel(model);
 }
+#endif
 
 
 //
 // load model
 //
 // Hurdler: the current path is the Legacy.exe path
-static md2_model_t *md2_readModel(const char *filename)
+static model_t *md2_readModel(const char *filename)
 {
-	FILE *file;
-	md2_model_t *model;
-	UINT8 buffer[MD2_MAX_FRAMESIZE];
-	size_t i;
-
-	model = calloc(1, sizeof (*model));
-	if (model == NULL)
-		return 0;
-
 	//Filename checking fixed ~Monster Iestyn and Golden
-	file = fopen(va("%s"PATHSEP"%s", srb2home, filename), "rb");
-	if (!file)
-	{
-		file = fopen(va("%s"PATHSEP"%s", srb2path, filename), "rb");
-		if (!file)
-		{
-			free(model);
-			return 0;
-		}
-	}
-
-	// initialize model and read header
-
-	if (fread(&model->header, sizeof (model->header), 1, file) != 1
-		|| model->header.magic != MD2_IDENT
-		|| model->header.version != MD2_VERSION)
-	{
-		fclose(file);
-		free(model);
-		return 0;
-	}
-
-	model->header.numSkins = 1;
-
-#define MD2LIMITCHECK(field, max, msgname) \
-	if (field > max) \
-	{ \
-		CONS_Alert(CONS_ERROR, "md2_readModel: %s has too many " msgname " (# found: %d, maximum: %d)\n", filename, field, max); \
-		md2_freeModel (model); \
-		fclose(file); \
-		return 0; \
-	}
-
-	// Uncomment if these are actually needed
-//	MD2LIMITCHECK(model->header.numSkins,     MD2_MAX_SKINS,     "skins")
-//	MD2LIMITCHECK(model->header.numTexCoords, MD2_MAX_TEXCOORDS, "texture coordinates")
-	MD2LIMITCHECK(model->header.numTriangles, MD2_MAX_TRIANGLES, "triangles")
-	MD2LIMITCHECK(model->header.numFrames,    MD2_MAX_FRAMES,    "frames")
-	MD2LIMITCHECK(model->header.numVertices,  MD2_MAX_VERTICES,  "vertices")
-
-#undef MD2LIMITCHECK
-
-	// read skins
-	fseek(file, model->header.offsetSkins, SEEK_SET);
-	if (model->header.numSkins > 0)
-	{
-		model->skins = calloc(sizeof (md2_skin_t), model->header.numSkins);
-		if (!model->skins || model->header.numSkins !=
-			fread(model->skins, sizeof (md2_skin_t), model->header.numSkins, file))
-		{
-			md2_freeModel (model);
-			fclose(file);
-			return 0;
-		}
-	}
-
-	// read texture coordinates
-	fseek(file, model->header.offsetTexCoords, SEEK_SET);
-	if (model->header.numTexCoords > 0)
-	{
-		model->texCoords = calloc(sizeof (md2_textureCoordinate_t), model->header.numTexCoords);
-		if (!model->texCoords || model->header.numTexCoords !=
-			fread(model->texCoords, sizeof (md2_textureCoordinate_t), model->header.numTexCoords, file))
-		{
-			md2_freeModel (model);
-			fclose(file);
-			return 0;
-		}
-	}
-
-	// read triangles
-	fseek(file, model->header.offsetTriangles, SEEK_SET);
-	if (model->header.numTriangles > 0)
-	{
-		model->triangles = calloc(sizeof (md2_triangle_t), model->header.numTriangles);
-		if (!model->triangles || model->header.numTriangles !=
-			fread(model->triangles, sizeof (md2_triangle_t), model->header.numTriangles, file))
-		{
-			md2_freeModel (model);
-			fclose(file);
-			return 0;
-		}
-	}
-
-	// read alias frames
-	fseek(file, model->header.offsetFrames, SEEK_SET);
-	if (model->header.numFrames > 0)
-	{
-		model->frames = calloc(sizeof (md2_frame_t), model->header.numFrames);
-		if (!model->frames)
-		{
-			md2_freeModel (model);
-			fclose(file);
-			return 0;
-		}
-
-		for (i = 0; i < model->header.numFrames; i++)
-		{
-			md2_alias_frame_t *frame = (md2_alias_frame_t *)(void *)buffer;
-			size_t j;
-
-			model->frames[i].vertices = calloc(sizeof (md2_triangleVertex_t), model->header.numVertices);
-			if (!model->frames[i].vertices || model->header.frameSize !=
-				fread(frame, 1, model->header.frameSize, file))
-			{
-				md2_freeModel (model);
-				fclose(file);
-				return 0;
-			}
-
-			strcpy(model->frames[i].name, frame->name);
-			for (j = 0; j < model->header.numVertices; j++)
-			{
-				model->frames[i].vertices[j].vertex[0] = (float) ((INT32) frame->alias_vertices[j].vertex[0]) * frame->scale[0] + frame->translate[0];
-				model->frames[i].vertices[j].vertex[2] = -1* ((float) ((INT32) frame->alias_vertices[j].vertex[1]) * frame->scale[1] + frame->translate[1]);
-				model->frames[i].vertices[j].vertex[1] = (float) ((INT32) frame->alias_vertices[j].vertex[2]) * frame->scale[2] + frame->translate[2];
-				model->frames[i].vertices[j].normal[0] = avertexnormals[frame->alias_vertices[j].lightNormalIndex][0];
-				model->frames[i].vertices[j].normal[1] = avertexnormals[frame->alias_vertices[j].lightNormalIndex][1];
-				model->frames[i].vertices[j].normal[2] = avertexnormals[frame->alias_vertices[j].lightNormalIndex][2];
-			}
-		}
-	}
-
-	// read gl commands
-	fseek(file, model->header.offsetGlCommands, SEEK_SET);
-	if (model->header.numGlCommands)
-	{
-		model->glCommandBuffer = calloc(sizeof (INT32), model->header.numGlCommands);
-		if (!model->glCommandBuffer || model->header.numGlCommands !=
-			fread(model->glCommandBuffer, sizeof (INT32), model->header.numGlCommands, file))
-		{
-			md2_freeModel (model);
-			fclose(file);
-			return 0;
-		}
-	}
-
-	fclose(file);
-
-	return model;
+	if (FIL_FileExists(va("%s"PATHSEP"%s", srb2home, filename)))
+		return LoadModel(va("%s"PATHSEP"%s", srb2home, filename), PU_STATIC);
+	else if (FIL_FileExists(va("%s"PATHSEP"%s", srb2path, filename)))
+		return LoadModel(va("%s"PATHSEP"%s", srb2path, filename), PU_STATIC);
+	return NULL;
 }
 
-static inline void md2_printModelInfo (md2_model_t *model)
+static inline void md2_printModelInfo (model_t *model)
 {
 #if 0
 	INT32 i;
@@ -498,13 +164,13 @@ static GrTextureFormat_t PNG_Load(const char *filename, int *w, int *h, GLPatch_
 #endif
 	volatile png_FILE_p png_FILE;
 	//Filename checking fixed ~Monster Iestyn and Golden
-	char *pngfilename = va("%s"PATHSEP"md2"PATHSEP"%s", srb2home, filename);
+	char *pngfilename = va("%s"PATHSEP"mdls"PATHSEP"%s", srb2home, filename);
 
 	FIL_ForceExtension(pngfilename, ".png");
 	png_FILE = fopen(pngfilename, "rb");
 	if (!png_FILE)
 	{
-		pngfilename = va("%s"PATHSEP"md2"PATHSEP"%s", srb2path, filename);
+		pngfilename = va("%s"PATHSEP"mdls"PATHSEP"%s", srb2path, filename);
 		FIL_ForceExtension(pngfilename, ".png");
 		png_FILE = fopen(pngfilename, "rb");
 		//CONS_Debug(DBG_RENDER, "M_SavePNG: Error on opening %s for loading\n", filename);
@@ -631,13 +297,13 @@ static GrTextureFormat_t PCX_Load(const char *filename, int *w, int *h,
 	INT32 ch, rep;
 	FILE *file;
 	//Filename checking fixed ~Monster Iestyn and Golden
-	char *pcxfilename = va("%s"PATHSEP"md2"PATHSEP"%s", srb2home, filename);
+	char *pcxfilename = va("%s"PATHSEP"mdls"PATHSEP"%s", srb2home, filename);
 
 	FIL_ForceExtension(pcxfilename, ".pcx");
 	file = fopen(pcxfilename, "rb");
 	if (!file)
 	{
-		pcxfilename = va("%s"PATHSEP"md2"PATHSEP"%s", srb2path, filename);
+		pcxfilename = va("%s"PATHSEP"mdls"PATHSEP"%s", srb2path, filename);
 		FIL_ForceExtension(pcxfilename, ".pcx");
 		file = fopen(pcxfilename, "rb");
 		if (!file)
@@ -826,16 +492,16 @@ void HWR_InitMD2(void)
 		md2_models[i].error = false;
 	}
 
-	// read the md2.dat file
+	// read the mdls.dat file
 	//Filename checking fixed ~Monster Iestyn and Golden
-	f = fopen(va("%s"PATHSEP"%s", srb2home, "kmd2.dat"), "rt");
+	f = fopen(va("%s"PATHSEP"%s", srb2home, "mdls.dat"), "rt");
 
 	if (!f)
 	{
-		f = fopen(va("%s"PATHSEP"%s", srb2path, "kmd2.dat"), "rt");
+		f = fopen(va("%s"PATHSEP"%s", srb2path, "mdls.dat"), "rt");
 		if (!f)
 		{
-			CONS_Printf("%s %s\n", M_GetText("Error while loading kmd2.dat:"), strerror(errno));
+			CONS_Printf("%s %s\n", M_GetText("Error while loading mdls.dat:"), strerror(errno));
 			nomd2s = true;
 			return;
 		}
@@ -844,7 +510,7 @@ void HWR_InitMD2(void)
 	{
 		/*if (stricmp(name, "PLAY") == 0)
 		{
-			CONS_Printf("MD2 for sprite PLAY detected in kmd2.dat, use a player skin instead!\n");
+			CONS_Printf("MD2 for sprite PLAY detected in mdls.dat, use a player skin instead!\n");
 			continue;
 		}*/
 		// 8/1/19: Allow PLAY to load for default MD2.
@@ -879,7 +545,7 @@ void HWR_InitMD2(void)
 			}
 		}
 		// no sprite/player skin name found?!?
-		CONS_Printf("Unknown sprite/player skin %s detected in kmd2.dat\n", name);
+		CONS_Printf("Unknown sprite/player skin %s detected in mdls.dat\n", name);
 md2found:
 		// move on to next line...
 		continue;
@@ -898,16 +564,16 @@ void HWR_AddPlayerMD2(int skin) // For MD2's that were added after startup
 
 	CONS_Printf("AddPlayerMD2()...\n");
 
-	// read the md2.dat file
+	// read the mdls.dat file
 	//Filename checking fixed ~Monster Iestyn and Golden
-	f = fopen(va("%s"PATHSEP"%s", srb2home, "kmd2.dat"), "rt");
+	f = fopen(va("%s"PATHSEP"%s", srb2home, "mdls.dat"), "rt");
 
 	if (!f)
 	{
-		f = fopen(va("%s"PATHSEP"%s", srb2path, "kmd2.dat"), "rt");
+		f = fopen(va("%s"PATHSEP"%s", srb2path, "mdls.dat"), "rt");
 		if (!f)
 		{
-			CONS_Printf("%s %s\n", M_GetText("Error while loading kmd2.dat:"), strerror(errno));
+			CONS_Printf("%s %s\n", M_GetText("Error while loading mdls.dat:"), strerror(errno));
 			nomd2s = true;
 			return;
 		}
@@ -937,7 +603,7 @@ playermd2found:
 void HWR_AddSpriteMD2(size_t spritenum) // For MD2s that were added after startup
 {
 	FILE *f;
-	// name[18] is used to check for names in the kmd2.dat file that match with sprites or player skins
+	// name[18] is used to check for names in the mdls.dat file that match with sprites or player skins
 	// sprite names are always 4 characters long, and names is for player skins can be up to 19 characters long
 	char name[18], filename[32];
 	float scale, offset;
@@ -950,20 +616,20 @@ void HWR_AddSpriteMD2(size_t spritenum) // For MD2s that were added after startu
 
 	// Read the md2.dat file
 	//Filename checking fixed ~Monster Iestyn and Golden
-	f = fopen(va("%s"PATHSEP"%s", srb2home, "kmd2.dat"), "rt");
+	f = fopen(va("%s"PATHSEP"%s", srb2home, "mdls.dat"), "rt");
 
 	if (!f)
 	{
-		f = fopen(va("%s"PATHSEP"%s", srb2path, "kmd2.dat"), "rt");
+		f = fopen(va("%s"PATHSEP"%s", srb2path, "mdls.dat"), "rt");
 		if (!f)
 		{
-			CONS_Printf("%s %s\n", M_GetText("Error while loading kmd2.dat:"), strerror(errno));
+			CONS_Printf("%s %s\n", M_GetText("Error while loading mdls.dat:"), strerror(errno));
 			nomd2s = true;
 			return;
 		}
 	}
 
-	// Check for any MD2s that match the names of player skins!
+	// Check for any MD2s that match the names of sprite names!
 	while (fscanf(f, "%19s %31s %f %f", name, filename, &scale, &offset) == 4)
 	{
 		if (stricmp(name, sprnames[spritenum]) == 0)
@@ -1195,12 +861,13 @@ void HWR_DrawMD2(gr_vissprite_t *spr)
 	FSurfaceInfo Surf;
 
 	char filename[64];
-	INT32 frame;
+	INT32 frame = 0;
+	INT32 nextFrame = -1;
 	FTransform p;
 	md2_t *md2;
 	UINT8 color[4];
 
-	if (!cv_grmd2.value)
+	if (!cv_grmdls.value)
 		return;
 
 	if (spr->precip)
@@ -1248,10 +915,9 @@ void HWR_DrawMD2(gr_vissprite_t *spr)
 	// Look at HWR_ProjectSprite for more
 	{
 		GLPatch_t *gpatch;
-		INT32 *buff;
 		INT32 durs = spr->mobj->state->tics;
 		INT32 tics = spr->mobj->tics;
-		md2_frame_t *curr, *next = NULL;
+		//mdlframe_t *next = NULL;
 		const UINT8 flip = (UINT8)((spr->mobj->eflags & MFE_VERTICALFLIP) == MFE_VERTICALFLIP);
 		spritedef_t *sprdef;
 		spriteframe_t *sprframe;
@@ -1285,13 +951,14 @@ void HWR_DrawMD2(gr_vissprite_t *spr)
 			return; // we already failed loading this before :(
 		if (!md2->model)
 		{
-			CONS_Debug(DBG_RENDER, "Loading MD2... (%s, %s)", sprnames[spr->mobj->sprite], md2->filename);
-			sprintf(filename, "md2/%s", md2->filename);
+			CONS_Debug(DBG_RENDER, "Loading model... (%s, %s)", sprnames[spr->mobj->sprite], md2->filename);
+			sprintf(filename, "mdls/%s", md2->filename);
 			md2->model = md2_readModel(filename);
 
 			if (md2->model)
 			{
 				md2_printModelInfo(md2->model);
+				HWD.pfnCreateModelVBOs(md2->model);
 			}
 			else
 			{
@@ -1364,27 +1031,27 @@ void HWR_DrawMD2(gr_vissprite_t *spr)
 		}
 
 		//FIXME: this is not yet correct
-		frame = (spr->mobj->frame & FF_FRAMEMASK) % md2->model->header.numFrames;
-		buff = md2->model->glCommandBuffer;
-		curr = &md2->model->frames[frame];
-#if 0
-		if (cv_grmd2.value == 1 && tics <= durs)
+		frame = (spr->mobj->frame & FF_FRAMEMASK) % md2->model->meshes[0].numFrames;
+
+#ifdef USE_MODEL_NEXTFRAME
+		if (cv_grmdls.value == 1 && tics <= durs)
 		{
 			// frames are handled differently for states with FF_ANIMATE, so get the next frame differently for the interpolation
 			if (spr->mobj->frame & FF_ANIMATE)
 			{
-				UINT32 nextframe = (spr->mobj->frame & FF_FRAMEMASK) + 1;
-				if (nextframe >= (UINT32)spr->mobj->state->var1)
-					nextframe = (spr->mobj->state->frame & FF_FRAMEMASK);
-				nextframe %= md2->model->header.numFrames;
-				next = &md2->model->frames[nextframe];
+				nextFrame = (spr->mobj->frame & FF_FRAMEMASK) + 1;
+				if (nextFrame >= spr->mobj->state->var1)
+					nextFrame = (spr->mobj->state->frame & FF_FRAMEMASK);
+				nextFrame %= md2->model->meshes[0].numFrames;
+				//next = &md2->model->meshes[0].frames[nextFrame];
 			}
 			else
 			{
-				if (spr->mobj->state->nextstate != S_NULL && states[spr->mobj->state->nextstate].sprite != SPR_NULL)
+				if (spr->mobj->state->nextstate != S_NULL && states[spr->mobj->state->nextstate].sprite != SPR_NULL
+					&& !(spr->mobj->player && (spr->mobj->state->nextstate == S_PLAY_TAP1 || spr->mobj->state->nextstate == S_PLAY_TAP2) && spr->mobj->state == &states[S_PLAY_STND]))
 				{
-					const UINT32 nextframe = (states[spr->mobj->state->nextstate].frame & FF_FRAMEMASK) % md2->model->header.numFrames;
-					next = &md2->model->frames[nextframe];
+					nextFrame = (states[spr->mobj->state->nextstate].frame & FF_FRAMEMASK) % md2->model->meshes[0].numFrames;
+					//next = &md2->model->meshes[0].frames[nextFrame];
 				}
 			}
 		}
@@ -1421,6 +1088,8 @@ void HWR_DrawMD2(gr_vissprite_t *spr)
 			p.angley = FIXED_TO_FLOAT(anglef);
 		}
 		p.anglex = 0.0f;
+#ifdef USE_FTRANSFORM_ANGLEZ
+		// Slope rotation from Kart
 		p.anglez = 0.0f;
 		if (spr->mobj->standingslope)
 		{
@@ -1432,7 +1101,7 @@ void HWR_DrawMD2(gr_vissprite_t *spr)
 			tempangle = -AngleFixed(R_PointToAngle2(0, 0, tempz, tempy));
 			p.anglex = FIXED_TO_FLOAT(tempangle);
 		}
-
+#endif
 
 		color[0] = Surf.FlatColor.s.red;
 		color[1] = Surf.FlatColor.s.green;
@@ -1443,9 +1112,11 @@ void HWR_DrawMD2(gr_vissprite_t *spr)
 		finalscale *= FIXED_TO_FLOAT(spr->mobj->scale);
 
 		p.flip = atransform.flip;
-		p.mirror = atransform.mirror;
+#ifdef USE_FTRANSFORM_MIRROR
+		p.mirror = atransform.mirror; // from Kart
+#endif
 
-		HWD.pfnDrawMD2i(buff, curr, durs, tics, next, &p, finalscale, flip, color);
+		HWD.pfnDrawModel(md2->model, frame, durs, tics, nextFrame, &p, finalscale, flip, color);
 	}
 }
 
diff --git a/src/hardware/hw_md2.h b/src/hardware/hw_md2.h
index ca43c7b4f44ad2e9539d116dcf6a1e2590210a75..57d8026b910451da53594cd8296f2917312aaf73 100644
--- a/src/hardware/hw_md2.h
+++ b/src/hardware/hw_md2.h
@@ -22,97 +22,7 @@
 #define _HW_MD2_H_
 
 #include "hw_glob.h"
-
-// magic number "IDP2" or 844121161
-#define MD2_IDENT                       (INT32)(('2' << 24) + ('P' << 16) + ('D' << 8) + 'I')
-// model version
-#define MD2_VERSION                     8
-
-#define MD2_MAX_TRIANGLES              16384
-#define MD2_MAX_VERTICES                4096
-#define MD2_MAX_TEXCOORDS               4096
-#define MD2_MAX_FRAMES                  512
-#define MD2_MAX_SKINS                   32
-#define MD2_MAX_FRAMESIZE               (MD2_MAX_VERTICES * 4 + 128)
-
-#if defined(_MSC_VER)
-#pragma pack(1)
-#endif
-typedef struct
-{
-	UINT32 magic;
-	UINT32 version;
-	UINT32 skinWidth;
-	UINT32 skinHeight;
-	UINT32 frameSize;
-	UINT32 numSkins;
-	UINT32 numVertices;
-	UINT32 numTexCoords;
-	UINT32 numTriangles;
-	UINT32 numGlCommands;
-	UINT32 numFrames;
-	UINT32 offsetSkins;
-	UINT32 offsetTexCoords;
-	UINT32 offsetTriangles;
-	UINT32 offsetFrames;
-	UINT32 offsetGlCommands;
-	UINT32 offsetEnd;
-} ATTRPACK md2_header_t; //NOTE: each of md2_header's members are 4 unsigned bytes
-
-typedef struct
-{
-	UINT8 vertex[3];
-	UINT8 lightNormalIndex;
-} ATTRPACK md2_alias_triangleVertex_t;
-
-typedef struct
-{
-	float vertex[3];
-	float normal[3];
-} ATTRPACK md2_triangleVertex_t;
-
-typedef struct
-{
-	INT16 vertexIndices[3];
-	INT16 textureIndices[3];
-} ATTRPACK md2_triangle_t;
-
-typedef struct
-{
-	INT16 s, t;
-} ATTRPACK md2_textureCoordinate_t;
-
-typedef struct
-{
-	float scale[3];
-	float translate[3];
-	char name[16];
-	md2_alias_triangleVertex_t alias_vertices[1];
-} ATTRPACK md2_alias_frame_t;
-
-typedef struct
-{
-	char name[16];
-	md2_triangleVertex_t *vertices;
-} ATTRPACK md2_frame_t;
-
-typedef char md2_skin_t[64];
-
-typedef struct
-{
-	float s, t;
-	INT32 vertexIndex;
-} ATTRPACK md2_glCommandVertex_t;
-
-typedef struct
-{
-	md2_header_t            header;
-	md2_skin_t              *skins;
-	md2_textureCoordinate_t *texCoords;
-	md2_triangle_t          *triangles;
-	md2_frame_t             *frames;
-	INT32                     *glCommandBuffer;
-} ATTRPACK md2_model_t;
+#include "hw_model.h"
 
 #if defined(_MSC_VER)
 #pragma pack()
@@ -123,7 +33,7 @@ typedef struct
 	char        filename[32];
 	float       scale;
 	float       offset;
-	md2_model_t *model;
+	model_t     *model;
 	void        *grpatch;
 	void        *blendgrpatch;
 	boolean     notfound;
diff --git a/src/hardware/hw_md2load.c b/src/hardware/hw_md2load.c
new file mode 100644
index 0000000000000000000000000000000000000000..3805deffb0657651a7157b753632ad7841259098
--- /dev/null
+++ b/src/hardware/hw_md2load.c
@@ -0,0 +1,564 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include "../doomdef.h"
+#include "hw_md2load.h"
+#include "hw_model.h"
+#include "../z_zone.h"
+
+#define NUMVERTEXNORMALS 162
+
+// Quake 2 normals are indexed. Use avertexnormals[normalindex][x/y/z] and
+// you'll have your normals.
+float avertexnormals[NUMVERTEXNORMALS][3] = {
+{-0.525731f, 0.000000f, 0.850651f},
+{-0.442863f, 0.238856f, 0.864188f},
+{-0.295242f, 0.000000f, 0.955423f},
+{-0.309017f, 0.500000f, 0.809017f},
+{-0.162460f, 0.262866f, 0.951056f},
+{0.000000f, 0.000000f, 1.000000f},
+{0.000000f, 0.850651f, 0.525731f},
+{-0.147621f, 0.716567f, 0.681718f},
+{0.147621f, 0.716567f, 0.681718f},
+{0.000000f, 0.525731f, 0.850651f},
+{0.309017f, 0.500000f, 0.809017f},
+{0.525731f, 0.000000f, 0.850651f},
+{0.295242f, 0.000000f, 0.955423f},
+{0.442863f, 0.238856f, 0.864188f},
+{0.162460f, 0.262866f, 0.951056f},
+{-0.681718f, 0.147621f, 0.716567f},
+{-0.809017f, 0.309017f, 0.500000f},
+{-0.587785f, 0.425325f, 0.688191f},
+{-0.850651f, 0.525731f, 0.000000f},
+{-0.864188f, 0.442863f, 0.238856f},
+{-0.716567f, 0.681718f, 0.147621f},
+{-0.688191f, 0.587785f, 0.425325f},
+{-0.500000f, 0.809017f, 0.309017f},
+{-0.238856f, 0.864188f, 0.442863f},
+{-0.425325f, 0.688191f, 0.587785f},
+{-0.716567f, 0.681718f, -0.147621f},
+{-0.500000f, 0.809017f, -0.309017f},
+{-0.525731f, 0.850651f, 0.000000f},
+{0.000000f, 0.850651f, -0.525731f},
+{-0.238856f, 0.864188f, -0.442863f},
+{0.000000f, 0.955423f, -0.295242f},
+{-0.262866f, 0.951056f, -0.162460f},
+{0.000000f, 1.000000f, 0.000000f},
+{0.000000f, 0.955423f, 0.295242f},
+{-0.262866f, 0.951056f, 0.162460f},
+{0.238856f, 0.864188f, 0.442863f},
+{0.262866f, 0.951056f, 0.162460f},
+{0.500000f, 0.809017f, 0.309017f},
+{0.238856f, 0.864188f, -0.442863f},
+{0.262866f, 0.951056f, -0.162460f},
+{0.500000f, 0.809017f, -0.309017f},
+{0.850651f, 0.525731f, 0.000000f},
+{0.716567f, 0.681718f, 0.147621f},
+{0.716567f, 0.681718f, -0.147621f},
+{0.525731f, 0.850651f, 0.000000f},
+{0.425325f, 0.688191f, 0.587785f},
+{0.864188f, 0.442863f, 0.238856f},
+{0.688191f, 0.587785f, 0.425325f},
+{0.809017f, 0.309017f, 0.500000f},
+{0.681718f, 0.147621f, 0.716567f},
+{0.587785f, 0.425325f, 0.688191f},
+{0.955423f, 0.295242f, 0.000000f},
+{1.000000f, 0.000000f, 0.000000f},
+{0.951056f, 0.162460f, 0.262866f},
+{0.850651f, -0.525731f, 0.000000f},
+{0.955423f, -0.295242f, 0.000000f},
+{0.864188f, -0.442863f, 0.238856f},
+{0.951056f, -0.162460f, 0.262866f},
+{0.809017f, -0.309017f, 0.500000f},
+{0.681718f, -0.147621f, 0.716567f},
+{0.850651f, 0.000000f, 0.525731f},
+{0.864188f, 0.442863f, -0.238856f},
+{0.809017f, 0.309017f, -0.500000f},
+{0.951056f, 0.162460f, -0.262866f},
+{0.525731f, 0.000000f, -0.850651f},
+{0.681718f, 0.147621f, -0.716567f},
+{0.681718f, -0.147621f, -0.716567f},
+{0.850651f, 0.000000f, -0.525731f},
+{0.809017f, -0.309017f, -0.500000f},
+{0.864188f, -0.442863f, -0.238856f},
+{0.951056f, -0.162460f, -0.262866f},
+{0.147621f, 0.716567f, -0.681718f},
+{0.309017f, 0.500000f, -0.809017f},
+{0.425325f, 0.688191f, -0.587785f},
+{0.442863f, 0.238856f, -0.864188f},
+{0.587785f, 0.425325f, -0.688191f},
+{0.688191f, 0.587785f, -0.425325f},
+{-0.147621f, 0.716567f, -0.681718f},
+{-0.309017f, 0.500000f, -0.809017f},
+{0.000000f, 0.525731f, -0.850651f},
+{-0.525731f, 0.000000f, -0.850651f},
+{-0.442863f, 0.238856f, -0.864188f},
+{-0.295242f, 0.000000f, -0.955423f},
+{-0.162460f, 0.262866f, -0.951056f},
+{0.000000f, 0.000000f, -1.000000f},
+{0.295242f, 0.000000f, -0.955423f},
+{0.162460f, 0.262866f, -0.951056f},
+{-0.442863f, -0.238856f, -0.864188f},
+{-0.309017f, -0.500000f, -0.809017f},
+{-0.162460f, -0.262866f, -0.951056f},
+{0.000000f, -0.850651f, -0.525731f},
+{-0.147621f, -0.716567f, -0.681718f},
+{0.147621f, -0.716567f, -0.681718f},
+{0.000000f, -0.525731f, -0.850651f},
+{0.309017f, -0.500000f, -0.809017f},
+{0.442863f, -0.238856f, -0.864188f},
+{0.162460f, -0.262866f, -0.951056f},
+{0.238856f, -0.864188f, -0.442863f},
+{0.500000f, -0.809017f, -0.309017f},
+{0.425325f, -0.688191f, -0.587785f},
+{0.716567f, -0.681718f, -0.147621f},
+{0.688191f, -0.587785f, -0.425325f},
+{0.587785f, -0.425325f, -0.688191f},
+{0.000000f, -0.955423f, -0.295242f},
+{0.000000f, -1.000000f, 0.000000f},
+{0.262866f, -0.951056f, -0.162460f},
+{0.000000f, -0.850651f, 0.525731f},
+{0.000000f, -0.955423f, 0.295242f},
+{0.238856f, -0.864188f, 0.442863f},
+{0.262866f, -0.951056f, 0.162460f},
+{0.500000f, -0.809017f, 0.309017f},
+{0.716567f, -0.681718f, 0.147621f},
+{0.525731f, -0.850651f, 0.000000f},
+{-0.238856f, -0.864188f, -0.442863f},
+{-0.500000f, -0.809017f, -0.309017f},
+{-0.262866f, -0.951056f, -0.162460f},
+{-0.850651f, -0.525731f, 0.000000f},
+{-0.716567f, -0.681718f, -0.147621f},
+{-0.716567f, -0.681718f, 0.147621f},
+{-0.525731f, -0.850651f, 0.000000f},
+{-0.500000f, -0.809017f, 0.309017f},
+{-0.238856f, -0.864188f, 0.442863f},
+{-0.262866f, -0.951056f, 0.162460f},
+{-0.864188f, -0.442863f, 0.238856f},
+{-0.809017f, -0.309017f, 0.500000f},
+{-0.688191f, -0.587785f, 0.425325f},
+{-0.681718f, -0.147621f, 0.716567f},
+{-0.442863f, -0.238856f, 0.864188f},
+{-0.587785f, -0.425325f, 0.688191f},
+{-0.309017f, -0.500000f, 0.809017f},
+{-0.147621f, -0.716567f, 0.681718f},
+{-0.425325f, -0.688191f, 0.587785f},
+{-0.162460f, -0.262866f, 0.951056f},
+{0.442863f, -0.238856f, 0.864188f},
+{0.162460f, -0.262866f, 0.951056f},
+{0.309017f, -0.500000f, 0.809017f},
+{0.147621f, -0.716567f, 0.681718f},
+{0.000000f, -0.525731f, 0.850651f},
+{0.425325f, -0.688191f, 0.587785f},
+{0.587785f, -0.425325f, 0.688191f},
+{0.688191f, -0.587785f, 0.425325f},
+{-0.955423f, 0.295242f, 0.000000f},
+{-0.951056f, 0.162460f, 0.262866f},
+{-1.000000f, 0.000000f, 0.000000f},
+{-0.850651f, 0.000000f, 0.525731f},
+{-0.955423f, -0.295242f, 0.000000f},
+{-0.951056f, -0.162460f, 0.262866f},
+{-0.864188f, 0.442863f, -0.238856f},
+{-0.951056f, 0.162460f, -0.262866f},
+{-0.809017f, 0.309017f, -0.500000f},
+{-0.864188f, -0.442863f, -0.238856f},
+{-0.951056f, -0.162460f, -0.262866f},
+{-0.809017f, -0.309017f, -0.500000f},
+{-0.681718f, 0.147621f, -0.716567f},
+{-0.681718f, -0.147621f, -0.716567f},
+{-0.850651f, 0.000000f, -0.525731f},
+{-0.688191f, 0.587785f, -0.425325f},
+{-0.587785f, 0.425325f, -0.688191f},
+{-0.425325f, 0.688191f, -0.587785f},
+{-0.425325f, -0.688191f, -0.587785f},
+{-0.587785f, -0.425325f, -0.688191f},
+{-0.688191f, -0.587785f, -0.425325f},
+};
+
+typedef struct
+{
+	int ident;        // A "magic number" that's used to identify the .md2 file
+	int version;      // The version of the file, always 8
+	int skinwidth;    // Width of the skin(s) in pixels
+	int skinheight;   // Height of the skin(s) in pixels
+	int framesize;    // Size of each frame in bytes
+	int numSkins;     // Number of skins with the model
+	int numXYZ;       // Number of vertices in each frame
+	int numST;        // Number of texture coordinates in each frame.
+	int numTris;      // Number of triangles in each frame
+	int numGLcmds;    // Number of dwords (4 bytes) in the gl command list.
+	int numFrames;    // Number of frames
+	int offsetSkins;  // Offset, in bytes from the start of the file, to the list of skin names.
+	int offsetST;     // Offset, in bytes from the start of the file, to the list of texture coordinates
+	int offsetTris;   // Offset, in bytes from the start of the file, to the list of triangles
+	int offsetFrames; // Offset, in bytes from the start of the file, to the list of frames
+	int offsetGLcmds; // Offset, in bytes from the start of the file, to the list of gl commands
+	int offsetEnd;    // Offset, in bytes from the start of the file, to the end of the file (filesize)
+} md2header_t;
+
+typedef struct
+{
+	unsigned short meshIndex[3]; // indices into the array of vertices in each frames
+	unsigned short stIndex[3];   // indices into the array of texture coordinates
+} md2triangle_t;
+
+typedef struct
+{
+	short s;
+	short t;
+} md2texcoord_t;
+
+typedef struct
+{
+	unsigned char v[3];             // Scaled vertices. You'll need to multiply them with scale[x] to make them normal.
+	unsigned char lightNormalIndex; // Index to the array of normals
+} md2vertex_t;
+
+typedef struct
+{
+	float scale[3];      // Used by the v member in the md2framePoint structure
+	float translate[3];  // Used by the v member in the md2framePoint structure
+	char name[16];       // Name of the frame
+} md2frame_t;
+
+// Load the model
+model_t *MD2_LoadModel(const char *fileName, int ztag, boolean useFloat)
+{
+	FILE *f;
+
+	model_t *retModel = NULL;
+	md2header_t *header;
+
+	size_t fileLen;
+	int i, j;
+	size_t namelen;
+	char *texturefilename;
+	const char *texPos;
+
+	char *buffer;
+
+	const float WUNITS = 1.0f;
+	float dataScale = WUNITS;
+
+	md2triangle_t *tris;
+	md2texcoord_t *texcoords;
+	md2frame_t *frames;
+
+	int t;
+
+	// MD2 currently does not work with tinyframes, so force useFloat = true
+	//
+	// <SSNTails>
+	// the UV coordinates in MD2 are not compatible with glDrawElements like MD3 is. So they need to be loaded as full float.
+	//
+	// MD2 is intended to be draw in triangle strips and fans
+	// not very compatible with a modern GL implementation, either
+	// so the idea would be to full float expand it, and put it in a vertex buffer object
+	// I'm sure there's a way to convert the UVs to 'tinyframes', but maybe that's a job for someone else.
+	// You'd have to decompress the model, then recompress, reindexing the triangles and weeding out duplicate coordinates
+	// I already have the decompression work done
+
+	useFloat = true;
+
+	f = fopen(fileName, "rb");
+
+	if (!f)
+		return NULL;
+
+	retModel = (model_t*)Z_Calloc(sizeof(model_t), ztag, 0);
+
+	//size_t fileLen;
+
+	//int i, j;
+
+	//size_t namelen;
+	//char *texturefilename;
+	texPos = strchr(fileName, '/');
+
+	if (texPos)
+	{
+		texPos++;
+		namelen = strlen(texPos) + 1;
+		texturefilename = (char*)Z_Malloc(namelen, PU_CACHE, 0);
+		strcpy(texturefilename, texPos);
+	}
+	else
+	{
+		namelen = strlen(fileName) + 1;
+		texturefilename = (char*)Z_Malloc(namelen, PU_CACHE, 0);
+		strcpy(texturefilename, fileName);
+	}
+
+	texturefilename[namelen - 2] = 'z';
+	texturefilename[namelen - 3] = 'u';
+	texturefilename[namelen - 4] = 'b';
+
+	// find length of file
+	fseek(f, 0, SEEK_END);
+	fileLen = ftell(f);
+	fseek(f, 0, SEEK_SET);
+
+	// read in file
+	buffer = malloc(fileLen);
+	if (fread(buffer, fileLen, 1, f)) { } // squash ignored fread error
+	fclose(f);
+
+	// get pointer to file header
+	header = (md2header_t*)buffer;
+
+	retModel->numMeshes = 1; // MD2 only has one mesh
+	retModel->meshes = (mesh_t*)Z_Calloc(sizeof(mesh_t) * retModel->numMeshes, ztag, 0);
+	retModel->meshes[0].numFrames = header->numFrames;
+	// const float WUNITS = 1.0f;
+	// float dataScale = WUNITS;
+
+	// Tris and ST are simple structures that can be straight-copied
+	tris = (md2triangle_t*)&buffer[header->offsetTris];
+	texcoords = (md2texcoord_t*)&buffer[header->offsetST];
+	frames = (md2frame_t*)&buffer[header->offsetFrames];
+
+	// Read in textures
+	retModel->numMaterials = header->numSkins;
+
+	if (retModel->numMaterials <= 0) // Always at least one skin, duh
+		retModel->numMaterials = 1;
+
+	retModel->materials = (material_t*)Z_Calloc(sizeof(material_t)*retModel->numMaterials, ztag, 0);
+
+	// int t;
+	for (t = 0; t < retModel->numMaterials; t++)
+	{
+		retModel->materials[t].ambient[0] = 0.8f;
+		retModel->materials[t].ambient[1] = 0.8f;
+		retModel->materials[t].ambient[2] = 0.8f;
+		retModel->materials[t].ambient[3] = 1.0f;
+		retModel->materials[t].diffuse[0] = 0.8f;
+		retModel->materials[t].diffuse[1] = 0.8f;
+		retModel->materials[t].diffuse[2] = 0.8f;
+		retModel->materials[t].diffuse[3] = 1.0f;
+		retModel->materials[t].emissive[0] = 0.0f;
+		retModel->materials[t].emissive[1] = 0.0f;
+		retModel->materials[t].emissive[2] = 0.0f;
+		retModel->materials[t].emissive[3] = 1.0f;
+		retModel->materials[t].specular[0] = 0.0f;
+		retModel->materials[t].specular[1] = 0.0f;
+		retModel->materials[t].specular[2] = 0.0f;
+		retModel->materials[t].specular[3] = 1.0f;
+		retModel->materials[t].shininess = 0.0f;
+		retModel->materials[t].spheremap = false;
+
+		/*		retModel->materials[t].texture = Texture::ReadTexture((char*)texturefilename, ZT_TEXTURE);
+
+				if (!systemSucks)
+				{
+					// Check for a normal map...??
+					char openfilename[1024];
+					char normalMapName[1024];
+					strcpy(normalMapName, texturefilename);
+					size_t len = strlen(normalMapName);
+					char *ptr = &normalMapName[len];
+					ptr--; // z
+					ptr--; // u
+					ptr--; // b
+					ptr--; // .
+					*ptr++ = '_';
+					*ptr++ = 'n';
+					*ptr++ = '.';
+					*ptr++ = 'b';
+					*ptr++ = 'u';
+					*ptr++ = 'z';
+					*ptr++ = '\0';
+
+					sprintf(openfilename, "%s/%s", "textures", normalMapName);
+					// Convert backslashes to forward slashes
+					for (int k = 0; k < 1024; k++)
+					{
+						if (openfilename[k] == '\0')
+							break;
+
+						if (openfilename[k] == '\\')
+							openfilename[k] = '/';
+					}
+
+					Resource::resource_t *res = Resource::Open(openfilename);
+					if (res)
+					{
+						Resource::Close(res);
+						retModel->materials[t].lightmap = Texture::ReadTexture(normalMapName, ZT_TEXTURE);
+					}
+				}*/
+	}
+
+	retModel->meshes[0].numTriangles = header->numTris;
+
+	if (!useFloat) // Decompress to MD3 'tinyframe' space
+	{
+		char *ptr;
+
+		md2triangle_t *trisPtr;
+		unsigned short *indexptr;
+		float *uvptr;
+
+		dataScale = 0.015624f; // 1 / 64.0f
+		retModel->meshes[0].tinyframes = (tinyframe_t*)Z_Calloc(sizeof(tinyframe_t)*header->numFrames, ztag, 0);
+		retModel->meshes[0].numVertices = header->numXYZ;
+		retModel->meshes[0].uvs = (float*)Z_Malloc(sizeof(float) * 2 * retModel->meshes[0].numVertices, ztag, 0);
+
+		ptr = (char*)frames;
+		for (i = 0; i < header->numFrames; i++, ptr += header->framesize)
+		{
+			short *vertptr;
+			char *normptr;
+			// char *tanptr;
+
+			md2vertex_t *vertex;
+
+			md2frame_t *framePtr = (md2frame_t*)ptr;
+			retModel->meshes[0].tinyframes[i].vertices = (short*)Z_Malloc(sizeof(short) * 3 * header->numXYZ, ztag, 0);
+			retModel->meshes[0].tinyframes[i].normals = (char*)Z_Malloc(sizeof(char) * 3 * header->numXYZ, ztag, 0);
+
+			//			if (retModel->materials[0].lightmap)
+			//				retModel->meshes[0].tinyframes[i].tangents = (char*)malloc(sizeof(char));//(char*)Z_Malloc(sizeof(char)*3*header->numVerts, ztag);
+			retModel->meshes[0].indices = (unsigned short*)Z_Malloc(sizeof(unsigned short) * 3 * header->numTris, ztag, 0);
+
+			vertptr = retModel->meshes[0].tinyframes[i].vertices;
+			normptr = retModel->meshes[0].tinyframes[i].normals;
+
+			//			tanptr = retModel->meshes[0].tinyframes[i].tangents;
+			retModel->meshes[0].tinyframes[i].material = &retModel->materials[0];
+
+			framePtr++; // Advance to vertex list
+			vertex = (md2vertex_t*)framePtr;
+			framePtr--;
+			for (j = 0; j < header->numXYZ; j++, vertex++)
+			{
+				*vertptr = (short)(((vertex->v[0] * framePtr->scale[0]) + framePtr->translate[0]) / dataScale);
+				vertptr++;
+				*vertptr = (short)(((vertex->v[2] * framePtr->scale[2]) + framePtr->translate[2]) / dataScale);
+				vertptr++;
+				*vertptr = -1.0f * (short)(((vertex->v[1] * framePtr->scale[1]) + framePtr->translate[1]) / dataScale);
+				vertptr++;
+
+				// Normal
+				*normptr++ = (char)(avertexnormals[vertex->lightNormalIndex][0] * 127);
+				*normptr++ = (char)(avertexnormals[vertex->lightNormalIndex][2] * 127);
+				*normptr++ = (char)(avertexnormals[vertex->lightNormalIndex][1] * 127);
+			}
+		}
+
+		// This doesn't need to be done every frame!
+		trisPtr = tris;
+		indexptr = retModel->meshes[0].indices;
+		uvptr = (float*)retModel->meshes[0].uvs;
+		for (j = 0; j < header->numTris; j++, trisPtr++)
+		{
+			*indexptr = trisPtr->meshIndex[0];
+			indexptr++;
+			*indexptr = trisPtr->meshIndex[1];
+			indexptr++;
+			*indexptr = trisPtr->meshIndex[2];
+			indexptr++;
+
+			uvptr[trisPtr->meshIndex[0] * 2] = texcoords[trisPtr->stIndex[0]].s / (float)header->skinwidth;
+			uvptr[trisPtr->meshIndex[0] * 2 + 1] = (texcoords[trisPtr->stIndex[0]].t / (float)header->skinheight);
+			uvptr[trisPtr->meshIndex[1] * 2] = texcoords[trisPtr->stIndex[1]].s / (float)header->skinwidth;
+			uvptr[trisPtr->meshIndex[1] * 2 + 1] = (texcoords[trisPtr->stIndex[1]].t / (float)header->skinheight);
+			uvptr[trisPtr->meshIndex[2] * 2] = texcoords[trisPtr->stIndex[2]].s / (float)header->skinwidth;
+			uvptr[trisPtr->meshIndex[2] * 2 + 1] = (texcoords[trisPtr->stIndex[2]].t / (float)header->skinheight);
+		}
+	}
+	else // Full float loading method
+	{
+		md2triangle_t *trisPtr;
+		float *uvptr;
+
+		char *ptr;
+
+		retModel->meshes[0].numVertices = header->numTris * 3;
+		retModel->meshes[0].frames = (mdlframe_t*)Z_Calloc(sizeof(mdlframe_t)*header->numFrames, ztag, 0);
+		retModel->meshes[0].uvs = (float*)Z_Malloc(sizeof(float) * 2 * retModel->meshes[0].numVertices, ztag, 0);
+
+		trisPtr = tris;
+		uvptr = retModel->meshes[0].uvs;
+		for (i = 0; i < retModel->meshes[0].numTriangles; i++, trisPtr++)
+		{
+			*uvptr++ = texcoords[trisPtr->stIndex[0]].s / (float)header->skinwidth;
+			*uvptr++ = (texcoords[trisPtr->stIndex[0]].t / (float)header->skinheight);
+			*uvptr++ = texcoords[trisPtr->stIndex[1]].s / (float)header->skinwidth;
+			*uvptr++ = (texcoords[trisPtr->stIndex[1]].t / (float)header->skinheight);
+			*uvptr++ = texcoords[trisPtr->stIndex[2]].s / (float)header->skinwidth;
+			*uvptr++ = (texcoords[trisPtr->stIndex[2]].t / (float)header->skinheight);
+		}
+
+		ptr = (char*)frames;
+		for (i = 0; i < header->numFrames; i++, ptr += header->framesize)
+		{
+			float *vertptr, *normptr;
+
+			md2vertex_t *vertex;
+
+			md2frame_t *framePtr = (md2frame_t*)ptr;
+			retModel->meshes[0].frames[i].normals = (float*)Z_Malloc(sizeof(float) * 3 * header->numTris * 3, ztag, 0);
+			retModel->meshes[0].frames[i].vertices = (float*)Z_Malloc(sizeof(float) * 3 * header->numTris * 3, ztag, 0);
+			//			if (retModel->materials[0].lightmap)
+			//				retModel->meshes[0].frames[i].tangents = (float*)malloc(sizeof(float));//(float*)Z_Malloc(sizeof(float)*3*header->numTris*3, ztag);
+			//float *vertptr, *normptr;
+			normptr = (float*)retModel->meshes[0].frames[i].normals;
+			vertptr = (float*)retModel->meshes[0].frames[i].vertices;
+			trisPtr = tris;
+
+			retModel->meshes[0].frames[i].material = &retModel->materials[0];
+
+			framePtr++; // Advance to vertex list
+			vertex = (md2vertex_t*)framePtr;
+			framePtr--;
+			for (j = 0; j < header->numTris; j++, trisPtr++)
+			{
+				*vertptr = ((vertex[trisPtr->meshIndex[0]].v[0] * framePtr->scale[0]) + framePtr->translate[0]) * WUNITS;
+				vertptr++;
+				*vertptr = ((vertex[trisPtr->meshIndex[0]].v[2] * framePtr->scale[2]) + framePtr->translate[2]) * WUNITS;
+				vertptr++;
+				*vertptr = -1.0f * ((vertex[trisPtr->meshIndex[0]].v[1] * framePtr->scale[1]) + framePtr->translate[1]) * WUNITS;
+				vertptr++;
+
+				*vertptr = ((vertex[trisPtr->meshIndex[1]].v[0] * framePtr->scale[0]) + framePtr->translate[0]) * WUNITS;
+				vertptr++;
+				*vertptr = ((vertex[trisPtr->meshIndex[1]].v[2] * framePtr->scale[2]) + framePtr->translate[2]) * WUNITS;
+				vertptr++;
+				*vertptr = -1.0f * ((vertex[trisPtr->meshIndex[1]].v[1] * framePtr->scale[1]) + framePtr->translate[1]) * WUNITS;
+				vertptr++;
+
+				*vertptr = ((vertex[trisPtr->meshIndex[2]].v[0] * framePtr->scale[0]) + framePtr->translate[0]) * WUNITS;
+				vertptr++;
+				*vertptr = ((vertex[trisPtr->meshIndex[2]].v[2] * framePtr->scale[2]) + framePtr->translate[2]) * WUNITS;
+				vertptr++;
+				*vertptr = -1.0f * ((vertex[trisPtr->meshIndex[2]].v[1] * framePtr->scale[1]) + framePtr->translate[1]) * WUNITS;
+				vertptr++;
+
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[0]].lightNormalIndex][0];
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[0]].lightNormalIndex][2];
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[0]].lightNormalIndex][1];
+
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[1]].lightNormalIndex][0];
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[1]].lightNormalIndex][2];
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[1]].lightNormalIndex][1];
+
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[2]].lightNormalIndex][0];
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[2]].lightNormalIndex][2];
+				*normptr++ = avertexnormals[vertex[trisPtr->meshIndex[2]].lightNormalIndex][1];
+			}
+		}
+	}
+
+	free(buffer);
+	return retModel;
+}
diff --git a/src/hardware/hw_md2load.h b/src/hardware/hw_md2load.h
new file mode 100644
index 0000000000000000000000000000000000000000..1662d6471eca6780ebd0213595b8985e4968da20
--- /dev/null
+++ b/src/hardware/hw_md2load.h
@@ -0,0 +1,19 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#ifndef _HW_MD2LOAD_H_
+#define _HW_MD2LOAD_H_
+
+#include "hw_model.h"
+#include "../doomtype.h"
+
+// Load the Model
+model_t *MD2_LoadModel(const char *fileName, int ztag, boolean useFloat);
+
+#endif
diff --git a/src/hardware/hw_md3load.c b/src/hardware/hw_md3load.c
new file mode 100644
index 0000000000000000000000000000000000000000..53f6034c046b9c02d87f04ee67f00bf15b57e819
--- /dev/null
+++ b/src/hardware/hw_md3load.c
@@ -0,0 +1,510 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include "../doomdef.h"
+#include "hw_md3load.h"
+#include "hw_model.h"
+#include "../z_zone.h"
+
+typedef struct
+{
+	int ident;			// A "magic number" that's used to identify the .md3 file
+	int version;		// The version of the file, always 15
+	char name[64];
+	int flags;
+	int numFrames;		// Number of frames
+	int numTags;
+	int numSurfaces;
+	int numSkins;		// Number of skins with the model
+	int offsetFrames;
+	int offsetTags;
+	int offsetSurfaces;
+	int offsetEnd;		// Offset, in bytes from the start of the file, to the end of the file (filesize)
+} md3modelHeader;
+
+typedef struct
+{
+	float minBounds[3];		// First corner of the bounding box
+	float maxBounds[3];		// Second corner of the bounding box
+	float localOrigin[3];	// Local origin, usually (0, 0, 0)
+	float radius;			// Radius of bounding sphere
+	char name[16];			// Name of frame
+} md3Frame;
+
+typedef struct
+{
+	char name[64];		// Name of tag
+	float origin[3];	// Coordinates of tag
+	float axis[9];		// Orientation of tag object
+} md3Tag;
+
+typedef struct
+{
+	int ident;
+	char name[64];			// Name of this surface
+	int flags;
+	int numFrames;			// # of keyframes
+	int numShaders;			// # of shaders
+	int numVerts;			// # of vertices
+	int numTriangles;		// # of triangles
+	int offsetTriangles;	// Relative offset from start of this struct to where the list of Triangles start
+	int offsetShaders;		// Relative offset from start of this struct to where the list of Shaders start
+	int offsetST;			// Relative offset from start of this struct to where the list of tex coords start
+	int offsetXYZNormal;	// Relative offset from start of this struct to where the list of vertices start
+	int offsetEnd;			// Relative offset from start of this struct to where this surface ends
+} md3Surface;
+
+typedef struct
+{
+	char name[64]; // Name of this shader
+	int shaderIndex; // Shader index number
+} md3Shader;
+
+typedef struct
+{
+	int index[3]; // List of offset values into the list of Vertex objects that constitute the corners of the Triangle object.
+} md3Triangle;
+
+typedef struct
+{
+	float st[2];
+} md3TexCoord;
+
+typedef struct
+{
+	short x, y, z, n;
+} md3Vertex;
+
+static float latlnglookup[256][256][3];
+
+static void GetNormalFromLatLong(short latlng, float *out)
+{
+	float *lookup = latlnglookup[(unsigned char)(latlng >> 8)][(unsigned char)(latlng & 255)];
+
+	out[0] = *lookup++;
+	out[1] = *lookup++;
+	out[2] = *lookup++;
+}
+
+#if 0
+static void NormalToLatLng(float *n, short *out)
+{
+	// Special cases
+	if (0.0f == n[0] && 0.0f == n[1])
+	{
+		if (n[2] > 0.0f)
+			*out = 0;
+		else
+			*out = 128;
+	}
+	else
+	{
+		char x, y;
+
+		x = (char)(57.2957795f * (atan2(n[1], n[0])) * (255.0f / 360.0f));
+		y = (char)(57.2957795f * (acos(n[2])) * (255.0f / 360.0f));
+
+		*out = (x << 8) + y;
+	}
+}
+#endif
+
+static inline void LatLngToNormal(short n, float *out)
+{
+	const float PI = (3.1415926535897932384626433832795f);
+	float lat = (float)(n >> 8);
+	float lng = (float)(n & 255);
+
+	lat *= PI / 128.0f;
+	lng *= PI / 128.0f;
+
+	out[0] = cosf(lat) * sinf(lng);
+	out[1] = sinf(lat) * sinf(lng);
+	out[2] = cosf(lng);
+}
+
+static void LatLngInit(void)
+{
+	int i, j;
+	for (i = 0; i < 256; i++)
+	{
+		for (j = 0; j < 256; j++)
+			LatLngToNormal((short)((i << 8) + j), latlnglookup[i][j]);
+	}
+}
+
+static boolean latlnginit = false;
+
+model_t *MD3_LoadModel(const char *fileName, int ztag, boolean useFloat)
+{
+	const float WUNITS = 1.0f;
+	model_t *retModel = NULL;
+	md3modelHeader *mdh;
+	long fileLen;
+	long fileReadLen;
+	char *buffer;
+	int surfEnd;
+	int i, t;
+	int matCount;
+	FILE *f;
+
+	if (!latlnginit)
+	{
+		LatLngInit();
+		latlnginit = true;
+	}
+
+	f = fopen(fileName, "rb");
+
+	if (!f)
+		return NULL;
+
+	retModel = (model_t*)Z_Calloc(sizeof(model_t), ztag, 0);
+
+	// find length of file
+	fseek(f, 0, SEEK_END);
+	fileLen = ftell(f);
+	fseek(f, 0, SEEK_SET);
+
+	// read in file
+	buffer = malloc(fileLen);
+	fileReadLen = fread(buffer, fileLen, 1, f);
+	fclose(f);
+
+	(void)fileReadLen; // intentionally ignore return value, per buildbot
+
+	// get pointer to file header
+	mdh = (md3modelHeader*)buffer;
+
+	retModel->numMeshes = mdh->numSurfaces;
+
+	retModel->numMaterials = 0;
+	surfEnd = 0;
+	for (i = 0; i < mdh->numSurfaces; i++)
+	{
+		md3Surface *mdS = (md3Surface*)&buffer[mdh->offsetSurfaces];
+		surfEnd += mdS->offsetEnd;
+
+		retModel->numMaterials += mdS->numShaders;
+	}
+
+	// Initialize materials
+	if (retModel->numMaterials <= 0) // Always at least one skin, duh
+		retModel->numMaterials = 1;
+
+	retModel->materials = (material_t*)Z_Calloc(sizeof(material_t)*retModel->numMaterials, ztag, 0);
+
+	for (t = 0; t < retModel->numMaterials; t++)
+	{
+		retModel->materials[t].ambient[0] = 0.3686f;
+		retModel->materials[t].ambient[1] = 0.3684f;
+		retModel->materials[t].ambient[2] = 0.3684f;
+		retModel->materials[t].ambient[3] = 1.0f;
+		retModel->materials[t].diffuse[0] = 0.8863f;
+		retModel->materials[t].diffuse[1] = 0.8850f;
+		retModel->materials[t].diffuse[2] = 0.8850f;
+		retModel->materials[t].diffuse[3] = 1.0f;
+		retModel->materials[t].emissive[0] = 0.0f;
+		retModel->materials[t].emissive[1] = 0.0f;
+		retModel->materials[t].emissive[2] = 0.0f;
+		retModel->materials[t].emissive[3] = 1.0f;
+		retModel->materials[t].specular[0] = 0.4902f;
+		retModel->materials[t].specular[1] = 0.4887f;
+		retModel->materials[t].specular[2] = 0.4887f;
+		retModel->materials[t].specular[3] = 1.0f;
+		retModel->materials[t].shininess = 25.0f;
+		retModel->materials[t].spheremap = false;
+	}
+
+	retModel->meshes = (mesh_t*)Z_Calloc(sizeof(mesh_t)*retModel->numMeshes, ztag, 0);
+
+	matCount = 0;
+	for (i = 0, surfEnd = 0; i < mdh->numSurfaces; i++)
+	{
+		int j;
+		md3Shader *mdShader;
+		md3Surface *mdS = (md3Surface*)&buffer[mdh->offsetSurfaces + surfEnd];
+		surfEnd += mdS->offsetEnd;
+
+		mdShader = (md3Shader*)((char*)mdS + mdS->offsetShaders);
+
+		for (j = 0; j < mdS->numShaders; j++, matCount++)
+		{
+			size_t len = strlen(mdShader[j].name);
+			mdShader[j].name[len-1] = 'z';
+			mdShader[j].name[len-2] = 'u';
+			mdShader[j].name[len-3] = 'b';
+
+			// Load material
+/*			retModel->materials[matCount].texture = Texture::ReadTexture(mdShader[j].name, ZT_TEXTURE);
+
+			if (!systemSucks)
+			{
+				// Check for a normal map...??
+				char openfilename[1024];
+				char normalMapName[1024];
+				strcpy(normalMapName, mdShader[j].name);
+				len = strlen(normalMapName);
+				char *ptr = &normalMapName[len];
+				ptr--; // z
+				ptr--; // u
+				ptr--; // b
+				ptr--; // .
+				*ptr++ = '_';
+				*ptr++ = 'n';
+				*ptr++ = '.';
+				*ptr++ = 'b';
+				*ptr++ = 'u';
+				*ptr++ = 'z';
+				*ptr++ = '\0';
+
+				sprintf(openfilename, "%s/%s", "textures", normalMapName);
+				// Convert backslashes to forward slashes
+				for (int k = 0; k < 1024; k++)
+				{
+					if (openfilename[k] == '\0')
+						break;
+
+					if (openfilename[k] == '\\')
+						openfilename[k] = '/';
+				}
+
+				Resource::resource_t *res = Resource::Open(openfilename);
+				if (res)
+				{
+					Resource::Close(res);
+					retModel->materials[matCount].lightmap = Texture::ReadTexture(normalMapName, ZT_TEXTURE);
+				}
+			}*/
+		}
+
+		retModel->meshes[i].numFrames = mdS->numFrames;
+		retModel->meshes[i].numTriangles = mdS->numTriangles;
+
+		if (!useFloat) // 'tinyframe' mode with indices
+		{
+			float tempNormal[3];
+			float *uvptr;
+			md3TexCoord *mdST;
+			unsigned short *indexptr;
+			md3Triangle *mdT;
+
+			retModel->meshes[i].tinyframes = (tinyframe_t*)Z_Calloc(sizeof(tinyframe_t)*mdS->numFrames, ztag, 0);
+			retModel->meshes[i].numVertices = mdS->numVerts;
+			retModel->meshes[i].uvs = (float*)Z_Malloc(sizeof(float)*2*mdS->numVerts, ztag, 0);
+			for (j = 0; j < mdS->numFrames; j++)
+			{
+				short *vertptr;
+				char *normptr;
+				// char *tanptr;
+				int k;
+				md3Vertex *mdV = (md3Vertex*)((char*)mdS + mdS->offsetXYZNormal + (mdS->numVerts*j*sizeof(md3Vertex)));
+				retModel->meshes[i].tinyframes[j].vertices = (short*)Z_Malloc(sizeof(short)*3*mdS->numVerts, ztag, 0);
+				retModel->meshes[i].tinyframes[j].normals = (char*)Z_Malloc(sizeof(char)*3*mdS->numVerts, ztag, 0);
+
+//				if (retModel->materials[0].lightmap)
+//					retModel->meshes[i].tinyframes[j].tangents = (char*)malloc(sizeof(char));//(char*)Z_Malloc(sizeof(char)*3*mdS->numVerts, ztag);
+				retModel->meshes[i].indices = (unsigned short*)Z_Malloc(sizeof(unsigned short) * 3 * mdS->numTriangles, ztag, 0);
+				vertptr = retModel->meshes[i].tinyframes[j].vertices;
+				normptr = retModel->meshes[i].tinyframes[j].normals;
+
+//				tanptr = retModel->meshes[i].tinyframes[j].tangents;
+				retModel->meshes[i].tinyframes[j].material = &retModel->materials[i];
+
+				for (k = 0; k < mdS->numVerts; k++)
+				{
+					// Vertex
+					*vertptr = mdV[k].x;
+					vertptr++;
+					*vertptr = mdV[k].z;
+					vertptr++;
+					*vertptr = 1.0f - mdV[k].y;
+					vertptr++;
+
+					// Normal
+					GetNormalFromLatLong(mdV[k].n, tempNormal);
+					*normptr = (char)(tempNormal[0] * 127);
+					normptr++;
+					*normptr = (char)(tempNormal[2] * 127);
+					normptr++;
+					*normptr = (char)(tempNormal[1] * 127);
+					normptr++;
+				}
+			}
+
+			uvptr = (float*)retModel->meshes[i].uvs;
+			mdST = (md3TexCoord*)((char*)mdS + mdS->offsetST);
+			for (j = 0; j < mdS->numVerts; j++)
+			{
+				*uvptr = mdST[j].st[0];
+				uvptr++;
+				*uvptr = mdST[j].st[1];
+				uvptr++;
+			}
+
+			indexptr = retModel->meshes[i].indices;
+			mdT = (md3Triangle*)((char*)mdS + mdS->offsetTriangles);
+			for (j = 0; j < mdS->numTriangles; j++, mdT++)
+			{
+				// Indices
+				*indexptr = (unsigned short)mdT->index[0];
+				indexptr++;
+				*indexptr = (unsigned short)mdT->index[1];
+				indexptr++;
+				*indexptr = (unsigned short)mdT->index[2];
+				indexptr++;
+			}
+		}
+		else // Traditional full-float loading method
+		{
+			float dataScale = 0.015624f * WUNITS;
+			float tempNormal[3];
+			md3TexCoord *mdST;
+			md3Triangle *mdT;
+			float *uvptr;
+			int k;
+
+			retModel->meshes[i].numVertices = mdS->numTriangles * 3;//mdS->numVerts;
+			retModel->meshes[i].frames = (mdlframe_t*)Z_Calloc(sizeof(mdlframe_t)*mdS->numFrames, ztag, 0);
+			retModel->meshes[i].uvs = (float*)Z_Malloc(sizeof(float)*2*mdS->numTriangles*3, ztag, 0);
+
+			for (j = 0; j < mdS->numFrames; j++)
+			{
+				float *vertptr;
+				float *normptr;
+				md3Vertex *mdV = (md3Vertex*)((char*)mdS + mdS->offsetXYZNormal + (mdS->numVerts*j*sizeof(md3Vertex)));
+				retModel->meshes[i].frames[j].vertices = (float*)Z_Malloc(sizeof(float)*3*mdS->numTriangles*3, ztag, 0);
+				retModel->meshes[i].frames[j].normals = (float*)Z_Malloc(sizeof(float)*3*mdS->numTriangles*3, ztag, 0);
+//				if (retModel->materials[i].lightmap)
+//					retModel->meshes[i].frames[j].tangents = (float*)malloc(sizeof(float));//(float*)Z_Malloc(sizeof(float)*3*mdS->numTriangles*3, ztag);
+				vertptr = retModel->meshes[i].frames[j].vertices;
+				normptr = retModel->meshes[i].frames[j].normals;
+				retModel->meshes[i].frames[j].material = &retModel->materials[i];
+
+				mdT = (md3Triangle*)((char*)mdS + mdS->offsetTriangles);
+
+				for (k = 0; k < mdS->numTriangles; k++)
+				{
+					// Vertex 1
+					*vertptr = mdV[mdT->index[0]].x * dataScale;
+					vertptr++;
+					*vertptr = mdV[mdT->index[0]].z * dataScale;
+					vertptr++;
+					*vertptr = 1.0f - mdV[mdT->index[0]].y * dataScale;
+					vertptr++;
+
+					GetNormalFromLatLong(mdV[mdT->index[0]].n, tempNormal);
+					*normptr = tempNormal[0];
+					normptr++;
+					*normptr = tempNormal[2];
+					normptr++;
+					*normptr = tempNormal[1];
+					normptr++;
+
+					// Vertex 2
+					*vertptr = mdV[mdT->index[1]].x * dataScale;
+					vertptr++;
+					*vertptr = mdV[mdT->index[1]].z * dataScale;
+					vertptr++;
+					*vertptr = 1.0f - mdV[mdT->index[1]].y * dataScale;
+					vertptr++;
+
+					GetNormalFromLatLong(mdV[mdT->index[1]].n, tempNormal);
+					*normptr = tempNormal[0];
+					normptr++;
+					*normptr = tempNormal[2];
+					normptr++;
+					*normptr = tempNormal[1];
+					normptr++;
+
+					// Vertex 3
+					*vertptr = mdV[mdT->index[2]].x * dataScale;
+					vertptr++;
+					*vertptr = mdV[mdT->index[2]].z * dataScale;
+					vertptr++;
+					*vertptr = 1.0f - mdV[mdT->index[2]].y * dataScale;
+					vertptr++;
+
+					GetNormalFromLatLong(mdV[mdT->index[2]].n, tempNormal);
+					*normptr = tempNormal[0];
+					normptr++;
+					*normptr = tempNormal[2];
+					normptr++;
+					*normptr = tempNormal[1];
+					normptr++;
+
+					mdT++; // Advance to next triangle
+				}
+			}
+
+			mdST = (md3TexCoord*)((char*)mdS + mdS->offsetST);
+			uvptr = (float*)retModel->meshes[i].uvs;
+			mdT = (md3Triangle*)((char*)mdS + mdS->offsetTriangles);
+
+			for (k = 0; k < mdS->numTriangles; k++)
+			{
+				*uvptr = mdST[mdT->index[0]].st[0];
+				uvptr++;
+				*uvptr = mdST[mdT->index[0]].st[1];
+				uvptr++;
+
+				*uvptr = mdST[mdT->index[1]].st[0];
+				uvptr++;
+				*uvptr = mdST[mdT->index[1]].st[1];
+				uvptr++;
+
+				*uvptr = mdST[mdT->index[2]].st[0];
+				uvptr++;
+				*uvptr = mdST[mdT->index[2]].st[1];
+				uvptr++;
+
+				mdT++; // Advance to next triangle
+			}
+		}
+	}
+	/*
+	// Tags?
+	retModel->numTags = mdh->numTags;
+	retModel->maxNumFrames = mdh->numFrames;
+	retModel->tags = (tag_t*)Z_Calloc(sizeof(tag_t) * retModel->numTags * mdh->numFrames, ztag);
+	md3Tag *mdTag = (md3Tag*)&buffer[mdh->offsetTags];
+	tag_t *curTag = retModel->tags;
+	for (i = 0; i < mdh->numFrames; i++)
+	{
+		int j;
+		for (j = 0; j < retModel->numTags; j++, mdTag++)
+		{
+			strcpys(curTag->name, mdTag->name, sizeof(curTag->name) / sizeof(char));
+			curTag->transform.m[0][0] = mdTag->axis[0];
+			curTag->transform.m[0][1] = mdTag->axis[1];
+			curTag->transform.m[0][2] = mdTag->axis[2];
+			curTag->transform.m[1][0] = mdTag->axis[3];
+			curTag->transform.m[1][1] = mdTag->axis[4];
+			curTag->transform.m[1][2] = mdTag->axis[5];
+			curTag->transform.m[2][0] = mdTag->axis[6];
+			curTag->transform.m[2][1] = mdTag->axis[7];
+			curTag->transform.m[2][2] = mdTag->axis[8];
+			curTag->transform.m[3][0] = mdTag->origin[0] * WUNITS;
+			curTag->transform.m[3][1] = mdTag->origin[1] * WUNITS;
+			curTag->transform.m[3][2] = mdTag->origin[2] * WUNITS;
+			curTag->transform.m[3][3] = 1.0f;
+
+			Matrix::Rotate(&curTag->transform, 90.0f, &Vector::Xaxis);
+			curTag++;
+		}
+	}*/
+
+
+	free(buffer);
+
+	return retModel;
+}
diff --git a/src/hardware/hw_md3load.h b/src/hardware/hw_md3load.h
new file mode 100644
index 0000000000000000000000000000000000000000..c0e0522ff6bf2e6e43c96021ab758ca64ae7b45b
--- /dev/null
+++ b/src/hardware/hw_md3load.h
@@ -0,0 +1,19 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#ifndef _HW_MD3LOAD_H_
+#define _HW_MD3LOAD_H_
+
+#include "hw_model.h"
+#include "../doomtype.h"
+
+// Load the Model
+model_t *MD3_LoadModel(const char *fileName, int ztag, boolean useFloat);
+
+#endif
diff --git a/src/hardware/hw_model.c b/src/hardware/hw_model.c
new file mode 100644
index 0000000000000000000000000000000000000000..2c36f974414df2b1319c5a774602483a4ecd6190
--- /dev/null
+++ b/src/hardware/hw_model.c
@@ -0,0 +1,593 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#include "../z_zone.h"
+#include "../doomdef.h"
+#include "hw_model.h"
+#include "hw_md2load.h"
+#include "hw_md3load.h"
+#include "u_list.h"
+#include <string.h>
+
+static float PI = (3.1415926535897932384626433832795f);
+static float U_Deg2Rad(float deg)
+{
+	return deg * ((float)PI / 180.0f);
+}
+
+vector_t vectorXaxis = { 1.0f, 0.0f, 0.0f };
+vector_t vectorYaxis = { 0.0f, 1.0f, 0.0f };
+vector_t vectorZaxis = { 0.0f, 0.0f, 1.0f };
+
+void VectorRotate(vector_t *rotVec, const vector_t *axisVec, float angle)
+{
+	float ux, uy, uz, vx, vy, vz, wx, wy, wz, sa, ca;
+
+	angle = U_Deg2Rad(angle);
+
+	// Rotate the point (x,y,z) around the vector (u,v,w)
+	ux = axisVec->x * rotVec->x;
+	uy = axisVec->x * rotVec->y;
+	uz = axisVec->x * rotVec->z;
+	vx = axisVec->y * rotVec->x;
+	vy = axisVec->y * rotVec->y;
+	vz = axisVec->y * rotVec->z;
+	wx = axisVec->z * rotVec->x;
+	wy = axisVec->z * rotVec->y;
+	wz = axisVec->z * rotVec->z;
+	sa = sinf(angle);
+	ca = cosf(angle);
+
+	rotVec->x = axisVec->x*(ux + vy + wz) + (rotVec->x*(axisVec->y*axisVec->y + axisVec->z*axisVec->z) - axisVec->x*(vy + wz))*ca + (-wy + vz)*sa;
+	rotVec->y = axisVec->y*(ux + vy + wz) + (rotVec->y*(axisVec->x*axisVec->x + axisVec->z*axisVec->z) - axisVec->y*(ux + wz))*ca + (wx - uz)*sa;
+	rotVec->z = axisVec->z*(ux + vy + wz) + (rotVec->z*(axisVec->x*axisVec->x + axisVec->y*axisVec->y) - axisVec->z*(ux + vy))*ca + (-vx + uy)*sa;
+}
+
+void UnloadModel(model_t *model)
+{
+	// Wouldn't it be great if C just had destructors?
+	int i;
+	for (i = 0; i < model->numMeshes; i++)
+	{
+		mesh_t *mesh = &model->meshes[i];
+
+		if (mesh->frames)
+		{
+			int j;
+			for (j = 0; j < mesh->numFrames; j++)
+			{
+				if (mesh->frames[j].normals)
+					Z_Free(mesh->frames[j].normals);
+
+				if (mesh->frames[j].tangents)
+					Z_Free(mesh->frames[j].tangents);
+
+				if (mesh->frames[j].vertices)
+					Z_Free(mesh->frames[j].vertices);
+
+				if (mesh->frames[j].colors)
+					Z_Free(mesh->frames[j].colors);
+			}
+
+			Z_Free(mesh->frames);
+		}
+		else if (mesh->tinyframes)
+		{
+			int j;
+			for (j = 0; j < mesh->numFrames; j++)
+			{
+				if (mesh->tinyframes[j].normals)
+					Z_Free(mesh->tinyframes[j].normals);
+
+				if (mesh->tinyframes[j].tangents)
+					Z_Free(mesh->tinyframes[j].tangents);
+
+				if (mesh->tinyframes[j].vertices)
+					Z_Free(mesh->tinyframes[j].vertices);
+			}
+
+			if (mesh->indices)
+				Z_Free(mesh->indices);
+
+			Z_Free(mesh->tinyframes);
+		}
+
+		if (mesh->uvs)
+			Z_Free(mesh->uvs);
+
+		if (mesh->lightuvs)
+			Z_Free(mesh->lightuvs);
+	}
+
+	if (model->meshes)
+		Z_Free(model->meshes);
+
+	if (model->tags)
+		Z_Free(model->tags);
+
+	if (model->materials)
+		Z_Free(model->materials);
+
+	DeleteVBOs(model);
+	Z_Free(model);
+}
+
+tag_t *GetTagByName(model_t *model, char *name, int frame)
+{
+	if (frame < model->maxNumFrames)
+	{
+		tag_t *iterator = &model->tags[frame * model->numTags];
+
+		int i;
+		for (i = 0; i < model->numTags; i++)
+		{
+			if (!stricmp(iterator[i].name, name))
+				return &iterator[i];
+		}
+	}
+
+	return NULL;
+}
+
+//
+// LoadModel
+//
+// Load a model and
+// convert it to the
+// internal format.
+//
+model_t *LoadModel(const char *filename, int ztag)
+{
+	model_t *model;
+
+	// What type of file?
+	const char *extension = NULL;
+	int i;
+	for (i = (int)strlen(filename)-1; i >= 0; i--)
+	{
+		if (filename[i] != '.')
+			continue;
+
+		extension = &filename[i];
+		break;
+	}
+
+	if (!extension)
+	{
+		CONS_Printf("Model %s is lacking a file extension, unable to determine type!\n", filename);
+		return NULL;
+	}
+
+	if (!strcmp(extension, ".md3"))
+	{
+		if (!(model = MD3_LoadModel(filename, ztag, false)))
+			return NULL;
+	}
+	else if (!strcmp(extension, ".md3s")) // MD3 that will be converted in memory to use full floats
+	{
+		if (!(model = MD3_LoadModel(filename, ztag, true)))
+			return NULL;
+	}
+	else if (!strcmp(extension, ".md2"))
+	{
+		if (!(model = MD2_LoadModel(filename, ztag, false)))
+			return NULL;
+	}
+	else if (!strcmp(extension, ".md2s"))
+	{
+		if (!(model = MD2_LoadModel(filename, ztag, true)))
+			return NULL;
+	}
+	else
+	{
+		CONS_Printf("Unknown model format: %s\n", extension);
+		return NULL;
+	}
+
+	model->mdlFilename = (char*)Z_Malloc(strlen(filename)+1, ztag, 0);
+	strcpy(model->mdlFilename, filename);
+
+	Optimize(model);
+	GeneratePolygonNormals(model, ztag);
+
+	// Default material properties
+	for (i = 0 ; i < model->numMaterials; i++)
+	{
+		material_t *material = &model->materials[i];
+		material->ambient[0] = 0.7686f;
+		material->ambient[1] = 0.7686f;
+		material->ambient[2] = 0.7686f;
+		material->ambient[3] = 1.0f;
+		material->diffuse[0] = 0.5863f;
+		material->diffuse[1] = 0.5863f;
+		material->diffuse[2] = 0.5863f;
+		material->diffuse[3] = 1.0f;
+		material->specular[0] = 0.4902f;
+		material->specular[1] = 0.4902f;
+		material->specular[2] = 0.4902f;
+		material->specular[3] = 1.0f;
+		material->shininess = 25.0f;
+	}
+
+	return model;
+}
+
+//
+// GenerateVertexNormals
+//
+// Creates a new normal for a vertex using the average of all of the polygons it belongs to.
+//
+void GenerateVertexNormals(model_t *model)
+{
+	int i;
+	for (i = 0; i < model->numMeshes; i++)
+	{
+		int j;
+
+		mesh_t *mesh = &model->meshes[i];
+
+		if (!mesh->frames)
+			continue;
+
+		for (j = 0; j < mesh->numFrames; j++)
+		{
+			mdlframe_t *frame = &mesh->frames[j];
+			int memTag = PU_STATIC;
+			float *newNormals = (float*)Z_Malloc(sizeof(float)*3*mesh->numTriangles*3, memTag, 0);
+			int k;
+			float *vertPtr = frame->vertices;
+			float *oldNormals;
+
+			M_Memcpy(newNormals, frame->normals, sizeof(float)*3*mesh->numTriangles*3);
+
+/*			if (!systemSucks)
+			{
+				memTag = Z_GetTag(frame->tangents);
+				float *newTangents = (float*)Z_Malloc(sizeof(float)*3*mesh->numTriangles*3, memTag);
+				M_Memcpy(newTangents, frame->tangents, sizeof(float)*3*mesh->numTriangles*3);
+			}*/
+
+			for (k = 0; k < mesh->numVertices; k++)
+			{
+				float x, y, z;
+				int vCount = 0;
+				vector_t normal;
+				int l;
+				float *testPtr = frame->vertices;
+
+				x = *vertPtr++;
+				y = *vertPtr++;
+				z = *vertPtr++;
+
+				normal.x = normal.y = normal.z = 0;
+
+				for (l = 0; l < mesh->numVertices; l++)
+				{
+					float testX, testY, testZ;
+					testX = *testPtr++;
+					testY = *testPtr++;
+					testZ = *testPtr++;
+
+					if (fabsf(x - testX) > FLT_EPSILON
+						|| fabsf(y - testY) > FLT_EPSILON
+						|| fabsf(z - testZ) > FLT_EPSILON)
+						continue;
+
+					// Found a vertex match! Add it...
+					normal.x += frame->normals[3 * l + 0];
+					normal.y += frame->normals[3 * l + 1];
+					normal.z += frame->normals[3 * l + 2];
+					vCount++;
+				}
+
+				if (vCount > 1)
+				{
+//					Vector::Normalize(&normal);
+					newNormals[3 * k + 0] = (float)normal.x;
+					newNormals[3 * k + 1] = (float)normal.y;
+					newNormals[3 * k + 2] = (float)normal.z;
+
+/*					if (!systemSucks)
+					{
+						Vector::vector_t tangent;
+						Vector::Tangent(&normal, &tangent);
+						newTangents[3 * k + 0] = tangent.x;
+						newTangents[3 * k + 1] = tangent.y;
+						newTangents[3 * k + 2] = tangent.z;
+					}*/
+				}
+			}
+
+			oldNormals = frame->normals;
+			frame->normals = newNormals;
+			Z_Free(oldNormals);
+
+/*			if (!systemSucks)
+			{
+				float *oldTangents = frame->tangents;
+				frame->tangents = newTangents;
+				Z_Free(oldTangents);
+			}*/
+		}
+	}
+}
+
+typedef struct materiallist_s
+{
+	struct materiallist_s *next;
+	struct materiallist_s *prev;
+	material_t *material;
+} materiallist_t;
+
+static boolean AddMaterialToList(materiallist_t **head, material_t *material)
+{
+	materiallist_t *node, *newMatNode;
+	for (node = *head; node; node = node->next)
+	{
+		if (node->material == material)
+			return false;
+	}
+
+	// Didn't find it, so add to the list
+	newMatNode = (materiallist_t*)Z_Malloc(sizeof(materiallist_t), PU_CACHE, 0);
+	newMatNode->material = material;
+	ListAdd(newMatNode, (listitem_t**)head);
+	return true;
+}
+
+//
+// Optimize
+//
+// Groups triangles from meshes in the model
+// Only works for models with 1 frame
+//
+void Optimize(model_t *model)
+{
+	int numMeshes = 0;
+	int i;
+	materiallist_t *matListHead = NULL;
+	int memTag;
+	mesh_t *newMeshes;
+	materiallist_t *node;
+
+	if (model->numMeshes <= 1)
+		return; // No need
+
+	for (i = 0; i < model->numMeshes; i++)
+	{
+		mesh_t *curMesh = &model->meshes[i];
+
+		if (curMesh->numFrames > 1)
+			return; // Can't optimize models with > 1 frame
+
+		if (!curMesh->frames)
+			return; // Don't optimize tinyframe models (no need)
+
+		// We are condensing to 1 mesh per material, so
+		// the # of materials we use will be the new
+		// # of meshes
+		if (AddMaterialToList(&matListHead, curMesh->frames[0].material))
+			numMeshes++;
+	}
+
+	memTag = PU_STATIC;
+	newMeshes = (mesh_t*)Z_Calloc(sizeof(mesh_t) * numMeshes, memTag, 0);
+
+	i = 0;
+	for (node = matListHead; node; node = node->next)
+	{
+		material_t *curMat = node->material;
+		mesh_t *newMesh = &newMeshes[i];
+		mdlframe_t *curFrame;
+		int uvCount;
+		int vertCount;
+		int colorCount;
+
+		// Find all triangles with this material and count them
+		int numTriangles = 0;
+		int j;
+		for (j = 0; j < model->numMeshes; j++)
+		{
+			mesh_t *curMesh = &model->meshes[j];
+
+			if (curMesh->frames[0].material == curMat)
+				numTriangles += curMesh->numTriangles;
+		}
+
+		newMesh->numFrames = 1;
+		newMesh->numTriangles = numTriangles;
+		newMesh->numVertices = numTriangles * 3;
+		newMesh->uvs = (float*)Z_Malloc(sizeof(float)*2*numTriangles*3, memTag, 0);
+//		if (node->material->lightmap)
+//			newMesh->lightuvs = (float*)Z_Malloc(sizeof(float)*2*numTriangles*3, memTag, 0);
+		newMesh->frames = (mdlframe_t*)Z_Calloc(sizeof(mdlframe_t), memTag, 0);
+		curFrame = &newMesh->frames[0];
+
+		curFrame->material = curMat;
+		curFrame->normals = (float*)Z_Malloc(sizeof(float)*3*numTriangles*3, memTag, 0);
+//		if (!systemSucks)
+//			curFrame->tangents = (float*)Z_Malloc(sizeof(float)*3*numTriangles*3, memTag, 0);
+		curFrame->vertices = (float*)Z_Malloc(sizeof(float)*3*numTriangles*3, memTag, 0);
+		curFrame->colors = (char*)Z_Malloc(sizeof(char)*4*numTriangles*3, memTag, 0);
+
+		// Now traverse the meshes of the model, adding in
+		// vertices/normals/uvs that match the current material
+		uvCount = 0;
+		vertCount = 0;
+		colorCount = 0;
+		for (j = 0; j < model->numMeshes; j++)
+		{
+			mesh_t *curMesh = &model->meshes[j];
+
+			if (curMesh->frames[0].material == curMat)
+			{
+				float *dest;
+				float *src;
+				char *destByte;
+				char *srcByte;
+
+				M_Memcpy(&newMesh->uvs[uvCount],
+					curMesh->uvs,
+					sizeof(float)*2*curMesh->numTriangles*3);
+
+/*				if (node->material->lightmap)
+				{
+					M_Memcpy(&newMesh->lightuvs[uvCount],
+						curMesh->lightuvs,
+						sizeof(float)*2*curMesh->numTriangles*3);
+				}*/
+				uvCount += 2*curMesh->numTriangles*3;
+
+				dest = (float*)newMesh->frames[0].vertices;
+				src = (float*)curMesh->frames[0].vertices;
+				M_Memcpy(&dest[vertCount],
+					src,
+					sizeof(float)*3*curMesh->numTriangles*3);
+
+				dest = (float*)newMesh->frames[0].normals;
+				src = (float*)curMesh->frames[0].normals;
+				M_Memcpy(&dest[vertCount],
+					src,
+					sizeof(float)*3*curMesh->numTriangles*3);
+
+/*				if (!systemSucks)
+				{
+					dest = (float*)newMesh->frames[0].tangents;
+					src = (float*)curMesh->frames[0].tangents;
+					M_Memcpy(&dest[vertCount],
+						src,
+						sizeof(float)*3*curMesh->numTriangles*3);
+				}*/
+
+				vertCount += 3 * curMesh->numTriangles * 3;
+
+				destByte = (char*)newMesh->frames[0].colors;
+				srcByte = (char*)curMesh->frames[0].colors;
+
+				if (srcByte)
+				{
+					M_Memcpy(&destByte[colorCount],
+						srcByte,
+						sizeof(char)*4*curMesh->numTriangles*3);
+				}
+				else
+				{
+					memset(&destByte[colorCount],
+						255,
+						sizeof(char)*4*curMesh->numTriangles*3);
+				}
+
+				colorCount += 4 * curMesh->numTriangles * 3;
+			}
+		}
+
+		i++;
+	}
+
+	CONS_Printf("Model::Optimize(): Model reduced from %d to %d meshes.\n", model->numMeshes, numMeshes);
+	model->meshes = newMeshes;
+	model->numMeshes = numMeshes;
+}
+
+void GeneratePolygonNormals(model_t *model, int ztag)
+{
+	int i;
+	for (i = 0; i < model->numMeshes; i++)
+	{
+		int j;
+		mesh_t *mesh = &model->meshes[i];
+
+		if (!mesh->frames)
+			continue;
+
+		for (j = 0; j < mesh->numFrames; j++)
+		{
+			int k;
+			mdlframe_t *frame = &mesh->frames[j];
+			const float *vertices = frame->vertices;
+			vector_t *polyNormals;
+
+			frame->polyNormals = (vector_t*)Z_Malloc(sizeof(vector_t) * mesh->numTriangles, ztag, 0);
+
+			polyNormals = frame->polyNormals;
+
+			for (k = 0; k < mesh->numTriangles; k++)
+			{
+//				Vector::Normal(vertices, polyNormals);
+				vertices += 3 * 3;
+				polyNormals++;
+			}
+		}
+	}
+}
+
+//
+// Reload
+//
+// Reload VBOs
+//
+#if 0
+static void Reload(void)
+{
+/*	model_t *node;
+	for (node = modelHead; node; node = node->next)
+	{
+		int i;
+		for (i = 0; i < node->numMeshes; i++)
+		{
+			mesh_t *mesh = &node->meshes[i];
+
+			if (mesh->frames)
+			{
+				int j;
+				for (j = 0; j < mesh->numFrames; j++)
+					CreateVBO(mesh, &mesh->frames[j]);
+			}
+			else if (mesh->tinyframes)
+			{
+				int j;
+				for (j = 0; j < mesh->numFrames; j++)
+					CreateVBO(mesh, &mesh->tinyframes[j]);
+			}
+		}
+	}*/
+}
+#endif
+
+void DeleteVBOs(model_t *model)
+{
+	(void)model;
+/*	for (int i = 0; i < model->numMeshes; i++)
+	{
+		mesh_t *mesh = &model->meshes[i];
+
+		if (mesh->frames)
+		{
+			for (int j = 0; j < mesh->numFrames; j++)
+			{
+				mdlframe_t *frame = &mesh->frames[j];
+				if (!frame->vboID)
+					continue;
+				bglDeleteBuffers(1, &frame->vboID);
+				frame->vboID = 0;
+			}
+		}
+		else if (mesh->tinyframes)
+		{
+			for (int j = 0; j < mesh->numFrames; j++)
+			{
+				tinyframe_t *frame = &mesh->tinyframes[j];
+				if (!frame->vboID)
+					continue;
+				bglDeleteBuffers(1, &frame->vboID);
+				frame->vboID = 0;
+			}
+		}
+	}*/
+}
diff --git a/src/hardware/hw_model.h b/src/hardware/hw_model.h
new file mode 100644
index 0000000000000000000000000000000000000000..1803f4c5c32f2e93dfe52a440c58742c8fd8d9cf
--- /dev/null
+++ b/src/hardware/hw_model.h
@@ -0,0 +1,104 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#ifndef _HW_MODEL_H_
+#define _HW_MODEL_H_
+
+#include "../doomtype.h"
+
+typedef struct
+{
+	float x, y, z;
+} vector_t;
+
+extern vector_t vectorXaxis;
+extern vector_t vectorYaxis;
+extern vector_t vectorZaxis;
+
+void VectorRotate(vector_t *rotVec, const vector_t *axisVec, float angle);
+
+typedef struct
+{
+	float ambient[4], diffuse[4], specular[4], emissive[4];
+	float shininess;
+	boolean spheremap;
+//	Texture::texture_t *texture;
+//	Texture::texture_t *lightmap;
+} material_t;
+
+typedef struct
+{
+	material_t *material; // Pointer to the allocated 'materials' list in model_t
+	float *vertices;
+	float *normals;
+	float *tangents;
+	char *colors;
+	unsigned int vboID;
+	vector_t *polyNormals;
+} mdlframe_t;
+
+typedef struct
+{
+	material_t *material;
+	short *vertices;
+	char *normals;
+	char *tangents;
+	unsigned int vboID;
+} tinyframe_t;
+
+// Equivalent to MD3's many 'surfaces'
+typedef struct mesh_s
+{
+	int numVertices;
+	int numTriangles;
+
+	float *uvs;
+	float *lightuvs;
+
+	int numFrames;
+	mdlframe_t *frames;
+	tinyframe_t *tinyframes;
+	unsigned short *indices;
+} mesh_t;
+
+typedef struct tag_s
+{
+	char name[64];
+//	matrix_t transform;
+} tag_t;
+
+typedef struct model_s
+{
+	int maxNumFrames;
+
+	int numMaterials;
+	material_t *materials;
+	int numMeshes;
+	mesh_t *meshes;
+	int numTags;
+	tag_t *tags;
+
+	char *mdlFilename;
+	boolean unloaded;
+} model_t;
+
+extern int numModels;
+extern model_t *modelHead;
+
+tag_t *GetTagByName(model_t *model, char *name, int frame);
+model_t *LoadModel(const char *filename, int ztag);
+void UnloadModel(model_t *model);
+void Optimize(model_t *model);
+void GenerateVertexNormals(model_t *model);
+void GeneratePolygonNormals(model_t *model, int ztag);
+void CreateVBOTiny(mesh_t *mesh, tinyframe_t *frame);
+void CreateVBO(mesh_t *mesh, mdlframe_t *frame);
+void DeleteVBOs(model_t *model);
+
+#endif
diff --git a/src/hardware/r_opengl/ogl_win.c b/src/hardware/r_opengl/ogl_win.c
index eb9a31a7d7f1e909d72e88cc5bc3c166471b73ab..562afe9989e0908f046ece44abe2fcf22d2fa439 100644
--- a/src/hardware/r_opengl/ogl_win.c
+++ b/src/hardware/r_opengl/ogl_win.c
@@ -347,13 +347,6 @@ static INT32 WINAPI SetRes(viddef_t *lvid, vmode_t *pcurrentmode)
 	if (strstr(renderer, "810"))   oglflags |= GLF_NOZBUFREAD;
 	DBG_Printf("oglflags   : 0x%X\n", oglflags);
 
-#ifdef USE_PALETTED_TEXTURE
-	if (isExtAvailable("GL_EXT_paletted_texture",gl_extensions))
-		glColorTableEXT = GetGLFunc("glColorTableEXT");
-	else
-		glColorTableEXT = NULL;
-#endif
-
 #ifdef USE_WGL_SWAP
 	if (isExtAvailable("WGL_EXT_swap_control",gl_extensions))
 		wglSwapIntervalEXT = GetGLFunc("wglSwapIntervalEXT");
@@ -582,19 +575,8 @@ EXPORT void HWRAPI(SetPalette) (RGBA_t *pal, RGBA_t *gamma)
 		myPaletteData[i].s.blue  = (UINT8)MIN((pal[i].s.blue*gamma->s.blue)/127,   255);
 		myPaletteData[i].s.alpha = pal[i].s.alpha;
 	}
-#ifdef USE_PALETTED_TEXTURE
-	if (glColorTableEXT)
-	{
-		for (i = 0; i < 256; i++)
-		{
-			palette_tex[3*i+0] = pal[i].s.red;
-			palette_tex[3*i+1] = pal[i].s.green;
-			palette_tex[3*i+2] = pal[i].s.blue;
-		}
-		glColorTableEXT(GL_TEXTURE_2D, GL_RGB8, 256, GL_RGB, GL_UNSIGNED_BYTE, palette_tex);
-	}
-#endif
-	// on a chang� de palette, il faut recharger toutes les textures
+
+	// on a palette change, you have to reload all of the textures
 	Flush();
 }
 
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index ad90a5187d00bd151528e8de6f3b37bfa59f8d2f..9fcc8d1578936faa0ae47904ac57fab3659358d6 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -29,15 +29,10 @@
 
 #include <stdarg.h>
 #include <math.h>
-#ifndef SHUFFLE
-#ifndef KOS_GL_COMPATIBILITY
-#define SHUFFLE
-#endif
-#endif
 #include "r_opengl.h"
+#include "r_vbo.h"
 
 #if defined (HWRENDER) && !defined (NOROPENGL)
-// for KOS: GL_TEXTURE_ENV, glAlphaFunc, glColorMask, glPolygonOffset, glReadPixels, GL_ALPHA_TEST, GL_POLYGON_OFFSET_FILL
 
 struct GLRGBAFloat
 {
@@ -47,6 +42,7 @@ struct GLRGBAFloat
 	GLfloat alpha;
 };
 typedef struct GLRGBAFloat GLRGBAFloat;
+static const GLubyte white[4] = { 255, 255, 255, 255 };
 
 // ==========================================================================
 //                                                                  CONSTANTS
@@ -70,8 +66,10 @@ static float NEAR_CLIPPING_PLANE =   NZCLIP_PLANE;
 static  GLuint      NextTexAvail    = FIRST_TEX_AVAIL;
 static  GLuint      tex_downloaded  = 0;
 static  GLfloat     fov             = 90.0f;
+#if 0
 static  GLuint      pal_col         = 0;
 static  FRGBAFloat  const_pal_col;
+#endif
 static  FBITFIELD   CurrentPolyFlags;
 
 static  FTextureInfo*  gr_cachetail = NULL;
@@ -83,9 +81,7 @@ GLint   screen_height   = 0;
 GLbyte  screen_depth    = 0;
 GLint   textureformatGL = 0;
 GLint maximumAnisotropy = 0;
-#ifndef KOS_GL_COMPATIBILITY
 static GLboolean MipMap = GL_FALSE;
-#endif
 static GLint min_filter = GL_LINEAR;
 static GLint mag_filter = GL_LINEAR;
 static GLint anisotropic_filter = 0;
@@ -94,17 +90,9 @@ static FTransform  md2_transform;
 const GLubyte *gl_extensions = NULL;
 
 //Hurdler: 04/10/2000: added for the kick ass coronas as Boris wanted;-)
-#ifndef MINI_GL_COMPATIBILITY
-static GLdouble    modelMatrix[16];
-static GLdouble    projMatrix[16];
+static GLfloat    modelMatrix[16];
+static GLfloat    projMatrix[16];
 static GLint       viewport[4];
-#endif
-
-
-#ifdef USE_PALETTED_TEXTURE
-	PFNGLCOLORTABLEEXTPROC  glColorTableEXT = NULL;
-	GLubyte                 palette_tex[256*3];
-#endif
 
 // Yay for arbitrary  numbers! NextTexAvail is buggy for some reason.
 // Sryder:	NextTexAvail is broken for these because palette changes or changes to the texture filter or antialiasing
@@ -167,11 +155,6 @@ float byteasfloat(UINT8 fbyte)
 
 static I_Error_t I_Error_GL = NULL;
 
-#ifndef MINI_GL_COMPATIBILITY
-static boolean gl13 = false; // whether we can use opengl 1.3 functions
-#endif
-
-
 // -----------------+
 // DBG_Printf       : Output error messages to debug log if DEBUG_TO_FILE is defined,
 //                  : else do nothing
@@ -202,19 +185,14 @@ FUNCPRINTF void DBG_Printf(const char *lpFmt, ...)
 #define pglAlphaFunc glAlphaFunc
 #define pglBlendFunc glBlendFunc
 #define pglCullFace glCullFace
-#define pglPolygonMode glPolygonMode
 #define pglPolygonOffset glPolygonOffset
 #define pglScissor glScissor
 #define pglEnable glEnable
 #define pglDisable glDisable
-#ifndef MINI_GL_COMPATIBILITY
-#define pglGetDoublev glGetDoublev
-#endif
+#define pglGetFloatv glGetFloatv
 //glGetIntegerv
 //glGetString
-#ifdef KOS_GL_COMPATIBILITY
 #define pglHint glHint
-#endif
 
 /* Depth Buffer */
 #define pglClearDepth glClearDepth
@@ -228,23 +206,25 @@ FUNCPRINTF void DBG_Printf(const char *lpFmt, ...)
 #define pglPushMatrix glPushMatrix
 #define pglPopMatrix glPopMatrix
 #define pglLoadIdentity glLoadIdentity
-#ifdef MINI_GL_COMPATIBILITY
 #define pglMultMatrixf glMultMatrixf
-#else
-#define pglMultMatrixd glMultMatrixd
-#endif
 #define pglRotatef glRotatef
 #define pglScalef glScalef
 #define pglTranslatef glTranslatef
 
 /* Drawing Functions */
-#define pglBegin glBegin
-#define pglEnd glEnd
-#define pglVertex3f glVertex3f
-#define pglNormal3f glNormal3f
-#define pglColor4f glColor4f
-#define pglColor4fv glColor4fv
-#define pglTexCoord2f glTexCoord2f
+#define pglColor4ubv glColor4ubv
+#define pglVertexPointer glVertexPointer
+#define pglNormalPointer glNormalPointer
+#define pglTexCoordPointer glTexCoordPointer
+#define pglDrawArrays glDrawArrays
+#define pglDrawElements glDrawElements
+#define pglEnableClientState glEnableClientState
+#define pglDisableClientState glDisableClientState
+#define pglClientActiveTexture glClientActiveTexture
+#define pglGenBuffers glGenBuffers
+#define pglBindBuffer glBindBuffer
+#define pglBufferData glBufferData
+#define pglDeleteBuffers glDeleteBuffers
 
 /* Lighting */
 #define pglShadeModel glShadeModel
@@ -270,10 +250,8 @@ FUNCPRINTF void DBG_Printf(const char *lpFmt, ...)
 #define pglDeleteTextures glDeleteTextures
 #define pglBindTexture glBindTexture
 /* texture mapping */ //GL_EXT_copy_texture
-#ifndef KOS_GL_COMPATIBILITY
 #define pglCopyTexImage2D glCopyTexImage2D
 #define pglCopyTexSubImage2D glCopyTexSubImage2D
-#endif
 
 #else //!STATIC_OPENGL
 
@@ -290,8 +268,6 @@ typedef void (APIENTRY * PFNglBlendFunc) (GLenum sfactor, GLenum dfactor);
 static PFNglBlendFunc pglBlendFunc;
 typedef void (APIENTRY * PFNglCullFace) (GLenum mode);
 static PFNglCullFace pglCullFace;
-typedef void (APIENTRY * PFNglPolygonMode) (GLenum face, GLenum mode);
-static PFNglPolygonMode pglPolygonMode;
 typedef void (APIENTRY * PFNglPolygonOffset) (GLfloat factor, GLfloat units);
 static PFNglPolygonOffset pglPolygonOffset;
 typedef void (APIENTRY * PFNglScissor) (GLint x, GLint y, GLsizei width, GLsizei height);
@@ -300,10 +276,8 @@ typedef void (APIENTRY * PFNglEnable) (GLenum cap);
 static PFNglEnable pglEnable;
 typedef void (APIENTRY * PFNglDisable) (GLenum cap);
 static PFNglDisable pglDisable;
-#ifndef MINI_GL_COMPATIBILITY
-typedef void (APIENTRY * PFNglGetDoublev) (GLenum pname, GLdouble *params);
-static PFNglGetDoublev pglGetDoublev;
-#endif
+typedef void (APIENTRY * PFNglGetFloatv) (GLenum pname, GLfloat *params);
+static PFNglGetFloatv pglGetFloatv;
 //glGetIntegerv
 //glGetString
 
@@ -328,13 +302,8 @@ typedef void (APIENTRY * PFNglPopMatrix) (void);
 static PFNglPopMatrix pglPopMatrix;
 typedef void (APIENTRY * PFNglLoadIdentity) (void);
 static PFNglLoadIdentity pglLoadIdentity;
-#ifdef MINI_GL_COMPATIBILITY
 typedef void (APIENTRY * PFNglMultMatrixf) (const GLfloat *m);
 static PFNglMultMatrixf pglMultMatrixf;
-#else
-typedef void (APIENTRY * PFNglMultMatrixd) (const GLdouble *m);
-static PFNglMultMatrixd pglMultMatrixd;
-#endif
 typedef void (APIENTRY * PFNglRotatef) (GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
 static PFNglRotatef pglRotatef;
 typedef void (APIENTRY * PFNglScalef) (GLfloat x, GLfloat y, GLfloat z);
@@ -343,20 +312,31 @@ typedef void (APIENTRY * PFNglTranslatef) (GLfloat x, GLfloat y, GLfloat z);
 static PFNglTranslatef pglTranslatef;
 
 /* Drawing Functions */
-typedef void (APIENTRY * PFNglBegin) (GLenum mode);
-static PFNglBegin pglBegin;
-typedef void (APIENTRY * PFNglEnd) (void);
-static PFNglEnd pglEnd;
-typedef void (APIENTRY * PFNglVertex3f) (GLfloat x, GLfloat y, GLfloat z);
-static PFNglVertex3f pglVertex3f;
-typedef void (APIENTRY * PFNglNormal3f) (GLfloat x, GLfloat y, GLfloat z);
-static PFNglNormal3f pglNormal3f;
-typedef void (APIENTRY * PFNglColor4f) (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
-static PFNglColor4f pglColor4f;
-typedef void (APIENTRY * PFNglColor4fv) (const GLfloat *v);
-static PFNglColor4fv pglColor4fv;
-typedef void (APIENTRY * PFNglTexCoord2f) (GLfloat s, GLfloat t);
-static PFNglTexCoord2f pglTexCoord2f;
+typedef void (APIENTRY * PFNglColor4ubv) (const GLubyte *v);
+static PFNglColor4ubv pglColor4ubv;
+typedef void (APIENTRY * PFNglVertexPointer) (GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
+static PFNglVertexPointer pglVertexPointer;
+typedef void (APIENTRY * PFNglNormalPointer) (GLenum type, GLsizei stride, const GLvoid *pointer);
+static PFNglNormalPointer pglNormalPointer;
+typedef void (APIENTRY * PFNglTexCoordPointer) (GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
+static PFNglTexCoordPointer pglTexCoordPointer;
+typedef void (APIENTRY * PFNglDrawArrays) (GLenum mode, GLint first, GLsizei count);
+static PFNglDrawArrays pglDrawArrays;
+typedef void (APIENTRY * PFNglDrawElements) (GLenum mode, GLsizei count, GLenum type, const GLvoid *indices);
+static PFNglDrawElements pglDrawElements;
+typedef void (APIENTRY * PFNglEnableClientState) (GLenum cap);
+static PFNglEnableClientState pglEnableClientState;
+typedef void (APIENTRY * PFNglDisableClientState) (GLenum cap);
+static PFNglDisableClientState pglDisableClientState;
+typedef void (APIENTRY * PFNglGenBuffers) (GLsizei n, GLuint *buffers);
+static PFNglGenBuffers pglGenBuffers;
+typedef void (APIENTRY * PFNglBindBuffer) (GLenum target, GLuint buffer);
+static PFNglBindBuffer pglBindBuffer;
+typedef void (APIENTRY * PFNglBufferData) (GLenum target, GLsizei size, const GLvoid *data, GLenum usage);
+static PFNglBufferData pglBufferData;
+typedef void (APIENTRY * PFNglDeleteBuffers) (GLsizei n, const GLuint *buffers);
+static PFNglDeleteBuffers pglDeleteBuffers;
+
 
 /* Lighting */
 typedef void (APIENTRY * PFNglShadeModel) (GLenum mode);
@@ -404,15 +384,16 @@ static PFNglCopyTexSubImage2D pglCopyTexSubImage2D;
 typedef GLint (APIENTRY * PFNgluBuild2DMipmaps) (GLenum target, GLint internalFormat, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *data);
 static PFNgluBuild2DMipmaps pgluBuild2DMipmaps;
 
-#ifndef MINI_GL_COMPATIBILITY
 /* 1.3 functions for multitexturing */
 typedef void (APIENTRY *PFNglActiveTexture) (GLenum);
 static PFNglActiveTexture pglActiveTexture;
 typedef void (APIENTRY *PFNglMultiTexCoord2f) (GLenum, GLfloat, GLfloat);
 static PFNglMultiTexCoord2f pglMultiTexCoord2f;
-#endif
+typedef void (APIENTRY *PFNglMultiTexCoord2fv) (GLenum target, const GLfloat *v);
+static PFNglMultiTexCoord2fv pglMultiTexCoord2fv;
+typedef void (APIENTRY *PFNglClientActiveTexture) (GLenum);
+static PFNglClientActiveTexture pglClientActiveTexture;
 
-#ifndef MINI_GL_COMPATIBILITY
 /* 1.2 Parms */
 /* GL_CLAMP_TO_EDGE_EXT */
 #ifndef GL_CLAMP_TO_EDGE
@@ -433,14 +414,6 @@ static PFNglMultiTexCoord2f pglMultiTexCoord2f;
 #define GL_TEXTURE1 0x84C1
 #endif
 
-#endif
-
-#ifdef MINI_GL_COMPATIBILITY
-#undef GL_CLAMP_TO_EDGE
-#undef GL_TEXTURE_MIN_LOD
-#undef GL_TEXTURE_MAX_LOD
-#endif
-
 boolean SetupGLfunc(void)
 {
 #ifndef STATIC_OPENGL
@@ -453,21 +426,18 @@ boolean SetupGLfunc(void)
 
 	GETOPENGLFUNC(pglClearColor, glClearColor)
 
-	GETOPENGLFUNC(pglClear , glClear)
-	GETOPENGLFUNC(pglColorMask , glColorMask)
-	GETOPENGLFUNC(pglAlphaFunc , glAlphaFunc)
-	GETOPENGLFUNC(pglBlendFunc , glBlendFunc)
-	GETOPENGLFUNC(pglCullFace , glCullFace)
-	GETOPENGLFUNC(pglPolygonMode , glPolygonMode)
-	GETOPENGLFUNC(pglPolygonOffset , glPolygonOffset)
-	GETOPENGLFUNC(pglScissor , glScissor)
-	GETOPENGLFUNC(pglEnable , glEnable)
-	GETOPENGLFUNC(pglDisable , glDisable)
-#ifndef MINI_GL_COMPATIBILITY
-	GETOPENGLFUNC(pglGetDoublev , glGetDoublev)
-#endif
-	GETOPENGLFUNC(pglGetIntegerv , glGetIntegerv)
-	GETOPENGLFUNC(pglGetString , glGetString)
+	GETOPENGLFUNC(pglClear, glClear)
+	GETOPENGLFUNC(pglColorMask, glColorMask)
+	GETOPENGLFUNC(pglAlphaFunc, glAlphaFunc)
+	GETOPENGLFUNC(pglBlendFunc, glBlendFunc)
+	GETOPENGLFUNC(pglCullFace, glCullFace)
+	GETOPENGLFUNC(pglPolygonOffset, glPolygonOffset)
+	GETOPENGLFUNC(pglScissor, glScissor)
+	GETOPENGLFUNC(pglEnable, glEnable)
+	GETOPENGLFUNC(pglDisable, glDisable)
+	GETOPENGLFUNC(pglGetFloatv, glGetFloatv)
+	GETOPENGLFUNC(pglGetIntegerv, glGetIntegerv)
+	GETOPENGLFUNC(pglGetString, glGetString)
 
 	GETOPENGLFUNC(pglClearDepth , glClearDepth)
 	GETOPENGLFUNC(pglDepthFunc , glDepthFunc)
@@ -479,22 +449,19 @@ boolean SetupGLfunc(void)
 	GETOPENGLFUNC(pglPushMatrix , glPushMatrix)
 	GETOPENGLFUNC(pglPopMatrix , glPopMatrix)
 	GETOPENGLFUNC(pglLoadIdentity , glLoadIdentity)
-#ifdef MINI_GL_COMPATIBILITY
 	GETOPENGLFUNC(pglMultMatrixf , glMultMatrixf)
-#else
-	GETOPENGLFUNC(pglMultMatrixd , glMultMatrixd)
-#endif
 	GETOPENGLFUNC(pglRotatef , glRotatef)
 	GETOPENGLFUNC(pglScalef , glScalef)
 	GETOPENGLFUNC(pglTranslatef , glTranslatef)
 
-	GETOPENGLFUNC(pglBegin , glBegin)
-	GETOPENGLFUNC(pglEnd , glEnd)
-	GETOPENGLFUNC(pglVertex3f , glVertex3f)
-	GETOPENGLFUNC(pglNormal3f , glNormal3f)
-	GETOPENGLFUNC(pglColor4f , glColor4f)
-	GETOPENGLFUNC(pglColor4fv , glColor4fv)
-	GETOPENGLFUNC(pglTexCoord2f , glTexCoord2f)
+	GETOPENGLFUNC(pglColor4ubv, glColor4ubv)
+	GETOPENGLFUNC(pglVertexPointer, glVertexPointer)
+	GETOPENGLFUNC(pglNormalPointer, glNormalPointer)
+	GETOPENGLFUNC(pglTexCoordPointer, glTexCoordPointer)
+	GETOPENGLFUNC(pglDrawArrays, glDrawArrays)
+	GETOPENGLFUNC(pglDrawElements, glDrawElements)
+	GETOPENGLFUNC(pglEnableClientState, glEnableClientState)
+	GETOPENGLFUNC(pglDisableClientState, glDisableClientState)
 
 	GETOPENGLFUNC(pglShadeModel , glShadeModel)
 	GETOPENGLFUNC(pglLightfv, glLightfv)
@@ -526,47 +493,19 @@ boolean SetupGLfunc(void)
 }
 
 // This has to be done after the context is created so the version number can be obtained
+// This is stupid -- even some of the oldest usable OpenGL hardware today supports 1.3-level featureset.
 boolean SetupGLFunc13(void)
 {
-#ifdef MINI_GL_COMPATIBILITY
-	return false;
-#else
-	const GLubyte *version = pglGetString(GL_VERSION);
-	int glmajor, glminor;
-
-	gl13 = false;
-	// Parse the GL version
-	if (version != NULL)
-	{
-		if (sscanf((const char*)version, "%d.%d", &glmajor, &glminor) == 2)
-		{
-			// Look, we gotta prepare for the inevitable arrival of GL 2.0 code...
-			if (glmajor == 1 && glminor >= 3)
-				gl13 = true;
-			else if (glmajor > 1)
-				gl13 = true;
-		}
-	}
-
-	if (gl13)
-	{
-		pglActiveTexture = GetGLFunc("glActiveTexture");
-		pglMultiTexCoord2f = GetGLFunc("glMultiTexCoord2f");
-	}
-	else if (isExtAvailable("GL_ARB_multitexture", gl_extensions))
-	{
-		// Get the functions
-		pglActiveTexture  = GetGLFunc("glActiveTextureARB");
-		pglMultiTexCoord2f  = GetGLFunc("glMultiTexCoord2fARB");
-
-		gl13 = true; // This is now true, so the new fade mask stuff can be done, if OpenGL version is less than 1.3, it still uses the old fade stuff.
-		DBG_Printf("GL_ARB_multitexture support: enabled\n");
+	pglActiveTexture = GetGLFunc("glActiveTexture");
+	pglMultiTexCoord2f = GetGLFunc("glMultiTexCoord2f");
+	pglClientActiveTexture = GetGLFunc("glClientActiveTexture");
+	pglMultiTexCoord2fv = GetGLFunc("glMultiTexCoord2fv");
+	pglGenBuffers = GetGLFunc("glGenBuffers");
+	pglBindBuffer = GetGLFunc("glBindBuffer");
+	pglBufferData = GetGLFunc("glBufferData");
+	pglDeleteBuffers = GetGLFunc("glDeleteBuffers");
 
-	}
-	else
-		DBG_Printf("GL_ARB_multitexture support: disabled\n");
 	return true;
-#endif
 }
 
 // -----------------+
@@ -582,48 +521,40 @@ static void SetNoTexture(void)
 	}
 }
 
-static void GLPerspective(GLdouble fovy, GLdouble aspect)
+static void GLPerspective(GLfloat fovy, GLfloat aspect)
 {
-#ifdef MINI_GL_COMPATIBILITY
 	GLfloat m[4][4] =
-#else
-	GLdouble m[4][4] =
-#endif
 	{
 		{ 1.0f, 0.0f, 0.0f, 0.0f},
 		{ 0.0f, 1.0f, 0.0f, 0.0f},
 		{ 0.0f, 0.0f, 1.0f,-1.0f},
 		{ 0.0f, 0.0f, 0.0f, 0.0f},
 	};
-	const GLdouble zNear = NEAR_CLIPPING_PLANE;
-	const GLdouble zFar = FAR_CLIPPING_PLANE;
-	const GLdouble radians = (GLdouble)(fovy / 2.0f * M_PIl / 180.0f);
-	const GLdouble sine = sin(radians);
-	const GLdouble deltaZ = zFar - zNear;
-	GLdouble cotangent;
+	const GLfloat zNear = NEAR_CLIPPING_PLANE;
+	const GLfloat zFar = FAR_CLIPPING_PLANE;
+	const GLfloat radians = (GLfloat)(fovy / 2.0f * M_PIl / 180.0f);
+	const GLfloat sine = sinf(radians);
+	const GLfloat deltaZ = zFar - zNear;
+	GLfloat cotangent;
 
 	if ((fabsf((float)deltaZ) < 1.0E-36f) || fpclassify(sine) == FP_ZERO || fpclassify(aspect) == FP_ZERO)
 	{
 		return;
 	}
-	cotangent = cos(radians) / sine;
+	cotangent = cosf(radians) / sine;
 
 	m[0][0] = cotangent / aspect;
 	m[1][1] = cotangent;
 	m[2][2] = -(zFar + zNear) / deltaZ;
 	m[3][2] = -2.0f * zNear * zFar / deltaZ;
-#ifdef MINI_GL_COMPATIBILITY
+
 	pglMultMatrixf(&m[0][0]);
-#else
-	pglMultMatrixd(&m[0][0]);
-#endif
 }
 
-#ifndef MINI_GL_COMPATIBILITY
-static void GLProject(GLdouble objX, GLdouble objY, GLdouble objZ,
-                      GLdouble* winX, GLdouble* winY, GLdouble* winZ)
+static void GLProject(GLfloat objX, GLfloat objY, GLfloat objZ,
+                      GLfloat* winX, GLfloat* winY, GLfloat* winZ)
 {
-	GLdouble in[4], out[4];
+	GLfloat in[4], out[4];
 	int i;
 
 	for (i=0; i<4; i++)
@@ -659,7 +590,6 @@ static void GLProject(GLdouble objX, GLdouble objY, GLdouble objZ,
 	*winY=in[1];
 	*winZ=in[2];
 }
-#endif
 
 // -----------------+
 // SetModelView     :
@@ -690,10 +620,8 @@ void SetModelView(GLint w, GLint h)
 	//pglScalef(1.0f, 320.0f/200.0f, 1.0f);  // gr_scalefrustum (ORIGINAL_ASPECT)
 
 	// added for new coronas' code (without depth buffer)
-#ifndef MINI_GL_COMPATIBILITY
 	pglGetIntegerv(GL_VIEWPORT, viewport);
-	pglGetDoublev(GL_PROJECTION_MATRIX, projMatrix);
-#endif
+	pglGetFloatv(GL_PROJECTION_MATRIX, projMatrix);
 }
 
 
@@ -718,17 +646,15 @@ void SetStates(void)
 	//pglShadeModel(GL_FLAT);
 
 	pglEnable(GL_TEXTURE_2D);      // two-dimensional texturing
-#ifndef KOS_GL_COMPATIBILITY
+
 	pglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
 
 	pglAlphaFunc(GL_NOTEQUAL, 0.0f);
-#endif
+
 	//pglBlendFunc(GL_ONE, GL_ZERO); // copy pixel to frame buffer (opaque)
 	pglEnable(GL_BLEND);           // enable color blending
 
-#ifndef KOS_GL_COMPATIBILITY
 	pglColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
-#endif
 
 	//pglDisable(GL_DITHER);         // faB: ??? (undocumented in OpenGL 1.1)
 	                              // Hurdler: yes, it is!
@@ -753,14 +679,10 @@ void SetStates(void)
 	//tex_downloaded = NOTEXTURE_NUM;
 	//pglTexImage2D(GL_TEXTURE_2D, 0, 4, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, Data);
 
-#ifndef KOS_GL_COMPATIBILITY
 	pglPolygonOffset(-1.0f, -1.0f);
-#endif
 
 	//pglEnable(GL_CULL_FACE);
 	//pglCullFace(GL_FRONT);
-	//pglPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
-	//pglPolygonMode(GL_FRONT, GL_LINE);
 
 	//glFogi(GL_FOG_MODE, GL_EXP);
 	//pglHint(GL_FOG_HINT, GL_FASTEST);
@@ -776,9 +698,7 @@ void SetStates(void)
 	// bp : when no t&l :)
 	pglLoadIdentity();
 	pglScalef(1.0f, 1.0f, -1.0f);
-#ifndef MINI_GL_COMPATIBILITY
-	pglGetDoublev(GL_MODELVIEW_MATRIX, modelMatrix); // added for new coronas' code (without depth buffer)
-#endif
+	pglGetFloatv(GL_MODELVIEW_MATRIX, modelMatrix); // added for new coronas' code (without depth buffer)
 }
 
 
@@ -882,14 +802,6 @@ EXPORT void HWRAPI(ClearMipMapCache) (void)
 EXPORT void HWRAPI(ReadRect) (INT32 x, INT32 y, INT32 width, INT32 height,
                                 INT32 dst_stride, UINT16 * dst_data)
 {
-#ifdef KOS_GL_COMPATIBILITY
-	(void)x;
-	(void)y;
-	(void)width;
-	(void)height;
-	(void)dst_stride;
-	(void)dst_data;
-#else
 	INT32 i;
 	// DBG_Printf ("ReadRect()\n");
 	if (dst_stride == width*3)
@@ -931,7 +843,6 @@ EXPORT void HWRAPI(ReadRect) (INT32 x, INT32 y, INT32 width, INT32 height,
 		}
 		free(image);
 	}
-#endif
 }
 
 
@@ -952,10 +863,8 @@ EXPORT void HWRAPI(GClipRect) (INT32 minx, INT32 miny, INT32 maxx, INT32 maxy, f
 	pglMatrixMode(GL_MODELVIEW);
 
 	// added for new coronas' code (without depth buffer)
-#ifndef MINI_GL_COMPATIBILITY
 	pglGetIntegerv(GL_VIEWPORT, viewport);
-	pglGetDoublev(GL_PROJECTION_MATRIX, projMatrix);
-#endif
+	pglGetFloatv(GL_PROJECTION_MATRIX, projMatrix);
 }
 
 
@@ -989,6 +898,8 @@ EXPORT void HWRAPI(ClearBuffer) (FBOOLEAN ColorMask,
 	SetBlend(DepthMask ? PF_Occlude | CurrentPolyFlags : CurrentPolyFlags&~PF_Occlude);
 
 	pglClear(ClearMask);
+	pglEnableClientState(GL_VERTEX_ARRAY); // We always use this one
+	pglEnableClientState(GL_TEXTURE_COORD_ARRAY); // And mostly this one, too
 }
 
 
@@ -999,54 +910,35 @@ EXPORT void HWRAPI(Draw2DLine) (F2DCoord * v1,
                                    F2DCoord * v2,
                                    RGBA_t Color)
 {
-	GLRGBAFloat c;
-
 	// DBG_Printf ("DrawLine() (%f %f %f) %d\n", v1->x, -v1->y, -v1->z, v1->argb);
-#ifdef MINI_GL_COMPATIBILITY
-	GLfloat px1, px2, px3, px4;
-	GLfloat py1, py2, py3, py4;
+	GLfloat p[12];
 	GLfloat dx, dy;
 	GLfloat angle;
-#endif
 
 	// BP: we should reflect the new state in our variable
 	//SetBlend(PF_Modulated|PF_NoTexture);
 
 	pglDisable(GL_TEXTURE_2D);
 
-	c.red   = byte2float[Color.s.red];
-	c.green = byte2float[Color.s.green];
-	c.blue  = byte2float[Color.s.blue];
-	c.alpha = byte2float[Color.s.alpha];
-
-#ifndef MINI_GL_COMPATIBILITY
-	pglColor4fv(&c.red);    // is in RGBA float format
-	pglBegin(GL_LINES);
-		pglVertex3f(v1->x, -v1->y, 1.0f);
-		pglVertex3f(v2->x, -v2->y, 1.0f);
-	pglEnd();
-#else
-	if (v2->x != v1->x)
+	// This is the preferred, 'modern' way of rendering lines -- creating a polygon.
+	if (fabsf(v2->x - v1->x) > FLT_EPSILON)
 		angle = (float)atan((v2->y-v1->y)/(v2->x-v1->x));
 	else
-		angle = N_PI_DEMI;
+		angle = (float)N_PI_DEMI;
 	dx = (float)sin(angle) / (float)screen_width;
 	dy = (float)cos(angle) / (float)screen_height;
 
-	px1 = v1->x - dx;  py1 = v1->y + dy;
-	px2 = v2->x - dx;  py2 = v2->y + dy;
-	px3 = v2->x + dx;  py3 = v2->y - dy;
-	px4 = v1->x + dx;  py4 = v1->y - dy;
-
-	pglColor4f(c.red, c.green, c.blue, c.alpha);
-	pglBegin(GL_TRIANGLE_FAN);
-		pglVertex3f(px1, -py1, 1);
-		pglVertex3f(px2, -py2, 1);
-		pglVertex3f(px3, -py3, 1);
-		pglVertex3f(px4, -py4, 1);
-	pglEnd();
-#endif
+	p[0] = v1->x - dx;  p[1] = -(v1->y + dy); p[2] = 1;
+	p[3] = v2->x - dx;  p[4] = -(v2->y + dy); p[5] = 1;
+	p[6] = v2->x + dx;  p[7] = -(v2->y - dy); p[8] = 1;
+	p[9] = v1->x + dx;  p[10] = -(v1->y - dy); p[11] = 1;
 
+	pglDisableClientState(GL_TEXTURE_COORD_ARRAY);
+	pglColor4ubv((GLubyte*)&Color.s);
+	pglVertexPointer(3, GL_FLOAT, 0, p);
+	pglDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+	pglEnableClientState(GL_TEXTURE_COORD_ARRAY);
 	pglEnable(GL_TEXTURE_2D);
 }
 
@@ -1075,60 +967,42 @@ EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags)
 			switch (PolyFlags & PF_Blending) {
 				case PF_Translucent & PF_Blending:
 					pglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // alpha = level of transparency
-#ifndef KOS_GL_COMPATIBILITY
 					pglAlphaFunc(GL_NOTEQUAL, 0.0f);
-#endif
 					break;
 				case PF_Masked & PF_Blending:
 					// Hurdler: does that mean lighting is only made by alpha src?
 					// it sounds ok, but not for polygonsmooth
 					pglBlendFunc(GL_SRC_ALPHA, GL_ZERO);                // 0 alpha = holes in texture
-#ifndef KOS_GL_COMPATIBILITY
 					pglAlphaFunc(GL_GREATER, 0.5f);
-#endif
 					break;
 				case PF_Additive & PF_Blending:
-#ifdef ATI_RAGE_PRO_COMPATIBILITY
-					pglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // alpha = level of transparency
-#else
 					pglBlendFunc(GL_SRC_ALPHA, GL_ONE);                 // src * alpha + dest
-#endif
-#ifndef KOS_GL_COMPATIBILITY
 					pglAlphaFunc(GL_NOTEQUAL, 0.0f);
-#endif
 					break;
 				case PF_Environment & PF_Blending:
 					pglBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
-#ifndef KOS_GL_COMPATIBILITY
 					pglAlphaFunc(GL_NOTEQUAL, 0.0f);
-#endif
 					break;
 				case PF_Substractive & PF_Blending:
 					// good for shadow
-					// not realy but what else ?
+					// not really but what else ?
 					pglBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
-#ifndef KOS_GL_COMPATIBILITY
 					pglAlphaFunc(GL_NOTEQUAL, 0.0f);
-#endif
 					break;
 				case PF_Fog & PF_Fog:
 					// Sryder: Fog
 					// multiplies input colour by input alpha, and destination colour by input colour, then adds them
 					pglBlendFunc(GL_SRC_ALPHA, GL_SRC_COLOR);
-#ifndef KOS_GL_COMPATIBILITY
 					pglAlphaFunc(GL_NOTEQUAL, 0.0f);
-#endif
 					break;
 				default : // must be 0, otherwise it's an error
 					// No blending
 					pglBlendFunc(GL_ONE, GL_ZERO);   // the same as no blending
-#ifndef KOS_GL_COMPATIBILITY
 					pglAlphaFunc(GL_GREATER, 0.5f);
-#endif
 					break;
 			}
 		}
-#ifndef KOS_GL_COMPATIBILITY
+
 		if (Xor & PF_NoAlphaTest)
 		{
 			if (PolyFlags & PF_NoAlphaTest)
@@ -1144,7 +1018,7 @@ EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags)
 			else
 				pglDisable(GL_POLYGON_OFFSET_FILL);
 		}
-#endif
+
 		if (Xor&PF_NoDepthTest)
 		{
 			if (PolyFlags & PF_NoDepthTest)
@@ -1171,17 +1045,13 @@ EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags)
 				pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
 		}
 
-#ifdef KOS_GL_COMPATIBILITY
-		if (Xor&PF_Modulated && !(PolyFlags & PF_Modulated))
-			pglColor4f(1.0f, 1.0f, 1.0f, 1.0f);
-#else
 		if (Xor&PF_Modulated)
 		{
 #if defined (__unix__) || defined (UNIXCOMMON)
 			if (oglflags & GLF_NOTEXENV)
 			{
 				if (!(PolyFlags & PF_Modulated))
-					pglColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+					pglColor4ubv(white);
 			}
 			else
 #endif
@@ -1194,7 +1064,6 @@ EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags)
 				pglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
 			}
 		}
-#endif
 
 		if (Xor & PF_Occlude) // depth test but (no) depth write
 		{
@@ -1248,11 +1117,7 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 	else
 	{
 		// Download a mipmap
-#ifdef KOS_GL_COMPATIBILITY
-		static GLushort tex[2048*2048];
-#else
 		static RGBA_t   tex[2048*2048];
-#endif
 		const GLvoid   *ptex = tex;
 		INT32             w, h;
 
@@ -1261,112 +1126,6 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 		w = pTexInfo->width;
 		h = pTexInfo->height;
 
-#ifdef USE_PALETTED_TEXTURE
-		if (glColorTableEXT &&
-			(pTexInfo->grInfo.format == GR_TEXFMT_P_8) &&
-			!(pTexInfo->flags & TF_CHROMAKEYED))
-		{
-			// do nothing here.
-			// Not a problem with MiniGL since we don't use paletted texture
-		}
-		else
-#endif
-#ifdef KOS_GL_COMPATIBILITY
-		if ((pTexInfo->grInfo.format == GR_TEXFMT_P_8) ||
-			(pTexInfo->grInfo.format == GR_TEXFMT_AP_88))
-		{
-			const GLubyte *pImgData = (const GLubyte *)pTexInfo->grInfo.data;
-			INT32 i, j;
-
-			for (j = 0; j < h; j++)
-			{
-				for (i = 0; i < w; i++)
-				{
-					if ((*pImgData == HWR_PATCHES_CHROMAKEY_COLORINDEX) &&
-					    (pTexInfo->flags & TF_CHROMAKEYED))
-					{
-						tex[w*j+i] = 0;
-					}
-					else
-					{
-						if (pTexInfo->grInfo.format == GR_TEXFMT_AP_88 && !(pTexInfo->flags & TF_CHROMAKEYED))
-							tex[w*j+i] = 0;
-						else
-							tex[w*j+i] = (myPaletteData[*pImgData].s.alpha>>4)<<12;
-
-						tex[w*j+i] |= (myPaletteData[*pImgData].s.red  >>4)<<8;
-						tex[w*j+i] |= (myPaletteData[*pImgData].s.green>>4)<<4;
-						tex[w*j+i] |= (myPaletteData[*pImgData].s.blue >>4);
-					}
-
-					pImgData++;
-
-					if (pTexInfo->grInfo.format == GR_TEXFMT_AP_88)
-					{
-						if (!(pTexInfo->flags & TF_CHROMAKEYED))
-							tex[w*j+i] |= ((*pImgData)>>4)<<12;
-						pImgData++;
-					}
-
-				}
-			}
-		}
-		else if (pTexInfo->grInfo.format == GR_RGBA)
-		{
-			// corona test : passed as ARGB 8888, which is not in glide formats
-			// Hurdler: not used for coronas anymore, just for dynamic lighting
-			const RGBA_t *pImgData = (const RGBA_t *)pTexInfo->grInfo.data;
-			INT32 i, j;
-
-			for (j = 0; j < h; j++)
-			{
-				for (i = 0; i < w; i++)
-				{
-					tex[w*j+i]  = (pImgData->s.alpha>>4)<<12;
-					tex[w*j+i] |= (pImgData->s.red  >>4)<<8;
-					tex[w*j+i] |= (pImgData->s.green>>4)<<4;
-					tex[w*j+i] |= (pImgData->s.blue >>4);
-					pImgData++;
-				}
-			}
-		}
-		else if (pTexInfo->grInfo.format == GR_TEXFMT_ALPHA_INTENSITY_88)
-		{
-			const GLubyte *pImgData = (const GLubyte *)pTexInfo->grInfo.data;
-			INT32 i, j;
-
-			for (j = 0; j < h; j++)
-			{
-				for (i = 0; i < w; i++)
-				{
-					const GLubyte sID = (*pImgData)>>4;
-					tex[w*j+i] = sID<<8 | sID<<4 | sID;
-					pImgData++;
-					tex[w*j+i] |= ((*pImgData)>>4)<<12;
-					pImgData++;
-				}
-			}
-		}
-		else if (pTexInfo->grInfo.format == GR_TEXFMT_ALPHA_8) // Used for fade masks
-		{
-			const GLubyte *pImgData = (const GLubyte *)pTexInfo->grInfo.data;
-			INT32 i, j;
-
-			for (j = 0; j < h; j++)
-			{
-				for (i = 0; i < w; i++)
-				{
-					tex[w*j+i]  = (pImgData>>4)<<12;
-					tex[w*j+i] |= (255>>4)<<8;
-					tex[w*j+i] |= (255>>4)<<4;
-					tex[w*j+i] |= (255>>4);
-					pImgData++;
-				}
-			}
-		}
-		else
-			DBG_Printf ("SetTexture(bad format) %ld\n", pTexInfo->grInfo.format);
-#else
 		if ((pTexInfo->grInfo.format == GR_TEXFMT_P_8) ||
 			(pTexInfo->grInfo.format == GR_TEXFMT_AP_88))
 		{
@@ -1449,7 +1208,6 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 		}
 		else
 			DBG_Printf ("SetTexture(bad format) %ld\n", pTexInfo->grInfo.format);
-#endif
 
 		pTexInfo->downloaded = NextTexAvail++;
 		tex_downloaded = pTexInfo->downloaded;
@@ -1458,13 +1216,8 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 		// disable texture filtering on any texture that has holes so there's no dumb borders or blending issues
 		if (pTexInfo->flags & TF_TRANSPARENT)
 		{
-#ifdef KOS_GL_COMPATIBILITY
-			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NONE);
-			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NONE);
-#else
 			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-#endif
 		}
 		else
 		{
@@ -1472,29 +1225,6 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
 		}
 
-#ifdef KOS_GL_COMPATIBILITY
-		pglTexImage2D(GL_TEXTURE_2D, 0, GL_ARGB4444, w, h, 0, GL_ARGB4444, GL_UNSIGNED_BYTE, ptex);
-#else
-#ifdef MINI_GL_COMPATIBILITY
-		//if (pTexInfo->grInfo.format == GR_TEXFMT_ALPHA_INTENSITY_88)
-			//pglTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
-		//else
-			if (MipMap)
-				pgluBuild2DMipmaps(GL_TEXTURE_2D, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
-			else
-				pglTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
-#else
-#ifdef USE_PALETTED_TEXTURE
-			//Hurdler: not really supported and not tested recently
-		if (glColorTableEXT &&
-			(pTexInfo->grInfo.format == GR_TEXFMT_P_8) &&
-			!(pTexInfo->flags & TF_CHROMAKEYED))
-		{
-			glColorTableEXT(GL_TEXTURE_2D, GL_RGB8, 256, GL_RGB, GL_UNSIGNED_BYTE, palette_tex);
-			pglTexImage2D(GL_TEXTURE_2D, 0, GL_COLOR_INDEX8_EXT, w, h, 0, GL_COLOR_INDEX, GL_UNSIGNED_BYTE, pTexInfo->grInfo.data);
-		}
-		else
-#endif
 		if (pTexInfo->grInfo.format == GR_TEXFMT_ALPHA_INTENSITY_88)
 		{
 			//pglTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
@@ -1554,8 +1284,6 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 			else
 				pglTexImage2D(GL_TEXTURE_2D, 0, textureformatGL, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 		}
-#endif
-#endif
 
 		if (pTexInfo->flags & TF_WRAPX)
 			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
@@ -1579,19 +1307,6 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 		else // initialisation de la liste
 			gr_cachetail = gr_cachehead =  pTexInfo;
 	}
-#ifdef MINI_GL_COMPATIBILITY
-	switch (pTexInfo->flags)
-	{
-		case 0 :
-			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
-			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
-			break;
-		default:
-			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
-			pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
-			break;
-	}
-#endif
 }
 
 
@@ -1605,57 +1320,31 @@ EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo  *pSurf,
                                     FBITFIELD     PolyFlags)
 {
 	FUINT i;
-#ifndef MINI_GL_COMPATIBILITY
 	FUINT j;
-#endif
-	GLRGBAFloat c = {0,0,0,0};
 
-#ifdef MINI_GL_COMPATIBILITY
-	if (PolyFlags & PF_Corona)
-		PolyFlags &= ~PF_NoDepthTest;
-#else
 	if ((PolyFlags & PF_Corona) && (oglflags & GLF_NOZBUFREAD))
 		PolyFlags &= ~(PF_NoDepthTest|PF_Corona);
-#endif
 
 	SetBlend(PolyFlags);    //TODO: inline (#pragma..)
 
 	// If Modulated, mix the surface colour to the texture
 	if ((CurrentPolyFlags & PF_Modulated) && pSurf)
-	{
-		if (pal_col)
-		{ // hack for non-palettized mode
-			c.red   = (const_pal_col.red  +byte2float[pSurf->FlatColor.s.red])  /2.0f;
-			c.green = (const_pal_col.green+byte2float[pSurf->FlatColor.s.green])/2.0f;
-			c.blue  = (const_pal_col.blue +byte2float[pSurf->FlatColor.s.blue]) /2.0f;
-			c.alpha = byte2float[pSurf->FlatColor.s.alpha];
-		}
-		else
-		{
-			c.red   = byte2float[pSurf->FlatColor.s.red];
-			c.green = byte2float[pSurf->FlatColor.s.green];
-			c.blue  = byte2float[pSurf->FlatColor.s.blue];
-			c.alpha = byte2float[pSurf->FlatColor.s.alpha];
-		}
-
-#ifdef MINI_GL_COMPATIBILITY
-		pglColor4f(c.red, c.green, c.blue, c.alpha);
-#else
-		pglColor4fv(&c.red);    // is in RGBA float format
-#endif
-	}
+		pglColor4ubv((GLubyte*)&pSurf->FlatColor.s);
 
 	// this test is added for new coronas' code (without depth buffer)
 	// I think I should do a separate function for drawing coronas, so it will be a little faster
-#ifndef MINI_GL_COMPATIBILITY
 	if (PolyFlags & PF_Corona) // check to see if we need to draw the corona
 	{
 		//rem: all 8 (or 8.0f) values are hard coded: it can be changed to a higher value
 		GLfloat     buf[8][8];
-		GLdouble    cx, cy, cz;
-		GLdouble    px = 0.0f, py = 0.0f, pz = -1.0f;
+		GLfloat    cx, cy, cz;
+		GLfloat    px = 0.0f, py = 0.0f, pz = -1.0f;
 		GLfloat     scalef = 0.0f;
 
+		GLubyte c[4];
+
+		float alpha;
+
 		cx = (pOutVerts[0].x + pOutVerts[2].x) / 2.0f; // we should change the coronas' ...
 		cy = (pOutVerts[0].y + pOutVerts[2].y) / 2.0f; // ... code so its only done once.
 		cz = pOutVerts[0].z;
@@ -1691,22 +1380,20 @@ EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo  *pSurf,
 		if (scalef < 0.05f)
 			return;
 
-		c.alpha *= scalef; // change the alpha value (it seems better than changing the size of the corona)
-		pglColor4fv(&c.red);
-	}
-#endif
-	if (PolyFlags & PF_MD2)
-		return;
+		// GLubyte c[4];
+		c[0] = pSurf->FlatColor.s.red;
+		c[1] = pSurf->FlatColor.s.green;
+		c[2] = pSurf->FlatColor.s.blue;
 
-	pglBegin(GL_TRIANGLE_FAN);
-	for (i = 0; i < iNumPts; i++)
-	{
-		pglTexCoord2f(pOutVerts[i].sow, pOutVerts[i].tow);
-		//Hurdler: test code: -pOutVerts[i].z => pOutVerts[i].z
-		pglVertex3f(pOutVerts[i].x, pOutVerts[i].y, pOutVerts[i].z);
-		//pglVertex3f(pOutVerts[i].x, pOutVerts[i].y, -pOutVerts[i].z);
+		alpha = byte2float[pSurf->FlatColor.s.alpha];
+		alpha *= scalef; // change the alpha value (it seems better than changing the size of the corona)
+		c[3] = (unsigned char)(alpha * 255);
+		pglColor4ubv(c);
 	}
-	pglEnd();
+
+	pglVertexPointer(3, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].x);
+	pglTexCoordPointer(2, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].sow);
+	pglDrawArrays(GL_TRIANGLE_FAN, 0, iNumPts);
 
 	if (PolyFlags & PF_RemoveYWrap)
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
@@ -1737,15 +1424,6 @@ EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value)
 		}
 #endif
 
-		case HWD_SET_PALETTECOLOR:
-		{
-			pal_col = Value;
-			const_pal_col.blue  = byte2float[((Value>>16)&0xff)];
-			const_pal_col.green = byte2float[((Value>>8)&0xff)];
-			const_pal_col.red   = byte2float[((Value)&0xff)];
-			break;
-		}
-
 		case HWD_SET_FOG_COLOR:
 		{
 			GLfloat fogcolor[4];
@@ -1787,42 +1465,9 @@ EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value)
 				pglDisable(GL_FOG);
 			break;
 
-		case HWD_SET_POLYGON_SMOOTH:
-#ifdef KOS_GL_COMPATIBILITY // GL_POLYGON_SMOOTH_HINT
-			if (Value)
-				pglHint(GL_POLYGON_SMOOTH_HINT,GL_NICEST);
-			else
-				pglHint(GL_POLYGON_SMOOTH_HINT,GL_FASTEST);
-#else
-			if (Value)
-				pglEnable(GL_POLYGON_SMOOTH);
-			else
-				pglDisable(GL_POLYGON_SMOOTH);
-#endif
-			break;
-
 		case HWD_SET_TEXTUREFILTERMODE:
 			switch (Value)
 			{
-#ifdef KOS_GL_COMPATIBILITY
-				case HWD_SET_TEXTUREFILTER_TRILINEAR:
-				case HWD_SET_TEXTUREFILTER_BILINEAR:
-					min_filter = mag_filter = GL_FILTER_BILINEAR;
-					break;
-				case HWD_SET_TEXTUREFILTER_POINTSAMPLED:
-					min_filter = mag_filter = GL_FILTER_NONE;
-				case HWD_SET_TEXTUREFILTER_MIXED1:
-					min_filter = GL_FILTER_NONE;
-					mag_filter = GL_LINEAR;
-				case HWD_SET_TEXTUREFILTER_MIXED2:
-					min_filter = GL_LINEAR;
-					mag_filter = GL_FILTER_NONE;
-					break;
-				case HWD_SET_TEXTUREFILTER_MIXED3:
-					min_filter = GL_FILTER_BILINEAR;
-					mag_filter = GL_FILTER_NONE;
-					break;
-#elif !defined (MINI_GL_COMPATIBILITY)
 				case HWD_SET_TEXTUREFILTER_TRILINEAR:
 					min_filter = GL_LINEAR_MIPMAP_LINEAR;
 					mag_filter = GL_LINEAR;
@@ -1851,14 +1496,9 @@ EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value)
 					mag_filter = GL_NEAREST;
 					MipMap = GL_TRUE;
 					break;
-#endif
 				default:
-#ifdef KOS_GL_COMPATIBILITY
-					min_filter = mag_filter = GL_FILTER_NONE;
-#else
 					mag_filter = GL_LINEAR;
 					min_filter = GL_NEAREST;
-#endif
 			}
 			if (!pgluBuild2DMipmaps)
 			{
@@ -1879,20 +1519,218 @@ EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value)
 	}
 }
 
-static  void DrawMD2Ex(INT32 *gl_cmd_buffer, md2_frame_t *frame, INT32 duration, INT32 tics, md2_frame_t *nextframe, FTransform *pos, float scale, UINT8 flipped, UINT8 *color)
+static float *vertBuffer = NULL;
+static float *normBuffer = NULL;
+static size_t lerpBufferSize = 0;
+static short *vertTinyBuffer = NULL;
+static char *normTinyBuffer = NULL;
+static size_t lerpTinyBufferSize = 0;
+
+// Static temporary buffer for doing frame interpolation
+// 'size' is the vertex size
+static void AllocLerpBuffer(size_t size)
+{
+	if (lerpBufferSize >= size)
+		return;
+
+	if (vertBuffer != NULL)
+		free(vertBuffer);
+
+	if (normBuffer != NULL)
+		free(normBuffer);
+
+	lerpBufferSize = size;
+	vertBuffer = malloc(lerpBufferSize);
+	normBuffer = malloc(lerpBufferSize);
+}
+
+// Static temporary buffer for doing frame interpolation
+// 'size' is the vertex size
+static void AllocLerpTinyBuffer(size_t size)
+{
+	if (lerpTinyBufferSize >= size)
+		return;
+
+	if (vertTinyBuffer != NULL)
+		free(vertTinyBuffer);
+
+	if (normTinyBuffer != NULL)
+		free(normTinyBuffer);
+
+	lerpTinyBufferSize = size;
+	vertTinyBuffer = malloc(lerpTinyBufferSize);
+	normTinyBuffer = malloc(lerpTinyBufferSize / 2);
+}
+
+#ifndef GL_STATIC_DRAW
+#define GL_STATIC_DRAW 0x88E4
+#endif
+
+#ifndef GL_ARRAY_BUFFER
+#define GL_ARRAY_BUFFER 0x8892
+#endif
+
+static void CreateModelVBO(mesh_t *mesh, mdlframe_t *frame)
+{
+	int bufferSize = sizeof(vbo64_t)*mesh->numTriangles * 3;
+	vbo64_t *buffer = (vbo64_t*)malloc(bufferSize);
+	vbo64_t *bufPtr = buffer;
+
+	float *vertPtr = frame->vertices;
+	float *normPtr = frame->normals;
+	float *tanPtr = frame->tangents;
+	float *uvPtr = mesh->uvs;
+	float *lightPtr = mesh->lightuvs;
+	char *colorPtr = frame->colors;
+
+	int i;
+	for (i = 0; i < mesh->numTriangles * 3; i++)
+	{
+		bufPtr->x = *vertPtr++;
+		bufPtr->y = *vertPtr++;
+		bufPtr->z = *vertPtr++;
+
+		bufPtr->nx = *normPtr++;
+		bufPtr->ny = *normPtr++;
+		bufPtr->nz = *normPtr++;
+
+		bufPtr->s0 = *uvPtr++;
+		bufPtr->t0 = *uvPtr++;
+
+		if (tanPtr != NULL)
+		{
+			bufPtr->tan0 = *tanPtr++;
+			bufPtr->tan1 = *tanPtr++;
+			bufPtr->tan2 = *tanPtr++;
+		}
+
+		if (lightPtr != NULL)
+		{
+			bufPtr->s1 = *lightPtr++;
+			bufPtr->t1 = *lightPtr++;
+		}
+
+		if (colorPtr)
+		{
+			bufPtr->r = *colorPtr++;
+			bufPtr->g = *colorPtr++;
+			bufPtr->b = *colorPtr++;
+			bufPtr->a = *colorPtr++;
+		}
+		else
+		{
+			bufPtr->r = 255;
+			bufPtr->g = 255;
+			bufPtr->b = 255;
+			bufPtr->a = 255;
+		}
+
+		bufPtr++;
+	}
+
+	pglGenBuffers(1, &frame->vboID);
+	pglBindBuffer(GL_ARRAY_BUFFER, frame->vboID);
+	pglBufferData(GL_ARRAY_BUFFER, bufferSize, buffer, GL_STATIC_DRAW);
+	free(buffer);
+}
+
+static void CreateModelVBOTiny(mesh_t *mesh, tinyframe_t *frame)
+{
+	int bufferSize = sizeof(vbotiny_t)*mesh->numTriangles * 3;
+	vbotiny_t *buffer = (vbotiny_t*)malloc(bufferSize);
+	vbotiny_t *bufPtr = buffer;
+
+	short *vertPtr = frame->vertices;
+	char *normPtr = frame->normals;
+	float *uvPtr = mesh->uvs;
+	char *tanPtr = frame->tangents;
+
+	int i;
+	for (i = 0; i < mesh->numVertices; i++)
+	{
+		bufPtr->x = *vertPtr++;
+		bufPtr->y = *vertPtr++;
+		bufPtr->z = *vertPtr++;
+
+		bufPtr->nx = *normPtr++;
+		bufPtr->ny = *normPtr++;
+		bufPtr->nz = *normPtr++;
+
+		bufPtr->s0 = *uvPtr++;
+		bufPtr->t0 = *uvPtr++;
+
+		if (tanPtr)
+		{
+			bufPtr->tanx = *tanPtr++;
+			bufPtr->tany = *tanPtr++;
+			bufPtr->tanz = *tanPtr++;
+		}
+
+		bufPtr++;
+	}
+
+	pglGenBuffers(1, &frame->vboID);
+	pglBindBuffer(GL_ARRAY_BUFFER, frame->vboID);
+	pglBufferData(GL_ARRAY_BUFFER, bufferSize, buffer, GL_STATIC_DRAW);
+	free(buffer);
+}
+
+EXPORT void HWRAPI(CreateModelVBOs) (model_t *model)
+{
+	int i;
+	for (i = 0; i < model->numMeshes; i++)
+	{
+		mesh_t *mesh = &model->meshes[i];
+
+		if (mesh->frames)
+		{
+			int j;
+			for (j = 0; j < model->meshes[i].numFrames; j++)
+			{
+				mdlframe_t *frame = &mesh->frames[j];
+				if (frame->vboID)
+					pglDeleteBuffers(1, &frame->vboID);
+				frame->vboID = 0;
+				CreateModelVBO(mesh, frame);
+			}
+		}
+		else if (mesh->tinyframes)
+		{
+			int j;
+			for (j = 0; j < model->meshes[i].numFrames; j++)
+			{
+				tinyframe_t *frame = &mesh->tinyframes[j];
+				if (frame->vboID)
+					pglDeleteBuffers(1, &frame->vboID);
+				frame->vboID = 0;
+				CreateModelVBOTiny(mesh, frame);
+			}
+		}
+	}
+}
+
+#define BUFFER_OFFSET(i) ((char*)NULL + (i))
+
+static void DrawModelEx(model_t *model, INT32 frameIndex, INT32 duration, INT32 tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 *color)
 {
-	INT32     val, count, pindex;
-	GLfloat s, t;
 	GLfloat ambient[4];
 	GLfloat diffuse[4];
 
 	float pol = 0.0f;
-	float scalex = scale, scaley = scale, scalez = scale;
+	float scalex, scaley, scalez;
+
+	boolean useTinyFrames;
+
+	int i;
 
 	// Because Otherwise, scaling the screen negatively vertically breaks the lighting
-#ifndef KOS_GL_COMPATIBILITY
 	GLfloat LightPos[] = {0.0f, 1.0f, 0.0f, 0.0f};
-#endif
+
+	// Affect input model scaling
+	scale *= 0.5f;
+	scalex = scale;
+	scaley = scale;
+	scalez = scale;
 
 	if (duration != 0 && duration != -1 && tics != -1) // don't interpolate if instantaneous or infinite in length
 	{
@@ -1927,7 +1765,9 @@ static  void DrawMD2Ex(INT32 *gl_cmd_buffer, md2_frame_t *frame, INT32 duration,
 	}
 
 	pglEnable(GL_CULL_FACE);
+	pglEnable(GL_NORMALIZE);
 
+#ifdef USE_FTRANSFORM_MIRROR
 	// flipped is if the object is flipped
 	// pos->flip is if the screen is flipped vertically
 	// pos->mirror is if the screen is flipped horizontally
@@ -1939,10 +1779,19 @@ static  void DrawMD2Ex(INT32 *gl_cmd_buffer, md2_frame_t *frame, INT32 duration,
 		else
 			pglCullFace(GL_BACK);
 	}
+#else
+	// pos->flip is if the screen is flipped too
+	if (flipped != pos->flip) // If either are active, but not both, invert the model's culling
+	{
+		pglCullFace(GL_FRONT);
+	}
+	else
+	{
+		pglCullFace(GL_BACK);
+	}
+#endif
 
-#ifndef KOS_GL_COMPATIBILITY
 	pglLightfv(GL_LIGHT0, GL_POSITION, LightPos);
-#endif
 
 	pglShadeModel(GL_SMOOTH);
 	if (color)
@@ -1963,93 +1812,139 @@ static  void DrawMD2Ex(INT32 *gl_cmd_buffer, md2_frame_t *frame, INT32 duration,
 	pglTranslatef(pos->x, pos->z, pos->y);
 	if (flipped)
 		scaley = -scaley;
-	pglRotatef(pos->anglez, 0.0f, 0.0f, -1.0f);
+#ifdef USE_FTRANSFORM_ANGLEZ
+	pglRotatef(pos->anglez, 0.0f, 0.0f, -1.0f); // rotate by slope from Kart
+#endif
 	pglRotatef(pos->anglex, -1.0f, 0.0f, 0.0f);
 	pglRotatef(pos->angley, 0.0f, -1.0f, 0.0f);
 
-	val = *gl_cmd_buffer++;
+	pglScalef(scalex, scaley, scalez);
+
+	useTinyFrames = model->meshes[0].tinyframes != NULL;
+
+	if (useTinyFrames)
+		pglScalef(1 / 64.0f, 1 / 64.0f, 1 / 64.0f);
+
+	pglEnableClientState(GL_NORMAL_ARRAY);
 
-	while (val != 0)
+	for (i = 0; i < model->numMeshes; i++)
 	{
-		if (val < 0)
-		{
-			pglBegin(GL_TRIANGLE_FAN);
-			count = -val;
-		}
-		else
-		{
-			pglBegin(GL_TRIANGLE_STRIP);
-			count = val;
-		}
+		mesh_t *mesh = &model->meshes[i];
 
-		while (count--)
+		if (useTinyFrames)
 		{
-			s = *(float *) gl_cmd_buffer++;
-			t = *(float *) gl_cmd_buffer++;
-			pindex = *gl_cmd_buffer++;
+			tinyframe_t *frame = &mesh->tinyframes[frameIndex % mesh->numFrames];
+			tinyframe_t *nextframe = NULL;
 
-			pglTexCoord2f(s, t);
+			if (nextFrameIndex != -1)
+				nextframe = &mesh->tinyframes[nextFrameIndex % mesh->numFrames];
 
 			if (!nextframe || fpclassify(pol) == FP_ZERO)
 			{
-				pglNormal3f(frame->vertices[pindex].normal[0],
-				            frame->vertices[pindex].normal[1],
-				            frame->vertices[pindex].normal[2]);
+				pglBindBuffer(GL_ARRAY_BUFFER, frame->vboID);
+				pglVertexPointer(3, GL_SHORT, sizeof(vbotiny_t), BUFFER_OFFSET(0));
+				pglNormalPointer(GL_BYTE, sizeof(vbotiny_t), BUFFER_OFFSET(sizeof(short)*3));
+				pglTexCoordPointer(2, GL_FLOAT, sizeof(vbotiny_t), BUFFER_OFFSET(sizeof(short) * 3 + sizeof(char) * 6));
 
-				pglVertex3f(frame->vertices[pindex].vertex[0]*scalex/2.0f,
-				            frame->vertices[pindex].vertex[1]*scaley/2.0f,
-				            frame->vertices[pindex].vertex[2]*scalez/2.0f);
+				pglDrawElements(GL_TRIANGLES, mesh->numTriangles * 3, GL_UNSIGNED_SHORT, mesh->indices);
+				pglBindBuffer(GL_ARRAY_BUFFER, 0);
 			}
 			else
 			{
-				// Interpolate
-				float px1 = frame->vertices[pindex].vertex[0]*scalex/2.0f;
-				float px2 = nextframe->vertices[pindex].vertex[0]*scalex/2.0f;
-				float py1 = frame->vertices[pindex].vertex[1]*scaley/2.0f;
-				float py2 = nextframe->vertices[pindex].vertex[1]*scaley/2.0f;
-				float pz1 = frame->vertices[pindex].vertex[2]*scalez/2.0f;
-				float pz2 = nextframe->vertices[pindex].vertex[2]*scalez/2.0f;
-				float nx1 = frame->vertices[pindex].normal[0];
-				float nx2 = nextframe->vertices[pindex].normal[0];
-				float ny1 = frame->vertices[pindex].normal[1];
-				float ny2 = nextframe->vertices[pindex].normal[1];
-				float nz1 = frame->vertices[pindex].normal[2];
-				float nz2 = nextframe->vertices[pindex].normal[2];
-
-				pglNormal3f((nx1 + pol * (nx2 - nx1)),
-				            (ny1 + pol * (ny2 - ny1)),
-				            (nz1 + pol * (nz2 - nz1)));
-				pglVertex3f((px1 + pol * (px2 - px1)),
-				            (py1 + pol * (py2 - py1)),
-				            (pz1 + pol * (pz2 - pz1)));
+				short *vertPtr;
+				char *normPtr;
+				int j;
+
+				// Dangit, I soooo want to do this in a GLSL shader...
+				AllocLerpTinyBuffer(mesh->numVertices * sizeof(short) * 3);
+				vertPtr = vertTinyBuffer;
+				normPtr = normTinyBuffer;
+				j = 0;
+
+				for (j = 0; j < mesh->numVertices * 3; j++)
+				{
+					// Interpolate
+					*vertPtr++ = (short)(frame->vertices[j] + (pol * (nextframe->vertices[j] - frame->vertices[j])));
+					*normPtr++ = (char)(frame->normals[j] + (pol * (nextframe->normals[j] - frame->normals[j])));
+				}
+
+				pglVertexPointer(3, GL_SHORT, 0, vertTinyBuffer);
+				pglNormalPointer(GL_BYTE, 0, normTinyBuffer);
+				pglTexCoordPointer(2, GL_FLOAT, 0, mesh->uvs);
+				pglDrawElements(GL_TRIANGLES, mesh->numTriangles * 3, GL_UNSIGNED_SHORT, mesh->indices);
 			}
 		}
+		else
+		{
+			mdlframe_t *frame = &mesh->frames[frameIndex % mesh->numFrames];
+			mdlframe_t *nextframe = NULL;
 
-		pglEnd();
+			if (nextFrameIndex != -1)
+				nextframe = &mesh->frames[nextFrameIndex % mesh->numFrames];
 
-		val = *gl_cmd_buffer++;
+			if (!nextframe || fpclassify(pol) == FP_ZERO)
+			{
+				// Zoom! Take advantage of just shoving the entire arrays to the GPU.
+/*				pglVertexPointer(3, GL_FLOAT, 0, frame->vertices);
+				pglNormalPointer(GL_FLOAT, 0, frame->normals);
+				pglTexCoordPointer(2, GL_FLOAT, 0, mesh->uvs);
+				pglDrawArrays(GL_TRIANGLES, 0, mesh->numTriangles * 3);*/
+
+				pglBindBuffer(GL_ARRAY_BUFFER, frame->vboID);
+				pglVertexPointer(3, GL_FLOAT, sizeof(vbo64_t), BUFFER_OFFSET(0));
+				pglNormalPointer(GL_FLOAT, sizeof(vbo64_t), BUFFER_OFFSET(sizeof(float) * 3));
+				pglTexCoordPointer(2, GL_FLOAT, sizeof(vbo64_t), BUFFER_OFFSET(sizeof(float) * 6));
+
+				pglDrawArrays(GL_TRIANGLES, 0, mesh->numTriangles * 3);
+				// No tinyframes, no mesh indices
+				//pglDrawElements(GL_TRIANGLES, mesh->numTriangles * 3, GL_UNSIGNED_SHORT, mesh->indices);
+				pglBindBuffer(GL_ARRAY_BUFFER, 0);
+			}
+			else
+			{
+				float *vertPtr;
+				float *normPtr;
+				int j = 0;
+
+				// Dangit, I soooo want to do this in a GLSL shader...
+				AllocLerpBuffer(mesh->numVertices * sizeof(float) * 3);
+				vertPtr = vertBuffer;
+				normPtr = normBuffer;
+				//int j = 0;
+
+				for (j = 0; j < mesh->numVertices * 3; j++)
+				{
+					// Interpolate
+					*vertPtr++ = frame->vertices[j] + (pol * (nextframe->vertices[j] - frame->vertices[j]));
+					*normPtr++ = frame->normals[j] + (pol * (nextframe->normals[j] - frame->normals[j]));
+				}
+
+				pglVertexPointer(3, GL_FLOAT, 0, vertBuffer);
+				pglNormalPointer(GL_FLOAT, 0, normBuffer);
+				pglTexCoordPointer(2, GL_FLOAT, 0, mesh->uvs);
+				pglDrawArrays(GL_TRIANGLES, 0, mesh->numVertices);
+			}
+		}
 	}
+
+	pglDisableClientState(GL_NORMAL_ARRAY);
+
 	pglPopMatrix(); // should be the same as glLoadIdentity
 	if (color)
 		pglDisable(GL_LIGHTING);
 	pglShadeModel(GL_FLAT);
 	pglDisable(GL_CULL_FACE);
+	pglDisable(GL_NORMALIZE);
 }
 
 // -----------------+
 // HWRAPI DrawMD2   : Draw an MD2 model with glcommands
 // -----------------+
-EXPORT void HWRAPI(DrawMD2i) (INT32 *gl_cmd_buffer, md2_frame_t *frame, INT32 duration, INT32 tics, md2_frame_t *nextframe, FTransform *pos, float scale, UINT8 flipped, UINT8 *color)
+EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, INT32 duration, INT32 tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 *color)
 {
-	DrawMD2Ex(gl_cmd_buffer, frame, duration, tics,  nextframe, pos, scale, flipped, color);
+	DrawModelEx(model, frameIndex, duration, tics,  nextFrameIndex, pos, scale, flipped, color);
 }
 
-EXPORT void HWRAPI(DrawMD2) (INT32 *gl_cmd_buffer, md2_frame_t *frame, FTransform *pos, float scale)
-{
-	DrawMD2Ex(gl_cmd_buffer, frame, 0, 0,  NULL, pos, scale, false, NULL);
-}
-
-
 // -----------------+
 // SetTransform     :
 // -----------------+
@@ -2064,9 +1959,13 @@ EXPORT void HWRAPI(SetTransform) (FTransform *stransform)
 		// keep a trace of the transformation for md2
 		memcpy(&md2_transform, stransform, sizeof (md2_transform));
 
+#ifdef USE_FTRANSFORM_MIRROR
+		// mirroring from Kart
 		if (stransform->mirror)
 			pglScalef(-stransform->scalex, stransform->scaley, -stransform->scalez);
-		else if (stransform->flip)
+		else
+#endif
+		if (stransform->flip)
 			pglScalef(stransform->scalex, -stransform->scaley, -stransform->scalez);
 		else
 			pglScalef(stransform->scalex, stransform->scaley, -stransform->scalez);
@@ -2089,18 +1988,14 @@ EXPORT void HWRAPI(SetTransform) (FTransform *stransform)
 	if (special_splitscreen)
 	{
 		used_fov = atan(tan(used_fov*M_PIl/360.0l)*0.8l)*360/M_PIl;
-		GLPerspective(used_fov, 2*ASPECT_RATIO);
+		GLPerspective((GLfloat)used_fov, 2*ASPECT_RATIO);
 	}
 	else
-		GLPerspective(used_fov, ASPECT_RATIO);
-#ifndef MINI_GL_COMPATIBILITY
-	pglGetDoublev(GL_PROJECTION_MATRIX, projMatrix); // added for new coronas' code (without depth buffer)
-#endif
+		GLPerspective((GLfloat)used_fov, ASPECT_RATIO);
+	pglGetFloatv(GL_PROJECTION_MATRIX, projMatrix); // added for new coronas' code (without depth buffer)
 	pglMatrixMode(GL_MODELVIEW);
 
-#ifndef MINI_GL_COMPATIBILITY
-	pglGetDoublev(GL_MODELVIEW_MATRIX, modelMatrix); // added for new coronas' code (without depth buffer)
-#endif
+	pglGetFloatv(GL_MODELVIEW_MATRIX, modelMatrix); // added for new coronas' code (without depth buffer)
 }
 
 EXPORT INT32  HWRAPI(GetTextureUsed) (void)
@@ -2121,7 +2016,6 @@ EXPORT INT32  HWRAPI(GetRenderVersion) (void)
 	return VERSION;
 }
 
-#ifdef SHUFFLE
 EXPORT void HWRAPI(PostImgRedraw) (float points[SCREENVERTS][SCREENVERTS][2])
 {
 	INT32 x, y;
@@ -2129,6 +2023,14 @@ EXPORT void HWRAPI(PostImgRedraw) (float points[SCREENVERTS][SCREENVERTS][2])
 	float xfix, yfix;
 	INT32 texsize = 2048;
 
+	const float blackBack[16] =
+	{
+		-16.0f, -16.0f, 6.0f,
+		-16.0f, 16.0f, 6.0f,
+		16.0f, 16.0f, 6.0f,
+		16.0f, -16.0f, 6.0f
+	};
+
 	// Use a power of two texture, dammit
 	if(screen_width <= 1024)
 		texsize = 1024;
@@ -2141,47 +2043,66 @@ EXPORT void HWRAPI(PostImgRedraw) (float points[SCREENVERTS][SCREENVERTS][2])
 
 	pglDisable(GL_DEPTH_TEST);
 	pglDisable(GL_BLEND);
-	pglBegin(GL_QUADS);
 
-		// Draw a black square behind the screen texture,
-		// so nothing shows through the edges
-		pglColor4f(1.0f, 1.0f, 1.0f, 1.0f);
-		pglVertex3f(-16.0f, -16.0f, 6.0f);
-		pglVertex3f(-16.0f, 16.0f, 6.0f);
-		pglVertex3f(16.0f, 16.0f, 6.0f);
-		pglVertex3f(16.0f, -16.0f, 6.0f);
+	// const float blackBack[16]
 
-		for(x=0;x<SCREENVERTS-1;x++)
+	// Draw a black square behind the screen texture,
+	// so nothing shows through the edges
+	pglColor4ubv(white);
+
+	pglVertexPointer(3, GL_FLOAT, 0, blackBack);
+	pglDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+	for(x=0;x<SCREENVERTS-1;x++)
+	{
+		for(y=0;y<SCREENVERTS-1;y++)
 		{
-			for(y=0;y<SCREENVERTS-1;y++)
-			{
-				// Used for texture coordinates
-				// Annoying magic numbers to scale the square texture to
-				// a non-square screen..
-				float_x = (float)(x/(xfix));
-				float_y = (float)(y/(yfix));
-				float_nextx = (float)(x+1)/(xfix);
-				float_nexty = (float)(y+1)/(yfix);
-
-				// Attach the squares together.
-				pglTexCoord2f( float_x, float_y);
-				pglVertex3f(points[x][y][0], points[x][y][1], 4.4f);
-
-				pglTexCoord2f( float_x, float_nexty);
-				pglVertex3f(points[x][y+1][0], points[x][y+1][1], 4.4f);
-
-				pglTexCoord2f( float_nextx, float_nexty);
-				pglVertex3f(points[x+1][y+1][0], points[x+1][y+1][1], 4.4f);
-
-				pglTexCoord2f( float_nextx, float_y);
-				pglVertex3f(points[x+1][y][0], points[x+1][y][1], 4.4f);
-			}
+			float stCoords[8];
+			float vertCoords[12];
+
+			// Used for texture coordinates
+			// Annoying magic numbers to scale the square texture to
+			// a non-square screen..
+			float_x = (float)(x/(xfix));
+			float_y = (float)(y/(yfix));
+			float_nextx = (float)(x+1)/(xfix);
+			float_nexty = (float)(y+1)/(yfix);
+
+			// float stCoords[8];
+			stCoords[0] = float_x;
+			stCoords[1] = float_y;
+			stCoords[2] = float_x;
+			stCoords[3] = float_nexty;
+			stCoords[4] = float_nextx;
+			stCoords[5] = float_nexty;
+			stCoords[6] = float_nextx;
+			stCoords[7] = float_y;
+
+			pglTexCoordPointer(2, GL_FLOAT, 0, stCoords);
+
+			// float vertCoords[12];
+			vertCoords[0] = points[x][y][0];
+			vertCoords[1] = points[x][y][1];
+			vertCoords[2] = 4.4f;
+			vertCoords[3] = points[x][y + 1][0];
+			vertCoords[4] = points[x][y + 1][1];
+			vertCoords[5] = 4.4f;
+			vertCoords[6] = points[x + 1][y + 1][0];
+			vertCoords[7] = points[x + 1][y + 1][1];
+			vertCoords[8] = 4.4f;
+			vertCoords[9] = points[x + 1][y][0];
+			vertCoords[10] = points[x + 1][y][1];
+			vertCoords[11] = 4.4f;
+
+			pglVertexPointer(3, GL_FLOAT, 0, vertCoords);
+
+			pglDrawArrays(GL_TRIANGLE_FAN, 0, 4);
 		}
-	pglEnd();
+	}
+
 	pglEnable(GL_DEPTH_TEST);
 	pglEnable(GL_BLEND);
 }
-#endif //SHUFFLE
 
 // Sryder:	This needs to be called whenever the screen changes resolution in order to reset the screen textures to use
 //			a new size
@@ -2216,23 +2137,14 @@ EXPORT void HWRAPI(StartScreenWipe) (void)
 
 	if (firstTime)
 	{
-#ifdef KOS_GL_COMPATIBILITY
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_FILTER_NONE);
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_FILTER_NONE);
-#else
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-#endif
 		Clamp2D(GL_TEXTURE_WRAP_S);
 		Clamp2D(GL_TEXTURE_WRAP_T);
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, texsize, texsize, 0);
-#endif
 	}
 	else
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, texsize, texsize);
-#endif
 
 	tex_downloaded = startScreenWipe;
 }
@@ -2256,24 +2168,14 @@ EXPORT void HWRAPI(EndScreenWipe)(void)
 
 	if (firstTime)
 	{
-#ifdef KOS_GL_COMPATIBILITY
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_FILTER_NONE);
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_FILTER_NONE);
-#else
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-#endif
 		Clamp2D(GL_TEXTURE_WRAP_S);
 		Clamp2D(GL_TEXTURE_WRAP_T);
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, texsize, texsize, 0);
-#endif
 	}
 	else
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, texsize, texsize);
-#endif
-
 
 	tex_downloaded = endScreenWipe;
 }
@@ -2285,6 +2187,16 @@ EXPORT void HWRAPI(DrawIntermissionBG)(void)
 	float xfix, yfix;
 	INT32 texsize = 2048;
 
+	const float screenVerts[12] =
+	{
+		-1.0f, -1.0f, 1.0f,
+		-1.0f, 1.0f, 1.0f,
+		1.0f, 1.0f, 1.0f,
+		1.0f, -1.0f, 1.0f
+	};
+
+	float fix[8];
+
 	if(screen_width <= 1024)
 		texsize = 1024;
 	if(screen_width <= 512)
@@ -2293,42 +2205,55 @@ EXPORT void HWRAPI(DrawIntermissionBG)(void)
 	xfix = 1/((float)(texsize)/((float)((screen_width))));
 	yfix = 1/((float)(texsize)/((float)((screen_height))));
 
-	pglClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
-
-	pglBindTexture(GL_TEXTURE_2D, screentexture);
-	pglBegin(GL_QUADS);
+	// const float screenVerts[12]
 
-		pglColor4f(1.0f, 1.0f, 1.0f, 1.0f);
-		// Bottom left
-		pglTexCoord2f(0.0f, 0.0f);
-		pglVertex3f(-1.0f, -1.0f, 1.0f);
+	// float fix[8];
+	fix[0] = 0.0f;
+	fix[1] = 0.0f;
+	fix[2] = 0.0f;
+	fix[3] = yfix;
+	fix[4] = xfix;
+	fix[5] = yfix;
+	fix[6] = xfix;
+	fix[7] = 0.0f;
 
-		// Top left
-		pglTexCoord2f(0.0f, yfix);
-		pglVertex3f(-1.0f, 1.0f, 1.0f);
-
-		// Top right
-		pglTexCoord2f(xfix, yfix);
-		pglVertex3f(1.0f, 1.0f, 1.0f);
+	pglClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
 
-		// Bottom right
-		pglTexCoord2f(xfix, 0.0f);
-		pglVertex3f(1.0f, -1.0f, 1.0f);
+	pglBindTexture(GL_TEXTURE_2D, screentexture);
+	pglColor4ubv(white);
 
-	pglEnd();
+	pglTexCoordPointer(2, GL_FLOAT, 0, fix);
+	pglVertexPointer(3, GL_FLOAT, 0, screenVerts);
+	pglDrawArrays(GL_TRIANGLE_FAN, 0, 4);
 
 	tex_downloaded = screentexture;
 }
 
 // Do screen fades!
-EXPORT void HWRAPI(DoScreenWipe)(float alpha)
+EXPORT void HWRAPI(DoScreenWipe)(void)
 {
 	INT32 texsize = 2048;
 	float xfix, yfix;
 
-#ifndef MINI_GL_COMPATIBILITY
 	INT32 fademaskdownloaded = tex_downloaded; // the fade mask that has been set
-#endif
+
+	const float screenVerts[12] =
+	{
+		-1.0f, -1.0f, 1.0f,
+		-1.0f, 1.0f, 1.0f,
+		1.0f, 1.0f, 1.0f,
+		1.0f, -1.0f, 1.0f
+	};
+
+	float fix[8];
+
+	const float defaultST[8] =
+	{
+		0.0f, 1.0f,
+		0.0f, 0.0f,
+		1.0f, 0.0f,
+		1.0f, 1.0f
+	};
 
 	// Use a power of two texture, dammit
 	if(screen_width <= 1024)
@@ -2339,105 +2264,60 @@ EXPORT void HWRAPI(DoScreenWipe)(float alpha)
 	xfix = 1/((float)(texsize)/((float)((screen_width))));
 	yfix = 1/((float)(texsize)/((float)((screen_height))));
 
+	// const float screenVerts[12]
+
+	// float fix[8];
+	fix[0] = 0.0f;
+	fix[1] = 0.0f;
+	fix[2] = 0.0f;
+	fix[3] = yfix;
+	fix[4] = xfix;
+	fix[5] = yfix;
+	fix[6] = xfix;
+	fix[7] = 0.0f;
+
 	pglClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
 
 	SetBlend(PF_Modulated|PF_NoDepthTest|PF_Clip|PF_NoZClip);
+	pglEnable(GL_TEXTURE_2D);
 
 	// Draw the original screen
 	pglBindTexture(GL_TEXTURE_2D, startScreenWipe);
-	pglBegin(GL_QUADS);
-		pglColor4f(1.0f, 1.0f, 1.0f, 1.0f);
-
-		// Bottom left
-		pglTexCoord2f(0.0f, 0.0f);
-		pglVertex3f(-1.0f, -1.0f, 1.0f);
-
-		// Top left
-		pglTexCoord2f(0.0f, yfix);
-		pglVertex3f(-1.0f, 1.0f, 1.0f);
-
-		// Top right
-		pglTexCoord2f(xfix, yfix);
-		pglVertex3f(1.0f, 1.0f, 1.0f);
-
-		// Bottom right
-		pglTexCoord2f(xfix, 0.0f);
-		pglVertex3f(1.0f, -1.0f, 1.0f);
-
-	pglEnd();
+	pglColor4ubv(white);
+	pglTexCoordPointer(2, GL_FLOAT, 0, fix);
+	pglVertexPointer(3, GL_FLOAT, 0, screenVerts);
+	pglDrawArrays(GL_TRIANGLE_FAN, 0, 4);
 
 	SetBlend(PF_Modulated|PF_Translucent|PF_NoDepthTest|PF_Clip|PF_NoZClip);
 
-#ifndef MINI_GL_COMPATIBILITY
-	if (gl13)
-	{
-		// Draw the end screen that fades in
-		pglActiveTexture(GL_TEXTURE0);
-		pglEnable(GL_TEXTURE_2D);
-		pglBindTexture(GL_TEXTURE_2D, endScreenWipe);
-		pglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
-
-		pglActiveTexture(GL_TEXTURE1);
-		pglEnable(GL_TEXTURE_2D);
-		pglBindTexture(GL_TEXTURE_2D, fademaskdownloaded);
-
-		pglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-		pglBegin(GL_QUADS);
-			pglColor4f(1.0f, 1.0f, 1.0f, 1.0f);
-
-			// Bottom left
-			pglMultiTexCoord2f(GL_TEXTURE0, 0.0f, 0.0f);
-			pglMultiTexCoord2f(GL_TEXTURE1, 0.0f, 1.0f);
-			pglVertex3f(-1.0f, -1.0f, 1.0f);
-
-			// Top left
-			pglMultiTexCoord2f(GL_TEXTURE0, 0.0f, yfix);
-			pglMultiTexCoord2f(GL_TEXTURE1, 0.0f, 0.0f);
-			pglVertex3f(-1.0f, 1.0f, 1.0f);
-
-			// Top right
-			pglMultiTexCoord2f(GL_TEXTURE0, xfix, yfix);
-			pglMultiTexCoord2f(GL_TEXTURE1, 1.0f, 0.0f);
-			pglVertex3f(1.0f, 1.0f, 1.0f);
-
-			// Bottom right
-			pglMultiTexCoord2f(GL_TEXTURE0, xfix, 0.0f);
-			pglMultiTexCoord2f(GL_TEXTURE1, 1.0f, 1.0f);
-			pglVertex3f(1.0f, -1.0f, 1.0f);
-		pglEnd();
-
-		pglDisable(GL_TEXTURE_2D); // disable the texture in the 2nd texture unit
-		pglActiveTexture(GL_TEXTURE0);
-		tex_downloaded = endScreenWipe;
-	}
-	else
-	{
-#endif
 	// Draw the end screen that fades in
+	pglActiveTexture(GL_TEXTURE0);
+	pglEnable(GL_TEXTURE_2D);
 	pglBindTexture(GL_TEXTURE_2D, endScreenWipe);
-	pglBegin(GL_QUADS);
-		pglColor4f(1.0f, 1.0f, 1.0f, alpha);
+	pglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
 
-		// Bottom left
-		pglTexCoord2f(0.0f, 0.0f);
-		pglVertex3f(-1.0f, -1.0f, 1.0f);
+	pglActiveTexture(GL_TEXTURE1);
+	pglEnable(GL_TEXTURE_2D);
+	pglBindTexture(GL_TEXTURE_2D, fademaskdownloaded);
 
-		// Top left
-		pglTexCoord2f(0.0f, yfix);
-		pglVertex3f(-1.0f, 1.0f, 1.0f);
+	pglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
+
+	// const float defaultST[8]
+
+	pglClientActiveTexture(GL_TEXTURE0);
+	pglTexCoordPointer(2, GL_FLOAT, 0, fix);
+	pglVertexPointer(3, GL_FLOAT, 0, screenVerts);
+	pglClientActiveTexture(GL_TEXTURE1);
+	pglEnableClientState(GL_TEXTURE_COORD_ARRAY);
+	pglTexCoordPointer(2, GL_FLOAT, 0, defaultST);
+	pglDrawArrays(GL_TRIANGLE_FAN, 0, 4);
 
-		// Top right
-		pglTexCoord2f(xfix, yfix);
-		pglVertex3f(1.0f, 1.0f, 1.0f);
+	pglDisable(GL_TEXTURE_2D); // disable the texture in the 2nd texture unit
+	pglDisableClientState(GL_TEXTURE_COORD_ARRAY);
 
-		// Bottom right
-		pglTexCoord2f(xfix, 0.0f);
-		pglVertex3f(1.0f, -1.0f, 1.0f);
-	pglEnd();
+	pglActiveTexture(GL_TEXTURE0);
+	pglClientActiveTexture(GL_TEXTURE0);
 	tex_downloaded = endScreenWipe;
-#ifndef MINI_GL_COMPATIBILITY
-	}
-#endif
 }
 
 
@@ -2460,23 +2340,14 @@ EXPORT void HWRAPI(MakeScreenTexture) (void)
 
 	if (firstTime)
 	{
-#ifdef KOS_GL_COMPATIBILITY
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_FILTER_NONE);
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_FILTER_NONE);
-#else
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-#endif
 		Clamp2D(GL_TEXTURE_WRAP_S);
 		Clamp2D(GL_TEXTURE_WRAP_T);
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, texsize, texsize, 0);
-#endif
 	}
 	else
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, texsize, texsize);
-#endif
 
 	tex_downloaded = screentexture;
 }
@@ -2499,26 +2370,16 @@ EXPORT void HWRAPI(MakeScreenFinalTexture) (void)
 
 	if (firstTime)
 	{
-#ifdef KOS_GL_COMPATIBILITY
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_FILTER_NONE);
-		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_FILTER_NONE);
-#else
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-#endif
 		Clamp2D(GL_TEXTURE_WRAP_S);
 		Clamp2D(GL_TEXTURE_WRAP_T);
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, texsize, texsize, 0);
-#endif
 	}
 	else
-#ifndef KOS_GL_COMPATIBILITY
 		pglCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, texsize, texsize);
-#endif
 
 	tex_downloaded = finalScreenTexture;
-
 }
 
 EXPORT void HWRAPI(DrawScreenFinalTexture)(int width, int height)
@@ -2529,6 +2390,9 @@ EXPORT void HWRAPI(DrawScreenFinalTexture)(int width, int height)
 	FRGBAFloat clearColour;
 	INT32 texsize = 2048;
 
+	float off[12];
+	float fix[8];
+
 	if(screen_width <= 1024)
 		texsize = 1024;
 	if(screen_width <= 512)
@@ -2550,33 +2414,43 @@ EXPORT void HWRAPI(DrawScreenFinalTexture)(int width, int height)
 		yoff = newaspect / origaspect;
 	}
 
+	// float off[12];
+	off[0] = -xoff;
+	off[1] = -yoff;
+	off[2] = 1.0f;
+	off[3] = -xoff;
+	off[4] = yoff;
+	off[5] = 1.0f;
+	off[6] = xoff;
+	off[7] = yoff;
+	off[8] = 1.0f;
+	off[9] = xoff;
+	off[10] = -yoff;
+	off[11] = 1.0f;
+
+	// float fix[8];
+	fix[0] = 0.0f;
+	fix[1] = 0.0f;
+	fix[2] = 0.0f;
+	fix[3] = yfix;
+	fix[4] = xfix;
+	fix[5] = yfix;
+	fix[6] = xfix;
+	fix[7] = 0.0f;
+
 	pglViewport(0, 0, width, height);
 
 	clearColour.red = clearColour.green = clearColour.blue = 0;
 	clearColour.alpha = 1;
 	ClearBuffer(true, false, &clearColour);
 	pglBindTexture(GL_TEXTURE_2D, finalScreenTexture);
-	pglBegin(GL_QUADS);
-
-		pglColor4f(1.0f, 1.0f, 1.0f, 1.0f);
-		// Bottom left
-		pglTexCoord2f(0.0f, 0.0f);
-		pglVertex3f(-xoff, -yoff, 1.0f);
-
-		// Top left
-		pglTexCoord2f(0.0f, yfix);
-		pglVertex3f(-xoff, yoff, 1.0f);
-
-		// Top right
-		pglTexCoord2f(xfix, yfix);
-		pglVertex3f(xoff, yoff, 1.0f);
 
-		// Bottom right
-		pglTexCoord2f(xfix, 0.0f);
-		pglVertex3f(xoff, -yoff, 1.0f);
+	pglColor4ubv(white);
 
-	pglEnd();
+	pglTexCoordPointer(2, GL_FLOAT, 0, fix);
+	pglVertexPointer(3, GL_FLOAT, 0, off);
 
+	pglDrawArrays(GL_TRIANGLE_FAN, 0, 4);
 	tex_downloaded = finalScreenTexture;
 }
 
diff --git a/src/hardware/r_opengl/r_opengl.h b/src/hardware/r_opengl/r_opengl.h
index 483ec171e6103b7fb1cb8a6c7b14b108162df100..baf5c7b2a86456b750f7f29db5145ac8615d9d52 100644
--- a/src/hardware/r_opengl/r_opengl.h
+++ b/src/hardware/r_opengl/r_opengl.h
@@ -37,13 +37,11 @@
 #include <GL/gl.h>
 #include <GL/glu.h>
 
-#ifndef MINI_GL_COMPATIBILITY
 #ifdef STATIC_OPENGL // Because of the 1.3 functions, you'll need GLext to compile it if static
 #define GL_GLEXT_PROTOTYPES
 #include <GL/glext.h>
 #endif
 #endif
-#endif
 
 #define  _CREATE_DLL_  // necessary for Unix AND Windows
 #include "../../doomdef.h"
@@ -73,7 +71,6 @@ extern FILE             *gllogstream;
 #endif
 
 #ifndef DRIVER_STRING
-//    #define USE_PALETTED_TEXTURE
 #define DRIVER_STRING "HWRAPI Init(): SRB2Kart OpenGL renderer" // Tails
 #endif
 
@@ -91,10 +88,6 @@ int SetupPixelFormat(INT32 WantColorBits, INT32 WantStencilBits, INT32 WantDepth
 void SetModelView(GLint w, GLint h);
 void SetStates(void);
 FUNCMATH float byteasfloat(UINT8 fbyte);
-#ifdef USE_PALETTED_TEXTURE
-extern PFNGLCOLORTABLEEXTPROC glColorTableEXT;
-extern GLubyte                palette_tex[256*3];
-#endif
 
 #ifndef GL_EXT_texture_filter_anisotropic
 #define GL_TEXTURE_MAX_ANISOTROPY_EXT     0x84FE
@@ -120,6 +113,10 @@ typedef void (APIENTRY * PFNglGetIntegerv) (GLenum pname, GLint *params);
 extern PFNglGetIntegerv pglGetIntegerv;
 typedef const GLubyte* (APIENTRY  * PFNglGetString) (GLenum name);
 extern PFNglGetString pglGetString;
+#if 0
+typedef void (APIENTRY * PFNglEnableClientState) (GLenum cap); // redefined in r_opengl.c
+static PFNglEnableClientState pglEnableClientState;
+#endif
 #endif
 
 // ==========================================================================
diff --git a/src/hardware/r_opengl/r_vbo.h b/src/hardware/r_opengl/r_vbo.h
new file mode 100644
index 0000000000000000000000000000000000000000..ca1a974dabbbe6cc296e0f237d2f7dacd1d2328b
--- /dev/null
+++ b/src/hardware/r_opengl/r_vbo.h
@@ -0,0 +1,52 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+#ifndef _R_VBO_H_
+#define _R_VBO_H_
+
+typedef struct
+{
+	float x, y, z;		// Vertex
+	float nx, ny, nz;	// Normal
+	float s0, t0;		// Texcoord0
+} vbo32_t;
+
+typedef struct
+{
+	float x, y, z;	// Vertex
+	float s0, t0;	// Texcoord0
+	unsigned char r, g, b, a; // Color
+	float pad[2]; // Pad
+} vbo2d32_t;
+
+typedef struct
+{
+	float x, y; // Vertex
+	float s0, t0; // Texcoord0
+} vbofont_t;
+
+typedef struct
+{
+	short x, y, z; // Vertex
+	char nx, ny, nz; // Normal
+	char tanx, tany, tanz; // Tangent
+	float s0, t0; // Texcoord0
+} vbotiny_t;
+
+typedef struct
+{
+	float x, y, z;      // Vertex
+	float nx, ny, nz;   // Normal
+	float s0, t0;       // Texcoord0
+	float s1, t1;       // Texcoord1
+	float s2, t2;       // Texcoord2
+	float tan0, tan1, tan2; // Tangent
+	unsigned char r, g, b, a;	// Color
+} vbo64_t;
+
+#endif
diff --git a/src/hardware/u_list.c b/src/hardware/u_list.c
new file mode 100644
index 0000000000000000000000000000000000000000..dc49a74e7f7ab1a0502fbf5fb5f13cbbad888275
--- /dev/null
+++ b/src/hardware/u_list.c
@@ -0,0 +1,230 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#include "u_list.h"
+#include "../z_zone.h"
+
+// Utility for managing
+// structures in a linked
+// list.
+//
+// Struct must have "next" and "prev" pointers
+// as its first two variables.
+//
+
+//
+// ListAdd
+//
+// Adds an item to the list
+//
+void ListAdd(void *pItem, listitem_t **itemHead)
+{
+	listitem_t *item = (listitem_t*)pItem;
+
+	if (*itemHead == NULL)
+	{
+		*itemHead = item;
+		(*itemHead)->prev = (*itemHead)->next = NULL;
+	}
+	else
+	{
+		listitem_t *tail;
+		tail = *itemHead;
+
+		while (tail->next != NULL)
+			tail = tail->next;
+
+		tail->next = item;
+
+		tail->next->prev = tail;
+
+		item->next = NULL;
+	}
+}
+
+//
+// ListAddFront
+//
+// Adds an item to the front of the list
+// (This is much faster)
+//
+void ListAddFront(void *pItem, listitem_t **itemHead)
+{
+	listitem_t *item = (listitem_t*)pItem;
+
+	if (*itemHead == NULL)
+	{
+		*itemHead = item;
+		(*itemHead)->prev = (*itemHead)->next = NULL;
+	}
+	else
+	{
+		(*itemHead)->prev = item;
+		item->next = (*itemHead);
+		item->prev = NULL;
+		*itemHead = item;
+	}
+}
+
+//
+// ListAddBefore
+//
+// Adds an item before the item specified in the list
+//
+void ListAddBefore(void *pItem, void *pSpot, listitem_t **itemHead)
+{
+	listitem_t *item = (listitem_t*)pItem;
+	listitem_t *spot = (listitem_t*)pSpot;
+
+	listitem_t *prev = spot->prev;
+
+	if (!prev)
+		ListAddFront(pItem, itemHead);
+	else
+	{
+		item->next = spot;
+		spot->prev = item;
+		item->prev = prev;
+		prev->next = item;
+	}
+}
+
+//
+// ListAddAfter
+//
+// Adds an item after the item specified in the list
+//
+void ListAddAfter(void *pItem, void *pSpot, listitem_t **itemHead)
+{
+	listitem_t *item = (listitem_t*)pItem;
+	listitem_t *spot = (listitem_t*)pSpot;
+
+	listitem_t *next = spot->next;
+
+	if (!next)
+		ListAdd(pItem, itemHead);
+	else
+	{
+		item->prev = spot;
+		spot->next = item;
+		item->next = next;
+		next->prev = item;
+	}
+}
+
+//
+// ListRemove
+//
+// Take an item out of the list and free its memory.
+//
+void ListRemove(void *pItem, listitem_t **itemHead)
+{
+	listitem_t *item = (listitem_t*)pItem;
+
+	if (item == *itemHead) // Start of list
+	{
+		*itemHead = item->next;
+
+		if (*itemHead)
+			(*itemHead)->prev = NULL;
+	}
+	else if (item->next == NULL) // end of list
+	{
+		item->prev->next = NULL;
+	}
+	else // Somewhere in between
+	{
+		item->prev->next = item->next;
+		item->next->prev = item->prev;
+	}
+
+	Z_Free (item);
+}
+
+//
+// ListRemoveAll
+//
+// Removes all items from the list, freeing their memory.
+//
+void ListRemoveAll(listitem_t **itemHead)
+{
+	listitem_t *item;
+	listitem_t *next;
+	for (item = *itemHead; item; item = next)
+	{
+		next = item->next;
+		ListRemove(item, itemHead);
+	}
+}
+
+//
+// ListRemoveNoFree
+//
+// Take an item out of the list, but don't free its memory.
+//
+void ListRemoveNoFree(void *pItem, listitem_t **itemHead)
+{
+	listitem_t *item = (listitem_t*)pItem;
+
+	if (item == *itemHead) // Start of list
+	{
+		*itemHead = item->next;
+
+		if (*itemHead)
+			(*itemHead)->prev = NULL;
+	}
+	else if (item->next == NULL) // end of list
+	{
+		item->prev->next = NULL;
+	}
+	else // Somewhere in between
+	{
+		item->prev->next = item->next;
+		item->next->prev = item->prev;
+	}
+}
+
+//
+// ListGetCount
+//
+// Counts the # of items in a list
+// Should not be used in performance-minded code
+//
+unsigned int ListGetCount(void *itemHead)
+{
+	listitem_t *item = (listitem_t*)itemHead;
+
+	unsigned int count = 0;
+	for (; item; item = item->next)
+		count++;
+
+	return count;
+}
+
+//
+// ListGetByIndex
+//
+// Gets an item in the list by its index
+// Should not be used in performance-minded code
+//
+listitem_t *ListGetByIndex(void *itemHead, unsigned int index)
+{
+	listitem_t *head = (listitem_t*)itemHead;
+	unsigned int count = 0;
+	listitem_t *node;
+	for (node = head; node; node = node->next)
+	{
+		if (count == index)
+			return node;
+
+		count++;
+	}
+
+	return NULL;
+}
diff --git a/src/hardware/u_list.h b/src/hardware/u_list.h
new file mode 100644
index 0000000000000000000000000000000000000000..7e9a3cabd3bb9e5a9ded2c2d9f1d0f34b074ec3f
--- /dev/null
+++ b/src/hardware/u_list.h
@@ -0,0 +1,29 @@
+/*
+	From the 'Wizard2' engine by Spaddlewit Inc. ( http://www.spaddlewit.com )
+	An experimental work-in-progress.
+
+	Donated to Sonic Team Junior and adapted to work with
+	Sonic Robo Blast 2. The license of this code matches whatever
+	the licensing is for Sonic Robo Blast 2.
+*/
+
+#ifndef _U_LIST_H_
+#define _U_LIST_H_
+
+typedef struct listitem_s
+{
+	struct listitem_s *next;
+	struct listitem_s *prev;
+} listitem_t;
+
+void ListAdd(void *pItem, listitem_t **itemHead);
+void ListAddFront(void *pItem, listitem_t **itemHead);
+void ListAddBefore(void *pItem, void *pSpot, listitem_t **itemHead);
+void ListAddAfter(void *pItem, void *pSpot, listitem_t **itemHead);
+void ListRemove(void *pItem, listitem_t **itemHead);
+void ListRemoveAll(listitem_t **itemHead);
+void ListRemoveNoFree(void *pItem, listitem_t **itemHead);
+unsigned int ListGetCount(void *itemHead);
+listitem_t *ListGetByIndex(void *itemHead, unsigned int index);
+
+#endif
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 60774d07152c2e1a6b9234b6475b4e802af3defb..10e0f5fc60b0e1f8f9e8340aea16413bd77b30e5 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -39,7 +39,7 @@
 #include "am_map.h"
 #include "d_main.h"
 
-#include "p_local.h" // camera, camera2, camera3, camera4
+#include "p_local.h" // camera[]
 #include "p_tick.h"
 
 #ifdef HWRENDER
@@ -1860,7 +1860,7 @@ static void HU_DrawChat_Old(void)
 	if (!i)
 		return;
 
-	if ((netgame || multiplayer) && players[displayplayer].spectator)
+	if ((netgame || multiplayer) && players[displayplayers[0]].spectator)
 		return;
 
 #ifdef HWRENDER
@@ -1887,7 +1887,7 @@ static inline void HU_DrawCrosshair2(void)
 	if (!i)
 		return;
 
-	if ((netgame || multiplayer) && players[secondarydisplayplayer].spectator)
+	if ((netgame || multiplayer) && players[displayplayers[1]].spectator)
 		return;
 
 #ifdef HWRENDER
@@ -1934,7 +1934,7 @@ static inline void HU_DrawCrosshair3(void)
 	if (!i)
 		return;
 
-	if ((netgame || multiplayer) && players[thirddisplayplayer].spectator)
+	if ((netgame || multiplayer) && players[displayplayers[2]].spectator)
 		return;
 
 #ifdef HWRENDER
@@ -1971,7 +1971,7 @@ static inline void HU_DrawCrosshair4(void)
 	if (!i)
 		return;
 
-	if ((netgame || multiplayer) && players[fourthdisplayplayer].spectator)
+	if ((netgame || multiplayer) && players[displayplayers[3]].spectator)
 		return;
 
 #ifdef HWRENDER
@@ -2072,8 +2072,16 @@ UINT32 hu_demolap;
 
 static void HU_DrawDemoInfo(void)
 {
-	V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-40, 0, M_GetText("Replay:"));
-	V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-32, V_ALLOWLOWERCASE, player_names[0]);
+	if (!multiplayer)/* netreplay */
+	{
+		V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-40, 0, M_GetText("Replay:"));
+		V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-32, V_ALLOWLOWERCASE, player_names[0]);
+	}
+	else
+	{
+		V_DrawRightAlignedThinString(BASEVIDWIDTH-2, BASEVIDHEIGHT-10, V_ALLOWLOWERCASE, demo.titlename);
+	}
+
 	if (modeattacking)
 	{
 		V_DrawRightAlignedString((BASEVIDWIDTH/2)-4, BASEVIDHEIGHT-24, V_YELLOWMAP|V_MONOSPACE, "BEST TIME:");
@@ -2100,7 +2108,7 @@ static void HU_DrawDemoInfo(void)
 //
 // Song credits
 //
-static void HU_DrawSongCredits(void)
+void HU_DrawSongCredits(void)
 {
 	char *str;
 	INT32 len, destx;
@@ -2146,6 +2154,9 @@ static void HU_DrawSongCredits(void)
 //
 void HU_Drawer(void)
 {
+	if (cv_vhseffect.value && (paused || (demo.playback && cv_playbackspeed.value > 1)))
+		V_DrawVhsEffect(demo.rewinding);
+
 #ifndef NONET
 	// draw chat string plus cursor
 	if (chat_on)
@@ -2191,10 +2202,7 @@ void HU_Drawer(void)
 	if (cechotimer)
 		HU_DrawCEcho();
 
-	if (demoplayback && hu_showscores)
-		HU_DrawDemoInfo();
-
-	if (!Playing()
+	if (!( Playing() || demo.playback )
 	 || gamestate == GS_INTERMISSION || gamestate == GS_CUTSCENE
 	 || gamestate == GS_CREDITS      || gamestate == GS_EVALUATION
 	 || gamestate == GS_GAMEEND
@@ -2214,24 +2222,28 @@ void HU_Drawer(void)
 		LUAh_ScoresHUD();
 #endif
 		}
+		if (demo.playback)
+		{
+			HU_DrawDemoInfo();
+		}
 	}
 
 	if (gamestate != GS_LEVEL)
 		return;
 
 	// draw the crosshair, not when viewing demos nor with chasecam
-	/*if (!automapactive && !demoplayback)
+	/*if (!automapactive && !demo.playback)
 	{
-		if (cv_crosshair.value && !camera.chase && !players[displayplayer].spectator)
+		if (cv_crosshair.value && !camera[0].chase && !players[displayplayers[0]].spectator)
 			HU_DrawCrosshair();
 
-		if (cv_crosshair2.value && !camera2.chase && !players[secondarydisplayplayer].spectator)
+		if (cv_crosshair2.value && !camera[1].chase && !players[displayplayers[1]].spectator)
 			HU_DrawCrosshair2();
 
-		if (cv_crosshair3.value && !camera3.chase && !players[thirddisplayplayer].spectator)
+		if (cv_crosshair3.value && !camera[2].chase && !players[displayplayers[2]].spectator)
 			HU_DrawCrosshair3();
 
-		if (cv_crosshair4.value && !camera4.chase && !players[fourthdisplayplayer].spectator)
+		if (cv_crosshair4.value && !camera[3].chase && !players[displayplayers[3]].spectator)
 			HU_DrawCrosshair4();
 	}*/
 
@@ -2873,7 +2885,7 @@ static void HU_DrawRankings(void)
 	// When you play, you quickly see your score because your name is displayed in white.
 	// When playing back a demo, you quickly see who's the view.
 	if (!splitscreen)
-		whiteplayer = demoplayback ? displayplayer : consoleplayer;
+		whiteplayer = demo.playback ? displayplayers[0] : consoleplayer;
 
 	scorelines = 0;
 	memset(completed, 0, sizeof (completed));
diff --git a/src/hu_stuff.h b/src/hu_stuff.h
index 0f316bc78fda631b186857a1ffcc1634743e335b..be6798a823c38cb64211c46fdca236879486a7aa 100644
--- a/src/hu_stuff.h
+++ b/src/hu_stuff.h
@@ -109,6 +109,7 @@ void HU_Start(void);
 
 boolean HU_Responder(event_t *ev);
 void HU_Ticker(void);
+void HU_DrawSongCredits(void);
 void HU_Drawer(void);
 char HU_dequeueChatChar(void);
 void HU_Erase(void);
diff --git a/src/i_sound.h b/src/i_sound.h
index fd73d14541b4e61a1cfc8717051d83e7a3d855fb..9a5c2930afeb6a6af98fb22b8d5b0296c2446bc7 100644
--- a/src/i_sound.h
+++ b/src/i_sound.h
@@ -146,6 +146,18 @@ boolean I_SongPaused(void);
 
 boolean I_SetSongSpeed(float speed);
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void);
+
+boolean I_SetSongLoopPoint(UINT32 looppoint);
+UINT32 I_GetSongLoopPoint(void);
+
+boolean I_SetSongPosition(UINT32 position);
+UINT32 I_GetSongPosition(void);
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -216,6 +228,17 @@ void I_SetMusicVolume(UINT8 volume);
 
 boolean I_SetSongTrack(INT32 track);
 
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume);
+void I_StopFadingSong(void);
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void));
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
+boolean I_FadeOutStopSong(UINT32 ms);
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping);
+
 /// ------------------------
 //  CD MUSIC I/O
 /// ------------------------
diff --git a/src/i_tcp.c b/src/i_tcp.c
index 11a84cebad5def2d4ae852261540f759a3feffd5..fb0e5852d291d0755247d94bfbffc9224be65e10 100644
--- a/src/i_tcp.c
+++ b/src/i_tcp.c
@@ -813,6 +813,8 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 #endif
 #endif
 	mysockaddr_t straddr;
+	struct sockaddr_in sin;
+	socklen_t len = sizeof(sin);
 
 	if (s == (SOCKET_TYPE)ERRSOCKET)
 		return (SOCKET_TYPE)ERRSOCKET;
@@ -906,12 +908,16 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 			CONS_Printf(M_GetText("Network system buffer set to: %dKb\n"), opt>>10);
 	}
 
+	if (getsockname(s, (struct sockaddr *)&sin, &len) == -1)
+		CONS_Alert(CONS_WARNING, M_GetText("Failed to get port number\n"));
+	else
+		current_port = (UINT16)ntohs(sin.sin_port);
+
 	return s;
 }
 
 static boolean UDP_Socket(void)
 {
-	const char *sock_port = NULL;
 	size_t s;
 	struct my_addrinfo *ai, *runp, hints;
 	int gaie;
@@ -933,20 +939,11 @@ static boolean UDP_Socket(void)
 	hints.ai_socktype = SOCK_DGRAM;
 	hints.ai_protocol = IPPROTO_UDP;
 
-	if (M_CheckParm("-clientport"))
-	{
-		if (!M_IsNextParm())
-			I_Error("syntax: -clientport <portnum>");
-		sock_port = M_GetNextParm();
-	}
-	else
-		sock_port = port_name;
-
 	if (M_CheckParm("-bindaddr"))
 	{
 		while (M_IsNextParm())
 		{
-			gaie = I_getaddrinfo(M_GetNextParm(), sock_port, &hints, &ai);
+			gaie = I_getaddrinfo(M_GetNextParm(), port_name, &hints, &ai);
 			if (gaie == 0)
 			{
 				runp = ai;
@@ -967,7 +964,7 @@ static boolean UDP_Socket(void)
 	}
 	else
 	{
-		gaie = I_getaddrinfo("0.0.0.0", sock_port, &hints, &ai);
+		gaie = I_getaddrinfo("0.0.0.0", port_name, &hints, &ai);
 		if (gaie == 0)
 		{
 			runp = ai;
@@ -982,8 +979,8 @@ static boolean UDP_Socket(void)
 #ifdef HAVE_MINIUPNPC
 					if (UPNP_support)
 					{
-						I_UPnP_rem(sock_port, "UDP");
-						I_UPnP_add(NULL, sock_port, "UDP");
+						I_UPnP_rem(port_name, "UDP");
+						I_UPnP_add(NULL, port_name, "UDP");
 					}
 #endif
 				}
@@ -1000,7 +997,7 @@ static boolean UDP_Socket(void)
 		{
 			while (M_IsNextParm())
 			{
-				gaie = I_getaddrinfo(M_GetNextParm(), sock_port, &hints, &ai);
+				gaie = I_getaddrinfo(M_GetNextParm(), port_name, &hints, &ai);
 				if (gaie == 0)
 				{
 					runp = ai;
@@ -1021,7 +1018,7 @@ static boolean UDP_Socket(void)
 		}
 		else
 		{
-			gaie = I_getaddrinfo("::", sock_port, &hints, &ai);
+			gaie = I_getaddrinfo("::", port_name, &hints, &ai);
 			if (gaie == 0)
 			{
 				runp = ai;
@@ -1478,14 +1475,15 @@ boolean I_InitTcpNetwork(void)
 	if (!I_InitTcpDriver())
 		return false;
 
-	if (M_CheckParm("-udpport"))
+	if (M_CheckParm("-port"))
+	// Combined -udpport and -clientport into -port
+	// As it was really redundant having two seperate parms that does the same thing
 	{
 		if (M_IsNextParm())
 			strcpy(port_name, M_GetNextParm());
 		else
 			strcpy(port_name, "0");
 	}
-	current_port = (UINT16)atoi(port_name);
 
 	// parse network game options,
 	if (M_CheckParm("-server") || dedicated)
diff --git a/src/k_kart.c b/src/k_kart.c
index 7007585854b6d970d40a3453b5471220d678ad7c..10d772ba0fe7db969466725d5269494749ebdec1 100644
--- a/src/k_kart.c
+++ b/src/k_kart.c
@@ -49,22 +49,28 @@ const char *KartColor_Names[MAXSKINCOLORS] =
 	"Grey",           // SKINCOLOR_GREY
 	"Nickel",         // SKINCOLOR_NICKEL
 	"Black",          // SKINCOLOR_BLACK
+	"Skunk",          // SKINCOLOR_SKUNK
 	"Fairy",          // SKINCOLOR_FAIRY
 	"Popcorn",        // SKINCOLOR_POPCORN
+	"Artichoke",      // SKINCOLOR_ARTICHOKE
+	"Pigeon",         // SKINCOLOR_PIGEON
 	"Sepia",          // SKINCOLOR_SEPIA
 	"Beige",          // SKINCOLOR_BEIGE
+	"Walnut",         // SKINCOLOR_WALNUT
 	"Brown",          // SKINCOLOR_BROWN
 	"Leather",        // SKINCOLOR_LEATHER
 	"Salmon",         // SKINCOLOR_SALMON
 	"Pink",           // SKINCOLOR_PINK
 	"Rose",           // SKINCOLOR_ROSE
 	"Brick",          // SKINCOLOR_BRICK
+	"Cinnamon",       // SKINCOLOR_CINNAMON
 	"Ruby",           // SKINCOLOR_RUBY
 	"Raspberry",      // SKINCOLOR_RASPBERRY
 	"Cherry",         // SKINCOLOR_CHERRY
 	"Red",            // SKINCOLOR_RED
 	"Crimson",        // SKINCOLOR_CRIMSON
 	"Maroon",         // SKINCOLOR_MAROON
+	"Lemonade",       // SKINCOLOR_LEMONADE
 	"Flame",          // SKINCOLOR_FLAME
 	"Scarlet",        // SKINCOLOR_SCARLET
 	"Ketchup",        // SKINCOLOR_KETCHUP
@@ -83,8 +89,10 @@ const char *KartColor_Names[MAXSKINCOLORS] =
 	"Royal",          // SKINCOLOR_ROYAL
 	"Bronze",         // SKINCOLOR_BRONZE
 	"Copper",         // SKINCOLOR_COPPER
+	"Quarry",         // SKINCOLOR_QUARRY
 	"Yellow",         // SKINCOLOR_YELLOW
 	"Mustard",        // SKINCOLOR_MUSTARD
+	"Crocodile",      // SKINCOLOR_CROCODILE
 	"Olive",          // SKINCOLOR_OLIVE
 	"Vomit",          // SKINCOLOR_VOMIT
 	"Garden",         // SKINCOLOR_GARDEN
@@ -104,6 +112,7 @@ const char *KartColor_Names[MAXSKINCOLORS] =
 	"Plague",         // SKINCOLOR_PLAGUE
 	"Algae",          // SKINCOLOR_ALGAE
 	"Caribbean",      // SKINCOLOR_CARIBBEAN
+	"Azure",          // SKINCOLOR_AZURE
 	"Aqua",           // SKINCOLOR_AQUA
 	"Teal",           // SKINCOLOR_TEAL
 	"Cyan",           // SKINCOLOR_CYAN
@@ -113,7 +122,9 @@ const char *KartColor_Names[MAXSKINCOLORS] =
 	"Platinum",       // SKINCOLOR_PLATINUM
 	"Slate",          // SKINCOLOR_SLATE
 	"Steel",          // SKINCOLOR_STEEL
+	"Thunder",        // SKINCOLOR_THUNDER
 	"Rust",           // SKINCOLOR_RUST
+	"Wristwatch",     // SKINCOLOR_WRISTWATCH
 	"Jet",            // SKINCOLOR_JET
 	"Sapphire",       // SKINCOLOR_SAPPHIRE
 	"Periwinkle",     // SKINCOLOR_PERIWINKLE
@@ -144,22 +155,28 @@ const UINT8 KartColor_Opposite[MAXSKINCOLORS*2] =
 	SKINCOLOR_GREY,8,         // SKINCOLOR_GREY
 	SKINCOLOR_SILVER,8,       // SKINCOLOR_NICKEL
 	SKINCOLOR_WHITE,8,        // SKINCOLOR_BLACK
-	SKINCOLOR_CAMOUFLAGE,8,   // SKINCOLOR_FAIRY
-	SKINCOLOR_BUBBLEGUM,8,    // SKINCOLOR_POPCORN
+	SKINCOLOR_SKUNK,8,        // SKINCOLOR_SKUNK
+	SKINCOLOR_ARTICHOKE,12,   // SKINCOLOR_FAIRY
+	SKINCOLOR_PIGEON,12,      // SKINCOLOR_POPCORN
+	SKINCOLOR_FAIRY,12,       // SKINCOLOR_ARTICHOKE
+	SKINCOLOR_POPCORN,12,     // SKINCOLOR_PIGEON
 	SKINCOLOR_LEATHER,6,      // SKINCOLOR_SEPIA
 	SKINCOLOR_BROWN,2,        // SKINCOLOR_BEIGE
+	SKINCOLOR_CAMOUFLAGE,8,   // SKINCOLOR_WALNUT
 	SKINCOLOR_BEIGE,8,        // SKINCOLOR_BROWN
 	SKINCOLOR_SEPIA,8,        // SKINCOLOR_LEATHER
 	SKINCOLOR_TEA,8,          // SKINCOLOR_SALMON
 	SKINCOLOR_PISTACHIO,8,    // SKINCOLOR_PINK
 	SKINCOLOR_MOSS,8,         // SKINCOLOR_ROSE
 	SKINCOLOR_RUST,8,         // SKINCOLOR_BRICK
+	SKINCOLOR_WRISTWATCH,6,   // SKINCOLOR_CINNAMON
 	SKINCOLOR_SAPPHIRE,8,     // SKINCOLOR_RUBY
 	SKINCOLOR_MINT,8,         // SKINCOLOR_RASPBERRY
 	SKINCOLOR_HANDHELD,10,    // SKINCOLOR_CHERRY
 	SKINCOLOR_GREEN,6,        // SKINCOLOR_RED
 	SKINCOLOR_PINETREE,6,     // SKINCOLOR_CRIMSON
 	SKINCOLOR_TOXIC,8,        // SKINCOLOR_MAROON
+	SKINCOLOR_THUNDER,8,      // SKINCOLOR_LEMONADE
 	SKINCOLOR_CARIBBEAN,10,   // SKINCOLOR_FLAME
 	SKINCOLOR_ALGAE,10,       // SKINCOLOR_SCARLET
 	SKINCOLOR_MUSTARD,10,     // SKINCOLOR_KETCHUP
@@ -178,8 +195,10 @@ const UINT8 KartColor_Opposite[MAXSKINCOLORS*2] =
 	SKINCOLOR_PLATINUM,6,     // SKINCOLOR_ROYAL
 	SKINCOLOR_STEEL,8,        // SKINCOLOR_BRONZE
 	SKINCOLOR_CREAM,6,        // SKINCOLOR_COPPER
+	SKINCOLOR_AZURE,8,        // SKINCOLOR_QUARRY
 	SKINCOLOR_AQUA,8,         // SKINCOLOR_YELLOW
 	SKINCOLOR_KETCHUP,8,      // SKINCOLOR_MUSTARD
+	SKINCOLOR_BUBBLEGUM,8,    // SKINCOLOR_CROCODILE
 	SKINCOLOR_TEAL,8,         // SKINCOLOR_OLIVE
 	SKINCOLOR_ROBOHOOD,8,     // SKINCOLOR_VOMIT
 	SKINCOLOR_LAVENDER,6,     // SKINCOLOR_GARDEN
@@ -188,7 +207,7 @@ const UINT8 KartColor_Opposite[MAXSKINCOLORS*2] =
 	SKINCOLOR_SALMON,8,       // SKINCOLOR_TEA
 	SKINCOLOR_PINK,6,         // SKINCOLOR_PISTACHIO
 	SKINCOLOR_ROSE,8,         // SKINCOLOR_MOSS
-	SKINCOLOR_FAIRY,10,       // SKINCOLOR_CAMOUFLAGE
+	SKINCOLOR_WALNUT,8,       // SKINCOLOR_CAMOUFLAGE
 	SKINCOLOR_VOMIT,8,        // SKINCOLOR_ROBOHOOD
 	SKINCOLOR_RASPBERRY,8,    // SKINCOLOR_MINT
 	SKINCOLOR_RED,8,          // SKINCOLOR_GREEN
@@ -199,6 +218,7 @@ const UINT8 KartColor_Opposite[MAXSKINCOLORS*2] =
 	SKINCOLOR_NOVA,8,         // SKINCOLOR_PLAGUE
 	SKINCOLOR_SCARLET,10,     // SKINCOLOR_ALGAE
 	SKINCOLOR_FLAME,8,        // SKINCOLOR_CARIBBEAN
+	SKINCOLOR_QUARRY,8,       // SKINCOLOR_AZURE
 	SKINCOLOR_YELLOW,8,       // SKINCOLOR_AQUA
 	SKINCOLOR_OLIVE,8,        // SKINCOLOR_TEAL
 	SKINCOLOR_PEACH,8,        // SKINCOLOR_CYAN
@@ -208,7 +228,9 @@ const UINT8 KartColor_Opposite[MAXSKINCOLORS*2] =
 	SKINCOLOR_ROYAL,8,        // SKINCOLOR_PLATINUM
 	SKINCOLOR_GOLD,10,        // SKINCOLOR_SLATE
 	SKINCOLOR_BRONZE,10,      // SKINCOLOR_STEEL
+	SKINCOLOR_LEMONADE,8,     // SKINCOLOR_THUNDER
 	SKINCOLOR_BRICK,10,       // SKINCOLOR_RUST
+	SKINCOLOR_CINNAMON,8,     // SKINCOLOR_WRISTWATCH
 	SKINCOLOR_BURGUNDY,8,     // SKINCOLOR_JET
 	SKINCOLOR_RUBY,6,         // SKINCOLOR_SAPPHIRE
 	SKINCOLOR_CREAMSICLE,8,   // SKINCOLOR_PERIWINKLE
@@ -219,6 +241,7 @@ const UINT8 KartColor_Opposite[MAXSKINCOLORS*2] =
 	SKINCOLOR_SUNSET,10,      // SKINCOLOR_MOONSLAM
 	SKINCOLOR_MAUVE,10,       // SKINCOLOR_ULTRAVIOLET
 	SKINCOLOR_DAWN,6,         // SKINCOLOR_DUSK
+	SKINCOLOR_CROCODILE,8,    // SKINCOLOR_BUBBLEGUM
 	SKINCOLOR_EMERALD,8,      // SKINCOLOR_PURPLE
 	SKINCOLOR_PASTEL,11,      // SKINCOLOR_FUCHSIA
 	SKINCOLOR_MAROON,8,       // SKINCOLOR_TOXIC
@@ -236,27 +259,33 @@ UINT8 colortranslations[MAXTRANSLATIONS][16] = {
 	{  1,   3,   5,   7,   9,  11,  13,  15,  17,  19,  21,  23,  25,  27,  29,  31}, // SKINCOLOR_GREY
 	{  3,   5,   8,  11,  15,  17,  19,  21,  23,  24,  25,  26,  27,  29,  30,  31}, // SKINCOLOR_NICKEL
 	{  4,   7,  11,  15,  20,  22,  24,  27,  28,  28,  28,  29,  29,  30,  30,  31}, // SKINCOLOR_BLACK
+	{120, 120,   0,   2,   4,  10,  16,  22,  23,  24,  25,  26,  27,  28,  29,  31}, // SKINCOLOR_SKUNK
 	{120, 120, 121, 121, 122, 123,  10,  14,  16,  18,  20,  22,  24,  26,  28,  31}, // SKINCOLOR_FAIRY
 	{120,  96,  97,  98,  99,  71,  32,  11,  13,  16,  18,  21,  23,  26,  28,  31}, // SKINCOLOR_POPCORN
+	{ 97, 176, 177, 162, 163, 179,  12,  14,  16,  18,  20,  22,  24,  26,  28,  31}, // SKINCOLOR_ARTICHOKE
+	{  0, 208, 209, 211, 226, 202,  14,  15,  17,  19,  21,  23,  25,  27,  29,  31}, // SKINCOLOR_PIGEON
 	{  0,   1,   3,   5,   7,   9,  34,  36,  38,  40,  42,  44,  60,  61,  62,  63}, // SKINCOLOR_SEPIA
 	{120,  65,  67,  69,  32,  34,  36,  38,  40,  42,  44,  45,  46,  47,  62,  63}, // SKINCOLOR_BEIGE
+	{  3,   6,  32,  33,  35,  37,  51,  52,  54,  55,  57,  58,  60,  61,  63,  30}, // SKINCOLOR_WALNUT
 	{ 67,  70,  73,  76,  48,  49,  51,  53,  54,  56,  58,  59,  61,  63,  29,  30}, // SKINCOLOR_BROWN
 	{ 72,  76,  48,  51,  53,  55,  57,  59,  61,  63,  28,  28,  29,  29,  30,  31}, // SKINCOLOR_LEATHER
 	{120, 120, 120, 121, 121, 122, 123, 124, 126, 127, 129, 131, 133, 135, 137, 139}, // SKINCOLOR_SALMON
 	{120, 121, 121, 122, 144, 145, 146, 147, 148, 149, 150, 151, 134, 136, 138, 140}, // SKINCOLOR_PINK
 	{144, 145, 146, 147, 148, 149, 150, 151, 134, 135, 136, 137, 138, 139, 140, 141}, // SKINCOLOR_ROSE
 	{ 64,  67,  70,  73, 146, 147, 148, 150, 118, 118, 119, 119, 156, 159, 141, 143}, // SKINCOLOR_BRICK
+	{ 68,  75,  48,  50,  52,  94, 152, 136, 137, 138, 139, 140, 141, 142, 143,  31}, // SKINCOLOR_CINNAMON
 	{120, 121, 144, 145, 147, 149, 132, 133, 134, 136, 198, 198, 199, 255,  30,  31}, // SKINCOLOR_RUBY
 	{120, 121, 122, 123, 124, 125, 126, 127, 128, 130, 131, 134, 136, 137, 139, 140}, // SKINCOLOR_RASPBERRY
 	{120,  65,  67,  69,  71, 124, 125, 127, 132, 133, 135, 136, 138, 139, 140, 141}, // SKINCOLOR_CHERRY
 	{122, 123, 124, 126, 129, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142}, // SKINCOLOR_RED
 	{123, 125, 128, 131, 133, 135, 136, 138, 140, 140, 141, 141, 142, 142, 143,  31}, // SKINCOLOR_CRIMSON
 	{123, 124, 126, 128, 132, 135, 137,  27,  28,  28,  28,  29,  29,  30,  30,  31}, // SKINCOLOR_MAROON
+	{120,  96,  97,  98,  99,  65, 122, 144, 123, 124, 147, 149, 151, 153, 156, 159}, // SKINCOLOR_LEMONADE
 	{120,  97, 112, 113, 113,  85,  87, 126, 149, 150, 151, 252, 253, 254, 255,  29}, // SKINCOLOR_FLAME
 	{ 99, 113, 113,  84,  85,  87, 126, 128, 130, 196, 197, 198, 199, 240, 243, 246}, // SKINCOLOR_SCARLET
 	{103, 113, 113,  84,  85,  88, 127, 130, 131, 133, 134, 136, 138, 139, 141, 143}, // SKINCOLOR_KETCHUP
-	{120, 121, 122, 123, 124, 147, 147, 148,  90,  91,  92,  93,  94,  95, 152, 154}, // SKINCOLOR_DAWN
-	{ 98, 112, 113,  84,  85,  87,  89, 149, 150, 251, 252, 206, 238, 240, 243, 246}, // SKINCOLOR_SUNSET
+	{120, 121, 122, 123, 124, 147, 148,  91,  93,  95, 152, 154, 156, 159, 141, 143}, // SKINCOLOR_DAWN
+	{ 98, 112, 113,  84,  85,  87,  89, 149, 150, 251, 251, 205, 206, 207,  29,  31}, // SKINCOLOR_SUNSET
 	{120, 120,  80,  80,  81,  82,  83,  83,  84,  85,  86,  88,  89,  91,  93,  95}, // SKINCOLOR_CREAMSICLE
 	{ 80,  81,  82,  83,  84,  85,  86,  88,  89,  91,  94,  95, 154, 156, 158, 159}, // SKINCOLOR_ORANGE
 	{ 82,  83,  84,  85,  87,  89,  90,  92,  94, 152, 153, 155, 157, 159, 141, 142}, // SKINCOLOR_PUMPKIN
@@ -265,17 +294,19 @@ UINT8 colortranslations[MAXTRANSLATIONS][16] = {
 	{ 98,  98, 112, 112, 113, 113,  84,  85,  87,  89,  91,  93,  95, 153, 156, 159}, // SKINCOLOR_TANGERINE
 	{120,  80,  66,  70,  72,  76, 148, 149, 150, 151, 153, 154, 156,  61,  62,  63}, // SKINCOLOR_PEACH
 	{ 64,  66,  68,  70,  72,  74,  76,  78,  48,  50,  52,  54,  56,  58,  60,  62}, // SKINCOLOR_CARAMEL
-	{120, 120,  96,  96,  97,  82,  84,  77,  50,  54,  57,  59,  61,  63,  29,  31}, // SKINCOLOR_CREAM
-	{112, 112, 112, 113, 113, 114, 114, 115, 115, 116, 116, 117, 117, 118, 118, 119}, // SKINCOLOR_GOLD
+	{120,  96,  96,  97,  98,  82,  84,  77,  50,  54,  57,  59,  61,  63,  29,  31}, // SKINCOLOR_CREAM
+	{ 96,  97,  98, 112, 113, 114, 115, 116, 117, 151, 118, 119, 157, 159, 140, 143}, // SKINCOLOR_GOLD
 	{ 97, 112, 113, 113, 114,  78,  53, 252, 252, 253, 253, 254, 255,  29,  30,  31}, // SKINCOLOR_ROYAL
 	{112, 113, 114, 115, 116, 117, 118, 119, 156, 157, 158, 159, 141, 141, 142, 143}, // SKINCOLOR_BRONZE
 	{120,  99, 113, 114, 116, 117, 119,  61,  63,  28,  28,  29,  29,  30,  30,  31}, // SKINCOLOR_COPPER
+	{ 96,  97,  98,  99, 104, 105, 106, 107, 117, 152, 154, 156, 159, 141, 142, 143}, // SKINCOLOR_QUARRY
 	{ 96,  97,  98, 100, 101, 102, 104, 113, 114, 115, 116, 117, 118, 119, 156, 159}, // SKINCOLOR_YELLOW
 	{ 96,  98,  99, 112, 113, 114, 114, 106, 106, 107, 107, 108, 108, 109, 110, 111}, // SKINCOLOR_MUSTARD
-	{105, 105, 105, 106, 106, 107, 107, 108, 108, 109, 109, 110, 110, 111, 111,  31}, // SKINCOLOR_OLIVE
-	{121, 144, 145,  72,  73,  84, 114, 115, 107, 108, 109, 183, 223, 207,  30, 246}, // SKINCOLOR_VOMIT
+	{120,  96,  97,  98, 176, 113, 114, 106, 115, 107, 108, 109, 110, 174, 175,  31}, // SKINCOLOR_CROCODILE
+	{ 98, 101, 104, 105, 106, 115, 107, 108, 182, 109, 183, 110, 174, 111,  30,  31}, // SKINCOLOR_OLIVE
+	{  0, 121, 122, 144,  71,  84, 114, 115, 107, 108, 109, 183, 223, 207,  30, 246}, // SKINCOLOR_VOMIT
 	{ 98,  99, 112, 101, 113, 114, 106, 179, 180, 180, 181, 182, 183, 173, 174, 175}, // SKINCOLOR_GARDEN
-	{ 96,  97,  99, 100, 102, 104, 160, 162, 164, 166, 168, 171, 223, 223, 207,  31}, // SKINCOLOR_LIME
+	{120,  96,  97,  98,  99, 176, 177, 163, 164, 166, 168, 170, 223, 207, 243,  31}, // SKINCOLOR_LIME
 	{ 98, 104, 105, 105, 106, 167, 168, 169, 170, 171, 172, 173, 174, 175,  30,  31}, // SKINCOLOR_HANDHELD
 	{120, 120, 176, 176, 176, 177, 177, 178, 178, 179, 179, 180, 180, 181, 182, 183}, // SKINCOLOR_TEA
 	{120, 120, 176, 176, 177, 177, 178, 179, 165, 166, 167, 168, 169, 170, 171, 172}, // SKINCOLOR_PISTACHIO
@@ -288,9 +319,10 @@ UINT8 colortranslations[MAXTRANSLATIONS][16] = {
 	{160, 184, 184, 185, 185, 186, 186, 187, 187, 188, 188, 189, 189, 190, 191, 175}, // SKINCOLOR_EMERALD
 	{160, 184, 185, 186, 187, 188, 189, 190, 191, 191,  29,  29,  30,  30,  31,  31}, // SKINCOLOR_SWAMP
 	{120, 120,  80,  80,  81, 177, 162, 164, 228, 228, 204, 204, 205, 205, 206, 207}, // SKINCOLOR_DREAM
-	{176, 160, 184, 185, 186, 187, 188, 230, 230, 206, 206, 207,  28,  29,  30,  31}, // SKINCOLOR_PLAGUE
+	{ 97, 176, 160, 184, 185, 186, 187, 229, 229, 205, 206, 207,  28,  29,  30,  31}, // SKINCOLOR_PLAGUE
 	{208, 209, 210, 211, 213, 220, 216, 167, 168, 188, 188, 189, 190, 191,  30,  31}, // SKINCOLOR_ALGAE
-	{120, 176, 177, 160, 185, 220, 216, 217, 221, 230, 206, 206, 254, 255,  29,  31}, // SKINCOLOR_CARIBBEAN
+	{120, 176, 177, 160, 185, 220, 216, 217, 229, 229, 204, 205, 206, 254, 255,  31}, // SKINCOLOR_CARIBBEAN
+	{120,  96,  97,  98, 177, 220, 216, 217, 218, 204, 252, 253, 254, 255,  30,  31}, // SKINCOLOR_AZURE
 	{120, 208, 208, 210, 212, 214, 220, 220, 220, 221, 221, 222, 222, 223, 223, 191}, // SKINCOLOR_AQUA
 	{210, 213, 220, 220, 220, 216, 216, 221, 221, 221, 222, 222, 223, 223, 191,  31}, // SKINCOLOR_TEAL
 	{120, 120, 208, 208, 209, 210, 211, 212, 213, 215, 216, 217, 218, 219, 222, 223}, // SKINCOLOR_CYAN
@@ -300,7 +332,9 @@ UINT8 colortranslations[MAXTRANSLATIONS][16] = {
 	{120,   0,   0, 200, 200, 201,  11,  14,  17, 218, 222, 223, 238, 240, 243, 246}, // SKINCOLOR_PLATINUM
 	{120, 120, 200, 200, 200, 201, 201, 201, 202, 202, 202, 203, 204, 205, 206, 207}, // SKINCOLOR_SLATE
 	{120, 200, 200, 201, 201, 202, 202, 203, 203, 204, 204, 205, 205, 206, 207,  31}, // SKINCOLOR_STEEL
+	{ 96,  97,  98, 112, 113, 114,  11, 203, 204, 205, 205, 237, 239, 241, 243, 246}, // SKINCOLOR_THUNDER
 	{ 64,  66,  68,  70,  32,  34,  36, 203, 204, 205,  24,  25,  26,  28,  29,  31}, // SKINCOLOR_RUST
+	{ 81,  72,  76,  48,  51,  55, 252, 205, 205, 206, 240, 241, 242, 243, 244, 246}, // SKINCOLOR_WRISTWATCH
 	{225, 226, 227, 228, 229, 205, 205, 206, 207, 207,  28,  28,  29,  29,  30,  31}, // SKINCOLOR_JET
 	{208, 209, 211, 213, 215, 217, 229, 230, 232, 234, 236, 238, 240, 242, 244, 246}, // SKINCOLOR_SAPPHIRE
 	{120, 120, 224, 225, 226, 202, 227, 228, 229, 230, 231, 233, 235, 237, 239, 241}, // SKINCOLOR_PERIWINKLE
@@ -317,9 +351,10 @@ UINT8 colortranslations[MAXTRANSLATIONS][16] = {
 	{120, 120, 176, 176, 177,   6,   8,  10, 249, 250, 196, 197, 198, 199, 143,  31}, // SKINCOLOR_TOXIC
 	{ 96,  97,  98, 112, 113,  73, 146, 248, 249, 251, 205, 205, 206, 207,  29,  31}, // SKINCOLOR_MAUVE
 	{121, 145, 192, 248, 249, 250, 251, 252, 252, 253, 253, 254, 254, 255,  30,  31}, // SKINCOLOR_LAVENDER
-	{144, 248, 249, 250, 251, 252, 253, 254, 255, 255,  29,  29,  30,  30,  31,  31}, // SKINCOLOR_BYZANTIUM
+	{201, 248, 249, 250, 251, 252, 253, 254, 255, 255,  29,  29,  30,  30,  31,  31}, // SKINCOLOR_BYZANTIUM
 	{144, 145, 146, 147, 148, 149, 150, 251, 251, 252, 252, 253, 254, 255,  29,  30}, // SKINCOLOR_POMEGRANATE
 	{120, 120, 120, 121, 121, 122, 122, 123, 192, 248, 249, 250, 251, 252, 253, 254}, // SKINCOLOR_LILAC
+
 	{120, 120, 120, 120, 120, 120, 120, 120, 120, 120,  96, 100, 104, 113, 116, 119}, // SKINCOLOR_SUPER1
 	{120, 120, 120, 120, 120, 120, 120, 120,  96,  98, 101, 104, 113, 115, 117, 119}, // SKINCOLOR_SUPER2
 	{120, 120, 120, 120, 120, 120,  96,  98, 100, 102, 104, 113, 114, 116, 117, 119}, // SKINCOLOR_SUPER3
@@ -1027,34 +1062,14 @@ static void K_KartItemRoulette(player_t *player, ticcmd_t *cmd)
 	}
 
 	// This makes the roulette produce the random noises.
-	if ((player->kartstuff[k_itemroulette] % 3) == 1 && P_IsLocalPlayer(player))
+	if ((player->kartstuff[k_itemroulette] % 3) == 1 && P_IsDisplayPlayer(player))
 	{
-#define PLAYROULETTESND S_StartSound(NULL, sfx_itrol1 + ((player->kartstuff[k_itemroulette] / 3) % 8));
-		if (splitscreen)
+#define PLAYROULETTESND S_StartSound(NULL, sfx_itrol1 + ((player->kartstuff[k_itemroulette] / 3) % 8))
+		for (i = 0; i <= splitscreen; i++)
 		{
-			if (players[displayplayer].kartstuff[k_itemroulette])
-			{
-				if (player == &players[displayplayer])
-					PLAYROULETTESND;
-			}
-			else if (players[secondarydisplayplayer].kartstuff[k_itemroulette])
-			{
-				if (player == &players[secondarydisplayplayer])
-					PLAYROULETTESND;
-			}
-			else if (players[thirddisplayplayer].kartstuff[k_itemroulette] && splitscreen > 1)
-			{
-				if (player == &players[thirddisplayplayer])
-					PLAYROULETTESND;
-			}
-			else if (players[fourthdisplayplayer].kartstuff[k_itemroulette] && splitscreen > 2)
-			{
-				if (player == &players[fourthdisplayplayer])
-					PLAYROULETTESND;
-			}
+			if (player == &players[displayplayers[i]] && players[displayplayers[i]].kartstuff[k_itemroulette])
+				PLAYROULETTESND;
 		}
-		else
-			PLAYROULETTESND;
 #undef PLAYROULETTESND
 	}
 
@@ -1081,7 +1096,7 @@ static void K_KartItemRoulette(player_t *player, ticcmd_t *cmd)
 		//player->kartstuff[k_itemblinkmode] = 1;
 		player->kartstuff[k_itemroulette] = 0;
 		player->kartstuff[k_roulettetype] = 0;
-		if (P_IsLocalPlayer(player))
+		if (P_IsDisplayPlayer(player))
 			S_StartSound(NULL, sfx_itrole);
 		return;
 	}
@@ -1094,7 +1109,7 @@ static void K_KartItemRoulette(player_t *player, ticcmd_t *cmd)
 		player->kartstuff[k_itemblinkmode] = 2;
 		player->kartstuff[k_itemroulette] = 0;
 		player->kartstuff[k_roulettetype] = 0;
-		if (P_IsLocalPlayer(player))
+		if (P_IsDisplayPlayer(player))
 			S_StartSound(NULL, sfx_dbgsal);
 		return;
 	}
@@ -1126,7 +1141,7 @@ static void K_KartItemRoulette(player_t *player, ticcmd_t *cmd)
 		player->kartstuff[k_itemamount] = 1;
 	}
 
-	if (P_IsLocalPlayer(player))
+	if (P_IsDisplayPlayer(player))
 		S_StartSound(NULL, ((player->kartstuff[k_roulettetype] == 1) ? sfx_itrolk : (mashed ? sfx_itrolm : sfx_itrolf)));
 
 	player->kartstuff[k_itemblink] = TICRATE;
@@ -1567,7 +1582,7 @@ void K_RespawnChecker(player_t *player)
 
 		if (!P_IsObjectOnGround(player->mo) && !mapreset)
 		{
-			player->powers[pw_flashing] = 2;
+			player->powers[pw_flashing] = K_GetKartFlashing(player);
 
 			// Sal: The old behavior was stupid and prone to accidental usage.
 			// Let's rip off Mania instead, and turn this into a Drop Dash!
@@ -1633,7 +1648,7 @@ void K_KartMoveAnimation(player_t *player)
 			P_SetPlayerMobjState(player->mo, S_KART_DRIFT1_R);
 	}
 	// Run frames - S_KART_RUN1   S_KART_RUN1_L   S_KART_RUN1_R
-	else if (player->speed > FixedMul(player->runspeed, player->mo->scale))
+	else if (player->speed > (20*player->mo->scale))
 	{
 		if (cmd->driftturn < 0 && !(player->mo->state >= &states[S_KART_RUN1_R] && player->mo->state <= &states[S_KART_RUN2_R]))
 			P_SetPlayerMobjState(player->mo, S_KART_RUN1_R);
@@ -1643,7 +1658,7 @@ void K_KartMoveAnimation(player_t *player)
 			P_SetPlayerMobjState(player->mo, S_KART_RUN1);
 	}
 	// Walk frames - S_KART_WALK1   S_KART_WALK1_L   S_KART_WALK1_R
-	else if (player->speed <= FixedMul(player->runspeed, player->mo->scale))
+	else if (player->speed <= (20*player->mo->scale))
 	{
 		if (cmd->driftturn < 0 && !(player->mo->state >= &states[S_KART_WALK1_R] && player->mo->state <= &states[S_KART_WALK2_R]))
 			P_SetPlayerMobjState(player->mo, S_KART_WALK1_R);
@@ -2123,11 +2138,13 @@ void K_SpinPlayer(player_t *player, mobj_t *source, INT32 type, mobj_t *inflicto
 
 static void K_RemoveGrowShrink(player_t *player)
 {
-	player->kartstuff[k_growshrinktimer] = 0;
-	player->kartstuff[k_growcancel] = 0;
-
 	if (player->mo && !P_MobjWasRemoved(player->mo))
 	{
+		if (player->kartstuff[k_growshrinktimer] > 0) // Play Shrink noise
+			S_StartSound(player->mo, sfx_kc59);
+		else if (player->kartstuff[k_growshrinktimer] < 0) // Play Grow noise
+			S_StartSound(player->mo, sfx_kc5a);
+
 		if (player->kartstuff[k_invincibilitytimer] == 0)
 			player->mo->color = player->skincolor;
 
@@ -2137,6 +2154,9 @@ static void K_RemoveGrowShrink(player_t *player)
 			player->mo->destscale = (6*player->mo->destscale)/8;
 	}
 
+	player->kartstuff[k_growshrinktimer] = 0;
+	player->kartstuff[k_growcancel] = -1;
+
 	P_RestoreMusic(player);
 }
 
@@ -3521,11 +3541,13 @@ void K_DoSneaker(player_t *player, INT32 type)
 
 	player->kartstuff[k_sneakertimer] = sneakertime;
 
+	// set angle for spun out players:
+	player->kartstuff[k_boostangle] = (INT32)player->mo->angle;
+
 	if (type != 0)
 	{
 		player->pflags |= PF_ATTACKDOWN;
 		K_PlayBoostTaunt(player->mo);
-		player->powers[pw_flashing] = 0; // Stop flashing after boosting
 	}
 }
 
@@ -3544,8 +3566,13 @@ static void K_DoShrink(player_t *user)
 			continue;
 		if (players[i].kartstuff[k_position] < user->kartstuff[k_position])
 		{
+			//P_FlashPal(&players[i], PAL_NUKE, 10);
+
+			// Grow should get taken away.
+			if (players[i].kartstuff[k_growshrinktimer] > 0)
+				K_RemoveGrowShrink(&players[i]);
 			// Don't hit while invulnerable!
-			if (!players[i].kartstuff[k_invincibilitytimer]
+			else if (!players[i].kartstuff[k_invincibilitytimer]
 				&& players[i].kartstuff[k_growshrinktimer] <= 0
 				&& !players[i].kartstuff[k_hyudorotimer])
 			{
@@ -3559,15 +3586,9 @@ static void K_DoShrink(player_t *user)
 					players[i].mo->destscale = (6*mapobjectscale)/8;
 					if (cv_kartdebugshrink.value && !modeattacking && !players[i].bot)
 						players[i].mo->destscale = (6*players[i].mo->destscale)/8;
+					S_StartSound(players[i].mo, sfx_kc59);
 				}
 			}
-
-			// Grow should get taken away.
-			if (players[i].kartstuff[k_growshrinktimer] > 0)
-				K_RemoveGrowShrink(&players[i]);
-
-			//P_FlashPal(&players[i], PAL_NUKE, 10);
-			S_StartSound(players[i].mo, sfx_kc59);
 		}
 	}
 }
@@ -4331,10 +4352,7 @@ static void K_UpdateEngineSounds(player_t *player, ticcmd_t *cmd)
 		if (!playeringame[i] || !players[i].mo || players[i].spectator || players[i].exiting)
 			continue;
 
-		if ((i == displayplayer)
-			|| (i == secondarydisplayplayer && splitscreen)
-			|| (i == thirddisplayplayer && splitscreen > 1)
-			|| (i == fourthdisplayplayer && splitscreen > 2))
+		if (P_IsDisplayPlayer(&players[i]))
 		{
 			volumedampen += FRACUNIT; // We already know what this is gonna be, let's not waste our time.
 			continue;
@@ -4454,6 +4472,11 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
 {
 	K_UpdateOffroad(player);
 	K_UpdateEngineSounds(player, cmd); // Thanks, VAda!
+
+	// update boost angle if not spun out
+	if (!player->kartstuff[k_spinouttimer] && !player->kartstuff[k_wipeoutslow])
+		player->kartstuff[k_boostangle] = (INT32)player->mo->angle;
+
 	K_GetKartBoostPower(player);
 
 	// Speed lines
@@ -4559,7 +4582,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
 	{
 		player->powers[pw_flashing] = K_GetKartFlashing(player);
 	}
-	else if (player->powers[pw_flashing] == K_GetKartFlashing(player))
+	else if (player->powers[pw_flashing] >= K_GetKartFlashing(player))
 	{
 		player->powers[pw_flashing]--;
 	}
@@ -5031,6 +5054,7 @@ static void K_KartDrift(player_t *player, boolean onground)
 
 	if ((!player->kartstuff[k_sneakertimer])
 	|| (!player->cmd.driftturn)
+	|| (!player->kartstuff[k_aizdriftstrat])
 	|| (player->cmd.driftturn > 0) != (player->kartstuff[k_aizdriftstrat] > 0))
 	{
 		if (!player->kartstuff[k_drift])
@@ -5306,14 +5330,24 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 		// Grow Canceling
 		else if (player->kartstuff[k_growshrinktimer] > 0)
 		{
-			if (cmd->buttons & BT_ATTACK)
+			if (player->kartstuff[k_growcancel] >= 0)
 			{
-				player->kartstuff[k_growcancel]++;
-				if (player->kartstuff[k_growcancel] > 26)
-					K_RemoveGrowShrink(player);
+				if (cmd->buttons & BT_ATTACK)
+				{
+					player->kartstuff[k_growcancel]++;
+					if (player->kartstuff[k_growcancel] > 26)
+						K_RemoveGrowShrink(player);
+				}
+				else
+					player->kartstuff[k_growcancel] = 0;
 			}
 			else
-				player->kartstuff[k_growcancel] = 0;
+			{
+				if ((cmd->buttons & BT_ATTACK) || (player->pflags & PF_ATTACKDOWN))
+					player->kartstuff[k_growcancel] = -1;
+				else
+					player->kartstuff[k_growcancel] = 0;
+			}
 		}
 		else if (player->kartstuff[k_itemamount] <= 0)
 		{
@@ -5569,16 +5603,21 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 					if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO
 						&& player->kartstuff[k_growshrinktimer] <= 0) // Grow holds the item box hostage
 					{
-						K_PlayPowerGloatSound(player->mo);
-						player->mo->scalespeed = mapobjectscale/TICRATE;
-						player->mo->destscale = (3*mapobjectscale)/2;
-						if (cv_kartdebugshrink.value && !modeattacking && !player->bot)
-							player->mo->destscale = (6*player->mo->destscale)/8;
-						player->kartstuff[k_growshrinktimer] = itemtime+(4*TICRATE); // 12 seconds
-						P_RestoreMusic(player);
-						if (!P_IsLocalPlayer(player))
-							S_StartSound(player->mo, (cv_kartinvinsfx.value ? sfx_alarmg : sfx_kgrow));
-						S_StartSound(player->mo, sfx_kc5a);
+						if (player->kartstuff[k_growshrinktimer] < 0) // If you're shrunk, then "grow" will just make you normal again.
+							K_RemoveGrowShrink(player);
+						else
+						{
+							K_PlayPowerGloatSound(player->mo);
+							player->mo->scalespeed = mapobjectscale/TICRATE;
+							player->mo->destscale = (3*mapobjectscale)/2;
+							if (cv_kartdebugshrink.value && !modeattacking && !player->bot)
+								player->mo->destscale = (6*player->mo->destscale)/8;
+							player->kartstuff[k_growshrinktimer] = itemtime+(4*TICRATE); // 12 seconds
+							P_RestoreMusic(player);
+							if (!P_IsLocalPlayer(player))
+								S_StartSound(player->mo, (cv_kartinvinsfx.value ? sfx_alarmg : sfx_kgrow));
+							S_StartSound(player->mo, sfx_kc5a);
+						}
 						player->kartstuff[k_itemamount]--;
 					}
 					break;
@@ -5673,7 +5712,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 			player->kartstuff[k_curshield] = 0;
 
 		if (player->kartstuff[k_growshrinktimer] <= 0)
-			player->kartstuff[k_growcancel] = 0;
+			player->kartstuff[k_growcancel] = -1;
 
 		if (player->kartstuff[k_itemtype] == KITEM_SPB
 			|| player->kartstuff[k_itemtype] == KITEM_SHRINK
@@ -5691,13 +5730,13 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 
 				if (player->kartstuff[k_hyudorotimer] >= (1*TICRATE/2) && player->kartstuff[k_hyudorotimer] <= hyudorotime-(1*TICRATE/2))
 				{
-					if (player == &players[secondarydisplayplayer])
+					if (player == &players[displayplayers[1]])
 						player->mo->eflags |= MFE_DRAWONLYFORP2;
-					else if (player == &players[thirddisplayplayer] && splitscreen > 1)
+					else if (player == &players[displayplayers[2]] && splitscreen > 1)
 						player->mo->eflags |= MFE_DRAWONLYFORP3;
-					else if (player == &players[fourthdisplayplayer] && splitscreen > 2)
+					else if (player == &players[displayplayers[3]] && splitscreen > 2)
 						player->mo->eflags |= MFE_DRAWONLYFORP4;
-					else if (player == &players[consoleplayer])
+					else if (player == &players[displayplayers[0]])
 						player->mo->eflags |= MFE_DRAWONLYFORP1;
 					else
 						player->mo->flags2 |= MF2_DONTDRAW;
@@ -5707,8 +5746,8 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 			}
 			else
 			{
-				if (player == &players[displayplayer]
-					|| (player != &players[displayplayer] && (player->kartstuff[k_hyudorotimer] < (1*TICRATE/2) || player->kartstuff[k_hyudorotimer] > hyudorotime-(1*TICRATE/2))))
+				if (P_IsDisplayPlayer(player)
+					|| (!P_IsDisplayPlayer(player) && (player->kartstuff[k_hyudorotimer] < (1*TICRATE/2) || player->kartstuff[k_hyudorotimer] > hyudorotime-(1*TICRATE/2))))
 				{
 					if (leveltime & 1)
 						player->mo->flags2 |= MF2_DONTDRAW;
@@ -5747,10 +5786,11 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 		{
 			if (player->speed > 0 && cmd->forwardmove == 0 && player->mo->friction == 59392)
 				player->mo->friction += 4608;
-			if (player->speed > 0 && cmd->forwardmove < 0 && player->mo->friction == 59392)
-				player->mo->friction += 1608;
 		}
 
+		if (player->speed > 0 && cmd->forwardmove < 0)	// change friction while braking no matter what, otherwise it's not any more effective than just letting go off accel
+			player->mo->friction -= 2048;
+
 		// Karma ice physics
 		if (G_BattleGametype() && player->kartstuff[k_bumper] <= 0)
 		{
@@ -5808,7 +5848,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
 	}
 
 	// Play the starting countdown sounds
-	if (player == &players[displayplayer]) // Don't play louder in splitscreen
+	if (player == &players[displayplayers[0]]) // Don't play louder in splitscreen
 	{
 		if ((leveltime == starttime-(3*TICRATE)) || (leveltime == starttime-(2*TICRATE)) || (leveltime == starttime-TICRATE))
 			S_StartSound(NULL, sfx_s3ka7);
@@ -6693,17 +6733,17 @@ INT32 K_calcSplitFlags(INT32 snapflags)
 	if (splitscreen == 0)
 		return snapflags;
 
-	if (stplyr != &players[displayplayer])
+	if (stplyr != &players[displayplayers[0]])
 	{
-		if (splitscreen == 1 && stplyr == &players[secondarydisplayplayer])
+		if (splitscreen == 1 && stplyr == &players[displayplayers[1]])
 		{
 			splitflags |= V_SPLITSCREEN;
 		}
 		else if (splitscreen > 1)
 		{
-			if (stplyr == &players[thirddisplayplayer] || stplyr == &players[fourthdisplayplayer])
+			if (stplyr == &players[displayplayers[2]] || (splitscreen == 3 && stplyr == &players[displayplayers[3]]))
 				splitflags |= V_SPLITSCREEN;
-			if (stplyr == &players[secondarydisplayplayer] || stplyr == &players[fourthdisplayplayer])
+			if (stplyr == &players[displayplayers[1]] || (splitscreen == 3 && stplyr == &players[displayplayers[3]]))
 				splitflags |= V_HORZSCREEN;
 		}
 	}
@@ -6858,7 +6898,7 @@ static void K_drawKartItem(void)
 		}
 		else if (stplyr->kartstuff[k_growshrinktimer] > 0)
 		{
-			if (stplyr->kartstuff[k_growcancel])
+			if (stplyr->kartstuff[k_growcancel] > 0)
 			{
 				itembar = stplyr->kartstuff[k_growcancel];
 				maxl = 26;
@@ -6965,25 +7005,25 @@ static void K_drawKartItem(void)
 	}
 
 	// pain and suffering defined below
-	if (splitscreen < 2)	// don't change shit for THIS splitscreen.
+	if (splitscreen < 2) // don't change shit for THIS splitscreen.
 	{
 		fx = ITEM_X;
 		fy = ITEM_Y;
 		fflags = K_calcSplitFlags(V_SNAPTOTOP|V_SNAPTOLEFT);
 	}
-	else				// now we're having a fun game.
+	else // now we're having a fun game.
 	{
-		if (stplyr == &players[displayplayer] || stplyr == &players[thirddisplayplayer])	// If we are P1 or P3...
+		if (stplyr == &players[displayplayers[0]] || stplyr == &players[displayplayers[2]]) // If we are P1 or P3...
 		{
 			fx = ITEM_X;
 			fy = ITEM_Y;
-			fflags = V_SNAPTOLEFT|((stplyr == &players[thirddisplayplayer]) ? V_SPLITSCREEN : V_SNAPTOTOP);	// flip P3 to the bottom.
+			fflags = V_SNAPTOLEFT|((stplyr == &players[displayplayers[2]]) ? V_SPLITSCREEN : V_SNAPTOTOP); // flip P3 to the bottom.
 		}
 		else // else, that means we're P2 or P4.
 		{
 			fx = ITEM2_X;
 			fy = ITEM2_Y;
-			fflags = V_SNAPTORIGHT|((stplyr == &players[fourthdisplayplayer]) ? V_SPLITSCREEN : V_SNAPTOTOP);	// flip P4 to the bottom
+			fflags = V_SNAPTORIGHT|((stplyr == &players[displayplayers[3]]) ? V_SPLITSCREEN : V_SNAPTOTOP); // flip P4 to the bottom
 			flipamount = true;
 		}
 	}
@@ -6996,10 +7036,10 @@ static void K_drawKartItem(void)
 	// Then, the numbers:
 	if (stplyr->kartstuff[k_itemamount] >= numberdisplaymin && !stplyr->kartstuff[k_itemroulette])
 	{
-		V_DrawScaledPatch(fx + (flipamount ? 48 : 0), fy, V_HUDTRANS|fflags|(flipamount ? V_FLIP : 0), kp_itemmulsticker[offset]);	// flip this graphic for p2 and p4 in split and shift it.
+		V_DrawScaledPatch(fx + (flipamount ? 48 : 0), fy, V_HUDTRANS|fflags|(flipamount ? V_FLIP : 0), kp_itemmulsticker[offset]); // flip this graphic for p2 and p4 in split and shift it.
 		V_DrawFixedPatch(fx<<FRACBITS, fy<<FRACBITS, FRACUNIT, V_HUDTRANS|fflags, localpatch, colmap);
 		if (offset)
-			if (flipamount)	// reminder that this is for 3/4p's right end of the screen.
+			if (flipamount) // reminder that this is for 3/4p's right end of the screen.
 				V_DrawString(fx+2, fy+31, V_ALLOWLOWERCASE|V_HUDTRANS|fflags, va("x%d", stplyr->kartstuff[k_itemamount]));
 			else
 				V_DrawString(fx+24, fy+31, V_ALLOWLOWERCASE|V_HUDTRANS|fflags, va("x%d", stplyr->kartstuff[k_itemamount]));
@@ -7115,7 +7155,7 @@ void K_drawKartTimestamp(tic_t drawtime, INT32 TX, INT32 TY, INT16 emblemmap, UI
 	else if ((drawtime/TICRATE) & 1)
 		V_DrawKartString(TX, TY+3, splitflags, va("99'59\"99"));
 
-	if (emblemmap && (modeattacking || (mode == 1)) && !demoplayback) // emblem time!
+	if (emblemmap && (modeattacking || (mode == 1)) && !demo.playback) // emblem time!
 	{
 		INT32 workx = TX + 96, worky = TY+18;
 		SINT8 curemb = 0;
@@ -7226,7 +7266,7 @@ static void K_DrawKartPositionNum(INT32 num)
 	else if (splitscreen == 1)	// for this splitscreen, we'll use case by case because it's a bit different.
 	{
 		fx = POSI_X;
-		if (stplyr == &players[displayplayer])	// for player 1: display this at the top right, above the minimap.
+		if (stplyr == &players[displayplayers[0]])	// for player 1: display this at the top right, above the minimap.
 		{
 			fy = 30;
 			fflags = V_SNAPTOTOP|V_SNAPTORIGHT;
@@ -7241,11 +7281,11 @@ static void K_DrawKartPositionNum(INT32 num)
 	}
 	else
 	{
-		if (stplyr == &players[displayplayer] || stplyr == &players[thirddisplayplayer])	// If we are P1 or P3...
+		if (stplyr == &players[displayplayers[0]] || stplyr == &players[displayplayers[2]])	// If we are P1 or P3...
 		{
 			fx = POSI_X;
 			fy = POSI_Y;
-			fflags = V_SNAPTOLEFT|((stplyr == &players[thirddisplayplayer]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P3 to the bottom.
+			fflags = V_SNAPTOLEFT|((stplyr == &players[displayplayers[2]]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P3 to the bottom.
 			flipdraw = true;
 			if (num && num >= 10)
 				fx += W;	// this seems dumb, but we need to do this in order for positions above 10 going off screen.
@@ -7254,7 +7294,7 @@ static void K_DrawKartPositionNum(INT32 num)
 		{
 			fx = POSI2_X;
 			fy = POSI2_Y;
-			fflags = V_SNAPTORIGHT|((stplyr == &players[fourthdisplayplayer]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P4 to the bottom
+			fflags = V_SNAPTORIGHT|((stplyr == &players[displayplayers[3]]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P4 to the bottom
 		}
 	}
 
@@ -7580,17 +7620,17 @@ static void K_drawKartLaps(void)
 		}
 		else
 		{
-			if (stplyr == &players[displayplayer] || stplyr == &players[thirddisplayplayer])	// If we are P1 or P3...
+			if (stplyr == &players[displayplayers[0]] || stplyr == &players[displayplayers[2]])	// If we are P1 or P3...
 			{
 				fx = LAPS_X;
 				fy = LAPS_Y;
-				fflags = V_SNAPTOLEFT|((stplyr == &players[thirddisplayplayer]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P3 to the bottom.
+				fflags = V_SNAPTOLEFT|((stplyr == &players[displayplayers[2]]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P3 to the bottom.
 			}
 			else // else, that means we're P2 or P4.
 			{
 				fx = LAPS2_X;
 				fy = LAPS2_Y;
-				fflags = V_SNAPTORIGHT|((stplyr == &players[fourthdisplayplayer]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P4 to the bottom
+				fflags = V_SNAPTORIGHT|((stplyr == &players[displayplayers[3]]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P4 to the bottom
 				flipstring = true;	// make the string right aligned and other shit
 			}
 		}
@@ -7662,17 +7702,17 @@ static void K_drawKartBumpersOrKarma(void)
 
 		// we will reuse lap coords here since it's essentially the same shit.
 
-		if (stplyr == &players[displayplayer] || stplyr == &players[thirddisplayplayer])	// If we are P1 or P3...
+		if (stplyr == &players[displayplayers[0]] || stplyr == &players[displayplayers[2]])	// If we are P1 or P3...
 		{
 			fx = LAPS_X;
 			fy = LAPS_Y;
-			fflags = V_SNAPTOLEFT|((stplyr == &players[thirddisplayplayer]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P3 to the bottom.
+			fflags = V_SNAPTOLEFT|((stplyr == &players[displayplayers[2]]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P3 to the bottom.
 		}
 		else // else, that means we're P2 or P4.
 		{
 			fx = LAPS2_X;
 			fy = LAPS2_Y;
-			fflags = V_SNAPTORIGHT|((stplyr == &players[fourthdisplayplayer]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P4 to the bottom
+			fflags = V_SNAPTORIGHT|((stplyr == &players[displayplayers[3]]) ? V_SPLITSCREEN|V_SNAPTOBOTTOM : 0);	// flip P4 to the bottom
 			flipstring = true;
 		}
 
@@ -7749,7 +7789,7 @@ static void K_drawKartWanted(void)
 	UINT8 *colormap = NULL;
 	INT32 basex = 0, basey = 0;
 
-	if (stplyr != &players[displayplayer])
+	if (stplyr != &players[displayplayers[0]])
 		return;
 
 	for (i = 0; i < 4; i++)
@@ -7829,7 +7869,7 @@ static void K_drawKartPlayerCheck(void)
 	if (stplyr->awayviewtics)
 		return;
 
-	if (camspin)
+	if (camspin[0])
 		return;
 
 	for (i = 0; i < MAXPLAYERS; i++)
@@ -7975,7 +8015,7 @@ static void K_drawKartMinimap(void)
 	if (gamestate != GS_LEVEL)
 		return;
 
-	if (stplyr != &players[displayplayer])
+	if (stplyr != &players[displayplayers[0]])
 		return;
 
 	lumpnum = W_CheckNumForName(va("%sR", G_BuildMapName(gamemap)));
@@ -8049,7 +8089,7 @@ static void K_drawKartMinimap(void)
 			if (!players[i].mo || players[i].spectator)
 				continue;
 
-			if (i != displayplayer || splitscreen)
+			if (i != displayplayers[0] || splitscreen)
 			{
 				if (G_BattleGametype() && players[i].kartstuff[k_bumper] <= 0)
 					continue;
@@ -8063,7 +8103,7 @@ static void K_drawKartMinimap(void)
 				}
 			}
 
-			if (i == displayplayer || i == secondarydisplayplayer || i == thirddisplayplayer || i == fourthdisplayplayer)
+			if (P_IsDisplayPlayer(&players[i]))
 			{
 				// Draw display players on top of everything else
 				localplayers[numlocalplayers] = i;
@@ -8133,7 +8173,7 @@ static void K_drawKartFinish(void)
 		xval = (SHORT(kp_racefinish[pnum]->width)<<FRACBITS);
 		x = ((TICRATE - stplyr->kartstuff[k_cardanimation])*(xval > x ? xval : x))/TICRATE;
 
-		if (splitscreen && stplyr == &players[secondarydisplayplayer])
+		if (splitscreen && stplyr == &players[displayplayers[1]])
 			x = -x;
 
 		V_DrawFixedPatch(x + (STCD_X<<FRACBITS) - (xval>>1),
@@ -8149,12 +8189,17 @@ static void K_drawBattleFullscreen(void)
 	INT32 y = -64+(stplyr->kartstuff[k_cardanimation]); // card animation goes from 0 to 164, 164 is the middle of the screen
 	INT32 splitflags = V_SNAPTOTOP; // I don't feel like properly supporting non-green resolutions, so you can have a misuse of SNAPTO instead
 	fixed_t scale = FRACUNIT;
+	boolean drawcomebacktimer = true;	// lazy hack because it's cleaner in the long run.
+#ifdef HAVE_BLUA
+	if (!LUA_HudEnabled(hud_battlecomebacktimer))
+		drawcomebacktimer = false;
+#endif
 
 	if (splitscreen)
 	{
-		if ((splitscreen == 1 && stplyr == &players[secondarydisplayplayer])
-			|| (splitscreen > 1 && (stplyr == &players[thirddisplayplayer]
-			|| (stplyr == &players[fourthdisplayplayer] && splitscreen > 2))))
+		if ((splitscreen == 1 && stplyr == &players[displayplayers[1]])
+			|| (splitscreen > 1 && (stplyr == &players[displayplayers[2]]
+			|| (stplyr == &players[displayplayers[3]] && splitscreen > 2))))
 		{
 			y = 232-(stplyr->kartstuff[k_cardanimation]/2);
 			splitflags = V_SNAPTOBOTTOM;
@@ -8166,8 +8211,8 @@ static void K_drawBattleFullscreen(void)
 		{
 			scale /= 2;
 
-			if (stplyr == &players[secondarydisplayplayer]
-				|| (stplyr == &players[fourthdisplayplayer] && splitscreen > 2))
+			if (stplyr == &players[displayplayers[1]]
+				|| (stplyr == &players[displayplayers[3]] && splitscreen > 2))
 				x = 3*BASEVIDWIDTH/4;
 			else
 				x = BASEVIDWIDTH/4;
@@ -8176,7 +8221,7 @@ static void K_drawBattleFullscreen(void)
 		{
 			if (stplyr->exiting)
 			{
-				if (stplyr == &players[secondarydisplayplayer])
+				if (stplyr == &players[displayplayers[1]])
 					x = BASEVIDWIDTH-96;
 				else
 					x = 96;
@@ -8188,7 +8233,7 @@ static void K_drawBattleFullscreen(void)
 
 	if (stplyr->exiting)
 	{
-		if (stplyr == &players[displayplayer])
+		if (stplyr == &players[displayplayers[0]])
 			V_DrawFadeScreen(0xFF00, 16);
 		if (stplyr->exiting < 6*TICRATE && !stplyr->spectator)
 		{
@@ -8200,7 +8245,7 @@ static void K_drawBattleFullscreen(void)
 		else
 			K_drawKartFinish();
 	}
-	else if (stplyr->kartstuff[k_bumper] <= 0 && stplyr->kartstuff[k_comebacktimer] && comeback && !stplyr->spectator)
+	else if (stplyr->kartstuff[k_bumper] <= 0 && stplyr->kartstuff[k_comebacktimer] && comeback && !stplyr->spectator && drawcomebacktimer)
 	{
 		UINT16 t = stplyr->kartstuff[k_comebacktimer]/(10*TICRATE);
 		INT32 txoff, adjust = (splitscreen > 1) ? 4 : 6; // normal string is 8, kart string is 12, half of that for ease
@@ -8218,9 +8263,9 @@ static void K_drawBattleFullscreen(void)
 		{
 			if (splitscreen > 1)
 				ty = (BASEVIDHEIGHT/4)+33;
-			if ((splitscreen == 1 && stplyr == &players[secondarydisplayplayer])
-				|| (stplyr == &players[thirddisplayplayer] && splitscreen > 1)
-				|| (stplyr == &players[fourthdisplayplayer] && splitscreen > 2))
+			if ((splitscreen == 1 && stplyr == &players[displayplayers[1]])
+				|| (stplyr == &players[displayplayers[2]] && splitscreen > 1)
+				|| (stplyr == &players[displayplayers[3]] && splitscreen > 2))
 				ty += (BASEVIDHEIGHT/2);
 		}
 		else
@@ -8247,7 +8292,7 @@ static void K_drawBattleFullscreen(void)
 		// check to see if there's anyone else at all
 		for (i = 0; i < MAXPLAYERS; i++)
 		{
-			if (i == displayplayer)
+			if (i == displayplayers[0])
 				continue;
 			if (playeringame[i] && !stplyr->spectator)
 				return;
@@ -8273,11 +8318,11 @@ static void K_drawKartFirstPerson(void)
 	if (stplyr->spectator || !stplyr->mo || (stplyr->mo->flags2 & MF2_DONTDRAW))
 		return;
 
-	if (stplyr == &players[secondarydisplayplayer] && splitscreen)
+	if (stplyr == &players[displayplayers[1]] && splitscreen)
 		{ pn = pnum[1]; tn = turn[1]; dr = drift[1]; }
-	else if (stplyr == &players[thirddisplayplayer] && splitscreen > 1)
+	else if (stplyr == &players[displayplayers[2]] && splitscreen > 1)
 		{ pn = pnum[2]; tn = turn[2]; dr = drift[2]; }
-	else if (stplyr == &players[fourthdisplayplayer] && splitscreen > 2)
+	else if (stplyr == &players[displayplayers[3]] && splitscreen > 2)
 		{ pn = pnum[3]; tn = turn[3]; dr = drift[3]; }
 	else
 		{ pn = pnum[0]; tn = turn[0]; dr = drift[0]; }
@@ -8290,7 +8335,7 @@ static void K_drawKartFirstPerson(void)
 	}
 
 	{
-		if (stplyr->speed < FixedMul(stplyr->runspeed, stplyr->mo->scale) && (leveltime & 1) && !splitscreen)
+		if (stplyr->speed < (20*stplyr->mo->scale) && (leveltime & 1) && !splitscreen)
 			y++;
 		// the following isn't EXPLICITLY right, it just gets the result we want, but i'm too lazy to look up the right way to do it
 		if (stplyr->mo->flags2 & MF2_SHADOW)
@@ -8402,11 +8447,11 @@ static void K_drawKartFirstPerson(void)
 
 	V_DrawFixedPatch(x, y, scale, splitflags, kp_fpview[target], colmap);
 
-	if (stplyr == &players[secondarydisplayplayer] && splitscreen)
+	if (stplyr == &players[displayplayers[1]] && splitscreen)
 		{ pnum[1] = pn; turn[1] = tn; drift[1] = dr; }
-	else if (stplyr == &players[thirddisplayplayer] && splitscreen > 1)
+	else if (stplyr == &players[displayplayers[2]] && splitscreen > 1)
 		{ pnum[2] = pn; turn[2] = tn; drift[2] = dr; }
-	else if (stplyr == &players[fourthdisplayplayer] && splitscreen > 2)
+	else if (stplyr == &players[displayplayers[3]] && splitscreen > 2)
 		{ pnum[3] = pn; turn[3] = tn; drift[3] = dr; }
 	else
 		{ pnum[0] = pn; turn[0] = tn; drift[0] = dr; }
@@ -8428,19 +8473,12 @@ static void K_drawInput(void)
 	if (timeinmap < 113)
 	{
 		INT32 count = ((INT32)(timeinmap) - 105);
-		offs = (titledemo ? 128 : 64);
+		offs = 64;
 		while (count-- > 0)
 			offs >>= 1;
 		x += offs;
 	}
 
-	if (titledemo)
-	{
-		V_DrawTinyScaledPatch(x-54, 128, splitflags, W_CachePatchName("TTKBANNR", PU_CACHE));
-		V_DrawTinyScaledPatch(x-54, 128+25, splitflags, W_CachePatchName("TTKART", PU_CACHE));
-		return;
-	}
-
 #define BUTTW 8
 #define BUTTH 11
 
@@ -8634,7 +8672,7 @@ static void K_drawDistributionDebugger(void)
 	boolean dontforcespb = false;
 	boolean spbrush = false;
 
-	if (stplyr != &players[displayplayer]) // only for p1
+	if (stplyr != &players[displayplayers[0]]) // only for p1
 		return;
 
 	// The only code duplication from the Kart, just to avoid the actual item function from calculating pingame twice
@@ -8698,7 +8736,7 @@ static void K_drawDistributionDebugger(void)
 
 static void K_drawCheckpointDebugger(void)
 {
-	if (stplyr != &players[displayplayer]) // only for p1
+	if (stplyr != &players[displayplayers[0]]) // only for p1
 		return;
 
 	if (stplyr->starpostnum >= (numstarposts - (numstarposts/2)))
@@ -8712,20 +8750,21 @@ void K_drawKartHUD(void)
 {
 	boolean isfreeplay = false;
 	boolean battlefullscreen = false;
+	UINT8 i;
 
 	// Define the X and Y for each drawn object
 	// This is handled by console/menu values
 	K_initKartHUD();
 
 	// Draw that fun first person HUD! Drawn ASAP so it looks more "real".
-	if ((stplyr == &players[displayplayer] && !camera.chase)
-		|| ((splitscreen && stplyr == &players[secondarydisplayplayer]) && !camera2.chase)
-		|| ((splitscreen > 1 && stplyr == &players[thirddisplayplayer]) && !camera3.chase)
-		|| ((splitscreen > 2 && stplyr == &players[fourthdisplayplayer]) && !camera4.chase))
-		K_drawKartFirstPerson();
+	for (i = 0; i <= splitscreen; i++)
+	{
+		if (stplyr == &players[displayplayers[i]] && !camera[i].chase)
+			K_drawKartFirstPerson();
+	}
 
 	// Draw full screen stuff that turns off the rest of the HUD
-	if (mapreset && stplyr == &players[displayplayer])
+	if (mapreset && stplyr == &players[displayplayers[0]])
 	{
 		K_drawChallengerScreen();
 		return;
@@ -8738,10 +8777,10 @@ void K_drawKartHUD(void)
 		&& comeback
 		&& stplyr->playerstate == PST_LIVE)));
 
-	if (!battlefullscreen || splitscreen)
+	if (!demo.title && (!battlefullscreen || splitscreen))
 	{
 		// Draw the CHECK indicator before the other items, so it's overlapped by everything else
-		if (cv_kartcheck.value && !splitscreen && !players[displayplayer].exiting)
+		if (cv_kartcheck.value && !splitscreen && !players[displayplayers[0]].exiting)
 			K_drawKartPlayerCheck();
 
 		// Draw WANTED status
@@ -8753,7 +8792,7 @@ void K_drawKartHUD(void)
 				K_drawKartWanted();
 		}
 
-		if (cv_kartminimap.value && !titledemo)
+		if (cv_kartminimap.value)
 		{
 #ifdef HAVE_BLUA
 			if (LUA_HudEnabled(hud_minimap))
@@ -8764,7 +8803,10 @@ void K_drawKartHUD(void)
 
 	if (battlefullscreen)
 	{
-		K_drawBattleFullscreen();
+#ifdef HAVE_BLUA
+		if (LUA_HudEnabled(hud_battlefullscreen))
+#endif
+			K_drawBattleFullscreen();
 		return;
 	}
 
@@ -8775,7 +8817,7 @@ void K_drawKartHUD(void)
 		K_drawKartItem();
 
 	// If not splitscreen, draw...
-	if (!splitscreen && !titledemo)
+	if (!splitscreen && !demo.title)
 	{
 		// Draw the timestamp
 #ifdef HAVE_BLUA
@@ -8795,25 +8837,44 @@ void K_drawKartHUD(void)
 
 	if (!stplyr->spectator) // Bottom of the screen elements, don't need in spectate mode
 	{
-		if (G_RaceGametype()) // Race-only elements
+		if (demo.title) // Draw title logo instead in demo.titles
 		{
-			if (!titledemo)
+			INT32 x = BASEVIDWIDTH - 32, y = 128, offs;
+
+			if (splitscreen == 3)
 			{
-				// Draw the lap counter
+				x = BASEVIDWIDTH/2 + 10;
+				y = BASEVIDHEIGHT/2 - 30;
+			}
+
+			if (timeinmap < 113)
+			{
+				INT32 count = ((INT32)(timeinmap) - 104);
+				offs = 256;
+				while (count-- > 0)
+					offs >>= 1;
+				x += offs;
+			}
+
+			V_DrawTinyScaledPatch(x-54, y, 0, W_CachePatchName("TTKBANNR", PU_CACHE));
+			V_DrawTinyScaledPatch(x-54, y+25, 0, W_CachePatchName("TTKART", PU_CACHE));
+		}
+		else if (G_RaceGametype()) // Race-only elements
+		{
+			// Draw the lap counter
 #ifdef HAVE_BLUA
-				if (LUA_HudEnabled(hud_gametypeinfo))
+			if (LUA_HudEnabled(hud_gametypeinfo))
 #endif
-					K_drawKartLaps();
+				K_drawKartLaps();
 
-				if (!splitscreen)
-				{
-					// Draw the speedometer
-					// TODO: Make a better speedometer.
+			if (!splitscreen)
+			{
+				// Draw the speedometer
+				// TODO: Make a better speedometer.
 #ifdef HAVE_BLUA
-				if (LUA_HudEnabled(hud_speedometer))
+			if (LUA_HudEnabled(hud_speedometer))
 #endif
-					K_drawKartSpeedometer();
-				}
+				K_drawKartSpeedometer();
 			}
 
 			if (isfreeplay)
@@ -8826,7 +8887,7 @@ void K_drawKartHUD(void)
 #endif
 					K_DrawKartPositionNum(stplyr->kartstuff[k_position]);
 			}
-			else //if (!(demoplayback && hu_showscores))
+			else //if (!(demo.playback && hu_showscores))
 			{
 				// Draw the input UI
 #ifdef HAVE_BLUA
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index dde57c2ddee5d96a96f0ba5e0b2776337b3751a7..6700d5af8fe418c731d2186cd5d93802271752c7 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -163,6 +163,7 @@ static int lib_pRandomFixed(lua_State *L)
 {
 	NOHUD
 	lua_pushfixed(L, P_RandomFixed());
+	demo_writerng = 2;
 	return 1;
 }
 
@@ -170,6 +171,7 @@ static int lib_pRandomByte(lua_State *L)
 {
 	NOHUD
 	lua_pushinteger(L, P_RandomByte());
+	demo_writerng = 2;
 	return 1;
 }
 
@@ -181,6 +183,7 @@ static int lib_pRandomKey(lua_State *L)
 	if (a > 65536)
 		LUA_UsageWarning(L, "P_RandomKey: range > 65536 is undefined behavior");
 	lua_pushinteger(L, P_RandomKey(a));
+	demo_writerng = 2;
 	return 1;
 }
 
@@ -198,6 +201,7 @@ static int lib_pRandomRange(lua_State *L)
 	if ((b-a+1) > 65536)
 		LUA_UsageWarning(L, "P_RandomRange: range > 65536 is undefined behavior");
 	lua_pushinteger(L, P_RandomRange(a, b));
+	demo_writerng = 2;
 	return 1;
 }
 
@@ -207,6 +211,7 @@ static int lib_pRandom(lua_State *L)
 	NOHUD
 	LUA_Deprecated(L, "P_Random", "P_RandomByte");
 	lua_pushinteger(L, P_RandomByte());
+	demo_writerng = 2;
 	return 1;
 }
 
@@ -214,6 +219,7 @@ static int lib_pSignedRandom(lua_State *L)
 {
 	NOHUD
 	lua_pushinteger(L, P_SignedRandom());
+	demo_writerng = 2;
 	return 1;
 }
 
@@ -222,6 +228,7 @@ static int lib_pRandomChance(lua_State *L)
 	fixed_t p = luaL_checkfixed(L, 1);
 	NOHUD
 	lua_pushboolean(L, P_RandomChance(p));
+	demo_writerng = 2;
 	return 1;
 }
 
@@ -769,7 +776,8 @@ static int lib_pRestoreMusic(lua_State *L)
 	NOHUD
 	if (!player)
 		return LUA_ErrInvalid(L, "player_t");
-	P_RestoreMusic(player);
+	if (P_IsLocalPlayer(player))
+		P_RestoreMusic(player);
 	return 0;
 }
 
@@ -946,40 +954,6 @@ static int lib_pHomingAttack(lua_State *L)
 	return 1;
 }*/
 
-static int lib_pDoJump(lua_State *L)
-{
-	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
-	boolean soundandstate = (boolean)lua_opttrueboolean(L, 2);
-	NOHUD
-	if (!player)
-		return LUA_ErrInvalid(L, "player_t");
-	P_DoJump(player, soundandstate);
-	return 0;
-}
-
-static int lib_pSpawnThokMobj(lua_State *L)
-{
-	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
-	NOHUD
-	if (!player)
-		return LUA_ErrInvalid(L, "player_t");
-	P_SpawnThokMobj(player);
-	return 0;
-}
-
-static int lib_pSpawnSpinMobj(lua_State *L)
-{
-	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
-	mobjtype_t type = luaL_checkinteger(L, 2);
-	NOHUD
-	if (!player)
-		return LUA_ErrInvalid(L, "player_t");
-	if (type >= NUMMOBJTYPES)
-		return luaL_error(L, "mobj type %d out of range (0 - %d)", type, NUMMOBJTYPES-1);
-	P_SpawnSpinMobj(player, type);
-	return 0;
-}
-
 static int lib_pTelekinesis(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -1836,7 +1810,7 @@ static int lib_sChangeMusic(lua_State *L)
 {
 #ifdef MUSICSLOT_COMPATIBILITY
 	const char *music_name;
-	UINT32 music_num;
+	UINT32 music_num, position, prefadems, fadeinms;
 	char music_compat_name[7];
 
 	boolean looping;
@@ -1864,7 +1838,6 @@ static int lib_sChangeMusic(lua_State *L)
 		music_name = luaL_checkstring(L, 1);
 	}
 
-
 	looping = (boolean)lua_opttrueboolean(L, 2);
 
 #else
@@ -1889,8 +1862,12 @@ static int lib_sChangeMusic(lua_State *L)
 #endif
 	music_flags = (UINT16)luaL_optinteger(L, 4, 0);
 
+	position = (UINT32)luaL_optinteger(L, 5, 0);
+	prefadems = (UINT32)luaL_optinteger(L, 6, 0);
+	fadeinms = (UINT32)luaL_optinteger(L, 7, 0);
+
 	if (!player || P_IsLocalPlayer(player))
-		S_ChangeMusic(music_name, music_flags, looping);
+		S_ChangeMusicEx(music_name, music_flags, looping, position, prefadems, fadeinms);
 	return 0;
 }
 
@@ -1907,10 +1884,8 @@ static int lib_sSpeedMusic(lua_State *L)
 			return LUA_ErrInvalid(L, "player_t");
 	}
 	if (!player || P_IsLocalPlayer(player))
-		lua_pushboolean(L, S_SpeedMusic(speed));
-	else
-		lua_pushboolean(L, false);
-	return 1;
+		S_SpeedMusic(speed);
+	return 0;
 }
 
 static int lib_sStopMusic(lua_State *L)
@@ -1928,6 +1903,110 @@ static int lib_sStopMusic(lua_State *L)
 	return 0;
 }
 
+static int lib_sSetInternalMusicVolume(lua_State *L)
+{
+	UINT32 volume = (UINT32)luaL_checkinteger(L, 1);
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 2) && lua_isuserdata(L, 2))
+	{
+		player = *((player_t **)luaL_checkudata(L, 2, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+	}
+	if (!player || P_IsLocalPlayer(player))
+	{
+		S_SetInternalMusicVolume(volume);
+		lua_pushboolean(L, true);
+	}
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_sStopFadingMusic(lua_State *L)
+{
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 1) && lua_isuserdata(L, 1))
+	{
+		player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+	}
+	if (!player || P_IsLocalPlayer(player))
+	{
+		S_StopFadingMusic();
+		lua_pushboolean(L, true);
+	}
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_sFadeMusic(lua_State *L)
+{
+	UINT32 target_volume = (UINT32)luaL_checkinteger(L, 1);
+	UINT32 ms;
+	INT32 source_volume;
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 3) && lua_isuserdata(L, 3))
+	{
+		player = *((player_t **)luaL_checkudata(L, 3, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+		ms = (UINT32)luaL_checkinteger(L, 2);
+		source_volume = -1;
+	}
+	else if (!lua_isnone(L, 4) && lua_isuserdata(L, 4))
+	{
+		player = *((player_t **)luaL_checkudata(L, 4, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+		source_volume = (INT32)luaL_checkinteger(L, 2);
+		ms = (UINT32)luaL_checkinteger(L, 3);
+	}
+	else if (luaL_optinteger(L, 3, INT32_MAX) == INT32_MAX)
+	{
+		ms = (UINT32)luaL_checkinteger(L, 2);
+		source_volume = -1;
+	}
+	else
+	{
+		source_volume = (INT32)luaL_checkinteger(L, 2);
+		ms = (UINT32)luaL_checkinteger(L, 3);
+	}
+
+	NOHUD
+
+	if (!player || P_IsLocalPlayer(player))
+		lua_pushboolean(L, S_FadeMusicFromVolume(target_volume, source_volume, ms));
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_sFadeOutStopMusic(lua_State *L)
+{
+	UINT32 ms = (UINT32)luaL_checkinteger(L, 1);
+	player_t *player = NULL;
+	NOHUD
+	if (!lua_isnone(L, 2) && lua_isuserdata(L, 2))
+	{
+		player = *((player_t **)luaL_checkudata(L, 2, META_PLAYER));
+		if (!player)
+			return LUA_ErrInvalid(L, "player_t");
+	}
+	if (!player || P_IsLocalPlayer(player))
+	{
+		lua_pushboolean(L, S_FadeOutStopMusic(ms));
+	}
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
 static int lib_sOriginPlaying(lua_State *L)
 {
 	void *origin = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -2645,9 +2724,6 @@ static luaL_Reg lib[] = {
 	{"P_NukeEnemies",lib_pNukeEnemies},
 	{"P_HomingAttack",lib_pHomingAttack},
 	//{"P_SuperReady",lib_pSuperReady},
-	{"P_DoJump",lib_pDoJump},
-	{"P_SpawnThokMobj",lib_pSpawnThokMobj},
-	{"P_SpawnSpinMobj",lib_pSpawnSpinMobj},
 	{"P_Telekinesis",lib_pTelekinesis},
 
 	// p_map
@@ -2727,6 +2803,10 @@ static luaL_Reg lib[] = {
 	{"S_ChangeMusic",lib_sChangeMusic},
 	{"S_SpeedMusic",lib_sSpeedMusic},
 	{"S_StopMusic",lib_sStopMusic},
+	{"S_SetInternalMusicVolume", lib_sSetInternalMusicVolume},
+	{"S_StopFadingMusic",lib_sStopFadingMusic},
+	{"S_FadeMusic",lib_sFadeMusic},
+	{"S_FadeOutStopMusic",lib_sFadeOutStopMusic},
 	{"S_OriginPlaying",lib_sOriginPlaying},
 	{"S_IdPlaying",lib_sIdPlaying},
 	{"S_SoundPlaying",lib_sSoundPlaying},
@@ -2752,7 +2832,7 @@ static luaL_Reg lib[] = {
 	// k_kart
 	{"K_PlayAttackTaunt", lib_kAttackSound},
 	{"K_PlayBoostTaunt", lib_kBoostSound},
-	{"K_PlayPowerGloatSund", lib_kGloatSound},
+	{"K_PlayPowerGloatSound", lib_kGloatSound},
 	{"K_PlayOvertakeSound", lib_kOvertakeSound},
 	{"K_PlayLossSound", lib_kLossSound},
 	{"K_PlayHitEmSound", lib_kHitEmSound},
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index 0ea0c80979f76eaaefb9789d0f864810a5e34df4..299870e0091e1cb01e8a52fe45f9f7afec802150 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -118,14 +118,14 @@ void COM_Lua_f(void)
 		flags = (UINT8)lua_tointeger(gL, -1);
 	lua_pop(gL, 1); // pop flags
 
-	if (flags & 2) // flag 2: splitscreen player command.
+	if (flags & 2) // flag 2: splitscreen player command. TODO: support 4P
 	{
 		if (!splitscreen)
 		{
 			lua_pop(gL, 1); // pop command info table
 			return; // can't execute splitscreen command without player 2!
 		}
-		playernum = secondarydisplayplayer;
+		playernum = displayplayers[1];
 	}
 
 	if (netgame)
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 126e7e405721075d7887ed0936ebc813d0c529ff..e61acdf138673e7bb09a2a969594c2f007aae3ed 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -51,6 +51,7 @@ enum hook {
 	hook_PlayerExplode,	//SRB2KART
 	hook_PlayerSquish,	//SRB2KART
 	hook_PlayerCmd,		//SRB2KART
+	hook_IntermissionThinker, //SRB2KART
 
 	hook_MAX // last hook
 };
@@ -99,4 +100,6 @@ boolean LUAh_PlayerSquish(player_t *player, mobj_t *inflictor, mobj_t *source);
 
 boolean LUAh_PlayerCmd(player_t *player, ticcmd_t *cmd);	// Allows to write to player cmd before the game does anything with them.
 
+void LUAh_IntermissionThinker(void); // Hook for Y_Ticker
+
 #endif
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 5a95877e36ac7d51972b31897fee7d38f84651e8..c15d13a0cbfc3ed1d84bdfe7e31597f9d34526ff 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -62,6 +62,7 @@ const char *const hookNames[hook_MAX+1] = {
 	"PlayerExplode",
 	"PlayerSquish",
 	"PlayerCmd",
+	"IntermissionThinker",
 	NULL
 };
 
@@ -420,6 +421,28 @@ void LUAh_ThinkFrame(void)
 		}
 }
 
+// Hook for Y_Ticker
+void LUAh_IntermissionThinker(void)
+{
+	hook_p hookp;
+	if (!gL || !(hooksAvailable[hook_IntermissionThinker/8] & (1<<(hook_IntermissionThinker%8))))
+		return;
+
+	for (hookp = roothook; hookp; hookp = hookp->next)
+		if (hookp->type == hook_IntermissionThinker)
+		{
+			lua_pushfstring(gL, FMT_HOOKID, hookp->id);
+			lua_gettable(gL, LUA_REGISTRYINDEX);
+			if (lua_pcall(gL, 0, 0, 0)) {
+				if (!hookp->error || cv_debug & DBG_LUA)
+					CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
+				lua_pop(gL, 1);
+				hookp->error = true;
+			}
+		}
+}
+
+
 // Hook for mobj collisions
 UINT8 LUAh_MobjCollideHook(mobj_t *thing1, mobj_t *thing2, enum hook which)
 {
diff --git a/src/lua_hud.h b/src/lua_hud.h
index 4fbbbace4b8a4a9e50c32024c25ce0e66fca3d95..88d7fd6bc96f2ab895652503fc78db9c1c25219d 100644
--- a/src/lua_hud.h
+++ b/src/lua_hud.h
@@ -21,6 +21,8 @@ enum hud {
 	hud_position,
 	hud_minirankings,	// Rankings to the left
 	hud_battlebumpers,	// mini rankings battle bumpers.
+	hud_battlefullscreen,	// battle huge text (WAIT, WIN, LOSE ...) + karma comeback time
+	hud_battlecomebacktimer,	// comeback timer in battlefullscreen. separated for ease of use.
 	hud_wanted,
 	hud_speedometer,
 	hud_freeplay,
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index fb6814b2520451d880a0539d9d06c7edfc70f741..22c89a234fe9ab1318ef3f28e317f869d478a15a 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -48,6 +48,8 @@ static const char *const hud_disable_options[] = {
 	"position",
 	"minirankings",	// Gametype rankings to the left
 	"battlerankingsbumpers",	// bumper drawer for battle. Useful if you want to make a custom battle gamemode without bumpers being involved.
+	"battlefullscreen",			// battlefullscreen func (WAIT, ATTACK OR PROTECT ...)
+	"battlecomebacktimer",		// come back timer in battlefullscreen
 	"wanted",
 	"speedometer",
 	"freeplay",
@@ -381,6 +383,179 @@ static int libd_drawScaled(lua_State *L)
 	return 0;
 }
 
+// KART: draw patch on minimap from x, y coordinates on the map
+static int libd_drawOnMinimap(lua_State *L)
+{
+	fixed_t x, y, scale;	// coordinates of the object
+	patch_t *patch;	// patch we want to draw
+	const UINT8 *colormap = NULL;	// do we want to colormap this patch?
+	boolean centered;	// the patch is centered and doesn't need readjusting on x/y coordinates.
+
+	// variables used to replicate k_kart's mmap drawer:
+	INT32 lumpnum;
+	patch_t *AutomapPic;
+	INT32 mx, my;
+	INT32 splitflags, minimaptrans;
+
+	// base position of the minimap which also takes splits into account:
+	INT32 MM_X, MM_Y;
+
+	// variables used for actually drawing the icon:
+	fixed_t amnumxpos, amnumypos;
+	INT32 amxpos, amypos;
+
+	node_t *bsp = &nodes[numnodes-1];
+	fixed_t maxx, minx, maxy, miny;
+
+	fixed_t mapwidth, mapheight;
+	fixed_t xoffset, yoffset;
+	fixed_t xscale, yscale, zoom;
+	fixed_t patchw, patchh;
+
+	HUDONLY	// only run this function in hud hooks
+	x = luaL_checkinteger(L, 1);
+	y = luaL_checkinteger(L, 2);
+	scale = luaL_checkinteger(L, 3);
+	patch = *((patch_t **)luaL_checkudata(L, 4, META_PATCH));
+	if (!lua_isnoneornil(L, 5))
+		colormap = *((UINT8 **)luaL_checkudata(L, 5, META_COLORMAP));
+	centered = lua_optboolean(L, 6);
+
+	// replicate exactly what source does for its minimap drawer; AKA hardcoded garbo.
+
+	// first, check what position the mmap is supposed to be in (pasted from k_kart.c):
+	MM_X = BASEVIDWIDTH - 50;		// 270
+	MM_Y = (BASEVIDHEIGHT/2)-16; //  84
+	if (splitscreen)
+	{
+		MM_Y = (BASEVIDHEIGHT/2);
+		if (splitscreen > 1)	// 3P : bottom right
+		{
+			MM_X = (3*BASEVIDWIDTH/4);
+			MM_Y = (3*BASEVIDHEIGHT/4);
+
+			if (splitscreen > 2) // 4P: centered
+			{
+				MM_X = (BASEVIDWIDTH/2);
+				MM_Y = (BASEVIDHEIGHT/2);
+			}
+		}
+	}
+
+	// splitscreen flags
+	splitflags = (splitscreen == 3 ? 0 : V_SNAPTORIGHT);	// flags should only be 0 when it's centered (4p split)
+
+	// translucency:
+	if (timeinmap > 105)
+	{
+		minimaptrans = cv_kartminimap.value;
+		if (timeinmap <= 113)
+			minimaptrans = ((((INT32)timeinmap) - 105)*minimaptrans)/(113-105);
+		if (!minimaptrans)
+			return 0;
+	}
+	else
+		return 0;
+
+
+	minimaptrans = ((10-minimaptrans)<<FF_TRANSSHIFT);
+	splitflags |= minimaptrans;
+
+	if (!(splitscreen == 2))
+	{
+		splitflags &= ~minimaptrans;
+		splitflags |= V_HUDTRANSHALF;
+	}
+
+	splitflags &= ~V_HUDTRANSHALF;
+	splitflags |= V_HUDTRANS;
+
+	// Draw the HUD only when playing in a level.
+	// hu_stuff needs this, unlike st_stuff.
+	if (gamestate != GS_LEVEL)
+		return 0;
+
+	if (stplyr != &players[displayplayers[0]])
+		return 0;
+
+	lumpnum = W_CheckNumForName(va("%sR", G_BuildMapName(gamemap)));
+
+	if (lumpnum != -1)
+		AutomapPic = W_CachePatchName(va("%sR", G_BuildMapName(gamemap)), PU_HUDGFX);
+	else
+		return 0; // no pic, just get outta here
+
+	mx = MM_X - (AutomapPic->width/2);
+	my = MM_Y - (AutomapPic->height/2);
+
+	// let offsets transfer to the heads, too!
+	if (encoremode)
+		mx += SHORT(AutomapPic->leftoffset);
+	else
+		mx -= SHORT(AutomapPic->leftoffset);
+	my -= SHORT(AutomapPic->topoffset);
+
+	// now that we have replicated this behavior, we can draw an icon from our supplied x, y coordinates by replicating k_kart.c's totally understandable uncommented code!!!
+
+	// get map boundaries using nodes
+	maxx = maxy = INT32_MAX;
+	minx = miny = INT32_MIN;
+	minx = bsp->bbox[0][BOXLEFT];
+	maxx = bsp->bbox[0][BOXRIGHT];
+	miny = bsp->bbox[0][BOXBOTTOM];
+	maxy = bsp->bbox[0][BOXTOP];
+
+	if (bsp->bbox[1][BOXLEFT] < minx)
+		minx = bsp->bbox[1][BOXLEFT];
+	if (bsp->bbox[1][BOXRIGHT] > maxx)
+		maxx = bsp->bbox[1][BOXRIGHT];
+	if (bsp->bbox[1][BOXBOTTOM] < miny)
+		miny = bsp->bbox[1][BOXBOTTOM];
+	if (bsp->bbox[1][BOXTOP] > maxy)
+		maxy = bsp->bbox[1][BOXTOP];
+
+	// You might be wondering why these are being bitshift here
+	// it's because mapwidth and height would otherwise overflow for maps larger than half the size possible...
+	// map boundaries and sizes will ALWAYS be whole numbers thankfully
+	// later calculations take into consideration that these are actually not in terms of FRACUNIT though
+	minx >>= FRACBITS;
+	maxx >>= FRACBITS;
+	miny >>= FRACBITS;
+	maxy >>= FRACBITS;
+
+	// these are our final map boundaries:
+	mapwidth = maxx - minx;
+	mapheight = maxy - miny;
+
+	// These should always be small enough to be bitshift back right now
+	xoffset = (minx + mapwidth/2)<<FRACBITS;
+	yoffset = (miny + mapheight/2)<<FRACBITS;
+
+	xscale = FixedDiv(AutomapPic->width, mapwidth);
+	yscale = FixedDiv(AutomapPic->height, mapheight);
+	zoom = FixedMul(min(xscale, yscale), FRACUNIT-FRACUNIT/20);
+
+	amnumxpos = (FixedMul(x, zoom) - FixedMul(xoffset, zoom));
+	amnumypos = -(FixedMul(y, zoom) - FixedMul(yoffset, zoom));
+
+	if (encoremode)
+		amnumxpos = -amnumxpos;
+
+	// scale patch coords
+	patchw = patch->width*scale /2;
+	patchh = patch->height*scale /2;
+
+	if (centered)
+		patchw = patchh = 0;	// patch is supposedly already centered, don't butt in.
+
+	amxpos = amnumxpos + ((mx + AutomapPic->width/2)<<FRACBITS) - patchw;
+	amypos = amnumypos + ((my + AutomapPic->height/2)<<FRACBITS) - patchh;
+
+	// and NOW we can FINALLY DRAW OUR GOD DAMN PATCH :V
+	V_DrawFixedPatch(amxpos, amypos, scale, splitflags, patch, colormap);
+	return 0;
+}
+
 static int libd_drawNum(lua_State *L)
 {
 	INT32 x, y, flags, num;
@@ -644,6 +819,7 @@ static luaL_Reg lib_draw[] = {
 	{"dupy", libd_dupy},
 	{"renderer", libd_renderer},
 	{"localTransFlag", libd_getlocaltransflag},
+	{"drawOnMinimap", libd_drawOnMinimap},
 	{NULL, NULL}
 };
 
@@ -797,24 +973,24 @@ void LUAh_GameHUD(player_t *stplayr)
 	lua_remove(gL, -3); // pop HUD
 	LUA_PushUserdata(gL, stplayr, META_PLAYER);
 
-	if (splitscreen > 2 && stplayr == &players[fourthdisplayplayer])
+	if (splitscreen > 2 && stplayr == &players[displayplayers[3]])
 	{
-		LUA_PushUserdata(gL, &camera4, META_CAMERA);
+		LUA_PushUserdata(gL, &camera[3], META_CAMERA);
 		camnum = 4;
 	}
-	else if (splitscreen > 1 && stplayr == &players[thirddisplayplayer])
+	else if (splitscreen > 1 && stplayr == &players[displayplayers[2]])
 	{
-		LUA_PushUserdata(gL, &camera3, META_CAMERA);
+		LUA_PushUserdata(gL, &camera[2], META_CAMERA);
 		camnum = 3;
 	}
-	else if (splitscreen && stplayr == &players[secondarydisplayplayer])
+	else if (splitscreen && stplayr == &players[displayplayers[1]])
 	{
-		LUA_PushUserdata(gL, &camera2, META_CAMERA);
+		LUA_PushUserdata(gL, &camera[1], META_CAMERA);
 		camnum = 2;
 	}
 	else
 	{
-		LUA_PushUserdata(gL, &camera, META_CAMERA);
+		LUA_PushUserdata(gL, &camera[0], META_CAMERA);
 		camnum = 1;
 	}
 
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 0bb9a99d69e96a71c7beab3f3356aa34906f5215..19292b3d6a041cd1adfd8b6407b82cde6a3a1e9c 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -1477,6 +1477,12 @@ static int mapheaderinfo_get(lua_State *L)
 		lua_pushstring(L, header->musname);
 	else if (fastcmp(field,"mustrack"))
 		lua_pushinteger(L, header->mustrack);
+	else if (fastcmp(field,"muspos"))
+		lua_pushinteger(L, header->muspos);
+	else if (fastcmp(field,"musinterfadeout"))
+		lua_pushinteger(L, header->musinterfadeout);
+	else if (fastcmp(field,"musintername"))
+		lua_pushstring(L, header->musintername);
 	else if (fastcmp(field,"forcecharacter"))
 		lua_pushstring(L, header->forcecharacter);
 	else if (fastcmp(field,"weather"))
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index b56538d0f76e1009298b254e9827d3e03945b0fc..dfb344e34e119ade015873eecee69727f8163c26 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -421,13 +421,13 @@ static int mobj_set(lua_State *L)
 	case mobj_angle:
 		mo->angle = luaL_checkangle(L, 3);
 		if (mo->player == &players[consoleplayer])
-			localangle = mo->angle;
-		else if (mo->player == &players[secondarydisplayplayer])
-			localangle2 = mo->angle;
-		else if (mo->player == &players[thirddisplayplayer])
-			localangle3 = mo->angle;
-		else if (mo->player == &players[fourthdisplayplayer])
-			localangle4 = mo->angle;
+			localangle[0] = mo->angle;
+		else if (mo->player == &players[displayplayers[1]])
+			localangle[1] = mo->angle;
+		else if (mo->player == &players[displayplayers[2]])
+			localangle[2] = mo->angle;
+		else if (mo->player == &players[displayplayers[3]])
+			localangle[3] = mo->angle;
 		break;
 	case mobj_sprite:
 		mo->sprite = luaL_checkinteger(L, 3);
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 73d5ecbc0a689e792d35349a6d730a3b91664a42..3cca1f91f8e02bb5dcb537ca6e03438ebca4ce09 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -155,36 +155,8 @@ static int player_get(lua_State *L)
 	else if (fastcmp(field,"kartweight"))
 		lua_pushinteger(L, plr->kartweight);
 	//
-	else if (fastcmp(field,"normalspeed"))
-		lua_pushfixed(L, plr->normalspeed);
-	else if (fastcmp(field,"runspeed"))
-		lua_pushfixed(L, plr->runspeed);
-	else if (fastcmp(field,"thrustfactor"))
-		lua_pushinteger(L, plr->thrustfactor);
-	else if (fastcmp(field,"accelstart"))
-		lua_pushinteger(L, plr->accelstart);
-	else if (fastcmp(field,"acceleration"))
-		lua_pushinteger(L, plr->acceleration);
-	else if (fastcmp(field,"charability"))
-		lua_pushinteger(L, plr->charability);
-	else if (fastcmp(field,"charability2"))
-		lua_pushinteger(L, plr->charability2);
 	else if (fastcmp(field,"charflags"))
 		lua_pushinteger(L, plr->charflags);
-	else if (fastcmp(field,"thokitem"))
-		lua_pushinteger(L, plr->thokitem);
-	else if (fastcmp(field,"spinitem"))
-		lua_pushinteger(L, plr->spinitem);
-	else if (fastcmp(field,"revitem"))
-		lua_pushinteger(L, plr->revitem);
-	else if (fastcmp(field,"actionspd"))
-		lua_pushfixed(L, plr->actionspd);
-	else if (fastcmp(field,"mindash"))
-		lua_pushfixed(L, plr->mindash);
-	else if (fastcmp(field,"maxdash"))
-		lua_pushfixed(L, plr->maxdash);
-	else if (fastcmp(field,"jumpfactor"))
-		lua_pushfixed(L, plr->jumpfactor);
 	else if (fastcmp(field,"lives"))
 		lua_pushinteger(L, plr->lives);
 	else if (fastcmp(field,"continues"))
@@ -382,13 +354,13 @@ static int player_set(lua_State *L)
 	else if (fastcmp(field,"aiming")) {
 		plr->aiming = luaL_checkangle(L, 3);
 		if (plr == &players[consoleplayer])
-			localaiming = plr->aiming;
-		else if (plr == &players[secondarydisplayplayer])
-			localaiming2 = plr->aiming;
-		else if (plr == &players[thirddisplayplayer])
-			localaiming3 = plr->aiming;
-		else if (plr == &players[fourthdisplayplayer])
-			localaiming4 = plr->aiming;
+			localaiming[0] = plr->aiming;
+		else if (plr == &players[displayplayers[1]])
+			localaiming[1] = plr->aiming;
+		else if (plr == &players[displayplayers[2]])
+			localaiming[2] = plr->aiming;
+		else if (plr == &players[displayplayers[3]])
+			localaiming[3] = plr->aiming;
 	}
 	else if (fastcmp(field,"health"))
 		plr->health = (INT32)luaL_checkinteger(L, 3);
@@ -431,36 +403,8 @@ static int player_set(lua_State *L)
 	else if (fastcmp(field,"kartweight"))
 		plr->kartweight = (UINT8)luaL_checkinteger(L, 3);
 	//
-	else if (fastcmp(field,"normalspeed"))
-		plr->normalspeed = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"runspeed"))
-		plr->runspeed = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"thrustfactor"))
-		plr->thrustfactor = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"accelstart"))
-		plr->accelstart = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"acceleration"))
-		plr->acceleration = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"charability"))
-		plr->charability = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"charability2"))
-		plr->charability2 = (UINT8)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"charflags"))
 		plr->charflags = (UINT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"thokitem"))
-		plr->thokitem = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"spinitem"))
-		plr->spinitem = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"revitem"))
-		plr->revitem = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"actionspd"))
-		plr->actionspd = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"mindash"))
-		plr->mindash = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"maxdash"))
-		plr->maxdash = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"jumpfactor"))
-		plr->jumpfactor = (INT32)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"lives"))
 		plr->lives = (SINT8)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"continues"))
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 2fc2e85c91f211c14723a7617f7a201a12cccf91..e7e877adaaf3d5ccb90bb704efa064c4a945ef23 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -273,7 +273,7 @@ boolean cht_Responder(event_t *ev)
 #define REQUIRE_OBJECTPLACE if (!objectplacing)\
 { CONS_Printf(M_GetText("OBJECTPLACE must be enabled.\n")); return; }
 
-#define REQUIRE_INLEVEL if (gamestate != GS_LEVEL || demoplayback)\
+#define REQUIRE_INLEVEL if (gamestate != GS_LEVEL || demo.playback)\
 { CONS_Printf(M_GetText("You must be in a level to use this.\n")); return; }
 
 #define REQUIRE_SINGLEPLAYER if (netgame || multiplayer)\
diff --git a/src/m_fixed.c b/src/m_fixed.c
index d45bb70bf572dabacef5f9874dfe84d3b9bdef32..5e78967396a4b9802dc29e301a14112f03b3168a 100644
--- a/src/m_fixed.c
+++ b/src/m_fixed.c
@@ -56,7 +56,7 @@ fixed_t FixedDiv2(fixed_t a, fixed_t b)
 	if (b == 0)
 		I_Error("FixedDiv: divide by zero");
 
-	ret = (((INT64)a * FRACUNIT) ) / b;
+	ret = (((INT64)a * FRACUNIT)) / b;
 
 	if ((ret > INT32_MAX) || (ret < INT32_MIN))
 		I_Error("FixedDiv: divide by zero");
@@ -117,7 +117,7 @@ fixed_t FixedHypot(fixed_t x, fixed_t y)
 		yx = FixedDiv(y, x); // (x/y)
 	}
 	yx2 = FixedMul(yx, yx); // (x/y)^2
-	yx1 = FixedSqrt(1*FRACUNIT + yx2); // (1 + (x/y)^2)^1/2
+	yx1 = FixedSqrt(1 * FRACUNIT + yx2); // (1 + (x/y)^2)^1/2
 	return FixedMul(ax, yx1); // |x|*((1 + (x/y)^2)^1/2)
 }
 
@@ -191,8 +191,8 @@ vector2_t *FV2_Divide(vector2_t *a_i, fixed_t a_c)
 // Vector Complex Math
 vector2_t *FV2_Midpoint(const vector2_t *a_1, const vector2_t *a_2, vector2_t *a_o)
 {
-	a_o->x = FixedDiv(a_2->x - a_1->x, 2*FRACUNIT);
-	a_o->y = FixedDiv(a_2->y - a_1->y, 2*FRACUNIT);
+	a_o->x = FixedDiv(a_2->x - a_1->x, 2 * FRACUNIT);
+	a_o->y = FixedDiv(a_2->y - a_1->y, 2 * FRACUNIT);
 	a_o->x = a_1->x + a_o->x;
 	a_o->y = a_1->y + a_o->y;
 	return a_o;
@@ -200,16 +200,16 @@ vector2_t *FV2_Midpoint(const vector2_t *a_1, const vector2_t *a_2, vector2_t *a
 
 fixed_t FV2_Distance(const vector2_t *p1, const vector2_t *p2)
 {
-	fixed_t xs = FixedMul(p2->x-p1->x,p2->x-p1->x);
-	fixed_t ys = FixedMul(p2->y-p1->y,p2->y-p1->y);
-	return FixedSqrt(xs+ys);
+	fixed_t xs = FixedMul(p2->x - p1->x, p2->x - p1->x);
+	fixed_t ys = FixedMul(p2->y - p1->y, p2->y - p1->y);
+	return FixedSqrt(xs + ys);
 }
 
 fixed_t FV2_Magnitude(const vector2_t *a_normal)
 {
-	fixed_t xs = FixedMul(a_normal->x,a_normal->x);
-	fixed_t ys = FixedMul(a_normal->y,a_normal->y);
-	return FixedSqrt(xs+ys);
+	fixed_t xs = FixedMul(a_normal->x, a_normal->x);
+	fixed_t ys = FixedMul(a_normal->y, a_normal->y);
+	return FixedSqrt(xs + ys);
 }
 
 // Also returns the magnitude
@@ -240,7 +240,7 @@ vector2_t *FV2_Negate(vector2_t *a_1)
 
 boolean FV2_Equal(const vector2_t *a_1, const vector2_t *a_2)
 {
-	fixed_t Epsilon = FRACUNIT/FRACUNIT;
+	fixed_t Epsilon = FRACUNIT / FRACUNIT;
 
 	if ((abs(a_2->x - a_1->x) > Epsilon) ||
 		(abs(a_2->y - a_1->y) > Epsilon))
@@ -261,7 +261,7 @@ fixed_t FV2_Dot(const vector2_t *a_1, const vector2_t *a_2)
 //
 // Given two points, create a vector between them.
 //
-vector2_t *FV2_Point2Vec (const vector2_t *point1, const vector2_t *point2, vector2_t *a_o)
+vector2_t *FV2_Point2Vec(const vector2_t *point1, const vector2_t *point2, vector2_t *a_o)
 {
 	a_o->x = point1->x - point2->x;
 	a_o->y = point1->y - point2->y;
@@ -344,9 +344,9 @@ vector3_t *FV3_Divide(vector3_t *a_i, fixed_t a_c)
 // Vector Complex Math
 vector3_t *FV3_Midpoint(const vector3_t *a_1, const vector3_t *a_2, vector3_t *a_o)
 {
-	a_o->x = FixedDiv(a_2->x - a_1->x, 2*FRACUNIT);
-	a_o->y = FixedDiv(a_2->y - a_1->y, 2*FRACUNIT);
-	a_o->z = FixedDiv(a_2->z - a_1->z, 2*FRACUNIT);
+	a_o->x = FixedDiv(a_2->x - a_1->x, 2 * FRACUNIT);
+	a_o->y = FixedDiv(a_2->y - a_1->y, 2 * FRACUNIT);
+	a_o->z = FixedDiv(a_2->z - a_1->z, 2 * FRACUNIT);
 	a_o->x = a_1->x + a_o->x;
 	a_o->y = a_1->y + a_o->y;
 	a_o->z = a_1->z + a_o->z;
@@ -355,18 +355,18 @@ vector3_t *FV3_Midpoint(const vector3_t *a_1, const vector3_t *a_2, vector3_t *a
 
 fixed_t FV3_Distance(const vector3_t *p1, const vector3_t *p2)
 {
-	fixed_t xs = FixedMul(p2->x-p1->x,p2->x-p1->x);
-	fixed_t ys = FixedMul(p2->y-p1->y,p2->y-p1->y);
-	fixed_t zs = FixedMul(p2->z-p1->z,p2->z-p1->z);
-	return FixedSqrt(xs+ys+zs);
+	fixed_t xs = FixedMul(p2->x - p1->x, p2->x - p1->x);
+	fixed_t ys = FixedMul(p2->y - p1->y, p2->y - p1->y);
+	fixed_t zs = FixedMul(p2->z - p1->z, p2->z - p1->z);
+	return FixedSqrt(xs + ys + zs);
 }
 
 fixed_t FV3_Magnitude(const vector3_t *a_normal)
 {
-	fixed_t xs = FixedMul(a_normal->x,a_normal->x);
-	fixed_t ys = FixedMul(a_normal->y,a_normal->y);
-	fixed_t zs = FixedMul(a_normal->z,a_normal->z);
-	return FixedSqrt(xs+ys+zs);
+	fixed_t xs = FixedMul(a_normal->x, a_normal->x);
+	fixed_t ys = FixedMul(a_normal->y, a_normal->y);
+	fixed_t zs = FixedMul(a_normal->z, a_normal->z);
+	return FixedSqrt(xs + ys + zs);
 }
 
 // Also returns the magnitude
@@ -399,7 +399,7 @@ vector3_t *FV3_Negate(vector3_t *a_1)
 
 boolean FV3_Equal(const vector3_t *a_1, const vector3_t *a_2)
 {
-	fixed_t Epsilon = FRACUNIT/FRACUNIT;
+	fixed_t Epsilon = FRACUNIT / FRACUNIT;
 
 	if ((abs(a_2->x - a_1->x) > Epsilon) ||
 		(abs(a_2->y - a_1->y) > Epsilon) ||
@@ -458,6 +458,20 @@ vector3_t *FV3_ClosestPointOnLine(const vector3_t *Line, const vector3_t *p, vec
 	return FV3_AddEx(&Line[0], &V, out);
 }
 
+//
+// ClosestPointOnVector
+//
+// Similar to ClosestPointOnLine, but uses a vector instead of two points.
+//
+void FV3_ClosestPointOnVector(const vector3_t *dir, const vector3_t *p, vector3_t *out)
+{
+	fixed_t t = FV3_Dot(dir, p);
+
+	// Return the point on the line closest
+	FV3_MulEx(dir, t, out);
+	return;
+}
+
 //
 // ClosestPointOnTriangle
 //
@@ -465,7 +479,7 @@ vector3_t *FV3_ClosestPointOnLine(const vector3_t *Line, const vector3_t *p, vec
 // the closest point on the edge of
 // the triangle is returned.
 //
-void FV3_ClosestPointOnTriangle (const vector3_t *tri, const vector3_t *point, vector3_t *result)
+void FV3_ClosestPointOnTriangle(const vector3_t *tri, const vector3_t *point, vector3_t *result)
 {
 	UINT8 i;
 	fixed_t dist, closestdist;
@@ -506,7 +520,7 @@ void FV3_ClosestPointOnTriangle (const vector3_t *tri, const vector3_t *point, v
 //
 // Given two points, create a vector between them.
 //
-vector3_t *FV3_Point2Vec (const vector3_t *point1, const vector3_t *point2, vector3_t *a_o)
+vector3_t *FV3_Point2Vec(const vector3_t *point1, const vector3_t *point2, vector3_t *a_o)
 {
 	a_o->x = point1->x - point2->x;
 	a_o->y = point1->y - point2->y;
@@ -519,7 +533,7 @@ vector3_t *FV3_Point2Vec (const vector3_t *point1, const vector3_t *point2, vect
 //
 // Calculates the normal of a polygon.
 //
-void FV3_Normal (const vector3_t *a_triangle, vector3_t *a_normal)
+fixed_t FV3_Normal(const vector3_t *a_triangle, vector3_t *a_normal)
 {
 	vector3_t a_1;
 	vector3_t a_2;
@@ -529,7 +543,28 @@ void FV3_Normal (const vector3_t *a_triangle, vector3_t *a_normal)
 
 	FV3_Cross(&a_1, &a_2, a_normal);
 
-	FV3_NormalizeEx(a_normal, a_normal);
+	return FV3_NormalizeEx(a_normal, a_normal);
+}
+
+//
+// Strength
+//
+// Measures the 'strength' of a vector in a particular direction.
+//
+fixed_t FV3_Strength(const vector3_t *a_1, const vector3_t *dir)
+{
+	vector3_t normal;
+	fixed_t dist = FV3_NormalizeEx(a_1, &normal);
+	fixed_t dot = FV3_Dot(&normal, dir);
+
+	FV3_ClosestPointOnVector(dir, a_1, &normal);
+
+	dist = FV3_Magnitude(&normal);
+
+	if (dot < 0) // Not facing same direction, so negate result.
+		dist = -dist;
+
+	return dist;
 }
 
 //
@@ -550,11 +585,11 @@ boolean FV3_IntersectedPlane(const vector3_t *a_triangle, const vector3_t *a_lin
 
 	*originDistance = FV3_PlaneDistance(a_normal, &a_triangle[0]);
 
-	distance1 = (FixedMul(a_normal->x, a_line[0].x)  + FixedMul(a_normal->y, a_line[0].y)
-				+ FixedMul(a_normal->z, a_line[0].z)) + *originDistance;
+	distance1 = (FixedMul(a_normal->x, a_line[0].x) + FixedMul(a_normal->y, a_line[0].y)
+		+ FixedMul(a_normal->z, a_line[0].z)) + *originDistance;
 
-	distance2 = (FixedMul(a_normal->x, a_line[1].x)  + FixedMul(a_normal->y, a_line[1].y)
-				+ FixedMul(a_normal->z, a_line[1].z)) + *originDistance;
+	distance2 = (FixedMul(a_normal->x, a_line[1].x) + FixedMul(a_normal->y, a_line[1].y)
+		+ FixedMul(a_normal->z, a_line[1].z)) + *originDistance;
 
 	// Positive or zero number means no intersection
 	if (FixedMul(distance1, distance2) >= 0)
@@ -575,8 +610,8 @@ boolean FV3_IntersectedPlane(const vector3_t *a_triangle, const vector3_t *a_lin
 fixed_t FV3_PlaneIntersection(const vector3_t *pOrigin, const vector3_t *pNormal, const vector3_t *rOrigin, const vector3_t *rVector)
 {
 	fixed_t d = -(FV3_Dot(pNormal, pOrigin));
-	fixed_t number = FV3_Dot(pNormal,rOrigin) + d;
-	fixed_t denom = FV3_Dot(pNormal,rVector);
+	fixed_t number = FV3_Dot(pNormal, rOrigin) + d;
+	fixed_t denom = FV3_Dot(pNormal, rVector);
 	return -FixedDiv(number, denom);
 }
 
@@ -597,11 +632,11 @@ fixed_t FV3_IntersectRaySphere(const vector3_t *rO, const vector3_t *rV, const v
 
 	c = FV3_Magnitude(&Q);
 	v = FV3_Dot(&Q, rV);
-	d = FixedMul(sR, sR) - (FixedMul(c,c) - FixedMul(v,v));
+	d = FixedMul(sR, sR) - (FixedMul(c, c) - FixedMul(v, v));
 
 	// If there was no intersection, return -1
-	if (d < 0*FRACUNIT)
-		return (-1*FRACUNIT);
+	if (d < 0 * FRACUNIT)
+		return (-1 * FRACUNIT);
 
 	// Return the distance to the [first] intersecting point
 	return (v - FixedSqrt(d));
@@ -629,9 +664,9 @@ vector3_t *FV3_IntersectionPoint(const vector3_t *vNormal, const vector3_t *vLin
 	//    Here I just chose a arbitrary point as the point to find that distance.  You notice we negate that
 	//    distance.  We negate the distance because we want to eventually go BACKWARDS from our point to the plane.
 	//    By doing this is will basically bring us back to the plane to find our intersection point.
-	Numerator = - (FixedMul(vNormal->x, vLine[0].x) +		// Use the plane equation with the normal and the line
-				   FixedMul(vNormal->y, vLine[0].y) +
-				   FixedMul(vNormal->z, vLine[0].z) + distance);
+	Numerator = -(FixedMul(vNormal->x, vLine[0].x) +		// Use the plane equation with the normal and the line
+		FixedMul(vNormal->y, vLine[0].y) +
+		FixedMul(vNormal->z, vLine[0].z) + distance);
 
 	// 3) If we take the dot product between our line vector and the normal of the polygon,
 	//    this will give us the cosine of the angle between the 2 (since they are both normalized - length 1).
@@ -643,7 +678,7 @@ vector3_t *FV3_IntersectionPoint(const vector3_t *vNormal, const vector3_t *vLin
 	// on the plane (the normal is perpendicular to the line - (Normal.Vector = 0)).
 	// In this case, we should just return any point on the line.
 
-	if( Denominator == 0*FRACUNIT) // Check so we don't divide by zero
+	if (Denominator == 0 * FRACUNIT) // Check so we don't divide by zero
 	{
 		ReturnVec->x = vLine[0].x;
 		ReturnVec->y = vLine[0].y;
@@ -686,8 +721,8 @@ vector3_t *FV3_IntersectionPoint(const vector3_t *vNormal, const vector3_t *vLin
 //
 UINT8 FV3_PointOnLineSide(const vector3_t *point, const vector3_t *line)
 {
-	fixed_t s1 = FixedMul((point->y - line[0].y),(line[1].x - line[0].x));
-	fixed_t s2 = FixedMul((point->x - line[0].x),(line[1].y - line[0].y));
+	fixed_t s1 = FixedMul((point->y - line[0].y), (line[1].x - line[0].x));
+	fixed_t s2 = FixedMul((point->x - line[0].x), (line[1].y - line[0].y));
 	return (UINT8)(s1 - s2 < 0);
 }
 
@@ -752,7 +787,7 @@ void FM_CreateObjectMatrix(matrix_t *matrix, fixed_t x, fixed_t y, fixed_t z, fi
 	matrix->m[0] = upcross.x;
 	matrix->m[1] = upcross.y;
 	matrix->m[2] = upcross.z;
-	matrix->m[3] = 0*FRACUNIT;
+	matrix->m[3] = 0 * FRACUNIT;
 
 	matrix->m[4] = upx;
 	matrix->m[5] = upy;
@@ -764,9 +799,9 @@ void FM_CreateObjectMatrix(matrix_t *matrix, fixed_t x, fixed_t y, fixed_t z, fi
 	matrix->m[10] = anglez;
 	matrix->m[11] = 0;
 
-	matrix->m[12] = x - FixedMul(upx,radius);
-	matrix->m[13] = y - FixedMul(upy,radius);
-	matrix->m[14] = z - FixedMul(upz,radius);
+	matrix->m[12] = x - FixedMul(upx, radius);
+	matrix->m[13] = y - FixedMul(upy, radius);
+	matrix->m[14] = z - FixedMul(upz, radius);
 	matrix->m[15] = FRACUNIT;
 }
 
@@ -778,20 +813,20 @@ void FM_CreateObjectMatrix(matrix_t *matrix, fixed_t x, fixed_t y, fixed_t z, fi
 void FM_MultMatrixVec3(const matrix_t *matrix, const vector3_t *vec, vector3_t *out)
 {
 #define M(row,col)  matrix->m[col * 4 + row]
-	out->x = FixedMul(vec->x,M(0, 0))
-	       + FixedMul(vec->y,M(0, 1))
-	       + FixedMul(vec->z,M(0, 2))
-	       + M(0, 3);
-
-	out->y = FixedMul(vec->x,M(1, 0))
-	       + FixedMul(vec->y,M(1, 1))
-	       + FixedMul(vec->z,M(1, 2))
-	       + M(1, 3);
-
-	out->z = FixedMul(vec->x,M(2, 0))
-	       + FixedMul(vec->y,M(2, 1))
-	       + FixedMul(vec->z,M(2, 2))
-	       + M(2, 3);
+	out->x = FixedMul(vec->x, M(0, 0))
+		+ FixedMul(vec->y, M(0, 1))
+		+ FixedMul(vec->z, M(0, 2))
+		+ M(0, 3);
+
+	out->y = FixedMul(vec->x, M(1, 0))
+		+ FixedMul(vec->y, M(1, 1))
+		+ FixedMul(vec->z, M(1, 2))
+		+ M(1, 3);
+
+	out->z = FixedMul(vec->x, M(2, 0))
+		+ FixedMul(vec->y, M(2, 1))
+		+ FixedMul(vec->z, M(2, 2))
+		+ M(2, 3);
 #undef M
 }
 
@@ -811,7 +846,7 @@ void FM_MultMatrix(matrix_t *dest, const matrix_t *multme)
 	for (i = 0; i < 4; i++)
 	{
 		for (j = 0; j < 4; j++)
-			R(i, j) = FixedMul(D(i, 0),M(0, j)) + FixedMul(D(i, 1),M(1, j)) + FixedMul(D(i, 2),M(2, j)) + FixedMul(D(i, 3),M(3, j));
+			R(i, j) = FixedMul(D(i, 0), M(0, j)) + FixedMul(D(i, 1), M(1, j)) + FixedMul(D(i, 2), M(2, j)) + FixedMul(D(i, 3), M(3, j));
 	}
 
 	M_Memcpy(dest, &result, sizeof(matrix_t));
@@ -869,8 +904,8 @@ void FM_Scale(matrix_t *dest, fixed_t x, fixed_t y, fixed_t z)
 
 static inline void M_print(INT64 a)
 {
-	const fixed_t w = (a>>FRACBITS);
-	fixed_t f = a%FRACUNIT;
+	const fixed_t w = (a >> FRACBITS);
+	fixed_t f = a % FRACUNIT;
 	fixed_t d = FRACUNIT;
 
 	if (f == 0)
@@ -878,7 +913,7 @@ static inline void M_print(INT64 a)
 		printf("%d", (fixed_t)w);
 		return;
 	}
-	else while (f != 1 && f/2 == f>>1)
+	else while (f != 1 && f / 2 == f >> 1)
 	{
 		d /= 2;
 		f /= 2;
@@ -892,7 +927,7 @@ static inline void M_print(INT64 a)
 
 FUNCMATH FUNCINLINE static inline fixed_t FixedMulC(fixed_t a, fixed_t b)
 {
-	return (fixed_t)((((INT64)a * b) ) / FRACUNIT);
+	return (fixed_t)((((INT64)a * b)) / FRACUNIT);
 }
 
 FUNCMATH FUNCINLINE static inline fixed_t FixedDivC2(fixed_t a, fixed_t b)
@@ -902,7 +937,7 @@ FUNCMATH FUNCINLINE static inline fixed_t FixedDivC2(fixed_t a, fixed_t b)
 	if (b == 0)
 		I_Error("FixedDiv: divide by zero");
 
-	ret = (((INT64)a * FRACUNIT) ) / b;
+	ret = (((INT64)a * FRACUNIT)) / b;
 
 	if ((ret > INT32_MAX) || (ret < INT32_MIN))
 		I_Error("FixedDiv: divide by zero");
@@ -911,7 +946,7 @@ FUNCMATH FUNCINLINE static inline fixed_t FixedDivC2(fixed_t a, fixed_t b)
 
 FUNCMATH FUNCINLINE static inline fixed_t FixedDivC(fixed_t a, fixed_t b)
 {
-	if ((abs(a) >> (FRACBITS-2)) >= abs(b))
+	if ((abs(a) >> (FRACBITS - 2)) >= abs(b))
 		return (a^b) < 0 ? INT32_MIN : INT32_MAX;
 
 	return FixedDivC2(a, b);
@@ -938,43 +973,43 @@ int main(int argc, char** argv)
 
 #ifdef MULDIV_TEST
 	for (a = 1; a <= INT32_MAX; a += FRACUNIT)
-	for (b = 0; b <= INT32_MAX; b += FRACUNIT)
-	{
-		c = FixedMul(a, b);
-		d = FixedMulC(a, b);
-		if (c != d)
+		for (b = 0; b <= INT32_MAX; b += FRACUNIT)
 		{
-			printf("(");
-			M_print(a);
-			printf(") * (");
-			M_print(b);
-			printf(") = (");
-			M_print(c);
-			printf(") != (");
-			M_print(d);
-			printf(") \n");
-			n--;
-			printf("%d != %d\n", c, d);
+			c = FixedMul(a, b);
+			d = FixedMulC(a, b);
+			if (c != d)
+			{
+				printf("(");
+				M_print(a);
+				printf(") * (");
+				M_print(b);
+				printf(") = (");
+				M_print(c);
+				printf(") != (");
+				M_print(d);
+				printf(") \n");
+				n--;
+				printf("%d != %d\n", c, d);
+			}
+			c = FixedDiv(a, b);
+			d = FixedDivC(a, b);
+			if (c != d)
+			{
+				printf("(");
+				M_print(a);
+				printf(") / (");
+				M_print(b);
+				printf(") = (");
+				M_print(c);
+				printf(") != (");
+				M_print(d);
+				printf(")\n");
+				n--;
+				printf("%d != %d\n", c, d);
+			}
+			if (n <= 0)
+				exit(-1);
 		}
-		c = FixedDiv(a, b);
-		d = FixedDivC(a, b);
-		if (c != d)
-		{
-			printf("(");
-			M_print(a);
-			printf(") / (");
-			M_print(b);
-			printf(") = (");
-			M_print(c);
-			printf(") != (");
-			M_print(d);
-			printf(")\n");
-			n--;
-			printf("%d != %d\n", c, d);
-		}
-		if (n <= 0)
-			exit(-1);
-	}
 #endif
 
 #ifdef SQRT_TEST
@@ -982,7 +1017,7 @@ int main(int argc, char** argv)
 	{
 		c = FixedSqrt(a);
 		d = FixedSqrtC(a);
-		b = abs(c-d);
+		b = abs(c - d);
 		if (b > 1)
 		{
 			printf("sqrt(");
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 4609913b7a413eba617146772c8f8d468c4120ef..8145a6917e7c6bf738d0e136bf7f4cb424f1c4c6 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -394,9 +394,11 @@ boolean FV3_Equal(const vector3_t *a_1, const vector3_t *a_2);
 fixed_t FV3_Dot(const vector3_t *a_1, const vector3_t *a_2);
 vector3_t *FV3_Cross(const vector3_t *a_1, const vector3_t *a_2, vector3_t *a_o);
 vector3_t *FV3_ClosestPointOnLine(const vector3_t *Line, const vector3_t *p, vector3_t *out);
+void FV3_ClosestPointOnVector(const vector3_t *dir, const vector3_t *p, vector3_t *out);
 void FV3_ClosestPointOnTriangle(const vector3_t *tri, const vector3_t *point, vector3_t *result);
 vector3_t *FV3_Point2Vec(const vector3_t *point1, const vector3_t *point2, vector3_t *a_o);
-void FV3_Normal(const vector3_t *a_triangle, vector3_t *a_normal);
+fixed_t FV3_Normal(const vector3_t *a_triangle, vector3_t *a_normal);
+fixed_t FV3_Strength(const vector3_t *a_1, const vector3_t *dir);
 fixed_t FV3_PlaneDistance(const vector3_t *a_normal, const vector3_t *a_point);
 boolean FV3_IntersectedPlane(const vector3_t *a_triangle, const vector3_t *a_line, vector3_t *a_normal, fixed_t *originDistance);
 fixed_t FV3_PlaneIntersection(const vector3_t *pOrigin, const vector3_t *pNormal, const vector3_t *rOrigin, const vector3_t *rVector);
diff --git a/src/m_menu.c b/src/m_menu.c
index fefafff31a56e99e380402685c90127536054ba7..97b1ce9b5c1bee155f1e1f08db2911a68503113d 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -121,41 +121,8 @@ typedef enum
 const char *quitmsg[NUM_QUITMESSAGES];
 
 // Stuff for customizing the player select screen Tails 09-22-2003
-description_t description[32] =
-{
-	{"\x82Sonic\x80\n\x82Speed:\x80 7\n\x82Weight:\x80 3", "", "sonic"},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""},
-	{"???", "", ""}
-};
+description_t description[MAXSKINS];
+
 //static char *char_notes = NULL;
 //static fixed_t char_scroll = 0;
 
@@ -233,7 +200,9 @@ static char *M_GetConditionString(condition_t cond);
 menu_t SR_MainDef, SR_UnlockChecklistDef;
 
 // Misc. Main Menu
+#if 0 // Bring this back when we have actual single-player
 static void M_SinglePlayerMenu(INT32 choice);
+#endif
 static void M_Options(INT32 choice);
 static void M_Manual(INT32 choice);
 static void M_SelectableClearMenus(INT32 choice);
@@ -317,7 +286,7 @@ menu_t OP_SoundOptionsDef;
 //static void M_RestartAudio(void);
 
 //Misc
-menu_t /*OP_DataOptionsDef,*/ OP_ScreenshotOptionsDef, OP_EraseDataDef;
+menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef;
 menu_t OP_HUDOptionsDef, OP_ChatOptionsDef;
 menu_t OP_GameOptionsDef, OP_ServerOptionsDef;
 #ifndef NONET
@@ -334,8 +303,29 @@ static patch_t *addonsp[NUM_EXT+5];
 
 #define numaddonsshown 4
 
+// Replay hut
+menu_t MISC_ReplayHutDef;
+menu_t MISC_ReplayOptionsDef;
+static void M_HandleReplayHutList(INT32 choice);
+static void M_DrawReplayHut(void);
+static void M_DrawReplayStartMenu(void);
+static boolean M_QuitReplayHut(void);
+static void M_HutStartReplay(INT32 choice);
+
+static void M_DrawPlaybackMenu(void);
+static void M_PlaybackRewind(INT32 choice);
+static void M_PlaybackPause(INT32 choice);
+static void M_PlaybackFastForward(INT32 choice);
+static void M_PlaybackAdvance(INT32 choice);
+static void M_PlaybackSetViews(INT32 choice);
+static void M_PlaybackAdjustView(INT32 choice);
+static void M_PlaybackQuit(INT32 choice);
+
+static UINT8 playback_enterheld = 0; // horrid hack to prevent holding the button from being extremely fucked
+
 // Drawing functions
 static void M_DrawGenericMenu(void);
+static void M_DrawGenericBackgroundMenu(void);
 static void M_DrawCenteredMenu(void);
 static void M_DrawAddons(void);
 static void M_DrawSkyRoom(void);
@@ -412,27 +402,9 @@ static CV_PossibleValue_t skins_cons_t[MAXSKINS+1] = {{1, DEFAULTSKIN}};
 consvar_t cv_chooseskin = {"chooseskin", DEFAULTSKIN, CV_HIDEN|CV_CALL, skins_cons_t, Nextmap_OnChange, 0, NULL, NULL, 0, 0, NULL};
 
 // This gametype list is integral for many different reasons.
-// When you add gametypes here, don't forget to update them in CV_AddValue!
-CV_PossibleValue_t gametype_cons_t[] =
-{
-	{GT_RACE, "Race"}, {GT_MATCH, "Battle"},
-
-	/*						// SRB2kart
-	{GT_COOP, "Co-op"},
-
-	{GT_COMPETITION, "Competition"},
-	{GT_RACE, "Race"},
-
-	{GT_MATCH, "Match"},
-	{GT_TEAMMATCH, "Team Match"},
+// When you add gametypes here, don't forget to update them in dehacked.c and doomstat.h!
+CV_PossibleValue_t gametype_cons_t[NUMGAMETYPES+1];
 
-	{GT_TAG, "Tag"},
-	{GT_HIDEANDSEEK, "Hide and Seek"},
-
-	{GT_CTF, "CTF"},
-	*/
-	{0, NULL}
-};
 consvar_t cv_newgametype = {"newgametype", "Race", CV_HIDEN|CV_CALL, gametype_cons_t, Newgametype_OnChange, 0, NULL, NULL, 0, 0, NULL};
 
 static CV_PossibleValue_t serversort_cons_t[] = {
@@ -500,12 +472,13 @@ static consvar_t cv_dummystaff = {"dummystaff", "0", CV_HIDEN|CV_CALL, dummystaf
 // ---------
 static menuitem_t MainMenu[] =
 {
-	{IT_SUBMENU|IT_STRING, NULL, "Extras",      &SR_UnlockChecklistDef, 76},
-	{IT_CALL   |IT_STRING, NULL, "1 Player",    M_SinglePlayerMenu,     84},
-	{IT_SUBMENU|IT_STRING, NULL, "Multiplayer", &MP_MainDef,            92},
-	{IT_CALL   |IT_STRING, NULL, "Options",     M_Options,             100},
-	{IT_CALL   |IT_STRING, NULL, "Addons",      M_Addons,              108},
-	{IT_CALL   |IT_STRING, NULL, "Quit  Game",  M_QuitSRB2,            116},
+	{IT_SUBMENU|IT_STRING, NULL, "Extras",      &SR_MainDef,        76},
+	//{IT_CALL   |IT_STRING, NULL, "1 Player",    M_SinglePlayerMenu, 84},
+	{IT_CALL   |IT_STRING, NULL, "Time Attack", M_TimeAttack,       84},
+	{IT_SUBMENU|IT_STRING, NULL, "Multiplayer", &MP_MainDef,        92},
+	{IT_CALL   |IT_STRING, NULL, "Options",     M_Options,          100},
+	{IT_CALL   |IT_STRING, NULL, "Addons",      M_Addons,           108},
+	{IT_CALL   |IT_STRING, NULL, "Quit  Game",  M_QuitSRB2,         116},
 };
 
 typedef enum
@@ -523,6 +496,65 @@ static menuitem_t MISC_AddonsMenu[] =
 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleAddons, 0},     // dummy menuitem for the control func
 };
 
+static menuitem_t MISC_ReplayHutMenu[] =
+{
+	{IT_KEYHANDLER|IT_NOTHING, NULL, "", M_HandleReplayHutList, 0}, // Dummy menuitem for the replay list
+	{IT_NOTHING,               NULL, "", NULL,                  0}, // Dummy for handling wrapping to the top of the menu..
+};
+
+static menuitem_t MISC_ReplayStartMenu[] =
+{
+	{IT_CALL      |IT_STRING,  NULL, "Load Addons and Watch", M_HutStartReplay,   0},
+	{IT_CALL      |IT_STRING,  NULL, "Watch Without Addons",  M_HutStartReplay,   10},
+	{IT_CALL      |IT_STRING,  NULL, "Watch Replay",          M_HutStartReplay,   10},
+	{IT_SUBMENU   |IT_STRING,  NULL, "Back",                  &MISC_ReplayHutDef, 30},
+};
+
+static menuitem_t MISC_ReplayOptionsMenu[] =
+{
+	{IT_CVAR|IT_STRING, NULL, "Record Replays",      &cv_recordmultiplayerdemos, 0},
+	{IT_CVAR|IT_STRING, NULL, "Sync Check Interval", &cv_netdemosyncquality,     10},
+};
+
+static menuitem_t PlaybackMenu[] =
+{
+	{IT_CALL   | IT_STRING, "M_PHIDE",  "Hide Menu", M_SelectableClearMenus, 0},
+
+	{IT_CALL   | IT_STRING, "M_PREW",   "Rewind",        M_PlaybackRewind,      20},
+	{IT_CALL   | IT_STRING, "M_PPAUSE", "Pause",         M_PlaybackPause,       36},
+	{IT_CALL   | IT_STRING, "M_PFFWD",  "Fast-Forward",  M_PlaybackFastForward, 52},
+	{IT_CALL   | IT_STRING, "M_PSTEPB", "Backup Frame",  M_PlaybackRewind,      20},
+	{IT_CALL   | IT_STRING, "M_PRESUM", "Resume",        M_PlaybackPause,       36},
+	{IT_CALL   | IT_STRING, "M_PFADV",  "Advance Frame", M_PlaybackAdvance,     52},
+
+	{IT_ARROWS | IT_STRING, "M_PVIEWS", "View Count",  M_PlaybackSetViews, 72},
+	{IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint",   M_PlaybackAdjustView, 88},
+	{IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint 2", M_PlaybackAdjustView, 104},
+	{IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint 3", M_PlaybackAdjustView, 120},
+	{IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint 4", M_PlaybackAdjustView, 136},
+
+	//{IT_CALL   | IT_STRING, "M_POPTS",  "More Options...", M_ReplayHut, 156},
+	//{IT_CALL   | IT_STRING, "M_PEXIT",  "Stop Playback",   M_PlaybackQuit, 172},
+	{IT_CALL   | IT_STRING, "M_PEXIT",  "Stop Playback",   M_PlaybackQuit, 156},
+};
+typedef enum
+{
+	playback_hide,
+	playback_rewind,
+	playback_pause,
+	playback_fastforward,
+	playback_backframe,
+	playback_resume,
+	playback_advanceframe,
+	playback_viewcount,
+	playback_view1,
+	playback_view2,
+	playback_view3,
+	playback_view4,
+	//playback_moreoptions,
+	playback_quit
+} playback_e;
+
 // ---------------------------------
 // Pause Menu Mode Attacking Edition
 // ---------------------------------
@@ -695,7 +727,9 @@ static menuitem_t SR_PandorasBox[] =
 // Sky Room Custom Unlocks
 static menuitem_t SR_MainMenu[] =
 {
-	{IT_STRING|IT_SUBMENU,NULL, "Secrets Checklist", &SR_UnlockChecklistDef, 0},
+	{IT_STRING|IT_SUBMENU,                  NULL, "Unlockables", &SR_UnlockChecklistDef, 100},
+	{IT_CALL|IT_STRING|IT_CALL_NOTMODIFIED, NULL, "Statistics",  M_Statistics,           108},
+	{IT_CALL|IT_STRING,                     NULL, "Replay Hut",  M_ReplayHut,            116},
 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom1
 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom2
 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom3
@@ -1044,15 +1078,13 @@ static menuitem_t OP_MainMenu[] =
 	{IT_SUBMENU|IT_STRING,		NULL, "Sound Options...",		&OP_SoundOptionsDef,		 40},
 
 	{IT_SUBMENU|IT_STRING,		NULL, "HUD Options...",			&OP_HUDOptionsDef,			 60},
-	{IT_STRING|IT_CALL,			NULL, "Screenshot Options...",	M_ScreenshotOptions,		 70},
+	{IT_SUBMENU|IT_STRING,		NULL, "Gameplay Options...",	&OP_GameOptionsDef,			 70},
+	{IT_SUBMENU|IT_STRING,		NULL, "Server Options...",		&OP_ServerOptionsDef,		 80},
 
-	{IT_SUBMENU|IT_STRING,		NULL, "Gameplay Options...",	&OP_GameOptionsDef,			 90},
-	{IT_SUBMENU|IT_STRING,		NULL, "Server Options...",		&OP_ServerOptionsDef,		100},
-	{IT_STRING|IT_CALL,			NULL, "Add-on Options...",      M_AddonsOptions,			110},
+	{IT_SUBMENU|IT_STRING,		NULL, "Data Options...",		&OP_DataOptionsDef,			100},
 
-	{IT_CALL|IT_STRING,			NULL, "Tricks & Secrets (F1)",	M_Manual,					130},
-	{IT_CALL|IT_STRING,			NULL, "Play Credits",			M_Credits,					140},
-	{IT_SUBMENU|IT_STRING,		NULL, "Erase Data...",			&OP_EraseDataDef,			150},
+	{IT_CALL|IT_STRING,			NULL, "Tricks & Secrets (F1)",	M_Manual,					120},
+	{IT_CALL|IT_STRING,			NULL, "Play Credits",			M_Credits,					130},
 };
 
 static menuitem_t OP_ControlsMenu[] =
@@ -1212,13 +1244,15 @@ static menuitem_t OP_VideoOptionsMenu[] =
 	{IT_STRING | IT_CVAR,	NULL,	"Weather Draw Distance",&cv_drawdist_precip,	 55},
 	//{IT_STRING | IT_CVAR,	NULL,	"Weather Density",		&cv_precipdensity,		 65},
 	{IT_STRING | IT_CVAR,	NULL,	"Skyboxes",				&cv_skybox,				 65},
+	{IT_STRING | IT_CVAR,	NULL,	"Field of View",		&cv_fov,					75},
 
-	{IT_STRING | IT_CVAR,	NULL,	"Show FPS",				&cv_ticrate,			 80},
-	{IT_STRING | IT_CVAR,	NULL,	"Vertical Sync",		&cv_vidwait,			 90},
+	{IT_STRING | IT_CVAR,	NULL,	"Show FPS",				&cv_ticrate,			 90},
+	{IT_STRING | IT_CVAR,	NULL,	"Vertical Sync",		&cv_vidwait,			100},
 
 #ifdef HWRENDER
-	{IT_STRING | IT_CVAR,	NULL,	"3D models",            &cv_grmd2,              105},
-	{IT_SUBMENU|IT_STRING,	NULL,	"OpenGL Options...",	&OP_OpenGLOptionsDef,   115},
+	{IT_STRING | IT_CVAR,	NULL,	"3D models",            &cv_grmdls,              115},
+	{IT_STRING | IT_CVAR,	NULL,	"Fallback Player 3D Model",	&cv_grfallbackplayermodel,	125},
+	{IT_SUBMENU|IT_STRING,	NULL,	"OpenGL Options...",	&OP_OpenGLOptionsDef,   135},
 #endif
 };
 
@@ -1233,10 +1267,12 @@ enum
 	op_video_wdd,
 	//op_video_wd,
 	op_video_skybox,
+	op_video_fov,
 	op_video_fps,
 	op_video_vsync,
 #ifdef HWRENDER
 	op_video_md2,
+	op_video_kartman,
 	op_video_ogl,
 #endif
 };
@@ -1252,10 +1288,9 @@ static menuitem_t OP_OpenGLOptionsMenu[] =
 	{IT_SUBMENU|IT_STRING,      NULL, "Fog...",          &OP_OpenGLFogDef,      10},
 	{IT_SUBMENU|IT_STRING,      NULL, "Gamma...",        &OP_OpenGLColorDef,    20},
 
-	{IT_STRING|IT_CVAR,         NULL, "Field of View",   &cv_fov,               35},
-	{IT_STRING|IT_CVAR,         NULL, "Quality",         &cv_scr_depth,         45},
-	{IT_STRING|IT_CVAR,         NULL, "Texture Filter",  &cv_grfiltermode,      55},
-	{IT_STRING|IT_CVAR,         NULL, "Anisotropic",     &cv_granisotropicmode, 65},
+	{IT_STRING|IT_CVAR,         NULL, "Quality",         &cv_scr_depth,         35},
+	{IT_STRING|IT_CVAR,         NULL, "Texture Filter",  &cv_grfiltermode,      45},
+	{IT_STRING|IT_CVAR,         NULL, "Anisotropic",     &cv_granisotropicmode, 55},
 /*#ifdef _WINDOWS
 	{IT_STRING|IT_CVAR,         NULL, "Fullscreen",      &cv_fullscreen,       50},
 #endif
@@ -1324,12 +1359,14 @@ static menuitem_t OP_SoundOptionsMenu[] =
 	{IT_STRING|IT_CVAR,        NULL, "Play SFX While Unfocused", &cv_playsoundifunfocused, 135},
 };
 
-/*static menuitem_t OP_DataOptionsMenu[] =
+static menuitem_t OP_DataOptionsMenu[] =
 {
-	{IT_STRING | IT_CALL, NULL, "Screenshot Options...", M_ScreenshotOptions, 10},
+	{IT_STRING | IT_CALL,		NULL, "Screenshot Options...",	M_ScreenshotOptions,	 10},
+	{IT_STRING | IT_CALL,		NULL, "Add-on Options...",		M_AddonsOptions,		 20},
+	{IT_STRING | IT_SUBMENU,	NULL, "Replay Options...",		&MISC_ReplayOptionsDef,	 30},
 
-	{IT_STRING | IT_SUBMENU, NULL, "Erase Data...", &OP_EraseDataDef, 30},
-};*/
+	{IT_STRING | IT_SUBMENU,	NULL, "Erase Data...",			&OP_EraseDataDef,		 50},
+};
 
 static menuitem_t OP_ScreenshotOptionsMenu[] =
 {
@@ -1570,7 +1607,7 @@ menu_t MISC_AddonsDef =
 {
 	NULL,
 	sizeof (MISC_AddonsMenu)/sizeof (menuitem_t),
-	&MainDef,
+	&OP_DataOptionsDef,
 	MISC_AddonsMenu,
 	M_DrawAddons,
 	50, 28,
@@ -1578,6 +1615,54 @@ menu_t MISC_AddonsDef =
 	NULL
 };
 
+menu_t MISC_ReplayHutDef =
+{
+	NULL,
+	sizeof (MISC_ReplayHutMenu)/sizeof (menuitem_t),
+	NULL,
+	MISC_ReplayHutMenu,
+	M_DrawReplayHut,
+	30, 80,
+	0,
+	M_QuitReplayHut
+};
+
+menu_t MISC_ReplayOptionsDef =
+{
+	"M_REPOPT",
+	sizeof (MISC_ReplayOptionsMenu)/sizeof (menuitem_t),
+	&OP_DataOptionsDef,
+	MISC_ReplayOptionsMenu,
+	M_DrawGenericMenu,
+	27, 40,
+	0,
+	NULL
+};
+
+menu_t MISC_ReplayStartDef =
+{
+	NULL,
+	sizeof (MISC_ReplayStartMenu)/sizeof (menuitem_t),
+	&MISC_ReplayHutDef,
+	MISC_ReplayStartMenu,
+	M_DrawReplayStartMenu,
+	30, 90,
+	0,
+	NULL
+};
+
+menu_t PlaybackMenuDef = {
+	NULL,
+	sizeof (PlaybackMenu)/sizeof (menuitem_t),
+	NULL,
+	PlaybackMenu,
+	M_DrawPlaybackMenu,
+	//BASEVIDWIDTH/2 - 94, 2,
+	BASEVIDWIDTH/2 - 88, 2,
+	0,
+	NULL
+};
+
 menu_t MAPauseDef = PAUSEMENUSTYLE(MAPauseMenu, 40, 72);
 menu_t SPauseDef = PAUSEMENUSTYLE(SPauseMenu, 40, 72);
 menu_t MPauseDef = PAUSEMENUSTYLE(MPauseMenu, 40, 72);
@@ -1675,17 +1760,7 @@ menu_t SR_PandoraDef =
 	0,
 	M_ExitPandorasBox
 };
-menu_t SR_MainDef =
-{
-	"M_SECRET",
-	sizeof (SR_MainMenu)/sizeof (menuitem_t),
-	&MainDef,
-	SR_MainMenu,
-	M_DrawSkyRoom,
-	60, 40,
-	0,
-	NULL
-};
+menu_t SR_MainDef = CENTERMENUSTYLE(NULL, SR_MainMenu, &MainDef, 72);
 
 //menu_t SR_LevelSelectDef = MAPICONMENUSTYLE(NULL, SR_LevelSelectMenu, &SR_MainDef);
 
@@ -1693,7 +1768,7 @@ menu_t SR_UnlockChecklistDef =
 {
 	NULL,
 	1,
-	&MainDef, //&SR_MainDef
+	&SR_MainDef,
 	SR_UnlockChecklistMenu,
 	M_DrawChecklist,
 	280, 185,
@@ -1731,7 +1806,7 @@ menu_t SP_LevelStatsDef =
 {
 	"M_STATS",
 	1,
-	&SP_MainDef,
+	&SR_MainDef,
 	SP_LevelStatsMenu,
 	M_DrawLevelStats,
 	280, 185,
@@ -2029,10 +2104,10 @@ menu_t OP_OpenGLColorDef =
 	NULL
 };
 #endif
-//menu_t OP_DataOptionsDef = DEFAULTMENUSTYLE("M_DATA", OP_DataOptionsMenu, &OP_MainDef, 60, 30);
-menu_t OP_ScreenshotOptionsDef = DEFAULTMENUSTYLE("M_SCSHOT", OP_ScreenshotOptionsMenu, &OP_MainDef, 30, 30);
-menu_t OP_AddonsOptionsDef = DEFAULTMENUSTYLE("M_ADDONS", OP_AddonsOptionsMenu, &OP_MainDef, 30, 30);
-menu_t OP_EraseDataDef = DEFAULTMENUSTYLE("M_DATA", OP_EraseDataMenu, &OP_MainDef, 30, 30);
+menu_t OP_DataOptionsDef = DEFAULTMENUSTYLE("M_DATA", OP_DataOptionsMenu, &OP_MainDef, 60, 30);
+menu_t OP_ScreenshotOptionsDef = DEFAULTMENUSTYLE("M_SCSHOT", OP_ScreenshotOptionsMenu, &OP_DataOptionsDef, 30, 30);
+menu_t OP_AddonsOptionsDef = DEFAULTMENUSTYLE("M_ADDONS", OP_AddonsOptionsMenu, &OP_DataOptionsDef, 30, 30);
+menu_t OP_EraseDataDef = DEFAULTMENUSTYLE("M_DATA", OP_EraseDataMenu, &OP_DataOptionsDef, 30, 30);
 
 // ==========================================================================
 // CVAR ONCHANGE EVENTS GO HERE
@@ -2331,10 +2406,8 @@ static void M_ChangeCvar(INT32 choice)
 			choice *= (TICRATE/7);
 		else if (cv == &cv_maxsend)
 			choice *= 512;
-#ifdef NEWPING
 		else if (cv == &cv_maxping)
 			choice *= 50;
-#endif
 #endif
 		CV_AddValue(cv,choice);
 	}
@@ -2445,7 +2518,7 @@ boolean M_Responder(event_t *ev)
 	static INT32 lastx = 0, lasty = 0;
 	void (*routine)(INT32 choice); // for some casting problem
 
-	if (dedicated || (demoplayback && titledemo)
+	if (dedicated || (demo.playback && demo.title)
 	|| gamestate == GS_INTRO || gamestate == GS_CUTSCENE || gamestate == GS_GAMEEND
 	|| gamestate == GS_CREDITS || gamestate == GS_EVALUATION)
 		return false;
@@ -2493,7 +2566,7 @@ boolean M_Responder(event_t *ev)
 	{
 		if (ev->type == ev_joystick  && ev->data1 == 0 && joywait < I_GetTime())
 		{
-			const INT32 jdeadzone = JOYAXISRANGE/4;
+			const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_deadzone.value) >> FRACBITS;
 			if (ev->data3 != INT32_MAX)
 			{
 				if (Joystick.bGamepadStyle || abs(ev->data3) > jdeadzone)
@@ -2565,8 +2638,6 @@ boolean M_Responder(event_t *ev)
 			}
 		}
 	}
-	else if (ev->type == ev_keydown) // Preserve event for other responders
-		ch = ev->data1;
 
 	if (ch == -1)
 		return false;
@@ -2712,6 +2783,19 @@ boolean M_Responder(event_t *ev)
 			routine = M_ChangeCvar;
 	}
 
+	if (currentMenu == &PlaybackMenuDef)
+	{
+		// Flip left/right with up/down for the playback menu, since it's a horizontal icon row.
+		switch (ch)
+		{
+			case KEY_LEFTARROW: ch = KEY_UPARROW; break;
+			case KEY_UPARROW: ch = KEY_RIGHTARROW; break;
+			case KEY_RIGHTARROW: ch = KEY_DOWNARROW; break;
+			case KEY_DOWNARROW: ch = KEY_LEFTARROW; break;
+			default: break;
+		}
+	}
+
 	// Keys usable within menu
 	switch (ch)
 	{
@@ -2758,6 +2842,15 @@ boolean M_Responder(event_t *ev)
 		case KEY_ENTER:
 			noFurtherInput = true;
 			currentMenu->lastOn = itemOn;
+
+			if (currentMenu == &PlaybackMenuDef)
+			{
+				boolean held = (boolean)playback_enterheld;
+				playback_enterheld = TICRATE/7;
+				if (held)
+					return true;
+			}
+
 			if (routine)
 			{
 				if (((currentMenu->menuitems[itemOn].status & IT_TYPE)==IT_CALL
@@ -2870,7 +2963,7 @@ void M_Drawer(void)
 	if (menuactive)
 	{
 		// now that's more readable with a faded background (yeah like Quake...)
-		if (!WipeInAction)
+		if (!WipeInAction && currentMenu != &PlaybackMenuDef) // Replay playback has its own background
 			V_DrawFadeScreen(0xFF00, 16);
 
 		if (currentMenu->drawroutine)
@@ -2916,14 +3009,6 @@ void M_Drawer(void)
 //
 void M_StartControlPanel(void)
 {
-	// time attack HACK
-	if (modeattacking && demoplayback)
-	{
-		G_CheckDemoStatus();
-		S_ChangeMusicInternal("racent", true);
-		return;
-	}
-
 	// intro might call this repeatedly
 	if (menuactive)
 	{
@@ -2933,7 +3018,11 @@ void M_StartControlPanel(void)
 
 	menuactive = true;
 
-	if (!Playing())
+	if (demo.playback)
+	{
+		currentMenu = &PlaybackMenuDef;
+	}
+	else if (!Playing())
 	{
 		// Secret menu!
 		//MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
@@ -3165,6 +3254,14 @@ void M_Ticker(void)
 	if (--skullAnimCounter <= 0)
 		skullAnimCounter = 8;
 
+	if (currentMenu == &PlaybackMenuDef)
+	{
+		if (playback_enterheld > 0)
+			playback_enterheld--;
+	}
+	else
+		playback_enterheld = 0;
+
 	//added : 30-01-98 : test mode for five seconds
 	if (vidm_testingmode > 0)
 	{
@@ -3239,7 +3336,9 @@ void M_Init(void)
 #ifdef HWRENDER
 	// Permanently hide some options based on render mode
 	if (rendermode == render_soft)
-		OP_VideoOptionsMenu[op_video_ogl].status = OP_VideoOptionsMenu[op_video_md2].status = IT_DISABLED;
+		OP_VideoOptionsMenu[op_video_ogl].status =
+			OP_VideoOptionsMenu[op_video_kartman].status =
+			OP_VideoOptionsMenu[op_video_md2]    .status = IT_DISABLED;
 #endif
 
 #ifndef NONET
@@ -3250,6 +3349,55 @@ void M_Init(void)
 	CV_RegisterVar(&cv_allcaps);
 }
 
+void M_InitCharacterTables(void)
+{
+	UINT8 i;
+
+	// Setup PlayerMenu table
+	for (i = 0; i < MAXSKINS; i++)
+	{
+		PlayerMenu[i].status = (i < 4 ? IT_CALL : IT_DISABLED);
+		PlayerMenu[i].patch = PlayerMenu[i].text = NULL;
+		PlayerMenu[i].itemaction = M_ChoosePlayer;
+		PlayerMenu[i].alphaKey = 0;
+	}
+
+	// Setup description table
+	for (i = 0; i < MAXSKINS; i++)
+	{
+		if (i == 0)
+		{
+			strcpy(description[i].notes, "\x82Sonic\x80 is the fastest of the three, but also the hardest to control. Beginners beware, but experts will find Sonic very powerful.\n\n\x82""Ability:\x80 Speed Thok\nDouble jump to zoom forward with a huge burst of speed.\n\n\x82Tip:\x80 Simply letting go of forward does not slow down in SRB2. To slow down, hold the opposite direction.");
+			strcpy(description[i].picname, "");
+			strcpy(description[i].skinname, "sonic");
+		}
+		else if (i == 1)
+		{
+			strcpy(description[i].notes, "\x82Tails\x80 is the most mobile of the three, but has the slowest speed. Because of his mobility, he's well-\nsuited to beginners.\n\n\x82""Ability:\x80 Fly\nDouble jump to start flying for a limited time. Repetitively hit the jump button to ascend.\n\n\x82Tip:\x80 To quickly descend while flying, hit the spin button.");
+			strcpy(description[i].picname, "");
+			strcpy(description[i].skinname, "tails");
+		}
+		else if (i == 2)
+		{
+			strcpy(description[i].notes, "\x82Knuckles\x80 is well-\nrounded and can destroy breakable walls simply by touching them, but he can't jump as high as the other two.\n\n\x82""Ability:\x80 Glide & Climb\nDouble jump to glide in the air as long as jump is held. Glide into a wall to climb it.\n\n\x82Tip:\x80 Press spin while climbing to jump off the wall; press jump instead to jump off\nand face away from\nthe wall.");
+			strcpy(description[i].picname, "");
+			strcpy(description[i].skinname, "knuckles");
+		}
+		else if (i == 3)
+		{
+			strcpy(description[i].notes, "\x82Sonic & Tails\x80 team up to take on Dr. Eggman!\nControl Sonic while Tails desperately struggles to keep up.\n\nPlayer 2 can control Tails directly by setting the controls in the options menu.\nTails's directional controls are relative to Player 1's camera.\n\nTails can pick up Sonic while flying and carry him around.");
+			strcpy(description[i].picname, "CHRS&T");
+			strcpy(description[i].skinname, "sonic&tails");
+		}
+		else
+		{
+			strcpy(description[i].notes, "???");
+			strcpy(description[i].picname, "");
+			strcpy(description[i].skinname, "");
+		}
+	}
+}
+
 // ==========================================================================
 // SPECIAL MENU OPTION DRAW ROUTINES GO HERE
 // ==========================================================================
@@ -3681,6 +3829,12 @@ static void M_DrawGenericMenu(void)
 	}
 }
 
+static void M_DrawGenericBackgroundMenu(void)
+{
+	V_DrawPatchFill(W_CachePatchName("SRB2BACK", PU_CACHE));
+	M_DrawGenericMenu();
+}
+
 static void M_DrawPauseMenu(void)
 {
 #if 0
@@ -4214,7 +4368,7 @@ void M_StartMessage(const char *string, void *routine,
 	M_StartControlPanel(); // can't put menuactive to true
 
 	if (currentMenu == &MessageDef) // Prevent recursion
-		MessageDef.prevMenu = &MainDef;
+		MessageDef.prevMenu = ((demo.playback) ? &PlaybackMenuDef : &MainDef);
 	else
 		MessageDef.prevMenu = currentMenu;
 
@@ -4465,7 +4619,7 @@ static void M_Addons(INT32 choice)
 	else
 		--menupathindex[menudepthleft];
 
-	if (!preparefilemenu(false))
+	if (!preparefilemenu(false, false))
 	{
 		M_StartMessage(va("No files/folders found.\n\n%s\n\n(Press a key)\n", (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1)),NULL,MM_NOTHING);
 		return;
@@ -4589,7 +4743,7 @@ static void M_AddonsClearName(INT32 choice)
 // returns whether to do message draw
 static boolean M_AddonsRefresh(void)
 {
-	if ((refreshdirmenu & REFRESHDIR_NORMAL) && !preparefilemenu(true))
+	if ((refreshdirmenu & REFRESHDIR_NORMAL) && !preparefilemenu(true, false))
 	{
 		UNEXIST;
 		if (refreshdirname)
@@ -4664,10 +4818,7 @@ static void M_DrawAddons(void)
 		y = FRACUNIT;
 	else
 	{
-		x = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))<<FRACBITS, ((ssize_t)MAX_WADFILES - (ssize_t)(mainwads+1))<<FRACBITS);
-		y = FixedDiv((((ssize_t)packetsizetally-(ssize_t)mainwadstally)<<FRACBITS), ((((ssize_t)MAXFILENEEDED*sizeof(UINT8)-(ssize_t)mainwadstally)-(5+22))<<FRACBITS)); // 5+22 = (a.ext + checksum length) is minimum addition to packet size tally
-		if (x > y)
-			y = x;
+		y = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))<<FRACBITS, ((ssize_t)MAX_WADFILES - (ssize_t)(mainwads+1))<<FRACBITS);
 		if (y > FRACUNIT) // happens because of how we're shrinkin' it a little
 			y = FRACUNIT;
 	}
@@ -4838,7 +4989,7 @@ static void M_HandleAddons(INT32 choice)
 		if (dirmenu && dirmenu[dir_on[menudepthleft]])
 			tempname = Z_StrDup(dirmenu[dir_on[menudepthleft]]+DIR_STRING); // don't need to I_Error if can't make - not important, just QoL
 #if 0 // much slower
-		if (!preparefilemenu(true))
+		if (!preparefilemenu(true, false))
 		{
 			UNEXIST;
 			return;
@@ -4892,13 +5043,13 @@ static void M_HandleAddons(INT32 choice)
 								menupathindex[--menudepthleft] = strlen(menupath);
 								menupath[menupathindex[menudepthleft]] = 0;
 
-								if (!preparefilemenu(false))
+								if (!preparefilemenu(false, false))
 								{
 									S_StartSound(NULL, sfx_s224);
 									M_StartMessage(va("%c%s\x80\nThis folder is empty.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
 									menupath[menupathindex[++menudepthleft]] = 0;
 
-									if (!preparefilemenu(true))
+									if (!preparefilemenu(true, false))
 									{
 										UNEXIST;
 										return;
@@ -4921,7 +5072,7 @@ static void M_HandleAddons(INT32 choice)
 						case EXT_UP:
 							S_StartSound(NULL, sfx_menu1);
 							menupath[menupathindex[++menudepthleft]] = 0;
-							if (!preparefilemenu(false))
+							if (!preparefilemenu(false, false))
 							{
 								UNEXIST;
 								return;
@@ -4978,154 +5129,977 @@ static void M_HandleAddons(INT32 choice)
 	}
 }
 
-static void M_PandorasBox(INT32 choice)
-{
-	(void)choice;
-	CV_StealthSetValue(&cv_dummyrings, max(players[consoleplayer].health - 1, 0));
-	CV_StealthSetValue(&cv_dummylives, players[consoleplayer].lives);
-	CV_StealthSetValue(&cv_dummycontinues, players[consoleplayer].continues);
-	M_SetupNextMenu(&SR_PandoraDef);
-}
+// ---- REPLAY HUT -----
+menudemo_t *demolist;
 
-static boolean M_ExitPandorasBox(void)
-{
-	if (cv_dummyrings.value != max(players[consoleplayer].health - 1, 0))
-		COM_ImmedExecute(va("setrings %d", cv_dummyrings.value));
-	if (cv_dummylives.value != players[consoleplayer].lives)
-		COM_ImmedExecute(va("setlives %d", cv_dummylives.value));
-	if (cv_dummycontinues.value != players[consoleplayer].continues)
-		COM_ImmedExecute(va("setcontinues %d", cv_dummycontinues.value));
-	return true;
-}
+#define DF_ENCORE       0x40
+static INT16 replayScrollTitle = 0;
+static SINT8 replayScrollDelay = TICRATE, replayScrollDir = 1;
 
-static void M_ChangeLevel(INT32 choice)
+static void PrepReplayList(void)
 {
-	char mapname[6];
-	(void)choice;
-
-	strlcpy(mapname, G_BuildMapName(cv_nextmap.value), sizeof (mapname));
-	strlwr(mapname);
-	mapname[5] = '\0';
+	size_t i;
 
-	M_ClearMenus(true);
-	COM_BufAddText(va("map %s -gametype \"%s\"\n", mapname, cv_newgametype.string));
-}
+	if (demolist)
+		Z_Free(demolist);
 
-static void M_ConfirmSpectate(INT32 choice)
-{
-	(void)choice;
-	// We allow switching to spectator even if team changing is not allowed
-	M_ClearMenus(true);
-	COM_ImmedExecute("changeteam spectator");
-}
+	demolist = Z_Calloc(sizeof(menudemo_t) * sizedirmenu, PU_STATIC, NULL);
 
-static void M_ConfirmEnterGame(INT32 choice)
-{
-	(void)choice;
-	if (!cv_allowteamchange.value)
+	for (i = 0; i < sizedirmenu; i++)
 	{
-		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
-		return;
+		if (dirmenu[i][DIR_TYPE] == EXT_UP)
+		{
+			demolist[i].type = MD_SUBDIR;
+			sprintf(demolist[i].title, "UP");
+		}
+		else if (dirmenu[i][DIR_TYPE] == EXT_FOLDER)
+		{
+			demolist[i].type = MD_SUBDIR;
+			strncpy(demolist[i].title, dirmenu[i] + DIR_STRING, 64);
+		}
+		else
+		{
+			demolist[i].type = MD_NOTLOADED;
+			snprintf(demolist[i].filepath, 255, "%s%s", menupath, dirmenu[i] + DIR_STRING);
+			sprintf(demolist[i].title, ".....");
+		}
 	}
-	M_ClearMenus(true);
-	COM_ImmedExecute("changeteam playing");
-}
-
-static void M_ConfirmTeamScramble(INT32 choice)
-{
-	(void)choice;
-	M_ClearMenus(true);
-
-	COM_ImmedExecute(va("teamscramble %d", cv_dummyscramble.value+1));
 }
 
-static void M_ConfirmTeamChange(INT32 choice)
+void M_ReplayHut(INT32 choice)
 {
 	(void)choice;
 
-	if (cv_dummymenuplayer.value > splitscreen+1)
-		return;
-
-	if (!cv_allowteamchange.value && cv_dummyteam.value)
+	if (!demo.inreplayhut)
 	{
-		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
-		return;
+		snprintf(menupath, 1024, "%s"PATHSEP"replay"PATHSEP"online"PATHSEP, srb2home);
+		menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath);
 	}
-
-	M_ClearMenus(true);
-
-	switch (cv_dummymenuplayer.value)
+	if (!preparefilemenu(false, true))
 	{
-		case 1:
-		default:
-			COM_ImmedExecute(va("changeteam %s", cv_dummyteam.string));
-			break;
-		case 2:
-			COM_ImmedExecute(va("changeteam2 %s", cv_dummyteam.string));
-			break;
-		case 3:
-			COM_ImmedExecute(va("changeteam3 %s", cv_dummyteam.string));
-			break;
-		case 4:
-			COM_ImmedExecute(va("changeteam4 %s", cv_dummyteam.string));
-			break;
+		M_StartMessage("No replays found.\n\n(Press a key)\n", NULL, MM_NOTHING);
+		return;
 	}
-}
+	else if (!demo.inreplayhut)
+		dir_on[menudepthleft] = 0;
+	demo.inreplayhut = true;
 
-static void M_ConfirmSpectateChange(INT32 choice)
-{
-	(void)choice;
+	replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1;
 
-	if (cv_dummymenuplayer.value > splitscreen+1)
-		return;
+	PrepReplayList();
 
-	if (!cv_allowteamchange.value && cv_dummyspectate.value)
-	{
-		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
-		return;
-	}
+	menuactive = true;
+	M_SetupNextMenu(&MISC_ReplayHutDef);
+	G_SetGamestate(GS_TIMEATTACK);
 
-	M_ClearMenus(true);
+	demo.rewinding = false;
 
-	switch (cv_dummymenuplayer.value)
-	{
-		case 1:
-		default:
-			COM_ImmedExecute(va("changeteam %s", cv_dummyspectate.string));
-			break;
-		case 2:
-			COM_ImmedExecute(va("changeteam2 %s", cv_dummyspectate.string));
-			break;
-		case 3:
-			COM_ImmedExecute(va("changeteam3 %s", cv_dummyspectate.string));
-			break;
-		case 4:
-			COM_ImmedExecute(va("changeteam4 %s", cv_dummyspectate.string));
-			break;
-	}
+	S_ChangeMusicInternal("replst", true);
 }
 
-static void M_Options(INT32 choice)
+static void M_HandleReplayHutList(INT32 choice)
 {
-	(void)choice;
-
-	// if the player is not admin or server, disable gameplay & server options
-	OP_MainMenu[5].status = OP_MainMenu[6].status = (Playing() && !(server || IsPlayerAdmin(consoleplayer))) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU);
+	switch (choice)
+	{
+	case KEY_UPARROW:
+		if (dir_on[menudepthleft])
+			dir_on[menudepthleft]--;
+		else
+			M_PrevOpt();
 
-	// if the player is playing _at all_, disable the erase data & credits options
-	OP_MainMenu[9].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_CALL);
-	OP_MainMenu[10].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU);
+		S_StartSound(NULL, sfx_menu1);
+		replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1;
+		break;
 
-	OP_GameOptionsMenu[3].status =
-		(M_SecretUnlocked(SECRET_ENCORE)) ? (IT_CVAR|IT_STRING) : IT_SECRET; // cv_kartencore
+	case KEY_DOWNARROW:
+		if (dir_on[menudepthleft] < sizedirmenu-1)
+			dir_on[menudepthleft]++;
+		else
+			itemOn = 0; // Not M_NextOpt because that would take us to the extra dummy item
 
-	OP_MainDef.prevMenu = currentMenu;
-	M_SetupNextMenu(&OP_MainDef);
-}
+		S_StartSound(NULL, sfx_menu1);
+		replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1;
+		break;
 
-static void M_Manual(INT32 choice)
-{
-	(void)choice;
+	case KEY_ESCAPE:
+		M_QuitReplayHut();
+		break;
+
+	case KEY_ENTER:
+		switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE])
+		{
+			case EXT_FOLDER:
+				strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING);
+				if (menudepthleft)
+				{
+					menupathindex[--menudepthleft] = strlen(menupath);
+					menupath[menupathindex[menudepthleft]] = 0;
+
+					if (!preparefilemenu(false, true))
+					{
+						S_StartSound(NULL, sfx_s224);
+						M_StartMessage(va("%c%s\x80\nThis folder is empty.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
+						menupath[menupathindex[++menudepthleft]] = 0;
+
+						if (!preparefilemenu(true, true))
+						{
+							M_QuitReplayHut();
+							return;
+						}
+					}
+					else
+					{
+						S_StartSound(NULL, sfx_menu1);
+						dir_on[menudepthleft] = 1;
+						PrepReplayList();
+					}
+				}
+				else
+				{
+					S_StartSound(NULL, sfx_s26d);
+					M_StartMessage(va("%c%s\x80\nThis folder is too deep to navigate to!\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
+					menupath[menupathindex[menudepthleft]] = 0;
+				}
+				break;
+			case EXT_UP:
+				S_StartSound(NULL, sfx_menu1);
+				menupath[menupathindex[++menudepthleft]] = 0;
+				if (!preparefilemenu(false, true))
+				{
+					M_QuitReplayHut();
+					return;
+				}
+				PrepReplayList();
+				break;
+			default:
+				// We can't just use M_SetupNextMenu because that'll run ReplayDef's quitroutine and boot us back to the title screen!
+				currentMenu->lastOn = itemOn;
+				currentMenu = &MISC_ReplayStartDef;
+
+				replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1;
+
+				switch (demolist[dir_on[menudepthleft]].addonstatus)
+				{
+				case DFILE_ERROR_CANNOTLOAD:
+					// Only show "Watch Replay Without Addons"
+					MISC_ReplayStartMenu[0].status = IT_DISABLED;
+					MISC_ReplayStartMenu[1].status = IT_CALL|IT_STRING;
+					//MISC_ReplayStartMenu[1].alphaKey = 0;
+					MISC_ReplayStartMenu[2].status = IT_DISABLED;
+					itemOn = 1;
+					break;
+
+				case DFILE_ERROR_NOTLOADED:
+				case DFILE_ERROR_INCOMPLETEOUTOFORDER:
+					// Show "Load Addons and Watch Replay" and "Watch Replay Without Addons"
+					MISC_ReplayStartMenu[0].status = IT_CALL|IT_STRING;
+					MISC_ReplayStartMenu[1].status = IT_CALL|IT_STRING;
+					//MISC_ReplayStartMenu[1].alphaKey = 10;
+					MISC_ReplayStartMenu[2].status = IT_DISABLED;
+					itemOn = 0;
+					break;
+
+				case DFILE_ERROR_EXTRAFILES:
+				case DFILE_ERROR_OUTOFORDER:
+				default:
+					// Show "Watch Replay"
+					MISC_ReplayStartMenu[0].status = IT_DISABLED;
+					MISC_ReplayStartMenu[1].status = IT_DISABLED;
+					MISC_ReplayStartMenu[2].status = IT_CALL|IT_STRING;
+					//MISC_ReplayStartMenu[2].alphaKey = 0;
+					itemOn = 2;
+					break;
+				}
+		}
+
+		break;
+	}
+}
+
+#define SCALEDVIEWWIDTH (vid.width/vid.dupx)
+#define SCALEDVIEWHEIGHT (vid.height/vid.dupy)
+static void DrawReplayHutReplayInfo(void)
+{
+	lumpnum_t lumpnum;
+	patch_t *patch;
+	UINT8 *colormap;
+	INT32 x, y, w, h;
+
+	switch (demolist[dir_on[menudepthleft]].type)
+	{
+	case MD_NOTLOADED:
+		V_DrawCenteredString(160, 40, V_SNAPTOTOP, "Loading replay information...");
+		break;
+
+	case MD_INVALID:
+		V_DrawCenteredString(160, 40, V_SNAPTOTOP|warningflags, "This replay cannot be played.");
+		break;
+
+	case MD_SUBDIR:
+		break; // Can't think of anything to draw here right now
+
+	case MD_OUTDATED:
+		V_DrawThinString(17, 64, V_SNAPTOTOP|V_ALLOWLOWERCASE|V_TRANSLUCENT|highlightflags, "Recorded on an outdated version.");
+		/*fallthru*/
+	default:
+		// Draw level stuff
+		x = 15; y = 15;
+
+		//  A 160x100 image of the level as entry MAPxxP
+		//CONS_Printf("%d %s\n", demolist[dir_on[menudepthleft]].map, G_BuildMapName(demolist[dir_on[menudepthleft]].map));
+		lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName(demolist[dir_on[menudepthleft]].map)));
+		if (lumpnum != LUMPERROR)
+			patch = W_CachePatchNum(lumpnum, PU_CACHE);
+		else
+			patch = W_CachePatchName("M_NOLVL", PU_CACHE);
+
+		if (!(demolist[dir_on[menudepthleft]].kartspeed & DF_ENCORE))
+			V_DrawSmallScaledPatch(x, y, V_SNAPTOTOP, patch);
+		else
+		{
+			w = SHORT(patch->width);
+			h = SHORT(patch->height);
+			V_DrawSmallScaledPatch(x+(w>>1), y, V_SNAPTOTOP|V_FLIP, patch);
+
+			{
+				static angle_t rubyfloattime = 0;
+				const fixed_t rubyheight = FINESINE(rubyfloattime>>ANGLETOFINESHIFT);
+				V_DrawFixedPatch((x+(w>>2))<<FRACBITS, ((y+(h>>2))<<FRACBITS) - (rubyheight<<1), FRACUNIT, V_SNAPTOTOP, W_CachePatchName("RUBYICON", PU_CACHE), NULL);
+				rubyfloattime += (ANGLE_MAX/NEWTICRATE);
+			}
+		}
+
+		x += 85;
+
+		if (mapheaderinfo[demolist[dir_on[menudepthleft]].map-1])
+			V_DrawString(x, y, V_SNAPTOTOP, G_BuildMapTitle(demolist[dir_on[menudepthleft]].map));
+		else
+			V_DrawString(x, y, V_SNAPTOTOP|V_ALLOWLOWERCASE|V_TRANSLUCENT, "Level is not loaded.");
+
+		if (demolist[dir_on[menudepthleft]].numlaps)
+			V_DrawThinString(x, y+9, V_SNAPTOTOP|V_ALLOWLOWERCASE, va("(%d laps)", demolist[dir_on[menudepthleft]].numlaps));
+
+		V_DrawString(x, y+20, V_SNAPTOTOP|V_ALLOWLOWERCASE, demolist[dir_on[menudepthleft]].gametype == GT_RACE ?
+			va("Race (%s speed)", kartspeed_cons_t[demolist[dir_on[menudepthleft]].kartspeed & ~DF_ENCORE].strvalue) :
+			"Battle Mode");
+
+		if (!demolist[dir_on[menudepthleft]].standings[0].ranking)
+		{
+			// No standings were loaded!
+			V_DrawString(x, y+39, V_SNAPTOTOP|V_ALLOWLOWERCASE|V_TRANSLUCENT, "No standings available.");
+
+
+			break;
+		}
+
+		V_DrawThinString(x, y+29, V_SNAPTOTOP|highlightflags, "WINNER");
+		V_DrawString(x+38, y+30, V_SNAPTOTOP|V_ALLOWLOWERCASE, demolist[dir_on[menudepthleft]].standings[0].name);
+
+		if (demolist[dir_on[menudepthleft]].gametype == GT_RACE)
+		{
+			V_DrawThinString(x, y+39, V_SNAPTOTOP|highlightflags, "TIME");
+			V_DrawRightAlignedString(x+84, y+40, V_SNAPTOTOP, va("%d'%02d\"%02d",
+											G_TicsToMinutes(demolist[dir_on[menudepthleft]].standings[0].timeorscore, true),
+											G_TicsToSeconds(demolist[dir_on[menudepthleft]].standings[0].timeorscore),
+											G_TicsToCentiseconds(demolist[dir_on[menudepthleft]].standings[0].timeorscore)
+			));
+		}
+		else
+		{
+			V_DrawThinString(x, y+39, V_SNAPTOTOP|highlightflags, "SCORE");
+			V_DrawString(x+32, y+40, V_SNAPTOTOP, va("%d", demolist[dir_on[menudepthleft]].standings[0].timeorscore));
+		}
+
+		// Character face!
+		if (W_CheckNumForName(skins[demolist[dir_on[menudepthleft]].standings[0].skin].facewant) != LUMPERROR)
+		{
+			patch = facewantprefix[demolist[dir_on[menudepthleft]].standings[0].skin];
+			colormap = R_GetTranslationColormap(
+				demolist[dir_on[menudepthleft]].standings[0].skin,
+				demolist[dir_on[menudepthleft]].standings[0].color,
+				GTC_MENUCACHE);
+		}
+		else
+		{
+			patch = W_CachePatchName("M_NOWANT", PU_CACHE);
+			colormap = R_GetTranslationColormap(
+				TC_RAINBOW,
+				demolist[dir_on[menudepthleft]].standings[0].color,
+				GTC_MENUCACHE);
+		}
+
+		V_DrawMappedPatch(BASEVIDWIDTH-15 - SHORT(patch->width), y+20, V_SNAPTOTOP, patch, colormap);
+
+		break;
+	}
+}
+
+static void M_DrawReplayHut(void)
+{
+	INT32 x, y, cursory = 0;
+	INT16 i;
+	INT16 replaylistitem = currentMenu->numitems-2;
+	boolean processed_one_this_frame = false;
+
+	static UINT16 replayhutmenuy = 0;
+
+	V_DrawPatchFill(W_CachePatchName("SRB2BACK", PU_CACHE));
+
+	if (cv_vhseffect.value)
+		V_DrawVhsEffect(false);
+
+	// Draw menu choices
+	x = currentMenu->x;
+	y = currentMenu->y;
+
+	if (itemOn > replaylistitem)
+	{
+		itemOn = replaylistitem;
+		dir_on[menudepthleft] = sizedirmenu-1;
+		replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1;
+	}
+	else if (itemOn < replaylistitem)
+	{
+		dir_on[menudepthleft] = 0;
+		replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1;
+	}
+
+	if (itemOn == replaylistitem)
+	{
+		INT32 maxy;
+		// Scroll menu items if needed
+		cursory = y + currentMenu->menuitems[replaylistitem].alphaKey + dir_on[menudepthleft]*10;
+		maxy = y + currentMenu->menuitems[replaylistitem].alphaKey + sizedirmenu*10;
+
+		if (cursory > maxy - 20)
+			cursory = maxy - 20;
+
+		if (cursory - replayhutmenuy > SCALEDVIEWHEIGHT-50)
+			replayhutmenuy += (cursory-SCALEDVIEWHEIGHT-replayhutmenuy + 51)/2;
+		else if (cursory - replayhutmenuy < 110)
+			replayhutmenuy += (max(0, cursory-110)-replayhutmenuy - 1)/2;
+	}
+	else
+		replayhutmenuy /= 2;
+
+	y -= replayhutmenuy;
+
+	// Draw static menu items
+	for (i = 0; i < replaylistitem; i++)
+	{
+		INT32 localy = y + currentMenu->menuitems[i].alphaKey;
+
+		if (localy < 65)
+			continue;
+
+		if (i == itemOn)
+			cursory = localy;
+
+		if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING)
+			V_DrawString(x, localy, V_SNAPTOTOP|V_SNAPTOLEFT, currentMenu->menuitems[i].text);
+		else
+			V_DrawString(x, localy, V_SNAPTOTOP|V_SNAPTOLEFT|highlightflags, currentMenu->menuitems[i].text);
+	}
+
+	y += currentMenu->menuitems[replaylistitem].alphaKey;
+
+	for (i = 0; i < (INT16)sizedirmenu; i++)
+	{
+		INT32 localy = y+i*10;
+		INT32 localx = x;
+
+		if (localy < 65)
+			continue;
+		if (localy >= SCALEDVIEWHEIGHT)
+			break;
+
+		if (demolist[i].type == MD_NOTLOADED && !processed_one_this_frame)
+		{
+			processed_one_this_frame = true;
+			G_LoadDemoInfo(&demolist[i]);
+		}
+
+		if (demolist[i].type == MD_SUBDIR)
+		{
+			localx += 8;
+			V_DrawScaledPatch(x - 4, localy, V_SNAPTOTOP|V_SNAPTOLEFT, W_CachePatchName(dirmenu[i][DIR_TYPE] == EXT_UP ? "M_RBACK" : "M_RFLDR", PU_CACHE));
+		}
+
+		if (itemOn == replaylistitem && i == (INT16)dir_on[menudepthleft])
+		{
+			cursory = localy;
+
+			if (replayScrollDelay)
+				replayScrollDelay--;
+			else if (replayScrollDir > 0)
+			{
+				if (replayScrollTitle < (V_StringWidth(demolist[i].title, 0) - (SCALEDVIEWWIDTH - (x<<1)))<<1)
+					replayScrollTitle++;
+				else
+				{
+					replayScrollDelay = TICRATE;
+					replayScrollDir = -1;
+				}
+			}
+			else
+			{
+				if (replayScrollTitle > 0)
+					replayScrollTitle--;
+				else
+				{
+					replayScrollDelay = TICRATE;
+					replayScrollDir = 1;
+				}
+			}
+
+			V_DrawString(localx - (replayScrollTitle>>1), localy, V_SNAPTOTOP|V_SNAPTOLEFT|highlightflags|V_ALLOWLOWERCASE, demolist[i].title);
+		}
+		else
+			V_DrawString(localx, localy, V_SNAPTOTOP|V_SNAPTOLEFT|V_ALLOWLOWERCASE, demolist[i].title);
+	}
+
+	// Draw scrollbar
+	y = sizedirmenu*10 + currentMenu->menuitems[replaylistitem].alphaKey + 30;
+	if (y > SCALEDVIEWHEIGHT-80)
+	{
+		V_DrawFill(BASEVIDWIDTH-4, 75, 4, SCALEDVIEWHEIGHT-80, V_SNAPTOTOP|V_SNAPTORIGHT|239);
+		V_DrawFill(BASEVIDWIDTH-3, 76 + (SCALEDVIEWHEIGHT-80) * replayhutmenuy / y, 2, (((SCALEDVIEWHEIGHT-80) * (SCALEDVIEWHEIGHT-80))-1) / y - 1, V_SNAPTOTOP|V_SNAPTORIGHT|229);
+	}
+
+	// Draw the cursor
+	V_DrawScaledPatch(currentMenu->x - 24, cursory, V_SNAPTOTOP|V_SNAPTOLEFT,
+		W_CachePatchName("M_CURSOR", PU_CACHE));
+	V_DrawString(currentMenu->x, cursory, V_SNAPTOTOP|V_SNAPTOLEFT|highlightflags, currentMenu->menuitems[itemOn].text);
+
+	// Now draw some replay info!
+	V_DrawFill(10, 10, 300, 60, V_SNAPTOTOP|239);
+
+	if (itemOn == replaylistitem)
+	{
+		DrawReplayHutReplayInfo();
+	}
+}
+
+static void M_DrawReplayStartMenu(void)
+{
+	const char *warning;
+	UINT8 i;
+
+	M_DrawGenericBackgroundMenu();
+
+#define STARTY 62-(replayScrollTitle>>1)
+	// Draw rankings beyond first
+	for (i = 1; i < MAXPLAYERS && demolist[dir_on[menudepthleft]].standings[i].ranking; i++)
+	{
+		patch_t *patch;
+		UINT8 *colormap;
+
+		V_DrawRightAlignedString(BASEVIDWIDTH-100, STARTY + i*20, V_SNAPTOTOP|highlightflags, va("%2d", demolist[dir_on[menudepthleft]].standings[i].ranking));
+		V_DrawThinString(BASEVIDWIDTH-96, STARTY + i*20, V_SNAPTOTOP|V_ALLOWLOWERCASE, demolist[dir_on[menudepthleft]].standings[i].name);
+
+		if (demolist[dir_on[menudepthleft]].standings[i].timeorscore == UINT32_MAX-1)
+			V_DrawThinString(BASEVIDWIDTH-92, STARTY + i*20 + 9, V_SNAPTOTOP, "NO CONTEST");
+		else if (demolist[dir_on[menudepthleft]].gametype == GT_RACE)
+			V_DrawRightAlignedString(BASEVIDWIDTH-40, STARTY + i*20 + 9, V_SNAPTOTOP, va("%d'%02d\"%02d",
+											G_TicsToMinutes(demolist[dir_on[menudepthleft]].standings[i].timeorscore, true),
+											G_TicsToSeconds(demolist[dir_on[menudepthleft]].standings[i].timeorscore),
+											G_TicsToCentiseconds(demolist[dir_on[menudepthleft]].standings[i].timeorscore)
+			));
+		else
+			V_DrawString(BASEVIDWIDTH-92, STARTY + i*20 + 9, V_SNAPTOTOP, va("%d", demolist[dir_on[menudepthleft]].standings[i].timeorscore));
+
+		// Character face!
+		if (W_CheckNumForName(skins[demolist[dir_on[menudepthleft]].standings[i].skin].facerank) != LUMPERROR)
+		{
+			patch = facerankprefix[demolist[dir_on[menudepthleft]].standings[i].skin];
+			colormap = R_GetTranslationColormap(
+				demolist[dir_on[menudepthleft]].standings[i].skin,
+				demolist[dir_on[menudepthleft]].standings[i].color,
+				GTC_MENUCACHE);
+		}
+		else
+		{
+			patch = W_CachePatchName("M_NORANK", PU_CACHE);
+			colormap = R_GetTranslationColormap(
+				TC_RAINBOW,
+				demolist[dir_on[menudepthleft]].standings[i].color,
+				GTC_MENUCACHE);
+		}
+
+		V_DrawMappedPatch(BASEVIDWIDTH-5 - SHORT(patch->width), STARTY + i*20, V_SNAPTOTOP, patch, colormap);
+	}
+#undef STARTY
+
+	// Handle scrolling rankings
+	if (replayScrollDelay)
+		replayScrollDelay--;
+	else if (replayScrollDir > 0)
+	{
+		if (replayScrollTitle < (i*20 - SCALEDVIEWHEIGHT + 100)<<1)
+			replayScrollTitle++;
+		else
+		{
+			replayScrollDelay = TICRATE;
+			replayScrollDir = -1;
+		}
+	}
+	else
+	{
+		if (replayScrollTitle > 0)
+			replayScrollTitle--;
+		else
+		{
+			replayScrollDelay = TICRATE;
+			replayScrollDir = 1;
+		}
+	}
+
+	V_DrawFill(10, 10, 300, 60, V_SNAPTOTOP|239);
+	DrawReplayHutReplayInfo();
+
+	V_DrawString(10, 72, V_SNAPTOTOP|highlightflags|V_ALLOWLOWERCASE, demolist[dir_on[menudepthleft]].title);
+
+	// Draw a warning prompt if needed
+	switch (demolist[dir_on[menudepthleft]].addonstatus)
+	{
+	case DFILE_ERROR_CANNOTLOAD:
+		warning = "Some addons in this replay cannot be loaded.\nYou can watch anyway, but desyncs may occur.";
+		break;
+
+	case DFILE_ERROR_NOTLOADED:
+	case DFILE_ERROR_INCOMPLETEOUTOFORDER:
+		warning = "Loading addons will mark your game as modified, and record attack may be unavailable.\nYou can watch without loading addons, but desyncs may occur.";
+		break;
+
+	case DFILE_ERROR_EXTRAFILES:
+		warning = "You have addons loaded that were not present in this replay.\nYou can watch anyway, but desyncs may occur.";
+		break;
+
+	case DFILE_ERROR_OUTOFORDER:
+		warning = "You have this replay's addons loaded, but they are out of order.\nYou can watch anyway, but desyncs may occur.";
+		break;
+
+	default:
+		return;
+	}
+
+	V_DrawSmallString(4, BASEVIDHEIGHT-14, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, warning);
+}
+
+static boolean M_QuitReplayHut(void)
+{
+	// D_StartTitle does its own wipe, since GS_TIMEATTACK is now a complete gamestate.
+	menuactive = false;
+	D_StartTitle();
+
+	if (demolist)
+		Z_Free(demolist);
+	demolist = NULL;
+
+	demo.inreplayhut = false;
+
+	return true;
+}
+
+static void M_HutStartReplay(INT32 choice)
+{
+	(void)choice;
+
+	M_ClearMenus(false);
+	demo.loadfiles = (itemOn == 0);
+	demo.ignorefiles = (itemOn != 0);
+
+	G_DoPlayDemo(demolist[dir_on[menudepthleft]].filepath);
+}
+
+void M_SetPlaybackMenuPointer(void)
+{
+	itemOn = playback_pause;
+}
+
+static void M_DrawPlaybackMenu(void)
+{
+	INT16 i;
+	patch_t *icon;
+	UINT8 *activemap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GOLD, GTC_MENUCACHE);
+
+	// Toggle items
+	if (paused && !demo.rewinding)
+	{
+		PlaybackMenu[playback_pause].status = PlaybackMenu[playback_fastforward].status = PlaybackMenu[playback_rewind].status = IT_DISABLED;
+		PlaybackMenu[playback_resume].status = PlaybackMenu[playback_advanceframe].status = PlaybackMenu[playback_backframe].status = IT_CALL|IT_STRING;
+
+		if (itemOn >= playback_rewind && itemOn <= playback_fastforward)
+			itemOn += playback_backframe - playback_rewind;
+	}
+	else
+	{
+		PlaybackMenu[playback_pause].status = PlaybackMenu[playback_fastforward].status = PlaybackMenu[playback_rewind].status = IT_CALL|IT_STRING;
+		PlaybackMenu[playback_resume].status = PlaybackMenu[playback_advanceframe].status = PlaybackMenu[playback_backframe].status = IT_DISABLED;
+
+		if (itemOn >= playback_backframe && itemOn <= playback_advanceframe)
+			itemOn -= playback_backframe - playback_rewind;
+	}
+
+	if (modeattacking)
+	{
+		for (i = playback_viewcount; i <= playback_view4; i++)
+			PlaybackMenu[i].status = IT_DISABLED;
+
+		//PlaybackMenu[playback_moreoptions].alphaKey = 72;
+		//PlaybackMenu[playback_quit].alphaKey = 88;
+		PlaybackMenu[playback_quit].alphaKey = 72;
+
+		//currentMenu->x = BASEVIDWIDTH/2 - 52;
+		currentMenu->x = BASEVIDWIDTH/2 - 44;
+	}
+	else
+	{
+		PlaybackMenu[playback_viewcount].status = IT_ARROWS|IT_STRING;
+
+		for (i = 0; i <= splitscreen; i++)
+			PlaybackMenu[playback_view1+i].status = IT_ARROWS|IT_STRING;
+		for (i = splitscreen+1; i < 4; i++)
+			PlaybackMenu[playback_view1+i].status = IT_DISABLED;
+
+		//PlaybackMenu[playback_moreoptions].alphaKey = 156;
+		//PlaybackMenu[playback_quit].alphaKey = 172;
+		PlaybackMenu[playback_quit].alphaKey = 156;
+
+		//currentMenu->x = BASEVIDWIDTH/2 - 94;
+		currentMenu->x = BASEVIDWIDTH/2 - 88;
+	}
+
+	// wip
+	//M_DrawTextBox(currentMenu->x-68, currentMenu->y-7, 15, 15);
+	//M_DrawCenteredMenu();
+
+	for (i = 0; i < currentMenu->numitems; i++)
+	{
+		UINT8 *inactivemap = NULL;
+
+		if (i >= playback_view1 && i <= playback_view4)
+		{
+			if (modeattacking) continue;
+
+			if (splitscreen >= i - playback_view1)
+			{
+				INT32 ply = displayplayers[i - playback_view1];
+
+				icon = facerankprefix[players[ply].skin];
+				if (i != itemOn)
+					inactivemap = R_GetTranslationColormap(players[ply].skin, players[ply].skincolor, GTC_MENUCACHE);
+			}
+			else if (currentMenu->menuitems[i].patch && W_CheckNumForName(currentMenu->menuitems[i].patch) != LUMPERROR)
+				icon = W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE);
+			else
+				icon = W_CachePatchName("PLAYRANK", PU_CACHE); // temp
+		}
+		else if (currentMenu->menuitems[i].status == IT_DISABLED)
+			continue;
+		else if (currentMenu->menuitems[i].patch && W_CheckNumForName(currentMenu->menuitems[i].patch) != LUMPERROR)
+			icon = W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE);
+		else
+			icon = W_CachePatchName("PLAYRANK", PU_CACHE); // temp
+
+		if ((i == playback_fastforward && cv_playbackspeed.value > 1) || (i == playback_rewind && demo.rewinding))
+			V_DrawMappedPatch(currentMenu->x + currentMenu->menuitems[i].alphaKey, currentMenu->y, V_SNAPTOTOP, icon, R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_JAWZ, GTC_MENUCACHE));
+		else
+			V_DrawMappedPatch(currentMenu->x + currentMenu->menuitems[i].alphaKey, currentMenu->y, V_SNAPTOTOP, icon, (i == itemOn) ? activemap : inactivemap);
+
+		if (i == itemOn)
+		{
+			V_DrawCharacter(currentMenu->x + currentMenu->menuitems[i].alphaKey + 4, currentMenu->y + 14,
+				'\x1A' | V_SNAPTOTOP|highlightflags, false);
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, currentMenu->y + 18, V_SNAPTOTOP|V_ALLOWLOWERCASE, currentMenu->menuitems[i].text);
+
+			if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_ARROWS)
+			{
+				char *str;
+
+				if (!(i == playback_viewcount && splitscreen == 3))
+					V_DrawCharacter(BASEVIDWIDTH/2 - 4, currentMenu->y + 28 - (skullAnimCounter/5),
+						'\x1A' | V_SNAPTOTOP|highlightflags, false); // up arrow
+
+				if (!(i == playback_viewcount && splitscreen == 0))
+					V_DrawCharacter(BASEVIDWIDTH/2 - 4, currentMenu->y + 48 + (skullAnimCounter/5),
+						'\x1B' | V_SNAPTOTOP|highlightflags, false); // down arrow
+
+				switch (i)
+				{
+				case playback_viewcount:
+					str = va("%d", splitscreen+1);
+					break;
+
+				case playback_view1:
+				case playback_view2:
+				case playback_view3:
+				case playback_view4:
+					str = player_names[displayplayers[i - playback_view1]]; // 0 to 3
+					break;
+
+				default: // shouldn't ever be reached but whatever
+					continue;
+				}
+
+				V_DrawCenteredString(BASEVIDWIDTH/2, currentMenu->y + 38, V_SNAPTOTOP|V_ALLOWLOWERCASE|highlightflags, str);
+			}
+		}
+	}
+}
+
+static void M_PlaybackRewind(INT32 choice)
+{
+	static tic_t lastconfirmtime;
+
+	(void)choice;
+
+	if (!demo.rewinding)
+	{
+		if (paused)
+		{
+			G_ConfirmRewind(leveltime-1);
+			paused = true;
+			S_PauseAudio();
+		}
+		else
+			demo.rewinding = paused = true;
+	}
+	else if (lastconfirmtime + TICRATE/2 < I_GetTime())
+	{
+		lastconfirmtime = I_GetTime();
+		G_ConfirmRewind(leveltime);
+	}
+
+	CV_SetValue(&cv_playbackspeed, 1);
+}
+
+static void M_PlaybackPause(INT32 choice)
+{
+	(void)choice;
+
+	paused = !paused;
+
+	if (demo.rewinding)
+	{
+		G_ConfirmRewind(leveltime);
+		paused = true;
+		S_PauseAudio();
+	}
+	else if (paused)
+		S_PauseAudio();
+	else
+		S_ResumeAudio();
+
+	CV_SetValue(&cv_playbackspeed, 1);
+}
+
+static void M_PlaybackFastForward(INT32 choice)
+{
+	(void)choice;
+
+	if (demo.rewinding)
+	{
+		G_ConfirmRewind(leveltime);
+		paused = false;
+		S_ResumeAudio();
+	}
+	CV_SetValue(&cv_playbackspeed, cv_playbackspeed.value == 1 ? 4 : 1);
+}
+
+static void M_PlaybackAdvance(INT32 choice)
+{
+	(void)choice;
+
+	paused = false;
+	TryRunTics(1);
+	paused = true;
+}
+
+
+static void M_PlaybackSetViews(INT32 choice)
+{
+	if (choice > 0)
+	{
+		if (splitscreen < 3)
+			G_AdjustView(splitscreen + 2, 0, true);
+	}
+	else if (splitscreen)
+	{
+		splitscreen--;
+		R_ExecuteSetViewSize();
+	}
+}
+
+static void M_PlaybackAdjustView(INT32 choice)
+{
+	G_AdjustView(itemOn - playback_viewcount, (choice > 0) ? 1 : -1, true);
+}
+
+static void M_PlaybackQuit(INT32 choice)
+{
+	(void)choice;
+	G_StopDemo();
+
+	if (demo.inreplayhut)
+		M_ReplayHut(choice);
+	else if (modeattacking)
+	{
+		M_EndModeAttackRun();
+		S_ChangeMusicInternal("racent", true);
+	}
+	else
+		D_StartTitle();
+}
+
+static void M_PandorasBox(INT32 choice)
+{
+	(void)choice;
+	CV_StealthSetValue(&cv_dummyrings, max(players[consoleplayer].health - 1, 0));
+	CV_StealthSetValue(&cv_dummylives, players[consoleplayer].lives);
+	CV_StealthSetValue(&cv_dummycontinues, players[consoleplayer].continues);
+	M_SetupNextMenu(&SR_PandoraDef);
+}
+
+static boolean M_ExitPandorasBox(void)
+{
+	if (cv_dummyrings.value != max(players[consoleplayer].health - 1, 0))
+		COM_ImmedExecute(va("setrings %d", cv_dummyrings.value));
+	if (cv_dummylives.value != players[consoleplayer].lives)
+		COM_ImmedExecute(va("setlives %d", cv_dummylives.value));
+	if (cv_dummycontinues.value != players[consoleplayer].continues)
+		COM_ImmedExecute(va("setcontinues %d", cv_dummycontinues.value));
+	return true;
+}
+
+static void M_ChangeLevel(INT32 choice)
+{
+	char mapname[6];
+	(void)choice;
+
+	strlcpy(mapname, G_BuildMapName(cv_nextmap.value), sizeof (mapname));
+	strlwr(mapname);
+	mapname[5] = '\0';
+
+	M_ClearMenus(true);
+	COM_BufAddText(va("map %s -gametype \"%s\"\n", mapname, cv_newgametype.string));
+}
+
+static void M_ConfirmSpectate(INT32 choice)
+{
+	(void)choice;
+	// We allow switching to spectator even if team changing is not allowed
+	M_ClearMenus(true);
+	COM_ImmedExecute("changeteam spectator");
+}
+
+static void M_ConfirmEnterGame(INT32 choice)
+{
+	(void)choice;
+	if (!cv_allowteamchange.value)
+	{
+		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
+		return;
+	}
+	M_ClearMenus(true);
+	COM_ImmedExecute("changeteam playing");
+}
+
+static void M_ConfirmTeamScramble(INT32 choice)
+{
+	(void)choice;
+	M_ClearMenus(true);
+
+	COM_ImmedExecute(va("teamscramble %d", cv_dummyscramble.value+1));
+}
+
+static void M_ConfirmTeamChange(INT32 choice)
+{
+	(void)choice;
+
+	if (cv_dummymenuplayer.value > splitscreen+1)
+		return;
+
+	if (!cv_allowteamchange.value && cv_dummyteam.value)
+	{
+		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
+		return;
+	}
+
+	M_ClearMenus(true);
+
+	switch (cv_dummymenuplayer.value)
+	{
+		case 1:
+		default:
+			COM_ImmedExecute(va("changeteam %s", cv_dummyteam.string));
+			break;
+		case 2:
+			COM_ImmedExecute(va("changeteam2 %s", cv_dummyteam.string));
+			break;
+		case 3:
+			COM_ImmedExecute(va("changeteam3 %s", cv_dummyteam.string));
+			break;
+		case 4:
+			COM_ImmedExecute(va("changeteam4 %s", cv_dummyteam.string));
+			break;
+	}
+}
+
+static void M_ConfirmSpectateChange(INT32 choice)
+{
+	(void)choice;
+
+	if (cv_dummymenuplayer.value > splitscreen+1)
+		return;
+
+	if (!cv_allowteamchange.value && cv_dummyspectate.value)
+	{
+		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
+		return;
+	}
+
+	M_ClearMenus(true);
+
+	switch (cv_dummymenuplayer.value)
+	{
+		case 1:
+		default:
+			COM_ImmedExecute(va("changeteam %s", cv_dummyspectate.string));
+			break;
+		case 2:
+			COM_ImmedExecute(va("changeteam2 %s", cv_dummyspectate.string));
+			break;
+		case 3:
+			COM_ImmedExecute(va("changeteam3 %s", cv_dummyspectate.string));
+			break;
+		case 4:
+			COM_ImmedExecute(va("changeteam4 %s", cv_dummyspectate.string));
+			break;
+	}
+}
+
+static void M_Options(INT32 choice)
+{
+	(void)choice;
+
+	// if the player is not admin or server, disable gameplay & server options
+	OP_MainMenu[4].status = OP_MainMenu[5].status = (Playing() && !(server || IsPlayerAdmin(consoleplayer))) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU);
+
+	OP_MainMenu[8].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_CALL); // Play credits
+	OP_DataOptionsMenu[3].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU); // Erase data
+
+	OP_GameOptionsMenu[3].status =
+		(M_SecretUnlocked(SECRET_ENCORE)) ? (IT_CVAR|IT_STRING) : IT_SECRET; // cv_kartencore
+
+	OP_MainDef.prevMenu = currentMenu;
+	M_SetupNextMenu(&OP_MainDef);
+}
+
+static void M_Manual(INT32 choice)
+{
+	(void)choice;
 
 	MISC_HelpDef.prevMenu = (choice == INT32_MAX ? NULL : currentMenu);
 	M_SetupNextMenu(&MISC_HelpDef);
@@ -5619,6 +6593,7 @@ static void M_Credits(INT32 choice)
 // SINGLE PLAYER MENU
 // ==================
 
+#if 0 // Bring this back when we have actual single-player
 static void M_SinglePlayerMenu(INT32 choice)
 {
 	(void)choice;
@@ -5629,6 +6604,7 @@ static void M_SinglePlayerMenu(INT32 choice)
 
 	M_SetupNextMenu(&SP_MainDef);
 }
+#endif
 
 /*static void M_LoadGameLevelSelect(INT32 choice)
 {
@@ -6946,6 +7922,7 @@ static void M_HandleStaffReplay(INT32 choice)
 				break;
 			M_ClearMenus(true);
 			modeattacking = ATTACKING_RECORD;
+			demo.loadfiles = false; demo.ignorefiles = true; // Just assume that record attack replays have the files needed
 			G_DoPlayDemo(va("%sS%02u",G_BuildMapName(cv_nextmap.value),cv_dummystaff.value));
 			break;
 		default:
@@ -6966,6 +7943,7 @@ static void M_ReplayTimeAttack(INT32 choice)
 	const char *which;
 	M_ClearMenus(true);
 	modeattacking = ATTACKING_RECORD; // set modeattacking before G_DoPlayDemo so the map loader knows
+	demo.loadfiles = false; demo.ignorefiles = true; // Just assume that record attack replays have the files needed
 
 	if (currentMenu == &SP_ReplayDef)
 	{
@@ -7162,7 +8140,7 @@ static void M_ExitGameResponse(INT32 ch)
 static void M_EndGame(INT32 choice)
 {
 	(void)choice;
-	if (demoplayback || demorecording)
+	if (demo.playback)
 		return;
 
 	if (!Playing())
@@ -7279,7 +8257,7 @@ static void M_DrawRoomMenu(void)
 
 static void M_DrawConnectMenu(void)
 {
-	UINT16 i, j;
+	UINT16 i;
 	const char *gt = "Unknown";
 	const char *spd = "";
 	INT32 numPages = (serverlistcount+(SERVERS_PER_PAGE-1))/SERVERS_PER_PAGE;
@@ -7326,11 +8304,8 @@ static void M_DrawConnectMenu(void)
 		                     va("Ping: %u", (UINT32)LONG(serverlist[slindex].info.time)));
 
 		gt = "Unknown";
-		for (j = 0; gametype_cons_t[j].strvalue; j++)
-		{
-			if (gametype_cons_t[j].value == serverlist[slindex].info.gametype)
-				gt = gametype_cons_t[j].strvalue;
-		}
+		if (serverlist[slindex].info.gametype < NUMGAMETYPES)
+			gt = Gametype_Names[serverlist[slindex].info.gametype];
 
 		V_DrawSmallString(currentMenu->x+46,S_LINEY(i)+8, globalflags,
 		                         va("Players: %02d/%02d", serverlist[slindex].info.numberofplayer, serverlist[slindex].info.maxplayer));
@@ -7457,7 +8432,11 @@ static void M_ConnectMenu(INT32 choice)
 	// first page of servers
 	serverlistpage = 0;
 	if (ms_RoomId < 0)
+	{
 		M_RoomMenu(0); // Select a room instead of staring at an empty list
+		// This prevents us from returning to the modified game alert.
+		currentMenu->prevMenu = &MP_MainDef;
+	}
 	else
 		M_SetupNextMenu(&MP_ConnectDef);
 	itemOn = 0;
@@ -7588,6 +8567,8 @@ static void M_StartServer(INT32 choice)
 
 	multiplayer = true;
 
+	strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);
+
 	// Still need to reset devmode
 	cv_debug = 0;
 
@@ -7596,7 +8577,7 @@ static void M_StartServer(INT32 choice)
 	else
 		joinpasswordset = false;
 
-	if (demoplayback)
+	if (demo.playback)
 		G_StopDemo();
 	if (metalrecording)
 		G_StopMetalDemo();
@@ -8345,6 +9326,9 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 	size_t   l;
 	boolean  exitmenu = false;  // exit to previous menu and send name change
 
+	if ((choice == gamecontrol[gc_fire][0] || choice == gamecontrol[gc_fire][1]) && itemOn == 2)
+		choice = KEY_BACKSPACE; // Hack to allow resetting prefcolor on controllers
+
 	switch (choice)
 	{
 		case KEY_DOWNARROW:
@@ -8490,7 +9474,7 @@ static void M_SetupMultiPlayer2(INT32 choice)
 	strcpy (setupm_name, cv_playername2.string);
 
 	// set for splitscreen secondary player
-	setupm_player = &players[secondarydisplayplayer];
+	setupm_player = &players[displayplayers[1]];
 	setupm_cvskin = &cv_skin2;
 	setupm_cvcolor = &cv_playercolor2;
 	setupm_cvname = &cv_playername2;
@@ -8502,7 +9486,7 @@ static void M_SetupMultiPlayer2(INT32 choice)
 	setupm_fakecolor = setupm_cvcolor->value;
 
 	// disable skin changes if we can't actually change skins
-	if (splitscreen && !CanChangeSkin(secondarydisplayplayer))
+	if (splitscreen && !CanChangeSkin(displayplayers[1]))
 		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
 	else
 		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER | IT_STRING);
@@ -8521,7 +9505,7 @@ static void M_SetupMultiPlayer3(INT32 choice)
 	strcpy(setupm_name, cv_playername3.string);
 
 	// set for splitscreen third player
-	setupm_player = &players[thirddisplayplayer];
+	setupm_player = &players[displayplayers[2]];
 	setupm_cvskin = &cv_skin3;
 	setupm_cvcolor = &cv_playercolor3;
 	setupm_cvname = &cv_playername3;
@@ -8533,7 +9517,7 @@ static void M_SetupMultiPlayer3(INT32 choice)
 	setupm_fakecolor = setupm_cvcolor->value;
 
 	// disable skin changes if we can't actually change skins
-	if (splitscreen > 1 && !CanChangeSkin(thirddisplayplayer))
+	if (splitscreen > 1 && !CanChangeSkin(displayplayers[2]))
 		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
 	else
 		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER | IT_STRING);
@@ -8552,7 +9536,7 @@ static void M_SetupMultiPlayer4(INT32 choice)
 	strcpy(setupm_name, cv_playername4.string);
 
 	// set for splitscreen fourth player
-	setupm_player = &players[fourthdisplayplayer];
+	setupm_player = &players[displayplayers[3]];
 	setupm_cvskin = &cv_skin4;
 	setupm_cvcolor = &cv_playercolor4;
 	setupm_cvname = &cv_playername4;
@@ -8564,7 +9548,7 @@ static void M_SetupMultiPlayer4(INT32 choice)
 	setupm_fakecolor = setupm_cvcolor->value;
 
 	// disable skin changes if we can't actually change skins
-	if (splitscreen > 2 && !CanChangeSkin(fourthdisplayplayer))
+	if (splitscreen > 2 && !CanChangeSkin(displayplayers[3]))
 		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
 	else
 		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER | IT_STRING);
@@ -8921,7 +9905,7 @@ static void M_Setup1PControlsMenu(INT32 choice)
 	OP_AllControlsMenu[15].status = IT_CONTROL; // Chat
 	//OP_AllControlsMenu[16].status = IT_CONTROL; // Team-chat
 	OP_AllControlsMenu[16].status = IT_CONTROL; // Rankings
-	OP_AllControlsMenu[17].status = IT_CONTROL; // Viewpoint
+	//OP_AllControlsMenu[17].status = IT_CONTROL; // Viewpoint
 	// 18 is Reset Camera, 19 is Toggle Chasecam
 	OP_AllControlsMenu[20].status = IT_CONTROL; // Pause
 	OP_AllControlsMenu[21].status = IT_CONTROL; // Screenshot
@@ -8953,7 +9937,7 @@ static void M_Setup2PControlsMenu(INT32 choice)
 	OP_AllControlsMenu[15].status = IT_GRAYEDOUT2; // Chat
 	//OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Team-chat
 	OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Rankings
-	OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Viewpoint
+	//OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Viewpoint
 	// 18 is Reset Camera, 19 is Toggle Chasecam
 	OP_AllControlsMenu[20].status = IT_GRAYEDOUT2; // Pause
 	OP_AllControlsMenu[21].status = IT_GRAYEDOUT2; // Screenshot
@@ -8985,7 +9969,7 @@ static void M_Setup3PControlsMenu(INT32 choice)
 	OP_AllControlsMenu[15].status = IT_GRAYEDOUT2; // Chat
 	//OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Team-chat
 	OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Rankings
-	OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Viewpoint
+	//OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Viewpoint
 	// 18 is Reset Camera, 19 is Toggle Chasecam
 	OP_AllControlsMenu[20].status = IT_GRAYEDOUT2; // Pause
 	OP_AllControlsMenu[21].status = IT_GRAYEDOUT2; // Screenshot
@@ -9017,7 +10001,7 @@ static void M_Setup4PControlsMenu(INT32 choice)
 	OP_AllControlsMenu[15].status = IT_GRAYEDOUT2; // Chat
 	//OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Team-chat
 	OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Rankings
-	OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Viewpoint
+	//OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Viewpoint
 	// 18 is Reset Camera, 19 is Toggle Chasecam
 	OP_AllControlsMenu[20].status = IT_GRAYEDOUT2; // Pause
 	OP_AllControlsMenu[21].status = IT_GRAYEDOUT2; // Screenshot
diff --git a/src/m_menu.h b/src/m_menu.h
index 33dc1e407f01a7d030fab16bdfebc28b533dab08..62c852e4d2c0aabd6102dc68df05c0009960262b 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -38,6 +38,9 @@ void M_Drawer(void);
 // Called by D_SRB2Main, loads the config file.
 void M_Init(void);
 
+// Called by D_SRB2Main also, sets up the playermenu and description tables.
+void M_InitCharacterTables(void);
+
 // Called by intro code to force menu up upon a keypress,
 // does nothing if menu is already up.
 void M_StartControlPanel(void);
@@ -210,7 +213,7 @@ typedef struct
 	UINT8 netgame;
 } saveinfo_t;
 
-extern description_t description[32];
+extern description_t description[MAXSKINS];
 
 extern consvar_t cv_showfocuslost;
 extern consvar_t cv_newgametype, cv_nextmap, cv_chooseskin, cv_serversort;
@@ -235,6 +238,9 @@ void Screenshot_option_Onchange(void);
 // Addons menu updating
 void Addons_option_Onchange(void);
 
+void M_ReplayHut(INT32 choice);
+void M_SetPlaybackMenuPointer(void);
+
 INT32 HU_GetHighlightColor(void);
 
 // These defines make it a little easier to make menus
diff --git a/src/m_misc.c b/src/m_misc.c
index c95aa392ccf46a598466b57ec32b9141368a58a6..f4a4ec29116f0885d5ff740fcc9d5cc090092f9f 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -743,12 +743,12 @@ static void M_PNGText(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png
 	else
 		snprintf(lvlttltext, 48, "Unknown");
 
-	if (gamestate == GS_LEVEL && &players[displayplayer] && players[displayplayer].mo)
+	if (gamestate == GS_LEVEL && &players[displayplayers[0]] && players[displayplayers[0]].mo)
 		snprintf(locationtxt, 40, "X:%d Y:%d Z:%d A:%d",
-			players[displayplayer].mo->x>>FRACBITS,
-			players[displayplayer].mo->y>>FRACBITS,
-			players[displayplayer].mo->z>>FRACBITS,
-			FixedInt(AngleFixed(players[displayplayer].mo->angle)));
+			players[displayplayers[0]].mo->x>>FRACBITS,
+			players[displayplayers[0]].mo->y>>FRACBITS,
+			players[displayplayers[0]].mo->z>>FRACBITS,
+			FixedInt(AngleFixed(players[displayplayers[0]].mo->angle)));
 	else
 		snprintf(locationtxt, 40, "Unknown");
 
diff --git a/src/mserv.c b/src/mserv.c
index f5c4fa8892fdecbcc235d259b7e2dd2a8a359a77..c7344b16a7ac6f2cfde07ecd7a501dfb1b52c54d 100644
--- a/src/mserv.c
+++ b/src/mserv.c
@@ -700,7 +700,13 @@ static INT32 AddToMasterServer(boolean firstadd)
 		return MS_CONNECT_ERROR;
 	}
 	retry = 0;
-	if (res == ERRSOCKET)
+	/*
+	Somehow we can still select our old socket despite it being closed(?).
+	Atleast, that's what I THINK is happening. Anyway, we have to check that we
+	haven't open a socket, and actually open it!
+	*/
+	/*if (res == ERRSOCKET)*//* wtf? no! */
+	if (socket_fd == (SOCKET_TYPE)ERRSOCKET)
 	{
 		if (MS_Connect(GetMasterServerIP(), GetMasterServerPort(), 0))
 		{
@@ -714,6 +720,13 @@ static INT32 AddToMasterServer(boolean firstadd)
 	// ok, or bad... let see that!
 	j = (socklen_t)sizeof (i);
 	getsockopt(socket_fd, SOL_SOCKET, SO_ERROR, (char *)&i, &j);
+	/*
+	This is also wrong. If getsockopt fails, i doesn't have to be set. Plus, if
+	it is set (which it appearantly is on linux), we check errno anyway. And in
+	the case that i is returned as normal, we don't even report the correct
+	value! So we accomplish NOTHING, except returning due to dumb luck.
+	If you care, fix this--I don't. -James (R.)
+	*/
 	if (i) // it was bad
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("Master Server socket error #%u: %s\n"), errno, strerror(errno));
diff --git a/src/p_enemy.c b/src/p_enemy.c
index d62ec7efbab61d2810650c3bb79b177f9f7fa0c5..1795a304bff3e5e47c45634d5ed93ea0f8c2e940 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -3079,7 +3079,7 @@ void A_Invincibility(mobj_t *actor)
 	{
 		S_StopMusic();
 		if (mariomode)
-			G_GhostAddColor(GHC_INVINCIBLE);
+			G_GhostAddColor((INT32) (player - players), GHC_INVINCIBLE);
 		S_ChangeMusicInternal((mariomode) ? "minvnc" : "invinc", false);
 	}
 }
@@ -4174,12 +4174,12 @@ void A_OverlayThink(mobj_t *actor)
 	{
 		angle_t viewingangle;
 
-		if (players[displayplayer].awayviewtics)
-			viewingangle = R_PointToAngle2(actor->target->x, actor->target->y, players[displayplayer].awayviewmobj->x, players[displayplayer].awayviewmobj->y);
-		else if (!camera.chase && players[displayplayer].mo)
-			viewingangle = R_PointToAngle2(actor->target->x, actor->target->y, players[displayplayer].mo->x, players[displayplayer].mo->y);
+		if (players[displayplayers[0]].awayviewtics)
+			viewingangle = R_PointToAngle2(actor->target->x, actor->target->y, players[displayplayers[0]].awayviewmobj->x, players[displayplayers[0]].awayviewmobj->y);
+		else if (!camera[0].chase && players[displayplayers[0]].mo)
+			viewingangle = R_PointToAngle2(actor->target->x, actor->target->y, players[displayplayers[0]].mo->x, players[displayplayers[0]].mo->y);
 		else
-			viewingangle = R_PointToAngle2(actor->target->x, actor->target->y, camera.x, camera.y);
+			viewingangle = R_PointToAngle2(actor->target->x, actor->target->y, camera[0].x, camera[0].y);
 
 		destx = actor->target->x + P_ReturnThrustX(actor->target, viewingangle, FixedMul(FRACUNIT, actor->scale));
 		desty = actor->target->y + P_ReturnThrustY(actor->target, viewingangle, FixedMul(FRACUNIT, actor->scale));
@@ -4781,8 +4781,8 @@ void A_DetonChase(mobj_t *actor)
 		actor->reactiontime = -42;
 
 		exact = actor->movedir>>ANGLETOFINESHIFT;
-		xyspeed = FixedMul(FixedMul(actor->tracer->player->normalspeed,3*FRACUNIT/4), FINECOSINE(exact));
-		actor->momz = FixedMul(FixedMul(actor->tracer->player->normalspeed,3*FRACUNIT/4), FINESINE(exact));
+		xyspeed = FixedMul(FixedMul(K_GetKartSpeed(actor->tracer->player, false),3*FRACUNIT/4), FINECOSINE(exact));
+		actor->momz = FixedMul(FixedMul(K_GetKartSpeed(actor->tracer->player, false),3*FRACUNIT/4), FINESINE(exact));
 
 		exact = actor->angle>>ANGLETOFINESHIFT;
 		actor->momx = FixedMul(xyspeed, FINECOSINE(exact));
@@ -8355,6 +8355,7 @@ void A_SPBChase(mobj_t *actor)
 		actor->lastlook = -1;
 		spbplace = -1;
 		P_InstaThrust(actor, actor->angle, wspeed);
+		actor->flags &=  ~MF_NOCLIPTHING;	// just in case.
 		return;
 	}
 
@@ -8384,10 +8385,14 @@ void A_SPBChase(mobj_t *actor)
 	{
 		if (actor->tracer && actor->tracer->health)
 		{
+
 			fixed_t defspeed = wspeed;
 			fixed_t range = (160*actor->tracer->scale);
 			fixed_t cx = 0, cy =0;
 
+			// we're tailing a player, now's a good time to regain our damage properties
+			actor->flags &=  ~MF_NOCLIPTHING;
+
 			// Play the intimidating gurgle
 			if (!S_SoundPlaying(actor, actor->info->activesound))
 				S_StartSound(actor, actor->info->activesound);
@@ -8434,6 +8439,9 @@ void A_SPBChase(mobj_t *actor)
 				wspeed = (3*defspeed)/2;
 			if (wspeed < 20*actor->tracer->scale)
 				wspeed = 20*actor->tracer->scale;
+			if (actor->tracer->player->pflags & PF_SLIDING)
+				wspeed = actor->tracer->player->speed/2;
+			//  ^^^^ current section: These are annoying, and grand metropolis in particular needs this.
 
 			hang = R_PointToAngle2(actor->x, actor->y, actor->tracer->x, actor->tracer->y);
 			vang = R_PointToAngle2(0, actor->z, dist, actor->tracer->z);
@@ -8512,6 +8520,9 @@ void A_SPBChase(mobj_t *actor)
 	{
 		actor->momx = actor->momy = actor->momz = 0; // Stoooop
 
+		// don't hurt players that have nothing to do with this:
+		actor->flags |= MF_NOCLIPTHING;
+
 		if (actor->lastlook != -1
 			&& playeringame[actor->lastlook]
 			&& !players[actor->lastlook].spectator
@@ -8547,6 +8558,10 @@ void A_SPBChase(mobj_t *actor)
 		}
 
 		// Found someone, now get close enough to initiate the slaughter...
+
+		// don't hurt players that have nothing to do with this:
+		actor->flags |= MF_NOCLIPTHING;
+
 		P_SetTarget(&actor->tracer, player->mo);
 		spbplace = bestrank;
 
diff --git a/src/p_floor.c b/src/p_floor.c
index e11fe4030a7cfabf57865fa130a33f14c8dbe564..ccbfd6eae872e40e044af57f8237681c5883c43d 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -2536,9 +2536,9 @@ void T_CameraScanner(elevator_t *elevator)
 		lastleveltime = leveltime;
 	}
 
-	if (players[displayplayer].mo)
+	if (players[displayplayers[0]].mo)
 	{
-		if (players[displayplayer].mo->subsector->sector == elevator->actionsector)
+		if (players[displayplayers[0]].mo->subsector->sector == elevator->actionsector)
 		{
 			if (t_cam_dist == -42)
 				t_cam_dist = cv_cam_dist.value;
@@ -2564,9 +2564,9 @@ void T_CameraScanner(elevator_t *elevator)
 		}
 	}
 
-	if (splitscreen && players[secondarydisplayplayer].mo)
+	if (splitscreen && players[displayplayers[1]].mo)
 	{
-		if (players[secondarydisplayplayer].mo->subsector->sector == elevator->actionsector)
+		if (players[displayplayers[1]].mo->subsector->sector == elevator->actionsector)
 		{
 			if (t_cam2_rotate == -42)
 				t_cam2_dist = cv_cam2_dist.value;
@@ -2592,9 +2592,9 @@ void T_CameraScanner(elevator_t *elevator)
 		}
 	}
 
-	if (splitscreen > 1 && players[thirddisplayplayer].mo)
+	if (splitscreen > 1 && players[displayplayers[2]].mo)
 	{
-		if (players[thirddisplayplayer].mo->subsector->sector == elevator->actionsector)
+		if (players[displayplayers[2]].mo->subsector->sector == elevator->actionsector)
 		{
 			if (t_cam3_rotate == -42)
 				t_cam3_dist = cv_cam3_dist.value;
@@ -2620,9 +2620,9 @@ void T_CameraScanner(elevator_t *elevator)
 		}
 	}
 
-	if (splitscreen > 2 && players[fourthdisplayplayer].mo)
+	if (splitscreen > 2 && players[displayplayers[3]].mo)
 	{
-		if (players[fourthdisplayplayer].mo->subsector->sector == elevator->actionsector)
+		if (players[displayplayers[3]].mo->subsector->sector == elevator->actionsector)
 		{
 			if (t_cam4_rotate == -42)
 				t_cam4_dist = cv_cam4_dist.value;
diff --git a/src/p_inter.c b/src/p_inter.c
index 673df055aa9f0c8e4b7069772cfde567763216ed..a910445da777d7f82a22825446edbd460d9b4c0e 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -62,11 +62,11 @@ void P_ForceConstant(const BasicFF_t *FFInfo)
 	ConstantQuake.Magnitude = FFInfo->Magnitude;
 	if (FFInfo->player == &players[consoleplayer])
 		I_Tactile(ConstantForce, &ConstantQuake);
-	else if (splitscreen && FFInfo->player == &players[secondarydisplayplayer])
+	else if (splitscreen && FFInfo->player == &players[displayplayers[1]])
 		I_Tactile2(ConstantForce, &ConstantQuake);
-	else if (splitscreen > 1 && FFInfo->player == &players[thirddisplayplayer])
+	else if (splitscreen > 1 && FFInfo->player == &players[displayplayers[2]])
 		I_Tactile3(ConstantForce, &ConstantQuake);
-	else if (splitscreen > 2 && FFInfo->player == &players[fourthdisplayplayer])
+	else if (splitscreen > 2 && FFInfo->player == &players[displayplayers[3]])
 		I_Tactile4(ConstantForce, &ConstantQuake);
 }
 void P_RampConstant(const BasicFF_t *FFInfo, INT32 Start, INT32 End)
@@ -83,11 +83,11 @@ void P_RampConstant(const BasicFF_t *FFInfo, INT32 Start, INT32 End)
 	RampQuake.End       = End;
 	if (FFInfo->player == &players[consoleplayer])
 		I_Tactile(ConstantForce, &RampQuake);
-	else if (splitscreen && FFInfo->player == &players[secondarydisplayplayer])
+	else if (splitscreen && FFInfo->player == &players[displayplayers[1]])
 		I_Tactile2(ConstantForce, &RampQuake);
-	else if (splitscreen > 1 && FFInfo->player == &players[thirddisplayplayer])
+	else if (splitscreen > 1 && FFInfo->player == &players[displayplayers[2]])
 		I_Tactile3(ConstantForce, &RampQuake);
-	else if (splitscreen > 2 && FFInfo->player == &players[fourthdisplayplayer])
+	else if (splitscreen > 2 && FFInfo->player == &players[displayplayers[3]])
 		I_Tactile4(ConstantForce, &RampQuake);
 }
 
@@ -218,7 +218,7 @@ void P_DoNightsScore(player_t *player)
 	dummymo->fuse = 3*TICRATE;
 
 	// What?! NO, don't use the camera! Scale up instead!
-	//P_InstaThrust(dummymo, R_PointToAngle2(dummymo->x, dummymo->y, camera.x, camera.y), 3*FRACUNIT);
+	//P_InstaThrust(dummymo, R_PointToAngle2(dummymo->x, dummymo->y, camera[0].x, camera[0].y), 3*FRACUNIT);
 	dummymo->scalespeed = FRACUNIT/25;
 	dummymo->destscale = 2*FRACUNIT;
 }
@@ -851,7 +851,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		// Secret emblem thingy
 		case MT_EMBLEM:
 			{
-				if (demoplayback || player->bot)
+				if (demo.playback || player->bot)
 					return;
 				emblemlocations[special->health-1].collected = true;
 
@@ -1180,13 +1180,13 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 					toucher->angle = special->angle;
 
 					if (player == &players[consoleplayer])
-						localangle = toucher->angle;
-					else if (player == &players[secondarydisplayplayer])
-						localangle2 = toucher->angle;
-					else if (player == &players[thirddisplayplayer])
-						localangle3 = toucher->angle;
-					else if (player == &players[fourthdisplayplayer])
-						localangle4 = toucher->angle;
+						localangle[0] = toucher->angle;
+					else if (player == &players[displayplayers[1]])
+						localangle[1] = toucher->angle;
+					else if (player == &players[displayplayers[2]])
+						localangle[2] = toucher->angle;
+					else if (player == &players[displayplayers[3]])
+						localangle[3] = toucher->angle;
 
 					P_ResetPlayer(player);
 
@@ -1209,7 +1209,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 
 			// CECHO showing you what this item is
-			if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
+			if (player == &players[displayplayers[0]] || G_IsSpecialStage(gamemap))
 			{
 				HU_SetCEchoFlags(V_AUTOFADEOUT);
 				HU_SetCEchoDuration(4);
@@ -1231,7 +1231,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 
 			// CECHO showing you what this item is
-			if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
+			if (player == &players[displayplayers[0]] || G_IsSpecialStage(gamemap))
 			{
 				HU_SetCEchoFlags(V_AUTOFADEOUT);
 				HU_SetCEchoDuration(4);
@@ -1263,7 +1263,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 
 			// CECHO showing you what this item is
-			if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
+			if (player == &players[displayplayers[0]] || G_IsSpecialStage(gamemap))
 			{
 				HU_SetCEchoFlags(V_AUTOFADEOUT);
 				HU_SetCEchoDuration(4);
@@ -1293,7 +1293,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 
 			// CECHO showing you what this item is
-			if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
+			if (player == &players[displayplayers[0]] || G_IsSpecialStage(gamemap))
 			{
 				HU_SetCEchoFlags(V_AUTOFADEOUT);
 				HU_SetCEchoDuration(4);
@@ -1321,7 +1321,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 
 			// CECHO showing you what this item is
-			if (player == &players[displayplayer] || G_IsSpecialStage(gamemap))
+			if (player == &players[displayplayers[0]] || G_IsSpecialStage(gamemap))
 			{
 				HU_SetCEchoFlags(V_AUTOFADEOUT);
 				HU_SetCEchoDuration(4);
@@ -1433,7 +1433,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				return;
 			player->powers[pw_shield] |= SH_FIREFLOWER;
 			toucher->color = SKINCOLOR_WHITE;
-			G_GhostAddColor(GHC_FIREFLOWER);
+			G_GhostAddColor(player - players, GHC_FIREFLOWER);
 			break;
 
 // *************** //
@@ -1837,6 +1837,9 @@ void P_CheckTimeLimit(void)
 					}
 				}
 
+				if (playercount > MAXPLAYERS)
+					playercount = MAXPLAYERS;
+
 				//Sort 'em.
 				for (i = 1; i < playercount; i++)
 				{
@@ -2324,17 +2327,17 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source)
 				AM_Stop();
 
 			//added : 22-02-98: recenter view for next life...
-			localaiming = 0;
+			localaiming[0] = 0;
 		}
-		if (target->player == &players[secondarydisplayplayer])
+		if (target->player == &players[displayplayers[1]])
 		{
 			// added : 22-02-98: recenter view for next life...
-			localaiming2 = 0;
+			localaiming[1] = 0;
 		}
-		if (target->player == &players[thirddisplayplayer])
-			localaiming3 = 0;
-		if (target->player == &players[fourthdisplayplayer])
-			localaiming4 = 0;
+		if (target->player == &players[displayplayers[2]])
+			localaiming[2] = 0;
+		if (target->player == &players[displayplayers[3]])
+			localaiming[3] = 0;
 
 		//tag deaths handled differently in suicide cases. Don't count spectators!
 		/*if (G_TagGametype()
@@ -2978,7 +2981,7 @@ void P_RemoveShield(player_t *player)
 		if (!player->powers[pw_super])
 		{
 			player->mo->color = player->skincolor;
-			G_GhostAddColor(GHC_NORMAL);
+			G_GhostAddColor((INT32) (player - players), GHC_NORMAL);
 		}
 	}
 	else if ((player->powers[pw_shield] & SH_NOSTACK) == SH_BOMB) // Give them what's coming to them!
@@ -3409,7 +3412,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 		target->health -= damage;
 
 	if (source && source->player && target)
-		G_GhostAddHit(target);
+		G_GhostAddHit((INT32) (source->player - players), target);
 
 	if (target->health <= 0)
 	{
diff --git a/src/p_local.h b/src/p_local.h
index 1ac613bdb131eacbe093822555bf75b0e21b3e54..0d0ddc89bffac46881ab2b1e9bbcf2098b45b68b 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -22,6 +22,7 @@
 #include "p_tick.h"
 #include "r_defs.h"
 #include "p_maputl.h"
+#include "doomstat.h" // MAXSPLITSCREENPLAYERS
 
 #define FLOATSPEED (FRACUNIT*4)
 
@@ -108,7 +109,7 @@ typedef struct camera_s
 	fixed_t pan;
 } camera_t;
 
-extern camera_t camera, camera2, camera3, camera4;
+extern camera_t camera[MAXSPLITSCREENPLAYERS];
 extern consvar_t cv_cam_dist, cv_cam_still, cv_cam_height;
 extern consvar_t cv_cam_speed, cv_cam_rotate, cv_cam_rotspeed;
 
@@ -137,6 +138,7 @@ boolean P_PlayerInPain(player_t *player);
 void P_DoPlayerPain(player_t *player, mobj_t *source, mobj_t *inflictor);
 void P_ResetPlayer(player_t *player);
 boolean P_IsLocalPlayer(player_t *player);
+boolean P_IsDisplayPlayer(player_t *player);
 boolean P_SpectatorJoinGame(player_t *player);
 
 boolean P_IsObjectInGoop(mobj_t *mo);
@@ -178,7 +180,6 @@ boolean P_LookForEnemies(player_t *player);
 void P_NukeEnemies(mobj_t *inflictor, mobj_t *source, fixed_t radius);
 void P_HomingAttack(mobj_t *source, mobj_t *enemy); /// \todo doesn't belong in p_user
 //boolean P_SuperReady(player_t *player);
-void P_DoJump(player_t *player, boolean soundandstate);
 boolean P_AnalogMove(player_t *player);
 /*boolean P_TransferToNextMare(player_t *player);
 UINT8 P_FindLowestMare(void);*/
@@ -187,8 +188,6 @@ UINT8 P_FindHighestLap(void);
 void P_FindEmerald(void);
 //void P_TransferToAxis(player_t *player, INT32 axisnum);
 boolean P_PlayerMoving(INT32 pnum);
-void P_SpawnThokMobj(player_t *player);
-void P_SpawnSpinMobj(player_t *player, mobjtype_t type);
 void P_Telekinesis(player_t *player, fixed_t thrust, fixed_t range);
 
 void P_PlayLivesJingle(player_t *player);
diff --git a/src/p_map.c b/src/p_map.c
index 256c9cef18e16fcb9ecac19058951804fb13baa1..d9b723650e4149ac3d85b797c514c708be717f54 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -212,16 +212,16 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 		{
 			object->angle = spring->angle;
 
-			if (!demoplayback || P_AnalogMove(object->player))
+			if (!demo.playback || P_AnalogMove(object->player))
 			{
 				if (object->player == &players[consoleplayer])
-					localangle = spring->angle;
-				else if (object->player == &players[secondarydisplayplayer])
-					localangle2 = spring->angle;
-				else if (object->player == &players[thirddisplayplayer])
-					localangle3 = spring->angle;
-				else if (object->player == &players[fourthdisplayplayer])
-					localangle4 = spring->angle;
+					localangle[0] = spring->angle;
+				else if (object->player == &players[displayplayers[1]])
+					localangle[1] = spring->angle;
+				else if (object->player == &players[displayplayers[2]])
+					localangle[2] = spring->angle;
+				else if (object->player == &players[displayplayers[3]])
+					localangle[3] = spring->angle;
 			}
 		}
 
@@ -1076,7 +1076,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				S_StartSound(tmthing, sfx_bsnipe);
 
 			// Player Damage
-			K_SpinPlayer(tmthing->player, thing->target, 0, tmthing, (thing->type == MT_BANANA || thing->type == MT_BANANA_SHIELD));
+			K_SpinPlayer(tmthing->player, thing->target, 0, thing, (thing->type == MT_BANANA || thing->type == MT_BANANA_SHIELD));
 
 			// Other Item Damage
 			if (thing->eflags & MFE_VERTICALFLIP)
@@ -1111,7 +1111,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if (thing->state == &states[S_MINEEXPLOSION1])
 				K_ExplodePlayer(tmthing->player, thing->target, thing);
 			else
-				K_SpinPlayer(tmthing->player, thing->target, 0, tmthing, false);
+				K_SpinPlayer(tmthing->player, thing->target, 0, thing, false);
 
 			return true;
 		}
@@ -1264,16 +1264,16 @@ static boolean PIT_CheckThing(mobj_t *thing)
 
 			thing->angle = tmthing->angle;
 
-			if (!demoplayback || P_AnalogMove(thing->player))
+			if (!demo.playback || P_AnalogMove(thing->player))
 			{
 				if (thing->player == &players[consoleplayer])
-					localangle = thing->angle;
-				else if (thing->player == &players[secondarydisplayplayer])
-					localangle2 = thing->angle;
-				else if (thing->player == &players[thirddisplayplayer])
-					localangle3 = thing->angle;
-				else if (thing->player == &players[fourthdisplayplayer])
-					localangle4 = thing->angle;
+					localangle[0] = thing->angle;
+				else if (thing->player == &players[displayplayers[1]])
+					localangle[1] = thing->angle;
+				else if (thing->player == &players[displayplayers[2]])
+					localangle[2] = thing->angle;
+				else if (thing->player == &players[displayplayers[3]])
+					localangle[3] = thing->angle;
 			}
 
 			return true;
@@ -1580,12 +1580,12 @@ static boolean PIT_CheckThing(mobj_t *thing)
 
 			if (G_BattleGametype())
 			{
-				if (thing->player->kartstuff[k_sneakertimer] && !(tmthing->player->kartstuff[k_sneakertimer]))
+				if (thing->player->kartstuff[k_sneakertimer] && !(tmthing->player->kartstuff[k_sneakertimer]) && !(thing->player->powers[pw_flashing])) // Don't steal bumpers while intangible
 				{
 					K_StealBumper(thing->player, tmthing->player, false);
 					K_SpinPlayer(tmthing->player, thing, 0, tmthing, false);
 				}
-				else if (tmthing->player->kartstuff[k_sneakertimer] && !(thing->player->kartstuff[k_sneakertimer]))
+				else if (tmthing->player->kartstuff[k_sneakertimer] && !(thing->player->kartstuff[k_sneakertimer]) && !(tmthing->player->powers[pw_flashing]))
 				{
 					K_StealBumper(tmthing->player, thing->player, false);
 					K_SpinPlayer(thing->player, tmthing, 0, thing, false);
@@ -2504,41 +2504,46 @@ boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam)
 	subsector_t *s = R_PointInSubsector(x, y);
 	boolean retval = true;
 	boolean itsatwodlevel = false;
+	UINT8 i;
 
 	floatok = false;
 
-	if (twodlevel
-		|| (thiscam == &camera && players[displayplayer].mo && (players[displayplayer].mo->flags2 & MF2_TWOD))
-		|| (thiscam == &camera2 && players[secondarydisplayplayer].mo && (players[secondarydisplayplayer].mo->flags2 & MF2_TWOD))
-		|| (thiscam == &camera3 && players[thirddisplayplayer].mo && (players[thirddisplayplayer].mo->flags2 & MF2_TWOD))
-		|| (thiscam == &camera4 && players[fourthdisplayplayer].mo && (players[fourthdisplayplayer].mo->flags2 & MF2_TWOD)))
+	if (twodlevel)
 		itsatwodlevel = true;
+	else
+	{
+		for (i = 0; i <= splitscreen; i++)
+		{
+			if (thiscam == &camera[i] && players[displayplayers[i]].mo
+				&& (players[displayplayers[i]].mo->flags2 & MF2_TWOD))
+			{
+				itsatwodlevel = true;
+				break;
+			}
+		}
+	}
 
-	if (!itsatwodlevel && players[displayplayer].mo)
+	if (!itsatwodlevel && players[displayplayers[0]].mo)
 	{
 		fixed_t tryx = thiscam->x;
 		fixed_t tryy = thiscam->y;
 
+		for (i = 0; i <= splitscreen; i++)
+		{
 #ifndef NOCLIPCAM
-		if ((thiscam == &camera && (players[displayplayer].pflags & PF_NOCLIP))
-		|| (thiscam == &camera2 && (players[secondarydisplayplayer].pflags & PF_NOCLIP))
-		|| (thiscam == &camera3 && (players[thirddisplayplayer].pflags & PF_NOCLIP))
-		|| (thiscam == &camera4 && (players[fourthdisplayplayer].pflags & PF_NOCLIP))
-		|| (leveltime < introtime))
+			if ((thiscam == &camera[i] && (players[displayplayers[i]].pflags & PF_NOCLIP)) || (leveltime < introtime)) // Noclipping player camera noclips too!!
 #else
-		if ((thiscam == &camera && !(players[displayplayer].pflags & PF_TIMEOVER))
-		|| (thiscam == &camera2 && !(players[secondarydisplayplayer].pflags & PF_TIMEOVER))
-		|| (thiscam == &camera3 && !(players[thirddisplayplayer].pflags & PF_TIMEOVER))
-		|| (thiscam == &camera4 && !(players[fourthdisplayplayer].pflags & PF_TIMEOVER)))
+			if (thiscam == &camera[i] && !(players[displayplayers[i]].pflags & PF_TIMEOVER)) // Time Over should not clip through walls
 #endif
-		{ // Noclipping player camera noclips too!!
-			floatok = true;
-			thiscam->floorz = thiscam->z;
-			thiscam->ceilingz = thiscam->z + thiscam->height;
-			thiscam->x = x;
-			thiscam->y = y;
-			thiscam->subsector = s;
-			return true;
+			{
+				floatok = true;
+				thiscam->floorz = thiscam->z;
+				thiscam->ceilingz = thiscam->z + thiscam->height;
+				thiscam->x = x;
+				thiscam->y = y;
+				thiscam->subsector = s;
+				return true;
+			}
 		}
 
 		do {
diff --git a/src/p_maputl.c b/src/p_maputl.c
index c5a593d3e9ab428447628c6b294c8a067a3f64f2..355c58db85167fb09793eaee650f92e9c21fd970 100644
--- a/src/p_maputl.c
+++ b/src/p_maputl.c
@@ -339,9 +339,9 @@ void P_CameraLineOpening(line_t *linedef)
 		frontceiling = sectors[front->camsec].ceilingheight;
 #ifdef ESLOPE
 		if (sectors[front->camsec].f_slope) // SRB2CBTODO: ESLOPE (sectors[front->heightsec].f_slope)
-			frontfloor = P_GetZAt(sectors[front->camsec].f_slope, camera.x, camera.y);
+			frontfloor = P_GetZAt(sectors[front->camsec].f_slope, camera[0].x, camera[0].y);
 		if (sectors[front->camsec].c_slope)
-			frontceiling = P_GetZAt(sectors[front->camsec].c_slope, camera.x, camera.y);
+			frontceiling = P_GetZAt(sectors[front->camsec].c_slope, camera[0].x, camera[0].y);
 #endif
 
 	}
@@ -351,9 +351,9 @@ void P_CameraLineOpening(line_t *linedef)
 		frontceiling = sectors[front->heightsec].ceilingheight;
 #ifdef ESLOPE
 		if (sectors[front->heightsec].f_slope) // SRB2CBTODO: ESLOPE (sectors[front->heightsec].f_slope)
-			frontfloor = P_GetZAt(sectors[front->heightsec].f_slope, camera.x, camera.y);
+			frontfloor = P_GetZAt(sectors[front->heightsec].f_slope, camera[0].x, camera[0].y);
 		if (sectors[front->heightsec].c_slope)
-			frontceiling = P_GetZAt(sectors[front->heightsec].c_slope, camera.x, camera.y);
+			frontceiling = P_GetZAt(sectors[front->heightsec].c_slope, camera[0].x, camera[0].y);
 #endif
 	}
 	else
@@ -367,9 +367,9 @@ void P_CameraLineOpening(line_t *linedef)
 		backceiling = sectors[back->camsec].ceilingheight;
 #ifdef ESLOPE
 		if (sectors[back->camsec].f_slope) // SRB2CBTODO: ESLOPE (sectors[front->heightsec].f_slope)
-			frontfloor = P_GetZAt(sectors[back->camsec].f_slope, camera.x, camera.y);
+			frontfloor = P_GetZAt(sectors[back->camsec].f_slope, camera[0].x, camera[0].y);
 		if (sectors[back->camsec].c_slope)
-			frontceiling = P_GetZAt(sectors[back->camsec].c_slope, camera.x, camera.y);
+			frontceiling = P_GetZAt(sectors[back->camsec].c_slope, camera[0].x, camera[0].y);
 #endif
 	}
 	else if (back->heightsec >= 0)
@@ -378,9 +378,9 @@ void P_CameraLineOpening(line_t *linedef)
 		backceiling = sectors[back->heightsec].ceilingheight;
 #ifdef ESLOPE
 		if (sectors[back->heightsec].f_slope) // SRB2CBTODO: ESLOPE (sectors[front->heightsec].f_slope)
-			frontfloor = P_GetZAt(sectors[back->heightsec].f_slope, camera.x, camera.y);
+			frontfloor = P_GetZAt(sectors[back->heightsec].f_slope, camera[0].x, camera[0].y);
 		if (sectors[back->heightsec].c_slope)
-			frontceiling = P_GetZAt(sectors[back->heightsec].c_slope, camera.x, camera.y);
+			frontceiling = P_GetZAt(sectors[back->heightsec].c_slope, camera[0].x, camera[0].y);
 #endif
 	}
 	else
diff --git a/src/p_mobj.c b/src/p_mobj.c
index e1ac3f2db08f4592ac76110971e455921dd773a0..f7f2afe3471bdca9c717228423f96c04bdb6f5fb 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -1125,7 +1125,7 @@ static void P_PlayerFlip(mobj_t *mo)
 	if (!mo->player)
 		return;
 
-	G_GhostAddFlip();
+	G_GhostAddFlip((INT32) (mo->player - players));
 	// Flip aiming to match!
 
 	if (mo->player->pflags & PF_NIGHTSMODE) // NiGHTS doesn't use flipcam
@@ -1135,45 +1135,21 @@ static void P_PlayerFlip(mobj_t *mo)
 	}
 	else if (mo->player->pflags & PF_FLIPCAM)
 	{
+		UINT8 i;
+
 		mo->player->aiming = InvAngle(mo->player->aiming);
-		if (mo->player-players == displayplayer)
-		{
-			localaiming = mo->player->aiming;
-			if (camera.chase) {
-				camera.aiming = InvAngle(camera.aiming);
-				camera.z = mo->z - camera.z + mo->z;
-				if (mo->eflags & MFE_VERTICALFLIP)
-					camera.z += FixedMul(20*FRACUNIT, mo->scale);
-			}
-		}
-		else if (mo->player-players == secondarydisplayplayer)
-		{
-			localaiming2 = mo->player->aiming;
-			if (camera2.chase) {
-				camera2.aiming = InvAngle(camera2.aiming);
-				camera2.z = mo->z - camera2.z + mo->z;
-				if (mo->eflags & MFE_VERTICALFLIP)
-					camera2.z += FixedMul(20*FRACUNIT, mo->scale);
-			}
-		}
-		else if (mo->player-players == thirddisplayplayer)
-		{
-			localaiming3 = mo->player->aiming;
-			if (camera3.chase) {
-				camera3.aiming = InvAngle(camera3.aiming);
-				camera3.z = mo->z - camera3.z + mo->z;
-				if (mo->eflags & MFE_VERTICALFLIP)
-					camera3.z += FixedMul(20*FRACUNIT, mo->scale);
-			}
-		}
-		else if (mo->player-players == fourthdisplayplayer)
+
+		for (i = 0; i <= splitscreen; i++)
 		{
-			localaiming4 = mo->player->aiming;
-			if (camera4.chase) {
-				camera4.aiming = InvAngle(camera4.aiming);
-				camera4.z = mo->z - camera4.z + mo->z;
-				if (mo->eflags & MFE_VERTICALFLIP)
-					camera4.z += FixedMul(20*FRACUNIT, mo->scale);
+			if (mo->player-players == displayplayers[i])
+			{
+				localaiming[i] = mo->player->aiming;
+				if (camera[i].chase) {
+					camera[i].aiming = InvAngle(camera[i].aiming);
+					camera[i].z = mo->z - camera[i].z + mo->z;
+					if (mo->eflags & MFE_VERTICALFLIP)
+						camera[i].z += FixedMul(20*FRACUNIT, mo->scale);
+				}
 			}
 		}
 	}
@@ -1932,7 +1908,7 @@ void P_XYMovement(mobj_t *mo)
 	if (mo->type == MT_ORBINAUT || mo->type == MT_JAWZ_DUD || mo->type == MT_JAWZ || mo->type == MT_BALLHOG) //(mo->type == MT_JAWZ && !mo->tracer))
 		return;
 
-	if (mo->player && (mo->player->kartstuff[k_spinouttimer] && !mo->player->kartstuff[k_wipeoutslow]) && mo->player->speed <= mo->player->normalspeed/2)
+	if (mo->player && (mo->player->kartstuff[k_spinouttimer] && !mo->player->kartstuff[k_wipeoutslow]) && mo->player->speed <= K_GetKartSpeed(mo->player, false)/2)
 		return;
 	//}
 
@@ -3546,17 +3522,26 @@ boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled
 {
 	boolean itsatwodlevel = false;
 	postimg_t postimg = postimg_none;
+	UINT8 i;
 
 	// This can happen when joining
 	if (thiscam->subsector == NULL || thiscam->subsector->sector == NULL)
 		return true;
 
-	if (twodlevel
-		|| (thiscam == &camera && players[displayplayer].mo && (players[displayplayer].mo->flags2 & MF2_TWOD))
-		|| (thiscam == &camera2 && players[secondarydisplayplayer].mo && (players[secondarydisplayplayer].mo->flags2 & MF2_TWOD))
-		|| (thiscam == &camera3 && players[thirddisplayplayer].mo && (players[thirddisplayplayer].mo->flags2 & MF2_TWOD))
-		|| (thiscam == &camera4 && players[fourthdisplayplayer].mo && (players[fourthdisplayplayer].mo->flags2 & MF2_TWOD)))
+	if (twodlevel)
 		itsatwodlevel = true;
+	else
+	{
+		for (i = 0; i <= splitscreen; i++)
+		{
+			if (thiscam == &camera[i] && players[displayplayers[i]].mo
+				&& (players[displayplayers[i]].mo->flags2 & MF2_TWOD))
+			{
+				itsatwodlevel = true;
+				break;
+			}
+		}
+	}
 
 	if (encoremode)
 		postimg = postimg_mirror;
@@ -3588,14 +3573,11 @@ boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled
 
 	if (postimg != postimg_none)
 	{
-		if (splitscreen && player == &players[secondarydisplayplayer])
-			postimgtype2 = postimg;
-		else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-			postimgtype3 = postimg;
-		else if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-			postimgtype4 = postimg;
-		else
-			postimgtype = postimg;
+		for (i = 0; i <= splitscreen; i++)
+		{
+			if (player == &players[displayplayers[i]])
+				postimgtype[i] = postimg;
+		}
 	}
 
 	if (thiscam->momx || thiscam->momy)
@@ -3641,11 +3623,11 @@ boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled
 				fixed_t cam_height = cv_cam_height.value;
 				thiscam->z = thiscam->floorz;
 
-				if (player == &players[secondarydisplayplayer])
+				if (player == &players[displayplayers[1]])
 					cam_height = cv_cam2_height.value;
-				if (player == &players[thirddisplayplayer])
+				if (player == &players[displayplayers[2]])
 					cam_height = cv_cam3_height.value;
-				if (player == &players[fourthdisplayplayer])
+				if (player == &players[displayplayers[3]])
 					cam_height = cv_cam4_height.value;
 				if (thiscam->z > player->mo->z + player->mo->height + FixedMul(cam_height*FRACUNIT + 16*FRACUNIT, player->mo->scale))
 				{
@@ -5897,7 +5879,7 @@ void P_SetScale(mobj_t *mobj, fixed_t newscale)
 
 	if (player)
 	{
-		G_GhostAddScale(newscale);
+		G_GhostAddScale((INT32) (player - players), newscale);
 		player->viewheight = FixedMul(FixedDiv(player->viewheight, oldscale), newscale); // Nonono don't calculate viewheight elsewhere, this is the best place for it!
 		player->dashspeed = FixedMul(FixedDiv(player->dashspeed, oldscale), newscale); // Prevents the player from having to re-charge up spindash if the player grew in size
 	}
@@ -6101,12 +6083,12 @@ void P_RunOverlays(void)
 		{
 			angle_t viewingangle;
 
-			if (players[displayplayer].awayviewtics)
-				viewingangle = R_PointToAngle2(mo->target->x, mo->target->y, players[displayplayer].awayviewmobj->x, players[displayplayer].awayviewmobj->y);
-			else if (!camera.chase && players[displayplayer].mo)
-				viewingangle = R_PointToAngle2(mo->target->x, mo->target->y, players[displayplayer].mo->x, players[displayplayer].mo->y);
+			if (players[displayplayers[0]].awayviewtics)
+				viewingangle = R_PointToAngle2(mo->target->x, mo->target->y, players[displayplayers[0]].awayviewmobj->x, players[displayplayers[0]].awayviewmobj->y);
+			else if (!camera[0].chase && players[displayplayers[0]].mo)
+				viewingangle = R_PointToAngle2(mo->target->x, mo->target->y, players[displayplayers[0]].mo->x, players[displayplayers[0]].mo->y);
 			else
-				viewingangle = R_PointToAngle2(mo->target->x, mo->target->y, camera.x, camera.y);
+				viewingangle = R_PointToAngle2(mo->target->x, mo->target->y, camera[0].x, camera[0].y);
 
 			if (!(mo->state->frame & FF_ANIMATE) && mo->state->var1)
 				viewingangle += ANGLE_180;
@@ -6680,7 +6662,7 @@ void P_MobjThinker(mobj_t *mobj)
 				if (mobj->target && mobj->target->health
 					&& mobj->target->player && !mobj->target->player->spectator
 					&& mobj->target->player->health && mobj->target->player->playerstate != PST_DEAD
-					/*&& players[displayplayer].mo && !players[displayplayer].spectator*/)
+					/*&& players[displayplayers[0]].mo && !players[displayplayers[0]].spectator*/)
 				{
 					fixed_t scale = 3*mobj->target->scale;
 					mobj->color = mobj->target->color;
@@ -6688,7 +6670,7 @@ void P_MobjThinker(mobj_t *mobj)
 
 					if ((G_RaceGametype() || mobj->target->player->kartstuff[k_bumper] <= 0)
 #if 1 // Set to 0 to test without needing to host
-						|| ((mobj->target->player == &players[displayplayer]) || P_IsLocalPlayer(mobj->target->player))
+						|| ((mobj->target->player == &players[displayplayers[0]]) || P_IsLocalPlayer(mobj->target->player))
 #endif
 						)
 						mobj->flags2 |= MF2_DONTDRAW;
@@ -6699,10 +6681,10 @@ void P_MobjThinker(mobj_t *mobj)
 
 					mobj->angle = R_PointToAngle(mobj->x, mobj->y) + ANGLE_90; // literally only happened because i wanted to ^L^R the SPR_ITEM's
 
-					if (!splitscreen && players[displayplayer].mo)
+					if (!splitscreen && players[displayplayers[0]].mo)
 					{
-						scale = mobj->target->scale + FixedMul(FixedDiv(abs(P_AproxDistance(players[displayplayer].mo->x-mobj->target->x,
-							players[displayplayer].mo->y-mobj->target->y)), RING_DIST), mobj->target->scale);
+						scale = mobj->target->scale + FixedMul(FixedDiv(abs(P_AproxDistance(players[displayplayers[0]].mo->x-mobj->target->x,
+							players[displayplayers[0]].mo->y-mobj->target->y)), RING_DIST), mobj->target->scale);
 						if (scale > 16*mobj->target->scale)
 							scale = 16*mobj->target->scale;
 					}
@@ -6887,7 +6869,7 @@ void P_MobjThinker(mobj_t *mobj)
 				if (mobj->target && mobj->target->health && mobj->tracer
 					&& mobj->target->player && !mobj->target->player->spectator
 					&& mobj->target->player->health && mobj->target->player->playerstate != PST_DEAD
-					&& players[displayplayer].mo && !players[displayplayer].spectator)
+					&& players[displayplayers[0]].mo && !players[displayplayers[0]].spectator)
 				{
 					fixed_t scale = 3*mobj->target->scale;
 
@@ -6909,8 +6891,8 @@ void P_MobjThinker(mobj_t *mobj)
 
 					if (!splitscreen)
 					{
-						scale = mobj->target->scale + FixedMul(FixedDiv(abs(P_AproxDistance(players[displayplayer].mo->x-mobj->target->x,
-							players[displayplayer].mo->y-mobj->target->y)), RING_DIST), mobj->target->scale);
+						scale = mobj->target->scale + FixedMul(FixedDiv(abs(P_AproxDistance(players[displayplayers[0]].mo->x-mobj->target->x,
+							players[displayplayers[0]].mo->y-mobj->target->y)), RING_DIST), mobj->target->scale);
 						if (scale > 16*mobj->target->scale)
 							scale = 16*mobj->target->scale;
 					}
@@ -8296,12 +8278,12 @@ void P_MobjThinker(mobj_t *mobj)
 				angle_t viewingangle;
 				statenum_t curstate = ((mobj->tics == 1) ? (mobj->state->nextstate) : ((statenum_t)(mobj->state-states)));
 
-				if (players[displayplayer].awayviewtics)
-					viewingangle = R_PointToAngle2(mobj->target->x, mobj->target->y, players[displayplayer].awayviewmobj->x, players[displayplayer].awayviewmobj->y);
-				else if (!camera.chase && players[displayplayer].mo)
-					viewingangle = R_PointToAngle2(mobj->target->x, mobj->target->y, players[displayplayer].mo->x, players[displayplayer].mo->y);
+				if (players[displayplayers[0]].awayviewtics)
+					viewingangle = R_PointToAngle2(mobj->target->x, mobj->target->y, players[displayplayers[0]].awayviewmobj->x, players[displayplayers[0]].awayviewmobj->y);
+				else if (!camera[0].chase && players[displayplayers[0]].mo)
+					viewingangle = R_PointToAngle2(mobj->target->x, mobj->target->y, players[displayplayers[0]].mo->x, players[displayplayers[0]].mo->y);
 				else
-					viewingangle = R_PointToAngle2(mobj->target->x, mobj->target->y, camera.x, camera.y);
+					viewingangle = R_PointToAngle2(mobj->target->x, mobj->target->y, camera[0].x, camera[0].y);
 
 				if (curstate > S_THUNDERSHIELD15)
 					viewingangle += ANGLE_180;
@@ -10562,13 +10544,13 @@ void P_PrecipitationEffects(void)
 
 	// Local effects from here on out!
 	// If we're not in game fully yet, we don't worry about them.
-	if (!playeringame[displayplayer] || !players[displayplayer].mo)
+	if (!playeringame[displayplayers[0]] || !players[displayplayers[0]].mo)
 		return;
 
 	if (sound_disabled)
 		return; // Sound off? D'aw, no fun.
 
-	if (players[displayplayer].mo->subsector->sector->ceilingpic == skyflatnum)
+	if (players[displayplayers[0]].mo->subsector->sector->ceilingpic == skyflatnum)
 		volume = 255; // Sky above? We get it full blast.
 	else
 	{
@@ -10576,17 +10558,17 @@ void P_PrecipitationEffects(void)
 		fixed_t closedist, newdist;
 
 		// Essentially check in a 1024 unit radius of the player for an outdoor area.
-		yl = players[displayplayer].mo->y - 1024*FRACUNIT;
-		yh = players[displayplayer].mo->y + 1024*FRACUNIT;
-		xl = players[displayplayer].mo->x - 1024*FRACUNIT;
-		xh = players[displayplayer].mo->x + 1024*FRACUNIT;
+		yl = players[displayplayers[0]].mo->y - 1024*FRACUNIT;
+		yh = players[displayplayers[0]].mo->y + 1024*FRACUNIT;
+		xl = players[displayplayers[0]].mo->x - 1024*FRACUNIT;
+		xh = players[displayplayers[0]].mo->x + 1024*FRACUNIT;
 		closedist = 2048*FRACUNIT;
 		for (y = yl; y <= yh; y += FRACUNIT*64)
 			for (x = xl; x <= xh; x += FRACUNIT*64)
 			{
 				if (R_PointInSubsector(x, y)->sector->ceilingpic == skyflatnum) // Found the outdoors!
 				{
-					newdist = S_CalculateSoundDistance(players[displayplayer].mo->x, players[displayplayer].mo->y, 0, x, y, 0);
+					newdist = S_CalculateSoundDistance(players[displayplayers[0]].mo->x, players[displayplayers[0]].mo->y, 0, x, y, 0);
 					if (newdist < closedist)
 						closedist = newdist;
 				}
@@ -10601,7 +10583,7 @@ void P_PrecipitationEffects(void)
 		volume = 255;
 
 	if (sounds_rain && (!leveltime || leveltime % 80 == 1))
-		S_StartSoundAtVolume(players[displayplayer].mo, sfx_rainin, volume);
+		S_StartSoundAtVolume(players[displayplayers[0]].mo, sfx_rainin, volume);
 
 	if (!sounds_thunder)
 		return;
@@ -10609,7 +10591,7 @@ void P_PrecipitationEffects(void)
 	if (effects_lightning && lightningStrike && volume)
 	{
 		// Large, close thunder sounds to go with our lightning.
-		S_StartSoundAtVolume(players[displayplayer].mo, sfx_litng1 + M_RandomKey(4), volume);
+		S_StartSoundAtVolume(players[displayplayers[0]].mo, sfx_litng1 + M_RandomKey(4), volume);
 	}
 	else if (thunderchance < 20)
 	{
@@ -10617,7 +10599,7 @@ void P_PrecipitationEffects(void)
 		if (volume < 80)
 			volume = 80;
 
-		S_StartSoundAtVolume(players[displayplayer].mo, sfx_athun1 + M_RandomKey(2), volume);
+		S_StartSoundAtVolume(players[displayplayers[0]].mo, sfx_athun1 + M_RandomKey(2), volume);
 	}
 }
 
@@ -10789,7 +10771,8 @@ void P_SpawnPlayer(INT32 playernum)
 	}
 
 	// spawn as spectator determination
-	if (!G_GametypeHasSpectators())
+	if (multiplayer && demo.playback); // Don't mess with spectator values since the demo setup handles them already.
+	else if (!G_GametypeHasSpectators())
 		p->spectator = false;
 	else if (netgame && p->jointime <= 1 && pcount)
 	{
@@ -10923,15 +10906,21 @@ void P_AfterPlayerSpawn(INT32 playernum)
 {
 	player_t *p = &players[playernum];
 	mobj_t *mobj = p->mo;
+	UINT8 i;
 
 	if (playernum == consoleplayer)
-		localangle = mobj->angle;
-	else if (playernum == secondarydisplayplayer)
-		localangle2 = mobj->angle;
-	else if (playernum == thirddisplayplayer)
-		localangle3 = mobj->angle;
-	else if (playernum == fourthdisplayplayer)
-		localangle4 = mobj->angle;
+		localangle[0] = mobj->angle;
+	else if (splitscreen)
+	{
+		for (i = 1; i <= splitscreen; i++)
+		{
+			if (playernum == displayplayers[i])
+			{
+				localangle[i] = mobj->angle;
+				break;
+			}
+		}
+	}
 
 	p->viewheight = 32<<FRACBITS;
 
@@ -10953,25 +10942,13 @@ void P_AfterPlayerSpawn(INT32 playernum)
 
 	SV_SpawnPlayer(playernum, mobj->x, mobj->y, mobj->angle);
 
-	if (camera.chase)
-	{
-		if (displayplayer == playernum)
-			P_ResetCamera(p, &camera);
-	}
-	if (camera2.chase && splitscreen)
-	{
-		if (secondarydisplayplayer == playernum)
-			P_ResetCamera(p, &camera2);
-	}
-	if (camera3.chase && splitscreen > 1)
-	{
-		if (thirddisplayplayer == playernum)
-			P_ResetCamera(p, &camera3);
-	}
-	if (camera4.chase && splitscreen > 2)
+	for (i = 0; i <= splitscreen; i++)
 	{
-		if (fourthdisplayplayer == playernum)
-			P_ResetCamera(p, &camera4);
+		if (camera[i].chase)
+		{
+			if (displayplayers[i] == playernum)
+				P_ResetCamera(p, &camera[i]);
+		}
 	}
 
 	if (CheckForReverseGravity)
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index 34402f1acd6bda3fab455e372081634dd7cc8cef..03fb10d0f4addf38171d963f654f1e9627bdfe7d 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -1336,13 +1336,13 @@ static void Polyobj_rotateThings(polyobj_t *po, vertex_t origin, angle_t delta,
 					if (turnthings == 2 || (turnthings == 1 && !mo->player)) {
 						mo->angle += delta;
 						if (mo->player == &players[consoleplayer])
-							localangle = mo->angle;
-						else if (mo->player == &players[secondarydisplayplayer])
-							localangle2 = mo->angle;
-						else if (mo->player == &players[thirddisplayplayer])
-							localangle3 = mo->angle;
-						else if (mo->player == &players[fourthdisplayplayer])
-							localangle4 = mo->angle;
+							localangle[0] += delta;
+						else if (mo->player == &players[displayplayers[1]])
+							localangle[1] += delta;
+						else if (mo->player == &players[displayplayers[2]])
+							localangle[2] += delta;
+						else if (mo->player == &players[displayplayers[3]])
+							localangle[3] += delta;
 					}
 				}
 			}
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 0061ee029c70feb6e8783dff2d5fe8f56469da2d..7d2e9a307e9de2c9fdcaf6e7c7980db57974eb23 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -265,25 +265,11 @@ static void P_NetArchivePlayers(void)
 		if (flags & AWAYVIEW)
 			WRITEUINT32(save_p, players[i].awayviewmobj->mobjnum);
 
-		WRITEUINT8(save_p, players[i].charability);
-		WRITEUINT8(save_p, players[i].charability2);
 		WRITEUINT32(save_p, players[i].charflags);
-		WRITEUINT32(save_p, (UINT32)players[i].thokitem);
-		WRITEUINT32(save_p, (UINT32)players[i].spinitem);
-		WRITEUINT32(save_p, (UINT32)players[i].revitem);
-		WRITEFIXED(save_p, players[i].actionspd);
-		WRITEFIXED(save_p, players[i].mindash);
-		WRITEFIXED(save_p, players[i].maxdash);
 		// SRB2kart
 		WRITEUINT8(save_p, players[i].kartspeed);
 		WRITEUINT8(save_p, players[i].kartweight);
 		//
-		WRITEFIXED(save_p, players[i].normalspeed);
-		WRITEFIXED(save_p, players[i].runspeed);
-		WRITEUINT8(save_p, players[i].thrustfactor);
-		WRITEUINT8(save_p, players[i].accelstart);
-		WRITEUINT8(save_p, players[i].acceleration);
-		WRITEFIXED(save_p, players[i].jumpfactor);
 
 		for (j = 0; j < MAXPREDICTTICS; j++)
 		{
@@ -447,25 +433,11 @@ static void P_NetUnArchivePlayers(void)
 		players[i].viewheight = 32<<FRACBITS;
 
 		//SetPlayerSkinByNum(i, players[i].skin);
-		players[i].charability = READUINT8(save_p);
-		players[i].charability2 = READUINT8(save_p);
 		players[i].charflags = READUINT32(save_p);
-		players[i].thokitem = (mobjtype_t)READUINT32(save_p);
-		players[i].spinitem = (mobjtype_t)READUINT32(save_p);
-		players[i].revitem = (mobjtype_t)READUINT32(save_p);
-		players[i].actionspd = READFIXED(save_p);
-		players[i].mindash = READFIXED(save_p);
-		players[i].maxdash = READFIXED(save_p);
 		// SRB2kart
 		players[i].kartspeed = READUINT8(save_p);
 		players[i].kartweight = READUINT8(save_p);
 		//
-		players[i].normalspeed = READFIXED(save_p);
-		players[i].runspeed = READFIXED(save_p);
-		players[i].thrustfactor = READUINT8(save_p);
-		players[i].accelstart = READUINT8(save_p);
-		players[i].acceleration = READUINT8(save_p);
-		players[i].jumpfactor = READFIXED(save_p);
 
 		for (j = 0; j < MAXPREDICTTICS; j++)
 		{
@@ -2106,13 +2078,13 @@ static void LoadMobjThinker(actionf_p1 thinker)
 		mobj->player->mo = mobj;
 		// added for angle prediction
 		if (consoleplayer == i)
-			localangle = mobj->angle;
-		if (secondarydisplayplayer == i)
-			localangle2 = mobj->angle;
-		if (thirddisplayplayer == i)
-			localangle3 = mobj->angle;
-		if (fourthdisplayplayer == i)
-			localangle4 = mobj->angle;
+			localangle[0] = mobj->angle;
+		if (displayplayers[1] == i)
+			localangle[1] = mobj->angle;
+		if (displayplayers[2] == i)
+			localangle[2] = mobj->angle;
+		if (displayplayers[3] == i)
+			localangle[3] = mobj->angle;
 	}
 	if (diff & MD_MOVEDIR)
 		mobj->movedir = READANGLE(save_p);
@@ -3451,7 +3423,7 @@ void P_SaveNetGame(void)
 	mobj_t *mobj;
 	INT32 i = 1; // don't start from 0, it'd be confused with a blank pointer otherwise
 
-	CV_SaveNetVars(&save_p);
+	CV_SaveNetVars(&save_p, false);
 	P_NetArchiveMisc();
 
 	// Assign the mobjnumber for pointer tracking
diff --git a/src/p_setup.c b/src/p_setup.c
index ba4554e6815b5bd8de20f95c80a9bcdf3520c791..bf13971b3cc3e5a0fcd39d91dec30ab7ccf4dfb7 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -65,6 +65,10 @@
 #include "lua_script.h"
 #include "lua_hook.h"
 
+#if !defined (UNDER_CE)
+#include <time.h>
+#endif
+
 #if defined (_WIN32) || defined (_WIN32_WCE)
 #include <malloc.h>
 #include <math.h>
@@ -193,6 +197,12 @@ static void P_ClearSingleMapHeaderInfo(INT16 i)
 	mapheaderinfo[num]->musname[6] = 0;
 	DEH_WriteUndoline("MUSICTRACK", va("%d", mapheaderinfo[num]->mustrack), UNDO_NONE);
 	mapheaderinfo[num]->mustrack = 0;
+	DEH_WriteUndoline("MUSICPOS", va("%d", mapheaderinfo[num]->muspos), UNDO_NONE);
+	mapheaderinfo[num]->muspos = 0;
+	DEH_WriteUndoline("MUSICINTERFADEOUT", va("%d", mapheaderinfo[num]->musinterfadeout), UNDO_NONE);
+	mapheaderinfo[num]->musinterfadeout = 0;
+	DEH_WriteUndoline("MUSICINTER", mapheaderinfo[num]->musintername, UNDO_NONE);
+	mapheaderinfo[num]->musintername[0] = '\0';
 	DEH_WriteUndoline("FORCECHARACTER", va("%d", mapheaderinfo[num]->forcecharacter), UNDO_NONE);
 	mapheaderinfo[num]->forcecharacter[0] = '\0';
 	DEH_WriteUndoline("WEATHER", va("%d", mapheaderinfo[num]->weather), UNDO_NONE);
@@ -1544,19 +1554,33 @@ static void P_LoadRawSideDefs2(void *data)
 				{
 					M_Memcpy(process,msd->bottomtexture,8);
 					process[8] = '\0';
-					sd->bottomtexture = get_number(process)-1;
+					sd->bottomtexture = get_number(process);
 				}
-				M_Memcpy(process,msd->toptexture,8);
-				process[8] = '\0';
-				sd->text = Z_Malloc(7, PU_LEVEL, NULL);
-
-				// If they type in O_ or D_ and their music name, just shrug,
-				// then copy the rest instead.
-				if ((process[0] == 'O' || process[0] == 'D') && process[7])
-					M_Memcpy(sd->text, process+2, 6);
-				else // Assume it's a proper music name.
-					M_Memcpy(sd->text, process, 6);
-				sd->text[6] = 0;
+
+				if (!(msd->midtexture[0] == '-' && msd->midtexture[1] == '\0') || msd->midtexture[1] != '\0')
+				{
+					M_Memcpy(process,msd->midtexture,8);
+					process[8] = '\0';
+					sd->midtexture = get_number(process);
+				}
+
+				// always process if back sidedef, because we need that - symbol
+ 				sd->text = Z_Malloc(7, PU_LEVEL, NULL);
+				if (i == 1 || msd->toptexture[0] != '-' || msd->toptexture[1] != '\0')
+				{
+					M_Memcpy(process,msd->toptexture,8);
+					process[8] = '\0';
+
+					// If they type in O_ or D_ and their music name, just shrug,
+					// then copy the rest instead.
+					if ((process[0] == 'O' || process[0] == 'D') && process[7])
+						M_Memcpy(sd->text, process+2, 6);
+					else // Assume it's a proper music name.
+						M_Memcpy(sd->text, process, 6);
+					sd->text[6] = 0;
+				}
+				else
+					sd->text[0] = 0;
 				break;
 			}
 
@@ -2265,7 +2289,7 @@ static void P_LevelInitStuff(void)
 
 	leveltime = 0;
 
-	localaiming = localaiming2 = localaiming3 = localaiming4 = 0;
+	memset(localaiming, 0, sizeof(localaiming));
 
 	// map object scale
 	mapobjectscale = mapheaderinfo[gamemap-1]->mobj_scale;
@@ -2532,29 +2556,29 @@ static void P_ForceCharacter(const char *forcecharskin)
 	{
 		if (splitscreen)
 		{
-			SetPlayerSkin(secondarydisplayplayer, forcecharskin);
-			if ((unsigned)cv_playercolor2.value != skins[players[secondarydisplayplayer].skin].prefcolor && !modeattacking)
+			SetPlayerSkin(displayplayers[1], forcecharskin);
+			if ((unsigned)cv_playercolor2.value != skins[players[displayplayers[1]].skin].prefcolor && !modeattacking)
 			{
-				CV_StealthSetValue(&cv_playercolor2, skins[players[secondarydisplayplayer].skin].prefcolor);
-				players[secondarydisplayplayer].skincolor = skins[players[secondarydisplayplayer].skin].prefcolor;
+				CV_StealthSetValue(&cv_playercolor2, skins[players[displayplayers[1]].skin].prefcolor);
+				players[displayplayers[1]].skincolor = skins[players[displayplayers[1]].skin].prefcolor;
 			}
 
 			if (splitscreen > 1)
 			{
-				SetPlayerSkin(thirddisplayplayer, forcecharskin);
-				if ((unsigned)cv_playercolor3.value != skins[players[thirddisplayplayer].skin].prefcolor && !modeattacking)
+				SetPlayerSkin(displayplayers[2], forcecharskin);
+				if ((unsigned)cv_playercolor3.value != skins[players[displayplayers[2]].skin].prefcolor && !modeattacking)
 				{
-					CV_StealthSetValue(&cv_playercolor3, skins[players[thirddisplayplayer].skin].prefcolor);
-					players[thirddisplayplayer].skincolor = skins[players[thirddisplayplayer].skin].prefcolor;
+					CV_StealthSetValue(&cv_playercolor3, skins[players[displayplayers[2]].skin].prefcolor);
+					players[displayplayers[2]].skincolor = skins[players[displayplayers[2]].skin].prefcolor;
 				}
 
 				if (splitscreen > 2)
 				{
-					SetPlayerSkin(fourthdisplayplayer, forcecharskin);
-					if ((unsigned)cv_playercolor4.value != skins[players[fourthdisplayplayer].skin].prefcolor && !modeattacking)
+					SetPlayerSkin(displayplayers[3], forcecharskin);
+					if ((unsigned)cv_playercolor4.value != skins[players[displayplayers[3]].skin].prefcolor && !modeattacking)
 					{
-						CV_StealthSetValue(&cv_playercolor4, skins[players[fourthdisplayplayer].skin].prefcolor);
-						players[fourthdisplayplayer].skincolor = skins[players[fourthdisplayplayer].skin].prefcolor;
+						CV_StealthSetValue(&cv_playercolor4, skins[players[displayplayers[3]].skin].prefcolor);
+						players[displayplayers[3]].skincolor = skins[players[displayplayers[3]].skin].prefcolor;
 					}
 				}
 			}
@@ -2725,7 +2749,7 @@ static boolean P_CanSave(void)
 	if ((cursaveslot < 0) // Playing without saving
 		|| (modifiedgame && !savemoddata) // Game is modified
 		|| (netgame || multiplayer) // Not in single-player
-		|| (demoplayback || demorecording || metalrecording) // Currently in demo
+		|| (demo.playback || demo.recording || metalrecording) // Currently in demo
 		|| (players[consoleplayer].lives <= 0) // Completely dead
 		|| (modeattacking || ultimatemode || G_IsSpecialStage(gamemap))) // Specialized instances
 		return false;
@@ -2789,7 +2813,8 @@ boolean P_SetupLevel(boolean skipprecip)
 
 	P_LevelInitStuff();
 
-	postimgtype = postimgtype2 = postimgtype3 = postimgtype4 = postimg_none;
+	for (i = 0; i <= splitscreen; i++)
+		postimgtype[i] = postimg_none;
 
 	if (mapheaderinfo[gamemap-1]->forcecharacter[0] != '\0'
 	&& atoi(mapheaderinfo[gamemap-1]->forcecharacter) != 255)
@@ -2825,7 +2850,7 @@ boolean P_SetupLevel(boolean skipprecip)
 
 	// Encore mode fade to pink to white
 	// This is handled BEFORE sounds are stopped.
-	if (rendermode != render_none && encoremode && !prevencoremode)
+	if (rendermode != render_none && encoremode && !prevencoremode && !demo.rewinding)
 	{
 		tic_t locstarttime, endtime, nowtime;
 
@@ -2877,7 +2902,7 @@ boolean P_SetupLevel(boolean skipprecip)
 
 	// Let's fade to white here
 	// But only if we didn't do the encore startup wipe
-	if (rendermode != render_none && !ranspecialwipe)
+	if (rendermode != render_none && !ranspecialwipe && !demo.rewinding)
 	{
 		F_WipeStartScreen();
 		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
@@ -3109,9 +3134,9 @@ boolean P_SetupLevel(boolean skipprecip)
 			}
 		}
 
-	if (modeattacking == ATTACKING_RECORD && !demoplayback)
+	if (modeattacking == ATTACKING_RECORD && !demo.playback)
 		P_LoadRecordGhosts();
-	/*else if (modeattacking == ATTACKING_NIGHTS && !demoplayback)
+	/*else if (modeattacking == ATTACKING_NIGHTS && !demo.playback)
 		P_LoadNightsGhosts();*/
 
 	if (G_TagGametype())
@@ -3159,25 +3184,25 @@ boolean P_SetupLevel(boolean skipprecip)
 			? cv_basenumlaps.value
 			: mapheaderinfo[gamemap - 1]->numlaps);
 
+	// Start recording replay in multiplayer with a temp filename
+	//@TODO I'd like to fix dedis crashing when recording replays for the future too...
+	if (!demo.playback && multiplayer && !dedicated) {
+		static char buf[256];
+		sprintf(buf, "replay"PATHSEP"online"PATHSEP"%d-%s", (int) (time(NULL)), G_BuildMapName(gamemap));
+
+		I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
+		I_mkdir(va("%s"PATHSEP"replay"PATHSEP"online", srb2home), 0755);
+		G_RecordDemo(buf);
+	}
+
 	// ===========
 	// landing point for netgames.
 	netgameskip:
 
 	if (!dedicated)
 	{
-		P_SetupCamera(displayplayer, &camera);
-		if (splitscreen)
-		{
-			P_SetupCamera(secondarydisplayplayer, &camera2);
-			if (splitscreen > 1)
-			{
-				P_SetupCamera(thirddisplayplayer, &camera3);
-				if (splitscreen > 2)
-				{
-					P_SetupCamera(fourthdisplayplayer, &camera4);
-				}
-			}
-		}
+		for (i = 0; i <= splitscreen; i++)
+			P_SetupCamera(displayplayers[i], &camera[i]);
 
 		// Salt: CV_ClearChangedFlags() messes with your settings :(
 		/*if (!cv_cam_height.changed)
@@ -3218,7 +3243,7 @@ boolean P_SetupLevel(boolean skipprecip)
 		/*if (rendermode != render_none)
 			CV_Set(&cv_fov, cv_fov.defaultvalue);*/
 
-		displayplayer = consoleplayer; // Start with your OWN view, please!
+		displayplayers[0] = consoleplayer; // Start with your OWN view, please!
 	}
 
 	/*if (cv_useranalog.value)
@@ -3297,7 +3322,10 @@ boolean P_SetupLevel(boolean skipprecip)
 		savedata.lives = 0;
 	}
 
-	skyVisible = skyVisible1 = skyVisible2 = skyVisible3 = skyVisible4 = true; // assume the skybox is visible on level load.
+	// assume the skybox is visible on level load.
+	skyVisible = true;
+	memset(skyVisiblePerPlayer, true, sizeof(skyVisiblePerPlayer));
+
 	if (loadprecip) // uglier hack
 	{ // to make a newly loaded level start on the second frame.
 		INT32 buf = gametic % BACKUPTICS;
diff --git a/src/p_slopes.c b/src/p_slopes.c
index c6416b75a24554d626b3ce3ccd82b923cd8499e9..76af7bfde86d8d372f032ac55ae618f80e9ad075 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -264,7 +264,7 @@ void P_SpawnSlope_Line(int linenum)
 
 	if(!line->frontsector || !line->backsector)
 	{
-		CONS_Printf("P_SpawnSlope_Line used on a line without two sides.\n");
+		CONS_Debug(DBG_SETUP, "P_SpawnSlope_Line used on a line without two sides. (line number %i)\n", linenum);
 		return;
 	}
 
diff --git a/src/p_spec.c b/src/p_spec.c
index 67bb74720aa3a5edd3d2c7858f7eb4c43ac01a31..a08bdbc34b263160181a7b3d86e9755a663504e1 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2243,7 +2243,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 	I_Assert(!mo || !P_MobjWasRemoved(mo)); // If mo is there, mo must be valid!
 
 	if (mo && mo->player && botingame)
-		bot = players[secondarydisplayplayer].mo;
+		bot = players[displayplayers[1]].mo;
 
 	// note: only commands with linedef types >= 400 && < 500 can be used
 	switch (line->special)
@@ -2381,35 +2381,21 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 					if (mo->player)
 					{
+						UINT8 i;
+
 						if (bot) // This might put poor Tails in a wall if he's too far behind! D: But okay, whatever! >:3
 							P_TeleportMove(bot, bot->x + x, bot->y + y, bot->z + z);
-						if (splitscreen > 2 && mo->player == &players[fourthdisplayplayer] && camera4.chase)
-						{
-							camera4.x += x;
-							camera4.y += y;
-							camera4.z += z;
-							camera4.subsector = R_PointInSubsector(camera4.x, camera4.y);
-						}
-						else if (splitscreen > 1 && mo->player == &players[thirddisplayplayer] && camera3.chase)
-						{
-							camera3.x += x;
-							camera3.y += y;
-							camera3.z += z;
-							camera3.subsector = R_PointInSubsector(camera3.x, camera3.y);
-						}
-						else if (splitscreen && mo->player == &players[secondarydisplayplayer] && camera2.chase)
-						{
-							camera2.x += x;
-							camera2.y += y;
-							camera2.z += z;
-							camera2.subsector = R_PointInSubsector(camera2.x, camera2.y);
-						}
-						else if (camera.chase && mo->player == &players[displayplayer])
+
+						for (i = 0; i <= splitscreen; i++)
 						{
-							camera.x += x;
-							camera.y += y;
-							camera.z += z;
-							camera.subsector = R_PointInSubsector(camera.x, camera.y);
+							if (mo->player == &players[displayplayers[i]] && camera[i].chase)
+							{
+								camera[i].x += x;
+								camera[i].y += y;
+								camera[i].z += z;
+								camera[i].subsector = R_PointInSubsector(camera[i].x, camera[i].y);
+								break;
+							}
 						}
 					}
 				}
@@ -2440,18 +2426,71 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			// console player only unless NOCLIMB is set
 			if ((line->flags & ML_NOCLIMB) || (mo && mo->player && P_IsLocalPlayer(mo->player)))
 			{
-				UINT16 tracknum = (UINT16)sides[line->sidenum[0]].bottomtexture;
+				boolean musicsame = (!sides[line->sidenum[0]].text[0] || !strnicmp(sides[line->sidenum[0]].text, S_MusicName(), 7));
+				UINT16 tracknum = (UINT16)max(sides[line->sidenum[0]].bottomtexture, 0);
+				INT32 position = (INT32)max(sides[line->sidenum[0]].midtexture, 0);
+				UINT32 prefadems = (UINT32)max(sides[line->sidenum[0]].textureoffset >> FRACBITS, 0);
+				UINT32 postfadems = (UINT32)max(sides[line->sidenum[0]].rowoffset >> FRACBITS, 0);
+				UINT8 fadetarget = (UINT8)max((line->sidenum[1] != 0xffff) ? sides[line->sidenum[1]].textureoffset >> FRACBITS : 0, 0);
+				INT16 fadesource = (INT16)max((line->sidenum[1] != 0xffff) ? sides[line->sidenum[1]].rowoffset >> FRACBITS : -1, -1);
+
+				// Seek offset from current song position
+				if (line->flags & ML_EFFECT1)
+				{
+					// adjust for loop point if subtracting
+					if (position < 0 && S_GetMusicLength() &&
+						S_GetMusicPosition() > S_GetMusicLoopPoint() &&
+						S_GetMusicPosition() + position < S_GetMusicLoopPoint())
+						position = max(S_GetMusicLength() - (S_GetMusicLoopPoint() - (S_GetMusicPosition() + position)), 0);
+					else
+						position = max(S_GetMusicPosition() + position, 0);
+				}
 
-				strncpy(mapmusname, sides[line->sidenum[0]].text, 7);
-				mapmusname[6] = 0;
+				// Fade current music to target volume (if music won't be changed)
+				if ((line->flags & ML_EFFECT2) && fadetarget && musicsame)
+				{
+					// 0 fadesource means fade from current volume.
+					// meaning that we can't specify volume 0 as the source volume -- this starts at 1.
+					if (!fadesource)
+						fadesource = -1;
+
+					if (!postfadems)
+						S_SetInternalMusicVolume(fadetarget);
+					else
+						S_FadeMusicFromVolume(fadetarget, fadesource, postfadems);
+
+					if (!(line->flags & ML_EFFECT3))
+						S_ShowMusicCredit();
+
+					if (position)
+						S_SetMusicPosition(position);
+				}
+				// Change the music and apply position/fade operations
+				else
+				{
+					strncpy(mapmusname, sides[line->sidenum[0]].text, 7);
+					mapmusname[6] = 0;
+
+					mapmusflags = tracknum & MUSIC_TRACKMASK;
+					if (!(line->flags & ML_BLOCKMONSTERS))
+						mapmusflags |= MUSIC_RELOADRESET;
+					if (line->flags & ML_BOUNCY)
+						mapmusflags |= MUSIC_FORCERESET;
 
-				mapmusflags = tracknum & MUSIC_TRACKMASK;
-				if (!(line->flags & ML_BLOCKMONSTERS))
-					mapmusflags |= MUSIC_RELOADRESET;
+					mapmusposition = position;
 
-				S_ChangeMusic(mapmusname, mapmusflags, !(line->flags & ML_EFFECT4));
-				if (!(line->flags & ML_EFFECT3))
-					S_ShowMusicCredit();
+					S_ChangeMusicEx(mapmusname, mapmusflags, !(line->flags & ML_EFFECT4), position,
+						!(line->flags & ML_EFFECT2) ? prefadems : 0,
+						!(line->flags & ML_EFFECT2) ? postfadems : 0);
+
+					if ((line->flags & ML_EFFECT2) && fadetarget)
+					{
+						if (!postfadems)
+							S_SetInternalMusicVolume(fadetarget);
+						else
+							S_FadeMusicFromVolume(fadetarget, fadesource, postfadems);
+					}
+				}
 
 				// Except, you can use the ML_BLOCKMONSTERS flag to change this behavior.
 				// if (mapmusflags & MUSIC_RELOADRESET) then it will reset the music in G_PlayerReborn.
@@ -2515,8 +2554,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (line->flags & ML_NOCLIMB)
 				{
 					// play the sound from nowhere, but only if display player triggered it
-					if (mo && mo->player && (mo->player == &players[displayplayer] || mo->player == &players[secondarydisplayplayer]
-						|| mo->player == &players[thirddisplayplayer] || mo->player == &players[fourthdisplayplayer]))
+					if (mo && mo->player && P_IsDisplayPlayer(mo->player))
 						S_StartSound(NULL, sfxnum);
 				}
 				else if (line->flags & ML_EFFECT4)
@@ -3834,16 +3872,16 @@ DoneSection2:
 				if (player->mo->scale > mapobjectscale)
 					linespeed = FixedMul(linespeed, mapobjectscale + (player->mo->scale - mapobjectscale));
 
-				if (!demoplayback || P_AnalogMove(player))
+				if (!demo.playback || P_AnalogMove(player))
 				{
 					if (player == &players[consoleplayer])
-						localangle = player->mo->angle;
-					else if (player == &players[secondarydisplayplayer])
-						localangle2 = player->mo->angle;
-					else if (player == &players[thirddisplayplayer])
-						localangle3 = player->mo->angle;
-					else if (player == &players[fourthdisplayplayer])
-						localangle4 = player->mo->angle;
+						localangle[0] = player->mo->angle;
+					else if (player == &players[displayplayers[1]])
+						localangle[1] = player->mo->angle;
+					else if (player == &players[displayplayers[2]])
+						localangle[2] = player->mo->angle;
+					else if (player == &players[displayplayers[3]])
+						localangle[3] = player->mo->angle;
 				}
 
 				if (!(lines[i].flags & ML_EFFECT4))
@@ -7841,44 +7879,44 @@ void T_Pusher(pusher_t *p)
 				thing->player->pflags |= PF_SLIDING;
 				thing->angle = R_PointToAngle2 (0, 0, xspeed<<(FRACBITS-PUSH_FACTOR), yspeed<<(FRACBITS-PUSH_FACTOR));
 
-				if (!demoplayback || P_AnalogMove(thing->player))
+				if (!demo.playback || P_AnalogMove(thing->player))
 				{
 					if (thing->player == &players[consoleplayer])
 					{
-						if (thing->angle - localangle > ANGLE_180)
-							localangle -= (localangle - thing->angle) / 8;
+						if (thing->angle - localangle[0] > ANGLE_180)
+							localangle[0] -= (localangle[0] - thing->angle) / 8;
 						else
-							localangle += (thing->angle - localangle) / 8;
+							localangle[0] += (thing->angle - localangle[0]) / 8;
 					}
-					else if (thing->player == &players[secondarydisplayplayer])
+					else if (thing->player == &players[displayplayers[1]])
 					{
-						if (thing->angle - localangle2 > ANGLE_180)
-							localangle2 -= (localangle2 - thing->angle) / 8;
+						if (thing->angle - localangle[1] > ANGLE_180)
+							localangle[1] -= (localangle[1] - thing->angle) / 8;
 						else
-							localangle2 += (thing->angle - localangle2) / 8;
+							localangle[1] += (thing->angle - localangle[1]) / 8;
 					}
-					else if (thing->player == &players[thirddisplayplayer])
+					else if (thing->player == &players[displayplayers[2]])
 					{
-						if (thing->angle - localangle3 > ANGLE_180)
-							localangle3 -= (localangle3 - thing->angle) / 8;
+						if (thing->angle - localangle[2] > ANGLE_180)
+							localangle[2] -= (localangle[2] - thing->angle) / 8;
 						else
-							localangle3 += (thing->angle - localangle3) / 8;
+							localangle[2] += (thing->angle - localangle[2]) / 8;
 					}
-					else if (thing->player == &players[fourthdisplayplayer])
+					else if (thing->player == &players[displayplayers[3]])
 					{
-						if (thing->angle - localangle4 > ANGLE_180)
-							localangle4 -= (localangle4 - thing->angle) / 8;
+						if (thing->angle - localangle[3] > ANGLE_180)
+							localangle[3] -= (localangle[3] - thing->angle) / 8;
 						else
-							localangle4 += (thing->angle - localangle4) / 8;
+							localangle[3] += (thing->angle - localangle[3]) / 8;
 					}
 					/*if (thing->player == &players[consoleplayer])
-						localangle = thing->angle;
-					else if (thing->player == &players[secondarydisplayplayer])
-						localangle2 = thing->angle;
-					else if (thing->player == &players[thirddisplayplayer])
-						localangle3 = thing->angle;
-					else if (thing->player == &players[fourthdisplayplayer])
-						localangle4 = thing->angle;*/
+						localangle[0] = thing->angle;
+					else if (thing->player == &players[displayplayers[1]])
+						localangle[1] = thing->angle;
+					else if (thing->player == &players[displayplayers[2]])
+						localangle[2] = thing->angle;
+					else if (thing->player == &players[displayplayers[3]])
+						localangle[3] = thing->angle;*/
 				}
 			}
 
diff --git a/src/p_telept.c b/src/p_telept.c
index 24e201fc1222e9e194859371f34be7d57b4edbf4..74f9d462c8591082fd8048f4413c235503b15a24 100644
--- a/src/p_telept.c
+++ b/src/p_telept.c
@@ -36,6 +36,7 @@ void P_MixUp(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle,
 			INT32 flags2)
 {
 	const INT32 takeflags2 = MF2_TWOD|MF2_OBJECTFLIP;
+	UINT8 i;
 
 	// the move is ok,
 	// so link the thing into its new position
@@ -64,23 +65,25 @@ void P_MixUp(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle,
 
 		// absolute angle position
 		if (thing == players[consoleplayer].mo)
-			localangle = angle;
-		if (thing == players[secondarydisplayplayer].mo)
-			localangle2 = angle;
-		if (thing == players[thirddisplayplayer].mo)
-			localangle3 = angle;
-		if (thing == players[fourthdisplayplayer].mo)
-			localangle4 = angle;
+			localangle[0] = angle;
+		else if (splitscreen)
+		{
+			for (i = 1; i <= splitscreen; i++)
+			{
+				if (thing == players[displayplayers[i]].mo)
+				{
+					localangle[i] = angle;
+					break;
+				}
+			}
+		}
 
 		// move chasecam at new player location
-		if (splitscreen > 2 && camera4.chase && thing->player == &players[fourthdisplayplayer])
-			P_ResetCamera(thing->player, &camera4);
-		else if (splitscreen > 1 && camera3.chase && thing->player == &players[thirddisplayplayer])
-			P_ResetCamera(thing->player, &camera3);
-		else if (splitscreen && camera2.chase && thing->player == &players[secondarydisplayplayer])
-			P_ResetCamera(thing->player, &camera2);
-		else if (camera.chase && thing->player == &players[displayplayer])
-			P_ResetCamera(thing->player, &camera);
+		for (i = 0; i <= splitscreen; i++)
+		{
+			if (thing->player == &players[displayplayers[i]] && camera[i].chase)
+				P_ResetCamera(thing->player, &camera[i]);
+		}
 
 		// don't run in place after a teleport
 		thing->player->cmomx = thing->player->cmomy = 0;
@@ -123,6 +126,8 @@ void P_MixUp(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle,
 */
 boolean P_Teleport(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle, boolean flash, boolean dontstopmove)
 {
+	UINT8 i;
+
 	if (!P_TeleportMove(thing, x, y, z))
 		return false;
 
@@ -144,24 +149,26 @@ boolean P_Teleport(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle
 			thing->reactiontime = TICRATE/2; // don't move for about half a second
 
 		// absolute angle position
-		if (thing->player == &players[consoleplayer])
-			localangle = angle;
-		if (thing->player == &players[secondarydisplayplayer])
-			localangle2 = angle;
-		if (thing->player == &players[thirddisplayplayer])
-			localangle3 = angle;
-		if (thing->player == &players[fourthdisplayplayer])
-			localangle4 = angle;
+		if (thing == players[consoleplayer].mo)
+			localangle[0] = angle;
+		else if (splitscreen)
+		{
+			for (i = 1; i <= splitscreen; i++)
+			{
+				if (thing == players[displayplayers[i]].mo)
+				{
+					localangle[i] = angle;
+					break;
+				}
+			}
+		}
 
 		// move chasecam at new player location
-		if (splitscreen > 2 && camera4.chase && thing->player == &players[fourthdisplayplayer])
-			P_ResetCamera(thing->player, &camera4);
-		else if (splitscreen > 1 && camera3.chase && thing->player == &players[thirddisplayplayer])
-			P_ResetCamera(thing->player, &camera3);
-		else if (splitscreen && camera2.chase && thing->player == &players[secondarydisplayplayer])
-			P_ResetCamera(thing->player, &camera2);
-		else if (camera.chase && thing->player == &players[displayplayer])
-			P_ResetCamera(thing->player, &camera);
+		for (i = 0; i <= splitscreen; i++)
+		{
+			if (thing->player == &players[displayplayers[i]] && camera[i].chase)
+				P_ResetCamera(thing->player, &camera[i]);
+		}
 
 		// don't run in place after a teleport
 		if (!dontstopmove)
diff --git a/src/p_tick.c b/src/p_tick.c
index 85eaea9bab0dbae633d598904ec0fbc875f719c3..2502c7213f46a1a09e0250111d3d737e0e55e785 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -13,6 +13,7 @@
 
 #include "doomstat.h"
 #include "g_game.h"
+#include "g_input.h"
 #include "p_local.h"
 #include "z_zone.h"
 #include "s_sound.h"
@@ -582,7 +583,7 @@ void P_Ticker(boolean run)
 		{
 			P_MapStart();
 			OP_ObjectplaceMovement(&players[0]);
-			P_MoveChaseCamera(&players[0], &camera, false);
+			P_MoveChaseCamera(&players[0], &camera[0], false);
 			P_MapEnd();
 			return;
 		}
@@ -590,18 +591,60 @@ void P_Ticker(boolean run)
 
 	// Check for pause or menu up in single player
 	if (paused || P_AutoPause())
+	{
+		if (demo.rewinding && leveltime > 0)
+		{
+			leveltime = (leveltime-1) & ~3;
+			G_PreviewRewind(leveltime);
+		}
+
 		return;
+	}
 
-	postimgtype = postimgtype2 = postimgtype3 = postimgtype4 = postimg_none;
+	for (i = 0; i <= splitscreen; i++)
+		postimgtype[i] = postimg_none;
 
 	P_MapStart();
 
 	if (run)
 	{
-		if (demorecording)
-			G_WriteDemoTiccmd(&players[consoleplayer].cmd, 0);
-		if (demoplayback)
-			G_ReadDemoTiccmd(&players[consoleplayer].cmd, 0);
+		if (demo.recording)
+		{
+			G_WriteDemoExtraData();
+			for (i = 0; i < MAXPLAYERS; i++)
+				if (playeringame[i])
+					G_WriteDemoTiccmd(&players[i].cmd, i);
+		}
+		if (demo.playback)
+		{
+
+#ifdef DEMO_COMPAT_100
+			if (demo.version == 0x0001)
+			{
+				G_ReadDemoTiccmd(&players[consoleplayer].cmd, 0);
+			}
+			else
+			{
+#endif
+				G_ReadDemoExtraData();
+				for (i = 0; i < MAXPLAYERS; i++)
+					if (playeringame[i])
+					{
+						//@TODO all this throwdir stuff shouldn't be here! But it's added to maintain 1.0.4 compat for now...
+						// Remove for 1.1!
+						if (players[i].cmd.buttons & BT_FORWARD)
+							players[i].kartstuff[k_throwdir] = 1;
+						else if (players[i].cmd.buttons & BT_BACKWARD)
+							players[i].kartstuff[k_throwdir] = -1;
+						else
+							players[i].kartstuff[k_throwdir] = 0;
+
+						G_ReadDemoTiccmd(&players[i].cmd, i);
+					}
+#ifdef DEMO_COMPAT_100
+			}
+#endif
+		}
 
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
@@ -609,7 +652,7 @@ void P_Ticker(boolean run)
 	}
 
 	// Keep track of how long they've been playing!
-	if (!demoplayback) // Don't increment if a demo is playing.
+	if (!demo.playback) // Don't increment if a demo is playing.
 		totalplaytime++;
 
 	/*if (!useNightsSS && G_IsSpecialStage(gamemap))
@@ -705,10 +748,25 @@ void P_Ticker(boolean run)
 			G_ReadMetalTic(metalplayback);
 		if (metalrecording)
 			G_WriteMetalTic(players[consoleplayer].mo);
-		if (demorecording)
-			G_WriteGhostTic(players[consoleplayer].mo);
-		if (demoplayback) // Use Ghost data for consistency checks.
-			G_ConsGhostTic();
+
+		if (demo.recording)
+		{
+			G_WriteAllGhostTics();
+
+			if (cv_recordmultiplayerdemos.value && (demo.savemode == DSM_NOTSAVING || demo.savemode == DSM_WILLAUTOSAVE))
+				if (demo.savebutton && demo.savebutton + 3*TICRATE < leveltime && InputDown(gc_lookback, 1))
+					demo.savemode = DSM_TITLEENTRY;
+		}
+		else if (demo.playback) // Use Ghost data for consistency checks.
+		{
+#ifdef DEMO_COMPAT_100
+			if (demo.version == 0x0001)
+				G_ConsGhostTic(0);
+			else
+#endif
+			G_ConsAllGhostTics();
+		}
+
 		if (modeattacking)
 			G_GhostTicker();
 
@@ -719,17 +777,17 @@ void P_Ticker(boolean run)
 	}
 
 	// Always move the camera.
-	if (camera.chase)
-		P_MoveChaseCamera(&players[displayplayer], &camera, false);
-	if (splitscreen && camera2.chase)
-		P_MoveChaseCamera(&players[secondarydisplayplayer], &camera2, false);
-	if (splitscreen > 1 && camera3.chase)
-		P_MoveChaseCamera(&players[thirddisplayplayer], &camera3, false);
-	if (splitscreen > 2 && camera4.chase)
-		P_MoveChaseCamera(&players[fourthdisplayplayer], &camera4, false);
+	for (i = 0; i <= splitscreen; i++)
+	{
+		if (camera[i].chase)
+			P_MoveChaseCamera(&players[displayplayers[i]], &camera[i], false);
+	}
 
 	P_MapEnd();
 
+	if (demo.playback)
+		G_StoreRewindInfo();
+
 //	Z_CheckMemCleanup();
 }
 
@@ -739,7 +797,8 @@ void P_PreTicker(INT32 frames)
 	INT32 i,framecnt;
 	ticcmd_t temptic;
 
-	postimgtype = postimgtype2 = postimgtype3 = postimgtype4 = postimg_none;
+	for (i = 0; i <= splitscreen; i++)
+		postimgtype[i] = postimg_none;
 
 	for (framecnt = 0; framecnt < frames; ++framecnt)
 	{
diff --git a/src/p_user.c b/src/p_user.c
index ebc525c4f0d8fd8353a34a39fcee406a1a73327f..ced8b2da07d18cfd501d7892e39347fb220fabb7 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -169,10 +169,10 @@ fixed_t P_ReturnThrustY(mobj_t *mo, angle_t angle, fixed_t move)
 boolean P_AutoPause(void)
 {
 	// Don't pause even on menu-up or focus-lost in netgames or record attack
-	if (netgame || modeattacking)
+	if (netgame || modeattacking || demo.title)
 		return false;
 
-	return (menuactive || ( window_notinfocus && cv_pauseifunfocused.value ));
+	return ((menuactive && !demo.playback) || ( window_notinfocus && cv_pauseifunfocused.value ));
 }
 
 //
@@ -654,13 +654,13 @@ static void P_DeNightserizePlayer(player_t *player)
 
 	// Restore aiming angle
 	if (player == &players[consoleplayer])
-		localaiming = 0;
-	else if (player == &players[secondarydisplayplayer])
-		localaiming2 = 0;
-	else if (player == &players[thirddisplayplayer])
-		localaiming3 = 0;
-	else if (player == &players[fourthdisplayplayer])
-		localaiming4 = 0;
+		localaiming[0] = 0;
+	else if (player == &players[displayplayers[1]])
+		localaiming[1] = 0;
+	else if (player == &players[displayplayers[2]])
+		localaiming[2] = 0;
+	else if (player == &players[displayplayers[3]])
+		localaiming[3] = 0;
 
 	if (player->mo->tracer)
 		P_RemoveMobj(player->mo->tracer);
@@ -1147,29 +1147,32 @@ boolean P_EndingMusic(player_t *player)
 	if (!P_IsLocalPlayer(player)) // Only applies to a local player
 		return false;
 
+	if (multiplayer && demo.playback) // Don't play this in multiplayer replays
+		return false;
+
 	// Event - Level Finish
 	// Check for if this is valid or not
 	if (splitscreen)
 	{
-		if (!((players[displayplayer].exiting || (players[displayplayer].pflags & PF_TIMEOVER))
-			|| (players[secondarydisplayplayer].exiting || (players[secondarydisplayplayer].pflags & PF_TIMEOVER))
-			|| ((splitscreen < 2) && (players[thirddisplayplayer].exiting || (players[thirddisplayplayer].pflags & PF_TIMEOVER)))
-			|| ((splitscreen < 3) && (players[fourthdisplayplayer].exiting || (players[fourthdisplayplayer].pflags & PF_TIMEOVER)))))
+		if (!((players[displayplayers[0]].exiting || (players[displayplayers[0]].pflags & PF_TIMEOVER))
+			|| (players[displayplayers[1]].exiting || (players[displayplayers[1]].pflags & PF_TIMEOVER))
+			|| ((splitscreen < 2) && (players[displayplayers[2]].exiting || (players[displayplayers[2]].pflags & PF_TIMEOVER)))
+			|| ((splitscreen < 3) && (players[displayplayers[3]].exiting || (players[displayplayers[3]].pflags & PF_TIMEOVER)))))
 			return false;
 
-		bestlocalplayer = &players[displayplayer];
-		bestlocalpos = ((players[displayplayer].pflags & PF_TIMEOVER) ? MAXPLAYERS+1 : players[displayplayer].kartstuff[k_position]);
+		bestlocalplayer = &players[displayplayers[0]];
+		bestlocalpos = ((players[displayplayers[0]].pflags & PF_TIMEOVER) ? MAXPLAYERS+1 : players[displayplayers[0]].kartstuff[k_position]);
 #define setbests(p) \
 	if (((players[p].pflags & PF_TIMEOVER) ? MAXPLAYERS+1 : players[p].kartstuff[k_position]) < bestlocalpos) \
 	{ \
 		bestlocalplayer = &players[p]; \
 		bestlocalpos = ((players[p].pflags & PF_TIMEOVER) ? MAXPLAYERS+1 : players[p].kartstuff[k_position]); \
 	}
-		setbests(secondarydisplayplayer);
+		setbests(displayplayers[1]);
 		if (splitscreen > 1)
-			setbests(thirddisplayplayer);
+			setbests(displayplayers[2]);
 		if (splitscreen > 2)
-			setbests(fourthdisplayplayer);
+			setbests(displayplayers[3]);
 #undef setbests
 	}
 	else
@@ -1250,12 +1253,12 @@ void P_RestoreMusic(player_t *player)
 		else if (players[p].kartstuff[k_invincibilitytimer] > bestlocaltimer) \
 		{ wantedmus = 1; bestlocaltimer = players[p].kartstuff[k_invincibilitytimer]; } \
 	}
-			setbests(displayplayer);
-			setbests(secondarydisplayplayer);
+			setbests(displayplayers[0]);
+			setbests(displayplayers[1]);
 			if (splitscreen > 1)
-				setbests(thirddisplayplayer);
+				setbests(displayplayers[2]);
 			if (splitscreen > 2)
-				setbests(fourthdisplayplayer);
+				setbests(displayplayers[3]);
 #undef setbests
 		}
 		else
@@ -1283,7 +1286,7 @@ void P_RestoreMusic(player_t *player)
 			if (G_RaceGametype() && player->laps >= (UINT8)(cv_numlaps.value - 1))
 				S_SpeedMusic(1.2f);
 #endif
-			S_ChangeMusic(mapmusname, mapmusflags, true);
+			S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 		}
 	}
 }
@@ -1513,10 +1516,39 @@ fixed_t P_GetPlayerSpinHeight(player_t *player)
 //
 boolean P_IsLocalPlayer(player_t *player)
 {
-	return ((splitscreen > 2 && player == &players[fourthdisplayplayer])
-		|| (splitscreen > 1 && player == &players[thirddisplayplayer])
-		|| (splitscreen && player == &players[secondarydisplayplayer])
-		|| player == &players[consoleplayer]);
+	UINT8 i;
+
+	if (player == &players[consoleplayer])
+		return true;
+	else if (splitscreen)
+	{
+		for (i = 1; i <= splitscreen; i++) // Skip P1
+		{
+			if (player == &players[displayplayers[i]])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+//
+// P_IsDisplayPlayer
+//
+// Returns true if player is
+// currently being watched.
+//
+boolean P_IsDisplayPlayer(player_t *player)
+{
+	UINT8 i;
+
+	for (i = 0; i <= splitscreen; i++) // DON'T skip P1
+	{
+		if (player == &players[displayplayers[i]])
+			return true;
+	}
+
+	return false;
 }
 
 //
@@ -1653,113 +1685,6 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 	return ghost;
 }
 
-//
-// P_SpawnThokMobj
-//
-// Spawns the appropriate thok object on the player
-//
-void P_SpawnThokMobj(player_t *player)
-{
-	mobj_t *mobj;
-	mobjtype_t type = player->thokitem;
-	fixed_t zheight;
-
-	if (player->skincolor == 0)
-		return;
-
-	if (player->spectator)
-		return;
-
-	if (type == MT_GHOST)
-		mobj = P_SpawnGhostMobj(player->mo); // virtually does everything here for us
-	else
-	{
-		if (player->mo->eflags & MFE_VERTICALFLIP)
-			zheight = player->mo->z + player->mo->height + FixedDiv(P_GetPlayerHeight(player) - player->mo->height, 3*FRACUNIT) - FixedMul(mobjinfo[type].height, player->mo->scale);
-		else
-			zheight = player->mo->z - FixedDiv(P_GetPlayerHeight(player) - player->mo->height, 3*FRACUNIT);
-
-		if (!(player->mo->eflags & MFE_VERTICALFLIP) && zheight < player->mo->floorz && !(mobjinfo[type].flags & MF_NOCLIPHEIGHT))
-			zheight = player->mo->floorz;
-		else if (player->mo->eflags & MFE_VERTICALFLIP && zheight + FixedMul(mobjinfo[type].height, player->mo->scale) > player->mo->ceilingz && !(mobjinfo[type].flags & MF_NOCLIPHEIGHT))
-			zheight = player->mo->ceilingz - FixedMul(mobjinfo[type].height, player->mo->scale);
-
-		mobj = P_SpawnMobj(player->mo->x, player->mo->y, zheight, type);
-
-		// set to player's angle, just in case
-		mobj->angle = player->mo->angle;
-
-		// color and skin
-		mobj->color = player->mo->color;
-		mobj->skin = player->mo->skin;
-
-		// vertical flip
-		if (player->mo->eflags & MFE_VERTICALFLIP)
-			mobj->flags2 |= MF2_OBJECTFLIP;
-		mobj->eflags |= (player->mo->eflags & MFE_VERTICALFLIP);
-
-		// scale
-		P_SetScale(mobj, player->mo->scale);
-		mobj->destscale = player->mo->scale;
-	}
-
-	P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
-	if (demorecording)
-		G_GhostAddThok();
-}
-
-//
-// P_SpawnSpinMobj
-//
-// Spawns the appropriate spin object on the player
-//
-void P_SpawnSpinMobj(player_t *player, mobjtype_t type)
-{
-	mobj_t *mobj;
-	fixed_t zheight;
-
-	if (player->skincolor == 0)
-		return;
-
-	if (player->spectator)
-		return;
-
-	if (type == MT_GHOST)
-		mobj = P_SpawnGhostMobj(player->mo); // virtually does everything here for us
-	else
-	{
-		if (player->mo->eflags & MFE_VERTICALFLIP)
-			zheight = player->mo->z + player->mo->height + FixedDiv(P_GetPlayerHeight(player) - player->mo->height, 3*FRACUNIT) - FixedMul(mobjinfo[type].height, player->mo->scale);
-		else
-			zheight = player->mo->z - FixedDiv(P_GetPlayerHeight(player) - player->mo->height, 3*FRACUNIT);
-
-		if (!(player->mo->eflags & MFE_VERTICALFLIP) && zheight < player->mo->floorz && !(mobjinfo[type].flags & MF_NOCLIPHEIGHT))
-			zheight = player->mo->floorz;
-		else if (player->mo->eflags & MFE_VERTICALFLIP && zheight + FixedMul(mobjinfo[type].height, player->mo->scale) > player->mo->ceilingz && !(mobjinfo[type].flags & MF_NOCLIPHEIGHT))
-			zheight = player->mo->ceilingz - FixedMul(mobjinfo[type].height, player->mo->scale);
-
-		mobj = P_SpawnMobj(player->mo->x, player->mo->y, zheight, type);
-
-		// set to player's angle, just in case
-		mobj->angle = player->mo->angle;
-
-		// color and skin
-		mobj->color = player->mo->color;
-		mobj->skin = player->mo->skin;
-
-		// vertical flip
-		if (player->mo->eflags & MFE_VERTICALFLIP)
-			mobj->flags2 |= MF2_OBJECTFLIP;
-		mobj->eflags |= (player->mo->eflags & MFE_VERTICALFLIP);
-
-		// scale
-		P_SetScale(mobj, player->mo->scale);
-		mobj->destscale = player->mo->scale;
-	}
-
-	P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
-}
-
 //
 // P_DoPlayerExit
 //
@@ -1769,11 +1694,7 @@ void P_DoPlayerExit(player_t *player)
 	if (player->exiting || mapreset)
 		return;
 
-	if ((player == &players[consoleplayer]
-		|| (splitscreen && player == &players[secondarydisplayplayer])
-		|| (splitscreen > 1 && player == &players[thirddisplayplayer])
-		|| (splitscreen > 2 && player == &players[fourthdisplayplayer]))
-		&& (!player->spectator && !demoplayback))
+	if (P_IsLocalPlayer(player) && (!player->spectator && !demo.playback))
 		legitimateexit = true;
 
 	if (G_RaceGametype()) // If in Race Mode, allow
@@ -1832,6 +1753,9 @@ void P_DoPlayerExit(player_t *player)
 	player->powers[pw_spacetime] = 0;
 	player->kartstuff[k_cardanimation] = 0; // srb2kart: reset battle animation
 
+	if (player == &players[consoleplayer])
+		demo.savebutton = leveltime;
+
 	/*if (playeringame[player-players] && netgame && !circuitmap)
 		CONS_Printf(M_GetText("%s has completed the level.\n"), player_names[player-players]);*/
 }
@@ -1960,13 +1884,13 @@ static void P_CheckBustableBlocks(player_t *player)
 					// ...or are drilling in NiGHTS (or Metal Sonic)
 					if (!(rover->flags & FF_SHATTER) && !(rover->flags & FF_SPINBUST)
 						&& !((player->pflags & PF_SPINNING) && !(player->pflags & PF_JUMPED))
-						&& (player->charability != CA_GLIDEANDCLIMB && !player->powers[pw_super])
+						&& (/*player->charability != CA_GLIDEANDCLIMB &&*/ !player->powers[pw_super])
 						&& !(player->pflags & PF_DRILLING) && !metalrecording)
 						continue;
 
 					// Only Knuckles can break this rock...
-					if (!(rover->flags & FF_SHATTER) && (rover->flags & FF_ONLYKNUX) && !(player->charability == CA_GLIDEANDCLIMB))
-						continue;
+					/*if (!(rover->flags & FF_SHATTER) && (rover->flags & FF_ONLYKNUX) && !(player->charability == CA_GLIDEANDCLIMB))
+						continue;*/
 
 					topheight = P_GetFOFTopZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);
 					bottomheight = P_GetFOFBottomZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);
@@ -2413,12 +2337,12 @@ static void P_CheckInvincibilityTimer(player_t *player)
 				//if (player->powers[pw_shield] & SH_FIREFLOWER)
 				//{
 				//	player->mo->color = SKINCOLOR_WHITE;
-				//	G_GhostAddColor(GHC_FIREFLOWER);
+				//	G_GhostAddColor((INT32) (player - players), GHC_FIREFLOWER);
 				//}
 				//else
 				{
 					player->mo->color = player->skincolor;
-					G_GhostAddColor(GHC_NORMAL);
+					G_GhostAddColor((INT32) (player - players), GHC_NORMAL);
 				}
 			}
 
@@ -2468,7 +2392,7 @@ static void P_DoBubbleBreath(player_t *player)
 		return;
 
 	// Tails stirs up the water while flying in it
-	if (player->powers[pw_tailsfly] && (leveltime & 1) && player->charability != CA_SWIM)
+	/*if (player->powers[pw_tailsfly] && (leveltime & 1) && player->charability != CA_SWIM)
 	{
 		fixed_t radius = (3*player->mo->radius)>>1;
 		angle_t fa = ((leveltime%45)*FINEANGLES/8) & FINEMASK;
@@ -2494,7 +2418,7 @@ static void P_DoBubbleBreath(player_t *player)
 			stirwaterz, MT_SMALLBUBBLE);
 		bubble->destscale = player->mo->scale;
 		P_SetScale(bubble,bubble->destscale);
-	}
+	}*/
 }
 
 //
@@ -2509,8 +2433,7 @@ static void P_DoPlayerHeadSigns(player_t *player)
 		// If you're "IT", show a big "IT" over your head for others to see.
 		if (player->pflags & PF_TAGIT)
 		{
-			if (!(player == &players[consoleplayer] || player == &players[displayplayer] || player == &players[secondarydisplayplayer]
-				|| player == &players[thirddisplayplayer] || player == &players[fourthdisplayplayer])) // Don't display it on your own view.
+			if (!P_IsDisplayPlayer(player)) // Don't display it on your own view.
 			{
 				if (!(player->mo->eflags & MFE_VERTICALFLIP))
 					P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z + player->mo->height, MT_TAG);
@@ -2998,13 +2921,13 @@ static void P_DoClimbing(player_t *player)  // SRB2kart - unused
 	}
 
 	if (player == &players[consoleplayer])
-		localangle = player->mo->angle;
-	else if (player == &players[secondarydisplayplayer])
-		localangle2 = player->mo->angle;
-	else if (player == &players[thirddisplayplayer])
-		localangle3 = player->mo->angle;
-	else if (player == &players[fourthdisplayplayer])
-		localangle4 = player->mo->angle;
+		localangle[0] = player->mo->angle;
+	else if (player == &players[displayplayers[1]])
+		localangle[1] = player->mo->angle;
+	else if (player == &players[displayplayers[2]])
+		localangle[2] = player->mo->angle;
+	else if (player == &players[displayplayers[3]])
+		localangle[3] = player->mo->angle;
 
 	if (player->climbing == 0)
 		P_SetPlayerMobjState(player->mo, S_PLAY_ATK1);
@@ -3634,195 +3557,6 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd) // SRB2kart - unused.
 }
 */
 
-//
-// P_DoJump
-//
-// Jump routine for the player
-//
-void P_DoJump(player_t *player, boolean soundandstate)
-{
-	fixed_t factor;
-	const fixed_t dist6 = FixedMul(FixedDiv(player->speed, player->mo->scale), player->actionspd)/20;
-
-	return;
-
-	if (player->pflags & PF_JUMPSTASIS)
-		return;
-
-	if (!player->jumpfactor)
-		return;
-
-	if (player->kartstuff[k_spinouttimer]) // SRB2kart
-		return;
-
-	/* // SRB2kart - climbing in a kart?
-	if (player->climbing)
-	{
-		// Jump this high.
-		if (player->powers[pw_super])
-			player->mo->momz = 5*FRACUNIT;
-		else if (player->mo->eflags & MFE_UNDERWATER)
-			player->mo->momz = 2*FRACUNIT;
-		else
-			player->mo->momz = 15*(FRACUNIT/4);
-
-		player->mo->angle = player->mo->angle - ANGLE_180; // Turn around from the wall you were climbing.
-
-		if (player == &players[consoleplayer])
-			localangle = player->mo->angle; // Adjust the local control angle.
-		else if (player == &players[secondarydisplayplayer])
-			localangle2 = player->mo->angle;
-		else if (player == &players[thirddisplayplayer])
-			localangle3 = player->mo->angle;
-		else if (player == &players[fourthdisplayplayer])
-			localangle4 = player->mo->angle;
-
-		player->climbing = 0; // Stop climbing, duh!
-		P_InstaThrust(player->mo, player->mo->angle, FixedMul(6*FRACUNIT, player->mo->scale)); // Jump off the wall.
-	}
-	// Quicksand jumping.
-	else if (P_InQuicksand(player->mo))
-	{
-		if (player->mo->ceilingz-player->mo->floorz <= player->mo->height-1)
-			return;
-		player->mo->momz += (39*(FRACUNIT/4))>>1;
-		if (player->mo->momz >= 6*FRACUNIT)
-			player->mo->momz = 6*FRACUNIT; //max momz in quicksand
-		else if (player->mo->momz < 0) // still descending?
-			player->mo->momz = (39*(FRACUNIT/4))>>1; // just default to the jump height.
-	}
-	else*/ if (!(player->pflags & PF_JUMPED)) // Spin Attack
-	{
-		if (player->mo->ceilingz-player->mo->floorz <= player->mo->height-1)
-			return;
-
-		// Jump this high.
-		if (player->pflags & PF_CARRIED)
-		{
-			player->mo->momz = 9*FRACUNIT;
-			player->pflags &= ~PF_CARRIED;
-			/*if (player-players == consoleplayer && botingame)
-				CV_SetValue(&cv_analog2, true);*/
-		}
-		else if (player->pflags & PF_ITEMHANG)
-		{
-			player->mo->momz = 9*FRACUNIT;
-			player->pflags &= ~PF_ITEMHANG;
-		}
-		else if (player->pflags & PF_ROPEHANG)
-		{
-			player->mo->momz = 12*FRACUNIT;
-			player->pflags &= ~PF_ROPEHANG;
-			P_SetTarget(&player->mo->tracer, NULL);
-		}
-		else if (player->mo->eflags & MFE_GOOWATER)
-		{
-			player->mo->momz = 7*FRACUNIT;
-			if (player->charability == CA_JUMPBOOST && onground)
-			{
-				if (player->charability2 == CA2_MULTIABILITY)
-					player->mo->momz += FixedMul(FRACUNIT/4, dist6);
-				else
-					player->mo->momz += FixedMul(FRACUNIT/8, dist6);
-			}
-		}
-		else if (maptol & TOL_NIGHTS)
-			player->mo->momz = 24*FRACUNIT;
-		else
-			player->mo->momz = 3*FRACUNIT; // Kart jump momentum.
-		/* // SRB2kart - Okay enough of that.
-		else if (player->powers[pw_super])
-		{
-			if (player->charability == CA_FLOAT)
-				player->mo->momz = 28*FRACUNIT; //Obscene jump height anyone?
-			else if (player->charability == CA_SLOWFALL)
-				player->mo->momz = 37*(FRACUNIT/2); //Less obscene because during super, floating propells oneself upward.
-			else // Default super jump momentum.
-				player->mo->momz = 13*FRACUNIT;
-
-			// Add a boost for super characters with float/slowfall and multiability.
-			if (player->charability2 == CA2_MULTIABILITY &&
-				(player->charability == CA_FLOAT || player->charability == CA_SLOWFALL))
-				player->mo->momz += 2*FRACUNIT;
-			else if (player->charability == CA_JUMPBOOST)
-			{
-				if (player->charability2 == CA2_MULTIABILITY)
-					player->mo->momz += FixedMul(FRACUNIT/4, dist6);
-				else
-					player->mo->momz += FixedMul(FRACUNIT/8, dist6);
-			}
-		}
-		else if (player->charability2 == CA2_MULTIABILITY &&
-			(player->charability == CA_DOUBLEJUMP || player->charability == CA_FLOAT || player->charability == CA_SLOWFALL))
-		{
-			// Multiability exceptions, since some abilities cannot effectively use it and need a boost.
-			if (player->charability == CA_DOUBLEJUMP)
-				player->mo->momz = 23*(FRACUNIT/2); // Increased jump height instead of infinite jumps.
-			else if (player->charability == CA_FLOAT || player->charability == CA_SLOWFALL)
-				player->mo->momz = 12*FRACUNIT; // Increased jump height due to ineffective repeat.
-		}
-		else
-		{
-			player->mo->momz = 39*(FRACUNIT/4); // Default jump momentum.
-			if (player->charability == CA_JUMPBOOST && onground)
-			{
-				if (player->charability2 == CA2_MULTIABILITY)
-					player->mo->momz += FixedMul(FRACUNIT/4, dist6);
-				else
-					player->mo->momz += FixedMul(FRACUNIT/8, dist6);
-			}
-		}
-		*/
-
-		// Reduce player momz by 58.5% when underwater.
-		if (player->mo->eflags & MFE_UNDERWATER)
-			player->mo->momz = FixedMul(player->mo->momz, FixedDiv(117*FRACUNIT, 200*FRACUNIT));
-
-		player->jumping = 1;
-	}
-
-	factor = player->jumpfactor;
-
-	if (twodlevel || (player->mo->flags2 & MF2_TWOD))
-		factor += player->jumpfactor / 10;
-
-	P_SetObjectMomZ(player->mo, FixedMul(factor, player->mo->momz), false); // Custom height
-
-	// set just an eensy above the ground
-	if (player->mo->eflags & MFE_VERTICALFLIP)
-	{
-		player->mo->z--;
-		if (player->mo->pmomz < 0)
-			player->mo->momz += player->mo->pmomz; // Add the platform's momentum to your jump.
-		else
-			player->mo->pmomz = 0;
-	}
-	else
-	{
-		player->mo->z++;
-		if (player->mo->pmomz > 0)
-			player->mo->momz += player->mo->pmomz; // Add the platform's momentum to your jump.
-		else
-			player->mo->pmomz = 0;
-	}
-	player->mo->eflags &= ~MFE_APPLYPMOMZ;
-
-	player->pflags |= PF_JUMPED;
-
-	if (soundandstate)
-	{
-		if (!player->spectator)
-			S_StartSound(player->mo, sfx_jump); // Play jump sound!
-
-		/* // SRB2kart - don't need jump frames
-		if (!(player->charability2 == CA2_SPINDASH))
-			P_SetPlayerMobjState(player->mo, S_PLAY_SPRING);
-		else
-			P_SetPlayerMobjState(player->mo, S_PLAY_ATK1);
-		*/
-	}
-}
-
 //
 // P_DoSpinDash
 //
@@ -3872,8 +3606,8 @@ static void P_DoSpinDash(player_t *player, ticcmd_t *cmd) // SRB2kart - unused.
 
 				// Now spawn the color thok circle.
 				P_SpawnSpinMobj(player, player->revitem);
-				if (demorecording)
-					G_GhostAddRev();
+				if (demo.recording)
+					G_GhostAddRev((INT32) (player - players));
 			}
 		}
 		// If not moving up or down, and travelling faster than a speed of four while not holding
@@ -3946,7 +3680,7 @@ void P_DoJumpShield(player_t *player)
 		return;
 
 	player->pflags &= ~PF_JUMPED;
-	P_DoJump(player, false);
+	//P_DoJump(player, false);
 	player->pflags &= ~PF_JUMPED;
 	player->secondjump = 0;
 	player->jumping = 0;
@@ -4004,338 +3738,10 @@ void P_Telekinesis(player_t *player, fixed_t thrust, fixed_t range)
 		}
 	}
 
-	P_SpawnThokMobj(player);
+	//P_SpawnThokMobj(player);
 	player->pflags |= PF_THOKKED;
 }
 
-//
-// P_DoJumpStuff
-//
-// Handles player jumping
-//
-static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
-{
-	if (player->pflags & PF_JUMPSTASIS)
-		return;
-
-	if (cmd->buttons & BT_BRAKE && !(player->pflags & PF_JUMPDOWN) && !player->exiting && !P_PlayerInPain(player))
-	{
-		if (onground || player->climbing || player->pflags & (PF_CARRIED|PF_ITEMHANG|PF_ROPEHANG))
-		{}
-		else if (player->pflags & PF_MACESPIN && player->mo->tracer)
-		{}
-		else if (!(player->pflags & PF_SLIDING) && ((gametype != GT_CTF) || (!player->gotflag)))
-		{
-#ifdef HAVE_BLUA
-			if (!LUAh_JumpSpinSpecial(player))
-#endif
-			switch (player->charability)
-			{
-				case CA_TELEKINESIS:
-					if (player->pflags & PF_JUMPED)
-					{
-						if (!(player->pflags & PF_THOKKED) || (player->charability2 == CA2_MULTIABILITY))
-						{
-							P_Telekinesis(player,
-								-FixedMul(player->actionspd, player->mo->scale), // -ve thrust (pulling towards player)
-								FixedMul(384*FRACUNIT, player->mo->scale));
-						}
-					}
-					break;
-				case CA_AIRDRILL:
-					if (player->pflags & PF_JUMPED)
-					{
-						if (player->pflags & PF_THOKKED) // speed up falling down
-						{
-							if (player->secondjump < 42)
-								player->secondjump ++;
-						}
-					}
-					break;
-				default:
-					break;
-			}
-		}
-	}
-
-	if (player->charability == CA_AIRDRILL)
-	{
-		if (player->pflags & PF_JUMPED)
-		{
-			if (player->flyangle > 0 && player->pflags & PF_THOKKED)
-			{
-				player->flyangle--;
-
-				P_SetObjectMomZ(player->mo, ((player->flyangle-24 - player->secondjump*3)*((player->actionspd>>FRACBITS)/12 + 1)<<FRACBITS)/7, false);
-
-				P_SpawnThokMobj(player);
-
-				if ((player->mo->eflags & MFE_UNDERWATER))
-					P_InstaThrust(player->mo, player->mo->angle, FixedMul(player->normalspeed, player->mo->scale)*(80-player->flyangle - (player->actionspd>>FRACBITS)/2)/80);
-				else
-					P_InstaThrust(player->mo, player->mo->angle, ((FixedMul(player->normalspeed - player->actionspd/4, player->mo->scale))*2)/3);
-			}
-		}
-	}
-
-	if (cmd->buttons & BT_DRIFT && !player->exiting && !P_PlayerInPain(player))
-	{
-#ifdef HAVE_BLUA
-		if (LUAh_JumpSpecial(player))
-			;
-		else
-#endif
-		if (player->pflags & PF_JUMPDOWN) // all situations below this require jump button not to be pressed already
-			;
-		else
-		// Jump S3&K style while in quicksand.
-		if (P_InQuicksand(player->mo))
-		{
-			P_DoJump(player, true);
-			player->secondjump = 0;
-			player->pflags &= ~PF_THOKKED;
-		}
-		else
-		// can't jump while in air, can't jump while jumping
-		if (onground || player->climbing || player->pflags & (PF_CARRIED|PF_ITEMHANG|PF_ROPEHANG))
-		{
-			P_DoJump(player, true);
-			player->secondjump = 0;
-			player->pflags &= ~PF_THOKKED;
-		}
-		/* // SRB2kart - no jumpy power things
-		else if (player->pflags & PF_MACESPIN && player->mo->tracer)
-		{
-			player->pflags &= ~PF_MACESPIN;
-			player->powers[pw_flashing] = TICRATE/4;
-		}
-		else if (player->pflags & PF_SLIDING || (gametype == GT_CTF && player->gotflag))
-			;
-		else if (P_SuperReady(player))
-		{
-			// If you can turn super and aren't already,
-			// and you don't have a shield, do it!
-			P_DoSuperTransformation(player, false);
-		}
-		else if (player->pflags & PF_JUMPED)
-		{
-#ifdef HAVE_BLUA
-			if (!LUAh_AbilitySpecial(player))
-#endif
-			switch (player->charability)
-			{
-				case CA_THOK:
-				case CA_HOMINGTHOK:
-				case CA_JUMPTHOK: // Credit goes to CZ64 and Sryder13 for the original
-					// Now it's Sonic's abilities turn!
-					// THOK!
-					if (!(player->pflags & PF_THOKKED) || (player->charability2 == CA2_MULTIABILITY))
-					{
-						// Catapult the player
-						fixed_t actionspd = player->actionspd;
-						if (player->mo->eflags & MFE_UNDERWATER)
-							actionspd >>= 1;
-						if ((player->charability == CA_JUMPTHOK) && !(player->pflags & PF_THOKKED))
-						{
-							player->pflags &= ~PF_JUMPED;
-							P_DoJump(player, false);
-						}
-						P_InstaThrust(player->mo, player->mo->angle, FixedMul(actionspd, player->mo->scale));
-
-						if (maptol & TOL_2D)
-						{
-							player->mo->momx /= 2;
-							player->mo->momy /= 2;
-						}
-						else if (player->charability == CA_HOMINGTHOK)
-						{
-							player->mo->momx /= 3;
-							player->mo->momy /= 3;
-						}
-
-						if (player->mo->info->attacksound && !player->spectator)
-							S_StartSound(player->mo, player->mo->info->attacksound); // Play the THOK sound
-
-						P_SpawnThokMobj(player);
-
-						if (player->charability == CA_HOMINGTHOK && !player->homing)
-						{
-							if (P_LookForEnemies(player))
-							{
-								if (player->mo->tracer)
-									player->homing = 3*TICRATE;
-							}
-						}
-
-						player->pflags &= ~(PF_SPINNING|PF_STARTDASH);
-						player->pflags |= PF_THOKKED;
-					}
-					break;
-
-				case CA_FLY:
-				case CA_SWIM: // Swim
-					// If currently in the air from a jump, and you pressed the
-					// button again and have the ability to fly, do so!
-					if (player->charability == CA_SWIM && !(player->mo->eflags & MFE_UNDERWATER))
-						; // Can't do anything if you're a fish out of water!
-					else if (!(player->pflags & PF_THOKKED) && !(player->powers[pw_tailsfly]))
-					{
-						//P_SetPlayerMobjState(player->mo, S_PLAY_ABL1); // Change to the flying animation
-
-						player->powers[pw_tailsfly] = tailsflytics + 1; // Set the fly timer
-
-						player->pflags &= ~(PF_JUMPED|PF_SPINNING|PF_STARTDASH);
-						player->pflags |= PF_THOKKED;
-					}
-					break;
-				case CA_GLIDEANDCLIMB:
-					// Now Knuckles-type abilities are checked.
-					// If you can turn super and aren't already,
-					// and you don't have a shield, do it!
-					if (!(player->pflags & PF_THOKKED) || player->charability2 == CA2_MULTIABILITY)
-					{
-						INT32 glidespeed = player->actionspd;
-
-						player->pflags |= PF_GLIDING|PF_THOKKED;
-						player->glidetime = 0;
-
-						if (player->powers[pw_super] && ALL7EMERALDS(player->powers[pw_emeralds]))
-						{
-							// Glide at double speed while super.
-							glidespeed *= 2;
-							player->pflags &= ~PF_THOKKED;
-						}
-
-						//P_SetPlayerMobjState(player->mo, S_PLAY_ABL1);
-						P_InstaThrust(player->mo, player->mo->angle, FixedMul(glidespeed, player->mo->scale));
-						player->pflags &= ~(PF_SPINNING|PF_STARTDASH);
-					}
-					break;
-				case CA_DOUBLEJUMP: // Double-Jump
-					if (!(player->pflags & PF_THOKKED))
-					{
-						player->pflags &= ~PF_JUMPED;
-						P_DoJump(player, true);
-
-						// Allow infinite double jumping if super.
-						if (!player->powers[pw_super])
-							player->pflags |= PF_THOKKED;
-					}
-					break;
-				case CA_FLOAT: // Float
-				case CA_SLOWFALL: // Slow descent hover
-					if (!player->secondjump)
-						player->secondjump = 1;
-					break;
-				case CA_TELEKINESIS:
-					if (!(player->pflags & PF_THOKKED) || player->charability2 == CA2_MULTIABILITY)
-					{
-						P_Telekinesis(player,
-							FixedMul(player->actionspd, player->mo->scale), // +ve thrust (pushing away from player)
-							FixedMul(384*FRACUNIT, player->mo->scale));
-					}
-					break;
-				case CA_FALLSWITCH:
-					if (!(player->pflags & PF_THOKKED) || player->charability2 == CA2_MULTIABILITY)
-					{
-						player->mo->momz = -player->mo->momz;
-						P_SpawnThokMobj(player);
-						player->pflags |= PF_THOKKED;
-					}
-					break;
-
-				case CA_AIRDRILL:
-					if (!(player->pflags & PF_THOKKED) || player->charability2 == CA2_MULTIABILITY)
-					{
-						player->flyangle = 56 + (60-(player->actionspd>>FRACBITS))/3;
-						player->pflags |= PF_THOKKED;
-						S_StartSound(player->mo, sfx_spndsh);
-					}
-					break;
-				default:
-					break;
-			}
-		}
-		else if (player->pflags & PF_THOKKED)
-		{
-#ifdef HAVE_BLUA
-			if (!LUAh_AbilitySpecial(player))
-#endif
-			switch (player->charability)
-			{
-				case CA_FLY:
-				case CA_SWIM: // Swim
-					if (player->charability == CA_SWIM && !(player->mo->eflags & MFE_UNDERWATER))
-						; // Can't do anything if you're a fish out of water!
-					else if (player->powers[pw_tailsfly]) // If currently flying, give an ascend boost.
-					{
-						if (!player->fly1)
-							player->fly1 = 20;
-						else
-							player->fly1 = 2;
-
-						if (player->charability == CA_SWIM)
-							player->fly1 /= 2;
-
-						// Slow down!
-						if (player->speed > FixedMul(8*FRACUNIT, player->mo->scale) && player->speed > FixedMul(player->normalspeed>>1, player->mo->scale))
-							P_Thrust(player->mo, R_PointToAngle2(0,0,player->mo->momx,player->mo->momy), FixedMul(-4*FRACUNIT, player->mo->scale));
-					}
-					break;
-				default:
-					break;
-			}
-		}
-		else if ((player->powers[pw_shield] & SH_NOSTACK) == SH_JUMP && !player->powers[pw_super])
-			P_DoJumpShield(player);
-		*/
-	}
-
-	if (cmd->buttons & BT_DRIFT)
-	{
-		player->pflags |= PF_JUMPDOWN;
-
-		if ((gametype != GT_CTF || !player->gotflag) && !player->exiting)
-		{
-			if (player->secondjump == 1)
-			{
-				if (player->charability == CA_FLOAT)
-					player->mo->momz = 0;
-				else if (player->charability == CA_SLOWFALL)
-				{
-					if (player->powers[pw_super])
-					{
-						if (P_MobjFlip(player->mo)*player->mo->momz < gravity*16)
-						player->mo->momz = P_MobjFlip(player->mo)*gravity*16; //Float upward 4x as fast while super.
-					}
-					else if (P_MobjFlip(player->mo)*player->mo->momz < -gravity*4)
-						player->mo->momz = P_MobjFlip(player->mo)*-gravity*4;
-				}
-				player->pflags &= ~PF_SPINNING;
-			}
-		}
-	}
-	else // If not pressing the jump button
-	{
-		player->pflags &= ~PF_JUMPDOWN;
-
-		// Repeat abilities, but not double jump!
-		if ((player->charability2 == CA2_MULTIABILITY && player->charability != CA_DOUBLEJUMP)
-			|| (player->powers[pw_super] && ALL7EMERALDS(player->powers[pw_emeralds])))
-			player->secondjump = 0;
-		else if (player->charability == CA_FLOAT && player->secondjump == 1)
-			player->secondjump = 2;
-
-
-		// If letting go of the jump button while still on ascent, cut the jump height.
-		if (player->pflags & PF_JUMPED && P_MobjFlip(player->mo)*player->mo->momz > 0 && player->jumping == 1)
-		{
-			player->mo->momz >>= 1;
-			player->jumping = 0;
-		}
-	}
-}
-
 boolean P_AnalogMove(player_t *player)
 {
 	return player->pflags & PF_ANALOGMODE;
@@ -4359,14 +3765,14 @@ boolean P_AnalogMove(player_t *player)
 	fixed_t tempx = 0, tempy = 0;
 	angle_t tempangle, origtempangle;
 
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		thiscam = &camera4;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		thiscam = &camera3;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		thiscam = &camera2;
+	if (splitscreen > 2 && player == &players[displayplayers[3]])
+		thiscam = &camera[3];
+	else if (splitscreen > 1 && player == &players[displayplayers[2]])
+		thiscam = &camera[2];
+	else if (splitscreen && player == &players[displayplayers[1]])
+		thiscam = &camera[1];
 	else
-		thiscam = &camera;
+		thiscam = &camera[0];
 
 	if (!cmd->forwardmove && !cmd->sidemove)
 		return 0;
@@ -4507,13 +3913,13 @@ static void P_2dMovement(player_t *player)
 	}
 
 	if (player == &players[consoleplayer])
-		localangle = player->mo->angle;
-	else if (player == &players[secondarydisplayplayer])
-		localangle2 = player->mo->angle;
-	else if (player == &players[thirddisplayplayer])
-		localangle3 = player->mo->angle;
-	else if (player == &players[fourthdisplayplayer])
-		localangle4 = player->mo->angle;
+		localangle[0] = player->mo->angle;
+	else if (player == &players[displayplayers[1]])
+		localangle[1] = player->mo->angle;
+	else if (player == &players[displayplayers[2]])
+		localangle[2] = player->mo->angle;
+	else if (player == &players[displayplayers[3]])
+		localangle[3] = player->mo->angle;
 
 	if (player->pflags & PF_GLIDING)
 		movepushangle = player->mo->angle;
@@ -4648,6 +4054,8 @@ static void P_3dMovement(player_t *player)
 	{
 		if (player->kartstuff[k_drift] != 0)
 			movepushangle = player->mo->angle-(ANGLE_45/5)*player->kartstuff[k_drift];
+		else if (player->kartstuff[k_spinouttimer] || player->kartstuff[k_wipeoutslow])	// if spun out, use the boost angle
+			movepushangle = (angle_t)player->kartstuff[k_boostangle];
 		else
 			movepushangle = player->mo->angle;
 	}
@@ -6043,13 +5451,13 @@ static void P_NiGHTSMovement(player_t *player)
 		P_SetMobjStateNF(player->mo->tracer, leveltime & 1 ? flystate : flystate+1);
 
 	if (player == &players[consoleplayer])
-		localangle = player->mo->angle;
-	else if (player == &players[secondarydisplayplayer])
-		localangle2 = player->mo->angle;
-	else if (player == &players[thirddisplayplayer])
-		localangle3 = player->mo->angle;
-	else if (player == &players[fourthdisplayplayer])
-		localangle4 = player->mo->angle;
+		localangle[0] = player->mo->angle;
+	else if (player == &players[displayplayers[1]])
+		localangle[1] = player->mo->angle;
+	else if (player == &players[displayplayers[2]])
+		localangle[2] = player->mo->angle;
+	else if (player == &players[displayplayers[3]])
+		localangle[3] = player->mo->angle;
 
 	if (still)
 	{
@@ -6076,13 +5484,13 @@ static void P_NiGHTSMovement(player_t *player)
 		movingangle = InvAngle(movingangle);
 
 	if (player == &players[consoleplayer])
-		localaiming = movingangle;
-	else if (player == &players[secondarydisplayplayer])
-		localaiming2 = movingangle;
-	else if (player == &players[thirddisplayplayer])
-		localaiming3 = movingangle;
-	else if (player == &players[fourthdisplayplayer])
-		localaiming4 = movingangle;
+		localaiming[0] = movingangle;
+	else if (player == &players[displayplayers[1]])
+		localaiming[1] = movingangle;
+	else if (player == &players[displayplayers[2]])
+		localaiming[2] = movingangle;
+	else if (player == &players[displayplayers[3]])
+		localaiming[3] = movingangle;
 
 	player->mo->tracer->angle = player->mo->angle;
 
@@ -6346,7 +5754,7 @@ static void P_MovePlayer(player_t *player)
 		{
 			if (G_IsSpecialStage(gamemap))
 			{
-				if (player == &players[displayplayer]) // only play the sound for yourself landing
+				if (player == &players[displayplayers[0]]) // only play the sound for yourself landing
 					S_StartSound(NULL, sfx_s3k6a);
 				for (i = 0; i < MAXPLAYERS; i++)
 					if (playeringame[i])
@@ -6374,7 +5782,7 @@ static void P_MovePlayer(player_t *player)
 			|| (leveltime > starttime && (cmd->buttons & BT_ACCELERATE && cmd->buttons & BT_BRAKE)) // Rubber-burn turn
 			|| (player->kartstuff[k_respawn]) // Respawning
 			|| (player->spectator || objectplacing)) // Not a physical player
-			&& !(player->kartstuff[k_spinouttimer] && player->kartstuff[k_sneakertimer])) // Spinning and boosting cancels out turning
+			) // ~~Spinning and boosting cancels out turning~~ Not anymore given spinout is more slippery and more prone to get you killed because of boosters.
 		{
 			player->lturn_max[leveltime%MAXPREDICTTICS] = K_GetKartTurnValue(player, KART_FULLTURN)+1;
 			player->rturn_max[leveltime%MAXPREDICTTICS] = K_GetKartTurnValue(player, -KART_FULLTURN)-1;
@@ -6538,14 +5946,7 @@ static void P_MovePlayer(player_t *player)
 		P_SetPlayerMobjState(player->mo, S_KART_STND1); // SRB2kart - was S_PLAY_STND
 	}
 
-	// Cap the speed limit on a spindash
-	// Up the 60*FRACUNIT number to boost faster, you speed demon you!
-	if (player->dashspeed > FixedMul(player->maxdash, player->mo->scale))
-		player->dashspeed = FixedMul(player->maxdash, player->mo->scale);
-	else if (player->dashspeed > 0 && player->dashspeed < FixedMul(player->mindash, player->mo->scale))
-		player->dashspeed = FixedMul(player->mindash, player->mo->scale);
-
-	if (!(player->charability == CA_GLIDEANDCLIMB) || player->gotflag) // If you can't glide, then why the heck would you be gliding?
+	if (/*!(player->charability == CA_GLIDEANDCLIMB) ||*/ player->gotflag) // If you can't glide, then why the heck would you be gliding?
 	{
 		/* // SRB2kart - ???
 		if (player->pflags & PF_GLIDING || player->climbing)
@@ -6761,8 +6162,8 @@ static void P_MovePlayer(player_t *player)
 	if (player->pflags & PF_SPINNING && player->speed > FixedMul(15<<FRACBITS, player->mo->scale) && !(player->pflags & PF_JUMPED))
 	{
 		P_SpawnSpinMobj(player, player->spinitem);
-		if (demorecording)
-			G_GhostAddSpin();
+		if (demo.recording)
+			G_GhostAddSpin((INT32) (player - players));
 	}
 	*/
 
@@ -6798,7 +6199,7 @@ static void P_MovePlayer(player_t *player)
 	P_DoSpinDash(player, cmd);
 	*/
 	// jumping
-	P_DoJumpStuff(player, cmd);
+	//P_DoJumpStuff(player, cmd);
 
 	/*
 	// If you're not spinning, you'd better not be spindashing!
@@ -6854,18 +6255,18 @@ static void P_MovePlayer(player_t *player)
 		}
 		// Otherwise, face the direction you're travelling.
 		else if (player->panim == PA_WALK || player->panim == PA_RUN || player->panim == PA_ROLL
-		|| (/*(player->mo->state >= &states[S_PLAY_ABL1] && player->mo->state <= &states[S_PLAY_SPC4]) && */player->charability == CA_FLY)) // SRB2kart - idk
+		/*|| ((player->mo->state >= &states[S_PLAY_ABL1] && player->mo->state <= &states[S_PLAY_SPC4]) && player->charability == CA_FLY)*/) // SRB2kart - idk
 			player->mo->angle = R_PointToAngle2(0, 0, player->rmomx, player->rmomy);
 
 		// Update the local angle control.
 		if (player == &players[consoleplayer])
-			localangle = player->mo->angle;
-		else if (player == &players[secondarydisplayplayer])
-			localangle2 = player->mo->angle;
-		else if (player == &players[thirddisplayplayer])
-			localangle3 = player->mo->angle;
-		else if (player == &players[fourthdisplayplayer])
-			localangle4 = player->mo->angle;
+			localangle[0] = player->mo->angle;
+		else if (player == &players[displayplayers[1]])
+			localangle[1] = player->mo->angle;
+		else if (player == &players[displayplayers[2]])
+			localangle[2] = player->mo->angle;
+		else if (player == &players[displayplayers[3]])
+			localangle[3] = player->mo->angle;
 	}
 #endif
 
@@ -7044,8 +6445,8 @@ static void P_MovePlayer(player_t *player)
 
 		speed = R_PointToDist2(player->rmomx, player->rmomy, 0, 0);
 
-		if (speed > player->normalspeed-5*FRACUNIT)
-			speed = player->normalspeed-5*FRACUNIT;
+		if (speed > K_GetKartSpeed(player, false)-(5<<FRACBITS))
+			speed = K_GetKartSpeed(player, false)-(5<<FRACBITS);
 
 		if (speed >= runnyspeed)
 			player->fovadd = speed-runnyspeed;
@@ -7187,13 +6588,13 @@ static void P_DoZoomTube(player_t *player)
 		player->mo->angle = R_PointToAngle2(player->mo->x, player->mo->y, player->mo->tracer->x, player->mo->tracer->y);
 
 		if (player == &players[consoleplayer])
-			localangle = player->mo->angle;
-		else if (player == &players[secondarydisplayplayer])
-			localangle2 = player->mo->angle;
-		else if (player == &players[thirddisplayplayer])
-			localangle3 = player->mo->angle;
-		else if (player == &players[fourthdisplayplayer])
-			localangle4 = player->mo->angle;
+			localangle[0] = player->mo->angle;
+		else if (player == &players[displayplayers[1]])
+			localangle[1] = player->mo->angle;
+		else if (player == &players[displayplayers[2]])
+			localangle[2] = player->mo->angle;
+		else if (player == &players[displayplayers[3]])
+			localangle[3] = player->mo->angle;
 	}
 #if 0
 	if (player->mo->state != &states[S_KART_SPIN])
@@ -7583,13 +6984,13 @@ void P_HomingAttack(mobj_t *source, mobj_t *enemy) // Home in on your target
 	if (source->player)
 	{
 		if (source->player == &players[consoleplayer])
-			localangle = source->angle;
-		else if (source->player == &players[secondarydisplayplayer])
-			localangle2 = source->angle;
-		else if (source->player == &players[thirddisplayplayer])
-			localangle3 = source->angle;
-		else if (source->player == &players[fourthdisplayplayer])
-			localangle4 = source->angle;
+			localangle[0] = source->angle;
+		else if (source->player == &players[displayplayers[1]])
+			localangle[1] = source->angle;
+		else if (source->player == &players[displayplayers[2]])
+			localangle[2] = source->angle;
+		else if (source->player == &players[displayplayers[3]])
+			localangle[3] = source->angle;
 	}
 
 	// change slope
@@ -7600,7 +7001,7 @@ void P_HomingAttack(mobj_t *source, mobj_t *enemy) // Home in on your target
 		dist = 1;
 
 	if (source->type == MT_DETON && enemy->player) // For Deton Chase (Unused)
-		ns = FixedDiv(FixedMul(enemy->player->normalspeed, enemy->scale), FixedDiv(20*FRACUNIT,17*FRACUNIT));
+		ns = FixedDiv(FixedMul(K_GetKartSpeed(enemy->player, false), enemy->scale), FixedDiv(20*FRACUNIT,17*FRACUNIT));
 	else if (source->type != MT_PLAYER)
 	{
 		if (source->threshold == 32000)
@@ -7609,7 +7010,7 @@ void P_HomingAttack(mobj_t *source, mobj_t *enemy) // Home in on your target
 			ns = FixedMul(source->info->speed, source->scale);
 	}
 	else if (source->player)
-		ns = FixedDiv(FixedMul(source->player->actionspd, source->scale), 3*FRACUNIT/2);
+		ns = FixedDiv(FixedMul(K_GetKartSpeed(source->player, false), source->scale), 3*FRACUNIT/2);
 
 	source->momx = FixedMul(FixedDiv(enemy->x - source->x, dist), ns);
 	source->momy = FixedMul(FixedDiv(enemy->y - source->y, dist), ns);
@@ -7722,7 +7123,7 @@ notrealplayer:
 // P_MoveCamera: make sure the camera is not outside the world and looks at the player avatar
 //
 
-camera_t camera, camera2, camera3, camera4; // Four cameras, three for splitscreen
+camera_t camera[MAXSPLITSCREENPLAYERS]; // Four cameras, three for splitscreen
 
 static void CV_CamRotate_OnChange(void)
 {
@@ -7827,10 +7228,10 @@ void P_ResetCamera(player_t *player, camera_t *thiscam)
 	thiscam->y = y;
 	thiscam->z = z;
 
-	if (!(thiscam == &camera && (cv_cam_still.value || cv_analog.value))
-		&& !(thiscam == &camera2 && (cv_cam2_still.value || cv_analog2.value))
-		&& !(thiscam == &camera3 && (cv_cam3_still.value || cv_analog3.value))
-		&& !(thiscam == &camera4 && (cv_cam4_still.value || cv_analog4.value)))
+	if (!(thiscam == &camera[0] && (cv_cam_still.value || cv_analog.value))
+		&& !(thiscam == &camera[1] && (cv_cam2_still.value || cv_analog2.value))
+		&& !(thiscam == &camera[2] && (cv_cam3_still.value || cv_analog3.value))
+		&& !(thiscam == &camera[3] && (cv_cam4_still.value || cv_analog4.value)))
 	{
 		thiscam->angle = player->mo->angle;
 		thiscam->aiming = 0;
@@ -7888,46 +7289,49 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		if (player->spectator) // force cam off for spectators
 			return true;
 
-		if (!cv_chasecam.value && thiscam == &camera)
+		if (!cv_chasecam.value && thiscam == &camera[0])
 			return true;
 
-		if (!cv_chasecam2.value && thiscam == &camera2)
+		if (!cv_chasecam2.value && thiscam == &camera[1])
 			return true;
 
-		if (!cv_chasecam3.value && thiscam == &camera3)
+		if (!cv_chasecam3.value && thiscam == &camera[2])
 			return true;
 
-		if (!cv_chasecam4.value && thiscam == &camera4)
+		if (!cv_chasecam4.value && thiscam == &camera[3])
 			return true;
 	}
 
 	if (!thiscam->chase && !resetcalled)
 	{
 		if (player == &players[consoleplayer])
-			focusangle = localangle;
-		else if (player == &players[secondarydisplayplayer])
-			focusangle = localangle2;
-		else if (player == &players[thirddisplayplayer])
-			focusangle = localangle3;
-		else if (player == &players[fourthdisplayplayer])
-			focusangle = localangle4;
+			focusangle = localangle[0];
+		else if (player == &players[displayplayers[1]])
+			focusangle = localangle[1];
+		else if (player == &players[displayplayers[2]])
+			focusangle = localangle[2];
+		else if (player == &players[displayplayers[3]])
+			focusangle = localangle[3];
 		else
 			focusangle = mo->angle;
-		if (thiscam == &camera)
+
+		if (thiscam == &camera[0])
 			camrotate = cv_cam_rotate.value;
-		else if (thiscam == &camera2)
+		else if (thiscam == &camera[1])
 			camrotate = cv_cam2_rotate.value;
-		else if (thiscam == &camera3)
+		else if (thiscam == &camera[2])
 			camrotate = cv_cam3_rotate.value;
-		else if (thiscam == &camera4)
+		else if (thiscam == &camera[3])
 			camrotate = cv_cam4_rotate.value;
 		else
 			camrotate = 0;
+
 		if (leveltime < introtime) // Whoooshy camera!
 		{
 			const INT32 introcam = (introtime - leveltime);
 			camrotate += introcam*5;
 		}
+
 		thiscam->angle = focusangle + FixedAngle(camrotate*FRACUNIT);
 		P_ResetCamera(player, thiscam);
 		return true;
@@ -7941,30 +7345,30 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 //	if (leveltime > 0 && timeinmap <= 0)
 //		return true;
 
-	if (demoplayback)
+	if (demo.playback)
 	{
 		focusangle = mo->angle;
 		focusaiming = 0;
 	}
 	else if (player == &players[consoleplayer])
 	{
-		focusangle = localangle;
-		focusaiming = localaiming;
+		focusangle = localangle[0];
+		focusaiming = localaiming[0];
 	}
-	else if (player == &players[secondarydisplayplayer])
+	else if (player == &players[displayplayers[1]])
 	{
-		focusangle = localangle2;
-		focusaiming = localaiming2;
+		focusangle = localangle[1];
+		focusaiming = localaiming[1];
 	}
-	else if (player == &players[thirddisplayplayer])
+	else if (player == &players[displayplayers[2]])
 	{
-		focusangle = localangle3;
-		focusaiming = localaiming3;
+		focusangle = localangle[2];
+		focusaiming = localaiming[2];
 	}
-	else if (player == &players[fourthdisplayplayer])
+	else if (player == &players[displayplayers[3]])
 	{
-		focusangle = localangle4;
-		focusaiming = localaiming4;
+		focusangle = localangle[3];
+		focusaiming = localaiming[3];
 	}
 	else
 	{
@@ -7975,17 +7379,8 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	if (P_CameraThinker(player, thiscam, resetcalled))
 		return true;
 
-	if (thiscam == &camera)
-	{
-		num = 0;
-		camspeed = cv_cam_speed.value;
-		camstill = cv_cam_still.value;
-		camrotate = cv_cam_rotate.value;
-		camdist = FixedMul(cv_cam_dist.value, mapobjectscale);
-		camheight = FixedMul(cv_cam_height.value, mapobjectscale);
-		lookback = camspin;
-	}
-	else if (thiscam == &camera2) // Camera 2
+
+	if (thiscam == &camera[1]) // Camera 2
 	{
 		num = 1;
 		camspeed = cv_cam2_speed.value;
@@ -7993,9 +7388,9 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		camrotate = cv_cam2_rotate.value;
 		camdist = FixedMul(cv_cam2_dist.value, mapobjectscale);
 		camheight = FixedMul(cv_cam2_height.value, mapobjectscale);
-		lookback = camspin2;
+		lookback = camspin[1];
 	}
-	else if (thiscam == &camera3) // Camera 3
+	else if (thiscam == &camera[2]) // Camera 3
 	{
 		num = 2;
 		camspeed = cv_cam3_speed.value;
@@ -8003,9 +7398,9 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		camrotate = cv_cam3_rotate.value;
 		camdist = FixedMul(cv_cam3_dist.value, mapobjectscale);
 		camheight = FixedMul(cv_cam3_height.value, mapobjectscale);
-		lookback = camspin3;
+		lookback = camspin[2];
 	}
-	else // Camera 4
+	else if (thiscam == &camera[3]) // Camera 4
 	{
 		num = 3;
 		camspeed = cv_cam4_speed.value;
@@ -8013,7 +7408,17 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 		camrotate = cv_cam4_rotate.value;
 		camdist = FixedMul(cv_cam4_dist.value, mapobjectscale);
 		camheight = FixedMul(cv_cam4_height.value, mapobjectscale);
-		lookback = camspin4;
+		lookback = camspin[3];
+	}
+	else // Camera 1
+	{
+		num = 0;
+		camspeed = cv_cam_speed.value;
+		camstill = cv_cam_still.value;
+		camrotate = cv_cam_rotate.value;
+		camdist = FixedMul(cv_cam_dist.value, mapobjectscale);
+		camheight = FixedMul(cv_cam_height.value, mapobjectscale);
+		lookback = camspin[0];
 	}
 
 	if (timeover)
@@ -8074,10 +7479,10 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	}
 
 	if (!resetcalled && (leveltime > starttime && timeover != 2)
-		&& ((thiscam == &camera && t_cam_rotate != -42)
-		|| (thiscam == &camera2 && t_cam2_rotate != -42)
-		|| (thiscam == &camera3 && t_cam3_rotate != -42)
-		|| (thiscam == &camera4 && t_cam4_rotate != -42)))
+		&& ((thiscam == &camera[0] && t_cam_rotate != -42)
+		|| (thiscam == &camera[1] && t_cam2_rotate != -42)
+		|| (thiscam == &camera[2] && t_cam3_rotate != -42)
+		|| (thiscam == &camera[3] && t_cam4_rotate != -42)))
 	{
 		angle = FixedAngle(camrotate*FRACUNIT);
 		thiscam->angle = angle;
@@ -8472,8 +7877,8 @@ boolean P_SpectatorJoinGame(player_t *player)
 		player->playerstate = PST_REBORN;
 
 		//Reset away view
-		if (P_IsLocalPlayer(player) && displayplayer != consoleplayer)
-			displayplayer = consoleplayer;
+		if (P_IsLocalPlayer(player) && displayplayers[0] != consoleplayer)
+			displayplayers[0] = consoleplayer;
 
 		if (changeto == 1)
 			CONS_Printf(M_GetText("%s switched to the %c%s%c.\n"), player_names[player-players], '\x85', M_GetText("Red team"), '\x80');
@@ -8496,8 +7901,8 @@ boolean P_SpectatorJoinGame(player_t *player)
 		player->playerstate = PST_REBORN;
 
 		//Reset away view
-		if (P_IsLocalPlayer(player) && displayplayer != consoleplayer)
-			displayplayer = consoleplayer;
+		if (P_IsLocalPlayer(player) && displayplayers[0] != consoleplayer)
+			displayplayers[0] = consoleplayer;
 
 		HU_AddChatText(va(M_GetText("\x82*%s entered the game."), player_names[player-players]), false);
 		return true; // no more player->mo, cannot continue.
@@ -8508,9 +7913,10 @@ boolean P_SpectatorJoinGame(player_t *player)
 static void P_CalcPostImg(player_t *player)
 {
 	sector_t *sector = player->mo->subsector->sector;
-	postimg_t *type;
+	postimg_t *type = NULL;
 	INT32 *param;
 	fixed_t pviewheight;
+	UINT8 i;
 
 	if (player->mo->eflags & MFE_VERTICALFLIP)
 		pviewheight = player->mo->z + player->mo->height - player->viewheight;
@@ -8523,25 +7929,14 @@ static void P_CalcPostImg(player_t *player)
 		pviewheight = player->awayviewmobj->z + 20*FRACUNIT;
 	}
 
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-	{
-		type = &postimgtype4;
-		param = &postimgparam4;
-	}
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-	{
-		type = &postimgtype3;
-		param = &postimgparam3;
-	}
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-	{
-		type = &postimgtype2;
-		param = &postimgparam2;
-	}
-	else
+	for (i = 0; i <= splitscreen; i++)
 	{
-		type = &postimgtype;
-		param = &postimgparam;
+		if (player == &players[displayplayers[i]])
+		{
+			type = &postimgtype[i];
+			param = &postimgparam[i];
+			break;
+		}
 	}
 
 	// see if we are in heat (no, not THAT kind of heat...)
@@ -8649,11 +8044,7 @@ void P_DoTimeOver(player_t *player)
 
 	player->pflags |= PF_TIMEOVER;
 
-	if ((player == &players[consoleplayer]
-		|| (splitscreen && player == &players[secondarydisplayplayer])
-		|| (splitscreen > 1 && player == &players[thirddisplayplayer])
-		|| (splitscreen > 2 && player == &players[fourthdisplayplayer]))
-		&& !demoplayback)
+	if (P_IsLocalPlayer(player) && !demo.playback)
 		legitimateexit = true; // SRB2kart: losing a race is still seeing it through to the end :p
 
 	if (player->mo)
@@ -8693,14 +8084,17 @@ void P_PlayerThink(player_t *player)
 
 	if (player->bot)
 	{
-		if (player->playerstate == PST_LIVE && B_CheckRespawn(player))
-			player->playerstate = PST_REBORN;
+		if (player->playerstate == PST_LIVE || player->playerstate == PST_DEAD)
+		{
+			if (B_CheckRespawn(player))
+				player->playerstate = PST_REBORN;
+		}
 		if (player->playerstate == PST_REBORN)
 			return;
 	}
 
 #ifdef SEENAMES
-	if (netgame && player == &players[displayplayer] && !(leveltime % (TICRATE/5)) && !splitscreen)
+	if (netgame && player == &players[displayplayers[0]] && !(leveltime % (TICRATE/5)) && !splitscreen)
 	{
 		seenplayer = NULL;
 
@@ -8762,6 +8156,19 @@ void P_PlayerThink(player_t *player)
 
 	cmd = &player->cmd;
 
+	//@TODO This fixes a one-tic latency on direction handling, AND makes behavior consistent while paused, but is not BC with 1.0.4. Do this for 1.1!
+#if 0
+	// SRB2kart
+	// Save the dir the player is holding
+	//  to allow items to be thrown forward or backward.
+	if (cmd->buttons & BT_FORWARD)
+		player->kartstuff[k_throwdir] = 1;
+	else if (cmd->buttons & BT_BACKWARD)
+		player->kartstuff[k_throwdir] = -1;
+	else
+		player->kartstuff[k_throwdir] = 0;
+#endif
+
 	// Add some extra randomization.
 	if (cmd->forwardmove)
 		P_RandomFixed();
@@ -9034,7 +8441,9 @@ void P_PlayerThink(player_t *player)
 		|| player->kartstuff[k_driftboost] || player->kartstuff[k_sneakertimer] || player->kartstuff[k_startboost]) && !player->kartstuff[k_invincibilitytimer] // SRB2kart
 		&& (player->speed + abs(player->mo->momz)) > FixedMul(20*FRACUNIT,player->mo->scale))
 	{
+		UINT8 i;
 		mobj_t *gmobj = P_SpawnGhostMobj(player->mo);
+
 		gmobj->fuse = 2;
 		if (leveltime & 1)
 		{
@@ -9042,15 +8451,17 @@ void P_PlayerThink(player_t *player)
 			gmobj->frame |= tr_trans70<<FF_TRANSSHIFT;
 		}
 
-		// Hide the mobj from our sights if we're the displayplayer and chasecam is off,
-		// or secondarydisplayplayer and chasecam2 is off.
+		// Hide the mobj from our sights if we're the displayplayer and chasecam is off.
 		// Why not just not spawn the mobj?  Well, I'd rather only flirt with
 		// consistency so much...
-		if ((player == &players[displayplayer] && !camera.chase)
-		|| (splitscreen && player == &players[secondarydisplayplayer] && !camera2.chase)
-		|| (splitscreen > 1 && player == &players[thirddisplayplayer] && !camera3.chase)
-		|| (splitscreen > 2 && player == &players[fourthdisplayplayer] && !camera4.chase))
-			gmobj->flags2 |= MF2_DONTDRAW;
+		for (i = 0; i <= splitscreen; i++)
+		{
+			if (player == &players[displayplayers[i]] && !camera[i].chase)
+			{
+				gmobj->flags2 |= MF2_DONTDRAW;
+				break;
+			}
+		}
 	}
 #endif
 
@@ -9086,11 +8497,11 @@ void P_PlayerThink(player_t *player)
 	if (player->powers[pw_invulnerability] && player->powers[pw_invulnerability] < UINT16_MAX)
 		player->powers[pw_invulnerability]--;
 
-	if (player->powers[pw_flashing] && player->powers[pw_flashing] < UINT16_MAX && ((player->pflags & PF_NIGHTSMODE)
-		|| (player->spectator || player->powers[pw_flashing] < K_GetKartFlashing(player))))
+	if (player->powers[pw_flashing] && player->powers[pw_flashing] < UINT16_MAX &&
+		(player->spectator || player->powers[pw_flashing] < K_GetKartFlashing(player)))
 		player->powers[pw_flashing]--;
 
-	if (player->powers[pw_tailsfly] && player->powers[pw_tailsfly] < UINT16_MAX && player->charability != CA_SWIM && !(player->powers[pw_super] && ALL7EMERALDS(player->powers[pw_emeralds]))) // tails fly counter
+	if (player->powers[pw_tailsfly] && player->powers[pw_tailsfly] < UINT16_MAX /*&& player->charability != CA_SWIM*/ && !(player->powers[pw_super] && ALL7EMERALDS(player->powers[pw_emeralds]))) // tails fly counter
 		player->powers[pw_tailsfly]--;
 
 	/* // SRB2kart - Can't drown.
@@ -9250,6 +8661,7 @@ void P_PlayerAfterThink(player_t *player)
 	ticcmd_t *cmd;
 	//INT32 oldweapon = player->currentweapon; // SRB2kart - unused
 	camera_t *thiscam = NULL; // if not one of the displayed players, just don't bother
+	UINT8 i;
 
 #ifdef PARANOIA
 	if (!player->mo)
@@ -9272,14 +8684,14 @@ void P_PlayerAfterThink(player_t *player)
 		P_PlayerInSpecialSector(player);
 #endif
 
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		thiscam = &camera4;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		thiscam = &camera3;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		thiscam = &camera2;
-	else if (player == &players[displayplayer])
-		thiscam = &camera;
+	for (i = 0; i <= splitscreen; i++)
+	{
+		if (player == &players[displayplayers[i]])
+		{
+			thiscam = &camera[i];
+			break;
+		}
+	}
 
 	if (player->playerstate == PST_DEAD)
 	{
@@ -9464,13 +8876,13 @@ void P_PlayerAfterThink(player_t *player)
 			player->mo->angle = player->mo->tracer->angle;
 
 			if (player == &players[consoleplayer])
-				localangle = player->mo->angle;
-			else if (player == &players[secondarydisplayplayer])
-				localangle2 = player->mo->angle;
-			else if (player == &players[thirddisplayplayer])
-				localangle3 = player->mo->angle;
-			else if (player == &players[fourthdisplayplayer])
-				localangle4 = player->mo->angle;
+				localangle[0] = player->mo->angle;
+			else if (player == &players[displayplayers[1]])
+				localangle[1] = player->mo->angle;
+			else if (player == &players[displayplayers[2]])
+				localangle[2] = player->mo->angle;
+			else if (player == &players[displayplayers[3]])
+				localangle[3] = player->mo->angle;
 		}
 
 		if (P_AproxDistance(player->mo->x - player->mo->tracer->x, player->mo->y - player->mo->tracer->y) > player->mo->radius)
@@ -9538,13 +8950,13 @@ void P_PlayerAfterThink(player_t *player)
 			player->mo->angle += cmd->sidemove<<ANGLETOFINESHIFT; // 2048 --> ANGLE_MAX
 
 			if (player == &players[consoleplayer])
-				localangle = player->mo->angle; // Adjust the local control angle.
-			else if (player == &players[secondarydisplayplayer])
-				localangle2 = player->mo->angle;
-			else if (player == &players[thirddisplayplayer])
-				localangle3 = player->mo->angle;
-			else if (player == &players[fourthdisplayplayer])
-				localangle4 = player->mo->angle;
+				localangle[0] = player->mo->angle; // Adjust the local control angle.
+			else if (player == &players[displayplayers[1]])
+				localangle[1] = player->mo->angle;
+			else if (player == &players[displayplayers[2]])
+				localangle[2] = player->mo->angle;
+			else if (player == &players[displayplayers[3]])
+				localangle[3] = player->mo->angle;
 		}
 	}
 
diff --git a/src/r_bsp.c b/src/r_bsp.c
index b819735e10cbf15b65866e5d396c5d7b3a0f8b5d..296cbbe87614369c1479b1095cef2a058e979358 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -252,20 +252,23 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 		mobj_t *viewmobj = viewplayer->mo;
 		INT32 heightsec;
 		boolean underwater;
+		UINT8 i;
 
-		if (splitscreen > 2 && viewplayer == &players[fourthdisplayplayer] && camera4.chase)
-			heightsec = R_PointInSubsector(camera4.x, camera4.y)->sector->heightsec;
-		else if (splitscreen > 1 && viewplayer == &players[thirddisplayplayer] && camera3.chase)
-			heightsec = R_PointInSubsector(camera3.x, camera3.y)->sector->heightsec;
-		else if (splitscreen && viewplayer == &players[secondarydisplayplayer] && camera2.chase)
-			heightsec = R_PointInSubsector(camera2.x, camera2.y)->sector->heightsec;
-		else if (camera.chase && viewplayer == &players[displayplayer])
-			heightsec = R_PointInSubsector(camera.x, camera.y)->sector->heightsec;
-		else if (viewmobj)
+		for (i = 0; i <= splitscreen; i++)
+		{
+			if (viewplayer == &players[displayplayers[i]] && camera[i].chase)
+			{
+				heightsec = R_PointInSubsector(camera[i].x, camera[i].y)->sector->heightsec;
+				break;
+			}
+		}
+
+		if (i > splitscreen && viewmobj)
 			heightsec = R_PointInSubsector(viewmobj->x, viewmobj->y)->sector->heightsec;
 		else
 			return sec;
-		underwater = heightsec != -1 && viewz <= sectors[heightsec].floorheight;
+
+		underwater = (heightsec != -1 && viewz <= sectors[heightsec].floorheight);
 
 		// Replace sector being drawn, with a copy to be hacked
 		*tempsec = *sec;
@@ -827,7 +830,7 @@ static void R_AddPolyObjects(subsector_t *sub)
 
 drawseg_t *firstseg;
 
-static void R_Subsector(size_t num, UINT8 viewnumber)
+static void R_Subsector(size_t num)
 {
 	INT32 count, floorlightlevel, ceilinglightlevel, light;
 	seg_t *line;
@@ -1149,7 +1152,7 @@ static void R_Subsector(size_t num, UINT8 viewnumber)
    // Either you must pass the fake sector and handle validcount here, on the
    // real sector, or you must account for the lighting in some other way,
    // like passing it as an argument.
-	R_AddSprites(sub->sector, (floorlightlevel+ceilinglightlevel)/2, viewnumber);
+	R_AddSprites(sub->sector, (floorlightlevel+ceilinglightlevel)/2);
 
 	firstseg = NULL;
 
@@ -1355,7 +1358,7 @@ INT32 R_GetPlaneLight(sector_t *sector, fixed_t planeheight, boolean underside)
 //
 // killough 5/2/98: reformatted, removed tail recursion
 
-void R_RenderBSPNode(INT32 bspnum, UINT8 viewnumber)
+void R_RenderBSPNode(INT32 bspnum)
 {
 	node_t *bsp;
 	INT32 side;
@@ -1366,7 +1369,7 @@ void R_RenderBSPNode(INT32 bspnum, UINT8 viewnumber)
 		// Decide which side the view point is on.
 		side = R_PointOnSide(viewx, viewy, bsp);
 		// Recursively divide front space.
-		R_RenderBSPNode(bsp->children[side], viewnumber);
+		R_RenderBSPNode(bsp->children[side]);
 
 		// Possibly divide back space.
 
@@ -1384,5 +1387,5 @@ void R_RenderBSPNode(INT32 bspnum, UINT8 viewnumber)
 		portalcullsector = NULL;
 	}
 
-	R_Subsector(bspnum == -1 ? 0 : bspnum & ~NF_SUBSECTOR, viewnumber);
+	R_Subsector(bspnum == -1 ? 0 : bspnum & ~NF_SUBSECTOR);
 }
diff --git a/src/r_bsp.h b/src/r_bsp.h
index 7810c9b5c401d87e4c135804c09dd1dd402a11e0..e3662e2e6ad23f6c25efed2ceb44f861a2dd15eb 100644
--- a/src/r_bsp.h
+++ b/src/r_bsp.h
@@ -37,7 +37,7 @@ extern INT32 doorclosed;
 void R_ClearClipSegs(void);
 void R_PortalClearClipSegs(INT32 start, INT32 end);
 void R_ClearDrawSegs(void);
-void R_RenderBSPNode(INT32 bspnum, UINT8 viewnumber);
+void R_RenderBSPNode(INT32 bspnum);
 void R_AddPortal(INT32 line1, INT32 line2, INT32 x1, INT32 x2);
 
 #ifdef POLYOBJECTS
diff --git a/src/r_data.c b/src/r_data.c
index 1a74f7336ed1cf354a9e44c0b4640909015b5284..7fb11855ffc1f807823df33424a676fdb0471e17 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -1600,7 +1600,7 @@ void R_PrecacheLevel(void)
 	thinker_t *th;
 	spriteframe_t *sf;
 
-	if (demoplayback)
+	if (demo.playback)
 		return;
 
 	// do not flush the memory, Z_Malloc twice with same user will cause error in Z_CheckHeap()
diff --git a/src/r_main.c b/src/r_main.c
index 36182d0e889f6d1d69ea8a85bec145ef66cc8eb4..358a24bb89cd9f3175b8452dce0ea868c590ab8d 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 "doomstat.h" // MAXSPLITSCREENPLAYERS
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -65,9 +66,10 @@ size_t loopcount;
 
 fixed_t viewx, viewy, viewz;
 angle_t viewangle, aimingangle;
+UINT8 viewssnum;
 fixed_t viewcos, viewsin;
 boolean viewsky, skyVisible;
-boolean skyVisible1, skyVisible2, skyVisible3, skyVisible4; // saved values of skyVisible for P1/P2/P3/P4, for splitscreen
+boolean skyVisiblePerPlayer[MAXSPLITSCREENPLAYERS]; // saved values of skyVisible for each splitscreen player
 sector_t *viewsector;
 player_t *viewplayer;
 
@@ -193,19 +195,12 @@ void SplitScreen_OnChange(void)
 	// recompute screen size
 	R_ExecuteSetViewSize();
 
-	if (!demoplayback && !botingame)
+	if (!demo.playback && !botingame)
 	{
-		for (i = 1; i < 3; i++)
+		for (i = 1; i < MAXSPLITSCREENPLAYERS; i++)
 		{
 			if (i > splitscreen)
-			{
-				if (i == 1)
-					CL_RemoveSplitscreenPlayer(secondarydisplayplayer);
-				else if (i == 2)
-					CL_RemoveSplitscreenPlayer(thirddisplayplayer);
-				else if (i == 3)
-					CL_RemoveSplitscreenPlayer(fourthdisplayplayer);
-			}
+				CL_RemoveSplitscreenPlayer(displayplayers[i]);
 			else
 				CL_AddSplitscreenPlayer();
 		}
@@ -215,21 +210,27 @@ void SplitScreen_OnChange(void)
 	}
 	else
 	{
-		secondarydisplayplayer = consoleplayer;
-		thirddisplayplayer = consoleplayer;
-		fourthdisplayplayer = consoleplayer;
+		for (i = 1; i < MAXSPLITSCREENPLAYERS; i++)
+			displayplayers[i] = consoleplayer;
+
 		for (i = 0; i < MAXPLAYERS; i++)
+		{
 			if (playeringame[i] && i != consoleplayer)
 			{
-				if (secondarydisplayplayer == consoleplayer)
-					secondarydisplayplayer = i;
-				else if (thirddisplayplayer == consoleplayer)
-					thirddisplayplayer = i;
-				else if (fourthdisplayplayer == consoleplayer)
-					fourthdisplayplayer = i;
-				else
+				UINT8 j;
+				for (j = 1; j < MAXSPLITSCREENPLAYERS; j++)
+				{
+					if (displayplayers[j] == consoleplayer)
+					{
+						displayplayers[j] = i;
+						break;
+					}
+				}
+
+				if (j == MAXSPLITSCREENPLAYERS)
 					break;
 			}
+		}
 	}
 }
 static void Fov_OnChange(void)
@@ -844,16 +845,20 @@ static void R_SetupFreelook(void)
 
 void R_SkyboxFrame(player_t *player)
 {
-	camera_t *thiscam;
+	camera_t *thiscam = &camera[0];
+	UINT8 i;
 
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		thiscam = &camera4;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		thiscam = &camera3;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		thiscam = &camera2;
-	else
-		thiscam = &camera;
+	if (splitscreen)
+	{
+		for (i = 1; i <= splitscreen; i++)
+		{
+			if (player == &players[displayplayers[i]])
+			{
+				thiscam = &camera[i];
+				break;
+			}
+		}
+	}
 
 	// cut-away view stuff
 	viewsky = true;
@@ -879,27 +884,24 @@ void R_SkyboxFrame(player_t *player)
 	{
 		aimingangle = player->aiming;
 		viewangle = player->mo->angle;
-		if (/*!demoplayback && */player->playerstate != PST_DEAD)
+		if (/*!demo.playback && */player->playerstate != PST_DEAD)
 		{
 			if (player == &players[consoleplayer])
 			{
-				viewangle = localangle; // WARNING: camera uses this
-				aimingangle = localaiming;
+				viewangle = localangle[0]; // WARNING: camera uses this
+				aimingangle = localaiming[0];
 			}
-			else if (player == &players[secondarydisplayplayer])
+			else if (splitscreen)
 			{
-				viewangle = localangle2;
-				aimingangle = localaiming2;
-			}
-			else if (player == &players[thirddisplayplayer])
-			{
-				viewangle = localangle3;
-				aimingangle = localaiming3;
-			}
-			else if (player == &players[fourthdisplayplayer])
-			{
-				viewangle = localangle4;
-				aimingangle = localaiming4;
+				for (i = 1; i <= splitscreen; i++)
+				{
+					if (player == &players[displayplayers[i]])
+					{
+						viewangle = localangle[i];
+						aimingangle = localaiming[i];
+						break;
+					}
+				}
 			}
 		}
 	}
@@ -1078,24 +1080,24 @@ void R_SetupFrame(player_t *player, boolean skybox)
 	camera_t *thiscam;
 	boolean chasecam = false;
 
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
+	if (splitscreen > 2 && player == &players[displayplayers[3]])
 	{
-		thiscam = &camera4;
+		thiscam = &camera[3];
 		chasecam = (cv_chasecam4.value != 0);
 	}
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
+	else if (splitscreen > 1 && player == &players[displayplayers[2]])
 	{
-		thiscam = &camera3;
+		thiscam = &camera[2];
 		chasecam = (cv_chasecam3.value != 0);
 	}
-	else if (splitscreen && player == &players[secondarydisplayplayer])
+	else if (splitscreen && player == &players[displayplayers[1]])
 	{
-		thiscam = &camera2;
+		thiscam = &camera[1];
 		chasecam = (cv_chasecam2.value != 0);
 	}
 	else
 	{
-		thiscam = &camera;
+		thiscam = &camera[0];
 		chasecam = (cv_chasecam.value != 0);
 	}
 
@@ -1141,27 +1143,25 @@ void R_SetupFrame(player_t *player, boolean skybox)
 		aimingangle = player->aiming;
 		viewangle = viewmobj->angle;
 
-		if (/*!demoplayback && */player->playerstate != PST_DEAD)
+		if (/*!demo.playback && */player->playerstate != PST_DEAD)
 		{
 			if (player == &players[consoleplayer])
 			{
-				viewangle = localangle; // WARNING: camera uses this
-				aimingangle = localaiming;
+				viewangle = localangle[0]; // WARNING: camera uses this
+				aimingangle = localaiming[0];
 			}
-			else if (player == &players[secondarydisplayplayer])
+			else if (splitscreen)
 			{
-				viewangle = localangle2;
-				aimingangle = localaiming2;
-			}
-			else if (player == &players[thirddisplayplayer])
-			{
-				viewangle = localangle3;
-				aimingangle = localaiming3;
-			}
-			else if (player == &players[fourthdisplayplayer])
-			{
-				viewangle = localangle4;
-				aimingangle = localaiming4;
+				UINT8 i;
+				for (i = 1; i <= splitscreen; i++)
+				{
+					if (player == &players[displayplayers[i]])
+					{
+						viewangle = localangle[i];
+						aimingangle = localaiming[i];
+						break;
+					}
+				}
 			}
 		}
 	}
@@ -1323,19 +1323,10 @@ void R_RenderPlayerView(player_t *player)
 {
 	portal_pair *portal;
 	const boolean skybox = (skyboxmo[0] && cv_skybox.value);
-	UINT8 viewnumber;
-
-	if (player == &players[secondarydisplayplayer] && splitscreen)
-		viewnumber = 1;
-	else if (player == &players[thirddisplayplayer] && splitscreen > 1)
-		viewnumber = 2;
-	else if (player == &players[fourthdisplayplayer] && splitscreen > 2)
-		viewnumber = 3;
-	else
-		viewnumber = 0;
+	UINT8 i;
 
 	// if this is display player 1
-	if (cv_homremoval.value && player == &players[displayplayer])
+	if (cv_homremoval.value && player == &players[displayplayers[0]])
 	{
 		if (cv_homremoval.value == 1)
 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); // No HOM effect!
@@ -1343,7 +1334,7 @@ void R_RenderPlayerView(player_t *player)
 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 128+(timeinmap&15));
 	}
 	// Draw over the fourth screen so you don't have to stare at a HOM :V
-	else if (splitscreen == 2 && player == &players[thirddisplayplayer])
+	else if (splitscreen == 2 && player == &players[displayplayers[2]])
 #if 1
 	{
 		// V_DrawPatchFill, but for the fourth screen only
@@ -1362,14 +1353,14 @@ void R_RenderPlayerView(player_t *player)
 #endif
 
 	// load previous saved value of skyVisible for the player
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		skyVisible = skyVisible4;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		skyVisible = skyVisible3;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		skyVisible = skyVisible2;
-	else
-		skyVisible = skyVisible1;
+	for (i = 0; i <= splitscreen; i++)
+	{
+		if (player == &players[displayplayers[i]])
+		{
+			skyVisible = skyVisiblePerPlayer[i];
+			break;
+		}
+	}
 
 	portalrender = 0;
 	portal_base = portal_cap = NULL;
@@ -1386,7 +1377,7 @@ void R_RenderPlayerView(player_t *player)
 		R_ClearVisibleFloorSplats();
 #endif
 
-		R_RenderBSPNode((INT32)numnodes - 1, viewnumber);
+		R_RenderBSPNode((INT32)numnodes - 1);
 		R_ClipSprites();
 		R_DrawPlanes();
 #ifdef FLOORSPLATS
@@ -1419,7 +1410,7 @@ void R_RenderPlayerView(player_t *player)
 	mytotal = 0;
 	ProfZeroTimer();
 #endif
-	R_RenderBSPNode((INT32)numnodes - 1, viewnumber);
+	R_RenderBSPNode((INT32)numnodes - 1);
 	R_ClipSprites();
 #ifdef TIMING
 	RDMSR(0x10, &mycount);
@@ -1444,7 +1435,7 @@ void R_RenderPlayerView(player_t *player)
 
 		validcount++;
 
-		R_RenderBSPNode((INT32)numnodes - 1, viewnumber);
+		R_RenderBSPNode((INT32)numnodes - 1);
 		R_ClipSprites();
 		//R_DrawPlanes();
 		//R_DrawMasked();
@@ -1470,16 +1461,16 @@ void R_RenderPlayerView(player_t *player)
 	// Check for new console commands.
 	NetUpdate();
 
-	// save value to skyVisible1 or skyVisible2
+	// save value to skyVisiblePerPlayer
 	// this is so that P1 can't affect whether P2 can see a skybox or not, or vice versa
-	if (splitscreen > 2 && player == &players[fourthdisplayplayer])
-		skyVisible4 = skyVisible;
-	else if (splitscreen > 1 && player == &players[thirddisplayplayer])
-		skyVisible3 = skyVisible;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
-		skyVisible2 = skyVisible;
-	else
-		skyVisible1 = skyVisible;
+	for (i = 0; i <= splitscreen; i++)
+	{
+		if (player == &players[displayplayers[i]])
+		{
+			skyVisiblePerPlayer[i] = skyVisible;
+			break;
+		}
+	}
 }
 
 // =========================================================================
@@ -1561,7 +1552,6 @@ void R_RegisterEngineStuff(void)
 	CV_RegisterVar(&cv_grgammared);
 	CV_RegisterVar(&cv_grfovchange);
 	CV_RegisterVar(&cv_grfog);
-	CV_RegisterVar(&cv_voodoocompatibility);
 	CV_RegisterVar(&cv_grfogcolor);
 	CV_RegisterVar(&cv_grsoftwarefog);
 #ifdef ALAM_LIGHTING
@@ -1570,7 +1560,9 @@ void R_RegisterEngineStuff(void)
 	CV_RegisterVar(&cv_grcoronas);
 	CV_RegisterVar(&cv_grcoronasize);
 #endif
-	CV_RegisterVar(&cv_grmd2);
+	CV_RegisterVar(&cv_grmdls);
+	CV_RegisterVar(&cv_grfallbackplayermodel);
+	CV_RegisterVar(&cv_grspritebillboarding);
 #endif
 
 #ifdef HWRENDER
diff --git a/src/r_plane.c b/src/r_plane.c
index 0ff97fcc326fd72c7d4494a43d18949ede194d91..db5bfbda28b3b98a22d6abf51eee2a7b5f433161 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -883,12 +883,12 @@ void R_DrawSinglePlane(visplane_t *pl)
 				if (bottom > vid.height)
 					bottom = vid.height;
 
-				if (splitscreen > 2 && viewplayer == &players[fourthdisplayplayer]) // Only copy the part of the screen we need
+				if (splitscreen > 2 && viewplayer == &players[displayplayers[3]]) // Only copy the part of the screen we need
 					scr = (screens[0] + (top+(viewheight))*vid.width + viewwidth);
-				else if ((splitscreen == 1 && viewplayer == &players[secondarydisplayplayer])
-					|| (splitscreen > 1 && viewplayer == &players[thirddisplayplayer]))
+				else if ((splitscreen == 1 && viewplayer == &players[displayplayers[1]])
+					|| (splitscreen > 1 && viewplayer == &players[displayplayers[2]]))
 					scr = (screens[0] + (top+(viewheight))*vid.width);
-				else if (splitscreen > 1 && viewplayer == &players[secondarydisplayplayer])
+				else if (splitscreen > 1 && viewplayer == &players[displayplayers[1]])
 					scr = (screens[0] + ((top)*vid.width) + viewwidth);
 				else
 					scr = (screens[0] + ((top)*vid.width));
diff --git a/src/r_segs.c b/src/r_segs.c
index 1e8f27dda0cb94ba3f9725afa59ebdf544a8644e..399f514bcaa111c99b70a622a9b44d75129474eb 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -862,8 +862,8 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 			if (leftheight > pfloorleft && rightheight > pfloorright && i+1 < dc_numlights)
 			{
 				lightlist_t *nextlight = &frontsector->lightlist[i+1];
-				if (nextlight->slope ? P_GetZAt(nextlight->slope, ds->leftpos.x, ds->leftpos.y) : nextlight->height > pfloorleft
-				 && nextlight->slope ? P_GetZAt(nextlight->slope, ds->rightpos.x, ds->rightpos.y) : nextlight->height > pfloorright)
+				if ((nextlight->slope ? P_GetZAt(nextlight->slope, ds->leftpos.x, ds->leftpos.y) : nextlight->height) > pfloorleft
+				 && (nextlight->slope ? P_GetZAt(nextlight->slope, ds->rightpos.x, ds->rightpos.y) : nextlight->height) > pfloorright)
 					continue;
 			}
 
diff --git a/src/r_state.h b/src/r_state.h
index d6d123e99ee17d846aa94ef1d4f9c039106ea1a2..e37bdf52ef4c5a44773e3b915ccecd705b3f878e 100644
--- a/src/r_state.h
+++ b/src/r_state.h
@@ -17,6 +17,7 @@
 // Need data structure definitions.
 #include "d_player.h"
 #include "r_data.h"
+#include "doomstat.h" // MAXSPLITSCREENPLAYERS
 
 #ifdef __GNUG__
 #pragma interface
@@ -88,8 +89,9 @@ extern side_t *sides;
 //
 extern fixed_t viewx, viewy, viewz;
 extern angle_t viewangle, aimingangle;
+extern UINT8 viewssnum; // splitscreen view number
 extern boolean viewsky, skyVisible;
-extern boolean skyVisible1, skyVisible2, skyVisible3, skyVisible4; // saved values of skyVisible for P1 and P2, for splitscreen
+extern boolean skyVisiblePerPlayer[MAXSPLITSCREENPLAYERS]; // saved values of skyVisible of each splitscreen player
 extern sector_t *viewsector;
 extern player_t *viewplayer;
 extern UINT8 portalrender;
diff --git a/src/r_things.c b/src/r_things.c
index c43fe8324660540191a4d953f97ea00df92e6b38..b2170924e68965910220373abd39ecfbf22b5409 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -40,6 +40,8 @@ int	snprintf(char *str, size_t n, const char *fmt, ...);
 //int	vsnprintf(char *str, size_t n, const char *fmt, va_list ap);
 #endif
 
+CV_PossibleValue_t Forceskin_cons_t[MAXSKINS+2];
+
 static void R_InitSkins(void);
 
 #define MINZ (FRACUNIT*4)
@@ -925,6 +927,13 @@ static void R_DrawVisSprite(vissprite_t *vis)
 	if (vis->x2 >= vid.width)
 		vis->x2 = vid.width-1;
 
+#if 1
+	// Something is occasionally setting 1px-wide sprites whose frac is exactly the width of the sprite, causing crashes due to
+	// accessing invalid column info. Until the cause is found, let's try to correct those manually...
+	while (frac + vis->xiscale*(vis->x2-vis->x1) > SHORT(patch->width)<<FRACBITS && vis->x2 >= vis->x1)
+		vis->x2--;
+#endif
+
 	for (dc_x = vis->x1; dc_x <= vis->x2; dc_x++, frac += vis->xiscale)
 	{
 		if (vis->scalestep) // currently papersprites only
@@ -1695,7 +1704,7 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 // R_AddSprites
 // During BSP traversal, this adds sprites by sector.
 //
-void R_AddSprites(sector_t *sec, INT32 lightlevel, UINT8 viewnumber)
+void R_AddSprites(sector_t *sec, INT32 lightlevel)
 {
 	mobj_t *thing;
 	precipmobj_t *precipthing; // Tails 08-25-2002
@@ -1741,19 +1750,19 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel, UINT8 viewnumber)
 			if (splitscreen)
 			{
 				if (thing->eflags & MFE_DRAWONLYFORP1)
-					if (viewnumber != 0)
+					if (viewssnum != 0)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP2)
-					if (viewnumber != 1)
+					if (viewssnum != 1)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP3 && splitscreen > 1)
-					if (viewnumber != 2)
+					if (viewssnum != 2)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP4 && splitscreen > 2)
-					if (viewnumber != 3)
+					if (viewssnum != 3)
 						continue;
 			}
 
@@ -1776,19 +1785,19 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel, UINT8 viewnumber)
 			if (splitscreen)
 			{
 				if (thing->eflags & MFE_DRAWONLYFORP1)
-					if (viewnumber != 0)
+					if (viewssnum != 0)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP2)
-					if (viewnumber != 1)
+					if (viewssnum != 1)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP3 && splitscreen > 1)
-					if (viewnumber != 2)
+					if (viewssnum != 2)
 						continue;
 
 				if (thing->eflags & MFE_DRAWONLYFORP4 && splitscreen > 2)
-					if (viewnumber != 3)
+					if (viewssnum != 3)
 						continue;
 			}
 
@@ -2581,6 +2590,10 @@ void R_InitSkins(void)
 	skin->spritedef.spriteframes = sprites[SPR_PLAY].spriteframes;
 	ST_LoadFaceGraphics(skin->facerank, skin->facewant, skin->facemmap, 0);
 
+	// Set values for Sonic skin
+	Forceskin_cons_t[1].value = 0;
+	Forceskin_cons_t[1].strvalue = skin->name;
+
 	//MD2 for sonic doesn't want to load in Linux.
 #ifdef HWRENDER
 	if (rendermode == render_opengl)
@@ -2646,15 +2659,15 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
 		player->kartspeed = skin->kartspeed;
 		player->kartweight = skin->kartweight;
 
-		/*if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback || modeattacking))
+		/*if (!(cv_debug || devparm) && !(netgame || multiplayer || demo.playback || modeattacking))
 		{
 			if (playernum == consoleplayer)
 				CV_StealthSetValue(&cv_playercolor, skin->prefcolor);
-			else if (playernum == secondarydisplayplayer)
+			else if (playernum == displayplayers[1])
 				CV_StealthSetValue(&cv_playercolor2, skin->prefcolor);
-			else if (playernum == thirddisplayplayer)
+			else if (playernum == displayplayers[2])
 				CV_StealthSetValue(&cv_playercolor3, skin->prefcolor);
-			else if (playernum == fourthdisplayplayer)
+			else if (playernum == displayplayers[3])
 				CV_StealthSetValue(&cv_playercolor4, skin->prefcolor);
 			player->skincolor = skin->prefcolor;
 			if (player->mo)
@@ -2663,6 +2676,9 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
 
 		if (player->mo)
 			P_SetScale(player->mo, player->mo->scale);
+
+		demo_extradata[playernum] |= DXD_SKIN;
+
 		return;
 	}
 
@@ -2964,6 +2980,10 @@ next_token:
 		skin_cons_t[numskins].strvalue = skin->name;
 #endif
 
+		// Update the forceskin possiblevalues
+		Forceskin_cons_t[numskins+1].value = numskins;
+		Forceskin_cons_t[numskins+1].strvalue = skins[numskins].name;
+
 		// add face graphics
 		ST_LoadFaceGraphics(skin->facerank, skin->facewant, skin->facemmap, numskins);
 
diff --git a/src/r_things.h b/src/r_things.h
index 6f48cc5bffb11fa055c37c5c0e47854a56b01a05..697cde2564ffdf69378a90e0fb608ef19d1a450e 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -55,7 +55,7 @@ void R_DelSpriteDefs(UINT16 wadnum);
 #endif
 
 //SoM: 6/5/2000: Light sprites correctly!
-void R_AddSprites(sector_t *sec, INT32 lightlevel, UINT8 viewnumber);
+void R_AddSprites(sector_t *sec, INT32 lightlevel);
 void R_InitSprites(void);
 void R_ClearSprites(void);
 void R_ClipSprites(void);
@@ -97,6 +97,8 @@ typedef struct
 	sfxenum_t soundsid[NUMSKINSOUNDS]; // sound # in S_sfx table
 } skin_t;
 
+extern CV_PossibleValue_t Forceskin_cons_t[];
+
 // -----------
 // NOT SKINS STUFF !
 // -----------
diff --git a/src/s_sound.c b/src/s_sound.c
index 58cc0592f771e746ef41a69ea48e42023c2ce2c0..21b668f28756531532ad84ffea7afceac8076067 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -38,6 +38,10 @@ extern INT32 msg_id;
 #include "p_local.h" // camera info
 #include "m_misc.h" // for tunes command
 
+#if defined(HAVE_BLUA) && defined(HAVE_LUA_MUSICPLUS)
+#include "lua_hook.h" // MusicChange hook
+#endif
+
 #ifdef HW3SOUND
 // 3D Sound Interface
 #include "hardware/hw3sound.h"
@@ -438,7 +442,7 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
 	listener_t listener3 = {0,0,0,0};
 	listener_t listener4 = {0,0,0,0};
 
-	mobj_t *listenmobj = players[displayplayer].mo;
+	mobj_t *listenmobj = players[displayplayers[0]].mo;
 	mobj_t *listenmobj2 = NULL;
 	mobj_t *listenmobj3 = NULL;
 	mobj_t *listenmobj4 = NULL;
@@ -450,26 +454,26 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
 	if (sfx_id == sfx_None)
 		return;
 
-	if (players[displayplayer].awayviewtics)
-		listenmobj = players[displayplayer].awayviewmobj;
+	if (players[displayplayers[0]].awayviewtics)
+		listenmobj = players[displayplayers[0]].awayviewmobj;
 
 	if (splitscreen)
 	{
-		listenmobj2 = players[secondarydisplayplayer].mo;
-		if (players[secondarydisplayplayer].awayviewtics)
-			listenmobj2 = players[secondarydisplayplayer].awayviewmobj;
+		listenmobj2 = players[displayplayers[1]].mo;
+		if (players[displayplayers[1]].awayviewtics)
+			listenmobj2 = players[displayplayers[1]].awayviewmobj;
 
 		if (splitscreen > 1)
 		{
-			listenmobj3 = players[thirddisplayplayer].mo;
-			if (players[thirddisplayplayer].awayviewtics)
-				listenmobj3 = players[thirddisplayplayer].awayviewmobj;
+			listenmobj3 = players[displayplayers[2]].mo;
+			if (players[displayplayers[2]].awayviewtics)
+				listenmobj3 = players[displayplayers[2]].awayviewmobj;
 
 			if (splitscreen > 2)
 			{
-				listenmobj4 = players[fourthdisplayplayer].mo;
-				if (players[fourthdisplayplayer].awayviewtics)
-					listenmobj4 = players[fourthdisplayplayer].awayviewmobj;
+				listenmobj4 = players[displayplayers[3]].mo;
+				if (players[displayplayers[3]].awayviewtics)
+					listenmobj4 = players[displayplayers[3]].awayviewmobj;
 			}
 		}
 	}
@@ -482,12 +486,12 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
 	};
 #endif
 
-	if (camera.chase && !players[displayplayer].awayviewtics)
+	if (camera[0].chase && !players[displayplayers[0]].awayviewtics)
 	{
-		listener.x = camera.x;
-		listener.y = camera.y;
-		listener.z = camera.z;
-		listener.angle = camera.angle;
+		listener.x = camera[0].x;
+		listener.y = camera[0].y;
+		listener.z = camera[0].z;
+		listener.angle = camera[0].angle;
 	}
 	else if (listenmobj)
 	{
@@ -501,12 +505,12 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
 
 	if (listenmobj2)
 	{
-		if (camera2.chase && !players[secondarydisplayplayer].awayviewtics)
+		if (camera[1].chase && !players[displayplayers[1]].awayviewtics)
 		{
-			listener2.x = camera2.x;
-			listener2.y = camera2.y;
-			listener2.z = camera2.z;
-			listener2.angle = camera2.angle;
+			listener2.x = camera[1].x;
+			listener2.y = camera[1].y;
+			listener2.z = camera[1].z;
+			listener2.angle = camera[1].angle;
 		}
 		else
 		{
@@ -519,12 +523,12 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
 
 	if (listenmobj3)
 	{
-		if (camera3.chase && !players[thirddisplayplayer].awayviewtics)
+		if (camera[2].chase && !players[displayplayers[2]].awayviewtics)
 		{
-			listener3.x = camera3.x;
-			listener3.y = camera3.y;
-			listener3.z = camera3.z;
-			listener3.angle = camera3.angle;
+			listener3.x = camera[2].x;
+			listener3.y = camera[2].y;
+			listener3.z = camera[2].z;
+			listener3.angle = camera[2].angle;
 		}
 		else
 		{
@@ -537,12 +541,12 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
 
 	if (listenmobj4)
 	{
-		if (camera4.chase && !players[fourthdisplayplayer].awayviewtics)
+		if (camera[3].chase && !players[displayplayers[3]].awayviewtics)
 		{
-			listener4.x = camera4.x;
-			listener4.y = camera4.y;
-			listener4.z = camera4.z;
-			listener4.angle = camera4.angle;
+			listener4.x = camera[3].x;
+			listener4.y = camera[3].y;
+			listener4.z = camera[3].z;
+			listener4.angle = camera[3].angle;
 		}
 		else
 		{
@@ -899,7 +903,7 @@ void S_UpdateSounds(void)
 	listener_t listener3;
 	listener_t listener4;
 
-	mobj_t *listenmobj = players[displayplayer].mo;
+	mobj_t *listenmobj = players[displayplayers[0]].mo;
 	mobj_t *listenmobj2 = NULL;
 	mobj_t *listenmobj3 = NULL;
 	mobj_t *listenmobj4 = NULL;
@@ -935,36 +939,36 @@ void S_UpdateSounds(void)
 	if (dedicated || sound_disabled)
 		return;
 
-	if (players[displayplayer].awayviewtics)
-		listenmobj = players[displayplayer].awayviewmobj;
+	if (players[displayplayers[0]].awayviewtics)
+		listenmobj = players[displayplayers[0]].awayviewmobj;
 
 	if (splitscreen)
 	{
-		listenmobj2 = players[secondarydisplayplayer].mo;
-		if (players[secondarydisplayplayer].awayviewtics)
-			listenmobj2 = players[secondarydisplayplayer].awayviewmobj;
+		listenmobj2 = players[displayplayers[1]].mo;
+		if (players[displayplayers[1]].awayviewtics)
+			listenmobj2 = players[displayplayers[1]].awayviewmobj;
 
 		if (splitscreen > 1)
 		{
-			listenmobj3 = players[thirddisplayplayer].mo;
-			if (players[thirddisplayplayer].awayviewtics)
-				listenmobj3 = players[thirddisplayplayer].awayviewmobj;
+			listenmobj3 = players[displayplayers[2]].mo;
+			if (players[displayplayers[2]].awayviewtics)
+				listenmobj3 = players[displayplayers[2]].awayviewmobj;
 
 			if (splitscreen > 2)
 			{
-				listenmobj4 = players[fourthdisplayplayer].mo;
-				if (players[fourthdisplayplayer].awayviewtics)
-					listenmobj4 = players[fourthdisplayplayer].awayviewmobj;
+				listenmobj4 = players[displayplayers[3]].mo;
+				if (players[displayplayers[3]].awayviewtics)
+					listenmobj4 = players[displayplayers[3]].awayviewmobj;
 			}
 		}
 	}
 
-	if (camera.chase && !players[displayplayer].awayviewtics)
+	if (camera[0].chase && !players[displayplayers[0]].awayviewtics)
 	{
-		listener.x = camera.x;
-		listener.y = camera.y;
-		listener.z = camera.z;
-		listener.angle = camera.angle;
+		listener.x = camera[0].x;
+		listener.y = camera[0].y;
+		listener.z = camera[0].z;
+		listener.angle = camera[0].angle;
 	}
 	else if (listenmobj)
 	{
@@ -989,12 +993,12 @@ void S_UpdateSounds(void)
 
 	if (listenmobj2)
 	{
-		if (camera2.chase && !players[secondarydisplayplayer].awayviewtics)
+		if (camera[1].chase && !players[displayplayers[1]].awayviewtics)
 		{
-			listener2.x = camera2.x;
-			listener2.y = camera2.y;
-			listener2.z = camera2.z;
-			listener2.angle = camera2.angle;
+			listener2.x = camera[1].x;
+			listener2.y = camera[1].y;
+			listener2.z = camera[1].z;
+			listener2.angle = camera[1].angle;
 		}
 		else
 		{
@@ -1007,12 +1011,12 @@ void S_UpdateSounds(void)
 
 	if (listenmobj3)
 	{
-		if (camera3.chase && !players[thirddisplayplayer].awayviewtics)
+		if (camera[2].chase && !players[displayplayers[2]].awayviewtics)
 		{
-			listener3.x = camera3.x;
-			listener3.y = camera3.y;
-			listener3.z = camera3.z;
-			listener3.angle = camera3.angle;
+			listener3.x = camera[2].x;
+			listener3.y = camera[2].y;
+			listener3.z = camera[2].z;
+			listener3.angle = camera[2].angle;
 		}
 		else
 		{
@@ -1025,12 +1029,12 @@ void S_UpdateSounds(void)
 
 	if (listenmobj4)
 	{
-		if (camera4.chase && !players[fourthdisplayplayer].awayviewtics)
+		if (camera[3].chase && !players[displayplayers[3]].awayviewtics)
 		{
-			listener4.x = camera4.x;
-			listener4.y = camera4.y;
-			listener4.z = camera4.z;
-			listener4.angle = camera4.angle;
+			listener4.x = camera[3].x;
+			listener4.y = camera[3].y;
+			listener4.z = camera[3].z;
+			listener4.angle = camera[3].angle;
 		}
 		else
 		{
@@ -1060,9 +1064,9 @@ void S_UpdateSounds(void)
 				// check non-local sounds for distance clipping
 				//  or modify their params
 				if (c->origin && ((c->origin != players[consoleplayer].mo)
-					|| (splitscreen && c->origin != players[secondarydisplayplayer].mo)
-					|| (splitscreen > 1 && c->origin != players[thirddisplayplayer].mo)
-					|| (splitscreen > 2 && c->origin != players[fourthdisplayplayer].mo)))
+					|| (splitscreen && c->origin != players[displayplayers[1]].mo)
+					|| (splitscreen > 1 && c->origin != players[displayplayers[2]].mo)
+					|| (splitscreen > 2 && c->origin != players[displayplayers[3]].mo)))
 				{
 					// Whomever is closer gets the sound, but only in splitscreen.
 					if (splitscreen)
@@ -1071,13 +1075,10 @@ void S_UpdateSounds(void)
 						fixed_t recdist = -1;
 						INT32 i, p = -1;
 
-						for (i = 0; i < 4; i++)
+						for (i = 0; i <= splitscreen; i++)
 						{
 							fixed_t thisdist = -1;
 
-							if (i > splitscreen)
-								break;
-
 							if (i == 0 && listenmobj)
 								thisdist = P_AproxDistance(listener.x-soundmobj->x, listener.y-soundmobj->y);
 							else if (i == 1 && listenmobj2)
@@ -1250,33 +1251,33 @@ INT32 S_AdjustSoundParams(const mobj_t *listener, const mobj_t *source, INT32 *v
 	if (!listener)
 		return false;
 
-	if (listener == players[displayplayer].mo && camera.chase)
+	if (listener == players[displayplayers[0]].mo && camera[0].chase)
 	{
-		listensource.x = camera.x;
-		listensource.y = camera.y;
-		listensource.z = camera.z;
-		listensource.angle = camera.angle;
+		listensource.x = camera[0].x;
+		listensource.y = camera[0].y;
+		listensource.z = camera[0].z;
+		listensource.angle = camera[0].angle;
 	}
-	else if (splitscreen && listener == players[secondarydisplayplayer].mo && camera2.chase)
+	else if (splitscreen && listener == players[displayplayers[1]].mo && camera[1].chase)
 	{
-		listensource.x = camera2.x;
-		listensource.y = camera2.y;
-		listensource.z = camera2.z;
-		listensource.angle = camera2.angle;
+		listensource.x = camera[1].x;
+		listensource.y = camera[1].y;
+		listensource.z = camera[1].z;
+		listensource.angle = camera[1].angle;
 	}
-	else if (splitscreen > 1 && listener == players[thirddisplayplayer].mo && camera3.chase)
+	else if (splitscreen > 1 && listener == players[displayplayers[2]].mo && camera[2].chase)
 	{
-		listensource.x = camera3.x;
-		listensource.y = camera3.y;
-		listensource.z = camera3.z;
-		listensource.angle = camera3.angle;
+		listensource.x = camera[2].x;
+		listensource.y = camera[2].y;
+		listensource.z = camera[2].z;
+		listensource.angle = camera[2].angle;
 	}
-	else if (splitscreen > 2 && listener == players[fourthdisplayplayer].mo && camera4.chase)
+	else if (splitscreen > 2 && listener == players[displayplayers[3]].mo && camera[3].chase)
 	{
-		listensource.x = camera4.x;
-		listensource.y = camera4.y;
-		listensource.z = camera4.z;
-		listensource.angle = camera4.angle;
+		listensource.x = camera[3].x;
+		listensource.y = camera[3].y;
+		listensource.z = camera[3].z;
+		listensource.angle = camera[3].angle;
 	}
 	else
 	{
@@ -1550,6 +1551,12 @@ static void      *music_data;
 static UINT16    music_flags;
 static boolean   music_looping;
 
+static char      queue_name[7];
+static UINT16    queue_flags;
+static boolean   queue_looping;
+static UINT32    queue_position;
+static UINT32    queue_fadeinms;
+
 /// ------------------------
 /// Music Definitions
 /// ------------------------
@@ -1733,7 +1740,7 @@ void S_ShowMusicCredit(void)
 {
 	musicdef_t *def = musicdefstart;
 
-	if (!cv_songcredits.value)
+	if (!cv_songcredits.value || demo.rewinding)
 		return;
 
 	if (!def) // No definitions
@@ -1788,6 +1795,11 @@ musictype_t S_MusicType(void)
 	return I_SongType();
 }
 
+const char *S_MusicName(void)
+{
+	return music_name;
+}
+
 boolean S_MusicInfo(char *mname, UINT16 *mflags, boolean *looping)
 {
 	if (!I_SongPlaying())
@@ -1818,6 +1830,35 @@ boolean S_SpeedMusic(float speed)
 	return I_SetSongSpeed(speed);
 }
 
+/// ------------------------
+/// Music Seeking
+/// ------------------------
+
+UINT32 S_GetMusicLength(void)
+{
+	return I_GetSongLength();
+}
+
+boolean S_SetMusicLoopPoint(UINT32 looppoint)
+{
+	return I_SetSongLoopPoint(looppoint);
+}
+
+UINT32 S_GetMusicLoopPoint(void)
+{
+	return I_GetSongLoopPoint();
+}
+
+boolean S_SetMusicPosition(UINT32 position)
+{
+	return I_SetSongPosition(position);
+}
+
+UINT32 S_GetMusicPosition(void)
+{
+	return I_GetSongPosition();
+}
+
 /// ------------------------
 /// Music Playback
 /// ------------------------
@@ -1894,12 +1935,13 @@ static void S_UnloadMusic(void)
 	music_looping = false;
 }
 
-static boolean S_PlayMusic(boolean looping)
+static boolean S_PlayMusic(boolean looping, UINT32 fadeinms)
 {
 	if (S_MusicDisabled())
 		return false;
 
-	if (!I_PlaySong(looping))
+	if ((!fadeinms && !I_PlaySong(looping)) ||
+		(fadeinms && !I_FadeInPlaySong(fadeinms, looping)))
 	{
 		S_UnloadMusic();
 		return false;
@@ -1913,49 +1955,106 @@ static boolean S_PlayMusic(boolean looping)
 	return true;
 }
 
-void S_ChangeMusic(const char *mmusic, UINT16 mflags, boolean looping)
+static void S_QueueMusic(const char *mmusic, UINT16 mflags, boolean looping, UINT32 position, UINT32 fadeinms)
+{
+	strncpy(queue_name, mmusic, 7);
+	queue_flags = mflags;
+	queue_looping = looping;
+	queue_position = position;
+	queue_fadeinms = fadeinms;
+}
+
+static void S_ClearQueue(void)
+{
+	queue_name[0] = queue_flags = queue_looping = queue_position = queue_fadeinms = 0;
+}
+
+static void S_ChangeMusicToQueue(void)
+{
+	S_ChangeMusicEx(queue_name, queue_flags, queue_looping, queue_position, 0, queue_fadeinms);
+	S_ClearQueue();
+}
+
+void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32 position, UINT32 prefadems, UINT32 fadeinms)
 {
+	char newmusic[7];
+
 #if defined (DC) || defined (_WIN32_WCE) || defined (PSP) || defined(GP2X)
 	S_ClearSfx();
 #endif
 
 	if (S_MusicDisabled()
-		|| titledemo) // SRB2Kart: Demos don't interrupt title screen music
+		|| demo.rewinding // Don't mess with music while rewinding!
+		|| demo.title) // SRB2Kart: Demos don't interrupt title screen music
 		return;
 
-	// No Music (empty string)
-	if (mmusic[0] == 0)
-	{
-		S_StopMusic();
+	strncpy(newmusic, mmusic, 7);
+#if defined(HAVE_BLUA) && defined(HAVE_LUA_MUSICPLUS)
+	if(LUAh_MusicChange(music_name, newmusic, &mflags, &looping, &position, &prefadems, &fadeinms))
+		return;
+#endif
+	newmusic[6] = 0;
+
+ 	// No Music (empty string)
+	if (newmusic[0] == 0)
+ 	{
+		if (prefadems)
+			I_FadeSong(0, prefadems, &S_StopMusic);
+		else
+			S_StopMusic();
 		return;
 	}
 
-	if (strnicmp(music_name, mmusic, 6))
+	if (prefadems && S_MusicPlaying()) // queue music change for after fade // allow even if the music is the same
 	{
-		S_StopMusic(); // shutdown old music
+		CONS_Debug(DBG_DETAILED, "Now fading out song %s\n", music_name);
+		S_QueueMusic(newmusic, mflags, looping, position, fadeinms);
+		I_FadeSong(0, prefadems, S_ChangeMusicToQueue);
+		return;
+	}
+	else if (strnicmp(music_name, newmusic, 6) || (mflags & MUSIC_FORCERESET))
+ 	{
+		CONS_Debug(DBG_DETAILED, "Now playing song %s\n", newmusic);
 
-		if (!S_LoadMusic(mmusic))
+		S_StopMusic();
+
+		if (!S_LoadMusic(newmusic))
 		{
-			CONS_Alert(CONS_ERROR, "Music %.6s could not be loaded!\n", mmusic);
+			CONS_Alert(CONS_ERROR, "Music %.6s could not be loaded!\n", newmusic);
 			return;
 		}
 
 		music_flags = mflags;
 		music_looping = looping;
 
-		if (!S_PlayMusic(looping))
-		{
-			CONS_Alert(CONS_ERROR, "Music %.6s could not be played!\n", mmusic);
+		if (!S_PlayMusic(looping, fadeinms))
+ 		{
+			CONS_Alert(CONS_ERROR, "Music %.6s could not be played!\n", newmusic);
 			return;
 		}
+
+		if (position)
+			I_SetSongPosition(position);
+
+		I_SetSongTrack(mflags & MUSIC_TRACKMASK);
+	}
+	else if (fadeinms) // let fades happen with same music
+	{
+		I_SetSongPosition(position);
+		I_FadeSong(100, fadeinms, NULL);
+ 	}
+	else // reset volume to 100 with same music
+	{
+		I_StopFadingSong();
+		I_FadeSong(100, 500, NULL);
 	}
-	I_SetSongTrack(mflags & MUSIC_TRACKMASK);
 }
 
 void S_StopMusic(void)
 {
 	if (!I_SongPlaying()
-		|| titledemo) // SRB2Kart: Demos don't interrupt title screen music
+		|| demo.rewinding // Don't mess with music while rewinding!
+		|| demo.title) // SRB2Kart: Demos don't interrupt title screen music
 		return;
 
 	if (I_SongPaused())
@@ -2055,6 +2154,32 @@ void S_SetMusicVolume(INT32 digvolume, INT32 seqvolume)
 	}
 }
 
+/// ------------------------
+/// Music Fading
+/// ------------------------
+
+void S_SetInternalMusicVolume(INT32 volume)
+{
+	I_SetInternalMusicVolume(min(max(volume, 0), 100));
+}
+
+void S_StopFadingMusic(void)
+{
+	I_StopFadingSong();
+}
+
+boolean S_FadeMusicFromVolume(UINT8 target_volume, INT16 source_volume, UINT32 ms)
+{
+	if (source_volume < 0)
+		return I_FadeSong(target_volume, ms, NULL);
+	else
+		return I_FadeSongFromVolume(target_volume, source_volume, ms, NULL);
+}
+
+boolean S_FadeOutStopMusic(UINT32 ms)
+{
+	return I_FadeSong(0, ms, &S_StopMusic);
+}
 
 /// ------------------------
 /// Init & Others
@@ -2072,26 +2197,28 @@ void S_Start(void)
 		strncpy(mapmusname, mapheaderinfo[gamemap-1]->musname, 7);
 		mapmusname[6] = 0;
 		mapmusflags = (mapheaderinfo[gamemap-1]->mustrack & MUSIC_TRACKMASK);
+		mapmusposition = mapheaderinfo[gamemap-1]->muspos;
 	}
 
 	//if (cv_resetmusic.value) // Starting ambience should always be restarted
 		S_StopMusic();
 
 	if (leveltime < (starttime + (TICRATE/2))) // SRB2Kart
-		S_ChangeMusic((encoremode ? "estart" : "kstart"), 0, false);
+		S_ChangeMusicEx((encoremode ? "estart" : "kstart"), 0, false, mapmusposition, 0, 0);
 	else
-		S_ChangeMusic(mapmusname, mapmusflags, true);
+		S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 }
 
 static void Command_Tunes_f(void)
 {
 	const char *tunearg;
 	UINT16 tunenum, track = 0;
+	UINT32 position = 0;
 	const size_t argc = COM_Argc();
 
 	if (argc < 2) //tunes slot ...
 	{
-		CONS_Printf("tunes <name/num> [track] [speed] / <-show> / <-default> / <-none>:\n");
+		CONS_Printf("tunes <name/num> [track] [speed] [position] / <-show> / <-default> / <-none>:\n");
 		CONS_Printf(M_GetText("Play an arbitrary music lump. If a map number is used, 'MAP##M' is played.\n"));
 		CONS_Printf(M_GetText("If the format supports multiple songs, you can specify which one to play.\n\n"));
 		CONS_Printf(M_GetText("* With \"-show\", shows the currently playing tune and track.\n"));
@@ -2138,10 +2265,15 @@ static void Command_Tunes_f(void)
 		snprintf(mapmusname, 7, "%sM", G_BuildMapName(tunenum));
 	else
 		strncpy(mapmusname, tunearg, 7);
+
+	if (argc > 4)
+		position = (UINT32)atoi(COM_Argv(4));
+
 	mapmusname[6] = 0;
 	mapmusflags = (track & MUSIC_TRACKMASK);
+	mapmusposition = position;
 
-	S_ChangeMusic(mapmusname, mapmusflags, true);
+	S_ChangeMusicEx(mapmusname, mapmusflags, true, mapmusposition, 0, 0);
 
 	if (argc > 3)
 	{
diff --git a/src/s_sound.h b/src/s_sound.h
index 1c9386816ffd05000ba6b9a908a3d6cfb546c830..2a904faff6bb314ac45867ddfba01ffbe46b135b 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -117,14 +117,14 @@ boolean S_MusicDisabled(void);
 boolean S_MusicPlaying(void);
 boolean S_MusicPaused(void);
 musictype_t S_MusicType(void);
+const char *S_MusicName(void);
 boolean S_MusicInfo(char *mname, UINT16 *mflags, boolean *looping);
 boolean S_MusicExists(const char *mname, boolean checkMIDI, boolean checkDigi);
 #define S_DigExists(a) S_MusicExists(a, false, true)
 #define S_MIDIExists(a) S_MusicExists(a, true, false)
 
-
 //
-// Music Properties
+// Music Effects
 //
 
 // Set Speed of Music
@@ -154,15 +154,35 @@ void S_InitMusicDefs(void);
 void S_ShowMusicCredit(void);
 
 //
-// Music Routines
+// Music Seeking
+//
+
+// Get Length of Music
+UINT32 S_GetMusicLength(void);
+
+// Set LoopPoint of Music
+boolean S_SetMusicLoopPoint(UINT32 looppoint);
+
+// Get LoopPoint of Music
+UINT32 S_GetMusicLoopPoint(void);
+
+// Set Position of Music
+boolean S_SetMusicPosition(UINT32 position);
+
+// Get Position of Music
+UINT32 S_GetMusicPosition(void);
+
+//
+// Music Playback
 //
 
 // Start music track, arbitrary, given its name, and set whether looping
 // note: music flags 12 bits for tracknum (gme, other formats with more than one track)
 //       13-15 aren't used yet
 //       and the last bit we ignore (internal game flag for resetting music on reload)
-#define S_ChangeMusicInternal(a,b) S_ChangeMusic(a,0,b)
-void S_ChangeMusic(const char *mmusic, UINT16 mflags, boolean looping);
+void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32 position, UINT32 prefadems, UINT32 fadeinms);
+#define S_ChangeMusicInternal(a,b) S_ChangeMusicEx(a,0,b,0,0,0)
+#define S_ChangeMusic(a,b,c) S_ChangeMusicEx(a,b,c,0,0,0)
 
 // Stops the music.
 void S_StopMusic(void);
@@ -175,6 +195,17 @@ void S_ResumeAudio(void);
 void S_EnableSound(void);
 void S_DisableSound(void);
 
+//
+// Music Fading
+//
+
+void S_SetInternalMusicVolume(INT32 volume);
+void S_StopFadingMusic(void);
+boolean S_FadeMusicFromVolume(UINT8 target_volume, INT16 source_volume, UINT32 ms);
+#define S_FadeMusic(a, b) S_FadeMusicFromVolume(a, -1, b)
+#define S_FadeInChangeMusic(a,b,c,d) S_ChangeMusicEx(a,b,c,0,0,d)
+boolean S_FadeOutStopMusic(UINT32 ms);
+
 //
 // Updates music & sounds
 //
diff --git a/src/screen.c b/src/screen.c
index 4de2abd05ee49705f7611194fd7b4c6c5ba1002d..4cb8bac5d81116000a475ae3f3da78dbd5b2b6b3 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -59,6 +59,8 @@ INT32 setmodeneeded; //video mode change needed if > 0 (the mode number to set +
 
 static CV_PossibleValue_t scr_depth_cons_t[] = {{8, "8 bits"}, {16, "16 bits"}, {24, "24 bits"}, {32, "32 bits"}, {0, NULL}};
 
+static CV_PossibleValue_t shittyscreen_cons_t[] = {{0, "Okay"}, {1, "Shitty"}, {2, "Extra Shitty"}, {0, NULL}};
+
 //added : 03-02-98: default screen mode, as loaded/saved in config
 #ifdef WII
 consvar_t cv_scr_width = {"scr_width", "640", CV_SAVE, CV_Unsigned, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -70,6 +72,8 @@ consvar_t cv_scr_height = {"scr_height", "800", CV_SAVE, CV_Unsigned, NULL, 0, N
 consvar_t cv_scr_depth = {"scr_depth", "16 bits", CV_SAVE, scr_depth_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 #endif
 consvar_t cv_renderview = {"renderview", "On", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_vhseffect = {"vhspause", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_shittyscreen = {"televisionsignal", "Okay", CV_NOSHOWHELP, shittyscreen_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 static void SCR_ChangeFullscreen (void);
 
diff --git a/src/screen.h b/src/screen.h
index 5b4a8e5839d2bfc30d1241028f46d4e71b62d05a..2e4d29b9519ab7bd12399ec1a64cd95c57490969 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -158,7 +158,7 @@ extern INT32 setmodeneeded; // mode number to set if needed, or 0
 extern INT32 scr_bpp;
 extern UINT8 *scr_borderpatch; // patch used to fill the view borders
 
-extern consvar_t cv_scr_width, cv_scr_height, cv_scr_depth, cv_renderview, cv_fullscreen;
+extern consvar_t cv_scr_width, cv_scr_height, cv_scr_depth, cv_renderview, cv_fullscreen, cv_vhseffect, cv_shittyscreen;
 // wait for page flipping to end or not
 extern consvar_t cv_vidwait;
 
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index d2466bd5b6c7a76434f52404ec970c3cf77c08d0..45b1faab7941016ddfcea7c2aed39a6e3dde515d 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -227,6 +227,10 @@
     <ClInclude Include="..\hardware\hw_light.h" />
     <ClInclude Include="..\hardware\hw_main.h" />
     <ClInclude Include="..\hardware\hw_md2.h" />
+    <ClInclude Include="..\hardware\hw_md2load.h" />
+    <ClInclude Include="..\hardware\hw_md3load.h" />
+    <ClInclude Include="..\hardware\hw_model.h" />
+    <ClInclude Include="..\hardware\u_list.h" />
     <ClInclude Include="..\hu_stuff.h" />
     <ClInclude Include="..\info.h" />
     <ClInclude Include="..\i_addrinfo.h" />
@@ -366,8 +370,12 @@
     <ClCompile Include="..\hardware\hw_light.c" />
     <ClCompile Include="..\hardware\hw_main.c" />
     <ClCompile Include="..\hardware\hw_md2.c" />
+    <ClCompile Include="..\hardware\hw_md2load.c" />
+    <ClCompile Include="..\hardware\hw_md3load.c" />
+    <ClCompile Include="..\hardware\hw_model.c" />
     <ClCompile Include="..\hardware\hw_trick.c" />
     <ClCompile Include="..\hardware\r_opengl\r_opengl.c" />
+    <ClCompile Include="..\hardware\u_list.c" />
     <ClCompile Include="..\hu_stuff.c" />
     <ClCompile Include="..\info.c" />
     <ClCompile Include="..\i_addrinfo.c">
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index daa13189f458034b0b533ce26079e0d61c9d62cd..8556627b8d992cd334ec5bf80313ab75e9bb3760 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -246,6 +246,18 @@
     <ClInclude Include="..\hardware\hw_md2.h">
       <Filter>Hw_Hardware</Filter>
     </ClInclude>
+    <ClInclude Include="..\hardware\hw_md2load.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
+    <ClInclude Include="..\hardware\hw_md3load.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
+    <ClInclude Include="..\hardware\hw_model.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
+    <ClInclude Include="..\hardware\u_list.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
     <ClInclude Include="..\byteptr.h">
       <Filter>I_Interface</Filter>
     </ClInclude>
@@ -627,9 +639,21 @@
     <ClCompile Include="..\hardware\hw_md2.c">
       <Filter>Hw_Hardware</Filter>
     </ClCompile>
+    <ClCompile Include="..\hardware\hw_md2load.c">
+      <Filter>Hw_Hardware</Filter>
+    </ClCompile>
+    <ClCompile Include="..\hardware\hw_md3load.c">
+      <Filter>Hw_Hardware</Filter>
+    </ClCompile>
+    <ClCompile Include="..\hardware\hw_model.c">
+      <Filter>Hw_Hardware</Filter>
+    </ClCompile>
     <ClCompile Include="..\hardware\hw_trick.c">
       <Filter>Hw_Hardware</Filter>
     </ClCompile>
+    <ClCompile Include="..\hardware\u_list.c">
+      <Filter>Hw_Hardware</Filter>
+    </ClCompile>
     <ClCompile Include="..\filesrch.c">
       <Filter>I_Interface</Filter>
     </ClCompile>
diff --git a/src/sdl/hwsym_sdl.c b/src/sdl/hwsym_sdl.c
index 05ac6450e2267c0c08182775cece05eeaefabeab..4e083b4c2949c8524fde8a0b396137365515c97d 100644
--- a/src/sdl/hwsym_sdl.c
+++ b/src/sdl/hwsym_sdl.c
@@ -87,13 +87,11 @@ void *hwSym(const char *funcName,void *handle)
 	GETFUNC(ClearMipMapCache);
 	GETFUNC(SetSpecialState);
 	GETFUNC(GetTextureUsed);
-	GETFUNC(DrawMD2);
-	GETFUNC(DrawMD2i);
+	GETFUNC(DrawModel);
+	GETFUNC(CreateModelVBOs);
 	GETFUNC(SetTransform);
 	GETFUNC(GetRenderVersion);
-#ifdef SHUFFLE
 	GETFUNC(PostImgRedraw);
-#endif //SHUFFLE
 	GETFUNC(FlushScreenTextures);
 	GETFUNC(StartScreenWipe);
 	GETFUNC(EndScreenWipe);
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index f92f1f14a2845741561e33760909559628c05b48..c0fca64daa8b6a87de77a0a7017fed4b06b0e30b 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -3061,7 +3061,7 @@ void I_Quit(void)
 	//added:16-02-98: when recording a demo, should exit using 'q' key,
 	//        but sometimes we forget and use 'F10'.. so save here too.
 
-	if (demorecording)
+	if (demo.recording)
 		G_CheckDemoStatus();
 	if (metalrecording)
 		G_StopMetalRecording();
@@ -3179,7 +3179,7 @@ void I_Error(const char *error, ...)
 	G_SaveGameData(false); // Tails 12-08-2002
 
 	// Shutdown. Here might be other errors.
-	if (demorecording)
+	if (demo.recording)
 		G_CheckDemoStatus();
 	if (metalrecording)
 		G_StopMetalRecording();
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 9fbe57b39f233c40746f9e5ac50529d3c77d0db6..42e0a917fcd60fe44b8add6842c72d2abac6a7b0 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -359,6 +359,14 @@ static INT32 Impl_SDL_Scancode_To_Keycode(SDL_Scancode code)
 	return 0;
 }
 
+static void SDLdoGrabMouse(void)
+{
+	SDL_ShowCursor(SDL_DISABLE);
+	SDL_SetWindowGrab(window, SDL_TRUE);
+	if (SDL_SetRelativeMouseMode(SDL_TRUE) == 0) // already warps mouse if successful
+		wrapmouseok = SDL_TRUE; // TODO: is wrapmouseok or HalfWarpMouse needed anymore?
+}
+
 static void SDLdoUngrabMouse(void)
 {
 	SDL_ShowCursor(SDL_ENABLE);
@@ -629,6 +637,9 @@ static void Impl_HandleWindowEvent(SDL_WindowEvent evt)
 		//else firsttimeonmouse = SDL_FALSE;
 
 		capslock = !!( SDL_GetModState() & KMOD_CAPS );// in case CL changes
+
+		if (USE_MOUSEINPUT)
+			SDLdoGrabMouse();
 	}
 	else if (!mousefocus && !kbfocus)
 	{
@@ -708,9 +719,7 @@ static void Impl_HandleMouseMotionEvent(SDL_MouseMotionEvent evt)
 		// -- Monster Iestyn
 		if (SDL_GetMouseFocus() == window && SDL_GetKeyboardFocus() == window)
 		{
-			SDL_SetWindowGrab(window, SDL_TRUE);
-			if (SDL_SetRelativeMouseMode(SDL_TRUE) == 0) // already warps mouse if successful
-				wrapmouseok = SDL_TRUE; // TODO: is wrapmouseok or HalfWarpMouse needed anymore?
+			SDLdoGrabMouse();
 		}
 	}
 }
@@ -1277,7 +1286,7 @@ void I_StartupMouse(void)
 	else
 		firsttimeonmouse = SDL_FALSE;
 	if (cv_usemouse.value)
-		return;
+		SDLdoGrabMouse();
 	else
 		SDLdoUngrabMouse();
 }
@@ -1845,13 +1854,11 @@ void I_StartupGraphics(void)
 		HWD.pfnSetSpecialState  = hwSym("SetSpecialState",NULL);
 		HWD.pfnSetPalette       = hwSym("SetPalette",NULL);
 		HWD.pfnGetTextureUsed   = hwSym("GetTextureUsed",NULL);
-		HWD.pfnDrawMD2          = hwSym("DrawMD2",NULL);
-		HWD.pfnDrawMD2i         = hwSym("DrawMD2i",NULL);
+		HWD.pfnDrawModel        = hwSym("DrawModel",NULL);
+		HWD.pfnCreateModelVBOs  = hwSym("CreateModelVBOs",NULL);
 		HWD.pfnSetTransform     = hwSym("SetTransform",NULL);
 		HWD.pfnGetRenderVersion = hwSym("GetRenderVersion",NULL);
-#ifdef SHUFFLE
 		HWD.pfnPostImgRedraw    = hwSym("PostImgRedraw",NULL);
-#endif
 		HWD.pfnFlushScreenTextures=hwSym("FlushScreenTextures",NULL);
 		HWD.pfnStartScreenWipe  = hwSym("StartScreenWipe",NULL);
 		HWD.pfnEndScreenWipe    = hwSym("EndScreenWipe",NULL);
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 954ef5ee9a6aa1cf7aa41461569f817bbc064175..1617da2a351d4d6dc3ef4a5a4e0dc28772dcd4a7 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -75,15 +75,41 @@
 UINT8 sound_started = false;
 
 static Mix_Music *music;
-static UINT8 music_volume, sfx_volume;
+static UINT8 music_volume, sfx_volume, internal_volume;
 static float loop_point;
+static float song_length; // length in seconds
 static boolean songpaused;
+static UINT32 music_bytes;
+static boolean is_looping;
+
+// fading
+static boolean is_fading;
+static UINT8 fading_source;
+static UINT8 fading_target;
+static UINT32 fading_timer;
+static UINT32 fading_duration;
+static INT32 fading_id;
+static void (*fading_callback)(void);
 
 #ifdef HAVE_LIBGME
 static Music_Emu *gme;
 static INT32 current_track;
 #endif
 
+static void var_cleanup(void)
+{
+	loop_point = song_length =\
+	 music_bytes = fading_source = fading_target =\
+	 fading_timer = fading_duration = 0;
+
+	songpaused = is_looping =\
+	 is_fading = false;
+
+	fading_callback = NULL;
+
+	internal_volume = 100;
+}
+
 /// ------------------------
 /// Audio System
 /// ------------------------
@@ -111,6 +137,8 @@ void I_StartupSound(void)
 		return;
 	}
 
+	var_cleanup();
+
 	music = NULL;
 	music_volume = sfx_volume = 0;
 
@@ -336,6 +364,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 					len = (info->play_length * 441 / 10) << 2;
 					mem = malloc(len);
 					gme_play(emu, len >> 1, mem);
+					gme_free_info(info);
 					gme_delete(emu);
 
 					return Mix_QuickLoad_RAW((Uint8 *)mem, len);
@@ -408,6 +437,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 		len = (info->play_length * 441 / 10) << 2;
 		mem = malloc(len);
 		gme_play(emu, len >> 1, mem);
+		gme_free_info(info);
 		gme_delete(emu);
 
 		return Mix_QuickLoad_RAW((Uint8 *)mem, len);
@@ -482,14 +512,102 @@ void I_SetSfxVolume(UINT8 volume)
 	sfx_volume = volume;
 }
 
+/// ------------------------
+/// Music Utilities
+/// ------------------------
+
+static UINT32 get_real_volume(UINT8 volume)
+{
+#ifdef _WIN32
+	if (I_SongType() == MU_MID)
+		// HACK: Until we stop using native MIDI,
+		// disable volume changes
+		return ((UINT32)31*128/31); // volume = 31
+	else
+#endif
+		// convert volume to mixer's 128 scale
+		// then apply internal_volume as a percentage
+		return ((UINT32)volume*128/31) * (UINT32)internal_volume / 100;
+}
+
+static UINT32 get_adjusted_position(UINT32 position)
+{
+	// all in milliseconds
+	UINT32 length = I_GetSongLength();
+	UINT32 looppoint = I_GetSongLoopPoint();
+	if (length)
+		return position >= length ? (position % (length-looppoint)) : position;
+	else
+		return position;
+}
+
+static void do_fading_callback(void)
+{
+	if (fading_callback)
+		(*fading_callback)();
+	fading_callback = NULL;
+}
+
 /// ------------------------
 /// Music Hooks
 /// ------------------------
 
+static void count_music_bytes(int chan, void *stream, int len, void *udata)
+{
+	(void)chan;
+	(void)stream;
+	(void)udata;
+
+	if (!music || I_SongType() == MU_GME || I_SongType() == MU_MOD || I_SongType() == MU_MID)
+		return;
+	music_bytes += len;
+}
+
 static void music_loop(void)
 {
-	Mix_PlayMusic(music, 0);
-	Mix_SetMusicPosition(loop_point);
+	if (is_looping)
+	{
+		Mix_PlayMusic(music, 0);
+		Mix_SetMusicPosition(loop_point);
+		music_bytes = loop_point*44100.0L*4; //assume 44.1khz, 4-byte length (see I_GetSongPosition)
+	}
+	else
+		I_StopSong();
+}
+
+static UINT32 music_fade(UINT32 interval, void *param)
+{
+	(void)param;
+
+	if (!is_fading ||
+		internal_volume == fading_target ||
+		fading_duration == 0)
+	{
+		I_StopFadingSong();
+		do_fading_callback();
+		return 0;
+	}
+	else if (songpaused) // don't decrement timer
+		return interval;
+	else if ((fading_timer -= 10) <= 0)
+	{
+		internal_volume = fading_target;
+		Mix_VolumeMusic(get_real_volume(music_volume));
+		I_StopFadingSong();
+		do_fading_callback();
+		return 0;
+	}
+	else
+	{
+		UINT8 delta = abs(fading_target - fading_source);
+		fixed_t factor = FixedDiv(fading_duration - fading_timer, fading_duration);
+		if (fading_target < fading_source)
+			internal_volume = max(min(internal_volume, fading_source - FixedMul(delta, factor)), fading_target);
+		else if (fading_target > fading_source)
+			internal_volume = min(max(internal_volume, fading_source + FixedMul(delta, factor)), fading_target);
+		Mix_VolumeMusic(get_real_volume(music_volume));
+		return interval;
+	}
 }
 
 #ifdef HAVE_LIBGME
@@ -509,7 +627,7 @@ static void mix_gme(void *udata, Uint8 *stream, int len)
 
 	// apply volume to stream
 	for (i = 0, p = (short *)stream; i < len/2; i++, p++)
-		*p = ((INT32)*p) * music_volume*2 / 42;
+		*p = ((INT32)*p) * (music_volume*internal_volume/100)*2 / 42;
 }
 #endif
 
@@ -586,6 +704,194 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+///  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	INT32 length;
+
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		gme_info_t *info;
+		gme_err_t gme_e = gme_track_info(gme, &info, current_track);
+
+		if (gme_e != NULL)
+		{
+			CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+			length = 0;
+		}
+		else
+		{
+			// reconstruct info->play_length, from GME source
+			// we only want intro + 1 loop, not 2
+			length = info->length;
+			if (length <= 0)
+			{
+				length = info->intro_length + info->loop_length; // intro + 1 loop
+				if (length <= 0)
+					length = 150 * 1000; // 2.5 minutes
+			}
+		}
+
+		gme_free_info(info);
+		return max(length, 0);
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MOD || I_SongType() == MU_MID)
+		return 0;
+	else
+	{
+		// VERY IMPORTANT to set your LENGTHMS= in your song files, folks!
+		// SDL mixer can't read music length itself.
+		length = (UINT32)(song_length*1000);
+		if (!length)
+			CONS_Debug(DBG_DETAILED, "Getting music length: music is missing LENGTHMS= tag. Needed for seeking.\n");
+		return length;
+	}
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+	if (!music || I_SongType() == MU_GME || I_SongType() == MU_MOD || I_SongType() == MU_MID || !is_looping)
+		return false;
+	else
+	{
+		UINT32 length = I_GetSongLength();
+
+		if (length > 0)
+			looppoint %= length;
+
+		loop_point = max((float)(looppoint / 1000.0L), 0);
+		return true;
+	}
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		INT32 looppoint;
+		gme_info_t *info;
+		gme_err_t gme_e = gme_track_info(gme, &info, current_track);
+
+		if (gme_e != NULL)
+		{
+			CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+			looppoint = 0;
+		}
+		else
+			looppoint = info->intro_length > 0 ? info->intro_length : 0;
+
+		gme_free_info(info);
+		return max(looppoint, 0);
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MOD || I_SongType() == MU_MID)
+		return 0;
+	else
+		return (UINT32)(loop_point * 1000);
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+	UINT32 length;
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		// this is unstable, so fail silently
+		return true;
+		// this isn't required technically, but GME thread-locks for a second
+		// if you seek too high from the counter
+		// length = I_GetSongLength();
+		// if (length)
+		// 	position = get_adjusted_position(position);
+
+		// SDL_LockAudio();
+		// gme_err_t gme_e = gme_seek(gme, position);
+		// SDL_UnlockAudio();
+
+		// if (gme_e != NULL)
+		// {
+		// 	CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+		// 	return false;
+		// }
+		// else
+		// 	return true;
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MID)
+		return false;
+	else if (I_SongType() == MU_MOD)
+		return Mix_SetMusicPosition(position); // Goes by channels
+	else
+	{
+		// Because SDL mixer can't identify song length, if you have
+		// a position input greater than the real length, then
+		// music_bytes becomes inaccurate.
+
+		length = I_GetSongLength(); // get it in MS
+		if (length)
+			position = get_adjusted_position(position);
+
+		Mix_RewindMusic(); // needed for mp3
+		if(Mix_SetMusicPosition((float)(position/1000.0L)) == 0)
+			music_bytes = position/1000.0L*44100.0L*4; //assume 44.1khz, 4-byte length (see I_GetSongPosition)
+		else
+			// NOTE: This block fires on incorrect song format,
+			// NOT if position input is greater than song length.
+			music_bytes = 0;
+
+		return true;
+	}
+}
+
+UINT32 I_GetSongPosition(void)
+{
+#ifdef HAVE_LIBGME
+	if (gme)
+	{
+		INT32 position = gme_tell(gme);
+
+		gme_info_t *info;
+		gme_err_t gme_e = gme_track_info(gme, &info, current_track);
+
+		if (gme_e != NULL)
+		{
+			CONS_Alert(CONS_ERROR, "GME error: %s\n", gme_e);
+			return position;
+		}
+		else
+		{
+			// adjust position, since GME's counter keeps going past loop
+			if (info->length > 0)
+				position %= info->length;
+			else if (info->intro_length + info->loop_length > 0)
+				position = position >= (info->intro_length + info->loop_length) ? (position % info->loop_length) : position;
+			else
+				position %= 150 * 1000; // 2.5 minutes
+		}
+
+		gme_free_info(info);
+		return max(position, 0);
+	}
+	else
+#endif
+	if (!music || I_SongType() == MU_MID)
+		return 0;
+	else
+		return music_bytes/44100.0L*1000.0L/4; //assume 44.1khz
+		// 4 = byte length for 16-bit samples (AUDIO_S16SYS), stereo (2-channel)
+		// This is hardcoded in I_StartupSound. Other formats for factor:
+		// 8M: 1 | 8S: 2 | 16M: 2 | 16S: 4
+}
+
 /// ------------------------
 /// Music Playback
 /// ------------------------
@@ -598,6 +904,7 @@ boolean I_LoadSong(char *data, size_t len)
 	const size_t key1len = strlen(key1);
 	const size_t key2len = strlen(key2);
 	const size_t key3len = strlen(key3);
+
 	char *p = data;
 	SDL_RWops *rw;
 
@@ -608,6 +915,9 @@ boolean I_LoadSong(char *data, size_t len)
 	)
 		I_UnloadSong();
 
+	// always do this whether or not a music already exists
+	var_cleanup();
+
 #ifdef HAVE_LIBGME
 	if ((UINT8)data[0] == 0x1F
 		&& (UINT8)data[1] == 0x8B)
@@ -717,30 +1027,35 @@ boolean I_LoadSong(char *data, size_t len)
 
 	// Find the OGG loop point.
 	loop_point = 0.0f;
+	song_length = 0.0f;
 
 	while ((UINT32)(p - data) < len)
 	{
-		if (strncmp(p++, key1, key1len))
-			continue;
-		p += key1len-1; // skip OOP (the L was skipped in strncmp)
-		if (!strncmp(p, key2, key2len)) // is it LOOPPOINT=?
-		{
-			p += key2len; // skip POINT=
-			loop_point = (float)((44.1L+atoi(p)) / 44100.0L); // LOOPPOINT works by sample count.
-			// because SDL_Mixer is USELESS and can't even tell us
-			// something simple like the frequency of the streaming music,
-			// we are unfortunately forced to assume that ALL MUSIC is 44100hz.
-			// This means a lot of tracks that are only 22050hz for a reasonable downloadable file size will loop VERY badly.
-		}
-		else if (!strncmp(p, key3, key3len)) // is it LOOPMS=?
+		if (fpclassify(loop_point) == FP_ZERO && !strncmp(p, key1, key1len))
 		{
-			p += key3len; // skip MS=
-			loop_point = (float)(atoi(p) / 1000.0L); // LOOPMS works by real time, as miliseconds.
-			// Everything that uses LOOPMS will work perfectly with SDL_Mixer.
+			p += key1len; // skip LOOP
+			if (!strncmp(p, key2, key2len)) // is it LOOPPOINT=?
+			{
+				p += key2len; // skip POINT=
+				loop_point = (float)((44.1L+atoi(p)) / 44100.0L); // LOOPPOINT works by sample count.
+				// because SDL_Mixer is USELESS and can't even tell us
+				// something simple like the frequency of the streaming music,
+				// we are unfortunately forced to assume that ALL MUSIC is 44100hz.
+				// This means a lot of tracks that are only 22050hz for a reasonable downloadable file size will loop VERY badly.
+			}
+			else if (!strncmp(p, key3, key3len)) // is it LOOPMS=?
+			{
+				p += key3len; // skip MS=
+				loop_point = (float)(atoi(p) / 1000.0L); // LOOPMS works by real time, as miliseconds.
+				// Everything that uses LOOPMS will work perfectly with SDL_Mixer.
+			}
 		}
-		// Neither?! Continue searching.
-	}
 
+		if (fpclassify(loop_point) != FP_ZERO) // Got what we needed
+			break;
+		else // continue searching
+			p++;
+	}
 	return true;
 }
 
@@ -764,7 +1079,6 @@ void I_UnloadSong(void)
 
 boolean I_PlaySong(boolean looping)
 {
-	boolean lpz = fpclassify(loop_point) == FP_ZERO;
 #ifdef HAVE_LIBGME
 	if (gme)
 	{
@@ -778,21 +1092,37 @@ boolean I_PlaySong(boolean looping)
 	if (!music)
 		return false;
 
+	if (fpclassify(song_length) == FP_ZERO && (I_SongType() == MU_OGG || I_SongType() == MU_MP3 || I_SongType() == MU_FLAC))
+		CONS_Debug(DBG_DETAILED, "This song is missing a LENGTHMS= tag! Required to make seeking work properly.\n");
 
-	if (Mix_PlayMusic(music, looping && lpz ? -1 : 0) == -1)
+	if (I_SongType() != MU_MOD && I_SongType() != MU_MID && Mix_PlayMusic(music, 0) == -1)
 	{
 		CONS_Alert(CONS_ERROR, "Mix_PlayMusic: %s\n", Mix_GetError());
 		return false;
 	}
-	Mix_VolumeMusic((UINT32)music_volume*128/31);
+	else if ((I_SongType() == MU_MOD || I_SongType() == MU_MID) && Mix_PlayMusic(music, looping ? -1 : 0) == -1) // if MOD, loop forever
+	{
+		CONS_Alert(CONS_ERROR, "Mix_PlayMusic: %s\n", Mix_GetError());
+		return false;
+	}
+
+	is_looping = looping;
+
+	I_SetMusicVolume(music_volume);
+
+	if (I_SongType() != MU_MOD && I_SongType() != MU_MID)
+		Mix_HookMusicFinished(music_loop); // don't bother counting if MOD
+
+	if(I_SongType() != MU_MOD && I_SongType() != MU_MID && !Mix_RegisterEffect(MIX_CHANNEL_POST, count_music_bytes, NULL, NULL))
+		CONS_Alert(CONS_WARNING, "Error registering SDL music position counter: %s\n", Mix_GetError());
 
-	if (!lpz)
-		Mix_HookMusicFinished(music_loop);
 	return true;
 }
 
 void I_StopSong(void)
 {
+	I_StopFadingSong();
+
 #ifdef HAVE_LIBGME
 	if (gme)
 	{
@@ -802,19 +1132,40 @@ void I_StopSong(void)
 #endif
 	if (music)
 	{
+		Mix_UnregisterEffect(MIX_CHANNEL_POST, count_music_bytes);
 		Mix_HookMusicFinished(NULL);
 		Mix_HaltMusic();
 	}
+
+	var_cleanup();
 }
 
 void I_PauseSong(void)
 {
+	if(I_SongType() == MU_MID) // really, SDL Mixer? why can't you pause MIDI???
+		return;
+
+	if(I_SongType() != MU_GME && I_SongType() != MU_MOD && I_SongType() != MU_MID)
+		Mix_UnregisterEffect(MIX_CHANNEL_POST, count_music_bytes);
+
 	Mix_PauseMusic();
 	songpaused = true;
 }
 
 void I_ResumeSong(void)
 {
+	if (I_SongType() == MU_MID)
+		return;
+
+	if (I_SongType() != MU_GME && I_SongType() != MU_MOD && I_SongType() != MU_MID)
+	{
+		while(Mix_UnregisterEffect(MIX_CHANNEL_POST, count_music_bytes) != 0) { }
+			// HACK: fixes issue of multiple effect callbacks being registered
+
+		if(music && I_SongType() != MU_MOD && I_SongType() != MU_MID && !Mix_RegisterEffect(MIX_CHANNEL_POST, count_music_bytes, NULL, NULL))
+			CONS_Alert(CONS_WARNING, "Error registering SDL music position counter: %s\n", Mix_GetError());
+	}
+
 	Mix_ResumeMusic();
 	songpaused = false;
 }
@@ -833,7 +1184,7 @@ void I_SetMusicVolume(UINT8 volume)
 #endif
 		music_volume = volume;
 
-	Mix_VolumeMusic((UINT32)music_volume*128/31);
+	Mix_VolumeMusic(get_real_volume(music_volume));
 }
 
 boolean I_SetSongTrack(int track)
@@ -862,9 +1213,100 @@ boolean I_SetSongTrack(int track)
 		SDL_UnlockAudio();
 		return false;
 	}
+	else
 #endif
+	if (I_SongType() == MU_MOD)
+		return !Mix_SetMusicPosition(track);
 	(void)track;
 	return false;
 }
 
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	internal_volume = volume;
+	if (!I_SongPlaying())
+		return;
+	Mix_VolumeMusic(get_real_volume(music_volume));
+}
+
+void I_StopFadingSong(void)
+{
+	if (fading_id)
+		SDL_RemoveTimer(fading_id);
+	is_fading = false;
+	fading_source = fading_target = fading_timer = fading_duration = fading_id = 0;
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void))
+{
+	INT16 volume_delta;
+
+	source_volume = min(source_volume, 100);
+	volume_delta = (INT16)(target_volume - source_volume);
+
+	I_StopFadingSong();
+
+	if (!ms && volume_delta)
+	{
+		I_SetInternalMusicVolume(target_volume);
+		if (callback)
+			(*callback)();
+		return true;
+
+	}
+	else if (!volume_delta)
+	{
+		if (callback)
+			(*callback)();
+		return true;
+	}
+
+	// Round MS to nearest 10
+	// If n - lower > higher - n, then round up
+	ms = (ms - ((ms / 10) * 10) > (((ms / 10) * 10) + 10) - ms) ?
+		(((ms / 10) * 10) + 10) // higher
+		: ((ms / 10) * 10); // lower
+
+	if (!ms)
+		I_SetInternalMusicVolume(target_volume);
+	else if (source_volume != target_volume)
+	{
+		fading_id = SDL_AddTimer(10, music_fade, NULL);
+		if (fading_id)
+		{
+			is_fading = true;
+			fading_timer = fading_duration = ms;
+			fading_source = source_volume;
+			fading_target = target_volume;
+			fading_callback = callback;
+
+			if (internal_volume != source_volume)
+				I_SetInternalMusicVolume(source_volume);
+		}
+	}
+
+	return is_fading;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void))
+{
+	return I_FadeSongFromVolume(target_volume, internal_volume, ms, callback);
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	return I_FadeSongFromVolume(0, internal_volume, ms, &I_StopSong);
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+	if (I_PlaySong(looping))
+		return I_FadeSongFromVolume(100, 0, ms, NULL);
+	else
+		return false;
+}
 #endif
diff --git a/src/sdl/sdl_sound.c b/src/sdl/sdl_sound.c
index 9ff1dd0b2bda00e0e8154a46aa4e8e6dd5d4bd7d..d9967ae0375d9ab30a3772f9c934e5ee26c75663 100644
--- a/src/sdl/sdl_sound.c
+++ b/src/sdl/sdl_sound.c
@@ -1375,6 +1375,37 @@ boolean I_SetSongSpeed(float speed)
 	return false;
 }
 
+/// ------------------------
+//  MUSIC SEEKING
+/// ------------------------
+
+UINT32 I_GetSongLength(void)
+{
+	return 0;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+	(void)position;
+	return false;
+}
+
+UINT32 I_GetSongPosition(void)
+{
+	return 0;
+}
+
 /// ------------------------
 //  MUSIC PLAYBACK
 /// ------------------------
@@ -1443,6 +1474,47 @@ boolean I_SetSongTrack(int track)
 	return false;
 }
 
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
+
 /// ------------------------
 //  MUSIC LOADING AND CLEANUP
 //  \todo Split logic between loading and playing,
diff --git a/src/sdl12/Srb2SDL-vc10.vcxproj b/src/sdl12/Srb2SDL-vc10.vcxproj
index 99916f58d405351a1cb15bb83063d623b6d67bf2..0ac7e9e555348f77cb0c2901214daa3edd0828f6 100644
--- a/src/sdl12/Srb2SDL-vc10.vcxproj
+++ b/src/sdl12/Srb2SDL-vc10.vcxproj
@@ -755,6 +755,36 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="..\hardware\hw_md2load.c">
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
+    <ClCompile Include="..\hardware\hw_md3load.c">
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
+    <ClCompile Include="..\hardware\hw_model.c">
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
     <ClCompile Include="..\hardware\hw_trick.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -765,6 +795,16 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="..\hardware\u_list.c">
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
     <ClCompile Include="..\i_tcp.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -1340,7 +1380,11 @@
     <ClInclude Include="..\hardware\hw_light.h" />
     <ClInclude Include="..\hardware\hw_main.h" />
     <ClInclude Include="..\hardware\hw_md2.h" />
+    <ClInclude Include="..\hardware\hw_md2load.h" />
+    <ClInclude Include="..\hardware\hw_md3load.h" />
+    <ClInclude Include="..\hardware\hw_model.h" />
     <ClInclude Include="..\hardware\hws_data.h" />
+    <ClInclude Include="..\hardware\u_list.h" />
     <ClInclude Include="..\byteptr.h" />
     <ClInclude Include="..\i_joy.h" />
     <ClInclude Include="..\i_net.h" />
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 36a658aecd3441ed1f5f76462222bc77b3619be3..50bac3eef07eea974aee42d1561d7e662dbaf29d 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -372,7 +372,7 @@ static inline void ST_InitData(void)
 	// 'link' the statusbar display to a player, which could be
 	// another player than consoleplayer, for example, when you
 	// change the view in a multiplayer demo with F12.
-	stplyr = &players[displayplayer];
+	stplyr = &players[displayplayers[0]];
 
 	st_palette = -1;
 }
@@ -442,7 +442,7 @@ static INT32 SCY(INT32 y)
 	if (splitscreen)
 	{
 		y >>= 1;
-		if (stplyr != &players[displayplayer])
+		if (stplyr != &players[displayplayers[0]])
 			y += vid.height / 2;
 	}
 	return y;
@@ -458,7 +458,7 @@ static INT32 STRINGY(INT32 y)
 	if (splitscreen)
 	{
 		y >>= 1;
-		if (stplyr != &players[displayplayer])
+		if (stplyr != &players[displayplayers[0]])
 			y += BASEVIDHEIGHT / 2;
 	}
 	return y;
@@ -471,7 +471,7 @@ static INT32 SPLITFLAGS(INT32 f)
 	// Pass this V_SNAPTO(TOP|BOTTOM) and it'll trim them to account for splitscreen! -Red
 	if (splitscreen)
 	{
-		if (stplyr != &players[displayplayer])
+		if (stplyr != &players[displayplayers[0]])
 			f &= ~V_SNAPTOTOP;
 		else
 			f &= ~V_SNAPTOBOTTOM;
@@ -498,7 +498,7 @@ static INT32 SCR(INT32 r)
 	if (splitscreen)
 	{
 		y >>= 1;
-		if (stplyr != &players[displayplayer])
+		if (stplyr != &players[displayplayers[0]])
 			y += vid.height / 2;
 	}
 	return FixedInt(FixedDiv(y, vid.fdupy));
@@ -573,17 +573,17 @@ static void ST_drawDebugInfo(void)
 
 	if (cv_debug & DBG_DETAILED)
 	{
-		V_DrawRightAlignedString(320, height - 104, V_MONOSPACE, va("SHIELD: %5x", stplyr->powers[pw_shield]));
+		//V_DrawRightAlignedString(320, height - 104, V_MONOSPACE, va("SHIELD: %5x", stplyr->powers[pw_shield]));
 		V_DrawRightAlignedString(320, height - 96,  V_MONOSPACE, va("SCALE: %5d%%", (stplyr->mo->scale*100)/FRACUNIT));
-		V_DrawRightAlignedString(320, height - 88,  V_MONOSPACE, va("DASH: %3d/%3d", stplyr->dashspeed>>FRACBITS, FixedMul(stplyr->maxdash,stplyr->mo->scale)>>FRACBITS));
-		V_DrawRightAlignedString(320, height - 80,  V_MONOSPACE, va("AIR: %4d, %3d", stplyr->powers[pw_underwater], stplyr->powers[pw_spacetime]));
+		//V_DrawRightAlignedString(320, height - 88,  V_MONOSPACE, va("DASH: %3d/%3d", stplyr->dashspeed>>FRACBITS, FixedMul(stplyr->maxdash,stplyr->mo->scale)>>FRACBITS));
+		//V_DrawRightAlignedString(320, height - 80,  V_MONOSPACE, va("AIR: %4d, %3d", stplyr->powers[pw_underwater], stplyr->powers[pw_spacetime]));
 
 		// Flags
-		V_DrawRightAlignedString(304-64, height - 72, V_MONOSPACE, "Flags:");
-		V_DrawString(304-60,             height - 72, (stplyr->jumping) ? V_GREENMAP : V_REDMAP, "JM");
-		V_DrawString(304-40,             height - 72, (stplyr->pflags & PF_JUMPED) ? V_GREENMAP : V_REDMAP, "JD");
-		V_DrawString(304-20,             height - 72, (stplyr->pflags & PF_SPINNING) ? V_GREENMAP : V_REDMAP, "SP");
-		V_DrawString(304,                height - 72, (stplyr->pflags & PF_STARTDASH) ? V_GREENMAP : V_REDMAP, "ST");
+		//V_DrawRightAlignedString(304-64, height - 72, V_MONOSPACE, "Flags:");
+		//V_DrawString(304-60,             height - 72, (stplyr->jumping) ? V_GREENMAP : V_REDMAP, "JM");
+		//V_DrawString(304-40,             height - 72, (stplyr->pflags & PF_JUMPED) ? V_GREENMAP : V_REDMAP, "JD");
+		//V_DrawString(304-20,             height - 72, (stplyr->pflags & PF_SPINNING) ? V_GREENMAP : V_REDMAP, "SP");
+		//V_DrawString(304,                height - 72, (stplyr->pflags & PF_STARTDASH) ? V_GREENMAP : V_REDMAP, "ST");
 
 		V_DrawRightAlignedString(320, height - 64, V_MONOSPACE, va("CEILZ: %6d", stplyr->mo->ceilingz>>FRACBITS));
 		V_DrawRightAlignedString(320, height - 56, V_MONOSPACE, va("FLOORZ: %6d", stplyr->mo->floorz>>FRACBITS));
@@ -701,7 +701,7 @@ static inline void ST_drawRings(void) // SRB2kart - unused.
 /*
 static void ST_drawLives(void) // SRB2kart - unused.
 {
-	const INT32 v_splitflag = (splitscreen && stplyr == &players[displayplayer] ? V_SPLITSCREEN : 0);
+	const INT32 v_splitflag = (splitscreen && stplyr == &players[displayplayers[0]] ? V_SPLITSCREEN : 0);
 
 	if (!stplyr->skincolor)
 		return; // Just joined a server, skin isn't loaded yet!
@@ -1019,7 +1019,7 @@ static void ST_drawNiGHTSHUD(void) // SRB2kart - unused.
 	if (G_IsSpecialStage(gamemap))
 	{ // Since special stages share score, time, rings, etc.
 		// disable splitscreen mode for its HUD.
-		if (stplyr != &players[displayplayer])
+		if (stplyr != &players[displayplayers[0]])
 			return;
 		nosshack = splitscreen;
 		splitscreen = 0;
@@ -1124,7 +1124,7 @@ static void ST_drawNiGHTSHUD(void) // SRB2kart - unused.
 			V_DrawScaledPatch(locx, STRINGY(locy)-3, V_HUDTRANS, drillbar);
 			for (dfill = 0; dfill < stplyr->drillmeter/20 && dfill < 96; ++dfill)
 				V_DrawScaledPatch(locx + 2 + dfill, STRINGY(locy + 3), V_HUDTRANS, drillfill[fillpatch]);
-			stplyr = &players[secondarydisplayplayer];
+			stplyr = &players[displayplayers[1]];
 			if (stplyr->pflags & PF_DRILLING)
 				fillpatch = (stplyr->drillmeter & 1) + 1;
 			else
@@ -1132,7 +1132,7 @@ static void ST_drawNiGHTSHUD(void) // SRB2kart - unused.
 			V_DrawScaledPatch(locx, STRINGY(locy-3), V_SNAPTOBOTTOM|V_HUDTRANS, drillbar);
 			for (dfill = 0; dfill < stplyr->drillmeter/20 && dfill < 96; ++dfill)
 				V_DrawScaledPatch(locx + 2 + dfill, STRINGY(locy + 3), V_SNAPTOBOTTOM|V_HUDTRANS, drillfill[fillpatch]);
-			stplyr = &players[displayplayer];
+			stplyr = &players[displayplayers[0]];
 			splitscreen = 0;
 		}
 		else
@@ -1881,7 +1881,7 @@ static void ST_overlayDrawer(void)
 			ST_drawTeamName();
 
 		// Special Stage HUD
-		if (!useNightsSS && G_IsSpecialStage(gamemap) && stplyr == &players[displayplayer])
+		if (!useNightsSS && G_IsSpecialStage(gamemap) && stplyr == &players[displayplayers[0]])
 			ST_drawSpecialStageHUD();
 
 		// Emerald Hunt Indicators
@@ -1894,22 +1894,46 @@ static void ST_overlayDrawer(void)
 			V_DrawScaledPatch(hudinfo[HUD_GRAVBOOTSICO].x, STRINGY(hudinfo[HUD_GRAVBOOTSICO].y), V_SNAPTORIGHT, gravboots);
 		*/
 
-		if(!P_IsLocalPlayer(stplyr))
+		if (!(multiplayer && demo.playback))
 		{
-			/*char name[MAXPLAYERNAME+1];
-			// shorten the name if its more than twelve characters.
-			strlcpy(name, player_names[stplyr-players], 13);*/
+			if(!P_IsLocalPlayer(stplyr))
+			{
+				/*char name[MAXPLAYERNAME+1];
+				// shorten the name if its more than twelve characters.
+				strlcpy(name, player_names[stplyr-players], 13);*/
+
+				// Show name of player being displayed
+				V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-40, 0, M_GetText("Viewpoint:"));
+				V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-32, V_ALLOWLOWERCASE, player_names[stplyr-players]);
+			}
+		}
+		else if (!demo.title)
+		{
+
+			if (!splitscreen)
+			{
+				V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-40, V_HUDTRANSHALF, M_GetText("Viewpoint:"));
+				V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-32, V_HUDTRANSHALF|V_ALLOWLOWERCASE, player_names[stplyr-players]);
+			}
+			else if (splitscreen == 1)
+			{
+				char name[MAXPLAYERNAME+12];
 
-			// Show name of player being displayed
-			V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-40, 0, M_GetText("Viewpoint:"));
-			V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-32, V_ALLOWLOWERCASE, player_names[stplyr-players]);
+				INT32 y = (stplyr == &players[displayplayers[0]]) ? 4 : BASEVIDHEIGHT/2-12;
+				sprintf(name, "VIEWPOINT: %s", player_names[stplyr-players]);
+				V_DrawRightAlignedThinString(BASEVIDWIDTH-40, y, V_HUDTRANSHALF|V_ALLOWLOWERCASE|K_calcSplitFlags(V_SNAPTOTOP|V_SNAPTOBOTTOM|V_SNAPTORIGHT), name);
+			}
+			else if (splitscreen)
+			{
+				V_DrawCenteredThinString((vid.width/vid.dupx)/4, BASEVIDHEIGHT/2 - 12, V_HUDTRANSHALF|V_ALLOWLOWERCASE|K_calcSplitFlags(V_SNAPTOBOTTOM|V_SNAPTOLEFT), player_names[stplyr-players]);
+			}
 		}
 
 		// This is where we draw all the fun cheese if you have the chasecam off!
-		/*if ((stplyr == &players[displayplayer] && !camera.chase)
-			|| ((splitscreen && stplyr == &players[secondarydisplayplayer]) && !camera2.chase)
-			|| ((splitscreen > 1 && stplyr == &players[thirddisplayplayer]) && !camera3.chase)
-			|| ((splitscreen > 2 && stplyr == &players[fourthdisplayplayer]) && !camera4.chase))
+		/*if ((stplyr == &players[displayplayers[0]] && !camera[0].chase)
+			|| ((splitscreen && stplyr == &players[displayplayers[1]]) && !camera[1].chase)
+			|| ((splitscreen > 1 && stplyr == &players[displayplayers[2]]) && !camera[2].chase)
+			|| ((splitscreen > 2 && stplyr == &players[displayplayers[3]]) && !camera[3].chase))
 		{
 			ST_drawFirstPersonHUD();
 		}*/
@@ -1990,9 +2014,65 @@ static void ST_overlayDrawer(void)
 		}
 	}
 
+	// Replay manual-save stuff
+	if (demo.recording && multiplayer && demo.savebutton && demo.savebutton + 3*TICRATE < leveltime)
+	{
+		switch (demo.savemode)
+		{
+		case DSM_NOTSAVING:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|(G_BattleGametype() ? V_REDMAP : V_SKYMAP), "Look Backward: Save replay");
+			break;
+
+		case DSM_WILLAUTOSAVE:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|(G_BattleGametype() ? V_REDMAP : V_SKYMAP), "Replay will be saved. (Look Backward: Change title)");
+			break;
+
+		case DSM_WILLSAVE:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_HUDTRANS|V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|(G_BattleGametype() ? V_REDMAP : V_SKYMAP), "Replay will be saved.");
+			break;
+
+		case DSM_TITLEENTRY:
+			ST_DrawDemoTitleEntry();
+			break;
+
+		default: // Don't render anything
+			break;
+		}
+	}
+
 	ST_drawDebugInfo();
 }
 
+void ST_DrawDemoTitleEntry(void)
+{
+	static UINT8 skullAnimCounter = 0;
+	char *nametodraw;
+
+	skullAnimCounter++;
+	skullAnimCounter %= 8;
+
+	nametodraw = demo.titlename;
+	while (V_StringWidth(nametodraw, 0) > MAXSTRINGLENGTH*8 - 8)
+		nametodraw++;
+
+#define x (BASEVIDWIDTH/2 - 139)
+#define y (BASEVIDHEIGHT/2)
+	M_DrawTextBox(x, y + 4, MAXSTRINGLENGTH, 1);
+	V_DrawString(x + 8, y + 12, V_ALLOWLOWERCASE, nametodraw);
+	if (skullAnimCounter < 4)
+		V_DrawCharacter(x + 8 + V_StringWidth(nametodraw, 0), y + 12,
+			'_' | 0x80, false);
+
+	M_DrawTextBox(x + 30, y - 24, 26, 1);
+	V_DrawString(x + 38, y - 16, V_ALLOWLOWERCASE, "Enter the name of the replay.");
+
+	M_DrawTextBox(x + 50, y + 20, 20, 1);
+	V_DrawThinString(x + 58, y + 28, V_ALLOWLOWERCASE, "Escape - Cancel");
+	V_DrawRightAlignedThinString(x + 220, y + 28, V_ALLOWLOWERCASE, "Enter - Confirm");
+#undef x
+#undef y
+}
+
 // MayonakaStatic: draw Midnight Channel's TV-like borders
 static void ST_MayonakaStatic(void)
 {
@@ -2006,8 +2086,10 @@ static void ST_MayonakaStatic(void)
 
 void ST_Drawer(void)
 {
+	UINT8 i;
+
 #ifdef SEENAMES
-	if (cv_seenames.value && cv_allowseenames.value && displayplayer == consoleplayer && seenplayer && seenplayer->mo && !mapreset)
+	if (cv_seenames.value && cv_allowseenames.value && displayplayers[0] == consoleplayer && seenplayer && seenplayer->mo && !mapreset)
 	{
 		if (cv_seenames.value == 1)
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2 + 15, V_HUDTRANSHALF, player_names[seenplayer-players]);
@@ -2041,26 +2123,12 @@ void ST_Drawer(void)
 	if (st_overlay)
 	{
 		// No deadview!
-		stplyr = &players[displayplayer];
-		ST_overlayDrawer();
-
-		if (splitscreen)
+		for (i = 0; i <= splitscreen; i++)
 		{
-			stplyr = &players[secondarydisplayplayer];
+			stplyr = &players[displayplayers[i]];
 			ST_overlayDrawer();
-
-			if (splitscreen > 1)
-			{
-				stplyr = &players[thirddisplayplayer];
-				ST_overlayDrawer();
-
-				if (splitscreen > 2)
-				{
-					stplyr = &players[fourthdisplayplayer];
-					ST_overlayDrawer();
-				}
-			}
 		}
+
 		// draw Midnight Channel's overlay ontop
 		if (mapheaderinfo[gamemap-1]->typeoflevel & TOL_TV)	// Very specific Midnight Channel stuff.
 			ST_MayonakaStatic();
diff --git a/src/st_stuff.h b/src/st_stuff.h
index f96aee9382762aade763d4ed2e3a64a7ca47aa22..16f7b88114c75b84e01687004ccac53a76a73950 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -26,6 +26,9 @@
 // Called by main loop.
 void ST_Ticker(void);
 
+// Called when naming a replay.
+void ST_DrawDemoTitleEntry(void);
+
 // Called by main loop.
 void ST_Drawer(void);
 
diff --git a/src/v_video.c b/src/v_video.c
index c11c0a6301023e2ee94b1989bd2c8314d53b42a5..0e402e62e0aa833799310fa333128f3ae52249fe 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -60,7 +60,6 @@ static void CV_Gammaxxx_ONChange(void);
 static CV_PossibleValue_t grgamma_cons_t[] = {{1, "MIN"}, {255, "MAX"}, {0, NULL}};
 static CV_PossibleValue_t grsoftwarefog_cons_t[] = {{0, "Off"}, {1, "On"}, {2, "LightPlanes"}, {0, NULL}};
 
-consvar_t cv_voodoocompatibility = {"gr_voodoocompatibility", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_grfovchange = {"gr_fovchange", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_grfog = {"gr_fog", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_grfogcolor = {"gr_fogcolor", "AAAAAA", CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -80,7 +79,9 @@ consvar_t cv_grcoronasize = {"gr_coronasize", "1", CV_SAVE| CV_FLOAT, 0, NULL, 0
 
 //static CV_PossibleValue_t CV_MD2[] = {{0, "Off"}, {1, "On"}, {2, "Old"}, {0, NULL}};
 // console variables in development
-consvar_t cv_grmd2 = {"gr_md2", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_grmdls = {"gr_mdls", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_grfallbackplayermodel = {"gr_fallbackplayermodel", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_grspritebillboarding = {"gr_spritebillboarding", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 #endif
 
 const UINT8 gammatable[5][256] =
@@ -291,7 +292,7 @@ void VID_BlitLinearScreen(const UINT8 *srcptr, UINT8 *destptr, INT32 width, INT3
 #ifdef HAVE_VIDCOPY
     VID_BlitLinearScreen_ASM(srcptr,destptr,width,height,srcrowbytes,destrowbytes);
 #else
-	if (srcrowbytes == destrowbytes)
+	if ((srcrowbytes == destrowbytes) && (srcrowbytes == (size_t)width))
 		M_Memcpy(destptr, srcptr, srcrowbytes * height);
 	else
 	{
@@ -1214,6 +1215,66 @@ void V_DrawPatchFill(patch_t *pat)
 	}
 }
 
+void V_DrawVhsEffect(boolean rewind)
+{
+	static fixed_t upbary = 100, downbary = 150;
+
+	UINT8 *buf = screens[0], *tmp = screens[4];
+	UINT16 y;
+	UINT32 x, pos = 0;
+
+	UINT8 *normalmapstart = ((UINT8 *)transtables + (8<<FF_TRANSSHIFT|(19<<8)));
+#ifdef HQ_VHS
+	UINT8 *tmapstart = ((UINT8 *)transtables + (6<<FF_TRANSSHIFT));
+#endif
+	UINT8 *thismapstart;
+	SINT8 offs;
+
+	UINT8 barsize = vid.dupy<<5;
+	UINT8 updistort = vid.dupx<<(rewind ? 5 : 3);
+	UINT8 downdistort = updistort>>1;
+
+	if (rewind)
+		V_DrawVhsEffect(false); // experimentation
+
+	upbary -= vid.dupy * (rewind ? 3 : 1.8f);
+	downbary += vid.dupy * (rewind ? 2 : 1);
+	if (upbary < -barsize) upbary = vid.height;
+	if (downbary > vid.height) downbary = -barsize;
+
+	for (y = 0; y < vid.height; y+=2)
+	{
+		thismapstart = normalmapstart;
+		offs = 0;
+
+		if (y >= upbary && y < upbary+barsize)
+		{
+			thismapstart -= (2<<FF_TRANSSHIFT) - (5<<8);
+			offs += updistort * 2.0f * min(y-upbary, upbary+barsize-y) / barsize;
+		}
+		if (y >= downbary && y < downbary+barsize)
+		{
+			thismapstart -= (2<<FF_TRANSSHIFT) - (5<<8);
+			offs -= downdistort * 2.0f * min(y-downbary, downbary+barsize-y) / barsize;
+		}
+		offs += M_RandomKey(vid.dupx<<1);
+
+		// lazy way to avoid crashes
+		if (y == 0 && offs < 0) offs = 0;
+		else if (y >= vid.height-2 && offs > 0) offs = 0;
+
+		for (x = pos+vid.rowbytes*2; pos < x; pos++)
+		{
+			tmp[pos] = thismapstart[buf[pos+offs]];
+#ifdef HQ_VHS
+			tmp[pos] = tmapstart[buf[pos]<<8 | tmp[pos]];
+#endif
+		}
+	}
+
+	memcpy(buf, tmp, vid.rowbytes*vid.height);
+}
+
 //
 // Fade all the screen buffer, so that the menu is more readable,
 // especially now that we use the small hufont in the menus...
@@ -1234,9 +1295,12 @@ void V_DrawFadeScreen(UINT16 color, UINT8 strength)
 #endif
 
     {
-        const UINT8 *fadetable = ((color & 0xFF00) // Color is not palette index?
-        ? ((UINT8 *)colormaps + strength*256) // Do COLORMAP fade.
-        : ((UINT8 *)transtables + ((9-strength)<<FF_TRANSSHIFT) + color*256)); // Else, do TRANSMAP** fade.
+        const UINT8 *fadetable =
+			(color > 0xFFF0) // Grab a specific colormap palette?
+			? R_GetTranslationColormap(color | 0xFFFF0000, strength, GTC_CACHE)
+			: ((color & 0xFF00) // Color is not palette index?
+			? ((UINT8 *)colormaps + strength*256) // Do COLORMAP fade.
+			: ((UINT8 *)transtables + ((9-strength)<<FF_TRANSSHIFT) + color*256)); // Else, do TRANSMAP** fade.
         const UINT8 *deststop = screens[0] + vid.rowbytes * vid.height;
         UINT8 *buf = screens[0];
 
@@ -1498,8 +1562,11 @@ void V_DrawString(INT32 x, INT32 y, INT32 option, const char *string)
 	{
 		dupx = dupy = 1;
 		scrwidth = vid.width/vid.dupx;
-		left = (scrwidth - BASEVIDWIDTH)/2;
-		scrwidth -= left;
+		if (!(option & V_SNAPTOLEFT))
+		{
+			left = (scrwidth - BASEVIDWIDTH)/2;
+			scrwidth -= left;
+		}
 	}
 
 	charflags = (option & V_CHARCOLORMASK);
@@ -1908,6 +1975,12 @@ void V_DrawThinString(INT32 x, INT32 y, INT32 option, const char *string)
 	}
 }
 
+void V_DrawCenteredThinString(INT32 x, INT32 y, INT32 option, const char *string)
+{
+	x -= V_ThinStringWidth(string, option)/2;
+	V_DrawThinString(x, y, option, string);
+}
+
 void V_DrawRightAlignedThinString(INT32 x, INT32 y, INT32 option, const char *string)
 {
 	x -= V_ThinStringWidth(string, option);
@@ -2369,7 +2442,7 @@ INT32 V_ThinStringWidth(const char *string, INT32 option)
 
 boolean *heatshifter = NULL;
 INT32 lastheight = 0;
-INT32 heatindex[2] = { 0, 0 };
+INT32 heatindex[MAXSPLITSCREENPLAYERS] = {0, 0, 0, 0};
 
 //
 // V_DoPostProcessor
@@ -2502,9 +2575,6 @@ Unoptimized version
 		UINT8 *srcscr = screens[0];
 		INT32 y;
 
-		if (splitscreen > 1) // 3P/4P has trouble supporting this, anyone want to fix it? :p
-			return;
-
 		// Make sure table is built
 		if (heatshifter == NULL || lastheight != viewheight)
 		{
@@ -2519,7 +2589,7 @@ Unoptimized version
 					heatshifter[y] = true;
 			}
 
-			heatindex[0] = heatindex[1] = 0;
+			heatindex[0] = heatindex[1] = heatindex[2] = heatindex[3] = 0;
 			lastheight = viewheight;
 		}
 
diff --git a/src/v_video.h b/src/v_video.h
index 276399928b6922097f4655db827ed641614c73c9..d0dae2c7dc827fd819481760046accf8aa71b672 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -153,6 +153,9 @@ void V_DrawDiag(INT32 x, INT32 y, INT32 wh, INT32 c);
 // fill a box with a flat as a pattern
 void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum);
 
+// draw wobbly VHS pause stuff
+void V_DrawVhsEffect(boolean rewind);
+
 // fade down the screen buffer before drawing the menu over
 void V_DrawFadeScreen(UINT16 color, UINT8 strength);
 
@@ -184,6 +187,7 @@ void V_DrawRightAlignedSmallString(INT32 x, INT32 y, INT32 option, const char *s
 
 // draw a string using the tny_font
 void V_DrawThinString(INT32 x, INT32 y, INT32 option, const char *string);
+void V_DrawCenteredThinString(INT32 x, INT32 y, INT32 option, const char *string);
 void V_DrawRightAlignedThinString(INT32 x, INT32 y, INT32 option, const char *string);
 
 void V_DrawStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string);
diff --git a/src/w_wad.c b/src/w_wad.c
index 915701840e831f8e1cbf347c0fdac774ab83f22d..da82a276def3077c89fcce8da4bc1f7903c82387 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -652,7 +652,6 @@ UINT16 W_InitFile(const char *filename)
 	restype_t type;
 	UINT16 numlumps = 0;
 	size_t i;
-	size_t packetsize;
 	UINT8 md5sum[16];
 	boolean important;
 
@@ -684,24 +683,7 @@ UINT16 W_InitFile(const char *filename)
 	if ((handle = W_OpenWadFile(&filename, true)) == NULL)
 		return INT16_MAX;
 
-	// Check if wad files will overflow fileneededbuffer. Only the filename part
-	// is send in the packet; cf.
-	// see PutFileNeeded in d_netfil.c
-	if ((important = !W_VerifyNMUSlumps(filename)))
-	{
-		packetsize = packetsizetally + nameonlylength(filename) + 22;
-
-		if (packetsize > MAXFILENEEDED*sizeof(UINT8))
-		{
-			CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
-			refreshdirmenu |= REFRESHDIR_MAX;
-			if (handle)
-				fclose(handle);
-			return INT16_MAX;
-		}
-
-		packetsizetally = packetsize;
-	}
+	important = !W_VerifyNMUSlumps(filename);
 
 #ifndef NOMD5
 	//
diff --git a/src/win32/Makefile.cfg b/src/win32/Makefile.cfg
index 157d9744689b9f3d80a65233766db5bffdbd38af..e6675421ea2187b08471a519e38cea6ec198bc4b 100644
--- a/src/win32/Makefile.cfg
+++ b/src/win32/Makefile.cfg
@@ -24,8 +24,10 @@ ifndef NOASM
 	USEASM=1
 endif
 
+ifndef NONET
 ifndef MINGW64 #miniupnc is broken with MINGW64
 	HAVE_MINIUPNPC=1
+endif
 endif
 
 	OPTS=-DSTDC_HEADERS
diff --git a/src/win32/Srb2win-vc10.vcxproj b/src/win32/Srb2win-vc10.vcxproj
index 774ce5cbe8c7560c592f2b6b97e3db7864221347..ced3d128e56ef09633eaf08d90112ad40828cfe7 100644
--- a/src/win32/Srb2win-vc10.vcxproj
+++ b/src/win32/Srb2win-vc10.vcxproj
@@ -230,7 +230,11 @@
     <ClCompile Include="..\hardware\hw_light.c" />
     <ClCompile Include="..\hardware\hw_main.c" />
     <ClCompile Include="..\hardware\hw_md2.c" />
+    <ClCompile Include="..\hardware\hw_md2load.c" />
+    <ClCompile Include="..\hardware\hw_md3load.c" />
+    <ClCompile Include="..\hardware\hw_model.c" />
     <ClCompile Include="..\hardware\hw_trick.c" />
+    <ClCompile Include="..\hardware\u_list.c" />
     <ClCompile Include="..\hu_stuff.c" />
     <ClCompile Include="..\info.c" />
     <ClCompile Include="..\i_addrinfo.c">
@@ -394,6 +398,10 @@
     <ClInclude Include="..\hardware\hw_light.h" />
     <ClInclude Include="..\hardware\hw_main.h" />
     <ClInclude Include="..\hardware\hw_md2.h" />
+    <ClInclude Include="..\hardware\hw_md2load.h" />
+    <ClInclude Include="..\hardware\hw_md3load.h" />
+    <ClInclude Include="..\hardware\hw_model.h" />
+    <ClInclude Include="..\hardware\u_list.h" />
     <ClInclude Include="..\hu_stuff.h" />
     <ClInclude Include="..\info.h" />
     <ClInclude Include="..\i_addrinfo.h" />
diff --git a/src/win32/Srb2win-vc10.vcxproj.filters b/src/win32/Srb2win-vc10.vcxproj.filters
index d20dd672bd6df3b28734d89aa5baff5ab5478d5a..8f607796971841cb543a447e368407872c729ea9 100644
--- a/src/win32/Srb2win-vc10.vcxproj.filters
+++ b/src/win32/Srb2win-vc10.vcxproj.filters
@@ -453,6 +453,10 @@
     <ClCompile Include="..\string.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
+    <ClCompile Include="..\hardware\hw_md2load.c" />
+    <ClCompile Include="..\hardware\hw_md3load.c" />
+    <ClCompile Include="..\hardware\hw_model.c" />
+    <ClCompile Include="..\hardware\u_list.c" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="afxres.h">
@@ -506,6 +510,15 @@
     <ClInclude Include="..\hardware\hw_md2.h">
       <Filter>Hw_Hardware</Filter>
     </ClInclude>
+    <ClInclude Include="..\hardware\hw_md2load.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
+    <ClInclude Include="..\hardware\hw_md3load.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
+    <ClInclude Include="..\hardware\hw_model.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
     <ClInclude Include="..\hardware\hw3dsdrv.h">
       <Filter>Hw_Hardware</Filter>
     </ClInclude>
@@ -515,6 +528,9 @@
     <ClInclude Include="..\hardware\hws_data.h">
       <Filter>Hw_Hardware</Filter>
     </ClInclude>
+    <ClInclude Include="..\hardware\u_list.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
     <ClInclude Include="..\blua\lapi.h">
       <Filter>BLUA</Filter>
     </ClInclude>
diff --git a/src/win32/win_dll.c b/src/win32/win_dll.c
index 71eda04371d4223dedcd5e193d8c27ed8cf0f70d..bc67f04a5902db769d59dbabc6e2e5a48c3e9a02 100644
--- a/src/win32/win_dll.c
+++ b/src/win32/win_dll.c
@@ -109,8 +109,7 @@ static loadfunc_t hwdFuncTable[] = {
 	{"GClipRect@20",        &hwdriver.pfnGClipRect},
 	{"ClearMipMapCache@0",  &hwdriver.pfnClearMipMapCache},
 	{"SetSpecialState@8",   &hwdriver.pfnSetSpecialState},
-	{"DrawMD2@16",          &hwdriver.pfnDrawMD2},
-	{"DrawMD2i@36",         &hwdriver.pfnDrawMD2i},
+	{"DrawModel@16",          &hwdriver.pfnDrawModel},
 	{"SetTransform@4",      &hwdriver.pfnSetTransform},
 	{"GetTextureUsed@0",    &hwdriver.pfnGetTextureUsed},
 	{"GetRenderVersion@0",  &hwdriver.pfnGetRenderVersion},
@@ -140,8 +139,7 @@ static loadfunc_t hwdFuncTable[] = {
 	{"GClipRect",           &hwdriver.pfnGClipRect},
 	{"ClearMipMapCache",    &hwdriver.pfnClearMipMapCache},
 	{"SetSpecialState",     &hwdriver.pfnSetSpecialState},
-	{"DrawMD2",             &hwdriver.pfnDrawMD2},
-	{"DrawMD2i",            &hwdriver.pfnDrawMD2i},
+	{"DrawModel",             &hwdriver.pfnDrawModel},
 	{"SetTransform",        &hwdriver.pfnSetTransform},
 	{"GetTextureUsed",      &hwdriver.pfnGetTextureUsed},
 	{"GetRenderVersion",    &hwdriver.pfnGetRenderVersion},
diff --git a/src/win32/win_snd.c b/src/win32/win_snd.c
index 454c53e37ee7d5a1fa72bdebbeff9715f94855e6..f3e3bbed487132ef0421474f3dfc90ee70f0e84a 100644
--- a/src/win32/win_snd.c
+++ b/src/win32/win_snd.c
@@ -815,6 +815,60 @@ void I_SetMusicVolume(UINT8 volume)
 	FMR_MUSIC(FMOD_Channel_SetVolume(music_channel, music_volume / 31.0));
 }
 
+UINT32 I_GetSongLength(void)
+{
+	UINT32 length;
+	if (I_SongType() == MU_MID)
+		return 0;
+	FMR_MUSIC(FMOD_Sound_GetLength(music_stream, &length, FMOD_TIMEUNIT_MS));
+	return length;
+}
+
+boolean I_SetSongLoopPoint(UINT32 looppoint)
+{
+        (void)looppoint;
+        return false;
+}
+
+UINT32 I_GetSongLoopPoint(void)
+{
+	return 0;
+}
+
+boolean I_SetSongPosition(UINT32 position)
+{
+	FMOD_RESULT e;
+	if(I_SongType() == MU_MID)
+		// Dummy out; this works for some MIDI, but not others.
+		// SDL does not support this for any MIDI.
+		return false;
+	e = FMOD_Channel_SetPosition(music_channel, position, FMOD_TIMEUNIT_MS);
+	if (e == FMOD_OK)
+		return true;
+	else if (e == FMOD_ERR_UNSUPPORTED // Only music modules, numbnuts!
+			|| e == FMOD_ERR_INVALID_POSITION) // Out-of-bounds!
+		return false;
+	else // Congrats, you horribly broke it somehow
+	{
+		FMR_MUSIC(e);
+		return false;
+	}
+}
+
+UINT32 I_GetSongPosition(void)
+{
+	FMOD_RESULT e;
+	unsigned int fmposition = 0;
+	if(I_SongType() == MU_MID)
+		// Dummy out because unsupported, even though FMOD does this correctly.
+		return 0;
+	e = FMOD_Channel_GetPosition(music_channel, &fmposition, FMOD_TIMEUNIT_MS);
+	if (e == FMOD_OK)
+		return (UINT32)fmposition;
+	else
+		return 0;
+}
+
 boolean I_SetSongTrack(INT32 track)
 {
 	if (track != current_track) // If the track's already playing, then why bother?
@@ -859,3 +913,46 @@ boolean I_SetSongTrack(INT32 track)
 	}
 	return false;
 }
+
+/// ------------------------
+/// MUSIC FADING
+/// ------------------------
+
+void I_SetInternalMusicVolume(UINT8 volume)
+{
+	(void)volume;
+}
+
+void I_StopFadingSong(void)
+{
+}
+
+boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)source_volume;
+	(void)ms;
+	(void)callback;
+	return false;
+}
+
+boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void))
+{
+	(void)target_volume;
+	(void)ms;
+	(void)callback;
+	return false;
+}
+
+boolean I_FadeOutStopSong(UINT32 ms)
+{
+	(void)ms;
+	return false;
+}
+
+boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
+{
+        (void)ms;
+        (void)looping;
+        return false;
+}
diff --git a/src/win32/win_sys.c b/src/win32/win_sys.c
index 472503341352074f20075160f3767a20ee215ef7..a98aa8615931879632ef040a6fda7fef13b0a42e 100644
--- a/src/win32/win_sys.c
+++ b/src/win32/win_sys.c
@@ -644,7 +644,7 @@ void I_Error(const char *error, ...)
 
 	// save demo, could be useful for debug
 	// NOTE: demos are normally not saved here.
-	if (demorecording)
+	if (demo.recording)
 		G_CheckDemoStatus();
 	if (metalrecording)
 		G_StopMetalRecording();
@@ -730,7 +730,7 @@ void I_Quit(void)
 	DWORD mode;
 	// when recording a demo, should exit using 'q',
 	// but sometimes we forget and use Alt+F4, so save here too.
-	if (demorecording)
+	if (demo.recording)
 		G_CheckDemoStatus();
 	if (metalrecording)
 		G_StopMetalRecording();
diff --git a/src/y_inter.c b/src/y_inter.c
index c7e966c5fda863e34367efb535f56cdd4e6e20db..379694a11378f6e6dac8fa51686ca52977cbaaf1 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -40,6 +40,7 @@
 #include "g_input.h" // PLAYER1INPUTDOWN
 #include "k_kart.h" // colortranslations
 #include "console.h" // cons_menuhighlight
+#include "lua_hook.h" // IntermissionThinker hook
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -304,6 +305,15 @@ static void Y_CalculateMatchData(UINT8 rankingsmode, void (*comparison)(INT32))
 			players[i].score += data.match.increase[i];
 		}
 
+		if (demo.recording && !rankingsmode)
+			G_WriteStanding(
+				data.match.pos[data.match.numplayers],
+				data.match.name[data.match.numplayers],
+				*data.match.character[data.match.numplayers],
+				*data.match.color[data.match.numplayers],
+				data.match.val[data.match.numplayers]
+			);
+
 		data.match.numplayers++;
 	}
 }
@@ -351,7 +361,7 @@ void Y_IntermissionDrawer(void)
 		V_DrawFadeScreen(0xFF00, 22);
 
 	if (!splitscreen)
-		whiteplayer = demoplayback ? displayplayer : consoleplayer;
+		whiteplayer = demo.playback ? displayplayers[0] : consoleplayer;
 
 	if (cons_menuhighlight.value)
 		hilicol = cons_menuhighlight.value;
@@ -360,7 +370,7 @@ void Y_IntermissionDrawer(void)
 	else
 		hilicol = ((intertype == int_race) ? V_SKYMAP : V_REDMAP);
 
-	if (sorttic != -1 && intertic > sorttic)
+	if (sorttic != -1 && intertic > sorttic && !demo.playback)
 	{
 		INT32 count = (intertic - sorttic);
 
@@ -550,11 +560,37 @@ void Y_IntermissionDrawer(void)
 dotimer:
 	if (timer)
 	{
+		char *string;
 		INT32 tickdown = (timer+1)/TICRATE;
+
+		if (multiplayer && demo.playback)
+			string = va("Replay ends in %d", tickdown);
+		else
+			string = va("%s starts in %d", cv_advancemap.string, tickdown);
+
 		V_DrawCenteredString(BASEVIDWIDTH/2, 188, hilicol,
-			va("%s starts in %d", cv_advancemap.string, tickdown));
+			string);
 	}
 
+	if ((demo.recording || demo.savemode == DSM_SAVED) && !demo.playback)
+		switch (demo.savemode)
+		{
+		case DSM_NOTSAVING:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|hilicol, "Look Backward: Save replay");
+			break;
+
+		case DSM_SAVED:
+			V_DrawRightAlignedThinString(BASEVIDWIDTH - 2, 2, V_SNAPTOTOP|V_SNAPTORIGHT|V_ALLOWLOWERCASE|hilicol, "Replay saved!");
+			break;
+
+		case DSM_TITLEENTRY:
+			ST_DrawDemoTitleEntry();
+			break;
+
+		default: // Don't render any text here
+			break;
+		}
+
 	// Make it obvious that scrambling is happening next round.
 	if (cv_scrambleonchange.value && cv_teamscramble.value && (intertic/TICRATE % 2 == 0))
 		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, hilicol, M_GetText("Teams will be scrambled next round!"));
@@ -570,10 +606,23 @@ void Y_Ticker(void)
 	if (intertype == int_none)
 		return;
 
+	if (demo.recording)
+	{
+		if (demo.savemode == DSM_NOTSAVING && InputDown(gc_lookback, 1))
+			demo.savemode = DSM_TITLEENTRY;
+
+		if (demo.savemode == DSM_WILLSAVE || demo.savemode == DSM_WILLAUTOSAVE)
+			G_SaveDemo();
+	}
+
 	// Check for pause or menu up in single player
 	if (paused || P_AutoPause())
 		return;
 
+#ifdef HAVE_BLUA
+	LUAh_IntermissionThinker();
+#endif
+
 	intertic++;
 
 	// Team scramble code for team match and CTF.
@@ -613,7 +662,7 @@ void Y_Ticker(void)
 		{
 			if (sorttic == -1)
 				sorttic = intertic + max((cv_inttime.value/2)-2, 2)*TICRATE; // 8 second pause after match results
-			else
+			else if (!(multiplayer && demo.playback)) // Don't advance to rankings in replays
 			{
 				if (!data.match.rankingsmode && (intertic >= sorttic + 8))
 					Y_CalculateMatchData(1, Y_CompareRank);
@@ -762,6 +811,8 @@ void Y_StartIntermission(void)
 	{
 		if (cv_inttime.value == 0 && gametype == GT_COOP)
 			timer = 0;
+		else if (demo.playback) // Override inttime (which is pulled from the replay anyway
+			timer = 10*TICRATE;
 		else
 		{
 			timer = cv_inttime.value*TICRATE;
@@ -796,7 +847,7 @@ void Y_StartIntermission(void)
 		}
 		case int_race: // (time-only race)
 		{
-			if (!majormods && !multiplayer && !demoplayback) // remove this once we have a proper time attack screen
+			if (!majormods && !multiplayer && !demo.playback) // remove this once we have a proper time attack screen
 			{
 				// Update visitation flags
 				mapvisited[gamemap-1] |= MV_BEATEN;
@@ -1005,19 +1056,19 @@ void Y_VoteDrawer(void)
 					{
 						case 1:
 							thiscurs = cursor2;
-							p = secondarydisplayplayer;
+							p = displayplayers[1];
 							break;
 						case 2:
 							thiscurs = cursor3;
-							p = thirddisplayplayer;
+							p = displayplayers[2];
 							break;
 						case 3:
 							thiscurs = cursor4;
-							p = fourthdisplayplayer;
+							p = displayplayers[3];
 							break;
 						default:
 							thiscurs = cursor1;
-							p = displayplayer;
+							p = displayplayers[0];
 							break;
 					}
 
@@ -1172,10 +1223,7 @@ static void Y_VoteStops(SINT8 pick, SINT8 level)
 		S_StartSound(NULL, sfx_noooo2); // gasp
 	else if (mapheaderinfo[nextmap] && (mapheaderinfo[nextmap]->menuflags & LF2_HIDEINMENU))
 		S_StartSound(NULL, sfx_noooo1); // this is bad
-	else if (netgame && (pick == consoleplayer
-		|| pick == secondarydisplayplayer
-		|| pick == thirddisplayplayer
-		|| pick == fourthdisplayplayer))
+	else if (netgame && P_IsLocalPlayer(&players[pick]))
 		S_StartSound(NULL, sfx_yeeeah); // yeeeah!
 	else
 		S_StartSound(NULL, sfx_kc48); // just a cool sound
@@ -1308,13 +1356,13 @@ void Y_VoteTicker(void)
 			switch (i)
 			{
 				case 1:
-					p = secondarydisplayplayer;
+					p = displayplayers[1];
 					break;
 				case 2:
-					p = thirddisplayplayer;
+					p = displayplayers[2];
 					break;
 				case 3:
-					p = fourthdisplayplayer;
+					p = displayplayers[3];
 					break;
 				default:
 					p = consoleplayer;