2026-09-06 15:34 UTC

View Issue Details Jump to Notes ]
IDProjectCategoryView StatusLast Update
0001420Zandronum[All Projects] Suggestionpublic2025-08-31 17:27
ReporterDusk 
Assigned ToDusk 
PrioritynormalSeverityfeatureReproducibilityN/A
StatusresolvedResolutionfixed 
Product Version 
Target Version3.2Fixed in Version 
Summary0001420: Custom vote definitions, aka VOTEINFO
Descriptionhttp://wiki.zandronum.com/VOTEDEF
Attached Files
  • diff file icon votedef.diff (79,646 bytes) 2014-11-01 02:32 -
    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;
    +}
    
    diff file icon votedef.diff (79,646 bytes) 2014-11-01 02:32 +

-Relationships
related to 0000700closed Random Captains function (callvote randomcaptains) 
parent of 0000918closedTorr Samaho callvote kickfromgame 
parent of 0002345resolvedDrinkyBird Callvote Nextmap 
+Relationships

-Notes
Dusk

~0006728

Dusk (developer)

Last edited: 2013-07-21 15:58

View 2 revisions

I wrote up a draft documentation page: http://wiki.zandronum.com/VOTEDEFS

I'm not totally convinced that going for console commands is a good idea here for obvious reasons even though that's what the current vote types base on. On the other hand the votes can be seen as the public voting for console commands being executed on the server.. and there isn't really any good alternative. ACS could do for mods' purposes but I don't know how to implement kick votes in that...

Maybe there could be native vote types (the current loadout + kickfromgame) and then ACS-based vote effects for mods to use?
EDIT: The more I think of it, the more I'm convinced that this is the best way to do it. Let's see here...

Dusk

~0006729

Dusk (developer)

OK I got the ACS-based stuff to work, draft updated.
Watermelon

~0006873

Watermelon (developer)

Truly awesome


Is there supposed to be two lines of
                type->flags = 0;


Is it operational?
Monsterovich

~0006874

Monsterovich (reporter)

Good. :)

Can you delete "Flags { ... }" section. Decorate like style is best.
Add argument type: string.
Dusk

~0006879

Dusk (developer)

Last edited: 2013-08-01 14:15

View 2 revisions

[quote="Watermelon"]
Is there supposed to be two lines of
                 type->flags = 0;
[/quote]
Probably not.

[quote="Watermelon"]Is it operational?[/quote]
Yeah, for the most part. It still needs work though.

[quote="Monsterovich"]Can you delete "Flags { ... }" section. Decorate like style is best.[/quote]
I have the Flags{} syntax so as to reserve the option to remove it later. Who knows how this will evolve later on.

[quote="Monsterovich"]Add argument type: string.[/quote]
I don't know how would a string argument work in ACS - dynamic strings seem to have problems (0001307).

Dusk

~0006889

Dusk (developer)

Last edited: 2013-08-01 23:27

View 4 revisions

Links at description updated, I think this is somewhat ready now.

EDIT: I just realized that I need to update the 'call a vote' menu too...
EDIT 2: OK, I got the menu working now too.
EDIT 3: Value range is now required for int and float types, made no-arg votes handled better.

Dusk

~0010755

Dusk (developer)

I've pretty much finally, finally, [i]finally[/i] gotten to the point that I'm pretty much finished with custom vote classes now. I tested this with edward-san and Water today and what this now needs is a build of some sort for public testing.

The current head is here:
https://bitbucket.org/crimsondusk/zandronum-sandbox/commits/0acc721f9f3a16ba9115a53aa7884927042b8dae

I've attached a diff containing the changes.
Torr Samaho

~0010790

Torr Samaho (administrator)

Looks promising at first glance, but we'll need to figure out a way to review patches of this size. Carefully going through such a patch line by line seems to be infeasible to me. Let's discuss this in the next dev meeting.
Dusk

~0012949

Dusk (developer)

Last edited: 2015-07-15 21:48

View 3 revisions

After a considerable amount of refining and some new features (named script support and callvote nextmap), I think this is in a testable state now.

