diff --git a/src/cu_events.c b/src/cu_events.c
index aeb733669c2e350d1043e0336e8fb96ce7adbc5a..dc3a9096696f040d1c9960ce940661c3249f7c29 100644
--- a/src/cu_events.c
+++ b/src/cu_events.c
@@ -6,19 +6,39 @@
 
 #define INACTIVE_MSG "Inactive for too long"
 
+typedef enum votetype_e
+{
+	VOTE_SKIP,
+	VOTE_EXITLEVEL,
+} votetype_t;
+
+static char const *votenames[] =
+{
+	"skipping the cutscene",
+	"exiting the level",
+};
+
 static consvar_t cv_leveltime = CVAR_INIT("leveltime", "10", 0, CV_Unsigned, NULL);
 static consvar_t cv_idletime = CVAR_INIT("idletime", "1", 0, CV_Unsigned, NULL);
+static consvar_t cv_votetime = CVAR_INIT("votetime", "30", 0, CV_Unsigned, NULL);
+static consvar_t cv_votecooldown = CVAR_INIT("votecooldown", "60", 0, CV_Unsigned, NULL);
 
 static tic_t idletics[MAXPLAYERS];
-static boolean skipvotes[MAXPLAYERS];
-static boolean isending = false;
 static boolean skiptime = 0;
 static boolean previousmap;
 
+static boolean votes[MAXNETNODES];
+static votetype_t currentvote;
+static INT32 votetime = 0;
+static INT32 voteplayer;
+static INT32 votecooldown[MAXNETNODES];
+
 void CU_Initialize(void)
 {
 	CV_RegisterVar(&cv_leveltime);
 	CV_RegisterVar(&cv_idletime);
+	CV_RegisterVar(&cv_votetime);
+	CV_RegisterVar(&cv_votecooldown);
 }
 
 void CU_PlayerJoin(INT32 playernum)
@@ -27,6 +47,94 @@ void CU_PlayerJoin(INT32 playernum)
 		return;
 
 	idletics[playernum] = 0;
+	votes[playernum] = false;
+	votecooldown[playernum] = 0;
+}
+
+static void BeginVote(votetype_t type, INT32 player)
+{
+	INT32 i;
+	if (votetime)
+	{
+		COM_BufInsertText("say \"A vote is already in progress.\"");
+		return;
+	}
+
+	if (votecooldown[player])
+	{
+		COM_BufInsertText(va("say \"Voting is currently on cooldown, please wait %d seconds.\"", votecooldown[player] / TICRATE));
+		return;
+	}
+
+	currentvote = type;
+	voteplayer = player;
+	for (i = 0; i < MAXNETNODES; i++)
+		votes[i] = false;
+
+	COM_BufInsertText(va("say \"A vote for %s has started! Type !yes or !no to vote!\"", votenames[type]));
+	votetime = TICRATE * cv_votetime.value;
+}
+
+static void EndVote(void)
+{
+	INT32 i;
+	UINT32 novotes = 0, yes = 0, no = 0;
+	for (i = 0; i < MAXNETNODES; i++)
+	{
+		if (nodeingame[i] && i != servernode)
+		{
+			if (votes[i] == 1)
+				yes++;
+			else if (votes[i] == 2)
+				no++;
+			else
+				novotes++;
+		}
+	}
+
+	if (yes > no)
+	{
+		COM_BufInsertText(va("say \"Vote for %s succeeded! (%d voted yes, %d voted no, %d did not vote)\"", votenames[currentvote], yes, no, novotes));
+		switch (currentvote)
+		{
+			case VOTE_SKIP:
+				skiptime = TICRATE * 3;
+				break;
+			case VOTE_EXITLEVEL:
+				COM_BufInsertText("exitlevel");
+				break;
+		}
+	}
+	else
+	{
+		votecooldown[voteplayer] = TICRATE * cv_votecooldown.value;
+		CONS_Printf("cooldown on %d for %d seconds\n", voteplayer, votecooldown[voteplayer]);
+		COM_BufInsertText(va("say \"Vote for %s failed! (%d voted yes, %d voted no, %d did not vote)\"", votenames[currentvote], yes, no, novotes));
+	}
+	votetime = 0;
+}
+
+static void UpdateVotes(void)
+{
+	INT32 i;
+	UINT32 novotes = 0, yes = 0, no = 0;
+	for (i = 0; i < MAXNETNODES; i++)
+	{
+		if (nodeingame[i] && i != servernode)
+		{
+			if (votes[i] == 1)
+				yes++;
+			else if (votes[i] == 2)
+				no++;
+			else
+				novotes++;
+		}
+	}
+
+	if (novotes == 0)
+		EndVote();
+	else
+		COM_BufInsertText(va("say \"Vote for %s: %d yes, %d no, %d not voted.\"", votenames[currentvote], yes, no, novotes));
 }
 
 void CU_PlayerMsg(INT32 playernum, SINT8 target, UINT8 flags, char const *msg)
