diff --git a/CMakeLists.txt b/CMakeLists.txt
index eb91866f003bd53629bdfbfcfcda362873d54ea2..c85f237435bcf73dff0e68ba96356d64654c724d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -98,10 +98,10 @@ add_subdirectory(assets)
 ## config.h generation
 set(GIT_EXECUTABLE "git" CACHE FILEPATH "Path to git binary")
 include(GitUtilities)
-git_describe(SRB2_GIT_DESCRIBE "${CMAKE_SOURCE_DIR}")
+git_latest_commit(SRB2_COMP_COMMIT "${CMAKE_SOURCE_DIR}")
 git_current_branch(SRB2_GIT_BRANCH "${CMAKE_SOURCE_DIR}")
 set(SRB2_COMP_BRANCH "${SRB2_GIT_BRANCH}")
-set(SRB2_COMP_REVISION "${SRB2_GIT_DESCRIBE}")
+set(SRB2_COMP_REVISION "${SRB2_COMP_COMMIT}")
 configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/src/config.h)
 
 ##### PACKAGE CONFIGURATION #####
diff --git a/bin/Resources/i686/exchndl.dll b/bin/Resources/i686/exchndl.dll
deleted file mode 100644
index d836a676225f6d81dbfab9f23a4c55cd57c2c063..0000000000000000000000000000000000000000
Binary files a/bin/Resources/i686/exchndl.dll and /dev/null differ
diff --git a/cmake/Modules/GitUtilities.cmake b/cmake/Modules/GitUtilities.cmake
index 683cf9b6b505e788bd3139bf0513b433a3190a2e..d29e6b509dd27015f429c9e8f4de12e20fcc5b12 100644
--- a/cmake/Modules/GitUtilities.cmake
+++ b/cmake/Modules/GitUtilities.cmake
@@ -27,5 +27,17 @@ function(git_current_branch variable path)
 		OUTPUT_STRIP_TRAILING_WHITESPACE
 	)
 
+	set(${variable} "${output}" PARENT_SCOPE)
+endfunction()
+
+function(git_latest_commit variable path)
+	execute_process(COMMAND ${GIT_EXECUTABLE} "rev-parse" "--short" "HEAD"
+		WORKING_DIRECTORY "${path}"
+		RESULT_VARIABLE result
+		OUTPUT_VARIABLE output
+		ERROR_QUIET
+		OUTPUT_STRIP_TRAILING_WHITESPACE
+	)
+
 	set(${variable} "${output}" PARENT_SCOPE)
 endfunction()
\ No newline at end of file
diff --git a/libs/DLL-README.txt b/libs/DLL-README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..058ec06857776433f042da0b888e1f7efa24a259
--- /dev/null
+++ b/libs/DLL-README.txt
@@ -0,0 +1,43 @@
+# SRB2 - Which DLLs do I need to bundle?
+
+Updated 12/4/2018 (v2.1.21)
+
+Here are the required DLLs, per build. For each architecture, copy all the binaries from these folders:
+
+* libs\dll-binaries\[i686/x86_64]
+* libs\SDL2\[i686/x86_64]...\bin
+* libs\SDL2_mixer\[i686/x86_64]...\bin
+
+and don't forget to build r_opengl.dll for srb2dd.
+
+## srb2win, 32-bit
+
+* libs\dll-binaries\i686\exchndl.dll
+* libs\dll-binaries\i686\libgme.dll
+* libs\dll-binaries\i686\mgwhelp.dll (depend for exchndl.dll)
+* libs\SDL2\i686-w64-mingw32\bin\SDL2.dll
+* libs\SDL2_mixer\i686-w64-mingw32\bin\*.dll (get everything)
+
+## srb2win, 64-bit
+
+* libs\dll-binaries\x86_64\exchndl.dll
+* libs\dll-binaries\x86_64\libgme.dll
+* libs\dll-binaries\x86_64\mgwhelp.dll (depend for exchndl.dll)
+* libs\SDL2\x86_64-w64-mingw32\bin\SDL2.dll
+* libs\SDL2_mixer\x86_64-w64-mingw32\bin\*.dll (get everything)
+
+## srb2dd, 32-bit
+
+* libs\dll-binaries\i686\exchndl.dll
+* libs\dll-binaries\i686\fmodex.dll
+* libs\dll-binaries\i686\libgme.dll
+* libs\dll-binaries\i686\mgwhelp.dll (depend for exchndl.dll)
+* r_opengl.dll (build this from make)
+
+## srb2dd, 64-bit
+
+* libs\dll-binaries\x86_64\exchndl.dll
+* libs\dll-binaries\x86_64\fmodex.dll
+* libs\dll-binaries\x86_64\libgme.dll
+* libs\dll-binaries\x86_64\mgwhelp.dll (depend for exchndl.dll)
+* r_opengl.dll (build this from make)
diff --git a/bin/Resources/i686/fmod.dll b/libs/dll-binaries/i686/Old/fmod.dll
similarity index 100%
rename from bin/Resources/i686/fmod.dll
rename to libs/dll-binaries/i686/Old/fmod.dll
diff --git a/bin/Resources/i686/fmodexL.dll b/libs/dll-binaries/i686/Old/fmodexL.dll
similarity index 100%
rename from bin/Resources/i686/fmodexL.dll
rename to libs/dll-binaries/i686/Old/fmodexL.dll
diff --git a/bin/Resources/i686/libgcc_s_dw2-1.dll b/libs/dll-binaries/i686/Old/libgcc_s_dw2-1.dll
similarity index 100%
rename from bin/Resources/i686/libgcc_s_dw2-1.dll
rename to libs/dll-binaries/i686/Old/libgcc_s_dw2-1.dll
diff --git a/bin/Resources/i686/libintl-8.dll b/libs/dll-binaries/i686/Old/libintl-8.dll
similarity index 100%
rename from bin/Resources/i686/libintl-8.dll
rename to libs/dll-binaries/i686/Old/libintl-8.dll
diff --git a/libs/dll-binaries/i686/exchndl.dll b/libs/dll-binaries/i686/exchndl.dll
new file mode 100644
index 0000000000000000000000000000000000000000..d6beb764a77629be1f8bcc3677fb85927eec8672
Binary files /dev/null and b/libs/dll-binaries/i686/exchndl.dll differ
diff --git a/bin/Resources/i686/fmodex.dll b/libs/dll-binaries/i686/fmodex.dll
similarity index 100%
rename from bin/Resources/i686/fmodex.dll
rename to libs/dll-binaries/i686/fmodex.dll
diff --git a/bin/Resources/i686/libgme.dll b/libs/dll-binaries/i686/libgme.dll
similarity index 100%
rename from bin/Resources/i686/libgme.dll
rename to libs/dll-binaries/i686/libgme.dll
diff --git a/libs/dll-binaries/i686/mgwhelp.dll b/libs/dll-binaries/i686/mgwhelp.dll
new file mode 100644
index 0000000000000000000000000000000000000000..3cf97424d2c3e762d768b36c4a6855056a6e20c4
Binary files /dev/null and b/libs/dll-binaries/i686/mgwhelp.dll differ
diff --git a/bin/Resources/x86_64/fmod64.dll b/libs/dll-binaries/x86_64/Old/fmod64.dll
similarity index 100%
rename from bin/Resources/x86_64/fmod64.dll
rename to libs/dll-binaries/x86_64/Old/fmod64.dll
diff --git a/bin/Resources/x86_64/fmodexL64.dll b/libs/dll-binaries/x86_64/Old/fmodexL64.dll
similarity index 100%
rename from bin/Resources/x86_64/fmodexL64.dll
rename to libs/dll-binaries/x86_64/Old/fmodexL64.dll
diff --git a/libs/dll-binaries/x86_64/exchndl.dll b/libs/dll-binaries/x86_64/exchndl.dll
new file mode 100644
index 0000000000000000000000000000000000000000..747d7a3d560364af4d1ab588e251afd5d7b12871
Binary files /dev/null and b/libs/dll-binaries/x86_64/exchndl.dll differ
diff --git a/bin/Resources/x86_64/fmodex64.dll b/libs/dll-binaries/x86_64/fmodex64.dll
similarity index 100%
rename from bin/Resources/x86_64/fmodex64.dll
rename to libs/dll-binaries/x86_64/fmodex64.dll
diff --git a/bin/Resources/x86_64/libgme.dll b/libs/dll-binaries/x86_64/libgme.dll
similarity index 100%
rename from bin/Resources/x86_64/libgme.dll
rename to libs/dll-binaries/x86_64/libgme.dll
diff --git a/libs/dll-binaries/x86_64/mgwhelp.dll b/libs/dll-binaries/x86_64/mgwhelp.dll
new file mode 100644
index 0000000000000000000000000000000000000000..4e30e140e1b4fdfbb1b6ca3b72092e9b85b9bb9a
Binary files /dev/null and b/libs/dll-binaries/x86_64/mgwhelp.dll differ
diff --git a/src/Makefile.cfg b/src/Makefile.cfg
index 20219168f7bcf57493afa7d80857f237ee7a24d1..cdd4c3c734cb4c3fde2d735903ce7fc4d42065a8 100644
--- a/src/Makefile.cfg
+++ b/src/Makefile.cfg
@@ -20,7 +20,7 @@ GCC64=1
 endif
 
 ifdef GCC64
