diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -799,6 +799,7 @@
 	bots.cpp #ST
 	browser.cpp #ST
 	callvote.cpp #ST
+	callvotetype.cpp #ZA
 	campaign.cpp #ST
 	c_bind.cpp
 	c_cmds.cpp
diff --git a/src/callvote.cpp b/src/callvote.cpp
--- a/src/callvote.cpp
+++ b/src/callvote.cpp
@@ -65,13 +65,20 @@
 #include "sv_main.h"
 #include "v_video.h"
 #include "maprotation.h"
+#include "deathmatch.h"
+#include "duel.h"
+#include "lastmanstanding.h"
+#include "team.h"
+#include "sv_ban.h"
+#include "callvotetype.h"
+#include "p_acs.h"
 #include <list>
 
 //*****************************************************************************
 //	VARIABLES
 
 static	VOTESTATE_e				g_VoteState;
-static	FString					g_VoteCommand;
+static	FString					g_VoteDisplay;
 static	FString					g_VoteReason;
 static	ULONG					g_ulVoteCaller;
 static	ULONG					g_ulVoteCountdownTicks = 0;
@@ -83,6 +90,8 @@
 static	ULONG					g_ulPlayersWhoVotedNo[(MAXPLAYERS / 2) + 1];
 static	NETADDRESS_s			g_KickVoteVictimAddress;
 static	std::list<VOTE_s>		g_PreviousVotes;
+static	VoteStateData			g_VoteStateData;
+static	FString					g_CurrentVoteSummary;
 
 //*****************************************************************************
 //	PROTOTYPES
@@ -90,10 +99,8 @@
 static	void			callvote_EndVote( void );
 static	ULONG			callvote_CountPlayersWhoVotedYes( void );
 static	ULONG			callvote_CountPlayersWhoVotedNo( void );
-static	bool			callvote_CheckForFlooding( FString &Command, FString &Parameters, ULONG ulPlayer );
-static	bool			callvote_CheckValidity( FString &Command, FString &Parameters );
-static	ULONG			callvote_GetVoteType( const char *pszCommand );
-static	bool			callvote_IsKickVote( const ULONG ulVoteType );
+static	bool			callvote_CheckForFlooding( ULONG& Command, FString& Parameters, ULONG ulPlayer );
+static	void			callvote_ExecuteVote( const VoteStateData& input );
 
 //*****************************************************************************
 //	FUNCTIONS
@@ -150,27 +157,16 @@
 				g_PreviousVotes.back( ).bPassed = g_bVotePassed;
 
 				// If the vote passed, execute the command string.
-				if (( g_bVotePassed ) && ( !g_bVoteCancelled ) &&
-					( NETWORK_GetState( ) != NETSTATE_CLIENT ) &&
-					( CLIENTDEMO_IsPlaying( ) == false ))
+				if (( g_bVotePassed )
+					&& ( g_bVoteCancelled == false )
+					&& ( NETWORK_InClientMode() == false ))
 				{
-					// [BB, RC] If the vote is a kick vote, we have to rewrite g_VoteCommand to both use the stored IP, and temporarily ban it.
-					// [Dusk] Write the kick reason into the ban reason, [BB] but only if it's not empty.
-					// [BB] "forcespec" votes need a similar handling.
-					if ( ( strncmp( g_VoteCommand, "kick", 4 ) == 0 ) || ( strncmp( g_VoteCommand, "forcespec", 9 ) == 0 ) )
-					{
-						if ( strncmp( g_VoteCommand, "kick", 4 ) == 0 )
-							g_VoteCommand.Format( "addban %s 10min \"Vote kick", NETWORK_AddressToString( g_KickVoteVictimAddress ) );
-						else
-							g_VoteCommand.Format( "kickfromgame_idx %d \"Vote forcespec", SERVER_FindClientByAddress ( g_KickVoteVictimAddress ) );
-						g_VoteCommand.AppendFormat( ", %d to %d", static_cast<int>(callvote_CountPlayersWhoVotedYes( )), static_cast<int>(callvote_CountPlayersWhoVotedNo( )) );
-						if ( g_VoteReason.IsNotEmpty() )
-							g_VoteCommand.AppendFormat ( " (%s)", g_VoteReason.GetChars( ) );
-						g_VoteCommand += ".\"";
-					}
+					VoteValidateResult output;
+					g_VoteStateData.yes = callvote_CountPlayersWhoVotedYes( );
+					g_VoteStateData.no = callvote_CountPlayersWhoVotedNo( );
+					callvote_ExecuteVote( g_VoteStateData );
+				}
 
-					AddCommandString( (char *)g_VoteCommand.GetChars( ));
-				}
 				// Reset the module.
 				CALLVOTE_ClearVote( );
 			}
@@ -181,8 +177,11 @@
 
 //*****************************************************************************
 //
-void CALLVOTE_BeginVote( FString Command, FString Parameters, FString Reason, ULONG ulPlayer )
+void CALLVOTE_BeginVote( ULONG Command, FString Parameters, FString Reason, ULONG ulPlayer )
 {
+	VoteValidateResult validateResult;
+	VoteClass* votetype = VOTEDEF_FindTypeByIndex( Command );
+
 	// Don't allow a vote in the middle of another vote.
 	if ( g_VoteState != VOTESTATE_NOVOTE )
 	{
@@ -191,47 +190,68 @@
 		return;
 	}
 
+	g_VoteStateData.type = votetype;
+	g_VoteStateData.arg = Parameters;
+	g_VoteStateData.caller = SERVER_GetCurrentClient( );
+	g_VoteStateData.yes = 0;
+	g_VoteStateData.no = 0;
+	g_VoteStateData.reason = Reason;
+
 	// Check and make sure all the parameters are valid.
-	if ( callvote_CheckValidity( Command, Parameters ) == false )
+	if (( VOTEDEF_Validate( g_VoteStateData, validateResult ) == false )
+		&& ( NETWORK_GetState() == NETSTATE_SERVER ))
+	{
+		SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient(),
+			"Cannot call that vote because %s\n", validateResult.error.GetChars() );
+		return;
+	}
+
+	if ( votetype == NULL )
 		return;
 
 	// Prevent excessive re-voting.
 	if (( NETWORK_GetState( ) == NETSTATE_SERVER ) && callvote_CheckForFlooding( Command, Parameters, ulPlayer ) == false )
 		return;
 
+	if ( votetype->argumentType() == VOTEARG_Player )
+	{
+		g_VoteStateData.targetAddress = SERVER_GetClient( validateResult.argument )->Address;
+		g_VoteStateData.targetAddressKnown = true;
+	}
+
+	g_CurrentVoteSummary = VOTEDEF_MakeVoteSummary( g_VoteStateData );
+
 	// Play the announcer sound for this.
 	ANNOUNCER_PlayEntry( cl_announcer, "VoteNow" );
 
-	// Create the vote console command.
-	g_VoteCommand = Command;
-	g_VoteCommand += " ";
-	g_VoteCommand += Parameters;
+	// [TP] How to display the vote?
+	g_VoteDisplay = votetype->name();
+
+	if ( votetype->hasArgument() )
+		g_VoteDisplay += " (" + Parameters + ")";
+
 	g_ulVoteCaller = ulPlayer;
 	g_VoteReason = Reason.Left(25);
 
 	// Create the record of the vote for flood prevention.
 	{
 		VOTE_s VoteRecord;
-		VoteRecord.fsParameter = Parameters;
-		time_t tNow;
-		time( &tNow );
-		VoteRecord.tTimeCalled = tNow;
+		time( &VoteRecord.tTimeCalled );
+		VoteRecord.VoteType = g_VoteStateData.type;
 		VoteRecord.Address = SERVER_GetClient( g_ulVoteCaller )->Address;
-		VoteRecord.ulVoteType = callvote_GetVoteType( Command );
-
-		if ( callvote_IsKickVote ( VoteRecord.ulVoteType ) )
-			VoteRecord.KickAddress = g_KickVoteVictimAddress; 
-
+		VoteRecord.Summary = g_CurrentVoteSummary;
 		g_PreviousVotes.push_back( VoteRecord );
 	}
 
 	// Display the message in the console.
 	{
-		FString	ReasonBlurb = ( g_VoteReason.Len( )) ? ( ", reason: \"" + g_VoteReason + "\"" ) : "";
+		FString ReasonBlurb = ( g_VoteReason.Len( )) ? ( ": \"" + g_VoteReason + "\"" ) : "";
+		FString IPString;
 		if ( NETWORK_GetState( ) == NETSTATE_SERVER )
-			Printf( "%s\\c- (%s) has called a vote (\"%s\"%s).\n", players[ulPlayer].userinfo.netname, NETWORK_AddressToString( SERVER_GetClient( ulPlayer )->Address ), g_VoteCommand.GetChars(), ReasonBlurb.GetChars() );
-		else
-			Printf( "%s\\c- has called a vote (\"%s\"%s).\n", players[ulPlayer].userinfo.netname, g_VoteCommand.GetChars(), ReasonBlurb.GetChars() );
+			IPString.Format( " (%s)", NETWORK_AddressToString( SERVER_GetClient( ulPlayer )->Address ));
+
+		Printf( "%s%s\\c- has called a %s vote%s.\n", players[ulPlayer].userinfo.netname,
+			IPString.GetChars(), g_VoteDisplay.GetChars(), ReasonBlurb.GetChars() );
 	}
 
 	g_VoteState = VOTESTATE_INVOTE;
@@ -251,10 +271,11 @@
 	ULONG	ulIdx;
 
 	g_VoteState = VOTESTATE_NOVOTE;
-	g_VoteCommand = "";
+	g_VoteDisplay = "";
 	g_ulVoteCaller = MAXPLAYERS;
 	g_ulVoteCountdownTicks = 0;
 	g_ulShowVoteScreenTicks = 0;
