#include "quakedef.h"
#include "netinc.h"

//#define com_gamedir com__gamedir

#include <ctype.h>
#include <limits.h>

#include "fs.h"
#include "shader.h"
#ifdef _WIN32
#include "winquake.h"
#endif

void fs_game_callback(cvar_t *var, char *oldvalue);
hashtable_t filesystemhash;
qboolean com_fschanged = true;
static unsigned int fs_restarts;

cvar_t com_fs_cache			= CVARF("fs_cache", IFMINIMAL("2","1"), CVAR_ARCHIVE);
cvar_t cfg_reload_on_gamedir = CVAR("cfg_reload_on_gamedir", "1");
cvar_t fs_game = CVARFDC("game", "", CVAR_NOSAVE|CVAR_NORESET, "Provided for Q2 compat.", fs_game_callback);
cvar_t fs_gamedir = CVARFD("fs_gamedir", "", CVAR_NOUNSAFEEXPAND|CVAR_NOSET|CVAR_NOSAVE, "Provided for Q2 compat.");
cvar_t fs_basedir = CVARFD("fs_basedir", "", CVAR_NOUNSAFEEXPAND|CVAR_NOSET|CVAR_NOSAVE, "Provided for Q2 compat.");
int active_fs_cachetype;
static int fs_referencetype;
int fs_finds;
void COM_CheckRegistered (void);

void fs_game_callback(cvar_t *var, char *oldvalue)
{
	static qboolean runaway = false;
	char buf[MAX_OSPATH];
	if (!strcmp(var->string, oldvalue))
		return;	//no change here.
	if (runaway)
		return;	//ignore that
	runaway = true;
	Cmd_ExecuteString(va("gamedir %s\n", COM_QuotedString(var->string, buf, sizeof(buf))), Cmd_ExecLevel);
	runaway = false;
}

struct
{
	void *module;
	const char *extension;
	searchpathfuncs_t *(QDECL *OpenNew)(vfsfile_t *file, const char *desc);
	qboolean loadscan;
} searchpathformats[64];

int FS_RegisterFileSystemType(void *module, const char *extension, searchpathfuncs_t *(QDECL *OpenNew)(vfsfile_t *file, const char *desc), qboolean loadscan)
{
	unsigned int i;
	for (i = 0; i < sizeof(searchpathformats)/sizeof(searchpathformats[0]); i++)
	{
		if (searchpathformats[i].extension && !strcmp(searchpathformats[i].extension, extension))
			break;	//extension match always replaces
		if (!searchpathformats[i].extension && !searchpathformats[i].OpenNew)
			break;
	}
	if (i == sizeof(searchpathformats)/sizeof(searchpathformats[0]))
		return 0;

	searchpathformats[i].module = module;
	searchpathformats[i].extension = extension;
	searchpathformats[i].OpenNew = OpenNew;
	searchpathformats[i].loadscan = loadscan;
	com_fschanged = true;

	return i+1;
}

void FS_UnRegisterFileSystemType(int idx)
{
	if ((unsigned int)(idx-1) >= sizeof(searchpathformats)/sizeof(searchpathformats[0]))
		return;

	searchpathformats[idx-1].OpenNew = NULL;
	searchpathformats[idx-1].module = NULL;
	com_fschanged = true;

	//FS_Restart will be needed
}
void FS_UnRegisterFileSystemModule(void *module)
{
	int i;
	qboolean found = false;
	for (i = 0; i < sizeof(searchpathformats)/sizeof(searchpathformats[0]); i++)
	{
		if (searchpathformats[i].module == module)
		{
			searchpathformats[i].OpenNew = NULL;
			searchpathformats[i].module = NULL;
			found = true;
		}
	}

	if (found)
	{
		Cmd_ExecuteString("fs_restart", RESTRICT_LOCAL);
	}
}


vfsfile_t *FS_OpenVFSLoc(flocation_t *loc, char *mode);



char *VFS_GETS(vfsfile_t *vf, char *buffer, int buflen)
{
	char in;
	char *out = buffer;
	int len;
	len = buflen-1;
	if (len == 0)
		return NULL;
	while (len > 0)
	{
		if (VFS_READ(vf, &in, 1) != 1)
		{
			if (len == buflen-1)
				return NULL;
			*out = '\0';
			return buffer;
		}
		if (in == '\n')
			break;
		*out++ = in;
		len--;
	}
	*out = '\0';

	//if there's a trailing \r, strip it.
	if (out > buffer)
		if (out[-1] == '\r')
			out[-1] = 0;

	return buffer;
}

void VARGS VFS_PRINTF(vfsfile_t *vf, char *format, ...)
{
	va_list		argptr;
	char		string[1024];

	va_start (argptr, format);
	vsnprintf (string,sizeof(string)-1, format,argptr);
	va_end (argptr);

	VFS_PUTS(vf, string);
}






char	gamedirfile[MAX_OSPATH];




//the various COM_LoadFiles set these on return
int	com_filesize;
qboolean com_file_copyprotected;//file should not be available for download.
qboolean com_file_untrusted;	//file was downloaded inside a package


//char	*com_basedir;				//obsolete

char	com_quakedir[MAX_OSPATH];
char	com_homedir[MAX_OSPATH];

char	com_configdir[MAX_OSPATH];	//homedir/fte/configs

int fs_hash_dups;
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);

#define ENFORCEFOPENMODE(mode) {if (strcmp(mode, "r") && strcmp(mode, "w")/* && strcmp(mode, "rw")*/)Sys_Error("fs mode %s is not permitted here\n");}






//forget a manifest entirely.
void FS_Manifest_Free(ftemanifest_t *man)
{
	int i, j;
	if (!man)
		return;
	Z_Free(man->updateurl);
	Z_Free(man->installation);
	Z_Free(man->formalname);
	Z_Free(man->protocolname);
	Z_Free(man->defaultexec);
	for (i = 0; i < sizeof(man->gamepath) / sizeof(man->gamepath[0]); i++)
	{
		Z_Free(man->gamepath[i].path);
	}
	for (i = 0; i < sizeof(man->package) / sizeof(man->package[0]); i++)
	{
		Z_Free(man->package[i].path);
		for (j = 0; j < sizeof(man->package[i].mirrors) / sizeof(man->package[i].mirrors[0]); j++)
			Z_Free(man->package[i].mirrors[j]);
	}
	Z_Free(man);
}

//clone a manifest, so we can hack at it.
static ftemanifest_t *FS_Manifest_Clone(ftemanifest_t *oldm)
{
	ftemanifest_t *newm;
	int i, j;
	newm = Z_Malloc(sizeof(*newm));
	if (oldm->updateurl)
		newm->updateurl = Z_StrDup(oldm->updateurl);
	if (oldm->installation)
		newm->installation = Z_StrDup(oldm->installation);
	if (oldm->formalname)
		newm->formalname = Z_StrDup(oldm->formalname);
	if (oldm->protocolname)
		newm->protocolname = Z_StrDup(oldm->protocolname);
	if (oldm->defaultexec)
		newm->defaultexec = Z_StrDup(oldm->defaultexec);

	for (i = 0; i < sizeof(newm->gamepath) / sizeof(newm->gamepath[0]); i++)
	{
		if (oldm->gamepath[i].path)
			newm->gamepath[i].path = Z_StrDup(oldm->gamepath[i].path);
		newm->gamepath[i].base = oldm->gamepath[i].base;
	}
	for (i = 0; i < sizeof(newm->package) / sizeof(newm->package[0]); i++)
	{
		if (oldm->package[i].path)
			newm->package[i].path = Z_StrDup(oldm->package[i].path);
		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]);
	}

	return newm;
}

void FS_Manifest_Print(ftemanifest_t *man)
{
	char buffer[1024];
	int i, j;
	if (man->updateurl)
		Con_Printf("updateurl %s\n", COM_QuotedString(man->updateurl, buffer, sizeof(buffer)));
	if (man->installation)
		Con_Printf("game %s\n", COM_QuotedString(man->installation, buffer, sizeof(buffer)));
	if (man->formalname)
		Con_Printf("name %s\n", COM_QuotedString(man->formalname, buffer, sizeof(buffer)));
	if (man->protocolname)
		Con_Printf("protocolname %s\n", COM_QuotedString(man->protocolname, buffer, sizeof(buffer)));
	if (man->defaultexec)
		Con_Printf("defaultexec %s\n", COM_QuotedString(man->defaultexec, buffer, sizeof(buffer)));

	for (i = 0; i < sizeof(man->gamepath) / sizeof(man->gamepath[0]); i++)
	{
		if (man->gamepath[i].path)
		{
			if (man->gamepath[i].base)
				Con_Printf("basegame %s\n", COM_QuotedString(man->gamepath[i].path, buffer, sizeof(buffer)));
			else
				Con_Printf("gamedir %s\n", COM_QuotedString(man->gamepath[i].path, buffer, sizeof(buffer)));
		}
	}

	for (i = 0; i < sizeof(man->package) / sizeof(man->package[0]); i++)
	{
		if (man->package[i].path)
		{
			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)));
			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("\n");
		}
	}
}

//forget any mod dirs.
static void FS_Manifest_PurgeGamedirs(ftemanifest_t *man)
{
	int i;
	for (i = 0; i < sizeof(man->gamepath) / sizeof(man->gamepath[0]); i++)
	{
		if (man->gamepath[i].path && !man->gamepath[i].base)
		{
			Z_Free(man->gamepath[i].path);
			man->gamepath[i].path = NULL;
		}
	}
}

//create a new empty manifest with default values.
static ftemanifest_t *FS_Manifest_Create(void)
{
	ftemanifest_t *man = Z_Malloc(sizeof(*man));

//	man->installation = Z_StrDup("quake");
	man->formalname = Z_StrDup(FULLENGINENAME);
	return man;
}
//parse Cmd_Argv tokens into the manifest.
static void FS_Manifest_ParseTokens(ftemanifest_t *man)
{
	char *fname;
	if (!Cmd_Argc())
		return;
	fname = Cmd_Argv(0);

	if (*fname == '*')
		fname++;

	if (!Q_strcasecmp(fname, "game"))
	{
		Z_Free(man->installation);
		man->installation = Z_StrDup(Cmd_Argv(1));
	}
	else if (!Q_strcasecmp(fname, "name"))
	{
		Z_Free(man->formalname);
		man->formalname = Z_StrDup(Cmd_Argv(1));
	}
	else if (!Q_strcasecmp(fname, "protocolname"))
	{
		Z_Free(man->protocolname);
		man->protocolname = Z_StrDup(Cmd_Argv(1));
	}
	else if (!Q_strcasecmp(fname, "defaultexec"))
	{
		Z_Free(man->defaultexec);
		man->defaultexec = Z_StrDup(Cmd_Argv(1));
	}
	else if (!Q_strcasecmp(fname, "basegame") || !Q_strcasecmp(fname, "gamedir"))
	{
		int i;
		char *newdir = Cmd_Argv(1);

		//reject various evil path arguments.
		if (!*newdir || strchr(newdir, '\n') || strchr(newdir, '\r') || !strcmp(newdir, ".") || !strcmp(newdir, "..") || strchr(newdir, ':') || strchr(newdir, '/') || strchr(newdir, '\\') || strchr(newdir, '$'))
		{
			Con_Printf("Illegal path specified: %s\n", newdir);
		}
		else
		{
			for (i = 0; i < sizeof(man->gamepath) / sizeof(man->gamepath[0]); i++)
			{
				if (!man->gamepath[i].path)
				{
					man->gamepath[i].base = !Q_strcasecmp(fname, "basegame");
					man->gamepath[i].path = Z_StrDup(newdir);
					break;
				}
			}
			if (i == sizeof(man->gamepath) / sizeof(man->gamepath[0]))
			{
				Con_Printf("Too many game paths specified in manifest\n");
			}
		}
	}
	else
	{
		qboolean crcknown;
		int crc;
		int i, j;
		if (!Q_strcasecmp(fname, "package"))
			Cmd_ShiftArgs(1, false);

		crcknown = (strcmp(Cmd_Argv(1), "-") && *Cmd_Argv(1));
		crc = strtoul(Cmd_Argv(1), NULL, 0);

		for (i = 0; i < sizeof(man->package) / sizeof(man->package[0]); i++)
		{
			if (!man->package[i].path)
			{
				man->package[i].path = Z_StrDup(Cmd_Argv(0));
				man->package[i].crcknown = crcknown;
				man->package[i].crc = crc;
				for (j = 0; j < Cmd_Argc()-2 && j < sizeof(man->package[i].mirrors) / sizeof(man->package[i].mirrors[0]); j++)
				{
					man->package[i].mirrors[j] = Z_StrDup(Cmd_Argv(2+j));
				}
				break;
			}
		}
		if (i == sizeof(man->package) / sizeof(man->package[0]))
		{
			Con_Printf("Too many packages specified in manifest\n");
		}
	}
}
//read a manifest file
ftemanifest_t *FS_Manifest_Parse(const char *data)
{
	ftemanifest_t *man;
	if (!data)
		return NULL;
	while (*data == ' ' || *data == '\t' || *data == '\r' || *data == '\n')
		data++;
	if (!*data)
		return NULL;


	man = FS_Manifest_Create();

	while (data && *data)
	{
		data = Cmd_TokenizeString((char*)data, false, false);
		FS_Manifest_ParseTokens(man);
	}
	if (!man->installation)
	{	//every manifest should have an internal name specified, so we can use the correct basedir
		//if we don't recognise it, then we'll typically prompt (or just use the working directory), but always assuming a default at least ensures things are sane.
		//fixme: we should probably fill in the basegame here (and share that logic with the legacy manifest generation code)
		data = Cmd_TokenizeString((char*)"game quake", false, false);
		FS_Manifest_ParseTokens(man);
	}
	return man;
}

//======================================================================================================



typedef struct searchpath_s
{
	searchpathfuncs_t *handle;

	unsigned int flags;

	char logicalpath[MAX_OSPATH];	//printable hunam-readable location of the package. generally includes a system path, including nested packages.
	char purepath[256];	//server tracks the path used to load them so it can tell the client
	int crc_check;	//client sorts packs according to this checksum
	int crc_reply;	//client sends a different crc back to the server, for the paks it's actually loaded.

	struct searchpath_s *next;
	struct searchpath_s *nextpure;
} searchpath_t;

static ftemanifest_t	*fs_manifest;	//currently active manifest.
static searchpath_t	*com_searchpaths;
static searchpath_t	*com_purepaths;
static searchpath_t	*com_base_searchpaths;	// without gamedirs

static int fs_puremode;				//0=deprioritise pure, 1=prioritise pure, 2=pure only.
static char *fs_purenames;			//list of allowed packages
static char *fs_purecrcs;			//list of crcs for those packages. one token per package.
static unsigned int fs_pureseed;	//used as a key so the server knows we're obeying. completely unreliable/redundant in an open source project, but needed for q3 network compat.

int QDECL COM_FileSize(const char *path)
{
	int len;
	flocation_t loc;
	len = FS_FLocateFile(path, FSLFRT_LENGTH, &loc);
	return len;
}

