mirror of
https://github.com/Q3Rally-Team/rallyunlimited-engine.git
synced 2024-11-26 06:01:34 +00:00
5083 lines
125 KiB
C
5083 lines
125 KiB
C
/*
|
|
===========================================================================
|
|
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
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
// cl_main.c -- client main loop
|
|
|
|
#include "client.h"
|
|
#include <limits.h>
|
|
|
|
cvar_t *cl_noprint;
|
|
cvar_t *cl_debugMove;
|
|
cvar_t *cl_motd;
|
|
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
cvar_t *cl_renderer;
|
|
#endif
|
|
|
|
cvar_t *rcon_client_password;
|
|
cvar_t *rconAddress;
|
|
|
|
cvar_t *cl_timeout;
|
|
cvar_t *cl_autoNudge;
|
|
cvar_t *cl_timeNudge;
|
|
cvar_t *cl_showTimeDelta;
|
|
|
|
cvar_t *cl_shownet;
|
|
cvar_t *cl_autoRecordDemo;
|
|
|
|
cvar_t *cl_aviFrameRate;
|
|
cvar_t *cl_aviMotionJpeg;
|
|
cvar_t *cl_forceavidemo;
|
|
cvar_t *cl_aviPipeFormat;
|
|
|
|
cvar_t *cl_activeAction;
|
|
|
|
cvar_t *cl_motdString;
|
|
|
|
cvar_t *cl_allowDownload;
|
|
#ifdef USE_CURL
|
|
cvar_t *cl_mapAutoDownload;
|
|
#endif
|
|
cvar_t *cl_conXOffset;
|
|
cvar_t *cl_conColor;
|
|
cvar_t *cl_inGameVideo;
|
|
|
|
cvar_t *cl_serverStatusResendTime;
|
|
|
|
cvar_t *cl_lanForcePackets;
|
|
|
|
cvar_t *cl_guidServerUniq;
|
|
|
|
cvar_t *cl_dlURL;
|
|
cvar_t *cl_dlDirectory;
|
|
|
|
cvar_t *cl_reconnectArgs;
|
|
|
|
// common cvars for GLimp modules
|
|
cvar_t *vid_xpos; // X coordinate of window position
|
|
cvar_t *vid_ypos; // Y coordinate of window position
|
|
cvar_t *r_noborder;
|
|
|
|
cvar_t *r_allowSoftwareGL; // don't abort out if the pixelformat claims software
|
|
cvar_t *r_swapInterval;
|
|
cvar_t *r_glDriver;
|
|
cvar_t *r_displayRefresh;
|
|
cvar_t *r_fullscreen;
|
|
cvar_t *r_mode;
|
|
cvar_t *r_modeFullscreen;
|
|
cvar_t *r_customwidth;
|
|
cvar_t *r_customheight;
|
|
cvar_t *r_customPixelAspect;
|
|
|
|
cvar_t *r_colorbits;
|
|
// these also shared with renderers:
|
|
cvar_t *cl_stencilbits;
|
|
cvar_t *cl_depthbits;
|
|
cvar_t *cl_drawBuffer;
|
|
|
|
clientActive_t cl;
|
|
clientConnection_t clc;
|
|
clientStatic_t cls;
|
|
vm_t *cgvm = NULL;
|
|
|
|
netadr_t rcon_address;
|
|
|
|
char cl_oldGame[ MAX_QPATH ];
|
|
qboolean cl_oldGameSet;
|
|
static qboolean noGameRestart = qfalse;
|
|
|
|
#ifdef USE_CURL
|
|
download_t download;
|
|
#endif
|
|
|
|
// Structure containing functions exported from refresh DLL
|
|
refexport_t re;
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
static void *rendererLib;
|
|
#endif
|
|
|
|
static ping_t cl_pinglist[MAX_PINGREQUESTS];
|
|
|
|
typedef struct serverStatus_s
|
|
{
|
|
char string[BIG_INFO_STRING];
|
|
netadr_t address;
|
|
int time, startTime;
|
|
qboolean pending;
|
|
qboolean print;
|
|
qboolean retrieved;
|
|
} serverStatus_t;
|
|
|
|
static serverStatus_t cl_serverStatusList[MAX_SERVERSTATUSREQUESTS];
|
|
|
|
static void CL_CheckForResend( void );
|
|
static void CL_ShowIP_f( void );
|
|
static void CL_ServerStatus_f( void );
|
|
static void CL_ServerStatusResponse( const netadr_t *from, msg_t *msg );
|
|
static void CL_ServerInfoPacket( const netadr_t *from, msg_t *msg );
|
|
|
|
static void CL_Download_f( void );
|
|
static void CL_LocalServers_f( void );
|
|
static void CL_GlobalServers_f( void );
|
|
static void CL_Ping_f( void );
|
|
|
|
static void CL_InitRef( void );
|
|
static void CL_ShutdownRef( refShutdownCode_t code );
|
|
static void CL_InitGLimp_Cvars( void );
|
|
|
|
static void CL_NextDemo( void );
|
|
|
|
/*
|
|
===============
|
|
CL_CDDialog
|
|
|
|
Called by Com_Error when a cd is needed
|
|
===============
|
|
*/
|
|
void CL_CDDialog( void ) {
|
|
cls.cddialog = qtrue; // start it next frame
|
|
}
|
|
|
|
|
|
/*
|
|
=======================================================================
|
|
|
|
CLIENT RELIABLE COMMAND COMMUNICATION
|
|
|
|
=======================================================================
|
|
*/
|
|
|
|
/*
|
|
======================
|
|
CL_AddReliableCommand
|
|
|
|
The given command will be transmitted to the server, and is guaranteed to
|
|
not have future usercmd_t executed before it is executed
|
|
======================
|
|
*/
|
|
void CL_AddReliableCommand( const char *cmd, qboolean isDisconnectCmd ) {
|
|
int index;
|
|
int unacknowledged = clc.reliableSequence - clc.reliableAcknowledge;
|
|
|
|
if ( clc.serverAddress.type == NA_BAD )
|
|
return;
|
|
|
|
// if we would be losing an old command that hasn't been acknowledged,
|
|
// we must drop the connection
|
|
// also leave one slot open for the disconnect command in this case.
|
|
|
|
if ((isDisconnectCmd && unacknowledged > MAX_RELIABLE_COMMANDS) ||
|
|
(!isDisconnectCmd && unacknowledged >= MAX_RELIABLE_COMMANDS))
|
|
{
|
|
if( com_errorEntered )
|
|
return;
|
|
else
|
|
Com_Error(ERR_DROP, "Client command overflow");
|
|
}
|
|
|
|
clc.reliableSequence++;
|
|
index = clc.reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 );
|
|
Q_strncpyz( clc.reliableCommands[ index ], cmd, sizeof( clc.reliableCommands[ index ] ) );
|
|
}
|
|
|
|
|
|
/*
|
|
=======================================================================
|
|
|
|
CLIENT SIDE DEMO RECORDING
|
|
|
|
=======================================================================
|
|
*/
|
|
|
|
/*
|
|
====================
|
|
CL_WriteDemoMessage
|
|
|
|
Dumps the current net message, prefixed by the length
|
|
====================
|
|
*/
|
|
static void CL_WriteDemoMessage( msg_t *msg, int headerBytes ) {
|
|
int len, swlen;
|
|
|
|
// write the packet sequence
|
|
len = clc.serverMessageSequence;
|
|
swlen = LittleLong( len );
|
|
FS_Write( &swlen, 4, clc.recordfile );
|
|
|
|
// skip the packet sequencing information
|
|
len = msg->cursize - headerBytes;
|
|
swlen = LittleLong(len);
|
|
FS_Write( &swlen, 4, clc.recordfile );
|
|
FS_Write( msg->data + headerBytes, len, clc.recordfile );
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_StopRecording_f
|
|
|
|
stop recording a demo
|
|
====================
|
|
*/
|
|
void CL_StopRecord_f( void ) {
|
|
|
|
if ( clc.recordfile != FS_INVALID_HANDLE ) {
|
|
char tempName[MAX_OSPATH];
|
|
char finalName[MAX_OSPATH];
|
|
int protocol;
|
|
int len, sequence;
|
|
|
|
// finish up
|
|
len = -1;
|
|
FS_Write( &len, 4, clc.recordfile );
|
|
FS_Write( &len, 4, clc.recordfile );
|
|
FS_FCloseFile( clc.recordfile );
|
|
clc.recordfile = FS_INVALID_HANDLE;
|
|
|
|
// select proper extension
|
|
if ( clc.dm68compat || clc.demoplaying ) {
|
|
protocol = OLD_PROTOCOL_VERSION;
|
|
} else {
|
|
protocol = NEW_PROTOCOL_VERSION;
|
|
}
|
|
|
|
if ( com_protocol->integer != DEFAULT_PROTOCOL_VERSION ) {
|
|
protocol = com_protocol->integer;
|
|
}
|
|
|
|
Com_sprintf( tempName, sizeof( tempName ), "%s.tmp", clc.recordName );
|
|
|
|
Com_sprintf( finalName, sizeof( finalName ), "%s.%s%d", clc.recordName, DEMOEXT, protocol );
|
|
|
|
if ( clc.explicitRecordName ) {
|
|
FS_Remove( finalName );
|
|
} else {
|
|
// add sequence suffix to avoid overwrite
|
|
sequence = 0;
|
|
while ( FS_FileExists( finalName ) && ++sequence < 1000 ) {
|
|
Com_sprintf( finalName, sizeof( finalName ), "%s-%02d.%s%d",
|
|
clc.recordName, sequence, DEMOEXT, protocol );
|
|
}
|
|
}
|
|
|
|
FS_Rename( tempName, finalName );
|
|
}
|
|
|
|
if ( !clc.demorecording ) {
|
|
Com_Printf( "Not recording a demo.\n" );
|
|
} else {
|
|
Com_Printf( "Stopped demo recording.\n" );
|
|
}
|
|
|
|
clc.demorecording = qfalse;
|
|
clc.spDemoRecording = qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_WriteServerCommands
|
|
====================
|
|
*/
|
|
static void CL_WriteServerCommands( msg_t *msg ) {
|
|
int i;
|
|
|
|
if ( clc.demoCommandSequence < clc.serverCommandSequence ) {
|
|
|
|
// do not write more than MAX_RELIABLE_COMMANDS
|
|
if ( clc.serverCommandSequence - clc.demoCommandSequence > MAX_RELIABLE_COMMANDS )
|
|
clc.demoCommandSequence = clc.serverCommandSequence - MAX_RELIABLE_COMMANDS;
|
|
|
|
for ( i = clc.demoCommandSequence + 1 ; i <= clc.serverCommandSequence; i++ ) {
|
|
MSG_WriteByte( msg, svc_serverCommand );
|
|
MSG_WriteLong( msg, i );
|
|
MSG_WriteString( msg, clc.serverCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] );
|
|
}
|
|
}
|
|
|
|
clc.demoCommandSequence = clc.serverCommandSequence;
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_WriteGamestate
|
|
====================
|
|
*/
|
|
static void CL_WriteGamestate( qboolean initial )
|
|
{
|
|
byte bufData[ MAX_MSGLEN_BUF ];
|
|
char *s;
|
|
msg_t msg;
|
|
int i;
|
|
int len;
|
|
entityState_t *ent;
|
|
entityState_t nullstate;
|
|
|
|
// write out the gamestate message
|
|
MSG_Init( &msg, bufData, MAX_MSGLEN );
|
|
MSG_Bitstream( &msg );
|
|
|
|
// NOTE, MRE: all server->client messages now acknowledge
|
|
MSG_WriteLong( &msg, clc.reliableSequence );
|
|
|
|
if ( initial ) {
|
|
clc.demoMessageSequence = 1;
|
|
clc.demoCommandSequence = clc.serverCommandSequence;
|
|
} else {
|
|
CL_WriteServerCommands( &msg );
|
|
}
|
|
|
|
clc.demoDeltaNum = 0; // reset delta for next snapshot
|
|
|
|
MSG_WriteByte( &msg, svc_gamestate );
|
|
MSG_WriteLong( &msg, clc.serverCommandSequence );
|
|
|
|
// configstrings
|
|
for ( i = 0 ; i < MAX_CONFIGSTRINGS ; i++ ) {
|
|
if ( !cl.gameState.stringOffsets[i] ) {
|
|
continue;
|
|
}
|
|
s = cl.gameState.stringData + cl.gameState.stringOffsets[i];
|
|
MSG_WriteByte( &msg, svc_configstring );
|
|
MSG_WriteShort( &msg, i );
|
|
MSG_WriteBigString( &msg, s );
|
|
}
|
|
|
|
// baselines
|
|
Com_Memset( &nullstate, 0, sizeof( nullstate ) );
|
|
for ( i = 0; i < MAX_GENTITIES ; i++ ) {
|
|
if ( !cl.baselineUsed[ i ] )
|
|
continue;
|
|
ent = &cl.entityBaselines[ i ];
|
|
MSG_WriteByte( &msg, svc_baseline );
|
|
MSG_WriteDeltaEntity( &msg, &nullstate, ent, qtrue );
|
|
}
|
|
|
|
// finalize message
|
|
MSG_WriteByte( &msg, svc_EOF );
|
|
|
|
// finished writing the gamestate stuff
|
|
|
|
// write the client num
|
|
MSG_WriteLong( &msg, clc.clientNum );
|
|
|
|
// write the checksum feed
|
|
MSG_WriteLong( &msg, clc.checksumFeed );
|
|
|
|
// finished writing the client packet
|
|
MSG_WriteByte( &msg, svc_EOF );
|
|
|
|
// write it to the demo file
|
|
if ( clc.demoplaying )
|
|
len = LittleLong( clc.demoMessageSequence - 1 );
|
|
else
|
|
len = LittleLong( clc.serverMessageSequence - 1 );
|
|
|
|
FS_Write( &len, 4, clc.recordfile );
|
|
|
|
len = LittleLong( msg.cursize );
|
|
FS_Write( &len, 4, clc.recordfile );
|
|
FS_Write( msg.data, msg.cursize, clc.recordfile );
|
|
}
|
|
|
|
|
|
/*
|
|
=============
|
|
CL_EmitPacketEntities
|
|
=============
|
|
*/
|
|
static void CL_EmitPacketEntities( clSnapshot_t *from, clSnapshot_t *to, msg_t *msg, entityState_t *oldents ) {
|
|
entityState_t *oldent, *newent;
|
|
int oldindex, newindex;
|
|
int oldnum, newnum;
|
|
int from_num_entities;
|
|
|
|
// generate the delta update
|
|
if ( !from ) {
|
|
from_num_entities = 0;
|
|
} else {
|
|
from_num_entities = from->numEntities;
|
|
}
|
|
|
|
newent = NULL;
|
|
oldent = NULL;
|
|
newindex = 0;
|
|
oldindex = 0;
|
|
while ( newindex < to->numEntities || oldindex < from_num_entities ) {
|
|
if ( newindex >= to->numEntities ) {
|
|
newnum = MAX_GENTITIES+1;
|
|
} else {
|
|
newent = &cl.parseEntities[(to->parseEntitiesNum + newindex) % MAX_PARSE_ENTITIES];
|
|
newnum = newent->number;
|
|
}
|
|
|
|
if ( oldindex >= from_num_entities ) {
|
|
oldnum = MAX_GENTITIES+1;
|
|
} else {
|
|
//oldent = &cl.parseEntities[(from->parseEntitiesNum + oldindex) % MAX_PARSE_ENTITIES];
|
|
oldent = &oldents[ oldindex ];
|
|
oldnum = oldent->number;
|
|
}
|
|
|
|
if ( newnum == oldnum ) {
|
|
// delta update from old position
|
|
// because the force parm is qfalse, this will not result
|
|
// in any bytes being emitted if the entity has not changed at all
|
|
MSG_WriteDeltaEntity (msg, oldent, newent, qfalse );
|
|
oldindex++;
|
|
newindex++;
|
|
continue;
|
|
}
|
|
|
|
if ( newnum < oldnum ) {
|
|
// this is a new entity, send it from the baseline
|
|
MSG_WriteDeltaEntity (msg, &cl.entityBaselines[newnum], newent, qtrue );
|
|
newindex++;
|
|
continue;
|
|
}
|
|
|
|
if ( newnum > oldnum ) {
|
|
// the old entity isn't present in the new message
|
|
MSG_WriteDeltaEntity (msg, oldent, NULL, qtrue );
|
|
oldindex++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
MSG_WriteBits( msg, (MAX_GENTITIES-1), GENTITYNUM_BITS ); // end of packetentities
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_WriteSnapshot
|
|
====================
|
|
*/
|
|
static void CL_WriteSnapshot( void ) {
|
|
|
|
static clSnapshot_t saved_snap;
|
|
static entityState_t saved_ents[ MAX_SNAPSHOT_ENTITIES ];
|
|
|
|
clSnapshot_t *snap, *oldSnap;
|
|
byte bufData[ MAX_MSGLEN_BUF ];
|
|
msg_t msg;
|
|
int i, len;
|
|
|
|
snap = &cl.snapshots[ cl.snap.messageNum & PACKET_MASK ]; // current snapshot
|
|
//if ( !snap->valid ) // should never happen?
|
|
// return;
|
|
|
|
if ( clc.demoDeltaNum == 0 ) {
|
|
oldSnap = NULL;
|
|
} else {
|
|
oldSnap = &saved_snap;
|
|
}
|
|
|
|
MSG_Init( &msg, bufData, MAX_MSGLEN );
|
|
MSG_Bitstream( &msg );
|
|
|
|
// NOTE, MRE: all server->client messages now acknowledge
|
|
MSG_WriteLong( &msg, clc.reliableSequence );
|
|
|
|
// Write all pending server commands
|
|
CL_WriteServerCommands( &msg );
|
|
|
|
MSG_WriteByte( &msg, svc_snapshot );
|
|
MSG_WriteLong( &msg, snap->serverTime ); // sv.time
|
|
MSG_WriteByte( &msg, clc.demoDeltaNum ); // 0 or 1
|
|
MSG_WriteByte( &msg, snap->snapFlags ); // snapFlags
|
|
MSG_WriteByte( &msg, snap->areabytes ); // areabytes
|
|
MSG_WriteData( &msg, snap->areamask, snap->areabytes );
|
|
if ( oldSnap )
|
|
MSG_WriteDeltaPlayerstate( &msg, &oldSnap->ps, &snap->ps );
|
|
else
|
|
MSG_WriteDeltaPlayerstate( &msg, NULL, &snap->ps );
|
|
|
|
CL_EmitPacketEntities( oldSnap, snap, &msg, saved_ents );
|
|
|
|
// finished writing the client packet
|
|
MSG_WriteByte( &msg, svc_EOF );
|
|
|
|
// write it to the demo file
|
|
if ( clc.demoplaying )
|
|
len = LittleLong( clc.demoMessageSequence );
|
|
else
|
|
len = LittleLong( clc.serverMessageSequence );
|
|
FS_Write( &len, 4, clc.recordfile );
|
|
|
|
len = LittleLong( msg.cursize );
|
|
FS_Write( &len, 4, clc.recordfile );
|
|
FS_Write( msg.data, msg.cursize, clc.recordfile );
|
|
|
|
// save last sent state so if there any need - we can skip any further incoming messages
|
|
for ( i = 0; i < snap->numEntities; i++ )
|
|
saved_ents[ i ] = cl.parseEntities[ (snap->parseEntitiesNum + i) % MAX_PARSE_ENTITIES ];
|
|
|
|
saved_snap = *snap;
|
|
saved_snap.parseEntitiesNum = 0;
|
|
|
|
clc.demoMessageSequence++;
|
|
clc.demoDeltaNum = 1;
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_Record_f
|
|
|
|
record <demoname>
|
|
|
|
Begins recording a demo from the current position
|
|
====================
|
|
*/
|
|
static void CL_Record_f( void ) {
|
|
char demoName[MAX_OSPATH];
|
|
char name[MAX_OSPATH];
|
|
char demoExt[16];
|
|
const char *ext;
|
|
qtime_t t;
|
|
|
|
if ( Cmd_Argc() > 2 ) {
|
|
Com_Printf( "record <demoname>\n" );
|
|
return;
|
|
}
|
|
|
|
if ( clc.demorecording ) {
|
|
if ( !clc.spDemoRecording ) {
|
|
Com_Printf( "Already recording.\n" );
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( cls.state != CA_ACTIVE ) {
|
|
Com_Printf( "You must be in a level to record.\n" );
|
|
return;
|
|
}
|
|
|
|
// sync 0 doesn't prevent recording, so not forcing it off .. everyone does g_sync 1 ; record ; g_sync 0 ..
|
|
if ( NET_IsLocalAddress( &clc.serverAddress ) && !Cvar_VariableIntegerValue( "g_synchronousClients" ) ) {
|
|
Com_Printf (S_COLOR_YELLOW "WARNING: You should set 'g_synchronousClients 1' for smoother demo recording\n");
|
|
}
|
|
|
|
if ( Cmd_Argc() == 2 ) {
|
|
// explicit demo name specified
|
|
Q_strncpyz( demoName, Cmd_Argv( 1 ), sizeof( demoName ) );
|
|
ext = COM_GetExtension( demoName );
|
|
if ( *ext ) {
|
|
// strip demo extension
|
|
sprintf( demoExt, "%s%d", DEMOEXT, OLD_PROTOCOL_VERSION );
|
|
if ( Q_stricmp( ext, demoExt ) == 0 ) {
|
|
*(strrchr( demoName, '.' )) = '\0';
|
|
} else {
|
|
// check both protocols
|
|
sprintf( demoExt, "%s%d", DEMOEXT, NEW_PROTOCOL_VERSION );
|
|
if ( Q_stricmp( ext, demoExt ) == 0 ) {
|
|
*(strrchr( demoName, '.' )) = '\0';
|
|
}
|
|
}
|
|
}
|
|
Com_sprintf( name, sizeof( name ), "demos/%s", demoName );
|
|
|
|
clc.explicitRecordName = qtrue;
|
|
} else {
|
|
|
|
Com_RealTime( &t );
|
|
Com_sprintf( name, sizeof( name ), "demos/demo-%04d%02d%02d-%02d%02d%02d",
|
|
1900 + t.tm_year, 1 + t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec );
|
|
|
|
clc.explicitRecordName = qfalse;
|
|
}
|
|
|
|
// save desired filename without extension
|
|
Q_strncpyz( clc.recordName, name, sizeof( clc.recordName ) );
|
|
|
|
Com_Printf( "recording to %s.\n", name );
|
|
|
|
// start new record with temporary extension
|
|
Q_strcat( name, sizeof( name ), ".tmp" );
|
|
|
|
// open the demo file
|
|
clc.recordfile = FS_FOpenFileWrite( name );
|
|
if ( clc.recordfile == FS_INVALID_HANDLE ) {
|
|
Com_Printf( "ERROR: couldn't open.\n" );
|
|
clc.recordName[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
clc.demorecording = qtrue;
|
|
|
|
Com_TruncateLongString( clc.recordNameShort, clc.recordName );
|
|
|
|
if ( Cvar_VariableIntegerValue( "ui_recordSPDemo" ) ) {
|
|
clc.spDemoRecording = qtrue;
|
|
} else {
|
|
clc.spDemoRecording = qfalse;
|
|
}
|
|
|
|
// don't start saving messages until a non-delta compressed message is received
|
|
clc.demowaiting = qtrue;
|
|
|
|
// we will rename record to dm_68 or dm_71 depending from this flag
|
|
clc.dm68compat = qtrue;
|
|
|
|
// write out the gamestate message
|
|
CL_WriteGamestate( qtrue );
|
|
|
|
// the rest of the demo file will be copied from net messages
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_CompleteRecordName
|
|
====================
|
|
*/
|
|
static void CL_CompleteRecordName(const char *args, int argNum )
|
|
{
|
|
if ( argNum == 2 )
|
|
{
|
|
char demoExt[ 16 ];
|
|
|
|
Com_sprintf( demoExt, sizeof( demoExt ), ".%s%d", DEMOEXT, com_protocol->integer );
|
|
Field_CompleteFilename( "demos", demoExt, qtrue, FS_MATCH_EXTERN | FS_MATCH_STICK );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=======================================================================
|
|
|
|
CLIENT SIDE DEMO PLAYBACK
|
|
|
|
=======================================================================
|
|
*/
|
|
|
|
/*
|
|
=================
|
|
CL_DemoCompleted
|
|
=================
|
|
*/
|
|
static void CL_DemoCompleted( void ) {
|
|
if ( com_timedemo->integer ) {
|
|
int time;
|
|
|
|
time = Sys_Milliseconds() - clc.timeDemoStart;
|
|
if ( time > 0 ) {
|
|
Com_Printf( "%i frames, %3.*f seconds: %3.1f fps\n", clc.timeDemoFrames,
|
|
time > 10000 ? 1 : 2, time/1000.0, clc.timeDemoFrames*1000.0 / time );
|
|
}
|
|
}
|
|
|
|
CL_Disconnect( qtrue );
|
|
CL_NextDemo();
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_ReadDemoMessage
|
|
=================
|
|
*/
|
|
void CL_ReadDemoMessage( void ) {
|
|
int r;
|
|
msg_t buf;
|
|
byte bufData[ MAX_MSGLEN_BUF ];
|
|
int s;
|
|
|
|
if ( clc.demofile == FS_INVALID_HANDLE ) {
|
|
CL_DemoCompleted();
|
|
return;
|
|
}
|
|
|
|
// get the sequence number
|
|
r = FS_Read( &s, 4, clc.demofile );
|
|
if ( r != 4 ) {
|
|
CL_DemoCompleted();
|
|
return;
|
|
}
|
|
clc.serverMessageSequence = LittleLong( s );
|
|
|
|
// init the message
|
|
MSG_Init( &buf, bufData, MAX_MSGLEN );
|
|
|
|
// get the length
|
|
r = FS_Read( &buf.cursize, 4, clc.demofile );
|
|
if ( r != 4 ) {
|
|
CL_DemoCompleted();
|
|
return;
|
|
}
|
|
buf.cursize = LittleLong( buf.cursize );
|
|
if ( buf.cursize == -1 ) {
|
|
CL_DemoCompleted();
|
|
return;
|
|
}
|
|
if ( buf.cursize > buf.maxsize ) {
|
|
Com_Error (ERR_DROP, "CL_ReadDemoMessage: demoMsglen > MAX_MSGLEN");
|
|
}
|
|
r = FS_Read( buf.data, buf.cursize, clc.demofile );
|
|
if ( r != buf.cursize ) {
|
|
Com_Printf( "Demo file was truncated.\n");
|
|
CL_DemoCompleted();
|
|
return;
|
|
}
|
|
|
|
clc.lastPacketTime = cls.realtime;
|
|
buf.readcount = 0;
|
|
|
|
clc.demoCommandSequence = clc.serverCommandSequence;
|
|
|
|
CL_ParseServerMessage( &buf );
|
|
|
|
if ( clc.demorecording ) {
|
|
// track changes and write new message
|
|
if ( clc.eventMask & EM_GAMESTATE ) {
|
|
CL_WriteGamestate( qfalse );
|
|
// nothing should came after gamestate in current message
|
|
} else if ( clc.eventMask & (EM_SNAPSHOT|EM_COMMAND) ) {
|
|
CL_WriteSnapshot();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_WalkDemoExt
|
|
====================
|
|
*/
|
|
static int CL_WalkDemoExt( const char *arg, char *name, int name_len, fileHandle_t *handle )
|
|
{
|
|
int i;
|
|
|
|
*handle = FS_INVALID_HANDLE;
|
|
i = 0;
|
|
|
|
while ( demo_protocols[ i ] )
|
|
{
|
|
Com_sprintf( name, name_len, "demos/%s.%s%d", arg, DEMOEXT, demo_protocols[ i ] );
|
|
FS_BypassPure();
|
|
FS_FOpenFileRead( name, handle, qtrue );
|
|
FS_RestorePure();
|
|
if ( *handle != FS_INVALID_HANDLE )
|
|
{
|
|
Com_Printf( "Demo file: %s\n", name );
|
|
return demo_protocols[ i ];
|
|
}
|
|
else
|
|
Com_Printf( "Not found: %s\n", name );
|
|
i++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_DemoExtCallback
|
|
====================
|
|
*/
|
|
static qboolean CL_DemoNameCallback_f( const char *filename, int length )
|
|
{
|
|
const int ext_len = strlen( "." DEMOEXT );
|
|
const int num_len = 2;
|
|
int version;
|
|
|
|
if ( length <= ext_len + num_len || Q_stricmpn( filename + length - (ext_len + num_len), "." DEMOEXT, ext_len ) != 0 )
|
|
return qfalse;
|
|
|
|
version = atoi( filename + length - num_len );
|
|
if ( version == com_protocol->integer )
|
|
return qtrue;
|
|
|
|
if ( version < 66 || version > NEW_PROTOCOL_VERSION )
|
|
return qfalse;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_CompleteDemoName
|
|
====================
|
|
*/
|
|
static void CL_CompleteDemoName(const char *args, int argNum )
|
|
{
|
|
if ( argNum == 2 )
|
|
{
|
|
FS_SetFilenameCallback( CL_DemoNameCallback_f );
|
|
Field_CompleteFilename( "demos", "." DEMOEXT "??", qfalse, FS_MATCH_ANY | FS_MATCH_STICK );
|
|
FS_SetFilenameCallback( NULL );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_PlayDemo_f
|
|
|
|
demo <demoname>
|
|
|
|
====================
|
|
*/
|
|
static void CL_PlayDemo_f( void ) {
|
|
char name[MAX_OSPATH];
|
|
const char *arg;
|
|
char *ext_test;
|
|
int protocol, i;
|
|
char retry[MAX_OSPATH];
|
|
const char *shortname, *slash;
|
|
fileHandle_t hFile;
|
|
|
|
if ( Cmd_Argc() != 2 ) {
|
|
Com_Printf( "demo <demoname>\n" );
|
|
return;
|
|
}
|
|
|
|
// open the demo file
|
|
arg = Cmd_Argv( 1 );
|
|
|
|
// check for an extension .DEMOEXT_?? (?? is protocol)
|
|
ext_test = strrchr(arg, '.');
|
|
if ( ext_test && !Q_stricmpn(ext_test + 1, DEMOEXT, ARRAY_LEN(DEMOEXT) - 1) )
|
|
{
|
|
protocol = atoi(ext_test + ARRAY_LEN(DEMOEXT));
|
|
|
|
for( i = 0; demo_protocols[ i ]; i++ )
|
|
{
|
|
if ( demo_protocols[ i ] == protocol )
|
|
break;
|
|
}
|
|
|
|
if ( demo_protocols[ i ] || protocol == com_protocol->integer )
|
|
{
|
|
Com_sprintf(name, sizeof(name), "demos/%s", arg);
|
|
FS_BypassPure();
|
|
FS_FOpenFileRead( name, &hFile, qtrue );
|
|
FS_RestorePure();
|
|
}
|
|
else
|
|
{
|
|
size_t len;
|
|
|
|
Com_Printf("Protocol %d not supported for demos\n", protocol );
|
|
len = ext_test - arg;
|
|
|
|
if ( len > ARRAY_LEN( retry ) - 1 ) {
|
|
len = ARRAY_LEN( retry ) - 1;
|
|
}
|
|
|
|
Q_strncpyz( retry, arg, len + 1);
|
|
retry[len] = '\0';
|
|
protocol = CL_WalkDemoExt( retry, name, sizeof( name ), &hFile );
|
|
}
|
|
}
|
|
else
|
|
protocol = CL_WalkDemoExt( arg, name, sizeof( name ), &hFile );
|
|
|
|
if ( hFile == FS_INVALID_HANDLE ) {
|
|
Com_Printf( S_COLOR_YELLOW "couldn't open %s\n", name );
|
|
return;
|
|
}
|
|
|
|
FS_FCloseFile( hFile );
|
|
hFile = FS_INVALID_HANDLE;
|
|
|
|
// make sure a local server is killed
|
|
// 2 means don't force disconnect of local client
|
|
Cvar_Set( "sv_killserver", "2" );
|
|
|
|
CL_Disconnect( qtrue );
|
|
|
|
// clc.demofile will be closed during CL_Disconnect so reopen it
|
|
if ( FS_FOpenFileRead( name, &clc.demofile, qtrue ) == -1 )
|
|
{
|
|
// drop this time
|
|
Com_Error( ERR_DROP, "couldn't open %s\n", name );
|
|
return;
|
|
}
|
|
|
|
if ( (slash = strrchr( name, '/' )) != NULL )
|
|
shortname = slash + 1;
|
|
else
|
|
shortname = name;
|
|
|
|
Q_strncpyz( clc.demoName, shortname, sizeof( clc.demoName ) );
|
|
|
|
Con_Close();
|
|
|
|
cls.state = CA_CONNECTED;
|
|
clc.demoplaying = qtrue;
|
|
Q_strncpyz( cls.servername, shortname, sizeof( cls.servername ) );
|
|
|
|
if ( protocol <= OLD_PROTOCOL_VERSION )
|
|
clc.compat = qtrue;
|
|
else
|
|
clc.compat = qfalse;
|
|
|
|
// read demo messages until connected
|
|
#ifdef USE_CURL
|
|
while ( cls.state >= CA_CONNECTED && cls.state < CA_PRIMED && !Com_DL_InProgress( &download ) ) {
|
|
#else
|
|
while ( cls.state >= CA_CONNECTED && cls.state < CA_PRIMED ) {
|
|
#endif
|
|
CL_ReadDemoMessage();
|
|
}
|
|
|
|
// don't get the first snapshot this frame, to prevent the long
|
|
// time from the gamestate load from messing causing a time skip
|
|
clc.firstDemoFrameSkipped = qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_NextDemo
|
|
|
|
Called when a demo or cinematic finishes
|
|
If the "nextdemo" cvar is set, that command will be issued
|
|
==================
|
|
*/
|
|
static void CL_NextDemo( void ) {
|
|
char v[ MAX_CVAR_VALUE_STRING ];
|
|
|
|
Cvar_VariableStringBuffer( "nextdemo", v, sizeof( v ) );
|
|
Com_DPrintf( "CL_NextDemo: %s\n", v );
|
|
if ( !v[0] ) {
|
|
return;
|
|
}
|
|
|
|
Cvar_Set( "nextdemo", "" );
|
|
Cbuf_AddText( v );
|
|
Cbuf_AddText( "\n" );
|
|
Cbuf_Execute();
|
|
}
|
|
|
|
|
|
//======================================================================
|
|
|
|
/*
|
|
=====================
|
|
CL_ShutdownVMs
|
|
=====================
|
|
*/
|
|
static void CL_ShutdownVMs( void )
|
|
{
|
|
CL_ShutdownCGame();
|
|
CL_ShutdownUI();
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
Called by Com_GameRestart, CL_FlushMemory and SV_SpawnServer
|
|
|
|
CL_ShutdownAll
|
|
=====================
|
|
*/
|
|
void CL_ShutdownAll( void ) {
|
|
|
|
#ifdef USE_CURL
|
|
CL_cURL_Shutdown();
|
|
#endif
|
|
|
|
// clear and mute all sounds until next registration
|
|
S_DisableSounds();
|
|
|
|
// shutdown VMs
|
|
CL_ShutdownVMs();
|
|
|
|
// shutdown the renderer
|
|
if ( re.Shutdown ) {
|
|
if ( CL_GameSwitch() ) {
|
|
CL_ShutdownRef( REF_DESTROY_WINDOW ); // shutdown renderer & GLimp
|
|
} else {
|
|
re.Shutdown( REF_KEEP_CONTEXT ); // don't destroy window or context
|
|
}
|
|
}
|
|
|
|
cls.rendererStarted = qfalse;
|
|
cls.soundRegistered = qfalse;
|
|
|
|
SCR_Done();
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_ClearMemory
|
|
=================
|
|
*/
|
|
void CL_ClearMemory( void ) {
|
|
// if not running a server clear the whole hunk
|
|
if ( !com_sv_running->integer ) {
|
|
// clear the whole hunk
|
|
Hunk_Clear();
|
|
// clear collision map data
|
|
CM_ClearMap();
|
|
} else {
|
|
// clear all the client data on the hunk
|
|
Hunk_ClearToMark();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_FlushMemory
|
|
|
|
Called by CL_Disconnect_f, CL_DownloadsComplete
|
|
Also called by Com_Error
|
|
=================
|
|
*/
|
|
void CL_FlushMemory( void ) {
|
|
|
|
// shutdown all the client stuff
|
|
CL_ShutdownAll();
|
|
|
|
CL_ClearMemory();
|
|
|
|
CL_StartHunkUsers();
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
CL_MapLoading
|
|
|
|
A local server is starting to load a map, so update the
|
|
screen to let the user know about it, then dump all client
|
|
memory on the hunk from cgame, ui, and renderer
|
|
=====================
|
|
*/
|
|
void CL_MapLoading( void ) {
|
|
if ( com_dedicated->integer ) {
|
|
cls.state = CA_DISCONNECTED;
|
|
Key_SetCatcher( KEYCATCH_CONSOLE );
|
|
return;
|
|
}
|
|
|
|
if ( !com_cl_running->integer ) {
|
|
return;
|
|
}
|
|
|
|
Con_Close();
|
|
Key_SetCatcher( 0 );
|
|
|
|
// if we are already connected to the local host, stay connected
|
|
if ( cls.state >= CA_CONNECTED && !Q_stricmp( cls.servername, "localhost" ) ) {
|
|
cls.state = CA_CONNECTED; // so the connect screen is drawn
|
|
Com_Memset( cls.updateInfoString, 0, sizeof( cls.updateInfoString ) );
|
|
Com_Memset( clc.serverMessage, 0, sizeof( clc.serverMessage ) );
|
|
Com_Memset( &cl.gameState, 0, sizeof( cl.gameState ) );
|
|
clc.lastPacketSentTime = -9999;
|
|
cls.framecount++;
|
|
SCR_UpdateScreen();
|
|
} else {
|
|
// clear nextmap so the cinematic shutdown doesn't execute it
|
|
Cvar_Set( "nextmap", "" );
|
|
CL_Disconnect( qtrue );
|
|
Q_strncpyz( cls.servername, "localhost", sizeof(cls.servername) );
|
|
cls.state = CA_CHALLENGING; // so the connect screen is drawn
|
|
Key_SetCatcher( 0 );
|
|
cls.framecount++;
|
|
SCR_UpdateScreen();
|
|
clc.connectTime = -RETRANSMIT_TIMEOUT;
|
|
NET_StringToAdr( cls.servername, &clc.serverAddress, NA_UNSPEC );
|
|
// we don't need a challenge on the localhost
|
|
CL_CheckForResend();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
CL_ClearState
|
|
|
|
Called before parsing a gamestate
|
|
=====================
|
|
*/
|
|
void CL_ClearState( void ) {
|
|
|
|
// S_StopAllSounds();
|
|
|
|
Com_Memset( &cl, 0, sizeof( cl ) );
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_UpdateGUID
|
|
|
|
update cl_guid using QKEY_FILE and optional prefix
|
|
====================
|
|
*/
|
|
static void CL_UpdateGUID( const char *prefix, int prefix_len )
|
|
{
|
|
#ifdef USE_Q3KEY
|
|
fileHandle_t f;
|
|
int len;
|
|
|
|
len = FS_SV_FOpenFileRead( QKEY_FILE, &f );
|
|
FS_FCloseFile( f );
|
|
|
|
if( len != QKEY_SIZE )
|
|
Cvar_Set( "cl_guid", "" );
|
|
else
|
|
Cvar_Set( "cl_guid", Com_MD5File( QKEY_FILE, QKEY_SIZE,
|
|
prefix, prefix_len ) );
|
|
#else
|
|
Cvar_Set( "cl_guid", Com_MD5Buf( &cl_cdkey[0], sizeof(cl_cdkey), prefix, prefix_len));
|
|
#endif
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
CL_ResetOldGame
|
|
=====================
|
|
*/
|
|
void CL_ResetOldGame( void )
|
|
{
|
|
cl_oldGameSet = qfalse;
|
|
cl_oldGame[0] = '\0';
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
CL_RestoreOldGame
|
|
|
|
change back to previous fs_game
|
|
=====================
|
|
*/
|
|
static qboolean CL_RestoreOldGame( void )
|
|
{
|
|
if ( cl_oldGameSet )
|
|
{
|
|
cl_oldGameSet = qfalse;
|
|
Cvar_Set( "fs_game", cl_oldGame );
|
|
FS_ConditionalRestart( clc.checksumFeed, qtrue );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
CL_Disconnect
|
|
|
|
Called when a connection, demo, or cinematic is being terminated.
|
|
Goes from a connected state to either a menu state or a console state
|
|
Sends a disconnect message to the server
|
|
This is also called on Com_Error and Com_Quit, so it shouldn't cause any errors
|
|
=====================
|
|
*/
|
|
qboolean CL_Disconnect( qboolean showMainMenu ) {
|
|
static qboolean cl_disconnecting = qfalse;
|
|
qboolean cl_restarted = qfalse;
|
|
|
|
if ( !com_cl_running || !com_cl_running->integer ) {
|
|
return cl_restarted;
|
|
}
|
|
|
|
if ( cl_disconnecting ) {
|
|
return cl_restarted;
|
|
}
|
|
|
|
cl_disconnecting = qtrue;
|
|
|
|
// Stop demo recording
|
|
if ( clc.demorecording ) {
|
|
CL_StopRecord_f();
|
|
}
|
|
|
|
// Stop demo playback
|
|
if ( clc.demofile != FS_INVALID_HANDLE ) {
|
|
FS_FCloseFile( clc.demofile );
|
|
clc.demofile = FS_INVALID_HANDLE;
|
|
}
|
|
|
|
// Finish downloads
|
|
if ( clc.download != FS_INVALID_HANDLE ) {
|
|
FS_FCloseFile( clc.download );
|
|
clc.download = FS_INVALID_HANDLE;
|
|
}
|
|
*clc.downloadTempName = *clc.downloadName = '\0';
|
|
Cvar_Set( "cl_downloadName", "" );
|
|
|
|
// Stop recording any video
|
|
if ( CL_VideoRecording() ) {
|
|
// Finish rendering current frame
|
|
cls.framecount++;
|
|
SCR_UpdateScreen();
|
|
CL_CloseAVI( qfalse );
|
|
}
|
|
|
|
if ( cgvm ) {
|
|
// do that right after we rendered last video frame
|
|
CL_ShutdownCGame();
|
|
}
|
|
|
|
SCR_StopCinematic();
|
|
S_StopAllSounds();
|
|
Key_ClearStates();
|
|
|
|
if ( uivm && showMainMenu ) {
|
|
VM_Call( uivm, 1, UI_SET_ACTIVE_MENU, UIMENU_NONE );
|
|
}
|
|
|
|
// Remove pure paks
|
|
FS_PureServerSetLoadedPaks( "", "" );
|
|
FS_PureServerSetReferencedPaks( "", "" );
|
|
|
|
FS_ClearPakReferences( FS_GENERAL_REF | FS_UI_REF | FS_CGAME_REF );
|
|
|
|
if ( CL_GameSwitch() ) {
|
|
// keep current gamestate and connection
|
|
cl_disconnecting = qfalse;
|
|
return qfalse;
|
|
}
|
|
|
|
// send a disconnect message to the server
|
|
// send it a few times in case one is dropped
|
|
if ( cls.state >= CA_CONNECTED && cls.state != CA_CINEMATIC && !clc.demoplaying ) {
|
|
CL_AddReliableCommand( "disconnect", qtrue );
|
|
CL_WritePacket();
|
|
CL_WritePacket();
|
|
CL_WritePacket();
|
|
}
|
|
|
|
CL_ClearState();
|
|
|
|
// wipe the client connection
|
|
Com_Memset( &clc, 0, sizeof( clc ) );
|
|
|
|
cls.state = CA_DISCONNECTED;
|
|
|
|
// allow cheats locally
|
|
Cvar_Set( "sv_cheats", "1" );
|
|
|
|
// not connected to a pure server anymore
|
|
cl_connectedToPureServer = 0;
|
|
|
|
CL_UpdateGUID( NULL, 0 );
|
|
|
|
// Cmd_RemoveCommand( "callvote" );
|
|
Cmd_RemoveCgameCommands();
|
|
|
|
if ( noGameRestart )
|
|
noGameRestart = qfalse;
|
|
else
|
|
cl_restarted = CL_RestoreOldGame();
|
|
|
|
cl_disconnecting = qfalse;
|
|
|
|
return cl_restarted;
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_ForwardCommandToServer
|
|
|
|
adds the current command line as a clientCommand
|
|
things like godmode, noclip, etc, are commands directed to the server,
|
|
so when they are typed in at the console, they will need to be forwarded.
|
|
===================
|
|
*/
|
|
void CL_ForwardCommandToServer( const char *string ) {
|
|
const char *cmd;
|
|
|
|
cmd = Cmd_Argv( 0 );
|
|
|
|
// ignore key up commands
|
|
if ( cmd[0] == '-' ) {
|
|
return;
|
|
}
|
|
|
|
// no userinfo updates from command line
|
|
if ( !strcmp( cmd, "userinfo" ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( clc.demoplaying || cls.state < CA_CONNECTED || cmd[0] == '+' ) {
|
|
Com_Printf( "Unknown command \"%s" S_COLOR_WHITE "\"\n", cmd );
|
|
return;
|
|
}
|
|
|
|
if ( Cmd_Argc() > 1 ) {
|
|
CL_AddReliableCommand( string, qfalse );
|
|
} else {
|
|
CL_AddReliableCommand( cmd, qfalse );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_RequestMotd
|
|
|
|
===================
|
|
*/
|
|
#if 0
|
|
static void CL_RequestMotd( void ) {
|
|
char info[MAX_INFO_STRING];
|
|
|
|
if ( !cl_motd->integer ) {
|
|
return;
|
|
}
|
|
Com_Printf( "Resolving %s\n", UPDATE_SERVER_NAME );
|
|
if ( !NET_StringToAdr( UPDATE_SERVER_NAME, &cls.updateServer, NA_IP ) ) {
|
|
Com_Printf( "Couldn't resolve address\n" );
|
|
return;
|
|
}
|
|
cls.updateServer.port = BigShort( PORT_UPDATE );
|
|
Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", UPDATE_SERVER_NAME,
|
|
cls.updateServer.ip[0], cls.updateServer.ip[1],
|
|
cls.updateServer.ip[2], cls.updateServer.ip[3],
|
|
BigShort( cls.updateServer.port ) );
|
|
|
|
info[0] = 0;
|
|
// NOTE TTimo xoring against Com_Milliseconds, otherwise we may not have a true randomization
|
|
// only srand I could catch before here is tr_noise.c l:26 srand(1001)
|
|
// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=382
|
|
// NOTE: the Com_Milliseconds xoring only affects the lower 16-bit word,
|
|
// but I decided it was enough randomization
|
|
Com_sprintf( cls.updateChallenge, sizeof( cls.updateChallenge ), "%i", ((rand() << 16) ^ rand()) ^ Com_Milliseconds());
|
|
|
|
Info_SetValueForKey( info, "challenge", cls.updateChallenge );
|
|
Info_SetValueForKey( info, "renderer", cls.glconfig.renderer_string );
|
|
Info_SetValueForKey( info, "version", com_version->string );
|
|
|
|
NET_OutOfBandPrint( NS_CLIENT, &cls.updateServer, "getmotd \"%s\"\n", info );
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
===================
|
|
CL_RequestAuthorization
|
|
|
|
Authorization server protocol
|
|
-----------------------------
|
|
|
|
All commands are text in Q3 out of band packets (leading 0xff 0xff 0xff 0xff).
|
|
|
|
Whenever the client tries to get a challenge from the server it wants to
|
|
connect to, it also blindly fires off a packet to the authorize server:
|
|
|
|
getKeyAuthorize <challenge> <cdkey>
|
|
|
|
cdkey may be "demo"
|
|
|
|
|
|
#OLD The authorize server returns a:
|
|
#OLD
|
|
#OLD keyAthorize <challenge> <accept | deny>
|
|
#OLD
|
|
#OLD A client will be accepted if the cdkey is valid and it has not been used by any other IP
|
|
#OLD address in the last 15 minutes.
|
|
|
|
|
|
The server sends a:
|
|
|
|
getIpAuthorize <challenge> <ip>
|
|
|
|
The authorize server returns a:
|
|
|
|
ipAuthorize <challenge> <accept | deny | demo | unknown >
|
|
|
|
A client will be accepted if a valid cdkey was sent by that ip (only) in the last 15 minutes.
|
|
If no response is received from the authorize server after two tries, the client will be let
|
|
in anyway.
|
|
===================
|
|
*/
|
|
#ifndef STANDALONE
|
|
static void CL_RequestAuthorization( void ) {
|
|
char nums[64];
|
|
int i, j, l;
|
|
cvar_t *fs;
|
|
|
|
if ( !cls.authorizeServer.port ) {
|
|
Com_Printf( "Resolving %s\n", AUTHORIZE_SERVER_NAME );
|
|
if ( !NET_StringToAdr( AUTHORIZE_SERVER_NAME, &cls.authorizeServer, NA_IP ) ) {
|
|
Com_Printf( "Couldn't resolve address\n" );
|
|
return;
|
|
}
|
|
|
|
cls.authorizeServer.port = BigShort( PORT_AUTHORIZE );
|
|
Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", AUTHORIZE_SERVER_NAME,
|
|
cls.authorizeServer.ipv._4[0], cls.authorizeServer.ipv._4[1],
|
|
cls.authorizeServer.ipv._4[2], cls.authorizeServer.ipv._4[3],
|
|
BigShort( cls.authorizeServer.port ) );
|
|
}
|
|
if ( cls.authorizeServer.type == NA_BAD ) {
|
|
return;
|
|
}
|
|
|
|
// only grab the alphanumeric values from the cdkey, to avoid any dashes or spaces
|
|
j = 0;
|
|
l = strlen( cl_cdkey );
|
|
if ( l > 32 ) {
|
|
l = 32;
|
|
}
|
|
for ( i = 0 ; i < l ; i++ ) {
|
|
if ( ( cl_cdkey[i] >= '0' && cl_cdkey[i] <= '9' )
|
|
|| ( cl_cdkey[i] >= 'a' && cl_cdkey[i] <= 'z' )
|
|
|| ( cl_cdkey[i] >= 'A' && cl_cdkey[i] <= 'Z' )
|
|
) {
|
|
nums[j] = cl_cdkey[i];
|
|
j++;
|
|
}
|
|
}
|
|
nums[j] = 0;
|
|
|
|
fs = Cvar_Get( "cl_anonymous", "0", CVAR_INIT | CVAR_SYSTEMINFO );
|
|
|
|
NET_OutOfBandPrint(NS_CLIENT, &cls.authorizeServer, "getKeyAuthorize %i %s", fs->integer, nums );
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
======================================================================
|
|
|
|
CONSOLE COMMANDS
|
|
|
|
======================================================================
|
|
*/
|
|
|
|
/*
|
|
==================
|
|
CL_ForwardToServer_f
|
|
==================
|
|
*/
|
|
static void CL_ForwardToServer_f( void ) {
|
|
if ( cls.state != CA_ACTIVE || clc.demoplaying ) {
|
|
Com_Printf ("Not connected to a server.\n");
|
|
return;
|
|
}
|
|
|
|
if ( Cmd_Argc() <= 1 || strcmp( Cmd_Argv( 1 ), "userinfo" ) == 0 )
|
|
return;
|
|
|
|
// don't forward the first argument
|
|
CL_AddReliableCommand( Cmd_ArgsFrom( 1 ), qfalse );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_Disconnect_f
|
|
==================
|
|
*/
|
|
void CL_Disconnect_f( void ) {
|
|
SCR_StopCinematic();
|
|
Cvar_Set( "ui_singlePlayerActive", "0" );
|
|
if ( cls.state != CA_DISCONNECTED && cls.state != CA_CINEMATIC ) {
|
|
if ( (uivm && uivm->callLevel) || (cgvm && cgvm->callLevel) ) {
|
|
Com_Error( ERR_DISCONNECT, "Disconnected from server" );
|
|
} else {
|
|
// clear any previous "server full" type messages
|
|
clc.serverMessage[0] = '\0';
|
|
if ( com_sv_running && com_sv_running->integer ) {
|
|
// if running a local server, kill it
|
|
SV_Shutdown( "Disconnected from server" );
|
|
} else {
|
|
Com_Printf( "Disconnected from %s\n", cls.servername );
|
|
}
|
|
Cvar_Set( "com_errorMessage", "" );
|
|
if ( !CL_Disconnect( qfalse ) ) { // restart client if not done already
|
|
CL_FlushMemory();
|
|
}
|
|
if ( uivm ) {
|
|
VM_Call( uivm, 1, UI_SET_ACTIVE_MENU, UIMENU_MAIN );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
CL_Reconnect_f
|
|
================
|
|
*/
|
|
static void CL_Reconnect_f( void ) {
|
|
if ( cl_reconnectArgs->string[0] == '\0' || Q_stricmp( cl_reconnectArgs->string, "localhost" ) == 0 )
|
|
return;
|
|
Cvar_Set( "ui_singlePlayerActive", "0" );
|
|
Cbuf_AddText( va( "connect %s\n", cl_reconnectArgs->string ) );
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
CL_Connect_f
|
|
================
|
|
*/
|
|
static void CL_Connect_f( void ) {
|
|
netadrtype_t family;
|
|
netadr_t addr;
|
|
char buffer[ sizeof( cls.servername ) ]; // same length as cls.servername
|
|
char args[ sizeof( cls.servername ) + MAX_CVAR_VALUE_STRING ];
|
|
const char *server;
|
|
const char *serverString;
|
|
int len;
|
|
int argc;
|
|
|
|
argc = Cmd_Argc();
|
|
family = NA_UNSPEC;
|
|
|
|
if ( argc != 2 && argc != 3 ) {
|
|
Com_Printf( "usage: connect [-4|-6] <server>\n");
|
|
return;
|
|
}
|
|
|
|
if ( argc == 2 ) {
|
|
server = Cmd_Argv(1);
|
|
} else {
|
|
if( !strcmp( Cmd_Argv(1), "-4" ) )
|
|
family = NA_IP;
|
|
#ifdef USE_IPV6
|
|
else if( !strcmp( Cmd_Argv(1), "-6" ) )
|
|
family = NA_IP6;
|
|
else
|
|
Com_Printf( S_COLOR_YELLOW "warning: only -4 or -6 as address type understood.\n" );
|
|
#else
|
|
Com_Printf( S_COLOR_YELLOW "warning: only -4 as address type understood.\n" );
|
|
#endif
|
|
server = Cmd_Argv(2);
|
|
}
|
|
|
|
Q_strncpyz( buffer, server, sizeof( buffer ) );
|
|
server = buffer;
|
|
|
|
// skip leading "q3a:/" in connection string
|
|
if ( !Q_stricmpn( server, "q3a:/", 5 ) ) {
|
|
server += 5;
|
|
}
|
|
|
|
// skip all slash prefixes
|
|
while ( *server == '/' ) {
|
|
server++;
|
|
}
|
|
|
|
len = strlen( server );
|
|
if ( len <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
// some programs may add ending slash
|
|
if ( buffer[len-1] == '/' ) {
|
|
buffer[len-1] = '\0';
|
|
}
|
|
|
|
if ( !*server ) {
|
|
return;
|
|
}
|
|
|
|
// try resolve remote server first
|
|
if ( !NET_StringToAdr( server, &addr, family ) ) {
|
|
Com_Printf( S_COLOR_YELLOW "Bad server address - %s\n", server );
|
|
return;
|
|
}
|
|
|
|
// save arguments for reconnect
|
|
Q_strncpyz( args, Cmd_ArgsFrom( 1 ), sizeof( args ) );
|
|
|
|
Cvar_Set( "ui_singlePlayerActive", "0" );
|
|
|
|
// clear any previous "server full" type messages
|
|
clc.serverMessage[0] = '\0';
|
|
|
|
// if running a local server, kill it
|
|
if ( com_sv_running->integer && !strcmp( server, "localhost" ) ) {
|
|
SV_Shutdown( "Server quit" );
|
|
}
|
|
|
|
// make sure a local server is killed
|
|
Cvar_Set( "sv_killserver", "1" );
|
|
SV_Frame( 0 );
|
|
|
|
noGameRestart = qtrue;
|
|
CL_Disconnect( qtrue );
|
|
Con_Close();
|
|
|
|
Q_strncpyz( cls.servername, server, sizeof( cls.servername ) );
|
|
|
|
// copy resolved address
|
|
clc.serverAddress = addr;
|
|
|
|
if (clc.serverAddress.port == 0) {
|
|
clc.serverAddress.port = BigShort( PORT_SERVER );
|
|
}
|
|
|
|
serverString = NET_AdrToStringwPort( &clc.serverAddress );
|
|
|
|
Com_Printf( "%s resolved to %s\n", cls.servername, serverString );
|
|
|
|
if ( cl_guidServerUniq->integer )
|
|
CL_UpdateGUID( serverString, strlen( serverString ) );
|
|
else
|
|
CL_UpdateGUID( NULL, 0 );
|
|
|
|
// if we aren't playing on a lan, we need to authenticate
|
|
// with the cd key
|
|
if ( NET_IsLocalAddress( &clc.serverAddress ) ) {
|
|
cls.state = CA_CHALLENGING;
|
|
} else {
|
|
cls.state = CA_CONNECTING;
|
|
|
|
// Set a client challenge number that ideally is mirrored back by the server.
|
|
//clc.challenge = ((rand() << 16) ^ rand()) ^ Com_Milliseconds();
|
|
Com_RandomBytes( (byte*)&clc.challenge, sizeof( clc.challenge ) );
|
|
}
|
|
|
|
Key_SetCatcher( 0 );
|
|
clc.connectTime = -99999; // CL_CheckForResend() will fire immediately
|
|
clc.connectPacketCount = 0;
|
|
|
|
Cvar_Set( "cl_reconnectArgs", args );
|
|
|
|
// server connection string
|
|
Cvar_Set( "cl_currentServerAddress", server );
|
|
}
|
|
|
|
#define MAX_RCON_MESSAGE (MAX_STRING_CHARS+4)
|
|
|
|
/*
|
|
==================
|
|
CL_CompleteRcon
|
|
==================
|
|
*/
|
|
static void CL_CompleteRcon(const char *args, int argNum )
|
|
{
|
|
if ( argNum >= 2 )
|
|
{
|
|
// Skip "rcon "
|
|
const char *p = Com_SkipTokens( args, 1, " " );
|
|
|
|
if ( p > args )
|
|
Field_CompleteCommand( p, qtrue, qtrue );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
CL_Rcon_f
|
|
|
|
Send the rest of the command line over as
|
|
an unconnected command.
|
|
=====================
|
|
*/
|
|
static void CL_Rcon_f( void ) {
|
|
char message[MAX_RCON_MESSAGE];
|
|
const char *sp;
|
|
int len;
|
|
|
|
if ( !rcon_client_password->string[0] ) {
|
|
Com_Printf( "You must set 'rconpassword' before\n"
|
|
"issuing an rcon command.\n" );
|
|
return;
|
|
}
|
|
|
|
if ( cls.state >= CA_CONNECTED ) {
|
|
rcon_address = clc.netchan.remoteAddress;
|
|
} else {
|
|
if ( !rconAddress->string[0] ) {
|
|
Com_Printf( "You must either be connected,\n"
|
|
"or set the 'rconAddress' cvar\n"
|
|
"to issue rcon commands\n" );
|
|
return;
|
|
}
|
|
if ( !NET_StringToAdr( rconAddress->string, &rcon_address, NA_UNSPEC ) ) {
|
|
return;
|
|
}
|
|
if ( rcon_address.port == 0 ) {
|
|
rcon_address.port = BigShort( PORT_SERVER );
|
|
}
|
|
}
|
|
|
|
message[0] = -1;
|
|
message[1] = -1;
|
|
message[2] = -1;
|
|
message[3] = -1;
|
|
message[4] = '\0';
|
|
|
|
// we may need to quote password if it contains spaces
|
|
sp = strchr( rcon_client_password->string, ' ' );
|
|
|
|
len = Com_sprintf( message+4, sizeof( message )-4,
|
|
sp ? "rcon \"%s\" %s" : "rcon %s %s",
|
|
rcon_client_password->string,
|
|
Cmd_Cmd() + 5 ) + 4 + 1; // including OOB marker and '\0'
|
|
|
|
NET_SendPacket( NS_CLIENT, len, message, &rcon_address );
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_SendPureChecksums
|
|
=================
|
|
*/
|
|
static void CL_SendPureChecksums( void ) {
|
|
char cMsg[ MAX_STRING_CHARS-1 ];
|
|
int len;
|
|
|
|
if ( !cl_connectedToPureServer || clc.demoplaying )
|
|
return;
|
|
|
|
// if we are pure we need to send back a command with our referenced pk3 checksums
|
|
len = sprintf( cMsg, "cp %d ", cl.serverId );
|
|
strcpy( cMsg + len, FS_ReferencedPakPureChecksums( sizeof( cMsg ) - len - 1 ) );
|
|
|
|
CL_AddReliableCommand( cMsg, qfalse );
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_ResetPureClientAtServer
|
|
=================
|
|
*/
|
|
static void CL_ResetPureClientAtServer( void ) {
|
|
CL_AddReliableCommand( "vdr", qfalse );
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_Vid_Restart
|
|
|
|
Restart the video subsystem
|
|
|
|
we also have to reload the UI and CGame because the renderer
|
|
doesn't know what graphics to reload
|
|
=================
|
|
*/
|
|
static void CL_Vid_Restart( refShutdownCode_t shutdownCode ) {
|
|
|
|
// Settings may have changed so stop recording now
|
|
if ( CL_VideoRecording() )
|
|
CL_CloseAVI( qfalse );
|
|
|
|
if ( clc.demorecording )
|
|
CL_StopRecord_f();
|
|
|
|
// clear and mute all sounds until next registration
|
|
S_DisableSounds();
|
|
|
|
// shutdown VMs
|
|
CL_ShutdownVMs();
|
|
|
|
// shutdown the renderer and clear the renderer interface
|
|
CL_ShutdownRef( shutdownCode ); // REF_KEEP_CONTEXT, REF_KEEP_WINDOW, REF_DESTROY_WINDOW
|
|
|
|
// client is no longer pure until new checksums are sent
|
|
CL_ResetPureClientAtServer();
|
|
|
|
// clear pak references
|
|
FS_ClearPakReferences( FS_UI_REF | FS_CGAME_REF );
|
|
|
|
// reinitialize the filesystem if the game directory or checksum has changed
|
|
if ( !clc.demoplaying ) // -EC-
|
|
FS_ConditionalRestart( clc.checksumFeed, qfalse );
|
|
|
|
cls.soundRegistered = qfalse;
|
|
|
|
// unpause so the cgame definitely gets a snapshot and renders a frame
|
|
Cvar_Set( "cl_paused", "0" );
|
|
|
|
CL_ClearMemory();
|
|
|
|
// startup all the client stuff
|
|
CL_StartHunkUsers();
|
|
|
|
// start the cgame if connected
|
|
if ( ( cls.state > CA_CONNECTED && cls.state != CA_CINEMATIC ) || cls.startCgame ) {
|
|
cls.cgameStarted = qtrue;
|
|
CL_InitCGame();
|
|
// send pure checksums
|
|
CL_SendPureChecksums();
|
|
}
|
|
|
|
cls.startCgame = qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_Vid_Restart_f
|
|
|
|
Wrapper for CL_Vid_Restart
|
|
=================
|
|
*/
|
|
static void CL_Vid_Restart_f( void ) {
|
|
|
|
if ( Q_stricmp( Cmd_Argv(1), "keep_window" ) == 0 ) {
|
|
// fast path: keep window
|
|
CL_Vid_Restart( REF_KEEP_WINDOW );
|
|
} else if ( Q_stricmp(Cmd_Argv(1), "fast") == 0 ) {
|
|
// fast path: keep context
|
|
CL_Vid_Restart( REF_KEEP_CONTEXT );
|
|
} else {
|
|
if ( cls.lastVidRestart ) {
|
|
if ( abs( cls.lastVidRestart - Sys_Milliseconds()) < 500 ) {
|
|
// hack for OSP mod: do not allow vid restart right after cgame init
|
|
return;
|
|
}
|
|
}
|
|
CL_Vid_Restart( REF_DESTROY_WINDOW );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_Snd_Restart_f
|
|
|
|
Restart the sound subsystem
|
|
The cgame and game must also be forced to restart because
|
|
handles will be invalid
|
|
=================
|
|
*/
|
|
static void CL_Snd_Restart_f( void )
|
|
{
|
|
S_Shutdown();
|
|
|
|
// sound will be reinitialized by vid_restart
|
|
CL_Vid_Restart( REF_KEEP_CONTEXT /*REF_KEEP_WINDOW*/ );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_PK3List_f
|
|
==================
|
|
*/
|
|
void CL_OpenedPK3List_f( void ) {
|
|
Com_Printf("Opened PK3 Names: %s\n", FS_LoadedPakNames());
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_PureList_f
|
|
==================
|
|
*/
|
|
static void CL_ReferencedPK3List_f( void ) {
|
|
Com_Printf( "Referenced PK3 Names: %s\n", FS_ReferencedPakNames() );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_Configstrings_f
|
|
==================
|
|
*/
|
|
static void CL_Configstrings_f( void ) {
|
|
int i;
|
|
int ofs;
|
|
|
|
if ( cls.state != CA_ACTIVE ) {
|
|
Com_Printf( "Not connected to a server.\n");
|
|
return;
|
|
}
|
|
|
|
for ( i = 0 ; i < MAX_CONFIGSTRINGS ; i++ ) {
|
|
ofs = cl.gameState.stringOffsets[ i ];
|
|
if ( !ofs ) {
|
|
continue;
|
|
}
|
|
Com_Printf( "%4i: %s\n", i, cl.gameState.stringData + ofs );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==============
|
|
CL_Clientinfo_f
|
|
==============
|
|
*/
|
|
static void CL_Clientinfo_f( void ) {
|
|
Com_Printf( "--------- Client Information ---------\n" );
|
|
Com_Printf( "state: %i\n", cls.state );
|
|
Com_Printf( "Server: %s\n", cls.servername );
|
|
Com_Printf ("User info settings:\n");
|
|
Info_Print( Cvar_InfoString( CVAR_USERINFO, NULL ) );
|
|
Com_Printf( "--------------------------------------\n" );
|
|
}
|
|
|
|
|
|
/*
|
|
==============
|
|
CL_Serverinfo_f
|
|
==============
|
|
*/
|
|
static void CL_Serverinfo_f( void ) {
|
|
int ofs;
|
|
|
|
ofs = cl.gameState.stringOffsets[ CS_SERVERINFO ];
|
|
if ( !ofs )
|
|
return;
|
|
|
|
Com_Printf( "Server info settings:\n" );
|
|
Info_Print( cl.gameState.stringData + ofs );
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
CL_Systeminfo_f
|
|
===========
|
|
*/
|
|
static void CL_Systeminfo_f( void ) {
|
|
int ofs;
|
|
|
|
ofs = cl.gameState.stringOffsets[ CS_SYSTEMINFO ];
|
|
if ( !ofs )
|
|
return;
|
|
|
|
Com_Printf( "System info settings:\n" );
|
|
Info_Print( cl.gameState.stringData + ofs );
|
|
}
|
|
|
|
|
|
static void CL_CompleteCallvote(const char *args, int argNum )
|
|
{
|
|
if( argNum >= 2 )
|
|
{
|
|
// Skip "callvote "
|
|
const char *p = Com_SkipTokens( args, 1, " " );
|
|
|
|
if ( p > args )
|
|
Field_CompleteCommand( p, qtrue, qtrue );
|
|
}
|
|
}
|
|
|
|
|
|
//====================================================================
|
|
|
|
/*
|
|
=================
|
|
CL_DownloadsComplete
|
|
|
|
Called when all downloading has been completed
|
|
=================
|
|
*/
|
|
static void CL_DownloadsComplete( void ) {
|
|
|
|
#ifdef USE_CURL
|
|
// if we downloaded with cURL
|
|
if ( clc.cURLUsed ) {
|
|
clc.cURLUsed = qfalse;
|
|
CL_cURL_Shutdown();
|
|
if ( clc.cURLDisconnected ) {
|
|
if ( clc.downloadRestart ) {
|
|
FS_Restart( clc.checksumFeed );
|
|
clc.downloadRestart = qfalse;
|
|
}
|
|
clc.cURLDisconnected = qfalse;
|
|
CL_Reconnect_f();
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// if we downloaded files we need to restart the file system
|
|
if ( clc.downloadRestart ) {
|
|
clc.downloadRestart = qfalse;
|
|
|
|
FS_Restart(clc.checksumFeed); // We possibly downloaded a pak, restart the file system to load it
|
|
|
|
// inform the server so we get new gamestate info
|
|
CL_AddReliableCommand( "donedl", qfalse );
|
|
|
|
// by sending the donedl command we request a new gamestate
|
|
// so we don't want to load stuff yet
|
|
return;
|
|
}
|
|
|
|
// let the client game init and load data
|
|
cls.state = CA_LOADING;
|
|
|
|
// Pump the loop, this may change gamestate!
|
|
Com_EventLoop();
|
|
|
|
// if the gamestate was changed by calling Com_EventLoop
|
|
// then we loaded everything already and we don't want to do it again.
|
|
if ( cls.state != CA_LOADING ) {
|
|
return;
|
|
}
|
|
|
|
// flush client memory and start loading stuff
|
|
// this will also (re)load the UI
|
|
// if this is a local client then only the client part of the hunk
|
|
// will be cleared, note that this is done after the hunk mark has been set
|
|
//if ( !com_sv_running->integer )
|
|
CL_FlushMemory();
|
|
|
|
// initialize the CGame
|
|
cls.cgameStarted = qtrue;
|
|
CL_InitCGame();
|
|
|
|
if ( clc.demofile == FS_INVALID_HANDLE ) {
|
|
Cmd_AddCommand( "callvote", NULL );
|
|
Cmd_SetCommandCompletionFunc( "callvote", CL_CompleteCallvote );
|
|
}
|
|
|
|
// set pure checksums
|
|
CL_SendPureChecksums();
|
|
|
|
CL_WritePacket();
|
|
CL_WritePacket();
|
|
CL_WritePacket();
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_BeginDownload
|
|
|
|
Requests a file to download from the server. Stores it in the current
|
|
game directory.
|
|
=================
|
|
*/
|
|
static void CL_BeginDownload( const char *localName, const char *remoteName ) {
|
|
|
|
Com_DPrintf("***** CL_BeginDownload *****\n"
|
|
"Localname: %s\n"
|
|
"Remotename: %s\n"
|
|
"****************************\n", localName, remoteName);
|
|
|
|
Q_strncpyz ( clc.downloadName, localName, sizeof(clc.downloadName) );
|
|
Com_sprintf( clc.downloadTempName, sizeof(clc.downloadTempName), "%s.tmp", localName );
|
|
|
|
// Set so UI gets access to it
|
|
Cvar_Set( "cl_downloadName", remoteName );
|
|
Cvar_Set( "cl_downloadSize", "0" );
|
|
Cvar_Set( "cl_downloadCount", "0" );
|
|
Cvar_SetIntegerValue( "cl_downloadTime", cls.realtime );
|
|
|
|
clc.downloadBlock = 0; // Starting new file
|
|
clc.downloadCount = 0;
|
|
|
|
CL_AddReliableCommand( va("download %s", remoteName), qfalse );
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_NextDownload
|
|
|
|
A download completed or failed
|
|
=================
|
|
*/
|
|
void CL_NextDownload( void )
|
|
{
|
|
char *s;
|
|
char *remoteName, *localName;
|
|
qboolean useCURL = qfalse;
|
|
|
|
// A download has finished, check whether this matches a referenced checksum
|
|
if(*clc.downloadName)
|
|
{
|
|
const char *zippath = FS_BuildOSPath(Cvar_VariableString("fs_homepath"), clc.downloadName, NULL );
|
|
|
|
if(!FS_CompareZipChecksum(zippath))
|
|
Com_Error(ERR_DROP, "Incorrect checksum for file: %s", clc.downloadName);
|
|
}
|
|
|
|
*clc.downloadTempName = *clc.downloadName = 0;
|
|
Cvar_Set("cl_downloadName", "");
|
|
|
|
// We are looking to start a download here
|
|
if (*clc.downloadList) {
|
|
s = clc.downloadList;
|
|
|
|
// format is:
|
|
// @remotename@localname@remotename@localname, etc.
|
|
|
|
if (*s == '@')
|
|
s++;
|
|
remoteName = s;
|
|
|
|
if ( (s = strchr(s, '@')) == NULL ) {
|
|
CL_DownloadsComplete();
|
|
return;
|
|
}
|
|
|
|
*s++ = '\0';
|
|
localName = s;
|
|
if ( (s = strchr(s, '@')) != NULL )
|
|
*s++ = '\0';
|
|
else
|
|
s = localName + strlen(localName); // point at the null byte
|
|
|
|
#ifdef USE_CURL
|
|
if(!(cl_allowDownload->integer & DLF_NO_REDIRECT)) {
|
|
if(clc.sv_allowDownload & DLF_NO_REDIRECT) {
|
|
Com_Printf("WARNING: server does not "
|
|
"allow download redirection "
|
|
"(sv_allowDownload is %d)\n",
|
|
clc.sv_allowDownload);
|
|
}
|
|
else if(!*clc.sv_dlURL) {
|
|
Com_Printf("WARNING: server allows "
|
|
"download redirection, but does not "
|
|
"have sv_dlURL set\n");
|
|
}
|
|
else if(!CL_cURL_Init()) {
|
|
Com_Printf("WARNING: could not load "
|
|
"cURL library\n");
|
|
}
|
|
else {
|
|
CL_cURL_BeginDownload(localName, va("%s/%s",
|
|
clc.sv_dlURL, remoteName));
|
|
useCURL = qtrue;
|
|
}
|
|
}
|
|
else if(!(clc.sv_allowDownload & DLF_NO_REDIRECT)) {
|
|
Com_Printf("WARNING: server allows download "
|
|
"redirection, but it disabled by client "
|
|
"configuration (cl_allowDownload is %d)\n",
|
|
cl_allowDownload->integer);
|
|
}
|
|
#endif /* USE_CURL */
|
|
|
|
if( !useCURL ) {
|
|
if( (cl_allowDownload->integer & DLF_NO_UDP) ) {
|
|
Com_Error(ERR_DROP, "UDP Downloads are "
|
|
"disabled on your client. "
|
|
"(cl_allowDownload is %d)",
|
|
cl_allowDownload->integer);
|
|
return;
|
|
}
|
|
else {
|
|
CL_BeginDownload( localName, remoteName );
|
|
}
|
|
}
|
|
clc.downloadRestart = qtrue;
|
|
|
|
// move over the rest
|
|
memmove( clc.downloadList, s, strlen(s) + 1 );
|
|
|
|
return;
|
|
}
|
|
|
|
CL_DownloadsComplete();
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_InitDownloads
|
|
|
|
After receiving a valid game state, we valid the cgame and local zip files here
|
|
and determine if we need to download them
|
|
=================
|
|
*/
|
|
void CL_InitDownloads( void ) {
|
|
|
|
if ( !(cl_allowDownload->integer & DLF_ENABLE) )
|
|
{
|
|
char missingfiles[ MAXPRINTMSG ];
|
|
|
|
// autodownload is disabled on the client
|
|
// but it's possible that some referenced files on the server are missing
|
|
if ( FS_ComparePaks( missingfiles, sizeof( missingfiles ), qfalse ) )
|
|
{
|
|
// NOTE TTimo I would rather have that printed as a modal message box
|
|
// but at this point while joining the game we don't know whether we will successfully join or not
|
|
Com_Printf( "\nWARNING: You are missing some files referenced by the server:\n%s"
|
|
"You might not be able to join the game\n"
|
|
"Go to the setting menu to turn on autodownload, or get the file elsewhere\n\n", missingfiles );
|
|
}
|
|
}
|
|
else if ( FS_ComparePaks( clc.downloadList, sizeof( clc.downloadList ) , qtrue ) ) {
|
|
|
|
Com_Printf( "Need paks: %s\n", clc.downloadList );
|
|
|
|
if ( *clc.downloadList ) {
|
|
// if autodownloading is not enabled on the server
|
|
cls.state = CA_CONNECTED;
|
|
|
|
*clc.downloadTempName = *clc.downloadName = '\0';
|
|
Cvar_Set( "cl_downloadName", "" );
|
|
|
|
CL_NextDownload();
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
#ifdef USE_CURL
|
|
if ( cl_mapAutoDownload->integer && ( !(clc.sv_allowDownload & DLF_ENABLE) || clc.demoplaying ) )
|
|
{
|
|
const char *info, *mapname, *bsp;
|
|
|
|
// get map name and BSP file name
|
|
info = cl.gameState.stringData + cl.gameState.stringOffsets[ CS_SERVERINFO ];
|
|
mapname = Info_ValueForKey( info, "mapname" );
|
|
bsp = va( "maps/%s.bsp", mapname );
|
|
|
|
if ( FS_FOpenFileRead( bsp, NULL, qfalse ) == -1 )
|
|
{
|
|
if ( CL_Download( "dlmap", mapname, qtrue ) )
|
|
{
|
|
cls.state = CA_CONNECTED; // prevent continue loading and shows the ui download progress screen
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
#endif // USE_CURL
|
|
|
|
CL_DownloadsComplete();
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_CheckForResend
|
|
|
|
Resend a connect message if the last one has timed out
|
|
=================
|
|
*/
|
|
static void CL_CheckForResend( void ) {
|
|
int port, len;
|
|
char info[MAX_INFO_STRING*2]; // larger buffer to detect overflows
|
|
char data[MAX_INFO_STRING];
|
|
qboolean notOverflowed;
|
|
qboolean infoTruncated;
|
|
|
|
// don't send anything if playing back a demo
|
|
if ( clc.demoplaying ) {
|
|
return;
|
|
}
|
|
|
|
// resend if we haven't gotten a reply yet
|
|
if ( cls.state != CA_CONNECTING && cls.state != CA_CHALLENGING ) {
|
|
return;
|
|
}
|
|
|
|
if ( cls.realtime - clc.connectTime < RETRANSMIT_TIMEOUT ) {
|
|
return;
|
|
}
|
|
|
|
clc.connectTime = cls.realtime; // for retransmit requests
|
|
clc.connectPacketCount++;
|
|
|
|
switch ( cls.state ) {
|
|
case CA_CONNECTING:
|
|
// requesting a challenge .. IPv6 users always get in as authorize server supports no ipv6.
|
|
#ifndef STANDALONE
|
|
if (!Cvar_VariableIntegerValue("com_standalone") && clc.serverAddress.type == NA_IP && !Sys_IsLANAddress( &clc.serverAddress ) )
|
|
CL_RequestAuthorization();
|
|
#endif
|
|
// The challenge request shall be followed by a client challenge so no malicious server can hijack this connection.
|
|
NET_OutOfBandPrint( NS_CLIENT, &clc.serverAddress, "getchallenge %d %s", clc.challenge, GAMENAME_FOR_MASTER );
|
|
break;
|
|
|
|
case CA_CHALLENGING:
|
|
// sending back the challenge
|
|
port = Cvar_VariableIntegerValue( "net_qport" );
|
|
|
|
infoTruncated = qfalse;
|
|
Q_strncpyz( info, Cvar_InfoString( CVAR_USERINFO, &infoTruncated ), sizeof( info ) );
|
|
|
|
// remove some non-important keys that may cause overflow during connection
|
|
if ( strlen( info ) > MAX_USERINFO_LENGTH - 64 ) {
|
|
infoTruncated |= Info_RemoveKey( info, "xp_name" ) ? qtrue : qfalse;
|
|
infoTruncated |= Info_RemoveKey( info, "xp_country" ) ? qtrue : qfalse;
|
|
}
|
|
|
|
len = strlen( info );
|
|
if ( len > MAX_USERINFO_LENGTH ) {
|
|
notOverflowed = qfalse;
|
|
} else {
|
|
notOverflowed = qtrue;
|
|
}
|
|
|
|
if ( com_protocol->integer != DEFAULT_PROTOCOL_VERSION ) {
|
|
notOverflowed &= Info_SetValueForKey_s( info, MAX_USERINFO_LENGTH, "protocol",
|
|
com_protocol->string );
|
|
} else {
|
|
notOverflowed &= Info_SetValueForKey_s( info, MAX_USERINFO_LENGTH, "protocol",
|
|
clc.compat ? XSTRING( OLD_PROTOCOL_VERSION ) : XSTRING( NEW_PROTOCOL_VERSION ) );
|
|
}
|
|
|
|
notOverflowed &= Info_SetValueForKey_s( info, MAX_USERINFO_LENGTH, "qport",
|
|
va( "%i", port ) );
|
|
|
|
notOverflowed &= Info_SetValueForKey_s( info, MAX_USERINFO_LENGTH, "challenge",
|
|
va( "%i", clc.challenge ) );
|
|
|
|
// for now - this will be used to inform server about q3msgboom fix
|
|
// this is optional key so will not trigger oversize warning
|
|
Info_SetValueForKey_s( info, MAX_USERINFO_LENGTH, "client", Q3_VERSION );
|
|
|
|
if ( !notOverflowed ) {
|
|
Com_Printf( S_COLOR_YELLOW "WARNING: oversize userinfo, you might be not able to join remote server!\n" );
|
|
}
|
|
|
|
len = Com_sprintf( data, sizeof( data ), "connect \"%s\"", info );
|
|
// NOTE TTimo don't forget to set the right data length!
|
|
NET_OutOfBandCompress( NS_CLIENT, &clc.serverAddress, (byte *) &data[0], len );
|
|
// the most current userinfo has been sent, so watch for any
|
|
// newer changes to userinfo variables
|
|
cvar_modifiedFlags &= ~CVAR_USERINFO;
|
|
|
|
// ... but force re-send if userinfo was truncated in any way
|
|
if ( infoTruncated || !notOverflowed ) {
|
|
cvar_modifiedFlags |= CVAR_USERINFO;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
Com_Error( ERR_FATAL, "CL_CheckForResend: bad cls.state" );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_MotdPacket
|
|
===================
|
|
*/
|
|
static void CL_MotdPacket( const netadr_t *from ) {
|
|
const char *challenge;
|
|
const char *info;
|
|
|
|
// if not from our server, ignore it
|
|
if ( !NET_CompareAdr( from, &cls.updateServer ) ) {
|
|
return;
|
|
}
|
|
|
|
info = Cmd_Argv(1);
|
|
|
|
// check challenge
|
|
challenge = Info_ValueForKey( info, "challenge" );
|
|
if ( strcmp( challenge, cls.updateChallenge ) ) {
|
|
return;
|
|
}
|
|
|
|
challenge = Info_ValueForKey( info, "motd" );
|
|
|
|
Q_strncpyz( cls.updateInfoString, info, sizeof( cls.updateInfoString ) );
|
|
Cvar_Set( "cl_motdString", challenge );
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_InitServerInfo
|
|
===================
|
|
*/
|
|
static void CL_InitServerInfo( serverInfo_t *server, const netadr_t *address ) {
|
|
server->adr = *address;
|
|
server->clients = 0;
|
|
server->hostName[0] = '\0';
|
|
server->mapName[0] = '\0';
|
|
server->maxClients = 0;
|
|
server->maxPing = 0;
|
|
server->minPing = 0;
|
|
server->ping = -1;
|
|
server->game[0] = '\0';
|
|
server->gameType = 0;
|
|
server->netType = 0;
|
|
server->punkbuster = 0;
|
|
server->g_humanplayers = 0;
|
|
server->g_needpass = 0;
|
|
}
|
|
|
|
#define MAX_SERVERSPERPACKET 256
|
|
|
|
typedef struct hash_chain_s {
|
|
netadr_t addr;
|
|
struct hash_chain_s *next;
|
|
} hash_chain_t;
|
|
|
|
static hash_chain_t *hash_table[1024];
|
|
static hash_chain_t hash_list[MAX_GLOBAL_SERVERS];
|
|
static unsigned int hash_count = 0;
|
|
|
|
static unsigned int hash_func( const netadr_t *addr ) {
|
|
|
|
const byte *ip = NULL;
|
|
unsigned int size;
|
|
unsigned int i;
|
|
unsigned int hash = 0;
|
|
|
|
switch ( addr->type ) {
|
|
case NA_IP: ip = addr->ipv._4; size = 4; break;
|
|
#ifdef USE_IPV6
|
|
case NA_IP6: ip = addr->ipv._6; size = 16; break;
|
|
#endif
|
|
default: size = 0; break;
|
|
}
|
|
|
|
for ( i = 0; i < size; i++ )
|
|
hash = hash * 101 + (int)( *ip++ );
|
|
|
|
hash = hash ^ ( hash >> 16 );
|
|
|
|
return (hash & 1023);
|
|
}
|
|
|
|
static void hash_insert( const netadr_t *addr )
|
|
{
|
|
hash_chain_t **tab, *cur;
|
|
unsigned int hash;
|
|
if ( hash_count >= MAX_GLOBAL_SERVERS )
|
|
return;
|
|
hash = hash_func( addr );
|
|
tab = &hash_table[ hash ];
|
|
cur = &hash_list[ hash_count++ ];
|
|
cur->addr = *addr;
|
|
if ( cur != *tab )
|
|
cur->next = *tab;
|
|
else
|
|
cur->next = NULL;
|
|
*tab = cur;
|
|
}
|
|
|
|
static void hash_reset( void )
|
|
{
|
|
hash_count = 0;
|
|
memset( hash_list, 0, sizeof( hash_list ) );
|
|
memset( hash_table, 0, sizeof( hash_table ) );
|
|
}
|
|
|
|
static hash_chain_t *hash_find( const netadr_t *addr )
|
|
{
|
|
hash_chain_t *cur;
|
|
cur = hash_table[ hash_func( addr ) ];
|
|
while ( cur != NULL ) {
|
|
if ( NET_CompareAdr( addr, &cur->addr ) )
|
|
return cur;
|
|
cur = cur->next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_ServersResponsePacket
|
|
===================
|
|
*/
|
|
static void CL_ServersResponsePacket( const netadr_t* from, msg_t *msg, qboolean extended ) {
|
|
int i, count, total;
|
|
netadr_t addresses[MAX_SERVERSPERPACKET];
|
|
int numservers;
|
|
byte* buffptr;
|
|
byte* buffend;
|
|
serverInfo_t *server;
|
|
|
|
//Com_Printf("CL_ServersResponsePacket\n"); // moved down
|
|
|
|
if (cls.numglobalservers == -1) {
|
|
// state to detect lack of servers or lack of response
|
|
cls.numglobalservers = 0;
|
|
cls.numGlobalServerAddresses = 0;
|
|
hash_reset();
|
|
}
|
|
|
|
// parse through server response string
|
|
numservers = 0;
|
|
buffptr = msg->data;
|
|
buffend = buffptr + msg->cursize;
|
|
|
|
// advance to initial token
|
|
do
|
|
{
|
|
if(*buffptr == '\\' || (extended && *buffptr == '/'))
|
|
break;
|
|
|
|
buffptr++;
|
|
} while (buffptr < buffend);
|
|
|
|
while (buffptr + 1 < buffend)
|
|
{
|
|
// IPv4 address
|
|
if (*buffptr == '\\')
|
|
{
|
|
buffptr++;
|
|
|
|
if (buffend - buffptr < sizeof(addresses[numservers].ipv._4) + sizeof(addresses[numservers].port) + 1)
|
|
break;
|
|
|
|
for(i = 0; i < sizeof(addresses[numservers].ipv._4); i++)
|
|
addresses[numservers].ipv._4[i] = *buffptr++;
|
|
|
|
addresses[numservers].type = NA_IP;
|
|
}
|
|
#ifdef USE_IPV6
|
|
// IPv6 address, if it's an extended response
|
|
else if (extended && *buffptr == '/')
|
|
{
|
|
buffptr++;
|
|
|
|
if (buffend - buffptr < sizeof(addresses[numservers].ipv._6) + sizeof(addresses[numservers].port) + 1)
|
|
break;
|
|
|
|
for(i = 0; i < sizeof(addresses[numservers].ipv._6); i++)
|
|
addresses[numservers].ipv._6[i] = *buffptr++;
|
|
|
|
addresses[numservers].type = NA_IP6;
|
|
addresses[numservers].scope_id = from->scope_id;
|
|
}
|
|
#endif
|
|
else
|
|
// syntax error!
|
|
break;
|
|
|
|
// parse out port
|
|
addresses[numservers].port = (*buffptr++) << 8;
|
|
addresses[numservers].port += *buffptr++;
|
|
addresses[numservers].port = BigShort( addresses[numservers].port );
|
|
|
|
// syntax check
|
|
if (*buffptr != '\\' && *buffptr != '/')
|
|
break;
|
|
|
|
numservers++;
|
|
if (numservers >= MAX_SERVERSPERPACKET)
|
|
break;
|
|
}
|
|
|
|
count = cls.numglobalservers;
|
|
|
|
for (i = 0; i < numservers && count < MAX_GLOBAL_SERVERS; i++) {
|
|
|
|
// Tequila: It's possible to have sent many master server requests. Then
|
|
// we may receive many times the same addresses from the master server.
|
|
// We just avoid to add a server if it is still in the global servers list.
|
|
if ( hash_find( &addresses[i] ) )
|
|
continue;
|
|
|
|
hash_insert( &addresses[i] );
|
|
|
|
// build net address
|
|
server = &cls.globalServers[count];
|
|
|
|
CL_InitServerInfo( server, &addresses[i] );
|
|
// advance to next slot
|
|
count++;
|
|
}
|
|
|
|
// if getting the global list
|
|
if ( count >= MAX_GLOBAL_SERVERS && cls.numGlobalServerAddresses < MAX_GLOBAL_SERVERS )
|
|
{
|
|
// if we couldn't store the servers in the main list anymore
|
|
for (; i < numservers && cls.numGlobalServerAddresses < MAX_GLOBAL_SERVERS; i++)
|
|
{
|
|
// just store the addresses in an additional list
|
|
cls.globalServerAddresses[cls.numGlobalServerAddresses++] = addresses[i];
|
|
}
|
|
}
|
|
|
|
cls.numglobalservers = count;
|
|
total = count + cls.numGlobalServerAddresses;
|
|
|
|
Com_Printf( "getserversResponse:%3d servers parsed (total %d)\n", numservers, total);
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_ConnectionlessPacket
|
|
|
|
Responses to broadcasts, etc
|
|
|
|
return true only for commands indicating that our server is alive
|
|
or connection sequence is going into the right way
|
|
=================
|
|
*/
|
|
static qboolean CL_ConnectionlessPacket( const netadr_t *from, msg_t *msg ) {
|
|
qboolean fromserver;
|
|
const char *s;
|
|
const char *c;
|
|
int challenge = 0;
|
|
|
|
MSG_BeginReadingOOB( msg );
|
|
MSG_ReadLong( msg ); // skip the -1
|
|
|
|
s = MSG_ReadStringLine( msg );
|
|
|
|
Cmd_TokenizeString( s );
|
|
|
|
c = Cmd_Argv(0);
|
|
|
|
if ( com_developer->integer ) {
|
|
Com_Printf( "CL packet %s: %s\n", NET_AdrToStringwPort( from ), s );
|
|
}
|
|
|
|
// challenge from the server we are connecting to
|
|
if ( !Q_stricmp(c, "challengeResponse" ) ) {
|
|
|
|
if ( cls.state != CA_CONNECTING ) {
|
|
Com_DPrintf( "Unwanted challenge response received. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
|
|
c = Cmd_Argv( 2 );
|
|
if ( *c != '\0' )
|
|
challenge = atoi( c );
|
|
|
|
clc.compat = qtrue;
|
|
s = Cmd_Argv( 3 ); // analyze server protocol version
|
|
if ( *s != '\0' ) {
|
|
int sv_proto = atoi( s );
|
|
if ( sv_proto > OLD_PROTOCOL_VERSION ) {
|
|
if ( sv_proto == NEW_PROTOCOL_VERSION || sv_proto == com_protocol->integer ) {
|
|
clc.compat = qfalse;
|
|
} else {
|
|
int cl_proto = com_protocol->integer;
|
|
if ( cl_proto == DEFAULT_PROTOCOL_VERSION ) {
|
|
// we support new protocol features by default
|
|
cl_proto = NEW_PROTOCOL_VERSION;
|
|
}
|
|
Com_Printf( S_COLOR_YELLOW "Warning: Server reports protocol version %d, "
|
|
"we have %d. Trying legacy protocol %d.\n",
|
|
sv_proto, cl_proto, OLD_PROTOCOL_VERSION );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( clc.compat )
|
|
{
|
|
if ( !NET_CompareAdr( from, &clc.serverAddress ) )
|
|
{
|
|
// This challenge response is not coming from the expected address.
|
|
// Check whether we have a matching client challenge to prevent
|
|
// connection hi-jacking.
|
|
if ( *c == '\0' || challenge != clc.challenge )
|
|
{
|
|
Com_DPrintf( "Challenge response received from unexpected source. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( *c == '\0' || challenge != clc.challenge )
|
|
{
|
|
Com_Printf( "Bad challenge for challengeResponse. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
// start sending connect instead of challenge request packets
|
|
clc.challenge = atoi(Cmd_Argv(1));
|
|
cls.state = CA_CHALLENGING;
|
|
clc.connectPacketCount = 0;
|
|
clc.connectTime = -99999;
|
|
|
|
// take this address as the new server address. This allows
|
|
// a server proxy to hand off connections to multiple servers
|
|
clc.serverAddress = *from;
|
|
Com_DPrintf( "challengeResponse: %d\n", clc.challenge );
|
|
return qtrue;
|
|
}
|
|
|
|
// server connection
|
|
if ( !Q_stricmp(c, "connectResponse") ) {
|
|
if ( cls.state >= CA_CONNECTED ) {
|
|
Com_Printf( "Dup connect received. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
if ( cls.state != CA_CHALLENGING ) {
|
|
Com_Printf( "connectResponse packet while not connecting. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
if ( !NET_CompareAdr( from, &clc.serverAddress ) ) {
|
|
Com_Printf( "connectResponse from wrong address. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !clc.compat ) {
|
|
// first argument: challenge response
|
|
c = Cmd_Argv( 1 );
|
|
if ( *c != '\0' ) {
|
|
challenge = atoi( c );
|
|
} else {
|
|
Com_Printf( "Bad connectResponse received. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( challenge != clc.challenge ) {
|
|
Com_Printf( "ConnectResponse with bad challenge received. Ignored.\n" );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( com_protocolCompat ) {
|
|
// enforce dm68-compatible stream for legacy/unknown servers
|
|
clc.compat = qtrue;
|
|
}
|
|
|
|
// second (optional) argument: actual protocol version used on server-side
|
|
c = Cmd_Argv( 2 );
|
|
if ( *c != '\0' ) {
|
|
int protocol = atoi( c );
|
|
if ( protocol > 0 ) {
|
|
if ( protocol <= OLD_PROTOCOL_VERSION ) {
|
|
clc.compat = qtrue;
|
|
} else {
|
|
clc.compat = qfalse;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Netchan_Setup( NS_CLIENT, &clc.netchan, from, Cvar_VariableIntegerValue( "net_qport" ), clc.challenge, clc.compat );
|
|
|
|
cls.state = CA_CONNECTED;
|
|
clc.lastPacketSentTime = -9999; // send first packet immediately
|
|
return qtrue;
|
|
}
|
|
|
|
// server responding to an info broadcast
|
|
if ( !Q_stricmp(c, "infoResponse") ) {
|
|
CL_ServerInfoPacket( from, msg );
|
|
return qfalse;
|
|
}
|
|
|
|
// server responding to a get playerlist
|
|
if ( !Q_stricmp(c, "statusResponse") ) {
|
|
CL_ServerStatusResponse( from, msg );
|
|
return qfalse;
|
|
}
|
|
|
|
// echo request from server
|
|
if ( !Q_stricmp(c, "echo") ) {
|
|
// NOTE: we may have to add exceptions for auth and update servers
|
|
if ( (fromserver = NET_CompareAdr( from, &clc.serverAddress )) != qfalse || NET_CompareAdr( from, &rcon_address ) ) {
|
|
NET_OutOfBandPrint( NS_CLIENT, from, "%s", Cmd_Argv(1) );
|
|
}
|
|
return fromserver;
|
|
}
|
|
|
|
// cd check
|
|
if ( !Q_stricmp(c, "keyAuthorize") ) {
|
|
// we don't use these now, so dump them on the floor
|
|
return qfalse;
|
|
}
|
|
|
|
// global MOTD from id
|
|
if ( !Q_stricmp(c, "motd") ) {
|
|
CL_MotdPacket( from );
|
|
return qfalse;
|
|
}
|
|
|
|
// print string from server
|
|
if ( !Q_stricmp(c, "print") ) {
|
|
// NOTE: we may have to add exceptions for auth and update servers
|
|
if ( (fromserver = NET_CompareAdr( from, &clc.serverAddress )) != qfalse || NET_CompareAdr( from, &rcon_address ) ) {
|
|
s = MSG_ReadString( msg );
|
|
Q_strncpyz( clc.serverMessage, s, sizeof( clc.serverMessage ) );
|
|
Com_Printf( "%s", s );
|
|
}
|
|
return fromserver;
|
|
}
|
|
|
|
// list of servers sent back by a master server (classic)
|
|
if ( !Q_strncmp(c, "getserversResponse", 18) ) {
|
|
CL_ServersResponsePacket( from, msg, qfalse );
|
|
return qfalse;
|
|
}
|
|
|
|
// list of servers sent back by a master server (extended)
|
|
if ( !Q_strncmp(c, "getserversExtResponse", 21) ) {
|
|
CL_ServersResponsePacket( from, msg, qtrue );
|
|
return qfalse;
|
|
}
|
|
|
|
Com_DPrintf( "Unknown connectionless packet command.\n" );
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
CL_PacketEvent
|
|
|
|
A packet has arrived from the main event loop
|
|
=================
|
|
*/
|
|
void CL_PacketEvent( const netadr_t *from, msg_t *msg ) {
|
|
int headerBytes;
|
|
|
|
if ( msg->cursize < 5 ) {
|
|
Com_DPrintf( "%s: Runt packet\n", NET_AdrToStringwPort( from ) );
|
|
return;
|
|
}
|
|
|
|
if ( *(int *)msg->data == -1 ) {
|
|
if ( CL_ConnectionlessPacket( from, msg ) )
|
|
clc.lastPacketTime = cls.realtime;
|
|
return;
|
|
}
|
|
|
|
if ( cls.state < CA_CONNECTED ) {
|
|
return; // can't be a valid sequenced packet
|
|
}
|
|
|
|
//
|
|
// packet from server
|
|
//
|
|
if ( !NET_CompareAdr( from, &clc.netchan.remoteAddress ) ) {
|
|
if ( com_developer->integer ) {
|
|
Com_Printf( "%s:sequenced packet without connection\n",
|
|
NET_AdrToStringwPort( from ) );
|
|
}
|
|
// FIXME: send a client disconnect?
|
|
return;
|
|
}
|
|
|
|
if ( !CL_Netchan_Process( &clc.netchan, msg ) ) {
|
|
return; // out of order, duplicated, etc
|
|
}
|
|
|
|
// the header is different lengths for reliable and unreliable messages
|
|
headerBytes = msg->readcount;
|
|
|
|
// track the last message received so it can be returned in
|
|
// client messages, allowing the server to detect a dropped
|
|
// gamestate
|
|
clc.serverMessageSequence = LittleLong( *(int32_t *)msg->data );
|
|
|
|
clc.lastPacketTime = cls.realtime;
|
|
CL_ParseServerMessage( msg );
|
|
|
|
//
|
|
// we don't know if it is ok to save a demo message until
|
|
// after we have parsed the frame
|
|
//
|
|
if ( clc.demorecording && !clc.demowaiting && !clc.demoplaying ) {
|
|
CL_WriteDemoMessage( msg, headerBytes );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_CheckTimeout
|
|
==================
|
|
*/
|
|
static void CL_CheckTimeout( void ) {
|
|
//
|
|
// check timeout
|
|
//
|
|
if ( ( !CL_CheckPaused() || !sv_paused->integer )
|
|
&& cls.state >= CA_CONNECTED && cls.state != CA_CINEMATIC
|
|
&& cls.realtime - clc.lastPacketTime > cl_timeout->integer * 1000 ) {
|
|
if ( ++cl.timeoutcount > 5 ) { // timeoutcount saves debugger
|
|
Com_Printf( "\nServer connection timed out.\n" );
|
|
Cvar_Set( "com_errorMessage", "Server connection timed out." );
|
|
if ( !CL_Disconnect( qfalse ) ) { // restart client if not done already
|
|
CL_FlushMemory();
|
|
}
|
|
if ( uivm ) {
|
|
VM_Call( uivm, 1, UI_SET_ACTIVE_MENU, UIMENU_MAIN );
|
|
}
|
|
return;
|
|
}
|
|
} else {
|
|
cl.timeoutcount = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_CheckPaused
|
|
Check whether client has been paused.
|
|
==================
|
|
*/
|
|
qboolean CL_CheckPaused( void )
|
|
{
|
|
// if cl_paused->modified is set, the cvar has only been changed in
|
|
// this frame. Keep paused in this frame to ensure the server doesn't
|
|
// lag behind.
|
|
if(cl_paused->integer || cl_paused->modified)
|
|
return qtrue;
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_NoDelay
|
|
==================
|
|
*/
|
|
qboolean CL_NoDelay( void )
|
|
{
|
|
if ( CL_VideoRecording() || ( com_timedemo->integer && clc.demofile != FS_INVALID_HANDLE ) )
|
|
return qtrue;
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_CheckUserinfo
|
|
==================
|
|
*/
|
|
static void CL_CheckUserinfo( void ) {
|
|
|
|
// don't add reliable commands when not yet connected
|
|
if ( cls.state < CA_CONNECTED )
|
|
return;
|
|
|
|
// don't overflow the reliable command buffer when paused
|
|
if ( CL_CheckPaused() )
|
|
return;
|
|
|
|
// send a reliable userinfo update if needed
|
|
if ( cvar_modifiedFlags & CVAR_USERINFO )
|
|
{
|
|
qboolean infoTruncated = qfalse;
|
|
const char *info;
|
|
|
|
cvar_modifiedFlags &= ~CVAR_USERINFO;
|
|
|
|
info = Cvar_InfoString( CVAR_USERINFO, &infoTruncated );
|
|
if ( strlen( info ) > MAX_USERINFO_LENGTH || infoTruncated ) {
|
|
Com_Printf( S_COLOR_YELLOW "WARNING: oversize userinfo, you might be not able to play on remote server!\n" );
|
|
}
|
|
|
|
CL_AddReliableCommand( va( "userinfo \"%s\"", info ), qfalse );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_Frame
|
|
==================
|
|
*/
|
|
void CL_Frame( int msec, int realMsec ) {
|
|
float fps;
|
|
float frameDuration;
|
|
|
|
#ifdef USE_CURL
|
|
if ( download.cURL ) {
|
|
Com_DL_Perform( &download );
|
|
}
|
|
#endif
|
|
|
|
if ( !com_cl_running->integer ) {
|
|
return;
|
|
}
|
|
|
|
// save the msec before checking pause
|
|
cls.realFrametime = realMsec;
|
|
|
|
#ifdef USE_CURL
|
|
if ( clc.downloadCURLM ) {
|
|
CL_cURL_PerformDownload();
|
|
// we can't process frames normally when in disconnected
|
|
// download mode since the ui vm expects cls.state to be
|
|
// CA_CONNECTED
|
|
if ( clc.cURLDisconnected ) {
|
|
cls.frametime = msec;
|
|
cls.realtime += msec;
|
|
cls.framecount++;
|
|
SCR_UpdateScreen();
|
|
S_Update( realMsec );
|
|
Con_RunConsole();
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if ( cls.cddialog ) {
|
|
// bring up the cd error dialog if needed
|
|
cls.cddialog = qfalse;
|
|
VM_Call( uivm, 1, UI_SET_ACTIVE_MENU, UIMENU_NEED_CD );
|
|
} else if ( cls.state == CA_DISCONNECTED && !( Key_GetCatcher( ) & KEYCATCH_UI )
|
|
&& !com_sv_running->integer && uivm ) {
|
|
// if disconnected, bring up the menu
|
|
S_StopAllSounds();
|
|
VM_Call( uivm, 1, UI_SET_ACTIVE_MENU, UIMENU_MAIN );
|
|
}
|
|
|
|
// if recording an avi, lock to a fixed fps
|
|
if ( CL_VideoRecording() && msec ) {
|
|
// save the current screen
|
|
if ( cls.state == CA_ACTIVE || cl_forceavidemo->integer ) {
|
|
|
|
if ( com_timescale->value > 0.0001f )
|
|
fps = MIN( cl_aviFrameRate->value / com_timescale->value, 1000.0f );
|
|
else
|
|
fps = 1000.0f;
|
|
|
|
frameDuration = MAX( 1000.0f / fps, 1.0f ) + clc.aviVideoFrameRemainder;
|
|
|
|
CL_TakeVideoFrame();
|
|
|
|
msec = (int)frameDuration;
|
|
clc.aviVideoFrameRemainder = frameDuration - msec;
|
|
|
|
realMsec = msec; // sync sound duration
|
|
}
|
|
}
|
|
|
|
if ( cl_autoRecordDemo->integer && !clc.demoplaying ) {
|
|
if ( cls.state == CA_ACTIVE && !clc.demorecording ) {
|
|
// If not recording a demo, and we should be, start one
|
|
qtime_t now;
|
|
const char *nowString;
|
|
char *p;
|
|
char mapName[ MAX_QPATH ];
|
|
char serverName[ MAX_OSPATH ];
|
|
|
|
Com_RealTime( &now );
|
|
nowString = va( "%04d%02d%02d%02d%02d%02d",
|
|
1900 + now.tm_year,
|
|
1 + now.tm_mon,
|
|
now.tm_mday,
|
|
now.tm_hour,
|
|
now.tm_min,
|
|
now.tm_sec );
|
|
|
|
Q_strncpyz( serverName, cls.servername, MAX_OSPATH );
|
|
// Replace the ":" in the address as it is not a valid
|
|
// file name character
|
|
p = strchr( serverName, ':' );
|
|
if( p ) {
|
|
*p = '.';
|
|
}
|
|
|
|
Q_strncpyz( mapName, COM_SkipPath( cl.mapname ), sizeof( cl.mapname ) );
|
|
COM_StripExtension(mapName, mapName, sizeof(mapName));
|
|
|
|
Cbuf_ExecuteText( EXEC_NOW,
|
|
va( "record %s-%s-%s", nowString, serverName, mapName ) );
|
|
}
|
|
else if( cls.state != CA_ACTIVE && clc.demorecording ) {
|
|
// Recording, but not CA_ACTIVE, so stop recording
|
|
CL_StopRecord_f( );
|
|
}
|
|
}
|
|
|
|
// decide the simulation time
|
|
cls.frametime = msec;
|
|
cls.realtime += msec;
|
|
|
|
if ( cl_timegraph->integer ) {
|
|
SCR_DebugGraph( msec * 0.25f );
|
|
}
|
|
|
|
// see if we need to update any userinfo
|
|
CL_CheckUserinfo();
|
|
|
|
// if we haven't gotten a packet in a long time, drop the connection
|
|
if ( !clc.demoplaying ) {
|
|
CL_CheckTimeout();
|
|
}
|
|
|
|
// send intentions now
|
|
CL_SendCmd();
|
|
|
|
// resend a connection request if necessary
|
|
CL_CheckForResend();
|
|
|
|
// decide on the serverTime to render
|
|
CL_SetCGameTime();
|
|
|
|
// update the screen
|
|
cls.framecount++;
|
|
SCR_UpdateScreen();
|
|
|
|
// update audio
|
|
S_Update( realMsec );
|
|
|
|
// advance local effects for next frame
|
|
SCR_RunCinematic();
|
|
|
|
Con_RunConsole();
|
|
}
|
|
|
|
|
|
//============================================================================
|
|
|
|
/*
|
|
================
|
|
CL_RefPrintf
|
|
================
|
|
*/
|
|
static __attribute__ ((format (printf, 2, 3))) void QDECL CL_RefPrintf( printParm_t level, const char *fmt, ... ) {
|
|
va_list argptr;
|
|
char msg[MAXPRINTMSG];
|
|
|
|
va_start( argptr, fmt );
|
|
Q_vsnprintf( msg, sizeof( msg ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
switch ( level ) {
|
|
default: Com_Printf( "%s", msg ); break;
|
|
case PRINT_DEVELOPER: Com_DPrintf( "%s", msg ); break;
|
|
case PRINT_WARNING: Com_Printf( S_COLOR_YELLOW "%s", msg ); break;
|
|
case PRINT_ERROR: Com_Printf( S_COLOR_RED "%s", msg ); break;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_ShutdownRef
|
|
============
|
|
*/
|
|
static void CL_ShutdownRef( refShutdownCode_t code ) {
|
|
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
if ( cl_renderer->modified ) {
|
|
code = REF_UNLOAD_DLL;
|
|
}
|
|
#endif
|
|
|
|
// clear and mute all sounds until next registration
|
|
// S_DisableSounds();
|
|
|
|
if ( code >= REF_DESTROY_WINDOW ) { // +REF_UNLOAD_DLL
|
|
// shutdown sound system before renderer
|
|
// because it may depend from window handle
|
|
S_Shutdown();
|
|
}
|
|
|
|
SCR_Done();
|
|
|
|
if ( re.Shutdown ) {
|
|
re.Shutdown( code );
|
|
}
|
|
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
if ( rendererLib ) {
|
|
Sys_UnloadLibrary( rendererLib );
|
|
rendererLib = NULL;
|
|
}
|
|
#endif
|
|
|
|
Com_Memset( &re, 0, sizeof( re ) );
|
|
|
|
cls.rendererStarted = qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_InitRenderer
|
|
============
|
|
*/
|
|
static void CL_InitRenderer( void ) {
|
|
|
|
// fixup renderer -EC-
|
|
if ( !re.BeginRegistration ) {
|
|
CL_InitRef();
|
|
}
|
|
|
|
// this sets up the renderer and calls R_Init
|
|
re.BeginRegistration( &cls.glconfig );
|
|
|
|
// load character sets
|
|
cls.charSetShader = re.RegisterShader( "gfx/2d/bigchars" );
|
|
cls.whiteShader = re.RegisterShader( "white" );
|
|
cls.consoleShader = re.RegisterShader( "console" );
|
|
|
|
Con_CheckResize();
|
|
|
|
g_console_field_width = ((cls.glconfig.vidWidth / smallchar_width)) - 2;
|
|
g_consoleField.widthInChars = g_console_field_width;
|
|
|
|
// for 640x480 virtualized screen
|
|
cls.biasY = 0;
|
|
cls.biasX = 0;
|
|
if ( cls.glconfig.vidWidth * 480 > cls.glconfig.vidHeight * 640 ) {
|
|
// wide screen, scale by height
|
|
cls.scale = cls.glconfig.vidHeight * (1.0/480.0);
|
|
cls.biasX = 0.5 * ( cls.glconfig.vidWidth - ( cls.glconfig.vidHeight * (640.0/480.0) ) );
|
|
} else {
|
|
// no wide screen, scale by width
|
|
cls.scale = cls.glconfig.vidWidth * (1.0/640.0);
|
|
cls.biasY = 0.5 * ( cls.glconfig.vidHeight - ( cls.glconfig.vidWidth * (480.0/640) ) );
|
|
}
|
|
|
|
SCR_Init();
|
|
}
|
|
|
|
|
|
/*
|
|
============================
|
|
CL_StartHunkUsers
|
|
|
|
After the server has cleared the hunk, these will need to be restarted
|
|
This is the only place that any of these functions are called from
|
|
============================
|
|
*/
|
|
void CL_StartHunkUsers( void ) {
|
|
|
|
if ( !com_cl_running || !com_cl_running->integer ) {
|
|
return;
|
|
}
|
|
|
|
if ( cls.state >= CA_LOADING ) {
|
|
// try to apply map-depending configuration from cvar cl_mapConfig_<mapname> cvars
|
|
const char *info = cl.gameState.stringData + cl.gameState.stringOffsets[ CS_SERVERINFO ];
|
|
const char *mapname = Info_ValueForKey( info, "mapname" );
|
|
if ( mapname && *mapname != '\0' ) {
|
|
const char *fmt = "cl_mapConfig_%s";
|
|
const char *cmd = Cvar_VariableString( va( fmt, mapname ) );
|
|
if ( cmd && *cmd != '\0' ) {
|
|
Cbuf_AddText( cmd );
|
|
} else {
|
|
// apply mapname "default" if present
|
|
cmd = Cvar_VariableString( va( fmt, "default" ) );
|
|
if ( cmd && *cmd != '\0' ) {
|
|
Cbuf_AddText( cmd );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FS_Reload();
|
|
|
|
if ( !cls.rendererStarted ) {
|
|
cls.rendererStarted = qtrue;
|
|
CL_InitRenderer();
|
|
}
|
|
|
|
if ( !cls.soundStarted ) {
|
|
cls.soundStarted = qtrue;
|
|
S_Init();
|
|
}
|
|
|
|
if ( !cls.soundRegistered ) {
|
|
cls.soundRegistered = qtrue;
|
|
S_BeginRegistration();
|
|
}
|
|
|
|
if ( !cls.uiStarted ) {
|
|
cls.uiStarted = qtrue;
|
|
CL_InitUI();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_RefMalloc
|
|
============
|
|
*/
|
|
static void *CL_RefMalloc( int size ) {
|
|
return Z_TagMalloc( size, TAG_RENDERER );
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_RefFreeAll
|
|
============
|
|
*/
|
|
static void CL_RefFreeAll( void ) {
|
|
Z_FreeTags( TAG_RENDERER );
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_ScaledMilliseconds
|
|
============
|
|
*/
|
|
int CL_ScaledMilliseconds( void ) {
|
|
return Sys_Milliseconds()*com_timescale->value;
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_IsMinimized
|
|
============
|
|
*/
|
|
static qboolean CL_IsMininized( void ) {
|
|
return gw_minimized;
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_SetScaling
|
|
|
|
Sets console chars height
|
|
============
|
|
*/
|
|
static void CL_SetScaling( float factor, int captureWidth, int captureHeight ) {
|
|
|
|
if ( cls.con_factor != factor ) {
|
|
// rescale console
|
|
con_scale->modified = qtrue;
|
|
}
|
|
|
|
cls.con_factor = factor;
|
|
|
|
// set custom capture resolution
|
|
cls.captureWidth = captureWidth;
|
|
cls.captureHeight = captureHeight;
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
CL_InitRef
|
|
============
|
|
*/
|
|
static void CL_InitRef( void ) {
|
|
refimport_t rimp;
|
|
refexport_t *ret;
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
GetRefAPI_t GetRefAPI;
|
|
char dllName[ MAX_OSPATH ];
|
|
#endif
|
|
|
|
CL_InitGLimp_Cvars();
|
|
|
|
Com_Printf( "----- Initializing Renderer ----\n" );
|
|
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
|
|
#if defined (__linux__) && defined(__i386__)
|
|
#define REND_ARCH_STRING "x86"
|
|
#else
|
|
#define REND_ARCH_STRING ARCH_STRING
|
|
#endif
|
|
|
|
Com_sprintf( dllName, sizeof( dllName ), RENDERER_PREFIX "_%s_" REND_ARCH_STRING DLL_EXT, cl_renderer->string );
|
|
rendererLib = FS_LoadLibrary( dllName );
|
|
if ( !rendererLib )
|
|
{
|
|
Cvar_ForceReset( "cl_renderer" );
|
|
Com_sprintf( dllName, sizeof( dllName ), RENDERER_PREFIX "_%s_" REND_ARCH_STRING DLL_EXT, cl_renderer->string );
|
|
rendererLib = FS_LoadLibrary( dllName );
|
|
if ( !rendererLib )
|
|
{
|
|
Com_Error( ERR_FATAL, "Failed to load renderer %s", dllName );
|
|
}
|
|
}
|
|
|
|
GetRefAPI = Sys_LoadFunction( rendererLib, "GetRefAPI" );
|
|
if( !GetRefAPI )
|
|
{
|
|
Com_Error( ERR_FATAL, "Can't load symbol GetRefAPI" );
|
|
return;
|
|
}
|
|
|
|
cl_renderer->modified = qfalse;
|
|
#endif
|
|
|
|
Com_Memset( &rimp, 0, sizeof( rimp ) );
|
|
|
|
rimp.Cmd_AddCommand = Cmd_AddCommand;
|
|
rimp.Cmd_RemoveCommand = Cmd_RemoveCommand;
|
|
rimp.Cmd_Argc = Cmd_Argc;
|
|
rimp.Cmd_Argv = Cmd_Argv;
|
|
rimp.Cmd_ExecuteText = Cbuf_ExecuteText;
|
|
rimp.Printf = CL_RefPrintf;
|
|
rimp.Error = Com_Error;
|
|
rimp.Milliseconds = CL_ScaledMilliseconds;
|
|
rimp.Microseconds = Sys_Microseconds;
|
|
rimp.Malloc = CL_RefMalloc;
|
|
rimp.FreeAll = CL_RefFreeAll;
|
|
rimp.Free = Z_Free;
|
|
#ifdef HUNK_DEBUG
|
|
rimp.Hunk_AllocDebug = Hunk_AllocDebug;
|
|
#else
|
|
rimp.Hunk_Alloc = Hunk_Alloc;
|
|
#endif
|
|
rimp.Hunk_AllocateTempMemory = Hunk_AllocateTempMemory;
|
|
rimp.Hunk_FreeTempMemory = Hunk_FreeTempMemory;
|
|
|
|
rimp.CM_ClusterPVS = CM_ClusterPVS;
|
|
rimp.CM_DrawDebugSurface = CM_DrawDebugSurface;
|
|
|
|
rimp.FS_ReadFile = FS_ReadFile;
|
|
rimp.FS_FreeFile = FS_FreeFile;
|
|
rimp.FS_WriteFile = FS_WriteFile;
|
|
rimp.FS_FreeFileList = FS_FreeFileList;
|
|
rimp.FS_ListFiles = FS_ListFiles;
|
|
//rimp.FS_FileIsInPAK = FS_FileIsInPAK;
|
|
rimp.FS_FileExists = FS_FileExists;
|
|
|
|
rimp.Cvar_Get = Cvar_Get;
|
|
rimp.Cvar_Set = Cvar_Set;
|
|
rimp.Cvar_SetValue = Cvar_SetValue;
|
|
rimp.Cvar_CheckRange = Cvar_CheckRange;
|
|
rimp.Cvar_SetDescription = Cvar_SetDescription;
|
|
rimp.Cvar_VariableStringBuffer = Cvar_VariableStringBuffer;
|
|
rimp.Cvar_VariableString = Cvar_VariableString;
|
|
rimp.Cvar_VariableIntegerValue = Cvar_VariableIntegerValue;
|
|
|
|
rimp.Cvar_SetGroup = Cvar_SetGroup;
|
|
rimp.Cvar_CheckGroup = Cvar_CheckGroup;
|
|
rimp.Cvar_ResetGroup = Cvar_ResetGroup;
|
|
|
|
// cinematic stuff
|
|
|
|
rimp.CIN_UploadCinematic = CIN_UploadCinematic;
|
|
rimp.CIN_PlayCinematic = CIN_PlayCinematic;
|
|
rimp.CIN_RunCinematic = CIN_RunCinematic;
|
|
|
|
rimp.CL_WriteAVIVideoFrame = CL_WriteAVIVideoFrame;
|
|
rimp.CL_SaveJPGToBuffer = CL_SaveJPGToBuffer;
|
|
rimp.CL_SaveJPG = CL_SaveJPG;
|
|
rimp.CL_LoadJPG = CL_LoadJPG;
|
|
|
|
rimp.CL_IsMinimized = CL_IsMininized;
|
|
rimp.CL_SetScaling = CL_SetScaling;
|
|
|
|
rimp.Sys_SetClipboardBitmap = Sys_SetClipboardBitmap;
|
|
rimp.Sys_LowPhysicalMemory = Sys_LowPhysicalMemory;
|
|
rimp.Com_RealTime = Com_RealTime;
|
|
|
|
rimp.GLimp_InitGamma = GLimp_InitGamma;
|
|
rimp.GLimp_SetGamma = GLimp_SetGamma;
|
|
|
|
// OpenGL API
|
|
#ifdef USE_OPENGL_API
|
|
rimp.GLimp_Init = GLimp_Init;
|
|
rimp.GLimp_Shutdown = GLimp_Shutdown;
|
|
rimp.GL_GetProcAddress = GL_GetProcAddress;
|
|
rimp.GLimp_EndFrame = GLimp_EndFrame;
|
|
#endif
|
|
|
|
// Vulkan API
|
|
#ifdef USE_VULKAN_API
|
|
rimp.VKimp_Init = VKimp_Init;
|
|
rimp.VKimp_Shutdown = VKimp_Shutdown;
|
|
rimp.VK_GetInstanceProcAddr = VK_GetInstanceProcAddr;
|
|
rimp.VK_CreateSurface = VK_CreateSurface;
|
|
#endif
|
|
|
|
ret = GetRefAPI( REF_API_VERSION, &rimp );
|
|
|
|
Com_Printf( "-------------------------------\n");
|
|
|
|
if ( !ret ) {
|
|
Com_Error (ERR_FATAL, "Couldn't initialize refresh" );
|
|
}
|
|
|
|
re = *ret;
|
|
|
|
// unpause so the cgame definitely gets a snapshot and renders a frame
|
|
Cvar_Set( "cl_paused", "0" );
|
|
}
|
|
|
|
|
|
//===========================================================================================
|
|
|
|
|
|
static void CL_SetModel_f( void ) {
|
|
const char *arg;
|
|
char name[ MAX_CVAR_VALUE_STRING ];
|
|
|
|
arg = Cmd_Argv( 1 );
|
|
if ( arg[0] ) {
|
|
Cvar_Set( "model", arg );
|
|
Cvar_Set( "headmodel", arg );
|
|
} else {
|
|
Cvar_VariableStringBuffer( "model", name, sizeof( name ) );
|
|
Com_Printf( "model is set to %s\n", name );
|
|
}
|
|
}
|
|
|
|
|
|
//===========================================================================================
|
|
|
|
|
|
/*
|
|
===============
|
|
CL_Video_f
|
|
|
|
video
|
|
video [filename]
|
|
===============
|
|
*/
|
|
static void CL_Video_f( void )
|
|
{
|
|
char filename[ MAX_OSPATH ];
|
|
const char *ext;
|
|
qboolean pipe;
|
|
int i;
|
|
|
|
/*if( !clc.demoplaying )
|
|
{
|
|
Com_Printf( "The %s command can only be used when playing back demos\n", Cmd_Argv( 0 ) );
|
|
return;
|
|
}*/
|
|
|
|
pipe = ( Q_stricmp( Cmd_Argv( 0 ), "video-pipe" ) == 0 );
|
|
|
|
if ( pipe )
|
|
ext = "mp4";
|
|
else
|
|
ext = "avi";
|
|
|
|
if ( Cmd_Argc() == 2 )
|
|
{
|
|
// explicit filename
|
|
Com_sprintf( filename, sizeof( filename ), "videos/%s", Cmd_Argv( 1 ) );
|
|
|
|
// override video file extension
|
|
if ( pipe )
|
|
{
|
|
char *sep = strrchr( filename, '/' ); // last path separator
|
|
char *e = strrchr( filename, '.' );
|
|
|
|
if ( e && e > sep && *(e+1) != '\0' ) {
|
|
ext = e + 1;
|
|
*e = '\0';
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// scan for a free filename
|
|
for ( i = 0; i <= 9999; i++ )
|
|
{
|
|
Com_sprintf( filename, sizeof( filename ), "videos/video%04d.%s", i, ext );
|
|
if ( !FS_FileExists( filename ) )
|
|
break; // file doesn't exist
|
|
}
|
|
|
|
if ( i > 9999 )
|
|
{
|
|
Com_Printf( S_COLOR_RED "ERROR: no free file names to create video\n" );
|
|
return;
|
|
}
|
|
|
|
// without extension
|
|
Com_sprintf( filename, sizeof( filename ), "videos/video%04d", i );
|
|
}
|
|
|
|
|
|
clc.aviSoundFrameRemainder = 0.0f;
|
|
clc.aviVideoFrameRemainder = 0.0f;
|
|
|
|
Q_strncpyz( clc.videoName, filename, sizeof( clc.videoName ) );
|
|
clc.videoIndex = 0;
|
|
|
|
CL_OpenAVIForWriting( va( "%s.%s", clc.videoName, ext ), pipe, qfalse );
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
CL_StopVideo_f
|
|
===============
|
|
*/
|
|
static void CL_StopVideo_f( void )
|
|
{
|
|
CL_CloseAVI( qfalse );
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_CompleteRecordName
|
|
====================
|
|
*/
|
|
static void CL_CompleteVideoName(const char *args, int argNum )
|
|
{
|
|
if ( argNum == 2 )
|
|
{
|
|
Field_CompleteFilename( "videos", ".avi", qtrue, FS_MATCH_EXTERN | FS_MATCH_STICK );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
CL_GenerateQKey
|
|
|
|
test to see if a valid QKEY_FILE exists. If one does not, try to generate
|
|
it by filling it with 2048 bytes of random data.
|
|
===============
|
|
*/
|
|
#ifdef USE_Q3KEY
|
|
static void CL_GenerateQKey(void)
|
|
{
|
|
int len = 0;
|
|
unsigned char buff[ QKEY_SIZE ];
|
|
fileHandle_t f;
|
|
|
|
len = FS_SV_FOpenFileRead( QKEY_FILE, &f );
|
|
FS_FCloseFile( f );
|
|
if( len == QKEY_SIZE ) {
|
|
Com_Printf( "QKEY found.\n" );
|
|
return;
|
|
}
|
|
else {
|
|
if( len > 0 ) {
|
|
Com_Printf( "QKEY file size != %d, regenerating\n",
|
|
QKEY_SIZE );
|
|
}
|
|
|
|
Com_Printf( "QKEY building random string\n" );
|
|
Com_RandomBytes( buff, sizeof(buff) );
|
|
|
|
f = FS_SV_FOpenFileWrite( QKEY_FILE );
|
|
if( !f ) {
|
|
Com_Printf( "QKEY could not open %s for write\n",
|
|
QKEY_FILE );
|
|
return;
|
|
}
|
|
FS_Write( buff, sizeof(buff), f );
|
|
FS_FCloseFile( f );
|
|
Com_Printf( "QKEY generated\n" );
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
** CL_GetModeInfo
|
|
*/
|
|
typedef struct vidmode_s
|
|
{
|
|
const char *description;
|
|
int width, height;
|
|
float pixelAspect; // pixel width / height
|
|
} vidmode_t;
|
|
|
|
static const vidmode_t cl_vidModes[] =
|
|
{
|
|
{ "Mode 0: 320x240", 320, 240, 1 },
|
|
{ "Mode 1: 400x300", 400, 300, 1 },
|
|
{ "Mode 2: 512x384", 512, 384, 1 },
|
|
{ "Mode 3: 640x480", 640, 480, 1 },
|
|
{ "Mode 4: 800x600", 800, 600, 1 },
|
|
{ "Mode 5: 960x720", 960, 720, 1 },
|
|
{ "Mode 6: 1024x768", 1024, 768, 1 },
|
|
{ "Mode 7: 1152x864", 1152, 864, 1 },
|
|
{ "Mode 8: 1280x1024 (5:4)", 1280, 1024, 1 },
|
|
{ "Mode 9: 1600x1200", 1600, 1200, 1 },
|
|
{ "Mode 10: 2048x1536", 2048, 1536, 1 },
|
|
{ "Mode 11: 856x480 (wide)", 856, 480, 1 },
|
|
// extra modes:
|
|
{ "Mode 12: 1280x960", 1280, 960, 1 },
|
|
{ "Mode 13: 1280x720", 1280, 720, 1 },
|
|
{ "Mode 14: 1280x800 (16:10)", 1280, 800, 1 },
|
|
{ "Mode 15: 1366x768", 1366, 768, 1 },
|
|
{ "Mode 16: 1440x900 (16:10)", 1440, 900, 1 },
|
|
{ "Mode 17: 1600x900", 1600, 900, 1 },
|
|
{ "Mode 18: 1680x1050 (16:10)", 1680, 1050, 1 },
|
|
{ "Mode 19: 1920x1080", 1920, 1080, 1 },
|
|
{ "Mode 20: 1920x1200 (16:10)", 1920, 1200, 1 },
|
|
{ "Mode 21: 2560x1080 (21:9)", 2560, 1080, 1 },
|
|
{ "Mode 22: 3440x1440 (21:9)", 3440, 1440, 1 },
|
|
{ "Mode 23: 3840x2160", 3840, 2160, 1 },
|
|
{ "Mode 24: 4096x2160 (4K)", 4096, 2160, 1 }
|
|
};
|
|
static const int s_numVidModes = ARRAY_LEN( cl_vidModes );
|
|
|
|
qboolean CL_GetModeInfo( int *width, int *height, float *windowAspect, int mode, const char *modeFS, int dw, int dh, qboolean fullscreen )
|
|
{
|
|
const vidmode_t *vm;
|
|
float pixelAspect;
|
|
|
|
// set dedicated fullscreen mode
|
|
if ( fullscreen && *modeFS )
|
|
mode = atoi( modeFS );
|
|
|
|
if ( mode < -2 )
|
|
return qfalse;
|
|
|
|
if ( mode >= s_numVidModes )
|
|
return qfalse;
|
|
|
|
// fix unknown desktop resolution
|
|
if ( mode == -2 && (dw == 0 || dh == 0) )
|
|
mode = 3;
|
|
|
|
if ( mode == -2 ) { // desktop resolution
|
|
*width = dw;
|
|
*height = dh;
|
|
pixelAspect = r_customPixelAspect->value;
|
|
} else if ( mode == -1 ) { // custom resolution
|
|
*width = r_customwidth->integer;
|
|
*height = r_customheight->integer;
|
|
pixelAspect = r_customPixelAspect->value;
|
|
} else { // predefined resolution
|
|
vm = &cl_vidModes[ mode ];
|
|
*width = vm->width;
|
|
*height = vm->height;
|
|
pixelAspect = vm->pixelAspect;
|
|
}
|
|
|
|
*windowAspect = (float)*width / ( *height * pixelAspect );
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
** CL_ModeList_f
|
|
*/
|
|
static void CL_ModeList_f( void )
|
|
{
|
|
int i;
|
|
|
|
Com_Printf( "\n" );
|
|
for ( i = 0; i < s_numVidModes; i++ )
|
|
{
|
|
Com_Printf( "%s\n", cl_vidModes[ i ].description );
|
|
}
|
|
Com_Printf( "\n" );
|
|
}
|
|
|
|
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
static qboolean isValidRenderer( const char *s ) {
|
|
while ( *s ) {
|
|
if ( !((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || (*s >= '1' && *s <= '9')) )
|
|
return qfalse;
|
|
++s;
|
|
}
|
|
return qtrue;
|
|
}
|
|
#endif
|
|
|
|
|
|
static void CL_InitGLimp_Cvars( void )
|
|
{
|
|
// shared with GLimp
|
|
r_allowSoftwareGL = Cvar_Get( "r_allowSoftwareGL", "0", CVAR_LATCH );
|
|
Cvar_SetDescription( r_allowSoftwareGL, "Toggle the use of the default software OpenGL driver supplied by the Operating System." );
|
|
r_swapInterval = Cvar_Get( "r_swapInterval", "0", CVAR_ARCHIVE_ND );
|
|
Cvar_SetDescription( r_swapInterval, "V-blanks to wait before swapping buffers.\n 0: No V-Sync\n 1: Synced to the monitor's refresh rate." );
|
|
r_glDriver = Cvar_Get( "r_glDriver", OPENGL_DRIVER_NAME, CVAR_ARCHIVE_ND | CVAR_LATCH );
|
|
Cvar_SetDescription( r_glDriver, "Specifies the OpenGL driver to use, will revert back to default if driver name set is invalid." );
|
|
|
|
r_displayRefresh = Cvar_Get( "r_displayRefresh", "0", CVAR_LATCH );
|
|
Cvar_CheckRange( r_displayRefresh, "0", "500", CV_INTEGER );
|
|
Cvar_SetDescription( r_displayRefresh, "Override monitor refresh rate in fullscreen mode:\n 0 - use current monitor refresh rate\n > 0 - use custom refresh rate" );
|
|
|
|
vid_xpos = Cvar_Get( "vid_xpos", "3", CVAR_ARCHIVE );
|
|
Cvar_CheckRange( vid_xpos, NULL, NULL, CV_INTEGER );
|
|
Cvar_SetDescription( vid_xpos, "Saves/sets window X-coordinate when windowed, requires \\vid_restart." );
|
|
vid_ypos = Cvar_Get( "vid_ypos", "22", CVAR_ARCHIVE );
|
|
Cvar_CheckRange( vid_ypos, NULL, NULL, CV_INTEGER );
|
|
Cvar_SetDescription( vid_ypos, "Saves/sets window Y-coordinate when windowed, requires \\vid_restart." );
|
|
|
|
r_noborder = Cvar_Get( "r_noborder", "0", CVAR_ARCHIVE_ND | CVAR_LATCH );
|
|
Cvar_CheckRange( r_noborder, "0", "1", CV_INTEGER );
|
|
Cvar_SetDescription( r_noborder, "Setting to 1 will remove window borders and title bar in windowed mode, hold ALT to drag & drop it with opened console." );
|
|
|
|
r_mode = Cvar_Get( "r_mode", "-2", CVAR_ARCHIVE | CVAR_LATCH );
|
|
Cvar_CheckRange( r_mode, "-2", va( "%i", s_numVidModes-1 ), CV_INTEGER );
|
|
Cvar_SetDescription( r_mode, "Set video mode:\n -2 - use current desktop resolution\n -1 - use \\r_customWidth and \\r_customHeight\n 0..N - enter \\modelist for details" );
|
|
r_modeFullscreen = Cvar_Get( "r_modeFullscreen", "-2", CVAR_ARCHIVE | CVAR_LATCH );
|
|
Cvar_SetDescription( r_modeFullscreen, "Dedicated fullscreen mode, set to \"\" to use \\r_mode in all cases." );
|
|
|
|
r_fullscreen = Cvar_Get( "r_fullscreen", "1", CVAR_ARCHIVE | CVAR_LATCH );
|
|
Cvar_SetDescription( r_fullscreen, "Fullscreen mode. Set to 0 for windowed mode." );
|
|
r_customPixelAspect = Cvar_Get( "r_customPixelAspect", "1", CVAR_ARCHIVE_ND | CVAR_LATCH );
|
|
Cvar_SetDescription( r_customPixelAspect, "Enables custom aspect of the screen, with \\r_mode -1." );
|
|
r_customwidth = Cvar_Get( "r_customWidth", "1600", CVAR_ARCHIVE | CVAR_LATCH );
|
|
Cvar_CheckRange( r_customwidth, "4", NULL, CV_INTEGER );
|
|
Cvar_SetDescription( r_customwidth, "Custom width to use with \\r_mode -1." );
|
|
r_customheight = Cvar_Get( "r_customHeight", "1024", CVAR_ARCHIVE | CVAR_LATCH );
|
|
Cvar_CheckRange( r_customheight, "4", NULL, CV_INTEGER );
|
|
Cvar_SetDescription( r_customheight, "Custom height to use with \\r_mode -1." );
|
|
|
|
r_colorbits = Cvar_Get( "r_colorbits", "0", CVAR_ARCHIVE_ND | CVAR_LATCH );
|
|
Cvar_CheckRange( r_colorbits, "0", "32", CV_INTEGER );
|
|
Cvar_SetDescription( r_colorbits, "Sets color bit depth, set to 0 to use desktop settings." );
|
|
|
|
// shared with renderer:
|
|
cl_stencilbits = Cvar_Get( "r_stencilbits", "8", CVAR_ARCHIVE_ND | CVAR_LATCH );
|
|
Cvar_CheckRange( cl_stencilbits, "0", "8", CV_INTEGER );
|
|
Cvar_SetDescription( cl_stencilbits, "Stencil buffer size, value decreases Z-buffer depth." );
|
|
cl_depthbits = Cvar_Get( "r_depthbits", "0", CVAR_ARCHIVE_ND | CVAR_LATCH );
|
|
Cvar_CheckRange( cl_depthbits, "0", "32", CV_INTEGER );
|
|
Cvar_SetDescription( cl_depthbits, "Sets precision of Z-buffer." );
|
|
|
|
cl_drawBuffer = Cvar_Get( "r_drawBuffer", "GL_BACK", CVAR_CHEAT );
|
|
Cvar_SetDescription( cl_drawBuffer, "Specifies buffer to draw from: GL_FRONT or GL_BACK." );
|
|
#ifdef USE_RENDERER_DLOPEN
|
|
#ifdef RENDERER_DEFAULT
|
|
cl_renderer = Cvar_Get( "cl_renderer", XSTRING( RENDERER_DEFAULT ), CVAR_ARCHIVE | CVAR_LATCH );
|
|
#else
|
|
cl_renderer = Cvar_Get( "cl_renderer", "opengl", CVAR_ARCHIVE | CVAR_LATCH );
|
|
#endif
|
|
Cvar_SetDescription( cl_renderer, "Sets your desired renderer, requires \\vid_restart." );
|
|
|
|
if ( !isValidRenderer( cl_renderer->string ) ) {
|
|
Cvar_ForceReset( "cl_renderer" );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
/*
|
|
====================
|
|
CL_Init
|
|
====================
|
|
*/
|
|
void CL_Init( void ) {
|
|
const char *s;
|
|
cvar_t *cv;
|
|
|
|
Com_Printf( "----- Client Initialization -----\n" );
|
|
|
|
Con_Init();
|
|
|
|
CL_ClearState();
|
|
cls.state = CA_DISCONNECTED; // no longer CA_UNINITIALIZED
|
|
|
|
CL_ResetOldGame();
|
|
|
|
cls.realtime = 0;
|
|
|
|
CL_InitInput();
|
|
|
|
//
|
|
// register client variables
|
|
//
|
|
cl_noprint = Cvar_Get( "cl_noprint", "0", 0 );
|
|
Cvar_SetDescription( cl_noprint, "Disable printing of information in the console." );
|
|
cl_motd = Cvar_Get( "cl_motd", "1", 0 );
|
|
Cvar_SetDescription( cl_motd, "Toggle the display of the 'Message of the day'. When Quake 3 Arena starts a map up, it sends the GL_RENDERER string to the Message Of The Day server at id. This responds back with a message of the day to the client." );
|
|
|
|
cl_timeout = Cvar_Get( "cl_timeout", "200", 0 );
|
|
Cvar_CheckRange( cl_timeout, "5", NULL, CV_INTEGER );
|
|
Cvar_SetDescription( cl_timeout, "Duration of receiving nothing from server for client to decide it must be disconnected (in seconds)." );
|
|
|
|
cl_autoNudge = Cvar_Get( "cl_autoNudge", "0", CVAR_TEMP );
|
|
Cvar_CheckRange( cl_autoNudge, "0", "1", CV_FLOAT );
|
|
Cvar_SetDescription( cl_autoNudge, "Automatic time nudge that uses your average ping as the time nudge, values:\n 0 - use fixed \\cl_timeNudge\n (0..1] - factor of median average ping to use as timenudge\n" );
|
|
cl_timeNudge = Cvar_Get( "cl_timeNudge", "0", CVAR_TEMP );
|
|
Cvar_CheckRange( cl_timeNudge, "-30", "30", CV_INTEGER );
|
|
Cvar_SetDescription( cl_timeNudge, "Allows more or less latency to be added in the interest of better smoothness or better responsiveness." );
|
|
|
|
cl_shownet = Cvar_Get ("cl_shownet", "0", CVAR_TEMP );
|
|
Cvar_SetDescription( cl_shownet, "Toggle the display of current network status." );
|
|
cl_showTimeDelta = Cvar_Get ("cl_showTimeDelta", "0", CVAR_TEMP );
|
|
Cvar_SetDescription( cl_showTimeDelta, "Prints the time delta of each packet to the console (the time delta between server updates)." );
|
|
rcon_client_password = Cvar_Get ("rconPassword", "", CVAR_TEMP );
|
|
Cvar_SetDescription( rcon_client_password, "Sets a remote console password so clients may change server settings without direct access to the server console." );
|
|
cl_activeAction = Cvar_Get( "activeAction", "", CVAR_TEMP );
|
|
Cvar_SetDescription( cl_activeAction, "Contents of this variable will be executed upon first frame of play.\nNote: It is cleared every time it is executed." );
|
|
|
|
cl_autoRecordDemo = Cvar_Get ("cl_autoRecordDemo", "0", CVAR_ARCHIVE);
|
|
Cvar_SetDescription( cl_autoRecordDemo, "Auto-record demos when starting or joining a game." );
|
|
|
|
cl_aviFrameRate = Cvar_Get ("cl_aviFrameRate", "25", CVAR_ARCHIVE);
|
|
Cvar_CheckRange( cl_aviFrameRate, "1", "1000", CV_INTEGER );
|
|
Cvar_SetDescription( cl_aviFrameRate, "The framerate used for capturing video." );
|
|
cl_aviMotionJpeg = Cvar_Get ("cl_aviMotionJpeg", "1", CVAR_ARCHIVE);
|
|
Cvar_SetDescription( cl_aviMotionJpeg, "Enable/disable the MJPEG codec for avi output." );
|
|
cl_forceavidemo = Cvar_Get ("cl_forceavidemo", "0", 0);
|
|
Cvar_SetDescription( cl_forceavidemo, "Forces all demo recording into a sequence of screenshots in TGA format." );
|
|
|
|
cl_aviPipeFormat = Cvar_Get( "cl_aviPipeFormat",
|
|
"-preset medium -crf 23 -vcodec libx264 -flags +cgop -pix_fmt yuvj420p "
|
|
"-bf 2 -codec:a aac -strict -2 -b:a 160k -movflags faststart",
|
|
CVAR_ARCHIVE );
|
|
Cvar_SetDescription( cl_aviPipeFormat, "Encoder parameters used for \\video-pipe." );
|
|
|
|
rconAddress = Cvar_Get ("rconAddress", "", 0);
|
|
Cvar_SetDescription( rconAddress, "The IP address of the remote console you wish to connect to." );
|
|
|
|
cl_allowDownload = Cvar_Get( "cl_allowDownload", "1", CVAR_ARCHIVE_ND );
|
|
Cvar_SetDescription( cl_allowDownload, "Enables downloading of content needed in server. Valid bitmask flags:\n 1: Downloading enabled\n 2: Do not use HTTP/FTP downloads\n 4: Do not use UDP downloads" );
|
|
#ifdef USE_CURL
|
|
cl_mapAutoDownload = Cvar_Get( "cl_mapAutoDownload", "0", CVAR_ARCHIVE_ND );
|
|
Cvar_SetDescription( cl_mapAutoDownload, "Automatic map download for play and demo playback (via automatic \\dlmap call)." );
|
|
#ifdef USE_CURL_DLOPEN
|
|
cl_cURLLib = Cvar_Get( "cl_cURLLib", DEFAULT_CURL_LIB, 0 );
|
|
Cvar_SetDescription( cl_cURLLib, "Filename of cURL library to load." );
|
|
#endif
|
|
#endif
|
|
|
|
cl_conXOffset = Cvar_Get ("cl_conXOffset", "0", 0);
|
|
Cvar_SetDescription( cl_conXOffset, "Console notifications X-offset." );
|
|
cl_conColor = Cvar_Get( "cl_conColor", "", 0 );
|
|
Cvar_SetDescription( cl_conColor, "Console background color, set as R G B A values from 0-255, use with \\seta to save in config." );
|
|
|
|
#ifdef MACOS_X
|
|
// In game video is REALLY slow in Mac OS X right now due to driver slowness
|
|
cl_inGameVideo = Cvar_Get( "r_inGameVideo", "0", CVAR_ARCHIVE_ND );
|
|
#else
|
|
cl_inGameVideo = Cvar_Get( "r_inGameVideo", "1", CVAR_ARCHIVE_ND );
|
|
#endif
|
|
Cvar_SetDescription( cl_inGameVideo, "Controls whether in-game video should be drawn." );
|
|
|
|
cl_serverStatusResendTime = Cvar_Get ("cl_serverStatusResendTime", "750", 0);
|
|
Cvar_SetDescription( cl_serverStatusResendTime, "Time between re-sending server status requests if no response is received (in milliseconds)." );
|
|
|
|
// init cg_autoswitch so the ui will have it correctly even
|
|
// if the cgame hasn't been started
|
|
Cvar_Get ("cg_autoswitch", "1", CVAR_ARCHIVE);
|
|
|
|
cl_motdString = Cvar_Get( "cl_motdString", "", CVAR_ROM );
|
|
Cvar_SetDescription( cl_motdString, "Message of the day string from id's master server, it is a read only variable." );
|
|
|
|
cv = Cvar_Get( "cl_maxPing", "800", CVAR_ARCHIVE_ND );
|
|
Cvar_CheckRange( cv, "100", "999", CV_INTEGER );
|
|
Cvar_SetDescription( cv, "Specify the maximum allowed ping to a server." );
|
|
|
|
cl_lanForcePackets = Cvar_Get( "cl_lanForcePackets", "1", CVAR_ARCHIVE_ND );
|
|
Cvar_SetDescription( cl_lanForcePackets, "Bypass \\cl_maxpackets for LAN games, send packets every frame." );
|
|
|
|
cl_guidServerUniq = Cvar_Get( "cl_guidServerUniq", "1", CVAR_ARCHIVE_ND );
|
|
Cvar_SetDescription( cl_guidServerUniq, "Makes cl_guid unique for each server." );
|
|
|
|
cl_dlURL = Cvar_Get( "cl_dlURL", "http://ws.q3df.org/maps/download/%1", CVAR_ARCHIVE_ND );
|
|
Cvar_SetDescription( cl_dlURL, "Cvar must point to download location." );
|
|
|
|
cl_dlDirectory = Cvar_Get( "cl_dlDirectory", "0", CVAR_ARCHIVE_ND );
|
|
Cvar_CheckRange( cl_dlDirectory, "0", "1", CV_INTEGER );
|
|
s = va( "Save downloads initiated by \\dlmap and \\download commands in:\n"
|
|
" 0 - current game directory\n"
|
|
" 1 - fs_basegame (%s) directory\n", FS_GetBaseGameDir() );
|
|
Cvar_SetDescription( cl_dlDirectory, s );
|
|
|
|
cl_reconnectArgs = Cvar_Get( "cl_reconnectArgs", "", CVAR_ARCHIVE_ND | CVAR_NOTABCOMPLETE );
|
|
|
|
// userinfo
|
|
Cvar_Get ("name", "UnnamedPlayer", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
Cvar_Get ("rate", "25000", CVAR_USERINFO | CVAR_ARCHIVE );
|
|
Cvar_Get ("snaps", "40", CVAR_USERINFO | CVAR_ARCHIVE );
|
|
Cvar_Get ("model", "sarge", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
Cvar_Get ("headmodel", "sarge", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
Cvar_Get ("team_model", "sarge", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
Cvar_Get ("team_headmodel", "sarge", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
// Cvar_Get ("g_redTeam", "Stroggs", CVAR_SERVERINFO | CVAR_ARCHIVE);
|
|
// Cvar_Get ("g_blueTeam", "Pagans", CVAR_SERVERINFO | CVAR_ARCHIVE);
|
|
Cvar_Get ("color1", "4", CVAR_USERINFO | CVAR_ARCHIVE );
|
|
Cvar_Get ("color2", "5", CVAR_USERINFO | CVAR_ARCHIVE );
|
|
Cvar_Get ("handicap", "100", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
// Cvar_Get ("teamtask", "0", CVAR_USERINFO );
|
|
Cvar_Get ("sex", "male", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
Cvar_Get ("cl_anonymous", "0", CVAR_USERINFO | CVAR_ARCHIVE_ND );
|
|
|
|
Cvar_Get ("password", "", CVAR_USERINFO | CVAR_NORESTART);
|
|
Cvar_Get ("cg_predictItems", "1", CVAR_USERINFO | CVAR_ARCHIVE );
|
|
|
|
|
|
// cgame might not be initialized before menu is used
|
|
Cvar_Get ("cg_viewsize", "100", CVAR_ARCHIVE_ND );
|
|
// Make sure cg_stereoSeparation is zero as that variable is deprecated and should not be used anymore.
|
|
Cvar_Get ("cg_stereoSeparation", "0", CVAR_ROM);
|
|
|
|
//
|
|
// register client commands
|
|
//
|
|
Cmd_AddCommand ("cmd", CL_ForwardToServer_f);
|
|
Cmd_AddCommand ("configstrings", CL_Configstrings_f);
|
|
Cmd_AddCommand ("clientinfo", CL_Clientinfo_f);
|
|
Cmd_AddCommand ("snd_restart", CL_Snd_Restart_f);
|
|
Cmd_AddCommand ("vid_restart", CL_Vid_Restart_f);
|
|
Cmd_AddCommand ("disconnect", CL_Disconnect_f);
|
|
Cmd_AddCommand ("record", CL_Record_f);
|
|
Cmd_SetCommandCompletionFunc( "record", CL_CompleteRecordName );
|
|
Cmd_AddCommand ("demo", CL_PlayDemo_f);
|
|
Cmd_SetCommandCompletionFunc( "demo", CL_CompleteDemoName );
|
|
Cmd_AddCommand ("cinematic", CL_PlayCinematic_f);
|
|
Cmd_AddCommand ("stoprecord", CL_StopRecord_f);
|
|
Cmd_AddCommand ("connect", CL_Connect_f);
|
|
Cmd_AddCommand ("reconnect", CL_Reconnect_f);
|
|
Cmd_AddCommand ("localservers", CL_LocalServers_f);
|
|
Cmd_AddCommand ("globalservers", CL_GlobalServers_f);
|
|
Cmd_AddCommand ("rcon", CL_Rcon_f);
|
|
Cmd_SetCommandCompletionFunc( "rcon", CL_CompleteRcon );
|
|
Cmd_AddCommand ("ping", CL_Ping_f );
|
|
Cmd_AddCommand ("serverstatus", CL_ServerStatus_f );
|
|
Cmd_AddCommand ("showip", CL_ShowIP_f );
|
|
Cmd_AddCommand ("fs_openedList", CL_OpenedPK3List_f );
|
|
Cmd_AddCommand ("fs_referencedList", CL_ReferencedPK3List_f );
|
|
Cmd_AddCommand ("model", CL_SetModel_f );
|
|
Cmd_AddCommand ("video", CL_Video_f );
|
|
Cmd_AddCommand ("video-pipe", CL_Video_f );
|
|
Cmd_SetCommandCompletionFunc( "video", CL_CompleteVideoName );
|
|
Cmd_AddCommand ("stopvideo", CL_StopVideo_f );
|
|
Cmd_AddCommand ("serverinfo", CL_Serverinfo_f );
|
|
Cmd_AddCommand ("systeminfo", CL_Systeminfo_f );
|
|
|
|
#ifdef USE_CURL
|
|
Cmd_AddCommand( "download", CL_Download_f );
|
|
Cmd_AddCommand( "dlmap", CL_Download_f );
|
|
#endif
|
|
Cmd_AddCommand( "modelist", CL_ModeList_f );
|
|
|
|
Cvar_Set( "cl_running", "1" );
|
|
#ifdef USE_MD5
|
|
CL_GenerateQKey();
|
|
#endif
|
|
Cvar_Get( "cl_guid", "", CVAR_USERINFO | CVAR_ROM | CVAR_PROTECTED );
|
|
CL_UpdateGUID( NULL, 0 );
|
|
|
|
Com_Printf( "----- Client Initialization Complete -----\n" );
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
CL_Shutdown
|
|
|
|
Called on fatal error, quit and dedicated mode switch
|
|
===============
|
|
*/
|
|
void CL_Shutdown( const char *finalmsg, qboolean quit ) {
|
|
static qboolean recursive = qfalse;
|
|
|
|
// check whether the client is running at all.
|
|
if ( !( com_cl_running && com_cl_running->integer ) )
|
|
return;
|
|
|
|
Com_Printf( "----- Client Shutdown (%s) -----\n", finalmsg );
|
|
|
|
if ( recursive ) {
|
|
Com_Printf( "WARNING: Recursive CL_Shutdown()\n" );
|
|
return;
|
|
}
|
|
recursive = qtrue;
|
|
|
|
noGameRestart = quit;
|
|
CL_Disconnect( qfalse );
|
|
|
|
// clear and mute all sounds until next registration
|
|
S_DisableSounds();
|
|
|
|
CL_ShutdownVMs();
|
|
|
|
CL_ShutdownRef( quit ? REF_UNLOAD_DLL : REF_DESTROY_WINDOW );
|
|
|
|
Con_Shutdown();
|
|
|
|
Cmd_RemoveCommand ("cmd");
|
|
Cmd_RemoveCommand ("configstrings");
|
|
Cmd_RemoveCommand ("userinfo");
|
|
Cmd_RemoveCommand ("clientinfo");
|
|
Cmd_RemoveCommand ("snd_restart");
|
|
Cmd_RemoveCommand ("vid_restart");
|
|
Cmd_RemoveCommand ("disconnect");
|
|
Cmd_RemoveCommand ("record");
|
|
Cmd_RemoveCommand ("demo");
|
|
Cmd_RemoveCommand ("cinematic");
|
|
Cmd_RemoveCommand ("stoprecord");
|
|
Cmd_RemoveCommand ("connect");
|
|
Cmd_RemoveCommand ("reconnect");
|
|
Cmd_RemoveCommand ("localservers");
|
|
Cmd_RemoveCommand ("globalservers");
|
|
Cmd_RemoveCommand ("rcon");
|
|
Cmd_RemoveCommand ("ping");
|
|
Cmd_RemoveCommand ("serverstatus");
|
|
Cmd_RemoveCommand ("showip");
|
|
Cmd_RemoveCommand ("fs_openedList");
|
|
Cmd_RemoveCommand ("fs_referencedList");
|
|
Cmd_RemoveCommand ("model");
|
|
Cmd_RemoveCommand ("video");
|
|
Cmd_RemoveCommand ("stopvideo");
|
|
Cmd_RemoveCommand ("serverinfo");
|
|
Cmd_RemoveCommand ("systeminfo");
|
|
Cmd_RemoveCommand ("modelist");
|
|
|
|
#ifdef USE_CURL
|
|
Com_DL_Cleanup( &download );
|
|
|
|
Cmd_RemoveCommand( "download" );
|
|
Cmd_RemoveCommand( "dlmap" );
|
|
#endif
|
|
|
|
CL_ClearInput();
|
|
|
|
Cvar_Set( "cl_running", "0" );
|
|
|
|
recursive = qfalse;
|
|
|
|
Com_Memset( &cls, 0, sizeof( cls ) );
|
|
Key_SetCatcher( 0 );
|
|
Com_Printf( "-----------------------\n" );
|
|
}
|
|
|
|
|
|
static void CL_SetServerInfo(serverInfo_t *server, const char *info, int ping) {
|
|
if (server) {
|
|
if (info) {
|
|
server->clients = atoi(Info_ValueForKey(info, "clients"));
|
|
Q_strncpyz(server->hostName,Info_ValueForKey(info, "hostname"), MAX_NAME_LENGTH);
|
|
Q_strncpyz(server->addonName,Info_ValueForKey(info, "addonname"), MAX_NAME_LENGTH);
|
|
Q_strncpyz(server->mapName, Info_ValueForKey(info, "mapname"), MAX_NAME_LENGTH);
|
|
server->maxClients = atoi(Info_ValueForKey(info, "sv_maxclients"));
|
|
Q_strncpyz(server->game,Info_ValueForKey(info, "game"), MAX_NAME_LENGTH);
|
|
server->gameType = atoi(Info_ValueForKey(info, "gametype"));
|
|
server->netType = atoi(Info_ValueForKey(info, "nettype"));
|
|
server->minPing = atoi(Info_ValueForKey(info, "minping"));
|
|
server->maxPing = atoi(Info_ValueForKey(info, "maxping"));
|
|
server->punkbuster = atoi(Info_ValueForKey(info, "punkbuster"));
|
|
server->g_humanplayers = atoi(Info_ValueForKey(info, "g_humanplayers"));
|
|
server->g_needpass = atoi(Info_ValueForKey(info, "g_needpass"));
|
|
}
|
|
server->ping = ping;
|
|
}
|
|
}
|
|
|
|
|
|
static void CL_SetServerInfoByAddress(const netadr_t *from, const char *info, int ping) {
|
|
int i;
|
|
|
|
for (i = 0; i < MAX_OTHER_SERVERS; i++) {
|
|
if (NET_CompareAdr(from, &cls.localServers[i].adr) ) {
|
|
CL_SetServerInfo(&cls.localServers[i], info, ping);
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < MAX_GLOBAL_SERVERS; i++) {
|
|
if (NET_CompareAdr(from, &cls.globalServers[i].adr)) {
|
|
CL_SetServerInfo(&cls.globalServers[i], info, ping);
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < MAX_OTHER_SERVERS; i++) {
|
|
if (NET_CompareAdr(from, &cls.favoriteServers[i].adr)) {
|
|
CL_SetServerInfo(&cls.favoriteServers[i], info, ping);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_ServerInfoPacket
|
|
===================
|
|
*/
|
|
static void CL_ServerInfoPacket( const netadr_t *from, msg_t *msg ) {
|
|
int i, type, len;
|
|
char info[MAX_INFO_STRING];
|
|
const char *infoString;
|
|
int prot;
|
|
|
|
infoString = MSG_ReadString( msg );
|
|
|
|
// if this isn't the correct protocol version, ignore it
|
|
prot = atoi( Info_ValueForKey( infoString, "protocol" ) );
|
|
if ( prot != OLD_PROTOCOL_VERSION && prot != NEW_PROTOCOL_VERSION && prot != com_protocol->integer ) {
|
|
Com_DPrintf( "Different protocol info packet: %s\n", infoString );
|
|
return;
|
|
}
|
|
|
|
// iterate servers waiting for ping response
|
|
for (i=0; i<MAX_PINGREQUESTS; i++)
|
|
{
|
|
if ( cl_pinglist[i].adr.port && !cl_pinglist[i].time && NET_CompareAdr( from, &cl_pinglist[i].adr ) )
|
|
{
|
|
// calc ping time
|
|
cl_pinglist[i].time = Sys_Milliseconds() - cl_pinglist[i].start;
|
|
if ( cl_pinglist[i].time < 1 )
|
|
{
|
|
cl_pinglist[i].time = 1;
|
|
}
|
|
if ( com_developer->integer )
|
|
{
|
|
Com_Printf( "ping time %dms from %s\n", cl_pinglist[i].time, NET_AdrToString( from ) );
|
|
}
|
|
|
|
// save of info
|
|
Q_strncpyz( cl_pinglist[i].info, infoString, sizeof( cl_pinglist[i].info ) );
|
|
|
|
// tack on the net type
|
|
// NOTE: make sure these types are in sync with the netnames strings in the UI
|
|
switch (from->type)
|
|
{
|
|
case NA_BROADCAST:
|
|
case NA_IP:
|
|
type = 1;
|
|
break;
|
|
#ifdef USE_IPV6
|
|
case NA_IP6:
|
|
type = 2;
|
|
break;
|
|
#endif
|
|
default:
|
|
type = 0;
|
|
break;
|
|
}
|
|
|
|
Info_SetValueForKey( cl_pinglist[i].info, "nettype", va( "%d", type ) );
|
|
CL_SetServerInfoByAddress( from, infoString, cl_pinglist[i].time );
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// if not just sent a local broadcast or pinging local servers
|
|
if (cls.pingUpdateSource != AS_LOCAL) {
|
|
return;
|
|
}
|
|
|
|
for ( i = 0 ; i < MAX_OTHER_SERVERS ; i++ ) {
|
|
// empty slot
|
|
if ( cls.localServers[i].adr.port == 0 ) {
|
|
break;
|
|
}
|
|
|
|
// avoid duplicate
|
|
if ( NET_CompareAdr( from, &cls.localServers[i].adr ) ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( i == MAX_OTHER_SERVERS ) {
|
|
Com_DPrintf( "MAX_OTHER_SERVERS hit, dropping infoResponse\n" );
|
|
return;
|
|
}
|
|
|
|
// add this to the list
|
|
cls.numlocalservers = i+1;
|
|
CL_InitServerInfo( &cls.localServers[i], from );
|
|
|
|
Q_strncpyz( info, MSG_ReadString( msg ), sizeof( info ) );
|
|
len = (int) strlen( info );
|
|
if ( len > 0 ) {
|
|
if ( info[ len-1 ] == '\n' ) {
|
|
info[ len-1 ] = '\0';
|
|
}
|
|
Com_Printf( "%s: %s\n", NET_AdrToStringwPort( from ), info );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_GetServerStatus
|
|
===================
|
|
*/
|
|
static serverStatus_t *CL_GetServerStatus( const netadr_t *from ) {
|
|
int i, oldest, oldestTime;
|
|
|
|
for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) {
|
|
if ( NET_CompareAdr( from, &cl_serverStatusList[i].address ) ) {
|
|
return &cl_serverStatusList[i];
|
|
}
|
|
}
|
|
for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) {
|
|
if ( cl_serverStatusList[i].retrieved ) {
|
|
return &cl_serverStatusList[i];
|
|
}
|
|
}
|
|
oldest = -1;
|
|
oldestTime = 0;
|
|
for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) {
|
|
if (oldest == -1 || cl_serverStatusList[i].startTime < oldestTime) {
|
|
oldest = i;
|
|
oldestTime = cl_serverStatusList[i].startTime;
|
|
}
|
|
}
|
|
return &cl_serverStatusList[oldest];
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_ServerStatus
|
|
===================
|
|
*/
|
|
int CL_ServerStatus( const char *serverAddress, char *serverStatusString, int maxLen ) {
|
|
int i;
|
|
netadr_t to;
|
|
serverStatus_t *serverStatus;
|
|
|
|
// if no server address then reset all server status requests
|
|
if ( !serverAddress ) {
|
|
for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) {
|
|
cl_serverStatusList[i].address.port = 0;
|
|
cl_serverStatusList[i].retrieved = qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
// get the address
|
|
if ( !NET_StringToAdr( serverAddress, &to, NA_UNSPEC ) ) {
|
|
return qfalse;
|
|
}
|
|
serverStatus = CL_GetServerStatus( &to );
|
|
// if no server status string then reset the server status request for this address
|
|
if ( !serverStatusString ) {
|
|
serverStatus->retrieved = qtrue;
|
|
return qfalse;
|
|
}
|
|
|
|
// if this server status request has the same address
|
|
if ( NET_CompareAdr( &to, &serverStatus->address) ) {
|
|
// if we received a response for this server status request
|
|
if (!serverStatus->pending) {
|
|
Q_strncpyz(serverStatusString, serverStatus->string, maxLen);
|
|
serverStatus->retrieved = qtrue;
|
|
serverStatus->startTime = 0;
|
|
return qtrue;
|
|
}
|
|
// resend the request regularly
|
|
else if ( Sys_Milliseconds() - serverStatus->startTime > cl_serverStatusResendTime->integer ) {
|
|
serverStatus->print = qfalse;
|
|
serverStatus->pending = qtrue;
|
|
serverStatus->retrieved = qfalse;
|
|
serverStatus->time = 0;
|
|
serverStatus->startTime = Sys_Milliseconds();
|
|
NET_OutOfBandPrint( NS_CLIENT, &to, "getstatus" );
|
|
return qfalse;
|
|
}
|
|
}
|
|
// if retrieved
|
|
else if ( serverStatus->retrieved ) {
|
|
serverStatus->address = to;
|
|
serverStatus->print = qfalse;
|
|
serverStatus->pending = qtrue;
|
|
serverStatus->retrieved = qfalse;
|
|
serverStatus->startTime = Sys_Milliseconds();
|
|
serverStatus->time = 0;
|
|
NET_OutOfBandPrint( NS_CLIENT, &to, "getstatus" );
|
|
return qfalse;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
CL_ServerStatusResponse
|
|
===================
|
|
*/
|
|
static void CL_ServerStatusResponse( const netadr_t *from, msg_t *msg ) {
|
|
const char *s;
|
|
char info[MAX_INFO_STRING];
|
|
char buf[64], *v[2];
|
|
int i, l, score, ping;
|
|
int len;
|
|
serverStatus_t *serverStatus;
|
|
|
|
serverStatus = NULL;
|
|
for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) {
|
|
if ( NET_CompareAdr( from, &cl_serverStatusList[i].address ) ) {
|
|
serverStatus = &cl_serverStatusList[i];
|
|
break;
|
|
}
|
|
}
|
|
// if we didn't request this server status
|
|
if (!serverStatus) {
|
|
return;
|
|
}
|
|
|
|
s = MSG_ReadStringLine( msg );
|
|
|
|
len = 0;
|
|
Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string)-len, "%s", s);
|
|
|
|
if (serverStatus->print) {
|
|
Com_Printf("Server settings:\n");
|
|
// print cvars
|
|
while (*s) {
|
|
for (i = 0; i < 2 && *s; i++) {
|
|
if (*s == '\\')
|
|
s++;
|
|
l = 0;
|
|
while (*s) {
|
|
info[l++] = *s;
|
|
if (l >= MAX_INFO_STRING-1)
|
|
break;
|
|
s++;
|
|
if (*s == '\\') {
|
|
break;
|
|
}
|
|
}
|
|
info[l] = '\0';
|
|
if (i) {
|
|
Com_Printf("%s\n", info);
|
|
}
|
|
else {
|
|
Com_Printf("%-24s", info);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
len = strlen(serverStatus->string);
|
|
Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string)-len, "\\");
|
|
|
|
if (serverStatus->print) {
|
|
Com_Printf("\nPlayers:\n");
|
|
Com_Printf("num: score: ping: name:\n");
|
|
}
|
|
for (i = 0, s = MSG_ReadStringLine( msg ); *s; s = MSG_ReadStringLine( msg ), i++) {
|
|
|
|
len = strlen(serverStatus->string);
|
|
Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string)-len, "\\%s", s);
|
|
|
|
if (serverStatus->print) {
|
|
//score = ping = 0;
|
|
//sscanf(s, "%d %d", &score, &ping);
|
|
Q_strncpyz( buf, s, sizeof (buf) );
|
|
Com_Split( buf, v, 2, ' ' );
|
|
score = atoi( v[0] );
|
|
ping = atoi( v[1] );
|
|
s = strchr(s, ' ');
|
|
if (s)
|
|
s = strchr(s+1, ' ');
|
|
if (s)
|
|
s++;
|
|
else
|
|
s = "unknown";
|
|
Com_Printf("%-2d %-3d %-3d %s\n", i, score, ping, s );
|
|
}
|
|
}
|
|
len = strlen(serverStatus->string);
|
|
Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string)-len, "\\");
|
|
|
|
serverStatus->time = Sys_Milliseconds();
|
|
serverStatus->address = *from;
|
|
serverStatus->pending = qfalse;
|
|
if (serverStatus->print) {
|
|
serverStatus->retrieved = qtrue;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_LocalServers_f
|
|
==================
|
|
*/
|
|
static void CL_LocalServers_f( void ) {
|
|
char *message;
|
|
int i, j, n;
|
|
netadr_t to;
|
|
|
|
Com_Printf( "Scanning for servers on the local network...\n");
|
|
|
|
// reset the list, waiting for response
|
|
cls.numlocalservers = 0;
|
|
cls.pingUpdateSource = AS_LOCAL;
|
|
|
|
for (i = 0; i < MAX_OTHER_SERVERS; i++) {
|
|
qboolean b = cls.localServers[i].visible;
|
|
Com_Memset(&cls.localServers[i], 0, sizeof(cls.localServers[i]));
|
|
cls.localServers[i].visible = b;
|
|
}
|
|
Com_Memset( &to, 0, sizeof( to ) );
|
|
|
|
// The 'xxx' in the message is a challenge that will be echoed back
|
|
// by the server. We don't care about that here, but master servers
|
|
// can use that to prevent spoofed server responses from invalid ip
|
|
message = "\377\377\377\377getinfo xxx";
|
|
n = (int)strlen( message );
|
|
|
|
// send each message twice in case one is dropped
|
|
for ( i = 0 ; i < 2 ; i++ ) {
|
|
// send a broadcast packet on each server port
|
|
// we support multiple server ports so a single machine
|
|
// can nicely run multiple servers
|
|
for ( j = 0 ; j < NUM_SERVER_PORTS ; j++ ) {
|
|
to.port = BigShort( (short)(PORT_SERVER + j) );
|
|
|
|
to.type = NA_BROADCAST;
|
|
NET_SendPacket( NS_CLIENT, n, message, &to );
|
|
#ifdef USE_IPV6
|
|
to.type = NA_MULTICAST6;
|
|
NET_SendPacket( NS_CLIENT, n, message, &to );
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_GlobalServers_f
|
|
|
|
Originally master 0 was Internet and master 1 was MPlayer.
|
|
ioquake3 2008; added support for requesting five separate master servers using 0-4.
|
|
ioquake3 2017; made master 0 fetch all master servers and 1-5 request a single master server.
|
|
==================
|
|
*/
|
|
static void CL_GlobalServers_f( void ) {
|
|
netadr_t to;
|
|
int count, i, masterNum;
|
|
char command[1024];
|
|
const char *masteraddress;
|
|
|
|
if ( (count = Cmd_Argc()) < 3 || (masterNum = atoi(Cmd_Argv(1))) < 0 || masterNum > MAX_MASTER_SERVERS )
|
|
{
|
|
Com_Printf( "usage: globalservers <master# 0-%d> <protocol> [keywords]\n", MAX_MASTER_SERVERS );
|
|
return;
|
|
}
|
|
|
|
// request from all master servers
|
|
if ( masterNum == 0 ) {
|
|
int numAddress = 0;
|
|
|
|
for ( i = 1; i <= MAX_MASTER_SERVERS; i++ ) {
|
|
sprintf( command, "sv_master%d", i );
|
|
masteraddress = Cvar_VariableString( command );
|
|
|
|
if ( !*masteraddress )
|
|
continue;
|
|
|
|
numAddress++;
|
|
|
|
Com_sprintf( command, sizeof( command ), "globalservers %d %s %s\n", i, Cmd_Argv( 2 ), Cmd_ArgsFrom( 3 ) );
|
|
Cbuf_AddText( command );
|
|
}
|
|
|
|
if ( !numAddress ) {
|
|
Com_Printf( "CL_GlobalServers_f: Error: No master server addresses.\n");
|
|
}
|
|
return;
|
|
}
|
|
|
|
sprintf( command, "sv_master%d", masterNum );
|
|
masteraddress = Cvar_VariableString( command );
|
|
|
|
if ( !*masteraddress )
|
|
{
|
|
Com_Printf( "CL_GlobalServers_f: Error: No master server address given.\n");
|
|
return;
|
|
}
|
|
|
|
// reset the list, waiting for response
|
|
// -1 is used to distinguish a "no response"
|
|
|
|
i = NET_StringToAdr( masteraddress, &to, NA_UNSPEC );
|
|
|
|
if ( i == 0 )
|
|
{
|
|
Com_Printf( "CL_GlobalServers_f: Error: could not resolve address of master %s\n", masteraddress );
|
|
return;
|
|
}
|
|
else if ( i == 2 )
|
|
to.port = BigShort( PORT_MASTER );
|
|
|
|
Com_Printf( "Requesting servers from %s (%s)...\n", masteraddress, NET_AdrToStringwPort( &to ) );
|
|
|
|
cls.numglobalservers = -1;
|
|
cls.pingUpdateSource = AS_GLOBAL;
|
|
|
|
// Use the extended query for IPv6 masters
|
|
#ifdef USE_IPV6
|
|
if ( to.type == NA_IP6 || to.type == NA_MULTICAST6 )
|
|
{
|
|
int v4enabled = Cvar_VariableIntegerValue( "net_enabled" ) & NET_ENABLEV4;
|
|
|
|
if ( v4enabled )
|
|
{
|
|
Com_sprintf( command, sizeof( command ), "getserversExt %s %s",
|
|
GAMENAME_FOR_MASTER, Cmd_Argv(2) );
|
|
}
|
|
else
|
|
{
|
|
Com_sprintf( command, sizeof( command ), "getserversExt %s %s ipv6",
|
|
GAMENAME_FOR_MASTER, Cmd_Argv(2) );
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
Com_sprintf( command, sizeof( command ), "getservers %s", Cmd_Argv(2) );
|
|
|
|
for ( i = 3; i < count; i++ )
|
|
{
|
|
Q_strcat( command, sizeof( command ), " " );
|
|
Q_strcat( command, sizeof( command ), Cmd_Argv( i ) );
|
|
}
|
|
|
|
NET_OutOfBandPrint( NS_SERVER, &to, "%s", command );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_GetPing
|
|
==================
|
|
*/
|
|
void CL_GetPing( int n, char *buf, int buflen, int *pingtime )
|
|
{
|
|
const char *str;
|
|
int time;
|
|
int maxPing;
|
|
|
|
if (n < 0 || n >= MAX_PINGREQUESTS || !cl_pinglist[n].adr.port)
|
|
{
|
|
// empty or invalid slot
|
|
buf[0] = '\0';
|
|
*pingtime = 0;
|
|
return;
|
|
}
|
|
|
|
str = NET_AdrToStringwPort( &cl_pinglist[n].adr );
|
|
Q_strncpyz( buf, str, buflen );
|
|
|
|
time = cl_pinglist[n].time;
|
|
if ( time == 0 )
|
|
{
|
|
// check for timeout
|
|
time = Sys_Milliseconds() - cl_pinglist[n].start;
|
|
maxPing = Cvar_VariableIntegerValue( "cl_maxPing" );
|
|
if ( time < maxPing )
|
|
{
|
|
// not timed out yet
|
|
time = 0;
|
|
}
|
|
}
|
|
|
|
CL_SetServerInfoByAddress(&cl_pinglist[n].adr, cl_pinglist[n].info, cl_pinglist[n].time);
|
|
|
|
*pingtime = time;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_GetPingInfo
|
|
==================
|
|
*/
|
|
void CL_GetPingInfo( int n, char *buf, int buflen )
|
|
{
|
|
if (n < 0 || n >= MAX_PINGREQUESTS || !cl_pinglist[n].adr.port)
|
|
{
|
|
// empty or invalid slot
|
|
if (buflen)
|
|
buf[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
Q_strncpyz( buf, cl_pinglist[n].info, buflen );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_ClearPing
|
|
==================
|
|
*/
|
|
void CL_ClearPing( int n )
|
|
{
|
|
if (n < 0 || n >= MAX_PINGREQUESTS)
|
|
return;
|
|
|
|
cl_pinglist[n].adr.port = 0;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_GetPingQueueCount
|
|
==================
|
|
*/
|
|
int CL_GetPingQueueCount( void )
|
|
{
|
|
int i;
|
|
int count;
|
|
ping_t* pingptr;
|
|
|
|
count = 0;
|
|
pingptr = cl_pinglist;
|
|
|
|
for (i=0; i<MAX_PINGREQUESTS; i++, pingptr++ ) {
|
|
if (pingptr->adr.port) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return (count);
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_GetFreePing
|
|
==================
|
|
*/
|
|
static ping_t* CL_GetFreePing( void )
|
|
{
|
|
ping_t* pingptr;
|
|
ping_t* best;
|
|
int oldest;
|
|
int i;
|
|
int time, msec;
|
|
|
|
msec = Sys_Milliseconds();
|
|
pingptr = cl_pinglist;
|
|
for ( i = 0; i < ARRAY_LEN( cl_pinglist ); i++, pingptr++ )
|
|
{
|
|
// find free ping slot
|
|
if ( pingptr->adr.port )
|
|
{
|
|
if ( pingptr->time == 0 )
|
|
{
|
|
if ( msec - pingptr->start < 500 )
|
|
{
|
|
// still waiting for response
|
|
continue;
|
|
}
|
|
}
|
|
else if ( pingptr->time < 500 )
|
|
{
|
|
// results have not been queried
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// clear it
|
|
pingptr->adr.port = 0;
|
|
return pingptr;
|
|
}
|
|
|
|
// use oldest entry
|
|
pingptr = cl_pinglist;
|
|
best = cl_pinglist;
|
|
oldest = INT_MIN;
|
|
for ( i = 0; i < ARRAY_LEN( cl_pinglist ); i++, pingptr++ )
|
|
{
|
|
// scan for oldest
|
|
time = msec - pingptr->start;
|
|
if ( time > oldest )
|
|
{
|
|
oldest = time;
|
|
best = pingptr;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_Ping_f
|
|
==================
|
|
*/
|
|
static void CL_Ping_f( void ) {
|
|
netadr_t to;
|
|
ping_t* pingptr;
|
|
const char* server;
|
|
int argc;
|
|
netadrtype_t family = NA_UNSPEC;
|
|
|
|
argc = Cmd_Argc();
|
|
|
|
if ( argc != 2 && argc != 3 ) {
|
|
Com_Printf( "usage: ping [-4|-6] <server>\n");
|
|
return;
|
|
}
|
|
|
|
if ( argc == 2 )
|
|
server = Cmd_Argv(1);
|
|
else
|
|
{
|
|
if( !strcmp( Cmd_Argv(1), "-4" ) )
|
|
family = NA_IP;
|
|
#ifdef USE_IPV6
|
|
else if( !strcmp( Cmd_Argv(1), "-6" ) )
|
|
family = NA_IP6;
|
|
else
|
|
Com_Printf( "warning: only -4 or -6 as address type understood.\n" );
|
|
#else
|
|
else
|
|
Com_Printf( "warning: only -4 as address type understood.\n" );
|
|
#endif
|
|
|
|
server = Cmd_Argv(2);
|
|
}
|
|
|
|
Com_Memset( &to, 0, sizeof( to ) );
|
|
|
|
if ( !NET_StringToAdr( server, &to, family ) ) {
|
|
return;
|
|
}
|
|
|
|
pingptr = CL_GetFreePing();
|
|
|
|
memcpy( &pingptr->adr, &to, sizeof (netadr_t) );
|
|
pingptr->start = Sys_Milliseconds();
|
|
pingptr->time = 0;
|
|
|
|
CL_SetServerInfoByAddress( &pingptr->adr, NULL, 0 );
|
|
|
|
NET_OutOfBandPrint( NS_CLIENT, &to, "getinfo xxx" );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_UpdateVisiblePings_f
|
|
==================
|
|
*/
|
|
qboolean CL_UpdateVisiblePings_f(int source) {
|
|
int slots, i;
|
|
char buff[MAX_STRING_CHARS];
|
|
int pingTime;
|
|
int max;
|
|
qboolean status = qfalse;
|
|
|
|
if (source < 0 || source > AS_FAVORITES) {
|
|
return qfalse;
|
|
}
|
|
|
|
cls.pingUpdateSource = source;
|
|
|
|
slots = CL_GetPingQueueCount();
|
|
if (slots < MAX_PINGREQUESTS) {
|
|
serverInfo_t *server = NULL;
|
|
|
|
switch (source) {
|
|
case AS_LOCAL :
|
|
server = &cls.localServers[0];
|
|
max = cls.numlocalservers;
|
|
break;
|
|
case AS_GLOBAL :
|
|
server = &cls.globalServers[0];
|
|
max = cls.numglobalservers;
|
|
break;
|
|
case AS_FAVORITES :
|
|
server = &cls.favoriteServers[0];
|
|
max = cls.numfavoriteservers;
|
|
break;
|
|
default:
|
|
return qfalse;
|
|
}
|
|
for (i = 0; i < max; i++) {
|
|
if (server[i].visible) {
|
|
if (server[i].ping == -1) {
|
|
int j;
|
|
|
|
if (slots >= MAX_PINGREQUESTS) {
|
|
break;
|
|
}
|
|
for (j = 0; j < MAX_PINGREQUESTS; j++) {
|
|
if (!cl_pinglist[j].adr.port) {
|
|
continue;
|
|
}
|
|
if (NET_CompareAdr( &cl_pinglist[j].adr, &server[i].adr)) {
|
|
// already on the list
|
|
break;
|
|
}
|
|
}
|
|
if (j >= MAX_PINGREQUESTS) {
|
|
status = qtrue;
|
|
for (j = 0; j < MAX_PINGREQUESTS; j++) {
|
|
if (!cl_pinglist[j].adr.port) {
|
|
memcpy(&cl_pinglist[j].adr, &server[i].adr, sizeof(netadr_t));
|
|
cl_pinglist[j].start = Sys_Milliseconds();
|
|
cl_pinglist[j].time = 0;
|
|
NET_OutOfBandPrint(NS_CLIENT, &cl_pinglist[j].adr, "getinfo xxx");
|
|
slots++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if the server has a ping higher than cl_maxPing or
|
|
// the ping packet got lost
|
|
else if (server[i].ping == 0) {
|
|
// if we are updating global servers
|
|
if (source == AS_GLOBAL) {
|
|
//
|
|
if ( cls.numGlobalServerAddresses > 0 ) {
|
|
// overwrite this server with one from the additional global servers
|
|
cls.numGlobalServerAddresses--;
|
|
CL_InitServerInfo(&server[i], &cls.globalServerAddresses[cls.numGlobalServerAddresses]);
|
|
// NOTE: the server[i].visible flag stays untouched
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (slots) {
|
|
status = qtrue;
|
|
}
|
|
for (i = 0; i < MAX_PINGREQUESTS; i++) {
|
|
if (!cl_pinglist[i].adr.port) {
|
|
continue;
|
|
}
|
|
CL_GetPing( i, buff, MAX_STRING_CHARS, &pingTime );
|
|
if (pingTime != 0) {
|
|
CL_ClearPing(i);
|
|
status = qtrue;
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_ServerStatus_f
|
|
==================
|
|
*/
|
|
static void CL_ServerStatus_f( void ) {
|
|
netadr_t to, *toptr = NULL;
|
|
const char *server;
|
|
serverStatus_t *serverStatus;
|
|
int argc;
|
|
netadrtype_t family = NA_UNSPEC;
|
|
|
|
argc = Cmd_Argc();
|
|
|
|
if ( argc != 2 && argc != 3 )
|
|
{
|
|
if (cls.state != CA_ACTIVE || clc.demoplaying)
|
|
{
|
|
Com_Printf( "Not connected to a server.\n" );
|
|
#ifdef USE_IPV6
|
|
Com_Printf( "usage: serverstatus [-4|-6] <server>\n" );
|
|
#else
|
|
Com_Printf("usage: serverstatus <server>\n");
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
toptr = &clc.serverAddress;
|
|
}
|
|
|
|
if ( !toptr )
|
|
{
|
|
Com_Memset( &to, 0, sizeof( to ) );
|
|
|
|
if ( argc == 2 )
|
|
server = Cmd_Argv(1);
|
|
else
|
|
{
|
|
if ( !strcmp( Cmd_Argv(1), "-4" ) )
|
|
family = NA_IP;
|
|
#ifdef USE_IPV6
|
|
else if ( !strcmp( Cmd_Argv(1), "-6" ) )
|
|
family = NA_IP6;
|
|
else
|
|
Com_Printf( "warning: only -4 or -6 as address type understood.\n" );
|
|
#else
|
|
else
|
|
Com_Printf( "warning: only -4 as address type understood.\n" );
|
|
#endif
|
|
|
|
server = Cmd_Argv(2);
|
|
}
|
|
|
|
toptr = &to;
|
|
if ( !NET_StringToAdr( server, toptr, family ) )
|
|
return;
|
|
}
|
|
|
|
NET_OutOfBandPrint( NS_CLIENT, toptr, "getstatus" );
|
|
|
|
serverStatus = CL_GetServerStatus( toptr );
|
|
serverStatus->address = *toptr;
|
|
serverStatus->print = qtrue;
|
|
serverStatus->pending = qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_ShowIP_f
|
|
==================
|
|
*/
|
|
static void CL_ShowIP_f( void ) {
|
|
Sys_ShowIP();
|
|
}
|
|
|
|
|
|
#ifdef USE_CURL
|
|
|
|
qboolean CL_Download( const char *cmd, const char *pakname, qboolean autoDownload )
|
|
{
|
|
char url[MAX_OSPATH];
|
|
char name[MAX_CVAR_VALUE_STRING];
|
|
const char *s;
|
|
|
|
if ( cl_dlURL->string[0] == '\0' )
|
|
{
|
|
Com_Printf( S_COLOR_YELLOW "cl_dlURL cvar is not set\n" );
|
|
return qfalse;
|
|
}
|
|
|
|
// skip leading slashes
|
|
while ( *pakname == '/' || *pakname == '\\' )
|
|
pakname++;
|
|
|
|
// skip gamedir
|
|
s = strrchr( pakname, '/' );
|
|
if ( s )
|
|
pakname = s+1;
|
|
|
|
if ( !Com_DL_ValidFileName( pakname ) )
|
|
{
|
|
Com_Printf( S_COLOR_YELLOW "invalid file name: '%s'.\n", pakname );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !Q_stricmp( cmd, "dlmap" ) )
|
|
{
|
|
Q_strncpyz( name, pakname, sizeof( name ) );
|
|
FS_StripExt( name, ".pk3" );
|
|
if ( !name[0] )
|
|
return qfalse;
|
|
s = va( "maps/%s.bsp", name );
|
|
if ( FS_FileIsInPAK( s, NULL, url ) )
|
|
{
|
|
Com_Printf( S_COLOR_YELLOW " map %s already exists in %s.pk3\n", name, url );
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
return Com_DL_Begin( &download, pakname, cl_dlURL->string, autoDownload );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
CL_Download_f
|
|
==================
|
|
*/
|
|
static void CL_Download_f( void )
|
|
{
|
|
if ( Cmd_Argc() < 2 || *Cmd_Argv( 1 ) == '\0' )
|
|
{
|
|
Com_Printf( "usage: %s <mapname>\n", Cmd_Argv( 0 ) );
|
|
return;
|
|
}
|
|
|
|
if ( !strcmp( Cmd_Argv(1), "-" ) )
|
|
{
|
|
Com_DL_Cleanup( &download );
|
|
return;
|
|
}
|
|
|
|
CL_Download( Cmd_Argv( 0 ), Cmd_Argv( 1 ), qfalse );
|
|
}
|
|
#endif // USE_CURL
|