/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#elif defined(__DJGPP__)
#include <dos.h>
#include <io.h>
#else /* unixish */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#endif

#include "qcommon.h"
#include "glob.h"

#ifndef DEDICATED_ONLY
#include "cdaudio.h"
#else
void CDAudio_Stop (void) {}
#endif

// enables faster binary pak searck, still experimental
#define BINARY_PACK_SEARCH

#define	BASEDIRNAME	"base"

#define	MAX_READ	0x10000		// read in blocks of 64k

/* FS: For reference that we are using v1.11 */
#define BASE_MD5_PAK0	d1f661d4d0d32643fff0a9a9615d9df9
#define BASE_MD5_PAK1	3834720a40c97373f26516ea00d7f22b
#define BASE_MD5_PAK2	450bb8b2ec1ea72cb2aac00dffe7cd7f
#define BASE_MD5_PAK3	3b05040b74fe6e579fa6b166e1cf4afc
#define BASE_MD5_PAK4	ba20bfca5f181819e67b00c6c34689ef
#define BASE_MD5_PAK5	7ec5d6fc53a90353f05c443f044eb22a
#define BASE_CRC_PAK0	0x7e3f15eb
#define BASE_CRC_PAK1	0xa517f946
#define BASE_CRC_PAK2	0xad9613d4
#define BASE_CRC_PAK3	0x7e7bb442
#define BASE_CRC_PAK4	0xb14e6b46
#define BASE_CRC_PAK5	0xc81aba8b

#define WOS_MD5_PAK0	ab89c86fed705d211cb089ee3c17a9aa
#define WOS_MD5_PAK1	4b434ef314eec2fc7cd79e193c0c0867
#define WOS_CRC_PAK0	0x87f3a2f6
#define WOS_CRC_PAK1	0xbcc19bf5

/*
=============================================================================

QUAKE FILESYSTEM

=============================================================================
*/


//
// in memory
//

typedef struct
{
	char	name[MAX_QPATH];
	long	hash; // Knightmare
	int		filepos, filelen;
	qboolean		ignore;		// Knightmare added- whether this file should be ignored
} packfile_t;

typedef struct pack_s
{
	char	filename[MAX_OSPATH];
	FILE	*handle;
	int		numfiles;
	packfile_t	*files;
	unsigned int	contentFlags;	// Knightmare added- to skip cetain paks
} pack_t;

char	fs_gamedir[MAX_OSPATH];
cvar_t	*fs_basedir;
cvar_t	*fs_cddir;
cvar_t	*fs_gamedirvar;

typedef struct filelink_s
{
	struct filelink_s	*next;
	char	*from;
	int		fromlength;
	char	*to;
} filelink_t;

filelink_t	*fs_links;

typedef struct searchpath_s
{
	char	filename[MAX_OSPATH];
	pack_t	*pack;		// only one of filename / pack will be used
	struct searchpath_s *next;
} searchpath_t;

searchpath_t	*fs_searchpaths;
searchpath_t	*fs_base_searchpaths;	// without gamedirs

static cvar_t *cl_player;
static char fs_relativeplayerdir[MAX_OSPATH];
static char fs_playerdir[MAX_OSPATH];

#ifdef _WIN32
static qboolean FS_IsDirectory(const char *path) {
	DWORD result = GetFileAttributes(path);
	return (result != INVALID_FILE_ATTRIBUTES && (result & FILE_ATTRIBUTE_DIRECTORY));
}
#elif defined(__DJGPP__)
static qboolean FS_IsDirectory(const char *path) {
	int attr = _chmod(path, 0);
	return (attr > 0 && (attr & _A_SUBDIR));
}
#else /* unixish */
static qboolean FS_IsDirectory(const char *path) {
	struct stat st;
	return (stat(path,&st) == 0 && S_ISDIR(st.st_mode));
}
#endif

/*

All of Quake's data access is through a hierchal file system, but the contents of the file system can be transparently merged from several sources.

The "base directory" is the path to the directory holding the quake.exe and all game directories.  The sys_* files pass this to host_init in quakeparms_t->basedir.  This can be overridden with the "-basedir" command line parm to allow code debugging in a different directory.  The base directory is
only used during filesystem initialization.

The "game directory" is the first tree on the search path and directory that all generated files (savegames, screenshots, demos, config files) will be saved to.  This can be overridden with the "-game" command line parameter.  The game directory can never be changed while quake is executing.  This is a precacution against having a malicious server instruct clients to write files over areas they shouldn't.

*/

// Knightmare added
static const char *type_extensions[] =
{
	"bsp",
	"md2",
	"sp2",
	"dm2",
	"cin",
	"wav",
	"ogg",
	"pcx",
	"wal",
	"tga",
//	"jpg",
//	"png",
	"cfg",
	"txt",
	"def",
	"alias",
	0
};

/*
=================
FS_TypeFlagForPakItem
Returns bit flag based on pak item's extension.
=================
*/
static unsigned int FS_TypeFlagForPakItem (const char *itemName)
{
	int		i;	
	const char	*tmp;
	char	extension[8];

	tmp = COM_FileExtension (itemName);
	strncpy(extension, tmp, sizeof(extension));
	for (i = 0; type_extensions[i]; i++) {
		if (!Q_stricmp(extension, type_extensions[i]))
			return (1<<i);
	}
	return 0;
}
// end Knightmare

