+#define LOADPROGRESSWEIGHT_SOUND 1.0
+#define LOADPROGRESSWEIGHT_MODEL 4.0
+#define LOADPROGRESSWEIGHT_WORLDMODEL 30.0
+#define LOADPROGRESSWEIGHT_WORLDMODEL_INIT 2.0
+
+void CL_BeginDownloads(qboolean aborteddownload)
+{
+ // quakeworld works differently
+ if (cls.protocol == PROTOCOL_QUAKEWORLD)
+ return;
+
+ // this would be a good place to do curl downloads
+ if(Curl_Have_forthismap())
+ {
+ Curl_Register_predownload(); // come back later
+ return;
+ }
+
+ // if we got here...
+ // curl is done, so let's start with the business
+ if(!cl.loadbegun)
+ SCR_PushLoadingScreen(false, "Loading precaches", 1);
+ cl.loadbegun = true;
+
+ // if already downloading something from the previous level, don't stop it
+ if (cls.qw_downloadname[0])
+ return;
+
+ if (cl.downloadcsqc)
+ {
+ size_t progsize;
+ cl.downloadcsqc = false;
+ if (cls.netcon
+ && !sv.active
+ && csqc_progname.string
+ && csqc_progname.string[0]
+ && csqc_progcrc.integer >= 0
+ && cl_serverextension_download.integer
+ && (FS_CRCFile(csqc_progname.string, &progsize) != csqc_progcrc.integer || ((int)progsize != csqc_progsize.integer && csqc_progsize.integer != -1))
+ && !FS_FileExists(va("dlcache/%s.%i.%i", csqc_progname.string, csqc_progsize.integer, csqc_progcrc.integer)))
+ {
+ Con_Printf("Downloading new CSQC code to dlcache/%s.%i.%i\n", csqc_progname.string, csqc_progsize.integer, csqc_progcrc.integer);
+ if(cl_serverextension_download.integer == 2 && FS_HasZlib())
+ Cmd_ForwardStringToServer(va("download %s deflate", csqc_progname.string));
+ else
+ Cmd_ForwardStringToServer(va("download %s", csqc_progname.string));
+ return;
+ }
+ }
+
+ if (cl.loadmodel_current < cl.loadmodel_total)
+ {
+ // loading models
+ if(cl.loadmodel_current == 1)
+ {
+ // worldmodel counts as 16 models (15 + world model setup), for better progress bar
+ SCR_PushLoadingScreen(false, "Loading precached models",
+ (
+ (cl.loadmodel_total - 1) * LOADPROGRESSWEIGHT_MODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL_INIT
+ ) / (
+ (cl.loadmodel_total - 1) * LOADPROGRESSWEIGHT_MODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL_INIT
+ + cl.loadsound_total * LOADPROGRESSWEIGHT_SOUND
+ )
+ );
+ SCR_BeginLoadingPlaque();
+ }
+ for (;cl.loadmodel_current < cl.loadmodel_total;cl.loadmodel_current++)
+ {
+ SCR_PushLoadingScreen(false, cl.model_name[cl.loadmodel_current],
+ (
+ (cl.loadmodel_current == 1) ? LOADPROGRESSWEIGHT_WORLDMODEL : LOADPROGRESSWEIGHT_MODEL
+ ) / (
+ (cl.loadmodel_total - 1) * LOADPROGRESSWEIGHT_MODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL_INIT
+ )
+ );
+ if (cl.model_precache[cl.loadmodel_current] && cl.model_precache[cl.loadmodel_current]->Draw)
+ {
+ SCR_PopLoadingScreen(false);
+ if(cl.loadmodel_current == 1)
+ {
+ SCR_PushLoadingScreen(false, cl.model_name[cl.loadmodel_current], 1.0 / cl.loadmodel_total);
+ SCR_PopLoadingScreen(false);
+ }
+ continue;
+ }
+ CL_KeepaliveMessage(true);
+
+ // if running a local game, calling Mod_ForName is a completely wasted effort...
+ if (sv.active)
+ cl.model_precache[cl.loadmodel_current] = sv.models[cl.loadmodel_current];
+ else
+ {
+ if(cl.loadmodel_current == 1)
+ {
+ // they'll be soon loaded, but make sure we apply freshly downloaded shaders from a curled pk3
+ Mod_FreeQ3Shaders();
+ }
+ cl.model_precache[cl.loadmodel_current] = Mod_ForName(cl.model_name[cl.loadmodel_current], false, false, cl.model_name[cl.loadmodel_current][0] == '*' ? cl.model_name[1] : NULL);
+ }
+ SCR_PopLoadingScreen(false);
+ if (cl.model_precache[cl.loadmodel_current] && cl.model_precache[cl.loadmodel_current]->Draw && cl.loadmodel_current == 1)
+ {
+ // we now have the worldmodel so we can set up the game world
+ SCR_PushLoadingScreen(true, "world model setup",
+ (
+ LOADPROGRESSWEIGHT_WORLDMODEL_INIT
+ ) / (
+ (cl.loadmodel_total - 1) * LOADPROGRESSWEIGHT_MODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL_INIT
+ )
+ );
+ CL_SetupWorldModel();
+ SCR_PopLoadingScreen(true);
+ if (!cl.loadfinished && cl_joinbeforedownloadsfinish.integer)
+ {
+ cl.loadfinished = true;
+ // now issue the spawn to move on to signon 2 like normal
+ if (cls.netcon)
+ Cmd_ForwardStringToServer("prespawn");
+ }
+ }
+ }
+ SCR_PopLoadingScreen(false);
+ // finished loading models
+ }
+
+ if (cl.loadsound_current < cl.loadsound_total)
+ {
+ // loading sounds
+ if(cl.loadsound_current == 1)
+ SCR_PushLoadingScreen(false, "Loading precached sounds",
+ (
+ cl.loadsound_total * LOADPROGRESSWEIGHT_SOUND
+ ) / (
+ (cl.loadmodel_total - 1) * LOADPROGRESSWEIGHT_MODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL
+ + LOADPROGRESSWEIGHT_WORLDMODEL_INIT
+ + cl.loadsound_total * LOADPROGRESSWEIGHT_SOUND
+ )
+ );
+ for (;cl.loadsound_current < cl.loadsound_total;cl.loadsound_current++)
+ {
+ SCR_PushLoadingScreen(false, cl.sound_name[cl.loadsound_current], 1.0 / cl.loadsound_total);
+ if (cl.sound_precache[cl.loadsound_current] && S_IsSoundPrecached(cl.sound_precache[cl.loadsound_current]))
+ {
+ SCR_PopLoadingScreen(false);
+ continue;
+ }
+ CL_KeepaliveMessage(true);
+ cl.sound_precache[cl.loadsound_current] = S_PrecacheSound(cl.sound_name[cl.loadsound_current], false, true);
+ SCR_PopLoadingScreen(false);
+ }
+ SCR_PopLoadingScreen(false);
+ // finished loading sounds
+ }
+
+ if(gamemode == GAME_NEXUIZ || gamemode == GAME_XONOTIC)
+ Cvar_SetValueQuick(&cl_serverextension_download, false);
+ // in Nexuiz/Xonotic, the built in download protocol is kinda broken (misses lots
+ // of dependencies) anyway, and can mess around with the game directory;
+ // until this is fixed, only support pk3 downloads via curl, and turn off
+ // individual file downloads other than for CSQC
+ // on the other end of the download protocol, GAME_NEXUIZ/GAME_XONOTIC enforces writing
+ // to dlcache only
+ // idea: support download of pk3 files using this protocol later
+
+ // note: the reason these loops skip already-loaded things is that it
+ // enables this command to be issued during the game if desired
+
+ if (cl.downloadmodel_current < cl.loadmodel_total)
+ {
+ // loading models
+
+ for (;cl.downloadmodel_current < cl.loadmodel_total;cl.downloadmodel_current++)
+ {
+ if (aborteddownload)
+ {
+
+ if (cl.downloadmodel_current == 1)
+ {
+ // the worldmodel failed, but we need to set up anyway
+ Mod_FreeQ3Shaders();
+ CL_SetupWorldModel();
+ if (!cl.loadfinished && cl_joinbeforedownloadsfinish.integer)
+ {
+ cl.loadfinished = true;
+ // now issue the spawn to move on to signon 2 like normal
+ if (cls.netcon)
+ Cmd_ForwardStringToServer("prespawn");
+ }
+ }
+ aborteddownload = false;
+ continue;
+ }
+ if (cl.model_precache[cl.downloadmodel_current] && cl.model_precache[cl.downloadmodel_current]->Draw)
+ continue;
+ CL_KeepaliveMessage(true);
+ if (cl.model_name[cl.downloadmodel_current][0] != '*' && strcmp(cl.model_name[cl.downloadmodel_current], "null") && !FS_FileExists(cl.model_name[cl.downloadmodel_current]))
+ {
+ if (cl.downloadmodel_current == 1)
+ Con_Printf("Map %s not found\n", cl.model_name[cl.downloadmodel_current]);
+ else
+ Con_Printf("Model %s not found\n", cl.model_name[cl.downloadmodel_current]);
+ // regarding the * check: don't try to download submodels
+ if (cl_serverextension_download.integer && cls.netcon && cl.model_name[cl.downloadmodel_current][0] != '*' && !sv.active)
+ {
+ Cmd_ForwardStringToServer(va("download %s", cl.model_name[cl.downloadmodel_current]));
+ // we'll try loading again when the download finishes
+ return;
+ }
+ }
+
+ if(cl.downloadmodel_current == 1)
+ {
+ // they'll be soon loaded, but make sure we apply freshly downloaded shaders from a curled pk3
+ Mod_FreeQ3Shaders();
+ }
+
+ cl.model_precache[cl.downloadmodel_current] = Mod_ForName(cl.model_name[cl.downloadmodel_current], false, false, cl.model_name[cl.downloadmodel_current][0] == '*' ? cl.model_name[1] : NULL);
+ if (cl.downloadmodel_current == 1)
+ {
+ // we now have the worldmodel so we can set up the game world
+ // or maybe we do not have it (cl_serverextension_download 0)
+ // then we need to continue loading ANYWAY!
+ CL_SetupWorldModel();
+ if (!cl.loadfinished && cl_joinbeforedownloadsfinish.integer)
+ {
+ cl.loadfinished = true;
+ // now issue the spawn to move on to signon 2 like normal
+ if (cls.netcon)
+ Cmd_ForwardStringToServer("prespawn");
+ }
+ }
+ }
+
+ // finished loading models
+ }
+
+ if (cl.downloadsound_current < cl.loadsound_total)
+ {
+ // loading sounds
+
+ for (;cl.downloadsound_current < cl.loadsound_total;cl.downloadsound_current++)
+ {
+ char soundname[MAX_QPATH];
+ if (aborteddownload)
+ {
+ aborteddownload = false;
+ continue;
+ }
+ if (cl.sound_precache[cl.downloadsound_current] && S_IsSoundPrecached(cl.sound_precache[cl.downloadsound_current]))
+ continue;
+ CL_KeepaliveMessage(true);
+ dpsnprintf(soundname, sizeof(soundname), "sound/%s", cl.sound_name[cl.downloadsound_current]);
+ if (!FS_FileExists(soundname) && !FS_FileExists(cl.sound_name[cl.downloadsound_current]))
+ {
+ Con_Printf("Sound %s not found\n", soundname);
+ if (cl_serverextension_download.integer && cls.netcon && !sv.active)
+ {
+ Cmd_ForwardStringToServer(va("download %s", soundname));
+ // we'll try loading again when the download finishes
+ return;
+ }
+ }
+ cl.sound_precache[cl.downloadsound_current] = S_PrecacheSound(cl.sound_name[cl.downloadsound_current], false, true);
+ }
+
+ // 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)
+ Cmd_ForwardStringToServer("prespawn");
+ }
+}
+
+void CL_BeginDownloads_f(void)
+{
+ // 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);
+}
+
+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 = 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 || gamemode == GAME_NEXUIZ || gamemode == GAME_XONOTIC || !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);
+ }
+ }
+ }
+ 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"))
+ 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;
+}
+
+void CL_ParseDownload(void)
+{
+ int i, start, size;
+ static unsigned char data[NET_MAXMESSAGE];
+ start = MSG_ReadLong();
+ size = (unsigned short)MSG_ReadShort();
+
+ // 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(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;
+}
+
+void CL_DownloadBegin_f(void)
+{
+ int size = atoi(Cmd_Argv(1));
+
+ if (size < 0 || size > 1<<30 || FS_CheckNastyPath(Cmd_Argv(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(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() >= 4)
+ {
+ if(!strcmp(Cmd_Argv(3), "deflate"))
+ cls.qw_download_deflate = true;
+ // check further encodings here
+ }
+
+ Cmd_ForwardStringToServer("sv_startdownload");
+}
+
+void CL_StopDownload_f(void)
+{
+ Curl_CancelAll();
+ if (cls.qw_downloadname[0])
+ {
+ Con_Printf("Download of %s aborted\n", cls.qw_downloadname);
+ CL_StopDownload(0, 0);
+ }
+ CL_BeginDownloads(true);
+}
+
+void CL_DownloadFinished_f(void)
+{
+ if (Cmd_Argc() < 3)
+ {
+ Con_Printf("Malformed cl_downloadfinished command\n");
+ return;
+ }
+ CL_StopDownload(atoi(Cmd_Argv(1)), atoi(Cmd_Argv(2)));
+ CL_BeginDownloads(false);
+}
+
+static void CL_SendPlayerInfo(void)
+{
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va("name \"%s\"", cl_name.string));
+
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va("color %i %i", cl_color.integer >> 4, cl_color.integer & 15));
+
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va("rate %i", cl_rate.integer));
+
+ if (cl_pmodel.integer)
+ {
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va("pmodel %i", cl_pmodel.integer));
+ }
+ if (*cl_playermodel.string)
+ {
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va("playermodel %s", cl_playermodel.string));
+ }
+ if (*cl_playerskin.string)
+ {
+ MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ MSG_WriteString (&cls.netcon->message, va("playerskin %s", cl_playerskin.string));
+ }
+}
+