-GCC64=1
+GCC63=1
 endif
 
 ifdef GCC63
diff --git a/src/config.h.in b/src/config.h.in
index 5f06ec45df306429a53eb26dd883939d5173df76..7452ec80c206d2111b9d9b52c7d308439565ba01 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -21,8 +21,6 @@
 
 #define SRB2_COMP_REVISION    "${SRB2_COMP_REVISION}"
 #define SRB2_COMP_BRANCH      "${SRB2_COMP_BRANCH}"
-#define SRB2_GIT_DESCRIBE     "${SRB2_GIT_DESCRIBE}"
-#define SRB2_GIT_BRANCH       "${SRB2_GIT_BRANCH}"
 
 #define CMAKE_ASSETS_DIR      "${CMAKE_SOURCE_DIR}/assets"
 
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 739d3009d6d8b4343d2fc69c7672c160c5d1ea1d..fe717c8d701405e60a4de6ea2551fa13c1392fc5 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -240,7 +240,7 @@ INT32 cv_debug;
 consvar_t cv_usemouse = {"use_mouse", "On", CV_SAVE|CV_CALL,usemouse_cons_t, I_StartupMouse, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_usemouse2 = {"use_mouse2", "Off", CV_SAVE|CV_CALL,usemouse_cons_t, I_StartupMouse2, 0, NULL, NULL, 0, 0, NULL};
 
-#if defined (DC) || defined (_XBOX) || defined (WMINPUT) || defined (_WII) //joystick 1 and 2
+#if defined (DC) || defined (_XBOX) || defined (WMINPUT) || defined (_WII) || defined(HAVE_SDL) || defined(_WIN32) //joystick 1 and 2
 consvar_t cv_usejoystick = {"use_joystick", "1", CV_SAVE|CV_CALL, usejoystick_cons_t,
 	I_InitJoystick, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_usejoystick2 = {"use_joystick2", "2", CV_SAVE|CV_CALL, usejoystick_cons_t,
diff --git a/src/m_anigif.c b/src/m_anigif.c
index 8450992110e1209a1679da7cbb3017cafd023f5b..4e68819bc70313bc120366abd3ffeba13578f4fc 100644
--- a/src/m_anigif.c
+++ b/src/m_anigif.c
@@ -492,8 +492,8 @@ static void GIF_framewrite(void)
 
 	// screen regions are handled in GIF_lzw
 	{
-		int d1 = (int)((100.0/NEWTICRATE)*(gif_frames+1));
-		int d2 = (int)((100.0/NEWTICRATE)*(gif_frames));
+		int d1 = (int)((100.0f/NEWTICRATE)*(gif_frames+1));
+		int d2 = (int)((100.0f/NEWTICRATE)*(gif_frames));
 		UINT16 delay = d1-d2;
 		INT32 startline;
 
diff --git a/src/m_menu.c b/src/m_menu.c
index 0ca5aaf666f16c855d22187f180e5519f2efa43a..3d759be3ed6d0fd798f1c34c3a36d06a10c8099b 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -54,6 +54,8 @@
 #include "st_stuff.h"
 #include "i_sound.h"
 
+#include "i_joy.h" // for joystick menu controls
+
 // Condition Sets
 #include "m_cond.h"
 
@@ -160,7 +162,7 @@ typedef enum
 levellist_mode_t levellistmode = LLM_CREATESERVER;
 UINT8 maplistoption = 0;
 
-static char joystickInfo[8][25];
+static char joystickInfo[8][29];
 #ifndef NONET
 static UINT32 serverlistpage;
 #endif
@@ -2074,6 +2076,7 @@ boolean M_Responder(event_t *ev)
 	INT32 ch = -1;
 //	INT32 i;
 	static tic_t joywait = 0, mousewait = 0;
+	static INT32 pjoyx = 0, pjoyy = 0;
 	static INT32 pmousex = 0, pmousey = 0;
 	static INT32 lastx = 0, lasty = 0;
 	void (*routine)(INT32 choice); // for some casting problem
@@ -2128,26 +2131,45 @@ boolean M_Responder(event_t *ev)
 		}
 		else if (ev->type == ev_joystick  && ev->data1 == 0 && joywait < I_GetTime())
 		{
-			if (ev->data3 == -1)
-			{
-				ch = KEY_UPARROW;
-				joywait = I_GetTime() + NEWTICRATE/7;
-			}
-			else if (ev->data3 == 1)
+			const INT32 jdeadzone = JOYAXISRANGE/4;
+			if (ev->data3 != INT32_MAX)
 			{
-				ch = KEY_DOWNARROW;
-				joywait = I_GetTime() + NEWTICRATE/7;
+				if (Joystick.bGamepadStyle || abs(ev->data3) > jdeadzone)
+				{
+					if (ev->data3 < 0 && pjoyy >= 0)
+					{
+						ch = KEY_UPARROW;
+						joywait = I_GetTime() + NEWTICRATE/7;
+					}
+					else if (ev->data3 > 0 && pjoyy <= 0)
+					{
+						ch = KEY_DOWNARROW;
+						joywait = I_GetTime() + NEWTICRATE/7;
+					}
+					pjoyy = ev->data3;
+				}
+				else
+					pjoyy = 0;
 			}
 
-			if (ev->data2 == -1)
+			if (ev->data2 != INT32_MAX)
 			{
-				ch = KEY_LEFTARROW;
-				joywait = I_GetTime() + NEWTICRATE/17;
-			}
-			else if (ev->data2 == 1)
-			{
-				ch = KEY_RIGHTARROW;
-				joywait = I_GetTime() + NEWTICRATE/17;
+				if (Joystick.bGamepadStyle || abs(ev->data2) > jdeadzone)
+				{
+					if (ev->data2 < 0 && pjoyx >= 0)
+					{
+						ch = KEY_LEFTARROW;
+						joywait = I_GetTime() + NEWTICRATE/17;
+					}
+					else if (ev->data2 > 0 && pjoyx <= 0)
+					{
+						ch = KEY_RIGHTARROW;
+						joywait = I_GetTime() + NEWTICRATE/17;
+					}
+					pjoyx = ev->data2;
+				}
+				else
+					pjoyx = 0;
 			}
 		}
 		else if (ev->type == ev_mouse && mousewait < I_GetTime())
@@ -6726,35 +6748,32 @@ static void M_DrawJoystick(void)
 
 	M_DrawGenericMenu();
 
-	for (i = 0;i < 8; i++)
+	for (i = 0;i < 7; i++)
 	{
-		M_DrawSaveLoadBorder(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i);
+		M_DrawTextBox(OP_JoystickSetDef.x-8, OP_JoystickSetDef.y+LINEHEIGHT*i-12, 28, 1);
+		//M_DrawSaveLoadBorder(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i);
 
 		if ((setupcontrols_secondaryplayer && (i == cv_usejoystick2.value))
 			|| (!setupcontrols_secondaryplayer && (i == cv_usejoystick.value)))
-			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i,V_GREENMAP,joystickInfo[i]);
+			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i-4,V_GREENMAP,joystickInfo[i]);
 		else
-			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i,0,joystickInfo[i]);
+			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i-4,0,joystickInfo[i]);
 	}
 }
 
 static void M_SetupJoystickMenu(INT32 choice)
 {
 	INT32 i = 0;
-	const char *joyname = "None";
 	const char *joyNA = "Unavailable";
 	INT32 n = I_NumJoys();
 	(void)choice;
 
-	strcpy(joystickInfo[i], joyname);
+	strcpy(joystickInfo[i], "None");
 
 	for (i = 1; i < 8; i++)
 	{
-		if (i <= n && (joyname = I_GetJoyName(i)) != NULL)
-		{
-			strncpy(joystickInfo[i], joyname, 24);
-			joystickInfo[i][24] = '\0';
-		}
+		if (i <= n && (I_GetJoyName(i)) != NULL)
+			strncpy(joystickInfo[i], I_GetJoyName(i), 28);
 		else
 			strcpy(joystickInfo[i], joyNA);
 	}
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 2b35ce8b8a044c824b6f825a117d5ae1cf9703dc..50c3018aa0603adba67d941deec8dcae38411a57 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -894,8 +894,8 @@ void I_GetJoystickEvents(void)
 	UINT64 joyhats = 0;
 #if 0
 	UINT64 joybuttons = 0;
-#endif
 	Sint16 axisx, axisy;
+#endif
 
 	if (!joystick_started) return;
 
@@ -963,6 +963,7 @@ void I_GetJoystickEvents(void)
 		}
 	}
 
