// PROTOCOL_DARKPLACES7 = 71 bytes per packet
}
+ if (cls.protocol != PROTOCOL_QUAKEWORLD)
+ {
+ // acknowledge any recently received data blocks
+ for (i = 0;i < CL_MAX_DOWNLOADACKS && (cls.dp_downloadack[i].start || cls.dp_downloadack[i].size);i++)
+ {
+ MSG_WriteByte(&buf, clc_ackdownloaddata);
+ MSG_WriteLong(&buf, cls.dp_downloadack[i].start);
+ MSG_WriteShort(&buf, cls.dp_downloadack[i].size);
+ cls.dp_downloadack[i].start = 0;
+ cls.dp_downloadack[i].size = 0;
+ }
+ }
+
// send the reliable message (forwarded commands) if there is one
NetConn_SendUnreliableMessage(cls.netcon, &buf, cls.protocol);
cl.worldmodel = NULL;
+ CL_Parse_ErrorCleanUp();
+
if (cls.demoplayback)
CL_StopPlayback();
else if (cls.netcon)
for (i = 0, e = cl.static_entities;i < cl.num_static_entities && r_refdef.numentities < r_refdef.maxentities;i++, e++)
{
e->render.flags = 0;
+ // if the model was not loaded when the static entity was created we
+ // need to re-fetch the model pointer
+ e->render.model = cl.model_precache[e->state_baseline.modelindex];
// transparent stuff can't be lit during the opaque stage
if (e->render.effects & (EF_ADDITIVE | EF_NODEPTHTEST) || e->render.alpha < 1)
e->render.flags |= RENDER_TRANSPARENT;
"", // 47
"", // 48
"", // 49
- "svc_unusedlh1", // 50 //
+ "svc_downloaddata", // 50 // [int] start [short] size [variable length] data
"svc_updatestatubyte", // 51 // [byte] stat [byte] value
"svc_effect", // 52 // [vector] org [byte] modelindex [byte] startframe [byte] framecount [byte] framerate
"svc_effect2", // 53 // [vector] org [short] modelindex [short] startframe [byte] framecount [byte] framerate
cvar_t cl_sound_ric2 = {0, "cl_sound_ric2", "weapons/ric2.wav", "sound to play with 5% chance during TE_SPIKE/TE_SUPERSPIKE (empty cvar disables sound)"};
cvar_t cl_sound_ric3 = {0, "cl_sound_ric3", "weapons/ric3.wav", "sound to play with 10% chance during TE_SPIKE/TE_SUPERSPIKE (empty cvar disables sound)"};
cvar_t cl_sound_r_exp3 = {0, "cl_sound_r_exp3", "weapons/r_exp3.wav", "sound to play during TE_EXPLOSION and related effects (empty cvar disables sound)"};
+cvar_t cl_serverextension_download = {0, "cl_serverextension_download", "0", "indicates whether the server supports the download command"};
+cvar_t cl_joinbeforedownloadsfinish = {0, "cl_joinbeforedownloadsfinish", "1", "if non-zero the game will begin after the map is loaded before other downloads finish"};
static qboolean QW_CL_CheckOrDownloadFile(const char *filename);
static void QW_CL_RequestNextDownload(void);
*/
static unsigned char olddata[NET_MAXMESSAGE];
-void CL_KeepaliveMessage (void)
+void CL_KeepaliveMessage (qboolean readmessages)
{
float time;
static double nextmsg = -1;
if (sv.active || !cls.netcon || cls.protocol == PROTOCOL_QUAKEWORLD)
return;
-// read messages from server, should just be nops
- oldreadcount = msg_readcount;
- oldbadread = msg_badread;
- old = net_message;
- memcpy(olddata, net_message.data, net_message.cursize);
-
- NetConn_ClientFrame();
-
- msg_readcount = oldreadcount;
- msg_badread = oldbadread;
- net_message = old;
- memcpy(net_message.data, olddata, net_message.cursize);
+ if (readmessages)
+ {
+ // read messages from server, should just be nops
+ oldreadcount = msg_readcount;
+ oldbadread = msg_badread;
+ old = net_message;
+ memcpy(olddata, net_message.data, net_message.cursize);
+
+ NetConn_ClientFrame();
+
+ msg_readcount = oldreadcount;
+ msg_badread = oldbadread;
+ net_message = old;
+ memcpy(net_message.data, olddata, net_message.cursize);
+ }
if (cls.netcon && (time = Sys_DoubleTime()) >= nextmsg)
{
Mem_Free(cls.qw_downloadmemory);
cls.qw_downloadmemory = NULL;
}
+
+ // done loading
+ cl.loadfinished = true;
break;
case dl_sound:
if (cls.qw_downloadnumber == 0)
// read the fragment out of the packet
MSG_ReadBytes(size, cls.qw_downloadmemory + cls.qw_downloadmemorycursize);
cls.qw_downloadmemorycursize += size;
+ cls.qw_downloadspeedcount += size;
cls.qw_downloadpercent = percent;
cl.activeweapon = cl.stats[STAT_ACTIVEWEAPON];
}
+void CL_BeginDownloads(qboolean aborteddownload)
+{
+ // quakeworld works differently
+ if (cls.protocol == PROTOCOL_QUAKEWORLD)
+ return;
+
+ // TODO: this would be a good place to do curl downloads
+
+ if (cl.loadmodel_current < cl.loadmodel_total)
+ {
+ // loading models
+
+ for (;cl.loadmodel_current < cl.loadmodel_total;cl.loadmodel_current++)
+ {
+ if (cl.model_precache[cl.loadmodel_current] && cl.model_precache[cl.loadmodel_current]->Draw)
+ continue;
+ if (cls.signon < SIGNONS)
+ CL_KeepaliveMessage(true);
+ cl.model_precache[cl.loadmodel_current] = Mod_ForName(cl.model_name[cl.loadmodel_current], false, false, cl.loadmodel_current == 1);
+ 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
+ cl.entities[0].render.model = cl.worldmodel = cl.model_precache[1];
+ CL_UpdateRenderEntity(&cl.entities[0].render);
+ R_Modules_NewMap();
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+ if (!cl.loadfinished && cl_joinbeforedownloadsfinish.integer)
+ {
+ cl.loadfinished = true;
+ // now issue the spawn to move on to signon 3 like normal
+ if (cls.netcon)
+ Cmd_ForwardStringToServer("spawn");
+ }
+ }
+ }
+
+ // finished loading models
+ }
+
+ if (cl.loadsound_current < cl.loadsound_total)
+ {
+ // loading sounds
+
+ for (;cl.loadsound_current < cl.loadsound_total;cl.loadsound_current++)
+ {
+ if (cl.sound_precache[cl.loadsound_current] && S_IsSoundPrecached(cl.sound_precache[cl.loadsound_current]))
+ continue;
+ if (cls.signon < SIGNONS)
+ CL_KeepaliveMessage(true);
+ // Don't lock the sfx here, S_ServerSounds already did that
+ cl.sound_precache[cl.loadsound_current] = S_PrecacheSound(cl.sound_name[cl.loadsound_current], false, false);
+ }
+
+ // finished loading sounds
+ }
+
+ // 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
+ cl.entities[0].render.model = cl.worldmodel = cl.model_precache[1];
+ CL_UpdateRenderEntity(&cl.entities[0].render);
+ R_Modules_NewMap();
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+ if (!cl.loadfinished && cl_joinbeforedownloadsfinish.integer)
+ {
+ cl.loadfinished = true;
+ // now issue the spawn to move on to signon 3 like normal
+ if (cls.netcon)
+ Cmd_ForwardStringToServer("spawn");
+ }
+ }
+ aborteddownload = false;
+ continue;
+ }
+ if (cl.model_precache[cl.downloadmodel_current] && cl.model_precache[cl.downloadmodel_current]->Draw)
+ continue;
+ if (cls.signon < SIGNONS)
+ CL_KeepaliveMessage(true);
+ if (!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] != '*')
+ {
+ Cmd_ForwardStringToServer(va("download %s", cl.model_name[cl.downloadmodel_current]));
+ // we'll try loading again when the download finishes
+ return;
+ }
+ }
+ cl.model_precache[cl.downloadmodel_current] = Mod_ForName(cl.model_name[cl.downloadmodel_current], false, false, cl.downloadmodel_current == 1);
+ if (cl.downloadmodel_current == 1)
+ {
+ // we now have the worldmodel so we can set up the game world
+ cl.entities[0].render.model = cl.worldmodel = cl.model_precache[1];
+ CL_UpdateRenderEntity(&cl.entities[0].render);
+ R_Modules_NewMap();
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+ if (!cl.loadfinished && cl_joinbeforedownloadsfinish.integer)
+ {
+ cl.loadfinished = true;
+ // now issue the spawn to move on to signon 3 like normal
+ if (cls.netcon)
+ Cmd_ForwardStringToServer("spawn");
+ }
+ }
+ }
+
+ // 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;
+ if (cls.signon < SIGNONS)
+ 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)
+ {
+ Cmd_ForwardStringToServer(va("download %s", soundname));
+ // we'll try loading again when the download finishes
+ return;
+ }
+ }
+ // Don't lock the sfx here, S_ServerSounds already did that
+ cl.sound_precache[cl.downloadsound_current] = S_PrecacheSound(cl.sound_name[cl.downloadsound_current], false, false);
+ }
+
+ // finished loading sounds
+ }
+
+ if (!cl.loadfinished)
+ {
+ cl.loadfinished = true;
+
+ // check memory integrity
+ Mem_CheckSentinelsGlobal();
+
+ // now issue the spawn to move on to signon 3 like normal
+ if (cls.netcon)
+ Cmd_ForwardStringToServer("spawn");
+ }
+}
+
+void CL_BeginDownloads_f(void)
+{
+ CL_BeginDownloads(false);
+}
+
+extern void FS_Rescan_f(void);
+void CL_StopDownload(int size, int crc)
+{
+ if (cls.qw_downloadmemory && cls.qw_downloadmemorycursize == size && CRC_Block(cls.qw_downloadmemory, size) == crc)
+ {
+ // finished file
+ // save to disk only if we don't already have it
+ // (this is mainly for playing back demos)
+ if (!FS_FileExists(cls.qw_downloadname))
+ {
+ const char *extension;
+
+ 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_f();
+ }
+ }
+
+ 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;
+ unsigned char data[65536];
+ 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 = Mem_Alloc(cls.permanentmempool, cls.qw_downloadmemorymaxsize);
+ cls.qw_downloadnumber++;
+
+ Cmd_ForwardStringToServer("sv_startdownload");
+}
+
+void CL_StopDownload_f(void)
+{
+ 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);
+}
+
/*
=====================
CL_SignonReply
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:
MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
MSG_WriteString (&cls.netcon->message, va("rate %i", cl_rate.integer));
- MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
- MSG_WriteString (&cls.netcon->message, "spawn");
+ // LordHavoc: changed to begin a loading stage and issue this when done
+ //MSG_WriteByte (&cls.netcon->message, clc_stringcmd);
+ //MSG_WriteString (&cls.netcon->message, "spawn");
+
+ // execute cl_begindownloads next frame after this message is sent
+ // (so that the server can see the player name while downloading)
+ Cbuf_AddText("\ncl_begindownloads\n");
}
break;
// check memory integrity
Mem_CheckSentinelsGlobal();
+ // clear cl_serverextension cvars
+ Cvar_SetValueQuick(&cl_serverextension_download, 0);
+
//
// wipe the client_state_t struct
//
MSG_WriteString(&cls.netcon->message, va("soundlist %i %i", cl.qw_servercount, 0));
}
+ cl.loadfinished = false;
+
cls.state = ca_connected;
cls.signon = 1;
cl.sfx_r_exp3 = S_PrecacheSound(cl_sound_r_exp3.string, false, true);
// now we try to load everything that is new
-
- // world model
- CL_KeepaliveMessage ();
- cl.model_precache[1] = Mod_ForName(cl.model_name[1], false, false, true);
- if (cl.model_precache[1]->Draw == NULL)
- Con_Printf("Map %s not found\n", cl.model_name[1]);
-
- // normal models
- for (i=2 ; i<nummodels ; i++)
- {
- CL_KeepaliveMessage();
- if ((cl.model_precache[i] = Mod_ForName(cl.model_name[i], false, false, false))->Draw == NULL)
- Con_Printf("Model %s not found\n", cl.model_name[i]);
- }
-
- // sounds
- for (i=1 ; i<numsounds ; i++)
- {
- CL_KeepaliveMessage();
- // Don't lock the sfx here, S_ServerSounds already did that
- cl.sound_precache[i] = S_PrecacheSound (cl.sound_name[i], true, false);
- }
-
- // we now have the worldmodel so we can set up the game world
- cl.entities[0].render.model = cl.worldmodel = cl.model_precache[1];
- CL_UpdateRenderEntity(&cl.entities[0].render);
- R_Modules_NewMap();
+ 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.loadfinished = false;
}
// check memory integrity
Matrix4x4_CreateFromQuakeEntity(&ent->render.matrix, ent->state_baseline.origin[0], ent->state_baseline.origin[1], ent->state_baseline.origin[2], ent->state_baseline.angles[0], ent->state_baseline.angles[1], ent->state_baseline.angles[2], 1);
CL_UpdateRenderEntity(&ent->render);
- // This is definitely cheating...
- if (ent->render.model == NULL)
- cl.num_static_entities--;
+ // This is definitely a cheesy way to conserve resources...
+ //if (ent->render.model == NULL)
+ // cl.num_static_entities--;
}
/*
cl.last_received_message = realtime;
+ if (cls.netcon && cls.signon < SIGNONS)
+ CL_KeepaliveMessage(false);
+
//
// if recording demos, copy the message out
//
cl.qw_num_nails = 0;
// fade weapon view kick
- cl.qw_weaponkick = min(cl.qw_weaponkick + 10 * (cl.time - cl.oldtime), 0);
+ cl.qw_weaponkick = min(cl.qw_weaponkick + 10 * bound(0, cl.time - cl.oldtime, 0.1), 0);
while (1)
{
case svc_csqcentities:
CSQC_ReadEntities();
break;
+ case svc_downloaddata:
+ CL_ParseDownload();
+ break;
}
}
}
- CL_UpdateItemsAndWeapon();
+ if (cls.signon == SIGNONS)
+ CL_UpdateItemsAndWeapon();
EntityFrameQuake_ISeeDeadEntities();
void CL_Parse_ErrorCleanUp(void)
{
- if (cls.qw_downloadmemory)
- {
- Mem_Free(cls.qw_downloadmemory);
- cls.qw_downloadmemory = NULL;
- }
- cls.qw_downloadpercent = 0;
+ CL_StopDownload(0, 0);
QW_CL_StopUpload();
}
Cvar_RegisterVariable(&cl_sound_ric3);
Cvar_RegisterVariable(&cl_sound_r_exp3);
+ Cvar_RegisterVariable(&cl_joinbeforedownloadsfinish);
+
+ // server extension cvars set by commands issued from the server during connect
+ Cvar_RegisterVariable(&cl_serverextension_download);
+
Cmd_AddCommand("nextul", QW_CL_NextUpload, "sends next fragment of current upload buffer (screenshot for example)");
Cmd_AddCommand("stopul", QW_CL_StopUpload, "aborts current upload (screenshot for example)");
Cmd_AddCommand("skins", QW_CL_Skins_f, "downloads missing qw skins from server");
Cmd_AddCommand("changing", QW_CL_Changing_f, "sent by qw servers to tell client to wait for level change");
+ Cmd_AddCommand("cl_begindownloads", CL_BeginDownloads_f, "used internally by darkplaces client while connecting (causes loading of models and sounds or triggers downloads for missing ones)");
+ Cmd_AddCommand("cl_downloadbegin", CL_DownloadBegin_f, "(networking) informs client of download file information, client replies with sv_startsoundload to begin the transfer");
+ Cmd_AddCommand("stopdownload", CL_StopDownload_f, "terminates a download");
+ Cmd_AddCommand("cl_downloadfinished", CL_DownloadFinished_f, "signals that a download has finished and provides the client with file size and crc to check its integrity");
}
void CL_Parse_Shutdown(void)
float size = 8;
char temp[256];
if (!cls.qw_downloadname[0])
+ {
+ cls.qw_downloadspeedrate = 0;
+ cls.qw_downloadspeedtime = realtime;
+ cls.qw_downloadspeedcount = 0;
return 0;
- dpsnprintf(temp, sizeof(temp), "Downloading %s ... %3i%%\n", cls.qw_downloadname, cls.qw_downloadpercent);
+ }
+ if (realtime >= cls.qw_downloadspeedtime + 1)
+ {
+ cls.qw_downloadspeedrate = cls.qw_downloadspeedcount;
+ cls.qw_downloadspeedtime = realtime;
+ cls.qw_downloadspeedcount = 0;
+ }
+ if (cls.protocol == PROTOCOL_QUAKEWORLD)
+ dpsnprintf(temp, sizeof(temp), "Downloading %s %3i%% (%i) at %i bytes/s\n", cls.qw_downloadname, cls.qw_downloadpercent, cls.qw_downloadmemorycursize, cls.qw_downloadspeedrate);
+ else
+ dpsnprintf(temp, sizeof(temp), "Downloading %s %3i%% (%i/%i) at %i bytes/s\n", cls.qw_downloadname, cls.qw_downloadpercent, cls.qw_downloadmemorycursize, cls.qw_downloadmemorymaxsize, cls.qw_downloadspeedrate);
len = (int)strlen(temp);
x = (vid_conwidth.integer - len*size) / 2;
y = vid_conheight.integer - size - offset;
}
capturevideostate_t;
+#define CL_MAX_DOWNLOADACKS 4
+
+typedef struct cl_downloadack_s
+{
+ int start, size;
+}
+cl_downloadack_t;
+
//
// the client_static_t structure is persistent through an arbitrary number
// of server connections
// network connection
netconn_t *netcon;
+ // download information
+ // (note: qw_download variables are also used)
+ cl_downloadack_t dp_downloadack[CL_MAX_DOWNLOADACKS];
+
// quakeworld stuff below
// value of "qport" cvar at time of connection
int qw_downloadnumber;
int qw_downloadpercent;
qw_downloadtype_t qw_downloadtype;
+ // transfer rate display
+ double qw_downloadspeedtime;
+ int qw_downloadspeedcount;
+ int qw_downloadspeedrate;
// current file upload buffer (for uploading screenshots to server)
unsigned char *qw_uploaddata;
int free_particle;
+ // cl_serverextension_download feature
+ int loadmodel_current;
+ int downloadmodel_current;
+ int loadmodel_total;
+ int loadsound_current;
+ int downloadsound_current;
+ int loadsound_total;
+ qboolean loadfinished;
+
// quakeworld stuff
// local copy of the server infostring
#include "quakedef.h"
-cvar_t registered = {0, "registered","0", "indicates if this is running registered quake (whether gfx/qpop.lmp was found)"};
+cvar_t registered = {0, "registered","0", "indicates if this is running registered quake (whether gfx/pop.lmp was found)"};
cvar_t cmdline = {0, "cmdline","0", "contains commandline the engine was launched with"};
char com_token[MAX_INPUTLINE];
}
+/*
+============
+FS_FileWithoutPath
+============
+*/
+const char *FS_FileWithoutPath (const char *in)
+{
+ const char *separator, *backslash, *colon;
+
+ separator = strrchr(in, '/');
+ backslash = strrchr(in, '\\');
+ if (!separator || separator < backslash)
+ separator = backslash;
+ colon = strrchr(in, ':');
+ if (!separator || separator < colon)
+ separator = colon;
+ return separator ? separator + 1 : in;
+}
+
+
/*
================
FS_ClearSearchPath
else
return 0;
}
+
+/*
+====================
+FS_IsRegisteredQuakePack
+
+Look for a proof of purchase file file in the requested package
+
+If it is found, this file should NOT be downloaded.
+====================
+*/
+qboolean FS_IsRegisteredQuakePack(const char *name)
+{
+ searchpath_t *search;
+ pack_t *pak;
+
+ // search through the path, one element at a time
+ for (search = fs_searchpaths;search;search = search->next)
+ {
+ if (search->pack && !strcasecmp(FS_FileWithoutPath(search->filename), name))
+ {
+ int (*strcmp_funct) (const char* str1, const char* str2);
+ int left, right, middle;
+
+ pak = search->pack;
+ strcmp_funct = pak->ignorecase ? strcasecmp : strcmp;
+
+ // Look for the file (binary search)
+ left = 0;
+ right = pak->numfiles - 1;
+ while (left <= right)
+ {
+ int diff;
+
+ middle = (left + right) / 2;
+ diff = !strcmp_funct (pak->files[middle].name, "gfx/pop.lmp");
+
+ // Found it
+ if (!diff)
+ return true;
+
+ // If we're too far in the list
+ if (diff > 0)
+ right = middle - 1;
+ else
+ left = middle + 1;
+ }
+
+ // we found the requested pack but it is not registered quake
+ return false;
+ }
+ }
+
+ return false;
+}
fs_offset_t FS_Tell (qfile_t* file);
fs_offset_t FS_FileSize (qfile_t* file);
void FS_Purge (qfile_t* file);
+const char *FS_FileWithoutPath (const char *in);
const char *FS_FileExtension (const char *in);
int FS_CheckNastyPath (const char *path, qboolean isgamedir);
qboolean FS_ChangeGameDir(const char *string);
+qboolean FS_IsRegisteredQuakePack(const char *name);
typedef struct fssearch_s
{
prog->globals.server->self = saveSelf;
}
+ // if a download is active, close it
+ if (host_client->download_file)
+ {
+ Con_DPrintf("Download of %s aborted when %s dropped\n", host_client->download_name, host_client->name);
+ FS_Close(host_client->download_file);
+ host_client->download_file = NULL;
+ host_client->download_name[0] = 0;
+ host_client->download_expectedposition = 0;
+ host_client->download_started = false;
+ }
+
// remove leaving player from scoreboard
//host_client->edict->fields.server->netname = PRVM_SetEngineString(host_client->name);
//if ((val = PRVM_GETEDICTFIELDVALUE(host_client->edict, eval_clientcolors)))
#define svc_skybox 37 // [string] skyname
// LordHavoc: my svc_ range, 50-59
-#define svc_unusedlh1 50 //
+#define svc_downloaddata 50 // [int] start [short] size
#define svc_updatestatubyte 51 // [byte] stat [byte] value
#define svc_effect 52 // [vector] org [byte] modelindex [byte] startframe [byte] framecount [byte] framerate
#define svc_effect2 53 // [vector] org [short] modelindex [short] startframe [byte] framecount [byte] framerate
// LordHavoc: my clc_ range, 50-59
#define clc_ackframe 50 // [int] framenumber
-#define clc_unusedlh1 51
+#define clc_ackdownloaddata 51 // [int] start [short] size (note: exact echo of latest values received in svc_downloaddata, packet-loss handling is in the server)
#define clc_unusedlh2 52
#define clc_unusedlh3 53
#define clc_unusedlh4 54
entityframe_database_t *entitydatabase;
entityframe4_database_t *entitydatabase4;
entityframe5_database_t *entitydatabase5;
+
+ // information on an active download if any
+ qfile_t *download_file;
+ int download_expectedposition; // next position the client should ack
+ qboolean download_started;
+ char download_name[MAX_QPATH];
} client_t;
sfx = S_FindName (serversound[i]);
if (sfx != NULL)
{
+ // clear the FILEMISSING flag so that S_LoadSound will try again on a
+ // previously missing file
+ sfx->flags &= ~ SFXFLAG_FILEMISSING;
S_LockSfx (sfx);
sfx->flags |= SFXFLAG_SERVERSOUND;
}
sfx = S_FindName (name);
+ if (sfx == NULL)
+ return NULL;
+
// clear the FILEMISSING flag so that S_LoadSound will try again on a
// previously missing file
sfx->flags &= ~ SFXFLAG_FILEMISSING;
- if (sfx == NULL)
- return NULL;
-
if (lock)
S_LockSfx (sfx);
cvar_t sv_protocolname = {0, "sv_protocolname", "DP7", "selects network protocol to host for (values include QUAKE, QUAKEDP, NEHAHRAMOVIE, DP1 and up)"};
cvar_t sv_ratelimitlocalplayer = {0, "sv_ratelimitlocalplayer", "0", "whether to apply rate limiting to the local player in a listen server (only useful for testing)"};
cvar_t sv_maxrate = {CVAR_SAVE | CVAR_NOTIFY, "sv_maxrate", "10000", "upper limit on client rate cvar, should reflect your network connection quality"};
+cvar_t sv_allowdownloads = {0, "sv_allowdownloads", "1", "whether to allow clients to download files from the server (does not affect http downloads)"};
+cvar_t sv_allowdownloads_inarchive = {0, "sv_allowdownloads_inarchive", "0", "whether to allow downloads from archives (pak/pk3)"};
+cvar_t sv_allowdownloads_archive = {0, "sv_allowdownloads_archive", "0", "whether to allow downloads of archives (pak/pk3)"};
extern cvar_t sv_random_seed;
extern void SV_Phys_Init (void);
extern void SV_World_Init (void);
static void SV_SaveEntFile_f(void);
+static void SV_StartDownload_f(void);
+static void SV_Download_f(void);
/*
===============
Cvar_RegisterVariable (&csqc_progcrc);
Cmd_AddCommand("sv_saveentfile", SV_SaveEntFile_f, "save map entities to .ent file (to allow external editing)");
+ Cmd_AddCommand_WithClientCommand("sv_startdownload", NULL, SV_StartDownload_f, "begins sending a file to the client (network protocol use only)");
+ Cmd_AddCommand_WithClientCommand("download", NULL, SV_Download_f, "downloads a specified file from the server");
Cvar_RegisterVariable (&sv_maxvelocity);
Cvar_RegisterVariable (&sv_gravity);
Cvar_RegisterVariable (&sv_friction);
Cvar_RegisterVariable (&sv_protocolname);
Cvar_RegisterVariable (&sv_ratelimitlocalplayer);
Cvar_RegisterVariable (&sv_maxrate);
+ Cvar_RegisterVariable (&sv_allowdownloads);
+ Cvar_RegisterVariable (&sv_allowdownloads_inarchive);
+ Cvar_RegisterVariable (&sv_allowdownloads_archive);
Cvar_RegisterVariable (&sv_progs);
SV_VM_Init();
}
}
+ if (sv_allowdownloads.integer)
+ {
+ MSG_WriteByte (&client->netconnection->message, svc_stufftext);
+ MSG_WriteString (&client->netconnection->message, "cl_serverextension_download 1");
+ }
+
MSG_WriteByte (&client->netconnection->message, svc_serverinfo);
MSG_WriteLong (&client->netconnection->message, Protocol_NumberForEnum(sv.protocol));
MSG_WriteByte (&client->netconnection->message, svs.maxclients);
static unsigned char sv_sendclientdatagram_buf[NET_MAXMESSAGE]; // FIXME?
void SV_SendClientDatagram (client_t *client)
{
- int rate, maxrate, maxsize, maxsize2;
+ int rate, maxrate, maxsize, maxsize2, downloadsize;
sizebuf_t msg;
int stats[MAX_CL_STATS];
maxsize2 = 1400;
}
+ // while downloading, limit entity updates to half the packet
+ // (any leftover space will be used for downloading)
+ if (host_client->download_file)
+ maxsize /= 2;
+
msg.data = sv_sendclientdatagram_buf;
msg.maxsize = maxsize;
msg.cursize = 0;
{
// the player isn't totally in the game yet
// send small keepalive messages if too much time has passed
+ msg.maxsize = maxsize2;
client->keepalivetime = realtime + 5;
MSG_WriteChar (&msg, svc_nop);
}
+ msg.maxsize = maxsize2;
+
+ // if a download is active, see if there is room to fit some download data
+ // in this packet
+ downloadsize = maxsize * 2 - msg.cursize - 7;
+ if (host_client->download_file && host_client->download_started && downloadsize > 0)
+ {
+ fs_offset_t downloadstart;
+ unsigned char data[1400];
+ downloadstart = FS_Tell(host_client->download_file);
+ downloadsize = min(downloadsize, (int)sizeof(data));
+ downloadsize = FS_Read(host_client->download_file, data, downloadsize);
+ // note this sends empty messages if at the end of the file, which is
+ // necessary to keep the packet loss logic working
+ // (the last blocks may be lost and need to be re-sent, and that will
+ // only occur if the client acks the empty end messages, revealing
+ // a gap in the download progress, causing the last blocks to be
+ // sent again)
+ MSG_WriteChar (&msg, svc_downloaddata);
+ MSG_WriteLong (&msg, downloadstart);
+ MSG_WriteShort (&msg, downloadsize);
+ if (downloadsize > 0)
+ SZ_Write (&msg, data, downloadsize);
+ }
+
// send the datagram
NetConn_SendUnreliableMessage (client->netconnection, &msg, sv.protocol);
}
SV_CleanupEnts();
}
+void SV_StartDownload_f(void)
+{
+ if (host_client->download_file)
+ host_client->download_started = true;
+}
+
+void SV_Download_f(void)
+{
+ const char *whichpack, *whichpack2, *extension;
+
+ if (Cmd_Argc() != 2)
+ {
+ SV_ClientPrintf("usage: download <filename>\n");
+ return;
+ }
+
+ if (FS_CheckNastyPath(Cmd_Argv(1), false))
+ {
+ SV_ClientPrintf("Download rejected: nasty filename \"%s\"\n", Cmd_Argv(1));
+ return;
+ }
+
+ if (host_client->download_file)
+ {
+ // at this point we'll assume the previous download should be aborted
+ Con_DPrintf("Download of %s aborted by %s starting a new download\n", host_client->download_name, host_client->name);
+ Host_ClientCommands("\nstopdownload\n");
+
+ // close the file and reset variables
+ FS_Close(host_client->download_file);
+ host_client->download_file = NULL;
+ host_client->download_name[0] = 0;
+ host_client->download_expectedposition = 0;
+ host_client->download_started = false;
+ }
+
+ if (!sv_allowdownloads.integer)
+ {
+ SV_ClientPrintf("Downloads are disabled on this server\n");
+ Host_ClientCommands("\nstopdownload\n");
+ return;
+ }
+
+ strlcpy(host_client->download_name, Cmd_Argv(1), sizeof(host_client->download_name));
+
+ // host_client is asking to download a specified file
+ if (developer.integer >= 100)
+ Con_Printf("Download request for %s by %s\n", host_client->download_name, host_client->name);
+
+ if (!FS_FileExists(host_client->download_name))
+ {
+ SV_ClientPrintf("Download rejected: server does not have the file \"%s\"\nYou may need to separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name);
+ Host_ClientCommands("\nstopdownload\n");
+ return;
+ }
+
+ // check if the user is trying to download part of registered Quake(r)
+ whichpack = FS_WhichPack(host_client->download_name);
+ whichpack2 = FS_WhichPack("gfx/pop.lmp");
+ if ((whichpack && whichpack2 && !strcasecmp(whichpack, whichpack2)) || FS_IsRegisteredQuakePack(host_client->download_name))
+ {
+ SV_ClientPrintf("Download rejected: file \"%s\" is part of registered Quake(r)\nYou must purchase Quake(r) from id Software or a retailer to get this file\nPlease go to http://www.idsoftware.com/games/quake/quake/index.php?game_section=buy\n", host_client->download_name);
+ Host_ClientCommands("\nstopdownload\n");
+ return;
+ }
+
+ // check if the server has forbidden archive downloads entirely
+ if (!sv_allowdownloads_inarchive.integer)
+ {
+ whichpack = FS_WhichPack(host_client->download_name);
+ if (whichpack)
+ {
+ SV_ClientPrintf("Download rejected: file \"%s\" is in an archive (\"%s\")\nYou must separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name, whichpack);
+ Host_ClientCommands("\nstopdownload\n");
+ return;
+ }
+ }
+
+ if (!sv_allowdownloads_archive.integer)
+ {
+ extension = FS_FileExtension(host_client->download_name);
+ if (!strcasecmp(extension, "pak") || !strcasecmp(extension, "pk3"))
+ {
+ SV_ClientPrintf("Download rejected: file \"%s\" is an archive\nYou must separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name);
+ Host_ClientCommands("\nstopdownload\n");
+ return;
+ }
+ }
+
+ host_client->download_file = FS_Open(host_client->download_name, "rb", true, false);
+ if (!host_client->download_file)
+ {
+ SV_ClientPrintf("Download rejected: server could not open the file \"%s\"\n", host_client->download_name);
+ Host_ClientCommands("\nstopdownload\n");
+ return;
+ }
+
+ if (FS_FileSize(host_client->download_file) > 1<<30)
+ {
+ SV_ClientPrintf("Download rejected: file \"%s\" is very large\n", host_client->download_name);
+ Host_ClientCommands("\nstopdownload\n");
+ FS_Close(host_client->download_file);
+ host_client->download_file = NULL;
+ return;
+ }
+
+ Con_DPrintf("Downloading %s to %s\n", host_client->download_name, host_client->name);
+
+ Host_ClientCommands("\ncl_downloadbegin %i %s\n", (int)FS_FileSize(host_client->download_file), host_client->download_name);
+
+ host_client->download_expectedposition = 0;
+ host_client->download_started = false;
+
+ // the rest of the download process is handled in SV_SendClientDatagram
+ // and other code dealing with svc_downloaddata and clc_ackdownloaddata
+ //
+ // no svc_downloaddata messages will be sent until sv_startdownload is
+ // sent by the client
+}
/*
==============================================================================
extern void SV_SendServerinfo(client_t *client);
void SV_ReadClientMessage(void)
{
- int cmd, num;
+ int cmd, num, start;
char *s;
//MSG_BeginReading ();
SV_DropClient (false);
break;
+ case clc_ackdownloaddata:
+ start = MSG_ReadLong();
+ num = MSG_ReadShort();
+ if (host_client->download_file && host_client->download_started)
+ {
+ if (host_client->download_expectedposition == start)
+ {
+ int size = (int)FS_FileSize(host_client->download_file);
+ // a data block was successfully received by the client,
+ // update the expected position on the next data block
+ host_client->download_expectedposition = start + num;
+ // if this was the last data block of the file, it's done
+ if (host_client->download_expectedposition >= FS_FileSize(host_client->download_file))
+ {
+ // tell the client that the download finished
+ // we need to calculate the crc now
+ //
+ // note: at this point the OS probably has the file
+ // entirely in memory, so this is a faster operation
+ // now than it was when the download started.
+ //
+ // it is also preferable to do this at the end of the
+ // download rather than the start because it reduces
+ // potential for Denial Of Service attacks against the
+ // server.
+ int crc;
+ unsigned char *temp;
+ FS_Seek(host_client->download_file, 0, SEEK_SET);
+ temp = Mem_Alloc(tempmempool, size);
+ FS_Read(host_client->download_file, temp, size);
+ crc = CRC_Block(temp, size);
+ Mem_Free(temp);
+ // calculated crc, send the file info to the client
+ // (so that it can verify the data)
+ Host_ClientCommands(va("\ncl_downloadfinished %i %i %s\n", size, crc, host_client->download_name));
+ Con_DPrintf("Download of %s by %s has finished\n", host_client->download_name, host_client->name);
+ FS_Close(host_client->download_file);
+ host_client->download_file = NULL;
+ host_client->download_name[0] = 0;
+ host_client->download_expectedposition = 0;
+ host_client->download_started = false;
+ }
+ }
+ else
+ {
+ // a data block was lost, reset to the expected position
+ // and resume sending from there
+ FS_Seek(host_client->download_file, host_client->download_expectedposition, SEEK_SET);
+ }
+ }
+ break;
+
case clc_ackframe:
if (msg_badread) Con_Printf("SV_ReadClientMessage: badread at %s:%i\n", __FILE__, __LINE__);
num = MSG_ReadLong();
0 bug darkplaces client: GAME_NEHAHRA: make sure cutscenes and movies work, got a report of seeing a black screen (NightFright)
0 bug darkplaces client: hipnotic: health is one character to the right on the sbar, covering up the key icons (M`Shacron)
0 bug darkplaces client: it has been reported that sometimes level changes on quakeworld servers don't load a map, this may be related to downloading? (Baker)
+0 bug darkplaces client: on crctf proquake servers the scoreboard does not contain exactly matching player names (READY is sometimes appended), the ping report and status parsing should ignore text after the player name
0 bug darkplaces client: svc_effect should post a warning and do nothing if given a framerate below 1 (Willis)
0 bug darkplaces console: commandline history won't scroll back past a blank line - the blank line should not be entered into history (Elric)
0 bug darkplaces console: when cursoring up and down through command history, shorter lines sometimes contain some text from the previous line
0 bug darkplaces loader: occasional crash due to memory corruption when doing "deathmatch 1;map start" during demo loop (Willis)
0 bug darkplaces loader: q3bsp deluxemap detection can fail on some files, thinking they have deluxemaps even though they don't? (jimmmy)
0 bug darkplaces loader: q3bsp lightgrid loading seems to be ignoring the "gridsize" key of worldspawn, but how?
+0 bug darkplaces loading: when gamedir (or -game) contains a directory which listdirectory() fails on, do a Host_Error with an appropriate message, rather than running with a non-existent directory
+0 bug darkplaces model loader: a q1 mdl file with a _1.tga but no _0.tga crashes at load (daemon)
0 bug darkplaces physics: GAME_TAOV: Vigil's movement isn't working properly, the qc uses MOVETYPE_STEP and clears FL_ONGROUND every frame and moves using velocity, this is causing a landing sound every frame and causing the player to slide down minor slopes very quickly, this did not occur in Quake, and seems that it must be related to a velocity_z check or FL_ONGROUND check in the MOVETYPE_STEP physics code (RenegadeC, xaGe)
0 bug darkplaces physics: in Prydon Gate the func_door2 entities are stuck in eachother, causing a continuous spew of warnings and causing one of them to be teleported slightly upward which looks bad (FrikaC)
0 bug darkplaces readme: commandline options are slightly out of date, update them (Baker)
0 bug darkplaces server: if sv_fixedframeratesingleplayer is 0 and cl_maxfps is something like 10, the server still runs every frame, consuming massive amounts of cpu and resulting in very small frametime values
0 bug darkplaces server: in X-Men: Ravages of Apocalypse the weapon part in x1m3 fails to follow the platform it is on, it is probably spawning inside the ceiling and for some reason not associating with the platform as its groundentity? (qwerasdf)
0 bug darkplaces server: in X-Men: Ravages of Apocalypse the weapon part in x2m4 falls out of the level, along with a few other items in the same secret (qwerasdf)
+0 bug darkplaces server: stats[TOTAL_MONSTERS] should be networked as a stat
0 bug darkplaces sound: remove playing sounds when their owner entity has been removed by network code, this would mean that Nexuiz could have rocket/electro noise again (Qantoursic)
0 bug darkplaces wgl client: during video mode setup, sometimes another application's window becomes permanently top most, not darkplaces' fullscreen window, why? (tZork)
0 bug darkplaces wgl client: hardware gamma is being retried every frame for unknown reasons, this is SEVERELY impacting framerates on win2k/xp (Jago)
0 bug dpmod: go through http://www.inside3d.com/qip/q1/bugsmap.htm and fix map bugs by using replacement .ent files (Lardarse)
0 bug dpmod: identify what could cause huge angles values (1187488512.0000) on a dog entity, may be related to anglemod, FacingIdeal, ai_run, or dog_run2 (Zombie13)
0 bug dpmod: impulse 102 isn't removing the bots
+0 bug dpmod: in dpmod_qcphysics_casings the two main particles have the same mass, realistically speaking the rear one should have more mass than the front one
0 bug dpmod: monsters don't properly chase you around corners, this is not an engine bug because they work fine in id1 but not dpmod, the knight at the start of e2m2 makes for good testing by shooting him and then hiding behind the crates (StrangerThanYou)
0 bug dpmod: monsters falling out of level?
0 bug dpmod: monsters shouldn't constantly do sightsounds on a slain player, it's annoying and silly
0 feature darkplaces client: add .loc file support and say macros
0 feature darkplaces client: add .mvd demo support
0 feature darkplaces client: add .qwd demo support
+0 feature darkplaces client: add BX_WAL_SUPPORT to extensions and document it, the feature has been in for a long time, also update wiki.quakesrc.org accordingly
+0 feature darkplaces client: add DP_GFX_EFFECTINFO_TXT to extensions and document it, the feature has been in for a long time, also update wiki.quakesrc.org accordingly
0 feature darkplaces client: add a cl_showspeed cvar to display a hud overlay of your current velocity, speed as length of velocity, and speed along forward vector (Spike)
0 feature darkplaces client: add a cvar to make the renderer use a different entity for pvs than for viewing, this might be useful for a third person camera that should only see what the player sees (Urre)
0 feature darkplaces client: add an alias to control whether the nexuiz logo.dpv splash video plays at startup and whether the menu opens, this way the config can change it before it happens, for instance to disable it, or to set up a similar thing in other games (green)
0 feature darkplaces renderer: add rtlight "avelocity" parameter to make lights that spin, useful with cubemaps (romi)
0 feature darkplaces renderer: make showfps display GL renderer string and CPU - figure out how to detect this from the OS
0 feature darkplaces renderer: save r_shadow_glsl* cvars (and possibly a few others) to config because they are useful user settings (SavageX)
+0 feature darkplaces renderer: support gl_picmip -1, -2, etc like Twilight does
0 feature darkplaces renderer: support tcgen in q3 shaders (ob3lisk)
0 feature darkplaces server: DP_SV_FINDPVS
0 feature darkplaces server: add .maxspeed field to control player movement speed in engine code, call it QW_SV_MAXSPEED (Carni)