+	g_VoteStateData = VoteStateData();
 
 	for ( ulIdx = 0; ulIdx < (( MAXPLAYERS / 2 ) + 1 ); ulIdx++ )
 	{
@@ -373,7 +394,7 @@
 	if ( ulPlayer == g_ulVoteCaller && ( NETWORK_GetState( ) == NETSTATE_SERVER ))
 	{
 		// [BB] If a player canceled his own vote, don't prevent others from making this type of vote again.
-		g_PreviousVotes.back( ).ulVoteType = NUM_VOTECMDS;
+		g_PreviousVotes.back( ).VoteType = NULL;
 
 		SERVER_Printf( PRINT_HIGH, "Vote caller cancelled the vote.\n" );
 		g_bVoteCancelled = true;
@@ -437,7 +458,7 @@
 	{
 		return ( true );
 	}
-	
+
 	SERVERCOMMANDS_PlayerVote( ulPlayer, false );
 
 	ulNumYes = callvote_CountPlayersWhoVotedYes( );
@@ -499,14 +520,108 @@
 
 //*****************************************************************************
 //
+// [TP] Executes the called vote
+//
+static void callvote_ExecuteVote( const VoteStateData& input )
+{
+	VoteValidateResult data;
+
+	// [TP] Validate again before executing it.
+	if ( VOTEDEF_Validate( input, data ) == false )
+	{
+		SERVER_Printf( PRINT_HIGH, TEXTCOLOR_ORANGE "Cannot execute vote: %s\n", data.error.GetChars() );
+		return;
+	}
+
+	if ( input.type->isNative() )
+	{
+		// [TP] Write the kick reason into the ban reason, [BB] but only if it's not empty.
+		FString votekickmessage;
+		votekickmessage.Format( "%s: %d to %d: %s",
+			input.type->nativeType() == VOTETYPE_Kick ? "Vote kick" : "Voted to be forced to spectate",
+			input.yes, input.no,
+			( input.reason.IsNotEmpty() ? input.reason.GetChars() : "No reason given" ));
+
+		switch ( input.type->nativeType() )
+		{
+		case VOTETYPE_Kick:
+			{
+				// [BB, RC] If the vote is a kick vote, we have to use stored IP and temporarily ban it.
+				std::string response;
+				SERVERBAN_BanIP ( input.targetAddress, 10 * MINUTE, votekickmessage, response );
+				Printf( "[VOTEKICK] %s\n", response.c_str() );
+				break;
+			}
+
+		case VOTETYPE_ForceSpec:
+			SERVER_KickPlayerFromGame( data.argument, votekickmessage );
+			break;
+
+		case VOTETYPE_ChangeMap:
+			strncpy( level.nextmap, data.stringArgument, 8 );
+			level.flags |= LEVEL_CHANGEMAPCHEAT;
+			G_ExitLevel( 0, false );
+			break;
+
+		case VOTETYPE_Map:
+			{
+				// [TP] There's enough stuff in the map ccmd that this needs to be done..
+				// This should be safe since the votedef code ensures that the argument
+				// is a valid map name and no valid map name should break this.
+				char command[64];
+				sprintf( command, "map \"%s\"", data.stringArgument.GetChars() );
+				AddCommandString( command );
+				break;
+			}
+
+		case VOTETYPE_FragLimit:
+			fraglimit = data.argument;
+			break;
+
+		case VOTETYPE_DuelLimit:
+			duellimit = data.argument;
+			break;
+
+		case VOTETYPE_WinLimit:
+			winlimit = data.argument;
+			break;
+
+		case VOTETYPE_PointLimit:
+			pointlimit = data.argument;
+			break;
+
+		case VOTETYPE_TimeLimit:
+			timelimit = data.floatArgument;
+			break;
+
+		case VOTETYPE_NumNatives:
+			break; // impossible
+		}
+	}
+	else
+	{
+		DWORD argvalue = data.argument;
+
+		if (( input.type->argumentType() == VOTEARG_String )
+			|| ( input.type->argumentType() == VOTEARG_Level ))
+		{
+			argvalue = ACS_PushAndReturnDynamicString( data.stringArgument, NULL, 0 );
+		}
+
+		Printf( "[VOTE] -> Script %d (%s)\n", input.type->scriptNumber(), input.arg.GetChars() );
+		int callerid = input.caller;
+		AActor* mo = PLAYER_IsValidPlayerWithMo( callerid ) ? players[callerid].mo : NULL;
+		P_StartScript( mo, NULL, input.type->scriptNumber(), NULL, false, argvalue, 0, 0, 1, false );
+	}
+}
+
+//*****************************************************************************
+//
 void CALLVOTE_EndVote( bool bPassed )
 {
 	// This is a client-only function.
-	if (( NETWORK_GetState( ) != NETSTATE_CLIENT ) &&
-		( CLIENTDEMO_IsPlaying( ) == false ))
-	{
+	if ( NETWORK_InClientMode() == false )
 		return;
-	}
 
 	g_bVotePassed = bPassed;
 	callvote_EndVote( );
@@ -516,7 +631,7 @@
 //
 const char *CALLVOTE_GetCommand( void )
 {
-	return ( g_VoteCommand.GetChars( ));
+	return ( g_VoteDisplay.GetChars( ));
 }
 
 //*****************************************************************************
@@ -651,10 +766,9 @@
 
 //*****************************************************************************
 //
-static bool callvote_CheckForFlooding( FString &Command, FString &Parameters, ULONG ulPlayer )
+static bool callvote_CheckForFlooding( ULONG &Command, FString &Parameters, ULONG ulPlayer )
 {
 	NETADDRESS_s	Address = SERVER_GetClient( ulPlayer )->Address;
-	ULONG			ulVoteType = callvote_GetVoteType( Command );
 	time_t tNow;
 	time( &tNow );
 
@@ -670,11 +784,18 @@
 	// Run through the vote cache (backwards, from recent to old) and search for grounds on which to reject the vote.
 	for( std::list<VOTE_s>::reverse_iterator i = g_PreviousVotes.rbegin(); i != g_PreviousVotes.rend(); ++i )
 	{
+		VoteClass* type = i->VoteType;
+
+		if ( type == NULL )
+			continue;
+
 		// One *type* of vote per voter per ## minutes (excluding kick votes if they passed).
-		if ( !( callvote_IsKickVote ( i->ulVoteType ) && i->bPassed ) && NETWORK_CompareAddress( i->Address, Address, true ) && ( ulVoteType == i->ulVoteType ) && (( tNow - i->tTimeCalled ) < VOTER_VOTETYPE_INTERVAL * MINUTE ))
+		if ( !(( type->flags() & VOTEF_NoLimitIfPassed ) && i->bPassed ) && NETWORK_CompareAddress( i->Address, Address, true ) &&
+			( g_VoteStateData.type == i->VoteType ) && (( tNow - i->tTimeCalled ) < VOTER_VOTETYPE_INTERVAL * MINUTE ))
 		{
 			int iMinutesLeft = static_cast<int>( 1 + ( i->tTimeCalled + VOTER_VOTETYPE_INTERVAL * MINUTE - tNow ) / MINUTE );
-			SERVER_PrintfPlayer( PRINT_HIGH, ulPlayer, "You must wait %d minute%s to call another %s vote.\n", iMinutesLeft, ( iMinutesLeft == 1 ? "" : "s" ), Command.GetChars() );
+			SERVER_PrintfPlayer( PRINT_HIGH, ulPlayer, "You must wait %d minute%s to call another %s vote.\n",
+				iMinutesLeft, ( iMinutesLeft == 1 ? "" : "s" ), type->name().GetChars() );
 			return false;
 		}
 
@@ -682,28 +803,21 @@
 		if ( NETWORK_CompareAddress( i->Address, Address, true ) && (( tNow - i->tTimeCalled ) < VOTER_NEWVOTE_INTERVAL * MINUTE ))
 		{
 			int iMinutesLeft = static_cast<int>( 1 + ( i->tTimeCalled + VOTER_NEWVOTE_INTERVAL * MINUTE - tNow ) / MINUTE );
-			SERVER_PrintfPlayer( PRINT_HIGH, ulPlayer, "You must wait %d minute%s to call another vote.\n", iMinutesLeft, ( iMinutesLeft == 1 ? "" : "s" ));
+			SERVER_PrintfPlayer( PRINT_HIGH, ulPlayer, "You must wait %d minute%s to call another vote.\n",
+					iMinutesLeft, ( iMinutesLeft == 1 ? "" : "s" ));
 			return false;
 		}
 
 		// Specific votes ("map map30") that fail can't be re-proposed for ## minutes.
-		if (( ulVoteType == i->ulVoteType ) && ( !i->bPassed ) && (( tNow - i->tTimeCalled ) < VOTE_LITERALREVOTE_INTERVAL * MINUTE ))
+		// [TP] Using generalized summaries. This takes care of the IP checking for instance.
+		if (( g_CurrentVoteSummary == i->Summary ) && ( !i->bPassed ) && (( tNow - i->tTimeCalled ) < VOTE_LITERALREVOTE_INTERVAL * MINUTE ))
 		{
-			int iMinutesLeft = static_cast<int>( 1 + ( i->tTimeCalled + VOTE_LITERALREVOTE_INTERVAL * MINUTE - tNow ) / MINUTE );
+			int iMinutesLeft ( 1 + ( i->tTimeCalled + VOTE_LITERALREVOTE_INTERVAL * MINUTE - tNow ) / MINUTE );
 
-			// Kickvotes (can't give the IP to clients!).
-			if ( callvote_IsKickVote ( i->ulVoteType ) && ( !i->bPassed ) && NETWORK_CompareAddress( i->KickAddress, g_KickVoteVictimAddress, true ))
-			{
-				SERVER_PrintfPlayer( PRINT_HIGH, ulPlayer, "That specific player was recently on voted to be kicked or forced to spectate, but the vote failed. You must wait %d minute%s to call it again.\n", iMinutesLeft, ( iMinutesLeft == 1 ? "" : "s" ));
-				return false;
-			}
-
-			// Other votes.
-			if ( ( callvote_IsKickVote ( i->ulVoteType ) == false ) && ( stricmp( i->fsParameter.GetChars(), Parameters.GetChars() ) == 0 ))
-			{
-				SERVER_PrintfPlayer( PRINT_HIGH, ulPlayer, "That specific vote (\"%s %s\") was recently called, and failed. You must wait %d minute%s to call it again.\n", Command.GetChars(), Parameters.GetChars(), iMinutesLeft, ( iMinutesLeft == 1 ? "" : "s" ));
-				return false;
-			}
+			SERVER_PrintfPlayer( PRINT_HIGH, ulPlayer, "That specific vote was recently "
+				"called, and failed. You must wait %d minute%s to call it again.\n",
+				iMinutesLeft, ( iMinutesLeft == 1 ? "" : "s" ));
+			return false;
 		}
 	}
 
@@ -711,167 +825,6 @@
 }
 
 //*****************************************************************************
-//
-static bool callvote_CheckValidity( FString &Command, FString &Parameters )
-{
-	// Get the type of vote this is.
-	ULONG	ulVoteCmd = callvote_GetVoteType( Command.GetChars( ));
-	if ( ulVoteCmd == NUM_VOTECMDS )
-		return ( false );
-
-	// Check for any illegal characters.
-	if ( callvote_IsKickVote ( ulVoteCmd ) == false )
-	{
-		int i = 0;
-		while ( Parameters.GetChars()[i] != '\0' )
-		{
-			if ( Parameters.GetChars()[i] == ';' || Parameters.GetChars()[i] == ' ' )
-			{
-				if ( NETWORK_GetState( ) == NETSTATE_SERVER )
-					SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "That vote command contained illegal characters.\n" );
-  				return ( false );
-			}
-			i++;
-		}
-	}
-
-	// Then, make sure the parameter for each vote is valid.
-	int parameterInt = atoi( Parameters.GetChars() );
-	switch ( ulVoteCmd )
-	{
-	case VOTECMD_KICK:
-	case VOTECMD_KICKFROMGAME:
-		{
-			if ( NETWORK_GetState( ) == NETSTATE_SERVER )
-			{
-				// Store the player's IP so he can't get away.
-				ULONG ulIdx = SERVER_GetPlayerIndexFromName( Parameters.GetChars( ), true, false );
-				if ( ulIdx < MAXPLAYERS )
-				{
-					if ( static_cast<LONG>(ulIdx) == SERVER_GetCurrentClient( ))
-					{
-						SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "You cannot call a vote to kick or to force to spectate yourself!\n" );
-  						return ( false );
-					}
-					// [BB] Don't allow anyone to kick somebody who is on the admin list. [K6] ...or is logged into RCON.
-					if ( SERVER_GetAdminList()->isIPInList( SERVER_GetClient( ulIdx )->Address )
-						|| SERVER_GetClient( ulIdx )->bRCONAccess )
-					{
-						SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "This player is a server admin and thus can't be kicked or forced to spectate!\n" );
-  						return ( false );
-					}
-					g_KickVoteVictimAddress = SERVER_GetClient( ulIdx )->Address;
-					return ( true );
-				}
-				else
-				{
-					SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "That player doesn't exist.\n" );
-					return ( false );
-				}
-			}
-		}
-		break;
-	case VOTECMD_MAP:
-	case VOTECMD_CHANGEMAP:
-
-		// Don't allow the command if the map doesn't exist.
-		if ( !P_CheckIfMapExists( Parameters.GetChars( )))
-		{
-			if ( NETWORK_GetState( ) == NETSTATE_SERVER )
-				SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "That map does not exist.\n" );
-			return ( false );
-		}
-		
-		// Don't allow us to leave the map rotation.
-		if ( NETWORK_GetState( ) == NETSTATE_SERVER )
-		{
-			// [BB] Regardless of sv_maprotation, if the server has maps in the rotation,
-			// assume players are restricted to these maps.
-			if ( ( MAPROTATION_GetNumEntries() > 0 ) && ( MAPROTATION_IsMapInRotation( Parameters.GetChars( ) ) == false ) )
-			{
-				SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "That map is not in the map rotation.\n" );
-				return ( false );
-			}
-		}
-		break;
-	case VOTECMD_FRAGLIMIT:
-	case VOTECMD_WINLIMIT:
-	case VOTECMD_DUELLIMIT:
-
-		// Parameteter be between 0 and 255.
-		if (( parameterInt < 0 ) || ( parameterInt >= 256 ))
-		{
-			if ( NETWORK_GetState( ) == NETSTATE_SERVER )
-				SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "%s parameters must be between 0 and 255.\n", Command.GetChars() );
-			return ( false );
-		}
-		else if ( parameterInt == 0 )
-		{
-			if (( Parameters.GetChars()[0] != '0' ) || ( Parameters.Len() != 1 ))
-				return ( false );
-		}
-		Parameters.Format( "%d", parameterInt );
-		break;
-	case VOTECMD_TIMELIMIT:
-	case VOTECMD_POINTLIMIT:
-
-		// Parameteter must be between 0 and 65535.
-		if (( parameterInt < 0 ) || ( parameterInt >= 65536 ))
-		{
-			if ( NETWORK_GetState( ) == NETSTATE_SERVER )
-				SERVER_PrintfPlayer( PRINT_HIGH, SERVER_GetCurrentClient( ), "%s parameters must be between 0 and 65535.\n", Command.GetChars() );
-			return ( false );
-		}
-		else if ( parameterInt == 0 )
-		{
-			if (( Parameters.GetChars()[0] != '0' ) || ( Parameters.Len() != 1 ))
-				return ( false );
-		}
-		Parameters.Format( "%d", parameterInt );
-		break;
-	default:
-
-		return ( false );
-	}
-
-	// Passed all checks!
-	return ( true );
-}
-
-//*****************************************************************************
-//
-static ULONG callvote_GetVoteType( const char *pszCommand )
-{
-	if ( stricmp( "kick", pszCommand ) == 0 )
-		return VOTECMD_KICK;
-	else if ( stricmp( "forcespec", pszCommand ) == 0 )
-		return VOTECMD_KICKFROMGAME;
-	else if ( stricmp( "map", pszCommand ) == 0 )
-		return VOTECMD_MAP;
-	else if ( stricmp( "changemap", pszCommand ) == 0 )
-		return VOTECMD_CHANGEMAP;
-	else if ( stricmp( "fraglimit", pszCommand ) == 0 )
-		return VOTECMD_FRAGLIMIT;
-	else if ( stricmp( "timelimit", pszCommand ) == 0 )
-		return VOTECMD_TIMELIMIT;
-	else if ( stricmp( "winlimit", pszCommand ) == 0 )
-		return VOTECMD_WINLIMIT;
-	else if ( stricmp( "duellimit", pszCommand ) == 0 )
-		return VOTECMD_DUELLIMIT;
-	else if ( stricmp( "pointlimit", pszCommand ) == 0 )
-		return VOTECMD_POINTLIMIT;
-
-	return NUM_VOTECMDS;
-}
-
-//*****************************************************************************
-//
-static bool callvote_IsKickVote( const ULONG ulVoteType )
-{
-	return ( ( ulVoteType == VOTECMD_KICK ) || ( ulVoteType == VOTECMD_KICKFROMGAME ) );
-}
-
-//*****************************************************************************
 //	CONSOLE COMMANDS/VARIABLES
 
 CUSTOM_CVAR( Int, sv_minvoters, 1, CVAR_ARCHIVE )