WaTaKiD has provided a build here: https://www.dropbox.com/s/td6ykj3c4iveex8/zandronum-3.0-r150715-2140-181819b-windows.zip?dl=0
Source changes here: https://bitbucket.org/crimsondusk/zandronum-sandbox/compare/votedef..Torr_Samaho/zandronum#diff
Documentation is still here and is now updated: http://wiki.zandronum.com/VOTEDEF

Would be nice if this got tested. I'm particularly concerned whether existing votes still behave like before.

Wirtualnosc

~0017205

Wirtualnosc (reporter)

Last edited: 2017-04-18 20:35

View 2 revisions

Is there a chance this will get implemented into 3.0 or do we have to wait a bit longer for this feature? :( Considering this is quite old now and is mostly finished, it would be really nice to see it working in the upcoming versions. I have tested this with my mod and I don't see any problems with it, as well as existing votes.

Torr Samaho

~0017337

Torr Samaho (administrator)

Last edited: 2017-04-26 06:14

View 1 revisions

Here is an updated link: https://bitbucket.org/zandronum/zandronum-sandbox/compare/votedef..Torr_Samaho/zandronum#diff

That's quite a large change and I'm hesitant to include this so shortly before the expected release of 3.0. In any case, as you can see in the link, there are a lot of conflicts in the code now that first need to be taken care of. Also, this needs very thorough testing of the entire voting system.

Dusk, what do you think about this?

Kaminsky

~0022940

Kaminsky (developer)

This lump (now called VOTEINFO) was pushed into the stable repo not too long ago: https://foss.heptapod.net/zandronum/zandronum-stable/-/commit/6834253b22fc9e0685da0695e16d376d5717ed12
Fused

~0023000

Fused (reporter)

Last edited: 2024-01-24 09:50

View 5 revisions

I have been using this feature using its initial implementation and here's my opinion.

Compared to the initial pitch (https://wiki.zandronum.com/VOTEDEF) the final implementation (https://wiki.zandronum.com/VOTEINFO) really needs some work in general. The amount of control that exists on creating custom votes is very bare bones. I understand this was done initially because most of it can be done with ACS when the vote passes, but there are a lot of flags and options in the initial idea that limit calling votes in the first place. The fact that I can now call a vote without these restrictions, and only be notified after the vote passes is bad user experience.

I suggest a new option is added to fix this, something like the existing Action parameter but instead this option specifies a script that is called to validate a vote when it is called. If the script results in `true`, the vote may be called. This gives full control over what restricts a vote, and even makes the ForbidCvar option obsolete.

Trillster

~0023012

Trillster (reporter)

In addition to the notes from Fused, I think this feature could be made more failure-proof by allowing VOTEINFO VoteType blocks to specify a display name and internal name for a user-defined menu that would automatically be added to the ZA_CallVote menu.

The current alternative is for mods with custom vote types to manually overwrite ZA_CallVote, but this can easily cause menu conflicts between mods that add new vote types.
Ru5tK1ng

~0023142

Ru5tK1ng (updater)

Setting status to Feedback while it is being determined if more changes will be made.
Trillster

~0023990

Trillster (reporter)

I've created an MR to address this feedback as well as implement some extra features that came to mind.
https://foss.heptapod.net/zandronum/zandronum-stable/-/merge_requests/181
Trillster

~0024214

Trillster (reporter)

Updates to VOTEINFO have just been merged, so it should now be able to more closely match the initially pitched VOTEDEF lump.
https://foss.heptapod.net/zandronum/zandronum-stable/-/commit/a8e2a2d9cb9a59a9b6ae8adb6ee0700553de16a7
Ru5tK1ng

~0024565

Ru5tK1ng (updater)

This was merged in. Reopen if necessary.
+Notes

-Issue History
Date Modified Username Field Change
2013-07-21 00:59 Dusk New Issue
2013-07-21 00:59 Dusk Status new => assigned
2013-07-21 00:59 Dusk Assigned To => Dusk
2013-07-21 00:59 Dusk Relationship added related to 0000700
2013-07-21 01:00 Dusk Relationship added parent of 0000918
2013-07-21 01:02 Dusk Description Updated View Revisions
2013-07-21 01:03 Dusk Description Updated View Revisions
2013-07-21 01:07 Dusk Description Updated View Revisions
2013-07-21 15:50 Dusk Note Added: 0006728
2013-07-21 15:58 Dusk Note Edited: 0006728 View Revisions
2013-07-21 17:09 Dusk Note Added: 0006729
2013-08-01 05:59 Watermelon Note Added: 0006873
2013-08-01 11:37 Monsterovich Note Added: 0006874
2013-08-01 14:14 Dusk Note Added: 0006879
2013-08-01 14:15 Dusk Note Edited: 0006879 View Revisions
2013-08-01 16:04 Dusk Summary Custom vote definitions, aka VOTEDEFS => Custom vote definitions, aka VOTEDEF
2013-08-01 16:04 Dusk Description Updated View Revisions
2013-08-01 18:35 Dusk Description Updated View Revisions
2013-08-01 18:36 Dusk Note Added: 0006889
2013-08-01 18:36 Dusk Status assigned => feedback
2013-08-01 19:56 Dusk Note Edited: 0006889 View Revisions
2013-08-01 19:56 Dusk Status feedback => assigned
2013-08-01 22:47 Dusk Status assigned => feedback
2013-08-01 22:48 Dusk Note Edited: 0006889 View Revisions
2013-08-01 23:27 Dusk Note Edited: 0006889 View Revisions
2014-11-01 02:30 Dusk Description Updated View Revisions
2014-11-01 02:32 Dusk File Added: votedef.diff
2014-11-01 02:34 Dusk Note Added: 0010755
2014-11-01 02:34 Dusk Status feedback => assigned
2014-11-01 02:34 Dusk Status assigned => needs review
2014-11-02 08:01 Torr Samaho Note Added: 0010790
2015-07-15 15:00 Dusk Relationship added parent of 0002345
2015-07-15 21:47 Dusk Note Added: 0012949
2015-07-15 21:47 Dusk Status needs review => needs testing
2015-07-15 21:47 Dusk Note Edited: 0012949 View Revisions
2015-07-15 21:48 Dusk Note Edited: 0012949 View Revisions
2017-04-18 20:25 Wirtualnosc Note Added: 0017205
2017-04-18 20:35 Wirtualnosc Note Edited: 0017205 View Revisions
2017-04-26 06:12 Torr Samaho Note Added: 0017337
2017-04-26 06:14 Torr Samaho Note Edited: 0017337 View Revisions
2017-04-26 06:14 Torr Samaho Note Revision Dropped: 17337: 0010406
2023-12-30 14:22 Kaminsky Note Added: 0022940
2023-12-30 14:22 Kaminsky Target Version => 3.2
2023-12-30 14:22 Kaminsky Summary Custom vote definitions, aka VOTEDEF => Custom vote definitions, aka VOTEINFO
2024-01-24 09:42 Fused Note Added: 0023000
2024-01-24 09:43 Fused Note Edited: 0023000 View Revisions
2024-01-24 09:44 Fused Note Edited: 0023000 View Revisions
2024-01-24 09:46 Fused Note Edited: 0023000 View Revisions
2024-01-24 09:50 Fused Note Edited: 0023000 View Revisions
2024-01-27 16:39 Trillster Note Added: 0023012
2024-03-01 03:08 Ru5tK1ng Note Added: 0023142
2024-03-01 03:08 Ru5tK1ng Status needs testing => feedback
2024-08-30 03:51 Trillster Note Added: 0023990
2024-08-30 03:53 Kaminsky Status feedback => needs review
2025-02-24 04:45 Trillster Note Added: 0024214
2025-02-24 11:39 Kaminsky Status needs review => needs testing
2025-08-31 17:27 Ru5tK1ng Note Added: 0024565
2025-08-31 17:27 Ru5tK1ng Status needs testing => resolved
2025-08-31 17:27 Ru5tK1ng Resolution open => fixed
+Issue History