+
+ // finished loading sounds
+ }
+
+ SCR_PopLoadingScreen(false);
+
+ if (!cl.loadfinished)
+ {
+ cl.loadfinished = true;
+
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+
+ // now issue the spawn to move on to signon 2 like normal
+ if (cls.netcon)
+ CL_ForwardToServer("prespawn");
+ }
+}
+
+static void CL_BeginDownloads_f(cmd_state_t *cmd)
+{
+ // prevent cl_begindownloads from being issued multiple times in one match
+ // to prevent accidentally cancelled downloads
+ if(cl.loadbegun)
+ Con_Printf("cl_begindownloads is only valid once per match\n");
+ else
+ CL_BeginDownloads(false);
+}
+
+static void CL_StopDownload(int size, int crc)
+{
+ if (cls.qw_downloadmemory && cls.qw_downloadmemorycursize == size && CRC_Block(cls.qw_downloadmemory, cls.qw_downloadmemorycursize) == crc)
+ {
+ int existingcrc;
+ size_t existingsize;
+ const char *extension;
+
+ if(cls.qw_download_deflate)
+ {
+ unsigned char *out;
+ size_t inflated_size;
+ out = FS_Inflate(cls.qw_downloadmemory, cls.qw_downloadmemorycursize, &inflated_size, tempmempool);
+ Mem_Free(cls.qw_downloadmemory);
+ if(out)
+ {
+ Con_Printf("Inflated download: new size: %u (%g%%)\n", (unsigned)inflated_size, 100.0 - 100.0*(cls.qw_downloadmemorycursize / (float)inflated_size));
+ cls.qw_downloadmemory = out;
+ cls.qw_downloadmemorycursize = (int)inflated_size;
+ }
+ else
+ {
+ cls.qw_downloadmemory = NULL;
+ cls.qw_downloadmemorycursize = 0;
+ Con_Printf("Cannot inflate download, possibly corrupt or zlib not present\n");
+ }
+ }
+
+ if(!cls.qw_downloadmemory)
+ {
+ Con_Printf("Download \"%s\" is corrupt (see above!)\n", cls.qw_downloadname);
+ }
+ else
+ {
+ crc = CRC_Block(cls.qw_downloadmemory, cls.qw_downloadmemorycursize);
+ size = cls.qw_downloadmemorycursize;
+ // finished file
+ // save to disk only if we don't already have it
+ // (this is mainly for playing back demos)
+ existingcrc = FS_CRCFile(cls.qw_downloadname, &existingsize);
+ if (existingsize || IS_NEXUIZ_DERIVED(gamemode) || !strcmp(cls.qw_downloadname, csqc_progname.string))
+ // let csprogs ALWAYS go to dlcache, to prevent "viral csprogs"; also, never put files outside dlcache for Nexuiz/Xonotic
+ {
+ if ((int)existingsize != size || existingcrc != crc)
+ {
+ // we have a mismatching file, pick another name for it
+ char name[MAX_QPATH*2];
+ dpsnprintf(name, sizeof(name), "dlcache/%s.%i.%i", cls.qw_downloadname, size, crc);
+ if (!FS_FileExists(name))
+ {
+ Con_Printf("Downloaded \"%s\" (%i bytes, %i CRC)\n", name, size, crc);
+ FS_WriteFile(name, cls.qw_downloadmemory, cls.qw_downloadmemorycursize);
+ if(!strcmp(cls.qw_downloadname, csqc_progname.string))
+ {
+ if(cls.caughtcsprogsdata)
+ Mem_Free(cls.caughtcsprogsdata);
+ cls.caughtcsprogsdata = (unsigned char *) Mem_Alloc(cls.permanentmempool, cls.qw_downloadmemorycursize);
+ memcpy(cls.caughtcsprogsdata, cls.qw_downloadmemory, cls.qw_downloadmemorycursize);
+ cls.caughtcsprogsdatasize = cls.qw_downloadmemorycursize;
+ Con_DPrintf("Buffered \"%s\"\n", name);
+ }
+ }
+ }
+ }
+ else
+ {
+ // we either don't have it or have a mismatching file...
+ // so it's time to accept the file
+ // but if we already have a mismatching file we need to rename
+ // this new one, and if we already have this file in renamed form,
+ // we do nothing
+ Con_Printf("Downloaded \"%s\" (%i bytes, %i CRC)\n", cls.qw_downloadname, size, crc);
+ FS_WriteFile(cls.qw_downloadname, cls.qw_downloadmemory, cls.qw_downloadmemorycursize);
+ extension = FS_FileExtension(cls.qw_downloadname);
+ if (!strcasecmp(extension, "pak") || !strcasecmp(extension, "pk3") || !strcasecmp(extension, "dpk"))
+ FS_Rescan();
+ }
+ }
+ }
+ else if (cls.qw_downloadmemory && size)
+ {
+ Con_Printf("Download \"%s\" is corrupt (%i bytes, %i CRC, should be %i bytes, %i CRC), discarding\n", cls.qw_downloadname, size, crc, (int)cls.qw_downloadmemorycursize, (int)CRC_Block(cls.qw_downloadmemory, cls.qw_downloadmemorycursize));
+ CL_BeginDownloads(true);
+ }
+
+ if (cls.qw_downloadmemory)
+ Mem_Free(cls.qw_downloadmemory);
+ cls.qw_downloadmemory = NULL;
+ cls.qw_downloadname[0] = 0;
+ cls.qw_downloadmemorymaxsize = 0;
+ cls.qw_downloadmemorycursize = 0;
+ cls.qw_downloadpercent = 0;
+}
+
+static void CL_ParseDownload(void)
+{
+ int i, start, size;
+ static unsigned char data[NET_MAXMESSAGE];
+ start = MSG_ReadLong(&cl_message);
+ size = (unsigned short)MSG_ReadShort(&cl_message);
+
+ // record the start/size information to ack in the next input packet
+ for (i = 0;i < CL_MAX_DOWNLOADACKS;i++)
+ {
+ if (!cls.dp_downloadack[i].start && !cls.dp_downloadack[i].size)
+ {
+ cls.dp_downloadack[i].start = start;
+ cls.dp_downloadack[i].size = size;
+ break;
+ }
+ }
+
+ MSG_ReadBytes(&cl_message, size, data);
+
+ if (!cls.qw_downloadname[0])
+ {
+ if (size > 0)
+ Con_Printf("CL_ParseDownload: received %i bytes with no download active\n", size);
+ return;
+ }
+
+ if (start + size > cls.qw_downloadmemorymaxsize)
+ Host_Error("corrupt download message\n");
+
+ // only advance cursize if the data is at the expected position
+ // (gaps are unacceptable)
+ memcpy(cls.qw_downloadmemory + start, data, size);
+ cls.qw_downloadmemorycursize = start + size;
+ cls.qw_downloadpercent = (int)floor((start+size) * 100.0 / cls.qw_downloadmemorymaxsize);
+ cls.qw_downloadpercent = bound(0, cls.qw_downloadpercent, 100);
+ cls.qw_downloadspeedcount += size;
+}
+
+static void CL_DownloadBegin_f(cmd_state_t *cmd)
+{
+ int size = atoi(Cmd_Argv(cmd, 1));
+
+ if (size < 0 || size > 1<<30 || FS_CheckNastyPath(Cmd_Argv(cmd, 2), false))
+ {
+ Con_Printf("cl_downloadbegin: received bogus information\n");
+ CL_StopDownload(0, 0);
+ return;
+ }
+
+ if (cls.qw_downloadname[0])
+ Con_Printf("Download of %s aborted\n", cls.qw_downloadname);
+
+ CL_StopDownload(0, 0);
+
+ // we're really beginning a download now, so initialize stuff
+ strlcpy(cls.qw_downloadname, Cmd_Argv(cmd, 2), sizeof(cls.qw_downloadname));
+ cls.qw_downloadmemorymaxsize = size;
+ cls.qw_downloadmemory = (unsigned char *) Mem_Alloc(cls.permanentmempool, cls.qw_downloadmemorymaxsize);
+ cls.qw_downloadnumber++;
+
+ cls.qw_download_deflate = false;
+ if(Cmd_Argc(cmd) >= 4)
+ {
+ if(!strcmp(Cmd_Argv(cmd, 3), "deflate"))
+ cls.qw_download_deflate = true;
+ // check further encodings here
+ }
+
+ CL_ForwardToServer("sv_startdownload");
+}
+
+static void CL_StopDownload_f(cmd_state_t *cmd)
+{
+ Curl_CancelAll();
+ if (cls.qw_downloadname[0])
+ {
+ Con_Printf("Download of %s aborted\n", cls.qw_downloadname);
+ CL_StopDownload(0, 0);
+ }
+ CL_BeginDownloads(true);
+}
+
+static void CL_DownloadFinished_f(cmd_state_t *cmd)
+{
+ if (Cmd_Argc(cmd) < 3)
+ {
+ Con_Printf("Malformed cl_downloadfinished command\n");
+ return;
+ }
+ CL_StopDownload(atoi(Cmd_Argv(cmd, 1)), atoi(Cmd_Argv(cmd, 2)));
+ CL_BeginDownloads(false);
+}
+
+extern cvar_t cl_topcolor;
+extern cvar_t cl_bottomcolor;
+static void CL_SendPlayerInfo(void)
+{
+ char vabuf[1024];
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va(vabuf, sizeof(vabuf), "name \"%s\"", cl_name.string));
+
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va(vabuf, sizeof(vabuf), "color %i %i", cl_topcolor.integer, cl_bottomcolor.integer));
+
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va(vabuf, sizeof(vabuf), "rate %i", cl_rate.integer));
+
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va(vabuf, sizeof(vabuf), "rate_burstsize %i", cl_rate_burstsize.integer));
+
+ if (cl_pmodel.integer)
+ {
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va(vabuf, sizeof(vabuf), "pmodel %i", cl_pmodel.integer));
+ }
+ if (*cl_playermodel.string)
+ {
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va(vabuf, sizeof(vabuf), "playermodel %s", cl_playermodel.string));
+ }
+ if (*cl_playerskin.string)
+ {
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va(vabuf, sizeof(vabuf), "playerskin %s", cl_playerskin.string));
+ }
+}
+
+/*
+=====================
+CL_SignonReply
+
+An svc_signonnum has been received, perform a client side setup
+=====================
+*/
+static void CL_SignonReply (void)
+{
+ Con_DPrintf("CL_SignonReply: %i\n", cls.signon);
+
+ switch (cls.signon)
+ {
+ case 1:
+ if (cls.netcon)
+ {
+ // send player info before we begin downloads
+ // (so that the server can see the player name while downloading)
+ CL_SendPlayerInfo();
+
+ // execute cl_begindownloads next frame
+ // (after any commands added by svc_stufftext have been executed)
+ // when done with downloads the "prespawn" will be sent
+ Cbuf_AddText(cmd_local, "\ncl_begindownloads\n");
+
+ //MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ //MSG_WriteString (&cls.netcon->message, "prespawn");
+ }
+ else // playing a demo... make sure loading occurs as soon as possible
+ CL_BeginDownloads(false);
+ break;
+
+ case 2:
+ if (cls.netcon)
+ {
+ // LadyHavoc: quake sent the player info here but due to downloads
+ // it is sent earlier instead
+ // CL_SendPlayerInfo();
+
+ // LadyHavoc: changed to begin a loading stage and issue this when done
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, "spawn");
+ }
+ break;
+
+ case 3:
+ if (cls.netcon)
+ {
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, "begin");
+ }
+ break;
+
+ case 4:
+ // after the level has been loaded, we shouldn't need the shaders, and
+ // if they are needed again they will be automatically loaded...
+ // we also don't need the unused models or sounds from the last level
+ Mod_FreeQ3Shaders();
+ Mod_PurgeUnused();
+ S_PurgeUnused();
+
+ Con_ClearNotify();
+ if (Sys_CheckParm("-profilegameonly"))
+ Sys_AllowProfiling(true);
+ break;
+ }
+}
+
+/*
+==================
+CL_ParseServerInfo
+==================
+*/
+static void CL_ParseServerInfo (void)
+{
+ char *str;
+ int i;
+ protocolversion_t protocol;
+ int nummodels, numsounds;
+ char vabuf[1024];
+
+ // if we start loading a level and a video is still playing, stop it
+ CL_VideoStop();
+
+ Con_DPrint("Serverinfo packet received.\n");
+ Collision_Cache_Reset(true);
+
+ // if server is active, we already began a loading plaque
+ if (!sv.active)
+ {
+ SCR_BeginLoadingPlaque(false);
+ S_StopAllSounds();
+ // free q3 shaders so that any newly downloaded shaders will be active
+ Mod_FreeQ3Shaders();
+ }
+
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+
+ // clear cl_serverextension cvars
+ Cvar_SetValueQuick(&cl_serverextension_download, 0);
+
+//
+// wipe the client_state_t struct
+//
+ CL_ClearState ();
+
+// parse protocol version number
+ i = MSG_ReadLong(&cl_message);
+ protocol = Protocol_EnumForNumber(i);
+ if (protocol == PROTOCOL_UNKNOWN)
+ {
+ Host_Error("CL_ParseServerInfo: Server is unrecognized protocol number (%i)", i);
+ return;
+ }
+ // hack for unmarked Nehahra movie demos which had a custom protocol
+ if (protocol == PROTOCOL_QUAKEDP && cls.demoplayback && gamemode == GAME_NEHAHRA)
+ protocol = PROTOCOL_NEHAHRAMOVIE;
+ cls.protocol = protocol;
+ Con_Printf("Server protocol is %s\n", Protocol_NameForEnum(cls.protocol));
+
+ cl.num_entities = 1;
+
+ if (protocol == PROTOCOL_QUAKEWORLD)
+ {
+ char gamedir[1][MAX_QPATH];
+
+ cl.qw_servercount = MSG_ReadLong(&cl_message);
+
+ str = MSG_ReadString(&cl_message, cl_readstring, sizeof(cl_readstring));
+ Con_Printf("server gamedir is %s\n", str);
+ strlcpy(gamedir[0], str, sizeof(gamedir[0]));
+
+ // change gamedir if needed
+ if (!FS_ChangeGameDirs(1, gamedir, true, false))
+ Host_Error("CL_ParseServerInfo: unable to switch to server specified gamedir");
+
+ cl.gametype = GAME_DEATHMATCH;
+ cl.maxclients = 32;
+
+ // parse player number
+ i = MSG_ReadByte(&cl_message);
+ // cl.qw_spectator is an unneeded flag, cl.scores[cl.playerentity].qw_spectator works better (it can be updated by the server during the game)
+ //cl.qw_spectator = (i & 128) != 0;
+ cl.realplayerentity = cl.playerentity = cl.viewentity = (i & 127) + 1;
+ cl.scores = (scoreboard_t *)Mem_Alloc(cls.levelmempool, cl.maxclients*sizeof(*cl.scores));
+
+ // get the full level name
+ str = MSG_ReadString(&cl_message, cl_readstring, sizeof(cl_readstring));
+ strlcpy (cl.worldmessage, str, sizeof(cl.worldmessage));
+
+ // get the movevars that are defined in the qw protocol
+ cl.movevars_gravity = MSG_ReadFloat(&cl_message);
+ cl.movevars_stopspeed = MSG_ReadFloat(&cl_message);
+ cl.movevars_maxspeed = MSG_ReadFloat(&cl_message);
+ cl.movevars_spectatormaxspeed = MSG_ReadFloat(&cl_message);
+ cl.movevars_accelerate = MSG_ReadFloat(&cl_message);
+ cl.movevars_airaccelerate = MSG_ReadFloat(&cl_message);
+ cl.movevars_wateraccelerate = MSG_ReadFloat(&cl_message);
+ cl.movevars_friction = MSG_ReadFloat(&cl_message);
+ cl.movevars_waterfriction = MSG_ReadFloat(&cl_message);
+ cl.movevars_entgravity = MSG_ReadFloat(&cl_message);
+
+ // other movevars not in the protocol...
+ cl.movevars_wallfriction = 0;
+ cl.movevars_timescale = 1;
+ cl.movevars_jumpvelocity = 270;
+ cl.movevars_edgefriction = 1;
+ cl.movevars_maxairspeed = 30;
+ cl.movevars_stepheight = 18;
+ cl.movevars_airaccel_qw = 1;
+ cl.movevars_airaccel_sideways_friction = 0;
+
+ // seperate the printfs so the server message can have a color
+ Con_Printf("\n\n<===================================>\n\n\2%s\n", str);
+
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+
+ if (cls.netcon)
+ {
+ MSG_WriteByte(&cls.netcon->message, qw_clc_stringcmd);
+ MSG_WriteString(&cls.netcon->message, va(vabuf, sizeof(vabuf), "soundlist %i %i", cl.qw_servercount, 0));
+ }
+
+ cl.loadbegun = false;
+ cl.loadfinished = false;
+
+ cls.state = ca_connected;
+ cls.signon = 1;
+
+ // note: on QW protocol we can't set up the gameworld until after
+ // downloads finish...
+ // (we don't even know the name of the map yet)
+ // this also means cl_autodemo does not work on QW protocol...
+
+ strlcpy(cl.worldname, "", sizeof(cl.worldname));
+ strlcpy(cl.worldnamenoextension, "", sizeof(cl.worldnamenoextension));
+ strlcpy(cl.worldbasename, "qw", sizeof(cl.worldbasename));
+ Cvar_SetQuick(&cl_worldname, cl.worldname);
+ Cvar_SetQuick(&cl_worldnamenoextension, cl.worldnamenoextension);
+ Cvar_SetQuick(&cl_worldbasename, cl.worldbasename);
+
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+ }
+ else
+ {
+ // parse maxclients
+ cl.maxclients = MSG_ReadByte(&cl_message);
+ if (cl.maxclients < 1 || cl.maxclients > MAX_SCOREBOARD)
+ {
+ Host_Error("Bad maxclients (%u) from server", cl.maxclients);
+ return;
+ }
+ cl.scores = (scoreboard_t *)Mem_Alloc(cls.levelmempool, cl.maxclients*sizeof(*cl.scores));
+
+ // parse gametype
+ cl.gametype = MSG_ReadByte(&cl_message);
+ // the original id singleplayer demos are bugged and contain
+ // GAME_DEATHMATCH even for singleplayer
+ if (cl.maxclients == 1 && cls.protocol == PROTOCOL_QUAKE)
+ cl.gametype = GAME_COOP;
+
+ // parse signon message
+ str = MSG_ReadString(&cl_message, cl_readstring, sizeof(cl_readstring));
+ strlcpy (cl.worldmessage, str, sizeof(cl.worldmessage));
+
+ // seperate the printfs so the server message can have a color
+ if (cls.protocol != PROTOCOL_NEHAHRAMOVIE) // no messages when playing the Nehahra movie
+ Con_Printf("\n<===================================>\n\n\2%s\n", str);
+
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+
+ // parse model precache list
+ for (nummodels=1 ; ; nummodels++)
+ {
+ str = MSG_ReadString(&cl_message, cl_readstring, sizeof(cl_readstring));
+ if (!str[0])
+ break;
+ if (nummodels==MAX_MODELS)
+ Host_Error ("Server sent too many model precaches");
+ if (strlen(str) >= MAX_QPATH)
+ Host_Error ("Server sent a precache name of %i characters (max %i)", (int)strlen(str), MAX_QPATH - 1);
+ strlcpy (cl.model_name[nummodels], str, sizeof (cl.model_name[nummodels]));
+ }
+ // parse sound precache list
+ for (numsounds=1 ; ; numsounds++)
+ {
+ str = MSG_ReadString(&cl_message, cl_readstring, sizeof(cl_readstring));
+ if (!str[0])
+ break;
+ if (numsounds==MAX_SOUNDS)
+ Host_Error("Server sent too many sound precaches");
+ if (strlen(str) >= MAX_QPATH)
+ Host_Error("Server sent a precache name of %i characters (max %i)", (int)strlen(str), MAX_QPATH - 1);
+ strlcpy (cl.sound_name[numsounds], str, sizeof (cl.sound_name[numsounds]));
+ }
+
+ // set the base name for level-specific things... this gets updated again by CL_SetupWorldModel later
+ strlcpy(cl.worldname, cl.model_name[1], sizeof(cl.worldname));
+ FS_StripExtension(cl.worldname, cl.worldnamenoextension, sizeof(cl.worldnamenoextension));
+ strlcpy(cl.worldbasename, !strncmp(cl.worldnamenoextension, "maps/", 5) ? cl.worldnamenoextension + 5 : cl.worldnamenoextension, sizeof(cl.worldbasename));
+ Cvar_SetQuick(&cl_worldmessage, cl.worldmessage);
+ Cvar_SetQuick(&cl_worldname, cl.worldname);
+ Cvar_SetQuick(&cl_worldnamenoextension, cl.worldnamenoextension);
+ Cvar_SetQuick(&cl_worldbasename, cl.worldbasename);
+
+ // touch all of the precached models that are still loaded so we can free
+ // anything that isn't needed
+ if (!sv.active)
+ Mod_ClearUsed();
+ for (i = 1;i < nummodels;i++)
+ Mod_FindName(cl.model_name[i], cl.model_name[i][0] == '*' ? cl.model_name[1] : NULL);
+ // precache any models used by the client (this also marks them used)
+ cl.model_bolt = Mod_ForName("progs/bolt.mdl", false, false, NULL);
+ cl.model_bolt2 = Mod_ForName("progs/bolt2.mdl", false, false, NULL);
+ cl.model_bolt3 = Mod_ForName("progs/bolt3.mdl", false, false, NULL);
+ cl.model_beam = Mod_ForName("progs/beam.mdl", false, false, NULL);
+
+ // we purge the models and sounds later in CL_SignonReply
+ //Mod_PurgeUnused();
+ //S_PurgeUnused();
+
+ // clear sound usage flags for purging of unused sounds
+ S_ClearUsed();
+
+ // precache any sounds used by the client
+ cl.sfx_wizhit = S_PrecacheSound(cl_sound_wizardhit.string, false, true);
+ cl.sfx_knighthit = S_PrecacheSound(cl_sound_hknighthit.string, false, true);
+ cl.sfx_tink1 = S_PrecacheSound(cl_sound_tink1.string, false, true);
+ cl.sfx_ric1 = S_PrecacheSound(cl_sound_ric1.string, false, true);
+ cl.sfx_ric2 = S_PrecacheSound(cl_sound_ric2.string, false, true);
+ cl.sfx_ric3 = S_PrecacheSound(cl_sound_ric3.string, false, true);
+ cl.sfx_r_exp3 = S_PrecacheSound(cl_sound_r_exp3.string, false, true);
+
+ // sounds used by the game
+ for (i = 1;i < MAX_SOUNDS && cl.sound_name[i][0];i++)
+ cl.sound_precache[i] = S_PrecacheSound(cl.sound_name[i], true, true);
+
+ // now we try to load everything that is new
+ cl.loadmodel_current = 1;
+ cl.downloadmodel_current = 1;
+ cl.loadmodel_total = nummodels;
+ cl.loadsound_current = 1;
+ cl.downloadsound_current = 1;
+ cl.loadsound_total = numsounds;
+ cl.downloadcsqc = true;
+ cl.loadbegun = false;
+ cl.loadfinished = false;
+ cl.loadcsqc = true;
+
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+
+ // if cl_autodemo is set, automatically start recording a demo if one isn't being recorded already
+ if (cl_autodemo.integer && cls.netcon && cls.protocol != PROTOCOL_QUAKEWORLD)
+ {
+ char demofile[MAX_OSPATH];
+
+ if (cls.demorecording)
+ {
+ // finish the previous level's demo file
+ CL_Stop_f(cmd_local);
+ }
+
+ // start a new demo file
+ dpsnprintf (demofile, sizeof(demofile), "%s_%s.dem", Sys_TimeString (cl_autodemo_nameformat.string), cl.worldbasename);
+
+ Con_Printf ("Auto-recording to %s.\n", demofile);
+
+ // Reset bit 0 for every new demo
+ Cvar_SetValueQuick(&cl_autodemo_delete,
+ (cl_autodemo_delete.integer & ~0x1)
+ |
+ ((cl_autodemo_delete.integer & 0x2) ? 0x1 : 0)
+ );
+
+ cls.demofile = FS_OpenRealFile(demofile, "wb", false);
+ if (cls.demofile)
+ {
+ cls.forcetrack = -1;
+ FS_Printf (cls.demofile, "%i\n", cls.forcetrack);
+ cls.demorecording = true;
+ strlcpy(cls.demoname, demofile, sizeof(cls.demoname));
+ cls.demo_lastcsprogssize = -1;
+ cls.demo_lastcsprogscrc = -1;
+ }
+ else
+ Con_Print(CON_ERROR "ERROR: couldn't open.\n");
+ }
+ }
+ cl.islocalgame = NetConn_IsLocalGame();
+}
+
+void CL_ValidateState(entity_state_t *s)
+{
+ model_t *model;
+
+ if (!s->active)
+ return;
+
+ if (s->modelindex >= MAX_MODELS)
+ Host_Error("CL_ValidateState: modelindex (%i) >= MAX_MODELS (%i)\n", s->modelindex, MAX_MODELS);
+
+ // these warnings are only warnings, no corrections are made to the state
+ // because states are often copied for decoding, which otherwise would
+ // propogate some of the corrections accidentally
+ // (this used to happen, sometimes affecting skin and frame)
+
+ // colormap is client index + 1
+ if (!(s->flags & RENDER_COLORMAPPED) && s->colormap > cl.maxclients)
+ Con_DPrintf("CL_ValidateState: colormap (%i) > cl.maxclients (%i)\n", s->colormap, cl.maxclients);
+
+ if (developer_extra.integer)
+ {
+ model = CL_GetModelByIndex(s->modelindex);
+ if (model && model->type && s->frame >= model->numframes)
+ Con_DPrintf("CL_ValidateState: no such frame %i in \"%s\" (which has %i frames)\n", s->frame, model->name, model->numframes);
+ if (model && model->type && s->skin > 0 && s->skin >= model->numskins && !(s->lightpflags & PFLAGS_FULLDYNAMIC))
+ Con_DPrintf("CL_ValidateState: no such skin %i in \"%s\" (which has %i skins)\n", s->skin, model->name, model->numskins);
+ }
+}
+
+void CL_MoveLerpEntityStates(entity_t *ent)
+{
+ float odelta[3], adelta[3];
+ VectorSubtract(ent->state_current.origin, ent->persistent.neworigin, odelta);
+ VectorSubtract(ent->state_current.angles, ent->persistent.newangles, adelta);
+ if (!ent->state_previous.active || ent->state_previous.modelindex != ent->state_current.modelindex)
+ {
+ // reset all persistent stuff if this is a new entity
+ ent->persistent.lerpdeltatime = 0;
+ ent->persistent.lerpstarttime = cl.mtime[1];
+ VectorCopy(ent->state_current.origin, ent->persistent.oldorigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.oldangles);
+ VectorCopy(ent->state_current.origin, ent->persistent.neworigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.newangles);
+ // reset animation interpolation as well
+ ent->render.framegroupblend[0].frame = ent->render.framegroupblend[1].frame = ent->state_current.frame;
+ ent->render.framegroupblend[0].start = ent->render.framegroupblend[1].start = cl.time;
+ ent->render.framegroupblend[0].lerp = 1;ent->render.framegroupblend[1].lerp = 0;
+ ent->render.shadertime = cl.time;
+ // reset various persistent stuff
+ ent->persistent.muzzleflash = 0;
+ ent->persistent.trail_allowed = false;
+ }
+ else if ((ent->state_previous.effects & EF_TELEPORT_BIT) != (ent->state_current.effects & EF_TELEPORT_BIT))
+ {
+ // don't interpolate the move
+ ent->persistent.lerpdeltatime = 0;
+ ent->persistent.lerpstarttime = cl.mtime[1];
+ VectorCopy(ent->state_current.origin, ent->persistent.oldorigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.oldangles);
+ VectorCopy(ent->state_current.origin, ent->persistent.neworigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.newangles);
+ ent->persistent.trail_allowed = false;
+
+ // if(ent->state_current.frame != ent->state_previous.frame)
+ // do this even if we did change the frame
+ // teleport bit is only used if an animation restart, or a jump, is necessary
+ // so it should be always harmless to do this
+ {
+ ent->render.framegroupblend[0].frame = ent->render.framegroupblend[1].frame = ent->state_current.frame;
+ ent->render.framegroupblend[0].start = ent->render.framegroupblend[1].start = cl.time;
+ ent->render.framegroupblend[0].lerp = 1;ent->render.framegroupblend[1].lerp = 0;
+ }
+
+ // note that this case must do everything the following case does too
+ }
+ else if ((ent->state_previous.effects & EF_RESTARTANIM_BIT) != (ent->state_current.effects & EF_RESTARTANIM_BIT))
+ {
+ ent->render.framegroupblend[1] = ent->render.framegroupblend[0];
+ ent->render.framegroupblend[1].lerp = 1;
+ ent->render.framegroupblend[0].frame = ent->state_current.frame;
+ ent->render.framegroupblend[0].start = cl.time;
+ ent->render.framegroupblend[0].lerp = 0;
+ }
+ else if (DotProduct(odelta, odelta) > 1000*1000
+ || (cl.fixangle[0] && !cl.fixangle[1])
+ || (ent->state_previous.tagindex != ent->state_current.tagindex)
+ || (ent->state_previous.tagentity != ent->state_current.tagentity))
+ {
+ // don't interpolate the move
+ // (the fixangle[] check detects teleports, but not constant fixangles
+ // such as when spectating)
+ ent->persistent.lerpdeltatime = 0;
+ ent->persistent.lerpstarttime = cl.mtime[1];
+ VectorCopy(ent->state_current.origin, ent->persistent.oldorigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.oldangles);
+ VectorCopy(ent->state_current.origin, ent->persistent.neworigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.newangles);
+ ent->persistent.trail_allowed = false;
+ }
+ else if (ent->state_current.flags & RENDER_STEP)
+ {
+ // monster interpolation
+ if (DotProduct(odelta, odelta) + DotProduct(adelta, adelta) > 0.01)
+ {
+ ent->persistent.lerpdeltatime = bound(0, cl.mtime[1] - ent->persistent.lerpstarttime, 0.1);
+ ent->persistent.lerpstarttime = cl.mtime[1];
+ VectorCopy(ent->persistent.neworigin, ent->persistent.oldorigin);
+ VectorCopy(ent->persistent.newangles, ent->persistent.oldangles);
+ VectorCopy(ent->state_current.origin, ent->persistent.neworigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.newangles);
+ }
+ }
+ else
+ {
+ // not a monster
+ ent->persistent.lerpstarttime = ent->state_previous.time;
+ ent->persistent.lerpdeltatime = bound(0, ent->state_current.time - ent->state_previous.time, 0.1);
+ VectorCopy(ent->persistent.neworigin, ent->persistent.oldorigin);
+ VectorCopy(ent->persistent.newangles, ent->persistent.oldangles);
+ VectorCopy(ent->state_current.origin, ent->persistent.neworigin);
+ VectorCopy(ent->state_current.angles, ent->persistent.newangles);
+ }
+ // trigger muzzleflash effect if necessary
+ if (ent->state_current.effects & EF_MUZZLEFLASH)
+ ent->persistent.muzzleflash = 1;
+
+ // restart animation bit
+ if ((ent->state_previous.effects & EF_RESTARTANIM_BIT) != (ent->state_current.effects & EF_RESTARTANIM_BIT))
+ {
+ ent->render.framegroupblend[1] = ent->render.framegroupblend[0];
+ ent->render.framegroupblend[1].lerp = 1;
+ ent->render.framegroupblend[0].frame = ent->state_current.frame;
+ ent->render.framegroupblend[0].start = cl.time;
+ ent->render.framegroupblend[0].lerp = 0;