/*
================
FS_filelength
================
*/
int FS_filelength (FILE *f)
{
	int		pos;
	int		end;

	pos = ftell (f);
	fseek (f, 0, SEEK_END);
	end = ftell (f);
	fseek (f, pos, SEEK_SET);

	return end;
}


/*
============
FS_CreatePath

Creates any directories needed to store the given filename
============
*/
void	FS_CreatePath (char *path)
{
	char	*ofs, c;

	// Knightmare added
	if (strstr(path, "..") || strstr(path, "::") || strstr(path, "\\\\") || strstr(path, "//"))
	{
		Com_Printf("WARNING: refusing to create relative path '%s'\n", path);
		return;
	}
	
	for (ofs = path+1 ; *ofs ; ofs++)
	{
		c = *ofs;
		/* FIXME: Should actually check this OS-specifically!! */
		/* FIXME: Added the '\\' check, because rhangar1.bsp from the rogue mission pack
		 *     wants a texture named "zor_temp\\door2.wal".  Their pak file does have it
		 *     with that name.  What we do here is band-aiding the file write for such a
		 *     stupidity.  Need a better way of handling things. */
		if (c == '/' || c == '\\')
		{	// create the directory
			*ofs = 0;
			Sys_Mkdir (path);
			*ofs = c;
		}
	}
}


/*
==============
FS_FCloseFile

For some reason, other dll's can't just cal fclose()
on files returned by FS_FOpenFile...
==============
*/
void FS_FCloseFile (FILE *f)
{
	fclose (f);
}


#ifdef BINARY_PACK_SEARCH /* Knightmare added */
/*
=================
FS_FindPackItem

Performs a binary search by hashed filename
to find pack items in a sorted pack
=================
*/
static int FS_FindPackItem (pack_t *pack, const char *itemName, long itemHash)
{
	int		smax, smin, smidpt;	//, counter = 0;
	int		i;

	// catch null pointers
	if (!pack || !itemName)
		return -1;

	smin = 0;
	smax = pack->numfiles;
	while (smax - smin > 5) //&& counter < pack->numFiles)
	{
		smidpt = (smax + smin) / 2;
		if (pack->files[smidpt].hash > itemHash)	// before midpoint
			smax = smidpt;
		else if (pack->files[smidpt].hash < itemHash)	// after midpoint
			smin = smidpt;
		else	// pack->files[smidpt].hash == itemHash
			break;
	//	counter++;
	}
	for (i = smin; i < smax; i++)
	{	// make sure this entry is not blacklisted & compare filenames
		if (pack->files[i].hash == itemHash && !pack->files[i].ignore
				&& !Q_stricmp(pack->files[i].name, itemName))
			return i;
	}
	return -1;
}
#endif /* BINARY_PACK_SEARCH */

/*
===========
FS_FOpenFile

Finds the file in the search path.
returns filesize and an open FILE *
Used for streaming data out of either a pak file or
a seperate file.
===========
*/
int file_from_pak = 0;
int FS_FOpenFile (const char *filename, FILE **file)
{
	searchpath_t	*search;
	char			netpath[MAX_OSPATH];
	pack_t			*pak;
	int				i;
	filelink_t		*link;
	// Knightmare added
	long			hash;
	unsigned int	typeFlag;

	if (!filename || !*filename) /* nul name */
	{
		*file = NULL;
		return -1;
	}
	/* FS: FIXME: Configstrings sends down 'models/' into Mod_ForName and netpath passes from fopen */
	if (filename[strlen(filename) - 1] == '/')
	{
		Com_DPrintf(DEVELOPER_MSG_IO, "Refusing to fopen() a directory: %s\n", filename);
		*file = NULL;
		return -1;
	}

	file_from_pak = 0;
	// Knightmare added
	hash = Com_HashFileName(filename, 0, false);
	typeFlag = FS_TypeFlagForPakItem(filename);

	// check for links first
	for (link = fs_links; link; link = link->next)
	{
		if (!strncmp(filename, link->from, link->fromlength))
		{
			Com_sprintf (netpath, sizeof(netpath), "%s%s",link->to, filename+link->fromlength);
			*file = fopen (netpath, "rb");
			if (*file)
			{
				Com_Printf("link file: %s\n",netpath);
				return FS_filelength (*file);
			}
			return -1;
		}
	}

//
// search through the path, one element at a time
//
	for (search = fs_searchpaths ; search ; search = search->next)
	{
	// is the element a pak file?
		if (search->pack)
		{
		// look through all the pak file elements
			pak = search->pack;

			// Knightmare- skip if pack doesn't contain this type of file
			if (typeFlag != 0 && !(pak->contentFlags & typeFlag))
				continue;

#ifdef BINARY_PACK_SEARCH /* Knightmare- use new binary algorithm */
			/* find index of pack item */
			i = FS_FindPackItem (pak, filename, hash);
			if (i >= 0) /* found it! */
			{
				file_from_pak = 1;
			// open a new file on the pakfile
				*file = fopen (pak->filename, "rb");
				if (!*file)
					Com_Error (ERR_FATAL, "Couldn't reopen %s", pak->filename);
				fseek (*file, pak->files[i].filepos, SEEK_SET);
				return pak->files[i].filelen;
			}
#else
			for (i = 0; i < pak->numfiles; i++)
			{
				if (pak->files[i].ignore)	// Knightmare- skip blacklisted files
					continue;
				if (hash != pak->files[i].hash)	// Knightmare- compare hash first
					continue;
				if (!Q_strcasecmp (pak->files[i].name, filename))
				{	// found it!
					file_from_pak = 1;
				// open a new file on the pakfile
					*file = fopen (pak->filename, "rb");
					if (!*file)
						Com_Error (ERR_FATAL, "Couldn't reopen %s", pak->filename);
					fseek (*file, pak->files[i].filepos, SEEK_SET);
					return pak->files[i].filelen;
				}
			}
#endif /* BINARY_PACK_SEARCH */
		}
		else
		{
		// check a file in the directory tree
			Com_sprintf (netpath, sizeof(netpath), "%s/%s",search->filename, filename);

			*file = fopen (netpath, "rb");
			if (!*file)
				continue;
			
			Com_DPrintf(DEVELOPER_MSG_IO, "FindFile: %s\n",netpath);

			return FS_filelength (*file);
		}
	}

	Com_DPrintf(DEVELOPER_MSG_IO, "FindFile: can't find %s\n", filename);
	
	*file = NULL;
	return -1;
}