//appends a / on the end of the directory if it does not already have one.
void FS_CleanDir(char *out, int outlen)
{
	int olen = strlen(out);
	if (!olen || olen >= outlen-1)
		return;

	if (out[olen-1] == '\\')
		out[olen-1] = '/';
	else if (out[olen-1] != '/')
	{
		out[olen+1] = '\0';
		out[olen] = '/';
	}
}

/*
============
COM_Path_f

============
*/
static void COM_PathLine(searchpath_t *s)
{
	Con_Printf("%s  %s%s%s%s%s\n", s->logicalpath,
		(s->flags & SPF_REFERENCED)?"^[(ref)\\tip\\Referenced\\desc\\Package will auto-download to clients^]":"",
		(s->flags & SPF_TEMPORARY)?"^[(temp)\\tip\\Temporary\\desc\\Flushed on map change^]":"",
		(s->flags & SPF_COPYPROTECTED)?"^[(c)\\tip\\Copyrighted\\desc\\Copy-Protected and is not downloadable^]":"",
		(s->flags & SPF_EXPLICIT)?"^[(e)\\tip\\Explicit\\desc\\Loaded explicitly by the gamedir^]":"",
		(s->flags & SPF_UNTRUSTED)?"^[(u)\\tip\\Untrusted\\desc\\Configs and scripts will not be given access to passwords^]":"" );
}
void COM_Path_f (void)
{
	searchpath_t	*s;

	Con_TPrintf ("Current search path:\n");

	if (com_purepaths || fs_puremode)
	{
		Con_Printf ("Pure paths:\n");
		for (s=com_purepaths ; s ; s=s->nextpure)
		{
			COM_PathLine(s);
		}
		Con_Printf ("----------\n");
		if (fs_puremode == 2)
			Con_Printf ("Inactive paths:\n");
		else
			Con_Printf ("Impure paths:\n");
	}


	for (s=com_searchpaths ; s ; s=s->next)
	{
		if (s == com_base_searchpaths)
			Con_Printf ("----------\n");

		COM_PathLine(s);
	}
}


/*
============
COM_Dir_f

============
*/
static int QDECL COM_Dir_List(const char *name, qofs_t size, void *parm, searchpathfuncs_t *spath)
{
	searchpath_t	*s;
	for (s=com_searchpaths ; s ; s=s->next)
	{
		if (s->handle == spath)
			break;
	}
	if (size > 1.0*1024*1024*1024)
		Con_Printf("%s \t(%#.3ggb) (%s)\n", name, size/(1024.0*1024*1024), s?s->logicalpath:"??");
	else if (size > 1.0*1024*1024)
		Con_Printf("%s \t(%#.3gmb) (%s)\n", name, size/(1024.0*1024), s?s->logicalpath:"??");
	else if (size > 1.0*1024)
		Con_Printf("%s \t(%#.3gkb) (%s)\n", name, size/1024.0, s?s->logicalpath:"??");
	else
		Con_Printf("%s \t(%ub) (%s)\n", name, (unsigned int)size, s?s->logicalpath:"??");
	return 1;
}

void COM_Dir_f (void)
{
	char match[MAX_QPATH];

	Q_strncpyz(match, Cmd_Argv(1), sizeof(match));
	if (Cmd_Argc()>2)
	{
		strncat(match, "/*.", sizeof(match)-1);
		match[sizeof(match)-1] = '\0';
		strncat(match, Cmd_Argv(2), sizeof(match)-1);
		match[sizeof(match)-1] = '\0';
	}
//	else
//		strncat(match, "/*", sizeof(match)-1);

	COM_EnumerateFiles(match, COM_Dir_List, NULL);
}

/*
============
COM_Locate_f

============
*/
void COM_Locate_f (void)
{
	flocation_t loc;
	if (FS_FLocateFile(Cmd_Argv(1), FSLFRT_LENGTH, &loc)>=0)
	{
		if (!*loc.rawname)
		{
			Con_Printf("File is %i bytes compressed inside %s\n", loc.len, loc.search->logicalpath);
		}
		else
		{
			Con_Printf("Inside %s (%i bytes)\n  %s\n", loc.rawname, loc.len, loc.search->logicalpath);
		}
	}
	else
		Con_Printf("Not found\n");
}

/*
============
COM_WriteFile

The filename will be prefixed by the current game directory
============
*/
void COM_WriteFile (const char *filename, const void *data, int len)
{
	vfsfile_t *vfs;

	Sys_Printf ("COM_WriteFile: %s\n", filename);

	FS_CreatePath(filename, FS_GAMEONLY);
	vfs = FS_OpenVFS(filename, "wb", FS_GAMEONLY);
	if (vfs)
	{
		VFS_WRITE(vfs, data, len);
		VFS_CLOSE(vfs);
	}

	com_fschanged=true;
}

/*
============
COM_CreatePath

Only used for CopyFile and download
============
*/
static void	COM_CreatePath (char *path)
{
	char	*ofs;

	for (ofs = path+1 ; *ofs ; ofs++)
	{
		if (*ofs == '/')
		{	// create the directory
			*ofs = 0;
			Sys_mkdir (path);
			*ofs = '/';
		}
	}
}


/*
===========
COM_CopyFile

Copies a file over from the net to the local cache, creating any directories
needed.  This is for the convenience of developers using ISDN from home.
===========
*/
/*
static void COM_CopyFile (char *netpath, char *cachepath)
{
	FILE	*in, *out;
	int		remaining, count;
	char	buf[4096];

	remaining = COM_FileOpenRead (netpath, &in);
	COM_CreatePath (cachepath);	// create directories up to the cache file
	out = fopen(cachepath, "wb");
	if (!out)
		Sys_Error ("Error opening %s", cachepath);

	while (remaining)
	{
		if (remaining < sizeof(buf))
			count = remaining;
		else
			count = sizeof(buf);
		fread (buf, 1, count, in);
		fwrite (buf, 1, count, out);
		remaining -= count;
	}

	fclose (in);
	fclose (out);
}
//*/

int fs_hash_dups;
int fs_hash_files;

//normally the filesystem drivers pass a pre-allocated bucket and static strings to us
//the OS driver can't really be expected to track things that reliably however, so it just gives names via the stack.
//these files are grouped up to avoid excessive memory allocations.
struct fsbucketblock
{
	struct fsbucketblock *prev;
	int used;
	int total;
	qbyte data[1];
};
static struct fsbucketblock *fs_hash_filebuckets;

void FS_FlushFSHashReally(void)
{
	if (filesystemhash.numbuckets)
	{
		int i;
		for (i = 0; i < filesystemhash.numbuckets; i++)
			filesystemhash.bucket[i] = NULL;
	}

	while (fs_hash_filebuckets)
	{
		struct fsbucketblock *n = fs_hash_filebuckets->prev;
		Z_Free(fs_hash_filebuckets);
		fs_hash_filebuckets = n;
	}

	com_fschanged = true;
}
void FS_FlushFSHashWritten(void)
{
	/*automatically handled*/
}
void FS_FlushFSHashRemoved(void)
{
	FS_FlushFSHashReally();
}

static void QDECL FS_AddFileHash(int depth, const char *fname, fsbucket_t *filehandle, void *pathhandle)
{
	fsbucket_t *old;

	old = Hash_GetInsensativeBucket(&filesystemhash, fname);

	if (old)
	{
		fs_hash_dups++;
		if (depth >= old->depth)
		{
			return;
		}

		//remove the old version
		Hash_RemoveBucket(&filesystemhash, fname, &old->buck);
	}

	if (!filehandle)
	{
		int nlen = strlen(fname)+1;
		if (!fs_hash_filebuckets || fs_hash_filebuckets->used+sizeof(*filehandle)+nlen > fs_hash_filebuckets->total)
		{
			void *o = fs_hash_filebuckets;
			fs_hash_filebuckets = Z_Malloc(65536);
			fs_hash_filebuckets->total = 65536 - sizeof(*fs_hash_filebuckets);
			fs_hash_filebuckets->prev = o;
		}
		filehandle = (fsbucket_t*)(fs_hash_filebuckets->data+fs_hash_filebuckets->used);
		fs_hash_filebuckets->used += sizeof(*filehandle)+nlen;

		if (!filehandle)
			return;	//eep!
		memcpy((char*)(filehandle+1), fname, nlen);
		fname = (char*)(filehandle+1);
	}
	filehandle->depth = depth;

	Hash_AddInsensative(&filesystemhash, fname, pathhandle, &filehandle->buck);
	fs_hash_files++;
}

void FS_RebuildFSHash(void)
{
	int depth = 1;
	searchpath_t	*search;
	if (!com_fschanged)
		return;

	if (!filesystemhash.numbuckets)
	{
		filesystemhash.numbuckets = 1024;
		filesystemhash.bucket = (bucket_t**)Z_Malloc(Hash_BytesForBuckets(filesystemhash.numbuckets));
	}
	else
	{
		FS_FlushFSHashRemoved();
	}
	Hash_InitTable(&filesystemhash, filesystemhash.numbuckets, filesystemhash.bucket);

	fs_hash_dups = 0;
	fs_hash_files = 0;

	if (com_purepaths)
	{	//go for the pure paths first.
		for (search = com_purepaths; search; search = search->nextpure)
		{
			search->handle->BuildHash(search->handle, depth++, FS_AddFileHash);
		}
	}
	if (fs_puremode < 2)
	{
		for (search = com_searchpaths ; search ; search = search->next)
		{
			search->handle->BuildHash(search->handle, depth++, FS_AddFileHash);
		}
	}

	com_fschanged = false;

	Con_DPrintf("%i unique files, %i duplicates\n", fs_hash_files, fs_hash_dups);
}

/*
===========
COM_FindFile

Finds the file in the search path.
Sets com_filesize and one of handle or file
===========
*/
//if loc is valid, loc->search is always filled in, the others are filled on success.
//returns -1 if couldn't find.
int FS_FLocateFile(const char *filename, FSLF_ReturnType_e returntype, flocation_t *loc)
{
	int depth=0;
	searchpath_t	*search;
	char cleanpath[MAX_QPATH];
	flocation_t allownoloc;

	void *pf;
	unsigned int found = FF_NOTFOUND;

	if (!loc)
		loc = &allownoloc;

	loc->index = 0;
	loc->offset = 0;
	*loc->rawname = 0;
	loc->search = NULL;
	loc->len = -1;

	filename = FS_GetCleanPath(filename, cleanpath, sizeof(cleanpath));
	if (!filename)
	{
		pf = NULL;
		goto fail;
	}

	if (com_fs_cache.ival && !com_fschanged)
	{
		pf = Hash_GetInsensative(&filesystemhash, filename);
		if (!pf)
			goto fail;
	}
	else
		pf = NULL;

	if (com_purepaths && found == FF_NOTFOUND)
	{
		//check if its in one of the 'pure' packages. these override the default ones.
		for (search = com_purepaths ; search ; search = search->nextpure)
		{
			depth += ((search->flags & SPF_EXPLICIT) || returntype == FSLFRT_DEPTH_ANYPATH);
			fs_finds++;
			found = search->handle->FindFile(search->handle, loc, filename, pf);
			if (found)
			{
				search->flags |= fs_referencetype;
				loc->search = search;

				com_file_copyprotected = !!(search->flags & SPF_COPYPROTECTED);
				com_file_untrusted = !!(search->flags & SPF_UNTRUSTED);
				break;
			}
		}
	}

	if (fs_puremode < 2 && found == FF_NOTFOUND)
	{
		// optionally check the non-pure paths too.
		for (search = com_searchpaths ; search ; search = search->next)
		{
			depth += ((search->flags & SPF_EXPLICIT) || returntype == FSLFRT_DEPTH_ANYPATH);
			fs_finds++;
			found = search->handle->FindFile(search->handle, loc, filename, pf);
			if (found)
			{
				search->flags |= fs_referencetype;
				loc->search = search;
				com_file_copyprotected = !!(search->flags & SPF_COPYPROTECTED);
				com_file_untrusted = !!(search->flags & SPF_UNTRUSTED);
				break;
			}
		}
	}
fail:
	if (found == FF_SYMLINK)
	{
		static int blocklink;
		if (blocklink < 4 && loc->len < MAX_QPATH)
		{
			//read the link target
			char *s, *b;
			char targname[MAX_QPATH];
			char mergedname[MAX_QPATH];
			targname[loc->len] = 0;
			loc->search->handle->ReadFile(loc->search->handle, loc, targname);

			//properlyish unixify
			while((s = strchr(targname, '\\')))
				*s = '/';
			if (*targname == '/')
				Q_strncpyz(mergedname, targname+1, sizeof(mergedname));
			else
			{
				Q_strncpyz(mergedname, filename, sizeof(mergedname));
				while((s = strchr(mergedname, '\\')))
					*s = '/';
				b = COM_SkipPath(mergedname);
				*b = 0;
				for (s = targname; !strncmp(s, "../", 3) && b > mergedname; )
				{
					s += 3;
					if (b[-1] == '/')
						*--b = 0;
					*b = 0;
					b = strrchr(mergedname, '/');
					if (b)
						*++b = 0;
					else
					{
						//no prefix left.
						*mergedname = 0;
						break;
					}
				}
				b = mergedname + strlen(mergedname);
				Q_strncpyz(b, s, sizeof(mergedname) - (b - mergedname));
			}

			//and locate that instead.
			blocklink++;
			depth = FS_FLocateFile(mergedname, returntype, loc);
			blocklink--;
			if (!loc->search)
				Con_Printf("Symlink %s -> %s (%s) is dead\n", filename, targname, mergedname);
			return depth;
		}
	}

/*	if (len>=0)
	{
		if (loc)
			Con_Printf("Found %s:%i\n", loc->rawname, loc->len);
		else
			Con_Printf("Found %s\n", filename);
	}
	else
		Con_Printf("Failed\n");
*/
	if (returntype == FSLFRT_IFFOUND)
		return (found != FF_NOTFOUND) && (loc->len != -1);
	else if (returntype == FSLFRT_LENGTH)
	{
		if (found == FF_NOTFOUND)
			return -1;
		return loc->len;
	}
	else
	{
		if (found == FF_NOTFOUND)
			return 0x7fffffff;
		return depth;
	}
}

char *FS_WhichPackForLocation(flocation_t *loc, qboolean makereferenced)
{
	char *ret;
	if (!loc->search)
		return NULL;	//huh? not a valid location.

	ret = strchr(loc->search->purepath, '/');
	if (ret)
	{
		ret++;
		if (!strchr(ret, '/'))
		{
			if (makereferenced)
				loc->search->flags |= SPF_REFERENCED;
			return ret;
		}
	}
	return NULL;
}

/*requires extension*/
qboolean FS_GetPackageDownloadable(const char *package)
{
	searchpath_t	*search;

	for (search = com_searchpaths ; search ; search = search->next)
	{
		if (!strcmp(package, search->purepath))
			return !(search->flags & SPF_COPYPROTECTED);
	}
	return false;
}

