/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. Copyright (C) 2000-2006 Tim Angus This file is part of Tremulous. Tremulous is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Tremulous is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tremulous; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // cl_main.c -- client main loop #include "client.h" #include #ifdef USE_MUMBLE #include "libmumblelink.h" #endif #ifdef USE_MUMBLE cvar_t *cl_useMumble; cvar_t *cl_mumbleScale; #endif #ifdef USE_VOIP cvar_t *cl_voipUseVAD; cvar_t *cl_voipVADThreshold; cvar_t *cl_voipSend; cvar_t *cl_voipSendTarget; cvar_t *cl_voipGainDuringCapture; cvar_t *cl_voipCaptureMult; cvar_t *cl_voipShowMeter; cvar_t *cl_voip; #endif cvar_t *cl_nodelta; cvar_t *cl_debugMove; cvar_t *cl_noprint; cvar_t *cl_motd; cvar_t *rcon_client_password; cvar_t *rconAddress; cvar_t *cl_timeout; cvar_t *cl_maxpackets; cvar_t *cl_packetdup; cvar_t *cl_timeNudge; cvar_t *cl_showTimeDelta; cvar_t *cl_freezeDemo; cvar_t *cl_shownet; cvar_t *cl_showSend; cvar_t *cl_timedemo; cvar_t *cl_timedemoLog; cvar_t *cl_autoRecordDemo; cvar_t *cl_aviFrameRate; cvar_t *cl_aviMotionJpeg; cvar_t *cl_forceavidemo; cvar_t *cl_freelook; cvar_t *cl_sensitivity; cvar_t *cl_mouseAccel; cvar_t *cl_showMouseRate; cvar_t *m_pitch; cvar_t *m_yaw; cvar_t *m_forward; cvar_t *m_side; cvar_t *m_filter; cvar_t *cl_activeAction; cvar_t *cl_motdString; cvar_t *cl_allowDownload; cvar_t *cl_conXOffset; cvar_t *cl_inGameVideo; cvar_t *cl_serverStatusResendTime; cvar_t *cl_trn; cvar_t *cl_lanForcePackets; cvar_t *cl_guidServerUniq; cvar_t *cl_consoleKeys; clientActive_t cl; clientConnection_t clc; clientStatic_t cls; vm_t *cgvm; // Structure containing functions exported from refresh DLL refexport_t re; 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; serverStatus_t cl_serverStatusList[MAX_SERVERSTATUSREQUESTS]; int serverStatusCount; #if defined __USEA3D && defined __A3D_GEOM void hA3Dg_ExportRenderGeom (refexport_t *incoming_re); #endif extern void SV_BotFrame( int time ); void CL_CheckForResend( void ); void CL_ShowIP_f(void); void CL_ServerStatus_f(void); void CL_ServerStatusResponse( netadr_t from, msg_t *msg ); /* =============== CL_CDDialog Called by Com_Error when a cd is needed =============== */ void CL_CDDialog( void ) { cls.cddialog = qtrue; // start it next frame } #ifdef USE_MUMBLE static void CL_UpdateMumble(void) { vec3_t pos, forward, up; float scale = cl_mumbleScale->value; float tmp; if(!cl_useMumble->integer) return; // !!! FIXME: not sure if this is even close to correct. AngleVectors( cl.snap.ps.viewangles, forward, NULL, up); pos[0] = cl.snap.ps.origin[0] * scale; pos[1] = cl.snap.ps.origin[2] * scale; pos[2] = cl.snap.ps.origin[1] * scale; tmp = forward[1]; forward[1] = forward[2]; forward[2] = tmp; tmp = up[1]; up[1] = up[2]; up[2] = tmp; if(cl_useMumble->integer > 1) { fprintf(stderr, "%f %f %f, %f %f %f, %f %f %f\n", pos[0], pos[1], pos[2], forward[0], forward[1], forward[2], up[0], up[1], up[2]); } mumble_update_coordinates(pos, forward, up); } #endif #ifdef USE_VOIP static void CL_UpdateVoipIgnore(const char *idstr, qboolean ignore) { if ((*idstr >= '0') && (*idstr <= '9')) { const int id = atoi(idstr); if ((id >= 0) && (id < MAX_CLIENTS)) { clc.voipIgnore[id] = ignore; CL_AddReliableCommand(va("voip %s %d", ignore ? "ignore" : "unignore", id)); Com_Printf("VoIP: %s ignoring player #%d\n", ignore ? "Now" : "No longer", id); } } } static void CL_UpdateVoipGain(const char *idstr, float gain) { if ((*idstr >= '0') && (*idstr <= '9')) { const int id = atoi(idstr); if (gain < 0.0f) gain = 0.0f; if ((id >= 0) && (id < MAX_CLIENTS)) { clc.voipGain[id] = gain; Com_Printf("VoIP: player #%d gain now set to %f\n", id, gain); } } } void CL_Voip_f( void ) { const char *cmd = Cmd_Argv(1); const char *reason = NULL; if (cls.state != CA_ACTIVE) reason = "Not connected to a server"; else if (!clc.speexInitialized) reason = "Speex not initialized"; else if (!cl_connectedToVoipServer) reason = "Server doesn't support VoIP"; if (reason != NULL) { Com_Printf("VoIP: command ignored: %s\n", reason); return; } if (strcmp(cmd, "ignore") == 0) { CL_UpdateVoipIgnore(Cmd_Argv(2), qtrue); } else if (strcmp(cmd, "unignore") == 0) { CL_UpdateVoipIgnore(Cmd_Argv(2), qfalse); } else if (strcmp(cmd, "gain") == 0) { CL_UpdateVoipGain(Cmd_Argv(2), atof(Cmd_Argv(3))); } else if (strcmp(cmd, "muteall") == 0) { Com_Printf("VoIP: muting incoming voice\n"); CL_AddReliableCommand("voip muteall"); clc.voipMuteAll = qtrue; } else if (strcmp(cmd, "unmuteall") == 0) { Com_Printf("VoIP: unmuting incoming voice\n"); CL_AddReliableCommand("voip unmuteall"); clc.voipMuteAll = qfalse; } } static void CL_VoipNewGeneration(void) { // don't have a zero generation so new clients won't match, and don't // wrap to negative so MSG_ReadLong() doesn't "fail." clc.voipOutgoingGeneration++; if (clc.voipOutgoingGeneration <= 0) clc.voipOutgoingGeneration = 1; clc.voipPower = 0.0f; clc.voipOutgoingSequence = 0; } /* =============== CL_CaptureVoip Record more audio from the hardware if required and encode it into Speex data for later transmission. =============== */ static void CL_CaptureVoip(void) { const float audioMult = cl_voipCaptureMult->value; const qboolean useVad = (cl_voipUseVAD->integer != 0); qboolean initialFrame = qfalse; qboolean finalFrame = qfalse; #if USE_MUMBLE // if we're using Mumble, don't try to handle VoIP transmission ourselves. if (cl_useMumble->integer) return; #endif if (!clc.speexInitialized) return; // just in case this gets called at a bad time. if (clc.voipOutgoingDataSize > 0) return; // packet is pending transmission, don't record more yet. if (cl_voipUseVAD->modified) { Cvar_Set("cl_voipSend", (useVad) ? "1" : "0"); cl_voipUseVAD->modified = qfalse; } if ((useVad) && (!cl_voipSend->integer)) Cvar_Set("cl_voipSend", "1"); // lots of things reset this. if (cl_voipSend->modified) { qboolean dontCapture = qfalse; if (cls.state != CA_ACTIVE) dontCapture = qtrue; // not connected to a server. else if (!cl_connectedToVoipServer) dontCapture = qtrue; // server doesn't support VoIP. else if (clc.demoplaying) dontCapture = qtrue; // playing back a demo. else if ( cl_voip->integer == 0 ) dontCapture = qtrue; // client has VoIP support disabled. else if ( audioMult == 0.0f ) dontCapture = qtrue; // basically silenced incoming audio. cl_voipSend->modified = qfalse; if (dontCapture) { cl_voipSend->integer = 0; return; } if (cl_voipSend->integer) { initialFrame = qtrue; } else { finalFrame = qtrue; } } // try to get more audio data from the sound card... if (initialFrame) { float gain = cl_voipGainDuringCapture->value; if (gain < 0.0f) gain = 0.0f; else if (gain >= 1.0f) gain = 1.0f; S_MasterGain(cl_voipGainDuringCapture->value); S_StartCapture(); CL_VoipNewGeneration(); } if ((cl_voipSend->integer) || (finalFrame)) { // user wants to capture audio? int samples = S_AvailableCaptureSamples(); const int mult = (finalFrame) ? 1 : 12; // 12 == 240ms of audio. // enough data buffered in audio hardware to process yet? if (samples >= (clc.speexFrameSize * mult)) { // audio capture is always MONO16 (and that's what speex wants!). // 2048 will cover 12 uncompressed frames in narrowband mode. static int16_t sampbuffer[2048]; float voipPower = 0.0f; int speexFrames = 0; int wpos = 0; int pos = 0; if (samples > (clc.speexFrameSize * 12)) samples = (clc.speexFrameSize * 12); // !!! FIXME: maybe separate recording from encoding, so voipPower // !!! FIXME: updates faster than 4Hz? samples -= samples % clc.speexFrameSize; S_Capture(samples, (byte *) sampbuffer); // grab from audio card. // this will probably generate multiple speex packets each time. while (samples > 0) { int16_t *sampptr = &sampbuffer[pos]; int i, bytes; // preprocess samples to remove noise... speex_preprocess_run(clc.speexPreprocessor, sampptr); // check the "power" of this packet... for (i = 0; i < clc.speexFrameSize; i++) { const float flsamp = (float) sampptr[i]; const float s = fabs(flsamp); voipPower += s * s; sampptr[i] = (int16_t) ((flsamp) * audioMult); } // encode raw audio samples into Speex data... speex_bits_reset(&clc.speexEncoderBits); speex_encode_int(clc.speexEncoder, sampptr, &clc.speexEncoderBits); bytes = speex_bits_write(&clc.speexEncoderBits, (char *) &clc.voipOutgoingData[wpos+1], sizeof (clc.voipOutgoingData) - (wpos+1)); assert((bytes > 0) && (bytes < 256)); clc.voipOutgoingData[wpos] = (byte) bytes; wpos += bytes + 1; // look at the data for the next packet... pos += clc.speexFrameSize; samples -= clc.speexFrameSize; speexFrames++; } clc.voipPower = (voipPower / (32768.0f * 32768.0f * ((float) (clc.speexFrameSize * speexFrames)))) * 100.0f; if ((useVad) && (clc.voipPower < cl_voipVADThreshold->value)) { CL_VoipNewGeneration(); // no "talk" for at least 1/4 second. } else { clc.voipOutgoingDataSize = wpos; clc.voipOutgoingDataFrames = speexFrames; Com_DPrintf("VoIP: Send %d frames, %d bytes, %f power\n", speexFrames, wpos, clc.voipPower); #if 0 static FILE *encio = NULL; if (encio == NULL) encio = fopen("voip-outgoing-encoded.bin", "wb"); if (encio != NULL) { fwrite(clc.voipOutgoingData, wpos, 1, encio); fflush(encio); } static FILE *decio = NULL; if (decio == NULL) decio = fopen("voip-outgoing-decoded.bin", "wb"); if (decio != NULL) { fwrite(sampbuffer, speexFrames * clc.speexFrameSize * 2, 1, decio); fflush(decio); } #endif } } } // User requested we stop recording, and we've now processed the last of // any previously-buffered data. Pause the capture device, etc. if (finalFrame) { S_StopCapture(); S_MasterGain(1.0f); clc.voipPower = 0.0f; // force this value so it doesn't linger. } } #endif /* ======================================================================= CLIENT RELIABLE COMMAND COMMUNICATION ======================================================================= */ /* ====================== CL_AddReliableCommand The given command will be transmitted to the server, and is gauranteed to not have future usercmd_t executed before it is executed ====================== */ void CL_AddReliableCommand( const char *cmd ) { int index; // if we would be losing an old command that hasn't been acknowledged, // we must drop the connection if ( clc.reliableSequence - clc.reliableAcknowledge > MAX_RELIABLE_COMMANDS ) { 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 ] ) ); } /* ====================== CL_ChangeReliableCommand ====================== */ void CL_ChangeReliableCommand( void ) { int r, index, l; r = clc.reliableSequence - (random() * 5); index = clc.reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); l = strlen(clc.reliableCommands[ index ]); if ( l >= MAX_STRING_CHARS - 1 ) { l = MAX_STRING_CHARS - 2; } clc.reliableCommands[ index ][ l ] = '\n'; clc.reliableCommands[ index ][ l+1 ] = '\0'; } /* ======================================================================= CLIENT SIDE DEMO RECORDING ======================================================================= */ /* ==================== CL_WriteDemoMessage Dumps the current net message, prefixed by the length ==================== */ 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.demofile); // skip the packet sequencing information len = msg->cursize - headerBytes; swlen = LittleLong(len); FS_Write (&swlen, 4, clc.demofile); FS_Write ( msg->data + headerBytes, len, clc.demofile ); } /* ==================== CL_StopRecording_f stop recording a demo ==================== */ void CL_StopRecord_f( void ) { int len; if ( !clc.demorecording ) { Com_Printf ("Not recording a demo.\n"); return; } // finish up len = -1; FS_Write (&len, 4, clc.demofile); FS_Write (&len, 4, clc.demofile); FS_FCloseFile (clc.demofile); clc.demofile = 0; clc.demorecording = qfalse; clc.spDemoRecording = qfalse; Com_Printf ("Stopped demo.\n"); } /* ================== CL_DemoFilename ================== */ void CL_DemoFilename( int number, char *fileName ) { int a,b,c,d; if(number < 0 || number > 9999) number = 9999; a = number / 1000; number -= a*1000; b = number / 100; number -= b*100; c = number / 10; number -= c*10; d = number; Com_sprintf( fileName, MAX_OSPATH, "demo%i%i%i%i" , a, b, c, d ); } /* ==================== CL_Record_f record Begins recording a demo from the current position ==================== */ static char demoName[MAX_QPATH]; // compiler bug workaround void CL_Record_f( void ) { char name[MAX_OSPATH]; byte bufData[MAX_MSGLEN]; msg_t buf; int i; int len; entityState_t *ent; entityState_t nullstate; char *s; if ( Cmd_Argc() > 2 ) { Com_Printf ("record \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_VariableValue( "g_synchronousClients" ) ) { Com_Printf (S_COLOR_YELLOW "WARNING: You should set 'g_synchronousClients 1' for smoother demo recording\n"); } if ( Cmd_Argc() == 2 ) { s = Cmd_Argv(1); Q_strncpyz( demoName, s, sizeof( demoName ) ); Com_sprintf (name, sizeof(name), "demos/%s.dm_%d", demoName, PROTOCOL_VERSION ); } else { int number; // scan for a free demo name for ( number = 0 ; number <= 9999 ; number++ ) { CL_DemoFilename( number, demoName ); Com_sprintf (name, sizeof(name), "demos/%s.dm_%d", demoName, PROTOCOL_VERSION ); if (!FS_FileExists(name)) break; // file doesn't exist } } // open the demo file Com_Printf ("recording to %s.\n", name); clc.demofile = FS_FOpenFileWrite( name ); if ( !clc.demofile ) { Com_Printf ("ERROR: couldn't open.\n"); return; } clc.demorecording = qtrue; if (Cvar_VariableValue("ui_recordSPDemo")) { clc.spDemoRecording = qtrue; } else { clc.spDemoRecording = qfalse; } Q_strncpyz( clc.demoName, demoName, sizeof( clc.demoName ) ); // don't start saving messages until a non-delta compressed message is received clc.demowaiting = qtrue; // write out the gamestate message MSG_Init (&buf, bufData, sizeof(bufData)); MSG_Bitstream(&buf); // NOTE, MRE: all server->client messages now acknowledge MSG_WriteLong( &buf, clc.reliableSequence ); MSG_WriteByte (&buf, svc_gamestate); MSG_WriteLong (&buf, 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 (&buf, svc_configstring); MSG_WriteShort (&buf, i); MSG_WriteBigString (&buf, s); } // baselines Com_Memset (&nullstate, 0, sizeof(nullstate)); for ( i = 0; i < MAX_GENTITIES ; i++ ) { ent = &cl.entityBaselines[i]; if ( !ent->number ) { continue; } MSG_WriteByte (&buf, svc_baseline); MSG_WriteDeltaEntity (&buf, &nullstate, ent, qtrue ); } MSG_WriteByte( &buf, svc_EOF ); // finished writing the gamestate stuff // write the client num MSG_WriteLong(&buf, clc.clientNum); // write the checksum feed MSG_WriteLong(&buf, clc.checksumFeed); // finished writing the client packet MSG_WriteByte( &buf, svc_EOF ); // write it to the demo file len = LittleLong( clc.serverMessageSequence - 1 ); FS_Write (&len, 4, clc.demofile); len = LittleLong (buf.cursize); FS_Write (&len, 4, clc.demofile); FS_Write (buf.data, buf.cursize, clc.demofile); // the rest of the demo file will be copied from net messages } /* ======================================================================= CLIENT SIDE DEMO PLAYBACK ======================================================================= */ /* ================= CL_DemoFrameDurationSDev ================= */ static float CL_DemoFrameDurationSDev( void ) { int i; int numFrames; float mean = 0.0f; float variance = 0.0f; if( ( clc.timeDemoFrames - 1 ) > MAX_TIMEDEMO_DURATIONS ) numFrames = MAX_TIMEDEMO_DURATIONS; else numFrames = clc.timeDemoFrames - 1; for( i = 0; i < numFrames; i++ ) mean += clc.timeDemoDurations[ i ]; mean /= numFrames; for( i = 0; i < numFrames; i++ ) { float x = clc.timeDemoDurations[ i ]; variance += ( ( x - mean ) * ( x - mean ) ); } variance /= numFrames; return sqrt( variance ); } /* ================= CL_DemoCompleted ================= */ void CL_DemoCompleted( void ) { char buffer[ MAX_STRING_CHARS ]; if( cl_timedemo && cl_timedemo->integer ) { int time; time = Sys_Milliseconds() - clc.timeDemoStart; if( time > 0 ) { // Millisecond times are frame durations: // minimum/average/maximum/std deviation Com_sprintf( buffer, sizeof( buffer ), "%i frames %3.1f seconds %3.1f fps %d.0/%.1f/%d.0/%.1f ms\n", clc.timeDemoFrames, time/1000.0, clc.timeDemoFrames*1000.0 / time, clc.timeDemoMinDuration, time / (float)clc.timeDemoFrames, clc.timeDemoMaxDuration, CL_DemoFrameDurationSDev( ) ); Com_Printf( "%s", buffer ); // Write a log of all the frame durations if( cl_timedemoLog && strlen( cl_timedemoLog->string ) > 0 ) { int i; int numFrames; fileHandle_t f; if( ( clc.timeDemoFrames - 1 ) > MAX_TIMEDEMO_DURATIONS ) numFrames = MAX_TIMEDEMO_DURATIONS; else numFrames = clc.timeDemoFrames - 1; f = FS_FOpenFileWrite( cl_timedemoLog->string ); if( f ) { FS_Printf( f, "# %s", buffer ); for( i = 0; i < numFrames; i++ ) FS_Printf( f, "%d\n", clc.timeDemoDurations[ i ] ); FS_FCloseFile( f ); Com_Printf( "%s written\n", cl_timedemoLog->string ); } else { Com_Printf( "Couldn't open %s for writing\n", cl_timedemoLog->string ); } } } } CL_Disconnect( qtrue ); CL_NextDemo(); } /* ================= CL_ReadDemoMessage ================= */ void CL_ReadDemoMessage( void ) { int r; msg_t buf; byte bufData[ MAX_MSGLEN ]; int s; if ( !clc.demofile ) { 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, sizeof( bufData ) ); // 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; CL_ParseServerMessage( &buf ); } /* ==================== CL_WalkDemoExt ==================== */ static void CL_WalkDemoExt(char *arg, char *name, int *demofile) { int i = 0; *demofile = 0; while(demo_protocols[i]) { Com_sprintf (name, MAX_OSPATH, "demos/%s.dm_%d", arg, demo_protocols[i]); FS_FOpenFileRead( name, demofile, qtrue ); if (*demofile) { Com_Printf("Demo file: %s\n", name); break; } else Com_Printf("Not found: %s\n", name); i++; } } /* ==================== CL_CompleteDemoName ==================== */ static void CL_CompleteDemoName( char *args, int argNum ) { if( argNum == 2 ) { char demoExt[ 16 ]; Com_sprintf( demoExt, sizeof( demoExt ), ".dm_%d", PROTOCOL_VERSION ); Field_CompleteFilename( "demos", demoExt, qtrue ); } } /* ==================== CL_PlayDemo_f demo ==================== */ void CL_PlayDemo_f( void ) { char name[MAX_OSPATH]; char *arg, *ext_test; int protocol, i; char retry[MAX_OSPATH]; if (Cmd_Argc() != 2) { Com_Printf ("playdemo \n"); return; } // make sure a local server is killed // 2 means don't force disconnect of local client Cvar_Set( "sv_killserver", "2" ); CL_Disconnect( qtrue ); // open the demo file arg = Cmd_Argv(1); // check for an extension .dm_?? (?? is protocol) ext_test = arg + strlen(arg) - 6; if ((strlen(arg) > 6) && (ext_test[0] == '.') && ((ext_test[1] == 'd') || (ext_test[1] == 'D')) && ((ext_test[2] == 'm') || (ext_test[2] == 'M')) && (ext_test[3] == '_')) { protocol = atoi(ext_test+4); i=0; while(demo_protocols[i]) { if (demo_protocols[i] == protocol) break; i++; } if (demo_protocols[i]) { Com_sprintf (name, sizeof(name), "demos/%s", arg); FS_FOpenFileRead( name, &clc.demofile, qtrue ); } else { Com_Printf("Protocol %d not supported for demos\n", protocol); Q_strncpyz(retry, arg, sizeof(retry)); retry[strlen(retry)-6] = 0; CL_WalkDemoExt( retry, name, &clc.demofile ); } } else { CL_WalkDemoExt( arg, name, &clc.demofile ); } if (!clc.demofile) { Com_Error( ERR_DROP, "couldn't open %s", name); return; } Q_strncpyz( clc.demoName, Cmd_Argv(1), sizeof( clc.demoName ) ); Con_Close(); cls.state = CA_CONNECTED; clc.demoplaying = qtrue; Q_strncpyz( cls.servername, Cmd_Argv(1), sizeof( cls.servername ) ); // read demo messages until connected while ( cls.state >= CA_CONNECTED && cls.state < CA_PRIMED ) { 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_StartDemoLoop Closing the main menu will restart the demo loop ==================== */ void CL_StartDemoLoop( void ) { // start the demo loop again Cbuf_AddText ("d1\n"); Key_SetCatcher( 0 ); } /* ================== CL_NextDemo Called when a demo or cinematic finishes If the "nextdemo" cvar is set, that command will be issued ================== */ void CL_NextDemo( void ) { char v[MAX_STRING_CHARS]; Q_strncpyz( v, Cvar_VariableString ("nextdemo"), sizeof(v) ); v[MAX_STRING_CHARS-1] = 0; Com_DPrintf("CL_NextDemo: %s\n", v ); if (!v[0]) { return; } Cvar_Set ("nextdemo",""); Cbuf_AddText (v); Cbuf_AddText ("\n"); Cbuf_Execute(); } /* ================== CL_DemoState Returns the current state of the demo system ================== */ demoState_t CL_DemoState( void ) { if( clc.demoplaying ) { return DS_PLAYBACK; } else if( clc.demorecording ) { return DS_RECORDING; } else { return DS_NONE; } } /* ================== CL_DemoPos Returns the current position of the demo ================== */ int CL_DemoPos( void ) { if( clc.demoplaying || clc.demorecording ) { return FS_FTell( clc.demofile ); } else { return 0; } } /* ================== CL_DemoName Returns the name of the demo ================== */ void CL_DemoName( char *buffer, int size ) { if( clc.demoplaying || clc.demorecording ) { Q_strncpyz( buffer, clc.demoName, size ); } else if( size >= 1 ) { buffer[ 0 ] = '\0'; } } //====================================================================== /* ===================== CL_ShutdownAll ===================== */ void CL_ShutdownAll(void) { #ifdef USE_CURL CL_cURL_Shutdown(); #endif // clear sounds S_DisableSounds(); // shutdown CGame CL_ShutdownCGame(); // shutdown UI CL_ShutdownUI(); // shutdown the renderer if ( re.Shutdown ) { re.Shutdown( qfalse ); // don't destroy window or context } cls.uiStarted = qfalse; cls.cgameStarted = qfalse; cls.rendererStarted = qfalse; cls.soundRegistered = qfalse; } /* ================= CL_FlushMemory Called by CL_MapLoading, CL_Connect_f, CL_PlayDemo_f, and CL_ParseGamestate the only ways a client gets into a game Also called by Com_Error ================= */ void CL_FlushMemory( void ) { // shutdown all the client stuff CL_ShutdownAll(); // 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_StartHunkUsers( qfalse ); } /* ===================== 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; SCR_UpdateScreen(); } else { CL_Disconnect( qtrue ); Q_strncpyz( cls.servername, "localhost", sizeof(cls.servername) ); cls.state = CA_CHALLENGING; // so the connect screen is drawn Key_SetCatcher( 0 ); 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 ) { 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 ) ); } /* ===================== 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 ===================== */ void CL_Disconnect( qboolean showMainMenu ) { if ( !com_cl_running || !com_cl_running->integer ) { return; } // shutting down the client so enter full screen ui mode Cvar_Set("r_uiFullScreen", "1"); if ( clc.demorecording ) { CL_StopRecord_f (); } if (clc.download) { FS_FCloseFile( clc.download ); clc.download = 0; } *clc.downloadTempName = *clc.downloadName = 0; Cvar_Set( "cl_downloadName", "" ); #ifdef USE_MUMBLE if (cl_useMumble->integer && mumble_islinked()) { Com_Printf("Mumble: Unlinking from Mumble application\n"); mumble_unlink(); } #endif #ifdef USE_VOIP if (cl_voipSend->integer) { int tmp = cl_voipUseVAD->integer; cl_voipUseVAD->integer = 0; // disable this for a moment. clc.voipOutgoingDataSize = 0; // dump any pending VoIP transmission. Cvar_Set("cl_voipSend", "0"); CL_CaptureVoip(); // clean up any state... cl_voipUseVAD->integer = tmp; } if (clc.speexInitialized) { int i; speex_bits_destroy(&clc.speexEncoderBits); speex_encoder_destroy(clc.speexEncoder); speex_preprocess_state_destroy(clc.speexPreprocessor); for (i = 0; i < MAX_CLIENTS; i++) { speex_bits_destroy(&clc.speexDecoderBits[i]); speex_decoder_destroy(clc.speexDecoder[i]); } } Cmd_RemoveCommand ("voip"); #endif if ( clc.demofile ) { FS_FCloseFile( clc.demofile ); clc.demofile = 0; } if ( uivm && showMainMenu ) { VM_Call( uivm, UI_SET_ACTIVE_MENU, UIMENU_NONE ); } SCR_StopCinematic (); S_ClearSoundBuffer(); // send a disconnect message to the server // send it a few times in case one is dropped if ( cls.state >= CA_CONNECTED ) { CL_AddReliableCommand( "disconnect" ); 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 = qfalse; #ifdef USE_VOIP // not connected to voip server anymore. cl_connectedToVoipServer = qfalse; #endif // Stop recording any video if( CL_VideoRecording( ) ) { // Finish rendering current frame SCR_UpdateScreen( ); CL_CloseAVI( ); } CL_UpdateGUID( NULL, 0 ); } /* =================== 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 ) { char *cmd; cmd = Cmd_Argv(0); // ignore key up commands if ( cmd[0] == '-' ) { 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 ); } else { CL_AddReliableCommand( cmd ); } } /* =================== CL_RequestMotd =================== */ void CL_RequestMotd( void ) { char info[MAX_INFO_STRING]; if ( !cl_motd->integer ) { return; } Com_Printf( "Resolving %s\n", MASTER_SERVER_NAME ); if ( !NET_StringToAdr( MASTER_SERVER_NAME, &cls.updateServer, NA_IP ) ) { Com_Printf( "Couldn't resolve address\n" ); return; } cls.updateServer.port = BigShort( PORT_MASTER ); Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", MASTER_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", info ); } /* ====================================================================== CONSOLE COMMANDS ====================================================================== */ /* ================== CL_ForwardToServer_f ================== */ void CL_ForwardToServer_f( void ) { if ( cls.state != CA_ACTIVE || clc.demoplaying ) { Com_Printf ("Not connected to a server.\n"); return; } // don't forward the first argument if ( Cmd_Argc() > 1 ) { CL_AddReliableCommand( Cmd_Args() ); } } /* ================== CL_Setenv_f Mostly for controlling voodoo environment variables ================== */ void CL_Setenv_f( void ) { int argc = Cmd_Argc(); if ( argc > 2 ) { char buffer[1024]; int i; strcpy( buffer, Cmd_Argv(1) ); strcat( buffer, "=" ); for ( i = 2; i < argc; i++ ) { strcat( buffer, Cmd_Argv( i ) ); strcat( buffer, " " ); } putenv( buffer ); } else if ( argc == 2 ) { char *env = getenv( Cmd_Argv(1) ); if ( env ) { Com_Printf( "%s=%s\n", Cmd_Argv(1), env ); } else { Com_Printf( "%s undefined\n", Cmd_Argv(1)); } } } /* ================== CL_Disconnect_f ================== */ void CL_Disconnect_f( void ) { SCR_StopCinematic(); Cvar_Set("ui_singlePlayerActive", "0"); if ( cls.state != CA_DISCONNECTED && cls.state != CA_CINEMATIC ) { Com_Error (ERR_DISCONNECT, "Disconnected from server"); } } /* ================ CL_Reconnect_f ================ */ void CL_Reconnect_f( void ) { if ( !strlen( cls.servername ) || !strcmp( cls.servername, "localhost" ) ) { Com_Printf( "Can't reconnect to localhost.\n" ); return; } Cvar_Set("ui_singlePlayerActive", "0"); Cbuf_AddText( va("connect %s\n", cls.servername ) ); } /* ================ CL_Connect_f ================ */ void CL_Connect_f( void ) { char *server; const char *serverString; int argc = Cmd_Argc(); netadrtype_t 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; else if(!strcmp(Cmd_Argv(1), "-6")) family = NA_IP6; else Com_Printf( "warning: only -4 or -6 as address type understood.\n"); server = Cmd_Argv(2); } Cvar_Set("ui_singlePlayerActive", "0"); // fire a message off to the motd server CL_RequestMotd(); // clear any previous "server full" type messages clc.serverMessage[0] = 0; if ( com_sv_running->integer && !strcmp( server, "localhost" ) ) { // if running a local server, kill it SV_Shutdown( "Server quit" ); } // make sure a local server is killed Cvar_Set( "sv_killserver", "1" ); SV_Frame( 0 ); CL_Disconnect( qtrue ); Con_Close(); Q_strncpyz( cls.servername, server, sizeof(cls.servername) ); if (!NET_StringToAdr(cls.servername, &clc.serverAddress, family) ) { Com_Printf ("Bad server address\n"); cls.state = CA_DISCONNECTED; return; } 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; } Key_SetCatcher( 0 ); clc.connectTime = -99999; // CL_CheckForResend() will fire immediately clc.connectPacketCount = 0; // server connection string Cvar_Set( "cl_currentServerAddress", server ); } #define MAX_RCON_MESSAGE 1024 /* ================== CL_CompleteRcon ================== */ static void CL_CompleteRcon( char *args, int argNum ) { if( argNum == 2 ) { // Skip "rcon " 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. ===================== */ void CL_Rcon_f( void ) { char message[MAX_RCON_MESSAGE]; netadr_t to; if ( !rcon_client_password->string ) { Com_Printf ("You must set 'rconpassword' before\n" "issuing an rcon command.\n"); return; } message[0] = -1; message[1] = -1; message[2] = -1; message[3] = -1; message[4] = 0; Q_strcat (message, MAX_RCON_MESSAGE, "rcon "); Q_strcat (message, MAX_RCON_MESSAGE, rcon_client_password->string); Q_strcat (message, MAX_RCON_MESSAGE, " "); // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=543 Q_strcat (message, MAX_RCON_MESSAGE, Cmd_Cmd()+5); if ( cls.state >= CA_CONNECTED ) { to = clc.netchan.remoteAddress; } else { if (!strlen(rconAddress->string)) { Com_Printf ("You must either be connected,\n" "or set the 'rconAddress' cvar\n" "to issue rcon commands\n"); return; } NET_StringToAdr (rconAddress->string, &to, NA_UNSPEC); if (to.port == 0) { to.port = BigShort (PORT_SERVER); } } NET_SendPacket (NS_CLIENT, strlen(message)+1, message, to); } /* ================= CL_SendPureChecksums ================= */ void CL_SendPureChecksums( void ) { char cMsg[MAX_INFO_VALUE]; // if we are pure we need to send back a command with our referenced pk3 checksums Com_sprintf(cMsg, sizeof(cMsg), "cp %d %s", cl.serverId, FS_ReferencedPakPureChecksums()); CL_AddReliableCommand( cMsg ); } /* ================= CL_ResetPureClientAtServer ================= */ void CL_ResetPureClientAtServer( void ) { CL_AddReliableCommand( va("vdr") ); } /* ================= CL_Vid_Restart_f Restart the video subsystem we also have to reload the UI and CGame because the renderer doesn't know what graphics to reload ================= */ void CL_Vid_Restart_f( void ) { // Settings may have changed so stop recording now if( CL_VideoRecording( ) ) { CL_CloseAVI( ); } if(clc.demorecording) CL_StopRecord_f(); // don't let them loop during the restart S_StopAllSounds(); // shutdown the UI CL_ShutdownUI(); // shutdown the CGame CL_ShutdownCGame(); // shutdown the renderer and clear the renderer interface CL_ShutdownRef(); // client is no longer pure untill 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 FS_ConditionalRestart( clc.checksumFeed ); cls.rendererStarted = qfalse; cls.uiStarted = qfalse; cls.cgameStarted = qfalse; cls.soundRegistered = qfalse; // unpause so the cgame definately gets a snapshot and renders a frame Cvar_Set( "cl_paused", "0" ); // if not running a server clear the whole hunk if ( !com_sv_running->integer ) { // clear the whole hunk Hunk_Clear(); } else { // clear all the client data on the hunk Hunk_ClearToMark(); } // initialize the renderer interface CL_InitRef(); // startup all the client stuff CL_StartHunkUsers( qfalse ); // start the cgame if connected if ( cls.state > CA_CONNECTED && cls.state != CA_CINEMATIC ) { cls.cgameStarted = qtrue; CL_InitCGame(); // send pure checksums CL_SendPureChecksums(); } } /* ================= CL_Snd_Restart_f Restart the sound subsystem The cgame and game must also be forced to restart because handles will be invalid ================= */ void CL_Snd_Restart_f( void ) { S_Shutdown(); S_Init(); CL_Vid_Restart_f(); } /* ================== CL_PK3List_f ================== */ void CL_OpenedPK3List_f( void ) { Com_Printf("Opened PK3 Names: %s\n", FS_LoadedPakNames()); } /* ================== CL_PureList_f ================== */ void CL_ReferencedPK3List_f( void ) { Com_Printf("Referenced PK3 Names: %s\n", FS_ReferencedPakNames()); } /* ================== CL_Configstrings_f ================== */ 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 ============== */ 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 ) ); Com_Printf( "--------------------------------------\n" ); } //==================================================================== /* ================= CL_DownloadsComplete Called when all downloading has been completed ================= */ 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" ); // 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; } // starting to load a map so we get out of full screen ui mode Cvar_Set("r_uiFullScreen", "0"); // 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 CL_FlushMemory(); // initialize the CGame cls.cgameStarted = qtrue; CL_InitCGame(); // 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. ================= */ 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_SetValue( "cl_downloadTime", cls.realtime ); clc.downloadBlock = 0; // Starting new file clc.downloadCount = 0; // Stop any errant looping sounds that may be playing S_ClearLoopingSounds( qtrue ); CL_AddReliableCommand( va("download %s", remoteName) ); } /* ================= CL_NextDownload A download completed or failed ================= */ void CL_NextDownload(void) { char *s; char *remoteName, *localName; qboolean useCURL = qfalse; // 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 nul 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) { char missingfiles[1024]; if ( !(cl_allowDownload->integer & DLF_ENABLE) ) { // 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 wether 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; CL_NextDownload(); return; } } CL_DownloadsComplete(); } /* ================= CL_CheckForResend Resend a connect message if the last one has timed out ================= */ void CL_CheckForResend( void ) { int port, i; char info[MAX_INFO_STRING]; char data[MAX_INFO_STRING]; // 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 NET_OutOfBandPrint(NS_CLIENT, clc.serverAddress, "getchallenge"); break; case CA_CHALLENGING: // sending back the challenge port = Cvar_VariableValue ("net_qport"); Q_strncpyz( info, Cvar_InfoString( CVAR_USERINFO ), sizeof( info ) ); Info_SetValueForKey( info, "protocol", va("%i", PROTOCOL_VERSION ) ); Info_SetValueForKey( info, "qport", va("%i", port ) ); Info_SetValueForKey( info, "challenge", va("%i", clc.challenge ) ); strcpy(data, "connect "); // TTimo adding " " around the userinfo string to avoid truncated userinfo on the server // (Com_TokenizeString tokenizes around spaces) data[8] = '"'; for(i=0;iadr = *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; } #define MAX_SERVERSPERPACKET 256 /* =================== CL_ServersResponsePacket =================== */ 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; Com_Printf("CL_ServersResponsePacket\n"); if (cls.numglobalservers == -1) { // state to detect lack of servers or lack of response cls.numglobalservers = 0; cls.numGlobalServerAddresses = 0; } // 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].ip) + sizeof(addresses[numservers].port) + 1) break; for(i = 0; i < sizeof(addresses[numservers].ip); i++) addresses[numservers].ip[i] = *buffptr++; addresses[numservers].type = NA_IP; } // IPv6 address, if it's an extended response else if (extended && *buffptr == '/') { buffptr++; if (buffend - buffptr < sizeof(addresses[numservers].ip6) + sizeof(addresses[numservers].port) + 1) break; for(i = 0; i < sizeof(addresses[numservers].ip6); i++) addresses[numservers].ip6[i] = *buffptr++; addresses[numservers].type = NA_IP6; addresses[numservers].scope_id = from->scope_id; } 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++) { // build net address serverInfo_t *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("%d servers parsed (total %d)\n", numservers, total); } /* ================= CL_ConnectionlessPacket Responses to broadcasts, etc ================= */ void CL_ConnectionlessPacket( netadr_t from, msg_t *msg ) { char *s; char c[ BIG_INFO_STRING ]; char arg1[ BIG_INFO_STRING ]; MSG_BeginReadingOOB( msg ); MSG_ReadLong( msg ); // skip the -1 s = MSG_ReadStringLine( msg ); Cmd_TokenizeString( s ); Q_strncpyz( c, Cmd_Argv( 0 ), BIG_INFO_STRING ); Q_strncpyz( arg1, Cmd_Argv( 1 ), BIG_INFO_STRING ); Com_DPrintf ("CL packet %s: %s\n", NET_AdrToStringwPort(from), c); // 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" ); } else { // start sending challenge repsonse instead of challenge request packets clc.challenge = atoi(arg1); 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; } // server connection if ( !Q_stricmp(c, "connectResponse") ) { if ( cls.state >= CA_CONNECTED ) { Com_Printf ("Dup connect received. Ignored.\n"); return; } if ( cls.state != CA_CHALLENGING ) { Com_Printf ("connectResponse packet while not connecting. Ignored.\n"); return; } if ( !NET_CompareBaseAdr( from, clc.serverAddress ) ) { Com_Printf( "connectResponse from a different address. Ignored.\n" ); Com_Printf( "%s should have been %s\n", NET_AdrToStringwPort( from ), NET_AdrToStringwPort( clc.serverAddress ) ); return; } Netchan_Setup (NS_CLIENT, &clc.netchan, from, Cvar_VariableValue( "net_qport" ) ); cls.state = CA_CONNECTED; clc.lastPacketSentTime = -9999; // send first packet immediately return; } // server responding to an info broadcast if ( !Q_stricmp(c, "infoResponse") ) { CL_ServerInfoPacket( from, msg ); return; } // server responding to a get playerlist if ( !Q_stricmp(c, "statusResponse") ) { CL_ServerStatusResponse( from, msg ); return; } // a disconnect message from the server, which will happen if the server // dropped the connection but it is still getting packets from us if (!Q_stricmp(c, "disconnect")) { CL_DisconnectPacket( from ); return; } // echo request from server if ( !Q_stricmp(c, "echo") ) { NET_OutOfBandPrint( NS_CLIENT, from, "%s", arg1 ); return; } // global MOTD from trem master if ( !Q_stricmp(c, "motd") ) { CL_MotdPacket( from, arg1 ); return; } // echo request from server if ( !Q_stricmp(c, "print") ) { s = MSG_ReadString( msg ); Q_strncpyz( clc.serverMessage, s, sizeof( clc.serverMessage ) ); while( clc.serverMessage[ strlen( clc.serverMessage ) - 1 ] == '\n' ) clc.serverMessage[ strlen( clc.serverMessage ) - 1 ] = '\0'; Com_Printf( "%s", s ); return; } // list of servers sent back by a master server (classic) if ( !Q_strncmp(c, "getserversResponse", 18) ) { CL_ServersResponsePacket( &from, msg, qfalse ); return; } // list of servers sent back by a master server (extended) if ( !Q_strncmp(c, "getserversExtResponse", 21) ) { CL_ServersResponsePacket( &from, msg, qtrue ); return; } Com_DPrintf ("Unknown connectionless packet command.\n"); } /* ================= CL_PacketEvent A packet has arrived from the main event loop ================= */ void CL_PacketEvent( netadr_t from, msg_t *msg ) { int headerBytes; clc.lastPacketTime = cls.realtime; if ( msg->cursize >= 4 && *(int *)msg->data == -1 ) { CL_ConnectionlessPacket( from, msg ); return; } if ( cls.state < CA_CONNECTED ) { return; // can't be a valid sequenced packet } if ( msg->cursize < 4 ) { Com_Printf ("%s: Runt packet\n", NET_AdrToStringwPort( from )); return; } // // packet from server // if ( !NET_CompareAdr( from, clc.netchan.remoteAddress ) ) { Com_DPrintf ("%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( *(int *)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 ) { CL_WriteDemoMessage( msg, headerBytes ); } } /* ================== CL_CheckTimeout ================== */ 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->value*1000) { if (++cl.timeoutcount > 5) { // timeoutcount saves debugger Com_Printf ("\nServer connection timed out.\n"); CL_Disconnect( qtrue ); 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_CheckUserinfo ================== */ void CL_CheckUserinfo( void ) { // don't add reliable commands when not yet connected if(cls.state < CA_CHALLENGING) 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) { cvar_modifiedFlags &= ~CVAR_USERINFO; CL_AddReliableCommand( va("userinfo \"%s\"", Cvar_InfoString( CVAR_USERINFO ) ) ); } } /* ================== CL_Frame ================== */ void CL_Frame ( int msec ) { if ( !com_cl_running->integer ) { return; } #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.realFrametime = msec; cls.frametime = msec; cls.realtime += cls.frametime; SCR_UpdateScreen(); S_Update(); Con_RunConsole(); cls.framecount++; return; } } #endif 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, UI_SET_ACTIVE_MENU, UIMENU_MAIN ); } // if recording an avi, lock to a fixed fps if ( CL_VideoRecording( ) && cl_aviFrameRate->integer && msec) { // save the current screen if ( cls.state == CA_ACTIVE || cl_forceavidemo->integer) { CL_TakeVideoFrame( ); // fixed time for next frame' msec = (int)ceil( (1000.0f / cl_aviFrameRate->value) * com_timescale->value ); if (msec == 0) { msec = 1; } } } if( cl_autoRecordDemo->integer ) { if( cls.state == CA_ACTIVE && !clc.demorecording && !clc.demoplaying ) { // If not recording a demo, and we should be, start one qtime_t now; 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 = strstr( 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( ); } } // save the msec before checking pause cls.realFrametime = msec; // decide the simulation time cls.frametime = msec; cls.realtime += cls.frametime; if ( cl_timegraph->integer ) { SCR_DebugGraph ( cls.realFrametime * 0.25, 0 ); } // see if we need to update any userinfo CL_CheckUserinfo(); // if we haven't gotten a packet in a long time, // drop the connection 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 SCR_UpdateScreen(); // update audio S_Update(); #ifdef USE_VOIP CL_CaptureVoip(); #endif #ifdef USE_MUMBLE CL_UpdateMumble(); #endif // advance local effects for next frame SCR_RunCinematic(); Con_RunConsole(); cls.framecount++; } //============================================================================ /* ================ CL_RefPrintf DLL glue ================ */ void QDECL CL_RefPrintf( int print_level, const char *fmt, ...) { va_list argptr; char msg[MAXPRINTMSG]; va_start (argptr,fmt); Q_vsnprintf (msg, sizeof(msg), fmt, argptr); va_end (argptr); if ( print_level == PRINT_ALL ) { Com_Printf ("%s", msg); } else if ( print_level == PRINT_WARNING ) { Com_Printf (S_COLOR_YELLOW "%s", msg); // yellow } else if ( print_level == PRINT_DEVELOPER ) { Com_DPrintf (S_COLOR_RED "%s", msg); // red } } /* ============ CL_ShutdownRef ============ */ void CL_ShutdownRef( void ) { if ( !re.Shutdown ) { return; } re.Shutdown( qtrue ); Com_Memset( &re, 0, sizeof( re ) ); } /* ============ CL_InitRenderer ============ */ void CL_InitRenderer( void ) { // 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" ); g_console_field_width = cls.glconfig.vidWidth / SMALLCHAR_WIDTH - 2; g_consoleField.widthInChars = g_console_field_width; } /* ============================ 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( qboolean rendererOnly ) { if (!com_cl_running) { return; } if ( !com_cl_running->integer ) { return; } if ( !cls.rendererStarted ) { cls.rendererStarted = qtrue; CL_InitRenderer(); } if ( rendererOnly ) { return; } if ( !cls.soundStarted ) { cls.soundStarted = qtrue; S_Init(); } if ( !cls.soundRegistered ) { cls.soundRegistered = qtrue; S_BeginRegistration(); } if( com_dedicated->integer ) { return; } if ( !cls.uiStarted ) { cls.uiStarted = qtrue; CL_InitUI(); } } /* ============ CL_RefMalloc ============ */ void *CL_RefMalloc( int size ) { return Z_TagMalloc( size, TAG_RENDERER ); } int CL_ScaledMilliseconds(void) { return Sys_Milliseconds()*com_timescale->value; } /* ============ CL_InitRef ============ */ void CL_InitRef( void ) { refimport_t ri; refexport_t *ret; Com_Printf( "----- Initializing Renderer ----\n" ); ri.Cmd_AddCommand = Cmd_AddCommand; ri.Cmd_RemoveCommand = Cmd_RemoveCommand; ri.Cmd_Argc = Cmd_Argc; ri.Cmd_Argv = Cmd_Argv; ri.Cmd_ExecuteText = Cbuf_ExecuteText; ri.Printf = CL_RefPrintf; ri.Error = Com_Error; ri.Milliseconds = CL_ScaledMilliseconds; ri.Malloc = CL_RefMalloc; ri.Free = Z_Free; #ifdef HUNK_DEBUG ri.Hunk_AllocDebug = Hunk_AllocDebug; #else ri.Hunk_Alloc = Hunk_Alloc; #endif ri.Hunk_AllocateTempMemory = Hunk_AllocateTempMemory; ri.Hunk_FreeTempMemory = Hunk_FreeTempMemory; ri.CM_DrawDebugSurface = CM_DrawDebugSurface; ri.FS_ReadFile = FS_ReadFile; ri.FS_FreeFile = FS_FreeFile; ri.FS_WriteFile = FS_WriteFile; ri.FS_FreeFileList = FS_FreeFileList; ri.FS_ListFiles = FS_ListFiles; ri.FS_FileIsInPAK = FS_FileIsInPAK; ri.FS_FileExists = FS_FileExists; ri.Cvar_Get = Cvar_Get; ri.Cvar_Set = Cvar_Set; ri.Cvar_CheckRange = Cvar_CheckRange; // cinematic stuff ri.CIN_UploadCinematic = CIN_UploadCinematic; ri.CIN_PlayCinematic = CIN_PlayCinematic; ri.CIN_RunCinematic = CIN_RunCinematic; ri.CL_WriteAVIVideoFrame = CL_WriteAVIVideoFrame; ret = GetRefAPI( REF_API_VERSION, &ri ); #if defined __USEA3D && defined __A3D_GEOM hA3Dg_ExportRenderGeom (ret); #endif Com_Printf( "-------------------------------\n"); if ( !ret ) { Com_Error (ERR_FATAL, "Couldn't initialize refresh" ); } re = *ret; // unpause so the cgame definately gets a snapshot and renders a frame Cvar_Set( "cl_paused", "0" ); } //=========================================================================================== void CL_SetModel_f( void ) { char *arg; char name[256]; 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] =============== */ void CL_Video_f( void ) { char filename[ MAX_OSPATH ]; int i, last; if( !clc.demoplaying ) { Com_Printf( "The video command can only be used when playing back demos\n" ); return; } if( Cmd_Argc( ) == 2 ) { // explicit filename Com_sprintf( filename, MAX_OSPATH, "videos/%s.avi", Cmd_Argv( 1 ) ); } else { // scan for a free filename for( i = 0; i <= 9999; i++ ) { int a, b, c, d; last = i; a = last / 1000; last -= a * 1000; b = last / 100; last -= b * 100; c = last / 10; last -= c * 10; d = last; Com_sprintf( filename, MAX_OSPATH, "videos/video%d%d%d%d.avi", a, b, c, d ); 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; } } CL_OpenAVIForWriting( filename ); } /* =============== CL_StopVideo_f =============== */ void CL_StopVideo_f( void ) { CL_CloseAVI( ); } /* =============== 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. =============== */ 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" ); } } /* ==================== CL_Init ==================== */ void CL_Init( void ) { Com_Printf( "----- Client Initialization -----\n" ); Con_Init (); CL_ClearState (); cls.state = CA_DISCONNECTED; // no longer CA_UNINITIALIZED cls.realtime = 0; CL_InitInput (); // // register our variables // cl_noprint = Cvar_Get( "cl_noprint", "0", 0 ); cl_motd = Cvar_Get ("cl_motd", "1", 0); cl_timeout = Cvar_Get ("cl_timeout", "200", 0); cl_timeNudge = Cvar_Get ("cl_timeNudge", "0", CVAR_TEMP ); cl_shownet = Cvar_Get ("cl_shownet", "0", CVAR_TEMP ); cl_showSend = Cvar_Get ("cl_showSend", "0", CVAR_TEMP ); cl_showTimeDelta = Cvar_Get ("cl_showTimeDelta", "0", CVAR_TEMP ); cl_freezeDemo = Cvar_Get ("cl_freezeDemo", "0", CVAR_TEMP ); rcon_client_password = Cvar_Get ("rconPassword", "", CVAR_TEMP ); cl_activeAction = Cvar_Get( "activeAction", "", CVAR_TEMP ); cl_timedemo = Cvar_Get ("timedemo", "0", 0); cl_timedemoLog = Cvar_Get ("cl_timedemoLog", "", CVAR_ARCHIVE); cl_autoRecordDemo = Cvar_Get ("cl_autoRecordDemo", "0", CVAR_ARCHIVE); cl_aviFrameRate = Cvar_Get ("cl_aviFrameRate", "25", CVAR_ARCHIVE); cl_aviMotionJpeg = Cvar_Get ("cl_aviMotionJpeg", "1", CVAR_ARCHIVE); cl_forceavidemo = Cvar_Get ("cl_forceavidemo", "0", 0); rconAddress = Cvar_Get ("rconAddress", "", 0); cl_yawspeed = Cvar_Get ("cl_yawspeed", "140", CVAR_ARCHIVE); cl_pitchspeed = Cvar_Get ("cl_pitchspeed", "140", CVAR_ARCHIVE); cl_anglespeedkey = Cvar_Get ("cl_anglespeedkey", "1.5", 0); cl_maxpackets = Cvar_Get ("cl_maxpackets", "30", CVAR_ARCHIVE ); cl_packetdup = Cvar_Get ("cl_packetdup", "1", CVAR_ARCHIVE ); cl_run = Cvar_Get ("cl_run", "1", CVAR_ARCHIVE); cl_sensitivity = Cvar_Get ("sensitivity", "5", CVAR_ARCHIVE); cl_mouseAccel = Cvar_Get ("cl_mouseAccel", "0", CVAR_ARCHIVE); cl_freelook = Cvar_Get( "cl_freelook", "1", CVAR_ARCHIVE ); cl_showMouseRate = Cvar_Get ("cl_showmouserate", "0", 0); cl_allowDownload = Cvar_Get ("cl_allowDownload", "0", CVAR_ARCHIVE); #ifdef USE_CURL cl_cURLLib = Cvar_Get("cl_cURLLib", DEFAULT_CURL_LIB, CVAR_ARCHIVE); #endif cl_conXOffset = Cvar_Get ("cl_conXOffset", "0", 0); #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); #else cl_inGameVideo = Cvar_Get ("r_inGameVideo", "1", CVAR_ARCHIVE); #endif cl_serverStatusResendTime = Cvar_Get ("cl_serverStatusResendTime", "750", 0); // init autoswitch so the ui will have it correctly even // if the cgame hasn't been started Cvar_Get ("cg_autoswitch", "1", CVAR_ARCHIVE); m_pitch = Cvar_Get ("m_pitch", "0.022", CVAR_ARCHIVE); m_yaw = Cvar_Get ("m_yaw", "0.022", CVAR_ARCHIVE); m_forward = Cvar_Get ("m_forward", "0.25", CVAR_ARCHIVE); m_side = Cvar_Get ("m_side", "0.25", CVAR_ARCHIVE); #ifdef MACOS_X // Input is jittery on OS X w/o this m_filter = Cvar_Get ("m_filter", "1", CVAR_ARCHIVE); #else m_filter = Cvar_Get ("m_filter", "0", CVAR_ARCHIVE); #endif cl_motdString = Cvar_Get( "cl_motdString", "", CVAR_ROM ); Cvar_Get( "cl_maxPing", "800", CVAR_ARCHIVE ); cl_lanForcePackets = Cvar_Get ("cl_lanForcePackets", "1", CVAR_ARCHIVE); cl_guidServerUniq = Cvar_Get ("cl_guidServerUniq", "1", CVAR_ARCHIVE); // ~ and `, as keys and characters cl_consoleKeys = Cvar_Get( "cl_consoleKeys", "~ ` 0x7e 0x60", CVAR_ARCHIVE); // userinfo Cvar_Get ("name", Sys_GetCurrentUser( ), CVAR_USERINFO | CVAR_ARCHIVE ); Cvar_Get ("rate", "25000", CVAR_USERINFO | CVAR_ARCHIVE ); Cvar_Get ("snaps", "20", CVAR_USERINFO | 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 ); Cvar_Get ("sex", "male", CVAR_USERINFO | CVAR_ARCHIVE ); Cvar_Get ("password", "", CVAR_USERINFO); #ifdef USE_MUMBLE cl_useMumble = Cvar_Get ("cl_useMumble", "0", CVAR_ARCHIVE | CVAR_LATCH); cl_mumbleScale = Cvar_Get ("cl_mumbleScale", "0.0254", CVAR_ARCHIVE); #endif #ifdef USE_VOIP cl_voipSend = Cvar_Get ("cl_voipSend", "0", 0); cl_voipSendTarget = Cvar_Get ("cl_voipSendTarget", "all", 0); cl_voipGainDuringCapture = Cvar_Get ("cl_voipGainDuringCapture", "0.2", CVAR_ARCHIVE); cl_voipCaptureMult = Cvar_Get ("cl_voipCaptureMult", "2.0", CVAR_ARCHIVE); cl_voipUseVAD = Cvar_Get ("cl_voipUseVAD", "0", CVAR_ARCHIVE); cl_voipVADThreshold = Cvar_Get ("cl_voipVADThreshold", "0.25", CVAR_ARCHIVE); cl_voipShowMeter = Cvar_Get ("cl_voipShowMeter", "1", CVAR_ARCHIVE); // This is a protocol version number. cl_voip = Cvar_Get ("cl_voip", "1", CVAR_USERINFO | CVAR_ARCHIVE | CVAR_LATCH); Cvar_CheckRange( cl_voip, 0, 1, qtrue ); // If your data rate is too low, you'll get Connection Interrupted warnings // when VoIP packets arrive, even if you have a broadband connection. // This might work on rates lower than 25000, but for safety's sake, we'll // just demand it. Who doesn't have at least a DSL line now, anyhow? If // you don't, you don't need VoIP. :) if ((cl_voip->integer) && (Cvar_VariableIntegerValue("rate") < 25000)) { Com_Printf("Your network rate is too slow for VoIP.\n"); Com_Printf("Set 'Data Rate' to 'LAN/Cable/xDSL' in 'Setup/System/Network' and restart.\n"); Com_Printf("Until then, VoIP is disabled.\n"); Cvar_Set("cl_voip", "0"); } #endif // cgame might not be initialized before menu is used Cvar_Get ("cg_viewsize", "100", CVAR_ARCHIVE ); // 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 our 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_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 ("setenv", CL_Setenv_f ); 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 ("stopvideo", CL_StopVideo_f ); CL_InitRef(); SCR_Init (); // Cbuf_Execute (); Cvar_Set( "cl_running", "1" ); CL_GenerateQKey(); Cvar_Get( "cl_guid", "", CVAR_USERINFO | CVAR_ROM ); CL_UpdateGUID( NULL, 0 ); Com_Printf( "----- Client Initialization Complete -----\n" ); } /* =============== CL_Shutdown =============== */ void CL_Shutdown( void ) { static qboolean recursive = qfalse; // check whether the client is running at all. if(!(com_cl_running && com_cl_running->integer)) return; Com_Printf( "----- CL_Shutdown -----\n" ); if ( recursive ) { Com_Printf( "WARNING: Recursive shutdown\n" ); return; } recursive = qtrue; CL_Disconnect( qtrue ); S_Shutdown(); CL_ShutdownRef(); CL_ShutdownUI(); Cmd_RemoveCommand ("cmd"); Cmd_RemoveCommand ("configstrings"); Cmd_RemoveCommand ("userinfo"); 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 ("localservers"); Cmd_RemoveCommand ("globalservers"); Cmd_RemoveCommand ("rcon"); Cmd_RemoveCommand ("setenv"); Cmd_RemoveCommand ("ping"); Cmd_RemoveCommand ("serverstatus"); Cmd_RemoveCommand ("showip"); Cmd_RemoveCommand ("model"); Cmd_RemoveCommand ("video"); Cmd_RemoveCommand ("stopvideo"); 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_HOSTNAME_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->ping = ping; } } static void CL_SetServerInfoByAddress(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 =================== */ void CL_ServerInfoPacket( netadr_t from, msg_t *msg ) { int i, type; char info[MAX_INFO_STRING]; 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 != PROTOCOL_VERSION ) { Com_DPrintf( "Different protocol info packet: %s\n", infoString ); return; } // iterate servers waiting for ping response for (i=0; iretrieved = qtrue; return qfalse; } // if this server status request has the same address if ( NET_CompareAdr( to, serverStatus->address) ) { // if we recieved an 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 ( serverStatus->startTime < Com_Milliseconds() - cl_serverStatusResendTime->integer ) { serverStatus->print = qfalse; serverStatus->pending = qtrue; serverStatus->retrieved = qfalse; serverStatus->time = 0; serverStatus->startTime = Com_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 = Com_Milliseconds(); serverStatus->time = 0; NET_OutOfBandPrint( NS_CLIENT, to, "getstatus" ); return qfalse; } return qfalse; } /* =================== CL_ServerStatusResponse =================== */ void CL_ServerStatusResponse( netadr_t from, msg_t *msg ) { char *s; char info[MAX_INFO_STRING]; 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); 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 = Com_Milliseconds(); serverStatus->address = from; serverStatus->pending = qfalse; if (serverStatus->print) { serverStatus->retrieved = qtrue; } } /* ================== CL_LocalServers_f ================== */ void CL_LocalServers_f( void ) { char *message; int i, j; 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"; // 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, strlen( message ), message, to ); to.type = NA_MULTICAST6; NET_SendPacket( NS_CLIENT, strlen( message ), message, to ); } } } /* ================== CL_GlobalServers_f ================== */ void CL_GlobalServers_f( void ) { netadr_t to; int count, i, masterNum; char command[1024], *masteraddress; char *cmdname; if ((count = Cmd_Argc()) < 3 || (masterNum = atoi(Cmd_Argv(1))) < 0 || masterNum > 4) { Com_Printf( "usage: globalservers [keywords]\n"); return; } sprintf(command, "sv_master%d", masterNum + 1); 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) { 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 master %s...\n", masteraddress); cls.numglobalservers = -1; cls.pingUpdateSource = AS_GLOBAL; // Use the extended query for IPv6 masters if (to.type == NA_IP6 || to.type == NA_MULTICAST6) { cmdname = "getserversExt " GAMENAME_FOR_MASTER; // TODO: test if we only have an IPv6 connection. If it's the case, // request IPv6 servers only by appending " ipv6" to the command } else cmdname = "getservers"; Com_sprintf( command, sizeof(command), "%s %s", cmdname, 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 (!cl_pinglist[n].adr.port) { // empty 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) { // check for timeout time = cls.realtime - cl_pinglist[n].start; maxPing = Cvar_VariableIntegerValue( "cl_maxPing" ); if( maxPing < 100 ) { maxPing = 100; } 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_UpdateServerInfo ================== */ void CL_UpdateServerInfo( int n ) { if (!cl_pinglist[n].adr.port) { return; } CL_SetServerInfoByAddress(cl_pinglist[n].adr, cl_pinglist[n].info, cl_pinglist[n].time ); } /* ================== CL_GetPingInfo ================== */ void CL_GetPingInfo( int n, char *buf, int buflen ) { if (!cl_pinglist[n].adr.port) { // empty 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; iadr.port) { count++; } } return (count); } /* ================== CL_GetFreePing ================== */ ping_t* CL_GetFreePing( void ) { ping_t* pingptr; ping_t* best; int oldest; int i; int time; pingptr = cl_pinglist; for (i=0; iadr.port) { if (!pingptr->time) { if (cls.realtime - 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; istart; if (time > oldest) { oldest = time; best = pingptr; } } return (best); } /* ================== CL_Ping_f ================== */ void CL_Ping_f( void ) { netadr_t to; ping_t* pingptr; 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; else if(!strcmp(Cmd_Argv(1), "-6")) family = NA_IP6; else Com_Printf( "warning: only -4 or -6 as address type understood.\n"); server = Cmd_Argv(2); } Com_Memset( &to, 0, sizeof(netadr_t) ); if ( !NET_StringToAdr( server, &to, family ) ) { return; } pingptr = CL_GetFreePing(); memcpy( &pingptr->adr, &to, sizeof (netadr_t) ); pingptr->start = cls.realtime; 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; max = (source == AS_GLOBAL) ? MAX_GLOBAL_SERVERS : MAX_OTHER_SERVERS; 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) { break; } } memcpy(&cl_pinglist[j].adr, &server[i].adr, sizeof(netadr_t)); cl_pinglist[j].start = cls.realtime; cl_pinglist[j].time = 0; NET_OutOfBandPrint( NS_CLIENT, cl_pinglist[j].adr, "getinfo xxx" ); slots++; } } // 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 ================== */ void CL_ServerStatus_f(void) { netadr_t to, *toptr = NULL; 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"); Com_Printf( "usage: serverstatus [-4|-6] server\n"); return; } toptr = &clc.serverAddress; } if(!toptr) { Com_Memset( &to, 0, sizeof(netadr_t) ); if(argc == 2) server = Cmd_Argv(1); else { if(!strcmp(Cmd_Argv(1), "-4")) family = NA_IP; else if(!strcmp(Cmd_Argv(1), "-6")) family = NA_IP6; else Com_Printf( "warning: only -4 or -6 as address type understood.\n"); 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 ================== */ void CL_ShowIP_f(void) { Sys_ShowIP(); }