From 425decdf7e9284d15aa726e3ae96b9942fb0e3ea Mon Sep 17 00:00:00 2001 From: IronClawTrem Date: Sun, 16 Feb 2020 03:40:06 +0000 Subject: create tremded branch --- src/client/cl_main.cpp | 5083 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 5083 insertions(+) create mode 100644 src/client/cl_main.cpp (limited to 'src/client/cl_main.cpp') diff --git a/src/client/cl_main.cpp b/src/client/cl_main.cpp new file mode 100644 index 0000000..8b33acd --- /dev/null +++ b/src/client/cl_main.cpp @@ -0,0 +1,5083 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +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 3 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, see + +=========================================================================== +*/ +// cl_main.c -- client main loop + +#include "client.h" + +#ifndef _WIN32 +#include +#endif + +#include + +#include "sys/sys_loadlib.h" +#include "sys/sys_local.h" + +#include "cl_updates.h" +#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_voipProtocol; +cvar_t *cl_voip; +#endif + +#ifdef USE_RENDERER_DLOPEN +cvar_t *cl_renderer; +#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_mouseAccelOffset; +cvar_t *cl_mouseAccelStyle; +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 *j_pitch; +cvar_t *j_yaw; +cvar_t *j_forward; +cvar_t *j_side; +cvar_t *j_up; +cvar_t *j_pitch_axis; +cvar_t *j_yaw_axis; +cvar_t *j_forward_axis; +cvar_t *j_side_axis; +cvar_t *j_up_axis; + +cvar_t *cl_activeAction; + +cvar_t *cl_motdString; + +cvar_t *cl_allowDownload; +cvar_t *com_downloadPrompt; +cvar_t *cl_conXOffset; +cvar_t *cl_inGameVideo; + +cvar_t *cl_serverStatusResendTime; + +cvar_t *cl_lanForcePackets; + +cvar_t *cl_guidServerUniq; + +cvar_t *cl_clantag; + +cvar_t *cl_consoleKeys; + +cvar_t *cl_rate; + +cvar_t *cl_rsaAuth; + +clientActive_t cl; +clientConnection_t clc; +clientStatic_t cls; + +char cl_reconnectArgs[MAX_OSPATH]; +char cl_oldGame[MAX_QPATH]; +bool cl_oldGameSet; + +// Structure containing functions exported from refresh DLL +refexport_t re; +#ifdef USE_RENDERER_DLOPEN +static void *rendererLib = NULL; +#endif + +ping_t cl_pinglist[MAX_PINGREQUESTS]; + +typedef struct serverStatus_s { + char string[BIG_INFO_STRING]; + netadr_t address; + int time, startTime; + bool pending; + bool print; + bool retrieved; +} serverStatus_t; + +serverStatus_t cl_serverStatusList[MAX_SERVERSTATUSREQUESTS]; + +static void CL_InitRef(void); + +#if defined __USEA3D && defined __A3D_GEOM +void hA3Dg_ExportRenderGeom(refexport_t *incoming_re); +#endif + +static bool noGameRestart = false; + +static void CL_DownloadUpdate_f() { CL_DownloadRelease(); } + +static void CL_InstallUpdate_f() +{ + if (Cmd_Argc() > 1) + CL_ExecuteInstaller(); + else + CL_ExecuteInstaller(); +} + +static void CL_CheckForUpdate_f() { CL_GetLatestRelease(); } + +static void CL_BrowseHomepath_f() { FS_BrowseHomepath(); } + +static void CL_BrowseDemos_f() { FS_OpenBaseGamePath( "demos/" ); } + +static void CL_BrowseScreenShots_f() { FS_OpenBaseGamePath( "screenshots/" ); } + +#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. + if (clc.netchan.alternateProtocol == 2) + { + AngleVectors(cl.snap.alternatePs.viewangles, forward, NULL, up); + + pos[0] = cl.snap.alternatePs.origin[0] * scale; + pos[1] = cl.snap.alternatePs.origin[2] * scale; + pos[2] = cl.snap.alternatePs.origin[1] * scale; + } + else + { + 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, bool 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), false); + Com_Printf("VoIP: %s ignoring player #%d\n", ignore ? "Now" : "No longer", id); + return; + } + } + Com_Printf("VoIP: invalid player ID#\n"); +} + +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 (clc.state != CA_ACTIVE) + reason = "Not connected to a server"; + else if (!clc.voipCodecInitialized) + reason = "Voip codec not initialized"; + else if (!clc.voipEnabled) + 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), true); + } + else if (strcmp(cmd, "unignore") == 0) + { + CL_UpdateVoipIgnore(Cmd_Argv(2), false); + } + else if (strcmp(cmd, "gain") == 0) + { + if (Cmd_Argc() > 3) + { + CL_UpdateVoipGain(Cmd_Argv(2), atof(Cmd_Argv(3))); + } + else if (Q_isanumber(Cmd_Argv(2))) + { + int id = atoi(Cmd_Argv(2)); + if (id >= 0 && id < MAX_CLIENTS) + { + Com_Printf( + "VoIP: current gain for player #%d " + "is %f\n", + id, clc.voipGain[id]); + } + else + { + Com_Printf("VoIP: invalid player ID#\n"); + } + } + else + { + Com_Printf("usage: voip gain [value]\n"); + } + } + else if (strcmp(cmd, "muteall") == 0) + { + Com_Printf("VoIP: muting incoming voice\n"); + CL_AddReliableCommand("voip muteall", false); + clc.voipMuteAll = true; + } + else if (strcmp(cmd, "unmuteall") == 0) + { + Com_Printf("VoIP: unmuting incoming voice\n"); + CL_AddReliableCommand("voip unmuteall", false); + clc.voipMuteAll = false; + } + else + { + Com_Printf( + "usage: voip [un]ignore \n" + " voip [un]muteall\n" + " voip gain [value]\n"); + } +} + +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; + + opus_encoder_ctl(clc.opusEncoder, OPUS_RESET_STATE); +} + +/* +=============== +CL_VoipParseTargets + +sets clc.voipTargets according to cl_voipSendTarget +Generally we don't want who's listening to change during a transmission, +so this is only called when the key is first pressed +=============== +*/ +static void CL_VoipParseTargets(void) +{ + const char *target = cl_voipSendTarget->string; + char *end; + int val; + + ::memset(clc.voipTargets, 0, sizeof(clc.voipTargets)); + clc.voipFlags &= ~VOIP_SPATIAL; + + while (target) + { + while (*target == ',' || *target == ' ') target++; + + if (!*target) break; + + if (isdigit(*target)) + { + val = strtol(target, &end, 10); + target = end; + } + else + { + if (!Q_stricmpn(target, "all", 3)) + { + ::memset(clc.voipTargets, ~0, sizeof(clc.voipTargets)); + return; + } + if (!Q_stricmpn(target, "spatial", 7)) + { + clc.voipFlags |= VOIP_SPATIAL; + target += 7; + continue; + } + else + { + if (!Q_stricmpn(target, "attacker", 8)) + { + val = VM_Call(cls.cgame, CG_LAST_ATTACKER); + target += 8; + } + else if (!Q_stricmpn(target, "crosshair", 9)) + { + val = VM_Call(cls.cgame, CG_CROSSHAIR_PLAYER); + target += 9; + } + else + { + while (*target && *target != ',' && *target != ' ') target++; + + continue; + } + + if (val < 0) continue; + } + } + + if (val < 0 || val >= MAX_CLIENTS) + { + Com_Printf(S_COLOR_YELLOW + "WARNING: VoIP " + "target %d is not a valid client " + "number\n", + val); + + continue; + } + + clc.voipTargets[val / 8] |= 1 << (val % 8); + } +} + +/* +=============== +CL_CaptureVoip + +Record more audio from the hardware if required and encode it into Opus + data for later transmission. +=============== +*/ +static void CL_CaptureVoip(void) +{ + const float audioMult = cl_voipCaptureMult->value; + const bool useVad = (cl_voipUseVAD->integer != 0); + bool initialFrame = false; + bool finalFrame = false; + +#if USE_MUMBLE + // if we're using Mumble, don't try to handle VoIP transmission ourselves. + if (cl_useMumble->integer) return; +#endif + + // 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->modified || cl_rate->modified) + { + if ((cl_voip->integer) && (cl_rate->integer < 25000)) + { + Com_Printf(S_COLOR_YELLOW "Your network rate is too slow for VoIP.\n"); + Com_Printf("Set 'Data Rate' to 'LAN/Cable/xDSL' in 'Setup/System/Network'.\n"); + Com_Printf("Until then, VoIP is disabled.\n"); + Cvar_Set("cl_voip", "0"); + } + Cvar_Set("cl_voipProtocol", cl_voip->integer ? "opus" : ""); + cl_voip->modified = false; + cl_rate->modified = false; + } + + if (!clc.voipCodecInitialized) 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 = false; + } + + if ((useVad) && (!cl_voipSend->integer)) Cvar_Set("cl_voipSend", "1"); // lots of things reset this. + + if (cl_voipSend->modified) + { + bool dontCapture = false; + if (clc.state != CA_ACTIVE) + dontCapture = true; // not connected to a server. + else if (!clc.voipEnabled) + dontCapture = true; // server doesn't support VoIP. + else if (clc.demoplaying) + dontCapture = true; // playing back a demo. + else if (cl_voip->integer == 0) + dontCapture = true; // client has VoIP support disabled. + else if (audioMult == 0.0f) + dontCapture = true; // basically silenced incoming audio. + + cl_voipSend->modified = false; + + if (dontCapture) + { + Cvar_Set("cl_voipSend", "0"); + return; + } + + if (cl_voipSend->integer) + { + initialFrame = true; + } + else + { + finalFrame = true; + } + } + + // try to get more audio data from the sound card... + + if (initialFrame) + { + S_MasterGain(Com_Clamp(0.0f, 1.0f, cl_voipGainDuringCapture->value)); + S_StartCapture(); + CL_VoipNewGeneration(); + CL_VoipParseTargets(); + } + + if ((cl_voipSend->integer) || (finalFrame)) + { // user wants to capture audio? + int samples = S_AvailableCaptureSamples(); + const int packetSamples = (finalFrame) ? VOIP_MAX_FRAME_SAMPLES : VOIP_MAX_PACKET_SAMPLES; + + // enough data buffered in audio hardware to process yet? + if (samples >= packetSamples) + { + // audio capture is always MONO16. + static int16_t sampbuffer[VOIP_MAX_PACKET_SAMPLES]; + float voipPower = 0.0f; + int voipFrames; + int i, bytes; + + if (samples > VOIP_MAX_PACKET_SAMPLES) samples = VOIP_MAX_PACKET_SAMPLES; + + // !!! FIXME: maybe separate recording from encoding, so voipPower + // !!! FIXME: updates faster than 4Hz? + + samples -= samples % VOIP_MAX_FRAME_SAMPLES; + if (samples != 120 && samples != 240 && samples != 480 && samples != 960 && samples != 1920 && + samples != 2880) + { + Com_Printf("Voip: bad number of samples %d\n", samples); + return; + } + voipFrames = samples / VOIP_MAX_FRAME_SAMPLES; + + S_Capture(samples, (byte *)sampbuffer); // grab from audio card. + + // check the "power" of this packet... + for (i = 0; i < samples; i++) + { + const float flsamp = (float)sampbuffer[i]; + const float s = fabs(flsamp); + voipPower += s * s; + sampbuffer[i] = (int16_t)((flsamp)*audioMult); + } + + // encode raw audio samples into Opus data... + bytes = opus_encode(clc.opusEncoder, sampbuffer, samples, (unsigned char *)clc.voipOutgoingData, + sizeof(clc.voipOutgoingData)); + if (bytes <= 0) + { + Com_DPrintf("VoIP: Error encoding %d samples\n", samples); + bytes = 0; + } + + clc.voipPower = (voipPower / (32768.0f * 32768.0f * ((float)samples))) * 100.0f; + + if ((useVad) && (clc.voipPower < cl_voipVADThreshold->value)) + { + CL_VoipNewGeneration(); // no "talk" for at least 1/4 second. + } + else + { + clc.voipOutgoingDataSize = bytes; + clc.voipOutgoingDataFrames = voipFrames; + + Com_DPrintf("VoIP: Send %d frames, %d bytes, %f power\n", voipFrames, bytes, clc.voipPower); + +#if 0 + static FILE *encio = NULL; + if (encio == NULL) encio = fopen("voip-outgoing-encoded.bin", "wb"); + if (encio != NULL) { fwrite(clc.voipOutgoingData, bytes, 1, encio); fflush(encio); } + static FILE *decio = NULL; + if (decio == NULL) decio = fopen("voip-outgoing-decoded.bin", "wb"); + if (decio != NULL) { fwrite(sampbuffer, voipFrames * VOIP_MAX_FRAME_SAMPLES * 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, bool isDisconnectCmd) +{ + int unacknowledged = clc.reliableSequence - clc.reliableAcknowledge; + + // if we would be losing an old command that hasn't been acknowledged, + // we must drop the connection + // also leave one slot open for the disconnect command in this case. + + if ((isDisconnectCmd && unacknowledged > MAX_RELIABLE_COMMANDS) || + (!isDisconnectCmd && unacknowledged >= MAX_RELIABLE_COMMANDS)) + { + if (com_errorEntered) + return; + else + Com_Error(ERR_DROP, "Client command overflow"); + } + + Q_strncpyz( + clc.reliableCommands[++clc.reliableSequence & (MAX_RELIABLE_COMMANDS - 1)], cmd, sizeof(*clc.reliableCommands)); +} + +/* +======================================================================= + +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 = false; + clc.spDemoRecording = false; + Com_Printf("Stopped demo.\n"); +} + +/* +================== +CL_DemoFilename +================== +*/ +static void CL_DemoFilename(int number, char *fileName, int fileNameSize) +{ + 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, fileNameSize, "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 + +static 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; + + if (Cmd_Argc() > 2) + { + Com_Printf("record \n"); + return; + } + + if (clc.demorecording) + { + if (!clc.spDemoRecording) + { + Com_Printf("Already recording.\n"); + } + return; + } + + if (clc.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) + { + const char *s = Cmd_Argv(1); + Q_strncpyz(demoName, s, sizeof(demoName)); + Com_sprintf( + name, sizeof(name), + "demos/%s.%s%d", demoName, DEMOEXT, + (clc.netchan.alternateProtocol == 0 ? + PROTOCOL_VERSION : clc.netchan.alternateProtocol == 1 ? 70 : 69)); + } + else + { + int number; + + // scan for a free demo name + for (number = 0; number <= 9999; number++) + { + CL_DemoFilename(number, demoName, sizeof(demoName)); + Com_sprintf(name, sizeof(name), "demos/%s.%s%d", demoName, DEMOEXT, 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 = true; + if (Cvar_VariableValue("ui_recordSPDemo")) + { + clc.spDemoRecording = true; + } + else + { + clc.spDemoRecording = false; + } + + Q_strncpyz(clc.demoName, demoName, sizeof(clc.demoName)); + + // don't start saving messages until a non-delta compressed message is received + clc.demowaiting = true; + + // 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; + } + const char *s = cl.gameState.stringData + cl.gameState.stringOffsets[i]; + MSG_WriteByte(&buf, svc_configstring); + MSG_WriteShort(&buf, i); + MSG_WriteBigString(&buf, s); + } + + // baselines + ::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(clc.netchan.alternateProtocol, &buf, &nullstate, ent, true); + } + + 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(true); + 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 int CL_WalkDemoExt(const char *arg, char *name, int *demofile) +{ + int i; + *demofile = 0; + + Com_sprintf(name, MAX_OSPATH, "demos/%s.%s%d", arg, DEMOEXT, PROTOCOL_VERSION); + FS_FOpenFileRead(name, demofile, true); + + if (*demofile) + { + Com_Printf("Demo file: %s\n", name); + return PROTOCOL_VERSION; + } + + Com_Printf("Not found: %s\n", name); + + for (i = 0; demo_protocols[i]; ++i) + { + if (demo_protocols[i] == PROTOCOL_VERSION) continue; + + Com_sprintf(name, MAX_OSPATH, "demos/%s.%s%d", arg, DEMOEXT, demo_protocols[i]); + FS_FOpenFileRead(name, demofile, true); + if (*demofile) + { + Com_Printf("Demo file: %s\n", name); + + return demo_protocols[i]; + } + else + Com_Printf("Not found: %s\n", name); + } + + return -1; +} + +/* +==================== +CL_CompleteDemoName +==================== +*/ +static void CL_CompleteDemoName(char *args, int argNum) +{ + if (argNum == 2) + { + char demoExt[16]; + + Com_sprintf(demoExt, sizeof(demoExt), ".%s%d", DEMOEXT, PROTOCOL_VERSION); + Field_CompleteFilename("demos", demoExt, true, true); + } +} + +/* +==================== +CL_PlayDemo_f + +demo + +==================== +*/ +void CL_PlayDemo_f(void) +{ + char name[MAX_OSPATH]; + const char *ext_test; + int protocol, i; + char retry[MAX_OSPATH]; + + if (Cmd_Argc() != 2) + { + Com_Printf("demo \n"); + return; + } + + // make sure a local server is killed + // 2 means don't force disconnect of local client + Cvar_Set("sv_killserver", "2"); + + // open the demo file + const char *arg = Cmd_Argv(1); + + CL_Disconnect(true); + + // check for an extension .DEMOEXT_?? (?? is protocol) + ext_test = strrchr(arg, '.'); + + if (ext_test && !Q_stricmpn(ext_test + 1, DEMOEXT, ARRAY_LEN(DEMOEXT) - 1)) + { + protocol = atoi(ext_test + ARRAY_LEN(DEMOEXT)); + + for (i = 0; demo_protocols[i]; i++) + { + if (demo_protocols[i] == protocol) break; + } + + if (demo_protocols[i] || protocol == PROTOCOL_VERSION) + { + Com_sprintf(name, sizeof(name), "demos/%s", arg); + FS_FOpenFileRead(name, &clc.demofile, true); + } + else + { + int len; + + Com_Printf("Protocol %d not supported for demos\n", protocol); + len = ext_test - arg; + + if (len >= ARRAY_LEN(retry)) len = ARRAY_LEN(retry) - 1; + + Q_strncpyz(retry, arg, len + 1); + retry[len] = '\0'; + protocol = CL_WalkDemoExt(retry, name, &clc.demofile); + } + } + else + protocol = CL_WalkDemoExt(arg, name, &clc.demofile); + + if (!clc.demofile) + { + Com_Error(ERR_DROP, "couldn't open %s", name); + return; + } + Q_strncpyz(clc.demoName, arg, sizeof(clc.demoName)); + + clc.state = CA_CONNECTED; + clc.demoplaying = true; + Q_strncpyz(clc.servername, arg, sizeof(clc.servername)); + clc.netchan.alternateProtocol = (protocol == 69 ? 2 : protocol == 70 ? 1 : 0); + + // read demo messages until connected + while (clc.state >= CA_CONNECTED && clc.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 = false; +} + +/* +==================== +CL_StartDemoLoop + +Closing the main menu will restart the demo loop +==================== +*/ +static void CL_StartDemoLoop(void) +{ + // start the demo loop again + Cbuf_AddText("d1\n"); + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); +} + +/* +================== +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_ClearState + +Called before parsing a gamestate +===================== +*/ +void CL_ClearState(void) +{ + // S_StopAllSounds(); + + ::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)); +} + +static void CL_OldGame(void) +{ + if (cl_oldGameSet) + { + // change back to previous fs_game + cl_oldGameSet = false; + Cvar_Set2("fs_game", cl_oldGame, true); + FS_ConditionalRestart(clc.checksumFeed, false); + } +} + +/* +===================== +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(bool 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.voipCodecInitialized) + { + int i; + opus_encoder_destroy(clc.opusEncoder); + for (i = 0; i < MAX_CLIENTS; i++) + { + opus_decoder_destroy(clc.opusDecoder[i]); + } + } + Cmd_RemoveCommand("voip"); +#endif + + if (clc.demofile) + { + FS_FCloseFile(clc.demofile); + clc.demofile = 0; + } + + if (cls.ui && showMainMenu) + { + VM_Call(cls.ui, UI_SET_ACTIVE_MENU - (cls.uiInterface == 2 ? 2 : 0), UIMENU_NONE); + } + + SCR_StopCinematic(); + S_ClearSoundBuffer(); + + // send a disconnect message to the server + // send it a few times in case one is dropped + if (clc.state >= CA_CONNECTED) + { + CL_AddReliableCommand("disconnect", true); + CL_WritePacket(); + CL_WritePacket(); + CL_WritePacket(); + } + + // Remove pure paks + FS_PureServerSetLoadedPaks("", ""); + FS_PureServerSetReferencedPaks("", ""); + + CL_ClearState(); + + // wipe the client connection + ::memset(&clc, 0, sizeof(clc)); + + clc.state = CA_DISCONNECTED; + + CL_ProtocolSpecificCommandsInit(); + + // allow cheats locally + Cvar_Set("sv_cheats", "1"); + + // not connected to a pure server anymore + cl_connectedToPureServer = false; + +#ifdef USE_VOIP + // not connected to voip server anymore. + clc.voipEnabled = false; +#endif + + // Stop recording any video + if (CL_VideoRecording()) + { + // Finish rendering current frame + SCR_UpdateScreen(); + CL_CloseAVI(); + } + + CL_UpdateGUID(NULL, 0); + + if (!noGameRestart) + CL_OldGame(); + else + noGameRestart = false; +} + +/* +=================== +CL_ForwardCommandToServer + +adds the current command line as a clientCommand +things like godmode, noclip, etc, are commands directed to the server, +so when they are typed in at the console, they will need to be forwarded. +=================== +*/ +void CL_ForwardCommandToServer(const char *string) +{ + const char *cmd = Cmd_Argv(0); + + // ignore key up commands + if (cmd[0] == '-') + { + return; + } + + if (clc.demoplaying || clc.state < CA_CONNECTED || cmd[0] == '+') + { + Com_Printf("Unknown command \"%s" S_COLOR_WHITE "\"\n", cmd); + return; + } + + if (Cmd_Argc() > 1) + { + CL_AddReliableCommand(string, false); + } + else + { + CL_AddReliableCommand(cmd, false); + } +} + +/* +=================== +CL_RequestMotd + +=================== +*/ +static void CL_RequestMotd(void) +{ + char info[MAX_INFO_STRING]; + + if (!cl_motd->integer) + { + return; + } + Com_Printf("Resolving %s\n", MASTER_SERVER_NAME); + + switch (NET_StringToAdr(MASTER_SERVER_NAME, &cls.updateServer, NA_UNSPEC)) + { + case 0: + Com_Printf("Couldn't resolve master address\n"); + return; + + case 2: + cls.updateServer.port = BigShort(PORT_MASTER); + default: + break; + } + + Com_Printf("%s resolved to %s\n", MASTER_SERVER_NAME, NET_AdrToStringwPort(cls.updateServer)); + + info[0] = 0; + + Com_sprintf(cls.updateChallenge, sizeof(cls.updateChallenge), "%i", ((rand() << 16) ^ rand()) ^ Com_Milliseconds()); + + Info_SetValueForKey(info, "challenge", cls.updateChallenge); + + NET_OutOfBandPrint(NS_CLIENT, cls.updateServer, "getmotd%s", info); +} + +/* +====================================================================== + +CONSOLE COMMANDS + +====================================================================== +*/ + +/* +================== +CL_ShowIP_f +================== +*/ +static void CL_ShowIP_f(void) { Sys_ShowIP(); } + +/* +================== +CL_ForwardToServer_f +================== +*/ +static void CL_ForwardToServer_f(void) +{ + if (clc.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(), false); + } +} + +/* +================== +CL_Disconnect_f +================== +*/ +void CL_Disconnect_f(void) +{ + SCR_StopCinematic(); + if (clc.state != CA_DISCONNECTED && clc.state != CA_CINEMATIC) + { + Com_Error(ERR_DISCONNECT, "Disconnected from server"); + } +} + +/* +================ +CL_Reconnect_f + +================ +*/ +static void CL_Reconnect_f(void) +{ + if (!strlen(cl_reconnectArgs)) return; + Cbuf_AddText(va("connect %s\n", cl_reconnectArgs)); +} + +/* +================ +CL_Connect_f + +================ +*/ +void CL_Connect_f(void) +{ + const char *server; + int alternateProtocol; + const char *serverString; + int argc = Cmd_Argc(); + netadrtype_t family = NA_UNSPEC; + + if (argc < 2 || argc > 4) + { + Com_Printf("usage: connect [-4|-6] server [-g|-1]\n"); + return; + } + + alternateProtocol = 0; + if (argc == 2) + { + } + else if (!strcmp(Cmd_Argv(argc - 1), "-g")) + { + alternateProtocol = 1; + --argc; + } + else if (!strcmp(Cmd_Argv(argc - 1), "-1")) + { + alternateProtocol = 2; + --argc; + } + else if (argc == 4) + { + --argc; + } + + 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); + } + + // save arguments for reconnect + Q_strncpyz(cl_reconnectArgs, Cmd_Args(), sizeof(cl_reconnectArgs)); + + // 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); + + noGameRestart = true; + CL_Disconnect(true); + + Q_strncpyz(clc.servername, server, sizeof(clc.servername)); + + if (!NET_StringToAdr(clc.servername, &clc.serverAddress, family)) + { + Com_Printf("Bad server address\n"); + clc.state = CA_DISCONNECTED; + CL_ProtocolSpecificCommandsInit(); + return; + } + if (clc.serverAddress.port == 0) + { + clc.serverAddress.port = BigShort(PORT_SERVER); + } + clc.serverAddress.alternateProtocol = alternateProtocol; + + serverString = NET_AdrToStringwPort(clc.serverAddress); + + Com_Printf("%s resolved to %s\n", clc.servername, serverString); + + if (cl_guidServerUniq->integer) + CL_UpdateGUID(serverString, strlen(serverString)); + else + CL_UpdateGUID(NULL, 0); + + clc.challenge2[0] = '\0'; + clc.sendSignature = false; + + // if we aren't playing on a lan, we need to authenticate + // with the cd key + if (NET_IsLocalAddress(clc.serverAddress)) + { + clc.state = CA_CHALLENGING; + clc.sendSignature = true; + } + else + { + clc.state = CA_CONNECTING; + + // Set a client challenge number that ideally is mirrored back by the server. + clc.challenge = ((rand() << 16) ^ rand()) ^ Com_Milliseconds(); + } + + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); + clc.connectTime = -99999; // CL_CheckForResend() will fire immediately + clc.connectPacketCount = 0; +} + +#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, true, true); + } +} + +/* +===================== +CL_Rcon_f + + Send the rest of the command line over as + an unconnected command. +===================== +*/ +static void CL_Rcon_f(void) +{ + char message[MAX_RCON_MESSAGE]; + netadr_t to; + + if (!rcon_client_password->string[0]) + { + 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 (clc.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 +================= +*/ +static 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, false); +} + +/* +================= +CL_ResetPureClientAtServer +================= +*/ +static void CL_ResetPureClientAtServer(void) { CL_AddReliableCommand("vdr", false); } + +/* +================= +CL_Snd_Restart + +Restart the sound subsystem +================= +*/ +static void CL_Snd_Shutdown(void) +{ + S_Shutdown(); + cls.soundStarted = false; +} +/* +================== +CL_PK3List_f +================== +*/ +static void CL_OpenedPK3List_f(void) { Com_Printf("Opened PK3 Names: %s\n", FS_LoadedPakNames(false)); } + +/* +================== +CL_PureList_f +================== +*/ +static void CL_ReferencedPK3List_f(void) { Com_Printf("Referenced PK3 Names: %s\n", FS_ReferencedPakNames(false)); } + +/* +================== +CL_Configstrings_f +================== +*/ +static void CL_Configstrings_f(void) +{ + int i; + int ofs; + + if (clc.state != CA_ACTIVE) + { + Com_Printf("Not connected to a server.\n"); + return; + } + + for (i = 0; i < MAX_CONFIGSTRINGS; i++) + { + ofs = cl.gameState.stringOffsets[i]; + if (!ofs) + { + continue; + } + Com_Printf("%4i: %s\n", i, cl.gameState.stringData + ofs); + } +} + +/* +============== +CL_Clientinfo_f +============== +*/ +static void CL_Clientinfo_f(void) +{ + Com_Printf("--------- Client Information ---------\n"); + Com_Printf("state: %i\n", clc.state); + Com_Printf("Server: %s\n", clc.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 +================= +*/ +static void CL_DownloadsComplete(void) +{ + Com_Printf("Downloads complete\n"); + + // if we downloaded with cURL + if (clc.cURLUsed) + { + clc.cURLUsed = false; + CL_cURL_Shutdown(); + if (clc.cURLDisconnected) + { + if (clc.downloadRestart) + { + if (!clc.activeCURLNotGameRelated) FS_Restart(clc.checksumFeed); + clc.downloadRestart = false; + } + clc.cURLDisconnected = false; + if (!clc.activeCURLNotGameRelated) CL_Reconnect_f(); + return; + } + } + + // if we downloaded files we need to restart the file system + if (clc.downloadRestart) + { + clc.downloadRestart = false; + + 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", false); + + // by sending the donedl command we request a new gamestate + // so we don't want to load stuff yet + return; + } + else + { + FS_ClearPakReferences(0); + } + + // let the client game init and load data + clc.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 (clc.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 = true; + 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. +================= +*/ +static void CL_BeginDownload(const char *localName, const char *remoteName) +{ + Com_DPrintf( + "***** CL_BeginDownload *****\n" + "Localname: %s\n" + "Remotename: %s\n" + "****************************\n", + localName, remoteName); + + Q_strncpyz(clc.downloadName, localName, sizeof(clc.downloadName)); + Com_sprintf(clc.downloadTempName, sizeof(clc.downloadTempName), "%s.tmp", localName); + + // Set so UI gets access to it + Cvar_Set("cl_downloadName", remoteName); + Cvar_Set("cl_downloadSize", "0"); + Cvar_Set("cl_downloadCount", "0"); + Cvar_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(true); + + CL_AddReliableCommand(va("download %s", remoteName), false); +} + +/* +================= +CL_NextDownload + +A download completed or failed +================= +*/ +void CL_NextDownload(void) +{ + char *s; + char *remoteName, *localName; + bool useCURL = false; + int prompt; + + // A download has finished, check whether this matches a referenced checksum + if (*clc.downloadName && !clc.activeCURLNotGameRelated) + { + char *zippath = FS_BuildOSPath(Cvar_VariableString("fs_homepath"), clc.downloadName, ""); + zippath[strlen(zippath) - 1] = '\0'; + + if (!FS_CompareZipChecksum(zippath)) Com_Error(ERR_DROP, "Incorrect checksum for file: %s", clc.downloadName); + } + + *clc.downloadTempName = *clc.downloadName = 0; + Cvar_Set("cl_downloadName", ""); + + // We are looking to start a download here + if (*clc.downloadList) + { + // Prompt if we do not allow automatic downloads + prompt = com_downloadPrompt->integer; + if (!(prompt & DLP_TYPE_MASK) && !(cl_allowDownload->integer & DLF_ENABLE)) + { + char files[MAX_INFO_STRING] = ""; + char *name, *head; + const char *pure_msg; + const char *url_msg = ""; + int i = 0, others = 0, swap = 0, max_list = 12; + + // Set the download URL message + if ((clc.sv_allowDownload & DLF_ENABLE) && !(clc.sv_allowDownload & DLF_NO_REDIRECT)) + { + url_msg = va("The server redirects to the following URL:\n%s", clc.sv_dlURL); + max_list -= 6; + } + + // Make a pretty version of the download list + name = clc.downloadList; + if (*name == '@') name++; + + do + { + // Copy remote name + head = name; + while (*head && *head != '@') head++; + + swap = *head; + *head = 0; + + if (i++ < max_list) + { + if (i > 1) Q_strcat(files, sizeof(files), ", "); + Q_strcat(files, sizeof(files), name); + } + else + { + others++; + } + + *head = swap; + if (!swap) break; + + // Skip local name + head++; + while (*head && *head != '@') head++; + + name = head + 1; + } while (*head); + + if (others) + { + Q_strcat(files, sizeof(files), va("(%d other file%s)\n", others, others > 1 ? "s" : "")); + } + + // Set the pure message + if (cl_connectedToPureServer) + { + if (!(clc.sv_allowDownload & DLF_ENABLE) || + ((clc.sv_allowDownload & DLF_NO_UDP) && (clc.sv_allowDownload & DLF_NO_REDIRECT))) + { + pure_msg = + "You are missing files required by the server. " + "The server does not allow downloading. " + "You must install these files manually:"; + } + else + { + pure_msg = + "You are missing files required by the server. " + "You must download these files or disconnect:"; + } + } + else + { + pure_msg = + "You are missing optional files provided by the " + "server. You may not need them to play but can " + "choose to download them anyway:"; + } + + Cvar_Set("com_downloadPromptText", va("%s\n\n%s\n%s", pure_msg, files, url_msg)); + Cvar_Set("com_downloadPrompt", va("%d", DLP_SHOW)); + return; + } + + if (!(prompt & DLP_PROMPTED)) Cvar_Set("com_downloadPrompt", va("%d", prompt | DLP_PROMPTED)); + + prompt &= DLP_TYPE_MASK; + + 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 + + if (((cl_allowDownload->integer & DLF_ENABLE) && !(cl_allowDownload->integer & DLF_NO_REDIRECT)) || + prompt == DLP_CURL) + { + Com_Printf("Trying CURL download: %s; %s\n", localName, remoteName); + 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 = true; + } + } + 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); + } + if (!useCURL) + { + Com_Printf("Trying UDP download: %s; %s\n", localName, remoteName); + + if ((!(cl_allowDownload->integer & DLF_ENABLE) || (cl_allowDownload->integer & DLF_NO_UDP)) && + prompt != DLP_UDP) + { + if (cl_connectedToPureServer) + { + Com_Error(ERR_DROP, + "Automatic downloads are " + "disabled on your client (cl_allowDownload is %d). " + "You can enable automatic downloads in the Options " + "menu.", + cl_allowDownload->integer); + return; + } + + Com_Printf("WARNING: UDP downloads are disabled.\n"); + CL_DownloadsComplete(); + return; + } + else + { + CL_BeginDownload(localName, remoteName); + } + } + clc.downloadRestart = true; + + // move over the rest + memmove(clc.downloadList, s, strlen(s) + 1); + + return; + } + + CL_DownloadsComplete(); +} + +/* +================= +CL_InitDownloads + +After receiving a valid game state, we valid the cgame and local zip files here +and determine if we need to download them +================= +*/ +void CL_InitDownloads(void) +{ + if (FS_ComparePaks(clc.downloadList, sizeof(clc.downloadList), true)) + { + Com_Printf("Need paks: %s\n", clc.downloadList); + + Cvar_Set("com_downloadPrompt", "0"); + if (*clc.downloadList) + { + // if autodownloading is not enabled on the server + clc.state = CA_CONNECTED; + *clc.downloadTempName = *clc.downloadName = 0; + Cvar_Set("cl_downloadName", ""); + CL_NextDownload(); + return; + } + } + CL_DownloadsComplete(); +} + +/* +=============== +CL_UnloadRSAKeypair +=============== +*/ +static void CL_UnloadRSAKeypair(void) +{ + rsa_public_key_clear(&cls.rsa.public_key); + rsa_private_key_clear(&cls.rsa.private_key); +} + +/* +=============== +CL_WriteRSAPublicKey +=============== +*/ +static bool CL_WriteRSAPublicKey(void) +{ + struct nettle_buffer key_buffer; + fileHandle_t f; + + f = FS_SV_FOpenFileWrite(RSA_PUBLIC_KEY_FILE); + if (!f) return false; + + nettle_buffer_init(&key_buffer); + if (!rsa_keypair_to_sexp(&key_buffer, NULL, &cls.rsa.public_key, NULL)) + { + FS_FCloseFile(f); + nettle_buffer_clear(&key_buffer); + return false; + } + + FS_Write(key_buffer.contents, key_buffer.size, f); + FS_FCloseFile(f); + + nettle_buffer_clear(&key_buffer); + return true; +} + +/* +=============== +CL_WriteRSAPrivateKey +=============== +*/ +static bool CL_WriteRSAPrivateKey(void) +{ + struct nettle_buffer key_buffer; + fileHandle_t f; + +#ifndef _WIN32 + int old_umask = umask(0377); +#endif + f = FS_SV_FOpenFileWrite(RSA_PRIVATE_KEY_FILE); +#ifndef _WIN32 + umask(old_umask); +#endif + if (!f) return false; + + nettle_buffer_init(&key_buffer); + if (!rsa_keypair_to_sexp(&key_buffer, NULL, &cls.rsa.public_key, &cls.rsa.private_key)) + { + FS_FCloseFile(f); + nettle_buffer_clear(&key_buffer); + return false; + } + + FS_Write(key_buffer.contents, key_buffer.size, f); + FS_FCloseFile(f); + + nettle_buffer_clear(&key_buffer); + return true; +} + +/* +=============== +CL_GenerateRSAKeypair + +public_key and private_key must already be inititalized before calling this +function. This is done by CL_LoadRSAKeypair. +=============== +*/ +static void CL_GenerateRSAKeypair(void) +{ + mpz_set_ui(cls.rsa.public_key.e, RSA_PUBLIC_EXPONENT); + + int success = rsa_generate_keypair( + &cls.rsa.public_key, &cls.rsa.private_key, NULL, qnettle_random, NULL, NULL, RSA_KEY_LENGTH, 0); + if (success) + if (CL_WriteRSAPrivateKey()) + if (CL_WriteRSAPublicKey()) + { + Com_Printf("RSA keypair generated\n"); + return; + } + + // failure + CL_UnloadRSAKeypair(); + Com_Printf("Error generating RSA keypair, setting cl_rsaAuth to 0\n"); + Cvar_Set("cl_rsaAuth", "0"); +} + +/* +=============== +CL_LoadRSAKeypair + +Attempt to load RSA keys from RSA_PRIVATE_KEY_FILE +If this fails, generate a new keypair +=============== +*/ +static void CL_LoadRSAKeypair(void) +{ + int len; + fileHandle_t f; + uint8_t *buf; + + rsa_public_key_init(&cls.rsa.public_key); + rsa_private_key_init(&cls.rsa.private_key); + + Com_DPrintf("Loading RSA private key from %s\n", RSA_PRIVATE_KEY_FILE); + + len = FS_SV_FOpenFileRead(RSA_PRIVATE_KEY_FILE, &f); + if (!f) + { + Com_DPrintf("RSA private key not found, generating\n"); + CL_GenerateRSAKeypair(); + return; + } + + if (len < 1) + { + Com_DPrintf("RSA private key empty, generating\n"); + FS_FCloseFile(f); + CL_GenerateRSAKeypair(); + return; + } + + buf = (uint8_t *)Z_Malloc(len); + FS_Read(buf, len, f); + FS_FCloseFile(f); + + if (!rsa_keypair_from_sexp(&cls.rsa.public_key, &cls.rsa.private_key, 0, len, buf)) + { + memset(buf, 0, len); + Z_Free(buf); + CL_UnloadRSAKeypair(); + Com_Error(ERR_FATAL, "Invalid RSA private key found."); + return; + } + + memset(buf, 0, len); + Z_Free(buf); + + len = FS_SV_FOpenFileRead(RSA_PUBLIC_KEY_FILE, &f); + if (!f || len < 1) CL_WriteRSAPublicKey(); + if (f) FS_FCloseFile(f); + + Com_DPrintf("RSA private key loaded\n"); +} + +/* +================= +CL_CheckForResend + +Resend a connect message if the last one has timed out +================= +*/ +static void CL_CheckForResend(void) +{ + int port; + char info[MAX_INFO_STRING]; + char data[MAX_MSGLEN]; + + // don't send anything if playing back a demo + if (clc.demoplaying) + { + return; + } + + // resend if we haven't gotten a reply yet + if (clc.state != CA_CONNECTING && clc.state != CA_CHALLENGING) + { + return; + } + + if (cls.realtime - clc.connectTime < RETRANSMIT_TIMEOUT) + { + return; + } + + clc.connectTime = cls.realtime; // for retransmit requests + clc.connectPacketCount++; + + switch (clc.state) + { + case CA_CONNECTING: + // requesting a challenge + + // The challenge request shall be followed by a client challenge so no malicious server can hijack this + // connection. + // Add the gamename so the server knows we're running the correct game or can reject the client + // with a meaningful message + if (clc.serverAddress.alternateProtocol == 2) + { + Com_sprintf(data, sizeof(data), "getchallenge"); + } + else if (clc.serverAddress.alternateProtocol == 1) + { + Com_sprintf(data, sizeof(data), "getchallenge %d", clc.challenge); + } + else + Com_sprintf(data, sizeof(data), "getchallenge %d %s", clc.challenge, com_gamename->string); + + NET_OutOfBandPrint(NS_CLIENT, clc.serverAddress, "%s", data); + 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", + (clc.serverAddress.alternateProtocol == 0 ? PROTOCOL_VERSION + : clc.serverAddress.alternateProtocol == 1 ? 70 : 69))); + Info_SetValueForKey(info, "qport", va("%i", port)); + Info_SetValueForKey(info, "challenge", va("%i", clc.challenge)); + + if (cl_rsaAuth->integer && clc.sendSignature) + { + char public_key[RSA_STRING_LENGTH]; + char signature[RSA_STRING_LENGTH]; + struct sha256_ctx sha256_hash; + mpz_t n; + + Info_SetValueForKey(info, "challenge2", clc.challenge2); + + sha256_init(&sha256_hash); + sha256_update(&sha256_hash, strlen(info), (uint8_t *)info); + + mpz_init(n); + rsa_sha256_sign(&cls.rsa.private_key, &sha256_hash, n); + mpz_get_str(signature, 16, n); + mpz_clear(n); + + mpz_get_str(public_key, 16, cls.rsa.public_key.n); + + Com_sprintf(data, sizeof(data), "connect \"%s\" %s %s", info, public_key, signature); + } + else + { + Com_sprintf(data, sizeof(data), "connect \"%s\"", info); + } + + NET_OutOfBandData(NS_CLIENT, clc.serverAddress, (byte *)data, strlen(data)); + // the most current userinfo has been sent, so watch for any + // newer changes to userinfo variables + cvar_modifiedFlags &= ~CVAR_USERINFO; + break; + + default: + Com_Error(ERR_FATAL, "CL_CheckForResend: bad clc.state"); + } +} + +/* +===================== +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) + { + clc.state = CA_DISCONNECTED; + Key_SetCatcher(KEYCATCH_CONSOLE); + CL_ProtocolSpecificCommandsInit(); + return; + } + + if (!com_cl_running->integer) + { + return; + } + + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); + + // if we are already connected to the local host, stay connected + if (clc.state >= CA_CONNECTED && !Q_stricmp(clc.servername, "localhost")) + { + clc.state = CA_CONNECTED; // so the connect screen is drawn + ::memset(cls.updateInfoString, 0, sizeof(cls.updateInfoString)); + ::memset(clc.serverMessage, 0, sizeof(clc.serverMessage)); + ::memset(&cl.gameState, 0, sizeof(cl.gameState)); + clc.lastPacketSentTime = -9999; + SCR_UpdateScreen(); + } + else + { + CL_Disconnect(true); + Q_strncpyz(clc.servername, "localhost", sizeof(clc.servername)); + clc.state = CA_CHALLENGING; // so the connect screen is drawn + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); + SCR_UpdateScreen(); + clc.connectTime = -RETRANSMIT_TIMEOUT; + NET_StringToAdr(clc.servername, &clc.serverAddress, NA_UNSPEC); + // we don't need a challenge on the localhost + + CL_CheckForResend(); + } +} + +/* +=================== +CL_MotdPacket + +=================== +*/ +static void CL_MotdPacket(netadr_t from, const char *info) +{ + const char *v; + + // if not from our server, ignore it + if (!NET_CompareAdr(from, cls.updateServer)) + { + Com_DPrintf("MOTD packet from unexpected source\n"); + return; + } + + Com_DPrintf("MOTD packet: %s\n", info); + while (*info != '\\') info++; + + // check challenge + v = Info_ValueForKey(info, "challenge"); + if (strcmp(v, cls.updateChallenge)) + { + Com_DPrintf( + "MOTD packet mismatched challenge: " + "'%s' != '%s'\n", + v, cls.updateChallenge); + return; + } + + v = Info_ValueForKey(info, "motd"); + + Q_strncpyz(cls.updateInfoString, info, sizeof(cls.updateInfoString)); + Cvar_Set("cl_motdString", v); +} + +/* +=================== +CL_InitServerInfo +=================== +*/ +static void CL_InitServerInfo(serverInfo_t *server, netadr_t *address) +{ + server->adr = *address; + server->clients = 0; + server->hostName[0] = '\0'; + server->mapName[0] = '\0'; + server->label[0] = '\0'; + server->maxClients = 0; + server->maxPing = 0; + server->minPing = 0; + server->ping = -1; + server->game[0] = '\0'; + server->gameType = 0; + server->netType = 0; +} + +/* +=================== +CL_GSRSequenceInformation + +Parses this packet's index and the number of packets from a master server's +response. Updates the packet count and returns the index. Advances the data +pointer as appropriate (but only when parsing was successful) + +The sequencing information isn't terribly useful at present (we can skip +duplicate packets, but we don't bother to make sure we've got all of them). +=================== +*/ +static int CL_GSRSequenceInformation(int alternateProtocol, byte **data) +{ + char *p = (char *)*data, *e; + int ind, num; + // '\0'-delimited fields: this packet's index, total number of packets + if (*p++ != '\0') return -1; + + ind = strtol(p, (char **)&e, 10); + if (*e++ != '\0') return -1; + + num = strtol(e, (char **)&p, 10); + if (*p++ != '\0') return -1; + + if (num <= 0 || ind <= 0 || ind > num) return -1; // nonsensical response + + if (cls.numAlternateMasterPackets[alternateProtocol] > 0 && num != cls.numAlternateMasterPackets[alternateProtocol]) + { + // Assume we sent two getservers and somehow they changed in + // between - only use the results that arrive later + Com_DPrintf("Master changed its mind about%s packet count!\n", + (alternateProtocol == 0 ? "" : alternateProtocol == 1 ? " alternate-1" : " alternate-2")); + cls.receivedAlternateMasterPackets[alternateProtocol] = 0; + // cls.numglobalservers = 0; + // cls.numGlobalServerAddresses = 0; + } + cls.numAlternateMasterPackets[alternateProtocol] = num; + + // successfully parsed + *data = (byte *)p; + return ind; +} + +/* +=================== +CL_GSRFeaturedLabel + +Parses from the data an arbitrary text string labelling the servers in the +following getserversresponse packet. +The result is copied to *buf, and *data is advanced as appropriate +=================== +*/ +static void CL_GSRFeaturedLabel(byte **data, char *buf, int size) +{ + char *l = buf; + + // copy until '\0' which indicates field break + // or slash which indicates beginning of server list + while (**data && **data != '\\' && **data != '/') + { + if (l < &buf[size - 1]) + *l = **data; + else if (l == &buf[size - 1]) + Com_DPrintf(S_COLOR_YELLOW + "Warning: " + "CL_GSRFeaturedLabel: overflow\n"); + l++, (*data)++; + } + + if (l < &buf[size - 1]) + *l = '\0'; + else + buf[size - 1] = '\0'; +} + +#define MAX_SERVERSPERPACKET 256 + +/* +=================== +CL_ServersResponsePacket +=================== +*/ +static void CL_ServersResponsePacket(const netadr_t *from, msg_t *msg, bool extended) +{ + int i, count, total; + netadr_t addresses[MAX_SERVERSPERPACKET]; + int numservers; + byte *buffptr; + byte *buffend; + char label[MAX_FEATLABEL_CHARS] = ""; + + Com_DPrintf("CL_ServersResponsePacket from %s %s\n", + NET_AdrToStringwPort(*from), + extended ? " (extended)" : ""); + + if (cls.numglobalservers == -1) + { + // state to detect lack of servers or lack of response + cls.numglobalservers = 0; + cls.numGlobalServerAddresses = 0; + for (i = 0; i < 3; ++i) + { + cls.numAlternateMasterPackets[i] = 0; + cls.receivedAlternateMasterPackets[i] = 0; + } + } + + // parse through server response string + numservers = 0; + buffptr = msg->data; + buffend = buffptr + msg->cursize; + + // skip header + buffptr += 4; + + // advance to initial token + // I considered using strchr for this but I don't feel like relying + // on its behaviour with '\0' + while (*buffptr && *buffptr != '\\' && *buffptr != '/') + { + buffptr++; + + if (buffptr >= buffend) break; + } + + if (*buffptr == '\0') + { + int ind = CL_GSRSequenceInformation(from->alternateProtocol, &buffptr); + if (ind >= 0) + { + // this denotes the start of new-syntax stuff + // have we already received this packet? + if (cls.receivedAlternateMasterPackets[from->alternateProtocol] & (1 << (ind - 1))) + { + Com_DPrintf( + "CL_ServersResponsePacket: " + "received packet %d again, ignoring\n", + ind); + return; + } + // TODO: detect dropped packets and make another + // request + Com_DPrintf( + "CL_ServersResponsePacket:%s packet " + "%d of %d\n", + (from->alternateProtocol == 0 ? "" : from->alternateProtocol == 1 ? " alternate-1" : " alternate-2"), + ind, cls.numAlternateMasterPackets[from->alternateProtocol]); + cls.receivedAlternateMasterPackets[from->alternateProtocol] |= (1 << (ind - 1)); + + CL_GSRFeaturedLabel(&buffptr, label, sizeof(label)); + } + // now skip to the server list + for (; buffptr < buffend && *buffptr != '\\' && *buffptr != '/'; buffptr++) + ; + } + + 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; + + addresses[numservers].alternateProtocol = from->alternateProtocol; + + 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]); + Q_strncpyz(server->label, label, sizeof(server->label)); + // 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_CheckTimeout + +================== +*/ +static void CL_CheckTimeout(void) +{ + // + // check timeout + // + if ((!CL_CheckPaused() || !sv_paused->integer) && clc.state >= CA_CONNECTED && clc.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(true); + return; + } + } + else + { + cl.timeoutcount = 0; + } +} + +/* +================== +CL_CheckPaused +Check whether client has been paused. +================== +*/ +bool 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 true; + + return false; +} + +//============================================================================ + +/* +================== +CL_CheckUserinfo + +================== +*/ +static void CL_CheckUserinfo(void) +{ + // don't add reliable commands when not yet connected + if (clc.state < CA_CONNECTED) return; + + // don't overflow the reliable command buffer when paused + if (CL_CheckPaused()) return; + + // send a reliable userinfo update if needed + if (cvar_modifiedFlags & CVAR_USERINFO) + { + cvar_modifiedFlags &= ~CVAR_USERINFO; + CL_AddReliableCommand(va("userinfo \"%s\"", Cvar_InfoString(CVAR_USERINFO)), false); + } +} + +/* +================== +CL_Frame + +================== +*/ +void CL_Frame(int msec) +{ + if (!com_cl_running->integer) return; + + // We may have a download prompt ready + if ((com_downloadPrompt->integer & DLP_TYPE_MASK) && !(com_downloadPrompt->integer & DLP_PROMPTED)) + { + Com_Printf("Download prompt returned %d\n", com_downloadPrompt->integer); + CL_NextDownload(); + } + else if (com_downloadPrompt->integer & DLP_SHOW) + { + // If the UI VM does not support the download prompt, we need to catch + // the prompt here and replicate regular behavior. + // One frame will always run between requesting and showing the prompt. + + if (com_downloadPrompt->integer & DLP_STALE) + { + Com_Printf("WARNING: UI VM does not support download prompt\n"); + Cvar_Set("com_downloadPrompt", va("%d", DLP_IGNORE)); + CL_NextDownload(); + } + else + { + Cvar_Set("com_downloadPrompt", va("%d", com_downloadPrompt->integer | DLP_STALE)); + } + } + + if (clc.downloadCURLM) + { + CL_cURL_PerformDownload(); + // we can't process frames normally when in disconnected download mode + // since the ui vm expects clc.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; + } + } + + if (clc.state == CA_DISCONNECTED && !(Key_GetCatcher() & KEYCATCH_UI) && !com_sv_running->integer && cls.ui) + { + // if disconnected, bring up the menu + S_StopAllSounds(); + VM_Call(cls.ui, UI_SET_ACTIVE_MENU - (cls.uiInterface == 2 ? 2 : 0), UIMENU_MAIN); + } + + // if recording an avi, lock to a fixed fps + if (CL_VideoRecording() && cl_aviFrameRate->integer && msec) + { + // save the current screen + if (clc.state == CA_ACTIVE || cl_forceavidemo->integer) + { + float fps = MIN(cl_aviFrameRate->value * com_timescale->value, 1000.0f); + float frameDuration = MAX(1000.0f / fps, 1.0f) + clc.aviVideoFrameRemainder; + + CL_TakeVideoFrame(); + + msec = (int)frameDuration; + clc.aviVideoFrameRemainder = frameDuration - msec; + } + } + + if (cl_autoRecordDemo->integer) + { + if (clc.state == CA_ACTIVE && !clc.demorecording && !clc.demoplaying) + { + // If not recording a demo, and we should be, start one + qtime_t now; + Com_RealTime(&now); + + const char *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); + + char serverName[MAX_OSPATH]; + Q_strncpyz(serverName, clc.servername, MAX_OSPATH); + + // Replace the ":" in the address as it is not a valid + // file name character + char *p = strstr(serverName, ":"); + if (p) *p = '.'; + + char mapName[MAX_QPATH]; + 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\"\n", nowString, serverName, mapName)); + } + else if (clc.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); + } + + // 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 +================ +*/ +static __attribute__((format(printf, 2, 3))) 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); + + switch (print_level) + { + case PRINT_ALL: + Com_Printf("%s", msg); + break; + + case PRINT_WARNING: + Com_Printf(S_COLOR_YELLOW "%s", msg); + break; + + case PRINT_DEVELOPER: + Com_DPrintf(S_COLOR_RED "%s", msg); + break; + } +} + +/* +============ +CL_ShutdownRef +============ +*/ +static void CL_ShutdownRef(void) +{ + if (re.Shutdown) re.Shutdown(true); + + ::memset(&re, 0, sizeof(re)); + +#ifdef USE_RENDERER_DLOPEN + if (rendererLib) + { + Sys_UnloadLibrary(rendererLib); + rendererLib = NULL; + } +#endif +} + +/* +===================== +CL_ShutdownAll +===================== +*/ +void CL_ShutdownAll(bool shutdownRef) +{ + if (CL_VideoRecording()) CL_CloseAVI(); + + if (clc.demorecording) CL_StopRecord_f(); + + CL_cURL_Shutdown(); + + // clear sounds + S_DisableSounds(); + // shutdown CGame + CL_ShutdownCGame(); + // shutdown UI + CL_ShutdownUI(); + + // shutdown the renderer + if (shutdownRef) + CL_ShutdownRef(); + else if (re.Shutdown) + re.Shutdown(false); // don't destroy window or context + + cls.uiStarted = false; + cls.cgameStarted = false; + cls.rendererStarted = false; + cls.soundRegistered = false; +} + +/* +================= +CL_ClearMemory + +Called by Com_GameRestart +================= +*/ +static void CL_ClearMemory(bool shutdownRef) +{ + // shutdown all the client stuff + CL_ShutdownAll(shutdownRef); + + // if not running a server clear the whole hunk + if (!com_sv_running->integer) + { + // clear the whole hunk + Hunk_Clear(); + // clear collision map data + CM_ClearMap(); + } + else + { + // clear all the client data on the hunk + Hunk_ClearToMark(); + } +} + +/* +================= +CL_FlushMemory + +Called by CL_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) +{ + CL_ClearMemory(false); + CL_StartHunkUsers(false); +} + +/* +================= +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 +================= +*/ +static 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(); + + if (!FS_ConditionalRestart(clc.checksumFeed, true)) + { + // if not running a server clear the whole hunk + if (com_sv_running->integer) + { + // clear all the client data on the hunk + Hunk_ClearToMark(); + } + else + { + // clear the whole hunk + Hunk_Clear(); + } + + // 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 + + cls.rendererStarted = false; + cls.uiStarted = false; + cls.cgameStarted = false; + cls.soundRegistered = false; + + // unpause so the cgame definately gets a snapshot and renders a frame + Cvar_Set("cl_paused", "0"); + + // initialize the renderer interface + CL_InitRef(); + + // startup all the client stuff + CL_StartHunkUsers(false); + + // start the cgame if connected + if (clc.state > CA_CONNECTED && clc.state != CA_CINEMATIC) + { + cls.cgameStarted = true; + 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 +================= +*/ +static void CL_Snd_Restart_f(void) +{ + CL_Snd_Shutdown(); + // sound will be reinitialized by vid_restart + CL_Vid_Restart_f(); +} + + +/* +============ +CL_InitRenderer +============ +*/ +static 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(bool rendererOnly) +{ + if (!com_cl_running) + { + return; + } + + if (!com_cl_running->integer) + { + return; + } + + if (!cls.rendererStarted) + { + cls.rendererStarted = true; + CL_InitRenderer(); + } + + if (rendererOnly) + { + return; + } + + if (!cls.soundStarted) + { + cls.soundStarted = true; + S_Init(); + } + + if (!cls.soundRegistered) + { + cls.soundRegistered = true; + S_BeginRegistration(); + } + + if (com_dedicated->integer) + { + return; + } + + if (!cls.uiStarted) + { + cls.uiStarted = true; + CL_InitUI(); + } +} + +/* +============ +CL_RefMalloc +============ +*/ +static void *CL_RefMalloc(int size) { return Z_TagMalloc(size, TAG_RENDERER); } + +/* +============ +CL_ScaledMilliseconds +============ +*/ +int CL_ScaledMilliseconds(void) { return Sys_Milliseconds() * com_timescale->value; } + +//=========================================================================================== + +static void CL_SetModel_f(void) +{ + char name[256]; + + const char *arg = Cmd_Argv(1); + if (arg[0]) + { + Cvar_Set("model", arg); + Cvar_Set("headmodel", arg); + } + else + { + Cvar_VariableStringBuffer("model", name, sizeof(name)); + Com_Printf("model is set to %s\n", name); + } +} + +//=========================================================================================== + +/* +=============== +CL_Video_f + +video +video [filename] +=============== +*/ +static void CL_Video_f(void) +{ + char filename[MAX_OSPATH]; + + 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 + { + int i, last; + + // 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 +=============== +*/ +static 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"); + } +} + +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")); + 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")); + const char *game = Info_ValueForKey(info, "game"); + Q_strncpyz(server->game, (game[0]) ? game : BASEGAME, MAX_NAME_LENGTH); + } + 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 +=================== +*/ +static void CL_ServerInfoPacket(netadr_t from, msg_t *msg) +{ + int i, type; + char info[MAX_INFO_STRING]; + char *infoString; + int prot; + char *gamename; + bool gameMismatch; + + infoString = MSG_ReadString(msg); + + if (from.alternateProtocol == 0) + { + // if this isn't the correct gamename, ignore it + gamename = Info_ValueForKey(infoString, "gamename"); + + gameMismatch = !*gamename || strcmp(gamename, com_gamename->string) != 0; + + if (gameMismatch) + { + Com_DPrintf("Game mismatch in info packet: %s\n", infoString); + return; + } + } + + // if this isn't the correct protocol version, ignore it + prot = atoi(Info_ValueForKey(infoString, "protocol")); + if (prot != (from.alternateProtocol == 0 ? PROTOCOL_VERSION : from.alternateProtocol == 1 ? 70 : 69)) + { + Com_DPrintf("Different protocol info packet: %s\n", infoString); + return; + } + + // iterate servers waiting for ping response + for (i = 0; i < MAX_PINGREQUESTS; i++) + { + if (cl_pinglist[i].adr.port && !cl_pinglist[i].time && NET_CompareAdr(from, cl_pinglist[i].adr)) + { + // calc ping time + cl_pinglist[i].time = Sys_Milliseconds() - cl_pinglist[i].start; + Com_DPrintf("ping time %dms from %s\n", cl_pinglist[i].time, NET_AdrToString(from)); + + // save of info + Q_strncpyz(cl_pinglist[i].info, infoString, sizeof(cl_pinglist[i].info)); + + // tack on the net type + // NOTE: make sure these types are in sync with the netnames strings in the UI + switch (from.type) + { + case NA_BROADCAST: + case NA_IP: + type = 1; + break; + case NA_IP6: + type = 2; + break; + default: + type = 0; + break; + } + Info_SetValueForKey(cl_pinglist[i].info, "nettype", va("%d", type)); + CL_SetServerInfoByAddress(from, infoString, cl_pinglist[i].time); + + return; + } + } + + // if not just sent a local broadcast or pinging local servers + if (cls.pingUpdateSource != AS_LOCAL) + { + return; + } + + for (i = 0; i < MAX_OTHER_SERVERS; i++) + { + // empty slot + if (cls.localServers[i].adr.port == 0) + { + break; + } + + // avoid duplicate + if (NET_CompareAdr(from, cls.localServers[i].adr)) + { + return; + } + } + + if (i == MAX_OTHER_SERVERS) + { + Com_DPrintf("MAX_OTHER_SERVERS hit, dropping infoResponse\n"); + return; + } + + // add this to the list + cls.numlocalservers = i + 1; + CL_InitServerInfo(&cls.localServers[i], &from); + + Q_strncpyz(info, MSG_ReadString(msg), MAX_INFO_STRING); + if (strlen(info)) + { + if (info[strlen(info) - 1] != '\n') + { + Q_strcat(info, sizeof(info), "\n"); + } + Com_Printf("%s: %s", NET_AdrToStringwPort(from), info); + } +} + +/* +=================== +CL_ServerStatusResponse +=================== +*/ +static void CL_ServerStatusResponse(netadr_t from, msg_t *msg) +{ + 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; + } + + const char *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 = false; + if (serverStatus->print) + { + serverStatus->retrieved = true; + } +} + +/* +================= +CL_ConnectionlessPacket + +Responses to broadcasts, etc +================= +*/ +static void CL_ConnectionlessPacket(netadr_t from, msg_t *msg) +{ + int challenge = 0; + + MSG_BeginReadingOOB(msg); + MSG_ReadLong(msg); // skip the -1 + + const char *s = MSG_ReadStringLine(msg); + + Cmd_TokenizeString(s); + + const char *c = Cmd_Argv(0); + + Com_DPrintf("CL packet %s: %s\n", NET_AdrToStringwPort(from), c); + + // challenge from the server we are connecting to + if (!Q_stricmp(c, "challengeResponse")) + { + int ver; + + if (clc.state != CA_CONNECTING) + { + Com_DPrintf("Unwanted challenge response received. Ignored.\n"); + return; + } + + const char *strver = Cmd_Argv(3); + if (*strver) + { + ver = atoi(strver); + + if (ver != PROTOCOL_VERSION) + { + Com_Printf(S_COLOR_YELLOW + "Warning: Server reports protocol version %d, we have %d. " + "Trying anyways.\n", + ver, PROTOCOL_VERSION); + } + } + if (clc.serverAddress.alternateProtocol == 0) + { + c = Cmd_Argv(2); + if (*c) challenge = atoi(c); + + if (!*c || challenge != clc.challenge) + { + Com_Printf("Bad challenge for challengeResponse. Ignored.\n"); + return; + } + } + + // start sending challenge response instead of challenge request packets + clc.challenge = atoi(Cmd_Argv(1)); + clc.state = CA_CHALLENGING; + clc.connectPacketCount = 0; + clc.connectTime = -99999; + + if (cl_rsaAuth->integer) + { + s = Cmd_Argv(4); + if (*s) + { + Q_strncpyz(clc.challenge2, s, sizeof(clc.challenge2)); + clc.sendSignature = true; + } + } + + // 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 (clc.state >= CA_CONNECTED) + { + Com_Printf("Dup connect received. Ignored.\n"); + return; + } + if (clc.state != CA_CHALLENGING) + { + Com_Printf("connectResponse packet while not connecting. Ignored.\n"); + return; + } + if (!NET_CompareAdr(from, clc.serverAddress)) + { + Com_Printf("connectResponse from wrong address. Ignored.\n"); + return; + } + + if (clc.serverAddress.alternateProtocol == 0) + { + c = Cmd_Argv(1); + + if (*c) + challenge = atoi(c); + else + { + Com_Printf("Bad connectResponse received. Ignored.\n"); + return; + } + + if (challenge != clc.challenge) + { + Com_Printf("ConnectResponse with bad challenge received. Ignored.\n"); + return; + } + } + + Netchan_Setup(clc.serverAddress.alternateProtocol, NS_CLIENT, &clc.netchan, from, + Cvar_VariableValue("net_qport"), clc.challenge); + + clc.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; + } + + // echo request from server + if (!Q_stricmp(c, "echo")) + { + NET_OutOfBandPrint(NS_CLIENT, from, "%s", Cmd_Argv(1)); + return; + } + + // global MOTD from trem master + if (!Q_stricmp(c, "motd")) + { + CL_MotdPacket(from, s); + 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, false); + + return; + } + + // list of servers sent back by a master server (extended) + if (!Q_strncmp(c, "getserversExtResponse", 21)) + { + CL_ServersResponsePacket(&from, msg, true); + 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 (clc.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_GetServerStatus +=================== +*/ +static serverStatus_t *CL_GetServerStatus(netadr_t from) +{ + int i, oldest, oldestTime; + + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + if (NET_CompareAdr(from, cl_serverStatusList[i].address)) + { + return &cl_serverStatusList[i]; + } + } + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + if (cl_serverStatusList[i].retrieved) + { + return &cl_serverStatusList[i]; + } + } + oldest = -1; + oldestTime = 0; + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + if (oldest == -1 || cl_serverStatusList[i].startTime < oldestTime) + { + oldest = i; + oldestTime = cl_serverStatusList[i].startTime; + } + } + return &cl_serverStatusList[oldest]; +} + +/* +=================== +CL_ServerStatus +=================== +*/ +bool CL_ServerStatus(char *serverAddress, char *serverStatusString, int maxLen) +{ + int i; + netadr_t to; + serverStatus_t *serverStatus; + + // if no server address then reset all server status requests + if (!serverAddress) + { + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + cl_serverStatusList[i].address.port = 0; + cl_serverStatusList[i].retrieved = true; + } + return false; + } + // get the address + if (!NET_StringToAdr(serverAddress, &to, NA_UNSPEC)) + { + return false; + } + serverStatus = CL_GetServerStatus(to); + // if no server status string then reset the server status request for this address + if (!serverStatusString) + { + serverStatus->retrieved = true; + return false; + } + + // if this server status request has the same address + if (NET_CompareAdr(to, serverStatus->address)) + { + // if we received a response for this server status request + if (!serverStatus->pending) + { + Q_strncpyz(serverStatusString, serverStatus->string, maxLen); + serverStatus->retrieved = true; + serverStatus->startTime = 0; + return true; + } + // resend the request regularly + else if (serverStatus->startTime < Com_Milliseconds() - cl_serverStatusResendTime->integer) + { + serverStatus->print = false; + serverStatus->pending = true; + serverStatus->retrieved = false; + serverStatus->time = 0; + serverStatus->startTime = Com_Milliseconds(); + NET_OutOfBandPrint(NS_CLIENT, to, "getstatus"); + return false; + } + } + // if retrieved + else if (serverStatus->retrieved) + { + serverStatus->address = to; + serverStatus->print = false; + serverStatus->pending = true; + serverStatus->retrieved = false; + serverStatus->startTime = Com_Milliseconds(); + serverStatus->time = 0; + NET_OutOfBandPrint(NS_CLIENT, to, "getstatus"); + return false; + } + return false; +} + +/* +================== +CL_LocalServers_f +================== +*/ +static void CL_LocalServers_f(void) +{ + const 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++) + { + bool b = cls.localServers[i].visible; + ::memset(&cls.localServers[i], 0, sizeof(cls.localServers[i])); + cls.localServers[i].visible = b; + } + ::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 +================== +*/ +static void CL_GlobalServers_f(void) +{ + int netAlternateProtocols, a; + int i; + char command[1024]; + const char *masteraddress; + + int masterNum; + int count = Cmd_Argc(); + if ( count < 2 || (masterNum = atoi(Cmd_Argv(1))) < 0 || masterNum > MAX_MASTER_SERVERS ) + { + Com_Printf("usage: globalservers [keywords]\n", MAX_MASTER_SERVERS); + return; + } + + netAlternateProtocols = Cvar_VariableIntegerValue("net_alternateProtocols"); + + for (a = 0; a < 3; ++a) + { + // indent + if (a == 0 && (netAlternateProtocols & NET_DISABLEPRIMPROTO)) continue; + if (a == 1 && !(netAlternateProtocols & NET_ENABLEALT1PROTO)) continue; + if (a == 2 && !(netAlternateProtocols & NET_ENABLEALT2PROTO)) continue; + + // request from all master servers + if ( masterNum == 0 ) + { + int numAddress = 0; + + for ( int i = 1; i <= MAX_MASTER_SERVERS; i++ ) + { + sprintf(command, "sv_master%d", i); + masteraddress = Cvar_VariableString(command); + + if(!*masteraddress) + continue; + + numAddress++; + + Com_sprintf(command, sizeof(command), "globalservers %d %s %s\n", i, Cmd_Argv(2), Cmd_ArgsFrom(3)); + Cbuf_AddText(command); + } + + if ( !numAddress ) + Com_Printf("CL_GlobalServers_f: Error: No master server addresses.\n"); + + return; + } + + sprintf(command, "sv_%smaster%d", (a == 0 ? "" : a == 1 ? "alt1" : "alt2"), masterNum); + masteraddress = Cvar_VariableString(command); + + if (!*masteraddress) + { + Com_Printf("CL_GlobalServers_f: Error: No%s master server address given.\n", + (a == 0 ? "" : a == 1 ? " alternate-1" : " alternate-2")); + continue; + } + + // reset the list, waiting for response + // -1 is used to distinguish a "no response" + netadr_t to; + int i = NET_StringToAdr(masteraddress, &to, NA_UNSPEC); + + if ( i == 0 ) + { + Com_Printf("CL_GlobalServers_f: Error: could not resolve address of%s master %s\n", + (a == 0 ? "" : a == 1 ? " alternate-1" : " alternate-2"), masteraddress); + continue; + } + else if ( i == 2 ) + { + to.port = BigShort(a == 0 ? PORT_MASTER : a == 1 ? ALT1PORT_MASTER : ALT2PORT_MASTER); + } + to.alternateProtocol = a; + + Com_Printf("Requesting servers from%s master %s...\n", + a == 0 ? "" : a == 1 ? " alternate-1" : " alternate-2", + masteraddress); + + cls.numglobalservers = -1; + cls.pingUpdateSource = AS_GLOBAL; + + Com_sprintf(command, sizeof(command), "getserversExt %s %i%s", + com_gamename->string, + a == 0 ? PROTOCOL_VERSION : a == 1 ? 70 : 69, + Cvar_VariableIntegerValue("net_enabled") & NET_ENABLEV4 ? "" : " ipv6"); + + 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); + // outdent + } + CL_RequestMotd(); +} + +/* +================== +CL_GetPing +================== +*/ +void CL_GetPing(int n, char *buf, int buflen, int *pingtime) +{ + const char *str; + int time; + int maxPing; + + if (n < 0 || n >= MAX_PINGREQUESTS || !cl_pinglist[n].adr.port) + { + // empty or invalid slot + buf[0] = '\0'; + *pingtime = 0; + return; + } + + str = NET_AdrToStringwPort(cl_pinglist[n].adr); + Q_strncpyz(buf, str, buflen); + + time = cl_pinglist[n].time; + if (!time) + { + // check for timeout + time = Sys_Milliseconds() - 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_GetPingInfo +================== +*/ +void CL_GetPingInfo(int n, char *buf, int buflen) +{ + if (n < 0 || n >= MAX_PINGREQUESTS || !cl_pinglist[n].adr.port) + { + // empty or invalid slot + if (buflen) buf[0] = '\0'; + return; + } + + Q_strncpyz(buf, cl_pinglist[n].info, buflen); +} + +/* +================== +CL_ClearPing +================== +*/ +void CL_ClearPing(int n) +{ + if (n < 0 || n >= MAX_PINGREQUESTS) return; + + cl_pinglist[n].adr.port = 0; +} + +/* +================== +CL_GetPingQueueCount +================== +*/ +int CL_GetPingQueueCount(void) +{ + int i; + int count; + ping_t *pingptr; + + count = 0; + pingptr = cl_pinglist; + + for (i = 0; i < MAX_PINGREQUESTS; i++, pingptr++) + { + if (pingptr->adr.port) + { + count++; + } + } + + return (count); +} + +/* +================== +CL_GetFreePing +================== +*/ +static ping_t *CL_GetFreePing(void) +{ + ping_t *pingptr; + ping_t *best; + int oldest; + int i; + int time; + + pingptr = cl_pinglist; + for (i = 0; i < MAX_PINGREQUESTS; i++, pingptr++) + { + // find free ping slot + if (pingptr->adr.port) + { + if (!pingptr->time) + { + if (Sys_Milliseconds() - pingptr->start < 500) + { + // still waiting for response + continue; + } + } + else if (pingptr->time < 500) + { + // results have not been queried + continue; + } + } + + // clear it + pingptr->adr.port = 0; + return (pingptr); + } + + // use oldest entry + pingptr = cl_pinglist; + best = cl_pinglist; + oldest = INT_MIN; + for (i = 0; i < MAX_PINGREQUESTS; i++, pingptr++) + { + // scan for oldest + time = Sys_Milliseconds() - pingptr->start; + if (time > oldest) + { + oldest = time; + best = pingptr; + } + } + + return (best); +} + +/* +================== +CL_Ping_f +================== +*/ +static void CL_Ping_f(void) +{ + netadr_t to; + ping_t *pingptr; + const char *server; + int argc; + netadrtype_t family = NA_UNSPEC; + + argc = Cmd_Argc(); + + if (argc != 2 && argc != 3) + { + Com_Printf("usage: ping [-4|-6] server\n"); + return; + } + + if (argc == 2) + server = Cmd_Argv(1); + else + { + if (!strcmp(Cmd_Argv(1), "-4")) + family = NA_IP; + 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); + } + + ::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 = Sys_Milliseconds(); + pingptr->time = 0; + + CL_SetServerInfoByAddress(pingptr->adr, NULL, 0); + + NET_OutOfBandPrint(NS_CLIENT, to, "getinfo xxx"); +} + +/* +================== +CL_UpdateVisiblePings_f +================== +*/ +bool CL_UpdateVisiblePings_f(int source) +{ + int slots, i; + char buff[MAX_STRING_CHARS]; + int pingTime; + int max; + bool status = false; + + if (source < 0 || source > AS_FAVORITES) + { + return false; + } + + cls.pingUpdateSource = source; + + slots = CL_GetPingQueueCount(); + if (slots < MAX_PINGREQUESTS) + { + serverInfo_t *server = NULL; + + switch (source) + { + case AS_LOCAL: + server = &cls.localServers[0]; + max = cls.numlocalservers; + break; + case AS_GLOBAL: + server = &cls.globalServers[0]; + max = cls.numglobalservers; + break; + case AS_FAVORITES: + server = &cls.favoriteServers[0]; + max = cls.numfavoriteservers; + break; + default: + return false; + } + 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 = true; + 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 = Sys_Milliseconds(); + 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 = true; + } + 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 = true; + } + } + + return status; +} + +/* +================== +CL_ServerStatus_f +================== +*/ +static void CL_ServerStatus_f(void) +{ + netadr_t to, *toptr = NULL; + const char *server; + serverStatus_t *serverStatus; + int argc; + netadrtype_t family = NA_UNSPEC; + + argc = Cmd_Argc(); + + if (argc != 2 && argc != 3) + { + if (clc.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) + { + ::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 = true; + serverStatus->pending = true; +} + +/* +============ +CL_InitRef +============ +*/ +static void CL_InitRef(void) +{ + refimport_t ri; + refexport_t *ret; +#ifdef USE_RENDERER_DLOPEN + GetRefAPI_t GetRefAPI; + char dllName[MAX_OSPATH]; +#endif + + Com_Printf("----- Initializing Renderer ----\n"); + +#ifdef USE_RENDERER_DLOPEN + cl_renderer = Cvar_Get("cl_renderer", "opengl2", CVAR_ARCHIVE | CVAR_LATCH); + + Com_sprintf(dllName, sizeof(dllName), "renderer_%s" DLL_EXT, cl_renderer->string); + + if (!(rendererLib = Sys_LoadDll(dllName, false)) && strcmp(cl_renderer->string, cl_renderer->resetString)) + { + Com_Printf("failed:\n\"%s\"\n", Sys_LibraryError()); + Cvar_ForceReset("cl_renderer"); + + Com_sprintf(dllName, sizeof(dllName), "renderer_opengl1" DLL_EXT); + rendererLib = Sys_LoadDll(dllName, false); + } + + if (!rendererLib) + { + Com_Printf("failed:\n\"%s\"\n", Sys_LibraryError()); + Com_Error(ERR_FATAL, "Failed to load renderer"); + } + + GetRefAPI = (GetRefAPI_t)Sys_LoadFunction(rendererLib, "GetRefAPI"); + if (!GetRefAPI) + { + Com_Error(ERR_FATAL, "Can't load symbol GetRefAPI: '%s'", Sys_LibraryError()); + } +#endif + + 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_ClusterPVS = CM_ClusterPVS; + 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_SetValue = Cvar_SetValue; + ri.Cvar_CheckRange = Cvar_CheckRange; + ri.Cvar_SetDescription = Cvar_SetDescription; + ri.Cvar_VariableIntegerValue = Cvar_VariableIntegerValue; + + // cinematic stuff + + ri.CIN_UploadCinematic = CIN_UploadCinematic; + ri.CIN_PlayCinematic = CIN_PlayCinematic; + ri.CIN_RunCinematic = CIN_RunCinematic; + + ri.CL_WriteAVIVideoFrame = CL_WriteAVIVideoFrame; + + ri.IN_Init = IN_Init; + ri.IN_Shutdown = IN_Shutdown; + ri.IN_Restart = IN_Restart; + + ri.Sys_GLimpSafeInit = Sys_GLimpSafeInit; + ri.Sys_GLimpInit = Sys_GLimpInit; + ri.Sys_LowPhysicalMemory = Sys_LowPhysicalMemory; + + 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"); +} + +/* +==================== +CL_ProtocolSpecificCommandsInit + +For adding/remove commands that depend on a/some +specific protocols, whenever the protcol may change +==================== +*/ +void CL_ProtocolSpecificCommandsInit(void) +{ + Con_MessageModesInit(); +} + +/* +==================== +CL_Init +==================== +*/ +void CL_Init(void) +{ + Com_Printf("----- Client Initialization -----\n"); + + Con_Init(); + + if (!com_fullyInitialized) + { + CL_ClearState(); + clc.state = CA_DISCONNECTED; // no longer CA_UNINITIALIZED + cl_oldGameSet = false; + } + + 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); + + // 0: legacy mouse acceleration + // 1: new implementation + cl_mouseAccelStyle = Cvar_Get("cl_mouseAccelStyle", "0", CVAR_ARCHIVE); + // offset for the power function (for style 1, ignored otherwise) + // this should be set to the max rate value + cl_mouseAccelOffset = Cvar_Get("cl_mouseAccelOffset", "5", CVAR_ARCHIVE); + Cvar_CheckRange(cl_mouseAccelOffset, 0.001f, 50000.0f, false); + + cl_showMouseRate = Cvar_Get("cl_showmouserate", "0", 0); + + cl_allowDownload = Cvar_Get("cl_allowDownload", "1", CVAR_ARCHIVE); + + if (cl_allowDownload->integer != -1) cl_allowDownload->integer = DLF_ENABLE; + + com_downloadPrompt = Cvar_Get("com_downloadPrompt", "0", CVAR_ROM); + Cvar_Get("com_downloadPromptText", "", CVAR_TEMP); + + cl_conXOffset = Cvar_Get("cl_conXOffset", "0", 0); +#ifdef __APPLE__ + // 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); + + 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 __APPLE__ + // 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 + + j_pitch = Cvar_Get("j_pitch", "0.022", CVAR_ARCHIVE); + j_yaw = Cvar_Get("j_yaw", "-0.022", CVAR_ARCHIVE); + j_forward = Cvar_Get("j_forward", "-0.25", CVAR_ARCHIVE); + j_side = Cvar_Get("j_side", "0.25", CVAR_ARCHIVE); + j_up = Cvar_Get("j_up", "0", CVAR_ARCHIVE); + + j_pitch_axis = Cvar_Get("j_pitch_axis", "3", CVAR_ARCHIVE); + j_yaw_axis = Cvar_Get("j_yaw_axis", "2", CVAR_ARCHIVE); + j_forward_axis = Cvar_Get("j_forward_axis", "1", CVAR_ARCHIVE); + j_side_axis = Cvar_Get("j_side_axis", "0", CVAR_ARCHIVE); + j_up_axis = Cvar_Get("j_up_axis", "4", CVAR_ARCHIVE); + + Cvar_CheckRange(j_pitch_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_yaw_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_forward_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_side_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_up_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + + 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); + + cl_rsaAuth = Cvar_Get("cl_rsaAuth", "0", CVAR_INIT | CVAR_PROTECTED); + + // ~ and `, as keys and characters + cl_consoleKeys = Cvar_Get("cl_consoleKeys", "~ ` 0x7e 0x60", CVAR_ARCHIVE); + + cl_clantag = Cvar_Get ("cl_clantag", "", CVAR_ARCHIVE); + + // userinfo + Cvar_Get("name", "UnnamedPlayer", CVAR_USERINFO | CVAR_ARCHIVE); + cl_rate = Cvar_Get("rate", "25000", CVAR_USERINFO | CVAR_ARCHIVE); + Cvar_Get("snaps", "40", 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", "spatial", 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); + + cl_voip = Cvar_Get("cl_voip", "1", CVAR_ARCHIVE); + Cvar_CheckRange(cl_voip, 0, 1, true); + cl_voipProtocol = Cvar_Get("cl_voipProtocol", cl_voip->integer ? "opus" : "", CVAR_USERINFO | CVAR_ROM); +#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("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); + Cmd_AddCommand("downloadUpdate", CL_DownloadUpdate_f); + Cmd_AddCommand("installUpdate", CL_InstallUpdate_f); + Cmd_AddCommand("checkForUpdate", CL_CheckForUpdate_f); + Cmd_AddCommand("browseHomepath", CL_BrowseHomepath_f); + Cmd_AddCommand("browseDemos", CL_BrowseDemos_f); + Cmd_AddCommand("browseScreenShots", CL_BrowseScreenShots_f); + + CL_InitRef(); + + SCR_Init(); + + // Cbuf_Execute (); + + Cvar_Set("cl_running", "1"); + + if (cl_rsaAuth->integer) CL_LoadRSAKeypair(); + + CL_GenerateQKey(); + Cvar_Get("cl_guid", "", CVAR_USERINFO | CVAR_ROM); + if (clc.state == CA_DISCONNECTED) CL_UpdateGUID(NULL, 0); + + Com_Printf("----- Client Initialization Complete -----\n"); +} + +/* +=============== +CL_Shutdown + +=============== +*/ +void CL_Shutdown(const char *finalmsg, bool disconnect, bool quit) +{ + static bool recursive = false; + int realtime; + + // check whether the client is running at all. + if (!(com_cl_running && com_cl_running->integer)) return; + + Com_Printf("----- Client Shutdown (%s) -----\n", finalmsg); + + if (recursive) + { + Com_Printf("WARNING: Recursive shutdown\n"); + return; + } + recursive = true; + + noGameRestart = quit; + + if (disconnect) CL_Disconnect(true); + + CL_ClearMemory(true); + CL_Snd_Shutdown(); + + Cmd_RemoveCommand("cmd"); + Cmd_RemoveCommand("configstrings"); + Cmd_RemoveCommand("clientinfo"); + Cmd_RemoveCommand("snd_restart"); + Cmd_RemoveCommand("vid_restart"); + Cmd_RemoveCommand("disconnect"); + Cmd_RemoveCommand("record"); + Cmd_RemoveCommand("demo"); + Cmd_RemoveCommand("cinematic"); + Cmd_RemoveCommand("stoprecord"); + Cmd_RemoveCommand("connect"); + Cmd_RemoveCommand("reconnect"); + Cmd_RemoveCommand("localservers"); + Cmd_RemoveCommand("globalservers"); + Cmd_RemoveCommand("rcon"); + Cmd_RemoveCommand("ping"); + Cmd_RemoveCommand("serverstatus"); + Cmd_RemoveCommand("showip"); + Cmd_RemoveCommand("fs_openedList"); + Cmd_RemoveCommand("fs_referencedList"); + Cmd_RemoveCommand("model"); + Cmd_RemoveCommand("video"); + Cmd_RemoveCommand("stopvideo"); + + CL_ShutdownInput(); + Con_Shutdown(); + + Cvar_Set("cl_running", "0"); + + recursive = false; + + if (cl_rsaAuth->integer) CL_UnloadRSAKeypair(); + + realtime = cls.realtime; + ::memset(&cls, 0, sizeof(cls)); + cls.realtime = realtime; + Key_SetCatcher(0); + + Com_Printf("-----------------------\n"); +} -- cgit