+#if 0
 	// send joystick axis positions
 	event.type = ev_joystick;
 
@@ -1013,6 +1014,7 @@ void I_GetJoystickEvents(void)
 		}
 		D_PostEvent(&event);
 	}
+#endif
 }
 
 /**	\brief	Open joystick handle
@@ -1176,8 +1178,8 @@ void I_GetJoystick2Events(void)
 	UINT64 joyhats = 0;
 #if 0
 	INT64 joybuttons = 0;
-#endif
 	INT32 axisx, axisy;
+#endif
 
 	if (!joystick2_started)
 		return;
@@ -1247,6 +1249,7 @@ void I_GetJoystick2Events(void)
 		}
 	}
 
+#if 0
 	// send joystick axis positions
 	event.type = ev_joystick2;
 
@@ -1297,7 +1300,7 @@ void I_GetJoystick2Events(void)
 		}
 		D_PostEvent(&event);
 	}
-
+#endif
 }
 
 /**	\brief	Open joystick handle
@@ -1453,18 +1456,28 @@ INT32 I_NumJoys(void)
 	return numjoy;
 }
 
+static char joyname[255]; // MAX_PATH; joystick name is straight from the driver
+
 const char *I_GetJoyName(INT32 joyindex)
 {
-	const char *joyname = "NA";
+	const char *tempname = NULL;
 	joyindex--; //SDL's Joystick System starts at 0, not 1
 	if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
 	{
 		if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != -1)
-			joyname = SDL_JoystickNameForIndex(joyindex);
+		{
+			tempname = SDL_JoystickNameForIndex(joyindex);
+			if (tempname)
+				strncpy(joyname, tempname, 255);
+		}
 		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
 	}
 	else
-		joyname = SDL_JoystickNameForIndex(joyindex);
+	{
+		tempname = SDL_JoystickNameForIndex(joyindex);
+		if (tempname)
+			strncpy(joyname, tempname, 255);
+	}
 	return joyname;
 }
 
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 3cc29dbb466476461d0daf21458271f739095db3..2c199c2d0ff76361fd1eb29414e5495d8119499d 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -767,6 +767,33 @@ static void Impl_HandleJoystickAxisEvent(SDL_JoyAxisEvent evt)
 	D_PostEvent(&event);
 }
 
+#if 0
+static void Impl_HandleJoystickHatEvent(SDL_JoyHatEvent evt)
+{
+	event_t event;
+	SDL_JoystickID joyid[2];
+
+	// Determine the Joystick IDs for each current open joystick
+	joyid[0] = SDL_JoystickInstanceID(JoyInfo.dev);
+	joyid[1] = SDL_JoystickInstanceID(JoyInfo2.dev);
+
+	if (evt.hat >= JOYHATS)
+		return; // ignore hats with too high an index
+
+	if (evt.which == joyid[0])
+	{
+		event.data1 = KEY_HAT1 + (evt.hat*4);
+	}
+	else if (evt.which == joyid[1])
+	{
+		event.data1 = KEY_2HAT1 + (evt.hat*4);
+	}
+	else return;
+
+	// NOTE: UNFINISHED
+}
+#endif
+
 static void Impl_HandleJoystickButtonEvent(SDL_JoyButtonEvent evt, Uint32 type)
 {
 	event_t event;
@@ -804,6 +831,8 @@ static void Impl_HandleJoystickButtonEvent(SDL_JoyButtonEvent evt, Uint32 type)
 	if (event.type != ev_console) D_PostEvent(&event);
 }
 
+
+
 void I_GetEvent(void)
 {
 	SDL_Event evt;
@@ -844,6 +873,11 @@ void I_GetEvent(void)
 			case SDL_JOYAXISMOTION:
 				Impl_HandleJoystickAxisEvent(evt.jaxis);
 				break;
+#if 0
+			case SDL_JOYHATMOTION:
+				Impl_HandleJoystickHatEvent(evt.jhat)
+				break;
+#endif
 			case SDL_JOYBUTTONUP:
 			case SDL_JOYBUTTONDOWN:
 				Impl_HandleJoystickButtonEvent(evt.jbutton, evt.type);
diff --git a/src/sdl/macosx/Srb2mac.icns b/src/sdl/macosx/Srb2mac.icns
index 4baedc1c5a091a3917d2a33bc3780da5b1b5e996..96cb8a36d991818eb03b7d80343518ab168bda7d 100644
Binary files a/src/sdl/macosx/Srb2mac.icns and b/src/sdl/macosx/Srb2mac.icns differ
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index a3c4219913627f5cf048c38a34b52ff70d224978..3f9b09f10e934d8db387139a6b7e65018a6d70a0 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -50,8 +50,8 @@
 
 #ifdef HAVE_LIBGME
 #include "gme/gme.h"
-#define GME_TREBLE 5.0
-#define GME_BASS 1.0
+#define GME_TREBLE 5.0f
+#define GME_BASS 1.0f
 
 #ifdef HAVE_ZLIB
 #ifndef _MSC_VER
diff --git a/src/sdl12/i_system.c b/src/sdl12/i_system.c
index 8729e5921d5b423945c47a58d2fb820a2f72d354..10fbc50eed0892fa9301560477b0f8c84d1d7530 100644
--- a/src/sdl12/i_system.c
+++ b/src/sdl12/i_system.c
@@ -1575,18 +1575,28 @@ INT32 I_NumJoys(void)
 	return numjoy;
 }
 
+static char joyname[255]; // MAX_PATH; joystick name is straight from the driver
+
 const char *I_GetJoyName(INT32 joyindex)
 {
-	const char *joyname = "NA";
+	const char *tempname = NULL;
 	joyindex--; //SDL's Joystick System starts at 0, not 1
 	if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
 	{
 		if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != -1)
-			joyname = SDL_JoystickName(joyindex);
+		{
+			tempname = SDL_JoystickNameForIndex(joyindex);
+			if (tempname)
+				strncpy(joyname, tempname, 255);
+		}
 		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
 	}
 	else
-		joyname = SDL_JoystickName(joyindex);
+	{
+		tempname = SDL_JoystickNameForIndex(joyindex);
+		if (tempname)
+			strncpy(joyname, tempname, 255);
+	}
 	return joyname;
 }
 
diff --git a/src/sdl12/macosx/Srb2mac.icns b/src/sdl12/macosx/Srb2mac.icns
index 4baedc1c5a091a3917d2a33bc3780da5b1b5e996..96cb8a36d991818eb03b7d80343518ab168bda7d 100644
Binary files a/src/sdl12/macosx/Srb2mac.icns and b/src/sdl12/macosx/Srb2mac.icns differ
diff --git a/windows-installer/! MAKE SURE FILES ARE IN new-install ARCHIVE FOLDER !.txt b/windows-installer/! MAKE SURE FILES ARE IN new-install ARCHIVE FOLDER !.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/windows-installer/.gitignore b/windows-installer/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40ca37130d71f523e934d82f7640b6897c0a58da
--- /dev/null
+++ b/windows-installer/.gitignore
@@ -0,0 +1,10 @@
+*
+*.*
+!staging
+!sfx
+!uninstaller
+!! MAKE SURE FILES ARE IN new-install ARCHIVE FOLDER !.txt
+!BuildInstaller.bat
+!README.txt
+!VersionFileName.txt
+!.gitignore
diff --git a/windows-installer/BuildInstaller.bat b/windows-installer/BuildInstaller.bat
new file mode 100644
index 0000000000000000000000000000000000000000..cc6ff74b42b7bb902f05818ed093ebb626db90cf
--- /dev/null
+++ b/windows-installer/BuildInstaller.bat
@@ -0,0 +1,76 @@
+@echo off
+
+set "SCRIPTDIR=%~dp0"
+set "SCRIPTDIR=%SCRIPTDIR:~0,-1%"
+
+IF [%SRB2VERSIONNAME%] == [] set /p SRB2VERSIONNAME=<"%SCRIPTDIR%\VersionFileName.txt"
+
+:: Find 7z
+
+set SVZIP=
+
+if NOT [%1] == [] (
+	echo.%~1 | findstr /C:"7z" 1>nul
+	if NOT errorlevel 1 (
+		if exist "%~1" set "SVZIP=%~1"
+	)
+)
+
+if ["%SVZIP%"] == [""] (
+	if exist "%ProgramFiles(x86)%\7-Zip\7z.exe" set "SVZIP=%ProgramFiles(x86)%\7-Zip\7z.exe"
+	if exist "%ProgramFiles%\7-Zip\7z.exe" set "SVZIP=%ProgramFiles%\7-Zip\7z.exe"
+	if exist "%ProgramW6432%\7-Zip\7z.exe" set "SVZIP=%ProgramW6432%\7-Zip\7z.exe"
+)
+
+:: Is it in PATH?
+
+if ["%SVZIP%"] == [""] (
+	"7z.exe" --help > NUL 2> NUL
+	if NOT errorlevel 1 (
+		set "SVZIP=7z.exe"
+	)
+)
+
+:: Create the uninstaller
+
+if NOT ["%SVZIP%"] == [""] (
+	del /f /q "%SCRIPTDIR%\Uninstaller.7z" 2> NUL
+	"%SVZIP%" a -t7z "%SCRIPTDIR%\Uninstaller.7z" "%SCRIPTDIR%\uninstaller\*" -m5=LZMA2
+	copy /y /b "%SCRIPTDIR%\sfx\7zsd_LZMA2.sfx" + "%SCRIPTDIR%\sfx\config-uninstaller.txt" + "%SCRIPTDIR%\Uninstaller.7z" "%SCRIPTDIR%\staging\new-install\uninstall.exe"
+	del /f /q "%SCRIPTDIR%\uninstaller.7z"
+)
+
+:: Operate on install archives
+
+type NUL > "%SCRIPTDIR%\staging\new-install\staging.txt"
+
+if exist "%SCRIPTDIR%\Installer.7z" (
+	if NOT ["%SVZIP%"] == [""] (
+		"%SVZIP%" a "%SCRIPTDIR%\Installer.7z" "%SCRIPTDIR%\staging\*"
+	)
+	copy /y /b "%SCRIPTDIR%\sfx\7zsd_LZMA2.sfx" + "%SCRIPTDIR%\sfx\config-installer.txt" + "%SCRIPTDIR%\Installer.7z" "%SCRIPTDIR%\SRB2-%SRB2VERSIONNAME%-Installer.exe"
+)
+
+if exist "%SCRIPTDIR%\Patch.7z" (
+	if NOT ["%SVZIP%"] == [""] (
+		"%SVZIP%" a "%SCRIPTDIR%\Patch.7z" "%SCRIPTDIR%\staging\*"
+	)
+	copy /y /b "%SCRIPTDIR%\sfx\7zsd_LZMA2.sfx" + "%SCRIPTDIR%\sfx\config-patch.txt" + "%SCRIPTDIR%\Patch.7z" "%SCRIPTDIR%\SRB2-%SRB2VERSIONNAME%-Patch.exe"
+)
+
+if exist "%SCRIPTDIR%\Installer_x64.7z" (
+	if NOT ["%SVZIP%"] == [""] (
+		"%SVZIP%" a "%SCRIPTDIR%\Installer_x64.7z" "%SCRIPTDIR%\staging\*"
+	)
+	copy /y /b "%SCRIPTDIR%\sfx\7zsd_LZMA2_x64.sfx" + "%SCRIPTDIR%\sfx\config-installer.txt" + "%SCRIPTDIR%\Installer_x64.7z" "%SCRIPTDIR%\SRB2-%SRB2VERSIONNAME%-x64-Installer.exe"
+)
+
+if exist "%SCRIPTDIR%\Patch_x64.7z" (
+	if NOT ["%SVZIP%"] == [""] (
+		"%SVZIP%" a "%SCRIPTDIR%\Patch_x64.7z" "%SCRIPTDIR%\staging\*"
+	)
+	copy /y /b "%SCRIPTDIR%\sfx\7zsd_LZMA2_x64.sfx" + "%SCRIPTDIR%\sfx\config-patch.txt" + "%SCRIPTDIR%\Patch_x64.7z" "%SCRIPTDIR%\SRB2-%SRB2VERSIONNAME%-x64-Patch.exe"
+)
+
+del /f /q "%SCRIPTDIR%\staging\new-install\staging.txt"
+del /f /q "%SCRIPTDIR%\staging\new-install\uninstall.exe"
diff --git a/windows-installer/README.txt b/windows-installer/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..67995f336a4807f4e91236ca2ae4d6da9a7792cd
--- /dev/null
+++ b/windows-installer/README.txt
@@ -0,0 +1,48 @@
+Windows Install Builder
+for SRB2
+
+This installer is much like the 7-Zip self-extracting archive, except
+this allows for scripting the post-install step.
+
+This also allows for some light customization, including dialog messages
+and program shortcuts.
+
+The included install scripts manage the game data location depending on the
+install location -- if installed in Program Files or AppData\Local, the
+game data location is set to %UserProfile%\SRB2.
+
+Program shortcuts are also added, as well as an uninstaller that
+will remove the icons and also selectively uninstall the core game files.
+The uninstaller gives you the option to preserve your game data and mods.
+
+How to Use
+----------
+
+1. Zip up the install contents in 7z format.
+    * ALL FILES MUST BE IN THE `new-install/` ARCHIVE SUBFOLDER, OR THE
+      POST-INSTALL SCRIPT WILL NOT WORK!
+    * Make sure you are using the LZMA2 algorithm, which is 7-Zip's default.
+
+2. Copy the 7z archive to this folder using the following names:
+    * Installer.7z     - 32-bit full installer
+    * Patch.7z         - 32-bit patch
+    * Installer_x64.7z - 64-bit full installer
+    * Patch_x64.7z     - 64-bit patch
+
+3. Set the text in VersionFilename.txt to the version identifier for the
+   installer's filename.
+    * e.g., v2121 for v2.1.21, from "SRB2-v2121-Installer.exe"
+    * Also look through sfx/config-installer.txt and sfx/config-patch.txt
+      and update the version strings. Templating is TODO.
+
+4. Run BuildInstaller.bat [7z.exe install path]
+    * First argument is the path to 7z.exe. If this is not specified, the
+      script will try to look for it in C:\Program Files [(x86)]
+    * This script will automatically add the install scripts to each
+      installer.
+
+Credit
+------
+
+OlegScherbakov/7zSFX
+https://github.com/OlegScherbakov/7zSFX
diff --git a/windows-installer/VersionFileName.txt b/windows-installer/VersionFileName.txt
new file mode 100644
index 0000000000000000000000000000000000000000..559abffe4b5bda69d860102145264731f3f50575
--- /dev/null
+++ b/windows-installer/VersionFileName.txt
@@ -0,0 +1 @@
+v2121
\ No newline at end of file
diff --git a/windows-installer/sfx/7zsd_LZMA2.sfx b/windows-installer/sfx/7zsd_LZMA2.sfx
new file mode 100644
index 0000000000000000000000000000000000000000..c4ba9c58477ac5dd141d8c02242c6547577d79b5
Binary files /dev/null and b/windows-installer/sfx/7zsd_LZMA2.sfx differ
diff --git a/windows-installer/sfx/7zsd_LZMA2_x64.sfx b/windows-installer/sfx/7zsd_LZMA2_x64.sfx
new file mode 100644
index 0000000000000000000000000000000000000000..758e4c2d02d96b897d50eed125344a23a8a691d0
Binary files /dev/null and b/windows-installer/sfx/7zsd_LZMA2_x64.sfx differ
diff --git a/windows-installer/sfx/config-installer.txt b/windows-installer/sfx/config-installer.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bdbddea55cf0a7e0c0f5e2f9b40fb8c5d43da974
--- /dev/null
+++ b/windows-installer/sfx/config-installer.txt
@@ -0,0 +1,24 @@
+;!@Install@!UTF-8!
+
+GUIFlags="8+32+64+4096"
+GUIMode="1"
+
+Title="Sonic Robo Blast 2 v2.1.21"
+BeginPrompt="Sonic Robo Blast 2 v2.1.21\nFull Installer\n\nSelect a folder to install SRB2 in.\n\nIf you install in \"AppData\\Local\" or \"Program Files\", your game data will be saved to:\n%UserProfile%\\SRB2\n\nOtherwise, your game data will be in the installation folder.\n\nShortcuts will be created in your Start Menu."
+
+ExtractPathText="Installation folder: (no exclamation points, please!)"
+InstallPath="%LocalAppData%\\SRB2"
+ExtractTitle="Installing..."
+ExtractDialogText="Installing SRB2 v2.1.21...\n\nCheck out our modding community!\nWe make levels, characters, and much more!\n\nVisit http://www.srb2.org/mods"
+
+Shortcut="Pu,{%%T\\srb2win.exe},{},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2win.exe},{-win},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (Windowed)},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2win.exe},{-opengl},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (OpenGL)},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2win.exe},{-opengl -win},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (OpenGL, Windowed)},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2dd.exe},{},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (DirectDraw)},{%%T\\},{%%T\\srb2dd.exe},{0}"
+Shortcut="Pu,{%%T\\srb2dd.exe},{-win},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (DirectDraw, Windowed)},{%%T\\},{%%T\\srb2dd.exe},{0}"
+Shortcut="Pu,{%%T\\uninstall.exe},{},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{Uninstall SRB2},{%%T\\},{shell32.dll},{31}"
+
+RunProgram="nowait:\"%%T\\new-install\\staging.bat\""
+
+;!@InstallEnd@!
diff --git a/windows-installer/sfx/config-patch.txt b/windows-installer/sfx/config-patch.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bde6135dcb501d89579d15cd6f0841a0b3d12c7b
--- /dev/null
+++ b/windows-installer/sfx/config-patch.txt
@@ -0,0 +1,24 @@
+;!@Install@!UTF-8!
+
+GUIFlags="8+32+64+4096"
+GUIMode="1"
+
+Title="Sonic Robo Blast 2 v2.1.21"
+BeginPrompt="Sonic Robo Blast 2 v2.1.21\nPatch Installer\n\nYou must have at least v2.1.15 to use this patch.\n\nSelect your current SRB2 folder.\n\nShortcuts will be created in your Start Menu."
+
+ExtractPathText="Current SRB2 folder: (no exclamation points, please!)"
+InstallPath="%LocalAppData%\\SRB2"
+ExtractTitle="Installing..."
+ExtractDialogText="Installing SRB2 v2.1.21...\n\nCheck out our modding community!\nWe make levels, characters, and much more!\n\nVisit http://www.srb2.org/mods"
+
+Shortcut="Pu,{%%T\\srb2win.exe},{},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2win.exe},{-win},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (Windowed)},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2win.exe},{-opengl},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (OpenGL)},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2win.exe},{-opengl -win},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (OpenGL, Windowed)},{%%T\\},{%%T\\srb2win.exe},{0}"
+Shortcut="Pu,{%%T\\srb2dd.exe},{},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (DirectDraw)},{%%T\\},{%%T\\srb2dd.exe},{0}"
+Shortcut="Pu,{%%T\\srb2dd.exe},{-win},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{SRB2 (DirectDraw, Windowed)},{%%T\\},{%%T\\srb2dd.exe},{0}"
+Shortcut="Pu,{%%T\\uninstall.exe},{},{Sonic Robo Blast 2},{Sonic Robo Blast 2 (SRB2), a 3D Sonic the Hedgehog fangame.},{Uninstall SRB2},{%%T\\},{shell32.dll},{31}"
+
+RunProgram="nowait:\"%%T\\new-install\\staging.bat\""
+
+;!@InstallEnd@!
diff --git a/windows-installer/sfx/config-uninstaller.txt b/windows-installer/sfx/config-uninstaller.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f3de9e2698694f4785427b0549bb8d8b025cae4c
--- /dev/null
+++ b/windows-installer/sfx/config-uninstaller.txt
@@ -0,0 +1,13 @@
+;!@Install@!UTF-8!
+
+GUIFlags="1+2+8"
+GUIMode="2"
+
+Title="Uninstall SRB2"
+BeginPrompt="Are you sure you want to uninstall Sonic Robo Blast 2?\n\nYour game data and mods will be preserved, as well as\nany extra files in your install folder."
+
+InstallPath="%%S"
+
+RunProgram="nowait:\"%%S\\uninstall.bat\" /y"
+
+;!@InstallEnd@!
diff --git a/windows-installer/staging/! SRB2 INSTALL INSTRUCTIONS !.txt b/windows-installer/staging/! SRB2 INSTALL INSTRUCTIONS !.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f367296f53eed8fa1bedeb2bafdc9d6cbccad5bf
--- /dev/null
+++ b/windows-installer/staging/! SRB2 INSTALL INSTRUCTIONS !.txt	
@@ -0,0 +1,11 @@
+SRB2 Install Instructions
+
+1. Move every file from the "new-install" folder to this main install folder.
+
+2. DELETE "staging.bat" and "staging.txt"! These can mess up your installation if run by accident!
+
+3. Optionally, create a folder in your user profile named "SRB2". This is where your game data and addons may live. For example,
+
+	C:\Users\[User]\SRB2
+
+4. Run the game! Double-click srb2win.exe -- or see if you have Start Menu icons under "Sonic Robo Blast 2".
\ No newline at end of file
diff --git a/windows-installer/staging/new-install/old-install-list.txt b/windows-installer/staging/new-install/old-install-list.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6f33660ac12c4e1e24ff6fc60efc0fe4389e79ee
--- /dev/null
+++ b/windows-installer/staging/new-install/old-install-list.txt
@@ -0,0 +1,15 @@
+exchndl.dll
+fmodex.dll
+libFLAC-8.dll
+libgme.dll
+libintl-8.dll
+libmikmod-2.dll
+libogg-0.dll
+libvorbis-0.dll
+libvorbisfile-3.dll
+r_opengl.dll
+SDL2.dll
+SDL2_mixer.dll
+smpeg2.dll
+srb2dd.exe
+srb2win.exe
\ No newline at end of file
diff --git a/windows-installer/staging/new-install/staging.bat b/windows-installer/staging/new-install/staging.bat
new file mode 100644
index 0000000000000000000000000000000000000000..2c3c94915e9c772fb5f255909c3608b93a1aa887
--- /dev/null
+++ b/windows-installer/staging/new-install/staging.bat
@@ -0,0 +1,363 @@
+@echo off
+
+setlocal enabledelayedexpansion
+
+cls
+
+:: SRB2 Install Staging
+::
+:: This accomplishes the following tasks:
+::
+:: 1. Creates a user profile folder if SRB2 is installed in AppData or Program Files, and config.cfg is not already in the install folder
+::
+:: 2. Moves old installation files into old-install
+::
+:: 3. Moves new installaton files into install folder
+::
+
+:: Get Parent folder (the SRB2 install folder)
+::
+:: https://wiert.me/2011/08/30/batch-file-to-get-parent-directory-not-the-directory-of-the-batch-file-but-the-parent-of-that-directory/
+
+set "STAGINGDIR=%~dp0"
+:: strip trailing backslash
+set "STAGINGDIR=!STAGINGDIR:~0,-1!"
+::  ~dp only works for batch file parameters and loop indexes
+for %%d in ("!STAGINGDIR!") do set "INSTALLDIR=%%~dpd"
+set "INSTALLDIR=!INSTALLDIR:~0,-1!"
+
+:: Find 7z
+
+set SVZIP=
+if ["%SVZIP%"] == [""] (
+	if exist "!ProgramFiles(x86)!\7-Zip\7z.exe" set "SVZIP=!ProgramFiles(x86)!\7-Zip\7z.exe"
+	if exist "!ProgramFiles!\7-Zip\7z.exe" set "SVZIP=!ProgramFiles!\7-Zip\7z.exe"
+	if exist "!ProgramW6432!\7-Zip\7z.exe" set "SVZIP=!ProgramW6432!\7-Zip\7z.exe"
+)
+
+:: Is it in PATH?
+
+if ["%SVZIP%"] == [""] (
+	"7z.exe" --help > NUL 2> NUL
+	if NOT errorlevel 1 (
+		set "SVZIP=7z.exe"
+	)
+)
+
+:: FAILSAFE: Check if staging.txt exists in the directory
+:: If not, exit, so we don't mess up anything by accident.
+
+if NOT exist "!STAGINGDIR!\staging.txt" (
+	exit
+)
+
+: CheckPermissionsInstall
+
+:: Write a dummy file and check for an error. If error, we need administrator rights
+
+:: NOTE: We should never have to deal with this because the main installer should
+:: already have the rights.
+
+mkdir "!INSTALLDIR!\install-dummy"
+
+:: TODO elevate automatically
+if errorlevel 1 (
+	echo Finish installing SRB2 with these steps:
+	echo.
+	echo 1. Go to your SRB2 install folder
+	echo.
+	echo     !INSTALLDIR!
+	echo.
+	echo 2. Copy all files from the "new-install" subfolder into the main folder
+	echo    and DELETE staging.bat and staging.txt!!!
+	echo.
+	echo 3. Optionally, create a folder in your user profile named "SRB2".
+	echo    This is where your game data and addons may live.
+	echo    To create the folder, go here:
+	echo.
+	echo     !USERPROFILE!
+	echo.
+	echo If anything fails, you may delete the files and try to run the installer
+	echo again with Administrator Rights: Right-click on the icon and click
+	echo "Run as administrator."
+	echo.
+	"!SystemRoot!\explorer.exe" "!INSTALLDIR!"
+	set /p ADMINFINAL="Press Enter key to exit. "
+
+	exit
+) else (
+	rmdir /s /q "!INSTALLDIR!\install-dummy"
+	goto CheckUserDir
+)
+
+: CheckUserDir
+
+:: Check if we need to create !userprofile!\SRB2
+
+set "USERDIR=!INSTALLDIR!"
+
+:: Is config.cfg in our install dir?
+if exist "!INSTALLDIR!\config.cfg" goto MoveOldInstall
+
+:: Are we in AppData?
+echo.!STAGINGDIR! | findstr /C:"!LocalAppData!" 1>nul
+if errorlevel 1 (
+	echo.
+) else (
+	goto SetUserDir
+)
+
+: Are we in Program Files?
+echo.!STAGINGDIR! | findstr /C:"!ProgramFiles!" 1>nul
+if NOT errorlevel 1 (
+	goto SetUserDir
+)
+
+:: Are we in Program Files (x86)?
+echo.!STAGINGDIR! | findstr /C:"!ProgramFiles(X86)!" 1>nul
+if NOT errorlevel 1 (
+	goto SetUserDir
+)
+
+:: Are we 32-bit and actually in Program Files?
+echo.!STAGINGDIR! | findstr /C:"!ProgramW6432!" 1>nul
+if NOT errorlevel 1 (
+	goto SetUserDir
+)
+
+goto MoveOldInstall
+
+: SetUserDir
+: CheckPermissionsUserDir
+
+set "USERDIR=!UserProfile!\SRB2"
+
+:: Check for permissions and create the folder
+if exist "!USERDIR!\*" (
+ 	mkdir "!USERDIR!\install-dummy"
+
+	if errorlevel 1 (
+		echo User profile folder exists, but we won't operate on it.
+		echo.
+		goto MoveOldInstall
+	) else (
+		rmdir /s /q "!USERDIR!\install-dummy"
+	)
+) else (
+	mkdir "!USERDIR!"
+
+	if errorlevel 1 (
+		echo Could not create user profile folder
+		echo Defaulting to install dir: "!INSTALLDIR!"
+		echo.
+		set "USERDIR=!INSTALLDIR!"
+		goto MoveOldInstall
+	)
+)
+
+:: Now copy READMEs
+:: echo f answers xcopy's prompt as to whether the destination is a file or a folder
+echo f | xcopy /y "!STAGINGDIR!\README.txt" "!USERDIR!\README.txt"
+echo f | xcopy /y "!STAGINGDIR!\LICENSE.txt" "!USERDIR!\LICENSE.txt"
+echo f | xcopy /y "!STAGINGDIR!\LICENSE-3RD-PARTY.txt" "!USERDIR!\LICENSE-3RD-PARTY.txt"
+echo Your game data and mods folder is: > "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo. >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo     !USERDIR! >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo. >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo Your install folder is: >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo. >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo     !INSTALLDIR! >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo. >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo To run SRB2, go to: >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo. >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+echo     Start Menu ^> Programs ^> Sonic Robo Blast 2 >> "!USERDIR!\^! Data and Mods Go Here ^!.txt"
+
+:: Copy path to install folder
+
+set "SCRIPT=!TEMP!\!RANDOM!-!RANDOM!-!RANDOM!-!RANDOM!.vbs"
+echo Set oWS = WScript.CreateObject("WScript.Shell") >> "!SCRIPT!"
+echo sLinkFile = "!USERDIR!\^! SRB2 Install Folder ^!.lnk" >> "!SCRIPT!"
+echo Set oLink = oWS.CreateShortcut(sLinkFile) >> "!SCRIPT!"
+echo oLink.TargetPath = "!INSTALLDIR!" >> "!SCRIPT!"
+echo oLink.WorkingDirectory = "!INSTALLDIR!" >> "!SCRIPT!"
+echo oLink.Arguments = "" >> "!SCRIPT!"
+echo oLink.IconLocation = "!INSTALLDIR!\srb2win.exe,0" >> "!SCRIPT!"
+echo oLink.Save >> "!SCRIPT!"
+cscript /nologo "!SCRIPT!"
+del "!SCRIPT!"
+
+:: Also do it the other way around
+
+set "SCRIPT=!TEMP!\!RANDOM!-!RANDOM!-!RANDOM!-!RANDOM!.vbs"
+echo Set oWS = WScript.CreateObject("WScript.Shell") >> "!SCRIPT!"
+echo sLinkFile = "!INSTALLDIR!\^! SRB2 Data Folder ^!.lnk" >> "!SCRIPT!"
+echo Set oLink = oWS.CreateShortcut(sLinkFile) >> "!SCRIPT!"
+echo oLink.TargetPath = "!USERDIR!" >> "!SCRIPT!"
+echo oLink.WorkingDirectory = "!USERDIR!" >> "!SCRIPT!"
+echo oLink.Arguments = "" >> "!SCRIPT!"
+echo oLink.IconLocation = "!INSTALLDIR!\srb2win.exe,0" >> "!SCRIPT!"
+echo oLink.Save >> "!SCRIPT!"
+cscript /nologo "!SCRIPT!"
+del "!SCRIPT!"
+
+: MoveOldInstall
+
+if exist "!INSTALLDIR!\old-install\*" (
+	set "OLDINSTALLDIR=!INSTALLDIR!\old-install-!RANDOM!"
+) else (
+	set "OLDINSTALLDIR=!INSTALLDIR!\old-install"
+)
+
+mkdir "!OLDINSTALLDIR!"
+
+::
+:: Move all old install files
+:: We support a list of explicit files to copy to old-install
+:: And later, we also loop through our staging files, look for the pre-existing copy in
+:: install root, then copy that also to old-install
+::
+
+:: Extract the uninstall-list.txt and uninstall-userdir.txt files from uninstaller.exe
+:: if it exists
+
+if exist "!INSTALLDIR!\uninstall.exe" (
+	if NOT ["!SVZIP!"] == [""] (
+		"!SVZIP!" x "!INSTALLDIR!\uninstall.exe" "uninstall-list.txt" -o"!INSTALLDIR!"
+		"!SVZIP!" x "!INSTALLDIR!\uninstall.exe" "uninstall-userdir.txt" -o"!INSTALLDIR!"
+	)
+)
+
+set OLDINSTALLCHANGED=
+
+if exist "!STAGINGDIR!\old-install-list.txt" (
+	goto MoveOldInstallOldFiles
+) else (
+	goto MoveOldInstallNewFiles
+)
+
+: MoveOldInstallOldFiles
+
+set "TESTFILE=!TEMP!\!RANDOM!.txt"
+
+:: Do our failsafes before copying the file in the list
+:: See uninstall.bat for details
+for /F "usebackq tokens=*" %%A in ("!STAGINGDIR!\old-install-list.txt") do (
+	if exist "!INSTALLDIR!\%%A" (
+		if NOT ["%%A"] == [""] (
+			if NOT ["%%A"] == ["%~nx0"] (
+				echo %%A> "!TESTFILE!"
+				findstr /r ".*[<>:\"\"/\\|?*%%].*" "!TESTFILE!" >nul
+				if !errorlevel! equ 0 (
+					echo %%A has invalid characters, skipping...
+				) else (
+					if exist "!INSTALLDIR!\%%A\*" (
+						echo %%A is a folder, skipping...
+					) else (
+						echo Moving !INSTALLDIR!\%%A to "old-install" folder
+						echo f | xcopy /y /v "!INSTALLDIR!\%%A" "!OLDINSTALLDIR!\%%A"
+						if errorlevel 0 del /f /q "!INSTALLDIR!\%%A"
+					)
+				)
+			)
+		)
+	)
+)
+
+del /q /f "!STAGINGDIR!\old-install-list.txt"
+
+for %%F in ("!OLDINSTALLDIR!\*") DO (
+	set OLDINSTALLCHANGED=1
+	goto MoveOldInstallNewFiles
+)
+
+: MoveOldInstallNewFiles
+
+:: Save a list of standard files
+:: So the uninstall script will know what to remove
+:: Append to any existing file, in case we are a patch
+
+dir /b /a-d "!STAGINGDIR!" >> "!INSTALLDIR!\uninstall-list.txt"
+
+:: Overwrite the last known gamedata folder
+
+echo !USERDIR! > "!INSTALLDIR!\uninstall-userdir.txt"
+
+:: Add the install-generated to the uninstall list
+:: NO FOLLOWING SPACES AFTER THE FILENAME!!!
+
+echo uninstall.bat>> "!INSTALLDIR!\uninstall-list.txt"
+echo uninstall-list.txt>> "!INSTALLDIR!\uninstall-list.txt"
+echo uninstall-userdir.txt>> "!INSTALLDIR!\uninstall-list.txt"
+:: *ahem* Prints as ^! SRB2 Data Folder ^!.lnk
+:: We need to escape the exclamations (^^!) and the carets themselves (^^^^)
+echo ^^^^^^! SRB2 Data Folder ^^^^^^!.lnk>> "!INSTALLDIR!\uninstall-list.txt"
+
+:: Add the uninstall list files to the uninstall EXE
+
+if NOT ["!SVZIP!"] == [""] (
+	if exist "!INSTALLDIR!\new-install\uninstall.exe" (
+		"!SVZIP!" a "!INSTALLDIR!\new-install\uninstall.exe" "!INSTALLDIR!\uninstall-list.txt" -sdel
+		"!SVZIP!" a "!INSTALLDIR!\new-install\uninstall.exe" "!INSTALLDIR!\uninstall-userdir.txt" -sdel
+	)
+)
+
+:: Start moving files
+
+for %%F in ("!STAGINGDIR!\*") DO (
+	if exist "!INSTALLDIR!\%%~nxF" (
+		set OLDINSTALLCHANGED=1
+		move "!INSTALLDIR!\%%~nxF" "!OLDINSTALLDIR!\%%~nxF"
+	)
+	if NOT ["%%~nxF"] == ["staging.bat"] (
+		if NOT ["%%~nxF"] == ["staging.txt"] (
+			move "!STAGINGDIR!\%%~nxF" "!INSTALLDIR!\%%~nxF"
+		)
+	)
+)
+
+: Finished
+
+del /q /f "!INSTALLDIR!\^! SRB2 INSTALL INSTRUCTIONS ^!.txt"
+
+set MSGEXE=
+if exist "!SystemRoot!\System32\msg.exe" (
+	set MSGEXE=!SystemRoot!\System32\msg.exe
+) else (
+	if exist "!SystemRoot!\Sysnative\msg.exe" (
+		set MSGEXE=!SystemRoot!\Sysnative\msg.exe
+	)
+)
+
+if ["!OLDINSTALLCHANGED!"] == ["1"] (
+	"!systemroot!\explorer.exe" /select, "!OLDINSTALLDIR!"
+	echo Finished^^! Some of your old installation files were moved to the "old-install" folder. > !TEMP!\srb2msgprompt.txt
+	echo. >> !TEMP!\srb2msgprompt.txt
+	echo If you no longer need these files, you may delete the folder safely. >> !TEMP!\srb2msgprompt.txt
+	echo. >> !TEMP!\srb2msgprompt.txt
+	echo To run SRB2, go to: Start Menu ^> Programs ^> Sonic Robo Blast 2. >> !TEMP!\srb2msgprompt.txt
+	!MSGEXE! "!username!" < !TEMP!\srb2msgprompt.txt
+	del !TEMP!\srb2msgprompt.txt
+) else (
+	if /I ["!USERDIR!"] == ["!INSTALLDIR!"] (
+		"!systemroot!\explorer.exe" "!INSTALLDIR!"
+		echo Finished^^! > !TEMP!\srb2msgprompt.txt
+		echo. >> !TEMP!\srb2msgprompt.txt
+		echo To run SRB2, go to: Start Menu ^> Programs ^> Sonic Robo Blast 2. >> !TEMP!\srb2msgprompt.txt
+		!MSGEXE! "!username!" < !TEMP!\srb2msgprompt.txt
+		del !TEMP!\srb2msgprompt.txt
+	) else (
+		"!systemroot!\explorer.exe" "!USERDIR!"
+		echo Finished^^! You may find your game data in this folder: > !TEMP!\srb2msgprompt.txt
+		echo. >> !TEMP!\srb2msgprompt.txt
+		echo     !USERDIR! >> !TEMP!\srb2msgprompt.txt
+		echo. >> !TEMP!\srb2msgprompt.txt
+		echo To run SRB2, go to: Start Menu ^> Programs ^> Sonic Robo Blast 2. >> !TEMP!\srb2msgprompt.txt
+		!MSGEXE! "!username!" < !TEMP!\srb2msgprompt.txt
+		del !TEMP!\srb2msgprompt.txt
+	)
+)
+
+: Attempt to remove OLDINSTALLDIR, in case it's empty
+rmdir /q "!OLDINSTALLDIR!"
+cd \
+start "" /b "cmd" /s /c " del /f /q "%STAGINGDIR%\*"&rmdir /s /q "%STAGINGDIR%"&exit /b "
diff --git a/windows-installer/uninstaller/uninstall.bat b/windows-installer/uninstaller/uninstall.bat
new file mode 100644
index 0000000000000000000000000000000000000000..ed7c33391ac3e83316adb0e8271020a998dddc99
--- /dev/null
+++ b/windows-installer/uninstaller/uninstall.bat
@@ -0,0 +1,183 @@
+@echo off
+
+setlocal enabledelayedexpansion
+
+cls
+
+set "INSTALLDIR=%~dp0"
+set "INSTALLDIR=!INSTALLDIR:~0,-1!"
+set /p USERDIR=<"!INSTALLDIR!\uninstall-userdir.txt"
+set "USERDIR=!USERDIR:~0,-1!"
+
+: ProceedPrompt
+
+if ["%1"] == ["/y"] (
+	set "PROCEED=1"
+) else (
+	set PROCEED=
+	set /p PROCEED="Are you sure you want to uninstall SRB2? [yes/no] "
+
+	if /I ["!PROCEED:~0,1!"] == ["n"] exit
+	if /I ["!PROCEED!"] == ["y"] (
+		echo Type Yes or No
+		echo.
+		goto ProceedPrompt
+	) else (
+		if /I ["!PROCEED!"] == ["yes"] (
+			set PROCEED=1
+		) else (
+			echo.
+			goto ProceedPrompt
+		)
+	)
+)
+
+:: Failsafe, in case we Ctrl+C and decline "Terminate batch file?"
+
+if NOT ["!PROCEED!"] == ["1"] (
+	exit
+)
+
+: CheckPermissions
+
+:: Write a dummy file and check for an error. If error, we need administrator rights
+
+mkdir "!INSTALLDIR!\uninstall-dummy"
+
+:: TODO elevate automatically
+if errorlevel 1 (
+	echo We need Administrator Rights to uninstall SRB2.
+	echo.
+	echo Try running this uninstaller by right-clicking on the icon
+	echo and click "Run as administrator"
+	echo.
+	set /p ADMINFINAL="Press Enter key to exit. "
+	exit
+) else (
+	rmdir /s /q "!INSTALLDIR!\uninstall-dummy"
+	goto DeleteFiles
+)
+
+: DeleteFiles
+
+:: Our deletion list is a list of filenames, no paths, in the current folder
+::
+:: We apply the following failsafes:
+:: 1. Is filename the script itself?
+:: 2. Does filename have illegal characters? https://stackoverflow.com/a/33625339/241046
+:: 3. Is filename a directory?
+::
+:: TODO hack this to support .\file.txt relative paths
+:: Can %%A be substring'd to get only the filename and extension?
+:: If so, print that to the temp file instead of the whole line
+:: And possibly do the folder check before the invalid char check.
+:: ALSO: Don't honor upward relative paths! (..\)
+::
+set "TESTFILE=!TEMP!\!RANDOM!.txt"
+
+for /F "usebackq tokens=*" %%A in ("!INSTALLDIR!\uninstall-list.txt") do (
+	if exist "!INSTALLDIR!\%%A" (
+		if NOT ["%%A"] == [""] (
+			if NOT ["%%A"] == ["%~nx0"] (
+				echo %%A> "!TESTFILE!"
+				findstr /r ".*[<>:\"\"/\\|?*%%].*" "!TESTFILE!" >nul
+				if !errorlevel! equ 0 (
+					echo %%A has invalid characters, skipping...
+				) else (
+					if exist "!INSTALLDIR!\%%A\*" (
+						echo %%A is a folder, skipping...
+					) else (
+						echo Deleting !INSTALLDIR!\%%A
+						del /q /f "!INSTALLDIR!\%%A"
+					)
+				)
+			)
+		)
+	)
+)
+
+del /q /f "!TESTFILE!"
+
+: AllDone
+
+:: Delete the program icons
+echo Deleting your program icons...
+echo.
+
+cd \
+rmdir /s /q "!AppData!\Microsoft\Windows\Start Menu\Programs\Sonic Robo Blast 2"
+
+:: Check if our install folder is non-empty
+
+set USERDIRFILLED=
+set INSTALLDIRFILLED=
+for /F %%i in ('dir /b /a "!USERDIR!\*"') do (
+    if NOT ["%%i"] == ["%~nx0"] (
+		set USERDIRFILLED=1
+		goto InstallFilledCheck
+	)
+)
+
+: InstallFilledCheck
+
+if /I NOT ["!USERDIR!"] == ["!INSTALLDIR!"] (
+	for /F %%i in ('dir /b /a "!INSTALLDIR!\*"') do (
+		if ["%%i"] == ["%~nx0"] (
+			echo.
+		) else (
+			set INSTALLDIRFILLED=1
+			goto Final
+		)
+	)
+)
+
+: Final
+
+echo All done^^! Visit http://www.srb2.org if you want to play SRB2 again^^!
+echo.
+
+set "FINALPROMPT=Press Enter key to exit."
+if ["!USERDIRFILLED!"] == ["1"] (
+	echo We left your game data and mods alone, so you may delete those manually.
+	echo.
+	echo    !USERDIR!
+	echo.
+	set "FINALPROMPT=Do you want to view your data? [yes/no]"
+)
+
+if ["!INSTALLDIRFILLED!"] == ["1"] (
+	echo We left some extra files alone in your install folder.
+	echo.
+	echo    !INSTALLDIR!
+	echo.
+	set "FINALPROMPT=Do you want to view your data? [yes/no]"
+)
+
+set FINALRESPONSE=
+set /p FINALRESPONSE="!FINALPROMPT! "
+
+if NOT ["!FINALPROMPT!"] == ["Press Enter key to exit."] (
+	if /I ["!FINALRESPONSE:~0,1!"] == ["y"] (
+		if ["!USERDIRFILLED!"] == ["1"] (
+			"!SystemRoot!\explorer.exe" "!USERDIR!"
+		)
+		if ["!INSTALLDIRFILLED!"] == ["1"] (
+			"!SystemRoot!\explorer.exe" "!INSTALLDIR!"
+		)
+	) else (
+		if ["!FINALRESPONSE!"] == [""] (
+			if ["!USERDIRFILLED!"] == ["1"] (
+				"!SystemRoot!\explorer.exe" "!USERDIR!"
+			)
+			if ["!INSTALLDIRFILLED!"] == ["1"] (
+				"!SystemRoot!\explorer.exe" "!INSTALLDIR!"
+			)
+		)
+	)
+)
+
+: DeferredDelete
+
+:: Now let's delete our installation folder!
+cd \
+start "" /b "cmd" /s /c " del /q /f "%INSTALLDIR%\uninstall.bat"&timeout /t 2 > NUL&rmdir "%INSTALLDIR%"&exit /b "