@@ -881,7 +834,6 @@
 }
 CVAR( Int, sv_nocallvote, 0, CVAR_ARCHIVE ); // 0 - everyone can call votes. 1 - nobody can. 2 - only players can.
 CVAR( Bool, sv_nokickvote, false, CVAR_ARCHIVE );
-CVAR( Bool, sv_noforcespecvote, false, CVAR_ARCHIVE );
 CVAR( Bool, sv_nomapvote, false, CVAR_ARCHIVE );
 CVAR( Bool, sv_nochangemapvote, false, CVAR_ARCHIVE );
 CVAR( Bool, sv_nofraglimitvote, false, CVAR_ARCHIVE );
@@ -895,6 +847,8 @@
 CCMD( callvote )
 {
 	ULONG	ulVoteCmd;
+	FString arg, reason;
+	VoteClass* votetype;
 
 	// Don't allow a vote unless the player is a client.
 	if ( NETWORK_GetState( ) != NETSTATE_CLIENT )
@@ -906,7 +860,7 @@
 	if ( CLIENT_GetConnectionState( ) != CTS_ACTIVE )
 		return;
 
-	if ( argv.argc( ) < 3 )
+	if ( argv.argc( ) < 2 )
 	{
 		Printf( "callvote <command> <parameters> [reason]: Calls a vote\n" );
 		return;
@@ -919,22 +873,33 @@
 		return;
 	}
 
-	ulVoteCmd = callvote_GetVoteType( argv[1] );
-	if ( ulVoteCmd == NUM_VOTECMDS )
+	if (( votetype = VOTEDEF_FindTypeByName( argv[1] )) == NULL )
 	{
-		Printf( "Invalid callvote command.\n" );
+		Printf( "No such vote type '%s'.\nTry use one of: %s\n", argv[1],
+			VOTEDEF_GetVoteTypeList().GetChars() );
 		return;
 	}
 
-	if ( argv.argc( ) >= 4 )
-		CLIENTCOMMANDS_CallVote( ulVoteCmd, argv[2], argv[3] );
+	// [TP] We may need a second argument
+	if ( votetype->hasArgument() )
+	{
+		if ( argv.argc() < 3 )
+		{
+			Printf( "You need to supply an argument to call a %s vote:\nUsage: %s",
+				votetype->name().GetChars(), votetype->usage().GetChars());
+			return;
+		}
+
+		arg = argv[2];
+		reason = ( argv.argc() >= 4 ) ? argv[3] : "";
+	}
 	else
-		CLIENTCOMMANDS_CallVote( ulVoteCmd, argv[2], "" );
-/*
-	g_lBytesSent += g_LocalBuffer.cursize;
-	if ( g_lBytesSent > g_lMaxBytesSent )
-		g_lMaxBytesSent = g_lBytesSent;
-*/
+	{
+		arg = "";
+		reason = ( argv.argc() >= 3 ) ? argv[2] : "";
+	}
+
+	CLIENTCOMMANDS_CallVote( votetype->index(), arg, reason );
 	NETWORK_LaunchPacket( CLIENT_GetLocalBuffer( ), CLIENT_GetServerAddress( ));
 	NETWORK_ClearBuffer( CLIENT_GetLocalBuffer( ));
 }
@@ -1010,3 +975,25 @@
 		}
 	}
 }
+
+//*****************************************************************************
+//
+// [TP] What has g_PreviousVotes eaten?
+//
+CCMD ( recentvotes )
+{
+	for( std::list<VOTE_s>::reverse_iterator i = g_PreviousVotes.rbegin(); i != g_PreviousVotes.rend(); ++i )
+	{
+		Printf( "- " );
+
+		if ( i->VoteType == NULL )
+			Printf( "<canceled>" );
+		else
+			Printf( "%s", i->VoteType->name().GetChars() );
+
+		char timestring[256];
+		strftime( timestring, sizeof timestring, "%F %T", localtime( &i->tTimeCalled ));
+
+		Printf( ", called by %s on %s\n", NETWORK_AddressToString( i->Address ), timestring );
+	}
+}
diff --git a/src/callvote.h b/src/callvote.h
--- a/src/callvote.h
+++ b/src/callvote.h
@@ -66,21 +66,7 @@
 #define VOTE_LITERALREVOTE_INTERVAL		10
 #define VOTE_LONGEST_INTERVAL			10	// Sets when old votes are removed from the flood cache. Set to the longest interval of the above.
 
-//*****************************************************************************
-enum
-{
-	VOTECMD_KICK,
-	VOTECMD_KICKFROMGAME,
-	VOTECMD_MAP,
-	VOTECMD_CHANGEMAP,
-	VOTECMD_FRAGLIMIT,
-	VOTECMD_TIMELIMIT,
-	VOTECMD_WINLIMIT,
-	VOTECMD_DUELLIMIT,
-	VOTECMD_POINTLIMIT,
-
-	NUM_VOTECMDS
-};
+class VoteClass;
 
 //*****************************************************************************
 typedef enum
@@ -102,18 +88,15 @@
 	// Time that this vote was called.
 	time_t			tTimeCalled;
 
-	// The type of vote (see NUM_VOTECMDS).
-	ULONG			ulVoteType;
-	
-	// Parameter of the vote ("map01", "50", etc).
-	FString			fsParameter;
-
-	// For kick votes: the address being kicked.
-	NETADDRESS_s	KickAddress;
-
 	// Was it passed?
 	bool			bPassed;
 
+	// [TP] The type of this vote
+	VoteClass*	VoteType;
+
+	// [TP] Unique summary for this vote (contains type and argument and whatever else)
+	FString			Summary;
+
 } VOTE_s;
 
 //*****************************************************************************
@@ -122,7 +105,7 @@
 void			CALLVOTE_Construct( void );
 void			CALLVOTE_Tick( void );
 //void			CALLVOTE_Render( void );
-void			CALLVOTE_BeginVote( FString Command, FString Parameters, FString Reason, ULONG ulPlayer );
+void			CALLVOTE_BeginVote( ULONG Command, FString Parameters, FString Reason, ULONG ulPlayer );
 void			CALLVOTE_ClearVote( void );
 bool			CALLVOTE_VoteYes( ULONG ulPlayer );
 bool			CALLVOTE_VoteNo( ULONG ulPlayer );
@@ -144,7 +127,6 @@
 EXTERN_CVAR( Int, sv_minvoters );
 EXTERN_CVAR( Int, sv_nocallvote )
 EXTERN_CVAR( Bool, sv_nokickvote );
-EXTERN_CVAR( Bool, sv_noforcespecvote );
 EXTERN_CVAR( Bool, sv_nomapvote );
 EXTERN_CVAR( Bool, sv_nochangemapvote );
 EXTERN_CVAR( Bool, sv_nofraglimitvote );