@@ -40,55 +148,64 @@ void CU_PlayerMsg(INT32 playernum, SINT8 target, UINT8 flags, char const *msg)
 	{
 		if (strcmp(msg, "!help") == 0)
 		{
-			COM_BufInsertText("say \"Available commands: !help, !skip\"");
+			COM_BufInsertText("say \"Available commands: !help, !vote, !yes, !no\"");
 		}
-		else if (strcmp(msg, "!skip") == 0)
+		else if (strcmp(msg, "!vote") == 0)
 		{
-			INT32 i;
-			INT32 numplayers = 0, votes = 0;
-			if (!isending)
+			COM_BufInsertText("say \"Available vote types: exitlevel, skipcutscene\"");
+		}
+		else if (strncmp(msg, "!vote ", 6) == 0)
+		{
+			if (strcmp(&msg[6], "skipcutscene") == 0)
 			{
-				COM_BufInsertText("say \"Ending cutscene hasn't started yet!\"");
-				return;
-			}
+				if (gamestate != GS_CREDITS && gamestate != GS_ENDING && gamestate != GS_CUTSCENE && gamestate != GS_EVALUATION)
+				{
+					COM_BufInsertText("say \"There's no active cutscene at the moment!\"");
+					return;
+				}
 
-			if (skipvotes[playernum])
-			{
-				COM_BufInsertText("say \"You have already voted to skip!\"");
-				return;
+				BeginVote(VOTE_SKIP, playernum);
 			}
-
-			skipvotes[playernum] = true;
-			for (i = 0; i < MAXPLAYERS; i++)
+			else if (strcmp(&msg[6], "exitlevel") == 0)
 			{
-				if (nodeingame[playernode[i]])
+				if (gamestate != GS_LEVEL)
 				{
-					numplayers++;
-					if (skipvotes[playernum])
-						votes++;
+					COM_BufInsertText("say \"You can't vote for this right now.\"");
+					return;
 				}
-			}
 
-			if (votes >= numplayers / 2)
-			{
-				COM_BufInsertText("say \"Skip vote succeeded! Cutscene will be skipped shortly...\"");
-				skiptime = TICRATE * 3;
+				BeginVote(VOTE_EXITLEVEL, playernum);
 			}
 			else
 			{
-				COM_BufInsertText(va("say \"One more player voted to skip, need %d more!\"", numplayers / 2 - votes));
+				COM_BufInsertText("say \"Unknown vote.\"");
 			}
 		}
+		else if (strcmp(msg, "!yes") == 0)
+		{
+			votes[playernode[playernum]] = 1;
+			UpdateVotes();
+		}
+		else if (strcmp(msg, "!no") == 0)
+		{
+			votes[playernode[playernum]] = 2;
+			UpdateVotes();
+		}
+		else
+		{
+			COM_BufInsertText("say \"Unknown command, try !help for commands.\"");
+		}
 	}
 }
 
 void CU_MapChange(INT16 mapnumber)
 {
-	INT32 i;
+	if (!dedicated)
+		return;
 	if (mapnumber == previousmap)
 		return; // warped to the same map
 
-	if (isending && mapnumber == 1)
+	if (mapnumber == 1)
 	{
 		INT32 next = 0;
 		switch (previousmap)
@@ -99,15 +216,10 @@ void CU_MapChange(INT16 mapnumber)
 			case 32: next = 33; break;
 			case 33: next = 40; break;
 		}
-		I_Assert(next != 0);
-		COM_BufInsertText(va("map map%d", next));
-	}
-	else if (mapnumber < 30 || mapnumber >= 40)
-	{
-		isending = false;
+		if (next != 0)
+			COM_BufInsertText(va("map map%d", next));
 	}
-	for (i = 0; i < MAXPLAYERS; i++)
-		skipvotes[i] = false;
+	skiptime = 0;
 	previousmap = mapnumber;
 }
 
@@ -143,16 +255,16 @@ void CU_ThinkFrame(void)
 	}
 
 	if (cv_leveltime.value != 0 && (tic_t)(cv_leveltime.value - 1) * TICRATE * 60 == leveltime && gamestate == GS_LEVEL)
-	{
 		COM_BufInsertText("say \"Only one minute remaining, hurry up!\"");
-	}
 	else if (cv_leveltime.value != 0 && (tic_t)cv_leveltime.value * TICRATE * 60 < leveltime && gamestate == GS_LEVEL)
 		COM_BufInsertText("exitlevel");
 }
 
 void CU_EndFrame(void)
 {
-	isending = true;
+	if (!dedicated)
+		return;
+
 	if (skiptime != 0)
 	{
 		skiptime--;
@@ -160,3 +272,23 @@ void CU_EndFrame(void)
 			COM_BufInsertText("map map30");
 	}
 }
+
+void CU_Ticker(void)
+{
+	INT32 i;
+	if (!dedicated)
+		return;
+
+	for (i = 0; i < MAXNETNODES; i++)
+	{
+		if (votecooldown[i] > 0)
+			votecooldown[i]--;
+	}
+
+	if (votetime)
+	{
+		votetime--;
+		if (votetime == 0)
+			EndVote();
+	}
+}
diff --git a/src/cu_events.h b/src/cu_events.h
index 6fdd05624372f3e1811605bb3274726131825393..44a966e0e5cea5d6e88b5d5e04af3da339dbe4bc 100644
--- a/src/cu_events.h
+++ b/src/cu_events.h
@@ -10,5 +10,6 @@ void CU_PreMapChange(INT16 mapnumber);
 void CU_MapChange(INT16 mapnumber);
 void CU_ThinkFrame(void);
 void CU_EndFrame(void);
+void CU_Ticker(void);
 
 #endif // cu_events_h_INCLUDED
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 9f58db21dbdf0ce3de96fac62dce76d052dbfa97..f8dca6dedebdbe51430d4301e03dedb811de5e52 100755
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -3870,8 +3870,10 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 		COM_BufAddText(va("sayto %d %s\n", newplayernum, motd));
 
 	if (!rejoined)
+	{
 		LUA_HookInt(newplayernum, HOOK(PlayerJoin));
-	CU_PlayerJoin(newplayernum);
+		CU_PlayerJoin(newplayernum);
+	}
 }
 
 static boolean SV_AddWaitingPlayers(const char *name, const char *name2)
@@ -5757,6 +5759,7 @@ void NetUpdate(void)
 				D_Clearticcmd(tictoclear);                    // Clear the maketic the new tic
 
 			SV_SendTics();
+			CU_Ticker();
 
 			neededtic = maketic; // The server is a client too
 		}