/*
=================
FS_Read

Properly handles partial reads
=================
*/
void FS_Read (void *buffer, int len, FILE *f)
{
	int		block, remaining;
	int		read;
	byte	*buf;
	int		tries;

	buf = (byte *)buffer;

	// read in chunks for progress bar
	remaining = len;
	tries = 0;
	while (remaining)
	{
		block = remaining;
		if (block > MAX_READ)
			block = MAX_READ;
		read = fread (buf, 1, block, f);
		if (read == 0)
		{
			// we might have been trying to read from a CD
			if (!tries)
			{
				tries = 1;
				CDAudio_Stop();
			}
			else
				Com_Error (ERR_FATAL, "FS_Read: 0 bytes read");
		}

		// do some progress bar thing here...

		remaining -= read;
		buf += read;
	}
}


// Knightmare added
/*
=================
FS_FRead

Properly handles partial reads of size up to count times
No error if it can't read
=================
*/
int FS_FRead (void *buffer, int size, int count, FILE *f)
{
	int			loops, remaining, r;
	byte		*buf;
	qboolean	tried = false;

	// Read
	loops = count;
	//remaining = size;
	buf = (byte *)buffer;

	while (loops)
	{	// Read in chunks
		remaining = size;
		while (remaining)
		{
			r = fread(buf, 1, remaining, f);

			if (r == 0)
			{
				if (!tried)
				{	// We might have been trying to read from a CD
					CDAudio_Stop();
					tried = true;
				}
				else {
				//	Com_Printf("FS_FRead: 0 bytes read\n");
					return size - remaining;
				}
			}

			remaining -= r;
			buf += r;
		}
		loops--;
	}
	return size;
}


/*
=================
FS_Seek
=================
*/
int FS_Seek (FILE *f, int offset, fsOrigin_t origin)
{
	switch (origin)
	{
	case FS_SEEK_SET:
		return fseek(f, offset, SEEK_SET);

	case FS_SEEK_CUR:
		return fseek(f, offset, SEEK_CUR);

	case FS_SEEK_END:
		return fseek(f, offset, SEEK_END);
	}
	Com_Error(ERR_FATAL, "FS_Seek: bad origin (%i)", origin);
	return -1;
}


/*
=================
FS_Tell

Returns -1 if an error occurs
=================
*/
long FS_Tell (FILE *f)
{
	return ftell(f);
}
// end Knightmare


/*
============
FS_LoadFile

Filename are reletive to the quake search path
a null buffer will just return the file length without loading
============
*/
int FS_LoadFile (const char *path, void **buffer)
{
	FILE *h;
	byte *buf;
	int len;

	// look for it in the filesystem or pack files
	len = FS_FOpenFile(path, &h);
	if (!h) {
		if (buffer) {
			*buffer = NULL;
		}
		return -1;
	}

	if (!buffer) {
		FS_FCloseFile(h);
		return len;
	}

	buf = (byte *)Z_Malloc(len+1);
	*buffer = buf;

	FS_Read(buf,len,h);		/* Read in the data */
	FS_FCloseFile(h);		/* Close the file */

	// Null terminate this thing, cause we need it for scripting.
	buf[len] = 0x0;

	return len;
}


/*
=============
FS_FreeFile
=============
*/
void FS_FreeFile (void *buffer)
{
	if(buffer)
		Z_Free (buffer);
}


// Some incompetently packaged mods have these files in their paks!
static const char *pakfile_ignore_names[] =
{
	"save/",
	"scrnshot/",
	"autoexec.cfg",
	"config.cfg",
	NULL
};

/*
=================
FS_FileInPakBlacklist

Checks against a blacklist to see if a file
should not be loaded from a pak.
=================
*/
static qboolean FS_FileInPakBlacklist (const char *filename)
{
	int			i;
	const char		*compare;
	qboolean	ignore = false;

	compare = filename;
	if (compare[0] == '/')	// remove leading slash
		compare++;

	for (i = 0; pakfile_ignore_names[i]; i++) {
		if (!Q_strncasecmp(compare, pakfile_ignore_names[i], (int)strlen(pakfile_ignore_names[i])))
			ignore = true;
	}

//	if (ignore)
//		Com_Printf ("FS_LoadPackFile: file %s blacklisted!\n", filename);
//	else if (!strncmp (filename, "save/", 5))
//		Com_Printf ("FS_LoadPackFile: file %s not blacklisted.\n", filename);
	return ignore;
}

