/* Copyright (C) 1997-2001 Id Software, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // cl_main.c -- client main loop #include "common.h" #include "client.h" cvar_t *rcon_client_password; cvar_t *rcon_address; cvar_t *cl_footsteps; cvar_t *cl_timeout; cvar_t *cl_predict; cvar_t *cl_showfps; cvar_t *cl_gun; cvar_t *cl_add_particles; cvar_t *cl_add_lights; cvar_t *cl_add_entities; cvar_t *cl_add_blend; cvar_t *cl_maxpackets; cvar_t *cl_shownet; cvar_t *cl_showmiss; cvar_t *cl_showclamp; cvar_t *cl_mouselook; cvar_t *cl_paused; cvar_t *lookspring; cvar_t *lookstrafe; cvar_t *m_pitch; cvar_t *m_yaw; cvar_t *m_forward; cvar_t *m_side; cvar_t *cl_lightlevel; // // userinfo // cvar_t *info_password; cvar_t *info_spectator; cvar_t *name; cvar_t *skin; cvar_t *rate; cvar_t *fov; cvar_t *hand; cvar_t *cl_vwep; client_static_t cls; client_t cl; entity_state_t cl_parse_entities[MAX_PARSE_ENTITIES]; extern cvar_t *allow_download; //====================================================================== //====================================================================== /* =================== Cmd_ForwardToServer adds the current command line as a clc_stringcmd to the client message. 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 Cmd_ForwardToServer (void) { char *cmd; if( cls.demoplayback ) return; // not really connected cmd = Cmd_Argv(0); if (cls.state <= ca_connected || *cmd == '-' || *cmd == '+') { Msg ("Unknown command \"%s\"\n", cmd); return; } MSG_WriteByte (&cls.netchan.message, clc_stringcmd); SZ_Print (&cls.netchan.message, cmd); if (Cmd_Argc() > 1) { SZ_Print (&cls.netchan.message, " "); SZ_Print (&cls.netchan.message, Cmd_Args()); Msg("clc_stringcmd: %s\n", Cmd_Args()); } } /* ================== CL_ForwardToServer_f ================== */ void CL_ForwardToServer_f (void) { if( cls.demoplayback ) return; // not really connected if (cls.state != ca_connected && cls.state != ca_active) { Msg("Can't \"%s\", not connected\n", Cmd_Argv(0)); return; } // don't forward the first argument if (Cmd_Argc() > 1) { MSG_WriteByte( &cls.netchan.message, clc_stringcmd ); SZ_Print( &cls.netchan.message, Cmd_Args()); } } /* ================== CL_Pause_f ================== */ void CL_Pause_f (void) { if(!Host_ServerState() && !cls.demoplayback ) return; // but allow pause in demos // never pause in multiplayer if( Cvar_VariableValue ("maxclients") > 1 ) { Cvar_SetValue ("paused", 0); return; } Cvar_SetValue( "paused", !cl_paused->value ); } /* ================== CL_Quit_f ================== */ void CL_Quit_f (void) { CL_Disconnect(); Sys_Quit(); } /* ================ CL_Drop Called after an Host_Error was thrown ================ */ void CL_Drop (void) { if (cls.state == ca_uninitialized) return; if (cls.state == ca_disconnected) return; CL_Disconnect(); } /* ======================= CL_SendConnectPacket We have gotten a challenge from the server, so try and connect. ====================== */ void CL_SendConnectPacket( void ) { netadr_t adr; int port; if(!NET_StringToAdr (cls.servername, &adr)) { MsgDev( D_INFO, "CL_SendConnectPacket: bad server address\n"); cls.connect_time = 0; return; } if(adr.port == 0) adr.port = BigShort( PORT_SERVER ); port = Cvar_VariableValue( "net_qport" ); userinfo_modified = false; Netchan_OutOfBandPrint(NS_CLIENT, adr, "connect %i %i %i \"%s\"\n", PROTOCOL_VERSION, port, cls.challenge, Cvar_Userinfo()); } /* ================= CL_CheckForResend Resend a connect message if the last one has timed out ================= */ void CL_CheckForResend (void) { netadr_t adr; // if the local server is running and we aren't // then connect if (cls.state == ca_disconnected && Host_ServerState()) { cls.state = ca_connecting; com.strncpy (cls.servername, "localhost", sizeof(cls.servername)-1); // we don't need a challenge on the localhost CL_SendConnectPacket (); return; } // resend if we haven't gotten a reply yet if(cls.demoplayback || cls.state != ca_connecting) return; if(cls.realtime - cls.connect_time < 3000) return; if(!NET_StringToAdr (cls.servername, &adr)) { MsgDev(D_INFO, "CL_CheckForResend: bad server address\n"); cls.state = ca_disconnected; return; } if (adr.port == 0) adr.port = BigShort (PORT_SERVER); cls.connect_time = cls.realtime; // for retransmit requests Msg ("Connecting to %s...\n", cls.servername); Netchan_OutOfBandPrint( NS_CLIENT, adr, "getchallenge\n" ); } /* ================ CL_Connect_f ================ */ void CL_Connect_f (void) { char *server; if (Cmd_Argc() != 2) { Msg ("usage: connect \n"); return; } if(Host_ServerState()) { // if running a local server, kill it and reissue com.strncpy( host.finalmsg, "Server quit\n", MAX_STRING ); SV_Shutdown( false ); } else CL_Disconnect(); server = Cmd_Argv (1); CL_Disconnect(); cls.state = ca_connecting; com.strncpy( cls.servername, server, sizeof(cls.servername) - 1); cls.connect_time = MAX_HEARTBEAT; // CL_CheckForResend() will fire immediately } /* ===================== CL_Rcon_f Send the rest of the command line over as an unconnected command. ===================== */ void CL_Rcon_f (void) { char message[1024]; int i; netadr_t to; if (!rcon_client_password->string) { Msg ("You must set 'rcon_password' before\n" "issuing an rcon command.\n"); return; } message[0] = (char)255; message[1] = (char)255; message[2] = (char)255; message[3] = (char)255; message[4] = 0; strcat (message, "rcon "); strcat (message, rcon_client_password->string); strcat (message, " "); for (i=1 ; i= ca_connected) to = cls.netchan.remote_address; else { if (!strlen(rcon_address->string)) { Msg ("You must either be connected,\n" "or set the 'rcon_address' cvar\n" "to issue rcon commands\n"); return; } NET_StringToAdr (rcon_address->string, &to); if (to.port == 0) to.port = BigShort (PORT_SERVER); } NET_SendPacket( NS_CLIENT, strlen(message)+1, message, to); } /* ===================== CL_ClearState ===================== */ void CL_ClearState (void) { S_StopAllSounds (); CL_ClearEffects (); CL_FreeEdicts(); // wipe the entire cl structure memset (&cl, 0, sizeof(cl)); SZ_Clear (&cls.netchan.message); } /* ===================== CL_Disconnect Goes from a connected state to full screen console state Sends a disconnect message to the server This is also called on Host_Error, so it shouldn't cause any errors ===================== */ void CL_Disconnect( void ) { byte final[32]; if (cls.state == ca_disconnected) return; VectorClear (cl.refdef.blend); UI_HideMenu(); cls.connect_time = 0; SCR_StopCinematic(); CL_Stop_f(); // send a disconnect message to the server final[0] = clc_stringcmd; com.strcpy((char *)final + 1, "disconnect"); Netchan_Transmit(&cls.netchan, strlen(final), final); Netchan_Transmit(&cls.netchan, strlen(final), final); Netchan_Transmit(&cls.netchan, strlen(final), final); CL_ClearState (); // stop download if (cls.download) { FS_Close(cls.download); cls.download = NULL; } cls.state = ca_disconnected; } /* =================== CL_DisconnectPacket Sometimes the server can drop the client and the netchan based disconnect can be lost. If the client continues to send packets to the server, the server will send out of band disconnect packets to the client so it doesn't have to wait for the full timeout period. =================== */ void CL_DisconnectPacket( netadr_t from ) { if( cls.state == ca_disconnected ) return; // if not from our server, ignore it if(!NET_CompareAdr( from, cls.netchan.remote_address )) return; // if we have received packets within three seconds, ignore it // (it might be a malicious spoof) if( cls.realtime - cls.netchan.last_received < 3000 ) return; // drop the connection MsgDev( D_ERROR, "Server disconnected for unknown reason\n" ); CL_Disconnect(); } void CL_Disconnect_f (void) { Host_Error("Disconnected from server\n"); } /* ==================== CL_Packet_f packet Contents allows \n escape character ==================== */ void CL_Packet_f (void) { char send[2048]; int i, l; char *in, *out; netadr_t adr; if (Cmd_Argc() != 3) { Msg ("packet \n"); return; } if (!NET_StringToAdr (Cmd_Argv(1), &adr)) { Msg ("Bad address\n"); return; } if (!adr.port) adr.port = BigShort (PORT_SERVER); in = Cmd_Argv(2); out = send+4; send[0] = send[1] = send[2] = send[3] = (char)0xff; l = strlen (in); for (i=0 ; i= ca_connected) { CL_Disconnect(); cls.connect_time = cls.realtime - 1500; } else cls.connect_time = MAX_HEARTBEAT; // fire immediately cls.state = ca_connecting; Msg ("reconnecting...\n"); } } /* =================== CL_StatusLocal_f =================== */ void CL_GetServerList_f( void ) { netadr_t adr; // send a broadcast packet MsgDev( D_INFO, "status pinging broadcast...\n" ); cls.pingtime = cls.realtime; adr.type = NA_BROADCAST; adr.port = BigShort( PORT_SERVER ); Netchan_OutOfBandPrint( NS_CLIENT, adr, "status" ); } /* ============= CL_FreeServerInfo ============= */ static void CL_FreeServerInfo( serverinfo_t *server ) { if( server->mapname ) Mem_Free( server->mapname ); if( server->hostname ) Mem_Free( server->hostname ); if( server->shortname ) Mem_Free( server->shortname ); if( server->gamename ) Mem_Free( server->gamename ); if( server->netaddress ) Mem_Free( server->netaddress ); if( server->playerstr ) Mem_Free( server->playerstr ); if( server->pingstring ) Mem_Free( server->pingstring); memset( server, 0, sizeof(serverinfo_t)); } /* ============= CL_FreeServerList ============= */ static void CL_FreeServerList_f( void ) { int i; for( i = 0; i < cls.numservers; i++ ) CL_FreeServerInfo( &cls.serverlist[i] ); cls.numservers = 0; } /* ============= CL_DupeCheckServerList Checks for duplicates and returns true if there is one... Since status has higher priority than info, if there is already an instance and it's not status, and the current one is status, the old one is removed. ============= */ static bool CL_DupeCheckServerList( char *adr, bool status ) { int i; for( i = 0; i < cls.numservers; i++ ) { if(!cls.serverlist[i].netaddress && !cls.serverlist[i].hostname ) { CL_FreeServerInfo( &cls.serverlist[i] ); continue; } if( cls.serverlist[i].netaddress && !com.strcmp( cls.serverlist[i].netaddress, adr )) { if( cls.serverlist[i].statusPacket && status ) { return true; } else if( status ) { CL_FreeServerInfo( &cls.serverlist[i] ); return false; } } } return false; } /* ============= CL_ParseServerStatus Parses a status packet from a server FIXME: check against a list of sent status requests so it's not attempting to parse things it shouldn't ============= */ bool CL_ParseServerStatus( char *adr, char *info ) { serverinfo_t *server; char *token; char shortName[32]; if( !info || !info[0] )return false; if( !adr || !adr[0] ) return false; if( !com.strchr( info, '\\')) return false; if( cls.numservers >= MAX_SERVERS ) return true; if( CL_DupeCheckServerList( adr, true )) return true; server = &cls.serverlist[cls.numservers]; CL_FreeServerInfo( server ); cls.numservers++; // add net address server->netaddress = copystring( adr ); server->mapname = copystring(Info_ValueForKey( info, "mapname")); server->maxplayers = com.atoi(Info_ValueForKey( info, "maxclients")); server->gamename = copystring(Info_ValueForKey( info, "gamename")); server->hostname = copystring(Info_ValueForKey( info, "hostname")); if( server->hostname ) { com.strncpy( shortName, server->hostname, sizeof(shortName)); server->shortname = copystring( shortName ); } // Check the player count server->numplayers = com.atoi(Info_ValueForKey( info, "curplayers")); if( server->numplayers <= 0 ) { server->numplayers = 0; token = strtok( info, "\n" ); if( token ) { token = strtok( NULL, "\n" ); while( token ) { server->numplayers++; token = strtok( NULL, "\n" ); } } } // check if it's valid if( !server->mapname[0] && !server->maxplayers && !server->gamename[0] && !server->hostname[0] ) { CL_FreeServerInfo( server ); return false; } server->playerstr = copystring( va("%i/%i", server->numplayers, server->maxplayers )); // add the ping server->ping = cls.realtime - cls.pingtime; server->pingstring = copystring( va("%ims", server->ping )); server->statusPacket = true; // print information MsgDev( D_NOTE, "%s %s ", server->hostname, server->mapname ); MsgDev( D_NOTE, "%i/%i %ims\n", server->numplayers, server->maxplayers, server->ping ); return true; } /* ================= CL_ParseStatusMessage Handle a reply from a ping ================= */ void CL_ParseStatusMessage( netadr_t from, sizebuf_t *msg ) { char *s; s = MSG_ReadString( msg ); Msg ("%s\n", s); CL_ParseServerStatus( NET_AdrToString(from), s ); } /* ================= CL_ConnectionlessPacket Responses to broadcasts, etc ================= */ void CL_ConnectionlessPacket( netadr_t from, sizebuf_t *msg ) { char *s, *c; MSG_BeginReading( msg ); MSG_ReadLong( msg ); // skip the -1 s = MSG_ReadStringLine( msg ); Cmd_TokenizeString( s ); c = Cmd_Argv(0); MsgDev(D_INFO, "%s: %s\n", NET_AdrToString (from), c); // server connection if(!com.strcmp( c, "client_connect")) { if (cls.state == ca_connected) { Msg ("Dup connect received. Ignored.\n"); return; } Netchan_Setup( NS_CLIENT, &cls.netchan, from, Cvar_VariableValue( "net_qport" )); MSG_WriteChar( &cls.netchan.message, clc_stringcmd ); MSG_WriteString( &cls.netchan.message, "new" ); cls.state = ca_connected; return; } // server responding to a status broadcast if(!com.strcmp( c, "info")) { CL_ParseStatusMessage( from, msg ); return; } // remote command from gui front end if(!strcmp(c, "cmd")) { if(!NET_IsLocalAddress(from)) { Msg ("Command packet from remote host. Ignored.\n"); return; } ShowWindow( host.hWnd, SW_RESTORE); SetForegroundWindow ( host.hWnd ); s = MSG_ReadString( msg ); Cbuf_AddText(s); Cbuf_AddText("\n"); return; } // print command from somewhere if (!strcmp(c, "print")) { // print command from somewhere s = MSG_ReadString( msg ); if(!CL_ParseServerStatus( NET_AdrToString( from ), s )) Msg( s ); return; } // ping from somewhere if (!strcmp(c, "ping")) { Netchan_OutOfBandPrint( NS_CLIENT, from, "ack" ); return; } // challenge from the server we are connecting to if (!strcmp(c, "challenge")) { cls.challenge = com.atoi(Cmd_Argv(1)); CL_SendConnectPacket (); return; } // echo request from server if (!strcmp(c, "echo")) { Netchan_OutOfBandPrint( NS_CLIENT, from, "%s", Cmd_Argv(1) ); 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(!com.strcmp( c, "disconnect" )) { CL_DisconnectPacket( from ); return; } Msg ("Unknown command.\n"); } //============================================================================= /* ============== CL_Userinfo_f ============== */ void CL_Userinfo_f (void) { Msg ("User info settings:\n"); Info_Print (Cvar_Userinfo()); } int precache_check; // for autodownload of precache items int precache_spawncount; int precache_tex; int precache_model_skin; byte *precache_model; // used for skin checking in alias models #define PLAYER_MULT 5 // ENV_CNT is map load, ENV_CNT+1 is first env map #define ENV_CNT (CS_PLAYERSKINS + MAX_CLIENTS * PLAYER_MULT) #define TEXTURE_CNT (ENV_CNT+13) static const char *env_suf[6] = {"rt", "bk", "lf", "ft", "up", "dn"}; void CL_RequestNextDownload (void) { uint map_checksum; // for detecting cheater maps char fn[MAX_OSPATH]; studiohdr_t *pheader; if(cls.state != ca_connected) return; if (!allow_download->value && precache_check < ENV_CNT) precache_check = ENV_CNT; if( precache_check == CS_MODELS ) { // confirm map precache_check = CS_MODELS+2; // 0 isn't used if(!CL_CheckOrDownloadFile(cl.configstrings[CS_MODELS+1])) return; // started a download } if (precache_check >= CS_MODELS && precache_check < CS_MODELS+MAX_MODELS) { while (precache_check < CS_MODELS+MAX_MODELS && cl.configstrings[precache_check][0]) { if (cl.configstrings[precache_check][0] == '*' || cl.configstrings[precache_check][0] == '#') { precache_check++; continue; } if (precache_model_skin == 0) { if (!CL_CheckOrDownloadFile(cl.configstrings[precache_check])) { precache_model_skin = 1; return; // started a download } precache_model_skin = 1; } // checking for skins in the model if (!precache_model) { precache_model = FS_LoadFile (cl.configstrings[precache_check], NULL); if (!precache_model) { precache_model_skin = 0; precache_check++; continue; // couldn't load it } if (LittleLong(*(uint *)precache_model) != IDSTUDIOHEADER) { // not an studio model precache_model = 0; precache_model_skin = 0; precache_check++; continue; } pheader = (studiohdr_t *)precache_model; if (LittleLong (pheader->version) != STUDIO_VERSION) { precache_check++; precache_model_skin = 0; continue; // couldn't load it } } pheader = (studiohdr_t *)precache_model; if (precache_model) { precache_model = 0; } precache_model_skin = 0; precache_check++; } precache_check = CS_SOUNDS; } if (precache_check >= CS_SOUNDS && precache_check < CS_SOUNDS+MAX_SOUNDS) { if (precache_check == CS_SOUNDS) precache_check++; // zero is blank while (precache_check < CS_SOUNDS+MAX_SOUNDS && cl.configstrings[precache_check][0]) { if (cl.configstrings[precache_check][0] == '*') { precache_check++; continue; } com.sprintf(fn, "sound/%s", cl.configstrings[precache_check++]); if (!CL_CheckOrDownloadFile(fn)) return; // started a download } precache_check = CS_IMAGES; } if (precache_check >= CS_IMAGES && precache_check < CS_IMAGES+MAX_IMAGES) { if (precache_check == CS_IMAGES) precache_check++; // zero is blank while (precache_check < CS_IMAGES+MAX_IMAGES && cl.configstrings[precache_check][0]) { com.sprintf(fn, "pics/%s.pcx", cl.configstrings[precache_check++]); if (!CL_CheckOrDownloadFile(fn)) return; // started a download } precache_check = CS_PLAYERSKINS; } // skins are special, since a player has three things to download: // model, weapon model and skin // so precache_check is now *3 if (precache_check >= CS_PLAYERSKINS && precache_check < CS_PLAYERSKINS + MAX_CLIENTS * PLAYER_MULT) { while (precache_check < CS_PLAYERSKINS + MAX_CLIENTS * PLAYER_MULT) { int i, n; char model[MAX_QPATH], skin[MAX_QPATH], *p; i = (precache_check - CS_PLAYERSKINS)/PLAYER_MULT; n = (precache_check - CS_PLAYERSKINS)%PLAYER_MULT; if (!cl.configstrings[CS_PLAYERSKINS+i][0]) { precache_check = CS_PLAYERSKINS + (i + 1) * PLAYER_MULT; continue; } if ((p = strchr(cl.configstrings[CS_PLAYERSKINS+i], '\\')) != NULL) p++; else p = cl.configstrings[CS_PLAYERSKINS+i]; strcpy(model, p); p = strchr(model, '/'); if (!p) p = strchr(model, '\\'); if (p) { *p++ = 0; strcpy(skin, p); } else *skin = 0; switch (n) { case 0: // model com.sprintf(fn, "models/players/%s/player.mdl", model); if (!CL_CheckOrDownloadFile(fn)) { precache_check = CS_PLAYERSKINS + i * PLAYER_MULT + 1; return; // started a download } n++; /*FALL THROUGH*/ case 1: // weapon model com.sprintf(fn, "weapons/%s.mdl", model); if (!CL_CheckOrDownloadFile(fn)) { precache_check = CS_PLAYERSKINS + i * PLAYER_MULT + 2; return; // started a download } n++; /*FALL THROUGH*/ default: break; } } // precache phase completed precache_check = ENV_CNT; } if (precache_check == ENV_CNT) { precache_check = ENV_CNT + 1; pe->BeginRegistration( cl.configstrings[CS_MODELS+1], true, &map_checksum ); if( map_checksum != com.atoi(cl.configstrings[CS_MAPCHECKSUM])) { Host_Error("Local map version differs from server: %i != '%s'\n", map_checksum, cl.configstrings[CS_MAPCHECKSUM]); return; } } if (precache_check > ENV_CNT && precache_check < TEXTURE_CNT) { if( allow_download->value ) { while (precache_check < TEXTURE_CNT) { int n = precache_check++ - ENV_CNT - 1; if (n & 1) com.sprintf(fn, "gfx/env/%s.dds", cl.configstrings[CS_SKY] ); // cubemap pack else com.sprintf(fn, "gfx/env/%s%s.tga", cl.configstrings[CS_SKY], env_suf[n/2]); if (!CL_CheckOrDownloadFile(fn)) return; // started a download } } precache_check = TEXTURE_CNT; } if (precache_check == TEXTURE_CNT) { precache_check = TEXTURE_CNT+1; precache_tex = 0; } // confirm existance of textures, download any that don't exist if (precache_check == TEXTURE_CNT+1) { if (allow_download->value ) { while( precache_tex < pe->NumTextures()) { char fn[MAX_OSPATH]; com.sprintf(fn, "textures/%s.tga", pe->GetTextureName( precache_tex++ )); if(!CL_CheckOrDownloadFile(fn)) return; // started a download } } precache_check = TEXTURE_CNT+999; } CL_RegisterSounds(); CL_PrepRefresh(); if( cls.demoplayback ) return; // not really connected MSG_WriteByte( &cls.netchan.message, clc_stringcmd ); MSG_WriteString( &cls.netchan.message, va("begin %i\n", precache_spawncount)); } /* ================= CL_Precache_f The server will send this command right before allowing the client into the server ================= */ void CL_Precache_f (void) { precache_check = CS_MODELS; precache_spawncount = com.atoi(Cmd_Argv(1)); precache_model = 0; precache_model_skin = 0; CL_RequestNextDownload(); } /* ================= CL_InitLocal ================= */ void CL_InitLocal (void) { cls.state = ca_disconnected; cls.realtime = Sys_Milliseconds(); CL_InitInput(); // register our variables cl_add_blend = Cvar_Get ("cl_blend", "1", 0, "disables client blends" ); cl_add_lights = Cvar_Get ("cl_lights", "1", 0, "disables dynamic lights" ); cl_add_particles = Cvar_Get ("cl_particles", "1", 0, "disables particles engine" ); cl_add_entities = Cvar_Get ("cl_entities", "1", 0, "disables client entities" ); cl_gun = Cvar_Get ("cl_gun", "1", 0, "hide firstperson viewmodel" ); cl_footsteps = Cvar_Get ("cl_footsteps", "1", 0, "disables player footsteps" ); cl_predict = Cvar_Get ("cl_predict", "1", CVAR_ARCHIVE, "disables client movement prediction" ); // cl_minfps = Cvar_Get ("cl_minfps", "5", 0); cl_upspeed = Cvar_Get ("cl_upspeed", "200", 0, "client upspeed limit" ); cl_forwardspeed = Cvar_Get ("cl_forwardspeed", "200", 0, "client forward speed limit" ); cl_sidespeed = Cvar_Get ("cl_sidespeed", "200", 0, "client side-speed limit" ); cl_yawspeed = Cvar_Get ("cl_yawspeed", "140", 0, "client yaw speed" ); cl_pitchspeed = Cvar_Get ("cl_pitchspeed", "150", 0, "client pitch speed" ); cl_anglespeedkey = Cvar_Get ("cl_anglespeedkey", "1.5", 0, "client anglespeed" ); cl_maxpackets = Cvar_Get( "cl_maxpackets", "30", CVAR_ARCHIVE, "number of usercmd packets" ); cl_run = Cvar_Get ("cl_run", "0", CVAR_ARCHIVE, "keep client for always run mode" ); cl_mouselook = Cvar_Get( "cl_mouselook", "1", CVAR_ARCHIVE, "enables mouse look" ); lookspring = Cvar_Get ("lookspring", "0", CVAR_ARCHIVE, "allow look spring" ); lookstrafe = Cvar_Get ("lookstrafe", "0", CVAR_ARCHIVE, "allow look strafe" ); m_pitch = Cvar_Get ("m_pitch", "0.022", CVAR_ARCHIVE, "mouse pitch value" ); m_yaw = Cvar_Get ("m_yaw", "0.022", 0, "mouse yaw value" ); m_forward = Cvar_Get ("m_forward", "1", 0, "mouse forward speed" ); m_side = Cvar_Get ("m_side", "1", 0, "mouse side speed" ); cl_shownet = Cvar_Get ("cl_shownet", "0", 0, "client show network packets" ); cl_showmiss = Cvar_Get ("cl_showmiss", "0", 0, "client show network errors" ); cl_showclamp = Cvar_Get ("showclamp", "0", 0, "show client clamping" ); cl_timeout = Cvar_Get ("cl_timeout", "120", 0, "connect timeout (in-seconds)" ); rcon_client_password = Cvar_Get ("rcon_password", "", 0, "remote control client password" ); rcon_address = Cvar_Get ("rcon_address", "", 0, "remote control address" ); cl_lightlevel = Cvar_Get ("r_lightlevel", "0", 0, "no description" ); // // userinfo // info_password = Cvar_Get ("password", "", CVAR_USERINFO, "player password" ); info_spectator = Cvar_Get ("spectator", "0", CVAR_USERINFO, "spectator mode" ); name = Cvar_Get ("name", "unnamed", CVAR_USERINFO | CVAR_ARCHIVE, "player name" ); skin = Cvar_Get ("skin", "male/grunt", CVAR_USERINFO | CVAR_ARCHIVE, "playerskin" ); rate = Cvar_Get ("rate", "25000", CVAR_USERINFO | CVAR_ARCHIVE, "player network rate" ); // FIXME hand = Cvar_Get ("hand", "0", CVAR_USERINFO | CVAR_ARCHIVE, "viewmodel handedness" ); fov = Cvar_Get ("fov", "90", CVAR_USERINFO | CVAR_ARCHIVE, "client fov" ); cl_vwep = Cvar_Get ("cl_vwep", "1", CVAR_ARCHIVE, "no description" ); cl_showfps = Cvar_Get ("cl_showfps", "1", CVAR_ARCHIVE, "show client fps" ); // register our commands Cmd_AddCommand ("cmd", CL_ForwardToServer_f, "send a console commandline to the server" ); Cmd_AddCommand ("pause", CL_Pause_f, "pause the game (if the server allows pausing)" ); Cmd_AddCommand ("getserverlist", CL_GetServerList_f, "get info about local servers" ); Cmd_AddCommand ("freeserverlist", CL_FreeServerList_f, "clear info about local servers" ); Cmd_AddCommand ("userinfo", CL_Userinfo_f, "print current client userinfo" ); Cmd_AddCommand ("changing", CL_Changing_f, "sent by server to tell client to wait for level change" ); Cmd_AddCommand ("disconnect", CL_Disconnect_f, "disconnect from server" ); Cmd_AddCommand ("record", CL_Record_f, "record a demo" ); Cmd_AddCommand ("playdemo", CL_PlayDemo_f, "playing a demo" ); Cmd_AddCommand ("stop", CL_Stop_f, "stop playing or recording a demo" ); Cmd_AddCommand ("quit", CL_Quit_f, "quit from game" ); Cmd_AddCommand ("exit", CL_Quit_f, "quit from game" ); Cmd_AddCommand ("screenshot", CL_ScreenShot_f, "takes a screenshot of the next rendered frame" ); Cmd_AddCommand ("levelshot", CL_LevelShot_f, "same as \"screenshot\", used for create plaque images" ); Cmd_AddCommand ("connect", CL_Connect_f, "connect to a server by hostname" ); Cmd_AddCommand ("reconnect", CL_Reconnect_f, "reconnect to current level" ); Cmd_AddCommand ("rcon", CL_Rcon_f, "sends a command to the server console (rcon_password and rcon_address required)" ); // this is dangerous to leave in // Cmd_AddCommand ("packet", CL_Packet_f, "send a packet with custom contents" ); Cmd_AddCommand ("precache", CL_Precache_f, "precache specified resource (by index)" ); Cmd_AddCommand ("download", CL_Download_f, "download specified resource (by name)" ); // // forward to server commands // // the only thing this does is allow command completion // to work -- all unknown commands are automatically // forwarded to the server Cmd_AddCommand ("wave", NULL, NULL ); Cmd_AddCommand ("inven", NULL, NULL ); Cmd_AddCommand ("kill", NULL, NULL ); Cmd_AddCommand ("use", NULL, NULL ); Cmd_AddCommand ("drop", NULL, NULL ); Cmd_AddCommand ("say", NULL, NULL ); Cmd_AddCommand ("say_team", NULL, NULL ); Cmd_AddCommand ("info", NULL, NULL ); Cmd_AddCommand ("prog", NULL, NULL ); Cmd_AddCommand ("give", NULL, NULL ); Cmd_AddCommand ("god", NULL, NULL ); Cmd_AddCommand ("notarget", NULL, NULL ); Cmd_AddCommand ("noclip", NULL, NULL ); Cmd_AddCommand ("invuse", NULL, NULL ); Cmd_AddCommand ("invprev", NULL, NULL ); Cmd_AddCommand ("invnext", NULL, NULL ); Cmd_AddCommand ("invdrop", NULL, NULL ); Cmd_AddCommand ("weapnext", NULL, NULL ); Cmd_AddCommand ("weapprev", NULL, NULL ); } /* =============== CL_WriteConfiguration Writes key bindings and archived cvars to config.cfg =============== */ void CL_WriteConfiguration (void) { file_t *f; if (cls.state == ca_uninitialized) return; f = FS_Open("scripts/config/keys.rc", "w"); if(f) { FS_Printf (f, "//=======================================================================\n"); FS_Printf (f, "//\t\t\tCopyright XashXT Group 2007 ©\n"); FS_Printf (f, "//\t\t\tkeys.rc - current key bindings\n"); FS_Printf (f, "//=======================================================================\n"); Key_WriteBindings(f); FS_Close (f); } else MsgDev( D_ERROR, "Couldn't write keys.rc.\n"); f = FS_Open("scripts/config/vars.rc", "w"); if(f) { FS_Printf (f, "//=======================================================================\n"); FS_Printf (f, "//\t\t\tCopyright XashXT Group 2007 ©\n"); FS_Printf (f, "//\t\t\tvars.rc - archive of cvars\n"); FS_Printf (f, "//=======================================================================\n"); Cmd_WriteVariables(f); FS_Close (f); } else MsgDev( D_ERROR, "Couldn't write vars.rc.\n"); } //============================================================================ void CL_ReadNetMessage( void ) { sizebuf_t net_message; sizebuf_t *msg = &net_message; byte buffer[MAX_MSGLEN]; netadr_t net_from; SZ_Init( msg, buffer, sizeof(buffer)); while( NET_GetPacket(NS_CLIENT, &net_from, msg )) { if( msg->cursize >= 4 && *(int *)msg->data == -1 ) { cls.netchan.last_received = cls.realtime; CL_ConnectionlessPacket( net_from, msg ); continue; } // can't be a valid sequenced packet if( cls.state < ca_connected ) continue; if( msg->cursize < 8 ) { MsgDev( D_WARN, "%s: runt packet\n", NET_AdrToString( net_from )); continue; } // packet from server if (!NET_CompareAdr( net_from, cls.netchan.remote_address)) { MsgDev( D_WARN, "CL_ReadPackets: %s:sequenced packet without connection\n", NET_AdrToString( net_from )); continue; } if(Netchan_Process( &cls.netchan, msg )) { // the header is different lengths for reliable and unreliable messages int headerBytes = msg->readcount; cls.netchan.last_received = 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( cls.demorecording && !cls.demowaiting ) CL_WriteDemoMessage( msg, headerBytes ); } } } /* ================= CL_ReadPackets ================= */ void CL_ReadPackets (void) { if( cls.demoplayback ) CL_ReadDemoMessage(); else CL_ReadNetMessage(); } /* ================== CL_CheckTimeout ================== */ void CL_CheckTimeout( void ) { // check timeout if( !cl_paused->integer && cls.state >= ca_connected && cls.state != ca_cinematic && !cls.demoplayback ) { if( cls.realtime - cls.netchan.last_received > cl_timeout->integer * 1000 ) { if( ++cl.timeoutcount > 5 ) // timeoutcount saves debugger { Msg ("\nServer connection timed out.\n"); CL_Disconnect(); return; } } } else cl.timeoutcount = 0; } /* ================== CL_SendCommand ================== */ void CL_SendCommand (void) { // get new key events CL_CheckTimeout(); // process console commands Cbuf_Execute(); // send intentions now CL_SendCmd(); // resend a connection request if necessary CL_CheckForResend(); } /* ================== CL_Frame ================== */ void CL_Frame( dword time ) { static dword extratime; if( host.type == HOST_DEDICATED ) return; extratime += time; if( cls.state == ca_connected && extratime < HOST_FRAMETIME ) return; // don't flood packets out while connecting // decide the simulation time cl.time += extratime; cls.realtime = Sys_Milliseconds(); //FIXME: teseing with += time; cls.frametime = extratime * 0.001; extratime = 0; if( cls.frametime > (1.0 / 5)) cls.frametime = (1.0 / 5); // setup the VM frame CL_VM_Begin(); // fetch results from server CL_ReadPackets(); // if we haven't gotten a packet in a long time, drop the connection CL_CheckTimeout(); // send intentions now CL_SendCommand(); // predict all unacknowledged movements CL_PredictMovement(); // allow rendering DLL change if(!cl.refresh_prepped && cls.state == ca_active ) CL_PrepRefresh(); // update the screen SCR_UpdateScreen(); // update audio S_Update( cl.playernum + 1, cl.refdef.vieworg, vec3_origin, cl.v_forward, cl.v_up ); // advance local effects for next frame CL_RunDLights (); CL_RunLightStyles (); SCR_RunCinematic(); Con_RunConsole(); // end the client VM frame CL_VM_End(); cls.framecount++; } //============================================================================ /* ==================== CL_Init ==================== */ void CL_Init( void ) { if( host.type == HOST_DEDICATED ) return; // nothing running on the client // all archived variables will now be loaded scr_loading = Cvar_Get("scr_loading", "0", 0, "loading bar progress" ); cl_paused = Cvar_Get( "paused", "0", 0, "game paused" ); Con_Init(); VID_Init(); V_Init(); CL_InitClientProgs(); UI_Init(); SCR_Init(); CL_InitLocal(); host.cl_running = true; } /* =============== CL_Shutdown FIXME: this is a callback from Sys_Quit and Host_Error. It would be better to run quit through here before the final handoff to the sys code. =============== */ void CL_Shutdown(void) { // already freed if(host.state == HOST_ERROR) return; CL_WriteConfiguration(); CL_FreeClientProgs(); UI_Shutdown(); S_Shutdown(); CL_ShutdownInput(); host.cl_running = false; }