mirror of
https://github.com/YGGverse/xash3d-fwgs.git
synced 2025-01-09 06:37:53 +00:00
5e0a0765ce
The `.editorconfig` file in this repo is configured to trim all trailing whitespace regardless of whether the line is modified. Trims all trailing whitespace in the repository to make the codebase easier to work with in editors that respect `.editorconfig`. `git blame` becomes less useful on these lines but it already isn't very useful. Commands: ``` find . -type f -name '*.h' -exec sed --in-place 's/[[:space:]]\+$//' {} \+ find . -type f -name '*.c' -exec sed --in-place 's/[[:space:]]\+$//' {} \+ ```
2585 lines
62 KiB
C
2585 lines
62 KiB
C
/*
|
|
sv_client.c - client interactions
|
|
Copyright (C) 2008 Uncle Mike
|
|
|
|
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 3 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.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "const.h"
|
|
#include "server.h"
|
|
#include "net_encode.h"
|
|
#include "net_api.h"
|
|
|
|
const char *clc_strings[clc_lastmsg+1] =
|
|
{
|
|
"clc_bad",
|
|
"clc_nop",
|
|
"clc_move",
|
|
"clc_stringcmd",
|
|
"clc_delta",
|
|
"clc_resourcelist",
|
|
"clc_unused6",
|
|
"clc_fileconsistency",
|
|
"clc_voicedata",
|
|
"clc_cvarvalue",
|
|
"clc_cvarvalue2",
|
|
};
|
|
|
|
typedef struct ucmd_s
|
|
{
|
|
const char *name;
|
|
qboolean (*func)( sv_client_t *cl );
|
|
} ucmd_t;
|
|
|
|
static int g_userid = 1;
|
|
|
|
/*
|
|
=================
|
|
SV_GetChallenge
|
|
|
|
Returns a challenge number that can be used
|
|
in a subsequent client_connect command.
|
|
We do this to prevent denial of service attacks that
|
|
flood the server with invalid connection IPs. With a
|
|
challenge, they must give a valid IP address.
|
|
=================
|
|
*/
|
|
void SV_GetChallenge( netadr_t from )
|
|
{
|
|
int i, oldest = 0;
|
|
double oldestTime;
|
|
|
|
oldestTime = 0x7fffffff;
|
|
|
|
// see if we already have a challenge for this ip
|
|
for( i = 0; i < MAX_CHALLENGES; i++ )
|
|
{
|
|
if( !svs.challenges[i].connected && NET_CompareAdr( from, svs.challenges[i].adr ))
|
|
break;
|
|
|
|
if( svs.challenges[i].time < oldestTime )
|
|
{
|
|
oldestTime = svs.challenges[i].time;
|
|
oldest = i;
|
|
}
|
|
}
|
|
|
|
if( i == MAX_CHALLENGES )
|
|
{
|
|
// this is the first time this client has asked for a challenge
|
|
svs.challenges[oldest].challenge = (COM_RandomLong( 0, 0xFFFF ) << 16) | COM_RandomLong( 0, 0xFFFF );
|
|
svs.challenges[oldest].adr = from;
|
|
svs.challenges[oldest].time = host.realtime;
|
|
svs.challenges[oldest].connected = false;
|
|
i = oldest;
|
|
}
|
|
|
|
// send it back
|
|
Netchan_OutOfBandPrint( NS_SERVER, svs.challenges[i].adr, "challenge %i", svs.challenges[i].challenge );
|
|
}
|
|
|
|
int SV_GetFragmentSize( void *pcl, fragsize_t mode )
|
|
{
|
|
sv_client_t *cl = (sv_client_t*)pcl;
|
|
int cl_frag_size;
|
|
|
|
if( Netchan_IsLocal( &cl->netchan ))
|
|
return FRAGMENT_LOCAL_SIZE;
|
|
|
|
if( mode == FRAGSIZE_UNRELIABLE )
|
|
{
|
|
// allow setting unreliable limit with "setinfo cl_urmax"
|
|
cl_frag_size = Q_atoi( Info_ValueForKey( cl->userinfo, "cl_urmax" ));
|
|
if( cl_frag_size == 0 )
|
|
return NET_MAX_MESSAGE;
|
|
return bound( FRAGMENT_MAX_SIZE, cl_frag_size, NET_MAX_MESSAGE );
|
|
}
|
|
|
|
cl_frag_size = Q_atoi( Info_ValueForKey( cl->userinfo, "cl_dlmax" ));
|
|
cl_frag_size = bound( FRAGMENT_MIN_SIZE, cl_frag_size, FRAGMENT_MAX_SIZE );
|
|
|
|
if( mode != FRAGSIZE_FRAG )
|
|
{
|
|
if( cl->extensions & NET_EXT_SPLITSIZE )
|
|
return cl_frag_size;
|
|
else
|
|
return 0; // original engine behaviour
|
|
}
|
|
|
|
// get in-game fragmentation size
|
|
if( cl->state == cs_spawned )
|
|
{
|
|
// allow setting in-game fragsize with "setinfo cl_frmax"
|
|
int frmax = Q_atoi( Info_ValueForKey( cl->userinfo, "cl_frmax" ));
|
|
|
|
if( frmax < FRAGMENT_MIN_SIZE || frmax > FRAGMENT_MAX_SIZE )
|
|
cl_frag_size = frmax;
|
|
else
|
|
cl_frag_size /= 2;// add window for unreliable
|
|
}
|
|
|
|
return cl_frag_size - HEADER_BYTES;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_RejectConnection
|
|
|
|
Rejects connection request and sends back a message
|
|
================
|
|
*/
|
|
void SV_RejectConnection( netadr_t from, char *fmt, ... )
|
|
{
|
|
char text[1024];
|
|
va_list argptr;
|
|
|
|
va_start( argptr, fmt );
|
|
Q_vsnprintf( text, sizeof( text ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
Con_Reportf( "%s connection refused. Reason: %s\n", NET_AdrToString( from ), text );
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "errormsg\n^1Server was reject the connection:^7 %s", text );
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "print\n^1Server was reject the connection:^7 %s", text );
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "disconnect\n" );
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_FailDownload
|
|
|
|
for some reasons file can't be downloaded
|
|
tell the client about this problem
|
|
================
|
|
*/
|
|
void SV_FailDownload( sv_client_t *cl, const char *filename )
|
|
{
|
|
if( !COM_CheckString( filename ))
|
|
return;
|
|
|
|
MSG_BeginServerCmd( &cl->netchan.message, svc_filetxferfailed );
|
|
MSG_WriteString( &cl->netchan.message, filename );
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_CheckChallenge
|
|
|
|
Make sure connecting client is not spoofing
|
|
================
|
|
*/
|
|
int SV_CheckChallenge( netadr_t from, int challenge )
|
|
{
|
|
int i;
|
|
|
|
// see if the challenge is valid
|
|
// don't care if it is a local address.
|
|
if( NET_IsLocalAddress( from ))
|
|
return 1;
|
|
|
|
for( i = 0; i < MAX_CHALLENGES; i++ )
|
|
{
|
|
if( NET_CompareAdr( from, svs.challenges[i].adr ))
|
|
{
|
|
if( challenge == svs.challenges[i].challenge )
|
|
break; // valid challenge
|
|
#if 0
|
|
// g-cont. this breaks multiple connections from single machine
|
|
SV_RejectConnection( from, "bad challenge %i\n", challenge );
|
|
return 0;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
if( i == MAX_CHALLENGES )
|
|
{
|
|
SV_RejectConnection( from, "no challenge for your address\n" );
|
|
return 0;
|
|
}
|
|
svs.challenges[i].connected = true;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_CheckIPRestrictions
|
|
|
|
Determine if client is outside appropriate address range
|
|
================
|
|
*/
|
|
int SV_CheckIPRestrictions( netadr_t from )
|
|
{
|
|
if( sv_lan.value )
|
|
{
|
|
if( !NET_CompareClassBAdr( from, net_local ) && !NET_IsReservedAdr( from ))
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_FindEmptySlot
|
|
|
|
Get slot # and set client_t pointer for player, if possible
|
|
We don't do this search on a "reconnect, we just reuse the slot
|
|
================
|
|
*/
|
|
int SV_FindEmptySlot( netadr_t from, int *pslot, sv_client_t **ppClient )
|
|
{
|
|
sv_client_t *cl;
|
|
int i;
|
|
|
|
for( i = 0, cl = svs.clients; i < svs.maxclients; i++, cl++ )
|
|
{
|
|
if( cl->state == cs_free )
|
|
{
|
|
*ppClient = cl;
|
|
*pslot = i;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
SV_RejectConnection( from, "server is full\n" );
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_ConnectClient
|
|
|
|
A connection request that did not come from the master
|
|
==================
|
|
*/
|
|
void SV_ConnectClient( netadr_t from )
|
|
{
|
|
char userinfo[MAX_INFO_STRING];
|
|
char protinfo[MAX_INFO_STRING];
|
|
sv_client_t *cl, *newcl = NULL;
|
|
qboolean reconnect = false;
|
|
int nClientSlot = 0;
|
|
int qport, version;
|
|
int i, count = 0;
|
|
int challenge;
|
|
const char *s;
|
|
int extensions;
|
|
|
|
if( Cmd_Argc() < 5 )
|
|
{
|
|
SV_RejectConnection( from, "insufficient connection info\n" );
|
|
return;
|
|
}
|
|
|
|
version = Q_atoi( Cmd_Argv( 1 ));
|
|
|
|
if( version != PROTOCOL_VERSION )
|
|
{
|
|
SV_RejectConnection( from, "unsupported protocol (%i should be %i)\n", version, PROTOCOL_VERSION );
|
|
return;
|
|
}
|
|
|
|
challenge = Q_atoi( Cmd_Argv( 2 )); // get challenge
|
|
|
|
// see if the challenge is valid (local clients don't need to challenge)
|
|
if( !SV_CheckChallenge( from, challenge ))
|
|
return;
|
|
|
|
s = Cmd_Argv( 3 ); // protocol info
|
|
|
|
if( !Info_IsValid( s ))
|
|
{
|
|
SV_RejectConnection( from, "invalid protinfo in connect command\n" );
|
|
return;
|
|
}
|
|
|
|
Q_strncpy( protinfo, s, sizeof( protinfo ));
|
|
|
|
if( !SV_ProcessUserAgent( from, protinfo ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// extract qport from protocol info
|
|
qport = Q_atoi( Info_ValueForKey( protinfo, "qport" ));
|
|
|
|
s = Info_ValueForKey( protinfo, "uuid" );
|
|
if( Q_strlen( s ) != 32 )
|
|
{
|
|
SV_RejectConnection( from, "invalid authentication certificate length\n" );
|
|
return;
|
|
}
|
|
|
|
extensions = Q_atoi( Info_ValueForKey( protinfo, "ext" ) );
|
|
|
|
// LAN servers restrict to class b IP addresses
|
|
if( !SV_CheckIPRestrictions( from ))
|
|
{
|
|
SV_RejectConnection( from, "LAN servers are restricted to local clients (class C)\n" );
|
|
return;
|
|
}
|
|
|
|
s = Cmd_Argv( 4 ); // user info
|
|
|
|
if( Q_strlen( s ) > MAX_INFO_STRING || !Info_IsValid( s ))
|
|
{
|
|
SV_RejectConnection( from, "invalid userinfo in connect command\n" );
|
|
return;
|
|
}
|
|
|
|
Q_strncpy( userinfo, s, sizeof( userinfo ));
|
|
|
|
// check connection password (don't verify local client)
|
|
if( !NET_IsLocalAddress( from ) && sv_password.string[0] && Q_stricmp( sv_password.string, Info_ValueForKey( userinfo, "password" )))
|
|
{
|
|
SV_RejectConnection( from, "invalid password\n" );
|
|
return;
|
|
}
|
|
|
|
// if there is already a slot for this ip, reuse it
|
|
for( i = 0, cl = svs.clients; i < svs.maxclients; i++, cl++ )
|
|
{
|
|
if( cl->state == cs_free || cl->state == cs_zombie )
|
|
continue;
|
|
|
|
if( NET_CompareBaseAdr( from, cl->netchan.remote_address ) && ( cl->netchan.qport == qport || from.port == cl->netchan.remote_address.port ))
|
|
{
|
|
reconnect = true;
|
|
newcl = cl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// A reconnecting client will re-use the slot found above when checking for reconnection.
|
|
// the slot will be wiped clean.
|
|
if( !reconnect )
|
|
{
|
|
// connect the client if there are empty slots.
|
|
if( !SV_FindEmptySlot( from, &nClientSlot, &newcl ))
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Con_Reportf( S_NOTE "%s:reconnect\n", NET_AdrToString( from ));
|
|
}
|
|
|
|
// find a client slot
|
|
ASSERT( newcl != NULL );
|
|
|
|
// build a new connection
|
|
// accept the new client
|
|
if( Q_strncpy( newcl->useragent, Cmd_Argv( 6 ), MAX_INFO_STRING ) )
|
|
{
|
|
const char *id = Info_ValueForKey( newcl->useragent, "i" );
|
|
|
|
if( *id )
|
|
{
|
|
//sscanf( id, "%llx", &newcl->WonID );
|
|
}
|
|
|
|
// Q_strncpy( cl->auth_id, id, sizeof( cl->auth_id ) );
|
|
}
|
|
|
|
sv.current_client = newcl;
|
|
newcl->edict = EDICT_NUM( (newcl - svs.clients) + 1 );
|
|
newcl->challenge = challenge; // save challenge for checksumming
|
|
if( newcl->frames ) Mem_Free( newcl->frames );
|
|
newcl->frames = (client_frame_t *)Z_Calloc( sizeof( client_frame_t ) * SV_UPDATE_BACKUP );
|
|
newcl->userid = g_userid++; // create unique userid
|
|
newcl->state = cs_connected;
|
|
newcl->extensions = extensions & (NET_EXT_SPLITSIZE);
|
|
|
|
// reset viewentities (from previous level)
|
|
memset( newcl->viewentity, 0, sizeof( newcl->viewentity ));
|
|
newcl->num_viewents = 0;
|
|
newcl->listeners = 0;
|
|
|
|
// initailize netchan
|
|
Netchan_Setup( NS_SERVER, &newcl->netchan, from, qport, newcl, SV_GetFragmentSize );
|
|
MSG_Init( &newcl->datagram, "Datagram", newcl->datagram_buf, sizeof( newcl->datagram_buf )); // datagram buf
|
|
|
|
Q_strncpy( newcl->hashedcdkey, Info_ValueForKey( protinfo, "uuid" ), 32 );
|
|
newcl->hashedcdkey[32] = '\0';
|
|
|
|
// build protinfo answer
|
|
protinfo[0] = '\0';
|
|
Info_SetValueForKey( protinfo, "ext", va( "%d",newcl->extensions ), sizeof( protinfo ) );
|
|
|
|
// send the connect packet to the client
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "client_connect %s", protinfo );
|
|
|
|
newcl->upstate = us_inactive;
|
|
newcl->connection_started = host.realtime;
|
|
newcl->cl_updaterate = 0.05; // 20 fps as default
|
|
newcl->delta_sequence = -1;
|
|
newcl->flags = 0;
|
|
|
|
|
|
|
|
// reset any remaining events
|
|
memset( &newcl->events, 0, sizeof( newcl->events ));
|
|
|
|
// parse some info from the info strings (this can override cl_updaterate)
|
|
Q_strncpy( newcl->userinfo, userinfo, sizeof( newcl->userinfo ));
|
|
SV_UserinfoChanged( newcl );
|
|
SV_ClearResourceLists( newcl );
|
|
#if 0
|
|
memset( &newcl->resourcesneeded, 0, sizeof( resource_t ));
|
|
memset( &newcl->resourcesonhand, 0, sizeof( resource_t ));
|
|
newcl->resourcesneeded.pNext = newcl->resourcesneeded.pPrev = &newcl->resourcesneeded;
|
|
newcl->resourcesonhand.pNext = newcl->resourcesonhand.pPrev = &newcl->resourcesonhand;
|
|
#endif
|
|
newcl->next_messagetime = host.realtime + newcl->cl_updaterate;
|
|
newcl->next_sendinfotime = 0.0;
|
|
newcl->ignored_ents = 0;
|
|
newcl->chokecount = 0;
|
|
|
|
// reset stats
|
|
newcl->next_checkpingtime = -1.0;
|
|
newcl->packet_loss = 0.0f;
|
|
|
|
// if this was the first client on the server, or the last client
|
|
// the server can hold, send a heartbeat to the master.
|
|
for( i = 0, cl = svs.clients; i < svs.maxclients; i++, cl++ )
|
|
if( cl->state >= cs_connected ) count++;
|
|
|
|
Log_Printf( "\"%s<%i><%i><>\" connected, address \"%s\"\n", newcl->name, newcl->userid, i, NET_AdrToString( newcl->netchan.remote_address ));
|
|
|
|
if( count == 1 || count == svs.maxclients )
|
|
svs.last_heartbeat = MAX_HEARTBEAT;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_FakeConnect
|
|
|
|
A connection request that came from the game module
|
|
==================
|
|
*/
|
|
edict_t *SV_FakeConnect( const char *netname )
|
|
{
|
|
char userinfo[MAX_INFO_STRING];
|
|
int i, count = 0;
|
|
sv_client_t *cl;
|
|
|
|
if( !COM_CheckString( netname ))
|
|
netname = "Bot";
|
|
|
|
// find a client slot
|
|
for( i = 0, cl = svs.clients; i < svs.maxclients; i++, cl++ )
|
|
{
|
|
if( cl->state == cs_free )
|
|
break;
|
|
}
|
|
|
|
if( i == svs.maxclients )
|
|
return NULL; // server is full
|
|
|
|
userinfo[0] = '\0';
|
|
|
|
// setup fake client params
|
|
Info_SetValueForKey( userinfo, "name", netname, MAX_INFO_STRING );
|
|
Info_SetValueForKey( userinfo, "model", "gordon", MAX_INFO_STRING );
|
|
Info_SetValueForKey( userinfo, "topcolor", "1", MAX_INFO_STRING );
|
|
Info_SetValueForKey( userinfo, "bottomcolor", "1", MAX_INFO_STRING );
|
|
|
|
// build a new connection
|
|
// accept the new client
|
|
sv.current_client = cl;
|
|
|
|
if( cl->frames ) Mem_Free( cl->frames ); // fakeclients doesn't have frames
|
|
memset( cl, 0, sizeof( sv_client_t ));
|
|
|
|
cl->edict = EDICT_NUM( (cl - svs.clients) + 1 );
|
|
cl->userid = g_userid++; // create unique userid
|
|
SetBits( cl->flags, FCL_FAKECLIENT );
|
|
|
|
// parse some info from the info strings
|
|
Q_strncpy( cl->userinfo, userinfo, sizeof( cl->userinfo ));
|
|
SV_UserinfoChanged( cl );
|
|
SetBits( cl->flags, FCL_RESEND_USERINFO );
|
|
cl->next_sendinfotime = 0.0;
|
|
|
|
// if this was the first client on the server, or the last client
|
|
// the server can hold, send a heartbeat to the master.
|
|
for( i = 0, cl = svs.clients; i < svs.maxclients; i++, cl++ )
|
|
if( cl->state >= cs_connected ) count++;
|
|
cl = sv.current_client;
|
|
|
|
Log_Printf( "\"%s<%i><%i><>\" connected, address \"local\"\n", cl->name, cl->userid, i );
|
|
|
|
SetBits( cl->edict->v.flags, FL_CLIENT|FL_FAKECLIENT ); // mark it as fakeclient
|
|
cl->connection_started = host.realtime;
|
|
cl->state = cs_spawned;
|
|
|
|
if( count == 1 || count == svs.maxclients )
|
|
svs.last_heartbeat = MAX_HEARTBEAT;
|
|
|
|
return cl->edict;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
SV_DropClient
|
|
|
|
Called when the player is totally leaving the server, either willingly
|
|
or unwillingly. This is NOT called if the entire server is quiting
|
|
or crashing.
|
|
=====================
|
|
*/
|
|
void SV_DropClient( sv_client_t *cl, qboolean crash )
|
|
{
|
|
int i;
|
|
|
|
if( cl->state == cs_zombie )
|
|
return; // already dropped
|
|
|
|
if( !crash )
|
|
{
|
|
// add the disconnect
|
|
if( !FBitSet( cl->flags, FCL_FAKECLIENT ) )
|
|
{
|
|
MSG_BeginServerCmd( &cl->netchan.message, svc_disconnect );
|
|
}
|
|
|
|
if( cl->edict && cl->state == cs_spawned )
|
|
{
|
|
svgame.dllFuncs.pfnClientDisconnect( cl->edict );
|
|
}
|
|
|
|
if( !FBitSet( cl->flags, FCL_FAKECLIENT ) )
|
|
{
|
|
Netchan_TransmitBits( &cl->netchan, 0, NULL );
|
|
}
|
|
}
|
|
|
|
ClearBits( cl->flags, FCL_FAKECLIENT );
|
|
ClearBits( cl->flags, FCL_HLTV_PROXY );
|
|
cl->state = cs_zombie; // become free in a few seconds
|
|
cl->name[0] = 0;
|
|
|
|
if( cl->frames )
|
|
Mem_Free( cl->frames ); // release delta
|
|
cl->frames = NULL;
|
|
|
|
if( NET_CompareBaseAdr( cl->netchan.remote_address, host.rd.address ))
|
|
SV_EndRedirect();
|
|
|
|
// throw away any residual garbage in the channel.
|
|
Netchan_Clear( &cl->netchan );
|
|
|
|
// clean client data on disconnect
|
|
memset( cl->userinfo, 0, MAX_INFO_STRING );
|
|
memset( cl->physinfo, 0, MAX_INFO_STRING );
|
|
COM_ClearCustomizationList( &cl->customdata, false );
|
|
|
|
// don't send to other clients
|
|
cl->edict = NULL;
|
|
|
|
// send notification to all other clients
|
|
SV_FullClientUpdate( cl, &sv.reliable_datagram );
|
|
|
|
// if this was the last client on the server, send a heartbeat
|
|
// to the master so it is known the server is empty
|
|
// send a heartbeat now so the master will get up to date info
|
|
// if there is already a slot for this ip, reuse it
|
|
for( i = 0; i < svs.maxclients; i++ )
|
|
{
|
|
if( svs.clients[i].state >= cs_connected )
|
|
break;
|
|
}
|
|
|
|
if( i == svs.maxclients )
|
|
svs.last_heartbeat = MAX_HEARTBEAT;
|
|
}
|
|
|
|
/*
|
|
==============================================================================
|
|
|
|
SVC COMMAND REDIRECT
|
|
|
|
==============================================================================
|
|
*/
|
|
void SV_BeginRedirect( netadr_t adr, int target, char *buffer, int buffersize, void (*flush))
|
|
{
|
|
if( !target || !buffer || !buffersize || !flush )
|
|
return;
|
|
|
|
host.rd.target = target;
|
|
host.rd.buffer = buffer;
|
|
host.rd.buffersize = buffersize;
|
|
host.rd.flush = flush;
|
|
host.rd.address = adr;
|
|
host.rd.buffer[0] = 0;
|
|
}
|
|
|
|
void SV_FlushRedirect( netadr_t adr, int dest, char *buf )
|
|
{
|
|
if( sv.current_client && FBitSet( sv.current_client->flags, FCL_FAKECLIENT ))
|
|
return;
|
|
|
|
switch( dest )
|
|
{
|
|
case RD_PACKET:
|
|
Netchan_OutOfBandPrint( NS_SERVER, adr, "print\n%s", buf );
|
|
break;
|
|
case RD_CLIENT:
|
|
if( !sv.current_client ) return; // client not set
|
|
MSG_BeginServerCmd( &sv.current_client->netchan.message, svc_print );
|
|
MSG_WriteString( &sv.current_client->netchan.message, buf );
|
|
break;
|
|
case RD_NONE:
|
|
Con_Printf( S_ERROR "SV_FlushRedirect: %s: invalid destination\n", NET_AdrToString( adr ));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SV_EndRedirect( void )
|
|
{
|
|
if( host.rd.flush )
|
|
host.rd.flush( host.rd.address, host.rd.target, host.rd.buffer );
|
|
|
|
host.rd.target = 0;
|
|
host.rd.buffer = NULL;
|
|
host.rd.buffersize = 0;
|
|
host.rd.flush = NULL;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
SV_GetClientIDString
|
|
|
|
Returns a pointer to a static char for most likely only printing.
|
|
===============
|
|
*/
|
|
const char *SV_GetClientIDString( sv_client_t *cl )
|
|
{
|
|
static char result[MAX_QPATH];
|
|
|
|
if( !cl ) return "";
|
|
|
|
if( FBitSet( cl->flags, FCL_FAKECLIENT ))
|
|
{
|
|
Q_strncpy( result, "ID_BOT", sizeof( result ));
|
|
}
|
|
else if( NET_IsLocalAddress( cl->netchan.remote_address ))
|
|
{
|
|
Q_strncpy( result, "ID_LOOPBACK", sizeof( result ));
|
|
}
|
|
else if( sv_lan.value )
|
|
{
|
|
Q_strncpy( result, "ID_LAN", sizeof( result ));
|
|
}
|
|
else
|
|
{
|
|
Q_snprintf( result, sizeof( result ), "ID_%s", MD5_Print( (byte *)cl->hashedcdkey ));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
sv_client_t *SV_ClientById( int id )
|
|
{
|
|
sv_client_t *cl;
|
|
int i;
|
|
|
|
ASSERT( id >= 0 );
|
|
|
|
for( i = 0, cl = svs.clients; i < svgame.globals->maxClients; i++, cl++ )
|
|
{
|
|
if( !cl->state )
|
|
continue;
|
|
|
|
if( cl->userid == id )
|
|
return cl;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
sv_client_t *SV_ClientByName( const char *name )
|
|
{
|
|
sv_client_t *cl;
|
|
int i;
|
|
|
|
ASSERT( name && *name );
|
|
|
|
for( i = 0, cl = svs.clients; i < svgame.globals->maxClients; i++, cl++ )
|
|
{
|
|
if( !cl->state )
|
|
continue;
|
|
|
|
if( !Q_strcmp( cl->name, name ) )
|
|
return cl;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_TestBandWidth
|
|
|
|
================
|
|
*/
|
|
void SV_TestBandWidth( netadr_t from )
|
|
{
|
|
int version = Q_atoi( Cmd_Argv( 1 ));
|
|
int packetsize = Q_atoi( Cmd_Argv( 2 ));
|
|
byte send_buf[FRAGMENT_MAX_SIZE];
|
|
dword crcValue = 0;
|
|
byte *filepos;
|
|
int crcpos;
|
|
file_t *test;
|
|
sizebuf_t send;
|
|
|
|
// don't waste time of protocol mismatched
|
|
if( version != PROTOCOL_VERSION )
|
|
{
|
|
SV_RejectConnection( from, "unsupported protocol (%i should be %i)\n", version, PROTOCOL_VERSION );
|
|
return;
|
|
}
|
|
|
|
test = FS_Open( "gfx.wad", "rb", false );
|
|
|
|
if( FS_FileLength( test ) < sizeof( send_buf ))
|
|
{
|
|
// skip the test and just get challenge
|
|
SV_GetChallenge( from );
|
|
return;
|
|
}
|
|
|
|
// write the packet header
|
|
MSG_Init( &send, "BandWidthPacket", send_buf, sizeof( send_buf ));
|
|
MSG_WriteLong( &send, -1 ); // -1 sequence means out of band
|
|
MSG_WriteString( &send, "testpacket" );
|
|
crcpos = MSG_GetNumBytesWritten( &send );
|
|
MSG_WriteLong( &send, 0 ); // reserve space for crc
|
|
filepos = send.pData + MSG_GetNumBytesWritten( &send );
|
|
packetsize = packetsize - MSG_GetNumBytesWritten( &send ); // adjust the packet size
|
|
FS_Read( test, filepos, packetsize );
|
|
FS_Close( test );
|
|
|
|
CRC32_ProcessBuffer( &crcValue, filepos, packetsize ); // calc CRC
|
|
MSG_SeekToBit( &send, packetsize << 3, SEEK_CUR );
|
|
*(uint *)&send.pData[crcpos] = crcValue;
|
|
|
|
// send the datagram
|
|
NET_SendPacket( NS_SERVER, MSG_GetNumBytesWritten( &send ), MSG_GetData( &send ), from );
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_Ack
|
|
|
|
================
|
|
*/
|
|
void SV_Ack( netadr_t from )
|
|
{
|
|
Con_Printf( "ping %s\n", NET_AdrToString( from ));
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_Info
|
|
|
|
Responds with short info for broadcast scans
|
|
The second parameter should be the current protocol version number.
|
|
================
|
|
*/
|
|
void SV_Info( netadr_t from )
|
|
{
|
|
char string[MAX_INFO_STRING];
|
|
int i, count = 0;
|
|
int version;
|
|
|
|
// ignore in single player
|
|
if( svs.maxclients == 1 || !svs.initialized )
|
|
return;
|
|
|
|
version = Q_atoi( Cmd_Argv( 1 ));
|
|
string[0] = '\0';
|
|
|
|
if( version != PROTOCOL_VERSION )
|
|
{
|
|
Q_snprintf( string, sizeof( string ), "%s: wrong version\n", hostname.string );
|
|
}
|
|
else
|
|
{
|
|
for( i = 0; i < svs.maxclients; i++ )
|
|
if( svs.clients[i].state >= cs_connected )
|
|
count++;
|
|
|
|
Info_SetValueForKey( string, "host", hostname.string, MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "map", sv.name, MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "dm", va( "%i", (int)svgame.globals->deathmatch ), MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "team", va( "%i", (int)svgame.globals->teamplay ), MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "coop", va( "%i", (int)svgame.globals->coop ), MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "numcl", va( "%i", count ), MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "maxcl", va( "%i", svs.maxclients ), MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "gamedir", GI->gamefolder, MAX_INFO_STRING );
|
|
}
|
|
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "info\n%s", string );
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_BuildNetAnswer
|
|
|
|
Responds with long info for local and broadcast requests
|
|
================
|
|
*/
|
|
void SV_BuildNetAnswer( netadr_t from )
|
|
{
|
|
char string[MAX_INFO_STRING];
|
|
int version, context, type;
|
|
int i, count = 0;
|
|
|
|
// ignore in single player
|
|
if( svs.maxclients == 1 || !svs.initialized )
|
|
return;
|
|
|
|
version = Q_atoi( Cmd_Argv( 1 ));
|
|
context = Q_atoi( Cmd_Argv( 2 ));
|
|
type = Q_atoi( Cmd_Argv( 3 ));
|
|
|
|
if( version != PROTOCOL_VERSION )
|
|
{
|
|
// handle the unsupported protocol
|
|
string[0] = '\0';
|
|
Info_SetValueForKey( string, "neterror", "protocol", MAX_INFO_STRING );
|
|
|
|
// send error unsupported protocol
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "netinfo %i %i %s\n", context, type, string );
|
|
return;
|
|
}
|
|
|
|
if( type == NETAPI_REQUEST_PING )
|
|
{
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "netinfo %i %i %s\n", context, type, "" );
|
|
}
|
|
else if( type == NETAPI_REQUEST_RULES )
|
|
{
|
|
// send serverinfo
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "netinfo %i %i %s\n", context, type, svs.serverinfo );
|
|
}
|
|
else if( type == NETAPI_REQUEST_PLAYERS )
|
|
{
|
|
string[0] = '\0';
|
|
|
|
for( i = 0; i < svs.maxclients; i++ )
|
|
{
|
|
if( svs.clients[i].state >= cs_connected )
|
|
{
|
|
edict_t *ed = svs.clients[i].edict;
|
|
float time = host.realtime - svs.clients[i].connection_started;
|
|
Q_strncat( string, va( "%c\\%s\\%i\\%f\\", count, svs.clients[i].name, (int)ed->v.frags, time ), sizeof( string ));
|
|
count++;
|
|
}
|
|
}
|
|
|
|
// send playernames
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "netinfo %i %i %s\n", context, type, string );
|
|
}
|
|
else if( type == NETAPI_REQUEST_DETAILS )
|
|
{
|
|
for( i = 0; i < svs.maxclients; i++ )
|
|
if( svs.clients[i].state >= cs_connected )
|
|
count++;
|
|
|
|
string[0] = '\0';
|
|
Info_SetValueForKey( string, "hostname", hostname.string, MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "gamedir", GI->gamefolder, MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "current", va( "%i", count ), MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "max", va( "%i", svs.maxclients ), MAX_INFO_STRING );
|
|
Info_SetValueForKey( string, "map", sv.name, MAX_INFO_STRING );
|
|
|
|
// send serverinfo
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "netinfo %i %i %s\n", context, type, string );
|
|
}
|
|
else
|
|
{
|
|
string[0] = '\0';
|
|
Info_SetValueForKey( string, "neterror", "undefined", MAX_INFO_STRING );
|
|
|
|
// send error undefined request type
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "netinfo %i %i %s\n", context, type, string );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_Ping
|
|
|
|
Just responds with an acknowledgement
|
|
================
|
|
*/
|
|
void SV_Ping( netadr_t from )
|
|
{
|
|
Netchan_OutOfBandPrint( NS_SERVER, from, "ack" );
|
|
}
|
|
|
|
/*
|
|
================
|
|
Rcon_Validate
|
|
================
|
|
*/
|
|
qboolean Rcon_Validate( void )
|
|
{
|
|
if( !COM_CheckString( rcon_password.string ))
|
|
return false;
|
|
if( Q_strcmp( Cmd_Argv( 1 ), rcon_password.string ))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
SV_RemoteCommand
|
|
|
|
A client issued an rcon command.
|
|
Shift down the remaining args
|
|
Redirect all printfs
|
|
===============
|
|
*/
|
|
void SV_RemoteCommand( netadr_t from, sizebuf_t *msg )
|
|
{
|
|
static char outputbuf[2048];
|
|
char remaining[1024];
|
|
int i;
|
|
|
|
Con_Printf( "Rcon from %s:\n%s\n", NET_AdrToString( from ), MSG_GetData( msg ) + 4 );
|
|
Log_Printf( "Rcon: \"%s\" from \"%s\"\n", MSG_GetData( msg ) + 4, NET_AdrToString( from ));
|
|
SV_BeginRedirect( from, RD_PACKET, outputbuf, sizeof( outputbuf ) - 16, SV_FlushRedirect );
|
|
|
|
if( Rcon_Validate( ))
|
|
{
|
|
remaining[0] = 0;
|
|
for( i = 2; i < Cmd_Argc(); i++ )
|
|
{
|
|
Q_strcat( remaining, Cmd_Argv( i ));
|
|
Q_strcat( remaining, " " );
|
|
}
|
|
Cmd_ExecuteString( remaining );
|
|
}
|
|
else Con_Printf( S_ERROR "Bad rcon_password.\n" );
|
|
|
|
SV_EndRedirect();
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_CalcPing
|
|
|
|
recalc ping on current client
|
|
===================
|
|
*/
|
|
int SV_CalcPing( sv_client_t *cl )
|
|
{
|
|
float ping = 0;
|
|
int i, count;
|
|
int idx, back;
|
|
client_frame_t *frame;
|
|
|
|
// bots don't have a real ping
|
|
if( FBitSet( cl->flags, FCL_FAKECLIENT ) || !cl->frames )
|
|
return 5;
|
|
|
|
if( SV_UPDATE_BACKUP <= 31 )
|
|
{
|
|
back = SV_UPDATE_BACKUP / 2;
|
|
if( back <= 0 ) return 0;
|
|
}
|
|
else back = 16;
|
|
|
|
count = 0;
|
|
|
|
for( i = 0; i < back; i++ )
|
|
{
|
|
idx = cl->netchan.incoming_acknowledged + ~i;
|
|
frame = &cl->frames[idx & SV_UPDATE_MASK];
|
|
|
|
if( frame->ping_time > 0.0f )
|
|
{
|
|
ping += frame->ping_time;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if( count > 0 )
|
|
return (( ping / count ) * 1000.0f );
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_EstablishTimeBase
|
|
|
|
Finangles latency and the like.
|
|
===================
|
|
*/
|
|
void SV_EstablishTimeBase( sv_client_t *cl, usercmd_t *cmds, int dropped, int numbackup, int numcmds )
|
|
{
|
|
double runcmd_time = 0.0;
|
|
int i, cmdnum = dropped;
|
|
|
|
if( dropped < 24 )
|
|
{
|
|
while( dropped > numbackup )
|
|
{
|
|
runcmd_time = (double)cl->lastcmd.msec / 1000.0;
|
|
dropped--;
|
|
}
|
|
|
|
while( dropped > 0 )
|
|
{
|
|
cmdnum = dropped + numcmds - 1;
|
|
runcmd_time += (double)cmds[cmdnum].msec / 1000.0;
|
|
dropped--;
|
|
}
|
|
}
|
|
|
|
for( i = numcmds - 1; i >= 0; i-- )
|
|
runcmd_time += cmds[i].msec / 1000.0;
|
|
|
|
cl->timebase = sv.time + sv.frametime - runcmd_time;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_CalcClientTime
|
|
|
|
compute latency for client
|
|
===================
|
|
*/
|
|
float SV_CalcClientTime( sv_client_t *cl )
|
|
{
|
|
float minping, maxping;
|
|
float ping = 0.0f;
|
|
int i, count = 0;
|
|
int backtrack;
|
|
|
|
backtrack = (int)sv_unlagsamples.value;
|
|
if( backtrack < 1 ) backtrack = 1;
|
|
|
|
if( backtrack >= (SV_UPDATE_BACKUP <= 16 ? SV_UPDATE_BACKUP : 16 ))
|
|
backtrack = ( SV_UPDATE_BACKUP <= 16 ? SV_UPDATE_BACKUP : 16 );
|
|
|
|
if( backtrack <= 0 )
|
|
return 0.0f;
|
|
|
|
for( i = 0; i < backtrack; i++ )
|
|
{
|
|
client_frame_t *frame = &cl->frames[SV_UPDATE_MASK & (cl->netchan.incoming_acknowledged - i)];
|
|
if( frame->ping_time <= 0.0f )
|
|
continue;
|
|
|
|
ping += frame->ping_time;
|
|
count++;
|
|
}
|
|
|
|
if( !count ) return 0.0f;
|
|
|
|
minping = 9999.0f;
|
|
maxping = -9999.0f;
|
|
ping /= count;
|
|
|
|
for( i = 0; i < ( SV_UPDATE_BACKUP <= 4 ? SV_UPDATE_BACKUP : 4 ); i++ )
|
|
{
|
|
client_frame_t *frame = &cl->frames[SV_UPDATE_MASK & (cl->netchan.incoming_acknowledged - i)];
|
|
if( frame->ping_time <= 0.0f )
|
|
continue;
|
|
|
|
if( frame->ping_time < minping )
|
|
minping = frame->ping_time;
|
|
|
|
if( frame->ping_time > maxping )
|
|
maxping = frame->ping_time;
|
|
}
|
|
|
|
if( maxping < minping || fabs( maxping - minping ) <= 0.2f )
|
|
return ping;
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_FullClientUpdate
|
|
|
|
Writes all update values to a bitbuf
|
|
===================
|
|
*/
|
|
void SV_FullClientUpdate( sv_client_t *cl, sizebuf_t *msg )
|
|
{
|
|
char info[MAX_INFO_STRING];
|
|
char digest[16];
|
|
MD5Context_t ctx;
|
|
int i;
|
|
|
|
// process userinfo before updating
|
|
SV_UserinfoChanged( cl );
|
|
|
|
i = cl - svs.clients;
|
|
|
|
MSG_BeginServerCmd( msg, svc_updateuserinfo );
|
|
MSG_WriteUBitLong( msg, i, MAX_CLIENT_BITS );
|
|
MSG_WriteLong( msg, cl->userid );
|
|
|
|
if( cl->name[0] )
|
|
{
|
|
MSG_WriteOneBit( msg, 1 );
|
|
|
|
Q_strncpy( info, cl->userinfo, sizeof( info ));
|
|
|
|
// remove server passwords, etc.
|
|
Info_RemovePrefixedKeys( info, '_' );
|
|
MSG_WriteString( msg, info );
|
|
|
|
MD5Init( &ctx );
|
|
MD5Update( &ctx, (byte *)cl->hashedcdkey, sizeof( cl->hashedcdkey ));
|
|
MD5Final( digest, &ctx );
|
|
|
|
MSG_WriteBytes( msg, digest, sizeof( digest ));
|
|
}
|
|
else MSG_WriteOneBit( msg, 0 );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_RefreshUserinfo
|
|
|
|
===================
|
|
*/
|
|
void SV_RefreshUserinfo( void )
|
|
{
|
|
sv_client_t *cl;
|
|
int i;
|
|
|
|
for( i = 0, cl = svs.clients; i < svs.maxclients; i++, cl++ )
|
|
{
|
|
if( cl->state >= cs_connected )
|
|
SetBits( cl->flags, FCL_RESEND_USERINFO );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_FullUpdateMovevars
|
|
|
|
this is send all movevars values when client connected
|
|
otherwise see code SV_UpdateMovevars()
|
|
===================
|
|
*/
|
|
void SV_FullUpdateMovevars( sv_client_t *cl, sizebuf_t *msg )
|
|
{
|
|
movevars_t nullmovevars;
|
|
|
|
memset( &nullmovevars, 0, sizeof( nullmovevars ));
|
|
MSG_WriteDeltaMovevars( msg, &nullmovevars, &svgame.movevars );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_ShouldUpdatePing
|
|
|
|
determine should we recalculate
|
|
ping times now
|
|
===================
|
|
*/
|
|
qboolean SV_ShouldUpdatePing( sv_client_t *cl )
|
|
{
|
|
if( FBitSet( cl->flags, FCL_HLTV_PROXY ))
|
|
{
|
|
if( host.realtime < cl->next_checkpingtime )
|
|
return false;
|
|
|
|
cl->next_checkpingtime = host.realtime + 2.0;
|
|
return true;
|
|
}
|
|
|
|
// they are viewing the scoreboard. Send them pings.
|
|
return FBitSet( cl->lastcmd.buttons, IN_SCORE ) ? true : false;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_IsPlayerIndex
|
|
|
|
===================
|
|
*/
|
|
qboolean SV_IsPlayerIndex( int idx )
|
|
{
|
|
if( idx > 0 && idx <= svs.maxclients )
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_GetPlayerStats
|
|
|
|
This function and its static vars track some of the networking
|
|
conditions. I haven't bothered to trace it beyond that, because
|
|
this fucntion sucks pretty badly.
|
|
===================
|
|
*/
|
|
void SV_GetPlayerStats( sv_client_t *cl, int *ping, int *packet_loss )
|
|
{
|
|
static int last_ping[MAX_CLIENTS];
|
|
static int last_loss[MAX_CLIENTS];
|
|
int i;
|
|
|
|
i = cl - svs.clients;
|
|
|
|
if( host.realtime >= cl->next_checkpingtime )
|
|
{
|
|
cl->next_checkpingtime = host.realtime + 2.0;
|
|
last_ping[i] = SV_CalcPing( cl );
|
|
last_loss[i] = cl->packet_loss;
|
|
}
|
|
|
|
if( ping ) *ping = last_ping[i];
|
|
if( packet_loss ) *packet_loss = last_loss[i];
|
|
}
|
|
|
|
/*
|
|
===========
|
|
PutClientInServer
|
|
|
|
Called when a player connects to a server or respawns in
|
|
a deathmatch.
|
|
============
|
|
*/
|
|
void SV_PutClientInServer( sv_client_t *cl )
|
|
{
|
|
static byte msg_buf[0x20200]; // MAX_INIT_MSG + some space
|
|
edict_t *ent = cl->edict;
|
|
sizebuf_t msg;
|
|
|
|
MSG_Init( &msg, "Spawn", msg_buf, sizeof( msg_buf ));
|
|
|
|
if( sv.loadgame )
|
|
{
|
|
// NOTE: we needs to setup angles on restore here
|
|
if( ent->v.fixangle == 1 )
|
|
{
|
|
MSG_BeginServerCmd( &msg, svc_setangle );
|
|
MSG_WriteVec3Angles( &msg, ent->v.angles );
|
|
ent->v.fixangle = 0;
|
|
}
|
|
|
|
if( svgame.dllFuncs.pfnParmsChangeLevel )
|
|
{
|
|
SAVERESTOREDATA levelData;
|
|
string name;
|
|
int i;
|
|
|
|
memset( &levelData, 0, sizeof( levelData ));
|
|
svgame.globals->pSaveData = &levelData;
|
|
svgame.dllFuncs.pfnParmsChangeLevel();
|
|
|
|
MSG_BeginServerCmd( &msg, svc_restore );
|
|
Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL2", sv.name );
|
|
COM_FixSlashes( name );
|
|
MSG_WriteString( &msg, name );
|
|
MSG_WriteByte( &msg, levelData.connectionCount );
|
|
|
|
for( i = 0; i < levelData.connectionCount; i++ )
|
|
MSG_WriteString( &msg, levelData.levelList[i].mapName );
|
|
|
|
svgame.globals->pSaveData = NULL;
|
|
}
|
|
|
|
// reset weaponanim
|
|
MSG_BeginServerCmd( &msg, svc_weaponanim );
|
|
MSG_WriteByte( &msg, 0 );
|
|
MSG_WriteByte( &msg, 0 );
|
|
|
|
sv.loadgame = false;
|
|
sv.paused = false;
|
|
}
|
|
else
|
|
{
|
|
if( Q_atoi( Info_ValueForKey( cl->userinfo, "hltv" )))
|
|
SetBits( cl->flags, FCL_HLTV_PROXY );
|
|
|
|
// need to realloc private data for client
|
|
SV_InitEdict( ent );
|
|
|
|
if( FBitSet( cl->flags, FCL_HLTV_PROXY ))
|
|
SetBits( ent->v.flags, FL_PROXY );
|
|
else ent->v.flags = 0;
|
|
|
|
ent->v.netname = MAKE_STRING( cl->name );
|
|
ent->v.colormap = NUM_FOR_EDICT( ent ); // ???
|
|
|
|
// fisrt entering
|
|
svgame.globals->time = sv.time;
|
|
svgame.dllFuncs.pfnClientPutInServer( ent );
|
|
|
|
if( sv.background ) // don't attack player in background mode
|
|
SetBits( ent->v.flags, FL_GODMODE|FL_NOTARGET );
|
|
|
|
cl->pViewEntity = NULL; // reset pViewEntity
|
|
}
|
|
|
|
if( svgame.globals->cdAudioTrack )
|
|
{
|
|
MSG_BeginServerCmd( &msg, svc_stufftext );
|
|
MSG_WriteString( &msg, va( "cd loop %3d\n", svgame.globals->cdAudioTrack ));
|
|
svgame.globals->cdAudioTrack = 0;
|
|
}
|
|
|
|
#ifdef HACKS_RELATED_HLMODS
|
|
// enable dev-mode to prevent crash cheat-protecting from Invasion mod
|
|
if( FBitSet( ent->v.flags, FL_GODMODE|FL_NOTARGET ) && !Q_stricmp( GI->gamefolder, "invasion" ))
|
|
SV_ExecuteClientCommand( cl, "test\n" );
|
|
#endif
|
|
// refresh the userinfo and movevars
|
|
// NOTE: because movevars can be changed during the connection process
|
|
SetBits( cl->flags, FCL_RESEND_USERINFO|FCL_RESEND_MOVEVARS );
|
|
|
|
// reset client times
|
|
cl->connecttime = 0.0;
|
|
cl->ignorecmdtime = 0.0;
|
|
cl->cmdtime = 0.0;
|
|
|
|
if( !FBitSet( cl->flags, FCL_FAKECLIENT ))
|
|
{
|
|
int viewEnt;
|
|
|
|
// NOTE: it's will be fragmented automatically in right ordering
|
|
MSG_WriteBits( &msg, MSG_GetData( &sv.signon ), MSG_GetNumBitsWritten( &sv.signon ));
|
|
|
|
if( cl->pViewEntity )
|
|
viewEnt = NUM_FOR_EDICT( cl->pViewEntity );
|
|
else viewEnt = NUM_FOR_EDICT( cl->edict );
|
|
|
|
MSG_BeginServerCmd( &msg, svc_setview );
|
|
MSG_WriteWord( &msg, viewEnt );
|
|
|
|
MSG_BeginServerCmd( &msg, svc_signonnum );
|
|
MSG_WriteByte( &msg, 1 );
|
|
|
|
if( MSG_CheckOverflow( &msg ))
|
|
{
|
|
if( svs.maxclients == 1 )
|
|
Host_Error( "spawn player: overflowed\n" );
|
|
else SV_DropClient( cl, false );
|
|
}
|
|
else
|
|
{
|
|
// send initialization data
|
|
Netchan_CreateFragments( &cl->netchan, &msg );
|
|
Netchan_FragSend( &cl->netchan );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
SV_UpdateClientView
|
|
|
|
Resend the client viewentity (used for demos)
|
|
============
|
|
*/
|
|
void SV_UpdateClientView( sv_client_t *cl )
|
|
{
|
|
int viewEnt;
|
|
|
|
if( cl->pViewEntity )
|
|
viewEnt = NUM_FOR_EDICT( cl->pViewEntity );
|
|
else viewEnt = NUM_FOR_EDICT( cl->edict );
|
|
|
|
MSG_BeginServerCmd( &cl->netchan.message, svc_setview );
|
|
MSG_WriteWord( &cl->netchan.message, viewEnt );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_TogglePause
|
|
==================
|
|
*/
|
|
void SV_TogglePause( const char *msg )
|
|
{
|
|
if( sv.background ) return;
|
|
|
|
sv.paused ^= 1;
|
|
|
|
if( COM_CheckString( msg ))
|
|
SV_BroadcastPrintf( NULL, "%s", msg );
|
|
|
|
// send notification to all clients
|
|
MSG_BeginServerCmd( &sv.reliable_datagram, svc_setpause );
|
|
MSG_WriteOneBit( &sv.reliable_datagram, sv.paused );
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_SendReconnect
|
|
|
|
Tell all the clients that the server is changing levels
|
|
================
|
|
*/
|
|
void SV_BuildReconnect( sizebuf_t *msg )
|
|
{
|
|
MSG_BeginServerCmd( msg, svc_stufftext );
|
|
MSG_WriteString( msg, "reconnect\n" );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_WriteDeltaDescriptionToClient
|
|
|
|
send delta communication encoding
|
|
==================
|
|
*/
|
|
void SV_WriteDeltaDescriptionToClient( sizebuf_t *msg )
|
|
{
|
|
int tableIndex;
|
|
int fieldIndex;
|
|
|
|
for( tableIndex = 0; tableIndex < Delta_NumTables(); tableIndex++ )
|
|
{
|
|
delta_info_t *dt = Delta_FindStructByIndex( tableIndex );
|
|
|
|
for( fieldIndex = 0; fieldIndex < dt->numFields; fieldIndex++ )
|
|
Delta_WriteTableField( msg, tableIndex, &dt->pFields[fieldIndex] );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_SendServerdata
|
|
|
|
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_SendServerdata( sizebuf_t *msg, sv_client_t *cl )
|
|
{
|
|
string message;
|
|
int i;
|
|
|
|
// Only send this message to developer console, or multiplayer clients.
|
|
if(( host_developer.value ) || ( svs.maxclients > 1 ))
|
|
{
|
|
MSG_BeginServerCmd( msg, svc_print );
|
|
Q_snprintf( message, sizeof( message ), "\n^3BUILD %d SERVER (%i CRC)\nServer #%i\n", Q_buildnum(), sv.progsCRC, svs.spawncount );
|
|
MSG_WriteString( msg, message );
|
|
}
|
|
|
|
// send the serverdata
|
|
MSG_BeginServerCmd( msg, svc_serverdata );
|
|
MSG_WriteLong( msg, PROTOCOL_VERSION );
|
|
MSG_WriteLong( msg, svs.spawncount );
|
|
MSG_WriteLong( msg, sv.worldmapCRC );
|
|
MSG_WriteByte( msg, cl - svs.clients );
|
|
MSG_WriteByte( msg, svs.maxclients );
|
|
MSG_WriteWord( msg, GI->max_edicts );
|
|
MSG_WriteWord( msg, MAX_MODELS );
|
|
MSG_WriteString( msg, sv.name );
|
|
MSG_WriteString( msg, STRING( svgame.edicts->v.message )); // Map Message
|
|
MSG_WriteOneBit( msg, sv.background ); // tell client about background map
|
|
MSG_WriteString( msg, GI->gamefolder );
|
|
MSG_WriteLong( msg, host.features );
|
|
|
|
// send the player hulls
|
|
for( i = 0; i < MAX_MAP_HULLS * 3; i++ )
|
|
{
|
|
MSG_WriteChar( msg, host.player_mins[i/3][i%3] );
|
|
MSG_WriteChar( msg, host.player_maxs[i/3][i%3] );
|
|
}
|
|
|
|
// send delta-encoding
|
|
SV_WriteDeltaDescriptionToClient( msg );
|
|
|
|
// now client know delta and can reading encoded messages
|
|
SV_FullUpdateMovevars( cl, msg );
|
|
|
|
// send the user messages registration
|
|
for( i = 1; i < MAX_USER_MESSAGES && svgame.msg[i].name[0]; i++ )
|
|
SV_SendUserReg( msg, &svgame.msg[i] );
|
|
|
|
for( i = 0; i < MAX_LIGHTSTYLES; i++ )
|
|
{
|
|
if( !sv.lightstyles[i].pattern[0] )
|
|
continue; // unused style
|
|
|
|
MSG_BeginServerCmd( msg, svc_lightstyle );
|
|
MSG_WriteByte( msg, i ); // stylenum
|
|
MSG_WriteString( msg, sv.lightstyles[i].pattern );
|
|
MSG_WriteFloat( msg, sv.lightstyles[i].time );
|
|
}
|
|
}
|
|
|
|
/*
|
|
============================================================
|
|
|
|
CLIENT COMMAND EXECUTION
|
|
|
|
============================================================
|
|
*/
|
|
/*
|
|
================
|
|
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.
|
|
================
|
|
*/
|
|
static qboolean SV_New_f( sv_client_t *cl )
|
|
{
|
|
byte msg_buf[MAX_INIT_MSG];
|
|
char szRejectReason[128];
|
|
char szAddress[128];
|
|
char szName[32];
|
|
sv_client_t *cur;
|
|
sizebuf_t msg;
|
|
int i;
|
|
|
|
MSG_Init( &msg, "New", msg_buf, sizeof( msg_buf ));
|
|
|
|
if( cl->state != cs_connected )
|
|
return false;
|
|
|
|
// send the serverdata
|
|
SV_SendServerdata( &msg, cl );
|
|
|
|
// if the client was connected, tell the game .dll to disconnect him/her.
|
|
if(( cl->state == cs_spawned ) && cl->edict )
|
|
svgame.dllFuncs.pfnClientDisconnect( cl->edict );
|
|
|
|
Q_strncpy( szName, cl->name, sizeof( szName ) );
|
|
Q_strncpy( szAddress, NET_AdrToString( cl->netchan.remote_address ), sizeof( szAddress ) );
|
|
Q_strncpy( szRejectReason, "Connection rejected by game\n", sizeof( szRejectReason ) );
|
|
|
|
// Allow the game dll to reject this client.
|
|
if( !svgame.dllFuncs.pfnClientConnect( cl->edict, szName, szAddress, szRejectReason ))
|
|
{
|
|
// reject the connection and drop the client.
|
|
SV_RejectConnection( cl->netchan.remote_address, "%s\n", szRejectReason );
|
|
SV_DropClient( cl, false );
|
|
return true;
|
|
}
|
|
|
|
// server info string
|
|
MSG_BeginServerCmd( &msg, svc_stufftext );
|
|
MSG_WriteString( &msg, va( "fullserverinfo \"%s\"\n", SV_Serverinfo( )));
|
|
|
|
// collect the info about all the players and send to me
|
|
for( i = 0, cur = svs.clients; i < svs.maxclients; i++, cur++ )
|
|
{
|
|
if( !cur->edict || cur->state != cs_spawned )
|
|
continue; // not in game yet
|
|
SV_FullClientUpdate( cur, &msg );
|
|
}
|
|
|
|
// g-cont. why this is there?
|
|
memset( &cl->lastcmd, 0, sizeof( cl->lastcmd ));
|
|
|
|
Netchan_CreateFragments( &cl->netchan, &msg );
|
|
Netchan_FragSend( &cl->netchan );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
SV_Disconnect_f
|
|
|
|
The client is going to disconnect, so remove the connection immediately
|
|
=================
|
|
*/
|
|
static qboolean SV_Disconnect_f( sv_client_t *cl )
|
|
{
|
|
SV_DropClient( cl, false );
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_ShowServerinfo_f
|
|
|
|
Dumps the serverinfo info string
|
|
==================
|
|
*/
|
|
static qboolean SV_ShowServerinfo_f( sv_client_t *cl )
|
|
{
|
|
Info_Print( svs.serverinfo );
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Pause_f
|
|
==================
|
|
*/
|
|
static qboolean SV_Pause_f( sv_client_t *cl )
|
|
{
|
|
string message;
|
|
|
|
if( UI_CreditsActive( ))
|
|
return true;
|
|
|
|
if( !sv_pausable->value )
|
|
{
|
|
SV_ClientPrintf( cl, "Pause not allowed.\n" );
|
|
return true;
|
|
}
|
|
|
|
if( FBitSet( cl->flags, FCL_HLTV_PROXY ))
|
|
{
|
|
SV_ClientPrintf( cl, "Spectators can not pause.\n" );
|
|
return true;
|
|
}
|
|
|
|
if( !sv.paused ) Q_snprintf( message, MAX_STRING, "^2%s^7 paused the game\n", cl->name );
|
|
else Q_snprintf( message, MAX_STRING, "^2%s^7 unpaused the game\n", cl->name );
|
|
|
|
SV_TogglePause( message );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
SV_UserinfoChanged
|
|
|
|
Pull specific info from a newly changed userinfo string
|
|
into a more C freindly form.
|
|
=================
|
|
*/
|
|
void SV_UserinfoChanged( sv_client_t *cl )
|
|
{
|
|
int i, dupc = 1;
|
|
edict_t *ent = cl->edict;
|
|
string name1, name2;
|
|
sv_client_t *current;
|
|
const char *val;
|
|
|
|
if( !COM_CheckString( cl->userinfo ))
|
|
return;
|
|
|
|
val = Info_ValueForKey( cl->userinfo, "name" );
|
|
Q_strncpy( name2, val, sizeof( name2 ));
|
|
COM_TrimSpace( name2, name1 );
|
|
|
|
if( !Q_stricmp( name1, "console" ))
|
|
{
|
|
Info_SetValueForKey( cl->userinfo, "name", "unnamed", MAX_INFO_STRING );
|
|
val = Info_ValueForKey( cl->userinfo, "name" );
|
|
}
|
|
else if( Q_strcmp( name1, val ))
|
|
{
|
|
Info_SetValueForKey( cl->userinfo, "name", name1, MAX_INFO_STRING );
|
|
val = Info_ValueForKey( cl->userinfo, "name" );
|
|
}
|
|
|
|
if( !COM_CheckStringEmpty( name1 ) )
|
|
{
|
|
Info_SetValueForKey( cl->userinfo, "name", "unnamed", MAX_INFO_STRING );
|
|
val = Info_ValueForKey( cl->userinfo, "name" );
|
|
Q_strncpy( name2, "unnamed", sizeof( name2 ));
|
|
Q_strncpy( name1, "unnamed", sizeof( name1 ));
|
|
}
|
|
|
|
// check to see if another user by the same name exists
|
|
while( 1 )
|
|
{
|
|
for( i = 0, current = svs.clients; i < svs.maxclients; i++, current++ )
|
|
{
|
|
if( current == cl || current->state != cs_spawned )
|
|
continue;
|
|
|
|
if( !Q_stricmp( current->name, val ))
|
|
break;
|
|
}
|
|
|
|
if( i != svs.maxclients )
|
|
{
|
|
// dup name
|
|
Q_snprintf( name2, sizeof( name2 ), "%s (%u)", name1, dupc++ );
|
|
Info_SetValueForKey( cl->userinfo, "name", name2, MAX_INFO_STRING );
|
|
val = Info_ValueForKey( cl->userinfo, "name" );
|
|
Q_strncpy( cl->name, name2, sizeof( cl->name ));
|
|
}
|
|
else
|
|
{
|
|
if( dupc == 1 ) // unchanged
|
|
Q_strncpy( cl->name, name1, sizeof( cl->name ));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// rate command
|
|
val = Info_ValueForKey( cl->userinfo, "rate" );
|
|
if( COM_CheckString( val ) )
|
|
cl->netchan.rate = bound( MIN_RATE, Q_atoi( val ), MAX_RATE );
|
|
else cl->netchan.rate = DEFAULT_RATE;
|
|
|
|
// movement prediction
|
|
if( Q_atoi( Info_ValueForKey( cl->userinfo, "cl_nopred" )))
|
|
ClearBits( cl->flags, FCL_PREDICT_MOVEMENT );
|
|
else SetBits( cl->flags, FCL_PREDICT_MOVEMENT );
|
|
|
|
// lag compensation
|
|
if( Q_atoi( Info_ValueForKey( cl->userinfo, "cl_lc" )))
|
|
SetBits( cl->flags, FCL_LAG_COMPENSATION );
|
|
else ClearBits( cl->flags, FCL_LAG_COMPENSATION );
|
|
|
|
// weapon perdiction
|
|
if( Q_atoi( Info_ValueForKey( cl->userinfo, "cl_lw" )))
|
|
SetBits( cl->flags, FCL_LOCAL_WEAPONS );
|
|
else ClearBits( cl->flags, FCL_LOCAL_WEAPONS );
|
|
|
|
val = Info_ValueForKey( cl->userinfo, "cl_updaterate" );
|
|
|
|
if( COM_CheckString( val ) )
|
|
{
|
|
if( Q_atoi( val ) != 0 )
|
|
{
|
|
int i = bound( 10, Q_atoi( val ), 300 );
|
|
cl->cl_updaterate = 1.0 / i;
|
|
}
|
|
else cl->cl_updaterate = 0.0;
|
|
}
|
|
|
|
// call prog code to allow overrides
|
|
svgame.dllFuncs.pfnClientUserInfoChanged( cl->edict, cl->userinfo );
|
|
|
|
val = Info_ValueForKey( cl->userinfo, "name" );
|
|
Q_strncpy( cl->name, val, sizeof( cl->name ));
|
|
ent->v.netname = MAKE_STRING( cl->name );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_UpdateUserinfo_f
|
|
==================
|
|
*/
|
|
static qboolean SV_UpdateUserinfo_f( sv_client_t *cl )
|
|
{
|
|
Q_strncpy( cl->userinfo, Cmd_Argv( 1 ), sizeof( cl->userinfo ));
|
|
|
|
if( cl->state >= cs_connected )
|
|
SetBits( cl->flags, FCL_RESEND_USERINFO ); // needs for update client info
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_SetInfo_f
|
|
==================
|
|
*/
|
|
static qboolean SV_SetInfo_f( sv_client_t *cl )
|
|
{
|
|
Info_SetValueForKey( cl->userinfo, Cmd_Argv( 1 ), Cmd_Argv( 2 ), MAX_INFO_STRING );
|
|
|
|
if( cl->state >= cs_connected )
|
|
SetBits( cl->flags, FCL_RESEND_USERINFO ); // needs for update client info
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Noclip_f
|
|
==================
|
|
*/
|
|
static qboolean SV_Noclip_f( sv_client_t *cl )
|
|
{
|
|
edict_t *pEntity = cl->edict;
|
|
|
|
if( !Cvar_VariableInteger( "sv_cheats" ) || sv.background )
|
|
return true;
|
|
|
|
if( pEntity->v.movetype != MOVETYPE_NOCLIP )
|
|
{
|
|
SV_ClientPrintf( cl, "noclip ON\n" );
|
|
pEntity->v.movetype = MOVETYPE_NOCLIP;
|
|
}
|
|
else
|
|
{
|
|
SV_ClientPrintf( cl, "noclip OFF\n" );
|
|
pEntity->v.movetype = MOVETYPE_WALK;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Godmode_f
|
|
==================
|
|
*/
|
|
static qboolean SV_Godmode_f( sv_client_t *cl )
|
|
{
|
|
edict_t *pEntity = cl->edict;
|
|
|
|
if( !Cvar_VariableInteger( "sv_cheats" ) || sv.background )
|
|
return true;
|
|
|
|
pEntity->v.flags = pEntity->v.flags ^ FL_GODMODE;
|
|
|
|
if( !FBitSet( pEntity->v.flags, FL_GODMODE ))
|
|
SV_ClientPrintf( cl, "godmode OFF\n" );
|
|
else SV_ClientPrintf( cl, "godmode ON\n" );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Notarget_f
|
|
==================
|
|
*/
|
|
static qboolean SV_Notarget_f( sv_client_t *cl )
|
|
{
|
|
edict_t *pEntity = cl->edict;
|
|
|
|
if( !Cvar_VariableInteger( "sv_cheats" ) || sv.background )
|
|
return true;
|
|
|
|
pEntity->v.flags = pEntity->v.flags ^ FL_NOTARGET;
|
|
|
|
if( !FBitSet( pEntity->v.flags, FL_NOTARGET ))
|
|
SV_ClientPrintf( cl, "notarget OFF\n" );
|
|
else SV_ClientPrintf( cl, "notarget ON\n" );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Kill_f
|
|
==================
|
|
*/
|
|
static qboolean SV_Kill_f( sv_client_t *cl )
|
|
{
|
|
if( !SV_IsValidEdict( cl->edict ))
|
|
return true;
|
|
|
|
if( cl->edict->v.health <= 0.0f )
|
|
{
|
|
SV_ClientPrintf( cl, "Can't suicide - already dead!\n");
|
|
return true;
|
|
}
|
|
|
|
svgame.dllFuncs.pfnClientKill( cl->edict );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_SendRes_f
|
|
==================
|
|
*/
|
|
static qboolean SV_SendRes_f( sv_client_t *cl )
|
|
{
|
|
byte buffer[MAX_INIT_MSG];
|
|
sizebuf_t msg;
|
|
|
|
if( cl->state != cs_connected )
|
|
return false;
|
|
|
|
MSG_Init( &msg, "SendResources", buffer, sizeof( buffer ));
|
|
|
|
if( svs.maxclients > 1 && FBitSet( cl->flags, FCL_SEND_RESOURCES ))
|
|
return true;
|
|
|
|
SetBits( cl->flags, FCL_SEND_RESOURCES );
|
|
SV_SendResources( cl, &msg );
|
|
|
|
Netchan_CreateFragments( &cl->netchan, &msg );
|
|
Netchan_FragSend( &cl->netchan );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_DownloadFile_f
|
|
==================
|
|
*/
|
|
static qboolean SV_DownloadFile_f( sv_client_t *cl )
|
|
{
|
|
const char *name;
|
|
|
|
if( Cmd_Argc() < 2 )
|
|
return true;
|
|
|
|
name = Cmd_Argv( 1 );
|
|
|
|
if( !COM_CheckString( name ))
|
|
return true;
|
|
|
|
if( !COM_IsSafeFileToDownload( name ) || !sv_allow_download.value )
|
|
{
|
|
SV_FailDownload( cl, name );
|
|
return true;
|
|
}
|
|
|
|
// g-cont. now we supports hot precache
|
|
if( name[0] != '!' )
|
|
{
|
|
if( sv_send_resources.value )
|
|
{
|
|
int i;
|
|
|
|
// security: allow download only precached resources
|
|
for( i = 0; i < sv.num_resources; i++ )
|
|
{
|
|
const char *cmpname = name;
|
|
|
|
if( sv.resources[i].type == t_sound )
|
|
cmpname += sizeof( DEFAULT_SOUNDPATH ) - 1; // cut "sound/" off
|
|
|
|
if( !Q_strncmp( sv.resources[i].szFileName, cmpname, 64 ) )
|
|
break;
|
|
}
|
|
|
|
if( i == sv.num_resources )
|
|
{
|
|
SV_FailDownload( cl, name );
|
|
return true;
|
|
}
|
|
|
|
// also check the model textures
|
|
if( !Q_stricmp( COM_FileExtension( name ), "mdl" ))
|
|
{
|
|
if( FS_FileExists( Mod_StudioTexName( name ), false ) > 0 )
|
|
Netchan_CreateFileFragments( &cl->netchan, Mod_StudioTexName( name ));
|
|
}
|
|
|
|
if( Netchan_CreateFileFragments( &cl->netchan, name ))
|
|
{
|
|
Netchan_FragSend( &cl->netchan );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
SV_FailDownload( cl, name );
|
|
return true;
|
|
}
|
|
|
|
if( Q_strlen( name ) == 36 && !Q_strnicmp( name, "!MD5", 4 ) && sv_send_logos.value )
|
|
{
|
|
resource_t custResource;
|
|
byte md5[32];
|
|
byte *pbuf;
|
|
int size;
|
|
|
|
memset( &custResource, 0, sizeof( custResource ) );
|
|
COM_HexConvert( name + 4, 32, md5 );
|
|
|
|
if( HPAK_ResourceForHash( CUSTOM_RES_PATH, md5, &custResource ))
|
|
{
|
|
if( HPAK_GetDataPointer( CUSTOM_RES_PATH, &custResource, &pbuf, &size ))
|
|
{
|
|
if( size )
|
|
{
|
|
Netchan_CreateFileFragmentsFromBuffer( &cl->netchan, name, pbuf, size );
|
|
Netchan_FragSend( &cl->netchan );
|
|
Mem_Free( pbuf );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SV_FailDownload( cl, name );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Spawn_f
|
|
==================
|
|
*/
|
|
static qboolean SV_Spawn_f( sv_client_t *cl )
|
|
{
|
|
if( cl->state != cs_connected )
|
|
return false;
|
|
|
|
// handle the case of a level changing while a client was connecting
|
|
if( Q_atoi( Cmd_Argv( 1 )) != svs.spawncount )
|
|
{
|
|
SV_New_f( cl );
|
|
return true;
|
|
}
|
|
|
|
SV_PutClientInServer( cl );
|
|
|
|
// if we are paused, tell the clients
|
|
if( sv.paused )
|
|
{
|
|
MSG_BeginServerCmd( &sv.reliable_datagram, svc_setpause );
|
|
MSG_WriteByte( &sv.reliable_datagram, sv.paused );
|
|
SV_ClientPrintf( cl, "Server is paused.\n" );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Begin_f
|
|
==================
|
|
*/
|
|
static qboolean SV_Begin_f( sv_client_t *cl )
|
|
{
|
|
if( cl->state != cs_connected )
|
|
return false;
|
|
|
|
// now client is spawned
|
|
cl->state = cs_spawned;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_SendBuildInfo_f
|
|
==================
|
|
*/
|
|
static qboolean SV_SendBuildInfo_f( sv_client_t *cl )
|
|
{
|
|
SV_ClientPrintf( cl, "Server running %s %s (build %i-%s, %s-%s)\n",
|
|
XASH_ENGINE_NAME, XASH_VERSION, Q_buildnum(), Q_buildcommit(), Q_buildos(), Q_buildarch() );
|
|
return true;
|
|
}
|
|
|
|
|
|
ucmd_t ucmds[] =
|
|
{
|
|
{ "new", SV_New_f },
|
|
{ "god", SV_Godmode_f },
|
|
{ "kill", SV_Kill_f },
|
|
{ "begin", SV_Begin_f },
|
|
{ "spawn", SV_Spawn_f },
|
|
{ "pause", SV_Pause_f },
|
|
{ "noclip", SV_Noclip_f },
|
|
{ "log", SV_ServerLog_f },
|
|
{ "setinfo", SV_SetInfo_f },
|
|
{ "sendres", SV_SendRes_f },
|
|
{ "notarget", SV_Notarget_f },
|
|
{ "info", SV_ShowServerinfo_f },
|
|
{ "dlfile", SV_DownloadFile_f },
|
|
{ "disconnect", SV_Disconnect_f },
|
|
{ "userinfo", SV_UpdateUserinfo_f },
|
|
{ "_sv_build_info", SV_SendBuildInfo_f },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
/*
|
|
==================
|
|
SV_ExecuteUserCommand
|
|
==================
|
|
*/
|
|
void SV_ExecuteClientCommand( sv_client_t *cl, char *s )
|
|
{
|
|
ucmd_t *u;
|
|
|
|
Cmd_TokenizeString( s );
|
|
|
|
for( u = ucmds; u->name; u++ )
|
|
{
|
|
if( !Q_strcmp( Cmd_Argv( 0 ), u->name ))
|
|
{
|
|
if( !u->func( cl ))
|
|
Con_Printf( "'%s' is not valid from the console\n", u->name );
|
|
else Con_Reportf( "ucmd->%s()\n", u->name );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !u->name && sv.state == ss_active )
|
|
{
|
|
// custom client commands
|
|
svgame.dllFuncs.pfnClientCommand( cl->edict );
|
|
|
|
if( !Q_strcmp( Cmd_Argv( 0 ), "fullupdate" ))
|
|
{
|
|
// resend the ambient sounds for demo recording
|
|
SV_RestartAmbientSounds();
|
|
// resend all the decals for demo recording
|
|
SV_RestartDecals();
|
|
// resend all the static ents for demo recording
|
|
SV_RestartStaticEnts();
|
|
// resend the viewentity
|
|
SV_UpdateClientView( cl );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_TSourceEngineQuery
|
|
==================
|
|
*/
|
|
void SV_TSourceEngineQuery( netadr_t from )
|
|
{
|
|
// A2S_INFO
|
|
char answer[1024] = "";
|
|
int count = 0, bots = 0;
|
|
int index;
|
|
sizebuf_t buf;
|
|
|
|
if( svs.clients )
|
|
{
|
|
for( index = 0; index < svs.maxclients; index++ )
|
|
{
|
|
if( svs.clients[index].state >= cs_connected )
|
|
{
|
|
if( FBitSet( svs.clients[index].flags, FCL_FAKECLIENT ))
|
|
bots++;
|
|
else count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
MSG_Init( &buf, "TSourceEngineQuery", answer, sizeof( answer ));
|
|
|
|
MSG_WriteByte( &buf, 'm' );
|
|
MSG_WriteString( &buf, NET_AdrToString( net_local ));
|
|
MSG_WriteString( &buf, hostname.string );
|
|
MSG_WriteString( &buf, sv.name );
|
|
MSG_WriteString( &buf, GI->gamefolder );
|
|
MSG_WriteString( &buf, GI->title );
|
|
MSG_WriteByte( &buf, count );
|
|
MSG_WriteByte( &buf, svs.maxclients );
|
|
MSG_WriteByte( &buf, PROTOCOL_VERSION );
|
|
MSG_WriteByte( &buf, Host_IsDedicated() ? 'D' : 'L' );
|
|
MSG_WriteByte( &buf, 'W' );
|
|
|
|
if( Q_stricmp( GI->gamefolder, "valve" ))
|
|
{
|
|
MSG_WriteByte( &buf, 1 ); // mod
|
|
MSG_WriteString( &buf, GI->game_url );
|
|
MSG_WriteString( &buf, GI->update_url );
|
|
MSG_WriteByte( &buf, 0 );
|
|
MSG_WriteLong( &buf, (int)GI->version );
|
|
MSG_WriteLong( &buf, GI->size );
|
|
|
|
if( GI->gamemode == 2 )
|
|
MSG_WriteByte( &buf, 1 ); // multiplayer_only
|
|
else MSG_WriteByte( &buf, 0 );
|
|
|
|
if( Q_strstr( GI->game_dll, "hl." ))
|
|
MSG_WriteByte( &buf, 0 ); // Half-Life DLL
|
|
else MSG_WriteByte( &buf, 1 ); // Own DLL
|
|
}
|
|
else MSG_WriteByte( &buf, 0 ); // Half-Life
|
|
|
|
MSG_WriteByte( &buf, GI->secure ); // unsecure
|
|
MSG_WriteByte( &buf, bots );
|
|
|
|
NET_SendPacket( NS_SERVER, MSG_GetNumBytesWritten( &buf ), MSG_GetData( &buf ), from );
|
|
}
|
|
|
|
/*
|
|
=================
|
|
SV_ConnectionlessPacket
|
|
|
|
A connectionless packet has four leading 0xff
|
|
characters to distinguish it from a game channel.
|
|
Clients that are in the game can still send
|
|
connectionless packets.
|
|
=================
|
|
*/
|
|
void SV_ConnectionlessPacket( netadr_t from, sizebuf_t *msg )
|
|
{
|
|
char *args;
|
|
const char *pcmd;
|
|
char buf[MAX_SYSPATH];
|
|
int len = sizeof( buf );
|
|
|
|
// prevent flooding from banned address
|
|
if( SV_CheckIP( &from ) )
|
|
return;
|
|
|
|
MSG_Clear( msg );
|
|
MSG_ReadLong( msg );// skip the -1 marker
|
|
|
|
args = MSG_ReadStringLine( msg );
|
|
Cmd_TokenizeString( args );
|
|
|
|
pcmd = Cmd_Argv( 0 );
|
|
Con_Reportf( "SV_ConnectionlessPacket: %s : %s\n", NET_AdrToString( from ), pcmd );
|
|
|
|
if( !Q_strcmp( pcmd, "ping" )) SV_Ping( from );
|
|
else if( !Q_strcmp( pcmd, "ack" )) SV_Ack( from );
|
|
else if( !Q_strcmp( pcmd, "info" )) SV_Info( from );
|
|
else if( !Q_strcmp( pcmd, "bandwidth" )) SV_TestBandWidth( from );
|
|
else if( !Q_strcmp( pcmd, "getchallenge" )) SV_GetChallenge( from );
|
|
else if( !Q_strcmp( pcmd, "connect" )) SV_ConnectClient( from );
|
|
else if( !Q_strcmp( pcmd, "rcon" )) SV_RemoteCommand( from, msg );
|
|
else if( !Q_strcmp( pcmd, "netinfo" )) SV_BuildNetAnswer( from );
|
|
else if( !Q_strcmp( pcmd, "s" )) SV_AddToMaster( from, msg );
|
|
else if( !Q_strcmp( pcmd, "T" "Source" )) SV_TSourceEngineQuery( from );
|
|
else if( !Q_strcmp( pcmd, "i" )) NET_SendPacket( NS_SERVER, 5, "\xFF\xFF\xFF\xFFj", from ); // A2A_PING
|
|
else if( svgame.dllFuncs.pfnConnectionlessPacket( &from, args, buf, &len ))
|
|
{
|
|
// user out of band message (must be handled in CL_ConnectionlessPacket)
|
|
if( len > 0 ) Netchan_OutOfBand( NS_SERVER, from, len, (byte*)buf );
|
|
}
|
|
else Con_DPrintf( S_ERROR "bad connectionless packet from %s:\n%s\n", NET_AdrToString( from ), args );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_ParseClientMove
|
|
|
|
The message usually contains all the movement commands
|
|
that were in the last three packets, so that the information
|
|
in dropped packets can be recovered.
|
|
|
|
On very fast clients, there may be multiple usercmd packed into
|
|
each of the backup packets.
|
|
==================
|
|
*/
|
|
static void SV_ParseClientMove( sv_client_t *cl, sizebuf_t *msg )
|
|
{
|
|
client_frame_t *frame;
|
|
int key, size, checksum1, checksum2;
|
|
int i, numbackup, totalcmds, numcmds;
|
|
usercmd_t nullcmd, *to, *from;
|
|
usercmd_t cmds[CMD_BACKUP];
|
|
float packet_loss;
|
|
edict_t *player;
|
|
model_t *model;
|
|
|
|
player = cl->edict;
|
|
|
|
frame = &cl->frames[cl->netchan.incoming_acknowledged & SV_UPDATE_MASK];
|
|
memset( &nullcmd, 0, sizeof( usercmd_t ));
|
|
memset( cmds, 0, sizeof( cmds ));
|
|
|
|
key = MSG_GetRealBytesRead( msg );
|
|
checksum1 = MSG_ReadByte( msg );
|
|
packet_loss = MSG_ReadByte( msg );
|
|
|
|
numbackup = MSG_ReadByte( msg );
|
|
numcmds = MSG_ReadByte( msg );
|
|
|
|
totalcmds = numcmds + numbackup;
|
|
net_drop -= (numcmds - 1);
|
|
|
|
if( totalcmds < 0 || totalcmds >= CMD_MASK )
|
|
{
|
|
Con_Reportf( S_ERROR "SV_ParseClientMove: %s sending too many commands %i\n", cl->name, totalcmds );
|
|
SV_DropClient( cl, false );
|
|
return;
|
|
}
|
|
|
|
from = &nullcmd; // first cmd are starting from null-compressed usercmd_t
|
|
|
|
for( i = totalcmds - 1; i >= 0; i-- )
|
|
{
|
|
to = &cmds[i];
|
|
MSG_ReadDeltaUsercmd( msg, from, to );
|
|
from = to; // get new baseline
|
|
}
|
|
|
|
if( cl->state != cs_spawned )
|
|
return;
|
|
|
|
// if the checksum fails, ignore the rest of the packet
|
|
size = MSG_GetRealBytesRead( msg ) - key - 1;
|
|
checksum2 = CRC32_BlockSequence( msg->pData + key + 1, size, cl->netchan.incoming_sequence );
|
|
|
|
if( checksum2 != checksum1 )
|
|
{
|
|
Con_Reportf( S_ERROR "SV_UserMove: failed command checksum for %s (%d != %d)\n", cl->name, checksum2, checksum1 );
|
|
return;
|
|
}
|
|
|
|
cl->packet_loss = packet_loss;
|
|
|
|
// freeze player for some reasons if loadgame was executed
|
|
if( GameState->loadGame )
|
|
return;
|
|
|
|
// check for pause or frozen
|
|
if( sv.paused || !CL_IsInGame() || SV_PlayerIsFrozen( player ))
|
|
{
|
|
for( i = 0; i < numcmds; i++ )
|
|
{
|
|
cmds[i].msec = 0;
|
|
cmds[i].forwardmove = 0;
|
|
cmds[i].sidemove = 0;
|
|
cmds[i].upmove = 0;
|
|
cmds[i].buttons = 0;
|
|
|
|
if( SV_PlayerIsFrozen( player ))
|
|
cmds[i].impulse = 0;
|
|
|
|
VectorCopy( cmds[i].viewangles, player->v.v_angle );
|
|
}
|
|
net_drop = 0;
|
|
}
|
|
else
|
|
{
|
|
if( !player->v.fixangle )
|
|
VectorCopy( cmds[0].viewangles, player->v.v_angle );
|
|
}
|
|
|
|
SV_EstablishTimeBase( cl, cmds, net_drop, numbackup, numcmds );
|
|
|
|
if( net_drop < 24 )
|
|
{
|
|
while( net_drop > numbackup )
|
|
{
|
|
SV_RunCmd( cl, &cl->lastcmd, 0 );
|
|
net_drop--;
|
|
}
|
|
|
|
while( net_drop > 0 )
|
|
{
|
|
i = numcmds + net_drop - 1;
|
|
SV_RunCmd( cl, &cmds[i], cl->netchan.incoming_sequence - i );
|
|
net_drop--;
|
|
}
|
|
}
|
|
|
|
for( i = numcmds - 1; i >= 0; i-- )
|
|
{
|
|
SV_RunCmd( cl, &cmds[i], cl->netchan.incoming_sequence - i );
|
|
}
|
|
|
|
cl->lastcmd = cmds[0];
|
|
|
|
// adjust latency time by 1/2 last client frame since
|
|
// the message probably arrived 1/2 through client's frame loop
|
|
frame->ping_time -= ( cl->lastcmd.msec * 0.5f ) / 1000.0f;
|
|
frame->ping_time = Q_max( 0.0f, frame->ping_time );
|
|
model = SV_ModelHandle( player->v.modelindex );
|
|
|
|
if( model && model->type == mod_studio )
|
|
{
|
|
// g-cont. yes we using svgame.globals->time instead of sv.time
|
|
if( player->v.animtime > svgame.globals->time + sv.frametime )
|
|
player->v.animtime = svgame.globals->time + sv.frametime;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_ParseResourceList
|
|
|
|
Parse resource list
|
|
===================
|
|
*/
|
|
void SV_ParseResourceList( sv_client_t *cl, sizebuf_t *msg )
|
|
{
|
|
int totalsize;
|
|
resource_t *resource;
|
|
int i, total;
|
|
resourceinfo_t ri;
|
|
|
|
total = MSG_ReadShort( msg );
|
|
|
|
SV_ClearResourceList( &cl->resourcesneeded );
|
|
SV_ClearResourceList( &cl->resourcesonhand );
|
|
|
|
for( i = 0; i < total; i++ )
|
|
{
|
|
resource = Z_Calloc( sizeof( resource_t ) );
|
|
Q_strncpy( resource->szFileName, MSG_ReadString( msg ), sizeof( resource->szFileName ));
|
|
resource->type = MSG_ReadByte( msg );
|
|
resource->nIndex = MSG_ReadShort( msg );
|
|
resource->nDownloadSize = MSG_ReadLong( msg );
|
|
resource->ucFlags = MSG_ReadByte( msg );
|
|
resource->pNext = NULL;
|
|
resource->pPrev = NULL;
|
|
ClearBits( resource->ucFlags, RES_WASMISSING );
|
|
|
|
if( FBitSet( resource->ucFlags, RES_CUSTOM ))
|
|
MSG_ReadBytes( msg, resource->rgucMD5_hash, 16 );
|
|
|
|
if( resource->type > t_world || resource->nDownloadSize > 1024 * 1024 * 1024 )
|
|
{
|
|
SV_ClearResourceList( &cl->resourcesneeded );
|
|
SV_ClearResourceList( &cl->resourcesonhand );
|
|
return;
|
|
}
|
|
SV_AddToResourceList( resource, &cl->resourcesneeded );
|
|
}
|
|
|
|
totalsize = COM_SizeofResourceList( &cl->resourcesneeded, &ri );
|
|
|
|
if( totalsize != 0 && sv_allow_upload.value )
|
|
{
|
|
Con_DPrintf( "Verifying and uploading resources...\n" );
|
|
|
|
if( totalsize != 0 )
|
|
{
|
|
Con_DPrintf( "Custom resources total %.2fK\n", totalsize / 1024.0 );
|
|
|
|
if ( ri.info[t_model].size != 0 )
|
|
Con_DPrintf( " Models: %.2fK\n", ri.info[t_model].size / 1024.0 );
|
|
|
|
if ( ri.info[t_sound].size != 0 )
|
|
Con_DPrintf( " Sounds: %.2fK\n", ri.info[t_sound].size / 1024.0 );
|
|
|
|
if ( ri.info[t_decal].size != 0 )
|
|
Con_DPrintf( " Decals: %.2fK\n", ri.info[t_decal].size / 1024.0 );
|
|
|
|
if ( ri.info[t_skin].size != 0 )
|
|
Con_DPrintf( " Skins : %.2fK\n", ri.info[t_skin].size / 1024.0 );
|
|
|
|
if ( ri.info[t_generic].size != 0 )
|
|
Con_DPrintf( " Generic : %.2fK\n", ri.info[t_generic].size / 1024.0 );
|
|
|
|
if ( ri.info[t_eventscript].size != 0 )
|
|
Con_DPrintf( " Events : %.2fK\n", ri.info[t_eventscript].size / 1024.0 );
|
|
|
|
Con_DPrintf( "----------------------\n" );
|
|
}
|
|
|
|
totalsize = SV_EstimateNeededResources( cl );
|
|
|
|
if( totalsize > sv_uploadmax.value * 1024 * 1024 )
|
|
{
|
|
SV_ClearResourceList( &cl->resourcesneeded );
|
|
SV_ClearResourceList( &cl->resourcesonhand );
|
|
return;
|
|
}
|
|
Con_DPrintf( "resources to request: %s\n", Q_memprint( totalsize ));
|
|
}
|
|
|
|
cl->upstate = us_processing;
|
|
SV_BatchUploadRequest( cl );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_ParseCvarValue
|
|
|
|
Parse a requested value from client cvar
|
|
===================
|
|
*/
|
|
void SV_ParseCvarValue( sv_client_t *cl, sizebuf_t *msg )
|
|
{
|
|
const char *value = MSG_ReadString( msg );
|
|
|
|
if( svgame.dllFuncs2.pfnCvarValue != NULL )
|
|
svgame.dllFuncs2.pfnCvarValue( cl->edict, value );
|
|
Con_Reportf( "Cvar query response: name:%s, value:%s\n", cl->name, value );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_ParseCvarValue2
|
|
|
|
Parse a requested value from client cvar
|
|
===================
|
|
*/
|
|
void SV_ParseCvarValue2( sv_client_t *cl, sizebuf_t *msg )
|
|
{
|
|
string name, value;
|
|
int requestID = MSG_ReadLong( msg );
|
|
|
|
Q_strcpy( name, MSG_ReadString( msg ));
|
|
Q_strcpy( value, MSG_ReadString( msg ));
|
|
|
|
if( svgame.dllFuncs2.pfnCvarValue2 != NULL )
|
|
svgame.dllFuncs2.pfnCvarValue2( cl->edict, requestID, name, value );
|
|
Con_Reportf( "Cvar query response: name:%s, request ID %d, cvar:%s, value:%s\n", cl->name, requestID, name, value );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_ExecuteClientMessage
|
|
|
|
Parse a client packet
|
|
===================
|
|
*/
|
|
void SV_ExecuteClientMessage( sv_client_t *cl, sizebuf_t *msg )
|
|
{
|
|
qboolean move_issued = false;
|
|
client_frame_t *frame;
|
|
int c;
|
|
|
|
ASSERT( cl->frames != NULL );
|
|
|
|
// calc ping time
|
|
frame = &cl->frames[cl->netchan.incoming_acknowledged & SV_UPDATE_MASK];
|
|
|
|
// ping time doesn't factor in message interval, either
|
|
frame->ping_time = host.realtime - frame->senttime - cl->cl_updaterate;
|
|
|
|
// on first frame ( no senttime ) don't skew ping
|
|
if( frame->senttime == 0.0f ) frame->ping_time = 0.0f;
|
|
|
|
// don't skew ping based on signon stuff either
|
|
if(( host.realtime - cl->connection_started ) < 2.0f && ( frame->ping_time > 0.0f ))
|
|
frame->ping_time = 0.0f;
|
|
|
|
cl->latency = SV_CalcClientTime( cl );
|
|
cl->delta_sequence = -1; // no delta unless requested
|
|
|
|
// read optional clientCommand strings
|
|
while( cl->state != cs_zombie )
|
|
{
|
|
if( MSG_CheckOverflow( msg ))
|
|
{
|
|
Con_DPrintf( S_ERROR "incoming overflow for %s\n", cl->name );
|
|
SV_DropClient( cl, false );
|
|
return;
|
|
}
|
|
|
|
// end of message
|
|
if( MSG_GetNumBitsLeft( msg ) < 8 )
|
|
break;
|
|
|
|
c = MSG_ReadClientCmd( msg );
|
|
|
|
switch( c )
|
|
{
|
|
case clc_nop:
|
|
break;
|
|
case clc_delta:
|
|
cl->delta_sequence = MSG_ReadByte( msg );
|
|
break;
|
|
case clc_move:
|
|
if( move_issued ) return; // someone is trying to cheat...
|
|
move_issued = true;
|
|
SV_ParseClientMove( cl, msg );
|
|
break;
|
|
case clc_stringcmd:
|
|
SV_ExecuteClientCommand( cl, MSG_ReadString( msg ));
|
|
if( cl->state == cs_zombie )
|
|
return; // disconnect command
|
|
break;
|
|
case clc_resourcelist:
|
|
SV_ParseResourceList( cl, msg );
|
|
break;
|
|
case clc_fileconsistency:
|
|
SV_ParseConsistencyResponse( cl, msg );
|
|
break;
|
|
case clc_requestcvarvalue:
|
|
SV_ParseCvarValue( cl, msg );
|
|
break;
|
|
case clc_requestcvarvalue2:
|
|
SV_ParseCvarValue2( cl, msg );
|
|
break;
|
|
default:
|
|
Con_DPrintf( S_ERROR "%s: clc_bad\n", cl->name );
|
|
SV_DropClient( cl, false );
|
|
return;
|
|
}
|
|
}
|
|
}
|