#ifdef BINARY_PACK_SEARCH
/*
=================
FS_PakFileCompare
 
Used for sorting pak entries by hash
=================
*/
static long *nameHashes = NULL;
static int FS_PakFileCompare (const void *f1, const void *f2)
{
	if (!nameHashes)
		return 1;

	return (nameHashes[*((int *)(f1))] - nameHashes[*((int *)(f2))]);
}
#endif /* BINARY_PACK_SEARCH */

#ifdef CHECK_PAK_VERSION
static qboolean FS_CalcChecksum (char *packfile, int checksum) /* FS */
{
	qboolean bBaseGame = false;
	qboolean bWosGame = false;

	if (!packfile || !checksum || !FS_Gamedir())
		return false;

	if(!strncmp(FS_Gamedir(), "./base", 6))
		bBaseGame = true;

	if(!strncmp(FS_Gamedir(), "./2015", 6))
		bWosGame = true;

	if((bBaseGame || bWosGame) && ((int)strlen(packfile) > 11))
	{
		char x = packfile[10];
		qboolean bPassed = true;

		switch(x)
		{
			case '0':
				if ((checksum != BASE_CRC_PAK0) && (checksum != WOS_CRC_PAK0))
					bPassed = false;
				break;
			case '1':
				if ((checksum != BASE_CRC_PAK1) && (checksum != WOS_CRC_PAK1))
					bPassed = false;
				break;
			case '2':
				if ((bBaseGame) && (checksum != BASE_CRC_PAK2))
					bPassed = false;
				break;
			case '3':
				if ((bBaseGame) && (checksum != BASE_CRC_PAK3))
					bPassed = false;
				break;
			case '4':
				if ((bBaseGame) && (checksum != BASE_CRC_PAK4))
					bPassed = false;
				break;
			case '5':
				if ((bBaseGame) && (checksum != BASE_CRC_PAK5))
					bPassed = false;
				break;
			default:
				break;
		}

		if(!bPassed)
		{
			printf("Checksum mismatch for %s [%02x], not loading.\n", packfile, checksum);
		}
		return bPassed;
	}

	return true;
}
#endif

/*
=================
FS_LoadPackFile

Takes an explicit (not game tree related) path to a pak file.

Loads the header and directory, adding the files at the beginning
of the list so they override previous pack files.
=================
*/
static pack_t *FS_LoadPackFile (const char *packfile)
{
	dpackheader_t	header;
	int				i;
	packfile_t		*newfiles;
	int				numpackfiles;
	pack_t			*pack;
	FILE			*packhandle;
	dpackfile_t		*info;
//  Knightmare added
	unsigned		contentFlags = 0;
#ifdef BINARY_PACK_SEARCH
	int				*sortIndices;
	long			*sortHashes;
#endif

	packhandle = fopen(packfile, "rb");
	if (!packhandle)
		return NULL;

	if (fread(&header, 1, sizeof(header), packhandle) != sizeof(header)) {	// Knightmare- catch bad header
		Com_Printf ("Failed to read header on %s\n", packfile);
		fclose(packhandle);
		return NULL;
	}
	if (LittleLong(header.ident) != SINPAKHEADER)
	{	// Knightmare- made this not fatal
		// Com_Error (ERR_FATAL, "%s is not a packfile", packfile);
		Com_Printf ("%s is not a pak file\n", packfile);
		fclose(packhandle);
		return NULL;
	}
	header.dirofs = LittleLong (header.dirofs);
	header.dirlen = LittleLong (header.dirlen);
	if (header.dirlen < 0 || header.dirofs < 0)
	{
		Com_Printf ("%s has a bad directory length/offset\n", packfile);
		fclose(packhandle);
		return NULL;
	}

	numpackfiles = header.dirlen / sizeof(dpackfile_t);
	if (!numpackfiles) {
		Com_Printf ("%s has no files\n", packfile);
		fclose(packhandle);
		return NULL;
	}
	if (numpackfiles > MAX_FILES_IN_PACK) { // Knightmare- made this not fatal
	//	Com_Error (ERR_FATAL, "%s has %i files", packfile, numpackfiles);
		Com_Printf ("%s has too many files: %i > %i\n", packfile, numpackfiles, MAX_FILES_IN_PACK);
		fclose(packhandle);
		return NULL;
	}

	info = (dpackfile_t *)calloc(numpackfiles, sizeof(dpackfile_t));
	if (!info) {
		Sys_Error("FS_LoadPackFile: Out of memory");
	}
	if (fseek(packhandle, header.dirofs, SEEK_SET) < 0)	// Knightmare- catch seek failure
	{
		Com_Printf ("Seeking to directory failed on %s\n", packfile);
		free(info);
		fclose(packhandle);
		return NULL;
	}
	if (fread(info, 1, header.dirlen, packhandle) != header.dirlen)	// Knightmare- catch dir read failure
	{
		Com_Printf ("Reading directory failed on %s\n", packfile);
		free(info);
		fclose(packhandle);
		return NULL;
	}

#ifdef CHECK_PAK_VERSION
	FS_CalcChecksum(packfile, Com_BlockChecksum ((void *)info, header.dirlen)); /* FS */
#endif

	newfiles = (packfile_t *)Z_Malloc (numpackfiles * sizeof(packfile_t));	// Knightmare- moved newfiles alloc here

#ifdef BINARY_PACK_SEARCH	/* Knightmare- sorting of pak contents for binary search */
	// create sort table
	sortIndices = (int *)Z_Malloc(numpackfiles * sizeof(int));
	sortHashes = (long *)Z_Malloc(numpackfiles * sizeof(long));
	nameHashes = sortHashes;
	for (i = 0; i < numpackfiles; i++)
	{
		sortIndices[i] = i;
		sortHashes[i] = Com_HashFileName(info[i].name, 0, false);
	}
	qsort((void *)sortIndices, numpackfiles, sizeof(int), FS_PakFileCompare);

	// Parse the directory
	for (i = 0; i < numpackfiles; i++)
	{
		strcpy(newfiles[i].name, info[sortIndices[i]].name);
		newfiles[i].hash = sortHashes[sortIndices[i]];
		newfiles[i].filepos = LittleLong(info[sortIndices[i]].filepos);
		newfiles[i].filelen = LittleLong(info[sortIndices[i]].filelen);
		newfiles[i].ignore = FS_FileInPakBlacklist(newfiles[i].name);	// check against pak loading blacklist
		if (!newfiles[i].ignore)	// add type flag for this file
			contentFlags |= FS_TypeFlagForPakItem(newfiles[i].name);
	}

	// free sort table
	Z_Free (sortIndices);
	Z_Free (sortHashes);
	nameHashes = NULL;
#else
// parse the directory
	for (i=0 ; i<numpackfiles ; i++)
	{
		strcpy (newfiles[i].name, info[i].name);
		newfiles[i].hash = Com_HashFileName(info[i].name, 0, false);	// Knightmare- added to speed up seaching
		newfiles[i].filepos = LittleLong(info[i].filepos);
		newfiles[i].filelen = LittleLong(info[i].filelen);
		newfiles[i].ignore = FS_FileInPakBlacklist(info[i].name);	// Knightmare- check against pak loading blacklist
		if (!newfiles[i].ignore)	// Knightmare- add type flag for this file
			contentFlags |= FS_TypeFlagForPakItem(newfiles[i].name);
	}
#endif	// BINARY_PACK_SEARCH

	pack = (pack_t *)Z_Malloc (sizeof (pack_t));
	strcpy (pack->filename, packfile);
	pack->handle = packhandle;
	pack->numfiles = numpackfiles;
	pack->files = newfiles;
	pack->contentFlags = contentFlags;	// Knightmare added

	Com_Printf ("Added packfile %s (%i files)\n", packfile, numpackfiles);

	free(info);

	return pack;
}

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