char *FS_GetPackHashes(char *buffer, int buffersize, qboolean referencedonly)
{
	searchpath_t	*search;
	buffersize--;
	*buffer = 0;

	if (com_purepaths)
	{
		for (search = com_purepaths ; search ; search = search->nextpure)
		{
			Q_strncatz(buffer, va("%i ", search->crc_check), buffersize);
		}
		return buffer;
	}
	else
	{
		for (search = com_searchpaths ; search ; search = search->next)
		{
			if (!search->crc_check && search->handle->GeneratePureCRC)
				search->crc_check = search->handle->GeneratePureCRC(search->handle, 0, 0);
			if (search->crc_check)
			{
				Q_strncatz(buffer, va("%i ", search->crc_check), buffersize);
			}
		}
		return buffer;
	}
}
/*
referencedonly=0: show all paks
referencedonly=1: show only paks that are referenced (q3-compat)
referencedonly=2: show all paks, but paks that are referenced are prefixed with a star
ext=0: hide extensions (q3-compat)
ext=1: show extensions.
*/
char *FS_GetPackNames(char *buffer, int buffersize, int referencedonly, qboolean ext)
{
	char temp[MAX_OSPATH];
	searchpath_t	*search;
	buffersize--;
	*buffer = 0;

	if (com_purepaths)
	{
		for (search = com_purepaths ; search ; search = search->nextpure)
		{
			if (referencedonly == 0 && !(search->flags & SPF_REFERENCED))
				continue;
			if (referencedonly == 2 && (search->flags & SPF_REFERENCED))
				Q_strncatz(buffer, "*", buffersize);

			if (!ext)
			{
				COM_StripExtension(search->purepath, temp, sizeof(temp));
				Q_strncatz(buffer, va("%s ", temp), buffersize);
			}
			else
			{
				Q_strncatz(buffer, va("%s ", search->purepath), buffersize);
			}
		}
		return buffer;
	}
	else
	{
		for (search = com_searchpaths ; search ; search = search->next)
		{
			if (!search->crc_check && search->handle->GeneratePureCRC)
				search->crc_check = search->handle->GeneratePureCRC(search->handle, 0, 0);
			if (search->crc_check)
			{
				if (referencedonly == 0 && !(search->flags & SPF_REFERENCED))
					continue;
				if (referencedonly == 2 && (search->flags & SPF_REFERENCED))
				{
					// '*' prefix is meant to mean 'referenced'.
					//really all that means to the client is that it definitely wants to download it.
					//if its copyrighted, the client shouldn't try to do so, as it won't be allowed.
					if (!(search->flags & SPF_COPYPROTECTED))
						Q_strncatz(buffer, "*", buffersize);
				}

				if (!ext)
				{
					COM_StripExtension(search->purepath, temp, sizeof(temp));
					Q_strncatz(buffer, va("%s ", temp), buffersize);
				}
				else
				{
					Q_strncatz(buffer, va("%s ", search->purepath), buffersize);
				}
			}
		}
		return buffer;
	}
}

void FS_ReferenceControl(unsigned int refflag, unsigned int resetflags)
{
	searchpath_t	*s;

	refflag &= SPF_REFERENCED;
	resetflags &= SPF_REFERENCED;

	if (resetflags)
	{
		for (s=com_searchpaths ; s ; s=s->next)
		{
			s->flags &= ~resetflags;
		}
	}

	fs_referencetype = refflag;
}

//outbuf might not be written into
static const char *FS_GetCleanPath(const char *pattern, char *outbuf, int outlen)
{
	char *s;

	if (strchr(pattern, '\\'))
	{
		Q_strncpyz(outbuf, pattern, outlen);
		pattern = outbuf;

		Con_DPrintf("Warning: \\ characters in filename %s\n", pattern);
		while((s = strchr(pattern, '\\')))
		{
			*s = '/';

		}
	}

	if (strstr(pattern, "//"))
	{
		//amiga uses // as equivelent to /../
		//so strip those out
		//any other system ignores the extras

		Q_strncpyz(outbuf, pattern, outlen);
		pattern = outbuf;

		Con_DPrintf("Warning: // characters in filename %s\n", pattern);
		while ((s=strstr(pattern, "//")))
		{
			s++;
			while (*s)
			{
				*s = *(s+1);
				s++;
			}
		}
	}
	if (*pattern == '/')
	{
		/*'fix up' and ignore, compat with q3*/
		Con_DPrintf("Error: absolute path in filename %s\n", pattern);
		pattern++;
	}

	if (strstr(pattern, ".."))
		Con_Printf("Error: '..' characters in filename %s\n", pattern);
	else if (strstr(pattern, ":")) //win32 drive seperator (or mac path seperator, but / works there and they're used to it) (or amiga device separator)
		Con_Printf("Error: absolute path in filename %s\n", pattern);
	else if (strlen(pattern) > outlen)
		Con_Printf("Error: path %s too long\n", pattern);
	else
	{
		return pattern;
	}
	return NULL;
}

vfsfile_t *VFS_Filter(const char *filename, vfsfile_t *handle)
{
//	char *ext;

	if (!handle || handle->WriteBytes || handle->seekingisabadplan)	//only on readonly files
		return handle;
//	ext = COM_FileExtension (filename);
#ifdef AVAIL_ZLIB
//	if (!Q_strcasecmp(ext, ".gz"))
	{
		return FS_DecompressGZip(handle, NULL);
	}
#endif
	return handle;
}

qboolean FS_NativePath(const char *fname, enum fs_relative relativeto, char *out, int outlen)
{
	char cleanname[MAX_QPATH];
	fname = FS_GetCleanPath(fname, cleanname, sizeof(cleanname));
	if (!fname)
		return false;

	switch (relativeto)
	{
	case FS_GAMEONLY:
	case FS_GAME:
		if (*com_homedir)
			snprintf(out, outlen, "%s%s/%s", com_homedir, gamedirfile, fname);
		else
			snprintf(out, outlen, "%s%s/%s", com_quakedir, gamedirfile, fname);
		break;
	case FS_SKINS:
		//FIXME: validate that qw/ is actually loaded and valid
		if (*com_homedir)
			snprintf(out, outlen, "%sqw/skins/%s", com_homedir, fname);
		else
			snprintf(out, outlen, "%sqw/skins/%s", com_quakedir, fname);
		break;
	case FS_BINARYPATH:
		if (host_parms.binarydir && *host_parms.binarydir)
			snprintf(out, outlen, "%s%s", host_parms.binarydir, fname);
		else
			snprintf(out, outlen, "%s%s", host_parms.basedir, fname);
		break;
	case FS_ROOT:
		if (*com_homedir)
			snprintf(out, outlen, "%s%s", com_homedir, fname);
		else
			snprintf(out, outlen, "%s%s", com_quakedir, fname);
		break;
	case FS_CONFIGONLY:
		//FIXME: use the highest-precidence active system path instead
		if (*com_homedir)
			snprintf(out, outlen, "%sfte/%s", com_homedir, fname);
		else
			snprintf(out, outlen, "%sfte/%s", com_quakedir, fname);
		break;
	default:
		Sys_Error("FS_NativePath case not handled\n");
	}
	return true;
}

/*locates and opens a file*/
vfsfile_t *FS_OpenVFS(const char *filename, const char *mode, enum fs_relative relativeto)
{
	char cleanname[MAX_QPATH];
	char fullname[MAX_OSPATH];
	flocation_t loc;
	vfsfile_t *vfs;

	//eventually, this function will be the *ONLY* way to get at files

	//blanket-bans


	filename = FS_GetCleanPath(filename, cleanname, sizeof(cleanname));
	if (!filename)
		return NULL;


	if (strcmp(mode, "rb"))
		if (strcmp(mode, "r+b"))
			if (strcmp(mode, "wb"))
				if (strcmp(mode, "w+b"))
					if (strcmp(mode, "ab"))
						return NULL; //urm, unable to write/append

	//if there can only be one file (eg: write access) find out where it is.
	switch (relativeto)
	{
	case FS_GAMEONLY:	//OS access only, no paks
		if (*com_homedir)
		{
			snprintf(fullname, sizeof(fullname), "%s%s/%s", com_homedir, gamedirfile, filename);
			if (*mode == 'w')
				COM_CreatePath(fullname);
			vfs = VFSOS_Open(fullname, mode);
			if (vfs)
				return vfs;
		}
		snprintf(fullname, sizeof(fullname), "%s%s/%s", com_quakedir, gamedirfile, filename);
		if (*mode == 'w')
			COM_CreatePath(fullname);
		return VFSOS_Open(fullname, mode);
	case FS_GAME:	//load from paks in preference to system paths. overwriting be damned.
	case FS_SKINS:	//load from paks in preference to system paths. overwriting be damned.
		FS_NativePath(filename, relativeto, fullname, sizeof(fullname));
		break;
	case FS_BINARYPATH:
		if (*mode == 'w')
			COM_CreatePath(fullname);
		FS_NativePath(filename, relativeto, fullname, sizeof(fullname));
		return VFSOS_Open(fullname, mode);
	case FS_ROOT:	//always bypass packs and gamedirs
		if (*com_homedir)
		{
			snprintf(fullname, sizeof(fullname), "%s%s", com_homedir, filename);
			vfs = VFSOS_Open(fullname, mode);
			if (vfs)
				return vfs;
		}
		snprintf(fullname, sizeof(fullname), "%s%s", com_quakedir, filename);
		return VFSOS_Open(fullname, mode);
	case FS_CONFIGONLY:		//always bypass packs+pure.
		if (*com_homedir)
		{
			snprintf(fullname, sizeof(fullname), "%sfte/%s", com_homedir, filename);
			vfs = VFSOS_Open(fullname, mode);
			if (vfs)
				return vfs;
		}
		snprintf(fullname, sizeof(fullname), "%sfte/%s", com_quakedir, filename);
		return VFSOS_Open(fullname, mode);
	default:
		Sys_Error("FS_OpenVFS: Bad relative path (%i)", relativeto);
		break;
	}

	FS_FLocateFile(filename, FSLFRT_IFFOUND, &loc);

	if (loc.search)
	{
		com_file_copyprotected = !!(loc.search->flags & SPF_COPYPROTECTED);
		com_file_untrusted = !!(loc.search->flags & SPF_UNTRUSTED);
		return VFS_Filter(filename, loc.search->handle->OpenVFS(loc.search->handle, &loc, mode));
	}

	//if we're meant to be writing, best write to it.
	if (strchr(mode , 'w') || strchr(mode , 'a'))
	{
		COM_CreatePath(fullname);
		return VFSOS_Open(fullname, mode);
	}
	return NULL;
}

/*opens a vfsfile from an already discovered location*/
vfsfile_t *FS_OpenReadLocation(flocation_t *location)
{
	if (location->search)
	{
		com_file_copyprotected = !!(location->search->flags & SPF_COPYPROTECTED);
		com_file_untrusted = !!(location->search->flags & SPF_UNTRUSTED);
		return VFS_Filter(NULL, location->search->handle->OpenVFS(location->search->handle, location, "rb"));
	}
	return NULL;
}

qboolean FS_Rename2(const char *oldf, const char *newf, enum fs_relative oldrelativeto, enum fs_relative newrelativeto)
{
	char oldfullname[MAX_OSPATH];
	char newfullname[MAX_OSPATH];

	if (!FS_NativePath(oldf, oldrelativeto, oldfullname, sizeof(oldfullname)))
		return false;
	if (!FS_NativePath(newf, newrelativeto, newfullname, sizeof(newfullname)))
		return false;

	FS_CreatePath(newf, newrelativeto);
	return Sys_Rename(oldfullname, newfullname);
}
qboolean FS_Rename(const char *oldf, const char *newf, enum fs_relative relativeto)
{
	return FS_Rename2(oldf, newf, relativeto, relativeto);
}
qboolean FS_Remove(const char *fname, enum fs_relative relativeto)
{
	char fullname[MAX_OSPATH];

	if (!FS_NativePath(fname, relativeto, fullname, sizeof(fullname)))
		return false;

	return Sys_remove (fullname);
}
//create a path for the given filename (dir-only must have trailing slash)
void FS_CreatePath(const char *pname, enum fs_relative relativeto)
{
	char fullname[MAX_OSPATH];
	if (!FS_NativePath(pname, relativeto, fullname, sizeof(fullname)))
		return;

	COM_CreatePath(fullname);
}

qboolean FS_WriteFile (const char *filename, const void *data, int len, enum fs_relative relativeto)
{
	vfsfile_t *f;
	FS_CreatePath(filename, relativeto);
	f = FS_OpenVFS(filename, "wb", relativeto);
	if (!f)
		return false;
	VFS_WRITE(f, data, len);
	VFS_CLOSE(f);

	return true;
}

qboolean FS_Copy(const char *source, const char *dest, enum fs_relative relativesource, enum fs_relative relativedest)
{
	vfsfile_t *d, *s;
	char buffer[8192*8];
	int read;
	qboolean result = false;
	FS_CreatePath(dest, relativedest);
	s = FS_OpenVFS(source, "rb", relativesource);
	if (s)
	{
		d = FS_OpenVFS(dest, "wb", relativedest);
		if (d)
		{
			result = true;

			for (;;)
			{
				read = VFS_READ(s, buffer, sizeof(buffer));
				if (read <= 0)
					break;
				if (VFS_WRITE(d, buffer, read) != read)
				{
					result = false;
					break;
				}
			}

			VFS_CLOSE(d);

			if (!result)
				FS_Remove(dest, relativedest);
		}
		VFS_CLOSE(s);
	}
	return result;
}

static qbyte	*loadbuf;
static int		loadsize;

/*
============
COM_LoadFile

Filename are reletive to the quake directory.
Always appends a 0 qbyte to the loaded data.
============
*/
qbyte *COM_LoadFile (const char *path, int usehunk)
{
	vfsfile_t *f;
	qbyte *buf;
	qofs_t len;
	char	base[MAX_OSPATH];
	flocation_t loc;
	FS_FLocateFile(path, FSLFRT_LENGTH, &loc);

	if (!loc.search)
		return NULL;	//wasn't found

	if (loc.len > 0x7fffffff)	//don't malloc 5000gb sparse files or anything crazy on a 32bit system...
		return NULL;

	f = loc.search->handle->OpenVFS(loc.search->handle, &loc, "rb");
	if (!f)
		return NULL;

	com_filesize = len = VFS_GETLEN(f);
	// extract the filename base name for hunk tag
	COM_FileBase (path, base, sizeof(base));

	if (usehunk == 0)
		buf = (qbyte*)Z_Malloc (len+1);
	else if (usehunk == 2)
		buf = (qbyte*)Hunk_TempAlloc (len+1);
	else if (usehunk == 4)
	{
		if (len+1 > loadsize)
			buf = (qbyte*)Hunk_TempAlloc (len+1);
		else
			buf = loadbuf;
	}
	else if (usehunk == 5)
		buf = (qbyte*)BZ_Malloc(len+1);
	else if (usehunk == 6)
		buf = (qbyte*)Hunk_TempAllocMore (len+1);
	else
	{
		Sys_Error ("COM_LoadFile: bad usehunk");
		buf = NULL;
	}

	if (!buf)
		Sys_Error ("COM_LoadFile: not enough space for %s", path);

	((qbyte *)buf)[len] = 0;

	VFS_READ(f, buf, len);
	VFS_CLOSE(f);

	return buf;
}

qbyte *FS_LoadMallocFile (const char *path)
{
	return COM_LoadFile (path, 5);
}

