diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index c5cb0c70299ac443c70dd06fb725b14c942bf73c..0bb3d3db75257ffa0bbfebdfe5e3a65f0ffeef43 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -95,6 +95,8 @@ UINT32 playerpingtable[MAXPLAYERS]; //table of player latency values.
 #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)
+SINT8 nodetoplayer4[MAXNETNODES]; // say the numplayer for this node if any (splitscreen)
 UINT8 playerpernode[MAXNETNODES]; // used specialy for scplitscreen
 boolean nodeingame[MAXNETNODES]; // set false as nodes leave game
 static tic_t nettics[MAXNETNODES]; // what tic the client have received
@@ -125,6 +127,8 @@ static UINT8 mynode; // my address pointofview server
 
 static UINT8 localtextcmd[MAXTEXTCMD];
 static UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
+static UINT8 localtextcmd3[MAXTEXTCMD]; // splitscreen
+static UINT8 localtextcmd4[MAXTEXTCMD]; // splitscreen
 static tic_t neededtic;
 SINT8 servernode = 0; // the number of the server node
 /// \brief do we accept new players?
@@ -2313,6 +2317,8 @@ static void Command_connect(void)
 	}
 
 	splitscreen = false;
+	splitscreen3 = false;
+	splitscreen4 = false;
 	SplitScreen_OnChange();
 	botingame = false;
 	botskin = 0;
@@ -3083,7 +3089,7 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 
 	// Clear player before joining, lest some things get set incorrectly
 	// HACK: don't do this for splitscreen, it relies on preset values
-	if (!splitscreen && !botingame)
+	if ((!splitscreen || !splitscreen3 || !splitscreen4) && !botingame)
 		CL_ClearPlayer(newplayernum);
 	playeringame[newplayernum] = true;
 	G_AddPlayer(newplayernum);
@@ -3287,7 +3293,7 @@ void SV_StartSinglePlayerServer(void)
 	// no more tic the game with this settings!
 	SV_StopServer();
 
-	if (splitscreen)
+	if (splitscreen || splitscreen3 || splitscreen4)
 		multiplayer = true;
 }
 
@@ -4410,9 +4416,19 @@ static void Local_Maketic(INT32 realtics)
 	if (!dedicated) rendergametic = gametic;
 	// translate inputs (keyboard/mouse/joystick) into game controls
 	G_BuildTiccmd(&localcmds, realtics);
-	if (splitscreen || botingame)
+	if ((splitscreen || splitscreen3 || splitscreen4) || botingame)
+	{
 		G_BuildTiccmd2(&localcmds2, realtics);
 
+		if (splitscreen3 || splitscreen4)
+		{
+			G_BuildTiccmd3(&localcmds3, realtics);
+
+			if (splitscreen4)
+				G_BuildTiccmd4(&localcmds4, realtics);
+		}
+	}
+
 	localcmds.angleturn |= TICCMD_RECEIVED;
 }
 
diff --git a/src/d_main.c b/src/d_main.c
index 7cc78b40eaeb48750c770f47e87784cb1049394a..7b262998e0a627ebbf6785e303319af5f6101c71 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -120,7 +120,10 @@ 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;
 #ifdef _XBOX
 boolean nomidimusic = true, nosound = true;
 boolean nodigimusic = true;
@@ -628,8 +631,12 @@ void D_SRB2Loop(void)
 			// Lagless camera! Yay!
 			if (gamestate == GS_LEVEL && netgame)
 			{
-				if (splitscreen && camera2.chase)
+				if ((splitscreen || splitscreen3 || splitscreen4) && camera2.chase)
 					P_MoveChaseCamera(&players[secondarydisplayplayer], &camera2, false);
+				if ((splitscreen3 || splitscreen4) && camera3.chase)
+					P_MoveChaseCamera(&players[thirddisplayplayer], &camera3, false);
+				if (splitscreen4 && camera4.chase)
+					P_MoveChaseCamera(&players[fourthdisplayplayer], &camera3, false);
 				if (camera.chase)
 					P_MoveChaseCamera(&players[displayplayer], &camera, false);
 			}
@@ -706,6 +713,8 @@ void D_StartTitle(void)
 		CL_ClearPlayer(i);
 
 	splitscreen = false;
+	splitscreen3 = false;
+	splitscreen4 = false;
 	SplitScreen_OnChange();
 	botingame = false;
 	botskin = 0;
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 5e0db8d4f0e478e3bc16fd35424ab1bc03e1797f..20316d993761676238444756cfabbf208d8d851b 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -1674,6 +1674,30 @@ void SendWeaponPref2(void)
 	SendNetXCmd2(XD_WEAPONPREF, buf, 1);
 }
 
+void SendWeaponPref3(void)
+{
+	XBOXSTATIC UINT8 buf[1];
+
+	buf[0] = 0;
+	if (players[thirddisplayplayer].pflags & PF_FLIPCAM)
+		buf[0] |= 1;
+	if (players[thirddisplayplayer].pflags & PF_ANALOGMODE)
+		buf[0] |= 2;
+	//SendNetXCmd3(XD_WEAPONPREF, buf, 1);
+}
+
+void SendWeaponPref4(void)
+{
+	XBOXSTATIC UINT8 buf[1];
+
+	buf[0] = 0;
+	if (players[fourthdisplayplayer].pflags & PF_FLIPCAM)
+		buf[0] |= 1;
+	if (players[fourthdisplayplayer].pflags & PF_ANALOGMODE)
+		buf[0] |= 2;
+	//SendNetXCmd4(XD_WEAPONPREF, buf, 1);
+}
+
 static void Got_WeaponPref(UINT8 **cp,INT32 playernum)
 {
 	UINT8 prefs = READUINT8(*cp);
@@ -1688,11 +1712,19 @@ static void Got_WeaponPref(UINT8 **cp,INT32 playernum)
 void D_SendPlayerConfig(void)
 {
 	SendNameAndColor();
-	if (splitscreen || botingame)
+	if ((splitscreen || splitscreen3 || splitscreen4) || botingame)
 		SendNameAndColor2();
+	if (splitscreen3 || splitscreen4)
+		SendNameAndColor3();
+	if (splitscreen4)
+		SendNameAndColor4();
 	SendWeaponPref();
-	if (splitscreen)
+	if (splitscreen || splitscreen3 || splitscreen4)
 		SendWeaponPref2();
+	if (splitscreen3 || splitscreen4)
+		SendWeaponPref3();
+	if (splitscreen4)
+		SendWeaponPref4();
 }
 
 // Only works for displayplayer, sorry!
@@ -2520,6 +2552,16 @@ static void Command_Teamchange2_f(void)
 	SendNetXCmd2(XD_TEAMCHANGE, &usvalue, sizeof(usvalue));
 }
 
+static void Command_Teamchange3_f(void)
+{
+	;
+}
+
+static void Command_Teamchange4_f(void)
+{
+	;
+}
+
 static void Command_ServerTeamChange_f(void)
 {
 	changeteam_union NetPacket;
@@ -4315,6 +4357,8 @@ void Command_ExitGame_f(void)
 		CL_ClearPlayer(i);
 
 	splitscreen = false;
+	splitscreen3 = false;
+	splitscreen4 = false;
 	SplitScreen_OnChange();
 	botingame = false;
 	botskin = 0;
@@ -4509,6 +4553,28 @@ static void Name2_OnChange(void)
 		SendNameAndColor2();
 }
 