Sets fs_gamedir, adds the directory to the head of the path,
then loads and adds pak1.pak pak2.pak ...
================
*/
static void FS_AddGameDirectory (const char *dir)
{
	int				i;
	searchpath_t	*search;
	pack_t			*pak = NULL;
	char			pakfile[MAX_OSPATH];

	strncpy (fs_gamedir, dir, sizeof(fs_gamedir)-1);
	fs_gamedir[sizeof(fs_gamedir)-1] = 0;

	//
	// add any pak files in the format pak0.pak pak1.pak, ...
	//
	for (i = 0; i < 32; i++)
	{
		Com_sprintf (pakfile, sizeof(pakfile), "%s/pak%i.sin", dir, i);
		pak = FS_LoadPackFile (pakfile);
		if (!pak)
			continue;
		search = (searchpath_t *) Z_Malloc(sizeof(searchpath_t));
		search->pack = pak;
		search->next = fs_searchpaths;
		fs_searchpaths = search;
	}

	//
	// add the directory to the search path
	//
	search = (searchpath_t *) Z_Malloc(sizeof(searchpath_t));
	strncpy (search->filename, dir, sizeof(search->filename)-1);
	search->filename[sizeof(search->filename)-1] = 0;
	search->next = fs_searchpaths;
	fs_searchpaths = search;

#if defined(_WIN32)		/* to support the Steam version of SiN: */
	Com_sprintf (pakfile, sizeof(pakfile), "%s/locale/en", dir);
	if (FS_IsDirectory(pakfile)) {
		search = (searchpath_t *) Z_Malloc(sizeof(searchpath_t));
		strncpy (search->filename, pakfile, sizeof(search->filename)-1);
		search->filename[sizeof(search->filename)-1] = 0;
		search->next = fs_searchpaths;
		fs_searchpaths = search;
	}
#endif
}

/*
============
FS_Gamedir

Called to find where to write a file (demos, savegames, etc)
============
*/
const char *FS_Gamedir (void)
{
	if (*fs_gamedir)
		return fs_gamedir;
	else
		return BASEDIRNAME;
}


/*
=============
FS_ExecAutoexec
=============
*/
void FS_ExecAutoexec (void)
{
	char *dir;
	char name [MAX_QPATH];

	dir = (char *)Cvar_VariableString("gamedir");
	if (*dir)
		Com_sprintf(name, sizeof(name), "%s/%s/autoexec.cfg", fs_basedir->string, dir);
	else
		Com_sprintf(name, sizeof(name), "%s/%s/autoexec.cfg", fs_basedir->string, BASEDIRNAME);
	if (Sys_FindFirst(name, 0, SFF_SUBDIR | SFF_HIDDEN | SFF_SYSTEM))
		Cbuf_AddText ("exec autoexec.cfg\n");
	Sys_FindClose();
}