void *FS_LoadMallocGroupFile(zonegroup_t *ctx, char *path)
{
	char *mem = NULL;
	vfsfile_t *f = FS_OpenVFS(path, "rb", FS_GAME);
	if (f)
	{
		int len = VFS_GETLEN(f);
		mem = ZG_Malloc(ctx, len+1);
		if (mem)
		{
			mem[len] = 0;
			if (VFS_READ(f, mem, len) == len)
				com_filesize = len;
			else
				mem = NULL;
		}

		VFS_CLOSE(f);
	}
	return mem;
}

qbyte *COM_LoadTempFile (const char *path)
{
	return COM_LoadFile (path, 2);
}
qbyte *COM_LoadTempMoreFile (const char *path)
{
	return COM_LoadFile (path, 6);
}

// uses temp hunk if larger than bufsize
qbyte *QDECL COM_LoadStackFile (const char *path, void *buffer, int bufsize)
{
	qbyte	*buf;

	loadbuf = (qbyte *)buffer;
	loadsize = bufsize;
	buf = COM_LoadFile (path, 4);

	return buf;
}


/*warning: at some point I'll change this function to return only read-only buffers*/
qofs_t FS_LoadFile(char *name, void **file)
{
	*file = FS_LoadMallocFile(name);
	if (!*file)
		return (qofs_t)-1;
	return com_filesize;
}
void FS_FreeFile(void *file)
{
	BZ_Free(file);
}



void COM_EnumerateFiles (const char *match, int (QDECL *func)(const char *, qofs_t, void *, searchpathfuncs_t*), void *parm)
{
	searchpath_t    *search;
	for (search = com_searchpaths; search ; search = search->next)
	{
	// is the element a pak file?
		if (!search->handle->EnumerateFiles(search->handle, match, func, parm))
			break;
	}
}

void COM_FlushTempoaryPacks(void)
{
	searchpath_t *sp, **link;
	link = &com_searchpaths;
	while (*link)
	{
		sp = *link;
		if (sp->flags & SPF_TEMPORARY)
		{
			FS_FlushFSHashReally();

			*link = sp->next;

			sp->handle->ClosePath(sp->handle);
			Z_Free (sp);
		}
		else
			link = &sp->next;
	}
	com_purepaths = NULL;
}

qboolean COM_LoadMapPackFile (const char *filename, qofs_t ofs)
{
	return false;
}

static searchpath_t *FS_AddPathHandle(searchpath_t **oldpaths, const char *purepath, const char *probablepath, searchpathfuncs_t *handle, unsigned int flags, unsigned int loadstuff);
searchpathfuncs_t *FS_GetOldPath(searchpath_t **oldpaths, const char *dir, unsigned int *keepflags)
{
	searchpath_t *p;
	searchpathfuncs_t *r = NULL;
	*keepflags = 0;
	while(*oldpaths)
	{
		p = *oldpaths;

		if (!Q_strcasecmp(p->logicalpath, dir))
		{
			*keepflags |= p->flags & (SPF_REFERENCED | SPF_UNTRUSTED);
			*oldpaths = p->next;
			r = p->handle;
			Z_Free(p);
			break;
		}

		oldpaths = &(*oldpaths)->next;
	}
	return r;
}

typedef struct {
	searchpathfuncs_t *(QDECL *OpenNew)(vfsfile_t *file, const char *desc);
	searchpath_t **oldpaths;
	const char *parentdesc;
	const char *puredesc;
} wildpaks_t;

static int QDECL FS_AddWildDataFiles (const char *descriptor, qofs_t size, void *vparam, searchpathfuncs_t *funcs)
{
	wildpaks_t *param = vparam;
	vfsfile_t *vfs;
	searchpath_t	*search;
	searchpathfuncs_t	*newpak;
	char			pakfile[MAX_OSPATH];
	char			purefile[MAX_OSPATH];
	flocation_t loc;
	unsigned int keptflags = 0;

	Q_snprintfz (pakfile, sizeof(pakfile), "%s%s", param->parentdesc, descriptor);

	for (search = com_searchpaths; search; search = search->next)
	{
		if (!Q_strcasecmp(search->logicalpath, pakfile))	//assumption: first member of structure is a char array
			return true; //already loaded (base paths?)
	}

	newpak = FS_GetOldPath(param->oldpaths, pakfile, &keptflags);
	if (!newpak)
	{
		if (param->OpenNew == VFSOS_OpenPath)
		{
			vfs = NULL;
		}
		else
		{
			fs_finds++;
			if (!funcs->FindFile(funcs, &loc, descriptor, NULL))
				return true;	//not found..
			vfs = funcs->OpenVFS(funcs, &loc, "rb");
			if (!vfs)
				return true;
		}
		newpak = param->OpenNew (vfs, pakfile);
		if (!newpak)
		{
			VFS_CLOSE(vfs);
			return true;
		}
	}

	Q_snprintfz (pakfile, sizeof(pakfile), "%s%s", param->parentdesc, descriptor);
	if (*param->puredesc)
		snprintf (purefile, sizeof(purefile), "%s/%s", param->puredesc, descriptor);
	else
		Q_strncpyz(purefile, descriptor, sizeof(purefile));
	FS_AddPathHandle(param->oldpaths, purefile, pakfile, newpak, ((!Q_strncasecmp(descriptor, "pak", 3))?SPF_COPYPROTECTED:0)|keptflags, (unsigned int)-1);

	return true;
}


static void FS_AddDataFiles(searchpath_t **oldpaths, const char *purepath, const char *logicalpath, searchpath_t *search, const char *extension, searchpathfuncs_t *(QDECL *OpenNew)(vfsfile_t *file, const char *desc))
{
	//search is the parent
	int				i;
	searchpathfuncs_t	*handle;
	char			pakfile[MAX_OSPATH];
	char			logicalpaths[MAX_OSPATH];	//with a slash
	char			purefile[MAX_OSPATH];
	unsigned int	keptflags;
	vfsfile_t *vfs;
	flocation_t loc;
	wildpaks_t wp;

	Q_strncpyz(logicalpaths, logicalpath, sizeof(logicalpaths));
	FS_CleanDir(logicalpaths, sizeof(logicalpaths));

	//first load all the numbered pak files
	for (i=0 ; ; i++)
	{
		snprintf (pakfile, sizeof(pakfile), "pak%i.%s", i, extension);
		fs_finds++;
		if (!search->handle->FindFile(search->handle, &loc, pakfile, NULL))
			break;	//not found..

		snprintf (pakfile, sizeof(pakfile), "%spak%i.%s", logicalpaths, i, extension);
		snprintf (purefile, sizeof(purefile), "%s/pak%i.%s", purepath, i, extension);

		handle = FS_GetOldPath(oldpaths, pakfile, &keptflags);
		if (!handle)
		{
			vfs = search->handle->OpenVFS(search->handle, &loc, "r");
			if (!vfs)
				break;
			handle = OpenNew (vfs, pakfile);
			if (!handle)
				break;
		}
		FS_AddPathHandle(oldpaths, purefile, pakfile, handle, SPF_COPYPROTECTED|keptflags, (unsigned int)-1);
	}

	//now load the random ones
	Q_snprintfz (pakfile, sizeof(pakfile), "*.%s", extension);
	wp.OpenNew = OpenNew;
	wp.parentdesc = logicalpaths;
	wp.puredesc = purepath;
	wp.oldpaths = oldpaths;
	search->handle->EnumerateFiles(search->handle, pakfile, FS_AddWildDataFiles, &wp);


	//and load any named in the manifest (this happens when they're crced or whatever)
	{
		int ptlen, palen;
		ptlen = strlen(purepath);
		for (i = 0; i < sizeof(fs_manifest->package) / sizeof(fs_manifest->package[0]); i++)
		{
			if (fs_manifest->package[i].path && !strcmp(COM_FileExtension(fs_manifest->package[i].path), extension))
			{
				palen = strlen(fs_manifest->package[i].path);
				if (palen > ptlen && (fs_manifest->package[i].path[ptlen] == '/' || fs_manifest->package[i].path[ptlen] == '\\' )&& !strncmp(purepath, fs_manifest->package[i].path, ptlen))
				{
					searchpath_t *oldp;
					char pname[MAX_OSPATH];
					char lname[MAX_OSPATH];
					if (fs_manifest->package[i].crcknown)
						snprintf(lname, sizeof(lname), "%#x", fs_manifest->package[i].crc);
					else
						snprintf(lname, sizeof(lname), "");
					if (!FS_GenCachedPakName(fs_manifest->package[i].path, lname, pname, sizeof(pname)))
						continue;
					snprintf (lname, sizeof(lname), "%s%s", logicalpaths, pname+ptlen+1);

					for (oldp = com_searchpaths; oldp; oldp = oldp->next)
					{
						if (!Q_strcasecmp(oldp->purepath, fs_manifest->package[i].path))
							break;
						if (!Q_strcasecmp(oldp->logicalpath, lname))
							break;
					}
					if (!oldp)
					{
						handle = FS_GetOldPath(oldpaths, lname, &keptflags);
						if (!handle)
						{
							if (search->handle->FindFile(search->handle, &loc, pname+ptlen+1, NULL))
							{
								vfs = search->handle->OpenVFS(search->handle, &loc, "r");
								if (vfs)
									handle = OpenNew (vfs, lname);
							}
						}
						if (handle && fs_manifest->package[i].crcknown)
						{
							int truecrc = handle->GeneratePureCRC(handle, 0, false);
							if (truecrc != fs_manifest->package[i].crc)
							{
								Con_Printf(CON_ERROR "File \"%s\" has hash %#x (required: %#x). Please delete it or move it away\n", lname, truecrc, fs_manifest->package[i].crc);
								handle->ClosePath(handle);
								handle = NULL;
							}
						}
						if (handle)
							FS_AddPathHandle(oldpaths, fs_manifest->package[i].path, lname, handle, SPF_COPYPROTECTED|SPF_UNTRUSTED|keptflags, (unsigned int)-1);
					}
				}
			}
		}
	}
}

static searchpath_t *FS_AddPathHandle(searchpath_t **oldpaths, const char *purepath, const char *logicalpath, searchpathfuncs_t *handle, unsigned int flags, unsigned int loadstuff)
{
	unsigned int i;

	searchpath_t *search, **link;

	if (!handle)
	{
		Con_Printf("COM_AddPathHandle: not a valid handle (%s)\n", logicalpath);
		return NULL;
	}

	if (handle->fsver != FSVER)
	{
		Con_Printf("%s: file system driver is outdated (%u should be %u)\n", logicalpath, handle->fsver, FSVER);
		handle->ClosePath(handle);
		return NULL;
	}

	search = (searchpath_t*)Z_Malloc (sizeof(searchpath_t));
	search->flags = flags;
	search->handle = handle;
	Q_strncpyz(search->purepath, purepath, sizeof(search->purepath));
	Q_strncpyz(search->logicalpath, logicalpath, sizeof(search->logicalpath));

	//temp packages also do not nest
	if (!(flags & SPF_TEMPORARY))
	{
		for (i = 0; i < sizeof(searchpathformats)/sizeof(searchpathformats[0]); i++)
		{
			if (!searchpathformats[i].extension || !searchpathformats[i].OpenNew || !searchpathformats[i].loadscan)
				continue;
			if (loadstuff & (1<<i))
			{
				FS_AddDataFiles(oldpaths, purepath, logicalpath, search, searchpathformats[i].extension, searchpathformats[i].OpenNew);
			}
		}
	}

	if (flags & SPF_TEMPORARY)
	{
		//add at end. pureness will reorder if needed.
		link = &com_searchpaths;
		while(*link)
		{
			link = &(*link)->next;
		}
		*link = search;
	}
	else
	{
		search->next = com_searchpaths;
		com_searchpaths = search;
	}
	com_fschanged = true;

	return search;
}

void COM_RefreshFSCache_f(void)
{
	com_fschanged=true;
}

void COM_FlushFSCache(void)
{
	searchpath_t *search;
	if (com_fs_cache.ival && com_fs_cache.ival != 2)
	{
		for (search = com_searchpaths ; search ; search = search->next)
		{
			if (search->handle->PollChanges)
				com_fschanged |= search->handle->PollChanges(search->handle);
		}
	}

	//rebuild it if needed
	FS_RebuildFSHash();
}

/*since should start as 0, otherwise this can be used to poll*/
qboolean FS_Restarted(unsigned int *since)
{
	if (*since < fs_restarts)
	{
		*since = fs_restarts;
		return true;
	}
	return false;
}

/*
================
FS_AddGameDirectory

Sets com_gamedir, adds the directory to the head of the path,
then loads and adds pak1.pak pak2.pak ...
================
*/
void FS_AddGameDirectory (searchpath_t **oldpaths, const char *puredir, const char *dir, unsigned int loadstuff)
{
	unsigned int	keptflags;
	searchpath_t	*search;

	char			*p;
	void			*handle;

	fs_restarts++;

	if ((p = strrchr(dir, '/')) != NULL)
		strcpy(gamedirfile, ++p);
	else
		strcpy(gamedirfile, dir);

	for (search = com_searchpaths; search; search = search->next)
	{
		if (!Q_strcasecmp(search->logicalpath, dir))
			return; //already loaded (base paths?)
	}

//
// add the directory to the search path
//
	handle = FS_GetOldPath(oldpaths, dir, &keptflags);
	if (!handle)
		handle = VFSOS_OpenPath(NULL, dir);

	FS_AddPathHandle(oldpaths, puredir, dir, handle, SPF_EXPLICIT|keptflags, loadstuff);
}

searchpathfuncs_t *COM_IteratePaths (void **iterator, char *buffer, int buffersize)
{
	searchpath_t	*s;
	void			*prev;

	prev = NULL;
	for (s=com_searchpaths ; s ; s=s->next)
	{
		if (!(s->flags & SPF_EXPLICIT))
			continue;

		if (*iterator == prev)
		{
			*iterator = s->handle;
			Q_strncpyz(buffer, s->logicalpath, buffersize-1);
			FS_CleanDir(buffer, buffersize);
			return s->handle;
		}
		prev = s->handle;
	}

	*iterator = NULL;
	*buffer = 0;
	return NULL;
}

char *FS_GetGamedir(void)
{
	return gamedirfile;
}

//given a 'c:/foo/bar/' path, will extract 'bar'.
void FS_ExtractDir(char *in, char *out, int outlen)
{
	char *end;
	if (!outlen)
		return;
	end = in + strlen(in);
	//skip over any trailing slashes
	while (end > in)
	{
		if (end[-1] == '/' || end[-1] == '\\')
			end--;
		else
			break;
	}

	//skip over the path
	while (end > in)
	{
		if (end[-1] != '/' && end[-1] != '\\')
			end--;
		else
			break;
	}

	//copy string into the dest
	while (--outlen)
	{
		if (*end == '/' || *end == '\\' || !*end)
			break;
		*out++ = *end++;
	}
	*out = 0;
}