diff --git a/src/callvotetype.cpp b/src/callvotetype.cpp
new file mode 100644
--- /dev/null
+++ b/src/callvotetype.cpp
@@ -0,0 +1,623 @@
+/*
+ * Zandronum source code
+ * Copyright (C) 2012-2014 Teemu Piippo
+ * Copyright (C) 2012-2014 Zandronum Development Team
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 3. Neither the name of the Skulltag Development Team nor the names of its
+ *    contributors may be used to endorse or promote products derived from this
+ *    software without specific prior written permission.
+ * 4. Redistributions in any form must be accompanied by information on how to
+ *    obtain complete source code for the software and any accompanying
+ *    software that uses the software. The source code must either be included
+ *    in the distribution or be available for no more than the cost of
+ *    distribution plus a nominal fee, and must be freely redistributable
+ *    under reasonable conditions. For an executable file, complete source
+ *    code means the source code for all modules it contains. It does not
+ *    include source code for modules or files that typically accompany the
+ *    major components of the operating system on which the executable file
+ *    runs.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Filename: votedef.cpp
+ *
+ * Description: VOTEDEF parser, vote type manager
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+#include "doomdef.h"
+#include "info.h"
+#include "tarray.h"
+#include "callvotetype.h"
+#include "network.h"
+#include "sv_main.h"
+#include "g_level.h"
+#include "v_text.h"
+#include "c_dispatch.h"
+#include "maprotation.h"
+#include "c_cvars.h"
+#include "i_system.h"
+#include "doomerrors.h"
+#include "p_acs.h"
+
+static TArray<VoteClass*> g_Defs;
+static NETADDRESS_s g_StoredIPAddress;
+
+const char* g_ParameterTokens[] =
+{
+	"INT",
+	"FLOAT",
+	"PLAYER",
+	"MAP",
+	"STRING",
+	NULL
+};
+
+const char* g_PropertyTokens[] =
+{
+	"ARGUMENT",
+	"SCRIPTNUMBER",
+	"DESCRIPTION",
+	"FORBIDCVAR",
+	"ARGUMENTLABEL",
+	"MENURANGE",
+	"MENUINCREMENT",
+	NULL
+};
+
+const char* g_FlagTokens[] =
+{
+	"NOPASSEDLIMIT",
+	"NOTONSELF",
+	"NOTONADMIN",
+	"PLAYERASINDEX",
+	"NOTONSPECTATORS",
+	"MENUSLIDER",
+	"MAPINROTATION",
+	NULL
+};
+
+const char* g_NativeNames[] =
+{
+	"KICK",
+	"FORCESPEC",
+	"CHANGEMAP",
+	"MAP",
+	"FRAGLIMIT",
+	"TIMELIMIT",
+	"POINTLIMIT",
+	"DUELLIMIT",
+	"WINLIMIT",
+	NULL
+};
+
+enum VotedefProperty
+{
+	VTPROP_Argument,
+	VTPROP_ScriptNumber,
+	VTPROP_Description,
+	VTPROP_ForbidCVar,
+	VTPROP_ArgumentLabel,
+	VTPROP_MenuRange,
+	VTPROP_MenuIncrement,
+};
+
+//
+// [TP] Vote definition constructor
+//
+VoteClass::VoteClass( const FString& name, NativeVoteType native ) :
+	_name( name ),
+	_scriptNumber( 0 ),
+	_flags( 0 ),
+	_argumentType( VOTEARG_NumArgTypes ),
+	_minimum( INT_MAX ),
+	_maximum( INT_MAX ),
+	_native( native ),
+	_index( g_Defs.Size() ),
+	_menuMinimum( 0.0f ),
+	_menuMaximum( 0.0f ),
+	_menuIncrement( 1.0f ) {}
+
+//
+// [TP] Vote definition destructor
+//
+VoteClass::~VoteClass() {}
+
+//
+// [TP] Initializes custom vote types.
+//
+void VOTEDEF_Construct()
+{
+	if ( Wads.CheckNumForName( "VOTEDEF" ) == -1 )
+		return;
+
+	// [TP] Startup message. :)
+	Printf( "VOTEDEF_Construct: Loading vote type definitions.\n" );
+	atterm( VOTEDEF_Destruct );
+
+	int lump, lastlump;
+	lastlump = 0;
+
+	while (( lump = Wads.FindLump( "VOTEDEF", &lastlump )) != -1 )
+		VOTEDEF_ParseLump( lump );
+}
+
+//
+// [TP] De-initializes the votedef stuff.
+//
+void VOTEDEF_Destruct()
+{
+	for ( unsigned i = 0; i < g_Defs.Size(); ++i )
+		delete g_Defs[i];
+
+	g_Defs.Clear();
+}
+
+static double VOTEDEF_GetNumeric( FScanner& sc )
+{
+	if ( sc.CheckToken( TK_FloatConst ) == false )
+		sc.MustGetToken( TK_IntConst );
+
+	return sc.Float;
+}
+
+//
+// [TP] Parse a single VOTEDEF lump.
+//
+void VOTEDEF_ParseLump( int lump )
+{
+	FScanner sc( lump );
+
+	while ( sc.GetToken() )
+	{
+		bool isnative = false;
+		NativeVoteType native = VOTETYPE_NumNatives;
+
+		// [TP] 'votetype' is the only command we have right now.
+		if ( sc.Compare( "VOTETYPE" ) == false )
+			sc.ScriptError( "Expected `votetype`, got %s", sc.String );
+
+		// [TP] See if this vote type is native.
+		if ( sc.CheckToken( TK_Native ))
+			isnative = true;
+
+		sc.MustGetAnyToken();
+		FString votename = sc.String;
+		FString forbidCVarName;
+
+		if ( isnative )
+			native = NativeVoteType( sc.MustMatchString( g_NativeNames ));
+
+		// [TP] Check that it's not already defined
+		if ( VOTEDEF_FindTypeByName( votename ))
+			sc.ScriptError( "Vote type `%s` is already defined", votename.GetChars() );
+
+		VoteClass* type = new VoteClass( votename, native );
+
+		bool gotMenuRange = false;
+		sc.MustGetToken( '{' );
+
+		while ( sc.CheckToken( '}' ) == false )
+		{
+			// [TP] The file shouldn't end in the middle of a block
+			if ( sc.GetToken() == false )
+				sc.ScriptError( "Unexpected end of file, did you miss a '}'?" );
+
+			// [TP] Try flags
+			int flagindex;
+			if (( flagindex = sc.MatchString( g_FlagTokens )) != -1 )
+			{
+				sc.MustGetToken( ';' );
+				type->_flags |= ( 1 << flagindex );
+				continue;
+			}
+
+			// [TP] Try properties
+			int prop = sc.MustMatchString( g_PropertyTokens );
+			sc.MustGetToken( '=' );
+
+			switch ( VotedefProperty( prop ))
+			{
+			case VTPROP_Argument:
+				sc.MustGetAnyToken();
+				type->_argumentType = VoteArgumentType( sc.MustMatchString( g_ParameterTokens ));
+
+				// [TP] Int and float types must have a value range.
+				if (( type->argumentType() == VOTEARG_Int )
+					|| ( type->argumentType() == VOTEARG_Float ))
+				{
+					sc.MustGetToken( '(' );
+					type->_minimum = VOTEDEF_GetNumeric( sc );
+					sc.MustGetToken( ',' );
+					type->_maximum = VOTEDEF_GetNumeric( sc );
+					sc.MustGetToken( ')' );
+				}
+				break;
+
+			case VTPROP_ForbidCVar:
+				sc.MustGetAnyToken();
+				forbidCVarName = sc.String;
+				break;
+
+			case VTPROP_ScriptNumber:
+				sc.MustGetToken( TK_IntConst );
+				type->_scriptNumber = sc.Number;
+
+				if ( type->scriptNumber() != clamp( type->scriptNumber(), 1, 999 ) )
+					sc.ScriptError( "Script number must be between values 1 and 999." );
+				break;
+
+			case VTPROP_Description:
+				sc.MustGetAnyToken();
+				type->_description = sc.String;
+				break;
+
+			case VTPROP_ArgumentLabel:
+				sc.MustGetToken( TK_StringConst );
+				type->_argumentLabel = sc.String;
+				break;
+
+			case VTPROP_MenuRange:
+				type->_menuMinimum = VOTEDEF_GetNumeric( sc );
+				sc.MustGetToken( ',' );
+				type->_menuMaximum = VOTEDEF_GetNumeric( sc );
+				gotMenuRange = true;
+				break;
+
+			case VTPROP_MenuIncrement:
+				type->_menuIncrement = VOTEDEF_GetNumeric( sc );
+				break;
+			}
+
+			sc.MustGetToken( ';' );
+		}
+
+		if ( type->isNative() == false
+			&& type->_scriptNumber == 0 )
+		{
+			sc.ScriptError( "Non-native vote type '%s' needs a script number.",
+				type->_name.GetChars() );
+		}
+
+		// [TP] Generate the forbid CVar name if it's not given.
+		if ( forbidCVarName.IsEmpty() )
+			forbidCVarName = "sv_no" + type->name() + "vote";
+
+		// [TP] If the CVar does not exist, create it now.
+		if (( type->_forbidCVar = FindCVar( forbidCVarName, NULL )) == NULL )
+		{
+			FBoolCVar* var = new FBoolCVar( forbidCVarName, false, CVAR_AUTO | CVAR_ARCHIVE );
+			var->SetArchiveBit();
+			type->_forbidCVar = var;
+		}
+
+		// [TP] Put a colon after the argument label if it's not empty.
+		// If it is empty, we want it to be an empty string.
+		if ( type->_argumentLabel.IsNotEmpty() )
+			type->_argumentLabel += ":";
+
+		// [TP] Fill in default menu range if not explicitly given
+		if ( gotMenuRange == false )
+		{
+			type->_menuMinimum = type->_minimum;
+			type->_menuMaximum = type->_maximum;
+		}
+
+		// [TP] If we use integral values, ensure the numbers given aren't floating point
+		if ( type->argumentType() == VOTEARG_Int )
+		{
+			const double epsilon = 0.0000000001;
+			float values[] = { type->minimum(), type->maximum(), type->menuMinimum(),
+				type->menuMaximum(), type->menuIncrement() };
+			const char* valueNames[] = { "Minimum", "Maximum", "Menu minimum",
+				"Menu maximum", "Menu increment" };
+
+			for( int i = 0; i < int( sizeof values / sizeof *values ); ++i )
+			{
+				if ( fabsf( values[i] - floor( values[i] )) > epsilon )
+				{
+					sc.ScriptError( "%s of vote type '%s' is not an integer",
+						valueNames[i], type->name().GetChars() );
+				}
+			}
+		}
+
+		g_Defs.Push( type );
+	}
+}
+
+//
+// [TP] An error function for VOTEDEF_Validate
+//
+static void STACK_ARGS VOTEDEF_ValidateError( const char* fmtstr, ... ) GCCPRINTF( 1, 2 );
+static void STACK_ARGS VOTEDEF_ValidateError( const char* fmtstr, ... )
+{
+	va_list va;
+	char errortext[1024];
+	va_start( va, fmtstr );
+	vsprintf( errortext, fmtstr, va );
+	va_end( va );
+	throw FString( errortext );
+}
+
+//
+// [TP] Apply given input data, performs validity checks and compiles some data
+// needed for vote execution.
+//
+bool VOTEDEF_Validate( VoteStateData const& input, VoteValidateResult& result )
+{
+	try
+	{
+		VoteClass* votetype = input.type;
+
+		if ( votetype == NULL )
+			VOTEDEF_ValidateError( "a bad vote class was given" );
+
+		// [TP] Reset the result
+		result.error = "";
+		result.stringArgument = "";
+		result.argument = 0;
+		result.floatArgument = 0.0;
+
+		// [TP] Check that we got the proper parameter
+		if ( votetype->hasArgument() )
+		{
+			if ( input.arg.IsEmpty() )
+				VOTEDEF_ValidateError( "the vote argument was not given" );
+
+			// [TP] The variable contens in various types
+			DWORD valueAsIntegral = atol( input.arg );
+			double valueAsDouble = atof( input.arg );
+			int idx = 0;
+
+			switch ( votetype->argumentType() )
+			{
+			case VOTEARG_Int:
+				if (( input.arg.IsInt() == false )
+					|| ( valueAsIntegral < DWORD( votetype->minimum() ))
+					|| ( valueAsIntegral > DWORD( votetype->maximum() )))
+				{
+					VOTEDEF_ValidateError( "%s is not an integral number between %d and %d",
+						input.arg.GetChars(), DWORD( votetype->minimum() ), DWORD( votetype->maximum() ));
+				}
+
+				result.argument = valueAsIntegral;
+				break;
+
+			case VOTEARG_Float:
+				if (( input.arg.IsFloat() == false )
+					|| ( valueAsDouble < votetype->minimum() )
+					|| ( valueAsDouble > votetype->maximum() ))
+				{
+					VOTEDEF_ValidateError( "%s is not a number between %f and %f",
+						input.arg.GetChars(), votetype->minimum(), votetype->maximum() );
+				}
+
+				// [TP] The argument needs to be converted to fixed point
+				result.argument = valueAsDouble * FRACUNIT;
+				result.floatArgument = valueAsDouble;
+				break;
+
+			case VOTEARG_Player:
+				// [TP] Only need to test the player index of the target address isn't already known
+				if (( votetype->nativeType() != VOTETYPE_Kick ) || ( input.targetAddressKnown == false ))
+				{
+					// [TP] Find the client by name, unless PLAYERINDEX is given
+					if ( votetype->flags() & VOTEF_PlayerAsIndex )
+					{
+						if ( input.arg.IsInt() == false )
+							VOTEDEF_ValidateError( "'%s' is not a valid player index", input.arg.GetChars() );
+
+						idx = valueAsIntegral;
+					}
+					else
+					{
+						idx = SERVER_GetPlayerIndexFromName( input.arg, true, false );
+					}
+
+					// [TP] Must be a valid player
+					if ( SERVER_IsValidClient( idx ) == false )
+						VOTEDEF_ValidateError( "there is no player #%d", idx );
+
+					// [TP] Check if this is to be done on spectators
+					if (( votetype->flags() & VOTEF_NotOnSpectators ) && ( players[idx].bSpectating ))
+						VOTEDEF_ValidateError( "%s is a spectator", players[idx].userinfo.netname );
+
+					// [BB] Don't allow anyone to kick somebody who is on the admin list.
+					if (( votetype->flags() & VOTEF_NotOnAdmin ) && ( VOTEDEF_IsPlayerAdmin( idx )))
+						VOTEDEF_ValidateError( "you may not do this on %s", players[idx].userinfo.netname );
+
+					// [TP] Not self if NOTSELF is set
+					if (( votetype->flags() & VOTEF_NotOnSelf ) && ( idx == input.caller ))
+						VOTEDEF_ValidateError( "you are %s!", players[idx].userinfo.netname );
+				}
+
+				result.argument = idx;
+				break;
+
+			case VOTEARG_Level:
+				if ( P_CheckIfMapExists( input.arg ) == false )
+					VOTEDEF_ValidateError( "there is no such map '%s'", input.arg.GetChars() );
+
+				if (( votetype->flags() & VOTEF_MapInRotation )
+					&& ( NETWORK_GetState() == NETSTATE_SERVER )
+					&& ( sv_maprotation )
+					&& ( MAPROTATION_IsMapInRotation( input.arg ) == false ))
+				{
+					VOTEDEF_ValidateError( "the map '%s' is not in rotation", input.arg.GetChars() );
+				}
+
+				result.stringArgument = input.arg;
+				break;
+
+			case VOTEARG_String:
+				if ( input.arg.Len() >= MAX_NETWORK_STRING )
+					VOTEDEF_ValidateError( "the argument is too long" );
+
+				result.stringArgument = input.arg;
+				break;
+
+			case VOTEARG_NumArgTypes:
+				break;
+			}
+		}
+
+		// [TP] Check that we won't run a client-side script as a result of this vote.
+		if ( votetype->isNative() == false )
+		{
+			FBehavior* module = NULL;
+			const ScriptPtr* scriptdata = FBehavior::StaticFindScript( votetype->scriptNumber(), module );
+
+			if (( scriptdata != NULL ) && ( scriptdata->Flags & SCRIPTF_ClientSide ))
+				VOTEDEF_ValidateError( "the vote would call the client-side script %d", votetype->scriptNumber() );
+		}
+	}
+	catch ( FString& err )
+	{
+		result.error = err;
+		return false;
+	}
+
+	return true;
+}
+
+//
+// [TP] Make a record of this vote for later comparing
+//
+FString VOTEDEF_MakeVoteSummary( const VoteStateData& input )
+{
+	FString record = input.type->name() + " ";
+
+	switch ( input.type->argumentType() )
+	{
+		case VOTEARG_Int:
+		case VOTEARG_Float:
+		case VOTEARG_Level:
+			record += input.arg;
+			break;
+
+		case VOTEARG_Player:
+			// [TP] Store the IP address of this player was this a kick vote or not,
+			// as this way we can have something unique to the player.
+			// This way a player index vote will not fail just because someone
+			// left and another person took his index.
+			record += NETWORK_AddressToStringIgnorePort( input.targetAddress );
+			break;
+
+		default:
+			break;
+	}
+
+	return record;
+}
+
+//
+// [TP] Get usage info of calling this vote
+//
+FString VoteClass::usage() const
+{
+	FString cmd = "callvote " + name();
+
+	if ( hasArgument() )
+	{
+		FString arg = g_ParameterTokens[argumentType()];
+		arg.ToLower();
+		cmd.AppendFormat( " <%s>", arg.GetChars() );
+	}
+
+	cmd += " [reason]";
+	return cmd;
+}
+
+//
+// [TP] Find a vote type by index number
+//
+VoteClass* VOTEDEF_FindTypeByIndex( int index )
+{
+	if (( index < 0 ) || ( index >= int( g_Defs.Size() )))
+		return NULL;
+
+	return g_Defs[index];
+}
+
+//
+// [TP] Find a vote type by name
+//
+VoteClass* VOTEDEF_FindTypeByName( FString name )
+{
+	for ( int i = 0; i < int( g_Defs.Size() ); i++ )
+	{
+		if ( g_Defs[i]->name().CompareNoCase( name ) == 0 )
+			return g_Defs[i];
+	}
+
+	return NULL;
+}
+
+int VOTEDEF_NumVoteTypes()
+{
+	return g_Defs.Size();
+}
+
+//
+// [TP] Is the given player in the adminlist?
+//
+bool VOTEDEF_IsPlayerAdmin( int idx )
+{
+	NETADDRESS_s address = SERVER_GetClient( idx )->Address;
+	return SERVER_GetAdminList()->isIPInList( address );
+}
+
+//
+// [TP] Gets a comma-delimeted list of all vote type names
+//
+FString VOTEDEF_GetVoteTypeList()
+{
+	FString result;
+
+	for ( int i = 0; i < VOTEDEF_NumVoteTypes(); i++ )
+	{
+		if ( i != 0 )
+			result += ", ";
+
+		VoteClass* votetype = VOTEDEF_FindTypeByIndex( i );
+		result += votetype->name();
+	}
+
+	return result;
+}
+
+//
+// [TP] List all vote types and information on how to use them.
+//
+CCMD( listvotetypes )
+{
+	for ( int i = 0; i < VOTEDEF_NumVoteTypes(); i++ )
+	{
+		VoteClass* votetype = VOTEDEF_FindTypeByIndex( i );
+		FString message, description;
+		message.Format( "\\c[Orange]%s\\c-", votetype->usage().GetChars() );
+
+		// [TP] Add the description, if we have it.
+		if (( description = votetype->description() ).IsNotEmpty() )
+			message.AppendFormat( ": %s", description.GetChars() );
+
+		Printf( "%s\n", message.GetChars() );
+	}
+}
diff --git a/src/callvotetype.h b/src/callvotetype.h
new file mode 100644
--- /dev/null
+++ b/src/callvotetype.h
@@ -0,0 +1,282 @@
+/*
+ * Zandronum source code
+ * Copyright (C) 2012-2014 Teemu Piippo
+ * Copyright (C) 2012-2014 Zandronum Development Team
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 3. Neither the name of the Skulltag Development Team nor the names of its
+ *    contributors may be used to endorse or promote products derived from this
+ *    software without specific prior written permission.
+ * 4. Redistributions in any form must be accompanied by information on how to
+ *    obtain complete source code for the software and any accompanying
+ *    software that uses the software. The source code must either be included
+ *    in the distribution or be available for no more than the cost of
+ *    distribution plus a nominal fee, and must be freely redistributable
+ *    under reasonable conditions. For an executable file, complete source
+ *    code means the source code for all modules it contains. It does not
+ *    include source code for modules or files that typically accompany the
+ *    major components of the operating system on which the executable file
+ *    runs.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Filename: callvotetype.h
+ *
+ * Description: VOTEDEF declarations
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+#ifndef VOTEDEF_H
+#define VOTEDEF_H
+
+#include "zstring.h"
+#include "basictypes.h"
+#include "networkshared.h"
+#include "doomdef.h"
+
+class FBaseCVar;
+class VoteClass;
+
+enum NativeVoteType
+{
+	VOTETYPE_Kick,
+	VOTETYPE_ForceSpec,
+	VOTETYPE_ChangeMap,
+	VOTETYPE_Map,
+	VOTETYPE_FragLimit,
+	VOTETYPE_TimeLimit,
+	VOTETYPE_PointLimit,
+	VOTETYPE_DuelLimit,
+	VOTETYPE_WinLimit,
+
+	VOTETYPE_NumNatives, // i.e. not native
+};
+
+//
+// Vote parameter types
+//
+enum VoteArgumentType
+{
+	VOTEARG_Int,
+	VOTEARG_Float,
+	VOTEARG_Player,
+	VOTEARG_Level,
+	VOTEARG_String,
+
+	VOTEARG_NumArgTypes, // i.e. no argument
+};
+
+//
+// Flags for vote classes
+//
+enum
+{
+	// Vote can be called without delay as long as it passes
+	VOTEF_NoLimitIfPassed	= ( 1 << 0 ),
+
+	// Player argument must not be the caller
+	VOTEF_NotOnSelf			= ( 1 << 1 ),
+
+	// Player argument must not be an admin
+	VOTEF_NotOnAdmin		= ( 1 << 2 ),
+
+	// Player argument is given as an index instead of a name
+	VOTEF_PlayerAsIndex		= ( 1 << 3 ),
+
+	// Player given must be in game
+	VOTEF_NotOnSpectators	= ( 1 << 4 ),
+
+	// Use a slider in the call vote menu (int and float only)
+	VOTEF_MenuSlider		= ( 1 << 5 ),
+
+	// If a map argument, map must be in rotation
+	VOTEF_MapInRotation		= ( 1 << 6 ),
+
+	VOTEF_NumFlags			=        7
+};
+
+//
+// A bunch of data for the state of a vote
+//
+struct VoteStateData
+{
+	VoteClass* type;			// Type of vote (note: only is guaranteed non-NULL if validate passes)
+	FString arg;				// Vote argument
+	FString reason;				// Reason string
+	int yes;					// Count of yes votes
+	int no;						// Count of no votes
+	int caller;					// Index of vote caller
+	NETADDRESS_s targetAddress;	// For a vote with a player argument, what's the target address?
+	bool targetAddressKnown;	// Is the targetAddress valid?
+
+	VoteStateData() :
+		type( NULL ),
+		yes( 0 ),
+		no( 0 ),
+		targetAddressKnown( false ) {}
+};
+
+//
+// Structure for result from VOTEDEF_Validate
+//
+struct VoteValidateResult
+{
+	FString command;		// Resulting command string (for natives)
+	FString error;			// Failed? Why?
+	DWORD argument;			// Processed argument
+	FString stringArgument;	// For a string or map argument, what's the string?
+	double floatArgument;	// For a float argument, what's the floating-point value?
+};
+
+//
+// A type of vote
+//
+class VoteClass
+{
+	FString				_name;						// Name of the type
+	int					_scriptNumber;				// Script number to execute
+	DWORD				_flags;						// Flags of this vote
+	VoteArgumentType	_argumentType;				// Argument type
+	FString				_argumentLabel;				// Label of the argument in menus
+	FString				_description;				// Description string
+	FBaseCVar*			_forbidCVar;				// CVar that forbids the use of this vote
+	float				_minimum;					// Minimum input value
+	float				_maximum;					// Maximum input value
+	NativeVoteType		_native;					// Native vote type, these have special behavior.
+	int					_index;						// The index of this type
+	float				_menuMinimum;				// Minimum value for menus
+	float				_menuMaximum;				// Maximum value for menus
+	float				_menuIncrement;				// Increment value for menus
+
+	friend void VOTEDEF_ParseLump( int );
+
+public:
+	VoteClass( const FString& name, NativeVoteType native );
+	~VoteClass();
+
+	inline VoteArgumentType	argumentType() const;
+	const FString&			argumentLabel() const;
+	inline const FString&	description() const;
+	inline DWORD			flags() const;
+	inline FBaseCVar*		forbidCVar() const;
+	inline bool				hasArgument() const;
+	inline int				index() const;
+	inline bool				isNative() const;
+	FString					usage() const;
+	inline float			maximum() const;
+	inline float			menuIncrement() const;
+	inline float			menuMaximum() const;
+	inline float			menuMinimum() const;
+	inline float			minimum() const;
+	inline const FString&	name() const;
+	inline NativeVoteType	nativeType() const;
+	inline int				scriptNumber() const;
+};
+
+void			VOTEDEF_Construct();
+void			VOTEDEF_Destruct();
+FString			VOTEDEF_GetVoteTypeList();
+VoteClass*		VOTEDEF_FindTypeByIndex( int index );
+VoteClass*		VOTEDEF_FindTypeByName( FString name );
+FString			VOTEDEF_MakeVoteSummary( VoteStateData const& input );
+int				VOTEDEF_NumVoteTypes();
+void			VOTEDEF_ParseLump( int lump );
+bool			VOTEDEF_IsPlayerAdmin( int idx );
+bool			VOTEDEF_Validate( const VoteStateData& input, VoteValidateResult& result );
+
+inline int VoteClass::index() const
+{
+	return _index;
+}
+
+inline int VoteClass::scriptNumber() const
+{
+	return _scriptNumber;
+}
+
+inline NativeVoteType VoteClass::nativeType() const
+{
+	return _native;
+}
+
+inline const FString& VoteClass::name() const
+{
+	return _name;
+}
+
+inline VoteArgumentType VoteClass::argumentType() const
+{
+	return _argumentType;
+}
+
+inline DWORD VoteClass::flags() const
+{
+	return _flags;
+}
+
+inline FBaseCVar* VoteClass::forbidCVar() const
+{
+	return _forbidCVar;
+}
+
+inline float VoteClass::minimum() const
+{
+	return _minimum;
+}
+
+inline float VoteClass::maximum() const
+{
+	return _maximum;
+}
+
+inline const FString& VoteClass::description() const
+{
+	return _description;
+}
+
+inline bool VoteClass::isNative() const
+{
+	return _native != VOTETYPE_NumNatives;
+}
+
+inline bool VoteClass::hasArgument() const
+{
+	return _argumentType != VOTEARG_NumArgTypes;
+}
+
+inline const FString& VoteClass::argumentLabel() const
+{
+	return _argumentLabel;
+}
+
+inline float VoteClass::menuMinimum() const
+{
+	return _menuMinimum;
+}
+
+inline float VoteClass::menuMaximum() const
+{
+	return _menuMaximum;
+}
+
+inline float VoteClass::menuIncrement() const
+{
+	return _menuIncrement;
+}
+
+#endif // VOTEDEF_H
diff --git a/src/cl_main.cpp b/src/cl_main.cpp
--- a/src/cl_main.cpp
+++ b/src/cl_main.cpp
@@ -9997,7 +9997,7 @@
 //
 static void client_CallVote( BYTESTREAM_s *pByteStream )
 {
-	FString		command;
+	ULONG		ulCommand;
 	FString		parameters;
 	FString		reason;
 	ULONG		ulVoteCaller;
@@ -10006,7 +10006,7 @@
 	ulVoteCaller = NETWORK_ReadByte( pByteStream );
 
 	// Read in the command.
-	command = NETWORK_ReadString( pByteStream );
+	ulCommand = NETWORK_ReadLong( pByteStream );
 
 	// Read in the parameters.
 	parameters = NETWORK_ReadString( pByteStream );
@@ -10015,7 +10015,7 @@
 	reason = NETWORK_ReadString( pByteStream );
 
 	// Begin the vote!
-	CALLVOTE_BeginVote( command, parameters, reason, ulVoteCaller );
+	CALLVOTE_BeginVote( ulCommand, parameters, reason, ulVoteCaller );
 }
 
 //*****************************************************************************
diff --git a/src/d_main.cpp b/src/d_main.cpp
--- a/src/d_main.cpp
+++ b/src/d_main.cpp
@@ -147,6 +147,7 @@
 #include "g_shared/pwo.h"
 
 #include "win32/g15/g15.h"
+#include "callvotetype.h"
 EXTERN_CVAR(Bool, hud_althud)
 void DrawHUD();
 
@@ -2737,6 +2738,9 @@
 	// [BB] At the moment Skulltag still doesn't use the new ZDoom TeamLibrary class.
 	TEAMINFO_Init ();
 
+	// [Dusk] Initialize vote definitions
+	VOTEDEF_Construct();
+
 	FActorInfo::StaticInit ();
 
 	// [GRB] Initialize player class list
diff --git a/src/g_level.cpp b/src/g_level.cpp
--- a/src/g_level.cpp
+++ b/src/g_level.cpp
@@ -261,7 +261,6 @@
 //
 //
 //==========================================================================
-
 CCMD (map)
 {
 	if (argv.argc() > 1)
diff --git a/src/m_menu.h b/src/m_menu.h
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -142,6 +142,7 @@
 	browserslot,
 	txslider,
 	mnnumber,
+	votetype, // [Dusk]
 
 } itemtype;
 
diff --git a/src/m_options.cpp b/src/m_options.cpp
--- a/src/m_options.cpp
+++ b/src/m_options.cpp
@@ -115,6 +115,7 @@
 
 // [ZZ] PWO header file
 #include "g_shared/pwo.h"
+#include "callvotetype.h"
 
 // EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
 
@@ -201,6 +202,12 @@
 static	LONG	g_lSavedColor;
 static	bool	g_bSwitchColorBack;
 
+// [BB]
+static TArray<valuestring_t> SkirmishLevels;
+
+// [BB]
+void InitSkirmishLevelList();
+
 value_t YesNo[2] = {
 	{ 0.0, "No" },
 	{ 1.0, "Yes" }
@@ -1827,8 +1834,10 @@
 	if ( !bAllowBots && ( players[lPlayer].bIsBot ))
 		return ( false );
 
+/*
 	if ( lPlayer == consoleplayer )
 		return ( false );
+*/
 
 	return ( true );
 }