/*
================
FS_SetGamedir

Sets the gamedir and path to a different directory.
================
*/
void FS_SetGamedir (const char *dir)
{
	searchpath_t	*next;

	if (!strcmp(dir, ".") || strstr(dir, "..") || strstr(dir, "/")
		|| strstr(dir, "\\") || strstr(dir, ":"))
	{
		Com_Printf ("Gamedir should be a single filename, not a path\n");
		return;
	}

	//
	// free up any current game dir info
	//
	while (fs_searchpaths != fs_base_searchpaths)
	{
		if (fs_searchpaths->pack)
		{
			fclose (fs_searchpaths->pack->handle);
			Z_Free (fs_searchpaths->pack->files);
			Z_Free (fs_searchpaths->pack);
		}
		next = fs_searchpaths->next;
		Z_Free (fs_searchpaths);
		fs_searchpaths = next;
	}

	//
	// flush all data, so it will be forced to reload
	//
	if (dedicated && !dedicated->value)
		Cbuf_AddText ("vid_restart\nsnd_restart\n");

	if (!strcmp(dir,BASEDIRNAME) || (*dir == 0))
	{
		Cvar_FullSet ("gamedir", "", CVAR_SERVERINFO|CVAR_NOSET);
		Cvar_FullSet ("game", "", CVAR_LATCH|CVAR_SERVERINFO);
		Com_sprintf (fs_gamedir, sizeof(fs_gamedir), "%s/%s", fs_basedir->string, BASEDIRNAME);
	}
	else
	{
		Cvar_FullSet ("gamedir", dir, CVAR_SERVERINFO|CVAR_NOSET);
		// 2015 code, add the 2015 game directory if game2015 is set but not a 2015 game
		if (strcmp(dir,"2015") != 0 && strstr(dir,"2015")) {
			FS_AddGameDirectory (va("%s/2015", fs_basedir->string));
			Cvar_FullSet ("game2015", "1", CVAR_SERVERINFO|CVAR_NOSET);
		}
		if (fs_cddir->string[0])
			FS_AddGameDirectory (va("%s/%s", fs_cddir->string, dir));
		/* FS_AddGameDirectory() also sets fs_basedir */
		FS_AddGameDirectory (va("%s/%s", fs_basedir->string, dir));
	}
}


/*
================
FS_Link_f

Creates a filelink_t
================
*/
void FS_Link_f (void)
{
	filelink_t	*l, **prev;
	const char	*to;

	if (Cmd_Argc() != 3)
	{
		Com_Printf ("USAGE: link <from> <to>\n");
		return;
	}

	// r1ch's fix to prevent filesystem browsing
	to = Cmd_Argv(2);
	if (to[0])
	{
		if (strstr(to, "..") || strchr(to, '\\') || *to != '.')
		{
			Com_Printf ("Illegal destination path.\n");
			return;
		}
	}

	// see if the link already exists
	prev = &fs_links;
	for (l = fs_links; l; l = l->next)
	{
		if (!strcmp (l->from, Cmd_Argv(1)))
		{
			Z_Free (l->to);
			if (!strlen(Cmd_Argv(2)))
			{	// delete it
				*prev = l->next;
				Z_Free (l->from);
				Z_Free (l);
				return;
			}
			l->to = CopyString (Cmd_Argv(2));
			return;
		}
		prev = &l->next;
	}

	// create a new link
	l = (filelink_t *)Z_Malloc(sizeof(*l));
	l->next = fs_links;
	fs_links = l;
	l->from = CopyString(Cmd_Argv(1));
	l->fromlength = (int)strlen(l->from);
	l->to = CopyString(Cmd_Argv(2));
}

/*
** FS_ListFiles
*/
char **FS_ListFiles (const char *findname, int *numfiles, unsigned musthave, unsigned canthave)
{
	char *s;
	int nfiles = 0;
	char **list = NULL;

	s = Sys_FindFirst(findname, musthave, canthave);
	while (s)
	{
		if (s[strlen(s)-1] != '.')
			nfiles++;
		s = Sys_FindNext(musthave, canthave);
	}
	Sys_FindClose ();

	if (!nfiles) {
		*numfiles = 0;
		return NULL;
	}

	nfiles++; // add space for a guard
	*numfiles = nfiles;

	list = (char **) malloc(sizeof(char *) * nfiles);
	memset(list, 0, sizeof(char *) * nfiles);

	s = Sys_FindFirst(findname, musthave, canthave);
	nfiles = 0;
	while (s)
	{
		if (s[strlen(s)-1] != '.')
		{
			list[nfiles] = strdup(s);
#if defined(_WIN32) || defined(__MSDOS__)
			Q_strlwr(list[nfiles]);
#endif
			nfiles++;
		}
		s = Sys_FindNext(musthave, canthave);
	}
	Sys_FindClose ();

	return list;
}

/*
** FS_Dir_f
*/
void FS_Dir_f (void)
{
	char	*path = NULL;
	char	findname[1024];
	char	wildcard[1024] = "*.*";
	char	**dirnames;
	int		ndirs;

	if (Cmd_Argc() != 1)
	{
		strcpy(wildcard, Cmd_Argv(1));
	}

	while ((path = FS_NextPath(path)) != NULL)
	{
		char *tmp = findname;

		Com_sprintf(findname, sizeof(findname), "%s/%s", path, wildcard);

		while (*tmp != 0)
		{
			if (*tmp == '\\')
				*tmp = '/';
			tmp++;
		}
		Com_Printf("Directory of %s\n", findname);
		Com_Printf("----\n");

		if ((dirnames = FS_ListFiles(findname, &ndirs, 0, 0)) != NULL)
		{
			int i;

			for (i = 0; i < ndirs-1; i++)
			{
				if (strrchr(dirnames[i], '/'))
					Com_Printf("%s\n", strrchr(dirnames[i], '/') + 1);
				else
					Com_Printf("%s\n", dirnames[i]);

				free(dirnames[i]);
			}
			free(dirnames);
		}
		Com_Printf("\n");
	};
}