/*
================
COM_Gamedir

Sets the gamedir and path to a different directory.
================
*/
void COM_Gamedir (const char *dir)
{
	ftemanifest_t *man;
	if (!fs_manifest)
		FS_ChangeGame(NULL, true);

	//don't allow leading dots, hidden files are evil.
	//don't allow complex paths. those are evil too.
	if (!*dir || *dir == '.' || !strcmp(dir, ".") || strstr(dir, "..") || strstr(dir, "/")
		|| strstr(dir, "\\") || strstr(dir, ":") )
	{
		Con_Printf ("Gamedir should be a single filename, not a path\n");
		return;
	}

	man = FS_Manifest_Clone(fs_manifest);
	FS_Manifest_PurgeGamedirs(man);
	if (*dir)
	{
		char *dup = Z_StrDup(dir);
		dir = dup;
		while ((dir = COM_ParseStringSet(dir)))
		{
			if (!strcmp(dir, ";"))
				continue;
			if (!*com_token)
				continue;

			Cmd_TokenizeString(va("gamedir \"%s\"", com_token), false, false);
			FS_Manifest_ParseTokens(man);
		}
		Z_Free(dup);
	}
	FS_ChangeGame(man, cfg_reload_on_gamedir.ival);

#if 0
	char thispath[64];
	searchpath_t	*next;
	qboolean isbase;

	//don't allow leading dots, hidden files are evil.
	//don't allow complex paths. those are evil too.
	if (!*dir || *dir == '.' || !strcmp(dir, ".") || strstr(dir, "..") || strstr(dir, "/")
		|| strstr(dir, "\\") || strstr(dir, ":") )
	{
		Con_TPrintf (TL_GAMEDIRAINTPATH);
		return;
	}

	isbase = false;
	for (next = com_searchpaths; next; next = next->next)
	{
		if (next == com_base_searchpaths)
			isbase = true;

		if (next->funcs == &osfilefuncs)
		{
			FS_CleanDir(next->purepath, thispath, sizeof(thispath));
			if (!strcmp(dir, thispath))
			{
				if (isbase && com_searchpaths == com_base_searchpaths)
				{
					Q_strncpyz (gamedirfile, dir, sizeof(gamedirfile));
					return;
				}
				if (!isbase)
					return;
				break;
			}
		}
	}

	FS_ForceToPure(NULL, NULL, 0);

#ifndef SERVERONLY
//	Host_WriteConfiguration();	//before we change anything.
#endif

	Q_strncpyz (gamedirfile, dir, sizeof(gamedirfile));

#ifndef CLIENTONLY
	sv.gamedirchanged = true;
#endif
#ifndef SERVERONLY
	cl.gamedirchanged = true;
#endif

	FS_FlushFSHashReally();

	//
	// free up any current game dir info
	//
	while (com_searchpaths != com_base_searchpaths)
	{
		com_searchpaths->handle->ClosePath(com_searchpaths->handle);
		next = com_searchpaths->next;
		Z_Free (com_searchpaths);
		com_searchpaths = next;
	}

	com_fschanged = true;

	//
	// flush all data, so it will be forced to reload
	//
	Cache_Flush ();

	if (strchr(dir, ';'))
	{
		//separate case because parsestringset splits by whitespace too
		while ((dir = COM_ParseStringSet(dir)))
		{
			if (!strcmp(dir, ";"))
				continue;
			if (!*dir)
				continue;

			FS_AddGameDirectory(dir, va("%s%s", com_quakedir, com_token), ~0);
			if (*com_homedir)
				FS_AddGameDirectory(dir, va("%s%s", com_homedir, com_token), ~0);
		}
	}
	else
	{
		FS_AddGameDirectory(dir, va("%s%s", com_quakedir, dir), ~0);
		if (*com_homedir)
			FS_AddGameDirectory(dir, va("%s%s", com_homedir, dir), ~0);
	}


#ifndef SERVERONLY
	if (!isDedicated)
	{
//		if (qrenderer != QR_NONE)	//only do this if we have already started the renderer
//			Cbuf_InsertText("vid_restart\n", RESTRICT_LOCAL);


		if (COM_FDepthFile("config.cfg", true) <= (*com_homedir?1:0))
		{
			Cbuf_InsertText("cl_warncmd 0\n"
							"exec config.cfg\n"
							"exec fte.cfg\n"
							"cl_warncmd 1\n", RESTRICT_LOCAL, false);
		}
	}

	Shader_Init();	//FIXME!

	COM_Effectinfo_Clear();

	Validation_FlushFileList();	//prevent previous hacks from making a difference.

	//FIXME: load new palette, if different cause a vid_restart.

#endif
#endif
}

#define QCFG "set allow_download_refpackages 0\n"
/*stuff that makes dp-only mods work a bit better*/
#define DPCOMPAT QCFG "set _cl_playermodel \"\"\n set dpcompat_set 1\n set dpcompat_trailparticles 1\nset dpcompat_corruptglobals 1\nset vid_pixelheight 1\n"
/*nexuiz/xonotic has a few quirks/annoyances...*/
#define NEXCFG DPCOMPAT "set r_particlesdesc effectinfo\nset sv_bigcoords 1\nset sv_maxairspeed \"400\"\nset sv_jumpvelocity 270\nset sv_mintic \"0.01\"\ncl_nolerp 0\npr_enable_uriget 0\n"
/*some modern non-compat settings*/
#define DMFCFG "set com_parseutf8 1\npm_airstep 1\nsv_demoExtensions 1\n"
/*set some stuff so our regular qw client appears more like hexen2*/
#define HEX2CFG "set com_parseutf8 -1\nset gl_font gfx/hexen2\nset in_builtinkeymap 0\nset_calc cl_playerclass int (random * 5) + 1\nset sv_maxspeed 640\nset watervis 1\nset r_wateralpha 0.5\nset sv_pupglow 1\nset cl_model_bobbing 1\nsv_sound_land \"fx/thngland.wav\"\n"
/*yay q2!*/
#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"

typedef struct {
	const char *argname;	//used if this was used as a parameter.
	const char *exename;	//used if the exe name contains this
	const char *protocolname;	//sent to the master server when this is the current gamemode (Typically set for DP compat).
	const char *auniquefile[4];	//used if this file is relative from the gamedir. needs just one file

	const char *customexec;

	const char *dir[4];
	const char *poshname;	//Full name for the game.
	const char *manifestfile;
} gamemode_info_t;
const gamemode_info_t gamemode_info[] = {
#define MASTER_PREFIX "FTE-"
//note that there is no basic 'fte' gamemode, this is because we aim for network compatability. Darkplaces-Quake is the closest we get.
//this is to avoid having too many gamemodes anyway.

//mission packs should generally come after the main game to avoid prefering the main game. we violate this for hexen2 as the mission pack is mostly a superset.
//whereas the quake mission packs replace start.bsp making the original episodes unreachable.
//for quake, we also allow extracting all files from paks. some people think it loads faster that way or something.

	//cmdline switch exename    protocol name(dpmaster)  identifying file		exec     dir1       dir2    dir3       dir(fte)     full name
	{"-quake",		"q1",		MASTER_PREFIX"Quake",	{"id1/pak0.pak",
														 "id1/quake.rc"},		QCFG,	{"id1",		"qw",				"fte"},		"Quake"/*,    "id1/pak0.pak|http://quakeservers.nquake.com/qsw106.zip|http://nquake.localghost.net/qsw106.zip|http://qw.quakephil.com/nquake/qsw106.zip|http://fnu.nquake.com/qsw106.zip"*/},
	{"-hipnotic",	"hipnotic",	MASTER_PREFIX"Hipnotic",{"hipnotic/pak0.pak"},	QCFG,	{"id1",		"qw",	"hipnotic",	"fte"},		"Quake: Scourge of Armagon"},
	{"-rogue",		"rogue",	MASTER_PREFIX"Rogue",	{"rogue/pak0.pak"},		QCFG,	{"id1",		"qw",	"rogue",	"fte"},		"Quake: Dissolution of Eternity"},
	{"-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"},

	//supported commercial mods (some are currently only partially supported)
	{"-portals",	"h2mp",		"FTE-H2MP",				{"portals/hexen.rc",
														 "portals/pak3.pak"},	HEX2CFG,{"data1",	"portals",			"fteh2"},	"Hexen II MP"},
	{"-hexen2",		"hexen2",	"FTE-Hexen2",			{"data1/pak0.pak"},		HEX2CFG,{"data1",						"fteh2"},	"Hexen II"},
	{"-quake2",		"q2",		"FTE-Quake2",			{"baseq2/pak0.pak"},	Q2CFG,	{"baseq2",						"fteq2"},	"Quake II"},
	{"-quake3",		"q3",		"FTE-Quake3",			{"baseq3/pak0.pk3"},	Q3CFG,	{"baseq3",						"fteq3"},	"Quake III Arena"},

	//can run in windows, needs 
	{"-halflife",	"hl",		"FTE-HalfLife",			{"valve/liblist.gam"},	NULL,	{"valve",						"ftehl"},	"Half-Life"},

	//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",			"et",		"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"},

	{"-doom",		"doom",		"FTE-Doom",				{"doom.wad"},			NULL,	{"*doom.wad",					"ftedoom"},	"Doom"},
	{"-doom2",		"doom2",	"FTE-Doom2",			{"doom2.wad"},			NULL,	{"*doom2.wad",					"ftedoom"},	"Doom2"},
	{"-doom3",		"doom3",	"FTE-Doom3",			{"doom3.wad"},			NULL,	{"based3",						"ftedoom3"},"Doom3"},

	//for the luls
	{"-diablo2",	NULL,		"FTE-Diablo2",			{"d2music.mpq"},		NULL,	{"**.mpq",						"fted2"},	"Diablo 2"},

	{NULL}
};

qboolean FS_GenCachedPakName(char *pname, char *crc, char *local, int llen)
{
	char *fn;
	char hex[16];
	if (strstr(pname, "dlcache"))
	{
		*local = 0;
		return false;
	}

	fn = COM_SkipPath(pname);
	if (fn == pname)
	{	//only allow it if it has some game path first.
		*local = 0;
		return false;
	}
	Q_strncpyz(local, pname, min((fn - pname) + 1, llen));
	Q_strncatz(local, "dlcache/", llen);
	Q_strncatz(local, fn, llen);
	if (*crc)
	{
		Q_strncatz(local, ".", llen);
		snprintf(hex, sizeof(hex), "%x", (unsigned int)strtoul(crc, NULL, 0));
		Q_strncatz(local, hex, llen);
	}
	return true;
}

#if 0
qboolean FS_LoadPackageFromFile(vfsfile_t *vfs, char *pname, char *localname, int *crc, unsigned int flags)
{
	int i;
	char *ext = COM_FileExtension(pname);
	searchpathfuncs_t *handle;
	searchpath_t *oldlist = NULL;

	searchpath_t *sp;

	for (i = 0; i < sizeof(searchpathformats)/sizeof(searchpathformats[0]); i++)
	{
		if (!searchpathformats[i].extension || !searchpathformats[i].OpenNew)
			continue;
		if (!strcmp(ext, searchpathformats[i].extension))
		{
			handle = searchpathformats[i].OpenNew (vfs, localname);
			if (!handle)
			{
				Con_Printf("file %s isn't a %s after all\n", pname, searchpathformats[i].extension);
				break;
			}
			if (crc)
			{
				int truecrc = handle->GeneratePureCRC(handle, 0, false);
				if (truecrc != *crc)
				{
					*crc = truecrc;
					VFS_CLOSE(vfs);
					return false;
				}
			}
			sp = FS_AddPathHandle(&oldlist, pname, localname, handle, flags, (unsigned int)-1);

			if (sp)
			{
				FS_FlushFSHashReally();
				return true;
			}
		}
	}

	VFS_CLOSE(vfs);
	return false;
}
#endif

void FS_PureMode(int puremode, char *packagenames, char *packagecrcs, int pureseed)
{
	qboolean pureflush;

	Z_Free(fs_purenames);
	Z_Free(fs_purecrcs);

	pureflush = (fs_puremode != 2 && puremode == 2);
	fs_puremode = puremode;
	fs_purenames = packagenames?Z_StrDup(packagenames):NULL;
	fs_purecrcs = packagecrcs?Z_StrDup(packagecrcs):NULL;
	fs_pureseed = pureseed;

	FS_ChangeGame(fs_manifest, false);

	if (pureflush)
	{
#ifndef SERVERONLY
		Shader_NeedReload(true);
#endif
		Mod_ClearAll();
		Cache_Flush();
	}
}

char *FSQ3_GenerateClientPacksList(char *buffer, int maxlen, int basechecksum)
{	//this is for q3 compatibility.

	flocation_t loc;
	int numpaks = 0;
	searchpath_t *sp;

	FS_FLocateFile("vm/cgame.qvm", FSLFRT_LENGTH, &loc);
	Q_strncatz(buffer, va("%i ", loc.search->crc_reply), maxlen);
	basechecksum ^= loc.search->crc_reply;

	FS_FLocateFile("vm/ui.qvm", FSLFRT_LENGTH, &loc);
	Q_strncatz(buffer, va("%i ", loc.search->crc_reply), maxlen);
	basechecksum ^= loc.search->crc_reply;

	Q_strncatz(buffer, "@ ", maxlen);

	for (sp = com_purepaths; sp; sp = sp->nextpure)
	{
		if (sp->crc_reply)
		{
			Q_strncatz(buffer, va("%i ", sp->crc_reply), maxlen);
			basechecksum ^= sp->crc_reply;
			numpaks++;
		}
	}

	basechecksum ^= numpaks;
	Q_strncatz(buffer, va("%i ", basechecksum), maxlen);

	return buffer;
}