+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]);
+	}
+	else
+		SendNameAndColor3();
+}
+
+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]);
+	}
+	else
+		SendNameAndColor4();
+}
+
 /** Sends a skin change for the console player, unless that player is moving.
   * \sa cv_skin, Skin2_OnChange, Color_OnChange
   * \author Graue <graue@oceanbase.org>
@@ -4541,7 +4607,7 @@ static void Skin_OnChange(void)
   */
 static void Skin2_OnChange(void)
 {
-	if (!Playing() || !splitscreen)
+	if (!Playing() || (!splitscreen || !splitscreen3 || !splitscreen4))
 		return; // do whatever you want
 
 	if (CanChangeSkin(secondarydisplayplayer) && !P_PlayerMoving(secondarydisplayplayer))
@@ -4553,6 +4619,34 @@ static void Skin2_OnChange(void)
 	}
 }
 
+static void Skin3_OnChange(void)
+{
+	if (!Playing() || (!splitscreen3 || !splitscreen4))
+		return; // do whatever you want
+
+	if (CanChangeSkin(thirddisplayplayer) && !P_PlayerMoving(thirddisplayplayer))
+		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);
+	}
+}
+
+static void Skin4_OnChange(void)
+{
+	if (!Playing() || !splitscreen4)
+		return; // do whatever you want
+
+	if (CanChangeSkin(fourthdisplayplayer) && !P_PlayerMoving(fourthdisplayplayer))
+		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);
+	}
+}
+
 /** Sends a color change for the console player, unless that player is moving.
   * \sa cv_playercolor, Color2_OnChange, Skin_OnChange
   * \author Graue <graue@oceanbase.org>
@@ -4587,7 +4681,7 @@ static void Color_OnChange(void)
   */
 static void Color2_OnChange(void)
 {
-	if (!Playing() || !splitscreen)
+	if (!Playing() || (!splitscreen || !splitscreen3 || !splitscreen4))
 		return; // do whatever you want
 
 	if (!P_PlayerMoving(secondarydisplayplayer))
@@ -4602,6 +4696,40 @@ static void Color2_OnChange(void)
 	}
 }
 
