/* Copyright (C) 1997-2001 Id Software, Inc. This program 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. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // sv_user.c -- server code for moving users #include "server.h" edict_t *sv_player; /* ============================================================ USER STRINGCMD EXECUTION sv_client and sv_player will be valid. ============================================================ */ /* ================ SV_New_f Sends the first message from the server to a connected client. This will be sent on the initial connection and upon each server load. ================ */ void SV_New_f( void ) { char *gamedir; int playernum; edict_t *ent; int request_confistrings = -1; int sv_bitflags = 0; Com_DPrintf( "New() from %s\n", sv_client->name ); // if in CS_AWAITING we have sended the response packet the new once already, // but client might have not got it so we send it again if( sv_client->state != CS_CONNECTED && sv_client->state != CS_AWAITING ) { Com_Printf( "New not valid -- already spawned\n" ); return; } // // serverdata needs to go over for all types of servers // to make sure the protocol is right, and to set the gamedir // gamedir = Cvar_VariableString( "fs_gamedir" ); MSG_Clear( &tmpMessage ); // send the serverdata MSG_WriteByte( &tmpMessage, svc_serverdata ); MSG_WriteLong( &tmpMessage, PROTOCOL_VERSION ); MSG_WriteLong( &tmpMessage, svs.spawncount ); MSG_WriteString( &tmpMessage, gamedir ); playernum = sv_client - svs.clients; MSG_WriteShort( &tmpMessage, playernum ); // send full levelname MSG_WriteString( &tmpMessage, sv.name ); // // game server // if( sv.state == ss_game ) { // set up the entity for the client ent = EDICT_NUM(playernum+1); ent->s.number = playernum+1; sv_client->edict = ent; memset( &sv_client->lastcmd, 0, sizeof(sv_client->lastcmd) ); #ifdef BATTLEYE if( sv_battleye->integer ) sv_bitflags |= 128; #endif MSG_WriteByte( &tmpMessage, sv_bitflags ); #if 1 // we want it to request configstrings request_confistrings = 0; #else // write as many configstrings as we can fit into the message { int start = 0; int ofs_configstrings = tmpMessage.cursize; int ofs_recover; MSG_WriteShort( &tmpMessage, 0 ); // write a packet full of data while( start < MAX_CONFIGSTRINGS && tmpMessage.cursize < MAX_PACKETLEN * 3 ) { if( sv.configstrings[start][0] ) { //SV_SendServerCommand( sv_client, "cs %i \"%s\"", start, sv.configstrings[start] ); MSG_WriteShort( &tmpMessage, start ); MSG_WriteString( &tmpMessage, sv.configstrings[start] ); } start++; } ofs_recover = tmpMessage.cursize; tmpMessage.cursize = ofs_configstrings; MSG_WriteShort( &tmpMessage, start ); tmpMessage.cursize = ofs_recover; request_confistrings = start; } #endif } MSG_WriteShort( &tmpMessage, request_confistrings ); // reset the reliable commands chain sv_client->clientCommandExecuted = 0; sv_client->reliableAcknowledge = 0; sv_client->reliableSequence = 0; sv_client->reliableSent = 0; memset( &sv_client->reliableCommands, 0, sizeof(sv_client->reliableCommands) ); #ifdef BATTLEYE memset( &sv_client->BE, 0, sizeof(sv_client->BE) ); #endif SV_SendMessageToClient( sv_client, &tmpMessage ); Netchan_PushAllFragments( &sv_client->netchan ); // don't let it send reliable commands until we get the first configstring request sv_client->state = CS_AWAITING; } //================== //SV_Configstrings_f //================== void SV_Configstrings_f( void ) { int start; if( sv_client->state == CS_AWAITING ) { Com_DPrintf( "Start Configstrings() from %s\n", sv_client->name ); sv_client->state = CS_CONNECTED; } else Com_DPrintf( "Configstrings() from %s\n", sv_client->name ); if( sv_client->state != CS_CONNECTED ) { Com_Printf( "configstrings not valid -- already spawned\n" ); return; } // handle the case of a level changing while a client was connecting if( atoi(Cmd_Argv(1)) != svs.spawncount ) { Com_Printf( "SV_Configstrings_f from different level\n" ); SV_New_f(); return; } start = atoi(Cmd_Argv(2)); if( start < 0 ) { start = 0; } // write a packet full of data while( start < MAX_CONFIGSTRINGS && sv_client->reliableSequence - sv_client->reliableAcknowledge < MAX_RELIABLE_COMMANDS - 8 ) { if( sv.configstrings[start][0] ) { SV_SendServerCommand( sv_client, "cs %i \"%s\"", start, sv.configstrings[start] ); } start++; } // send next command if( start == MAX_CONFIGSTRINGS ) { SV_SendServerCommand( sv_client, "cmd baselines %i 0", svs.spawncount ); } else { SV_SendServerCommand( sv_client, "cmd configstrings %i %i", svs.spawncount, start ); } } #define MAX_BASELINE_MSGLEN 2000 //================== //SV_Baselines_f //================== void SV_Baselines_f( void ) { int start; entity_state_t nullstate; entity_state_t *base; Com_DPrintf( "Baselines() from %s\n", sv_client->name ); if( sv_client->state != CS_CONNECTED ) { Com_Printf( "baselines not valid -- already spawned\n" ); return; } // handle the case of a level changing while a client was connecting if( atoi(Cmd_Argv(1)) != svs.spawncount ) { Com_Printf( "SV_Baselines_f from different level\n" ); SV_New_f(); return; } start = atoi(Cmd_Argv(2)); if( start < 0 ) start = 0; memset( &nullstate, 0, sizeof(nullstate) ); // write a packet full of data MSG_Clear( &tmpMessage ); while( tmpMessage.cursize < FRAGMENT_SIZE * 3 && start < MAX_EDICTS ) { base = &sv.baselines[start]; if( base->modelindex || base->sound || base->effects ) { MSG_WriteByte( &tmpMessage, svc_spawnbaseline ); MSG_WriteDeltaEntity( &nullstate, base, &tmpMessage, qtrue, qtrue ); } start++; } // send next command if( start == MAX_EDICTS ) { SV_SendServerCommand( sv_client, "precache %i", svs.spawncount ); } else { SV_SendServerCommand( sv_client, "cmd baselines %i %i", svs.spawncount, start ); } SV_AddReliableCommandsToMessage( sv_client, &tmpMessage ); SV_SendMessageToClient( sv_client, &tmpMessage ); } /* ================== SV_Begin_f ================== */ void SV_Begin_f( void ) { Com_DPrintf( "Begin() from %s\n", sv_client->name ); // wsw : r1q2[start] : could be abused to respawn or cause spam/other mod-specific problems if( sv_client->state != CS_CONNECTED ) { if( dedicated->integer ) Com_Printf( "SV_Begin_f: 'Begin' from already spawned client: %s.\n", sv_client->name ); SV_DropClient( sv_client, DROP_TYPE_GENERAL, "Already spawned client" ); return; } // wsw : r1q2[end] // handle the case of a level changing while a client was connecting if( atoi(Cmd_Argv(1)) != svs.spawncount ) { Com_Printf( "SV_Begin_f from different level\n" ); SV_New_f(); return; } sv_client->state = CS_SPAWNED; // call the game begin function ge->ClientBegin( sv_player ); Cbuf_InsertFromDefer(); } //============================================================================= /* ================== SV_NextDownload_f Responds to reliable nextdl packet with unreliable download packet If nextdl packet's offet information is negative, download will be stopped ================== */ void SV_NextDownload_f( void ) { int blocksize; int offset; if( !sv_uploads->integer || !sv_uploads_from_server->integer ) return; if( strlen(sv_client->downloadname) == 0 ) { Com_Printf( "nextdl message for client with no download active from %s\n", sv_client->name ); return; } if( Q_stricmp(sv_client->downloadname, Cmd_Argv(1)) ) { Com_Printf( "nextdl message for wrong filename from %s\n", sv_client->name ); return; } offset = atoi(Cmd_Argv(2)); if( offset >= sv_client->downloadsize ) { Com_Printf( "nextdl message with too big offset from %s\n", sv_client->name ); return; } if( offset == -1 ) { Com_Printf( "Upload of %s to %s completed\n", sv_client->downloadname, sv_client->name ); if( sv_client->download ) { FS_FreeFile( sv_client->download ); sv_client->download = NULL; } sv_client->downloadtimeout = 0; sv_client->downloadname[0] = 0; sv_client->downloadsize = 0; return; } if( offset < 0 ) { Com_Printf( "Upload of %s to %s failed\n", sv_client->downloadname, sv_client->name ); if( sv_client->download ) { FS_FreeFile( sv_client->download ); sv_client->download = NULL; } sv_client->downloadtimeout = 0; sv_client->downloadname[0] = 0; sv_client->downloadsize = 0; return; } if( !sv_client->download ) { char fullname[MAX_OSPATH]; Com_Printf( "Starting server upload of %s to %s\n", sv_client->downloadname, sv_client->name ); Q_snprintfz( fullname, sizeof(fullname), "%s/%s", FS_Gamedir(), sv_client->downloadname ); FS_LoadAbsoluteFile( fullname, (void **)&sv_client->download, NULL, 0 ); if( !sv_client->download ) { sv_client->downloadname[0] = 0; sv_client->downloadsize = 0; Com_Printf( "Error loading %s for uploading\n", fullname ); return; } } MSG_Clear( &tmpMessage ); SV_AddReliableCommandsToMessage( sv_client, &tmpMessage ); blocksize = sv_client->downloadsize - offset; // jalfixme: addapt download to user rate setting and sv_maxrate setting. if( blocksize > FRAGMENT_SIZE * 2 ) blocksize = FRAGMENT_SIZE * 2; if( offset + blocksize > sv_client->downloadsize ) blocksize = sv_client->downloadsize - offset; MSG_WriteByte( &tmpMessage, svc_download ); MSG_WriteString( &tmpMessage, sv_client->downloadname ); MSG_WriteLong( &tmpMessage, offset ); MSG_WriteLong( &tmpMessage, blocksize ); MSG_Write( &tmpMessage, sv_client->download + offset, blocksize ); SV_SendMessageToClient( sv_client, &tmpMessage ); sv_client->downloadtimeout = svs.realtime + 10000; } /* ================== SV_DenyDownload Helper function for generating initdownload packets for denying download ================== */ static void SV_DenyDownload( const char *reason ) { // size -1 is used to signal that it's refused // URL field is used for deny reason MSG_Clear( &tmpMessage ); SV_SendServerCommand( sv_client, "initdownload \"%s\" %i %u %i \"%s\"", "", -1, 0, qfalse, reason ? reason : "" ); SV_AddReliableCommandsToMessage( sv_client, &tmpMessage ); SV_SendMessageToClient( sv_client, &tmpMessage ); } /* ================== SV_BeginDownload_f Responds to reliable download packet with reliable initdownload packet ================== */ void SV_BeginDownload_f( void ) { static char *allowed_directories[] = { "maps/", "sounds/", NULL }; char *name, *pakname; qboolean allowed, found; char tempname[MAX_QPATH]; int i; unsigned checksum; if( !sv_uploads->integer || (!sv_uploads_from_server->integer && (strlen(sv_uploads_baseurl->string) == 0)) ) { SV_DenyDownload( "Downloading is not allowed on this server" ); return; } name = Cmd_Argv(1); // hacked by zoid to allow more conrol over download // first off, no .. or global allow check if( strstr (name, "..") || *name == '.' || *name == '/' || strchr (name, '\"') || *name == '$' // deny player models by now || !strstr (name, "/") ) // MUST be in a subdirectory { SV_DenyDownload("Invalid filename"); return; } // only allow downloads from certain subdirectories allowed = qfalse; for( i = 0; allowed_directories[i] != NULL; i++ ) { if( Q_strnicmp(name, allowed_directories[i], strlen(allowed_directories[i])) ) { allowed = qtrue; break; } } if( !allowed) { SV_DenyDownload("Invalid file location"); return; } // only allow files that are in precache list found = qfalse; for( i = CS_MODELS; i < CS_IMAGES+MAX_IMAGES; i++ ) { if( strlen(sv.configstrings[i]) == 0 ) continue; if( i == CS_MODELS || i == CS_SOUNDS || i == CS_IMAGES ) // empty fields continue; if( i == CS_MODELS+1 ) // map { if( !Q_stricmp( sv.configstrings[i], name ) ) { found = qtrue; break; } continue; } else if( i < CS_MODELS+MAX_MODELS ) { if (sv.configstrings[i][0] == '*' || sv.configstrings[i][0] == '$' || // disable playermodel downloading for now sv.configstrings[i][0] == '#') { continue; } if( !Q_stricmp( sv.configstrings[i], name ) ) { found = qtrue; break; } continue; } else if( i < CS_SOUNDS+MAX_SOUNDS ) { if( sv.configstrings[i][0] == '*' ) // sexed sounds continue; Q_strncpyz( tempname, sv.configstrings[i], sizeof(tempname) ); COM_DefaultExtension( tempname, ".wav", sizeof(tempname) ); if( !Q_stricmp( tempname, name ) ) { found = qtrue; break; } continue; } else if( i < CS_IMAGES+MAX_IMAGES ) { // disabled for now continue; } } if( !found ) { SV_DenyDownload("File is not in precache list"); return; } if( FS_FOpenFile ( name, NULL, FS_READ ) == -1 ) { SV_DenyDownload("Server doesn't have this file"); return; } // check if file is inside a PAK pakname = FS_PakNameForFile( name ); if( !pakname ) { SV_DenyDownload("Not inside pak file"); return; } if( sv_client->download ) { FS_FreeFile( sv_client->download ); sv_client->download = NULL; } sv_client->downloadsize = FS_LoadAbsoluteFile( pakname, NULL, NULL, 0 ); if( sv_client->downloadsize <= 0 ) { Com_Printf( "Error getting size of %s for uploading\n", sv_client->downloadname ); sv_client->downloadname[0] = 0; sv_client->downloadsize = 0; SV_DenyDownload( "Error getting file size" ); return; } checksum = SV_GetCachedPakChecksum(pakname); // remove gamedir if( strlen(pakname) > 2 ) pakname += 2; while( pakname && *pakname != '/' ) { pakname++; } if( *pakname == '/' ) pakname++; if( !Q_stricmp("data0.pk3", pakname) ) { if( sv_client->download ) { FS_FreeFile( sv_client->download ); sv_client->download = NULL; } sv_client->downloadtimeout = 0; sv_client->downloadname[0] = 0; sv_client->downloadsize = 0; SV_DenyDownload("Can't download data0.pk3"); return; } Q_strncpyz( sv_client->downloadname, pakname, sizeof(sv_client->downloadname) ); Com_Printf( "Offering %s to %s\n", sv_client->downloadname, sv_client->name ); // start the download MSG_Clear( &tmpMessage ); SV_SendServerCommand( sv_client, "initdownload \"%s\" %i %u %i \"%s\"", sv_client->downloadname, sv_client->downloadsize, checksum, (sv_uploads_from_server->integer != 0), (strlen(sv_uploads_baseurl->string) > 0) ? va("%s/%s", sv_uploads_baseurl->string, pakname) : "" ); SV_AddReliableCommandsToMessage( sv_client, &tmpMessage ); SV_SendMessageToClient( sv_client, &tmpMessage ); } //============================================================================ /* ================= SV_Disconnect_f The client is going to disconnect, so remove the connection immediately ================= */ void SV_Disconnect_f( void ) { // SV_EndRedirect(); SV_DropClient( sv_client, DROP_TYPE_GENERAL, "User disconnected" ); } /* ================== SV_ShowServerinfo_f Dumps the serverinfo info string ================== */ void SV_ShowServerinfo_f( void ) { Info_Print( Cvar_Serverinfo() ); } void SV_Nextserver( void ) { char *v; //ZOID, ss_pic can be nextserver'd in coop mode if( sv.state == ss_game ) return; // can't nextserver while playing a normal game svs.spawncount++; // make sure another doesn't sneak in v = Cvar_VariableString("nextserver"); if( !v[0] ) Cbuf_AddText( "killserver\n" ); else { Cbuf_AddText(v); Cbuf_AddText("\n"); } Cvar_Set("nextserver",""); } /* ================== SV_Nextserver_f A cinematic has completed or been aborted by a client, so move to the next server, ================== */ void SV_Nextserver_f( void ) { if( atoi(Cmd_Argv(1)) != svs.spawncount ) { Com_DPrintf( "Nextserver() from wrong level, from %s\n", sv_client->name ); return; // leftover from last server } Com_DPrintf( "Nextserver() from %s\n", sv_client->name ); SV_Nextserver(); } /* ================== SV_UserinfoCommand_f ================== */ void SV_UserinfoCommand_f( void ) { char *uinfo; uinfo = Cmd_Argv(1); if( Cmd_Argc() > 2 ) { // invalid ? Com_DPrintf( "WARNING: SV_ParseUserinfoCommand_f: userinfo string with more than 1 tokem received from client %s\n", sv_client->name ); } // empty userinfo received if( !uinfo || !uinfo[0] ) { if( !strlen(sv_client->userinfo) ) SV_DropClient( sv_client, DROP_TYPE_GENERAL, "Empty userinfo received" ); return; } Q_strncpyz( sv_client->userinfo, uinfo, sizeof(sv_client->userinfo) ); SV_UserinfoChanged( sv_client ); } typedef struct { char *name; void (*func) (void); } ucmd_t; ucmd_t ucmds[] = { // auto issued { "new", SV_New_f }, { "configstrings", SV_Configstrings_f }, { "baselines", SV_Baselines_f }, { "begin", SV_Begin_f }, { "nextserver", SV_Nextserver_f }, { "disconnect", SV_Disconnect_f }, { "usri", SV_UserinfoCommand_f }, // issued by hand at client consoles { "info", SV_ShowServerinfo_f }, { "download", SV_BeginDownload_f }, { "nextdl", SV_NextDownload_f }, { NULL, NULL } }; /* ================== SV_ExecuteUserCommand ================== */ char *Cmd_MacroExpandString(char *text); void SV_ExecuteUserCommand( char *s ) { ucmd_t *u; // wsw : r1q2[start] : catch attempted command expansions if( strchr(s, '$') ) { char *teststring = Cmd_MacroExpandString(s); if( !teststring ) return; if( strcmp(teststring, s) ) { Com_Printf( "Exploit filtered: Client %s[%s] attempted macro-expansion\n", sv_client->name, NET_AdrToString(&sv_client->netchan.remoteAddress) ); return; } } // wsw : r1q2[end] // wsw : r1q2[start] : catch end-of-message exploit if( strchr (s, '\xFF') ) { if( dedicated->integer ) Com_Printf( "%s [%s] Dropped. Found end-of-message inside command string\n", sv_client->name, NET_AdrToString(&sv_client->netchan.remoteAddress) ); SV_DropClient( sv_client, DROP_TYPE_GENERAL, "Found end-of-message inside command string" ); return; } // wsw : r1q2[end] Cmd_TokenizeString( s, qfalse ); sv_player = sv_client->edict; for( u=ucmds ; u->name ; u++ ) { if( !strcmp(Cmd_Argv(0), u->name) ) { u->func(); break; } } // wsw : r1q2: don't pass commands to game before being fully connected if( sv_client->state < CS_SPAWNED ) return; if( !u->name && sv.state == ss_game ) ge->ClientCommand(sv_player); } /* =========================================================================== USER CMD EXECUTION =========================================================================== */ void SV_ClientThink( client_t *cl, usercmd_t *cmd ) { cl->commandMsec -= cmd->msec; if( cl->commandMsec < 0 && sv_enforcetime->integer ) { Com_Printf/*Com_DPrintf*/( "commandMsec underflow from %s\n", cl->name ); return; } ge->ClientThink( cl->edict, cmd ); } #ifdef BATTLEYE void SV_ParseCLCBattleye( client_t *client, msg_t *msg ) { static qbyte tempdata[BE_MAX_PACKET_SIZE+4]; short len; unsigned int id; client->BE.acknowledgedPacket = MSG_ReadLong( msg ); len = MSG_ReadShort( msg ); if( len > 0 && len <= BE_MAX_PACKET_SIZE ) { MSG_ReadData( msg, tempdata+4, len ); id = MSG_ReadLong( msg ); if( id > client->BE.commandReceived ) { client->BE.commandReceived = id; // pass this packet to BE Master tempdata[0] = 0; // packet type tempdata[1] = client - svs.clients; // player id *(short*)(tempdata+2) = len; if( sv_battleye->integer && client->battleye ) // should never happen, but checking doesn't hurt SV_BE_SendToMaster( tempdata, len+4 ); } } else if( len != 0 ) SV_DropClient( client, DROP_TYPE_GENERAL, "Invalid BattlEye packet size" ); } #endif #define MAX_STRINGCMDS 8 #define MAX_USERINFO_UPDATES 8 // wsw : r1q2 : limit userinfo update per packet /* =================== SV_ExecuteClientMessage The current message is parsed for the given client =================== */ void SV_ExecuteClientMessage( client_t *client, msg_t *msg ) { int c; char *s; usercmd_t nullcmd; usercmd_t oldest, oldcmd, newcmd; int net_drop; int checksum, calculatedChecksum; int checksumIndex; qboolean move_issued; int lastframe; int userinfoCount; // wsw : r1q2 if( !msg ) return; sv_client = client; sv_player = sv_client->edict; // only allow one move command move_issued = qfalse; userinfoCount = 0; // wsw : r1q2 while( 1 ) { if( msg->readcount > msg->cursize ) { Com_Printf( "SV_ReadClientMessage: badread\n" ); SV_DropClient( client, DROP_TYPE_GENERAL, "SV_ReadClientMessage: bad message from client" ); return; } c = MSG_ReadByte( msg ); if( c == -1 ) break; switch( c ) { default: Com_Printf( "SV_ReadClientMessage: unknown command char\n" ); SV_DropClient( client, DROP_TYPE_GENERAL, "SV_ReadClientMessage: unknown command char" ); return; case clc_nop: break; case clc_move: { if( move_issued ) return; // someone is trying to cheat... move_issued = qtrue; checksumIndex = msg->readcount; checksum = MSG_ReadByte( msg ); lastframe = MSG_ReadLong( msg ); memset( &nullcmd, 0, sizeof(nullcmd) ); MSG_ReadDeltaUsercmd( msg, &nullcmd, &oldest ); MSG_ReadDeltaUsercmd( msg, &oldest, &oldcmd ); MSG_ReadDeltaUsercmd( msg, &oldcmd, &newcmd ); // calc ping if( lastframe != client->lastframe ) { client->lastframe = lastframe; if( client->lastframe > 0 ) { int timeonclient = newcmd.serverTimeStamp - client->frames[lastframe & UPDATE_MASK].sentTimeStamp; int roundtriprealtime = svs.realtime - client->frames[lastframe & UPDATE_MASK].sentTimeStamp; client->frame_latency[client->lastframe&(LATENCY_COUNTS-1)] = roundtriprealtime - timeonclient; } } if( client->state != CS_SPAWNED ) { client->lastframe = -1; break; } // if the checksum fails, ignore the rest of the packet calculatedChecksum = COM_BlockSequenceCRCByte ( msg->data + checksumIndex + 1, msg->readcount - checksumIndex - 1, client->netchan.incomingSequence ); if( calculatedChecksum != checksum ) { Com_DPrintf( "Failed command checksum for %s (%d != %d)/%d\n", client->name, calculatedChecksum, checksum, client->netchan.incomingSequence ); return; } net_drop = client->netchan.dropped; if( net_drop < 20 ) { while( net_drop > 2 ) { SV_ClientThink( client, &client->lastcmd ); net_drop--; } if( net_drop > 1 ) SV_ClientThink( client, &oldest ); if( net_drop > 0 ) SV_ClientThink( client, &oldcmd ); } SV_ClientThink( client, &newcmd ); client->lastcmd = newcmd; } break; case clc_svcack: { int cmdNum = MSG_ReadLong( msg ); if( cmdNum < 0 ) { SV_DropClient( client, DROP_TYPE_GENERAL, "SV_ExecuteClientMessage: bad server command acknowledged" ); return; } client->reliableAcknowledge = cmdNum; } break; case clc_clientcommand: { int cmdNum; cmdNum = MSG_ReadLong( msg ); if( cmdNum <= client->clientCommandExecuted ) { s = MSG_ReadString( msg ); // read but ignore continue; } client->clientCommandExecuted = cmdNum; s = MSG_ReadString( msg ); SV_ExecuteUserCommand(s); if( client->state == CS_ZOMBIE ) return; // disconnect command } break; #ifdef BATTLEYE case clc_battleye: SV_ParseCLCBattleye( client, msg ); break; #endif } } }