Polished and documented the manifest support a little. Feels a bit more robust now.

Try to avoid certain prediction issues with positions truncated into solid areas, that appears to happen on q3bsp and thus might happen elsewhere too.

git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@4780 fc73d0e0-1445-4013-8a0c-d673dee63da5
This commit is contained in:
Spoike 2014-10-29 05:03:03 +00:00
parent 661d64056a
commit 3998821865
9 changed files with 325 additions and 136 deletions

View File

@ -947,7 +947,6 @@ void CL_BeginServerConnect(int port);
char *CL_TryingToConnect(void);
void CL_ExecInitialConfigs(char *defaultexec);
ftemanifest_t *CL_Manifest_Parse(vfsfile_t *file, const char *defaultsourceurl);
extern int cl_numvisedicts;
extern int cl_maxvisedicts;

View File

@ -1309,19 +1309,6 @@ TRACE(("dbg: R_ApplyRenderer: reloading ALL models\n"));
cl.model_precache[i] = Mod_ForName (cl.model_name[i], MLV_SILENT);
else
cl.model_precache[i] = Mod_FindName (Mod_FixName(cl.model_name[i], cl.model_name[1]));
if ((!cl.model_precache[i] || cl.model_precache[i]->type == mod_dummy) && i == 1)
{
Con_Printf ("\nThe required model file '%s' could not be found.\n\n"
, cl.model_name[i]);
Con_Printf ("You may need to download or purchase a client "
"pack in order to play on this server.\n\n");
CL_Disconnect ();
#ifdef VM_UI
UI_Reset();
#endif
return false;
}
}
for (i=0; i < MAX_VWEP_MODELS; i++)
@ -1341,22 +1328,10 @@ TRACE(("dbg: R_ApplyRenderer: reloading ALL models\n"));
cl.model_csqcprecache[i] = NULL;
TRACE(("dbg: R_ApplyRenderer: reloading csqc model %s\n", cl.model_csqcname[i]));
cl.model_csqcprecache[i] = Mod_ForName (Mod_FixName(cl.model_csqcname[i], cl.model_name[1]), MLV_SILENT);
if (!cl.model_csqcprecache[i])
{
Con_Printf ("\nThe required model file '%s' could not be found.\n\n"
, cl.model_csqcname[i]);
Con_Printf ("You may need to download or purchase a client "
"pack in order to play on this server.\n\n");
CL_Disconnect ();
#ifdef VM_UI
UI_Reset();
#endif
return false;
}
}
#endif
//fixme: worldmodel could be ssqc or csqc.
cl.worldmodel = cl.model_precache[1];
if (cl.worldmodel && cl.worldmodel->loadstate == MLS_LOADING)
@ -1365,6 +1340,9 @@ TRACE(("dbg: R_ApplyRenderer: reloading ALL models\n"));
TRACE(("dbg: R_ApplyRenderer: done the models\n"));
if (!cl.worldmodel || cl.worldmodel->loadstate != MLS_LOADED)
{
// Con_Printf ("\nThe required model file '%s' could not be found.\n\n", cl.model_name[i]);
// Con_Printf ("You may need to download or purchase a client pack in order to play on this server.\n\n");
CL_Disconnect ();
#ifdef VM_UI
UI_Reset();

View File

@ -339,8 +339,6 @@ void Skin_WorkerLoad(void *skinptr, void *data, size_t a, size_t b)
}
Skin_WorkerDone(skin, out, srcw, srch);
skin->loadstate = SKIN_LOADED;
}
/*
@ -529,8 +527,12 @@ void Skin_FlushAll(void)
{ //wipe the skin info
int i;
for (i=0 ; i<numskins ; i++)
{
if (skins[i].loadstate==SKIN_LOADING)
COM_WorkerPartialSync(&skins[i], &skins[i].loadstate, SKIN_LOADING);
if (skins[i].skindata)
BZ_Free(skins[i].skindata);
}
numskins = 0;
for (i = 0; i < MAX_CLIENTS; i++)
cl.players[i].lastskin = NULL;
@ -557,8 +559,12 @@ void Skin_Skins_f (void)
R_GAliasFlushSkinCache(false);
for (i=0 ; i<numskins ; i++)
{
if (skins[i].loadstate==SKIN_LOADING)
COM_WorkerPartialSync(&skins[i], &skins[i].loadstate, SKIN_LOADING);
if (skins[i].skindata)
BZ_Free(skins[i].skindata);
}
numskins = 0;
for (i = 0; i < MAX_CLIENTS; i++)
cl.players[i].lastskin = NULL;

View File

@ -114,12 +114,12 @@ cvar_t snd_mixerthread = CVARAD( "s_mixerthread", "1",
"snd_mixerthread", "When enabled sound mixing will be run on a separate thread. Currently supported only by directsound. Other drivers may unconditionally thread audio. Set to 0 only if you have issues.");
cvar_t snd_device = CVARAF( "s_device", "",
"snd_device", CVAR_ARCHIVE);
cvar_t snd_device_opts = CVARFD( "_s_device_opts", "", CVAR_NOSET, "The possible audio output devices, in \"value\" \"description\" pairs.");
cvar_t snd_device_opts = CVARFD( "_s_device_opts", "", CVAR_NOSET, "The possible audio output devices, in \"value\" \"description\" pairs, for gamecode to read.");
#ifdef VOICECHAT
static void S_Voip_Play_Callback(cvar_t *var, char *oldval);
cvar_t snd_voip_capturedevice = CVARF("cl_voip_capturedevice", "", CVAR_ARCHIVE);
cvar_t snd_voip_capturedevice_opts = CVARFD("_cl_voip_capturedevice_opts", "", CVAR_NOSET, "The possible audio capture devices, in \"value\" \"description\" pairs.");
cvar_t snd_voip_capturedevice_opts = CVARFD("_cl_voip_capturedevice_opts", "", CVAR_NOSET, "The possible audio capture devices, in \"value\" \"description\" pairs, for gamecode to read.");
int voipbutton; //+voip, no longer part of cl_voip_send to avoid it getting saved
cvar_t snd_voip_send = CVARFD("cl_voip_send", "0", CVAR_ARCHIVE, "Sends voice-over-ip data to the server whenever it is set.\n0: only send voice if +voip is pressed.\n1: voice activation.\n2: constantly send.\n+4: Do not send to game, only to rtp sessions.");
cvar_t snd_voip_test = CVARD("cl_voip_test", "0", "If 1, enables you to hear your own voice directly, bypassing the server and thus without networking latency, but is fine for checking audio levels. Note that sv_voip_echo can be set if you want to include latency and packetloss considerations, but setting that cvar requires server admin access and is thus much harder to use.");

View File

@ -499,7 +499,8 @@ typedef struct
char *path; //the 'pure' name
qboolean crcknown; //if the crc was specified
unsigned int crc; //the public crc
char *mirrors[8]; //a randomized (prioritized) list of http mirrors to use.
char *extractname; //if specified, this is the filename that should be extracted.
char *mirrors[8]; //a randomized (prioritized-on-load) list of http mirrors to use.
int mirrornum; //the index we last tried to download from, so we still work even if mirrors are down.
} package[64];
} ftemanifest_t;

View File

@ -179,6 +179,7 @@ int fs_hash_files;
static const char *FS_GetCleanPath(const char *pattern, char *outbuf, int outlen);
void FS_RegisterDefaultFileSystems(void);
static void COM_CreatePath (char *path);
ftemanifest_t *FS_ReadDefaultManifest(char *newbasedir, size_t newbasedirsize, qboolean fixedbasedir);
#define ENFORCEFOPENMODE(mode) {if (strcmp(mode, "r") && strcmp(mode, "w")/* && strcmp(mode, "rw")*/)Sys_Error("fs mode %s is not permitted here\n");}
@ -206,6 +207,7 @@ void FS_Manifest_Free(ftemanifest_t *man)
for (i = 0; i < sizeof(man->package) / sizeof(man->package[0]); i++)
{
Z_Free(man->package[i].path);
Z_Free(man->package[i].extractname);
for (j = 0; j < sizeof(man->package[i].mirrors) / sizeof(man->package[i].mirrors[0]); j++)
Z_Free(man->package[i].mirrors[j]);
}
@ -240,6 +242,8 @@ static ftemanifest_t *FS_Manifest_Clone(ftemanifest_t *oldm)
{
if (oldm->package[i].path)
newm->package[i].path = Z_StrDup(oldm->package[i].path);
if (oldm->package[i].extractname)
newm->package[i].extractname = Z_StrDup(oldm->package[i].extractname);
for (j = 0; j < sizeof(newm->package[i].mirrors) / sizeof(newm->package[i].mirrors[0]); j++)
if (oldm->package[i].mirrors[j])
newm->package[i].mirrors[j] = Z_StrDup(oldm->package[i].mirrors[j]);
@ -278,13 +282,17 @@ void FS_Manifest_Print(ftemanifest_t *man)
{
if (man->package[i].path)
{
if (man->package[i].extractname)
Con_Printf("achived");
if (man->package[i].crcknown)
Con_Printf("package %s 0x%x", COM_QuotedString(man->package[i].path, buffer, sizeof(buffer)), man->package[i].crc);
else
Con_Printf("package %s -", COM_QuotedString(man->package[i].path, buffer, sizeof(buffer)));
if (man->package[i].extractname)
Con_Printf(" %s", COM_QuotedString(man->package[i].extractname, buffer, sizeof(buffer)));
for (j = 0; j < sizeof(man->package[i].mirrors) / sizeof(man->package[i].mirrors[0]); j++)
if (man->package[i].mirrors[j])
Con_Printf(" \"%s\"", COM_QuotedString(man->package[i].mirrors[j], buffer, sizeof(buffer)));
Con_Printf(" %s", COM_QuotedString(man->package[i].mirrors[j], buffer, sizeof(buffer)));
Con_Printf("\n");
}
}
@ -300,6 +308,8 @@ static void FS_Manifest_PurgeGamedirs(ftemanifest_t *man)
{
Z_Free(man->gamepath[i].path);
man->gamepath[i].path = NULL;
//FIXME: remove packages from the removed paths.
}
}
}
@ -394,16 +404,17 @@ static qboolean FS_Manifest_ParseTokens(ftemanifest_t *man)
}
}
}
else if (!Q_strcasecmp(fname, "package"))
else if (!Q_strcasecmp(fname, "package") || !Q_strcasecmp(fname, "archivedpackage"))
{
qboolean crcknown;
int crc;
int i, j;
int arg = 1;
fname = Cmd_Argv(1);
fname = Cmd_Argv(arg++);
crcknown = (strcmp(Cmd_Argv(2), "-") && *Cmd_Argv(2));
crc = strtoul(Cmd_Argv(2), NULL, 0);
crcknown = (strcmp(Cmd_Argv(arg), "-") && *Cmd_Argv(arg));
crc = strtoul(Cmd_Argv(arg++), NULL, 0);
for (i = 0; i < sizeof(man->package) / sizeof(man->package[0]); i++)
{
@ -412,9 +423,13 @@ static qboolean FS_Manifest_ParseTokens(ftemanifest_t *man)
man->package[i].path = Z_StrDup(fname);
man->package[i].crcknown = crcknown;
man->package[i].crc = crc;
for (j = 0; j < Cmd_Argc()-3 && j < sizeof(man->package[i].mirrors) / sizeof(man->package[i].mirrors[0]); j++)
if (!Q_strcasecmp(fname, "archivedpackage"))
man->package[i].extractname = Z_StrDup(Cmd_Argv(arg++));
else
man->package[i].extractname = NULL;
for (j = 0; arg+j < Cmd_Argc() && j < sizeof(man->package[i].mirrors) / sizeof(man->package[i].mirrors[0]); j++)
{
man->package[i].mirrors[j] = Z_StrDup(Cmd_Argv(3+j));
man->package[i].mirrors[j] = Z_StrDup(Cmd_Argv(arg+j));
}
break;
}
@ -2215,24 +2230,54 @@ void COM_Gamedir (const char *dir)
return;
}
man = FS_Manifest_Clone(fs_manifest);
FS_Manifest_PurgeGamedirs(man);
if (*dir)
man = NULL;
if (!man)
{
char token[MAX_QPATH];
char *dup = Z_StrDup(dir); //FIXME: is this really needed?
dir = dup;
while ((dir = COM_ParseStringSet(dir, token, sizeof(token))))
vfsfile_t *f = VFSOS_Open(va("%s%s.fmf", com_gamepath, dir), "rb");
if (f)
{
if (!strcmp(dir, ";"))
continue;
if (!*token)
continue;
Cmd_TokenizeString(va("gamedir \"%s\"", token), false, false);
FS_Manifest_ParseTokens(man);
size_t len = VFS_GETLEN(f);
char *fdata = BZ_Malloc(len+1);
if (fdata)
{
VFS_READ(f, fdata, len);
fdata[len] = 0;
man = FS_Manifest_Parse(NULL, fdata);
BZ_Free(fdata);
}
VFS_CLOSE(f);
}
}
if (!man)
{
//generate a new manifest based upon the current one.
man = FS_ReadDefaultManifest(com_gamepath, sizeof(com_gamepath), true);
if (man && strcmp(man->installation, fs_manifest->installation))
{
FS_Manifest_Free(man);
man = NULL;
}
if (!man)
man = FS_Manifest_Clone(fs_manifest);
FS_Manifest_PurgeGamedirs(man);
if (*dir)
{
char token[MAX_QPATH];
char *dup = Z_StrDup(dir); //FIXME: is this really needed?
dir = dup;
while ((dir = COM_ParseStringSet(dir, token, sizeof(token))))
{
if (!strcmp(dir, ";"))
continue;
if (!*token)
continue;
Cmd_TokenizeString(va("gamedir \"%s\"", token), false, false);
FS_Manifest_ParseTokens(man);
}
Z_Free(dup);
}
Z_Free(dup);
}
FS_ChangeGame(man, cfg_reload_on_gamedir.ival);
}
@ -2250,7 +2295,7 @@ void COM_Gamedir (const char *dir)
#define Q2CFG "com_nogamedirnativecode 0\n"
/*Q3's ui doesn't like empty model/headmodel/handicap cvars, even if the gamecode copes*/
#define Q3CFG "gl_overbright 2\nseta model sarge\nseta headmodel sarge\nseta handicap 100\ncom_nogamedirnativecode 0\n"
#define RMQCFG "sv_bigcoords 1\n"
//#define RMQCFG "sv_bigcoords 1\n"
typedef struct {
const char *argname; //used if this was used as a parameter.
@ -2282,13 +2327,13 @@ const gamemode_info_t gamemode_info[] = {
//various quake-based mods.
{"-nexuiz", "nexuiz", "Nexuiz", {"nexuiz.exe"}, NEXCFG, {"data", "*ftedata"}, "Nexuiz"},
{"-xonotic", "xonotic", "Xonotic", {"xonotic.exe"}, NEXCFG, {"data", "*ftedata"}, "Xonotic"},
{"-spark", "spark", "Spark", {"base/src/progs.src",
"base/qwprogs.dat",
"base/pak0.pak"}, DMFCFG, {"base", }, "Spark"},
{"-scouts", "scouts", "FTE-SJ", {"basesj/src/progs.src",
"basesj/progs.dat",
"basesj/pak0.pak"}, NULL, {"basesj", }, "Scouts Journey"},
{"-rmq", "rmq", "RMQ", {NULL}, RMQCFG, {"id1", "qw", "rmq", "*fte"}, "Remake Quake"},
// {"-spark", "spark", "Spark", {"base/src/progs.src",
// "base/qwprogs.dat",
// "base/pak0.pak"}, DMFCFG, {"base", }, "Spark"},
// {"-scouts", "scouts", "FTE-SJ", {"basesj/src/progs.src",
// "basesj/progs.dat",
// "basesj/pak0.pak"}, NULL, {"basesj", }, "Scouts Journey"},
// {"-rmq", "rmq", "RMQ", {NULL}, RMQCFG, {"id1", "qw", "rmq", "*fte"}, "Remake Quake"},
#ifdef HEXEN2
//hexen2's mission pack generally takes precedence if both are installed.
@ -2305,21 +2350,22 @@ const gamemode_info_t gamemode_info[] = {
#if defined(Q3CLIENT) || defined(Q3SERVER)
{"-quake3", "q3", "FTE-Quake3", {"baseq3/pak0.pk3"}, Q3CFG, {"baseq3", "*fteq3"}, "Quake III Arena"},
//the rest are not supported in any real way. maps-only mostly, if that
{"-quake4", "q4", "FTE-Quake4", {"q4base/pak00.pk4"}, NULL, {"q4base", "*fteq4"}, "Quake 4"},
{"-et", NULL, "FTE-EnemyTerritory", {"etmain/pak0.pk3"}, NULL, {"etmain", "*fteet"}, "Wolfenstein - Enemy Territory"},
// {"-quake4", "q4", "FTE-Quake4", {"q4base/pak00.pk4"}, NULL, {"q4base", "*fteq4"}, "Quake 4"},
// {"-et", NULL, "FTE-EnemyTerritory", {"etmain/pak0.pk3"}, NULL, {"etmain", "*fteet"}, "Wolfenstein - Enemy Territory"},
{"-jk2", "jk2", "FTE-JK2", {"base/assets0.pk3"}, NULL, {"base", "*ftejk2"}, "Jedi Knight II: Jedi Outcast"},
{"-warsow", "warsow", "FTE-Warsow", {"basewsw/pak0.pk3"}, NULL, {"basewsw", "*ftewsw"}, "Warsow"},
// {"-jk2", "jk2", "FTE-JK2", {"base/assets0.pk3"}, NULL, {"base", "*ftejk2"}, "Jedi Knight II: Jedi Outcast"},
// {"-warsow", "warsow", "FTE-Warsow", {"basewsw/pak0.pk3"}, NULL, {"basewsw", "*ftewsw"}, "Warsow"},
#endif
#if !defined(QUAKETC) && !defined(MINIMAL)
{"-doom", "doom", "FTE-Doom", {"doom.wad"}, NULL, {"*", "*ftedoom"}, "Doom"},
{"-doom2", "doom2", "FTE-Doom2", {"doom2.wad"}, NULL, {"*", "*ftedoom"}, "Doom2"},
{"-doom3", "doom3", "FTE-Doom3", {"doom3.wad"}, NULL, {"based3", "*ftedoom3"},"Doom3"},
// {"-doom", "doom", "FTE-Doom", {"doom.wad"}, NULL, {"*", "*ftedoom"}, "Doom"},
// {"-doom2", "doom2", "FTE-Doom2", {"doom2.wad"}, NULL, {"*", "*ftedoom"}, "Doom2"},
// {"-doom3", "doom3", "FTE-Doom3", {"doom3.wad"}, NULL, {"based3", "*ftedoom3"},"Doom3"},
//for the luls
{"-diablo2", NULL, "FTE-Diablo2", {"d2music.mpq"}, NULL, {"*", "*fted2"}, "Diablo 2"},
//can run in windows, needs hl gamecode enabled.
// {"-diablo2", NULL, "FTE-Diablo2", {"d2music.mpq"}, NULL, {"*", "*fted2"}, "Diablo 2"},
#endif
#if defined(HLSERVER) || defined(HLCLIENT)
//can run in windows, needs hl gamecode enabled. maps can always be viewed, but meh.
{"-halflife", "halflife", "FTE-HalfLife", {"valve/liblist.gam"}, NULL, {"valve", "*ftehl"}, "Half-Life"},
#endif
@ -3316,10 +3362,8 @@ static int FS_IdentifyDefaultGame(char *newbase, int sizeof_newbase, qboolean fi
return gamenum;
}
static
//allowed to modify newbasedir if fixedbasedir isn't set
ftemanifest_t *FS_GenerateLegacyManifest(char *newbasedir, int sizeof_newbasedir, qboolean fixedbasedir, int game)
static ftemanifest_t *FS_GenerateLegacyManifest(char *newbasedir, int sizeof_newbasedir, qboolean fixedbasedir, int game)
{
int i;
ftemanifest_t *man;
@ -3383,13 +3427,13 @@ ftemanifest_t *FS_GenerateLegacyManifest(char *newbasedir, int sizeof_newbasedir
static char *FS_RelativeURL(char *base, char *file, char *buffer, int bufferlen)
{
//fixme: cope with windows paths
qboolean baseisurl = !!strchr(base, ':');
qboolean baseisurl = base?!!strchr(base, ':'):false;
qboolean fileisurl = !!strchr(file, ':');
//qboolean baseisabsolute = (*base == '/' || *base == '\\');
qboolean fileisabsolute = (*file == '/' || *file == '\\');
char *ebase;
if (fileisurl)
if (fileisurl || !base)
return file;
if (fileisabsolute)
{
@ -3415,6 +3459,7 @@ static char *FS_RelativeURL(char *base, char *file, char *buffer, int bufferlen)
#ifdef WEBCLIENT
static struct dl_download *curpackagedownload;
static char fspdl_internalname[MAX_QPATH];
static char fspdl_temppath[MAX_OSPATH];
static char fspdl_finalpath[MAX_OSPATH];
static void FS_BeginNextPackageDownload(void);
@ -3431,9 +3476,60 @@ static void FS_PackageDownloaded(struct dl_download *dl)
{
//rename the file as needed.
COM_CreatePath(fspdl_finalpath);
if (!Sys_Rename(fspdl_temppath, fspdl_finalpath))
if (*fspdl_internalname) //if zip...
{ //archive
searchpathfuncs_t *archive = FSZIP_LoadArchive(VFSOS_Open(fspdl_temppath, "rb"), dl->url);
if (archive)
{
flocation_t loc;
int status = FF_NOTFOUND;
//FIXME: loop through all packages to extract all of them as appropriate
if (status == FF_NOTFOUND)
status = archive->FindFile(archive, &loc, fspdl_internalname, NULL);
if (status == FF_NOTFOUND)
status = archive->FindFile(archive, &loc, COM_SkipPath(fspdl_internalname), NULL);
if (status == FF_FOUND)
{
vfsfile_t *in = archive->OpenVFS(archive, &loc, "rb");
if (in)
{
vfsfile_t *out = VFSOS_Open(fspdl_finalpath, "wb");
qofs_t remaining = VFS_GETLEN(in);
size_t count;
if (out)
{
char buf[65536];
while (remaining)
{
if (remaining < sizeof(buf))
count = remaining;
else
count = sizeof(buf);
VFS_READ(in, buf, count);
VFS_WRITE(out, buf, count);
remaining -= count;
}
VFS_CLOSE(out);
}
VFS_CLOSE(in);
}
}
else
{
Con_Printf("Unable to find %s in %s\n", fspdl_internalname, fspdl_temppath);
}
archive->ClosePath(archive);
}
}
else
{
Con_Printf("Unable to rename \"%s\" to \"%s\"\n", fspdl_temppath, fspdl_finalpath);
//direct file
if (!Sys_Rename(fspdl_temppath, fspdl_finalpath))
{
Con_Printf("Unable to rename \"%s\" to \"%s\"\n", fspdl_temppath, fspdl_finalpath);
}
}
}
Sys_remove (fspdl_temppath);
@ -3481,6 +3577,11 @@ static void FS_BeginNextPackageDownload(void)
continue;
}
if (man->package[j].extractname)
Q_strncpyz(fspdl_internalname, man->package[j].extractname, sizeof(fspdl_internalname));
else
Q_strncpyz(fspdl_internalname, "", sizeof(fspdl_internalname));
//figure out a temp name and figure out where we're going to get it from.
FS_NativePath(buffer, FS_ROOT, fspdl_finalpath, sizeof(fspdl_finalpath));
if (!FS_GenCachedPakName(va("%s.tmp", man->package[j].path), crcstr, buffer, sizeof(buffer)))
@ -3615,6 +3716,42 @@ qboolean FS_FoundManifest(void *usr, ftemanifest_t *man)
return false;
}
//reads the default manifest based upon the basedir, the commandline arguments, the name of the exe, etc.
//may still fail if no game was identified.
//if fixedbasedir is true, stuff like -quake won't override/change the active basedir (ie: -basedir or gamedir switching without breaking gamedir)
ftemanifest_t *FS_ReadDefaultManifest(char *newbasedir, size_t newbasedirsize, qboolean fixedbasedir)
{
ftemanifest_t *man = NULL;
vfsfile_t *f;
#ifdef BRANDING_NAME
f = VFSOS_Open(va("%s"STRINGIFY(BRANDING_NAME)".fmf", newbasedir), "rb");
if (!f)
#endif
f = VFSOS_Open(va("%sdefault.fmf", newbasedir), "rb");
if (f)
{
size_t len = VFS_GETLEN(f);
char *fdata = BZ_Malloc(len+1);
if (fdata)
{
VFS_READ(f, fdata, len);
fdata[len] = 0;
man = FS_Manifest_Parse(NULL, fdata);
BZ_Free(fdata);
}
VFS_CLOSE(f);
}
if (!man)
{
int game = FS_IdentifyDefaultGame(newbasedir, newbasedirsize, fixedbasedir);
if (game != -1)
man = FS_GenerateLegacyManifest(newbasedir, newbasedirsize, fixedbasedir, game);
}
return man;
}
//this is potentially unsafe. needs lots of testing.
qboolean FS_ChangeGame(ftemanifest_t *man, qboolean allowreloadconfigs)
{
@ -3649,35 +3786,7 @@ qboolean FS_ChangeGame(ftemanifest_t *man, qboolean allowreloadconfigs)
if (fs_manifest)
return false;
if (!man)
{
vfsfile_t *f;
#ifdef BRANDING_NAME
f = VFSOS_Open(va("%s"STRINGIFY(BRANDING_NAME)".fmf", newbasedir), "rb");
if (!f)
#endif
f = VFSOS_Open(va("%sdefault.fmf", newbasedir), "rb");
if (f)
{
size_t len = VFS_GETLEN(f);
char *fdata = BZ_Malloc(len+1);
if (fdata)
{
VFS_READ(f, fdata, len);
fdata[len] = 0;
man = FS_Manifest_Parse(NULL, fdata);
BZ_Free(fdata);
}
VFS_CLOSE(f);
}
}
if (!man)
{
int game = FS_IdentifyDefaultGame(newbasedir, sizeof(newbasedir), fixedbasedir);
if (game != -1)
man = FS_GenerateLegacyManifest(newbasedir, sizeof(newbasedir), fixedbasedir, game);
}
man = FS_ReadDefaultManifest(newbasedir, sizeof(newbasedir), fixedbasedir);
if (!man && isDedicated)
{ //dedicated servers have no menu code, so just pick the first fmf we could find.
@ -4093,33 +4202,6 @@ void FS_ShowManifest_f(void)
else
Con_Printf("no manifest loaded...\n");
}
int FS_ThreadTest(void*arg)
{
vfsfile_t *f = NULL;
char startdata[256];
char lastdata[256];
while(!f)
{
if (Sys_LockMutex(fs_thread_mutex))
{
f = FS_OpenVFS("gfx/pop.lmp", "rb", FS_GAME);
Sys_UnlockMutex(fs_thread_mutex);
}
}
VFS_READ(f, startdata, sizeof(startdata));
VFS_CLOSE(f);
for(;;)
{
Sys_LockMutex(fs_thread_mutex);
f = FS_OpenVFS("gfx/pop.lmp", "rb", FS_GAME);
Sys_UnlockMutex(fs_thread_mutex);
VFS_READ(f, lastdata, sizeof(lastdata));
VFS_CLOSE(f);
if (memcmp(startdata, lastdata, sizeof(lastdata)))
Con_Printf("FS_ThreadTest failed\n");
}
}
/*
================
COM_InitFilesystem

View File

@ -1274,6 +1274,9 @@ searchpathfuncs_t *QDECL FSZIP_LoadArchive (vfsfile_t *packhandle, const char *d
zipfile_t *zip;
struct zipinfo info;
if (!packhandle)
return NULL;
zip = Z_Malloc(sizeof(zipfile_t));
Q_strncpyz(zip->filename, desc, sizeof(zip->filename));
zip->raw = packhandle;

View File

@ -863,7 +863,7 @@ void PM_CategorizePosition (void)
else
{
trace = PM_PlayerTracePortals (pmove.origin, point, MASK_PLAYERSOLID, NULL);
if (trace.fraction == 1 || -DotProduct(pmove.gravitydir, trace.plane.normal) < MIN_STEP_NORMAL)
if (!trace.startsolid && (trace.fraction == 1 || -DotProduct(pmove.gravitydir, trace.plane.normal) < MIN_STEP_NORMAL))
pmove.onground = false;
else
{

120
specs/fte_manifests.txt Normal file
View File

@ -0,0 +1,120 @@
Manifest files are small files that describe various attributes of the game, primarily branding and filesystem configuration for mods or standalone games.
Manifest files are useful for standalone games that need to distance themselves from other games using the same engine. They are also useful for auto-updating mods or for mod compilations where mods should not be downloaded up-front.
FTE Manifest Files (.fmf) can either be invoked by opening them with FTE (set up a file association), or automatically via a default.fmf file in the path specified by the -basedir argument, or in the working directory (settable via shortcuts). A default.fmf file will override the engine's game determination routines completely.
These files are also supported by the Android port, where opening one will give an option to open with FTEDroid automatically (much like on a desktop OS), or in the webgl port with the url specified after a # in the url.
The gamedir command (eg: 'gamedir foo', or '-game foo' on the commandline) will attempt to load an fmf file with the specified name (ie: foo.fmf) from the current basedir. If the new manifest has a different 'game' value, the engine may ignore.
Each line of a manifest file forms a single command. New lines denote new commands. Comments are C++-style // double forward slashes.
Example:
//example manifest file for Sock's 'In The Shadows' stealth mod. Because why not?
FTEMANIFEST 1
GAME quake
NAME "In The Shadows"
PROTOCOLNAME ITShadows
DEFAULTEXEC ""
//UPDATEURL "http://example.com/mods/its.fmf"
BASEGAME id1
BASEGAME qw
BASEGAME *fte
GAMEDIR shadows
//it would be nice if we could specify pak0.pak and pak1.pak here, alas doing so would violate id's various copyrights. instead, hope the user has a copy already installed.
//this is a major pain for systems where there is no official installer (read: fte's webgl port, or android... or win64).
//its pak0 can be downloaded from sock's public server
ARCHIVEDPACKAGE shadows/pak0.pak "0xb1768147" "shadows/pak0.pak" "http://simonoc.com/files/maps/sp/its_demo_v1_1.zip"
Commands:
FTEMANIFEST 1
Specifies the actual version of the manifest file, and what the file actually is. Only valid as the first line. The version number is included for potential future use. Version 1 is the current version.
GAME quake
This specifies the code-name of the game upon which your mod is based.
Standalone games should pick their own string to use here.
Mods for one of the games that FTE intrinsically understands may be automatically located via the system registry for an existing installation. This is useful when the fmf file has been downloaded and opened via a web browser.
Known game name (namely: quake, quake2, quake3, hexen2) may imply other settings if they were not otherwise specified.
It is NOT recommended to omit this, but if you do, 'quake' will be assumed.
NAME "Example Quake Mod"
This specifies what the game should be referred to by various console prints etc. This should be the formal name of your mod.
PROTOCOLNAME "foo"
This is how the game/mod should be identified to master servers. A unique value here helps prevent servers from other games being listed, as well as preventing other games from seeing this game/mod.
FIXME: engine support for this should be improved.
DEFAULTEXEC ""
If specified, this is inserted before the game's normal default.cfg file, and can be used to override the engine's default cvar settings for mods where the default.cfg was originally written for a different engine (and thus omits certain settings). This should only be used for games where the default.cfg itself is not replacable. Standalone mods should not need to specify this, and should instead just put everything in the mod's default.cfg file.
For recognised games, the engine will automatically assume usable defaults, but its possible that a mod depends upon a setting that I overlooked.
BASEGAME "id1"
Multiple basegame lines can be specified. These are the core subdirectories that should always be loaded, even if the gamedir command was used to load a different mod.
Standadlone games will typically specify only a single value.
Each additional basegame will be favoured in preference to the prior ones.
(note: 'basedir' refers to the path from which all game subdirs are found in, thus manifests use basegame to avoid ambiguity at the cost of consistancy)
A basedir with a leading * is a private directory that will not be reported to clients nor appear in server browsers. This is typically used on fte-specific game directories, so that other-engine clients will not get confused.
GAMEDIR "mymod"
These act almost exactly like basegame, except they flushed and replaced if the gamedir command is used. Additionally these are never implied by the 'game' command.
These will always have a higher preference than any BASEGAME subdir.
There are arguments both for and against a mod specifying its subdir as a basegame or as just a gamedir.
DISABLEHOMEDIR 1
If non-zero, the game/mod will not attempt to use any directory within the user's homedir by default. This may cause issues if the base game was installed in some read-only system location like C:\Program Files\, and so should only be used for manually installed games (ones that contain just the exe+fmf file).
UPDATEURL "http://example.com/foo.fmf"
This command gives the url from which to update the manifest file from. The updated manifest *MUST* contain the same url for the update to be accepted. Because of this, you should ensure that it is has a url that will be valid for as long as your mod will be relevent.
This replaces just the manifest, but because the manifest file specifies a list of packages that should be used, a new set of packages can be downloaded automatically, and this is the true power of the update url.
PACKAGE "id1/pak0.pak" 0x4f069cac "http://mirror1.example.com/qsw106_pak0.pak" "http://mirror2.example.com/qsw106_pak0.pak"
WARNING: be sure to respect third-party copyrights.
This command, of which multiple can be specified, lists the packages which should automatically be downloaded.
The first name (in this case 'id1/pak0.pak') is the logical name of the package, including the gamedir in which it is to be found. The second argument is the CRC of the package's table of contents, and any additional arguments are alternative mirrors from which to download updated versions. The engine will randomize the order on load (for load balancing) and will try other mirrors if the any fails to provide a file.
Multiple versions of the same package may be installed. This is handled via the crc. If a mirror provides a file who's crc does not match, the download will be ignored and another mirror will be tried.
A CRC of '-' can be used if you really do not want to provide crcs. This suppresses all crc checks, and so is not recommended (if you instead use a different filename for each version, this is not always an issue). The CRC can be determined by using the wrong crc and fixing it up to the correct value that the engine claims that it should be.
If the user already has an actual id1/pak0.pak installed (ie: a prior install of quake), then an updated version will not be downloaded even if their crc does not match. Otherwise the downloaded file will be placed inside a 'dlcache' directory, with a crc postfix, allowing multiple versions.
If you are directly serving pk3 files, it is recommended to just compress the pk3.
If you are directly serving pak files, you will likely wish to ensure that gzip transfer compression is enabled on your mirrors for the files in question.
Its recommended to place pk3 files directly on your mirrors as they are already compressed, which keeps things consistant and avoids stalls. However, if you use a single installation zip that you wish to reuse with manifest files, you can use ARCHIVEDPACKAGE instead.
ARCHIVEDPACKAGE "id1/pak0.pak" 0x4f069cac "id1/pak0.pak" "http://mirror1.example.com/qsw106.zip" "http://mirror2.example.com/qsw106.zip"
Mostly identical to the PACKAGE command, but with an extra argument before the mirror list.
The difference is that if the file is downloaded, it is assumed to be a zip archive from which to extract the package. The extra/third argument is the filename that is read from the zip.
This command is provided for compatibility with existing installation media from third parties who do not themselves support manifest files.
MINVER FTE 4778
The manifest should not be used if the engine parsing it is a lower version than this. There is no default limit.
The first arg (ie: 'fte' in this case) specifies that the limit is specific to a specific fork or reimplementation of the file format.
The second arg should be an svn revision number (visible from the 'version' console command).
When updating a manifest, the engine will ignore the new manifest if its minver is too high. This can be used to avoid breaking installations if the user tries running the mod with too old an engine.
MAXVER FTE 4778
The manifest should not be used if the engine parsing it is a higher version than this. See minver for argument info.
I'm not sure how useful this will actually be, but it is included for completeness.
Console Commands:
/gamedir foo
This console command will read foo.fmf from the current basedir and load that instead. The basedir will not be changed to match the installation path of the specified game.
If there was no fmf file with that name, it will load the default.fmf file from that basedir instead, or try to guess which game is installed in the current basedir instead. From this default.fmf file, the gamedir attributes will be stripped and 'GAMEDIR foo' will be inserted internally.
/fs_showmanifest
This console command displays the effective/active manifest file that is currently in effect.
/fs_changegame
This command recognises various game names and will switch the basedir in order to try loading it (the game may be found via a prompt for the game's basedir, via a cached result of that prompt, or via known registry keys from that game's installer).
If the specified game is not recognised, the argument will be assumed to be a manifest named via a url or a system path.
Possible Future Direction:
ARCHIVEDPACKAGE should support multiple files without redownloading.
ARCHIVEDPACKAGE needs some way to not care about crcs/filenames, and extract+use all included files. May be better to just decompress the zip and directly use it with a path prefix.
Uninstallation/auto-pruning.
Extra polish.
Multiple mods specified within a single fmf.
Individual files (ie: maps) that can be downloaded on demand?
Other stuff.
Scan for a new manifest instead of merely switching gamedirs.
Readme/license/credits info is needed.