summaryrefslogtreecommitdiff
path: root/src/game/g_cmds.c.orig
diff options
context:
space:
mode:
Diffstat (limited to 'src/game/g_cmds.c.orig')
-rw-r--r--src/game/g_cmds.c.orig3588
1 files changed, 3588 insertions, 0 deletions
diff --git a/src/game/g_cmds.c.orig b/src/game/g_cmds.c.orig
new file mode 100644
index 0000000..5a30f7b
--- /dev/null
+++ b/src/game/g_cmds.c.orig
@@ -0,0 +1,3588 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous is free software; you can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation; either version 2 of the License,
+or (at your option) any later version.
+
+Tremulous is distributed in the hope that it will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+
+#include "g_local.h"
+
+/*
+==================
+G_SanitiseString
+
+Remove color codes and non-alphanumeric characters from a string
+==================
+*/
+void G_SanitiseString( char *in, char *out, int len )
+{
+ len--;
+
+ while( *in && len > 0 )
+ {
+ if( Q_IsColorString( in ) )
+ {
+ in += 2; // skip color code
+ continue;
+ }
+
+ if( isalnum( *in ) )
+ {
+ *out++ = tolower( *in );
+ len--;
+ }
+ in++;
+ }
+ *out = 0;
+}
+
+/*
+==================
+G_ClientNumberFromString
+
+Returns a player number for either a number or name string
+Returns -1 and optionally sets err if invalid or not exactly 1 match
+err will have a trailing \n if set
+==================
+*/
+int G_ClientNumberFromString( char *s, char *err, int len )
+{
+ gclient_t *cl;
+ int i, found = 0, m = -1;
+ char s2[ MAX_NAME_LENGTH ];
+ char n2[ MAX_NAME_LENGTH ];
+ char *p = err;
+ int l, l2 = len;
+
+ if( !s[ 0 ] )
+ {
+ if( p )
+ Q_strncpyz( p, "no player name or slot # provided\n", len );
+
+ return -1;
+ }
+
+ // numeric values are just slot numbers
+ for( i = 0; s[ i ] && isdigit( s[ i ] ); i++ );
+ if( !s[ i ] )
+ {
+ i = atoi( s );
+
+ if( i < 0 || i >= level.maxclients )
+ return -1;
+
+ cl = &level.clients[ i ];
+
+ if( cl->pers.connected == CON_DISCONNECTED )
+ {
+ if( p )
+ Q_strncpyz( p, "no player connected in that slot #\n", len );
+
+ return -1;
+ }
+
+ return i;
+ }
+
+ G_SanitiseString( s, s2, sizeof( s2 ) );
+ if( !s2[ 0 ] )
+ {
+ if( p )
+ Q_strncpyz( p, "no player name provided\n", len );
+
+ return -1;
+ }
+
+ if( p )
+ {
+ Q_strncpyz( p, "more than one player name matches. "
+ "be more specific or use the slot #:\n", l2 );
+ l = strlen( p );
+ p += l;
+ l2 -= l;
+ }
+
+ // check for a name match
+ for( i = 0, cl = level.clients; i < level.maxclients; i++, cl++ )
+ {
+ if( cl->pers.connected == CON_DISCONNECTED )
+ continue;
+
+ G_SanitiseString( cl->pers.netname, n2, sizeof( n2 ) );
+
+ if( !strcmp( n2, s2 ) )
+ return i;
+
+ if( strstr( n2, s2 ) )
+ {
+ if( p )
+ {
+ l = Q_snprintf( p, l2, "%-2d - %s^7\n", i, cl->pers.netname );
+ p += l;
+ l2 -= l;
+ }
+
+ found++;
+ m = i;
+ }
+ }
+
+ if( found == 1 )
+ return m;
+
+ if( found == 0 && err )
+ Q_strncpyz( err, "no connected player by that name or slot #\n", len );
+
+ return -1;
+}
+
+/*
+==================
+G_ClientNumbersFromString
+
+Sets plist to an array of integers that represent client numbers that have
+names that are a partial match for s.
+
+Returns number of matching clientids up to max.
+==================
+*/
+int G_ClientNumbersFromString( char *s, int *plist, int max )
+{
+ gclient_t *p;
+ int i, found = 0;
+ char *endptr;
+ char n2[ MAX_NAME_LENGTH ] = {""};
+ char s2[ MAX_NAME_LENGTH ] = {""};
+
+ if( max == 0 )
+ return 0;
+
+ if( !s[ 0 ] )
+ return 0;
+
+ // if a number is provided, it is a clientnum
+ i = strtol( s, &endptr, 10 );
+ if( *endptr == '\0' )
+ {
+ if( i >= 0 && i < level.maxclients )
+ {
+ p = &level.clients[ i ];
+ if( p->pers.connected != CON_DISCONNECTED )
+ {
+ *plist = i;
+ return 1;
+ }
+ }
+ // we must assume that if only a number is provided, it is a clientNum
+ return 0;
+ }
+
+ // now look for name matches
+ G_SanitiseString( s, s2, sizeof( s2 ) );
+ if( !s2[ 0 ] )
+ return 0;
+ for( i = 0; i < level.maxclients && found < max; i++ )
+ {
+ p = &level.clients[ i ];
+ if( p->pers.connected == CON_DISCONNECTED )
+ {
+ continue;
+ }
+ G_SanitiseString( p->pers.netname, n2, sizeof( n2 ) );
+ if( strstr( n2, s2 ) )
+ {
+ *plist++ = i;
+ found++;
+ }
+ }
+ return found;
+}
+
+/*
+==================
+ScoreboardMessage
+
+==================
+*/
+void ScoreboardMessage( gentity_t *ent )
+{
+ char entry[ 1024 ];
+ char string[ 1400 ];
+ int stringlength;
+ int i, j;
+ gclient_t *cl;
+ int numSorted;
+ weapon_t weapon = WP_NONE;
+ upgrade_t upgrade = UP_NONE;
+
+ // send the latest information on all clients
+ string[ 0 ] = 0;
+ stringlength = 0;
+
+ numSorted = level.numConnectedClients;
+
+ for( i = 0; i < numSorted; i++ )
+ {
+ int ping;
+
+ cl = &level.clients[ level.sortedClients[ i ] ];
+
+ if( cl->pers.connected == CON_CONNECTING )
+ ping = -1;
+ else
+ ping = cl->ps.ping < 999 ? cl->ps.ping : 999;
+
+ if( cl->sess.spectatorState == SPECTATOR_NOT &&
+ ( ent->client->pers.teamSelection == TEAM_NONE ||
+ cl->pers.teamSelection == ent->client->pers.teamSelection ) )
+ {
+ weapon = cl->ps.weapon;
+
+ if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, cl->ps.stats ) )
+ upgrade = UP_BATTLESUIT;
+ else if( BG_InventoryContainsUpgrade( UP_JETPACK, cl->ps.stats ) )
+ upgrade = UP_JETPACK;
+ else if( BG_InventoryContainsUpgrade( UP_BATTPACK, cl->ps.stats ) )
+ upgrade = UP_BATTPACK;
+ else if( BG_InventoryContainsUpgrade( UP_HELMET_MK1, cl->ps.stats ) )
+ upgrade = UP_HELMET_MK1;
+ else if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, cl->ps.stats ) )
+ upgrade = UP_HELMET_MK2;
+ else if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, cl->ps.stats ) )
+ upgrade = UP_LIGHTARMOUR;
+ else
+ upgrade = UP_NONE;
+ }
+ else
+ {
+ weapon = WP_NONE;
+ upgrade = UP_NONE;
+ }
+
+ Com_sprintf( entry, sizeof( entry ),
+ " %d %d %d %d %d %d", level.sortedClients[ i ], cl->ps.persistant[ PERS_SCORE ],
+ ping, ( level.time - cl->pers.enterTime ) / 60000, weapon, upgrade );
+
+ j = strlen( entry );
+
+ if( stringlength + j >= 1024 )
+ break;
+
+ strcpy( string + stringlength, entry );
+ stringlength += j;
+ }
+
+ trap_SendServerCommand( ent-g_entities, va( "scores %i %i%s",
+ level.alienKills, level.humanKills, string ) );
+}
+
+
+/*
+==================
+ConcatArgs
+==================
+*/
+char *ConcatArgs( int start )
+{
+ int i, c, tlen;
+ static char line[ MAX_STRING_CHARS ];
+ int len;
+ char arg[ MAX_STRING_CHARS ];
+
+ len = 0;
+ c = trap_Argc( );
+
+ for( i = start; i < c; i++ )
+ {
+ trap_Argv( i, arg, sizeof( arg ) );
+ tlen = strlen( arg );
+
+ if( len + tlen >= MAX_STRING_CHARS - 1 )
+ break;
+
+ memcpy( line + len, arg, tlen );
+ len += tlen;
+
+ if( len == MAX_STRING_CHARS - 1 )
+ break;
+
+ if( i != c - 1 )
+ {
+ line[ len ] = ' ';
+ len++;
+ }
+ }
+
+ line[ len ] = 0;
+
+ return line;
+}
+
+/*
+==================
+ConcatArgsPrintable
+Duplicate of concatargs but enquotes things that need to be
+Used to log command arguments in a way that preserves user intended tokenizing
+==================
+*/
+char *ConcatArgsPrintable( int start )
+{
+ int i, c, tlen;
+ static char line[ MAX_STRING_CHARS ];
+ int len;
+ char arg[ MAX_STRING_CHARS + 2 ];
+ char *printArg;
+
+ len = 0;
+ c = trap_Argc( );
+
+ for( i = start; i < c; i++ )
+ {
+ printArg = arg;
+ trap_Argv( i, arg, sizeof( arg ) );
+ if( strchr( arg, ' ' ) )
+ printArg = va( "\"%s\"", arg );
+ tlen = strlen( printArg );
+
+ if( len + tlen >= MAX_STRING_CHARS - 1 )
+ break;
+
+ memcpy( line + len, printArg, tlen );
+ len += tlen;
+
+ if( len == MAX_STRING_CHARS - 1 )
+ break;
+
+ if( i != c - 1 )
+ {
+ line[ len ] = ' ';
+ len++;
+ }
+ }
+
+ line[ len ] = 0;
+
+ return line;
+}
+
+
+/*
+==================
+Cmd_Give_f
+
+Give items to a client
+==================
+*/
+void Cmd_Give_f( gentity_t *ent )
+{
+ char *name;
+ qboolean give_all = qfalse;
+
+ if( trap_Argc( ) < 2 )
+ {
+ ADMP( "usage: give [what]\n" );
+ ADMP( "usage: valid choices are: all, health, funds [amount], stamina, "
+ "poison, gas, ammo\n" );
+ return;
+ }
+
+ name = ConcatArgs( 1 );
+ if( Q_stricmp( name, "all" ) == 0 )
+ give_all = qtrue;
+
+ if( give_all || Q_stricmp( name, "health" ) == 0 )
+ {
+ ent->health = ent->client->ps.stats[ STAT_MAX_HEALTH ];
+ BG_AddUpgradeToInventory( UP_MEDKIT, ent->client->ps.stats );
+ }
+
+ if( give_all || Q_stricmpn( name, "funds", 5 ) == 0 )
+ {
+ float credits;
+
+ if( give_all || trap_Argc( ) < 3 )
+ credits = 30000.0f;
+ else
+ {
+ credits = atof( name + 6 ) *
+ ( ent->client->pers.teamSelection ==
+ TEAM_ALIENS ? ALIEN_CREDITS_PER_KILL : 1.0f );
+
+ // clamp credits manually, as G_AddCreditToClient() expects a short int
+ if( credits > SHRT_MAX )
+ credits = 30000.0f;
+ else if( credits < SHRT_MIN )
+ credits = -30000.0f;
+ }
+
+ G_AddCreditToClient( ent->client, (short)credits, qtrue );
+ }
+
+ if( give_all || Q_stricmp( name, "stamina" ) == 0 )
+ ent->client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX;
+
+ if( Q_stricmp( name, "poison" ) == 0 )
+ {
+ if( ent->client->pers.teamSelection == TEAM_HUMANS )
+ {
+ ent->client->ps.stats[ STAT_STATE ] |= SS_POISONED;
+ ent->client->lastPoisonTime = level.time;
+ ent->client->lastPoisonClient = ent;
+ }
+ else
+ {
+ ent->client->ps.stats[ STAT_STATE ] |= SS_BOOSTED;
+ ent->client->boostedTime = level.time;
+ }
+ }
+
+ if( Q_stricmp( name, "gas" ) == 0 )
+ {
+ ent->client->ps.eFlags |= EF_POISONCLOUDED;
+ ent->client->lastPoisonCloudedTime = level.time;
+ trap_SendServerCommand( ent->client->ps.clientNum,
+ "poisoncloud" );
+ }
+
+ if( give_all || Q_stricmp( name, "ammo" ) == 0 )
+ {
+ gclient_t *client = ent->client;
+
+ if( client->ps.weapon != WP_ALEVEL3_UPG &&
+ BG_Weapon( client->ps.weapon )->infiniteAmmo )
+ return;
+
+ client->ps.ammo = BG_Weapon( client->ps.weapon )->maxAmmo;
+ client->ps.clips = BG_Weapon( client->ps.weapon )->maxClips;
+
+ if( BG_Weapon( client->ps.weapon )->usesEnergy &&
+ BG_InventoryContainsUpgrade( UP_BATTPACK, client->ps.stats ) )
+ client->ps.ammo = (int)( (float)client->ps.ammo * BATTPACK_MODIFIER );
+ }
+
+ if( give_all || Q_stricmp( name, "fuel" ) == 0 )
+ {
+ if( BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) )
+ ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL;
+ }
+}
+
+
+/*
+==================
+Cmd_God_f
+
+Sets client to godmode
+
+argv(0) god
+==================
+*/
+void Cmd_God_f( gentity_t *ent )
+{
+ char *msg;
+
+ ent->flags ^= FL_GODMODE;
+
+ if( !( ent->flags & FL_GODMODE ) )
+ msg = "godmode OFF\n";
+ else
+ msg = "godmode ON\n";
+
+ trap_SendServerCommand( ent - g_entities, va( "print \"%s\"", msg ) );
+}
+
+
+/*
+==================
+Cmd_Notarget_f
+
+Sets client to notarget
+
+argv(0) notarget
+==================
+*/
+void Cmd_Notarget_f( gentity_t *ent )
+{
+ char *msg;
+
+ ent->flags ^= FL_NOTARGET;
+
+ if( !( ent->flags & FL_NOTARGET ) )
+ msg = "notarget OFF\n";
+ else
+ msg = "notarget ON\n";
+
+ trap_SendServerCommand( ent - g_entities, va( "print \"%s\"", msg ) );
+}
+
+
+/*
+==================
+Cmd_Noclip_f
+
+argv(0) noclip
+==================
+*/
+void Cmd_Noclip_f( gentity_t *ent )
+{
+ char *msg;
+
+ if( ent->client->noclip )
+ msg = "noclip OFF\n";
+ else
+ msg = "noclip ON\n";
+
+ ent->client->noclip = !ent->client->noclip;
+
+ trap_SendServerCommand( ent - g_entities, va( "print \"%s\"", msg ) );
+}
+
+
+/*
+==================
+Cmd_LevelShot_f
+
+This is just to help generate the level pictures
+for the menus. It goes to the intermission immediately
+and sends over a command to the client to resize the view,
+hide the scoreboard, and take a special screenshot
+==================
+*/
+void Cmd_LevelShot_f( gentity_t *ent )
+{
+ BeginIntermission( );
+ trap_SendServerCommand( ent - g_entities, "clientLevelShot" );
+}
+
+/*
+=================
+Cmd_Kill_f
+=================
+*/
+void Cmd_Kill_f( gentity_t *ent )
+{
+ if( g_cheats.integer )
+ {
+ ent->flags &= ~FL_GODMODE;
+ ent->client->ps.stats[ STAT_HEALTH ] = ent->health = 0;
+ player_die( ent, ent, ent, 100000, MOD_SUICIDE );
+ }
+ else
+ {
+ if( ent->suicideTime == 0 )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You will suicide in 20 seconds\n\"" );
+ ent->suicideTime = level.time + 20000;
+ }
+ else if( ent->suicideTime > level.time )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"Suicide cancelled\n\"" );
+ ent->suicideTime = 0;
+ }
+ }
+}
+
+/*
+=================
+Cmd_Team_f
+=================
+*/
+void Cmd_Team_f( gentity_t *ent )
+{
+ team_t team;
+ team_t oldteam = ent->client->pers.teamSelection;
+ char s[ MAX_TOKEN_CHARS ];
+ qboolean force = G_admin_permission( ent, ADMF_FORCETEAMCHANGE );
+ int aliens = level.numAlienClients;
+ int humans = level.numHumanClients;
+
+ if( oldteam == TEAM_ALIENS )
+ aliens--;
+ else if( oldteam == TEAM_HUMANS )
+ humans--;
+
+ // stop team join spam
+ if( ent->client->pers.teamChangeTime &&
+ level.time - ent->client->pers.teamChangeTime < 1000 )
+ return;
+
+ // stop switching teams for gameplay exploit reasons by enforcing a long
+ // wait before they can come back
+ if( !force && !g_cheats.integer && ent->client->pers.aliveSeconds &&
+ level.time - ent->client->pers.teamChangeTime < 30000 )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"You must wait another %d seconds before changing teams again\n\"",
+ (int) ( ( 30000 - ( level.time - ent->client->pers.teamChangeTime ) ) / 1000.f ) ) );
+ return;
+ }
+
+ // disallow joining teams during warmup
+ if( g_doWarmup.integer && ( ( level.warmupTime - level.time ) / 1000 ) > 0 )
+ {
+ G_TriggerMenu( ent - g_entities, MN_WARMUP );
+ return;
+ }
+
+ trap_Argv( 1, s, sizeof( s ) );
+
+ if( !s[ 0 ] )
+ {
+ trap_SendServerCommand( ent-g_entities, va( "print \"team: %s\n\"",
+ BG_TeamName( oldteam ) ) );
+ return;
+ }
+
+ if( !Q_stricmp( s, "auto" ) )
+ {
+ if( level.humanTeamLocked && level.alienTeamLocked )
+ team = TEAM_NONE;
+ else if( level.humanTeamLocked || humans > aliens )
+ team = TEAM_ALIENS;
+
+ else if( level.alienTeamLocked || aliens > humans )
+ team = TEAM_HUMANS;
+ else
+ team = TEAM_ALIENS + rand( ) / ( RAND_MAX / 2 + 1 );
+ }
+ else switch( G_TeamFromString( s ) )
+ {
+ case TEAM_NONE:
+ team = TEAM_NONE;
+ break;
+
+ case TEAM_ALIENS:
+ if( level.alienTeamLocked )
+ {
+ G_TriggerMenu( ent - g_entities, MN_A_TEAMLOCKED );
+ return;
+ }
+ else if( level.humanTeamLocked )
+ force = qtrue;
+
+ if( !force && g_teamForceBalance.integer && aliens > humans )
+ {
+ G_TriggerMenu( ent - g_entities, MN_A_TEAMFULL );
+ return;
+ }
+
+ team = TEAM_ALIENS;
+ break;
+
+ case TEAM_HUMANS:
+ if( level.humanTeamLocked )
+ {
+ G_TriggerMenu( ent - g_entities, MN_H_TEAMLOCKED );
+ return;
+ }
+ else if( level.alienTeamLocked )
+ force = qtrue;
+
+ if( !force && g_teamForceBalance.integer && humans > aliens )
+ {
+ G_TriggerMenu( ent - g_entities, MN_H_TEAMFULL );
+ return;
+ }
+
+ team = TEAM_HUMANS;
+ break;
+
+ default:
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"Unknown team: %s\n\"", s ) );
+ return;
+ }
+
+ // stop team join spam
+ if( oldteam == team )
+ return;
+
+ if( team != TEAM_NONE && g_maxGameClients.integer &&
+ level.numPlayingClients >= g_maxGameClients.integer )
+ {
+ G_TriggerMenu( ent - g_entities, MN_PLAYERLIMIT );
+ return;
+ }
+
+ // Apply the change
+ G_ChangeTeam( ent, team );
+}
+
+/*
+==================
+G_CensorString
+==================
+*/
+static char censors[ 20000 ];
+static int numcensors;
+
+void G_LoadCensors( void )
+{
+ char *text_p, *token;
+ char text[ 20000 ];
+ char *term;
+ int len;
+ fileHandle_t f;
+
+ numcensors = 0;
+
+ if( !g_censorship.string[ 0 ] )
+ return;
+
+ len = trap_FS_FOpenFile( g_censorship.string, &f, FS_READ );
+ if( len < 0 )
+ {
+ Com_Printf( S_COLOR_RED "ERROR: Censors file %s doesn't exist\n",
+ g_censorship.string );
+ return;
+ }
+ if( len == 0 || len >= sizeof( text ) - 1 )
+ {
+ trap_FS_FCloseFile( f );
+ Com_Printf( S_COLOR_RED "ERROR: Censors file %s is %s\n",
+ g_censorship.string, len == 0 ? "empty" : "too long" );
+ return;
+ }
+ trap_FS_Read( text, len, f );
+ trap_FS_FCloseFile( f );
+ text[ len ] = 0;
+
+ term = censors;
+
+ text_p = text;
+ while( 1 )
+ {
+ token = COM_Parse( &text_p );
+ if( !*token || sizeof( censors ) - ( term - censors ) < 4 )
+ break;
+ Q_strncpyz( term, token, sizeof( censors ) - ( term - censors ) );
+ Q_strlwr( term );
+ term += strlen( term ) + 1;
+ if( sizeof( censors ) - ( term - censors ) == 0 )
+ break;
+ token = COM_ParseExt( &text_p, qfalse );
+ Q_strncpyz( term, token, sizeof( censors ) - ( term - censors ) );
+ term += strlen( term ) + 1;
+ numcensors++;
+ }
+ G_Printf( "Parsed %d string replacements\n", numcensors );
+}
+
+void G_CensorString( char *out, const char *in, int len, gentity_t *ent )
+{
+ const char *s, *m;
+ int i;
+
+ if( !numcensors || G_admin_permission( ent, ADMF_NOCENSORFLOOD) )
+ {
+ Q_strncpyz( out, in, len );
+ return;
+ }
+
+ len--;
+ while( *in )
+ {
+ if( Q_IsColorString( in ) )
+ {
+ if( len < 2 )
+ break;
+ *out++ = *in++;
+ *out++ = *in++;
+ len -= 2;
+ continue;
+ }
+ if( !isalnum( *in ) )
+ {
+ if( len < 1 )
+ break;
+ *out++ = *in++;
+ len--;
+ continue;
+ }
+ m = censors;
+ for( i = 0; i < numcensors; i++, m++ )
+ {
+ s = in;
+ while( *s && *m )
+ {
+ if( Q_IsColorString( s ) )
+ {
+ s += 2;
+ continue;
+ }
+ if( !isalnum( *s ) )
+ {
+ s++;
+ continue;
+ }
+ if( tolower( *s ) != *m )
+ break;
+ s++;
+ m++;
+ }
+ // match
+ if( !*m )
+ {
+ in = s;
+ m++;
+ while( *m )
+ {
+ if( len < 1 )
+ break;
+ *out++ = *m++;
+ len--;
+ }
+ break;
+ }
+ else
+ {
+ while( *m )
+ m++;
+ m++;
+ while( *m )
+ m++;
+ }
+ }
+ if( len < 1 )
+ break;
+ // no match
+ if( i == numcensors )
+ {
+ *out++ = *in++;
+ len--;
+ }
+ }
+ *out = 0;
+}
+
+/*
+==================
+G_Say
+==================
+*/
+static qboolean G_SayTo( gentity_t *ent, gentity_t *other, saymode_t mode, const char *message )
+{
+ if( !other )
+ return qfalse;
+
+ if( !other->inuse )
+ return qfalse;
+
+ if( !other->client )
+ return qfalse;
+
+ if( other->client->pers.connected != CON_CONNECTED )
+ return qfalse;
+
+ if( ( ent && !OnSameTeam( ent, other ) ) &&
+ ( mode == SAY_TEAM || mode == SAY_AREA || mode == SAY_TPRIVMSG ) )
+ {
+ if( other->client->pers.teamSelection != TEAM_NONE )
+ return qfalse;
+
+ // specs with ADMF_SPEC_ALLCHAT flag can see team chat
+ if( !G_admin_permission( other, ADMF_SPEC_ALLCHAT ) && mode != SAY_TPRIVMSG )
+ return qfalse;
+ }
+
+ trap_SendServerCommand( other-g_entities, va( "chat %d %d \"%s\"",
+ ent ? ent-g_entities : -1,
+ mode,
+ message ) );
+
+ return qtrue;
+}
+
+void G_Say( gentity_t *ent, saymode_t mode, const char *chatText )
+{
+ int j;
+ gentity_t *other;
+ // don't let text be too long for malicious reasons
+ char text[ MAX_SAY_TEXT ];
+
+ // check if blocked by g_specChat 0
+ if( ( !g_specChat.integer ) && ( mode != SAY_TEAM ) &&
+ ( ent ) && ( ent->client->pers.teamSelection == TEAM_NONE ) &&
+ ( !G_admin_permission( ent, ADMF_NOCENSORFLOOD ) ) )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"say: Global chatting for "
+ "spectators has been disabled. You may only use team chat.\n\"" );
+ mode = SAY_TEAM;
+ }
+
+ switch( mode )
+ {
+ case SAY_ALL:
+ G_LogPrintf( "Say: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_GREEN "%s\n",
+ ( ent ) ? ent - g_entities : -1,
+ ( ent ) ? ent->client->pers.netname : "console", chatText );
+ break;
+ case SAY_TEAM:
+ // console say_team is handled in g_svscmds, not here
+ if( !ent || !ent->client )
+ Com_Error( ERR_FATAL, "SAY_TEAM by non-client entity\n" );
+ G_LogPrintf( "SayTeam: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_CYAN "%s\n",
+ ent - g_entities, ent->client->pers.netname, chatText );
+ break;
+ case SAY_RAW:
+ if( ent )
+ Com_Error( ERR_FATAL, "SAY_RAW by client entity\n" );
+ G_LogPrintf( "Chat: -1 \"console\": %s\n", chatText );
+ default:
+ break;
+ }
+
+ G_CensorString( text, chatText, sizeof( text ), ent );
+
+ // send it to all the apropriate clients
+ for( j = 0; j < level.maxclients; j++ )
+ {
+ other = &g_entities[ j ];
+ G_SayTo( ent, other, mode, text );
+ }
+}
+
+/*
+==================
+Cmd_SayArea_f
+==================
+*/
+static void Cmd_SayArea_f( gentity_t *ent )
+{
+ int entityList[ MAX_GENTITIES ];
+ int num, i;
+ vec3_t range = { 1000.0f, 1000.0f, 1000.0f };
+ vec3_t mins, maxs;
+ char *msg;
+
+ if( trap_Argc( ) < 2 )
+ {
+ ADMP( "usage: say_area [message]\n" );
+ return;
+ }
+
+ msg = ConcatArgs( 1 );
+
+ for(i = 0; i < 3; i++ )
+ range[ i ] = g_sayAreaRange.value;
+
+ G_LogPrintf( "SayArea: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_BLUE "%s\n",
+ ent - g_entities, ent->client->pers.netname, msg );
+
+ VectorAdd( ent->s.origin, range, maxs );
+ VectorSubtract( ent->s.origin, range, mins );
+
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ G_SayTo( ent, &g_entities[ entityList[ i ] ], SAY_AREA, msg );
+
+ //Send to ADMF_SPEC_ALLCHAT candidates
+ for( i = 0; i < level.maxclients; i++ )
+ {
+ if( g_entities[ i ].client->pers.teamSelection == TEAM_NONE &&
+ G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) )
+ {
+ G_SayTo( ent, &g_entities[ i ], SAY_AREA, msg );
+ }
+ }
+}
+
+
+/*
+==================
+Cmd_Say_f
+==================
+*/
+static void Cmd_Say_f( gentity_t *ent )
+{
+ char *p;
+ char cmd[ MAX_TOKEN_CHARS ];
+ saymode_t mode = SAY_ALL;
+
+ if( trap_Argc( ) < 2 )
+ return;
+
+ trap_Argv( 0, cmd, sizeof( cmd ) );
+ if( Q_stricmp( cmd, "say_team" ) == 0 )
+ mode = SAY_TEAM;
+
+ p = ConcatArgs( 1 );
+
+ G_Say( ent, mode, p );
+}
+
+/*
+==================
+Cmd_VSay_f
+==================
+*/
+void Cmd_VSay_f( gentity_t *ent )
+{
+ char arg[MAX_TOKEN_CHARS];
+ char text[ MAX_TOKEN_CHARS ];
+ voiceChannel_t vchan;
+ voice_t *voice;
+ voiceCmd_t *cmd;
+ voiceTrack_t *track;
+ int cmdNum = 0;
+ int trackNum = 0;
+ char voiceName[ MAX_VOICE_NAME_LEN ] = {"default"};
+ char voiceCmd[ MAX_VOICE_CMD_LEN ] = {""};
+ char vsay[ 12 ] = {""};
+ weapon_t weapon;
+
+ if( !ent || !ent->client )
+ Com_Error( ERR_FATAL, "Cmd_VSay_f() called by non-client entity\n" );
+
+ trap_Argv( 0, arg, sizeof( arg ) );
+ if( trap_Argc( ) < 2 )
+ {
+ trap_SendServerCommand( ent-g_entities, va(
+ "print \"usage: %s command [text] \n\"", arg ) );
+ return;
+ }
+ if( !level.voices )
+ {
+ trap_SendServerCommand( ent-g_entities, va(
+ "print \"%s: voice system is not installed on this server\n\"", arg ) );
+ return;
+ }
+ if( !g_voiceChats.integer )
+ {
+ trap_SendServerCommand( ent-g_entities, va(
+ "print \"%s: voice system administratively disabled on this server\n\"",
+ arg ) );
+ return;
+ }
+ if( !Q_stricmp( arg, "vsay" ) )
+ vchan = VOICE_CHAN_ALL;
+ else if( !Q_stricmp( arg, "vsay_team" ) )
+ vchan = VOICE_CHAN_TEAM;
+ else if( !Q_stricmp( arg, "vsay_local" ) )
+ vchan = VOICE_CHAN_LOCAL;
+ else
+ return;
+ Q_strncpyz( vsay, arg, sizeof( vsay ) );
+
+ if( ent->client->pers.voice[ 0 ] )
+ Q_strncpyz( voiceName, ent->client->pers.voice, sizeof( voiceName ) );
+ voice = BG_VoiceByName( level.voices, voiceName );
+ if( !voice )
+ {
+ trap_SendServerCommand( ent-g_entities, va(
+ "print \"%s: voice '%s' not found\n\"", vsay, voiceName ) );
+ return;
+ }
+
+ trap_Argv( 1, voiceCmd, sizeof( voiceCmd ) ) ;
+ cmd = BG_VoiceCmdFind( voice->cmds, voiceCmd, &cmdNum );
+ if( !cmd )
+ {
+ trap_SendServerCommand( ent-g_entities, va(
+ "print \"%s: command '%s' not found in voice '%s'\n\"",
+ vsay, voiceCmd, voiceName ) );
+ return;
+ }
+
+ // filter non-spec humans by their primary weapon as well
+ weapon = WP_NONE;
+ if( ent->client->sess.spectatorState == SPECTATOR_NOT )
+ {
+ weapon = BG_PrimaryWeapon( ent->client->ps.stats );
+ }
+
+ track = BG_VoiceTrackFind( cmd->tracks, ent->client->pers.teamSelection,
+ ent->client->pers.classSelection, weapon, (int)ent->client->voiceEnthusiasm,
+ &trackNum );
+ if( !track )
+ {
+ trap_SendServerCommand( ent-g_entities, va(
+ "print \"%s: no available track for command '%s', team %d, "
+ "class %d, weapon %d, and enthusiasm %d in voice '%s'\n\"",
+ vsay, voiceCmd, ent->client->pers.teamSelection,
+ ent->client->pers.classSelection, weapon,
+ (int)ent->client->voiceEnthusiasm, voiceName ) );
+ return;
+ }
+
+ if( !Q_stricmp( ent->client->lastVoiceCmd, cmd->cmd ) )
+ ent->client->voiceEnthusiasm++;
+
+ Q_strncpyz( ent->client->lastVoiceCmd, cmd->cmd,
+ sizeof( ent->client->lastVoiceCmd ) );
+
+ // optional user supplied text
+ trap_Argv( 2, arg, sizeof( arg ) );
+ G_CensorString( text, arg, sizeof( text ), ent );
+
+ switch( vchan )
+ {
+ case VOICE_CHAN_ALL:
+ case VOICE_CHAN_LOCAL:
+ trap_SendServerCommand( -1, va(
+ "voice %d %d %d %d \"%s\"\n",
+ ent-g_entities, vchan, cmdNum, trackNum, text ) );
+ break;
+ case VOICE_CHAN_TEAM:
+ G_TeamCommand( ent->client->pers.teamSelection, va(
+ "voice %d %d %d %d \"%s\"\n",
+ ent-g_entities, vchan, cmdNum, trackNum, text ) );
+ break;
+ default:
+ break;
+ }
+}
+
+/*
+==================
+Cmd_Where_f
+==================
+*/
+void Cmd_Where_f( gentity_t *ent )
+{
+ if( !ent->client )
+ return;
+ trap_SendServerCommand( ent - g_entities,
+ va( "print \"origin: %f %f %f\n\"",
+ ent->s.origin[ 0 ], ent->s.origin[ 1 ],
+ ent->s.origin[ 2 ] ) );
+}
+
+/*
+==================
+Cmd_CallVote_f
+==================
+*/
+void Cmd_CallVote_f( gentity_t *ent )
+{
+ char cmd[ MAX_TOKEN_CHARS ],
+ vote[ MAX_TOKEN_CHARS ],
+ arg[ MAX_TOKEN_CHARS ];
+ char name[ MAX_NAME_LENGTH ] = "";
+ char caller[ MAX_NAME_LENGTH ] = "";
+ char reason[ MAX_TOKEN_CHARS ];
+ char *creason;
+ int clientNum = -1;
+ int id = -1;
+ team_t team;
+
+ trap_Argv( 0, cmd, sizeof( cmd ) );
+ trap_Argv( 1, vote, sizeof( vote ) );
+ trap_Argv( 2, arg, sizeof( arg ) );
+ creason = ConcatArgs( 3 );
+ G_DecolorString( creason, reason, sizeof( reason ) );
+
+ if( !Q_stricmp( cmd, "callteamvote" ) )
+ team = ent->client->pers.teamSelection;
+ else
+ team = TEAM_NONE;
+
+ if( !g_allowVote.integer )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: voting not allowed here\n\"", cmd ) );
+ return;
+ }
+
+ if( level.voteTime[ team ] )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: a vote is already in progress\n\"", cmd ) );
+ return;
+ }
+
+ if( level.voteExecuteTime[ team ] )
+ G_ExecuteVote( team );
+
+ level.voteDelay[ team ] = 0;
+ level.voteThreshold[ team ] = 50;
+
+ if( g_voteLimit.integer > 0 &&
+ ent->client->pers.namelog->voteCount >= g_voteLimit.integer &&
+ !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) )
+ {
+ trap_SendServerCommand( ent-g_entities, va(
+ "print \"%s: you have already called the maximum number of votes (%d)\n\"",
+ cmd, g_voteLimit.integer ) );
+ return;
+ }
+
+ // kick, mute, unmute, denybuild, allowbuild
+ if( !Q_stricmp( vote, "kick" ) ||
+ !Q_stricmp( vote, "mute" ) || !Q_stricmp( vote, "unmute" ) ||
+ !Q_stricmp( vote, "denybuild" ) || !Q_stricmp( vote, "allowbuild" ) )
+ {
+ char err[ MAX_STRING_CHARS ];
+
+ if( !arg[ 0 ] )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: no target\n\"", cmd ) );
+ return;
+ }
+
+ // with a little extra work only players from the right team are considered
+ clientNum = G_ClientNumberFromString( arg, err, sizeof( err ) );
+
+ if( clientNum == -1 )
+ {
+ ADMP( va( "%s: %s", cmd, err ) );
+ return;
+ }
+
+ G_DecolorString( level.clients[ clientNum ].pers.netname, name, sizeof( name ) );
+ id = level.clients[ clientNum ].pers.namelog->id;
+
+ if( !Q_stricmp( vote, "kick" ) || !Q_stricmp( vote, "mute" ) ||
+ !Q_stricmp( vote, "denybuild" ) )
+ {
+ if( G_admin_permission( g_entities + clientNum, ADMF_IMMUNITY ) )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: admin is immune\n\"", cmd ) );
+
+ G_AdminMessage( NULL, va( S_COLOR_WHITE "%s" S_COLOR_YELLOW " attempted %s %s"
+ " on immune admin " S_COLOR_WHITE "%s" S_COLOR_YELLOW
+ " for: %s",
+ ent->client->pers.netname, cmd, vote,
+ g_entities[ clientNum ].client->pers.netname,
+ reason[ 0 ] ? reason : "no reason" ) );
+ return;
+ }
+
+ if( team != TEAM_NONE &&
+ ( ent->client->pers.teamSelection !=
+ level.clients[ clientNum ].pers.teamSelection ) )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: player is not on your team\n\"", cmd ) );
+ return;
+ }
+
+ if( !reason[ 0 ] && !G_admin_permission( ent, ADMF_UNACCOUNTABLE ) )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: You must provide a reason\n\"", cmd ) );
+ return;
+ }
+ }
+ }
+
+ if( !Q_stricmp( vote, "kick" ) )
+ {
+ if( level.clients[ clientNum ].pers.localClient )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: admin is immune\n\"", cmd ) );
+ return;
+ }
+
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "ban %s \"1s%s\" vote kick (%s)", level.clients[ clientNum ].pers.ip.str,
+ g_adminTempBan.string, reason );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ), "Kick player '%s'", name );
+ if( reason[ 0 ] )
+ {
+ Q_strcat( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) );
+ }
+ }
+ else if( team == TEAM_NONE )
+ {
+ if( !Q_stricmp( vote, "mute" ) )
+ {
+ if( level.clients[ clientNum ].pers.namelog->muted )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: player is already muted\n\"", cmd ) );
+ return;
+ }
+
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "mute %d", id );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ),
+ "Mute player '%s'", name );
+ if( reason[ 0 ] )
+ {
+ Q_strcat( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) );
+ }
+ }
+ else if( !Q_stricmp( vote, "unmute" ) )
+ {
+ if( !level.clients[ clientNum ].pers.namelog->muted )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: player is not currently muted\n\"", cmd ) );
+ return;
+ }
+
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "unmute %d", id );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ),
+ "Unmute player '%s'", name );
+ }
+ else if( !Q_stricmp( vote, "map_restart" ) )
+ {
+ strcpy( level.voteString[ team ], vote );
+ strcpy( level.voteDisplayString[ team ], "Restart current map" );
+ // map_restart comes with a default delay
+ }
+ else if( !Q_stricmp( vote, "map" ) )
+ {
+ if( !G_MapExists( arg ) )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: 'maps/%s.bsp' could not be found on the server\n\"",
+ cmd, arg ) );
+ return;
+ }
+
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString ),
+ "%s \"%s\"", vote, arg );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ),
+ "Change to map '%s'", arg );
+ level.voteDelay[ team ] = 3000;
+ }
+ else if( !Q_stricmp( vote, "nextmap" ) )
+ {
+ if( G_MapExists( g_nextMap.string ) )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: the next map is already set to '%s'\n\"",
+ cmd, g_nextMap.string ) );
+ return;
+ }
+
+ if( !G_MapExists( arg ) )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: 'maps/%s.bsp' could not be found on the server\n\"",
+ cmd, arg ) );
+ return;
+ }
+
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "set g_nextMap \"%s\"", arg );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ),
+ "Set the next map to '%s'", arg );
+ }
+ else if( !Q_stricmp( vote, "draw" ) )
+ {
+ strcpy( level.voteString[ team ], "evacuation" );
+ strcpy( level.voteDisplayString[ team ], "End match in a draw" );
+ level.voteDelay[ team ] = 3000;
+ }
+ else if( !Q_stricmp( vote, "sudden_death" ) )
+ {
+ if(!g_suddenDeathVotePercent.integer)
+ {
+ trap_SendServerCommand( ent-g_entities,
+ "print \"Sudden Death votes have been disabled\n\"" );
+ return;
+ }
+ if( G_TimeTilSuddenDeath( ) <= 0 )
+ {
+ trap_SendServerCommand( ent - g_entities,
+ va( "print \"callvote: Sudden Death has already begun\n\"") );
+ return;
+ }
+ if( level.suddenDeathBeginTime > 0 &&
+ G_TimeTilSuddenDeath() <= g_suddenDeathVoteDelay.integer * 1000 )
+ {
+ trap_SendServerCommand( ent - g_entities,
+ va( "print \"callvote: Sudden Death is imminent\n\"") );
+ return;
+ }
+ level.voteThreshold[ team ] = g_suddenDeathVotePercent.integer;
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "suddendeath %d", g_suddenDeathVoteDelay.integer );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ),
+ "Begin sudden death in %d seconds",
+ g_suddenDeathVoteDelay.integer );
+ }
+ else
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string\n\"" );
+ trap_SendServerCommand( ent-g_entities, "print \"Valid vote commands are: "
+ "map, nextmap, map_restart, draw, sudden_death, kick, mute and unmute\n" );
+ return;
+ }
+ }
+ else if( !Q_stricmp( vote, "denybuild" ) )
+ {
+ if( level.clients[ clientNum ].pers.namelog->denyBuild )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: player already lost building rights\n\"", cmd ) );
+ return;
+ }
+
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "denybuild %d", id );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ),
+ "Take away building rights from '%s'", name );
+ if( reason[ 0 ] )
+ {
+ Q_strcat( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) );
+ }
+ }
+ else if( !Q_stricmp( vote, "allowbuild" ) )
+ {
+ if( !level.clients[ clientNum ].pers.namelog->denyBuild )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: player already has building rights\n\"", cmd ) );
+ return;
+ }
+
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "allowbuild %d", id );
+ Com_sprintf( level.voteDisplayString[ team ],
+ sizeof( level.voteDisplayString[ team ] ),
+ "Allow '%s' to build", name );
+ }
+ else if( !Q_stricmp( vote, "admitdefeat" ) )
+ {
+ Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ),
+ "admitdefeat %d", team );
+ strcpy( level.voteDisplayString[ team ], "Admit Defeat" );
+ level.voteDelay[ team ] = 3000;
+ }
+ else
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string\n\"" );
+ trap_SendServerCommand( ent-g_entities,
+ "print \"Valid team vote commands are: "
+ "kick, denybuild, allowbuild and admitdefeat\n\"" );
+ return;
+ }
+
+ G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\": %s\n",
+ team == TEAM_NONE ? "CallVote" : "CallTeamVote",
+ ent - g_entities, ent->client->pers.netname, level.voteString[ team ] );
+
+ if( team == TEAM_NONE )
+ {
+ trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE
+ " called a vote: %s\n\"", ent->client->pers.netname,
+ level.voteDisplayString[ team ] ) );
+ }
+ else
+ {
+ int i;
+
+ for( i = 0 ; i < level.maxclients ; i++ )
+ {
+ if( level.clients[ i ].pers.connected == CON_CONNECTED )
+ {
+ if( level.clients[ i ].pers.teamSelection == team ||
+ ( level.clients[ i ].pers.teamSelection == TEAM_NONE &&
+ G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) )
+ {
+ trap_SendServerCommand( i, va( "print \"%s" S_COLOR_WHITE
+ " called a team vote: %s\n\"", ent->client->pers.netname,
+ level.voteDisplayString[ team ] ) );
+ }
+ else if( G_admin_permission( &g_entities[ i ], ADMF_ADMINCHAT ) )
+ {
+ trap_SendServerCommand( i, va( "chat -1 %d \"" S_COLOR_YELLOW "%s"
+ S_COLOR_YELLOW " called a team vote (%ss): %s\"", SAY_ADMINS,
+ ent->client->pers.netname, BG_TeamName( team ),
+ level.voteDisplayString[ team ] ) );
+ }
+ }
+ }
+ }
+
+ G_DecolorString( ent->client->pers.netname, caller, sizeof( caller ) );
+
+ level.voteTime[ team ] = level.time;
+ trap_SetConfigstring( CS_VOTE_TIME + team,
+ va( "%d", level.voteTime[ team ] ) );
+ trap_SetConfigstring( CS_VOTE_STRING + team,
+ level.voteDisplayString[ team ] );
+ trap_SetConfigstring( CS_VOTE_CALLER + team,
+ caller );
+
+ ent->client->pers.namelog->voteCount++;
+ ent->client->pers.vote |= 1 << team;
+ G_Vote( ent, team, qtrue );
+}
+
+/*
+==================
+Cmd_Vote_f
+==================
+*/
+void Cmd_Vote_f( gentity_t *ent )
+{
+ char cmd[ MAX_TOKEN_CHARS ], vote[ MAX_TOKEN_CHARS ];
+ team_t team = ent->client->pers.teamSelection;
+
+ trap_Argv( 0, cmd, sizeof( cmd ) );
+ if( Q_stricmp( cmd, "teamvote" ) )
+ team = TEAM_NONE;
+
+ if( !level.voteTime[ team ] )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: no vote in progress\n\"", cmd ) );
+ return;
+ }
+
+ if( ent->client->pers.voted & ( 1 << team ) )
+ {
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: vote already cast\n\"", cmd ) );
+ return;
+ }
+
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"%s: vote cast\n\"", cmd ) );
+
+ trap_Argv( 1, vote, sizeof( vote ) );
+ if( vote[ 0 ] == 'y' )
+ ent->client->pers.vote |= 1 << team;
+ else
+ ent->client->pers.vote &= ~( 1 << team );
+ G_Vote( ent, team, qtrue );
+}
+
+
+/*
+=================
+Cmd_SetViewpos_f
+=================
+*/
+void Cmd_SetViewpos_f( gentity_t *ent )
+{
+ vec3_t origin, angles;
+ char buffer[ MAX_TOKEN_CHARS ];
+ int i;
+
+ if( trap_Argc( ) != 5 )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"usage: setviewpos x y z yaw\n\"" );
+ return;
+ }
+
+ VectorClear( angles );
+
+ for( i = 0; i < 3; i++ )
+ {
+ trap_Argv( i + 1, buffer, sizeof( buffer ) );
+ origin[ i ] = atof( buffer );
+ }
+
+ trap_Argv( 4, buffer, sizeof( buffer ) );
+ angles[ YAW ] = atof( buffer );
+
+ TeleportPlayer( ent, origin, angles );
+}
+
+#define AS_OVER_RT3 ((ALIENSENSE_RANGE*0.5f)/M_ROOT3)
+
+static qboolean G_RoomForClassChange( gentity_t *ent, class_t class,
+ vec3_t newOrigin )
+{
+ vec3_t fromMins, fromMaxs;
+ vec3_t toMins, toMaxs;
+ vec3_t temp;
+ trace_t tr;
+ float nudgeHeight;
+ float maxHorizGrowth;
+ class_t oldClass = ent->client->ps.stats[ STAT_CLASS ];
+
+ BG_ClassBoundingBox( oldClass, fromMins, fromMaxs, NULL, NULL, NULL );
+ BG_ClassBoundingBox( class, toMins, toMaxs, NULL, NULL, NULL );
+
+ VectorCopy( ent->s.origin, newOrigin );
+
+ // find max x/y diff
+ maxHorizGrowth = toMaxs[ 0 ] - fromMaxs[ 0 ];
+ if( toMaxs[ 1 ] - fromMaxs[ 1 ] > maxHorizGrowth )
+ maxHorizGrowth = toMaxs[ 1 ] - fromMaxs[ 1 ];
+ if( toMins[ 0 ] - fromMins[ 0 ] > -maxHorizGrowth )
+ maxHorizGrowth = -( toMins[ 0 ] - fromMins[ 0 ] );
+ if( toMins[ 1 ] - fromMins[ 1 ] > -maxHorizGrowth )
+ maxHorizGrowth = -( toMins[ 1 ] - fromMins[ 1 ] );
+
+ if( maxHorizGrowth > 0.0f )
+ {
+ // test by moving the player up the max required on a 60 degree slope
+ nudgeHeight = maxHorizGrowth * 2.0f;
+ }
+ else
+ {
+ // player is shrinking, so there's no need to nudge them upwards
+ nudgeHeight = 0.0f;
+ }
+
+ // find what the new origin would be on a level surface
+ newOrigin[ 2 ] -= toMins[ 2 ] - fromMins[ 2 ];
+
+ //compute a place up in the air to start the real trace
+ VectorCopy( newOrigin, temp );
+ temp[ 2 ] += nudgeHeight;
+ trap_Trace( &tr, newOrigin, toMins, toMaxs, temp, ent->s.number, MASK_PLAYERSOLID );
+
+ //trace down to the ground so that we can evolve on slopes
+ VectorCopy( newOrigin, temp );
+ temp[ 2 ] += ( nudgeHeight * tr.fraction );
+ trap_Trace( &tr, temp, toMins, toMaxs, newOrigin, ent->s.number, MASK_PLAYERSOLID );
+ VectorCopy( tr.endpos, newOrigin );
+
+ //make REALLY sure
+ trap_Trace( &tr, newOrigin, toMins, toMaxs, newOrigin,
+ ent->s.number, MASK_PLAYERSOLID );
+
+ //check there is room to evolve
+ return ( !tr.startsolid && tr.fraction == 1.0f );
+}
+
+/*
+=================
+Cmd_Class_f
+=================
+*/
+void Cmd_Class_f( gentity_t *ent )
+{
+ char s[ MAX_TOKEN_CHARS ];
+ int clientNum;
+ int i;
+ vec3_t infestOrigin;
+ class_t currentClass = ent->client->pers.classSelection;
+ class_t newClass;
+ int entityList[ MAX_GENTITIES ];
+ vec3_t range = { AS_OVER_RT3, AS_OVER_RT3, AS_OVER_RT3 };
+ vec3_t mins, maxs;
+ int num;
+ gentity_t *other;
+ int oldBoostTime = -1;
+ vec3_t oldVel;
+
+ clientNum = ent->client - level.clients;
+ trap_Argv( 1, s, sizeof( s ) );
+ newClass = BG_ClassByName( s )->number;
+
+ if( ent->client->sess.spectatorState != SPECTATOR_NOT )
+ {
+ if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW )
+ G_StopFollowing( ent );
+ if( ent->client->pers.teamSelection == TEAM_ALIENS )
+ {
+ if( newClass != PCL_ALIEN_BUILDER0 &&
+ newClass != PCL_ALIEN_BUILDER0_UPG &&
+ newClass != PCL_ALIEN_LEVEL0 )
+ {
+ G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTSPAWN, newClass );
+ return;
+ }
+
+ if( !BG_ClassIsAllowed( newClass ) )
+ {
+ G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTALLOWED, newClass );
+ return;
+ }
+
+ if( !BG_ClassAllowedInStage( newClass, g_alienStage.integer ) )
+ {
+ G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTATSTAGE, newClass );
+ return;
+ }
+
+ // spawn from an egg
+ if( G_PushSpawnQueue( &level.alienSpawnQueue, clientNum ) )
+ {
+ ent->client->pers.classSelection = newClass;
+ ent->client->ps.stats[ STAT_CLASS ] = newClass;
+ }
+ }
+ else if( ent->client->pers.teamSelection == TEAM_HUMANS )
+ {
+ //set the item to spawn with
+ if( !Q_stricmp( s, BG_Weapon( WP_MACHINEGUN )->name ) &&
+ BG_WeaponIsAllowed( WP_MACHINEGUN ) )
+ {
+ ent->client->pers.humanItemSelection = WP_MACHINEGUN;
+ }
+ else if( !Q_stricmp( s, BG_Weapon( WP_HBUILD )->name ) &&
+ BG_WeaponIsAllowed( WP_HBUILD ) )
+ {
+ ent->client->pers.humanItemSelection = WP_HBUILD;
+ }
+ else
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNSPAWNITEM );
+ return;
+ }
+ // spawn from a telenode
+ if( G_PushSpawnQueue( &level.humanSpawnQueue, clientNum ) )
+ {
+ ent->client->pers.classSelection = PCL_HUMAN;
+ ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN;
+ }
+ }
+ return;
+ }
+
+ if( ent->health <= 0 )
+ return;
+
+ if( ent->client->pers.teamSelection == TEAM_ALIENS )
+ {
+ if( newClass == PCL_NONE )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_A_UNKNOWNCLASS );
+ return;
+ }
+
+ //if we are not currently spectating, we are attempting evolution
+ if( ent->client->pers.classSelection != PCL_NONE )
+ {
+ int cost;
+
+ //check that we have an overmind
+ if( !G_Overmind( ) )
+ {
+ G_TriggerMenu( clientNum, MN_A_NOOVMND_EVOLVE );
+ return;
+ }
+
+ //check there are no humans nearby
+ VectorAdd( ent->client->ps.origin, range, maxs );
+ VectorSubtract( ent->client->ps.origin, range, mins );
+
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ other = &g_entities[ entityList[ i ] ];
+
+ if( ( other->client && other->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) ||
+ ( other->s.eType == ET_BUILDABLE && other->buildableTeam == TEAM_HUMANS &&
+ other->powered ) )
+ {
+ trace_t tr;
+
+ trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, other->s.origin, ent->client->ps.clientNum, MASK_SOLID );
+
+ if( tr.fraction > 0.99f )
+ {
+ G_TriggerMenu( clientNum, MN_A_TOOCLOSE );
+ return;
+ }
+ }
+ }
+
+ //check that we are not wallwalking
+ if( ent->client->ps.eFlags & EF_WALLCLIMB )
+ {
+ G_TriggerMenu( clientNum, MN_A_EVOLVEWALLWALK );
+ return;
+ }
+
+ //guard against selling the HBUILD weapons exploit
+ if( ent->client->sess.spectatorState == SPECTATOR_NOT &&
+ ( currentClass == PCL_ALIEN_BUILDER0 ||
+ currentClass == PCL_ALIEN_BUILDER0_UPG ) &&
+ ent->client->buildTimer )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_A_EVOLVEBUILDTIMER );
+ return;
+ }
+
+ cost = BG_ClassCanEvolveFromTo( currentClass, newClass,
+ ent->client->pers.credit,
+ g_alienStage.integer, 0 );
+
+ if( G_RoomForClassChange( ent, newClass, infestOrigin ) )
+ {
+ if( cost >= 0 )
+ {
+
+ ent->client->pers.evolveHealthFraction = (float)ent->client->ps.stats[ STAT_HEALTH ] /
+ (float)BG_Class( currentClass )->health;
+
+ if( ent->client->pers.evolveHealthFraction < 0.0f )
+ ent->client->pers.evolveHealthFraction = 0.0f;
+ else if( ent->client->pers.evolveHealthFraction > 1.0f )
+ ent->client->pers.evolveHealthFraction = 1.0f;
+
+ //remove credit
+ G_AddCreditToClient( ent->client, -cost, qtrue );
+ ent->client->pers.classSelection = newClass;
+ ClientUserinfoChanged( clientNum, qfalse );
+ VectorCopy( infestOrigin, ent->s.pos.trBase );
+ VectorCopy( ent->client->ps.velocity, oldVel );
+
+ if( ent->client->ps.stats[ STAT_STATE ] & SS_BOOSTED )
+ oldBoostTime = ent->client->boostedTime;
+
+ ClientSpawn( ent, ent, ent->s.pos.trBase, ent->s.apos.trBase );
+
+ VectorCopy( oldVel, ent->client->ps.velocity );
+ if( oldBoostTime > 0 )
+ {
+ ent->client->boostedTime = oldBoostTime;
+ ent->client->ps.stats[ STAT_STATE ] |= SS_BOOSTED;
+ }
+ }
+ else
+ G_TriggerMenuArgs( clientNum, MN_A_CANTEVOLVE, newClass );
+ }
+ else
+ G_TriggerMenu( clientNum, MN_A_NOEROOM );
+ }
+ }
+ else if( ent->client->pers.teamSelection == TEAM_HUMANS )
+ G_TriggerMenu( clientNum, MN_H_DEADTOCLASS );
+}
+
+
+/*
+=================
+Cmd_Destroy_f
+=================
+*/
+void Cmd_Destroy_f( gentity_t *ent )
+{
+ vec3_t viewOrigin, forward, end;
+ trace_t tr;
+ gentity_t *traceEnt;
+ char cmd[ 12 ];
+ qboolean deconstruct = qtrue;
+ qboolean lastSpawn = qfalse;
+
+ if( ent->client->pers.namelog->denyBuild )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_REVOKED );
+ return;
+ }
+
+ trap_Argv( 0, cmd, sizeof( cmd ) );
+ if( Q_stricmp( cmd, "destroy" ) == 0 )
+ deconstruct = qfalse;
+
+ BG_GetClientViewOrigin( &ent->client->ps, viewOrigin );
+ AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL );
+ VectorMA( viewOrigin, 100, forward, end );
+
+ trap_Trace( &tr, viewOrigin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID );
+ traceEnt = &g_entities[ tr.entityNum ];
+
+ if( tr.fraction < 1.0f &&
+ ( traceEnt->s.eType == ET_BUILDABLE ) &&
+ ( traceEnt->buildableTeam == ent->client->pers.teamSelection ) &&
+ ( ( ent->client->ps.weapon >= WP_ABUILD ) &&
+ ( ent->client->ps.weapon <= WP_HBUILD ) ) )
+ {
+ // Always let the builder prevent the explosion
+ if( traceEnt->health <= 0 )
+ {
+ G_QueueBuildPoints( traceEnt );
+ G_RewardAttackers( traceEnt );
+ G_FreeEntity( traceEnt );
+ return;
+ }
+
+ // Cancel deconstruction (unmark)
+ if( deconstruct && g_markDeconstruct.integer && traceEnt->deconstruct )
+ {
+ traceEnt->deconstruct = qfalse;
+ return;
+ }
+
+ // Prevent destruction of the last spawn
+ if( ent->client->pers.teamSelection == TEAM_ALIENS &&
+ traceEnt->s.modelindex == BA_A_SPAWN )
+ {
+ if( level.numAlienSpawns <= 1 )
+ lastSpawn = qtrue;
+ }
+ else if( ent->client->pers.teamSelection == TEAM_HUMANS &&
+ traceEnt->s.modelindex == BA_H_SPAWN )
+ {
+ if( level.numHumanSpawns <= 1 )
+ lastSpawn = qtrue;
+ }
+
+ if( lastSpawn && !g_cheats.integer &&
+ !g_markDeconstruct.integer )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_LASTSPAWN );
+ return;
+ }
+
+ // Don't allow destruction of buildables that cannot be rebuilt
+ if( G_TimeTilSuddenDeath( ) <= 0 )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_SUDDENDEATH );
+ return;
+ }
+
+ if( !g_markDeconstruct.integer ||
+ ( ent->client->pers.teamSelection == TEAM_HUMANS &&
+ !G_FindPower( traceEnt, qtrue ) ) )
+ {
+ if( ent->client->buildTimer )
+ {
+ G_AddEvent( ent, EV_BUILD_DELAY, ent->client->ps.clientNum );
+ return;
+ }
+ }
+
+ if( traceEnt->health > 0 )
+ {
+ if( !deconstruct )
+ {
+ G_Damage( traceEnt, ent, ent, forward, tr.endpos,
+ traceEnt->health, 0, MOD_SUICIDE );
+ }
+ else if( g_markDeconstruct.integer &&
+ ( ent->client->pers.teamSelection != TEAM_HUMANS ||
+ G_FindPower( traceEnt , qtrue ) || lastSpawn ) )
+ {
+ traceEnt->deconstruct = qtrue; // Mark buildable for deconstruction
+ traceEnt->deconstructTime = level.time;
+ }
+ else
+ {
+ if( !g_cheats.integer && !g_instantBuild.integer ) // add a bit to the build timer
+ {
+ ent->client->buildTimer +=
+ BG_Buildable( traceEnt->s.modelindex, traceEnt->cuboidSize )->buildTime / 4;
+ G_RecalcBuildTimer(ent->client);
+ }
+ G_Damage( traceEnt, ent, ent, forward, tr.endpos,
+ traceEnt->health, 0, MOD_DECONSTRUCT );
+ G_FreeEntity( traceEnt );
+ }
+ }
+ }
+}
+
+/*
+=================
+Cmd_ActivateItem_f
+
+Activate an item
+=================
+*/
+void Cmd_ActivateItem_f( gentity_t *ent )
+{
+ char s[ MAX_TOKEN_CHARS ];
+ int upgrade, weapon;
+
+ trap_Argv( 1, s, sizeof( s ) );
+
+ // "weapon" aliased to whatever weapon you have
+ if( !Q_stricmp( "weapon", s ) )
+ {
+ if( ent->client->ps.weapon == WP_BLASTER &&
+ BG_PlayerCanChangeWeapon( &ent->client->ps ) )
+ G_ForceWeaponChange( ent, WP_NONE );
+ return;
+ }
+
+ upgrade = BG_UpgradeByName( s )->number;
+ weapon = BG_WeaponByName( s )->number;
+
+ if( upgrade != UP_NONE && BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) )
+ BG_ActivateUpgrade( upgrade, ent->client->ps.stats );
+ else if( weapon != WP_NONE &&
+ BG_InventoryContainsWeapon( weapon, ent->client->ps.stats ) )
+ {
+ if( ent->client->ps.weapon != weapon &&
+ BG_PlayerCanChangeWeapon( &ent->client->ps ) )
+ G_ForceWeaponChange( ent, weapon );
+ }
+ else
+ trap_SendServerCommand( ent-g_entities, va( "print \"You don't have the %s\n\"", s ) );
+}
+
+
+/*
+=================
+Cmd_DeActivateItem_f
+
+Deactivate an item
+=================
+*/
+void Cmd_DeActivateItem_f( gentity_t *ent )
+{
+ char s[ MAX_TOKEN_CHARS ];
+ upgrade_t upgrade;
+
+ trap_Argv( 1, s, sizeof( s ) );
+ upgrade = BG_UpgradeByName( s )->number;
+
+ if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) )
+ {
+ BG_DeactivateUpgrade( upgrade, ent->client->ps.stats );
+ if( upgrade == UP_JETPACK )
+ BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &ent->client->ps );
+ }
+ else
+ trap_SendServerCommand( ent-g_entities, va( "print \"You don't have the %s\n\"", s ) );
+}
+
+
+/*
+=================
+Cmd_ToggleItem_f
+=================
+*/
+void Cmd_ToggleItem_f( gentity_t *ent )
+{
+ char s[ MAX_TOKEN_CHARS ];
+ weapon_t weapon;
+ upgrade_t upgrade;
+
+ trap_Argv( 1, s, sizeof( s ) );
+ upgrade = BG_UpgradeByName( s )->number;
+ weapon = BG_WeaponByName( s )->number;
+
+ if( weapon != WP_NONE )
+ {
+ if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) )
+ return;
+
+ //special case to allow switching between
+ //the blaster and the primary weapon
+ if( ent->client->ps.weapon != WP_BLASTER )
+ weapon = WP_BLASTER;
+ else
+ weapon = WP_NONE;
+
+ G_ForceWeaponChange( ent, weapon );
+ }
+ else if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) )
+ {
+ if( BG_UpgradeIsActive( upgrade, ent->client->ps.stats ) )
+ {
+ BG_DeactivateUpgrade( upgrade, ent->client->ps.stats );
+ if( upgrade == UP_JETPACK )
+ BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &ent->client->ps );
+ }
+ else
+ BG_ActivateUpgrade( upgrade, ent->client->ps.stats );
+ }
+ else
+ trap_SendServerCommand( ent-g_entities, va( "print \"You don't have the %s\n\"", s ) );
+}
+
+/*
+=================
+Cmd_Buy_f
+=================
+*/
+void Cmd_Buy_f( gentity_t *ent )
+{
+ char s[ MAX_TOKEN_CHARS ];
+ weapon_t weapon;
+ upgrade_t upgrade;
+ qboolean energyOnly, sellHelmet = qfalse;
+
+ trap_Argv( 1, s, sizeof( s ) );
+
+ weapon = BG_WeaponByName( s )->number;
+ upgrade = BG_UpgradeByName( s )->number;
+
+ if( upgrade == UP_NONE && !Q_stricmp(s, "helmet") )
+ {
+ if( g_humanStage.integer < S2 )
+ upgrade = UP_HELMET_MK1;
+ else
+ upgrade = UP_HELMET_MK2;
+ sellHelmet = qtrue;
+ }
+
+ // Only give energy from reactors or repeaters
+ if( G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) )
+ energyOnly = qfalse;
+ else if( upgrade == UP_AMMO &&
+ BG_Weapon( ent->client->ps.stats[ STAT_WEAPON ] )->usesEnergy &&
+ ( G_BuildableRange( ent->client->ps.origin, 100, BA_H_REACTOR ) ||
+ G_BuildableRange( ent->client->ps.origin, 100, BA_H_REPEATER ) ) )
+ energyOnly = qtrue;
+ else
+ {
+ if( upgrade == UP_AMMO &&
+ BG_Weapon( ent->client->ps.weapon )->usesEnergy )
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOENERGYAMMOHERE );
+ else
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOARMOURYHERE );
+ return;
+ }
+
+ if( weapon != WP_NONE )
+ {
+ //already got this?
+ if( BG_InventoryContainsWeapon( weapon, ent->client->ps.stats ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_ITEMHELD );
+ return;
+ }
+
+ // Only humans can buy stuff
+ if( BG_Weapon( weapon )->team != TEAM_HUMANS )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't buy alien items\n\"" );
+ return;
+ }
+
+ //are we /allowed/ to buy this?
+ if( !BG_Weapon( weapon )->purchasable )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" );
+ return;
+ }
+
+ //are we /allowed/ to buy this?
+ if( !BG_WeaponAllowedInStage( weapon, g_humanStage.integer ) || !BG_WeaponIsAllowed( weapon ) )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" );
+ return;
+ }
+
+ //can afford this?
+ if( BG_Weapon( weapon )->price > (short)ent->client->pers.credit )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS );
+ return;
+ }
+
+ //have space to carry this?
+ if( BG_Weapon( weapon )->slots & BG_SlotsForInventory( ent->client->ps.stats ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS );
+ return;
+ }
+
+ // In some instances, weapons can't be changed
+ if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) )
+ return;
+
+ ent->client->ps.stats[ STAT_WEAPON ] = weapon;
+ ent->client->ps.ammo = BG_Weapon( weapon )->maxAmmo;
+ ent->client->ps.clips = BG_Weapon( weapon )->maxClips;
+
+ if( BG_Weapon( weapon )->usesEnergy &&
+ BG_InventoryContainsUpgrade( UP_BATTPACK, ent->client->ps.stats ) )
+ ent->client->ps.ammo *= BATTPACK_MODIFIER;
+
+ G_ForceWeaponChange( ent, weapon );
+
+ //set build delay/pounce etc to 0
+ ent->client->ps.stats[ STAT_MISC ] = 0;
+ ent->client->buildTimer = 0;
+
+ //subtract from funds
+ G_AddCreditToClient( ent->client, -(short)BG_Weapon( weapon )->price, qfalse );
+ }
+ else if( upgrade != UP_NONE )
+ {
+ //already got this?
+ if( !sellHelmet && BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_ITEMHELD );
+ return;
+ }
+
+ //can afford this?
+ if( BG_Upgrade( upgrade )->price > (short)ent->client->pers.credit )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS );
+ return;
+ }
+
+ //have space to carry this?
+ if( !sellHelmet && BG_Upgrade( upgrade )->slots & BG_SlotsForInventory( ent->client->ps.stats ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS );
+ return;
+ }
+
+ // Only humans can buy stuff
+ if( BG_Upgrade( upgrade )->team != TEAM_HUMANS )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't buy alien items\n\"" );
+ return;
+ }
+
+ //are we /allowed/ to buy this?
+ if( !BG_Upgrade( upgrade )->purchasable )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" );
+ return;
+ }
+
+ //are we /allowed/ to buy this?
+ if( !BG_UpgradeAllowedInStage( upgrade, g_humanStage.integer ) || !BG_UpgradeIsAllowed( upgrade ) )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" );
+ return;
+ }
+
+ if( upgrade == UP_AMMO )
+ {
+ G_GiveClientMaxAmmo( ent, energyOnly );
+ if( !energyOnly && BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) &&
+ ent->client->ps.stats[ STAT_FUEL ] < JETPACK_FUEL_FULL )
+ {
+ G_AddEvent( ent, EV_JETPACK_REFUEL, 0) ;
+ ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL;
+ }
+ }
+ else
+ {
+ if( upgrade == UP_BATTLESUIT )
+ {
+ vec3_t newOrigin;
+
+ if( !G_RoomForClassChange( ent, PCL_HUMAN_BSUIT, newOrigin ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITON );
+ return;
+ }
+ VectorCopy( newOrigin, ent->client->ps.origin );
+ ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN_BSUIT;
+ ent->client->pers.classSelection = PCL_HUMAN_BSUIT;
+ ent->client->ps.eFlags ^= EF_TELEPORT_BIT;
+ }
+ else if( upgrade == UP_JETPACK )
+ ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL;
+
+ if( sellHelmet )
+ {
+ BG_RemoveUpgradeFromInventory( UP_HELMET_MK1, ent->client->ps.stats );
+ BG_RemoveUpgradeFromInventory( UP_HELMET_MK2, ent->client->ps.stats );
+ }
+
+ //add to inventory
+ BG_AddUpgradeToInventory( upgrade, ent->client->ps.stats );
+ }
+
+ if( upgrade == UP_BATTPACK )
+ G_GiveClientMaxAmmo( ent, qtrue );
+
+ //subtract from funds
+ G_AddCreditToClient( ent->client, -(short)BG_Upgrade( upgrade )->price, qfalse );
+ }
+ else
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNITEM );
+
+ //update ClientInfo
+ ClientUserinfoChanged( ent->client->ps.clientNum, qfalse );
+}
+
+
+/*
+=================
+Cmd_Sell_f
+=================
+*/
+void Cmd_Sell_f( gentity_t *ent )
+{
+ char s[ MAX_TOKEN_CHARS ];
+ int i;
+ weapon_t weapon;
+ upgrade_t upgrade;
+
+ trap_Argv( 1, s, sizeof( s ) );
+
+ //no armoury nearby
+ if( !G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOARMOURYHERE );
+ return;
+ }
+
+ if( !Q_stricmpn( s, "weapon", 6 ) )
+ weapon = ent->client->ps.stats[ STAT_WEAPON ];
+ else
+ weapon = BG_WeaponByName( s )->number;
+
+ upgrade = BG_UpgradeByName( s )->number;
+
+ if( !upgrade && !Q_stricmp( s, "helmet" ) )
+ {
+ if( BG_InventoryContainsUpgrade( UP_HELMET_MK1, ent->client->ps.stats ) )
+ upgrade = UP_HELMET_MK1;
+ else if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, ent->client->ps.stats ) )
+ upgrade = UP_HELMET_MK2;
+ }
+
+ if( weapon != WP_NONE )
+ {
+ weapon_t selected = BG_GetPlayerWeapon( &ent->client->ps );
+
+ if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) )
+ return;
+
+ //are we /allowed/ to sell this?
+ if( !BG_Weapon( weapon )->purchasable )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't sell this weapon\n\"" );
+ return;
+ }
+
+ //remove weapon if carried
+ if( BG_InventoryContainsWeapon( weapon, ent->client->ps.stats ) )
+ {
+ //guard against selling the HBUILD weapons exploit
+ if( weapon == WP_HBUILD && ent->client->buildTimer )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_ARMOURYBUILDTIMER );
+ return;
+ }
+
+ ent->client->ps.stats[ STAT_WEAPON ] = WP_NONE;
+ // Cancel ghost buildables
+ ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE;
+
+ //add to funds
+ G_AddCreditToClient( ent->client, (short)BG_Weapon( weapon )->price, qfalse );
+ }
+
+ //if we have this weapon selected, force a new selection
+ if( weapon == selected )
+ G_ForceWeaponChange( ent, WP_NONE );
+ }
+ else if( upgrade != UP_NONE )
+ {
+ //are we /allowed/ to sell this?
+ if( !BG_Upgrade( upgrade )->purchasable )
+ {
+ trap_SendServerCommand( ent-g_entities, "print \"You can't sell this item\n\"" );
+ return;
+ }
+ //remove upgrade if carried
+ if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) )
+ {
+ // shouldn't really need to test for this, but just to be safe
+ if( upgrade == UP_BATTLESUIT )
+ {
+ vec3_t newOrigin;
+
+ if( !G_RoomForClassChange( ent, PCL_HUMAN, newOrigin ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITOFF );
+ return;
+ }
+ VectorCopy( newOrigin, ent->client->ps.origin );
+ ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN;
+ ent->client->pers.classSelection = PCL_HUMAN;
+ ent->client->ps.eFlags ^= EF_TELEPORT_BIT;
+ }
+
+ //add to inventory
+ BG_RemoveUpgradeFromInventory( upgrade, ent->client->ps.stats );
+
+ if( upgrade == UP_BATTPACK )
+ G_GiveClientMaxAmmo( ent, qtrue );
+
+ //add to funds
+ G_AddCreditToClient( ent->client, (short)BG_Upgrade( upgrade )->price, qfalse );
+ }
+ }
+ else if( !Q_stricmp( s, "weapons" ) )
+ {
+ weapon_t selected = BG_GetPlayerWeapon( &ent->client->ps );
+
+ if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) )
+ return;
+
+ for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
+ {
+ //guard against selling the HBUILD weapons exploit
+ if( i == WP_HBUILD && ent->client->buildTimer )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_ARMOURYBUILDTIMER );
+ continue;
+ }
+
+ if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) &&
+ BG_Weapon( i )->purchasable )
+ {
+ ent->client->ps.stats[ STAT_WEAPON ] = WP_NONE;
+
+ //add to funds
+ G_AddCreditToClient( ent->client, (short)BG_Weapon( i )->price, qfalse );
+ }
+
+ //if we have this weapon selected, force a new selection
+ if( i == selected )
+ G_ForceWeaponChange( ent, WP_NONE );
+ }
+ }
+ else if( !Q_stricmp( s, "upgrades" ) )
+ {
+ for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ )
+ {
+ //remove upgrade if carried
+ if( BG_InventoryContainsUpgrade( i, ent->client->ps.stats ) &&
+ BG_Upgrade( i )->purchasable )
+ {
+
+ // shouldn't really need to test for this, but just to be safe
+ if( i == UP_BATTLESUIT )
+ {
+ vec3_t newOrigin;
+
+ if( !G_RoomForClassChange( ent, PCL_HUMAN, newOrigin ) )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITOFF );
+ continue;
+ }
+ VectorCopy( newOrigin, ent->client->ps.origin );
+ ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN;
+ ent->client->pers.classSelection = PCL_HUMAN;
+ ent->client->ps.eFlags ^= EF_TELEPORT_BIT;
+ }
+
+ BG_RemoveUpgradeFromInventory( i, ent->client->ps.stats );
+
+ if( i == UP_BATTPACK )
+ G_GiveClientMaxAmmo( ent, qtrue );
+
+ //add to funds
+ G_AddCreditToClient( ent->client, (short)BG_Upgrade( i )->price, qfalse );
+ }
+ }
+ }
+ else
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNITEM );
+
+ //update ClientInfo
+ ClientUserinfoChanged( ent->client->ps.clientNum, qfalse );
+}
+
+/*
+=================
+Cmd_CheckCuboidSize
+
+Check if the specified dimensions are valid.
+=================
+*/
+qboolean Cmd_CheckCuboidSize(vec3_t dims)
+{
+ if(g_cuboidSizeLimit.integer)
+ if(dims[0]>g_cuboidSizeLimit.integer||dims[1]>g_cuboidSizeLimit.integer||dims[2]>g_cuboidSizeLimit.integer)
+ return qfalse;
+ if(dims[0]*dims[1]*dims[2]<CUBOID_MINVOLUME)
+ return qfalse;
+ if(dims[0]<1||dims[1]<1||dims[2]<1)
+ return qfalse;
+ return qtrue;
+}
+
+/*
+=================
+Cmd_Cb_f
+
+Sent by cgame in background (not directly by a person).
+Update player's cuboid selection (after validation) with data sent over network.
+=================
+*/
+void Cmd_Cb_f(gentity_t *ent)
+{
+ char s[MAX_TOKEN_CHARS];
+ int echo;
+ vec3_t dims;
+
+ if(trap_Argc()!=5)
+ return;
+ trap_Argv(1,s,sizeof(s));
+ echo=atoi(s);
+ trap_Argv(2,s,sizeof(s));
+ dims[0]=atof(s);
+ trap_Argv(3,s,sizeof(s));
+ dims[1]=atof(s);
+ trap_Argv(4,s,sizeof(s));
+ dims[2]=atof(s);
+ if(Cmd_CheckCuboidSize(dims))
+ {
+ VectorCopy(dims,ent->client->cuboidSelection);
+ trap_SendServerCommand(ent->client-level.clients,va("cb3 %i\n",echo));
+ G_RelayCuboidToSpectators(ent->client);
+ }
+ else
+ {
+ if(Cmd_CheckCuboidSize(ent->client->cuboidSelection))
+ trap_SendServerCommand(ent->client-level.clients,va("cb3 %i %f %f %f\n",echo,ent->client->cuboidSelection[0],ent->client->cuboidSelection[1],ent->client->cuboidSelection[2]));
+ else
+ trap_SendServerCommand(ent->client-level.clients,va("cb3 %i %f %f %f\n",echo,g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer));
+ }
+}
+
+
+/*
+=================
+Cmd_Build_f
+=================
+*/
+void Cmd_Build_f( gentity_t *ent )
+{
+ char s[ MAX_TOKEN_CHARS ];
+ buildable_t buildable;
+ float dist;
+ vec3_t origin, normal;
+ team_t team;
+ char buf[128];
+ vec3_t dims;
+
+ if( ent->client->pers.namelog->denyBuild )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_REVOKED );
+ return;
+ }
+
+ if( ent->client->pers.teamSelection == level.surrenderTeam )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_SURRENDER );
+ return;
+ }
+
+ trap_Argv( 1, s, sizeof( s ) );
+
+ buildable = BG_BuildableByName( s )->number;
+
+ /* To allow players build cuboids with completely arbitrary
+ * dimensions (the current resizing method doesn't provide
+ * much precision) the build command was extended with the
+ * following syntax:
+ * build [building] [X] [Y] [Z]
+ * where X, Y and Z are respectively the cuboid's length
+ * on the X, Y and Z axis.
+ */
+ if( BG_Buildable(buildable,NULL)->cuboid )
+ {
+ if( trap_Argc() >= 5 )
+ {
+ trap_Argv(2,s,sizeof(s));
+ dims[0]=MAX(1,atof(s));
+ trap_Argv(3,s,sizeof(s));
+ dims[1]=MAX(1,atof(s));
+ trap_Argv(4,s,sizeof(s));
+ dims[2]=MAX(1,atof(s));
+ if(!Cmd_CheckCuboidSize(dims))
+ {
+ Com_sprintf(buf,sizeof(buf),"print \"^1error: invalid cuboid size (min volume: %i, max size: %s)\n\"",
+ CUBOID_MINVOLUME,(g_cuboidSizeLimit.integer?va("%ix%ix%i",g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer, g_cuboidSizeLimit.integer):"no limit"));
+ trap_SendServerCommand(ent->client-level.clients,buf);
+ return;
+ }
+ VectorCopy(dims,ent->client->cuboidSelection);
+ }
+ // client is building a cuboid for the first time so reset the selection to default
+ if(!Cmd_CheckCuboidSize(ent->client->cuboidSelection))
+ {
+ ent->client->cuboidSelection[0]=32;
+ ent->client->cuboidSelection[1]=32;
+ ent->client->cuboidSelection[2]=32;
+ trap_SendServerCommand(ent->client-level.clients,"cb2 32 32 32");
+ G_RelayCuboidToSpectators(ent->client);
+ }
+
+ if(!BG_CuboidAllowed((team==TEAM_ALIENS?g_alienStage.integer:g_humanStage.integer)))
+ {
+ if(BG_CuboidMode()==1)
+ G_TriggerMenu(ent->client->ps.clientNum,MN_B_CUBOID_MODE1);
+ else
+ G_TriggerMenu(ent->client->ps.clientNum,MN_B_CUBOID_MODE2);
+ return;
+ }
+ }
+
+ if( G_TimeTilSuddenDeath( ) <= 0 )
+ {
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_SUDDENDEATH );
+ return;
+ }
+
+ team = ent->client->ps.stats[ STAT_TEAM ];
+
+ if( buildable != BA_NONE &&
+ ( ( 1 << ent->client->ps.weapon ) & BG_Buildable( buildable, NULL )->buildWeapon ) &&
+ BG_BuildableIsAllowed( buildable ) &&
+ ( ( team == TEAM_ALIENS && BG_BuildableAllowedInStage( buildable, g_alienStage.integer ) ) ||
+ ( team == TEAM_HUMANS && BG_BuildableAllowedInStage( buildable, g_humanStage.integer ) ) ) )
+ {
+ dynMenu_t err;
+ dist = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->buildDist;
+
+ ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE;
+
+ //these are the errors displayed when the builder first selects something to use
+ switch( G_CanBuild( ent, buildable, dist, origin, normal, ent->client->cuboidSelection ) )
+ {
+ // can place right away, set the blueprint and the valid togglebit
+ case IBE_NONE:
+ case IBE_TNODEWARN:
+ case IBE_RPTNOREAC:
+ case IBE_RPTPOWERHERE:
+ case IBE_SPWNWARN:
+ err = MN_NONE;
+ // we OR-in the selected builable later
+ ent->client->ps.stats[ STAT_BUILDABLE ] = SB_VALID_TOGGLEBIT;
+ break;
+
+ // can't place yet but maybe soon: start with valid togglebit off
+ case IBE_NORMAL:
+ case IBE_NOCREEP:
+ case IBE_NOROOM:
+ case IBE_NOOVERMIND:
+ case IBE_NOPOWERHERE:
+ case IBE_NOSURF:
+ err = MN_NONE;
+ break;
+
+ // more serious errors just pop a menu
+ case IBE_NOALIENBP:
+ err = MN_A_NOBP;
+ break;
+
+ case IBE_ONEOVERMIND:
+ err = MN_A_ONEOVERMIND;
+ break;
+
+ case IBE_ONEREACTOR:
+ err = MN_H_ONEREACTOR;
+ break;
+
+ case IBE_NOHUMANBP:
+ err = MN_H_NOBP;
+ break;
+
+ case IBE_NODCC:
+ err = MN_H_NODCC;
+ break;
+
+ case IBE_PERMISSION:
+ err = MN_B_CANNOT;
+ break;
+
+ case IBE_LASTSPAWN:
+ err = MN_B_LASTSPAWN;
+ break;
+
+ case IBE_TOODENSE:
+ err = MN_B_TOODENSE;
+ break;
+
+ default:
+ err = -1; // stop uninitialised warning
+ break;
+ }
+
+ if( ( err == MN_A_NOBP || err == MN_H_NOBP ) && BG_Buildable(buildable,NULL)->cuboid )
+ {
+ err = MN_NONE;
+ ent->client->ps.stats[ STAT_BUILDABLE ] = SB_VALID_TOGGLEBIT;
+ }
+
+ if( err == MN_NONE || ent->client->pers.disableBlueprintErrors )
+ {
+ trap_SendServerCommand(ent->client-level.clients,va("cb2 %f %f %f\n",
+ ent->client->cuboidSelection[0],
+ ent->client->cuboidSelection[1],
+ ent->client->cuboidSelection[2]));
+ G_RelayCuboidToSpectators(ent->client);
+ ent->client->ps.stats[ STAT_BUILDABLE ] |= buildable;
+ }
+ else
+ G_TriggerMenu( ent->client->ps.clientNum, err );
+ }
+ else
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_CANNOT );
+}
+
+/*
+=================
+Cmd_Reload_f
+=================
+*/
+void Cmd_Reload_f( gentity_t *ent )
+{
+ playerState_t *ps = &ent->client->ps;
+ int ammo;
+
+ // weapon doesn't ever need reloading
+ if( BG_Weapon( ps->weapon )->infiniteAmmo )
+ return;
+
+ if( ps->clips <= 0 )
+ return;
+
+ if( BG_Weapon( ps->weapon )->usesEnergy &&
+ BG_InventoryContainsUpgrade( UP_BATTPACK, ps->stats ) )
+ ammo = BG_Weapon( ps->weapon )->maxAmmo * BATTPACK_MODIFIER;
+ else
+ ammo = BG_Weapon( ps->weapon )->maxAmmo;
+
+ // don't reload when full
+ if( ps->ammo >= ammo )
+ return;
+
+ // the animation, ammo refilling etc. is handled by PM_Weapon
+ if( ent->client->ps.weaponstate != WEAPON_RELOADING )
+ ent->client->ps.pm_flags |= PMF_WEAPON_RELOAD;
+}
+
+/*
+=================
+G_StopFromFollowing
+
+stops any other clients from following this one
+called when a player leaves a team or dies
+=================
+*/
+void G_StopFromFollowing( gentity_t *ent )
+{
+ int i;
+
+ for( i = 0; i < level.maxclients; i++ )
+ {
+ if( level.clients[ i ].sess.spectatorState == SPECTATOR_FOLLOW &&
+ level.clients[ i ].sess.spectatorClient == ent->client->ps.clientNum )
+ {
+ if( !G_FollowNewClient( &g_entities[ i ], 1 ) )
+ G_StopFollowing( &g_entities[ i ] );
+ }
+ }
+}
+
+/*
+=================
+G_StopFollowing
+
+If the client being followed leaves the game, or you just want to drop
+to free floating spectator mode
+=================
+*/
+void G_StopFollowing( gentity_t *ent )
+{
+ ent->client->ps.stats[ STAT_TEAM ] = ent->client->pers.teamSelection;
+
+ if( ent->client->pers.teamSelection == TEAM_NONE )
+ {
+ ent->client->sess.spectatorState =
+ ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_FREE;
+ }
+ else
+ {
+ vec3_t spawn_origin, spawn_angles;
+
+ ent->client->sess.spectatorState =
+ ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_LOCKED;
+
+ if( ent->client->pers.teamSelection == TEAM_ALIENS )
+ G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles );
+ else if( ent->client->pers.teamSelection == TEAM_HUMANS )
+ G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles );
+
+ G_SetOrigin( ent, spawn_origin );
+ VectorCopy( spawn_origin, ent->client->ps.origin );
+ G_SetClientViewAngle( ent, spawn_angles );
+ }
+ ent->client->sess.spectatorClient = -1;
+ ent->client->ps.pm_flags &= ~PMF_FOLLOW;
+ ent->client->ps.groundEntityNum = ENTITYNUM_NONE;
+ ent->client->ps.stats[ STAT_STATE ] = 0;
+ ent->client->ps.stats[ STAT_VIEWLOCK ] = 0;
+ ent->client->ps.eFlags &= ~( EF_WALLCLIMB | EF_WALLCLIMBCEILING );
+ ent->client->ps.viewangles[ PITCH ] = 0.0f;
+ ent->client->ps.clientNum = ent - g_entities;
+ ent->client->ps.persistant[ PERS_CREDIT ] = ent->client->pers.credit;
+
+ CalculateRanks( );
+}
+
+/*
+=================
+G_FollowLockView
+
+Client is still following a player, but that player has gone to spectator
+mode and cannot be followed for the moment
+=================
+*/
+void G_FollowLockView( gentity_t *ent )
+{
+ vec3_t spawn_origin, spawn_angles;
+ int clientNum;
+
+ clientNum = ent->client->sess.spectatorClient;
+ ent->client->sess.spectatorState =
+ ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_FOLLOW;
+ ent->client->ps.clientNum = clientNum;
+ ent->client->ps.pm_flags &= ~PMF_FOLLOW;
+ ent->client->ps.stats[ STAT_TEAM ] = ent->client->pers.teamSelection;
+ ent->client->ps.stats[ STAT_STATE ] &= ~SS_WALLCLIMBING;
+ ent->client->ps.stats[ STAT_VIEWLOCK ] = 0;
+ ent->client->ps.eFlags &= ~( EF_WALLCLIMB | EF_WALLCLIMBCEILING );
+ ent->client->ps.eFlags ^= EF_TELEPORT_BIT;
+ ent->client->ps.viewangles[ PITCH ] = 0.0f;
+
+ // Put the view at the team spectator lock position
+ if( level.clients[ clientNum ].pers.teamSelection == TEAM_ALIENS )
+ G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles );
+ else if( level.clients[ clientNum ].pers.teamSelection == TEAM_HUMANS )
+ G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles );
+
+ G_SetOrigin( ent, spawn_origin );
+ VectorCopy( spawn_origin, ent->client->ps.origin );
+ G_SetClientViewAngle( ent, spawn_angles );
+}
+
+/*
+=================
+G_FollowNewClient
+
+This was a really nice, elegant function. Then I fucked it up.
+=================
+*/
+qboolean G_FollowNewClient( gentity_t *ent, int dir )
+{
+ int clientnum = ent->client->sess.spectatorClient;
+ int original = clientnum;
+ qboolean selectAny = qfalse;
+
+ if( dir > 1 )
+ dir = 1;
+ else if( dir < -1 )
+ dir = -1;
+ else if( dir == 0 )
+ return qtrue;
+
+ if( ent->client->sess.spectatorState == SPECTATOR_NOT )
+ return qfalse;
+
+ // select any if no target exists
+ if( clientnum < 0 || clientnum >= level.maxclients )
+ {
+ clientnum = original = 0;
+ selectAny = qtrue;
+ }
+
+ do
+ {
+ clientnum += dir;
+
+ if( clientnum >= level.maxclients )
+ clientnum = 0;
+
+ if( clientnum < 0 )
+ clientnum = level.maxclients - 1;
+
+ // can't follow self
+ if( &g_entities[ clientnum ] == ent )
+ continue;
+
+ // avoid selecting existing follow target
+ if( clientnum == original && !selectAny )
+ continue; //effectively break;
+
+ // can only follow connected clients
+ if( level.clients[ clientnum ].pers.connected != CON_CONNECTED )
+ continue;
+
+ // can't follow a spectator
+ if( level.clients[ clientnum ].pers.teamSelection == TEAM_NONE )
+ continue;
+
+ // if stickyspec is disabled, can't follow someone in queue either
+ if( !ent->client->pers.stickySpec &&
+ level.clients[ clientnum ].sess.spectatorState != SPECTATOR_NOT )
+ continue;
+
+ // can only follow teammates when dead and on a team
+ if( ent->client->pers.teamSelection != TEAM_NONE &&
+ ( level.clients[ clientnum ].pers.teamSelection !=
+ ent->client->pers.teamSelection ) )
+ continue;
+
+ // this is good, we can use it
+ ent->client->sess.spectatorClient = clientnum;
+ ent->client->sess.spectatorState = SPECTATOR_FOLLOW;
+
+ // if this client is in the spawn queue, we need to do something special
+ if( level.clients[ clientnum ].sess.spectatorState != SPECTATOR_NOT )
+ G_FollowLockView( ent );
+
+ return qtrue;
+
+ } while( clientnum != original );
+
+ return qfalse;
+}
+
+/*
+=================
+G_ToggleFollow
+=================
+*/
+void G_ToggleFollow( gentity_t *ent )
+{
+ if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW )
+ G_StopFollowing( ent );
+ else
+ G_FollowNewClient( ent, 1 );
+}
+
+/*
+=================
+Cmd_Follow_f
+=================
+*/
+void Cmd_Follow_f( gentity_t *ent )
+{
+ int i;
+ char arg[ MAX_NAME_LENGTH ];
+
+ // won't work unless spectating
+ if( ent->client->sess.spectatorState == SPECTATOR_NOT )
+ return;
+
+ if( trap_Argc( ) != 2 )
+ {
+ G_ToggleFollow( ent );
+ }
+ else
+ {
+ char err[ MAX_STRING_CHARS ];
+ trap_Argv( 1, arg, sizeof( arg ) );
+
+ i = G_ClientNumberFromString( arg, err, sizeof( err ) );
+
+ if( i == -1 )
+ {
+ trap_SendServerCommand( ent - g_entities,
+ va( "print \"follow: %s\"", err ) );
+ return;
+ }
+
+ // can't follow self
+ if( &level.clients[ i ] == ent->client )
+ return;
+
+ // can't follow another spectator if sticky spec is off
+ if( !ent->client->pers.stickySpec &&
+ level.clients[ i ].sess.spectatorState != SPECTATOR_NOT )
+ return;
+
+ // if not on team spectator, you can only follow teammates
+ if( ent->client->pers.teamSelection != TEAM_NONE &&
+ ( level.clients[ i ].pers.teamSelection !=
+ ent->client->pers.teamSelection ) )
+ return;
+
+ ent->client->sess.spectatorState = SPECTATOR_FOLLOW;
+ ent->client->sess.spectatorClient = i;
+ }
+}
+
+/*
+=================
+Cmd_FollowCycle_f
+=================
+*/
+void Cmd_FollowCycle_f( gentity_t *ent )
+{
+ char args[ 11 ];
+ int dir = 1;
+
+ trap_Argv( 0, args, sizeof( args ) );
+ if( Q_stricmp( args, "followprev" ) == 0 )
+ dir = -1;
+
+ // won't work unless spectating
+ if( ent->client->sess.spectatorState == SPECTATOR_NOT )
+ return;
+
+ G_FollowNewClient( ent, dir );
+}
+
+static void Cmd_Ignore_f( gentity_t *ent )
+{
+ int pids[ MAX_CLIENTS ];
+ char name[ MAX_NAME_LENGTH ];
+ char cmd[ 9 ];
+ int matches = 0;
+ int i;
+ qboolean ignore = qfalse;
+
+ trap_Argv( 0, cmd, sizeof( cmd ) );
+ if( Q_stricmp( cmd, "ignore" ) == 0 )
+ ignore = qtrue;
+
+ if( trap_Argc() < 2 )
+ {
+ trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]"
+ "usage: %s [clientNum | partial name match]\n\"", cmd ) );
+ return;
+ }
+
+ Q_strncpyz( name, ConcatArgs( 1 ), sizeof( name ) );
+ matches = G_ClientNumbersFromString( name, pids, MAX_CLIENTS );
+ if( matches < 1 )
+ {
+ trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]"
+ "%s: no clients match the name '%s'\n\"", cmd, name ) );
+ return;
+ }
+
+ for( i = 0; i < matches; i++ )
+ {
+ if( ignore )
+ {
+ if( !Com_ClientListContains( &ent->client->sess.ignoreList, pids[ i ] ) )
+ {
+ Com_ClientListAdd( &ent->client->sess.ignoreList, pids[ i ] );
+ ClientUserinfoChanged( ent->client->ps.clientNum, qfalse );
+ trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]"
+ "ignore: added %s^7 to your ignore list\n\"",
+ level.clients[ pids[ i ] ].pers.netname ) );
+ }
+ else
+ {
+ trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]"
+ "ignore: %s^7 is already on your ignore list\n\"",
+ level.clients[ pids[ i ] ].pers.netname ) );
+ }
+ }
+ else
+ {
+ if( Com_ClientListContains( &ent->client->sess.ignoreList, pids[ i ] ) )
+ {
+ Com_ClientListRemove( &ent->client->sess.ignoreList, pids[ i ] );
+ ClientUserinfoChanged( ent->client->ps.clientNum, qfalse );
+ trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]"
+ "unignore: removed %s^7 from your ignore list\n\"",
+ level.clients[ pids[ i ] ].pers.netname ) );
+ }
+ else
+ {
+ trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]"
+ "unignore: %s^7 is not on your ignore list\n\"",
+ level.clients[ pids[ i ] ].pers.netname ) );
+ }
+ }
+ }
+}
+
+/*
+=================
+Cmd_ListMaps_f
+
+List all maps on the server
+=================
+*/
+
+static int SortMaps( const void *a, const void *b )
+{
+ return strcmp( *(char **)a, *(char **)b );
+}
+
+#define MAX_MAPLIST_MAPS 256
+#define MAX_MAPLIST_ROWS 9
+void Cmd_ListMaps_f( gentity_t *ent )
+{
+ char search[ 16 ] = {""};
+ char fileList[ 4096 ] = {""};
+ char *fileSort[ MAX_MAPLIST_MAPS ];
+ char *filePtr, *p;
+ int numFiles;
+ int fileLen = 0;
+ int shown = 0;
+ int count = 0;
+ int page = 0;
+ int pages;
+ int row, rows;
+ int start, i, j;
+
+ if( trap_Argc( ) > 1 )
+ {
+ trap_Argv( 1, search, sizeof( search ) );
+ for( p = search; ( *p ) && isdigit( *p ); p++ );
+ if( !( *p ) )
+ {
+ page = atoi( search );
+ search[ 0 ] = '\0';
+ }
+ else if( trap_Argc( ) > 2 )
+ {
+ char lp[ 8 ];
+ trap_Argv( 2, lp, sizeof( lp ) );
+ page = atoi( lp );
+ }
+
+ if( page > 0 )
+ page--;
+ else if( page < 0 )
+ page = 0;
+ }
+
+ numFiles = trap_FS_GetFileList( "maps/", ".bsp",
+ fileList, sizeof( fileList ) );
+ filePtr = fileList;
+ for( i = 0; i < numFiles && count < MAX_MAPLIST_MAPS; i++, filePtr += fileLen + 1 )
+ {
+ fileLen = strlen( filePtr );
+ if ( fileLen < 5 )
+ continue;
+
+ filePtr[ fileLen - 4 ] = '\0';
+
+ if( search[ 0 ] && !strstr( filePtr, search ) )
+ continue;
+
+ fileSort[ count ] = filePtr;
+ count++;
+ }
+ qsort( fileSort, count, sizeof( fileSort[ 0 ] ), SortMaps );
+
+ rows = ( count + 2 ) / 3;
+ pages = MAX( 1, ( rows + MAX_MAPLIST_ROWS - 1 ) / MAX_MAPLIST_ROWS );
+ if( page >= pages )
+ page = pages - 1;
+
+ start = page * MAX_MAPLIST_ROWS * 3;
+ if( count < start + ( 3 * MAX_MAPLIST_ROWS ) )
+ rows = ( count - start + 2 ) / 3;
+ else
+ rows = MAX_MAPLIST_ROWS;
+
+ ADMBP_begin( );
+ for( row = 0; row < rows; row++ )
+ {
+ for( i = start + row, j = 0; i < count && j < 3; i += rows, j++ )
+ {
+ ADMBP( va( "^7 %-20s", fileSort[ i ] ) );
+ shown++;
+ }
+ ADMBP( "\n" );
+ }
+ if ( search[ 0 ] )
+ ADMBP( va( "^3listmaps: ^7found %d maps matching '%s^7'", count, search ) );
+ else
+ ADMBP( va( "^3listmaps: ^7listing %d of %d maps", shown, count ) );
+ if( pages > 1 )
+ ADMBP( va( ", page %d of %d", page + 1, pages ) );
+ if( page + 1 < pages )
+ ADMBP( va( ", use 'listmaps %s%s%d' to see more",
+ search, ( search[ 0 ] ) ? " ": "", page + 2 ) );
+ ADMBP( ".\n" );
+ ADMBP_end( );
+}
+
+/*
+=================
+Cmd_Test_f
+=================
+*/
+void Cmd_Test_f( gentity_t *humanPlayer )
+{
+}
+
+/*
+=================
+Cmd_Damage_f
+
+Deals damage to you (for testing), arguments: [damage] [dx] [dy] [dz]
+The dx/dy arguments describe the damage point's offset from the entity origin
+=================
+*/
+void Cmd_Damage_f( gentity_t *ent )
+{
+ vec3_t point;
+ char arg[ 16 ];
+ float dx = 0.0f, dy = 0.0f, dz = 100.0f;
+ int damage = 100;
+ qboolean nonloc = qtrue;
+
+ if( trap_Argc() > 1 )
+ {
+ trap_Argv( 1, arg, sizeof( arg ) );
+ damage = atoi( arg );
+ }
+ if( trap_Argc() > 4 )
+ {
+ trap_Argv( 2, arg, sizeof( arg ) );
+ dx = atof( arg );
+ trap_Argv( 3, arg, sizeof( arg ) );
+ dy = atof( arg );
+ trap_Argv( 4, arg, sizeof( arg ) );
+ dz = atof( arg );
+ nonloc = qfalse;
+ }
+ VectorCopy( ent->s.origin, point );
+ point[ 0 ] += dx;
+ point[ 1 ] += dy;
+ point[ 2 ] += dz;
+ G_Damage( ent, NULL, NULL, NULL, point, damage,
+ ( nonloc ? DAMAGE_NO_LOCDAMAGE : 0 ), MOD_TARGET_LASER );
+}
+
+/*
+==================
+G_FloodLimited
+
+Determine whether a user is flood limited, and adjust their flood demerits
+Print them a warning message if they are over the limit
+Return is time in msec until the user can speak again
+==================
+*/
+int G_FloodLimited( gentity_t *ent )
+{
+ int deltatime, ms;
+
+ if( g_floodMinTime.integer <= 0 )
+ return 0;
+
+ // handles !ent
+ if( G_admin_permission( ent, ADMF_NOCENSORFLOOD ) )
+ return 0;
+
+ deltatime = level.time - ent->client->pers.floodTime;
+
+ ent->client->pers.floodDemerits += g_floodMinTime.integer - deltatime;
+ if( ent->client->pers.floodDemerits < 0 )
+ ent->client->pers.floodDemerits = 0;
+ ent->client->pers.floodTime = level.time;
+
+ ms = ent->client->pers.floodDemerits - g_floodMaxDemerits.integer;
+ if( ms <= 0 )
+ return 0;
+ trap_SendServerCommand( ent - g_entities, va( "print \"You are flooding: "
+ "please wait %d second%s before trying again\n",
+ ( ms + 999 ) / 1000, ( ms > 1000 ) ? "s" : "" ) );
+ return ms;
+}
+
+commands_t cmds[ ] = {
+ { "a", CMD_MESSAGE|CMD_INTERMISSION, Cmd_AdminMessage_f },
+ { "build", CMD_TEAM|CMD_LIVING, Cmd_Build_f },
+ { "buy", CMD_HUMAN|CMD_LIVING, Cmd_Buy_f },
+ { "callteamvote", CMD_MESSAGE|CMD_TEAM, Cmd_CallVote_f },
+ { "callvote", CMD_MESSAGE, Cmd_CallVote_f },
+ { "cb", 0, Cmd_Cb_f }, //NOTE: it's a command used only by cgame
+ { "class", CMD_TEAM, Cmd_Class_f },
+ { "damage", CMD_CHEAT|CMD_LIVING, Cmd_Damage_f },
+ { "deconstruct", CMD_TEAM|CMD_LIVING, Cmd_Destroy_f },
+ { "destroy", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Destroy_f },
+ { "follow", CMD_SPEC, Cmd_Follow_f },
+ { "follownext", CMD_SPEC, Cmd_FollowCycle_f },
+ { "followprev", CMD_SPEC, Cmd_FollowCycle_f },
+ { "give", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Give_f },
+ { "god", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_God_f },
+ { "ignore", 0, Cmd_Ignore_f },
+ { "itemact", CMD_HUMAN|CMD_LIVING, Cmd_ActivateItem_f },
+ { "itemdeact", CMD_HUMAN|CMD_LIVING, Cmd_DeActivateItem_f },
+ { "itemtoggle", CMD_HUMAN|CMD_LIVING, Cmd_ToggleItem_f },
+ { "kill", CMD_TEAM|CMD_LIVING, Cmd_Kill_f },
+ { "levelshot", CMD_CHEAT, Cmd_LevelShot_f },
+ { "listmaps", CMD_MESSAGE|CMD_INTERMISSION, Cmd_ListMaps_f },
+ { "m", CMD_MESSAGE|CMD_INTERMISSION, Cmd_PrivateMessage_f },
+ { "mt", CMD_MESSAGE|CMD_INTERMISSION, Cmd_PrivateMessage_f },
+ { "noclip", CMD_CHEAT_TEAM, Cmd_Noclip_f },
+ { "notarget", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Notarget_f },
+ { "reload", CMD_HUMAN|CMD_LIVING, Cmd_Reload_f },
+ { "say", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f },
+ { "say_area", CMD_MESSAGE|CMD_TEAM|CMD_LIVING, Cmd_SayArea_f },
+ { "say_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f },
+ { "score", CMD_INTERMISSION, ScoreboardMessage },
+ { "sell", CMD_HUMAN|CMD_LIVING, Cmd_Sell_f },
+ { "setviewpos", CMD_CHEAT_TEAM, Cmd_SetViewpos_f },
+ { "team", 0, Cmd_Team_f },
+ { "teamvote", CMD_TEAM, Cmd_Vote_f },
+ { "test", CMD_CHEAT, Cmd_Test_f },
+ { "unignore", 0, Cmd_Ignore_f },
+ { "vote", 0, Cmd_Vote_f },
+ { "vsay", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f },
+ { "vsay_local", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f },
+ { "vsay_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f },
+ { "where", 0, Cmd_Where_f }
+};
+static size_t numCmds = sizeof( cmds ) / sizeof( cmds[ 0 ] );
+
+/*
+=================
+ClientCommand
+=================
+*/
+void ClientCommand( int clientNum )
+{
+ gentity_t *ent;
+ char cmd[ MAX_TOKEN_CHARS ];
+ commands_t *command;
+
+ ent = g_entities + clientNum;
+ if( !ent->client || ent->client->pers.connected != CON_CONNECTED )
+ return; // not fully in game yet
+
+ trap_Argv( 0, cmd, sizeof( cmd ) );
+
+ command = bsearch( cmd, cmds, numCmds, sizeof( cmds[ 0 ] ), cmdcmp );
+
+ if( !command )
+ {
+ if( !G_admin_cmd_check( ent ) )
+ trap_SendServerCommand( clientNum,
+ va( "print \"Unknown command %s\n\"", cmd ) );
+ return;
+ }
+
+ // do tests here to reduce the amount of repeated code
+
+ if( !( command->cmdFlags & CMD_INTERMISSION ) &&
+ ( level.intermissiontime || level.pausedTime ) )
+ return;
+
+ if( command->cmdFlags & CMD_CHEAT && !g_cheats.integer )
+ {
+ G_TriggerMenu( clientNum, MN_CMD_CHEAT );
+ return;
+ }
+
+ if( command->cmdFlags & CMD_MESSAGE && ( ent->client->pers.namelog->muted ||
+ G_FloodLimited( ent ) ) )
+ return;
+
+ if( command->cmdFlags & CMD_TEAM &&
+ ent->client->pers.teamSelection == TEAM_NONE )
+ {
+ G_TriggerMenu( clientNum, MN_CMD_TEAM );
+ return;
+ }
+
+ if( command->cmdFlags & CMD_CHEAT_TEAM && !g_cheats.integer &&
+ ent->client->pers.teamSelection != TEAM_NONE )
+ {
+ G_TriggerMenu( clientNum, MN_CMD_CHEAT_TEAM );
+ return;
+ }
+
+ if( command->cmdFlags & CMD_SPEC &&
+ ent->client->sess.spectatorState == SPECTATOR_NOT )
+ {
+ G_TriggerMenu( clientNum, MN_CMD_SPEC );
+ return;
+ }
+
+ if( command->cmdFlags & CMD_ALIEN &&
+ ent->client->pers.teamSelection != TEAM_ALIENS )
+ {
+ G_TriggerMenu( clientNum, MN_CMD_ALIEN );
+ return;
+ }
+
+ if( command->cmdFlags & CMD_HUMAN &&
+ ent->client->pers.teamSelection != TEAM_HUMANS )
+ {
+ G_TriggerMenu( clientNum, MN_CMD_HUMAN );
+ return;
+ }
+
+ if( command->cmdFlags & CMD_LIVING &&
+ ( ent->client->ps.stats[ STAT_HEALTH ] <= 0 ||
+ ent->client->sess.spectatorState != SPECTATOR_NOT ) )
+ {
+ G_TriggerMenu( clientNum, MN_CMD_LIVING );
+ return;
+ }
+
+ command->cmdHandler( ent );
+}
+
+void G_ListCommands( gentity_t *ent )
+{
+ int i;
+ char out[ MAX_STRING_CHARS ] = "";
+ int len, outlen;
+
+ outlen = 0;
+
+ for( i = 0; i < numCmds; i++ )
+ {
+ len = strlen( cmds[ i ].cmdName ) + 1;
+ if( len + outlen >= sizeof( out ) - 1 )
+ {
+ trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) );
+ outlen = 0;
+ }
+
+ strcpy( out + outlen, va( " %s", cmds[ i ].cmdName ) );
+ outlen += len;
+ }
+
+ trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) );
+ G_admin_cmdlist( ent );
+}
+
+void G_DecolorString( char *in, char *out, int len )
+{
+ qboolean decolor = qtrue;
+
+ len--;
+
+ while( *in && len > 0 ) {
+ if( *in == DECOLOR_OFF || *in == DECOLOR_ON )
+ {
+ decolor = ( *in == DECOLOR_ON );
+ in++;
+ continue;
+ }
+ if( Q_IsColorString( in ) && decolor ) {
+ in += 2;
+ continue;
+ }
+ *out++ = *in++;
+ len--;
+ }
+ *out = '\0';
+}
+
+void G_UnEscapeString( char *in, char *out, int len )
+{
+ len--;
+
+ while( *in && len > 0 )
+ {
+ if( *in >= ' ' || *in == '\n' )
+ {
+ *out++ = *in;
+ len--;
+ }
+ in++;
+ }
+ *out = '\0';
+}
+
+void Cmd_PrivateMessage_f( gentity_t *ent )
+{
+ int pids[ MAX_CLIENTS ];
+ char name[ MAX_NAME_LENGTH ];
+ char cmd[ 12 ];
+ char text[ MAX_STRING_CHARS ];
+ char *msg;
+ char color;
+ int i, pcount;
+ int count = 0;
+ qboolean teamonly = qfalse;
+ char recipients[ MAX_STRING_CHARS ] = "";
+
+ if( !g_privateMessages.integer && ent )
+ {
+ ADMP( "Sorry, but private messages have been disabled\n" );
+ return;
+ }
+
+ trap_Argv( 0, cmd, sizeof( cmd ) );
+ if( trap_Argc( ) < 3 )
+ {
+ ADMP( va( "usage: %s [name|slot#] [message]\n", cmd ) );
+ return;
+ }
+
+ if( !Q_stricmp( cmd, "mt" ) )
+ teamonly = qtrue;
+
+ trap_Argv( 1, name, sizeof( name ) );
+ msg = ConcatArgs( 2 );
+ pcount = G_ClientNumbersFromString( name, pids, MAX_CLIENTS );
+
+ G_CensorString( text, msg, sizeof( text ), ent );
+
+ // send the message
+ for( i = 0; i < pcount; i++ )
+ {
+ if( G_SayTo( ent, &g_entities[ pids[ i ] ],
+ teamonly ? SAY_TPRIVMSG : SAY_PRIVMSG, text ) )
+ {
+ count++;
+ Q_strcat( recipients, sizeof( recipients ), va( "%s" S_COLOR_WHITE ", ",
+ level.clients[ pids[ i ] ].pers.netname ) );
+ }
+ }
+
+ // report the results
+ color = teamonly ? COLOR_CYAN : COLOR_YELLOW;
+
+ if( !count )
+ ADMP( va( "^3No player matching ^7\'%s^7\' ^3to send message to.\n",
+ name ) );
+ else
+ {
+ ADMP( va( "^%cPrivate message: ^7%s\n", color, text ) );
+ // remove trailing ", "
+ recipients[ strlen( recipients ) - 2 ] = '\0';
+ ADMP( va( "^%csent to %i player%s: " S_COLOR_WHITE "%s\n", color, count,
+ count == 1 ? "" : "s", recipients ) );
+
+ G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\" \"%s\": ^%c%s\n",
+ ( teamonly ) ? "TPrivMsg" : "PrivMsg",
+ ( ent ) ? ent - g_entities : -1,
+ ( ent ) ? ent->client->pers.netname : "console",
+ name, color, msg );
+ }
+}
+
+/*
+=================
+Cmd_AdminMessage_f
+
+Send a message to all active admins
+=================
+*/
+void Cmd_AdminMessage_f( gentity_t *ent )
+{
+ // Check permissions and add the appropriate user [prefix]
+ if( !G_admin_permission( ent, ADMF_ADMINCHAT ) )
+ {
+ if( !g_publicAdminMessages.integer )
+ {
+ ADMP( "Sorry, but use of /a by non-admins has been disabled.\n" );
+ return;
+ }
+ else
+ {
+ ADMP( "Your message has been sent to any available admins "
+ "and to the server logs.\n" );
+ }
+ }
+
+ if( trap_Argc( ) < 2 )
+ {
+ ADMP( "usage: a [message]\n" );
+ return;
+ }
+
+ G_AdminMessage( ent, ConcatArgs( 1 ) );
+}
+