@@ -1946,221 +1955,26 @@
 
 //====================================================================================
 //
-// Call Vote-->Kick Player Menu
-//
-//====================================================================================
-
-CVAR( String, menu_votereason, "", 0 );
-
-void kickplayermenu_Kick( void );
-
-//*****************************************************************************
-//
-static menuitem_t kickplayermenu_Items[] =
-{
-	{ discretes,"Player",			{&menu_playerslider_idx},		   	{1.0}, {0.0},	{0.0}, {NULL} },
-	{ redtext,	" ",				{NULL},					{0.0}, {0.0},	{0.0}, {NULL} },
-	{ string,	"Reason for kicking:",{&menu_votereason},			{0.0}, {0.0},	{0.0}, {NULL} },
-	{ more,		"Kick!",			{NULL},							{0.0}, {0.0},	{0.0}, {(value_t *)kickplayermenu_Kick} },
-};
-
-// [BB, RC] Line number of the "Player:" entry from kickplayermenu_Items. If the line number is changed, the value has to be adjusted.
-#define KICKPLAYER_SLIDER_LOCATION 0
-
-//*****************************************************************************
-//
-menu_t KickPlayerMenu = {
-	"KICK A PLAYER",
-	0,
-	countof(kickplayermenu_Items),
-	0,
-	kickplayermenu_Items,
-	0,
-	0,
-};
-
-//*****************************************************************************
-//
-void kickplayermenu_Kick( void )
-{
-	// Clean the name of color codes.
-	FString Name = AvailablePlayers[menu_playerslider_idx].name.GetChars( );
-	V_RemoveColorCodes( Name );
-	V_EscapeBacklashes( Name );
-
-	FString Reason = menu_votereason.GetGenericRep( CVAR_String ).String;
-	V_EscapeBacklashes( Reason );
-
-	// Execute the command.
-	char	szString[256];
-	sprintf( szString, "callvote kick \"%s\" \"%s\"", Name.Left( 96 ).GetChars(), Reason.Left( 25 ).GetChars());
-	AddCommandString( szString );
-	M_ClearMenus( );
-}
-
-//*****************************************************************************
-//
-void kickplayermenu_Show( void )
-{
-	if ( SERVER_CountPlayers( false ) < 2 )
-	{
-		M_ClearMenus( );
-		M_StartMessage( "There is nobody else here to kick!\n\npress any key.", NULL );
-		return;
-	}
-
-	// Set up the player selection slider.
-	playerslider_BuildList( false );
-	kickplayermenu_Items[KICKPLAYER_SLIDER_LOCATION].b.numvalues = static_cast<float>(AvailablePlayers.Size());
-	kickplayermenu_Items[KICKPLAYER_SLIDER_LOCATION].e.valuestrings = &AvailablePlayers[0];
-
-	M_SwitchMenu( &KickPlayerMenu );
-}
-
-//====================================================================================
-//
-// Call Vote-->Map Menu
-//
-//====================================================================================
-
-CVAR( String, menu_mapvotemap, "", 0 );
-CVAR( Bool, menu_mapvoteintermission, false, 0 );
-
-//*****************************************************************************
-//
-void mapvotemenu_Vote( void )
-{
-	// Sanitize the inputs.
-	FString		Map = menu_mapvotemap.GetGenericRep( CVAR_String ).String;
-	FString		Reason = menu_votereason.GetGenericRep( CVAR_String ).String;
-	V_EscapeBacklashes( Map );
-	V_EscapeBacklashes( Reason );
-
-	if ( !Map.Len( ) )
-	{
-		Printf( "You didn't specify a map!\n" );
-		return;
-	}
-
-	// Execute the command.
-	char		szString[256];
-	sprintf( szString, "callvote %s %s \"%s\"", ( menu_mapvoteintermission.GetGenericRep( CVAR_Bool ).Bool ? "changemap" : "map" ), Map.Left( 128 ).GetChars(), Reason.Left( 25 ).GetChars() );
-	AddCommandString( szString );
-	M_ClearMenus( );
-}
-
-//*****************************************************************************
-//
-static menuitem_t mapvotemenu_Items[] =
-{
-	{ string,	"Map",				{&menu_mapvotemap},				{0.0}, {0.0},	{0.0}, {NULL} },
-	{ discrete, "Intermission",		{&menu_mapvoteintermission},	{2.0}, {0.0},	{0.0}, {YesNo} },	
-	{ redtext,	" ",				{NULL},							{0.0}, {0.0},	{0.0}, {NULL} },
-	{ string,	"Reason for change:",{&menu_votereason},			{0.0}, {0.0},	{0.0}, {NULL} },
-	{ more,		"Vote!",			{NULL},							{0.0}, {0.0},	{0.0}, {(value_t *)mapvotemenu_Vote} },
-};
-
-//*****************************************************************************
-//
-menu_t MapVoteMenu = {
-	"CHANGE MAP",
-	0,
-	countof(mapvotemenu_Items),
-	0,
-	mapvotemenu_Items,
-	0,
-	0,
-};
-
-//*****************************************************************************
-//
-void mapvotemenu_Show( void )
-{
-	M_SwitchMenu( &MapVoteMenu );
-}
-
-//====================================================================================
-//
-// Call Vote-->Limit Menu
-//
-//====================================================================================
-
-//*****************************************************************************
-//
-value_t limitvote_Types[5] = {
-	{ 0.0, "fraglimit" },
-	{ 1.0, "timelimit" },
-	{ 2.0, "winlimit" },
-	{ 3.0, "duellimit" },
-	{ 4.0, "pointlimit" }
-};
-
-//*****************************************************************************
-//
-CVAR( Int, menu_limitvote_type, 0, 0 );
-CVAR( String, menu_limitvote_value, "", 0 );
-
-//*****************************************************************************
-//
-void limitvotemenu_Vote( void )
-{
-	// Sanitize the inputs.
-	int			iVoteType = menu_limitvote_type.GetGenericRep( CVAR_Int ).Int;
-	int			iLimit =  atoi( menu_limitvote_value.GetGenericRep( CVAR_String ).String );
-	FString		Reason = menu_votereason.GetGenericRep( CVAR_String ).String;
-	V_EscapeBacklashes( Reason );
-
-	if ( iVoteType >= 5 || iVoteType < 0 )
-		return;
-
-	// Execute the command.
-	char		szString[512];
-	sprintf( szString, "callvote %s %d \"%s\"", limitvote_Types[iVoteType].name, iLimit, Reason.Left( 25 ).GetChars() );
-	AddCommandString( szString );
-	M_ClearMenus( );
-}
-
-//*****************************************************************************
-//
-static menuitem_t limitvotemenu_Items[] =
-{
-	{ discrete,	"Type of limit",	{&menu_limitvote_type},			{5.0}, {0.0},	{0.0}, {limitvote_Types} },
-	{ string,	"New value",		{&menu_limitvote_value},		{0.0}, {0.0},	{0.0}, {NULL} },
-	{ redtext,	" ",				{NULL},							{0.0}, {0.0},	{0.0}, {NULL} },
-	{ string,	"Reason for change:",{&menu_votereason},			{0.0}, {0.0},	{0.0}, {NULL} },	
-	{ more,		"Vote!",			{NULL},							{0.0}, {0.0},	{0.0}, {(value_t *)limitvotemenu_Vote} },
-};
-
-//*****************************************************************************
-//
-menu_t limitvoteMenu = {
-	"CHANGE LIMIT",
-	0,
-	countof(limitvotemenu_Items),
-	0,
-	limitvotemenu_Items,
-	0,
-	0,
-};
-
-//*****************************************************************************
-//
-void limitvotemenu_Show( void )
-{
-	M_SwitchMenu( &limitvoteMenu );
-}
-
-//====================================================================================
-//
 // Call Vote Menu
 //
 //====================================================================================
 
+// [TP]
+CVAR( Int, menu_votetype, 0, 0 )
+CVAR( Int, menu_voteargument, 0, 0 )
+CVAR( Float, menu_votefloatargument, 0, 0 )
+CVAR( String, menu_votestringargument, "", 0 )
+CVAR( String, menu_votereason, "", 0 )
+static void M_VoteTypeChanged( bool first );
+static void votemenu_go();
+
 static menuitem_t CallVoteItems[] =
 {
-	{ more,		"Kick a player",			{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *)kickplayermenu_Show} },
-	{ more,		"Change the map",			{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *)mapvotemenu_Show} },
-	{ more,		"Change a limit",			{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *)limitvotemenu_Show} },
+	{ votetype,	"Vote action",		{&menu_votetype},		{0.0}, {100.0}, {1.0}, {NULL} },
+	{ string,	"<dynamic text>",	{&menu_voteargument},		{0.0}, {0.0},	{0.0}, {NULL} },
+	{ string,	"Reason for vote",	{&menu_votereason},		{0.0}, {0.0},	{0.0}, {NULL} },
+	{ redtext,	" ",				{NULL},					{0.0}, {0.0},	{0.0}, {NULL} },
+	{ more,		"Call the vote",	{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *) votemenu_go} },
 };
 
 menu_t CallVoteMenu = {
@@ -2173,6 +1987,182 @@
 	0,
 };
 