/*
============
FS_Path_f

============
*/
void FS_Path_f (void)
{
	searchpath_t	*s;
	filelink_t		*l;

	Com_Printf ("Current search path:\n");
	for (s = fs_searchpaths; s; s = s->next)
	{
		if (s == fs_base_searchpaths)
			Com_Printf ("----------\n");
		if (s->pack)
		//	Com_Printf("%s (%i files, content flags: %d)\n", s->pack->filename, s->pack->numfiles, s->pack->contentFlags);
			Com_Printf ("%s (%i files)\n", s->pack->filename, s->pack->numfiles);
		else
			Com_Printf ("%s\n", s->filename);
	}

	Com_Printf ("\nLinks:\n");
	for (l = fs_links; l; l = l->next)
		Com_Printf ("%s : %s\n", l->from, l->to);
}

/*
================
FS_NextPath

Allows enumerating all of the directories in the search path
================
*/
char *FS_NextPath (const char *prevpath)
{
	searchpath_t	*s;
	char			*prev;

	if (!prevpath)
		return fs_gamedir;

	prev = fs_gamedir;
	for (s = fs_searchpaths; s; s = s->next)
	{
		if (s->pack)
			continue;
		if (prevpath == prev)
			return s->filename;
		prev = s->filename;
	}

	return NULL;
}


/*
================
FS_InitFilesystem
================
*/
void FS_InitFilesystem (void)
{
	size_t sz;
	char *str;

	Cmd_AddCommand ("path", FS_Path_f);
	Cmd_AddCommand ("link", FS_Link_f);
	Cmd_AddCommand ("dir", FS_Dir_f);

	//
	// basedir <path>
	// allows the game to run from outside the data tree
	//
	fs_basedir = Cvar_Get ("basedir", ".", CVAR_NOSET);
	sz = strlen(fs_basedir->string);
	if (!sz) { /* no empty basedir name */
		Cvar_ForceSet ("basedir", ".");
	}
	else { /* remove trailing dirsep */
		str = fs_basedir->string + (sz - 1);
		if (*str == '/' || *str == '\\') {
			str = strdup(fs_basedir->string);
			str[sz-1] = '\0';
			Cvar_ForceSet ("basedir", str);
			free(str);
		}
	}

	//
	// cddir <path>
	// Logically concatenates the cddir after the basedir for 
	// allows the game to run from outside the data tree
	//
	fs_cddir = Cvar_Get ("cddir", "", CVAR_NOSET);
	if ((sz = strlen(fs_cddir->string)) != 0) {
		/* remove trailing dirsep */
		str = fs_cddir->string + (sz - 1);
		if (*str == '/' || *str == '\\') {
			str = strdup(fs_cddir->string);
			str[sz-1] = '\0';
			Cvar_ForceSet ("cddir", str);
			free(str);
		}
	}
	if (fs_cddir->string[0])
		FS_AddGameDirectory (va("%s/" BASEDIRNAME, fs_cddir->string));

	//
	// start up with baseq2 by default
	//
	FS_AddGameDirectory (va("%s/" BASEDIRNAME, fs_basedir->string));

	// any set gamedirs will be freed up to here
	fs_base_searchpaths = fs_searchpaths;

	// check for game override
	fs_gamedirvar = Cvar_Get ("game", "", CVAR_LATCH|CVAR_SERVERINFO);
	if (fs_gamedirvar->string[0])
		FS_SetGamedir (fs_gamedirvar->string);

	FS_InitPlayerdir();
}

qboolean FS_RenameFile (const char *src, const char *dst)
{
	return (rename(src, dst) == 0);
}

qboolean FS_CopyFile (const char *src, const char *dst)
{
	FILE	*f1, *f2;
	int		l;
	byte	buffer[65536];

	Com_DPrintf(DEVELOPER_MSG_IO, "CopyFile (%s, %s)\n", src, dst);
	strcpy ((char *) buffer, dst);
	FS_CreatePath ((char *) buffer);

	f1 = fopen (src, "rb");
	if (!f1)
		return false;
	f2 = fopen (dst, "wb");
	if (!f2)
	{
		fclose (f1);
		return false;
	}

	while (1)
	{
		l = fread (buffer, 1, sizeof(buffer), f1);
		if (!l)
			break;
		fwrite (buffer, 1, l, f2);
	}

	fclose (f1);
	fclose (f2);

	return true;
}

void FS_FreeList (const char **list, int n)
{
	int i;
	
	if (list) {
		for (i = 0; i < n; i++) {
			if (list[i]) {
				free((void *)list[i]);
				list[i] = 0;
			}
		}
		free((void *)list);
	}
}

/*
============
FS_InitPlayerdir

Sets the playerdir and path to a different directory.
============
*/
void FS_InitPlayerdir (void)
{
	char path[MAX_OSPATH];
	char playerpath[MAX_OSPATH];
	int i;

	cl_player = Cvar_Get ("player", "blade", 0);

	strcpy(playerpath, cl_player->string);

	// Convert spaces to underscores
	for (i = 0; i < strlen(playerpath); i++)
	{
		if (playerpath[i] == ' ')
			playerpath[i] = '_';
	}

	Com_sprintf(fs_relativeplayerdir, sizeof(fs_relativeplayerdir), "players/%s", playerpath);
	Com_sprintf(fs_playerdir, sizeof(fs_playerdir), "%s/%s", FS_Gamedir(), fs_relativeplayerdir);
	Com_sprintf(path, sizeof(path), "%s/", fs_playerdir);
	FS_CreatePath(path);
}