/*
================
FS_ReloadPackFiles
================

Called when the client has downloaded a new pak/pk3 file
*/
void FS_ReloadPackFilesFlags(unsigned int reloadflags)
{
	searchpath_t	*oldpaths;
	searchpath_t	*next;
	int i;

	FS_FlushFSHashReally();

	oldpaths = com_searchpaths;
	com_searchpaths = NULL;
	com_purepaths = NULL;
	com_base_searchpaths = NULL;

	for (i = 0; i < sizeof(fs_manifest->gamepath) / sizeof(fs_manifest->gamepath[0]); i++)
	{
		char *dir = fs_manifest->gamepath[i].path;
		if (dir && fs_manifest->gamepath[i].base)
		{
			//don't allow leading dots, hidden files are evil.
			//don't allow complex paths. those are evil too.
			if (!*dir || *dir == '.' || !strcmp(dir, ".") || strstr(dir, "..") || strstr(dir, "/")
				|| strstr(dir, "\\") || strstr(dir, ":") )
			{
				Con_Printf ("Gamedir should be a single filename, not a path\n");
				continue;
			}

			//paths with '*' actually result in loading packages without an actual gamedir. note that this does not imply that we can write anything.
			if (*dir == '*')
			{
				int j;
				searchpathfuncs_t *handle = VFSOS_OpenPath(NULL, com_quakedir);
				searchpath_t *search = (searchpath_t*)Z_Malloc (sizeof(searchpath_t));
				search->flags = 0;
				search->handle = handle;
				Q_strncpyz(search->purepath, "", sizeof(search->purepath));
				Q_strncpyz(search->logicalpath, com_quakedir, sizeof(search->logicalpath));

				for (j = 0; j < sizeof(searchpathformats)/sizeof(searchpathformats[0]); j++)
				{
					if (!searchpathformats[j].extension || !searchpathformats[j].OpenNew || !searchpathformats[j].loadscan)
						continue;
					if (reloadflags & (1<<j))
					{
						FS_AddDataFiles(&oldpaths, search->purepath, search->logicalpath, search, searchpathformats[j].extension, searchpathformats[j].OpenNew);
					}
				}
				handle->ClosePath(handle);
				Z_Free(search);
			}
			else
			{
				FS_AddGameDirectory(&oldpaths, dir, va("%s%s", com_quakedir, dir), reloadflags);
				if (*com_homedir)
					FS_AddGameDirectory(&oldpaths, dir, va("%s%s", com_homedir, dir), reloadflags);
			}
		}
	}
	com_base_searchpaths = com_searchpaths;
	for (i = 0; i < sizeof(fs_manifest->gamepath) / sizeof(fs_manifest->gamepath[0]); i++)
	{
		char *dir = fs_manifest->gamepath[i].path;
		if (dir && !fs_manifest->gamepath[i].base)
		{
			//don't allow leading dots, hidden files are evil.
			//don't allow complex paths. those are evil too.
			if (!*dir || *dir == '.' || !strcmp(dir, ".") || strstr(dir, "..") || strstr(dir, "/")
				|| strstr(dir, "\\") || strstr(dir, ":") )
			{
				Con_Printf ("Gamedir should be a single filename, not a path\n");
				continue;
			}

			if (*dir == '*')
			{
			}
			else
			{
				FS_AddGameDirectory(&oldpaths, dir, va("%s%s", com_quakedir, dir), reloadflags);
				if (*com_homedir)
					FS_AddGameDirectory(&oldpaths, dir, va("%s%s", com_homedir, dir), reloadflags);
			}
		}
	}

	/*sv_pure: Reload pure paths*/
	if (fs_purenames && fs_purecrcs)
	{
		char crctok[64];
		char nametok[MAX_QPATH];
		searchpath_t *sp, *lastpure = NULL;
		char *names = fs_purenames, *pname;
		char *crcs = fs_purecrcs;
		int crc;

		for (sp = com_searchpaths; sp; sp = sp->next)
		{
			if (sp->handle->GeneratePureCRC)
			{
				sp->nextpure = (void*)0x1;
				sp->crc_check = sp->handle->GeneratePureCRC(sp->handle, fs_pureseed, 0);
				sp->crc_reply = sp->handle->GeneratePureCRC(sp->handle, fs_pureseed, 1);
			}
			else
			{
				sp->nextpure = NULL;
				sp->crc_check = 0;
				sp->crc_reply = 0;
			}
		}

		while(names && crcs)
		{
			crcs = COM_ParseOut(crcs, crctok, sizeof(crctok));
			names = COM_ParseOut(names, nametok, sizeof(nametok));

			crc = strtoul(crctok, NULL, 0);
			if (!crc)
				continue;

			pname = nametok;
			if (*pname == '*')	// * means that its 'referenced' (read: actually useful) thus should be downloaded, which is not relevent here.
				pname++;

			for (sp = com_searchpaths; sp; sp = sp->next)
			{
				if (sp->nextpure == (void*)0x1)	//don't add twice.
					if (sp->crc_check == crc)
					{
						if (fs_puremode)
						{
							if (lastpure)
								lastpure->nextpure = sp;
							else
								com_purepaths = sp;
							sp->nextpure = NULL;
							lastpure = sp;
						}
						break;
					}
			}
			if (!fs_puremode && !sp)
			{	//if we're not pure, we don't care if the version differs. don't load the server's version.
				//this works around 1.01 vs 1.06 issues.
				for (sp = com_searchpaths; sp; sp = sp->next)
				{
					if (!Q_strcasecmp(pname, sp->purepath))
						break;
				}
			}
			//if its not already loaded (via wildcards), load it from the download cache, if we can
			if (!sp)
			{
				char local[MAX_OSPATH];
				vfsfile_t *vfs;
				char *ext = COM_FileExtension(pname);
				void *handle;
				int i;

				if (FS_GenCachedPakName(pname, va("%i", crc), local, sizeof(local)))
					vfs = FS_OpenVFS(local, "rb", FS_ROOT);
				else
					vfs = NULL;
				if (vfs)
				{
					for (i = 0; i < sizeof(searchpathformats)/sizeof(searchpathformats[0]); i++)
					{
						if (!searchpathformats[i].extension || !searchpathformats[i].OpenNew)
							continue;
						if (!strcmp(ext, searchpathformats[i].extension))
						{
							handle = searchpathformats[i].OpenNew (vfs, local);
							if (!handle)
								break;
							sp = FS_AddPathHandle(&oldpaths, pname, local, handle, SPF_COPYPROTECTED|SPF_TEMPORARY, (unsigned int)-1);

							sp->crc_check = sp->handle->GeneratePureCRC(sp->handle, fs_pureseed, 0);
							sp->crc_reply = sp->handle->GeneratePureCRC(sp->handle, fs_pureseed, 1);

							if (sp->crc_check == crc)
							{
								if (fs_puremode)
								{
									if (lastpure)
										lastpure->nextpure = sp;
									else
										com_purepaths = sp;
									sp->nextpure = NULL;
									lastpure = sp;
								}
							}
							break;
						}
					}
				}

				if (!sp)
					Con_DPrintf("Pure crc %i wasn't found\n", crc);
			}
		}
	}

	while(oldpaths)
	{
		next = oldpaths->next;

		Con_Printf("%s is no longer needed\n", oldpaths->logicalpath);
		oldpaths->handle->ClosePath(oldpaths->handle);
		Z_Free(oldpaths);
		oldpaths = next;
	}
}

void FS_UnloadPackFiles(void)
{
	FS_ReloadPackFilesFlags(1);
}

void FS_ReloadPackFiles(void)
{
	FS_ReloadPackFilesFlags(~0);
}

void FS_ReloadPackFiles_f(void)
{
	if (atoi(Cmd_Argv(1)))
		FS_ReloadPackFilesFlags(atoi(Cmd_Argv(1)));
	else
		FS_ReloadPackFilesFlags(~0);
}