+static void Color3_OnChange(void)
+{
+	if (!Playing() || (!splitscreen3 || !splitscreen4))
+		return; // do whatever you want
+
+	if (!P_PlayerMoving(thirddisplayplayer))
+	{
+		// Color change menu scrolling fix is no longer necessary
+		SendNameAndColor3();
+	}
+	else
+	{
+		CV_StealthSetValue(&cv_playercolor3,
+			players[thirddisplayplayer].skincolor);
+	}
+}
+
+static void Color4_OnChange(void)
+{
+	if (!Playing() || !splitscreen4)
+		return; // do whatever you want
+
+	if (!P_PlayerMoving(fourthdisplayplayer))
+	{
+		// Color change menu scrolling fix is no longer necessary
+		SendNameAndColor4();
+	}
+	else
+	{
+		CV_StealthSetValue(&cv_playercolor4,
+			players[fourthdisplayplayer].skincolor);
+	}
+}
+
 /** Displays the result of the chat being muted or unmuted.
   * The server or remote admin should already know and be able to talk
   * regardless, so this is only displayed to clients.
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index d9506105543dd1edf570f0b32082a40e44df9685..bbac7975d8edc52dd00677e7338f785265211168 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -87,8 +87,8 @@ extern consvar_t cv_autobalance;
 extern consvar_t cv_teamscramble;
 extern consvar_t cv_scrambleonchange;
 
-extern consvar_t cv_useranalog, cv_useranalog2;
-extern consvar_t cv_analog, cv_analog2;
+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_netstat;
 #ifdef WALLSPLATS
diff --git a/src/doomstat.h b/src/doomstat.h
index 0e04a78127b57a08eb761c4f17ad7923fa7282f5..5163a709bc78dcb3bab00e8637bbd6fcb549069f 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -110,6 +110,10 @@ 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 INT32 viewwindowx, viewwindowy;
 extern INT32 viewwidth, scaledviewwidth;
diff --git a/src/g_game.c b/src/g_game.c
index c3a0f38162d5206b57f09f6350446598dac592b5..0927d21384d957decdbdbdac251d8a4dd2c15712 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -57,6 +57,8 @@ UINT8 botcolor;
 
 JoyType_t Joystick;
 JoyType_t Joystick2;
+JoyType_t Joystick3;
+JoyType_t Joystick4;
 
 // 1024 bytes is plenty for a savegame
 #define SAVEGAMESIZE (1024)
@@ -370,6 +372,8 @@ static CV_PossibleValue_t joyaxis_cons_t[] = {{0, "None"},
 
 consvar_t cv_crosshair = {"crosshair", "Cross", CV_SAVE, crosshair_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_crosshair2 = {"crosshair2", "Cross", CV_SAVE, crosshair_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_crosshair3 = {"crosshair3", "Cross", CV_SAVE, crosshair_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_crosshair4 = {"crosshair4", "Cross", CV_SAVE, crosshair_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_invertmouse = {"invertmouse", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_alwaysfreelook = {"alwaysmlook", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_invertmouse2 = {"invertmouse2", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -957,6 +961,162 @@ static INT32 Joy2Axis(axis_input_e axissel)
 	return retaxis;
 }
 
+static INT32 Joy3Axis(axis_input_e axissel)
+{
+	INT32 retaxis;
+	INT32 axisval;
+	boolean flp = false;
+
+	//find what axis to get
+	switch (axissel)
+	{
+	case AXISTURN:
+		axisval = cv_turnaxis3.value;
+		break;
+	case AXISMOVE:
+		axisval = cv_moveaxis3.value;
+		break;
+	case AXISLOOK:
+		axisval = cv_lookaxis3.value;
+		break;
+	case AXISSTRAFE:
+		axisval = cv_sideaxis3.value;
+		break;
+	case AXISFIRE:
+		axisval = cv_fireaxis3.value;
+		break;
+	case AXISFIRENORMAL:
+		axisval = cv_firenaxis3.value;
+		break;
+	default:
+		return 0;
+	}
+
+
+	if (axisval < 0) //odd -axises
+	{
+		axisval = -axisval;
+		flp = true;
+	}
+#ifdef _arch_dreamcast
+	if (axisval == 7) // special case
+	{
+		retaxis = joy3xmove[1] - joy3ymove[1];
+		goto skipDC;
+	}
+	else
+#endif
+		if (axisval > JOYAXISSET*2 || axisval == 0) //not there in array or None
+			return 0;
+
+	if (axisval%2)
+	{
+		axisval /= 2;
+		retaxis = joy3xmove[axisval];
+	}
+	else
+	{
+		axisval--;
+		axisval /= 2;
+		retaxis = joy3ymove[axisval];
+	}
+
+#ifdef _arch_dreamcast
+	skipDC:
+#endif
+
+		  if (retaxis < (-JOYAXISRANGE))
+			  retaxis = -JOYAXISRANGE;
+		  if (retaxis > (+JOYAXISRANGE))
+			  retaxis = +JOYAXISRANGE;
+		  if (!Joystick3.bGamepadStyle && axissel < AXISDEAD)
+		  {
+			  const INT32 jdeadzone = JOYAXISRANGE/4;
+			  if (-jdeadzone < retaxis && retaxis < jdeadzone)
+				  return 0;
+		  }
+		  if (flp) retaxis = -retaxis; //flip it around
+		  return retaxis;
+}
+
+static INT32 Joy3Axis(axis_input_e axissel)
+{
+	INT32 retaxis;
+	INT32 axisval;
+	boolean flp = false;
+
+	//find what axis to get
+	switch (axissel)
+	{
+	case AXISTURN:
+		axisval = cv_turnaxis4.value;
+		break;
+	case AXISMOVE:
+		axisval = cv_moveaxis4.value;
+		break;
+	case AXISLOOK:
+		axisval = cv_lookaxis4.value;
+		break;
+	case AXISSTRAFE:
+		axisval = cv_sideaxis4.value;
+		break;
+	case AXISFIRE:
+		axisval = cv_fireaxis4.value;
+		break;
+	case AXISFIRENORMAL:
+		axisval = cv_firenaxis4.value;
+		break;
+	default:
+		return 0;
+	}
+
+
+	if (axisval < 0) //odd -axises
+	{
+		axisval = -axisval;
+		flp = true;
+	}
+#ifdef _arch_dreamcast
+	if (axisval == 7) // special case
+	{
+		retaxis = joy4xmove[1] - joy4ymove[1];
+		goto skipDC;
+	}
+	else
+#endif
+		if (axisval > JOYAXISSET*2 || axisval == 0) //not there in array or None
+			return 0;
+
+	if (axisval%2)
+	{
+		axisval /= 2;
+		retaxis = joy4xmove[axisval];
+	}
+	else
+	{
+		axisval--;
+		axisval /= 2;
+		retaxis = joy4ymove[axisval];
+	}
+
+#ifdef _arch_dreamcast
+	skipDC:
+#endif
+
+		  if (retaxis < (-JOYAXISRANGE))
+			  retaxis = -JOYAXISRANGE;
+		  if (retaxis > (+JOYAXISRANGE))
+			  retaxis = +JOYAXISRANGE;
+		  if (!Joystick4.bGamepadStyle && axissel < AXISDEAD)
+		  {
+			  const INT32 jdeadzone = JOYAXISRANGE/4;
+			  if (-jdeadzone < retaxis && retaxis < jdeadzone)
+				  return 0;
+		  }
+		  if (flp) retaxis = -retaxis; //flip it around
+		  return retaxis;
+}
+
 
 //
 // G_BuildTiccmd
@@ -1680,6 +1840,26 @@ static void UserAnalog2_OnChange(void)
 		CV_SetValue(&cv_analog2, 0);
 }
 
+static void UserAnalog3_OnChange(void)
+{
+	if (botingame)
+		return;
+	if (cv_useranalog3.value)
+		CV_SetValue(&cv_analog3, 1);
+	else
+		CV_SetValue(&cv_analog3, 0);
+}
+
+static void UserAnalog4_OnChange(void)
+{
+	if (botingame)
+		return;
+	if (cv_useranalog4.value)
+		CV_SetValue(&cv_analog4, 1);
+	else
+		CV_SetValue(&cv_analog4, 0);
+}
+
 static void Analog_OnChange(void)
 {
 	if (!cv_cam_dist.string)
@@ -1702,7 +1882,7 @@ static void Analog_OnChange(void)
 
 static void Analog2_OnChange(void)
 {
-	if (!(splitscreen || botingame) || !cv_cam2_dist.string)
+	if (!((splitscreen || splitscreen3 || splitscreen4) || botingame) || !cv_cam2_dist.string)
 		return;
 
 	// cameras are not initialized at this point
@@ -1720,6 +1900,46 @@ static void Analog2_OnChange(void)
 	SendWeaponPref2();
 }
 
+static void Analog3_OnChange(void)
+{
+	if (!((splitscreen3 || splitscreen4) || botingame) || !cv_cam3_dist.string)
+		return;
+
+	// cameras are not initialized at this point
+
+	if (!cv_chasecam3.value && cv_analog3.value) {
+		CV_SetValue(&cv_analog3, 0);
+		return;
+	}
+
+	if (cv_analog3.value)
+		players[thirddisplayplayer].pflags |= PF_ANALOGMODE;
+	else
+		players[thirddisplayplayer].pflags &= ~PF_ANALOGMODE;
+
+	SendWeaponPref3();
+}
+
+static void Analog4_OnChange(void)
+{
+	if (!(splitscreen4 || botingame) || !cv_cam4_dist.string)
+		return;
+
+	// cameras are not initialized at this point
+
+	if (!cv_chasecam4.value && cv_analog4.value) {
+		CV_SetValue(&cv_analog4, 0);
+		return;
+	}
+
+	if (cv_analog4.value)
+		players[fourthdisplayplayer].pflags |= PF_ANALOGMODE;
+	else
+		players[fourthdisplayplayer].pflags &= ~PF_ANALOGMODE;
+
+	SendWeaponPref4();
+}
+
 //
 // G_DoLoadLevel
 //
@@ -1768,8 +1988,12 @@ void G_DoLoadLevel(boolean resetplayer)
 
 	if (camera.chase)
 		P_ResetCamera(&players[displayplayer], &camera);
-	if (camera2.chase && splitscreen)
+	if (camera2.chase && (splitscreen || splitscreen3 || splitscreen4))
 		P_ResetCamera(&players[secondarydisplayplayer], &camera2);
+	if (camera3.chase && (splitscreen3 || splitscreen4))
+		P_ResetCamera(&players[thirddisplayplayer], &camera3);
+	if (camera4.chase && splitscreen4)
+		P_ResetCamera(&players[fourthdisplayplayer], &camera4);
 
 	// clear cmd building stuff
 	memset(gamekeydown, 0, sizeof (gamekeydown));
@@ -1777,6 +2001,8 @@ void G_DoLoadLevel(boolean resetplayer)
 	{
 		joyxmove[i] = joyymove[i] = 0;
 		joy2xmove[i] = joy2ymove[i] = 0;
+		joy3xmove[i] = joy3ymove[i] = 0;
+		joy4xmove[i] = joy4ymove[i] = 0;
 	}
 	mousex = mousey = 0;
 	mouse2x = mouse2y = 0;
@@ -1786,7 +2012,7 @@ void G_DoLoadLevel(boolean resetplayer)
 }
 
 static INT32 pausedelay = 0;
-static INT32 camtoggledelay, camtoggledelay2 = 0;
+static INT32 camtoggledelay, camtoggledelay2, camtoggledelay3, camtoggledelay4 = 0;
 
 //
 // G_Responder
@@ -1797,7 +2023,7 @@ boolean G_Responder(event_t *ev)
 	// allow spy mode changes even during the demo
 	if (gamestate == GS_LEVEL && ev->type == ev_keydown && ev->data1 == KEY_F12)
 	{
-		if (splitscreen || !netgame)
+		if ((splitscreen || splitscreen3 || splitscreen4) || !netgame)
 			displayplayer = consoleplayer;
 		else
 		{
@@ -1974,6 +2200,24 @@ boolean G_Responder(event_t *ev)
 					CV_SetValue(&cv_chasecam2, cv_chasecam2.value ? 0 : 1);
 				}
 			}
+			if (ev->data1 == gamecontrol3[gc_camtoggle][0]
+				|| ev->data1 == gamecontrol3[gc_camtoggle][1])
+			{
+				if (!camtoggledelay3)
+				{
+					camtoggledelay3 = NEWTICRATE / 7;
+					CV_SetValue(&cv_chasecam3, cv_chasecam3.value ? 0 : 1);
+				}
+			}
+			if (ev->data1 == gamecontrol4[gc_camtoggle][0]
+				|| ev->data1 == gamecontrol4[gc_camtoggle][1])
+			{
+				if (!camtoggledelay4)
+				{
+					camtoggledelay4 = NEWTICRATE / 7;
+					CV_SetValue(&cv_chasecam4, cv_chasecam4.value ? 0 : 1);
+				}
+			}
 			return true;
 
 		case ev_keyup:
@@ -2508,13 +2752,13 @@ void G_SpawnPlayer(INT32 playernum, boolean starpost)
 	{
 		if (nummapthings)
 		{
-			if (playernum == consoleplayer || (splitscreen && playernum == secondarydisplayplayer))
+			if (playernum == consoleplayer || ((splitscreen || splitscreen3 || splitscreen4) && playernum == secondarydisplayplayer) || ((splitscreen3 || splitscreen4) && playernum == thirddisplayplayer) || (splitscreen4 && playernum == fourthdisplayplayer))
 				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))
+			if (playernum == consoleplayer || ((splitscreen || splitscreen3 || splitscreen4) && playernum == secondarydisplayplayer) || ((splitscreen3 || splitscreen4) && playernum == thirddisplayplayer) || (splitscreen4 && playernum == fourthdisplayplayer))
 				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.
 		}
@@ -2713,8 +2957,12 @@ void G_DoReborn(INT32 playernum)
 
 			if (camera.chase)
 				P_ResetCamera(&players[displayplayer], &camera);
-			if (camera2.chase && splitscreen)
+			if (camera2.chase && (splitscreen || splitscreen3 || splitscreen4))
 				P_ResetCamera(&players[secondarydisplayplayer], &camera2);
+			if (camera3.chase && (splitscreen3 || splitscreen4))
+				P_ResetCamera(&players[thirddisplayplayer], &camera3);
+			if (camera4.chase && splitscreen4)
+				P_ResetCamera(&players[fourthdisplayplayer], &camera4);
 
 			// clear cmd building stuff
 			memset(gamekeydown, 0, sizeof (gamekeydown));
@@ -2722,6 +2970,8 @@ void G_DoReborn(INT32 playernum)
 			{
 				joyxmove[i] = joyymove[i] = 0;
 				joy2xmove[i] = joy2ymove[i] = 0;
+				joy3xmove[i] = joy3ymove[i] = 0;
+				joy4xmove[i] = joy4ymove[i] = 0;
 			}
 			mousex = mousey = 0;
 			mouse2x = mouse2y = 0;
@@ -3523,7 +3773,7 @@ static void M_ForceLoadGameResponse(INT32 ch)
 	cursaveslot = -1;
 
 	displayplayer = consoleplayer;
-	multiplayer = splitscreen = false;
+	multiplayer = splitscreen = splitscreen3 = splitscreen4 = false;
 
 	if (setsizeneeded)
 		R_ExecuteSetViewSize();
@@ -3611,7 +3861,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 //	gameaction = ga_nothing;
 //	G_SetGamestate(GS_LEVEL);
 	displayplayer = consoleplayer;
-	multiplayer = splitscreen = false;
+	multiplayer = splitscreen = splitscreen3 = splitscreen4 = false;
 
 //	G_DeferedInitNew(sk_medium, G_BuildMapName(1), 0, 0, 1);
 	if (setsizeneeded)
diff --git a/src/g_game.h b/src/g_game.h
index a60f75618d879a17faa5df1b213fd51e1e755319..9369665cab9cf912fcd5d265a3293aad853e81f5 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -54,10 +54,12 @@ extern tic_t timeinmap; // Ticker for time spent in level (used for levelcard di
 extern INT16 rw_maximums[NUM_WEAPONS];
 
 // used in game menu
-extern consvar_t cv_crosshair, cv_crosshair2;
+extern consvar_t cv_crosshair, cv_crosshair2, cv_crosshair3, cv_crosshair4;
 extern consvar_t cv_invertmouse, cv_alwaysfreelook, cv_mousemove;
 extern consvar_t cv_sideaxis,cv_turnaxis,cv_moveaxis,cv_lookaxis,cv_fireaxis,cv_firenaxis;
 extern consvar_t cv_sideaxis2,cv_turnaxis2,cv_moveaxis2,cv_lookaxis2,cv_fireaxis2,cv_firenaxis2;
+extern consvar_t cv_sideaxis3,cv_turnaxis3,cv_moveaxis3,cv_lookaxis3,cv_fireaxis3,cv_firenaxis3;
+extern consvar_t cv_sideaxis4,cv_turnaxis4,cv_moveaxis4,cv_lookaxis4,cv_fireaxis4,cv_firenaxis4;
 extern consvar_t cv_ghost_bestscore, cv_ghost_besttime, cv_ghost_last, cv_ghost_guest, cv_ghost_staff; //cv_ghost_bestlap
 
 // mouseaiming (looking up/down with the mouse or keyboard)
@@ -79,7 +81,7 @@ ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n);
 INT16 G_ClipAimingPitch(INT32 *aiming);
 INT16 G_SoftwareClipAimingPitch(INT32 *aiming);
 
-extern angle_t localangle, localangle2;
+extern angle_t localangle, localangle2, localangle3, localangle4;
 extern INT32 localaiming, localaiming2; // should be an angle_t but signed
 
 //
diff --git a/src/g_input.c b/src/g_input.c
index a8b56a337343ebda4b373f3a295d4428dfb6e8a0..0b2573a9de659a220858593553414f5e32f344a5 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -37,7 +37,8 @@ INT32 mlooky; // like mousey but with a custom sensitivity for mlook
 INT32 mouse2x, mouse2y, mlook2y;
 
 // joystick values are repeated
-INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
+INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET], 
+joy3xmove[JOYAXISSET], joy3ymove[JOYAXISSET], joy4xmove[JOYAXISSET], joy4ymove[JOYAXISSET];
 
 // current state of the keys: true if pushed
 UINT8 gamekeydown[NUMINPUTS];
@@ -58,6 +59,8 @@ static dclick_t mousedclicks[MOUSEBUTTONS];
 static dclick_t joydclicks[JOYBUTTONS + JOYHATS*4];
 static dclick_t mouse2dclicks[MOUSEBUTTONS];
 static dclick_t joy2dclicks[JOYBUTTONS + JOYHATS*4];
+static dclick_t joy3dclicks[JOYBUTTONS + JOYHATS*4];
+static dclick_t joy4dclicks[JOYBUTTONS + JOYHATS*4];
 
 // protos
 static UINT8 G_CheckDoubleClick(UINT8 state, dclick_t *dt);
@@ -121,6 +124,22 @@ void G_MapEventsToControls(event_t *ev)
 			if (ev->data3 != INT32_MAX) joy2ymove[i] = ev->data3;
 			break;
 
+		case ev_joystick3:
+			i = ev->data1;
+			if (i >= JOYAXISSET)
+				break;
+			if (ev->data2 != INT32_MAX) joy3xmove[i] = ev->data2;
+			if (ev->data3 != INT32_MAX) joy3ymove[i] = ev->data3;
+			break;
+
+		case ev_joystick4:
+			i = ev->data1;
+			if (i >= JOYAXISSET)
+				break;
+			if (ev->data2 != INT32_MAX) joy4xmove[i] = ev->data2;
+			if (ev->data3 != INT32_MAX) joy4ymove[i] = ev->data3;
+			break;
+
 		case ev_mouse2: // buttons are virtual keys
 			mouse2x = (INT32)(ev->data2*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
 			mouse2y = (INT32)(ev->data3*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
@@ -1299,10 +1318,10 @@ void G_SaveKeySetting(FILE *f)
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol3 \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(gamecontrolbis[i][0]));
+			G_KeynumToString(gamecontrol3[i][0]));
 
-		if (gamecontrolbis[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(gamecontrolbis[i][1]));
+		if (gamecontrol3[i][1])
+			fprintf(f, " \"%s\"\n", G_KeynumToString(gamecontrol3[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -1310,10 +1329,10 @@ void G_SaveKeySetting(FILE *f)
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol4 \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(gamecontrolbis[i][0]));
+			G_KeynumToString(gamecontrol4[i][0]));
 
-		if (gamecontrolbis[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(gamecontrolbis[i][1]));
+		if (gamecontrol4[i][1])
+			fprintf(f, " \"%s\"\n", G_KeynumToString(gamecontrol4[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -1334,6 +1353,14 @@ void G_CheckDoubleUsage(INT32 keynum)
 				gamecontrolbis[i][0] = KEY_NULL;
 			if (gamecontrolbis[i][1] == keynum)
 				gamecontrolbis[i][1] = KEY_NULL;
+			if (gamecontrol3[i][0] == keynum)
+				gamecontrol3[i][0] = KEY_NULL;
+			if (gamecontrol3[i][1] == keynum)
+				gamecontrol3[i][1] = KEY_NULL;
+			if (gamecontrol4[i][0] == keynum)
+				gamecontrol4[i][0] = KEY_NULL;
+			if (gamecontrol4[i][1] == keynum)
+				gamecontrol4[i][1] = KEY_NULL;
 		}
 	}
 }
@@ -1405,7 +1432,7 @@ void Command_Setcontrol3_f(void)
 		return;
 	}
 
-	setcontrol(gamecontrolbis, na);
+	setcontrol(gamecontrol3, na);
 }
 
 void Command_Setcontrol4_f(void)
@@ -1420,5 +1447,5 @@ void Command_Setcontrol4_f(void)
 		return;
 	}
 
-	setcontrol(gamecontrolbis, na);
+	setcontrol(gamecontrol4, na);
 }
\ No newline at end of file
diff --git a/src/g_input.h b/src/g_input.h
index 42eaaed98b62098363761718511efded4a3a1a6c..180816ae501d0598cbf22852832a443be0537342 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -131,7 +131,8 @@ extern INT32 mousex, mousey;
 extern INT32 mlooky; //mousey with mlookSensitivity
 extern INT32 mouse2x, mouse2y, mlook2y;
 
-extern INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
+extern INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET],
+joy3xmove[JOYAXISSET], joy3ymove[JOYAXISSET], joy4xmove[JOYAXISSET], joy4ymove[JOYAXISSET];
 
 // current state of the keys: true if pushed
 extern UINT8 gamekeydown[NUMINPUTS];
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index a4785f1226a8ca30560ea702d1c679668bd3dba6..3797e94baa052d94d2b9bf0b39753e5055718653 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -4055,8 +4055,18 @@ static void HWR_DrawSpriteShadow(gr_vissprite_t *spr, GLPatch_t *gpatch, float t
 		angle_t shadowdir;
 
 		// Set direction
-		if (splitscreen && stplyr != &players[displayplayer])
+		if ((splitscreen || splitscreen3 || splitscreen4) && stplyr != &players[displayplayer])
+		{
 			shadowdir = localangle2 + FixedAngle(cv_cam2_rotate.value);
+
+			if ((splitscreen3 || splitscreen4) && stplyr != &players[displayplayer])
+			{
+				shadowdir = localangle3 + FixedAngle(cv_cam3_rotate.value);
+
+				if (splitscreen4 && stplyr != &players[displayplayer])
+					shadowdir = localangle4 + FixedAngle(cv_cam4_rotate.value);
+			}
+		}
 		else
 			shadowdir = localangle + FixedAngle(cv_cam_rotate.value);
 
@@ -5534,8 +5544,12 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 	FTransform stransform;
 	postimg_t *type;
 
-	if (splitscreen && player == &players[secondarydisplayplayer])
+	if ((splitscreen || splitscreen3 || splitscreen4) && player == &players[secondarydisplayplayer])
 		type = &postimgtype2;
+	else if ((splitscreen3 || splitscreen4) && player == &players[thirddisplayplayer])
+		type = &postimgtype3;
+	else if (splitscreen4 && player == &players[fourthdisplayplayer])
+		type = &postimgtype4;
 	else
 		type = &postimgtype;
 
@@ -5563,7 +5577,13 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 	gr_centery = gr_basecentery;
 	gr_viewwindowy = gr_baseviewwindowy;
 	gr_windowcentery = gr_basewindowcentery;
-	if (splitscreen && viewnumber == 1)
+	if ((splitscreen || splitscreen3) && (viewnumber == 1 || viewnumber == 2))
+	{
+		gr_viewwindowy += (vid.height/2);
+		gr_windowcentery += (vid.height/2);
+	}
+
+	if (splitscreen4 && (viewnumber == 2 || viewnumber == 3))
 	{
 		gr_viewwindowy += (vid.height/2);
 		gr_windowcentery += (vid.height/2);
@@ -5641,7 +5661,7 @@ if (0)
 #endif
 
 	//Hurdler: it doesn't work in splitscreen mode
-	drawsky = splitscreen;
+	drawsky = splitscreen || splitscreen3 || splitscreen4;
 
 	HWR_ClearSprites();
 
@@ -5661,8 +5681,12 @@ if (0)
 	// Make a viewangle int so we can render things based on mouselook
 	if (player == &players[consoleplayer])
 		viewangle = localaiming;
-	else if (splitscreen && player == &players[secondarydisplayplayer])
+	else if ((splitscreen || splitscreen3 || splitscreen4) && player == &players[secondarydisplayplayer])
 		viewangle = localaiming2;
+	else if ((splitscreen3 || splitscreen4) && player == &players[thirddisplayplayer])
+		viewangle = localaiming3;
+	else if (splitscreen4 && player == &players[fourthdisplayplayer])
+		viewangle = localaiming4;
 
 	// Handle stuff when you are looking farther up or down.
 	if ((aimingangle || cv_grfov.value+player->fovadd > 90*FRACUNIT))
@@ -5753,8 +5777,12 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 
 	FRGBAFloat ClearColor;
 
-	if (splitscreen && player == &players[secondarydisplayplayer])
+	if ((splitscreen || splitscreen3 || splitscreen4) && player == &players[secondarydisplayplayer])
 		type = &postimgtype2;
+	else if ((splitscreen3 || splitscreen4) && player == &players[thirddisplayplayer])
+		type = &postimgtype3;
+	else if (splitscreen4 && player == &players[fourthdisplayplayer])
+		type = &postimgtype4;
 	else
 		type = &postimgtype;
 
@@ -5793,7 +5821,13 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	gr_centery = gr_basecentery;
 	gr_viewwindowy = gr_baseviewwindowy;
 	gr_windowcentery = gr_basewindowcentery;
-	if (splitscreen && viewnumber == 1)
+	if ((splitscreen || splitscreen3) && (viewnumber == 1 ||  viewnumber == 2))
+	{
+		gr_viewwindowy += (vid.height/2);
+		gr_windowcentery += (vid.height/2);
+	}
+
+	if (splitscreen4 && (viewnumber == 2 || viewnumber == 3))
 	{
 		gr_viewwindowy += (vid.height/2);
 		gr_windowcentery += (vid.height/2);
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 231edb90a3d638b59526446cfb9cf67f7c6c09c5..72d465dc0e00ae55e07ddbe4afbf65ef963dac8c 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -1802,7 +1802,7 @@ static void HU_DrawRankings(void)
 		HU_DrawDualTabRankings(32, 32, tab, scorelines, whiteplayer);
 
 	// draw spectators in a ticker across the bottom
-	if (!splitscreen && G_GametypeHasSpectators())
+	if ((!splitscreen || !splitscreen3 || !splitscreen4) && G_GametypeHasSpectators())
 		HU_DrawSpectatorTicker();
 }
 
diff --git a/src/i_joy.h b/src/i_joy.h
index 5cba1af0abff4e8514d2797377d099a4857e1e6d..316011fc5680dff863e8e9ba984a71acaf80b491 100644
--- a/src/i_joy.h
+++ b/src/i_joy.h
@@ -53,6 +53,6 @@ typedef struct JoyType_s JoyType_t;
 	for palyer 1 and 2's joystick/gamepad
 */
 
