/* =========================================================================== 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 =========================================================================== */ // cg_servercmds.c -- reliably sequenced text commands sent by the server // these are processed at snapshot transition time, so there will definately // be a valid snapshot this frame #include "cg_local.h" /* ================= CG_ParseScores ================= */ static void CG_ParseScores( void ) { int i; cg.numScores = ( trap_Argc( ) - 3 ) / 6; if( cg.numScores > MAX_CLIENTS ) cg.numScores = MAX_CLIENTS; cg.teamScores[ 0 ] = atoi( CG_Argv( 1 ) ); cg.teamScores[ 1 ] = atoi( CG_Argv( 2 ) ); memset( cg.scores, 0, sizeof( cg.scores ) ); if( cg_debugRandom.integer ) CG_Printf( "cg.numScores: %d\n", cg.numScores ); for( i = 0; i < cg.numScores; i++ ) { // cg.scores[ i ].client = atoi( CG_Argv( i * 6 + 3 ) ); cg.scores[ i ].score = atoi( CG_Argv( i * 6 + 4 ) ); cg.scores[ i ].ping = atoi( CG_Argv( i * 6 + 5 ) ); cg.scores[ i ].time = atoi( CG_Argv( i * 6 + 6 ) ); cg.scores[ i ].weapon = atoi( CG_Argv( i * 6 + 7 ) ); cg.scores[ i ].upgrade = atoi( CG_Argv( i * 6 + 8 ) ); if( cg.scores[ i ].client < 0 || cg.scores[ i ].client >= MAX_CLIENTS ) cg.scores[ i ].client = 0; cgs.clientinfo[ cg.scores[ i ].client ].score = cg.scores[ i ].score; cg.scores[ i ].team = cgs.clientinfo[ cg.scores[ i ].client ].team; } } /* ================= CG_ParseTeamInfo ================= */ static void CG_ParseTeamInfo( void ) { int i; int count; int client; count = ( trap_Argc( ) - 1 ) / 5; cgs.teaminfoReceievedTime = cg.time; for( i = 0; i < count; i++ ) { client = atoi( CG_Argv( i * 5 + 1 ) ); if( client < 0 || client >= MAX_CLIENTS ) { CG_Printf( "[skipnotify]CG_ParseTeamInfo: bad client number: %d\n", client ); return; } cgs.clientinfo[ client ].location = atoi( CG_Argv( i * 5 + 2 ) ); cgs.clientinfo[ client ].health = atoi( CG_Argv( i * 5 + 3 ) ); cgs.clientinfo[ client ].curWeaponClass = atoi( CG_Argv( i * 5 + 4 ) ); cgs.clientinfo[ client ].upgrade = atoi( CG_Argv( i * 5 + 5 ) ); } } /* ================ CG_ParseServerinfo This is called explicitly when the gamestate is first received, and whenever the server updates any serverinfo flagged cvars ================ */ void CG_ParseServerinfo( void ) { const char *info; char *mapname; info = CG_ConfigString( CS_SERVERINFO ); cgs.timelimit = atoi( Info_ValueForKey( info, "timelimit" ) ); cgs.maxclients = atoi( Info_ValueForKey( info, "sv_maxclients" ) ); cgs.markDeconstruct = atoi( Info_ValueForKey( info, "g_markDeconstruct" ) ); mapname = Info_ValueForKey( info, "mapname" ); Com_sprintf( cgs.mapname, sizeof( cgs.mapname ), "maps/%s.bsp", mapname ); } /* ================== CG_ParseWarmup ================== */ static void CG_ParseWarmup( void ) { const char *info; int warmup; info = CG_ConfigString( CS_WARMUP ); warmup = atoi( info ); cg.warmupTime = warmup; } /* ================ CG_SetConfigValues Called on load to set the initial values from configure strings ================ */ void CG_SetConfigValues( void ) { const char *alienStages = CG_ConfigString( CS_ALIEN_STAGES ); const char *humanStages = CG_ConfigString( CS_HUMAN_STAGES ); if( alienStages[0] ) { sscanf( alienStages, "%d %d %d", &cgs.alienStage, &cgs.alienCredits, &cgs.alienNextStageThreshold ); } else cgs.alienStage = cgs.alienCredits = cgs.alienNextStageThreshold = 0; if( humanStages[0] ) { sscanf( humanStages, "%d %d %d", &cgs.humanStage, &cgs.humanCredits, &cgs.humanNextStageThreshold ); } else cgs.humanStage = cgs.humanCredits = cgs.humanNextStageThreshold = 0; cgs.levelStartTime = atoi( CG_ConfigString( CS_LEVEL_START_TIME ) ); cg.warmupTime = atoi( CG_ConfigString( CS_WARMUP ) ); } /* ===================== CG_ShaderStateChanged ===================== */ void CG_ShaderStateChanged( void ) { char originalShader[ MAX_QPATH ]; char newShader[ MAX_QPATH ]; char timeOffset[ 16 ]; const char *o; char *n, *t; o = CG_ConfigString( CS_SHADERSTATE ); while( o && *o ) { n = strstr( o, "=" ); if( n && *n ) { strncpy( originalShader, o, n - o ); originalShader[ n - o ] = 0; n++; t = strstr( n, ":" ); if( t && *t ) { strncpy( newShader, n, t - n ); newShader[ t - n ] = 0; } else break; t++; o = strstr( t, "@" ); if( o ) { strncpy( timeOffset, t, o - t ); timeOffset[ o - t ] = 0; o++; trap_R_RemapShader( originalShader, newShader, timeOffset ); } } else break; } } /* ================ CG_AnnounceAlienStageTransistion ================ */ static void CG_AnnounceAlienStageTransistion( stage_t from, stage_t to ) { if( cg.predictedPlayerState.stats[ STAT_TEAM ] != TEAM_ALIENS ) return; trap_S_StartLocalSound( cgs.media.alienStageTransition, CHAN_ANNOUNCER ); CG_CenterPrint( "^5We have evolved!", 200, GIANTCHAR_WIDTH * 4 ); } /* ================ CG_AnnounceHumanStageTransistion ================ */ static void CG_AnnounceHumanStageTransistion( stage_t from, stage_t to ) { if( cg.predictedPlayerState.stats[ STAT_TEAM ] != TEAM_HUMANS ) return; trap_S_StartLocalSound( cgs.media.humanStageTransition, CHAN_ANNOUNCER ); CG_CenterPrint( "^5Reinforcements have arrived!", 200, GIANTCHAR_WIDTH * 4 ); } /* ================ CG_ConfigStringModified ================ */ static void CG_ConfigStringModified( void ) { const char *str; int num; num = atoi( CG_Argv( 1 ) ); // get the gamestate from the client system, which will have the // new configstring already integrated trap_GetGameState( &cgs.gameState ); // look up the individual string that was modified str = CG_ConfigString( num ); // do something with it if necessary if( num == CS_MUSIC ) CG_StartMusic( ); else if( num == CS_SERVERINFO ) CG_ParseServerinfo( ); else if( num == CS_WARMUP ) CG_ParseWarmup( ); else if( num == CS_ALIEN_STAGES ) { stage_t oldAlienStage = cgs.alienStage; if( str[0] ) { sscanf( str, "%d %d %d", &cgs.alienStage, &cgs.alienCredits, &cgs.alienNextStageThreshold ); if( cgs.alienStage != oldAlienStage ) CG_AnnounceAlienStageTransistion( oldAlienStage, cgs.alienStage ); } else { cgs.alienStage = cgs.alienCredits = cgs.alienNextStageThreshold = 0; } } else if( num == CS_HUMAN_STAGES ) { stage_t oldHumanStage = cgs.humanStage; if( str[0] ) { sscanf( str, "%d %d %d", &cgs.humanStage, &cgs.humanCredits, &cgs.humanNextStageThreshold ); if( cgs.humanStage != oldHumanStage ) CG_AnnounceHumanStageTransistion( oldHumanStage, cgs.humanStage ); } else { cgs.humanStage = cgs.humanCredits = cgs.humanNextStageThreshold = 0; } } else if( num == CS_LEVEL_START_TIME ) cgs.levelStartTime = atoi( str ); else if( num >= CS_VOTE_TIME && num < CS_VOTE_TIME + NUM_TEAMS ) { cgs.voteTime[ num - CS_VOTE_TIME ] = atoi( str ); cgs.voteModified[ num - CS_VOTE_TIME ] = qtrue; if( num - CS_VOTE_TIME == TEAM_NONE ) trap_Cvar_Set( "ui_voteActive", cgs.voteTime[ TEAM_NONE ] ? "1" : "0" ); else if( num - CS_VOTE_TIME == TEAM_ALIENS ) trap_Cvar_Set( "ui_alienTeamVoteActive", cgs.voteTime[ TEAM_ALIENS ] ? "1" : "0" ); else if( num - CS_VOTE_TIME == TEAM_HUMANS ) trap_Cvar_Set( "ui_humanTeamVoteActive", cgs.voteTime[ TEAM_HUMANS ] ? "1" : "0" ); } else if( num >= CS_VOTE_YES && num < CS_VOTE_YES + NUM_TEAMS ) { cgs.voteYes[ num - CS_VOTE_YES ] = atoi( str ); cgs.voteModified[ num - CS_VOTE_YES ] = qtrue; } else if( num >= CS_VOTE_NO && num < CS_VOTE_NO + NUM_TEAMS ) { cgs.voteNo[ num - CS_VOTE_NO ] = atoi( str ); cgs.voteModified[ num - CS_VOTE_NO ] = qtrue; } else if( num >= CS_VOTE_STRING && num < CS_VOTE_STRING + NUM_TEAMS ) Q_strncpyz( cgs.voteString[ num - CS_VOTE_STRING ], str, sizeof( cgs.voteString[ num - CS_VOTE_STRING ] ) ); else if( num >= CS_VOTE_CALLER && num < CS_VOTE_CALLER + NUM_TEAMS ) Q_strncpyz( cgs.voteCaller[ num - CS_VOTE_CALLER ], str, sizeof( cgs.voteCaller[ num - CS_VOTE_CALLER ] ) ); else if( num == CS_INTERMISSION ) cg.intermissionStarted = atoi( str ); else if( num >= CS_MODELS && num < CS_MODELS+MAX_MODELS ) cgs.gameModels[ num - CS_MODELS ] = trap_R_RegisterModel( str ); else if( num >= CS_SHADERS && num < CS_SHADERS+MAX_GAME_SHADERS ) cgs.gameShaders[ num - CS_SHADERS ] = trap_R_RegisterShader( str ); else if( num >= CS_PARTICLE_SYSTEMS && num < CS_PARTICLE_SYSTEMS+MAX_GAME_PARTICLE_SYSTEMS ) cgs.gameParticleSystems[ num - CS_PARTICLE_SYSTEMS ] = CG_RegisterParticleSystem( (char *)str ); else if( num >= CS_SOUNDS && num < CS_SOUNDS+MAX_SOUNDS ) { if( str[ 0 ] != '*' ) { // player specific sounds don't register here cgs.gameSounds[ num - CS_SOUNDS ] = trap_S_RegisterSound( str, qfalse ); } } else if( num >= CS_PLAYERS && num < CS_PLAYERS+MAX_CLIENTS ) { CG_NewClientInfo( num - CS_PLAYERS ); CG_BuildSpectatorString( ); } else if( num == CS_WINNER ) { trap_Cvar_Set( "ui_winner", str ); } else if( num == CS_SHADERSTATE ) { CG_ShaderStateChanged( ); } } /* =============== CG_MapRestart The server has issued a map_restart, so the next snapshot is completely new and should not be interpolated to. A tournement restart will clear everything, but doesn't require a reload of all the media =============== */ static void CG_MapRestart( void ) { if( cg_showmiss.integer ) CG_Printf( "CG_MapRestart\n" ); CG_InitMarkPolys( ); // make sure the "3 frags left" warnings play again cg.fraglimitWarnings = 0; cg.timelimitWarnings = 0; cg.intermissionStarted = qfalse; cgs.voteTime[ TEAM_NONE ] = 0; cg.mapRestart = qtrue; CG_StartMusic( ); trap_S_ClearLoopingSounds( qtrue ); // we really should clear more parts of cg here and stop sounds trap_Cvar_Set( "cg_thirdPerson", "0" ); } /* ============== CG_Menu ============== */ void CG_Menu( int menu, int arg ) { const char *cmd; // command to send const char *longMsg = NULL; // command parameter const char *shortMsg = NULL; // non-modal version of message const char *dialog; dialogType_t type = 0; // controls which cg_disable var will switch it off switch( cg.snap->ps.stats[ STAT_TEAM ] ) { case TEAM_ALIENS: dialog = "menu tremulous_alien_dialog\n"; break; case TEAM_HUMANS: dialog = "menu tremulous_human_dialog\n"; break; default: dialog = "menu tremulous_default_dialog\n"; } cmd = dialog; switch( menu ) { case MN_WELCOME: cmd = "menu ingame\n"; type = DT_INTERACTIVE; break; case MN_TEAM: cmd = "menu tremulous_teamselect\n"; type = DT_INTERACTIVE; break; case MN_A_CLASS: cmd = "menu tremulous_alienclass\n"; type = DT_INTERACTIVE; break; case MN_H_SPAWN: cmd = "menu tremulous_humanitem\n"; type = DT_INTERACTIVE; break; case MN_A_BUILD: cmd = "menu tremulous_alienbuild\n"; type = DT_INTERACTIVE; break; case MN_H_BUILD: cmd = "menu tremulous_humanbuild\n"; type = DT_INTERACTIVE; break; case MN_H_ARMOURY: cmd = "menu tremulous_humanarmoury\n"; type = DT_INTERACTIVE; break; case MN_H_UNKNOWNITEM: shortMsg = "Unknown item"; type = DT_ARMOURYEVOLVE; break; case MN_A_TEAMFULL: longMsg = "The alien team has too many players. Please wait until slots " "become available or join the human team."; shortMsg = "^5The alien team has too many players"; type = DT_COMMAND; break; case MN_H_TEAMFULL: longMsg = "The human team has too many players. Please wait until slots " "become available or join the alien team."; shortMsg = "^5The human team has too many players"; type = DT_COMMAND; break; case MN_A_TEAMLOCKED: longMsg = "The alien team is locked. You cannot join the aliens " "at this time."; shortMsg = "^5The alien team is locked"; type = DT_COMMAND; break; case MN_H_TEAMLOCKED: longMsg = "The human team is locked. You cannot join the humans " "at this time."; shortMsg = "^5The human team is locked"; type = DT_COMMAND; break; case MN_PLAYERLIMIT: longMsg = "The maximum number of playing clients has been reached. " "Please wait until slots become available."; shortMsg = "^5No free player slots"; type = DT_COMMAND; break; case MN_WARMUP: longMsg = "You must wait until the warmup time is finished " "before joining a team. "; shortMsg = "^5You cannot join a team during warmup."; type = DT_COMMAND; break; //=============================== // Since cheating commands have no default binds, they will often be done // via console. In light of this, perhaps opening a menu is // counterintuitive case MN_CMD_CHEAT: //longMsg = "This action is considered cheating. It can only be used " // "in cheat mode, which is not enabled on this server."; shortMsg = "^5Cheats are not enabled on this server"; type = DT_COMMAND; break; case MN_CMD_CHEAT_TEAM: shortMsg = "^5Cheats are not enabled on this server, so " "^5you may not use this command while on a team"; type = DT_COMMAND; break; case MN_CMD_TEAM: //longMsg = "You must be on a team to perform this action. Join the alien" // "or human team and try again."; shortMsg = "^5Join a team first"; type = DT_COMMAND; break; case MN_CMD_SPEC: //longMsg = "You may not perform this action while on a team. Become a " // "spectator before trying again."; shortMsg = "^5You can only use this command when spectating"; type = DT_COMMAND; break; case MN_CMD_ALIEN: //longMsg = "You must be on the alien team to perform this action."; shortMsg = "^5Must be alien to use this command"; type = DT_COMMAND; break; case MN_CMD_HUMAN: //longMsg = "You must be on the human team to perform this action."; shortMsg = "^5Must be human to use this command"; type = DT_COMMAND; break; case MN_CMD_LIVING: //longMsg = "You must be living to perform this action."; shortMsg = "^5Must be living to use this command"; type = DT_COMMAND; break; //=============================== case MN_B_NOROOM: longMsg = "There is no room to build here. Move until the structure turns " "translucent green, indicating a valid build location."; shortMsg = "^5There is no room to build here"; type = DT_BUILD; break; case MN_B_NORMAL: longMsg = "Cannot build on this surface. The surface is too steep or " "unsuitable for building. Please choose another site for this " "structure."; shortMsg = "^5Cannot build on this surface"; type = DT_BUILD; break; case MN_B_CANNOT: longMsg = NULL; shortMsg = "^5You cannot build that structure"; type = DT_BUILD; break; // FIXME: MN_H_ and MN_A_? case MN_B_LASTSPAWN: longMsg = "This action would remove your team's last spawn point, " "which often quickly results in a loss. Try building more " "spawns."; shortMsg = "^5You may not deconstruct the last spawn"; break; case MN_B_BLOCKEDBYENEMY: switch( cg.snap->ps.stats[ STAT_TEAM ] ) { case TEAM_ALIENS: longMsg = "You cannot build within the range of a human reactor " "or a repeater. In order to build here your team has " "to destroy the nearby human structure first."; shortMsg = "^5A nearby human reactor or a repeater is disrupting creep here."; break; case TEAM_HUMANS: longMsg = "You cannot build on alien creep, within the range of " "an egg. In order to build here your team has " "to destroy the nearby alien egg(s) first."; shortMsg = "^5Alien creep is disrupting power here."; break; } break; case MN_B_GTHRBLOCKED: switch( cg.snap->ps.stats[ STAT_TEAM ] ) { case TEAM_ALIENS: longMsg = "Creep colonies cannot be near each other, " // or near the Overmind, " "there must be enough distance between them. You need to build " "a creep colony at a place that is far enough from others."; // " and form the Overmind."; shortMsg = "^5A creep colony has already been built in this area."; break; case TEAM_HUMANS: longMsg = "Refineries cannot be near each other, " // or near the Reactor, " "there must be enough distance between them. You need to build " "a refinery at a place that is far enough from others."; // " and from the Reactor."; shortMsg = "^5A refinery has already been built in this area."; break; } break; case MN_B_WSD_INBASE: longMsg = "Neither team has prevailed after a certain time and the " "game has entered Weak Sudden Death. During Sudden Death " "building creep colonies or refineries is not allowed, " "and neither is allowed building near the Overmind or the Reactor."; shortMsg = "^5Cannot build near the Reactor/Overmind during Weak Sudden Death"; type = DT_BUILD; break; case MN_B_WSD_REFSCOLS: longMsg = "Neither team has prevailed after a certain time and the " "game has entered Weak Sudden Death. During Weak Sudden Death " "building creep colonies or refineries is not allowed, " "and neither is allowed building near the Overmind or the Reactor."; shortMsg = "^5Cannot build refineries/colonies during Weak Sudden Death"; type = DT_BUILD; break; case MN_B_SUDDENDEATH: longMsg = "Neither team has prevailed after a certain time and the " "game has entered Sudden Death. During Sudden Death " "building is not allowed."; shortMsg = "^5Cannot build during Sudden Death"; type = DT_BUILD; break; case MN_B_REVOKED: longMsg = "Your teammates have lost faith in your ability to build " "for the team. You will not be allowed to build until your " "team votes to reinstate your building rights."; shortMsg = "^5Your building rights have been revoked"; type = DT_BUILD; break; case MN_B_SURRENDER: longMsg = "Your team has decided to admit defeat and concede the game:" "traitors and cowards are not allowed to build."; // too harsh? shortMsg = "^5Building is denied to traitorous cowards"; break; //=============================== case MN_H_NOBP: if( cgs.markDeconstruct ) longMsg = "^5There is no power remaining. Free up power by marking " "existing buildable objects."; else longMsg = "There is no power remaining. Free up power by deconstructing " "existing buildable objects."; shortMsg = "^5There is no power remaining"; type = DT_BUILD; break; case MN_H_NOTPOWERED: longMsg = "This buildable is not powered. Build a Reactor and/or Repeater " "in order to power it."; shortMsg = "^5This buildable is not powered"; type = DT_BUILD; break; case MN_H_ONEREACTOR: longMsg = "There can only be one Reactor. Deconstruct the existing one if you " "wish to move it."; shortMsg = "^5There can only be one Reactor"; type = DT_BUILD; break; case MN_H_NOPOWERHERE: longMsg = "There is no power here. If available, a Repeater may be used to " "transmit power to this location."; shortMsg = "^5There is no power here"; type = DT_BUILD; break; case MN_H_NODCC: longMsg = "There is no Defense Computer. A Defense Computer is needed to " "build this."; shortMsg = "^5There is no Defense Computer"; type = DT_BUILD; break; case MN_H_RPTPOWERHERE: longMsg = "This area already has power. A Repeater is not required here."; shortMsg = "^5This area already has power"; type = DT_BUILD; break; case MN_H_NOSLOTS: longMsg = "You have no room to carry this. Please sell any conflicting " "upgrades before purchasing this item."; shortMsg = "^5You have no room to carry this"; type = DT_ARMOURYEVOLVE; break; case MN_H_NOFUNDS: longMsg = "Insufficient funds. You do not have enough credits to perform " "this action."; shortMsg = "^5Insufficient funds"; type = DT_ARMOURYEVOLVE; break; case MN_H_ITEMHELD: longMsg = "You already hold this item. It is not possible to carry multiple " "items of the same type."; shortMsg = "^5You already hold this item"; type = DT_ARMOURYEVOLVE; break; case MN_H_NOARMOURYHERE: longMsg = "You must be near a powered Armoury in order to purchase " "weapons, upgrades or ammunition."; shortMsg = "^5You must be near a powered Armoury or Ammo Supply"; type = DT_ARMOURYEVOLVE; break; case MN_H_NOENERGYAMMOHERE: longMsg = "You must be near a Reactor or a powered Armoury or Repeater " "in order to purchase energy ammunition."; shortMsg = "^5You must be near a Reactor or a powered Armoury or Repeater"; type = DT_ARMOURYEVOLVE; break; case MN_H_NOROOMBSUITON: longMsg = "There is not enough room here to put on a Battle Suit. " "Make sure you have enough head room to climb in."; shortMsg = "^5Not enough room here to put on a Battle Suit"; type = DT_ARMOURYEVOLVE; break; case MN_H_NOROOMBSUITOFF: longMsg = "There is not enough room here to take off your Battle Suit. " "Make sure you have enough head room to climb out."; shortMsg = "^5Not enough room here to take off your Battle Suit"; type = DT_ARMOURYEVOLVE; break; case MN_H_ARMOURYBUILDTIMER: longMsg = "You are not allowed to buy or sell weapons until your " "build timer has expired."; shortMsg = "^5You can not buy or sell weapons until your build timer " "^5expires"; type = DT_ARMOURYEVOLVE; break; case MN_H_DEADTOCLASS: shortMsg = "^5You must be dead to use the class command"; type = DT_COMMAND; break; case MN_H_UNKNOWNSPAWNITEM: shortMsg = "^5Builder limit hit, wait until a free builder slot is free / unknown item"; type = DT_COMMAND; break; //=============================== case MN_A_NOCREEP: longMsg = "There is no creep here. You must build near existing Eggs or " "the Overmind. Alien structures will not support themselves."; shortMsg = "^5There is no creep here"; type = DT_BUILD; break; case MN_A_NOOVMND: longMsg = "There is no Overmind. An Overmind must be built to control " "the structure you tried to place."; shortMsg = "^5There is no Overmind"; type = DT_BUILD; break; case MN_A_ONEOVERMIND: longMsg = "There can only be one Overmind. Deconstruct the existing one if you " "wish to move it."; shortMsg = "^5There can only be one Overmind"; type = DT_BUILD; break; case MN_A_COCOON: longMsg = "There can only be one Cocoon. Deconstruct the existing one if you " "wish to move it."; shortMsg = "^5There can only be one Cocoon"; type = DT_BUILD; break; case MN_A_NOBP: longMsg = "The Overmind cannot control any more structures. Deconstruct existing " "structures to build more."; shortMsg = "^5The Overmind cannot control any more structures"; type = DT_BUILD; break; case MN_A_NOEROOM: longMsg = "There is no room to evolve here. Move away from walls or other " "nearby objects and try again."; shortMsg = "^5There is no room to evolve here"; type = DT_ARMOURYEVOLVE; break; case MN_A_TOOCLOSE: longMsg = "This location is too close to the enemy to evolve. Move away " "from the enemy's presence and try again."; shortMsg = "^5This location is too close to the enemy to evolve"; type = DT_ARMOURYEVOLVE; break; case MN_A_NOOVMND_EVOLVE: longMsg = "There is no Overmind. An Overmind must be built to allow " "you to upgrade."; shortMsg = "^5There is no Overmind"; type = DT_ARMOURYEVOLVE; break; case MN_A_EVOLVEBUILDTIMER: longMsg = "You cannot evolve until your build timer has expired."; shortMsg = "^5You cannot evolve until your build timer expires"; type = DT_ARMOURYEVOLVE; break; case MN_A_INFEST: trap_Cvar_Set( "ui_currentClass", va( "%d %d", cg.snap->ps.stats[ STAT_CLASS ], cg.snap->ps.persistant[ PERS_CREDIT ] ) ); cmd = "menu tremulous_alienupgrade\n"; type = DT_INTERACTIVE; break; case MN_A_CANTEVOLVE: shortMsg = va( "^5You cannot evolve into a %s", BG_ClassConfig( arg )->humanName ); type = DT_ARMOURYEVOLVE; break; case MN_A_EVOLVEWALLWALK: shortMsg = "^5You cannot evolve while wallwalking"; type = DT_ARMOURYEVOLVE; break; case MN_A_UNKNOWNCLASS: shortMsg = "^5Unknown class"; type = DT_ARMOURYEVOLVE; break; case MN_A_CLASSNOTSPAWN: shortMsg = va( "^5You cannot spawn as a %s after SD", BG_ClassConfig( arg )->humanName ); type = DT_ARMOURYEVOLVE; break; case MN_A_CLASSNOTALLOWED: shortMsg = va( "^5The %s is not allowed due builder limit. Wait until a builder slots gets free / unknown class", BG_ClassConfig( arg )->humanName ); type = DT_ARMOURYEVOLVE; break; case MN_A_CLASSNOTATSTAGE: shortMsg = va( "^5The %s is not allowed at Stage %d", BG_ClassConfig( arg )->humanName, cgs.alienStage + 1 ); type = DT_ARMOURYEVOLVE; break; default: Com_Printf( "cgame: debug: no such menu %d\n", menu ); } if( type == DT_ARMOURYEVOLVE && cg_disableUpgradeDialogs.integer ) return; if( type == DT_BUILD && cg_disableBuildDialogs.integer ) return; if( type == DT_COMMAND && cg_disableCommandDialogs.integer ) return; if( cmd != dialog ) { trap_SendConsoleCommand( cmd ); } else if( longMsg && cg_disableWarningDialogs.integer == 0 ) { trap_Cvar_Set( "ui_dialog", longMsg ); trap_SendConsoleCommand( cmd ); } else if( shortMsg && cg_disableWarningDialogs.integer < 2 ) { CG_Printf( "%s\n", shortMsg ); } } /* ================= CG_Say ================= */ static void CG_Say( int clientNum, saymode_t mode, const char *text ) { char *name; char prefix[ 11 ] = ""; char *ignore = ""; char *location = ""; char *color; char *maybeColon; if( clientNum >= 0 && clientNum < MAX_CLIENTS ) { clientInfo_t *ci = &cgs.clientinfo[ clientNum ]; char *tcolor = S_COLOR_WHITE; name = ci->name; if( ci->team == TEAM_ALIENS ) tcolor = S_COLOR_RED; else if( ci->team == TEAM_HUMANS ) tcolor = S_COLOR_CYAN; if( cg_chatTeamPrefix.integer ) Com_sprintf( prefix, sizeof( prefix ), "[%s%c" S_COLOR_WHITE "] ", tcolor, toupper( *( BG_TeamName( ci->team ) ) ) ); if( Com_ClientListContains( &cgs.ignoreList, clientNum ) ) ignore = "[skipnotify]"; if( ( mode == SAY_TEAM || mode == SAY_AREA ) && cg.snap->ps.pm_type != PM_INTERMISSION ) { int locationNum; if( clientNum == cg.snap->ps.clientNum ) { centity_t *locent; locent = CG_GetPlayerLocation( ); if( locent ) locationNum = locent->currentState.generic1; else locationNum = 0; } else locationNum = ci->location; if( locationNum > 0 && locationNum < MAX_LOCATIONS ) { const char *s = CG_ConfigString( CS_LOCATIONS + locationNum ); if( *s ) location = va( " (%s" S_COLOR_WHITE ")", s ); } } } else name = "console"; // IRC-like /me parsing if( mode != SAY_RAW && Q_stricmpn( text, "/me ", 4 ) == 0 ) { text += 4; Q_strcat( prefix, sizeof( prefix ), "* " ); maybeColon = ""; } else maybeColon = ":"; switch( mode ) { case SAY_ALL: // might already be ignored but in that case no harm is done if( cg_teamChatsOnly.integer ) ignore = "[skipnotify]"; CG_Printf( "%s%s%s" S_COLOR_WHITE "%s %c" S_COLOR_GREEN "%s\n", ignore, prefix, name, maybeColon, INDENT_MARKER, text ); break; case SAY_TEAM: CG_Printf( "%s%s(%s" S_COLOR_WHITE ")%s%s %c" S_COLOR_CYAN "%s\n", ignore, prefix, name, location, maybeColon, INDENT_MARKER, text ); break; case SAY_ADMINS: case SAY_ADMINS_PUBLIC: CG_Printf( "%s%s%s%s" S_COLOR_WHITE "%s %c" S_COLOR_MAGENTA "%s\n", ignore, prefix, ( mode == SAY_ADMINS ) ? "[ADMIN]" : "[PLAYER]", name, maybeColon, INDENT_MARKER, text ); break; case SAY_AREA: CG_Printf( "%s%s<%s" S_COLOR_WHITE ">%s%s %c" S_COLOR_BLUE "%s\n", ignore, prefix, name, location, maybeColon, INDENT_MARKER, text ); break; case SAY_PRIVMSG: case SAY_TPRIVMSG: color = ( mode == SAY_TPRIVMSG ) ? S_COLOR_CYAN : S_COLOR_GREEN; CG_Printf( "%s%s[%s" S_COLOR_WHITE " -> %s" S_COLOR_WHITE "]%s %c%s%s\n", ignore, prefix, name, cgs.clientinfo[ cg.clientNum ].name, maybeColon, INDENT_MARKER, color, text ); if( !ignore[0] ) CG_CenterPrint( va( "%sPrivate message from: " S_COLOR_WHITE "%s", color, name ), 200, GIANTCHAR_WIDTH * 4 ); break; case SAY_RAW: CG_Printf( "%s\n", text ); break; } switch( mode ) { case SAY_TEAM: case SAY_AREA: case SAY_TPRIVMSG: if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) { trap_S_StartLocalSound( cgs.media.alienTalkSound, CHAN_LOCAL_SOUND ); break; } else if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) { trap_S_StartLocalSound( cgs.media.humanTalkSound, CHAN_LOCAL_SOUND ); break; } default: trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND ); } } /* ================= CG_VoiceTrack return the voice indexed voice track or print errors quietly to console in case someone is on an unpure server and wants to know which voice pak is missing or incomplete ================= */ static voiceTrack_t *CG_VoiceTrack( char *voice, int cmd, int track ) { voice_t *v; voiceCmd_t *c; voiceTrack_t *t; v = BG_VoiceByName( cgs.voices, voice ); if( !v ) { CG_Printf( "[skipnotify]WARNING: could not find voice \"%s\"\n", voice ); return NULL; } c = BG_VoiceCmdByNum( v->cmds, cmd ); if( !c ) { CG_Printf( "[skipnotify]WARNING: could not find command %d " "in voice \"%s\"\n", cmd, voice ); return NULL; } t = BG_VoiceTrackByNum( c->tracks, track ); if( !t ) { CG_Printf( "[skipnotify]WARNING: could not find track %d for command %d in " "voice \"%s\"\n", track, cmd, voice ); return NULL; } return t; } /* ================= CG_ParseVoice voice clientNum vChan cmdNum trackNum [sayText] ================= */ static void CG_ParseVoice( void ) { int clientNum; voiceChannel_t vChan; char sayText[ MAX_SAY_TEXT] = {""}; voiceTrack_t *track; clientInfo_t *ci; if( trap_Argc() < 5 || trap_Argc() > 6 ) return; if( trap_Argc() == 6 ) Q_strncpyz( sayText, CG_Argv( 5 ), sizeof( sayText ) ); clientNum = atoi( CG_Argv( 1 ) ); if( clientNum < 0 || clientNum >= MAX_CLIENTS ) return; vChan = atoi( CG_Argv( 2 ) ); if( vChan < 0 || vChan >= VOICE_CHAN_NUM_CHANS ) return; if( cg_teamChatsOnly.integer && vChan != VOICE_CHAN_TEAM ) return; ci = &cgs.clientinfo[ clientNum ]; // this joker is still talking if( ci->voiceTime > cg.time ) return; track = CG_VoiceTrack( ci->voice, atoi( CG_Argv( 3 ) ), atoi( CG_Argv( 4 ) ) ); // keep track of how long the player will be speaking // assume it takes 3s to say "*unintelligible gibberish*" if( track ) ci->voiceTime = cg.time + track->duration; else ci->voiceTime = cg.time + 3000; if( !sayText[ 0 ] ) { if( track ) Q_strncpyz( sayText, track->text, sizeof( sayText ) ); else Q_strncpyz( sayText, "*unintelligible gibberish*", sizeof( sayText ) ); } if( !cg_noVoiceText.integer ) { switch( vChan ) { case VOICE_CHAN_ALL: CG_Say( clientNum, SAY_ALL, sayText ); break; case VOICE_CHAN_TEAM: CG_Say( clientNum, SAY_TEAM, sayText ); break; default: break; } } // playing voice audio tracks disabled if( cg_noVoiceChats.integer ) return; // no audio track to play if( !track ) return; // don't play audio track for lamers if( Com_ClientListContains( &cgs.ignoreList, clientNum ) ) return; switch( vChan ) { case VOICE_CHAN_ALL: trap_S_StartLocalSound( track->track, CHAN_VOICE ); break; case VOICE_CHAN_TEAM: trap_S_StartLocalSound( track->track, CHAN_VOICE ); break; case VOICE_CHAN_LOCAL: trap_S_StartSound( NULL, clientNum, CHAN_VOICE, track->track ); break; default: break; } } /* ================= CG_CenterPrint_f ================= */ static void CG_CenterPrint_f( void ) { CG_CenterPrint( CG_Argv( 1 ), SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH ); } /* ================= CG_Print_f ================= */ static void CG_Print_f( void ) { CG_Printf( "%s", CG_Argv( 1 ) ); } /* ================= CG_Chat_f ================= */ static void CG_Chat_f( void ) { char id[ 3 ]; char mode[ 3 ]; /* char chatclientnum[ 3 ]; char teamSelection[ 3 ]; trap_Argv( 1, chatclientnum, sizeof( id ) ); trap_Argv( 2, teamSelection, sizeof( mode ) ); trap_Argv( 3, id, sizeof( id ) ); trap_Argv( 4, mode, sizeof( mode ) ); if (atoi( chatclientnum ) == -1)//pro kompatibilitu se starymi funkcemi, jen se prida parametr -1 { trap_Argv( 1, id, sizeof( id ) ); trap_Argv( 2, mode, sizeof( mode ) ); } CG_Say( atoi( chatclientnum ), atoi( teamSelection ), atoi( id ), atoi( mode ), CG_Argv( 5 ) ); */ trap_Argv( 1, id, sizeof( id ) ); trap_Argv( 2, mode, sizeof( mode ) ); CG_Say( atoi( id ), atoi( mode ), CG_Argv( 3 ) ); } /* ================= CG_ClientLevelShot_f ================= */ static void CG_ClientLevelShot_f( void ) { cg.levelShot = qtrue; } /* ================= CG_ServerMenu_f ================= */ static void CG_ServerMenu_f( void ) { if( !cg.demoPlayback ) { if( trap_Argc( ) == 2 ) CG_Menu( atoi( CG_Argv( 1 ) ), 0 ); else if( trap_Argc( ) == 3 ) CG_Menu( atoi( CG_Argv( 1 ) ), atoi( CG_Argv( 2 ) ) ); } } /* ================= CG_ServerCloseMenus_f ================= */ static void CG_ServerCloseMenus_f( void ) { trap_SendConsoleCommand( "closemenus\n" ); } /* ================= CG_PoisonCloud_f ================= */ static void CG_PoisonCloud_f( void ) { cg.poisonedTime = cg.time; if( CG_IsParticleSystemValid( &cg.poisonCloudPS ) ) { cg.poisonCloudPS = CG_SpawnNewParticleSystem( cgs.media.poisonCloudPS ); CG_SetAttachmentCent( &cg.poisonCloudPS->attachment, &cg.predictedPlayerEntity ); CG_AttachToCent( &cg.poisonCloudPS->attachment ); } } static void CG_GameCmds_f( void ) { int i; int c = trap_Argc( ); /* There is no corresponding trap_RemoveCommand because a server could send something like cmds quit which would result in trap_RemoveCommand( "quit" ), which would be really bad */ for( i = 1; i < c; i++ ) trap_AddCommand( CG_Argv( i ) ); } static void CG_DamageBlob_f( void ) { int i, count = trap_Argc( ), value, flags; vec3_t origin; for( i = 1; i + 4 < count; i += 5 ) { origin[ 0 ] = atof( CG_Argv( i ) ); origin[ 1 ] = atof( CG_Argv( i + 1 ) ); origin[ 2 ] = atof( CG_Argv( i + 2 ) ); value = atoi( CG_Argv( i + 3 ) ); flags = atoi( CG_Argv( i + 4 ) ); CG_SpawnDamageBlob( origin, value, flags ); i += 5; } } static consoleCommand_t svcommands[ ] = { { "chat", CG_Chat_f }, { "clientLevelShot", CG_ClientLevelShot_f }, { "cmds", CG_GameCmds_f }, { "cp", CG_CenterPrint_f }, { "cs", CG_ConfigStringModified }, { "dblob", CG_DamageBlob_f }, { "map_restart", CG_MapRestart }, { "poisoncloud", CG_PoisonCloud_f }, { "print", CG_Print_f }, { "scores", CG_ParseScores }, { "serverclosemenus", CG_ServerCloseMenus_f }, { "servermenu", CG_ServerMenu_f }, { "tinfo", CG_ParseTeamInfo }, { "voice", CG_ParseVoice } }; /* ================= CG_ServerCommand The string has been tokenized and can be retrieved with Cmd_Argc() / Cmd_Argv() ================= */ static void CG_ServerCommand( void ) { const char *cmd; consoleCommand_t *command; cmd = CG_Argv( 0 ); command = bsearch( cmd, svcommands, sizeof( svcommands ) / sizeof( svcommands[ 0 ]), sizeof( svcommands[ 0 ] ), cmdcmp ); if( command ) { command->function( ); return; } CG_Printf( "Unknown client game command: %s\n", cmd ); } /* ==================== CG_ExecuteNewServerCommands Execute all of the server commands that were received along with this this snapshot. ==================== */ void CG_ExecuteNewServerCommands( int latestSequence ) { while( cgs.serverCommandSequence < latestSequence ) { if( trap_GetServerCommand( ++cgs.serverCommandSequence ) ) CG_ServerCommand( ); } }