#ifdef _WIN32
#include <windows.h>
#ifdef MINGW
#define byte BYTE	//some versions of mingw headers are broken slightly. this lets it compile.
#endif
#include <shlobj.h>
static qboolean Sys_SteamHasFile(char *basepath, int basepathlen, char *steamdir, char *fname)
{
	/*
	Find where Valve's Steam distribution platform is installed.
	Then take a look at that location for the relevent installed app.
	*/
	FILE *f;
	DWORD resultlen;
	HKEY key = NULL;
	
	if (RegOpenKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\Valve\\Steam", 0, STANDARD_RIGHTS_READ|KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
	{
		resultlen = basepathlen;
		RegQueryValueEx(key, "SteamPath", NULL, NULL, basepath, &resultlen);
		RegCloseKey(key);
		Q_strncatz(basepath, va("/SteamApps/common/%s", steamdir), basepathlen);
		if ((f = fopen(va("%s/%s", basepath, fname), "rb")))
		{
			fclose(f);
			return true;
		}
	}
	return false;
}
qboolean Sys_FindGameData(const char *poshname, const char *gamename, char *basepath, int basepathlen)
{
	DWORD resultlen;
	HKEY key = NULL;

#ifndef INVALID_FILE_ATTRIBUTES
	#define INVALID_FILE_ATTRIBUTES ((DWORD)-1)
#endif

	//first, try and find it in our game paths location
	if (RegOpenKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\" FULLENGINENAME "\\GamePaths", 0, STANDARD_RIGHTS_READ|KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
	{
		resultlen = basepathlen;
		if (!RegQueryValueEx(key, gamename, NULL, NULL, basepath, &resultlen))
		{
			if (GetFileAttributes(basepath) != INVALID_FILE_ATTRIBUTES)
			{
				RegCloseKey(key);
				return true;
			}
		}

		RegCloseKey(key);
	}


	if (!strcmp(gamename, "quake"))
	{
		char *prefix[] =
		{
			"c:/quake/",						//quite a lot of people have it in c:\quake, as that's the default install location from the quake cd.
			"c:/games/quake/",					//personally I use this
#ifdef _WIN64
			//quite a few people have nquake installed. we need to an api function to read the directory for non-english-windows users.
			va("%s/nQuake/", getenv("%ProgramFiles(x86)%")),	//64bit builds should look in both places
			va("%s/nQuake/", getenv("%ProgramFiles%")),			//
#else
			va("%s/nQuake/", getenv("%ProgramFiles%")),			//32bit builds will get the x86 version anyway.
#endif
			NULL
		};
		int i;
		FILE *f;

		//try and find it via steam
		//reads HKEY_LOCAL_MACHINE\SOFTWARE\Valve\Steam\InstallPath
		//append SteamApps\common\quake
		//use it if we find winquake.exe there
		if (Sys_SteamHasFile(basepath, basepathlen, "quake", "Winquake.exe"))
			return true;
		//well, okay, so they don't have quake installed from steam.

		//check various 'unadvertised' paths
		for (i = 0; prefix[i]; i++)
		{
			char syspath[MAX_OSPATH];
			Q_snprintfz(syspath, sizeof(syspath), "%sid1/pak0.pak", prefix[i]);
			if ((f = fopen("c:/quake/quake.exe", "rb")))
			{
				fclose(f);
				Q_strncpyz(basepath, prefix[i], sizeof(basepath));
				return true;
			}
		}
	}

	if (!strcmp(gamename, "quake2"))
	{
		FILE *f;
		DWORD resultlen;
		HKEY key = NULL;

		//look for HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Quake2_exe\Path
		if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Quake2_exe", 0, STANDARD_RIGHTS_READ|KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
		{
			resultlen = basepathlen;
			RegQueryValueEx(key, "Path", NULL, NULL, basepath, &resultlen);
			RegCloseKey(key);
			if ((f = fopen(va("%s/quake2.exe", basepath), "rb")))
			{
				fclose(f);
				return true;
			}
		}

		if (Sys_SteamHasFile(basepath, basepathlen, "quake 2", "quake2.exe"))
			return true;
	}

	if (!strcmp(gamename, "et"))
	{
		FILE *f;
		DWORD resultlen;
		HKEY key = NULL;
		//reads HKEY_LOCAL_MACHINE\SOFTWARE\Activision\Wolfenstein - Enemy Territory
		if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\Activision\\Wolfenstein - Enemy Territory", 0, STANDARD_RIGHTS_READ|KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
		{
			resultlen = basepathlen;
			RegQueryValueEx(key, "InstallPath", NULL, NULL, basepath, &resultlen);
			RegCloseKey(key);

			if ((f = fopen(va("%s/ET.exe", basepath), "rb")))
			{
				fclose(f);
				return true;
			}
			return true;
		}
	}

	if (!strcmp(gamename, "quake3"))
	{
		FILE *f;
		DWORD resultlen;
		HKEY key = NULL;

		//reads HKEY_LOCAL_MACHINE\SOFTWARE\id\Quake III Arena\InstallPath
		if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\id\\Quake III Arena", 0, STANDARD_RIGHTS_READ|KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
		{
			resultlen = basepathlen;
			RegQueryValueEx(key, "InstallPath", NULL, NULL, basepath, &resultlen);
			RegCloseKey(key);

			if ((f = fopen(va("%s/quake3.exe", basepath), "rb")))
			{
				fclose(f);
				return true;
			}
		}

		if (Sys_SteamHasFile(basepath, basepathlen, "quake 3 arena", "quake3.exe"))
			return true;
	}

	if (!strcmp(gamename, "wop"))
	{
		DWORD resultlen;
		HKEY key = NULL;
		//reads HKEY_LOCAL_MACHINE\SOFTWARE\World Of Padman\Path
		if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\World Of Padman", 0, STANDARD_RIGHTS_READ|KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
		{
			resultlen = basepathlen;
			RegQueryValueEx(key, "Path", NULL, NULL, basepath, &resultlen);
			RegCloseKey(key);
			return true;
		}
	}

/*
	if (!strcmp(gamename, "d3"))
	{
		DWORD resultlen;
		HKEY key = NULL;
		//reads HKEY_LOCAL_MACHINE\SOFTWARE\id\Doom 3\InstallPath
		if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\id\\Doom 3", 0, STANDARD_RIGHTS_READ|KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
		{
			resultlen = basepathlen;
			RegQueryValueEx(key, "InstallPath", NULL, NULL, basepath, &resultlen);
			RegCloseKey(key);
			return true;
		}
	}
*/

	if (!strcmp(gamename, "hexen2") || !strcmp(gamename, "h2mp"))
	{
		//append SteamApps\common\hexen 2
		if (Sys_SteamHasFile(basepath, basepathlen, "hexen 2", "glh2.exe"))
			return true;
	}

#if !defined(NPFTE) && !defined(SERVERONLY) //this is *really* unfortunate, but doing this crashes the browser
	if (poshname && !COM_CheckParm("-manifest"))
	{
		char resultpath[MAX_PATH];
		BROWSEINFO bi;
		LPITEMIDLIST il;
		memset(&bi, 0, sizeof(bi));
		bi.hwndOwner = mainwindow; //note that this is usually still null
		bi.pidlRoot = NULL;
		bi.pszDisplayName = resultpath;
		bi.lpszTitle = va("Please locate your existing %s installation", poshname);
		bi.ulFlags = BIF_RETURNONLYFSDIRS;
		bi.lpfn = NULL;
		bi.lParam = 0;
		bi.iImage = 0;

		il = SHBrowseForFolder(&bi);
		if (il)
		{
			SHGetPathFromIDList(il, resultpath);
			CoTaskMemFree(il);
			Q_strncpyz(basepath, resultpath, basepathlen-1);

			//and save it into the windows registry
			if (RegCreateKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\" FULLENGINENAME "\\GamePaths",
				0, NULL,
				REG_OPTION_NON_VOLATILE,
				KEY_WRITE,
				NULL,
				&key,
				NULL) == ERROR_SUCCESS)
			{
				RegSetValueEx(key, gamename, 0, REG_SZ, basepath, strlen(basepath));

				RegCloseKey(key);
			}

			return true;
		}
	}
#endif

	return false;
}
#else
#ifdef __linux__
#include <sys/stat.h>
#endif
qboolean Sys_FindGameData(const char *poshname, const char *gamename, char *basepath, int basepathlen)
{
#ifdef __linux__
	struct stat sb;
        if (!strcmp(gamename, "quake"))
	{
		if (stat("/usr/share/quake/", &sb) == 0)
		{
			// /usr/share/quake
			if (S_ISDIR(sb.st_mode))
			{
				Q_strncpyz(basepath, "/usr/share/quake/", basepathlen);
				return true;
			}
		}
	}
#endif
	return false;
}
#endif

void FS_Shutdown(void)
{
	searchpath_t *next;
	FS_FlushFSHashReally();

	//
	// free up any current game dir info
	//
	while (com_searchpaths)
	{
		com_searchpaths->handle->ClosePath(com_searchpaths->handle);
		next = com_searchpaths->next;
		Z_Free (com_searchpaths);
		com_searchpaths = next;
	}

	com_fschanged = true;


	if (filesystemhash.numbuckets)
	{
		BZ_Free(filesystemhash.bucket);
		filesystemhash.bucket = NULL;
		filesystemhash.numbuckets = 0;
	}

	FS_Manifest_Free(fs_manifest);
	fs_manifest = NULL;
}

#if 0
static void FS_AddGamePack(const char *pakname)
{
	int j;
	char *ext = COM_FileExtension(pakname);
	vfsfile_t *vfs = VFSOS_Open(pakname, "rb");
	void *pak;
	searchpath_t *oldlist = NULL;
	if (!vfs)
		Con_Printf("Unable to open %s - missing?\n", pakname);
	else
	{
		for (j = 0; j < sizeof(searchpathformats)/sizeof(searchpathformats[0]); j++)
		{
			if (!searchpathformats[j].extension || !searchpathformats[j].OpenNew)
				continue;
			if (!strcmp(ext, searchpathformats[j].extension))
			{
				pak = searchpathformats[j].OpenNew(vfs, pakname);
				if (pak)
				{
					FS_AddPathHandle(&oldlist, "", pakname, pak, SPF_COPYPROTECTED|SPF_EXPLICIT, (unsigned int)-1);
				}
				else
				{
					Con_Printf("Unable to open %s - corrupt?\n", pakname);
					VFS_CLOSE(vfs);
				}
				vfs = NULL;
				break;
			}
		}
		if (vfs)
		{
			VFS_CLOSE(vfs);
			Con_Printf("Unable to open %s - unsupported?\n", pakname);
		}
	}
}

static void FS_StartupWithGame(int gamenum)
{
	int i;
	searchpath_t *oldlist = NULL;

#ifdef AVAIL_ZLIB
	LibZ_Init();
#endif

	Cvar_Set(&com_protocolname, gamemode_info[gamenum].protocolname);
	Cvar_ForceSet(&fs_gamename, gamemode_info[gamenum].poshname);
//	Cvar_ForceSet(&fs_gamemanifest, gamemode_info[gamenum].manifestaddr?gamemode_info[gamenum].manifestaddr:"");

	i = COM_CheckParm ("-basepack");
	while (i && i < com_argc-1)
	{
//		Con_Printf("found -basepack: %s\n", com_argv[i+1]);
		FS_AddGamePack(com_argv[i+1]);
		i = COM_CheckNextParm ("-basepack", i);
	}

//
// start up with id1 by default
//
	i = COM_CheckParm ("-basegame");
	if (i && i < com_argc-1)
	{
		do	//use multiple -basegames
		{
			FS_AddGameDirectory (&oldlist, com_argv[i+1], va("%s%s", com_quakedir, com_argv[i+1]), ~0);
			if (*com_homedir)
				FS_AddGameDirectory (&oldlist, com_argv[i+1], va("%s%s", com_homedir, com_argv[i+1]), ~0);

			i = COM_CheckNextParm ("-basegame", i);
		}
		while (i && i < com_argc-1);
	}
	else
	{
		for (i = 0; i < sizeof(gamemode_info[gamenum].dir)/sizeof(gamemode_info[gamenum].dir[0]); i++)
		{
			if (gamemode_info[gamenum].dir[i] && *gamemode_info[gamenum].dir[i] == '*')
			{
				char buf[MAX_OSPATH];
				snprintf(buf, sizeof(buf), "%s%s", com_quakedir, gamemode_info[gamenum].dir[i]+1);
				FS_AddGamePack(buf);
			}
			else if (gamemode_info[gamenum].dir[i])
			{
				FS_AddGameDirectory (&oldlist, gamemode_info[gamenum].dir[i], va("%s%s", com_quakedir, gamemode_info[gamenum].dir[i]), ~0);
				if (*com_homedir)
					FS_AddGameDirectory (&oldlist, gamemode_info[gamenum].dir[i], va("%s%s", com_homedir, gamemode_info[gamenum].dir[i]), ~0);
			}
		}
	}

	i = COM_CheckParm ("-addbasegame");
	while (i && i < com_argc-1)	//use multiple -addbasegames (this is so the basic dirs don't die)
	{
		//reject various evil path arguments.
		if (*com_argv[i+1] && !(strchr(com_argv[i+1], '.') || strchr(com_argv[i+1], ':') || strchr(com_argv[i+1], '?') || strchr(com_argv[i+1], '*') || strchr(com_argv[i+1], '/') || strchr(com_argv[i+1], '\\') || strchr(com_argv[i+1], '$')))
		{
			FS_AddGameDirectory (&oldlist, com_argv[i+1], va("%s%s", com_quakedir, com_argv[i+1]), ~0);
			if (*com_homedir)
				FS_AddGameDirectory (&oldlist, com_argv[i+1], va("%s%s", com_homedir, com_argv[i+1]), ~0);
		}

		i = COM_CheckNextParm ("-addbasegame", i);
	}

	// any set gamedirs will be freed up to here
	com_base_searchpaths = com_searchpaths;

	//-game specifies the mod gamedir to use in NQ
	i = COM_CheckParm ("-game");	//effectivly replace with +gamedir x (But overridable)
	if (i && i < com_argc-1)
	{
		COM_Gamedir(com_argv[i+1]);
#ifndef CLIENTONLY
		Info_SetValueForStarKey (svs.info, "*gamedir", com_argv[i+1], MAX_SERVERINFO_STRING);
#endif
	}

	//+gamedir specifies the mod gamedir to use in QW
	//hack - we parse the commandline after the config so commandline always overrides
	//but this means ktpro/server.cfg (for example) is not found
	//so if they specify a gamedir on the commandline, let the default configs be loaded from that gamedir
	//note that -game +gamedir will result in both being loaded. but hey, who cares
	i = COM_CheckParm ("+gamedir");	//effectivly replace with +gamedir x (But overridable)
	if (i && i < com_argc-1)
	{
		COM_Gamedir(com_argv[i+1]);
#ifndef CLIENTONLY
		Info_SetValueForStarKey (svs.info, "*gamedir", com_argv[i+1], MAX_SERVERINFO_STRING);
#endif
	}

#ifdef ANDROID
	{
		vfsfile_t *f;
		//write a .nomedia file to avoid people from getting random explosion sounds etc intersperced with their music
		f = FS_OpenVFS(".nomedia", "rb", FS_ROOT);
		if (f)
			VFS_CLOSE(f);
		else
			FS_WriteFile(".nomedia", NULL, 0, FS_ROOT);
	}
#endif

	if (gamemode_info[gamenum].customexec)
		Cbuf_AddText(gamemode_info[gamenum].customexec, RESTRICT_LOCAL);
}
#endif

static qboolean FS_DirHasAPackage(char *basedir, ftemanifest_t *man)
{
	int j;
	vfsfile_t *f;
	for (j = 0; j < sizeof(fs_manifest->package) / sizeof(fs_manifest->package[0]); j++)
	{
		if (!man->package[j].path)
			continue;

		f = VFSOS_Open(va("%s%s", basedir, man->package[j].path), "rb");
		if (f)
		{
			VFS_CLOSE(f);
			return true;
		}
	}
	return false;
}

//just check each possible file, see if one is there.
static qboolean FS_DirHasGame(char *basedir, int gameidx)
{
	int j;
	vfsfile_t *f;
	for (j = 0; j < 4; j++)
	{
		if (!gamemode_info[gameidx].auniquefile[j])
			continue;	//no more
		f = VFSOS_Open(va("%s%s", basedir, gamemode_info[gameidx].auniquefile[j]), "rb");
		if (f)
		{
			VFS_CLOSE(f);
			return true;
		}
	}
	return false;
}

//check em all
static int FS_IdentifyDefaultGameFromDir(char *basedir)
{
	int i;
	for (i = 0; gamemode_info[i].argname; i++)
	{
		if (FS_DirHasGame(basedir, i))
			return i;
	}
	return -1;
}

//attempt to work out which game we're meant to be trying to run based upon a few things
//1: fs_changegame console command override. fixme: needs to cope with manifests too.
//2: -quake3 argument implies that the user wants to run quake3.
//3: if we are ftequake3.exe then we always try to run quake3.
//4: identify characteristic files within the working directory (like id1/pak0.pak implies we're running quake)
//5: check where the exe actually is instead of simply where we're being run from.
//6: fallback to quake
//if autobasedir is not set, block gamedir changes/prompts.
static int FS_IdentifyDefaultGame(char *newbase, int sizeof_newbase, qboolean fixedbase)
{
	int i;
	int gamenum = -1;

	if (gamenum == -1)
	{
		for (i = 0; gamemode_info[i].argname; i++)
		{
			if (COM_CheckParm(gamemode_info[i].argname))
			{
				gamenum = i;
				break;
			}
		}
	}
	//use the game based on an exe name over the filesystem one (could easily have multiple fs path matches).
	if (gamenum == -1)
	{
		char *ev, *v0 = COM_SkipPath(com_argv[0]);
		for (i = 0; gamemode_info[i].argname; i++)
		{
			if (!gamemode_info[i].exename)
				continue;
			ev = strstr(v0, gamemode_info[i].exename);
			if (ev && (!strchr(ev, '\\') && !strchr(ev, '/')))
				gamenum = i;
		}
	}

	//identify the game from a telling file in the working directory
	if (gamenum == -1)
		gamenum = FS_IdentifyDefaultGameFromDir(newbase);
	//identify the game from a telling file relative to the exe's directory. for when shortcuts don't set the working dir sensibly.
	if (gamenum == -1 && host_parms.binarydir && *host_parms.binarydir && !fixedbase)
	{
		gamenum = FS_IdentifyDefaultGameFromDir(host_parms.binarydir);
		if (gamenum != -1)
			Q_strncpyz(newbase, host_parms.binarydir, sizeof_newbase);
	}

	//still failed? find quake and use that one by default
	if (gamenum<0)
	{
		for (i = 0; gamemode_info[i].argname; i++)
		{
			if (!strcmp(gamemode_info[i].argname, "-quake"))
			{
				gamenum = i;
				break;
			}
		}
	}
	return gamenum;
}

//allowed to modify newbasedir if fixedbasedir isn't set
ftemanifest_t *FS_GenerateLegacyManifest(char *newbasedir, int sizeof_newbasedir, qboolean fixedbasedir, int game)
{
	int i;
	ftemanifest_t *man;

	if (game == -1)
		game = FS_IdentifyDefaultGame(newbasedir, sizeof_newbasedir, fixedbasedir);
	if (gamemode_info[game].manifestfile)
		man = FS_Manifest_Parse(gamemode_info[game].manifestfile);
	else
	{
		man = FS_Manifest_Create();

		Cmd_TokenizeString(va("game \"%s\"", gamemode_info[game].argname+1), false, false);
		FS_Manifest_ParseTokens(man);
		if (gamemode_info[game].poshname)
		{
			Cmd_TokenizeString(va("name \"%s\"", gamemode_info[game].poshname), false, false);
			FS_Manifest_ParseTokens(man);
		}

		i = COM_CheckParm ("-basegame");
		if (i)
		{
			do
			{
				Cmd_TokenizeString(va("basegame \"%s\"", com_argv[i+1]), false, false);
				FS_Manifest_ParseTokens(man);

				i = COM_CheckNextParm ("-basegame", i);
			}
			while (i && i < com_argc-1);
		}

		i = COM_CheckParm ("-game");
		if (i)
		{
			do
			{
				Cmd_TokenizeString(va("gamedir \"%s\"", com_argv[i+1]), false, false);
				FS_Manifest_ParseTokens(man);

				i = COM_CheckNextParm ("-game", i);
			}
			while (i && i < com_argc-1);
		}

		i = COM_CheckParm ("+gamedir");
		if (i)
		{
			do
			{
				Cmd_TokenizeString(va("gamedir \"%s\"", com_argv[i+1]), false, false);
				FS_Manifest_ParseTokens(man);

				i = COM_CheckNextParm ("+gamedir", i);
			}
			while (i && i < com_argc-1);
		}
	}
	return man;
}

static char *FS_RelativeURL(char *base, char *file, char *buffer, int bufferlen)
{
	//fixme: cope with windows paths
	qboolean baseisurl = !!strchr(base, ':');
	qboolean fileisurl = !!strchr(file, ':');
	//qboolean baseisabsolute = (*base == '/' || *base == '\\');
	qboolean fileisabsolute = (*file == '/' || *file == '\\');
	char *ebase;
	
	if (fileisurl)
		return file;
	if (fileisabsolute)
	{
		if (baseisurl)
		{
			ebase = strchr(base, ':');
			ebase++;
			while(*ebase == '/')
				ebase++;
			while(*ebase && *ebase != '/')
				ebase++;
		}
		else
			ebase = base;
	}
	else
		ebase = COM_SkipPath(base);
	memcpy(buffer, base, ebase-base);
	strcpy(buffer+(ebase-base), file);

	return buffer;
}

#ifdef WEBCLIENT
static struct dl_download *curpackagedownload;
static char fspdl_temppath[MAX_OSPATH];
static char fspdl_finalpath[MAX_OSPATH];
static void FS_BeginNextPackageDownload(void);
static void FS_PackageDownloaded(struct dl_download *dl)
{
	curpackagedownload = NULL;
	
	if (dl->file)
	{
		VFS_CLOSE(dl->file);
		dl->file = NULL;
	}
	if (dl->status == DL_FINISHED)
	{
		//rename the file as needed.
		COM_CreatePath(fspdl_finalpath);
		if (!Sys_Rename(fspdl_temppath, fspdl_finalpath))
		{
			Con_Printf("Unable to rename \"%s\" to \"%s\"\n", fspdl_temppath, fspdl_finalpath);
		}
	}
	Sys_remove (fspdl_temppath);

	FS_ChangeGame(fs_manifest, true);

	FS_BeginNextPackageDownload();
}
static void FS_BeginNextPackageDownload(void)
{
	char *crcstr;
	int j;
	ftemanifest_t *man = fs_manifest;
	vfsfile_t *check;
	if (curpackagedownload || !man)
		return;

	for (j = 0; j < sizeof(fs_manifest->package) / sizeof(fs_manifest->package[0]); j++)
	{
		char buffer[MAX_OSPATH], *url;
		if (!man->package[j].path)
			continue;

		if (man->package[j].crcknown)
			crcstr = va("%#x", man->package[j].crc);
		else
			crcstr = "";
		if (!FS_GenCachedPakName(man->package[j].path, crcstr, buffer, sizeof(buffer)))
			continue;

		check = FS_OpenVFS(buffer, "rb", FS_ROOT);
		if (check)
		{
			VFS_CLOSE(check);
			continue;
		}
		FS_NativePath(buffer, FS_ROOT, fspdl_finalpath, sizeof(fspdl_finalpath));
		if (!FS_GenCachedPakName(va("%s.tmp", man->package[j].path), crcstr, buffer, sizeof(buffer)))
			continue;
		FS_NativePath(buffer, FS_ROOT, fspdl_temppath, sizeof(fspdl_temppath));

		url = NULL;
		while(!url)
		{
			//ran out of mirrors?
			if (man->package[j].mirrornum == (sizeof(man->package[j].mirrors) / sizeof(man->package[j].mirrors[0])))
				break;

			if (man->package[j].mirrors[man->package[j].mirrornum])
				url = FS_RelativeURL(man->updateurl, man->package[j].mirrors[man->package[j].mirrornum], buffer, sizeof(buffer));
			man->package[j].mirrornum++;
		}
		//no valid mirrors
		if (!url)
			continue;

		curpackagedownload = HTTP_CL_Get(url, NULL, FS_PackageDownloaded);
		if (curpackagedownload)
		{
			COM_CreatePath(fspdl_temppath);
			curpackagedownload->file = VFSOS_Open(fspdl_temppath, "wb");
			return;
		}
	}
}
#else
void FS_BeginNextPackageDownload(void)
{
}
#endif

//this is potentially unsafe. needs lots of testing.
qboolean FS_ChangeGame(ftemanifest_t *man, qboolean allowreloadconfigs)
{
	int i, j;
	char realpath[MAX_OSPATH-1];
	char newbasedir[MAX_OSPATH];
	qboolean fixedbasedir;
	qboolean reloadconfigs = false;
	qboolean builtingame = false;
	flocation_t loc;

	//if any of these files change location, the configs will be re-execed.
	//note that we reuse path handles if they're still valid, so we can just check the pointer to see if it got unloaded/replaced.
	char *conffile[] = {"quake.rc", "hexen.rc", "default.cfg", "server.cfg", NULL};
	searchpathfuncs_t *confpath[sizeof(conffile)/sizeof(conffile[0])];
	for (i = 0; conffile[i]; i++)
	{
		FS_FLocateFile(conffile[i], FSLFRT_IFFOUND, &loc);	//q1
		confpath[i] = loc.search?loc.search->handle:NULL;
	}

	i = COM_CheckParm ("-basedir");
	fixedbasedir = i && i < com_argc-1;
	Q_strncpyz (newbasedir, fixedbasedir?com_argv[i+1]:host_parms.basedir, sizeof(newbasedir));

	//make sure it has a trailing slash, or is empty. woo.
	FS_CleanDir(newbasedir, sizeof(newbasedir));

	if (!man)
	{
		//if we're already running a game, don't autodetect.
		if (fs_manifest)
			return false;

		man = FS_GenerateLegacyManifest(newbasedir, sizeof(newbasedir), fixedbasedir, -1);
	}

	if (man == fs_manifest)
	{
		//don't close anything. theoretically nothing is changing, and we don't want to load new defaults either.
	}
	else if (!fs_manifest || !strcmp(fs_manifest->installation?fs_manifest->installation:"", man->installation?man->installation:""))
	{
		if (!fs_manifest)
			reloadconfigs = true;
		FS_Manifest_Free(fs_manifest);
	}
	else
	{
		FS_Shutdown();

		reloadconfigs = true;
	}
	fs_manifest = man;

	if (man->installation && *man->installation)
	{
		for (i = 0; gamemode_info[i].argname; i++)
		{
			if (!strcmp(man->installation, gamemode_info[i].argname+1))
			{
				//if there's no base dirs, edit the manifest to give it its default ones.
				for (j = 0; j < sizeof(man->gamepath) / sizeof(man->gamepath[0]); j++)
				{
					if (man->gamepath[j].path && man->gamepath[j].base)
						break;
				}
				if (j == sizeof(man->gamepath) / sizeof(man->gamepath[0]))
				{
					for (j = 0; j < 4; j++)
						if (gamemode_info[i].dir[j])
						{
							Cmd_TokenizeString(va("basegame \"%s\"", gamemode_info[i].dir[j]), false, false);
							FS_Manifest_ParseTokens(man);
						}
				}

				if (!man->protocolname && *gamemode_info[i].protocolname)
				{
					Cmd_TokenizeString(va("protocolname \"%s\"", gamemode_info[i].protocolname), false, false);
					FS_Manifest_ParseTokens(man);
				}
				if (!man->defaultexec && gamemode_info[i].customexec)
				{
					man->defaultexec = Z_StrDup(gamemode_info[i].customexec);
				}

				builtingame = true;
				if (!fixedbasedir && !FS_DirHasGame(newbasedir, i))
					if (Sys_FindGameData(man->formalname, man->installation, realpath, sizeof(realpath)))
						Q_strncpyz (newbasedir, realpath, sizeof(newbasedir));
				break;
			}
		}
	}

	if (!builtingame && !fixedbasedir && !FS_DirHasAPackage(newbasedir, man))
		if (Sys_FindGameData(man->formalname, man->installation, realpath, sizeof(realpath)))
			Q_strncpyz (newbasedir, realpath, sizeof(newbasedir));

	Q_strncpyz (com_quakedir, newbasedir, sizeof(com_quakedir));
	//make sure it has a trailing slash, or is empty. woo.
	FS_CleanDir(com_quakedir, sizeof(com_quakedir));

#ifdef ANDROID
	{
		vfsfile_t *f;
		//write a .nomedia file to avoid people from getting random explosion sounds etc intersperced with their music
		f = FS_OpenVFS(".nomedia", "rb", FS_ROOT);
		if (f)
			VFS_CLOSE(f);
		else
			FS_WriteFile(".nomedia", NULL, 0, FS_ROOT);
	}
#endif

	FS_ReloadPackFilesFlags(~0);

	FS_BeginNextPackageDownload();

	COM_CheckRegistered();

	if (allowreloadconfigs)
	{
		for (i = 0; conffile[i]; i++)
		{
			FS_FLocateFile(conffile[i], FSLFRT_IFFOUND, &loc);
			if (confpath[i] != (loc.search?loc.search->handle:NULL))
			{
				reloadconfigs = true;
				Con_DPrintf("Reloading configs because %s has changed\n", conffile[i]);
			}
		}

		if (reloadconfigs)
		{
			//FIXME: flag this instead and do it after a delay
			Cvar_ForceSet(&fs_gamename, man->formalname?man->formalname:"FTE");
			Cvar_ForceSet(&com_protocolname, man->protocolname?man->protocolname:"FTE");

			if (isDedicated)
			{
#ifndef CLIENTONLY
				SV_ExecInitialConfigs(man->defaultexec?man->defaultexec:"");
#endif
			}
			else
			{
#ifndef SERVERONLY
				CL_ExecInitialConfigs(man->defaultexec?man->defaultexec:"");
#endif
			}
		}
	}

	//rebuild the cache now, should be safe to waste some cycles on it
	FS_RebuildFSHash();

	COM_Effectinfo_Clear();
#ifndef SERVERONLY
	Validation_FlushFileList();	//prevent previous hacks from making a difference.
#endif

	Cvar_ForceSet(&fs_game, FS_GetGamedir());
#ifdef Q2SERVER
	Cvar_ForceSet(&fs_gamedir, va("%s%s", com_quakedir, FS_GetGamedir()));
	Cvar_ForceSet(&fs_basedir, com_quakedir);
#endif

	return true;
}

void FS_ChangeGame_f(void)
{
	int i;
	char *arg = Cmd_Argv(1);

	if (!*arg)
	{
		Con_Printf("Valid games are:\n");
		for (i = 0; gamemode_info[i].argname; i++)
		{
			Con_Printf(" %s\n", gamemode_info[i].argname+1);
		}
	}
	else
	{
		for (i = 0; gamemode_info[i].argname; i++)
		{			
			if (!Q_strcasecmp(gamemode_info[i].argname+1, arg))
			{
				Con_Printf("Switching to %s\n", gamemode_info[i].argname+1);
				FS_ChangeGame(FS_GenerateLegacyManifest(NULL, 0, true, i), true);
				return;
			}
		}

#ifndef SERVERONLY
		if (!Host_RunFile(arg, strlen(arg), NULL))
			Con_Printf("Game unknown\n");
#endif
	}
}
void FS_ShowManifest_f(void)
{
	if (fs_manifest)
		FS_Manifest_Print(fs_manifest);
	else
		Con_Printf("no manifest loaded...\n");
}
/*
================
COM_InitFilesystem

note: does not actually load any packs, just makes sure the basedir+cvars+etc is set up. vfs_fopens will still fail.
================
*/
void COM_InitFilesystem (void)
{
	int		i;

	char *ev;
	qboolean usehome;

	FS_RegisterDefaultFileSystems();

	Cmd_AddCommand("fs_restart", FS_ReloadPackFiles_f);
	Cmd_AddCommand("fs_changegame", FS_ChangeGame_f);
	Cmd_AddCommand("fs_showmanifest", FS_ShowManifest_f);

//
// -basedir <path>
// Overrides the system supplied base directory (under id1)
//
	i = COM_CheckParm ("-basedir");
	if (i && i < com_argc-1)
		strcpy (com_quakedir, com_argv[i+1]);
	else
		strcpy (com_quakedir, host_parms.basedir);

	FS_CleanDir(com_quakedir, sizeof(com_quakedir));


	Cvar_Register(&cfg_reload_on_gamedir, "Filesystem");
	Cvar_Register(&com_fs_cache, "Filesystem");
	Cvar_Register(&fs_gamename, "Filesystem");
	Cvar_Register(&fs_gamemanifest, "Filesystem");
	Cvar_Register(&com_protocolname, "Server Info");
	Cvar_Register(&com_modname, "Server Info");
	Cvar_Register(&fs_game, "Filesystem");
#ifdef Q2SERVER
	Cvar_Register(&fs_gamedir, "Filesystem");
	Cvar_Register(&fs_basedir, "Filesystem");
#endif

	usehome = false;

#ifdef _WIN32
	{	//win32 sucks.
		HMODULE shfolder = LoadLibrary("shfolder.dll");
		DWORD winver = (DWORD)LOBYTE(LOWORD(GetVersion()));

		if (shfolder)
		{
			HRESULT (WINAPI *dSHGetFolderPath) (HWND hwndOwner, int nFolder, HANDLE hToken, DWORD dwFlags, LPTSTR pszPath);
			dSHGetFolderPath = (void *)GetProcAddress(shfolder, "SHGetFolderPathA");
			if (dSHGetFolderPath)
			{
				char folder[MAX_PATH];
				// 0x5 == CSIDL_PERSONAL
				if (dSHGetFolderPath(NULL, 0x5, NULL, 0, folder) == S_OK)
					Q_snprintfz(com_homedir, sizeof(com_homedir), "%s/My Games/%s/", folder, FULLENGINENAME);
			}
//			FreeLibrary(shfolder);
		}

		if (!*com_homedir)
		{
			ev = getenv("USERPROFILE");
			if (ev)
				Q_snprintfz(com_homedir, sizeof(com_homedir), "%s/My Documents/My Games/%s/", ev, FULLENGINENAME);
		}

#ifdef NPFTE
		if (!*com_homedir)
			Q_snprintfz(com_homedir, sizeof(com_homedir), "/%s/", FULLENGINENAME);
		//as a browser plugin, always use their home directory
		usehome = true;
#else
		/*would it not be better to just check to see if we have write permission to the basedir?*/
		if (winver >= 0x6) // Windows Vista and above
			usehome = true; // always use home directory by default, as Vista+ mimics this behavior anyway
		else if (winver >= 0x5) // Windows 2000/XP/2003
		{
			HMODULE advapi32;
			advapi32 = LoadLibrary("advapi32.dll");

			if (advapi32)
			{
				BOOL (WINAPI *dCheckTokenMembership) (HANDLE TokenHandle, PSID SidToCheck, PBOOL IsMember);
				dCheckTokenMembership = (void *)GetProcAddress(advapi32, "CheckTokenMembership");

				if (dCheckTokenMembership)
				{
					// on XP systems, only use a home directory by default if we're a limited user or if we're on a network
					BOOL isadmin, isonnetwork;
					SID_IDENTIFIER_AUTHORITY ntauth = {SECURITY_NT_AUTHORITY};
					PSID adminSID, networkSID;

					isadmin = AllocateAndInitializeSid(&ntauth,
						2,
						SECURITY_BUILTIN_DOMAIN_RID,
						DOMAIN_ALIAS_RID_ADMINS,
						0, 0, 0, 0, 0, 0,
						&adminSID);

					// just checking the network rid should be close enough to matching domain logins
					isonnetwork = AllocateAndInitializeSid(&ntauth,
						1,
						SECURITY_NETWORK_RID,
						0, 0, 0, 0, 0, 0, 0,
						&networkSID);

					if (isadmin && !dCheckTokenMembership(0, adminSID, &isadmin))
						isadmin = 0;

					if (isonnetwork && !dCheckTokenMembership(0, networkSID, &isonnetwork))
						isonnetwork = 0;

					usehome = isonnetwork || !isadmin;

					FreeSid(networkSID);
					FreeSid(adminSID);
				}

				FreeLibrary(advapi32);
			}
		}
#endif
	}
#else
	//yay for unix!.
	ev = getenv("HOME");
	if (ev && *ev)
	{
		if (ev[strlen(ev)-1] == '/')
			Q_snprintfz(com_homedir, sizeof(com_homedir), "%s.fte/", ev);
		else
			Q_snprintfz(com_homedir, sizeof(com_homedir), "%s/.fte/", ev);
		usehome = true; // always use home on unix unless told not to
	}
	else
		*com_homedir = '\0';
#endif

	if (!usehome && !COM_CheckParm("-usehome"))
		*com_homedir = '\0';

	if (COM_CheckParm("-nohome"))
		*com_homedir = '\0';

	if (*com_homedir)
		Con_TPrintf("Using home directory \"%s\"\n", com_homedir);

#ifdef PLUGINS
	Plug_Initialise(false);
#endif
}





//this is at the bottom of the file to ensure these globals are not used elsewhere
extern searchpathfuncs_t *(QDECL VFSOS_OpenPath) (vfsfile_t *file, const char *desc);
#ifdef AVAIL_ZLIB
extern searchpathfuncs_t *(QDECL FSZIP_LoadArchive) (vfsfile_t *packhandle, const char *desc);
#endif
extern searchpathfuncs_t *(QDECL FSPAK_LoadArchive) (vfsfile_t *packhandle, const char *desc);
#ifdef DOOMWADS
extern searchpathfuncs_t *(QDECL FSDWD_LoadArchive) (vfsfile_t *packhandle, const char *desc);
#endif
void FS_RegisterDefaultFileSystems(void)
{
	FS_RegisterFileSystemType(NULL, "pak", FSPAK_LoadArchive, true);
#if !defined(_WIN32) && !defined(ANDROID)
	/*for systems that have case sensitive paths, also include *.PAK */
	FS_RegisterFileSystemType(NULL, "PAK", FSPAK_LoadArchive, true);
#endif
	FS_RegisterFileSystemType(NULL, "pk3dir", VFSOS_OpenPath, true);
#ifdef AVAIL_ZLIB
	FS_RegisterFileSystemType(NULL, "pk3", FSZIP_LoadArchive, true);
	FS_RegisterFileSystemType(NULL, "pk4", FSZIP_LoadArchive, true);
	FS_RegisterFileSystemType(NULL, "apk", FSZIP_LoadArchive, false);
	FS_RegisterFileSystemType(NULL, "zip", FSZIP_LoadArchive, false);
#endif
#ifdef DOOMWADS
	FS_RegisterFileSystemType(NULL, "wad", FSDWD_LoadArchive, true);
#endif
}