-extern JoyType_t Joystick, Joystick2;
+extern JoyType_t Joystick, Joystick2, Joystick3, Joystick4;
 
 #endif // __I_JOY_H__
diff --git a/src/i_system.h b/src/i_system.h
index d61f2d16e7730c8516b556d3d1f9705e5fb565fc..a166629bc5705da255f69368466eca95fc6c3468 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -138,6 +138,24 @@ void I_Tactile(FFType Type, const JoyFF_t *Effect);
 */
 void I_Tactile2(FFType Type, const JoyFF_t *Effect);
 
+/**	\brief	Forcefeedback for the third joystick
+
+\param	Type   what kind of Effect
+\param	Effect Effect Info
+
+\return	void
+*/
+void I_Tactile3(FFType Type, const JoyFF_t *Effect);
+
+/**	\brief	Forcefeedback for the fourth joystick
+
+\param	Type   what kind of Effect
+\param	Effect Effect Info
+
+\return	void
+*/
+void I_Tactile4(FFType Type, const JoyFF_t *Effect);
+
 /**	\brief to set up the first joystick scale
 */
 void I_JoyScale(void);
@@ -146,6 +164,14 @@ void I_JoyScale(void);
 */
 void I_JoyScale2(void);
 
+/**	\brief to set up the third joystick scale
+*/
+void I_JoyScale3(void);
+
+/**	\brief to set up the fourth joystick scale
+*/
+void I_JoyScale4(void);
+
 // Called by D_SRB2Main.
 
 /**	\brief to startup the first joystick
@@ -156,6 +182,14 @@ void I_InitJoystick(void);
 */
 void I_InitJoystick2(void);
 
