Advertisement
slatenails

votedef.cpp

Aug 1st, 2013
170
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 18.73 KB | None | 0 0
  1. /*
  2.  * Zandronum source code
  3.  * Copyright (C) 2012 Santeri Piippo
  4.  *
  5.  * 1. Redistributions of source code must retain the above copyright notice,
  6.  *    this list of conditions and the following disclaimer.
  7.  * 2. Redistributions in binary form must reproduce the above copyright notice,
  8.  *    this list of conditions and the following disclaimer in the documentation
  9.  *    and/or other materials provided with the distribution.
  10.  * 3. Neither the name of the Skulltag Development Team nor the names of its
  11.  *    contributors may be used to endorse or promote products derived from this
  12.  *    software without specific prior written permission.
  13.  * 4. Redistributions in any form must be accompanied by information on how to
  14.  *    obtain complete source code for the software and any accompanying
  15.  *    software that uses the software. The source code must either be included
  16.  *    in the distribution or be available for no more than the cost of
  17.  *    distribution plus a nominal fee, and must be freely redistributable
  18.  *    under reasonable conditions. For an executable file, complete source
  19.  *    code means the source code for all modules it contains. It does not
  20.  *    include source code for modules or files that typically accompany the
  21.  *    major components of the operating system on which the executable file
  22.  *    runs.
  23.  *
  24.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  25.  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  26.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  27.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  28.  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  29.  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  30.  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  31.  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  32.  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  33.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  34.  * POSSIBILITY OF SUCH DAMAGE.
  35.  *
  36.  * Filename: votedef.cpp
  37.  *
  38.  * Description: VOTEDEF parser, vote type manager
  39.  *
  40.  * -----------------------------------------------------------------------------
  41.  */
  42.  
  43. #include "doomdef.h"
  44. #include "info.h"
  45. #include "tarray.h"
  46. #include "votedef.h"
  47. #include "network.h"
  48. #include "sv_main.h"
  49. #include "g_level.h"
  50. #include "v_text.h"
  51. #include "c_dispatch.h"
  52. #include "maprotation.h"
  53. #include "c_cvars.h"
  54.  
  55. // ============================================================================
  56. // Definitions for the static members
  57. TArray<VoteDef*> VoteDef::k_Defs;
  58. NETADDRESS_s     VoteDef::k_IPStorage[MAXPLAYERS];
  59.  
  60. const char* VoteDef::k_ParameterTokens[] =
  61. {
  62.     "INT",
  63.     "FLOAT",
  64.     "PLAYER",
  65.     "MAP",
  66.     NULL
  67. };
  68.  
  69. const char* VoteDef::k_PropertyTokens[] =
  70. {
  71.     "ARGUMENT",
  72.     "ACSSCRIPT",
  73.     "DESCRIPTION",
  74.     "FLAGS",
  75.     "FORBIDCVAR",
  76.     "VALUERANGE",
  77.     NULL
  78. };
  79.  
  80. const char* VoteDef::k_FlagTokens[] =
  81. {
  82.     "NOPASSEDLIMIT",
  83.     "NOTSELF",
  84.     "NOTADMIN",
  85.     "PLAYERINDEX",
  86.     "PLAYERINGAME",
  87.     NULL
  88. };
  89.  
  90. const char* VoteDef::k_NativeNames[] =
  91. {
  92.     "KICK",
  93.     "KICKFROMGAME",
  94.     "CHANGEMAP",
  95.     "MAP",
  96.     "FRAGLIMIT",
  97.     "TIMELIMIT",
  98.     "POINTLIMIT",
  99.     "DUELLIMIT",
  100.     "WINLIMIT",
  101.     NULL
  102. };
  103.  
  104. // ============================================================================
  105. // VoteDef constructor
  106. VoteDef::VoteDef( const FString& name, VoteDef::NativeType native ) :
  107.     m_name( name ),
  108.     m_script( 0 ),
  109.     m_flags( 0 ),
  110.     m_argType( NumArgTypes ),
  111.     m_min( INT_MAX ),
  112.     m_max( INT_MAX ),
  113.     m_native( native ) {}
  114.  
  115. // ============================================================================
  116. // VoteDef destructor
  117. VoteDef::~VoteDef( ) {}
  118.  
  119. // ============================================================================
  120. // Inherit properties from this vote type to the other type
  121. void VoteDef::copyTo( VoteDef* other ) const
  122. {
  123.     other->m_description = m_description;
  124.     other->m_argType = m_argType;
  125.     other->m_flags = m_flags;
  126.     other->m_min = m_min;
  127.     other->m_max = m_max;
  128.     other->m_script = m_script;
  129.  
  130.     // Only copy the forbid cvar name if it was not automatically generated.
  131.     if ( !m_autogenForbidCVar )
  132.         other->m_forbidCVar = m_forbidCVar;
  133. }
  134.  
  135. // ============================================================================
  136. // Initializes VOTEDEF. This is called at startup.
  137. void VoteDef::init( )
  138. {
  139.     if ( Wads.CheckNumForName( "VOTEDEF" ) == -1 )
  140.         return;
  141.  
  142.     // Startup message. :)
  143.     Printf( "VoteDef: Loading vote type definitions\n" );
  144.     atexit( VoteDef::deinit );
  145.  
  146.     int lump, lastlump;
  147.     lastlump = 0;
  148.  
  149.     while (( lump = Wads.FindLump( "VOTEDEF", &lastlump )) != -1 )
  150.         parseLump( lump );
  151. }
  152.  
  153. // ============================================================================
  154. // De-initializes the votedef stuff.
  155. void VoteDef::deinit( )
  156. {
  157.     for ( unsigned i = 0; i < k_Defs.Size( ); ++i )
  158.         delete k_Defs[i];
  159.  
  160.     k_Defs.Clear( );
  161. }
  162.  
  163. // ============================================================================
  164. // Parse a single VOTEDEF lump.
  165. void VoteDef::parseLump( int lump )
  166. {
  167.     FScanner sc( lump );
  168.  
  169.     while ( sc.GetToken( ))
  170.     {
  171.         bool isnative = false;
  172.  
  173.         // 'votetype' is the only command we have right now.
  174.         if ( !sc.Compare( "VOTETYPE" ))
  175.             sc.ScriptError( "Expected `votetype`, got %s", sc.String );
  176.  
  177.         // See if this vote type is native.
  178.         NativeType native = NumNatives;
  179.         if ( sc.CheckToken( TK_Native ))
  180.             isnative = true;
  181.  
  182.         sc.MustGetToken( TK_Identifier );
  183.         FString votename = sc.String;
  184.         FString forbidCVarName;
  185.  
  186.         if ( isnative == true )
  187.             native = (NativeType) sc.MustMatchString( k_NativeNames );
  188.  
  189.         // Check that it's not already defined
  190.         if ( VoteDef::findByName( votename ))
  191.             sc.ScriptError( "Vote type `%s` is already defined", votename.GetChars( ));
  192.  
  193.         VoteDef* type = new VoteDef( votename, native );
  194.  
  195.         if ( sc.CheckToken( ':' )) {
  196.             // We're inheriting this vote type from another. Copy the properties
  197.             // of the other vote here.
  198.             sc.MustGetToken( TK_Identifier );
  199.             VoteDef* src = VoteDef::findByName( sc.String );
  200.  
  201.             if ( !src )
  202.                 sc.ScriptError( "Cannot inherit from undefined vote type `%s`", sc.String );
  203.  
  204.             src->copyTo( type );
  205.         }
  206.  
  207.         sc.MustGetToken( '{' );
  208.  
  209.         for ( ;; )
  210.         {
  211.             // The file shouldn't end in the middle of a block
  212.             if ( !sc.GetToken( ))
  213.                 sc.ScriptError( "Unexpected end of file, did you miss a '}'?" );
  214.  
  215.             if ( sc.Compare( "}" ))
  216.                 break; // terminating brace
  217.  
  218.             int prop = sc.MustMatchString( k_PropertyTokens );
  219.  
  220.             // Flags uses block format, everything else needs '='
  221.             if ( prop != VTPROP_FLAGS )
  222.                 sc.MustGetToken( '=' );
  223.  
  224.             switch ( prop )
  225.             {
  226.                 case VTPROP_ARGUMENT:
  227.                     if ( sc.CheckToken( TK_Float ))
  228.                         type->m_argType = Float;
  229.                     else if ( sc.CheckToken( TK_Int ))
  230.                         type->m_argType = Int;
  231.                     else {
  232.                         sc.MustGetToken( TK_Identifier );
  233.                         type->m_argType = (ArgumentType) sc.MustMatchString( k_ParameterTokens );
  234.                     }
  235.                     break;
  236.  
  237.                 case VTPROP_FLAGS:
  238.                     sc.MustGetToken( '{' );
  239.  
  240.                     while ( !sc.CheckToken( '}' ))
  241.                     {
  242.                         // Get the sign
  243.                         bool set = false;
  244.                         if ( sc.CheckToken( '+' ))
  245.                             set = true;
  246.                         else if ( !sc.CheckToken( '-' ))
  247.                             sc.ScriptError( "Badly formed flag (expected + or -)" );
  248.  
  249.                         // Name of the flag
  250.                         sc.MustGetToken( TK_Identifier );
  251.                         int flag = sc.MustMatchString( k_FlagTokens );
  252.  
  253.                         // Set or unset the flag now.
  254.                         if ( set )
  255.                             type->m_flags |= 1 << flag;
  256.                         else
  257.                             type->m_flags &= ~( 1 << flag );
  258.                     }
  259.                     break;
  260.  
  261.                 case VTPROP_FORBIDCVAR:
  262.                     sc.MustGetToken( TK_StringConst );
  263.                     forbidCVarName = sc.String;
  264.                     break;
  265.  
  266.                 case VTPROP_ACSSCRIPT:
  267.                     sc.MustGetToken( TK_IntConst );
  268.                     type->m_script = sc.Number;
  269.                     break;
  270.  
  271.                 case VTPROP_DESCRIPTION:
  272.                     sc.MustGetToken( TK_StringConst );
  273.                     type->m_description = sc.String;
  274.                     break;
  275.  
  276.                 case VTPROP_VALUERANGE:
  277.                     if ( !sc.CheckToken ( TK_FloatConst ))
  278.                         sc.MustGetToken( TK_IntConst );
  279.                     type->m_min = sc.Float;
  280.                     sc.MustGetToken( ',' );
  281.  
  282.                     if ( !sc.CheckToken( TK_FloatConst ))
  283.                         sc.MustGetToken( TK_IntConst );
  284.                     type->m_max = sc.Float;
  285.                     break;
  286.             }
  287.  
  288.             if ( prop != VTPROP_FLAGS )
  289.                 sc.MustGetToken( ';' );
  290.         }
  291.  
  292.         if ( type->m_native == NumNatives && type->m_script == 0 )
  293.             sc.ScriptError( "Non-native vote type '%s' needs a script number.",
  294.                 type->m_name.GetChars( ));
  295.  
  296.         // Int and float types must have a value range.
  297.         if (( type->argType() == Int || type->argType() == Float ) &&
  298.             type->min() == INT_MAX && type->max() == INT_MAX )
  299.         {
  300.             sc.ScriptError( "Vote type \"%s\" needs a value range.", type->name( ).GetChars( ));
  301.         }
  302.  
  303.         // Generate the forbid CVar name if it's not given. Note that the default
  304.         // is different from the usual: sv_vote_no<name>. This way we can group
  305.         // new autogenerated forbid cvars within sv_vote_* and not let them flow
  306.         // into the sv_ namespace.
  307.         if ( forbidCVarName.IsEmpty( ))
  308.         {
  309.             forbidCVarName.Format( "sv_vote_no%s", type->m_name.GetChars( ));
  310.             type->m_autogenForbidCVar = true;
  311.         }
  312.  
  313.         if ( type->m_native == NumNatives && type->m_argType == MapType )
  314.             sc.ScriptError( "Only native types may use a map argument. " );
  315.  
  316.         // If the CVar does not exist, create it now.
  317.         if (( type->m_forbidCVar = FindCVar( forbidCVarName, NULL )) == NULL )
  318.         {
  319.             FBoolCVar* var = new FBoolCVar( forbidCVarName, false, CVAR_AUTO | CVAR_ARCHIVE );
  320.             var->SetArchiveBit( );
  321.             type->m_forbidCVar = var;
  322.         }
  323.  
  324.         k_Defs.Push( type );
  325.     }
  326. }
  327.  
  328. #define APPLY_ERROR( ... ) \
  329. { \
  330.     result.error.Format( __VA_ARGS__ ); \
  331.     return false; \
  332. }
  333.  
  334. // ============================================================================
  335. // Apply given input data, performs validity checks and compiles the vote
  336. // command string, or argument for the vote's ACS script.
  337. bool VoteDef::applyCommand( VoteDef::ApplyInput input, VoteDef::ApplyResult& result )
  338. {
  339.     FString needle;
  340.     VoteDef* type = k_Defs[input.type];
  341.  
  342.     result.error = "";
  343.     result.arg = 0;
  344.     long argvalue;
  345.     FString mapname;
  346.  
  347.     // Check that we got the proper parameter
  348.     if ( type->m_argType != NumArgTypes )
  349.     {
  350.         if ( input.arg.IsEmpty( ))
  351.             APPLY_ERROR( "argument not given" );
  352.  
  353.         // The variable contens in various types
  354.         FString fsval = input.arg;
  355.         const char* sval = fsval.GetChars( );
  356.         long lval = atol( sval );
  357.         double fval = atof( sval );
  358.         unsigned uval = lval;
  359.         unsigned idx;
  360.  
  361.         switch ( type->m_argType )
  362.         {
  363.             case Int:
  364.                 if ( !fsval.IsInt( ) || lval < type->m_min || lval > type->m_max )
  365.                     APPLY_ERROR( "Value must be a number between %d and %d, was %s",
  366.                         (int) type->min( ), (int) type->max( ), sval );
  367.                 argvalue = lval;
  368.                 break;
  369.  
  370.             case Float:
  371.                 if ( !fsval.IsFloat( ))
  372.                     APPLY_ERROR( "Value must be a decimal number between %f and %f, was %s",
  373.                         type->min( ), type->max( ), sval );
  374.  
  375.                 argvalue = fval * FRACUNIT; // Convert to fixed
  376.                 break;
  377.  
  378.             case Player:
  379.                 // Find the client by name, unless PLAYERINDEX is given
  380.                 if ( type->m_flags & PlayerIndex ) {
  381.                     if ( !fsval.IsInt( ) || lval < 0 || lval >= MAXPLAYERS )
  382.                         APPLY_ERROR( "Expected player index, got %s", sval );
  383.  
  384.                     idx = lval;
  385.                 } else
  386.                     idx = SERVER_GetPlayerIndexFromName( sval, true, false );
  387.  
  388.                 // Must be valid
  389.                 if ( !SERVER_IsValidClient( idx ))
  390.                     APPLY_ERROR( "Player %s does not exist", sval );
  391.  
  392.                 // Must be in-game if PLAYERINGAME is given
  393.                 if ( type->m_flags & PlayerInGame && players[idx].bSpectating )
  394.                     APPLY_ERROR( "%s is a spectator", sval );
  395.  
  396.                 // [BB] Don't allow anyone to kick somebody who is on the admin list.
  397.                 // [Dusk] Brought this check here and flagged
  398.                 if (( type->m_flags & NotAdmin ) && playerIsAdmin( idx ))
  399.                     APPLY_ERROR( "%s is a server administrator!", sval );
  400.  
  401.                 // Not self if NOTSELF is set
  402.                 if (( type->m_flags & NotSelf ) && uval == input.caller )
  403.                     APPLY_ERROR( "Cannot invoke this on yourself" );
  404.  
  405.                 argvalue = idx;
  406.                 break;
  407.  
  408.             case MapType:
  409.                 if ( !P_CheckIfMapExists( input.arg ))
  410.                     APPLY_ERROR( "No such map '%s'", input.arg.GetChars( ));
  411.  
  412.                 if ( NETWORK_GetState( ) == NETSTATE_SERVER && sv_maprotation &&
  413.                     !MAPROTATION_IsMapInRotation( input.arg ))
  414.                 {
  415.                     APPLY_ERROR( "Map `%s` is not in rotation\n", input.arg.GetChars( ));
  416.                 }
  417.  
  418.                 mapname = input.arg;
  419.                 break;
  420.  
  421.             case NumArgTypes:
  422.                 break;
  423.         }
  424.     }
  425.  
  426.     // Handle native types
  427.     if ( type->m_native != NumNatives )
  428.     {
  429.         // [Dusk] Write the kick reason into the ban reason, [BB] but only if it's not empty.
  430.         FString votekickblurb;
  431.         votekickblurb.Format( "\"Vote kick: %d to %d: %s\"",
  432.             input.yes, input.no,
  433.             input.reason.IsNotEmpty( ) ? input.reason.GetChars( ) : "No reason given" );
  434.  
  435.         switch ( type->m_native )
  436.         {
  437.             case Kick:
  438.                 // [BB, RC] If the vote is a kick vote, we have to rewrite g_VoteCommand to both use
  439.                 // the stored IP, and temporarily ban it.
  440.                 result.command.Format( "addban %s 10min %s",
  441.                     NETWORK_AddressToStringIgnorePort( SERVER_GetClient( argvalue )->Address ),
  442.                     votekickblurb.GetChars( ));
  443.                 break;
  444.  
  445.             case KickFromGame:
  446.                 result.command.Format( "kickfromgame_idx %ld %s", argvalue, votekickblurb.GetChars( ));
  447.                 break;
  448.  
  449.             case ChangeMap:
  450.             case Map:
  451.                 result.command.Format( "%s %s", type->m_name.GetChars( ), mapname.GetChars( ));
  452.                 break;
  453.  
  454.             case FragLimit:
  455.             case DuelLimit:
  456.             case WinLimit:
  457.             case PointLimit:
  458.                 result.command.Format( "%s %ld", type->m_name.GetChars( ), argvalue );
  459.                 break;
  460.  
  461.             case TimeLimit:
  462.                 result.command.Format( "timelimit %f", atof( input.arg.GetChars( )));
  463.                 break;
  464.  
  465.             case NumNatives:
  466.                 break;
  467.         }
  468.     }
  469.     else
  470.         result.arg = argvalue;
  471.  
  472.     // Application successful.
  473.     return true;
  474. }
  475.  
  476. // ============================================================================
  477. // Make a record of this vote for later comparing
  478. FString VoteDef::makeSummary( const VoteDef::ApplyInput& input )
  479. {
  480.     VoteDef* type = k_Defs[input.type];
  481.     FString record = type->m_name + " ";
  482.  
  483.     int vartype = type->m_argType;
  484.     FString fsval = input.arg;
  485.     const char* sval = fsval.GetChars( );
  486.     long lval = atol( sval );
  487.     unsigned idx;
  488.  
  489.     switch ( vartype )
  490.     {
  491.         case Int:
  492.         case Float:
  493.         case MapType:
  494.             record += sval;
  495.             break;
  496.  
  497.         case Player:
  498.             idx = ( type->m_flags & PlayerIndex ) ?
  499.                 lval : SERVER_GetPlayerIndexFromName( sval, true, false );
  500.  
  501.             // Kick votes need the IP address of the victim.
  502.             if ( type->m_native == Kick )
  503.                 record += NETWORK_AddressToStringIgnorePort( k_IPStorage[lval] );
  504.             else
  505.                 record += sval;
  506.             break;
  507.  
  508.         default:
  509.             break;
  510.     }
  511.  
  512.     return record;
  513. }
  514.  
  515. // ============================================================================
  516. // Store everybody's IPs so that they don't get lost when the command is
  517. // actually applied. The client could disconnect in the midst of a kick vote...
  518. void VoteDef::storeIPAddresses( )
  519. {
  520.     for ( unsigned i = 0; i < MAXPLAYERS; i++ )
  521.     {
  522.         if ( !SERVER_IsValidClient( i ))
  523.             continue;
  524.  
  525.         k_IPStorage[i] = SERVER_GetClient( i )->Address;
  526.     }
  527. }
  528.  
  529. // ============================================================================
  530. // Get usage info of calling this vote.
  531. FString VoteDef::getUsage( ) const
  532. {
  533.     FString cmd = "callvote ";
  534.     FString arg;
  535.  
  536.     // Name of command
  537.     cmd += name( );
  538.  
  539.     // Parameters
  540.     if ( argType( ) != NumArgTypes )
  541.     {
  542.         arg = k_ParameterTokens[argType( )];
  543.         arg.ToLower( );
  544.         cmd.AppendFormat( " <%s>", arg.GetChars( ));
  545.     }
  546.  
  547.     // Reasons
  548.     cmd += " [reason]";
  549.  
  550.     return cmd;
  551. }
  552.  
  553. // ============================================================================
  554. VoteDef* VoteDef::findByIndex( unsigned index )
  555. {
  556.     if ( index >= k_Defs.Size( ))
  557.         return NULL;
  558.  
  559.     return k_Defs[index];
  560. }
  561.  
  562. // ============================================================================
  563. VoteDef* VoteDef::findByName( FString name )
  564. {
  565.     for ( unsigned i = 0; i < k_Defs.Size( ); i++ )
  566.         if ( !k_Defs[i]->m_name.CompareNoCase( name ))
  567.             return k_Defs[i];
  568.  
  569.     return NULL;
  570. }
  571.  
  572. // ============================================================================
  573. int VoteDef::getIndex( ) const
  574. {
  575.     for ( unsigned i = 0; i < k_Defs.Size( ); i++ )
  576.         if ( k_Defs[i] == this )
  577.             return i;
  578.  
  579.     return -1;
  580. }
  581.  
  582. // ============================================================================
  583. const ULONG& VoteDef::scriptNum( ) const
  584. {
  585.     return m_script;
  586. }
  587.  
  588. // ============================================================================
  589. const VoteDef::NativeType& VoteDef::nativeType( ) const
  590. {
  591.     return m_native;
  592. }
  593.  
  594. // ============================================================================
  595. const FString& VoteDef::name( ) const
  596. {
  597.     return m_name;
  598. }
  599.  
  600. // ============================================================================
  601. const VoteDef::ArgumentType& VoteDef::argType( ) const
  602. {
  603.     return m_argType;
  604. }
  605.  
  606. // ============================================================================
  607. const DWORD& VoteDef::flags( ) const
  608. {
  609.     return m_flags;
  610. }
  611.  
  612. // ============================================================================
  613. FBaseCVar* VoteDef::forbidCVar( ) const
  614. {
  615.     return m_forbidCVar;
  616. }
  617.  
  618. // ============================================================================
  619. unsigned VoteDef::numTypes( )
  620. {
  621.     return k_Defs.Size( );
  622. }
  623.  
  624. // ============================================================================
  625. const float& VoteDef::min( ) const
  626. {
  627.     return m_min;
  628. }
  629.  
  630. // ============================================================================
  631. const float& VoteDef::max( ) const
  632. {
  633.     return m_max;
  634. }
  635.  
  636. // ============================================================================
  637. const FString& VoteDef::description() const
  638. {
  639.     return m_description;
  640. }
  641.  
  642. // ============================================================================
  643. // Is the given player in the adminlist?
  644. bool VoteDef::playerIsAdmin( ULONG idx )
  645. {
  646.     NETADDRESS_s address = SERVER_GetClient( idx )->Address;
  647.     return SERVER_GetAdminList( )->isIPInList( address );
  648. }
  649.  
  650. // ============================================================================
  651. // List all vote types and information on how to use them.
  652. CCMD( listvotetypes )
  653. {
  654.     for ( unsigned i = 0; i < VoteDef::numTypes( ); i++ )
  655.     {
  656.         VoteDef* pVoteType = VoteDef::findByIndex( i );
  657.         FString message;
  658.         message.Format( "\\c[Orange]%s\\c-\n", pVoteType->getUsage( ).GetChars( ));
  659.  
  660.         // Add the description, if we have it.
  661.         FString descr = pVoteType->description( );
  662.         if ( descr.IsNotEmpty( )) {
  663.             message.AppendFormat( "%s\n", descr.GetChars( ));
  664.  
  665.             // Add a second newline to separate the vote types, but not if this
  666.             // is the last one left.
  667.             if ( i < VoteDef::numTypes() - 1 )
  668.                 message += "\n";
  669.         }
  670.  
  671.         Printf( "%s", message.GetChars( ));
  672.     }
  673. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement