Anonymous | Login | Signup for a new account | 2024-03-29 13:48 UTC |
My View | View Issues | Change Log | Roadmap | Zandronum Issue Support Ranking | Rules | My Account |
View Issue Details [ Jump to Notes ] | [ Issue History ] [ Print ] | ||||||||||||
ID | Project | Category | View Status | Date Submitted | Last Update | ||||||||
0001420 | Zandronum | [All Projects] Suggestion | public | 2013-07-21 00:59 | 2024-03-01 03:08 | ||||||||
Reporter | Dusk | ||||||||||||
Assigned To | Dusk | ||||||||||||
Priority | normal | Severity | feature | Reproducibility | N/A | ||||||||
Status | feedback | Resolution | open | ||||||||||
Platform | OS | OS Version | |||||||||||
Product Version | |||||||||||||
Target Version | 3.2 | Fixed in Version | |||||||||||
Summary | 0001420: Custom vote definitions, aka VOTEINFO | ||||||||||||
Description | 'http://wiki.zandronum.com/VOTEDEF [^]' | ||||||||||||
Attached Files | votedef.diff [^] (79,646 bytes) 2014-11-01 02:32 [Show Content] [Hide Content]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; +} | ||||||||||||
Relationships | ||||||||||||||||
|
Notes | |
(0006728) Dusk (developer) 2013-07-21 15:50 edited on: 2013-07-21 15:58 |
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... |
(0006729) Dusk (developer) 2013-07-21 17:09 |
OK I got the ACS-based stuff to work, draft updated. |
(0006873) Watermelon (developer) 2013-08-01 05:59 |
Truly awesome Is there supposed to be two lines of type->flags = 0; Is it operational? |
(0006874) Monsterovich (reporter) 2013-08-01 11:37 |
Good. :) Can you delete "Flags { ... }" section. Decorate like style is best. Add argument type: string. |
(0006879) Dusk (developer) 2013-08-01 14:14 edited on: 2013-08-01 14:15 |
Quote from "Watermelon" Probably not. Quote from "Watermelon" Yeah, for the most part. It still needs work though. Quote from "Monsterovich" I have the Flags{} syntax so as to reserve the option to remove it later. Who knows how this will evolve later on. Quote from "Monsterovich" I don't know how would a string argument work in ACS - dynamic strings seem to have problems (0001307). |
(0006889) Dusk (developer) 2013-08-01 18:36 edited on: 2013-08-01 23:27 |
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. |
(0010755) Dusk (developer) 2014-11-01 02:34 |
I've pretty much finally, finally, finally 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. |
(0010790) Torr Samaho (administrator) 2014-11-02 08:01 |
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. |
(0012949) Dusk (developer) 2015-07-15 21:47 edited on: 2015-07-15 21:48 |
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. |
(0017205) Wirtualnosc (reporter) 2017-04-18 20:25 edited on: 2017-04-18 20:35 |
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. |
(0017337) Torr Samaho (administrator) 2017-04-26 06:12 edited on: 2017-04-26 06:14 |
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? |
(0022940) Kaminsky (developer) 2023-12-30 14:22 |
This lump (now called VOTEINFO) was pushed into the stable repo not too long ago:'https://foss.heptapod.net/zandronum/zandronum-stable/-/commit/6834253b22fc9e0685da0695e16d376d5717ed12 [^]' |
(0023000) Fused (reporter) 2024-01-24 09:42 edited on: 2024-01-24 09:50 |
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. |
(0023012) Trillster (reporter) 2024-01-27 16:39 |
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. |
(0023142) Ru5tK1ng (updater) 2024-03-01 03:08 |
Setting status to Feedback while it is being determined if more changes will be made. |
Only registered users can voice their support. Click here to register, or here to log in. | |
Supporters: | Gummywormz Monsterovich Korshun djskaarj JC Tenton MaxRideWizardLord Unknown Wirtualnosc mifu Juanba Lollipop |
Opponents: | No one explicitly opposes this issue yet. |
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 |
Copyright © 2012-2024, Torr Samaho & Zandronum Team.
Doom and Doom II are the property of id Software.
Copyright © 2000 - 2024 MantisBT Team |