2005-08-26 17:39:27 +00:00
|
|
|
/*
|
|
|
|
===========================================================================
|
|
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
|
|
|
|
Quake III Arena source code 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.
|
|
|
|
|
|
|
|
Quake III Arena source code 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
|
2005-10-29 01:53:09 +00:00
|
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
2005-08-26 17:39:27 +00:00
|
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
===========================================================================
|
|
|
|
*/
|
|
|
|
// cl_parse.c -- parse a message received from the server
|
|
|
|
|
|
|
|
#include "client.h"
|
|
|
|
|
|
|
|
char *svc_strings[256] = {
|
|
|
|
"svc_bad",
|
|
|
|
|
|
|
|
"svc_nop",
|
|
|
|
"svc_gamestate",
|
|
|
|
"svc_configstring",
|
|
|
|
"svc_baseline",
|
|
|
|
"svc_serverCommand",
|
|
|
|
"svc_download",
|
2008-06-01 07:51:23 +00:00
|
|
|
"svc_snapshot",
|
|
|
|
"svc_EOF",
|
2008-06-03 02:32:52 +00:00
|
|
|
"svc_extension",
|
|
|
|
"svc_voip",
|
2005-08-26 17:39:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
void SHOWNET( msg_t *msg, char *s) {
|
|
|
|
if ( cl_shownet->integer >= 2) {
|
|
|
|
Com_Printf ("%3i:%s\n", msg->readcount-1, s);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
=========================================================================
|
|
|
|
|
|
|
|
MESSAGE PARSING
|
|
|
|
|
|
|
|
=========================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
==================
|
|
|
|
CL_DeltaEntity
|
|
|
|
|
|
|
|
Parses deltas from the given base and adds the resulting entity
|
|
|
|
to the current frame
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
void CL_DeltaEntity (msg_t *msg, clSnapshot_t *frame, int newnum, entityState_t *old,
|
|
|
|
qboolean unchanged) {
|
|
|
|
entityState_t *state;
|
|
|
|
|
|
|
|
// save the parsed entity state into the big circular buffer so
|
|
|
|
// it can be used as the source for a later delta
|
|
|
|
state = &cl.parseEntities[cl.parseEntitiesNum & (MAX_PARSE_ENTITIES-1)];
|
|
|
|
|
|
|
|
if ( unchanged ) {
|
|
|
|
*state = *old;
|
|
|
|
} else {
|
|
|
|
MSG_ReadDeltaEntity( msg, old, state, newnum );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( state->number == (MAX_GENTITIES-1) ) {
|
|
|
|
return; // entity was delta removed
|
|
|
|
}
|
|
|
|
cl.parseEntitiesNum++;
|
|
|
|
frame->numEntities++;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
==================
|
|
|
|
CL_ParsePacketEntities
|
|
|
|
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
void CL_ParsePacketEntities( msg_t *msg, clSnapshot_t *oldframe, clSnapshot_t *newframe) {
|
|
|
|
int newnum;
|
|
|
|
entityState_t *oldstate;
|
|
|
|
int oldindex, oldnum;
|
|
|
|
|
|
|
|
newframe->parseEntitiesNum = cl.parseEntitiesNum;
|
|
|
|
newframe->numEntities = 0;
|
|
|
|
|
|
|
|
// delta from the entities present in oldframe
|
|
|
|
oldindex = 0;
|
|
|
|
oldstate = NULL;
|
|
|
|
if (!oldframe) {
|
|
|
|
oldnum = 99999;
|
|
|
|
} else {
|
|
|
|
if ( oldindex >= oldframe->numEntities ) {
|
|
|
|
oldnum = 99999;
|
|
|
|
} else {
|
|
|
|
oldstate = &cl.parseEntities[
|
|
|
|
(oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)];
|
|
|
|
oldnum = oldstate->number;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
while ( 1 ) {
|
|
|
|
// read the entity index number
|
|
|
|
newnum = MSG_ReadBits( msg, GENTITYNUM_BITS );
|
|
|
|
|
|
|
|
if ( newnum == (MAX_GENTITIES-1) ) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( msg->readcount > msg->cursize ) {
|
|
|
|
Com_Error (ERR_DROP,"CL_ParsePacketEntities: end of message");
|
|
|
|
}
|
|
|
|
|
|
|
|
while ( oldnum < newnum ) {
|
|
|
|
// one or more entities from the old packet are unchanged
|
|
|
|
if ( cl_shownet->integer == 3 ) {
|
|
|
|
Com_Printf ("%3i: unchanged: %i\n", msg->readcount, oldnum);
|
|
|
|
}
|
|
|
|
CL_DeltaEntity( msg, newframe, oldnum, oldstate, qtrue );
|
|
|
|
|
|
|
|
oldindex++;
|
|
|
|
|
|
|
|
if ( oldindex >= oldframe->numEntities ) {
|
|
|
|
oldnum = 99999;
|
|
|
|
} else {
|
|
|
|
oldstate = &cl.parseEntities[
|
|
|
|
(oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)];
|
|
|
|
oldnum = oldstate->number;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (oldnum == newnum) {
|
|
|
|
// delta from previous state
|
|
|
|
if ( cl_shownet->integer == 3 ) {
|
|
|
|
Com_Printf ("%3i: delta: %i\n", msg->readcount, newnum);
|
|
|
|
}
|
|
|
|
CL_DeltaEntity( msg, newframe, newnum, oldstate, qfalse );
|
|
|
|
|
|
|
|
oldindex++;
|
|
|
|
|
|
|
|
if ( oldindex >= oldframe->numEntities ) {
|
|
|
|
oldnum = 99999;
|
|
|
|
} else {
|
|
|
|
oldstate = &cl.parseEntities[
|
|
|
|
(oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)];
|
|
|
|
oldnum = oldstate->number;
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( oldnum > newnum ) {
|
|
|
|
// delta from baseline
|
|
|
|
if ( cl_shownet->integer == 3 ) {
|
|
|
|
Com_Printf ("%3i: baseline: %i\n", msg->readcount, newnum);
|
|
|
|
}
|
|
|
|
CL_DeltaEntity( msg, newframe, newnum, &cl.entityBaselines[newnum], qfalse );
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// any remaining entities in the old frame are copied over
|
|
|
|
while ( oldnum != 99999 ) {
|
|
|
|
// one or more entities from the old packet are unchanged
|
|
|
|
if ( cl_shownet->integer == 3 ) {
|
|
|
|
Com_Printf ("%3i: unchanged: %i\n", msg->readcount, oldnum);
|
|
|
|
}
|
|
|
|
CL_DeltaEntity( msg, newframe, oldnum, oldstate, qtrue );
|
|
|
|
|
|
|
|
oldindex++;
|
|
|
|
|
|
|
|
if ( oldindex >= oldframe->numEntities ) {
|
|
|
|
oldnum = 99999;
|
|
|
|
} else {
|
|
|
|
oldstate = &cl.parseEntities[
|
|
|
|
(oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)];
|
|
|
|
oldnum = oldstate->number;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
================
|
|
|
|
CL_ParseSnapshot
|
|
|
|
|
|
|
|
If the snapshot is parsed properly, it will be copied to
|
|
|
|
cl.snap and saved in cl.snapshots[]. If the snapshot is invalid
|
|
|
|
for any reason, no changes to the state will be made at all.
|
|
|
|
================
|
|
|
|
*/
|
|
|
|
void CL_ParseSnapshot( msg_t *msg ) {
|
|
|
|
int len;
|
|
|
|
clSnapshot_t *old;
|
|
|
|
clSnapshot_t newSnap;
|
|
|
|
int deltaNum;
|
|
|
|
int oldMessageNum;
|
|
|
|
int i, packetNum;
|
|
|
|
|
|
|
|
// get the reliable sequence acknowledge number
|
|
|
|
// NOTE: now sent with all server to client messages
|
|
|
|
//clc.reliableAcknowledge = MSG_ReadLong( msg );
|
|
|
|
|
|
|
|
// read in the new snapshot to a temporary buffer
|
|
|
|
// we will only copy to cl.snap if it is valid
|
|
|
|
Com_Memset (&newSnap, 0, sizeof(newSnap));
|
|
|
|
|
|
|
|
// we will have read any new server commands in this
|
|
|
|
// message before we got to svc_snapshot
|
|
|
|
newSnap.serverCommandNum = clc.serverCommandSequence;
|
|
|
|
|
|
|
|
newSnap.serverTime = MSG_ReadLong( msg );
|
|
|
|
|
2006-08-26 01:45:27 +00:00
|
|
|
// if we were just unpaused, we can only *now* really let the
|
|
|
|
// change come into effect or the client hangs.
|
|
|
|
cl_paused->modified = 0;
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
newSnap.messageNum = clc.serverMessageSequence;
|
|
|
|
|
|
|
|
deltaNum = MSG_ReadByte( msg );
|
|
|
|
if ( !deltaNum ) {
|
|
|
|
newSnap.deltaNum = -1;
|
|
|
|
} else {
|
|
|
|
newSnap.deltaNum = newSnap.messageNum - deltaNum;
|
|
|
|
}
|
|
|
|
newSnap.snapFlags = MSG_ReadByte( msg );
|
|
|
|
|
|
|
|
// If the frame is delta compressed from data that we
|
|
|
|
// no longer have available, we must suck up the rest of
|
|
|
|
// the frame, but not use it, then ask for a non-compressed
|
|
|
|
// message
|
|
|
|
if ( newSnap.deltaNum <= 0 ) {
|
|
|
|
newSnap.valid = qtrue; // uncompressed frame
|
|
|
|
old = NULL;
|
|
|
|
clc.demowaiting = qfalse; // we can start recording now
|
|
|
|
} else {
|
|
|
|
old = &cl.snapshots[newSnap.deltaNum & PACKET_MASK];
|
|
|
|
if ( !old->valid ) {
|
|
|
|
// should never happen
|
|
|
|
Com_Printf ("Delta from invalid frame (not supposed to happen!).\n");
|
|
|
|
} else if ( old->messageNum != newSnap.deltaNum ) {
|
|
|
|
// The frame that the server did the delta from
|
|
|
|
// is too old, so we can't reconstruct it properly.
|
|
|
|
Com_Printf ("Delta frame too old.\n");
|
|
|
|
} else if ( cl.parseEntitiesNum - old->parseEntitiesNum > MAX_PARSE_ENTITIES-128 ) {
|
|
|
|
Com_Printf ("Delta parseEntitiesNum too old.\n");
|
|
|
|
} else {
|
|
|
|
newSnap.valid = qtrue; // valid delta parse
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// read areamask
|
|
|
|
len = MSG_ReadByte( msg );
|
2006-06-04 13:45:53 +00:00
|
|
|
|
|
|
|
if(len > sizeof(newSnap.areamask))
|
|
|
|
{
|
2011-05-12 14:02:48 +00:00
|
|
|
Com_Error (ERR_DROP,"CL_ParseSnapshot: Invalid size %d for areamask", len);
|
2006-06-04 13:45:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
MSG_ReadData( msg, &newSnap.areamask, len);
|
|
|
|
|
|
|
|
// read playerinfo
|
|
|
|
SHOWNET( msg, "playerstate" );
|
|
|
|
if ( old ) {
|
|
|
|
MSG_ReadDeltaPlayerstate( msg, &old->ps, &newSnap.ps );
|
|
|
|
} else {
|
|
|
|
MSG_ReadDeltaPlayerstate( msg, NULL, &newSnap.ps );
|
|
|
|
}
|
|
|
|
|
|
|
|
// read packet entities
|
|
|
|
SHOWNET( msg, "packet entities" );
|
|
|
|
CL_ParsePacketEntities( msg, old, &newSnap );
|
|
|
|
|
|
|
|
// if not valid, dump the entire thing now that it has
|
|
|
|
// been properly read
|
|
|
|
if ( !newSnap.valid ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear the valid flags of any snapshots between the last
|
|
|
|
// received and this one, so if there was a dropped packet
|
|
|
|
// it won't look like something valid to delta from next
|
|
|
|
// time we wrap around in the buffer
|
|
|
|
oldMessageNum = cl.snap.messageNum + 1;
|
|
|
|
|
|
|
|
if ( newSnap.messageNum - oldMessageNum >= PACKET_BACKUP ) {
|
|
|
|
oldMessageNum = newSnap.messageNum - ( PACKET_BACKUP - 1 );
|
|
|
|
}
|
|
|
|
for ( ; oldMessageNum < newSnap.messageNum ; oldMessageNum++ ) {
|
|
|
|
cl.snapshots[oldMessageNum & PACKET_MASK].valid = qfalse;
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy to the current good spot
|
|
|
|
cl.snap = newSnap;
|
|
|
|
cl.snap.ping = 999;
|
|
|
|
// calculate ping time
|
|
|
|
for ( i = 0 ; i < PACKET_BACKUP ; i++ ) {
|
|
|
|
packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK;
|
|
|
|
if ( cl.snap.ps.commandTime >= cl.outPackets[ packetNum ].p_serverTime ) {
|
|
|
|
cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// save the frame off in the backup array for later delta comparisons
|
|
|
|
cl.snapshots[cl.snap.messageNum & PACKET_MASK] = cl.snap;
|
|
|
|
|
|
|
|
if (cl_shownet->integer == 3) {
|
|
|
|
Com_Printf( " snapshot:%i delta:%i ping:%i\n", cl.snap.messageNum,
|
|
|
|
cl.snap.deltaNum, cl.snap.ping );
|
|
|
|
}
|
|
|
|
|
|
|
|
cl.newSnapshots = qtrue;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//=====================================================================
|
|
|
|
|
|
|
|
int cl_connectedToPureServer;
|
2007-03-02 17:32:22 +00:00
|
|
|
int cl_connectedToCheatServer;
|
2005-08-26 17:39:27 +00:00
|
|
|
|
2008-07-07 22:31:39 +00:00
|
|
|
#ifdef USE_VOIP
|
2008-06-01 07:51:23 +00:00
|
|
|
int cl_connectedToVoipServer;
|
|
|
|
#endif
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
/*
|
|
|
|
==================
|
|
|
|
CL_SystemInfoChanged
|
|
|
|
|
|
|
|
The systeminfo configstring has been changed, so parse
|
|
|
|
new information out of it. This will happen at every
|
|
|
|
gamestate, and possibly during gameplay.
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
void CL_SystemInfoChanged( void ) {
|
|
|
|
char *systemInfo;
|
|
|
|
const char *s, *t;
|
|
|
|
char key[BIG_INFO_KEY];
|
|
|
|
char value[BIG_INFO_VALUE];
|
|
|
|
qboolean gameSet;
|
|
|
|
|
|
|
|
systemInfo = cl.gameState.stringData + cl.gameState.stringOffsets[ CS_SYSTEMINFO ];
|
|
|
|
// NOTE TTimo:
|
|
|
|
// when the serverId changes, any further messages we send to the server will use this new serverId
|
|
|
|
// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=475
|
|
|
|
// in some cases, outdated cp commands might get sent with this news serverId
|
|
|
|
cl.serverId = atoi( Info_ValueForKey( systemInfo, "sv_serverid" ) );
|
|
|
|
|
|
|
|
// don't set any vars when playing a demo
|
|
|
|
if ( clc.demoplaying ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2008-07-07 22:31:39 +00:00
|
|
|
#ifdef USE_VOIP
|
2008-06-02 19:52:13 +00:00
|
|
|
// in the future, (val) will be a protocol version string, so only
|
|
|
|
// accept explicitly 1, not generally non-zero.
|
2008-06-01 07:51:23 +00:00
|
|
|
s = Info_ValueForKey( systemInfo, "sv_voip" );
|
2010-01-06 13:47:41 +00:00
|
|
|
if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive"))
|
|
|
|
cl_connectedToVoipServer = qfalse;
|
|
|
|
else
|
|
|
|
cl_connectedToVoipServer = (atoi( s ) == 1);
|
|
|
|
|
2008-06-01 07:51:23 +00:00
|
|
|
#endif
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
s = Info_ValueForKey( systemInfo, "sv_cheats" );
|
2007-03-02 17:32:22 +00:00
|
|
|
cl_connectedToCheatServer = atoi( s );
|
|
|
|
if ( !cl_connectedToCheatServer ) {
|
2005-08-26 17:39:27 +00:00
|
|
|
Cvar_SetCheatState();
|
|
|
|
}
|
|
|
|
|
|
|
|
// check pure server string
|
|
|
|
s = Info_ValueForKey( systemInfo, "sv_paks" );
|
|
|
|
t = Info_ValueForKey( systemInfo, "sv_pakNames" );
|
|
|
|
FS_PureServerSetLoadedPaks( s, t );
|
|
|
|
|
|
|
|
s = Info_ValueForKey( systemInfo, "sv_referencedPaks" );
|
|
|
|
t = Info_ValueForKey( systemInfo, "sv_referencedPakNames" );
|
|
|
|
FS_PureServerSetReferencedPaks( s, t );
|
|
|
|
|
|
|
|
gameSet = qfalse;
|
|
|
|
// scan through all the variables in the systeminfo and locally set cvars to match
|
|
|
|
s = systemInfo;
|
|
|
|
while ( s ) {
|
2006-07-03 21:37:50 +00:00
|
|
|
int cvar_flags;
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
Info_NextPair( &s, key, value );
|
|
|
|
if ( !key[0] ) {
|
|
|
|
break;
|
|
|
|
}
|
2006-07-03 21:37:50 +00:00
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
// ehw!
|
2006-07-03 21:37:50 +00:00
|
|
|
if (!Q_stricmp(key, "fs_game"))
|
|
|
|
{
|
|
|
|
if(FS_CheckDirTraversal(value))
|
|
|
|
{
|
2006-07-05 20:30:53 +00:00
|
|
|
Com_Printf(S_COLOR_YELLOW "WARNING: Server sent invalid fs_game value %s\n", value);
|
2006-07-03 21:37:50 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
gameSet = qtrue;
|
|
|
|
}
|
|
|
|
|
2006-07-03 21:37:50 +00:00
|
|
|
if((cvar_flags = Cvar_Flags(key)) == CVAR_NONEXISTENT)
|
|
|
|
Cvar_Get(key, value, CVAR_SERVER_CREATED | CVAR_ROM);
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// If this cvar may not be modified by a server discard the value.
|
2011-03-08 01:39:34 +00:00
|
|
|
if(!(cvar_flags & (CVAR_SYSTEMINFO | CVAR_SERVER_CREATED | CVAR_USER_CREATED)))
|
2006-07-05 20:30:53 +00:00
|
|
|
{
|
|
|
|
Com_Printf(S_COLOR_YELLOW "WARNING: server is not allowed to set %s=%s\n", key, value);
|
2006-07-03 21:37:50 +00:00
|
|
|
continue;
|
2006-07-05 20:30:53 +00:00
|
|
|
}
|
2006-07-03 21:37:50 +00:00
|
|
|
|
2011-03-08 01:39:34 +00:00
|
|
|
Cvar_SetSafe(key, value);
|
2006-07-03 21:37:50 +00:00
|
|
|
}
|
2005-08-26 17:39:27 +00:00
|
|
|
}
|
|
|
|
// if game folder should not be set and it is set at the client side
|
|
|
|
if ( !gameSet && *Cvar_VariableString("fs_game") ) {
|
|
|
|
Cvar_Set( "fs_game", "" );
|
|
|
|
}
|
|
|
|
cl_connectedToPureServer = Cvar_VariableValue( "sv_pure" );
|
|
|
|
}
|
|
|
|
|
2006-09-11 16:41:55 +00:00
|
|
|
/*
|
|
|
|
==================
|
|
|
|
CL_ParseServerInfo
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
static void CL_ParseServerInfo(void)
|
|
|
|
{
|
|
|
|
const char *serverInfo;
|
|
|
|
|
|
|
|
serverInfo = cl.gameState.stringData
|
|
|
|
+ cl.gameState.stringOffsets[ CS_SERVERINFO ];
|
|
|
|
|
|
|
|
clc.sv_allowDownload = atoi(Info_ValueForKey(serverInfo,
|
|
|
|
"sv_allowDownload"));
|
|
|
|
Q_strncpyz(clc.sv_dlURL,
|
|
|
|
Info_ValueForKey(serverInfo, "sv_dlURL"),
|
|
|
|
sizeof(clc.sv_dlURL));
|
|
|
|
}
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
/*
|
|
|
|
==================
|
|
|
|
CL_ParseGamestate
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
void CL_ParseGamestate( msg_t *msg ) {
|
|
|
|
int i;
|
|
|
|
entityState_t *es;
|
|
|
|
int newnum;
|
|
|
|
entityState_t nullstate;
|
|
|
|
int cmd;
|
|
|
|
char *s;
|
2011-06-21 11:18:35 +00:00
|
|
|
char oldGame[MAX_QPATH];
|
2005-08-26 17:39:27 +00:00
|
|
|
|
|
|
|
Con_Close();
|
|
|
|
|
|
|
|
clc.connectPacketCount = 0;
|
|
|
|
|
|
|
|
// wipe local client state
|
|
|
|
CL_ClearState();
|
|
|
|
|
|
|
|
// a gamestate always marks a server command sequence
|
|
|
|
clc.serverCommandSequence = MSG_ReadLong( msg );
|
|
|
|
|
|
|
|
// parse all the configstrings and baselines
|
|
|
|
cl.gameState.dataCount = 1; // leave a 0 at the beginning for uninitialized configstrings
|
|
|
|
while ( 1 ) {
|
|
|
|
cmd = MSG_ReadByte( msg );
|
|
|
|
|
|
|
|
if ( cmd == svc_EOF ) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( cmd == svc_configstring ) {
|
|
|
|
int len;
|
|
|
|
|
|
|
|
i = MSG_ReadShort( msg );
|
|
|
|
if ( i < 0 || i >= MAX_CONFIGSTRINGS ) {
|
|
|
|
Com_Error( ERR_DROP, "configstring > MAX_CONFIGSTRINGS" );
|
|
|
|
}
|
|
|
|
s = MSG_ReadBigString( msg );
|
|
|
|
len = strlen( s );
|
|
|
|
|
|
|
|
if ( len + 1 + cl.gameState.dataCount > MAX_GAMESTATE_CHARS ) {
|
|
|
|
Com_Error( ERR_DROP, "MAX_GAMESTATE_CHARS exceeded" );
|
|
|
|
}
|
|
|
|
|
|
|
|
// append it to the gameState string buffer
|
|
|
|
cl.gameState.stringOffsets[ i ] = cl.gameState.dataCount;
|
|
|
|
Com_Memcpy( cl.gameState.stringData + cl.gameState.dataCount, s, len + 1 );
|
|
|
|
cl.gameState.dataCount += len + 1;
|
|
|
|
} else if ( cmd == svc_baseline ) {
|
|
|
|
newnum = MSG_ReadBits( msg, GENTITYNUM_BITS );
|
|
|
|
if ( newnum < 0 || newnum >= MAX_GENTITIES ) {
|
|
|
|
Com_Error( ERR_DROP, "Baseline number out of range: %i", newnum );
|
|
|
|
}
|
|
|
|
Com_Memset (&nullstate, 0, sizeof(nullstate));
|
|
|
|
es = &cl.entityBaselines[ newnum ];
|
|
|
|
MSG_ReadDeltaEntity( msg, &nullstate, es, newnum );
|
|
|
|
} else {
|
|
|
|
Com_Error( ERR_DROP, "CL_ParseGamestate: bad command byte" );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
clc.clientNum = MSG_ReadLong(msg);
|
|
|
|
// read the checksum feed
|
|
|
|
clc.checksumFeed = MSG_ReadLong( msg );
|
|
|
|
|
2011-06-21 11:18:35 +00:00
|
|
|
// save old gamedir
|
|
|
|
Cvar_VariableStringBuffer("fs_game", oldGame, sizeof(oldGame));
|
|
|
|
|
2006-09-11 16:41:55 +00:00
|
|
|
// parse useful values out of CS_SERVERINFO
|
|
|
|
CL_ParseServerInfo();
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
// parse serverId and other cvars
|
|
|
|
CL_SystemInfoChanged();
|
|
|
|
|
2006-10-27 12:49:55 +00:00
|
|
|
// stop recording now so the demo won't have an unnecessary level load at the end.
|
2006-11-03 08:42:21 +00:00
|
|
|
if(cl_autoRecordDemo->integer && clc.demorecording)
|
2006-10-27 12:49:55 +00:00
|
|
|
CL_StopRecord_f();
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
// reinitialize the filesystem if the game directory has changed
|
2011-06-21 11:18:35 +00:00
|
|
|
if(FS_ConditionalRestart(clc.checksumFeed, qfalse) && !cls.oldGame[0])
|
|
|
|
Q_strncpyz(cls.oldGame, oldGame, sizeof(cls.oldGame));
|
2005-08-26 17:39:27 +00:00
|
|
|
|
|
|
|
// This used to call CL_StartHunkUsers, but now we enter the download state before loading the
|
|
|
|
// cgame
|
|
|
|
CL_InitDownloads();
|
|
|
|
|
|
|
|
// make sure the game starts
|
|
|
|
Cvar_Set( "cl_paused", "0" );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//=====================================================================
|
|
|
|
|
|
|
|
/*
|
|
|
|
=====================
|
|
|
|
CL_ParseDownload
|
|
|
|
|
|
|
|
A download message has been received from the server
|
|
|
|
=====================
|
|
|
|
*/
|
|
|
|
void CL_ParseDownload ( msg_t *msg ) {
|
|
|
|
int size;
|
|
|
|
unsigned char data[MAX_MSGLEN];
|
|
|
|
int block;
|
|
|
|
|
2006-06-04 13:45:53 +00:00
|
|
|
if (!*clc.downloadTempName) {
|
|
|
|
Com_Printf("Server sending download, but no download was requested\n");
|
2009-10-11 18:31:00 +00:00
|
|
|
CL_AddReliableCommand("stopdl", qfalse);
|
2006-06-04 13:45:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
// read the data
|
|
|
|
block = MSG_ReadShort ( msg );
|
|
|
|
|
|
|
|
if ( !block )
|
|
|
|
{
|
|
|
|
// block zero is special, contains file size
|
|
|
|
clc.downloadSize = MSG_ReadLong ( msg );
|
|
|
|
|
|
|
|
Cvar_SetValue( "cl_downloadSize", clc.downloadSize );
|
|
|
|
|
|
|
|
if (clc.downloadSize < 0)
|
|
|
|
{
|
2008-01-22 23:44:10 +00:00
|
|
|
Com_Error( ERR_DROP, "%s", MSG_ReadString( msg ) );
|
2005-08-26 17:39:27 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
size = MSG_ReadShort ( msg );
|
2006-06-04 13:45:53 +00:00
|
|
|
if (size < 0 || size > sizeof(data))
|
|
|
|
{
|
2011-05-12 14:02:48 +00:00
|
|
|
Com_Error(ERR_DROP, "CL_ParseDownload: Invalid size %d for download chunk", size);
|
2006-06-04 13:45:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
MSG_ReadData(msg, data, size);
|
2005-08-26 17:39:27 +00:00
|
|
|
|
|
|
|
if (clc.downloadBlock != block) {
|
|
|
|
Com_DPrintf( "CL_ParseDownload: Expected block %d, got %d\n", clc.downloadBlock, block);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// open the file if not opened yet
|
|
|
|
if (!clc.download)
|
|
|
|
{
|
|
|
|
clc.download = FS_SV_FOpenFileWrite( clc.downloadTempName );
|
|
|
|
|
|
|
|
if (!clc.download) {
|
|
|
|
Com_Printf( "Could not create %s\n", clc.downloadTempName );
|
2009-10-11 18:31:00 +00:00
|
|
|
CL_AddReliableCommand("stopdl", qfalse);
|
2005-08-26 17:39:27 +00:00
|
|
|
CL_NextDownload();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (size)
|
|
|
|
FS_Write( data, size, clc.download );
|
|
|
|
|
2009-10-11 18:31:00 +00:00
|
|
|
CL_AddReliableCommand(va("nextdl %d", clc.downloadBlock), qfalse);
|
2005-08-26 17:39:27 +00:00
|
|
|
clc.downloadBlock++;
|
|
|
|
|
|
|
|
clc.downloadCount += size;
|
|
|
|
|
|
|
|
// So UI gets access to it
|
|
|
|
Cvar_SetValue( "cl_downloadCount", clc.downloadCount );
|
|
|
|
|
|
|
|
if (!size) { // A zero length block means EOF
|
|
|
|
if (clc.download) {
|
|
|
|
FS_FCloseFile( clc.download );
|
|
|
|
clc.download = 0;
|
|
|
|
|
|
|
|
// rename the file
|
|
|
|
FS_SV_Rename ( clc.downloadTempName, clc.downloadName );
|
|
|
|
}
|
|
|
|
|
|
|
|
// send intentions now
|
|
|
|
// We need this because without it, we would hold the last nextdl and then start
|
|
|
|
// loading right away. If we take a while to load, the server is happily trying
|
|
|
|
// to send us that last block over and over.
|
|
|
|
// Write it twice to help make sure we acknowledge the download
|
|
|
|
CL_WritePacket();
|
|
|
|
CL_WritePacket();
|
|
|
|
|
|
|
|
// get another file if needed
|
|
|
|
CL_NextDownload ();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-07-07 22:31:39 +00:00
|
|
|
#ifdef USE_VOIP
|
2008-06-01 07:51:23 +00:00
|
|
|
static
|
|
|
|
qboolean CL_ShouldIgnoreVoipSender(int sender)
|
|
|
|
{
|
2008-07-07 22:31:39 +00:00
|
|
|
if (!cl_voip->integer)
|
2008-06-01 07:51:23 +00:00
|
|
|
return qtrue; // VoIP is disabled.
|
2008-06-08 08:25:25 +00:00
|
|
|
else if ((sender == clc.clientNum) && (!clc.demoplaying))
|
|
|
|
return qtrue; // ignore own voice (unless playing back a demo).
|
2008-06-01 07:51:23 +00:00
|
|
|
else if (clc.voipMuteAll)
|
|
|
|
return qtrue; // all channels are muted with extreme prejudice.
|
|
|
|
else if (clc.voipIgnore[sender])
|
|
|
|
return qtrue; // just ignoring this guy.
|
2008-06-04 21:49:15 +00:00
|
|
|
else if (clc.voipGain[sender] == 0.0f)
|
|
|
|
return qtrue; // too quiet to play.
|
2008-06-01 07:51:23 +00:00
|
|
|
|
2008-06-08 08:23:55 +00:00
|
|
|
return qfalse;
|
2008-06-01 07:51:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
=====================
|
|
|
|
CL_ParseVoip
|
|
|
|
|
|
|
|
A VoIP message has been received from the server
|
|
|
|
=====================
|
|
|
|
*/
|
|
|
|
static
|
|
|
|
void CL_ParseVoip ( msg_t *msg ) {
|
|
|
|
static short decoded[4096]; // !!! FIXME: don't hardcode.
|
|
|
|
|
|
|
|
const int sender = MSG_ReadShort(msg);
|
|
|
|
const int generation = MSG_ReadByte(msg);
|
|
|
|
const int sequence = MSG_ReadLong(msg);
|
|
|
|
const int frames = MSG_ReadByte(msg);
|
|
|
|
const int packetsize = MSG_ReadShort(msg);
|
|
|
|
char encoded[1024];
|
|
|
|
int seqdiff = sequence - clc.voipIncomingSequence[sender];
|
|
|
|
int written = 0;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
Com_DPrintf("VoIP: %d-byte packet from client %d\n", packetsize, sender);
|
|
|
|
|
|
|
|
if (sender < 0)
|
|
|
|
return; // short/invalid packet, bail.
|
|
|
|
else if (generation < 0)
|
|
|
|
return; // short/invalid packet, bail.
|
|
|
|
else if (sequence < 0)
|
|
|
|
return; // short/invalid packet, bail.
|
|
|
|
else if (frames < 0)
|
|
|
|
return; // short/invalid packet, bail.
|
|
|
|
else if (packetsize < 0)
|
|
|
|
return; // short/invalid packet, bail.
|
|
|
|
|
|
|
|
if (packetsize > sizeof (encoded)) { // overlarge packet?
|
|
|
|
int bytesleft = packetsize;
|
|
|
|
while (bytesleft) {
|
|
|
|
int br = bytesleft;
|
|
|
|
if (br > sizeof (encoded))
|
|
|
|
br = sizeof (encoded);
|
|
|
|
MSG_ReadData(msg, encoded, br);
|
|
|
|
bytesleft -= br;
|
|
|
|
}
|
|
|
|
return; // overlarge packet, bail.
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!clc.speexInitialized) {
|
|
|
|
MSG_ReadData(msg, encoded, packetsize); // skip payload.
|
|
|
|
return; // can't handle VoIP without libspeex!
|
|
|
|
} else if (sender >= MAX_CLIENTS) {
|
|
|
|
MSG_ReadData(msg, encoded, packetsize); // skip payload.
|
|
|
|
return; // bogus sender.
|
|
|
|
} else if (CL_ShouldIgnoreVoipSender(sender)) {
|
|
|
|
MSG_ReadData(msg, encoded, packetsize); // skip payload.
|
|
|
|
return; // Channel is muted, bail.
|
|
|
|
}
|
|
|
|
|
|
|
|
// !!! FIXME: make sure data is narrowband? Does decoder handle this?
|
|
|
|
|
|
|
|
Com_DPrintf("VoIP: packet accepted!\n");
|
|
|
|
|
|
|
|
// This is a new "generation" ... a new recording started, reset the bits.
|
|
|
|
if (generation != clc.voipIncomingGeneration[sender]) {
|
|
|
|
Com_DPrintf("VoIP: new generation %d!\n", generation);
|
|
|
|
speex_bits_reset(&clc.speexDecoderBits[sender]);
|
|
|
|
clc.voipIncomingGeneration[sender] = generation;
|
|
|
|
seqdiff = 0;
|
|
|
|
} else if (seqdiff < 0) { // we're ahead of the sequence?!
|
|
|
|
// This shouldn't happen unless the packet is corrupted or something.
|
|
|
|
Com_DPrintf("VoIP: misordered sequence! %d < %d!\n",
|
|
|
|
sequence, clc.voipIncomingSequence[sender]);
|
|
|
|
// reset the bits just in case.
|
|
|
|
speex_bits_reset(&clc.speexDecoderBits[sender]);
|
|
|
|
seqdiff = 0;
|
|
|
|
} else if (seqdiff > 100) { // more than 2 seconds of audio dropped?
|
|
|
|
// just start over.
|
|
|
|
Com_DPrintf("VoIP: Dropped way too many (%d) frames from client #%d\n",
|
|
|
|
seqdiff, sender);
|
|
|
|
speex_bits_reset(&clc.speexDecoderBits[sender]);
|
|
|
|
seqdiff = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (seqdiff != 0) {
|
|
|
|
Com_DPrintf("VoIP: Dropped %d frames from client #%d\n",
|
|
|
|
seqdiff, sender);
|
|
|
|
// tell speex that we're missing frames...
|
|
|
|
for (i = 0; i < seqdiff; i++) {
|
|
|
|
assert((written + clc.speexFrameSize) * 2 < sizeof (decoded));
|
|
|
|
speex_decode_int(clc.speexDecoder[sender], NULL, decoded + written);
|
|
|
|
written += clc.speexFrameSize;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i = 0; i < frames; i++) {
|
|
|
|
char encoded[256];
|
|
|
|
const int len = MSG_ReadByte(msg);
|
|
|
|
if (len < 0) {
|
|
|
|
Com_DPrintf("VoIP: Short packet!\n");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
MSG_ReadData(msg, encoded, len);
|
|
|
|
|
|
|
|
// shouldn't happen, but just in case...
|
|
|
|
if ((written + clc.speexFrameSize) * 2 > sizeof (decoded)) {
|
|
|
|
Com_DPrintf("VoIP: playback %d bytes, %d samples, %d frames\n",
|
|
|
|
written * 2, written, i);
|
2008-06-07 14:38:46 +00:00
|
|
|
S_RawSamples(sender + 1, written, clc.speexSampleRate, 2, 1,
|
|
|
|
(const byte *) decoded, clc.voipGain[sender]);
|
2008-06-01 07:51:23 +00:00
|
|
|
written = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
speex_bits_read_from(&clc.speexDecoderBits[sender], encoded, len);
|
|
|
|
speex_decode_int(clc.speexDecoder[sender],
|
|
|
|
&clc.speexDecoderBits[sender], decoded + written);
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
static FILE *encio = NULL;
|
2008-06-07 14:41:21 +00:00
|
|
|
if (encio == NULL) encio = fopen("voip-incoming-encoded.bin", "wb");
|
2008-06-01 07:51:23 +00:00
|
|
|
if (encio != NULL) { fwrite(encoded, len, 1, encio); fflush(encio); }
|
|
|
|
static FILE *decio = NULL;
|
2008-06-07 14:41:21 +00:00
|
|
|
if (decio == NULL) decio = fopen("voip-incoming-decoded.bin", "wb");
|
2008-06-01 07:51:23 +00:00
|
|
|
if (decio != NULL) { fwrite(decoded+written, clc.speexFrameSize*2, 1, decio); fflush(decio); }
|
|
|
|
#endif
|
|
|
|
|
|
|
|
written += clc.speexFrameSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
Com_DPrintf("VoIP: playback %d bytes, %d samples, %d frames\n",
|
|
|
|
written * 2, written, i);
|
|
|
|
|
|
|
|
if (written > 0) {
|
2008-06-07 14:38:46 +00:00
|
|
|
S_RawSamples(sender + 1, written, clc.speexSampleRate, 2, 1,
|
|
|
|
(const byte *) decoded, clc.voipGain[sender]);
|
2008-06-01 07:51:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
clc.voipIncomingSequence[sender] = sequence + frames;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
2005-08-26 17:39:27 +00:00
|
|
|
/*
|
|
|
|
=====================
|
|
|
|
CL_ParseCommandString
|
|
|
|
|
|
|
|
Command strings are just saved off until cgame asks for them
|
|
|
|
when it transitions a snapshot
|
|
|
|
=====================
|
|
|
|
*/
|
|
|
|
void CL_ParseCommandString( msg_t *msg ) {
|
|
|
|
char *s;
|
|
|
|
int seq;
|
|
|
|
int index;
|
|
|
|
|
|
|
|
seq = MSG_ReadLong( msg );
|
|
|
|
s = MSG_ReadString( msg );
|
|
|
|
|
|
|
|
// see if we have already executed stored it off
|
|
|
|
if ( clc.serverCommandSequence >= seq ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
clc.serverCommandSequence = seq;
|
|
|
|
|
|
|
|
index = seq & (MAX_RELIABLE_COMMANDS-1);
|
|
|
|
Q_strncpyz( clc.serverCommands[ index ], s, sizeof( clc.serverCommands[ index ] ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
=====================
|
|
|
|
CL_ParseServerMessage
|
|
|
|
=====================
|
|
|
|
*/
|
|
|
|
void CL_ParseServerMessage( msg_t *msg ) {
|
|
|
|
int cmd;
|
|
|
|
|
|
|
|
if ( cl_shownet->integer == 1 ) {
|
|
|
|
Com_Printf ("%i ",msg->cursize);
|
|
|
|
} else if ( cl_shownet->integer >= 2 ) {
|
|
|
|
Com_Printf ("------------------\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
MSG_Bitstream(msg);
|
|
|
|
|
|
|
|
// get the reliable sequence acknowledge number
|
|
|
|
clc.reliableAcknowledge = MSG_ReadLong( msg );
|
|
|
|
//
|
|
|
|
if ( clc.reliableAcknowledge < clc.reliableSequence - MAX_RELIABLE_COMMANDS ) {
|
|
|
|
clc.reliableAcknowledge = clc.reliableSequence;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// parse the message
|
|
|
|
//
|
|
|
|
while ( 1 ) {
|
|
|
|
if ( msg->readcount > msg->cursize ) {
|
|
|
|
Com_Error (ERR_DROP,"CL_ParseServerMessage: read past end of server message");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd = MSG_ReadByte( msg );
|
|
|
|
|
2008-06-03 02:32:52 +00:00
|
|
|
// See if this is an extension command after the EOF, which means we
|
|
|
|
// got data that a legacy client should ignore.
|
|
|
|
if ((cmd == svc_EOF) && (MSG_LookaheadByte( msg ) == svc_extension)) {
|
|
|
|
SHOWNET( msg, "EXTENSION" );
|
|
|
|
MSG_ReadByte( msg ); // throw the svc_extension byte away.
|
|
|
|
cmd = MSG_ReadByte( msg ); // something legacy clients can't do!
|
|
|
|
// sometimes you get a svc_extension at end of stream...dangling
|
|
|
|
// bits in the huffman decoder giving a bogus value?
|
|
|
|
if (cmd == -1) {
|
|
|
|
cmd = svc_EOF;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cmd == svc_EOF) {
|
2005-08-26 17:39:27 +00:00
|
|
|
SHOWNET( msg, "END OF MESSAGE" );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( cl_shownet->integer >= 2 ) {
|
2008-06-03 02:32:52 +00:00
|
|
|
if ( (cmd < 0) || (!svc_strings[cmd]) ) {
|
2005-08-26 17:39:27 +00:00
|
|
|
Com_Printf( "%3i:BAD CMD %i\n", msg->readcount-1, cmd );
|
|
|
|
} else {
|
|
|
|
SHOWNET( msg, svc_strings[cmd] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// other commands
|
|
|
|
switch ( cmd ) {
|
|
|
|
default:
|
2011-05-12 14:02:48 +00:00
|
|
|
Com_Error (ERR_DROP,"CL_ParseServerMessage: Illegible server message");
|
2005-08-26 17:39:27 +00:00
|
|
|
break;
|
|
|
|
case svc_nop:
|
|
|
|
break;
|
|
|
|
case svc_serverCommand:
|
|
|
|
CL_ParseCommandString( msg );
|
|
|
|
break;
|
|
|
|
case svc_gamestate:
|
|
|
|
CL_ParseGamestate( msg );
|
|
|
|
break;
|
|
|
|
case svc_snapshot:
|
|
|
|
CL_ParseSnapshot( msg );
|
|
|
|
break;
|
|
|
|
case svc_download:
|
|
|
|
CL_ParseDownload( msg );
|
|
|
|
break;
|
2008-06-01 07:51:23 +00:00
|
|
|
case svc_voip:
|
2008-07-07 22:31:39 +00:00
|
|
|
#ifdef USE_VOIP
|
2008-06-01 07:51:23 +00:00
|
|
|
CL_ParseVoip( msg );
|
|
|
|
#endif
|
2008-06-03 02:32:52 +00:00
|
|
|
break;
|
2005-08-26 17:39:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|