/*
============
FS_RelativePlayerdir

Called to find where to write a file (configs, savegames, etc) relative to the basedir
============
*/
const char *FS_RelativePlayerdir (void)
{
	return fs_relativeplayerdir;
}

/*
============
FS_Playerdir

Called to find where to write a file (configs, savegames, etc)
============
*/
const char *FS_Playerdir (void)
{
	return fs_playerdir;
}

/*
============
FS_CDdir

Called to get path to the cd drive
============
*/
const char *FS_CDdir (void)
{
	static char name[MAX_QPATH];

	name[0] = 0;
	if (strlen(fs_cddir->string) > 1)
		sprintf(name, "%s/%s", fs_cddir->string, BASEDIRNAME);

	return name;
}

#define MAX_FIND_FILES 1024 /* FS: Was 512 and bombing Wages of Sin since there's 583 objects */

/*
** FS_FindFiles
*/
const char **FS_FindFiles (const char *path, const char * pattern, int *numfiles)
{
	const char **list = NULL;
	searchpath_t *search;
	const char *ptr;
	pack_t *pak;
	int i, j, len, pathlen;
	qboolean found;
	char searchpath[MAX_OSPATH];
	char globpattern[32];

	// bullet proof the code
	if (!path || !pattern || !numfiles)
		return NULL;

	// initialize
	*numfiles = 0;

	// copy over the path
	// fix the slashes and make sure we end with one
	len = (int)strlen( path );
	for (i = 0; i < len; i++)
	{
		if (path[i] == '\\')
			searchpath[i] = '/';
		else
			searchpath[i] = path[i];
	}

	// check for trailing slash
	if (searchpath[i-1] != '/')
	{
		searchpath[i] = '/';
		i++;
	}

	// terminate it
	searchpath[i] = '\0';
	pathlen = i;

	// copy over the pattern
	strncpy(globpattern, pattern, sizeof(globpattern)-1);
	// see if the pattern should be simplified
	if (!strcmp(globpattern, "*.*"))
		strcpy(globpattern, "*");

	// allocate the space necessary
	list = (const char **) malloc(sizeof(char *) * MAX_FIND_FILES); /* FS: FIXME: Either make this dynamically grow as needed or bail out when limit is hit */
	if (!list)
	{
		Com_Printf("Could not allocate list for FS_FindFiles\n");
		return NULL;
	}

	memset((void *)list, 0, sizeof(char *) * MAX_FIND_FILES);

	// search through the path, one element at a time
	for (search = fs_searchpaths; search; search = search->next)
	{
		// is the element a pak file?
		if (search->pack)
		{
			// look through all the pak file elements
			pak = search->pack;
			for (i = 0; i < pak->numfiles ; i++)
			{
				if (!strnicmp(searchpath, pak->files[i].name, pathlen))
				{
					ptr = pak->files[i].name + pathlen;

					if (glob_match(pattern, ptr))
					{
						found = false;
						for (j = 0; j < *numfiles; j++)
						{
							if (!stricmp(pak->files[i].name, list[j]))
							{
								found = true;
								break;
							}
						}

						if (!found)
						{
							list[*numfiles] = strdup(pak->files[i].name);
							*numfiles = *numfiles + 1;
						}
					}
				}
			}
		}
		else
		{
			char foundname[MAX_OSPATH];
			char findname[MAX_OSPATH];
			char ** files;
			int numf;

			// check a file in the directory tree
			Com_sprintf(findname, sizeof(findname), "%s/%s%s", search->filename, searchpath, pattern);
			files = FS_ListFiles(findname, &numf, 0, 0);

			// see if it exists in the list and if it doesn't, add it to the list
			if (files)
			{
				for (i = 0; i < (numf - 1); i++)
				{
					ptr = strstr(files[i], searchpath);
					// make sure we found the path
					if (ptr)
					{
						strcpy (foundname, ptr);
						found = false;

						for (j = 0; j < *numfiles; j++)
						{
							if (!stricmp(list[j], foundname))
							{
								found = true;
								break;
							}
						}

						if (!found)
						{
							list[*numfiles] = strdup(foundname);
							*numfiles = *numfiles + 1;
						}
					}

					if (files[i])
					{
						free(files[i]);
					}
				}

				free(files);
			}
		}
	}

	return list;
}

/*
===========
FS_GameDirectoryExists

Checks to see if a particular game directory exists
===========
*/
qboolean FS_GameDirectoryExists (const char *gamedirname)
{
	char fullpath[MAX_OSPATH];

	// check base dir first
	Com_sprintf (fullpath, sizeof(fullpath), "%s/%s", fs_basedir->string, gamedirname);
	// see if the directory exists
	if (FS_IsDirectory(fullpath))
		return true;

	// check cdrom
	if (!fs_cddir->string[0])
		return false;
	Com_sprintf (fullpath, sizeof(fullpath), "%s/%s", fs_cddir->string, gamedirname);
	// see if the directory exists
	if (FS_IsDirectory(fullpath))
		return true;

	return false;
}