+//*****************************************************************************
+//
+void M_CallVote( void )
+{
+	// Don't allow a vote unless the player is a client.
+	if (( NETWORK_GetState( ) != NETSTATE_CLIENT )
+		|| ( CLIENT_GetConnectionState() != CTS_ACTIVE ))
+	{
+		M_ClearMenus();
+		M_StartMessage( "You must be in a multiplayer game to call a vote.\n\npress any key.", NULL );
+		return;
+	}
+
+	// [TP] Init the skirmish level list as we're using it for map votes
+	InitSkirmishLevelList();
+	M_VoteTypeChanged( true );
+	M_SwitchMenu( &CallVoteMenu );
+}
+
+//*****************************************************************************
+//
+// [TP] Hmm. Now that votes are generalized the argument passed is of variable
+// type. Thus, we have to change the argument field dynamically based on the type
+// of the argument of the vote we scrolled to.
+// 
+// This also resets the argument, first because the argument of one type may be
+// out of bounds or otherwise be of completely different meaning for another.
+//
+static void M_VoteTypeChanged( bool first )
+{
+	VoteClass* def = VOTEDEF_FindTypeByIndex( menu_votetype );
+
+	if ( def == NULL )
+		return;
+
+	VoteArgumentType argtype = def->argumentType();
+	menuitem_t* item = &CallVoteItems[1];
+	item->label = def->argumentLabel().GetChars();
+	
+	switch ( argtype )
+	{
+	case VOTEARG_Player:
+		// Set up the player selection slider.
+		playerslider_BuildList( false );
+		item->type = discretes;
+		item->a.intcvar = &menu_voteargument;
+		item->b.numvalues = float( AvailablePlayers.Size() );
+		item->e.valuestrings = &AvailablePlayers[0];
+
+		if ( item->label[0] == '\0' )
+			item->label = "Player";
+
+		if ( first == false )
+			menu_voteargument = AvailablePlayers[0].value;
+		break;
+
+	case VOTEARG_Int:
+	case VOTEARG_Float:
+		item->type = ( def->flags() & VOTEF_MenuSlider ) ? slider : number;
+		item->a.cvar = &menu_votefloatargument;
+		item->b.min = def->menuMinimum();
+		item->c.max = def->menuMaximum();
+		item->d.step = def->menuIncrement();
+		item->e.values = NULL;
+
+		if ( item->label[0] == '\0' )
+			item->label = "Value";
+
+		if ( first == false )
+			menu_votefloatargument = def->minimum();
+		break;
+
+	case VOTEARG_Level:
+		item->type = discretes;
+		item->a.intcvar = &menu_voteargument;
+		item->b.numvalues = float( SkirmishLevels.Size() );
+		item->e.valuestrings = ( SkirmishLevels.Size() > 0 ) ? &SkirmishLevels[0] : NULL;
+
+		if ( item->label[0] == '\0' )
+			item->label = "Level:";
+
+		if ( first == false )
+			menu_voteargument = 0;
+		break;
+
+	case VOTEARG_String:
+		item->type = string;
+		item->a.stringcvar = &menu_votestringargument;
+		item->b.min = 0.0f;
+		item->c.max = 0.0f;
+		item->d.step = 0.0f;
+		item->e.values = NULL;
+		break;
+
+	case VOTEARG_NumArgTypes:
+		item->type = redtext;
+		item->label = "";
+		break;
+	}
+}
+
+static void votemenu_go()
+{
+	VoteClass* def = VOTEDEF_FindTypeByIndex( menu_votetype );
+
+	if ( def == NULL )
+		return;
+
+	VoteStateData in;
+	VoteValidateResult out;
+	in.type = VOTEDEF_FindTypeByIndex( menu_votetype );
+	in.caller = consoleplayer;
+
+	// [TP] Determine the argument to callvote.
+	if ( def->hasArgument() )
+	{
+		if ( def->argumentType() == VOTEARG_Level )
+		{
+			// [TP] If this is a map, we need to pass the string argument
+			in.arg = wadlevelinfos[menu_voteargument].mapname;
+		}
+		else if (( def->argumentType() == VOTEARG_Player )
+			&& (( def->flags() & VOTEF_PlayerAsIndex ) == 0 ))
+		{
+			// [TP] A player's name is wanted - pass that.
+			in.arg = players[menu_voteargument].userinfo.netname;
+			V_ColorizeString( in.arg );
+			V_RemoveColorCodes( in.arg );
+		}
+		else if ( def->argumentType() == VOTEARG_Float )
+		{
+			in.arg.Format( "%f", float( menu_votefloatargument ));
+
+			// Remove trailing zeroes
+			if ( in.arg.IndexOf( "." ) != -1 )
+			{
+				while ( in.arg[in.arg.Len() - 1] == '0' )
+					in.arg = FString( in.arg.GetChars(), in.arg.Len() - 1 );
+
+				if ( in.arg[in.arg.Len() - 1] == '.' )
+					in.arg = FString( in.arg.GetChars(), in.arg.Len() - 1 );
+			}
+		}
+		else if ( def->argumentType() == VOTEARG_String )
+		{
+			in.arg = menu_votestringargument;
+		}
+		else if ( def->argumentType() == VOTEARG_Int )
+		{
+			in.arg.Format( "%d", int( menu_votefloatargument ));
+		}
+		else
+		{
+			in.arg.Format( "%d", int( menu_voteargument ));
+		}
+	}
+
+	M_ClearMenus();
+
+	// [TP] Validate the vote command now
+	if ( VOTEDEF_Validate( in, out ))
+	{
+		CLIENTCOMMANDS_CallVote( menu_votetype, in.arg, menu_votereason );
+		NETWORK_LaunchPacket( CLIENT_GetLocalBuffer( ), CLIENT_GetServerAddress( ));
+		NETWORK_ClearBuffer( CLIENT_GetLocalBuffer( ));
+	}
+	else
+	{
+		// [TP] Failed - tell the user why.
+		static char errortext[256];
+		snprintf( errortext, sizeof errortext, "Couldn't call that because %s\n\npress any key.",
+			out.error.GetChars() );
+		M_StartMessage( errortext, NULL );
+	}
+}
+
 /*=======================================
  *
  * Multiplayer Menu
@@ -2186,6 +2176,7 @@
 void M_StartBrowserMenu( void );
 void M_Spectate( void );
 void M_CallVote( void );
+void M_VoteTypeChanged( bool first );
 void M_ChangeTeam( void );
 void M_Skirmish( void );
 
@@ -2199,7 +2190,7 @@
 	{ more,		"Spectate",				{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *)M_Spectate} },
 	{ more,		"Switch teams",				{NULL},					{0.0}, {0.0}, {0.0}, {(value_t *)M_ChangeTeam} },
 	{ redtext,	" ",					{NULL},					{0.0}, {0.0},	{0.0}, {NULL} },
-	{ more,		"Call a vote",			{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *)M_CallVote} },	
+	{ more,		"Call a vote",			{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *)M_CallVote} },
 	{ more,		"Ignore a player",			{NULL},					{0.0}, {0.0},	{0.0}, {(value_t *)ignoreplayermenu_Show} },
 	{ redtext,	" ",					{NULL},					{0.0}, {0.0},	{0.0}, {NULL} },
 	{ discrete, "Allow skins",			{&cl_skins},			{3.0}, {0.0},	{0.0}, {AllowSkinVals} },
@@ -2239,22 +2230,6 @@
 
 //*****************************************************************************
 //
-void M_CallVote( void )
-{
-	// Don't allow a vote unless the player is a client.
-	if ( NETWORK_GetState( ) != NETSTATE_CLIENT )
-	{
-		M_ClearMenus( );
-		M_StartMessage( "You must be in a multiplayer game to vote.\n\npress any key.", NULL );
-
-		return;
-	}
-
-	M_SwitchMenu( &CallVoteMenu );
-}
-
-//*****************************************************************************
-//
 bool M_SkulltagVersionDrawer( void )
 {
 	ULONG	ulTextHeight;
@@ -3278,9 +3253,6 @@
 #define SKIRMISHITEMS_LEVEL_INDEX 0
 
 // [BB]
-static TArray<valuestring_t> SkirmishLevels;
-
-// [BB]
 void InitSkirmishLevelList()
 {
 	SkirmishLevels.Clear();
@@ -5070,6 +5042,24 @@
 					screen->DrawText( SmallFont, CR_GREY, x, y, szString, DTA_CleanNoMove_1, true, TAG_DONE );
 				}
 				break;
+
+			// [TP]
+			case votetype:
+				{
+					VoteClass* def = VOTEDEF_FindTypeByIndex( *item->a.intcvar );
+					FString text;
+
+					if ( def == NULL )
+						text = "UNKNOWN";
+					else if ( def->description().IsNotEmpty() )
+						text = def->description();
+					else
+						text = def->name();
+
+					screen->DrawText( SmallFont, CR_GREY, x, y, text.GetChars(), DTA_CleanNoMove_1, true, TAG_DONE );
+				}
+				break;
+
 			default:
 				break;
 			}
@@ -5931,6 +5921,17 @@
 				S_Sound (CHAN_VOICE | CHAN_UI, "menu/change", 1, ATTN_NONE);
 				break;
 
+			// [TP]
+			case votetype:
+				{
+					FIntCVar& cv = *item->a.intcvar;
+					int num = VOTEDEF_NumVoteTypes();
+					cv = ( cv + num - 1 ) % num;
+				}
+				M_VoteTypeChanged( false );
+				S_Sound( CHAN_VOICE | CHAN_UI, "menu/change", 1, ATTN_NONE );
+				break;
+
 			default:
 				break;
 		}
@@ -6363,6 +6364,16 @@
 				S_Sound (CHAN_VOICE | CHAN_UI, "menu/change", 1, ATTN_NONE);
 				break;
 
+			// [TP]
+			case votetype:
+				{
+					FIntCVar& cv = *item->a.intcvar;
+					cv = ( cv + 1 ) % VOTEDEF_NumVoteTypes();
+				}
+				M_VoteTypeChanged( false );
+				S_Sound( CHAN_VOICE | CHAN_UI, "menu/change", 1, ATTN_NONE );
+				break;
+
 			default:
 				break;
 		}
diff --git a/src/network.cpp b/src/network.cpp
--- a/src/network.cpp
+++ b/src/network.cpp
@@ -310,6 +310,8 @@
 	lumpsToAuthenticateMode.push_back( ALL_LUMPS );
 	lumpsToAuthenticate.push_back( "MAPINFO" );
 	lumpsToAuthenticateMode.push_back( ALL_LUMPS );
+	lumpsToAuthenticate.push_back( "VOTEDEF" );
+	lumpsToAuthenticateMode.push_back( ALL_LUMPS );
 
 	FString checksum, longChecksum;
 	bool noProtectedLumpsAutoloaded = true;
diff --git a/src/sv_ban.cpp b/src/sv_ban.cpp
--- a/src/sv_ban.cpp
+++ b/src/sv_ban.cpp
@@ -600,6 +600,15 @@
 
 //*****************************************************************************
 //
+void SERVERBAN_BanIP ( NETADDRESS_s address, time_t duration, FString const& comment, std::string& message )
+{
+	time_t expiration;
+	time ( &expiration );
+	expiration += duration;
+	g_ServerBans.addEntry( NETWORK_AddressToStringIgnorePort ( address ), NULL, comment.GetChars(), message, expiration );
+	serverban_KickBannedPlayers();
+}
+
 CCMD( addban )
 {
 	if ( argv.argc( ) < 3 )
diff --git a/src/sv_ban.h b/src/sv_ban.h
--- a/src/sv_ban.h
+++ b/src/sv_ban.h
@@ -67,6 +67,7 @@
 IPList			*SERVERBAN_GetBanList( void );
 IPList			*SERVERBAN_GetBanExemptionList( void );
 void			SERVERBAN_BanPlayer( ULONG ulPlayer, const char *pszBanLength, const char *pszBanReason );
+void			SERVERBAN_BanIP ( NETADDRESS_s address, time_t duration, FString const& comment, std::string& message );
 
 //--------------------------------------------------------------------------------------------------------------------------------------------------
 //-- EXTERNAL CONSOLE VARIABLES --------------------------------------------------------------------------------------------------------------------
diff --git a/src/sv_commands.cpp b/src/sv_commands.cpp
--- a/src/sv_commands.cpp
+++ b/src/sv_commands.cpp
@@ -3525,17 +3525,17 @@
 //*****************************************************************************
 //*****************************************************************************
 //
-void SERVERCOMMANDS_CallVote( ULONG ulPlayer, FString Command, FString Parameters, FString Reason, ULONG ulPlayerExtra, ULONG ulFlags )
+void SERVERCOMMANDS_CallVote( ULONG ulPlayer, ULONG Command, FString Parameters, FString Reason, ULONG ulPlayerExtra, ULONG ulFlags )
 {
 	if ( PLAYER_IsValidPlayer( ulPlayer ) == false )
 		return;
 
-	NetCommand command ( SVC_CALLVOTE );
-	command.addByte ( ulPlayer );
-	command.addString ( Command.GetChars() );
-	command.addString ( Parameters.GetChars() );
-	command.addString ( Reason.GetChars() );
-	command.sendCommandToClients ( ulPlayerExtra, ulFlags );
+	NetCommand command( SVC_CALLVOTE );
+	command.addByte( ulPlayer );
+	command.addLong( Command );
+	command.addString( Parameters );
+	command.addString( Reason );
+	command.sendCommandToClients( ulPlayerExtra, ulFlags );
 }
 
 //*****************************************************************************
diff --git a/src/sv_commands.h b/src/sv_commands.h
--- a/src/sv_commands.h
+++ b/src/sv_commands.h
@@ -273,7 +273,7 @@
 void	SERVERCOMMANDS_StopSectorSequence( sector_t *pSector, ULONG ulPlayerExtra = MAXPLAYERS, ULONG ulFlags = 0 );
 
 // Voting commands. These handle the voting.
-void	SERVERCOMMANDS_CallVote( ULONG ulPlayer, FString Command, FString Parameters, FString Reason, ULONG ulPlayerExtra = MAXPLAYERS, ULONG ulFlags = 0 );
+void	SERVERCOMMANDS_CallVote( ULONG ulPlayer, ULONG Command, FString Parameters, FString Reason, ULONG ulPlayerExtra = MAXPLAYERS, ULONG ulFlags = 0 );
 void	SERVERCOMMANDS_PlayerVote( ULONG ulPlayer, bool bVoteYes, ULONG ulPlayerExtra = MAXPLAYERS, ULONG ulFlags = 0 );
 void	SERVERCOMMANDS_VoteEnded( bool bVotePassed, ULONG ulPlayerExtra = MAXPLAYERS, ULONG ulFlags = 0 );
 
diff --git a/src/sv_main.cpp b/src/sv_main.cpp
--- a/src/sv_main.cpp
+++ b/src/sv_main.cpp
@@ -117,6 +117,7 @@
 #include "po_man.h"
 #include "network/cl_auth.h"
 #include "network/sv_auth.h"
+#include "callvotetype.h"
 
 //*****************************************************************************
 //	MISC CRAP THAT SHOULDN'T BE HERE BUT HAS TO BE BECAUSE OF SLOPPY CODING
@@ -6048,68 +6049,22 @@
 	}
 
 	// Check if the specific type of vote is allowed.
-	bool bVoteAllowed = false;
-	switch ( ulVoteCmd )
-	{
-	case VOTECMD_KICK:
-
-		bVoteAllowed = !sv_nokickvote;
-		sprintf( szCommand, "kick" );
-		break;
-
-	case VOTECMD_KICKFROMGAME:
-
-		bVoteAllowed = !sv_noforcespecvote;
-		sprintf( szCommand, "forcespec" );
-		break;
-
-	case VOTECMD_MAP:
-
-		bVoteAllowed = !sv_nomapvote;
-		sprintf( szCommand, "map" );
-		break;
-	case VOTECMD_CHANGEMAP:
-
-		bVoteAllowed = !sv_nochangemapvote;
-		sprintf( szCommand, "changemap" );
-		break;
-	case VOTECMD_FRAGLIMIT:
-
-		bVoteAllowed = !sv_nofraglimitvote;
-		sprintf( szCommand, "fraglimit" );
-		break;
-	case VOTECMD_TIMELIMIT:
-
-		bVoteAllowed = !sv_notimelimitvote;
-		sprintf( szCommand, "timelimit" );
-		break;
-	case VOTECMD_WINLIMIT:
-
-		bVoteAllowed = !sv_nowinlimitvote;
-		sprintf( szCommand, "winlimit" );
-		break;
-	case VOTECMD_DUELLIMIT:
-
-		bVoteAllowed = !sv_noduellimitvote;
-		sprintf( szCommand, "duellimit" );
-		break;
-	case VOTECMD_POINTLIMIT:
-
-		bVoteAllowed = !sv_nopointlimitvote;
-		sprintf( szCommand, "pointlimit" );
-		break;
-	default:
-
-		return ( false );
-	}
-
-	// Begin the vote, if that type is allowed.
-	if ( bVoteAllowed )
-		CALLVOTE_BeginVote( szCommand, Parameters, Reason, g_lCurrentClient );
-	else
-		SERVER_PrintfPlayer( PRINT_HIGH, g_lCurrentClient, "%s votes are disabled on this server.\n", szCommand );
-
-	return ( false );
+	// [Dusk] Generalized for custom vote types.
+	VoteClass* votetype = VOTEDEF_FindTypeByIndex( ulVoteCmd );
+
+	if( votetype == NULL )
+		return false; // Unknown vote type
+
+	if( votetype->forbidCVar()->GetGenericRep( CVAR_Bool ).Bool )
+	{
+		SERVER_PrintfPlayer( PRINT_HIGH, g_lCurrentClient, "%s votes are disabled on this server.\n",
+			votetype->name().GetChars( ));
+		return false;
+	}
+
+	// Begin the vote
+	CALLVOTE_BeginVote( ulVoteCmd, Parameters, Reason, g_lCurrentClient );
+	return false;
 }
 
 //*****************************************************************************
diff --git a/wadsrc/static/votedef.txt b/wadsrc/static/votedef.txt
new file mode 100644
--- /dev/null
+++ b/wadsrc/static/votedef.txt
@@ -0,0 +1,108 @@
+//
+// Kick a player from the server and ban for 10 minutes
+//
+VoteType native kick
+{
+	Argument = player;
+	ForbidCVar = "sv_nokickvote";
+	Description = "Kick a player";
+	NotOnSelf;
+	NotOnAdmin;
+	NoPassedLimit;
+}
+
+//
+// Remove a player from game to spectators
+//
+VoteType native forcespec
+{
+	Argument = player;
+	ForbidCVar = "sv_noforcespecvote";
+	Description = "Force to spectate";
+	NotOnSelf;
+	NotOnAdmin;
+	NotOnSpectators;
+	NoPassedLimit;
+}
+
+//
+// Set the frag limit
+//
+VoteType native fraglimit
+{
+	Description = "Set frag limit";
+	Argument = int( 0, 65535 );
+	ArgumentLabel = "New limit";
+	MenuRange = 0, 100;
+	ForbidCVar = "sv_nofraglimitvote";
+}
+
+//
+// Set the LMS win limit
+//
+VoteType native winlimit
+{
+	Description = "Set LMS win limit";
+	Argument = int( 0, 255 );
+	ArgumentLabel = "New limit";
+	MenuRange = 0, 100;
+	ForbidCVar = "sv_nowinlimitvote";
+}
+
+//
+// Set the time limit
+//
+VoteType native timelimit
+{
+	Argument = float( 0, 255 );
+	Description = "Set time limit";
+	ArgumentLabel = "New limit";
+	MenuRange = 0, 100;
+	ForbidCVar = "sv_notimelimitvote";
+}
+
+//
+// Set the point limit
+//
+VoteType native pointlimit
+{
+	Description = "Set point limit";
+	Argument = int( 0, 65535 );
+	ArgumentLabel = "New limit";
+	MenuRange = 0, 100;
+	ForbidCVar = "sv_nopointlimitvote";
+}
+
+//
+// Set the duel win limit
+//
+VoteType native duellimit
+{
+	Description = "Set duel win limit";
+	Argument = int( 0, 255 );
+	ArgumentLabel = "New limit";
+	MenuRange = 0, 100;
+	ForbidCVar = "sv_noduellimitvote";
+}
+
+//
+// Change to a given level gracefully with intermission
+//
+VoteType native changemap
+{
+	Description = "Change level";
+	ForbidCVar = "sv_nochangemapvote";
+	Argument = map;
+	MapInRotation;
+}
+
+//
+// Restart the game on a given level
+//
+VoteType native map
+{
+	Description = "Restart game";
+	ForbidCVar = "sv_nomapvote";
+	Argument = map;
+	MapInRotation;
+}
