/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. Copyright (C) 2007 HermitWorks Entertainment Corporation This file is part of the Space Trader source code. The Space Trader 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. The Space Trader 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 the Space Trader 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" 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_avidemo; 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_conXOffset; cvar_t *cl_conYOffset; cvar_t *cl_conWidth; cvar_t *cl_conHeight; cvar_t *cl_inGameVideo; cvar_t *cl_serverStatusResendTime; cvar_t *cl_trn; cvar_t *cl_authserver; cvar_t *cl_ircport; cvar_t *cl_lang; clientActive_t cl; clientConnection_t clc; clientStatic_t cls; vm_t *cgvm; // Structure containing functions exported from refresh DLL refexport_t re; 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; extern void SV_BotFrame( int time ); void CL_CheckForResend( void ); void Q_EXTERNAL_CALL CL_ShowIP_f(void); void Q_EXTERNAL_CALL CL_ServerStatus_f(void); void CL_ServerStatusResponse( netadr_t from, msg_t *msg ); sqlInfo_t * sql_getclientdb( void ) { return &cl.db; } /* =============== 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 ======================================================================= */ char * CL_GetReliableCommand( int index ) { char * cmd = clc.reliableCommands[ index & (MAX_RELIABLE_COMMANDS-1) ]; return cmd?cmd:""; } /* ====================== 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 ); clc.reliableCommands[ index ] = Q_strcpy_ringbuffer( clc.reliableCommandBuffer, sizeof( clc.reliableCommandBuffer ), clc.reliableCommands[ (clc.reliableAcknowledge) & ( MAX_RELIABLE_COMMANDS - 1 ) ], clc.reliableCommands[ (clc.reliableSequence-1) & ( MAX_RELIABLE_COMMANDS - 1 ) ], cmd ); if ( !clc.reliableCommands[ index ] ) { Com_Error( ERR_DROP, "Client command buffer overflow" ); } } /* ======================================================================= 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 Q_EXTERNAL_CALL 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; Com_Printf ("Stopped demo.\n"); } /* ================== CL_DemoFilename ================== */ void CL_DemoFilename( int number, char *fileName ) { int a,b,c,d; if ( number < 0 || number > 9999 ) { Com_sprintf( fileName, MAX_OSPATH, "demo9999.tga" ); return; } 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 ==================== */ void Q_EXTERNAL_CALL CL_Record_f( void ) { char name[MAX_OSPATH]; byte bufData[MAX_MSGLEN]; char demoName[MAX_QPATH]; 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 ) { 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_"PROTOCOL_VERSION, demoName ); } 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_"PROTOCOL_VERSION, demoName ); len = FS_ReadFile( name, NULL ); if ( len <= 0 ) { 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; 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); } // write sql tables MSG_WriteAllTables( &buf, &cl.db ); // 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_DemoCompleted ================= */ void CL_DemoCompleted( void ) { if (cl_timedemo && cl_timedemo->integer) { int time; time = Sys_Milliseconds() - clc.timeDemoStart; if ( time > 0 ) { Com_Printf ("%i frames, %3.1f seconds: %3.1f fps\n", clc.timeDemoFrames, 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 ]; 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_PlayDemo_f demo ==================== */ void Q_EXTERNAL_CALL 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 Cvar_Set( "sv_killserver", "1" ); 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"); cls.keyCatchers = 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]) { CL_FlushMemory(); return; } Cvar_Set ("nextdemo",""); Cbuf_AddText (v); Cbuf_AddText ("\n"); Cbuf_Execute(); } //====================================================================== /* ===================== CL_ShutdownAll ===================== */ void CL_ShutdownAll( void ) { // clear sounds S_DisableSounds(); // shutdown CGame CL_ShutdownCGame(); // shutdown UI CL_ShutdownUI( qfalse ); // shutdown the renderer if( re.Shutdown ) { re.Shutdown( qfalse ); // don't destroy window or context cls.rendererStarted = qfalse; } cls.uiStarted = qfalse; cls.cgameStarted = 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(); CM_ClearPreloaderData(); // clear collision map data CM_ClearMap(); } else { // clear all the client data on the hunk Hunk_ClearToMark(); } 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_cl_running->integer ) { return; } Con_Close(); cls.keyCatchers = 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 { // 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 cls.keyCatchers = 0; SCR_UpdateScreen(); clc.connectTime = -RETRANSMIT_TIMEOUT; NET_StringToAdr( cls.servername, &clc.serverAddress); // we don't need a challenge on the localhost CL_CheckForResend(); } } static int sql_global_int( const char * name ) { if ( name ) { switch( SWITCHSTRING( name ) ) { case CS('c','l','i','e'): return clc.clientNum; default: Cmd_TokenizeString( name ); return VM_Call( uivm, UI_GLOBAL_INT ); } } return 0; } static qhandle_t sql_portrait( const char * path ) { char model[ MAX_QPATH ]; char * skin; Q_strncpyz( model, path, sizeof(model) ); skin = Q_strrchr( model, '/' ); if ( skin ) { *skin++ = '\0'; } else { skin = "default"; } return re.RegisterShaderNoMip( va("models/players/%s/icon_%s", model, skin ) ); } static qhandle_t sql_shader( const char * name ) { return re.RegisterShader( name ); } static qhandle_t sql_sound( const char * name ) { return S_RegisterSound( name, qtrue ); } static qhandle_t sql_model( const char * name ) { return re.RegisterModel( name ); } /* ===================== CL_ClearState Called before parsing a gamestate ===================== */ void CL_ClearState (void) { // S_StopAllSounds(); Com_Memset( &cl, 0, sizeof( cl ) ); // clear sql db sql_reset ( &cl.db, TAG_SQL_CLIENT ); sql_assign_gs ( &cl.db, (int*)&cl.snap.ps.gs ); cl.db.ps = (int*)&cl.snap.ps; sql_exec( &cl.db, "CREATE TABLE servers" "(" "addr STRING, " "clients INTEGER, " "hostname STRING, " "mapname STRING, " "sv_maxclients INTEGER, " "game STRING, " "gametype INTEGER, " "nettype INTEGER, " "minping INTEGER, " "maxping INTEGER, " "punkbuster INTEGER, " "ping INTEGER, " "start INTEGER, " "source INTEGER, " "lastupdate INTEGER, " "channel STRING " ");" ); cl.db.global_int = sql_global_int; cl.db.portrait = sql_portrait; cl.db.shader = sql_shader; cl.db.sound = sql_sound; cl.db.model = sql_model; } /* ===================== 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.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 ) ); Cvar_Set( "cl_spacetrader", "0" ); cls.state = CA_DISCONNECTED; // allow cheats locally Cvar_Set( "sv_cheats", "1" ); // not connected to a pure server anymore cl_connectedToPureServer = qfalse; } /* =================== 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\"\n", cmd); return; } if ( Cmd_Argc() > 1 ) { CL_AddReliableCommand( string ); } else { CL_AddReliableCommand( cmd ); } } #ifdef USE_WEBAUTH /* =================== CL_Highscore_response gets highscores results from the website =================== */ static int QDECL CL_Highscore_response( httpInfo_e code, const char * buffer, int length, void * notifyData ) { if ( code == HTTP_WRITE ) { Cvar_Set( "st_postresults", buffer ); VM_Call(uivm, UI_REPORT_HIGHSCORE_RESPONSE ); } return 1; } /* =================== CL_Highscore_f asks for highscores from the website =================== */ static void Q_EXTERNAL_CALL CL_Highscore_f( void ) { if ( Cmd_Argc() != 7 ) { Com_Printf("Usage: highscore