+/**	\brief to startup the third joystick
+*/
+void I_InitJoystick3(void);
+
+/**	\brief to startup the fourth joystick
+*/
+void I_InitJoystick4(void);
+
 /**	\brief return the number of joystick on the system
 */
 INT32 I_NumJoys(void);
diff --git a/src/k_kart.c b/src/k_kart.c
index df25cfa8d5a974717a72ab0703f6a272f496ec93..f79b5f1eca0d959a1784990b99cfcc47a9cfdcb0 100644
--- a/src/k_kart.c
+++ b/src/k_kart.c
@@ -4057,7 +4057,7 @@ void K_LoadKartHUDGraphics(void)
 static INT32 STRINGY(INT32 y)
 {
 	// Copied from st_stuff.c
-	if (splitscreen)
+	if (splitscreen || splitscreen3)
 	{
 		y >>= 1;
 		if (stplyr != &players[displayplayer])
diff --git a/src/m_menu.c b/src/m_menu.c
index 4c06c3e603d953dede72dad804b32c3289979143..fb9fb0d4d1911e83e11a74d2e1d73f862fef0c9e 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -277,8 +277,8 @@ static void M_SetupMultiPlayer4(INT32 choice);
 // Controls
 menu_t OP_ControlsDef, /*OP_ControlListDef,*/ OP_MoveControlsDef;
 menu_t /*OP_MPControlsDef, OP_CameraControlsDef, OP_MiscControlsDef,*/ OP_CustomControlsDef;
-menu_t OP_P1ControlsDef, OP_P2ControlsDef, OP_MouseOptionsDef;
-menu_t OP_Mouse2OptionsDef, OP_Joystick1Def, OP_Joystick2Def;
+menu_t OP_P1ControlsDef, OP_P2ControlsDef, OP_P3ControlsDef, OP_P4ControlsDef, OP_MouseOptionsDef;
+menu_t OP_Mouse2OptionsDef, OP_Joystick1Def, OP_Joystick2Def, OP_Joystick3Def, OP_Joystick4Def;
 static void M_VideoModeMenu(INT32 choice);
 static void M_Setup1PControlsMenu(INT32 choice);
 static void M_Setup2PControlsMenu(INT32 choice);
@@ -888,11 +888,13 @@ static menuitem_t MP_MainMenu[] =
 	{IT_CALL | IT_STRING, NULL, "JOIN GAME (Specify IP)", M_ConnectIPMenu,        40},
 #endif
 	{IT_CALL | IT_STRING, NULL, "TWO PLAYER GAME",        M_StartSplitServerMenu, 60},
+	{IT_CALL | IT_STRING, NULL, "THREE PLAYER GAME",      M_StartSplitServerMenu, 70},
+	{IT_CALL | IT_STRING, NULL, "FOUR PLAYER GAME",		  M_StartSplitServerMenu, 80},
 
-	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 1",         M_SetupMultiPlayer,     80},
-	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 2",         M_SetupMultiPlayer2,    90},
-	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 3",         M_SetupMultiPlayer3,   100},
-	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 4",         M_SetupMultiPlayer4,   110},
+	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 1",         M_SetupMultiPlayer,     100},
+	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 2",         M_SetupMultiPlayer2,    110},
+	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 3",         M_SetupMultiPlayer3,   120},
+	{IT_CALL | IT_STRING, NULL, "SETUP PLAYER 4",         M_SetupMultiPlayer4,   130},
 };
 
 static menuitem_t MP_ServerMenu[] =
@@ -7114,6 +7116,8 @@ static void M_Setup1PControlsMenu(INT32 choice)
 {
 	(void)choice;
 	setupcontrols_secondaryplayer = false;
+	setupcontrols_thirdplayer = false;
+	setupcontrols_fourthplayer = false;
 	setupcontrols = gamecontrol;        // was called from main Options (for console player, then)
 	currentMenu->lastOn = itemOn;
 
@@ -7133,6 +7137,8 @@ static void M_Setup2PControlsMenu(INT32 choice)
 {
 	(void)choice;
 	setupcontrols_secondaryplayer = true;
+	setupcontrols_thirdplayer = false;
+	setupcontrols_fourthplayer = false;
 	setupcontrols = gamecontrolbis;
 	currentMenu->lastOn = itemOn;
 
@@ -7152,7 +7158,9 @@ static void M_Setup3PControlsMenu(INT32 choice)
 {
 	(void)choice;
 	setupcontrols_thirdplayer = true;
-	setupcontrols = gamecontrolbis;
+	setupcontrols_secondaryplayer = false;
+	setupcontrols_fourthplayer = false;
+	setupcontrols = gamecontrol3;
 	currentMenu->lastOn = itemOn;
 
 	// Hide the three non-P3 controls
@@ -7171,7 +7179,9 @@ static void M_Setup4PControlsMenu(INT32 choice)
 {
 	(void)choice;
 	setupcontrols_fourthplayer = true;
-	setupcontrols = gamecontrolbis;
+	setupcontrols_secondaryplayer = false;
+	setupcontrols_thirdplayer = false;
+	setupcontrols = gamecontrol4;
 	currentMenu->lastOn = itemOn;
 
 	// Hide the three non-P4 controls
diff --git a/src/p_local.h b/src/p_local.h
index 673aa4fc02f375067a45f2643468b5c4642df2bc..c8a4095ef9ade66e090f6532dfd419d218a20d53 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -109,15 +109,23 @@ typedef struct camera_s
 	fixed_t momx, momy, momz;
 } camera_t;
 
-extern camera_t camera, camera2;
+extern camera_t camera, camera2, camera3, camera4;
 extern consvar_t cv_cam_dist, cv_cam_still, cv_cam_height;
 extern consvar_t cv_cam_speed, cv_cam_rotate, cv_cam_rotspeed;
 
 extern consvar_t cv_cam2_dist, cv_cam2_still, cv_cam2_height;
 extern consvar_t cv_cam2_speed, cv_cam2_rotate, cv_cam2_rotspeed;
 
+extern consvar_t cv_cam3_dist, cv_cam3_still, cv_cam3_height;
+extern consvar_t cv_cam3_speed, cv_cam3_rotate, cv_cam3_rotspeed;
+
+extern consvar_t cv_cam4_dist, cv_cam4_still, cv_cam4_height;
+extern consvar_t cv_cam4_speed, cv_cam4_rotate, cv_cam4_rotspeed;
+
 extern fixed_t t_cam_dist, t_cam_height, t_cam_rotate;
 extern fixed_t t_cam2_dist, t_cam2_height, t_cam2_rotate;
+extern fixed_t t_cam3_dist, t_cam3_height, t_cam3_rotate;
+extern fixed_t t_cam4_dist, t_cam4_height, t_cam4_rotate;
 
 fixed_t P_GetPlayerHeight(player_t *player);
 fixed_t P_GetPlayerSpinHeight(player_t *player);
diff --git a/src/p_user.c b/src/p_user.c
index 5e47cdde3b90305bc9a7312070174675f8b38c29..d4f01a03b22996fc510f2897669cb3874293a924 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -8090,6 +8090,22 @@ static void CV_CamRotate2_OnChange(void)
 		CV_SetValue(&cv_cam2_rotate, cv_cam2_rotate.value % 360);
 }
 
+static void CV_CamRotate3_OnChange(void)
+{
+	if (cv_cam3_rotate.value < 0)
+		CV_SetValue(&cv_cam3_rotate, cv_cam3_rotate.value + 360);
+	else if (cv_cam3_rotate.value > 359)
+		CV_SetValue(&cv_cam3_rotate, cv_cam3_rotate.value % 360);
+}
+
+static void CV_CamRotate4_OnChange(void)
+{
+	if (cv_cam4_rotate.value < 0)
+		CV_SetValue(&cv_cam4_rotate, cv_cam4_rotate.value + 360);
+	else if (cv_cam4_rotate.value > 359)
+		CV_SetValue(&cv_cam4_rotate, cv_cam4_rotate.value % 360);
+}
+
 static CV_PossibleValue_t CV_CamSpeed[] = {{0, "MIN"}, {1*FRACUNIT, "MAX"}, {0, NULL}};
 static CV_PossibleValue_t rotation_cons_t[] = {{1, "MIN"}, {45, "MAX"}, {0, NULL}};
 static CV_PossibleValue_t CV_CamRotate[] = {{-720, "MIN"}, {720, "MAX"}, {0, NULL}};
@@ -8106,6 +8122,18 @@ consvar_t cv_cam2_still = {"cam2_still", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL
 consvar_t cv_cam2_speed = {"cam2_speed", "0.3", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_cam2_rotate = {"cam2_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate2_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_cam2_rotspeed = {"cam2_rotspeed", "10", CV_SAVE, rotation_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam3_dist = {"cam3_dist", "160", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam3_height = {"cam3_height", "50", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam3_still = {"cam3_still", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam3_speed = {"cam3_speed", "0.3", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam3_rotate = {"cam3_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate3_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam3_rotspeed = {"cam3_rotspeed", "10", CV_SAVE, rotation_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam4_dist = {"cam4_dist", "160", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam4_height = {"cam4_height", "50", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam4_still = {"cam4_still", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam4_speed = {"cam4_speed", "0.3", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam4_rotate = {"cam4_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate4_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam4_rotspeed = {"cam4_rotspeed", "10", CV_SAVE, rotation_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 fixed_t t_cam_dist = -42;
 fixed_t t_cam_height = -42;
diff --git a/src/r_main.c b/src/r_main.c
index e50e80013006cf6ad36b86ff72812464cb21377c..93222deb13697de119bea16d35e67b5704bc0eb5 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -134,16 +134,26 @@ static CV_PossibleValue_t homremoval_cons_t[] = {{0, "No"}, {1, "Yes"}, {2, "Fla
 
 static void ChaseCam_OnChange(void);
 static void ChaseCam2_OnChange(void);
+static void ChaseCam3_OnChange(void);
+static void ChaseCam4_OnChange(void);
 static void FlipCam_OnChange(void);
 static void FlipCam2_OnChange(void);
+static void FlipCam3_OnChange(void);
+static void FlipCam4_OnChange(void);
 void SendWeaponPref(void);
 void SendWeaponPref2(void);
+void SendWeaponPref3(void);
+void SendWeaponPref4(void);
 
 consvar_t cv_tailspickup = {"tailspickup", "On", CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_chasecam = {"chasecam", "On", CV_CALL, CV_OnOff, ChaseCam_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_chasecam2 = {"chasecam2", "On", CV_CALL, CV_OnOff, ChaseCam2_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_chasecam3 = {"chasecam3", "On", CV_CALL, CV_OnOff, ChaseCam3_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_chasecam4 = {"chasecam4", "On", CV_CALL, CV_OnOff, ChaseCam4_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_flipcam = {"flipcam", "No", CV_SAVE|CV_CALL|CV_NOINIT, CV_YesNo, FlipCam_OnChange, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_flipcam2 = {"flipcam2", "No", CV_SAVE|CV_CALL|CV_NOINIT, CV_YesNo, FlipCam2_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_flipcam3 = {"flipcam3", "No", CV_SAVE|CV_CALL|CV_NOINIT, CV_YesNo, FlipCam3_OnChange, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_flipcam4 = {"flipcam4", "No", CV_SAVE|CV_CALL|CV_NOINIT, CV_YesNo, FlipCam4_OnChange, 0, NULL, NULL, 0, 0, NULL};
 
 consvar_t cv_shadow = {"shadow", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_shadowoffs = {"offsetshadows", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
@@ -220,6 +230,26 @@ static void ChaseCam2_OnChange(void)
 		CV_SetValue(&cv_analog2, 1);
 }
 
+static void ChaseCam3_OnChange(void)
+{
+	if (botingame)
+		return;
+	if (!cv_chasecam3.value || !cv_useranalog3.value)
+		CV_SetValue(&cv_analog3, 0);
+	else
+		CV_SetValue(&cv_analog3, 1);
+}
+
+static void ChaseCam4_OnChange(void)
+{
+	if (botingame)
+		return;
+	if (!cv_chasecam4.value || !cv_useranalog4.value)
+		CV_SetValue(&cv_analog4, 0);
+	else
+		CV_SetValue(&cv_analog4, 1);
+}
+
 static void FlipCam_OnChange(void)
 {
 	if (cv_flipcam.value)
@@ -240,6 +270,26 @@ static void FlipCam2_OnChange(void)
 	SendWeaponPref2();
 }
 
+static void FlipCam3_OnChange(void)
+{
+	if (cv_flipcam3.value)
+		players[thirddisplayplayer].pflags |= PF_FLIPCAM;
+	else
+		players[thirddisplayplayer].pflags &= ~PF_FLIPCAM;
+
+	SendWeaponPref3();
+}
+
+static void FlipCam4_OnChange(void)
+{
+	if (cv_flipcam4.value)
+		players[fourthdisplayplayer].pflags |= PF_FLIPCAM;
+	else
+		players[fourthdisplayplayer].pflags &= ~PF_FLIPCAM;
+
+	SendWeaponPref4();
+}
+
 //
 // R_PointOnSide
 // Traverse BSP (sub) tree,
@@ -1379,6 +1429,8 @@ void R_RegisterEngineStuff(void)
 
 	CV_RegisterVar(&cv_chasecam);
 	CV_RegisterVar(&cv_chasecam2);
+	CV_RegisterVar(&cv_chasecam3);
+	CV_RegisterVar(&cv_chasecam4);
 	CV_RegisterVar(&cv_shadow);
 	CV_RegisterVar(&cv_shadowoffs);
 	CV_RegisterVar(&cv_skybox);
@@ -1397,6 +1449,20 @@ void R_RegisterEngineStuff(void)
 	CV_RegisterVar(&cv_cam2_rotate);
 	CV_RegisterVar(&cv_cam2_rotspeed);
 
+	CV_RegisterVar(&cv_cam3_dist);
+	CV_RegisterVar(&cv_cam3_still);
+	CV_RegisterVar(&cv_cam3_height);
+	CV_RegisterVar(&cv_cam3_speed);
+	CV_RegisterVar(&cv_cam3_rotate);
+	CV_RegisterVar(&cv_cam3_rotspeed);
+
+	CV_RegisterVar(&cv_cam4_dist);
+	CV_RegisterVar(&cv_cam4_still);
+	CV_RegisterVar(&cv_cam4_height);
+	CV_RegisterVar(&cv_cam4_speed);
+	CV_RegisterVar(&cv_cam4_rotate);
+	CV_RegisterVar(&cv_cam4_rotspeed);
+
 	CV_RegisterVar(&cv_showhud);
 	CV_RegisterVar(&cv_translucenthud);
 
diff --git a/src/r_main.h b/src/r_main.h
index 2e768cb9c91cfeff09f67f6ce520427aefc292b1..cabf17b66c2f6f874cb0aed15d8e034aa6e6190a 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -73,8 +73,8 @@ boolean R_DoCulling(line_t *cullheight, line_t *viewcullheight, fixed_t vz, fixe
 
 extern consvar_t cv_showhud, cv_translucenthud;
 extern consvar_t cv_homremoval;
-extern consvar_t cv_chasecam, cv_chasecam2;
-extern consvar_t cv_flipcam, cv_flipcam2;
+extern consvar_t cv_chasecam, cv_chasecam2, cv_chasecam3, cv_chasecam4;
+extern consvar_t cv_flipcam, cv_flipcam2, cv_flipcam3, cv_flipcam4;
 extern consvar_t cv_shadow, cv_shadowoffs;
 extern consvar_t cv_translucency;
 extern consvar_t cv_precipdensity, cv_drawdist, cv_drawdist_nights, cv_drawdist_precip;
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index b6c42f3f35a56348360114903f4b1dc67f6d9434..4d1ed0a5f8659847d6feb98829dbf8e1f873d51c 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -77,6 +77,11 @@
     <_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>
     <RunCodeAnalysis>false</RunCodeAnalysis>
   </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <ClCompile>
+      <TreatWarningAsError>false</TreatWarningAsError>
+    </ClCompile>
+  </ItemDefinitionGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\libs\libpng-src\projects\visualc10\libpng.vcxproj">
       <Project>{72b01aca-7a1a-4f7b-acef-2607299cf052}</Project>
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index bd4c93a63e67a98f7b0c45e9d9384c84f9cc7b55..7f3f2791b7cde457bff088c566c90d7c1b9d0c42 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -209,10 +209,26 @@ SDLJoyInfo_t JoyInfo;
 */
 static INT32 joystick2_started = 0;
 
-/**	\brief SDL inof about joystick 2
+/**	\brief SDL info about joystick 2
 */
 SDLJoyInfo_t JoyInfo2;
 
+/**	\brief Third joystick up and running
+*/
+static INT32 joystick3_started = 0;
+
+/**	\brief SDL info about joystick 3
+*/
+SDLJoyInfo_t JoyInfo3;
+
+/**	\brief Fourth joystick up and running
+*/
+static INT32 joystick4_started = 0;
+
+/**	\brief SDL info about joystick 4
+*/
+SDLJoyInfo_t JoyInfo4;
+
 #ifdef HAVE_TERMIOS
 static INT32 fdmouse2 = -1;
 static INT32 mouse2_started = 0;
@@ -821,6 +837,18 @@ void I_JoyScale2(void)
 	JoyInfo2.scale = Joystick2.bGamepadStyle?1:cv_joyscale2.value;
 }
 
+void I_JoyScale3(void)
+{
+	Joystick3.bGamepadStyle = cv_joyscale3.value==0;
+	JoyInfo3.scale = Joystick3.bGamepadStyle?1:cv_joyscale3.value;
+}
+
+void I_JoyScale4(void)
+{
+	Joystick4.bGamepadStyle = cv_joyscale4.value==0;
+	JoyInfo4.scale = Joystick4.bGamepadStyle?1:cv_joyscale4.value;
+}
+
 /**	\brief Joystick 1 buttons states
 */
 static UINT64 lastjoybuttons = 0;
@@ -1161,6 +1189,54 @@ static void I_ShutdownJoystick2(void)
 	}
 }
 
+/**	\brief	Shuts down joystick 3
+
+
+\return	void
+*/
+static void I_ShutdownJoystick3(void)
+{
+	INT32 i;
+	event_t event;
+	event.type = ev_keyup;
+	event.data2 = 0;
+	event.data3 = 0;
+
+	lastjoy3buttons = lastjoy3hats = 0;
+
+	// emulate the up of all joystick buttons
+	for (i = 0; i < JOYBUTTONS; i++)
+	{
+		event.data1 = KEY_2JOY1 + i;
+		D_PostEvent(&event);
+	}
+
+	// emulate the up of all joystick hats
+	for (i = 0; i < JOYHATS*4; i++)
+	{
+		event.data1 = KEY_2HAT1 + i;
+		D_PostEvent(&event);
+	}
+
+	// reset joystick position
+	event.type = ev_joystick3;
+	for (i = 0; i < JOYAXISSET; i++)
+	{
+		event.data1 = i;
+		D_PostEvent(&event);
+	}
+
+	JoyReset(&JoyInfo3);
+	if (!joystick_started && !joystick3_started && SDL_WasInit(SDL_INIT_JOYSTICK) == SDL_INIT_JOYSTICK)
+	{
+		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
+		if (cv_usejoystick3.value == 0)
+		{
+			DEBFILE("I_Joystick3: SDL's Joystick system has been shutdown\n");
+		}
+	}
+}
+
 void I_GetJoystick2Events(void)
 {
 	static event_t event = {0,0,0,0};
@@ -1420,12 +1496,46 @@ void I_InitJoystick2(void)
 	joystick2_started = 1;
 }
 
+void I_InitJoystick3(void)
+{
+	//I_ShutdownJoystick3();
+	SDL_SetHintWithPriority("SDL_XINPUT_ENABLED", "0", SDL_HINT_OVERRIDE);
+	if (!strcmp(cv_usejoystick3.string, "0") || M_CheckParm("-nojoy"))
+		return;
+	/*if (joy_open3(cv_usejoystick3.string) != -1)
+		JoyInfo3.oldjoy = atoi(cv_usejoystick3.string);
+	else
+	{
+		cv_usejoystick3.value = 0;
+		return;
+	}
+	joystick3_started = 1;*/
+}
+
+void I_InitJoystick4(void)
+{
+	//I_ShutdownJoystick4();
+	SDL_SetHintWithPriority("SDL_XINPUT_ENABLED", "0", SDL_HINT_OVERRIDE);
+	if (!strcmp(cv_usejoystick3.string, "0") || M_CheckParm("-nojoy"))
+		return;
+	/*if (joy_open4(cv_usejoystick4.string) != -1)
+		JoyInfo4.oldjoy = atoi(cv_usejoystick4.string);
+	else
+	{
+		cv_usejoystick4.value = 0;
+		return;
+	}
+	joystick4_started = 1;*/
+}
+
 static void I_ShutdownInput(void)
 {
 	if (SDL_WasInit(SDL_INIT_JOYSTICK) == SDL_INIT_JOYSTICK)
 	{
 		JoyReset(&JoyInfo);
 		JoyReset(&JoyInfo2);
+		JoyReset(&JoyInfo3);
+		JoyReset(&JoyInfo4);
 		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
 	}
 
@@ -1954,6 +2064,24 @@ FUNCMATH ticcmd_t *I_BaseTiccmd2(void)
 	return &emptycmd2;
 }
 
+/**	\brief empty ticcmd for player 3
+*/
+static ticcmd_t emptycmd3;
+
+FUNCMATH ticcmd_t *I_BaseTiccmd3(void)
+{
+	return &emptycmd3;
+}
+
+/**	\brief empty ticcmd for player 4
+*/
+static ticcmd_t emptycmd4;
+
+FUNCMATH ticcmd_t *I_BaseTiccmd4(void)
+{
+	return &emptycmd4;
+}
+
 #if defined (_WIN32)
 static HMODULE winmm = NULL;
 static DWORD starttickcount = 0; // hack for win2k time bug