/*
filesystem.c - game filesystem based on DP fs
Copyright (C) 2003-2006 Mathieu Olivier
Copyright (C) 2000-2007 DarkPlaces contributors
Copyright (C) 2007 Uncle Mike
Copyright (C) 2015-2023 Xash3D FWGS contributors

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 3 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.
*/

#include "build.h"
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#if XASH_WIN32
#include <direct.h>
#include <io.h>
#elif XASH_DOS4GW
#include <direct.h>
#include <errno.h>
#else
#include <dirent.h>
#include <errno.h>
#endif
#include <stdio.h>
#include <stdarg.h>
#include "port.h"
#include "defaults.h"
#include "const.h"
#include "crtlib.h"
#include "crclib.h"
#include "filesystem.h"
#include "filesystem_internal.h"
#include "xash3d_mathlib.h"
#include "common/com_strings.h"
#include "common/protocol.h"

#define FILE_COPY_SIZE		(1024 * 1024)

fs_globals_t FI;
qboolean      fs_ext_path = false;	// attempt to read\write from ./ or ../ pathes
poolhandle_t  fs_mempool;
char          fs_rodir[MAX_SYSPATH];
char          fs_rootdir[MAX_SYSPATH];
searchpath_t *fs_writepath;

static searchpath_t *fs_searchpaths = NULL;	// chain
static char			fs_basedir[MAX_SYSPATH];	// base game directory
static char			fs_gamedir[MAX_SYSPATH];	// game current directory

// add archives in specific order PAK -> PK3 -> WAD
// so raw WADs takes precedence over WADs included into PAKs and PK3s
const fs_archive_t g_archives[] =
{
{ "pak",    SEARCHPATH_PAK,    FS_AddPak_Fullpath, true },
{ "pk3",    SEARCHPATH_ZIP,    FS_AddZip_Fullpath, true },
{ "pk3dir", SEARCHPATH_PK3DIR, FS_AddDir_Fullpath, true },
{ "wad",    SEARCHPATH_WAD,    FS_AddWad_Fullpath, false },
{ NULL }, // end marker
};

// special fs_archive_t for plain directories
static const fs_archive_t g_directory_archive =
{ NULL, SEARCHPATH_PLAIN, FS_AddDir_Fullpath, false };

#ifdef XASH_ANDROID
static const fs_archive_t g_android_archive =
{ NULL, SEARCHPATH_ANDROID_ASSETS, FS_AddAndroidAssets_Fullpath, false };
#endif

#ifdef XASH_REDUCE_FD
static file_t *fs_last_readfile;
static zip_t *fs_last_zip;

static void FS_EnsureOpenFile( file_t *file )
{
	if( fs_last_readfile == file )
		return;

	if( file && !file->backup_path )
		return;

	if( fs_last_readfile && (fs_last_readfile->handle != -1) )
	{
		fs_last_readfile->backup_position = lseek(  fs_last_readfile->handle, 0, SEEK_CUR );
		close( fs_last_readfile->handle );
		fs_last_readfile->handle = -1;
	}
	fs_last_readfile = file;
	if( file && (file->handle == -1) )
	{
		file->handle = open( file->backup_path, file->backup_options );
		lseek( file->handle, file->backup_position, SEEK_SET );
	}
}

static void FS_BackupFileName( file_t *file, const char *path, uint options )
{
	if( path == NULL )
	{
		if( file->backup_path )
			Mem_Free( (void*)file->backup_path );
		if( file == fs_last_readfile )
			FS_EnsureOpenFile( NULL );
	}
	else if( options == O_RDONLY || options == (O_RDONLY|O_BINARY) )
	{
		file->backup_path = copystring( path );
		file->backup_options = options;
	}
}
#else
static void FS_EnsureOpenFile( file_t *file ) {}
static void FS_BackupFileName( file_t *file, const char *path, uint options ) {}
#endif

static void FS_InitMemory( void );
static void FS_Purge( file_t* file );

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

FILEMATCH COMMON SYSTEM

=============================================================================
*/
void stringlistinit( stringlist_t *list )
{
	memset( list, 0, sizeof( *list ));
}

void stringlistfreecontents( stringlist_t *list )
{
	int	i;

	for( i = 0; i < list->numstrings; i++ )
	{
		if( list->strings[i] )
			Mem_Free( list->strings[i] );
		list->strings[i] = NULL;
	}

	if( list->strings )
		Mem_Free( list->strings );

	list->numstrings = 0;
	list->maxstrings = 0;
	list->strings = NULL;
}

void stringlistappend( stringlist_t *list, const char *text )
{
	size_t	textlen;

	if( !Q_strcmp( text, "." ) || !Q_strcmp( text, ".." ))
		return; // ignore the virtual directories

	if( list->numstrings >= list->maxstrings )
	{
		list->maxstrings += 4096;
		list->strings = Mem_Realloc( fs_mempool, list->strings, list->maxstrings * sizeof( *list->strings ));
	}

	textlen = Q_strlen( text ) + 1;
	list->strings[list->numstrings] = Mem_Calloc( fs_mempool, textlen );
	memcpy( list->strings[list->numstrings], text, textlen );
	list->numstrings++;
}

void stringlistsort( stringlist_t *list )
{
	char	*temp;
	int	i, j;

	// this is a selection sort (finds the best entry for each slot)
	for( i = 0; i < list->numstrings - 1; i++ )
	{
		for( j = i + 1; j < list->numstrings; j++ )
		{
			if( Q_strcmp( list->strings[i], list->strings[j] ) > 0 )
			{
				temp = list->strings[i];
				list->strings[i] = list->strings[j];
				list->strings[j] = temp;
			}
		}
	}
}

#if XASH_DOS4GW
// convert names to lowercase because dos doesn't care, but pattern matching code often does
static void listlowercase( stringlist_t *list )
{
	char	*c;
	int	i;

	for( i = 0; i < list->numstrings; i++ )
	{
		for( c = list->strings[i]; *c; c++ )
			*c = Q_tolower( *c );
	}
}
#endif

void listdirectory( stringlist_t *list, const char *path )
{
#if XASH_WIN32
	char pattern[4096];
	struct _finddata_t	n_file;
	intptr_t		hFile;
#else
	DIR *dir;
	struct dirent *entry;
#endif

#if XASH_WIN32
	Q_snprintf( pattern, sizeof( pattern ), "%s/*", path );

	// ask for the directory listing handle
	hFile = _findfirst( pattern, &n_file );
	if( hFile == -1 ) return;

	// start a new chain with the the first name
	stringlistappend( list, n_file.name );
	// iterate through the directory
	while( _findnext( hFile, &n_file ) == 0 )
		stringlistappend( list, n_file.name );
	_findclose( hFile );
#else
	if( !( dir = opendir( path ) ) )
		return;

	// iterate through the directory
	while( ( entry = readdir( dir ) ))
		stringlistappend( list, entry->d_name );
	closedir( dir );
#endif

#if XASH_DOS4GW
	// convert names to lowercase because 8.3 always in CAPS
	listlowercase( list );
#endif
}

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

OTHER PRIVATE FUNCTIONS

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

#if XASH_WIN32
/*
====================
FS_PathToWideChar

Converts input UTF-8 string to wide char string.
====================
*/
static const wchar_t *FS_PathToWideChar( const char *path )
{
	static wchar_t pathBuffer[MAX_PATH];
	MultiByteToWideChar( CP_UTF8, 0, path, -1, pathBuffer, MAX_PATH );
	return pathBuffer;
}
#endif

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

Only used for FS_Open.
============
*/
void FS_CreatePath( char *path )
{
	char	*ofs, save;

	for( ofs = path + 1; *ofs; ofs++ )
	{
		if( *ofs == '/' || *ofs == '\\' )
		{
			// create the directory
			save = *ofs;
			*ofs = 0;
			_mkdir( path );
			*ofs = save;
		}
	}
}

searchpath_t *FS_AddArchive_Fullpath( const fs_archive_t *archive, const char *file, int flags )
{
	searchpath_t *search;

	for( search = fs_searchpaths; search; search = search->next )
	{
		if( search->type == archive->type && !Q_stricmp( search->filename, file ))
			return search; // already loaded
	}

	search = archive->pfnAddArchive_Fullpath( file, flags );

	if( !search )
		return NULL;

	search->next = fs_searchpaths;
	fs_searchpaths = search;

	// time to add in search list all the wads from this archive
	if( archive->load_wads && !FBitSet( flags, FS_SKIP_ARCHIVED_WADS ))
	{
		stringlist_t list;
		int i;

		stringlistinit( &list );
		search->pfnSearch( search, &list, "*.wad", true );
		stringlistsort( &list ); // keep always sorted

		for( i = 0; i < list.numstrings; i++ )
		{
			searchpath_t *wad;
			char fullpath[MAX_SYSPATH];

			Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", file, list.strings[i] );
			if(( wad = FS_AddWad_Fullpath( fullpath, flags )))
			{
				wad->next = fs_searchpaths;
				fs_searchpaths = wad;
			}
		}

		stringlistfreecontents( &list );
	}

	return search;
}

/*
================
FS_AddArchive_Fullpath
================
*/
static searchpath_t *FS_MountArchive_Fullpath( const char *file, int flags )
{
	const fs_archive_t *archive;
	const char *ext = COM_FileExtension( file );

	for( archive = g_archives; archive->ext; archive++ )
	{
		if( !Q_stricmp( ext, archive->ext ))
			return FS_AddArchive_Fullpath( archive, file, flags );
	}

	return NULL;
}

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

Sets fs_writepath, adds the directory to the head of the path,
then loads and adds pak1.pak pak2.pak ...
================
*/
void FS_AddGameDirectory( const char *dir, uint flags )
{
	const fs_archive_t *archive;
	stringlist_t list;
	searchpath_t *search;
	char fullpath[MAX_SYSPATH];
	int i;

	stringlistinit( &list );
	listdirectory( &list, dir );
	stringlistsort( &list );

	for( archive = g_archives; archive->ext; archive++ )
	{
		if( archive->type == SEARCHPATH_WAD ) // HACKHACK: wads need direct paths but only in this function
			FS_AllowDirectPaths( true );

		for( i = 0; i < list.numstrings; i++ )
		{
			const char *ext = COM_FileExtension( list.strings[i] );

			if( Q_stricmp( ext, archive->ext ))
				continue;

			Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", dir, list.strings[i] );
			FS_AddArchive_Fullpath( archive, fullpath, flags );
		}

		FS_AllowDirectPaths( false );
	}

	stringlistfreecontents( &list );

#ifdef XASH_ANDROID
	FS_AddArchive_Fullpath( &g_android_archive, dir, flags );
#endif

	// add the directory to the search path
	// (unpacked files have the priority over packed files)
	search = FS_AddArchive_Fullpath( &g_directory_archive, dir, flags );
	if( !FBitSet( flags, FS_NOWRITE_PATH ))
		fs_writepath = search;
}

/*
================
FS_ClearSearchPath
================
*/
void FS_ClearSearchPath( void )
{
	searchpath_t *cur, **prev;

	prev = &fs_searchpaths;

	while( true )
	{
		cur = *prev;

		if( !cur )
			break;

		// never delete static paths
		if( FBitSet( cur->flags, FS_STATIC_PATH ))
		{
			prev = &cur->next;
			continue;
		}

		*prev = cur->next;
		cur->pfnClose( cur );
		Mem_Free( cur );
	}
}

/*
====================
FS_CheckNastyPath
Return true if the path should be rejected due to one of the following:
1: path elements that are non-portable
2: path elements that would allow access to files outside the game directory,
	or are just not a good idea for a mod to be using.
====================
*/
static int FS_CheckNastyPath( const char *path )
{
	// all: never allow an empty path, as for gamedir it would access the parent directory and a non-gamedir path it is just useless
	if( !COM_CheckString( path )) return 2;

	if( fs_ext_path ) return 0;     // allow any path

	// Mac: don't allow Mac-only filenames - : is a directory separator
	// instead of /, but we rely on / working already, so there's no reason to
	// support a Mac-only path
	// Amiga and Windows: : tries to go to root of drive
	if( Q_strchr( path, ':' )) return 1; // non-portable attempt to go to root of drive

	// Amiga: // is parent directory
	if( Q_strstr( path, "//")) return 1; // non-portable attempt to go to parent directory

	// all: don't allow going to parent directory (../ or /../)
	if( Q_strstr( path, "..")) return 2; // attempt to go outside the game directory

	// Windows and UNIXes: don't allow absolute paths
	if( path[0] == '/') return 2; // attempt to go outside the game directory

#if 0
	// all: forbid trailing slash on gamedir
	if( isgamedir && path[Q_strlen(path)-1] == '/' ) return 2;
#endif

	// all: forbid leading dot on any filename for any reason
	if( Q_strstr(path, "/.")) return 2; // attempt to go outside the game directory

	// after all these checks we're pretty sure it's a / separated filename
	// and won't do much if any harm
	return false;
}

/*
================
FS_WriteGameInfo

assume GameInfo is valid
================
*/
static void FS_WriteGameInfo( const char *filepath, gameinfo_t *GameInfo )
{
	file_t	*f = FS_Open( filepath, "w", false ); // we in binary-mode
	int	i, write_ambients = false;

	if( !f ) Sys_Error( "FS_WriteGameInfo: can't write %s\n", filepath );	// may be disk-space is out?

	FS_Printf( f, "// generated by " XASH_ENGINE_NAME " " XASH_VERSION "-%s (%s-%s)\n\n\n", Q_buildcommit(), Q_buildos(), Q_buildarch() );

	if( COM_CheckStringEmpty( GameInfo->basedir ) )
		FS_Printf( f, "basedir\t\t\"%s\"\n", GameInfo->basedir );

	// DEPRECATED: gamedir key isn't supported by FWGS fork
	// but write it anyway to keep compability with original Xash3D
	if( COM_CheckStringEmpty( GameInfo->gamefolder ) )
		FS_Printf( f, "gamedir\t\t\"%s\"\n", GameInfo->gamefolder );

	if( COM_CheckStringEmpty( GameInfo->falldir ) )
		FS_Printf( f, "fallback_dir\t\"%s\"\n", GameInfo->falldir );

	if( COM_CheckStringEmpty( GameInfo->title ) )
		FS_Printf( f, "title\t\t\"%s\"\n", GameInfo->title );

	if( COM_CheckStringEmpty( GameInfo->startmap ) )
		FS_Printf( f, "startmap\t\t\"%s\"\n", GameInfo->startmap );

	if( COM_CheckStringEmpty( GameInfo->trainmap ) )
		FS_Printf( f, "trainmap\t\t\"%s\"\n", GameInfo->trainmap );

	if( GameInfo->version != 0.0f )
		FS_Printf( f, "version\t\t%g\n", GameInfo->version );

	if( GameInfo->size != 0 )
		FS_Printf( f, "size\t\t%lu\n", GameInfo->size );

	if( COM_CheckStringEmpty( GameInfo->game_url ) )
		FS_Printf( f, "url_info\t\t\"%s\"\n", GameInfo->game_url );

	if( COM_CheckStringEmpty( GameInfo->update_url ) )
		FS_Printf( f, "url_update\t\t\"%s\"\n", GameInfo->update_url );

	if( COM_CheckStringEmpty( GameInfo->type ) )
		FS_Printf( f, "type\t\t\"%s\"\n", GameInfo->type );

	if( COM_CheckStringEmpty( GameInfo->date ) )
		FS_Printf( f, "date\t\t\"%s\"\n", GameInfo->date );

	if( COM_CheckStringEmpty( GameInfo->dll_path ) )
		FS_Printf( f, "dllpath\t\t\"%s\"\n", GameInfo->dll_path );

	if( COM_CheckStringEmpty( GameInfo->game_dll ) )
		FS_Printf( f, "gamedll\t\t\"%s\"\n", GameInfo->game_dll );

	if( COM_CheckStringEmpty( GameInfo->game_dll_linux ) )
		FS_Printf( f, "gamedll_linux\t\t\"%s\"\n", GameInfo->game_dll_linux );

	if( COM_CheckStringEmpty( GameInfo->game_dll_osx ) )
		FS_Printf( f, "gamedll_osx\t\t\"%s\"\n", GameInfo->game_dll_osx );

	if( COM_CheckStringEmpty( GameInfo->iconpath ))
		FS_Printf( f, "icon\t\t\"%s\"\n", GameInfo->iconpath );

	switch( GameInfo->gamemode )
	{
	case 1: FS_Print( f, "gamemode\t\t\"singleplayer_only\"\n" ); break;
	case 2: FS_Print( f, "gamemode\t\t\"multiplayer_only\"\n" ); break;
	}

	if( COM_CheckStringEmpty( GameInfo->sp_entity ))
		FS_Printf( f, "sp_entity\t\t\"%s\"\n", GameInfo->sp_entity );
	if( COM_CheckStringEmpty( GameInfo->mp_entity ))
		FS_Printf( f, "mp_entity\t\t\"%s\"\n", GameInfo->mp_entity );
	if( COM_CheckStringEmpty( GameInfo->mp_filter ))
		FS_Printf( f, "mp_filter\t\t\"%s\"\n", GameInfo->mp_filter );

	if( GameInfo->secure )
		FS_Printf( f, "secure\t\t\"%i\"\n", GameInfo->secure );

	if( GameInfo->nomodels )
		FS_Printf( f, "nomodels\t\t\"%i\"\n", GameInfo->nomodels );

	if( GameInfo->max_edicts > 0 )
		FS_Printf( f, "max_edicts\t%i\n", GameInfo->max_edicts );
	if( GameInfo->max_tents > 0 )
		FS_Printf( f, "max_tempents\t%i\n", GameInfo->max_tents );
	if( GameInfo->max_beams > 0 )
		FS_Printf( f, "max_beams\t\t%i\n", GameInfo->max_beams );
	if( GameInfo->max_particles > 0 )
		FS_Printf( f, "max_particles\t%i\n", GameInfo->max_particles );

	for( i = 0; i < NUM_AMBIENTS; i++ )
	{
		if( *GameInfo->ambientsound[i] )
		{
			if( !write_ambients )
			{
				FS_Print( f, "\n" );
				write_ambients = true;
			}
			FS_Printf( f, "ambient%i\t\t%s\n", i, GameInfo->ambientsound[i] );
		}
	}

	if( GameInfo->noskills )
		FS_Printf( f, "noskills\t\t\"%i\"\n", GameInfo->noskills );

#define SAVE_AGED_COUNT 2 // the default count of quick and auto saves
	if( GameInfo->quicksave_aged_count != SAVE_AGED_COUNT )
		FS_Printf( f, "quicksave_aged_count\t\t%d\n", GameInfo->quicksave_aged_count );

	if( GameInfo->autosave_aged_count != SAVE_AGED_COUNT )
		FS_Printf( f, "autosave_aged_count\t\t%d\n", GameInfo->autosave_aged_count );
#undef SAVE_AGED_COUNT

	// HL25 compatibility
	if( GameInfo->animated_title )
		FS_Printf( f, "animated_title\t\t%i\n", GameInfo->animated_title );

	if( GameInfo->hd_background )
		FS_Printf( f, "hd_background\t\t%i\n", GameInfo->hd_background );

	// always expose our extensions :)
	FS_Printf( f, "internal_vgui_support\t\t%s\n", GameInfo->internal_vgui_support ? "1" : "0" );
	FS_Printf( f, "render_picbutton_text\t\t%s\n", GameInfo->render_picbutton_text ? "1" : "0" );

	FS_Close( f );	// all done
}

void FS_InitGameInfo( gameinfo_t *GameInfo, const char *gamedir )
{
	memset( GameInfo, 0, sizeof( *GameInfo ));

	// filesystem info
	Q_strncpy( GameInfo->gamefolder, gamedir, sizeof( GameInfo->gamefolder ));
	Q_strncpy( GameInfo->basedir, "valve", sizeof( GameInfo->basedir ));
	GameInfo->falldir[0] = 0;
	Q_strncpy( GameInfo->startmap, "c0a0", sizeof( GameInfo->startmap ));
	Q_strncpy( GameInfo->trainmap, "t0a0", sizeof( GameInfo->trainmap ));
	Q_strncpy( GameInfo->title, "New Game", sizeof( GameInfo->title ));
	GameInfo->version = 1.0f;

	// .dll pathes
	Q_strncpy( GameInfo->dll_path, "cl_dlls", sizeof( GameInfo->dll_path ));
	Q_strncpy( GameInfo->game_dll, "dlls/hl.dll", sizeof( GameInfo->game_dll ));
	Q_strncpy( GameInfo->game_dll_linux, "dlls/hl.so", sizeof( GameInfo->game_dll_linux ));
	Q_strncpy( GameInfo->game_dll_osx, "dlls/hl.dylib", sizeof( GameInfo->game_dll_osx ));

	// .ico path
	Q_strncpy( GameInfo->iconpath, "game.ico", sizeof( GameInfo->iconpath ));

	Q_strncpy( GameInfo->sp_entity, "info_player_start", sizeof( GameInfo->sp_entity ));
	Q_strncpy( GameInfo->mp_entity, "info_player_deathmatch", sizeof( GameInfo->mp_entity ));

	GameInfo->max_edicts     = DEFAULT_MAX_EDICTS; // default value if not specified
	GameInfo->max_tents      = 500;
	GameInfo->max_beams      = 128;
	GameInfo->max_particles  = 4096;
}

void FS_ParseGenericGameInfo( gameinfo_t *GameInfo, const char *buf, const qboolean isGameInfo )
{
	char *pfile = (char*) buf;
	qboolean found_linux = false, found_osx = false;
	string token;

	while(( pfile = COM_ParseFile( pfile, token, sizeof( token ))) != NULL )
	{
		// different names in liblist/gameinfo
		if( !Q_stricmp( token, isGameInfo ? "title" : "game" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->title, sizeof( GameInfo->title ));
		}
		// valid for both
		else if( !Q_stricmp( token, "fallback_dir" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->falldir, sizeof( GameInfo->falldir ));
		}
		// valid for both
		else if( !Q_stricmp( token, "startmap" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->startmap, sizeof( GameInfo->startmap ));
			COM_StripExtension( GameInfo->startmap ); // HQ2:Amen has extension .bsp
		}
		// only trainmap is valid for gameinfo
		else if( !Q_stricmp( token, "trainmap" ) ||
			(!isGameInfo && !Q_stricmp( token, "trainingmap" )))
		{
			pfile = COM_ParseFile( pfile, GameInfo->trainmap, sizeof( GameInfo->trainmap ));
			COM_StripExtension( GameInfo->trainmap ); // HQ2:Amen has extension .bsp
		}
		// valid for both
		else if( !Q_stricmp( token, "url_info" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->game_url, sizeof( GameInfo->game_url ));
		}
		// different names
		else if( !Q_stricmp( token, isGameInfo ? "url_update" : "url_dl" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->update_url, sizeof( GameInfo->update_url ));
		}
		// valid for both
		else if( !Q_stricmp( token, "gamedll" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->game_dll, sizeof( GameInfo->game_dll ));
			COM_FixSlashes( GameInfo->game_dll );
		}
		// valid for both
		else if( !Q_stricmp( token, "gamedll_linux" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->game_dll_linux, sizeof( GameInfo->game_dll_linux ));
			found_linux = true;
		}
		// valid for both
		else if( !Q_stricmp( token, "gamedll_osx" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->game_dll_osx, sizeof( GameInfo->game_dll_osx ));
			found_osx = true;
		}
		// valid for both
		else if( !Q_stricmp( token, "icon" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->iconpath, sizeof( GameInfo->iconpath ));
			COM_FixSlashes( GameInfo->iconpath );
			COM_DefaultExtension( GameInfo->iconpath, ".ico", sizeof( GameInfo->iconpath ));
		}
		else if( !Q_stricmp( token, "type" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));

			if( isGameInfo )
			{
				Q_strncpy( GameInfo->type, token, sizeof( GameInfo->type ));
			}
			else
			{
				if( !Q_stricmp( token, "singleplayer_only" ))
				{
					// TODO: Remove this ugly hack too.
					// This was made because Half-Life has multiplayer,
					// but for some reason it's marked as singleplayer_only.
					// Old WON version is fine.
					if( !Q_stricmp( GameInfo->gamefolder, "valve") )
						GameInfo->gamemode = GAME_NORMAL;
					else
						GameInfo->gamemode = GAME_SINGLEPLAYER_ONLY;
					Q_strncpy( GameInfo->type, "Single", sizeof( GameInfo->type ));
				}
				else if( !Q_stricmp( token, "multiplayer_only" ))
				{
					GameInfo->gamemode = GAME_MULTIPLAYER_ONLY;
					Q_strncpy( GameInfo->type, "Multiplayer", sizeof( GameInfo->type ));
				}
				else
				{
					// pass type without changes
					if( !isGameInfo )
						GameInfo->gamemode = GAME_NORMAL;
					Q_strncpy( GameInfo->type, token, sizeof( GameInfo->type ));
				}
			}
		}
		// valid for both
		else if( !Q_stricmp( token, "version" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));
			GameInfo->version = Q_atof( token );
		}
		// valid for both
		else if( !Q_stricmp( token, "size" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));
			GameInfo->size = Q_atoi( token );
		}
		else if( !Q_stricmp( token, isGameInfo ? "mp_entity" : "mpentity" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->mp_entity, sizeof( GameInfo->mp_entity ));
		}
		else if( !Q_stricmp( token, isGameInfo ? "mp_filter" : "mpfilter" ))
		{
			pfile = COM_ParseFile( pfile, GameInfo->mp_filter, sizeof( GameInfo->mp_filter ));
		}
		// valid for both
		else if( !Q_stricmp( token, "secure" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));
			GameInfo->secure = Q_atoi( token ) ? true : false;
		}
		// valid for both
		else if( !Q_stricmp( token, "nomodels" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));
			GameInfo->nomodels = Q_atoi( token ) ? true : false;
		}
		else if( !Q_stricmp( token, isGameInfo ? "max_edicts" : "edicts" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));
			GameInfo->max_edicts = bound( MIN_EDICTS, Q_atoi( token ), MAX_EDICTS );
		}
		// valid for both
		else if( !Q_stricmp( token, "hd_background" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));
			GameInfo->hd_background = Q_atoi( token ) ? true : false;
		}
		else if( !Q_stricmp( token, "animated_title" ))
		{
			pfile = COM_ParseFile( pfile, token, sizeof( token ));
			GameInfo->animated_title = Q_atoi( token ) ? true : false;
		}
		// only for gameinfo
		else if( isGameInfo )
		{
			if( !Q_stricmp( token, "basedir" ))
			{
				string fs_path;
				pfile = COM_ParseFile( pfile, fs_path, sizeof( fs_path ));
				if( Q_stricmp( fs_path, GameInfo->basedir ) || Q_stricmp( fs_path, GameInfo->gamefolder ))
					Q_strncpy( GameInfo->basedir, fs_path, sizeof( GameInfo->basedir ));
			}
			else if( !Q_stricmp( token, "sp_entity" ))
			{
				pfile = COM_ParseFile( pfile, GameInfo->sp_entity, sizeof( GameInfo->sp_entity ));
			}
			else if( isGameInfo && !Q_stricmp( token, "dllpath" ))
			{
				pfile = COM_ParseFile( pfile, GameInfo->dll_path, sizeof( GameInfo->dll_path ));
			}
			else if( isGameInfo && !Q_stricmp( token, "date" ))
			{
				pfile = COM_ParseFile( pfile, GameInfo->date, sizeof( GameInfo->date ));
			}
			else if( !Q_stricmp( token, "max_tempents" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->max_tents = bound( 300, Q_atoi( token ), 2048 );
			}
			else if( !Q_stricmp( token, "max_beams" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->max_beams = bound( 64, Q_atoi( token ), 512 );
			}
			else if( !Q_stricmp( token, "max_particles" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->max_particles = bound( 1024, Q_atoi( token ), 131072 );
			}
			else if( !Q_stricmp( token, "gamemode" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				// TODO: Remove this ugly hack too.
				// This was made because Half-Life has multiplayer,
				// but for some reason it's marked as singleplayer_only.
				// Old WON version is fine.
				if( !Q_stricmp( token, "singleplayer_only" ) && Q_stricmp( GameInfo->gamefolder, "valve") )
					GameInfo->gamemode = GAME_SINGLEPLAYER_ONLY;
				else if( !Q_stricmp( token, "multiplayer_only" ))
					GameInfo->gamemode = GAME_MULTIPLAYER_ONLY;
			}
			else if( !Q_strnicmp( token, "ambient", 7 ))
			{
				int	ambientNum = Q_atoi( token + 7 );

				if( ambientNum < 0 || ambientNum > ( NUM_AMBIENTS - 1 ))
					ambientNum = 0;
				pfile = COM_ParseFile( pfile, GameInfo->ambientsound[ambientNum],
					sizeof( GameInfo->ambientsound[ambientNum] ));
			}
			else if( !Q_stricmp( token, "noskills" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->noskills = Q_atoi( token ) ? true : false;
			}
			else if( !Q_stricmp( token, "render_picbutton_text" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->render_picbutton_text = Q_atoi( token ) ? true : false;
			}
			else if( !Q_stricmp( token, "internal_vgui_support" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->internal_vgui_support = Q_atoi( token ) ? true : false;
			}
			else if( !Q_stricmp( token, "quicksave_aged_count" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->quicksave_aged_count = bound( 2, Q_atoi( token ), 99 );
			}
			else if( !Q_stricmp( token, "autosave_aged_count" ))
			{
				pfile = COM_ParseFile( pfile, token, sizeof( token ));
				GameInfo->autosave_aged_count = bound( 2, Q_atoi( token ), 99 );
			}
		}
	}

	if( !found_linux || !found_osx )
	{
		// just replace extension from dll to so/dylib
		char gamedll[64];
		Q_strncpy( gamedll, GameInfo->game_dll, sizeof( gamedll ));
		COM_StripExtension( gamedll );

		if( !found_linux )
			Q_snprintf( GameInfo->game_dll_linux, sizeof( GameInfo->game_dll_linux ), "%s.so", gamedll );

		if( !found_osx )
			Q_snprintf( GameInfo->game_dll_osx, sizeof( GameInfo->game_dll_osx ), "%s.dylib", gamedll );
	}

	// make sure what gamedir is really exist
	Q_snprintf( token, sizeof( token ), "%s/%s", fs_rootdir, GameInfo->falldir );
	if( !FS_SysFolderExists( token ))
		GameInfo->falldir[0] = '\0';
}

/*
================
FS_CreateDefaultGameInfo
================
*/
void FS_CreateDefaultGameInfo( const char *filename )
{
	gameinfo_t	defGI;

	FS_InitGameInfo( &defGI, fs_basedir );

	// make simple gameinfo.txt
	FS_WriteGameInfo( filename, &defGI );
}

/*
================
FS_ParseLiblistGam
================
*/
static qboolean FS_ParseLiblistGam( const char *filename, const char *gamedir, gameinfo_t *GameInfo )
{
	char	*afile;

	if( !GameInfo ) return false;
	afile = (char *)FS_LoadDirectFile( filename, NULL );
	if( !afile ) return false;

	FS_InitGameInfo( GameInfo, gamedir );

	FS_ParseGenericGameInfo( GameInfo, afile, false );

	Mem_Free( afile );

	return true;
}

/*
================
FS_ConvertGameInfo
================
*/
static qboolean FS_ConvertGameInfo( const char *gamedir, const char *gameinfo_path, const char *liblist_path )
{
	gameinfo_t	GameInfo;

	memset( &GameInfo, 0, sizeof( GameInfo ));

	if( FS_ParseLiblistGam( liblist_path, gamedir, &GameInfo ))
	{
		Con_DPrintf( "Convert %s to %s\n", liblist_path, gameinfo_path );
		FS_WriteGameInfo( gameinfo_path, &GameInfo );

		return true;
	}

	return false;
}

/*
================
FS_ReadGameInfo
================
*/
static qboolean FS_ReadGameInfo( const char *filepath, const char *gamedir, gameinfo_t *GameInfo )
{
	char	*afile;

	afile = (char *)FS_LoadFile( filepath, NULL, false );
	if( !afile )
		return false;

	FS_InitGameInfo( GameInfo, gamedir );

	FS_ParseGenericGameInfo( GameInfo, afile, true );

	Mem_Free( afile );

	return true;
}

/*
================
FS_CheckForQuakeGameDir

Checks if game directory resembles Quake Engine game directory
(some of checks may as well work with Xash gamedirs, it's not a bug)
================
*/
static qboolean FS_CheckForQuakeGameDir( const char *gamedir, qboolean direct )
{
	// if directory contain config.cfg or progs.dat it's 100% gamedir
	// quake mods probably always archived but can missed config.cfg before first running
	const char *files[] = { "config.cfg", "progs.dat", "pak0.pak" };
	int i;

	for( i = 0; i < sizeof( files ) / sizeof( files[0] ); i++ )
	{
		char	buf[MAX_VA_STRING];

		Q_snprintf( buf, sizeof( buf ), "%s/%s", gamedir, files[i] );
		if( direct ? FS_SysFileExists( buf ) : FS_FileExists( buf, false ))
			return true;
	}

	return false;
}

/*
===============
FS_CheckForXashGameDir

Checks if game directory resembles Xash3D game directory
===============
*/
static qboolean FS_CheckForXashGameDir( const char *gamedir, qboolean direct )
{
	// if directory contain gameinfo.txt or liblist.gam it's 100% gamedir
	const char *files[] = { "gameinfo.txt", "liblist.gam" };
	int i;

	for( i = 0; i < sizeof( files ) / sizeof( files[0] ); i++ )
	{
		char	buf[MAX_SYSPATH];

		Q_snprintf( buf, sizeof( buf ), "%s/%s", gamedir, files[i] );
		if( direct ? FS_SysFileExists( buf ) : FS_FileExists( buf, false ))
			return true;
	}

	return false;

}

/*
================
FS_ParseGameInfo
================
*/
static qboolean FS_ParseGameInfo( const char *gamedir, gameinfo_t *GameInfo )
{
	string		liblist_path, gameinfo_path;
	string		default_gameinfo_path;
	qboolean	haveUpdate = false;

	Q_snprintf( default_gameinfo_path, sizeof( default_gameinfo_path ), "%s/gameinfo.txt", fs_basedir );
	Q_snprintf( gameinfo_path, sizeof( gameinfo_path ), "%s/gameinfo.txt", gamedir );
	Q_snprintf( liblist_path, sizeof( liblist_path ), "%s/liblist.gam", gamedir );

	// here goes some RoDir magic...
	if( COM_CheckStringEmpty( fs_rodir ))
	{
		string	gameinfo_ro, liblist_ro;
		fs_offset_t roLibListTime, roGameInfoTime, rwGameInfoTime;

		FS_AllowDirectPaths( true );

		Q_snprintf( gameinfo_ro, sizeof( gameinfo_ro ), "%s/%s/gameinfo.txt", fs_rodir, gamedir );
		Q_snprintf( liblist_ro, sizeof( liblist_ro ), "%s/%s/liblist.gam", fs_rodir, gamedir );

		roLibListTime = FS_SysFileTime( liblist_ro );
		roGameInfoTime = FS_SysFileTime( gameinfo_ro );
		rwGameInfoTime = FS_SysFileTime( gameinfo_path );

		// if rodir's liblist.gam newer than rwdir's gameinfo.txt, then convert it
		if( roLibListTime > rwGameInfoTime )
		{
			haveUpdate = FS_ConvertGameInfo( gamedir, gameinfo_path, liblist_ro );
		}
		// if rodir's gameinfo.txt newer than rwdir's gameinfo.txt, just copy the file
		else if( roGameInfoTime > rwGameInfoTime )
		{
			file_t *ro, *rw;
			fs_offset_t ro_size;

			// read & write as binary to copy the exact file
			ro = FS_SysOpen( gameinfo_ro, "rb" );
			rw = FS_SysOpen( gameinfo_path, "wb" );

			FS_Seek( ro, 0, SEEK_END );
			ro_size = FS_Tell( ro );
			FS_Seek( ro, 0, SEEK_SET );

			FS_FileCopy( rw, ro, ro_size );

			FS_Close( rw );
			FS_Close( ro );

			haveUpdate = true;
		}

		FS_AllowDirectPaths( false );
	}

	// do not update gameinfo.txt, if it was just copied from rodir's
	// if user change liblist.gam update the gameinfo.txt
	if( !haveUpdate && FS_FileTime( liblist_path, false ) > FS_FileTime( gameinfo_path, false ))
		FS_ConvertGameInfo( gamedir, gameinfo_path, liblist_path );

	// force to create gameinfo for specified game if missing
	if(( FS_CheckForQuakeGameDir( gamedir, false ) || !Q_stricmp( fs_gamedir, gamedir )) && !FS_FileExists( gameinfo_path, false ))
	{
		gameinfo_t tmpGameInfo;
		memset( &tmpGameInfo, 0, sizeof( tmpGameInfo ));

		if( FS_ReadGameInfo( default_gameinfo_path, gamedir, &tmpGameInfo ))
		{
			// now we have copy of game info from basedir but needs to change gamedir
			Con_DPrintf( "Convert %s to %s\n", default_gameinfo_path, gameinfo_path );
			Q_strncpy( tmpGameInfo.gamefolder, gamedir, sizeof( tmpGameInfo.gamefolder ));
			FS_WriteGameInfo( gameinfo_path, &tmpGameInfo );
		}
		else FS_CreateDefaultGameInfo( gameinfo_path );
	}

	if( !GameInfo || !FS_FileExists( gameinfo_path, false ))
		return false; // no dest

	return FS_ReadGameInfo( gameinfo_path, gamedir, GameInfo );
}

/*
================
FS_AddGameHierarchy
================
*/
void FS_AddGameHierarchy( const char *dir, uint flags )
{
	int i;
	qboolean isGameDir = flags & FS_GAMEDIR_PATH;
	char buf[MAX_VA_STRING];

	GI->added = true;

	if( !COM_CheckString( dir ))
		return;

	// add the common game directory

	// recursive gamedirs
	// for example, czeror->czero->cstrike->valve
	for( i = 0; i < FI.numgames; i++ )
	{
		if( !Q_stricmp( FI.games[i]->gamefolder, dir ))
		{
			dir = FI.games[i]->gamefolder; // fixup directory case

			Con_Reportf( "FS_AddGameHierarchy: adding recursive basedir %s\n", FI.games[i]->basedir );
			if( !FI.games[i]->added && Q_stricmp( FI.games[i]->gamefolder, FI.games[i]->basedir ))
			{
				FI.games[i]->added = true;
				FS_AddGameHierarchy( FI.games[i]->basedir, flags & (~FS_GAMEDIR_PATH) );
			}
			break;
		}
	}

	if( COM_CheckStringEmpty( fs_rodir ) )
	{
		// append new flags to rodir, except FS_GAMEDIR_PATH and FS_CUSTOM_PATH
		uint newFlags = FS_NOWRITE_PATH | (flags & (~FS_GAMEDIR_PATH|FS_CUSTOM_PATH));
		if( isGameDir )
			newFlags |= FS_GAMERODIR_PATH;

		FS_AllowDirectPaths( true );
		Q_snprintf( buf, sizeof( buf ), "%s/%s/", fs_rodir, dir );
		FS_AddGameDirectory( buf, newFlags );
		FS_AllowDirectPaths( false );
	}

	if( isGameDir )
	{
		Q_snprintf( buf, sizeof( buf ), "%s/downloaded/", dir );
		FS_AddGameDirectory( buf, FS_NOWRITE_PATH | FS_CUSTOM_PATH );
	}
	Q_snprintf( buf, sizeof( buf ), "%s/", dir );
	FS_AddGameDirectory( buf, flags );
	if( isGameDir )
	{
		Q_snprintf( buf, sizeof( buf ), "%s/custom/", dir );
		FS_AddGameDirectory( buf, FS_NOWRITE_PATH | FS_CUSTOM_PATH );
	}
}

/*
================
FS_Rescan
================
*/
void FS_Rescan( void )
{
	const char *str;
	const int extrasFlags = FS_NOWRITE_PATH | FS_CUSTOM_PATH;
	Con_Reportf( "FS_Rescan( %s )\n", GI->title );

	FS_ClearSearchPath();

	str = getenv( "XASH3D_EXTRAS_PAK1" );
	if( COM_CheckString( str ))
		FS_MountArchive_Fullpath( str, extrasFlags );

	str = getenv( "XASH3D_EXTRAS_PAK2" );
	if( COM_CheckString( str ))
		FS_MountArchive_Fullpath( str, extrasFlags );

	if( Q_stricmp( GI->basedir, GI->gamefolder ))
		FS_AddGameHierarchy( GI->basedir, 0 );
	if( Q_stricmp( GI->basedir, GI->falldir ) && Q_stricmp( GI->gamefolder, GI->falldir ))
		FS_AddGameHierarchy( GI->falldir, 0 );
	FS_AddGameHierarchy( GI->gamefolder, FS_GAMEDIR_PATH );
}

/*
================
FS_LoadGameInfo

can be passed null arg
================
*/
void FS_LoadGameInfo( const char *rootfolder )
{
	int	i;

	// lock uplevel of gamedir for read\write
	fs_ext_path = false;

	if( rootfolder ) Q_strncpy( fs_gamedir, rootfolder, sizeof( fs_gamedir ));
	Con_Reportf( "FS_LoadGameInfo( %s )\n", fs_gamedir );

	// clear any old pathes
	FS_ClearSearchPath();

	// validate gamedir
	for( i = 0; i < FI.numgames; i++ )
	{
		if( !Q_stricmp( FI.games[i]->gamefolder, fs_gamedir ))
			break;
	}

	if( i == FI.numgames )
		Sys_Error( "Couldn't find game directory '%s'\n", fs_gamedir );

	FI.GameInfo = FI.games[i];

	FS_Rescan(); // create new filesystem
}

/*
==================
FS_CheckForCrypt

return true if library is crypted
==================
*/
static qboolean FS_CheckForCrypt( const char *dllname )
{
	file_t	*f;
	int	key;

	// this encryption is specific to DLLs
	if( Q_stricmp( COM_FileExtension( dllname ), "dll" ))
		return false;

	f = FS_Open( dllname, "rb", false );
	if( !f ) return false;

	FS_Seek( f, 64, SEEK_SET );	// skip first 64 bytes
	FS_Read( f, &key, sizeof( key ));
	FS_Close( f );

	return ( key == 0x12345678 ) ? true : false;
}

static int FS_StripIdiotRelativePath( const char *dllname, const char *gamefolder )
{
	string idiot_relpath;
	int len;

	if(( len = Q_snprintf( idiot_relpath, sizeof( idiot_relpath ), "../%s/", gamefolder )) >= 4 )
	{
		if( !Q_strnicmp( dllname, idiot_relpath, len ))
			return len;

		// try backslashes
		idiot_relpath[1] = '\\';
		idiot_relpath[len - 1] = '\\';
		if( !Q_strnicmp( dllname, idiot_relpath, len ))
			return len;
	}

	return 0;
}

/*
==================
FS_FindLibrary

search for library, assume index is valid
==================
*/
static qboolean FS_FindLibrary( const char *dllname, qboolean directpath, fs_dllinfo_t *dllInfo )
{
	string fixedname;
	searchpath_t	*search;
	int index, start = 0, i, len;

	// check for bad exports
	if( !COM_CheckString( dllname ))
		return false;

	fs_ext_path = directpath;

	// HACKHACK remove relative path to game folder
	if( !Q_strnicmp( dllname, "..", 2 ))
	{
		// some modders put relative path to themselves???
		len = FS_StripIdiotRelativePath( dllname, GI->gamefolder );

		if( len == 0 ) // or put relative path to Half-Life game libs
			len = FS_StripIdiotRelativePath( dllname, "valve" );
		start += len;
	}

	// replace all backward slashes
	len = Q_strlen( dllname );
	for( i = 0; i < len; i++ )
	{
		if( dllname[i+start] == '\\' ) dllInfo->shortPath[i] = '/';
		else dllInfo->shortPath[i] = Q_tolower( dllname[i+start] );
	}
	dllInfo->shortPath[i] = '\0';

	COM_DefaultExtension( dllInfo->shortPath, "."OS_LIB_EXT, sizeof( dllInfo->shortPath ));	// apply ext if forget

	search = FS_FindFile( dllInfo->shortPath, &index, fixedname, sizeof( fixedname ), false );

	if( search )
	{
		Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath ));
	}
	else if( !directpath )
	{
		fs_ext_path = false;

		// trying check also 'bin' folder for indirect paths
		search = FS_FindFile( dllname, &index, fixedname, sizeof( fixedname ), false );
		if( !search )
			return false; // unable to find

		Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath ));
	}

	dllInfo->encrypted = FS_CheckForCrypt( dllInfo->shortPath );

	if( index >= 0 && !dllInfo->encrypted && search )
	{
		Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ),
			"%s%s", search->filename, dllInfo->shortPath );
		dllInfo->custom_loader = false;	// we can loading from disk and use normal debugging
	}
	else
	{
		// NOTE: if search is NULL let the OS found library himself
		Q_strncpy( dllInfo->fullPath, dllInfo->shortPath, sizeof( dllInfo->fullPath ));

		if( search && search->type != SEARCHPATH_PLAIN )
		{
#if XASH_WIN32 && XASH_X86 // a1ba: custom loader is non-portable (I just don't want to touch it)
			Con_Printf( S_WARN "%s: loading libraries from packs is deprecated "
				"and will be removed in the future\n", __FUNCTION__ );
			dllInfo->custom_loader = true;
#else
			Con_Printf( S_WARN "%s: loading libraries from packs is unsupported on "
				"this platform\n", __FUNCTION__ );
			dllInfo->custom_loader = false;
#endif
		}
		else
		{
			dllInfo->custom_loader = false;
		}
	}
	fs_ext_path = false; // always reset direct paths

	return true;
}

static poolhandle_t _Mem_AllocPool( const char *name, const char *filename, int fileline )
{
	return (poolhandle_t)0xDEADC0DE;
}

static void _Mem_FreePool( poolhandle_t *poolptr, const char *filename, int fileline )
{
	// stub
}

static void *_Mem_Alloc( poolhandle_t poolptr, size_t size, qboolean clear, const char *filename, int fileline )
{
	void *ptr = malloc( size );
	if( clear ) memset( ptr, 0, size );
	return ptr;
}

static void *_Mem_Realloc( poolhandle_t poolptr, void *memptr, size_t size, qboolean clear, const char *filename, int fileline )
{
	return realloc( memptr, size );
}

static void _Mem_Free( void *data, const char *filename, int fileline )
{
	free( data );
}

static void _Con_Printf( const char *fmt, ... )
{
	va_list ap;

	va_start( ap, fmt );
	vprintf( fmt, ap );
	va_end( ap );
}

static void _Sys_Error( const char *fmt, ... )
{
	va_list ap;

	va_start( ap, fmt );
	vfprintf( stderr, fmt, ap );
	va_end( ap );

	exit( 1 );
}

static void *Sys_GetNativeObject_stub( const char *object )
{
	return NULL;
}

/*
================
FS_Init
================
*/
qboolean FS_InitStdio( qboolean unused_set_to_true, const char *rootdir, const char *basedir, const char *gamedir, const char *rodir )
{
	stringlist_t	dirs;
	qboolean		hasBaseDir = false;
	qboolean		hasGameDir = false;
	int		i;
	char		buf[MAX_VA_STRING];

	FS_InitMemory();

#ifdef XASH_ANDROID
	FS_InitAndroid();
#endif

	Q_strncpy( fs_rootdir, rootdir, sizeof( fs_rootdir ));
	Q_strncpy( fs_gamedir, gamedir, sizeof( fs_gamedir ));
	Q_strncpy( fs_basedir, basedir, sizeof( fs_basedir ));
	Q_strncpy( fs_rodir, rodir, sizeof( fs_rodir ));

	// add readonly directories first
	if( COM_CheckStringEmpty( fs_rodir ))
	{
		if( !Q_stricmp( fs_rodir, fs_rootdir ))
		{
			Sys_Error( "RoDir and default rootdir can't point to same directory!" );
			return false;
		}

		stringlistinit( &dirs );
		listdirectory( &dirs, fs_rodir );
		stringlistsort( &dirs );

		for( i = 0; i < dirs.numstrings; i++ )
		{
			char roPath[MAX_SYSPATH];

			Q_snprintf( roPath, sizeof( roPath ), "%s/%s/", fs_rodir, dirs.strings[i] );

			// check if it's a directory
			if( !FS_SysFolderExists( roPath ))
				continue;

			// check if it's gamedir
			if( FS_CheckForXashGameDir( roPath, true ) || FS_CheckForQuakeGameDir( roPath, true ))
			{
				char rwPath[MAX_SYSPATH];

				Q_snprintf( rwPath, sizeof( rwPath ), "%s/%s/", fs_rootdir, dirs.strings[i] );
				FS_CreatePath( rwPath );
			}
		}

		stringlistfreecontents( &dirs );
	}

	// validate directories
	stringlistinit( &dirs );
	listdirectory( &dirs, "./" );
	stringlistsort( &dirs );

	for( i = 0; i < dirs.numstrings; i++ )
	{
		if( !Q_stricmp( fs_basedir, dirs.strings[i] ))
			hasBaseDir = true;

		if( !Q_stricmp( fs_gamedir, dirs.strings[i] ))
			hasGameDir = true;
	}

	if( !hasGameDir )
	{
		Con_Printf( S_ERROR "game directory \"%s\" not exist\n", fs_gamedir );
		if( hasBaseDir ) Q_strncpy( fs_gamedir, fs_basedir, sizeof( fs_gamedir ));
	}

	// build list of game directories here
	if( COM_CheckStringEmpty( fs_rodir ))
	{
		Q_snprintf( buf, sizeof( buf ), "%s/", fs_rodir );
		FS_AddGameDirectory( buf, FS_STATIC_PATH|FS_NOWRITE_PATH );
	}
	FS_AddGameDirectory( "./", FS_STATIC_PATH );

	for( i = 0; i < dirs.numstrings; i++ )
	{
		if( !FS_SysFolderExists( dirs.strings[i] ))
			continue;

		if( FI.games[FI.numgames] == NULL )
			FI.games[FI.numgames] = (gameinfo_t *)Mem_Calloc( fs_mempool, sizeof( gameinfo_t ));

		if( FS_ParseGameInfo( dirs.strings[i], FI.games[FI.numgames] ))
			FI.numgames++; // added
	}

	stringlistfreecontents( &dirs );

	Con_Reportf( "FS_Init: done\n" );

	return true;
}

void FS_AllowDirectPaths( qboolean enable )
{
	fs_ext_path = enable;
}

/*
================
FS_Shutdown
================
*/
void FS_ShutdownStdio( void )
{
	int i;
	// release gamedirs
	for( i = 0; i < FI.numgames; i++ )
	{
		if( FI.games[i] )
			Mem_Free( FI.games[i] );
	}

	FS_ClearSearchPath(); // release all wad files too
	Mem_FreePool( &fs_mempool );
}

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

debug info
============
*/
void FS_Path_f( void )
{
	searchpath_t	*s;

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

	for( s = fs_searchpaths; s; s = s->next )
	{
		string info;

		s->pfnPrintInfo( s, info, sizeof(info) );

		Con_Printf( "%s", info );

		if( s->flags & FS_GAMERODIR_PATH ) Con_Printf( " ^2rodir^7" );
		if( s->flags & FS_GAMEDIR_PATH ) Con_Printf( " ^2gamedir^7" );
		if( s->flags & FS_CUSTOM_PATH ) Con_Printf( " ^2custom^7" );
		if( s->flags & FS_NOWRITE_PATH ) Con_Printf( " ^2nowrite^7" );
		if( s->flags & FS_STATIC_PATH ) Con_Printf( " ^2static^7" );

		Con_Printf( "\n" );
	}
}

/*
====================
FS_SysFileTime

Internal function used to determine filetime
====================
*/
int FS_SysFileTime( const char *filename )
{
#if XASH_WIN32
	struct _stat buf;
	if( _wstat( FS_PathToWideChar( filename ), &buf ) < 0 )
#else
	struct stat buf;
	if( stat( filename, &buf ) < 0 )
#endif
		return -1;

	return buf.st_mtime;
}

/*
====================
FS_SysOpen

Internal function used to create a file_t and open the relevant non-packed file on disk
====================
*/
file_t *FS_SysOpen( const char *filepath, const char *mode )
{
	file_t	*file;
	int	mod, opt;
	uint	ind;

	// Parse the mode string
	switch( mode[0] )
	{
	case 'r':	// read
		mod = O_RDONLY;
		opt = 0;
		break;
	case 'w': // write
		mod = O_WRONLY;
		opt = O_CREAT | O_TRUNC;
		break;
	case 'a': // append
		mod = O_WRONLY;
		opt = O_CREAT | O_APPEND;
		break;
	case 'e': // edit
		mod = O_WRONLY;
		opt = O_CREAT;
		break;
	default:
		return NULL;
	}

	for( ind = 1; mode[ind] != '\0'; ind++ )
	{
		switch( mode[ind] )
		{
		case '+':
			mod = O_RDWR;
			break;
		case 'b':
			opt |= O_BINARY;
			break;
		default:
			break;
		}
	}

	file = (file_t *)Mem_Calloc( fs_mempool, sizeof( *file ));
	file->filetime = FS_SysFileTime( filepath );
	file->ungetc = EOF;

#if XASH_WIN32
	file->handle = _wopen( FS_PathToWideChar( filepath ), mod | opt, 0666 );
#else
	file->handle = open( filepath, mod|opt, 0666 );
#endif

#if !XASH_WIN32
	if( file->handle < 0 )
		FS_BackupFileName( file, filepath, mod|opt );
#endif

	if( file->handle < 0 )
	{
		if( errno != ENOENT )
			Con_Printf( S_ERROR "%s: can't open file %s: %s\n", __func__, filepath, strerror( errno ));

		Mem_Free( file );
		return NULL;
	}


	file->real_length = lseek( file->handle, 0, SEEK_END );

	// uncomment do disable write
	//if( opt & O_CREAT )
	//	return NULL;

	// For files opened in append mode, we start at the end of the file
	if( opt & O_APPEND )  file->position = file->real_length;
	else lseek( file->handle, 0, SEEK_SET );

	return file;
}
/*
static int FS_DuplicateHandle( const char *filename, int handle, fs_offset_t pos )
{
#ifdef HAVE_DUP
	return dup( handle );
#else
	int newhandle = open( filename, O_RDONLY|O_BINARY );
	lseek( newhandle, pos, SEEK_SET );
	return newhandle;
#endif
}
*/

file_t *FS_OpenHandle( const char *syspath, int handle, fs_offset_t offset, fs_offset_t len )
{
	file_t *file = (file_t *)Mem_Calloc( fs_mempool, sizeof( file_t ));
#ifndef XASH_REDUCE_FD
#ifdef HAVE_DUP
	file->handle = dup( handle );
#else
	file->handle = open( syspath, O_RDONLY|O_BINARY );
#endif

	if( lseek( file->handle, offset, SEEK_SET ) == -1 )
	{
		Mem_Free( file );
		return NULL;
	}

#else
	file->backup_position = offset;
	file->backup_path = copystring( syspath );
	file->backup_options = O_RDONLY|O_BINARY;
	file->handle = -1;
#endif

	file->real_length = len;
	file->offset = offset;
	file->position = 0;
	file->ungetc = EOF;

	return file;
}

#if !defined( S_ISREG )
#define S_ISREG( m ) ( FBitSet( m, S_IFMT ) == S_IFREG )
#endif

#if !defined( S_ISDIR )
#define S_ISDIR( m ) ( FBitSet( m, S_IFMT ) == S_IFDIR )
#endif

/*
==================
FS_SysFileExists

Look for a file in the filesystem only
==================
*/
qboolean FS_SysFileExists( const char *path )
{
#if XASH_WIN32
	struct _stat buf;
	if( _wstat( FS_PathToWideChar( path ), &buf ) < 0 )
#else
	struct stat buf;
	if( stat( path, &buf ) < 0 )
#endif
		return false;

	return S_ISREG( buf.st_mode );
}

/*
==================
FS_SysFolderExists

Look for a existing folder
==================
*/
qboolean FS_SysFolderExists( const char *path )
{
#if XASH_WIN32
	struct _stat buf;
	if( _wstat( FS_PathToWideChar( path ), &buf ) < 0 )
#else
	struct stat buf;
	if( stat( path, &buf ) < 0 )
#endif
		return false;

	return S_ISDIR( buf.st_mode );
}

/*
==============
FS_SysFileOrFolderExists

Check if filesystem entry exists at all, don't mind the type
==============
*/
qboolean FS_SysFileOrFolderExists( const char *path )
{
#if XASH_WIN32
	struct _stat buf;
	return _wstat( FS_PathToWideChar( path ), &buf ) >= 0;
#else
	struct stat buf;
	return stat( path, &buf ) >= 0;
#endif
}

/*
==================
FS_SetCurrentDirectory

Sets current directory, path should be in UTF-8 encoding
TODO: make this non-fatal
==================
*/
int FS_SetCurrentDirectory( const char *path )
{
#if XASH_WIN32
	if( !SetCurrentDirectoryW( FS_PathToWideChar( path )))
	{
		char buf[1024];
		FormatMessageA( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL, GetLastError(), MAKELANGID( LANG_ENGLISH, SUBLANG_DEFAULT ),
			buf, sizeof( buf ), NULL );

		Sys_Error( "Changing directory to %s failed: %s\n", path, buf );
		return false;
	}
#elif XASH_POSIX
	if( chdir( path ) < 0 )
	{
		Sys_Error( "Changing directory to %s failed: %s\n", path, strerror( errno ));
		return false;
	}
#else
	// it may be fine for some systems to skip chdir
	Con_Printf( "FS_SetCurrentDirectory: not implemented, ignoring...\n" );
	return true;
#endif

	Con_Printf( "%s is working directory now\n", path );
	return true;
}

/*
====================
FS_FindFile

Look for a file in the packages and in the filesystem

Return the searchpath where the file was found (or NULL)
and the file index in the package if relevant
====================
*/
searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, qboolean gamedironly )
{
	searchpath_t	*search;

	// search through the path, one element at a time
	for( search = fs_searchpaths; search; search = search->next )
	{
		int pack_ind;

		if( gamedironly & !FBitSet( search->flags, FS_GAMEDIRONLY_SEARCH_FLAGS ))
			continue;

		pack_ind = search->pfnFindFile( search, name, fixedname, len );
		if( pack_ind >= 0 )
		{
			if( index )
				*index = pack_ind;
			return search;
		}
	}

	if( fs_ext_path )
	{
		char netpath[MAX_SYSPATH], dirpath[MAX_SYSPATH];

		Q_snprintf( dirpath, sizeof( dirpath ), "%s/", fs_rootdir );
		Q_snprintf( netpath, sizeof( netpath ), "%s%s", dirpath, name );

		if( FS_SysFileExists( netpath ))
		{
			static searchpath_t fs_directpath;

			// clear old dir cache, if needed
			if( 0 != Q_strcmp( fs_directpath.filename, dirpath ))
			{
				if( fs_directpath.pfnClose )
					fs_directpath.pfnClose( &fs_directpath );
				FS_InitDirectorySearchpath( &fs_directpath, dirpath, 0 );
			}

			// just copy the name, we don't do case sensitivity fix there
			if( fixedname )
				Q_strncpy( fixedname, name, len );

			if( index != NULL )
				*index = 0;

			return &fs_directpath;
		}
	}

	if( index != NULL )
		*index = -1;

	return NULL;
}

/*
===========================
FS_FullPathToRelativePath

Converts full path to the relative path considering current searchpaths
(do not use this function, implemented only for VFileSystem009)
===========================
*/
qboolean FS_FullPathToRelativePath( char *dst, const char *src, size_t size )
{
	searchpath_t *sp;

	for( sp = fs_searchpaths; sp; sp = sp->next )
	{
		size_t splen = Q_strlen( sp->filename );

		if( !Q_strnicmp( sp->filename, src, splen ))
		{
			Q_strncpy( dst, src + splen + 1, size );
			return true;
		}
	}

	Q_strncpy( dst, src, size );
	return false;
}


/*
===========
FS_OpenReadFile

Look for a file in the search paths and open it in read-only mode
===========
*/
file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedironly )
{
	searchpath_t *search;
	char netpath[MAX_SYSPATH];
	int pack_ind;

	search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly );

	// not found?
	if( search == NULL )
		return NULL;

	return search->pfnOpenFile( search, netpath, mode, pack_ind );
}

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

MAIN PUBLIC FUNCTIONS

=============================================================================
*/
/*
====================
FS_Open

Open a file. The syntax is the same as fopen
====================
*/
file_t *FS_Open( const char *filepath, const char *mode, qboolean gamedironly )
{
	if( !fs_searchpaths )
		return NULL;

	// some mappers used leading '/' or '\' in path to models or sounds
	if( filepath[0] == '/' || filepath[0] == '\\' )
		filepath++;

	if( filepath[0] == '/' || filepath[0] == '\\' )
		filepath++;

	if( FS_CheckNastyPath( filepath ))
		return NULL;

	// if the file is opened in "write", "append", or "read/write" mode
	if( mode[0] == 'w' || mode[0] == 'a'|| mode[0] == 'e' || Q_strchr( mode, '+' ))
	{
		char	real_path[MAX_SYSPATH];

		// open the file on disk directly
		if( !FS_FixFileCase( fs_writepath->dir, filepath, real_path, sizeof( real_path ), true ))
			return NULL;

		FS_CreatePath( real_path ); // Create directories up to the file

		return FS_SysOpen( real_path, mode );
	}

	// else, we look at the various search paths and open the file in read-only mode
	return FS_OpenReadFile( filepath, mode, gamedironly );
}

/*
====================
FS_Close

Close a file
====================
*/
int FS_Close( file_t *file )
{
	if( !file ) return 0;

	FS_BackupFileName( file, NULL, 0 );

	if( file->handle >= 0 )
		if( close( file->handle ))
			return EOF;

	Mem_Free( file );
	return 0;
}

/*
====================
FS_Flush

flushes written data to disk
====================
*/
int FS_Flush( file_t *file )
{
	if( !file ) return 0;

	// purge cached data
	FS_Purge( file );

	// sync
#if XASH_POSIX
	if( fsync( file->handle ) < 0 )
		return EOF;
#else
	if( _commit( file->handle ) < 0 )
		return EOF;
#endif

	return 0;
}

/*
====================
FS_Write

Write "datasize" bytes into a file
====================
*/
fs_offset_t FS_Write( file_t *file, const void *data, size_t datasize )
{
	fs_offset_t	result;

	if( !file ) return 0;

	// if necessary, seek to the exact file position we're supposed to be
	if( file->buff_ind != file->buff_len )
		lseek( file->handle, file->buff_ind - file->buff_len, SEEK_CUR );

	// purge cached data
	FS_Purge( file );

	// write the buffer and update the position
	result = write( file->handle, data, (fs_offset_t)datasize );
	file->position = lseek( file->handle, 0, SEEK_CUR );

	if( file->real_length < file->position )
		file->real_length = file->position;

	if( result < 0 )
		return 0;
	return result;
}

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

Read up to "buffersize" bytes from a file
====================
*/
fs_offset_t FS_Read( file_t *file, void *buffer, size_t buffersize )
{
	fs_offset_t	done;
	fs_offset_t	nb;
	size_t	count;

	// nothing to copy
	if( buffersize == 0 ) return 1;

	// Get rid of the ungetc character
	if( file->ungetc != EOF )
	{
		((char*)buffer)[0] = file->ungetc;
		buffersize--;
		file->ungetc = EOF;
		done = 1;
	}
	else done = 0;

	// first, we copy as many bytes as we can from "buff"
	if( file->buff_ind < file->buff_len )
	{
		count = file->buff_len - file->buff_ind;

		done += ( buffersize > count ) ? (fs_offset_t)count : (fs_offset_t)buffersize;
		memcpy( buffer, &file->buff[file->buff_ind], done );
		file->buff_ind += done;

		buffersize -= done;
		if( buffersize == 0 )
			return done;
	}

	// NOTE: at this point, the read buffer is always empty

	FS_EnsureOpenFile( file );
	// we must take care to not read after the end of the file
	count = file->real_length - file->position;

	// if we have a lot of data to get, put them directly into "buffer"
	if( buffersize > sizeof( file->buff ) / 2 )
	{
		if( count > buffersize )
			count = buffersize;
		lseek( file->handle, file->offset + file->position, SEEK_SET );
		nb = read( file->handle, (byte *)buffer + done, count );

		if( nb > 0 )
		{
			done += nb;
			file->position += nb;
			// purge cached data
			FS_Purge( file );
		}
	}
	else
	{
		if( count > sizeof( file->buff ))
			count = sizeof( file->buff );
		lseek( file->handle, file->offset + file->position, SEEK_SET );
		nb = read( file->handle, file->buff, count );

		if( nb > 0 )
		{
			file->buff_len = nb;
			file->position += nb;

			// copy the requested data in "buffer" (as much as we can)
			count = (fs_offset_t)buffersize > file->buff_len ? file->buff_len : (fs_offset_t)buffersize;
			memcpy( &((byte *)buffer)[done], file->buff, count );
			file->buff_ind = count;
			done += count;
		}
	}

	return done;
}

/*
====================
FS_Print

Print a string into a file
====================
*/
int FS_Print( file_t *file, const char *msg )
{
	return FS_Write( file, msg, Q_strlen( msg ));
}

/*
====================
FS_Printf

Print a string into a file
====================
*/
int FS_Printf( file_t *file, const char *format, ... )
{
	int	result;
	va_list	args;

	va_start( args, format );
	result = FS_VPrintf( file, format, args );
	va_end( args );

	return result;
}

/*
====================
FS_VPrintf

Print a string into a file
====================
*/
int FS_VPrintf( file_t *file, const char *format, va_list ap )
{
	int	len;
	fs_offset_t	buff_size = MAX_SYSPATH;
	char	*tempbuff;

	if( !file ) return 0;

	while( 1 )
	{
		tempbuff = (char *)Mem_Malloc( fs_mempool, buff_size );
		len = Q_vsnprintf( tempbuff, buff_size, format, ap );

		if( len >= 0 && len < buff_size )
			break;

		Mem_Free( tempbuff );
		buff_size *= 2;
	}

	len = write( file->handle, tempbuff, len );
	Mem_Free( tempbuff );

	return len;
}

/*
====================
FS_Getc

Get the next character of a file
====================
*/
int FS_Getc( file_t *file )
{
	char	c;

	if( FS_Read( file, &c, 1 ) != 1 )
		return EOF;

	return c;
}

/*
====================
FS_UnGetc

Put a character back into the read buffer (only supports one character!)
====================
*/
int FS_UnGetc( file_t *file, char c )
{
	// If there's already a character waiting to be read
	if( file->ungetc != EOF )
		return EOF;

	file->ungetc = c;
	return c;
}

/*
====================
FS_Gets

Same as fgets
====================
*/
int FS_Gets( file_t *file, char *string, size_t bufsize )
{
	int	c, end = 0;

	while( 1 )
	{
		c = FS_Getc( file );

		if( c == '\r' || c == '\n' || c < 0 )
			break;

		if( end < bufsize - 1 )
			string[end++] = c;
	}
	string[end] = 0;

	// remove \n following \r
	if( c == '\r' )
	{
		c = FS_Getc( file );

		if( c != '\n' )
			FS_UnGetc( file, c );
	}

	return c;
}

/*
====================
FS_Seek

Move the position index in a file
====================
*/
int FS_Seek( file_t *file, fs_offset_t offset, int whence )
{
	// compute the file offset
	switch( whence )
	{
	case SEEK_CUR:
		offset += file->position - file->buff_len + file->buff_ind;
		break;
	case SEEK_SET:
		break;
	case SEEK_END:
		offset += file->real_length;
		break;
	default:
		return -1;
	}

	if( offset < 0 || offset > file->real_length )
		return -1;

	// if we have the data in our read buffer, we don't need to actually seek
	if( file->position - file->buff_len <= offset && offset <= file->position )
	{
		file->buff_ind = offset + file->buff_len - file->position;
		return 0;
	}

	FS_EnsureOpenFile( file );
	// Purge cached data
	FS_Purge( file );

	if( lseek( file->handle, file->offset + offset, SEEK_SET ) == -1 )
		return -1;
	file->position = offset;

	return 0;
}

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

Give the current position in a file
====================
*/
fs_offset_t FS_Tell( file_t *file )
{
	if( !file ) return 0;
	return file->position - file->buff_len + file->buff_ind;
}

/*
====================
FS_Eof

indicates at reached end of file
====================
*/
qboolean FS_Eof( file_t *file )
{
	if( !file ) return true;
	return (( file->position - file->buff_len + file->buff_ind ) == file->real_length ) ? true : false;
}

/*
====================
FS_Purge

Erases any buffered input or output data
====================
*/
static void FS_Purge( file_t *file )
{
	file->buff_len = 0;
	file->buff_ind = 0;
	file->ungetc = EOF;
}

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

Filename are relative to the xash directory.
Always appends a 0 byte.
============
*/
byte *FS_LoadFile( const char *path, fs_offset_t *filesizeptr, qboolean gamedironly )
{
	searchpath_t *search;
	file_t *file;
	char netpath[MAX_SYSPATH];
	int pack_ind;

	// some mappers used leading '/' or '\' in path to models or sounds
	if( path[0] == '/' || path[0] == '\\' )
		path++;

	if( path[0] == '/' || path[0] == '\\' )
		path++;

	if( !fs_searchpaths || FS_CheckNastyPath( path ))
		return NULL;

	search = FS_FindFile( path, &pack_ind, netpath, sizeof( netpath ), gamedironly );

	if( !search )
		return NULL;

	// custom load file function for compressed files
	if( search->pfnLoadFile )
		return search->pfnLoadFile( search, netpath, pack_ind, filesizeptr );

	file = search->pfnOpenFile( search, netpath, "rb", pack_ind );

	if( file )
	{
		fs_offset_t	filesize = file->real_length;
		byte *buf;

		buf = (byte *)Mem_Malloc( fs_mempool, filesize + 1 );
		buf[filesize] = '\0';
		FS_Read( file, buf, filesize );
		FS_Close( file );

		if( filesizeptr )
			*filesizeptr = filesize;

		return buf;
	}

	return NULL;
}

qboolean CRC32_File( dword *crcvalue, const char *filename )
{
	char	buffer[1024];
	int	num_bytes;
	file_t	*f;

	f = FS_Open( filename, "rb", false );
	if( !f ) return false;

	CRC32_Init( crcvalue );

	while( 1 )
	{
		num_bytes = FS_Read( f, buffer, sizeof( buffer ));

		if( num_bytes > 0 )
			CRC32_ProcessBuffer( crcvalue, buffer, num_bytes );

		if( FS_Eof( f )) break;
	}

	FS_Close( f );
	return true;
}

qboolean MD5_HashFile( byte digest[16], const char *pszFileName, uint seed[4] )
{
	file_t		*file;
	byte		buffer[1024];
	MD5Context_t	MD5_Hash;
	int		bytes;

	if(( file = FS_Open( pszFileName, "rb", false )) == NULL )
		return false;

	memset( &MD5_Hash, 0, sizeof( MD5Context_t ));

	MD5Init( &MD5_Hash );

	if( seed )
	{
		MD5Update( &MD5_Hash, (const byte *)seed, 16 );
	}

	while( 1 )
	{
		bytes = FS_Read( file, buffer, sizeof( buffer ));

		if( bytes > 0 )
			MD5Update( &MD5_Hash, buffer, bytes );

		if( FS_Eof( file ))
			break;
	}

	FS_Close( file );
	MD5Final( digest, &MD5_Hash );

	return true;
}

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

Filename are relative to the xash directory.
Always appends a 0 byte.
============
*/
byte *FS_LoadDirectFile( const char *path, fs_offset_t *filesizeptr )
{
	file_t		*file;
	byte		*buf = NULL;
	fs_offset_t	filesize = 0;

	file = FS_SysOpen( path, "rb" );

	if( !file )
		return NULL;

	// Try to load
	filesize = file->real_length;
	buf = (byte *)Mem_Malloc( fs_mempool, filesize + 1 );
	buf[filesize] = '\0';
	FS_Read( file, buf, filesize );
	FS_Close( file );

	if( filesizeptr )
		*filesizeptr = filesize;

	return buf;
}


/*
============
FS_WriteFile

The filename will be prefixed by the current game directory
============
*/
qboolean FS_WriteFile( const char *filename, const void *data, fs_offset_t len )
{
	file_t *file;

	file = FS_Open( filename, "wb", false );

	if( !file )
	{
		Con_Reportf( S_ERROR "FS_WriteFile: failed on %s\n", filename);
		return false;
	}

	FS_Write( file, data, len );
	FS_Close( file );

	return true;
}

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

OTHERS PUBLIC FUNCTIONS

=============================================================================
*/
/*
==================
FS_FileExists

Look for a file in the packages and in the filesystem
==================
*/
int GAME_EXPORT FS_FileExists( const char *filename, int gamedironly )
{
	return FS_FindFile( filename, NULL, NULL, 0, gamedironly ) != NULL;
}

/*
==================
FS_GetDiskPath

Build direct path for file in the filesystem
return NULL for file in pack
==================
*/
const char *FS_GetDiskPath( const char *name, qboolean gamedironly )
{
	static char diskpath[MAX_SYSPATH];
	char fullpath[MAX_SYSPATH];
	searchpath_t	*search;

	search = FS_FindFile( name, NULL, fullpath, sizeof( fullpath ), gamedironly );

	if( search )
	{
		if( search->type != SEARCHPATH_PLAIN ) // file in pack or wad
			return NULL;

		Q_snprintf( diskpath, sizeof( diskpath ), "%s/%s", search->filename, fullpath );
		return diskpath;
	}

	return NULL;
}

/*
==================
FS_GetFullDiskPath

Build full path for file on disk
return false for file in pack
==================
*/
qboolean FS_GetFullDiskPath( char *buffer, size_t size, const char *name, qboolean gamedironly )
{
	searchpath_t *search;
	char temp[MAX_SYSPATH];

	search = FS_FindFile( name, NULL, temp, sizeof( temp ), gamedironly );

	if( search && search->type == SEARCHPATH_PLAIN )
	{
		Q_snprintf( buffer, size, "%s/%s", search->filename, temp );
		return true;
	}

	return false;
}

/*
==================
FS_FileSize

return size of file in bytes
==================
*/
fs_offset_t FS_FileSize( const char *filename, qboolean gamedironly )
{
	int	length = -1; // in case file was missed
	file_t	*fp;

	fp = FS_Open( filename, "rb", gamedironly );

	if( fp )
	{
		// it exists
		FS_Seek( fp, 0, SEEK_END );
		length = FS_Tell( fp );
		FS_Close( fp );
	}

	return length;
}

/*
==================
FS_FileLength

return size of file in bytes
==================
*/
fs_offset_t FS_FileLength( file_t *f )
{
	if( !f ) return 0;
	return f->real_length;
}

/*
==================
FS_FileTime

return time of creation file in seconds
==================
*/
int FS_FileTime( const char *filename, qboolean gamedironly )
{
	searchpath_t *search;
	char netpath[MAX_SYSPATH];
	int pack_ind;

	search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly );
	if( !search ) return -1; // doesn't exist

	return search->pfnFileTime( search, netpath );
}

/*
==================
FS_Rename

rename specified file from gamefolder
==================
*/
qboolean FS_Rename( const char *oldname, const char *newname )
{
	char oldname2[MAX_SYSPATH], newname2[MAX_SYSPATH], oldpath[MAX_SYSPATH], newpath[MAX_SYSPATH];
	int ret;

	if( !fs_writepath )
		return false;

	if( !COM_CheckString( oldname ) || !COM_CheckString( newname ))
		return false;

	// no work done
	if( !Q_stricmp( oldname, newname ))
		return true;

	// fix up slashes
	Q_strncpy( oldname2, oldname, sizeof( oldname2 ));
	Q_strncpy( newname2, newname, sizeof( newname2 ));

	COM_FixSlashes( oldname2 );
	COM_FixSlashes( newname2 );

	// file does not exist
	if( !FS_FixFileCase( fs_writepath->dir, oldname2, oldpath, sizeof( oldpath ), false ))
		return false;

	// exit if overflowed
	if( !FS_FixFileCase( fs_writepath->dir, newname2, newpath, sizeof( newpath ), true ))
		return false;

	ret = rename( oldpath, newpath );
	if( ret < 0 )
	{
		Con_Printf( "%s: failed to rename file %s (%s) to %s (%s): %s\n",
			__FUNCTION__, oldpath, oldname2, newpath, newname2, strerror( errno ));
		return false;
	}

	return true;
}

/*
==================
FS_Delete

delete specified file from gamefolder
==================
*/
qboolean GAME_EXPORT FS_Delete( const char *path )
{
	char path2[MAX_SYSPATH], real_path[MAX_SYSPATH];
	int ret;

	if( !fs_writepath || !COM_CheckString( path ))
		return false;

	Q_strncpy( path2, path, sizeof( path2 ));
	COM_FixSlashes( path2 );

	if( !FS_FixFileCase( fs_writepath->dir, path2, real_path, sizeof( real_path ), true ))
		return true;

	ret = remove( real_path );
	if( ret < 0 && errno != ENOENT )
	{
		Con_Printf( "%s: failed to delete file %s (%s): %s\n", __FUNCTION__, real_path, path, strerror( errno ));
		return false;
	}

	return true;
}

/*
==================
FS_FileCopy

==================
*/
qboolean FS_FileCopy( file_t *pOutput, file_t *pInput, int fileSize )
{
	char	*buf = Mem_Malloc( fs_mempool, FILE_COPY_SIZE );
	int	size, readSize;
	qboolean	done = true;

	while( fileSize > 0 )
	{
		if( fileSize > FILE_COPY_SIZE )
			size = FILE_COPY_SIZE;
		else size = fileSize;

		if(( readSize = FS_Read( pInput, buf, size )) < size )
		{
			Con_Reportf( S_ERROR "FS_FileCopy: unexpected end of input file (%d < %d)\n", readSize, size );
			fileSize = 0;
			done = false;
			break;
		}

		FS_Write( pOutput, buf, readSize );
		fileSize -= size;
	}

	Mem_Free( buf );
	return done;
}

/*
===========
FS_Search

Allocate and fill a search structure with information on matching filenames.
===========
*/
search_t *FS_Search( const char *pattern, int caseinsensitive, int gamedironly )
{
	search_t *search = NULL;
	searchpath_t *searchpath;
	int i, numfiles, numchars;
	stringlist_t resultlist;

	if( pattern[0] == '.' || pattern[0] == ':' || pattern[0] == '/' || pattern[0] == '\\' )
		return NULL; // punctuation issues

	stringlistinit( &resultlist );

	// search through the path, one element at a time
	for( searchpath = fs_searchpaths; searchpath; searchpath = searchpath->next )
	{
		if( gamedironly && !FBitSet( searchpath->flags, FS_GAMEDIRONLY_SEARCH_FLAGS ))
			continue;

		searchpath->pfnSearch( searchpath, &resultlist, pattern, caseinsensitive );
	}

	if( resultlist.numstrings )
	{
		stringlistsort( &resultlist );
		numfiles = resultlist.numstrings;
		numchars = 0;

		for( i = 0; i < resultlist.numstrings; i++ )
			numchars += (int)Q_strlen( resultlist.strings[i]) + 1;
		search = Mem_Calloc( fs_mempool, sizeof(search_t) + numchars + numfiles * sizeof( char* ));
		search->filenames = (char **)((char *)search + sizeof( search_t ));
		search->filenamesbuffer = (char *)((char *)search + sizeof( search_t ) + numfiles * sizeof( char* ));
		search->numfilenames = (int)numfiles;
		numfiles = numchars = 0;

		for( i = 0; i < resultlist.numstrings; i++ )
		{
			size_t	textlen;

			search->filenames[numfiles] = search->filenamesbuffer + numchars;
			textlen = Q_strlen(resultlist.strings[i]) + 1;
			memcpy( search->filenames[numfiles], resultlist.strings[i], textlen );
			numfiles++;
			numchars += (int)textlen;
		}
	}

	stringlistfreecontents( &resultlist );

	return search;
}

void FS_InitMemory( void )
{
	fs_mempool = Mem_AllocPool( "FileSystem Pool" );
	fs_searchpaths = NULL;
}

fs_interface_t g_engfuncs =
{
	_Con_Printf,
	_Con_Printf,
	_Con_Printf,
	_Sys_Error,
	_Mem_AllocPool,
	_Mem_FreePool,
	_Mem_Alloc,
	_Mem_Realloc,
	_Mem_Free,
	Sys_GetNativeObject_stub
};

static qboolean FS_InitInterface( int version, fs_interface_t *engfuncs )
{
	// to be extended in future interface revisions
	if( version != FS_API_VERSION )
	{
		Con_Printf( S_ERROR "filesystem optional interface version mismatch: expected %d, got %d\n",
			FS_API_VERSION, version );
		return false;
	}

	if( engfuncs->_Con_Printf )
		g_engfuncs._Con_Printf = engfuncs->_Con_Printf;

	if( engfuncs->_Con_DPrintf )
		g_engfuncs._Con_DPrintf = engfuncs->_Con_DPrintf;

	if( engfuncs->_Con_Reportf )
		g_engfuncs._Con_Reportf = engfuncs->_Con_Reportf;

	if( engfuncs->_Sys_Error )
		g_engfuncs._Sys_Error = engfuncs->_Sys_Error;

	if( engfuncs->_Mem_AllocPool && engfuncs->_Mem_FreePool )
	{
		g_engfuncs._Mem_AllocPool = engfuncs->_Mem_AllocPool;
		g_engfuncs._Mem_FreePool = engfuncs->_Mem_FreePool;

		Con_Reportf( "filesystem_stdio: custom pool allocation functions found\n" );
	}

	if( engfuncs->_Mem_Alloc && engfuncs->_Mem_Realloc && engfuncs->_Mem_Free )
	{
		g_engfuncs._Mem_Alloc = engfuncs->_Mem_Alloc;
		g_engfuncs._Mem_Realloc = engfuncs->_Mem_Realloc;
		g_engfuncs._Mem_Free = engfuncs->_Mem_Free;

		Con_Reportf( "filesystem_stdio: custom memory allocation functions found\n" );
	}

	if( engfuncs->_Sys_GetNativeObject )
	{
		g_engfuncs._Sys_GetNativeObject = engfuncs->_Sys_GetNativeObject;
		Con_Reportf( "filesystem_stdio: custom platform-specific functions found\n" );
	}

	return true;
}

fs_api_t g_api =
{
	FS_InitStdio,
	FS_ShutdownStdio,

	// search path utils
	FS_Rescan,
	FS_ClearSearchPath,
	FS_AllowDirectPaths,
	FS_AddGameDirectory,
	FS_AddGameHierarchy,
	FS_Search,
	FS_SetCurrentDirectory,
	FS_FindLibrary,
	FS_Path_f,

	// gameinfo utils
	FS_LoadGameInfo,

	// file ops
	FS_Open,
	FS_Write,
	FS_Read,
	FS_Seek,
	FS_Tell,
	FS_Eof,
	FS_Flush,
	FS_Close,
	FS_Gets,
	FS_UnGetc,
	FS_Getc,
	FS_VPrintf,
	FS_Printf,
	FS_Print,
	FS_FileLength,
	FS_FileCopy,

	// file buffer ops
	FS_LoadFile,
	FS_LoadDirectFile,
	FS_WriteFile,

	// file hashing
	CRC32_File,
	MD5_HashFile,

	// filesystem ops
	FS_FileExists,
	FS_FileTime,
	FS_FileSize,
	FS_Rename,
	FS_Delete,
	FS_SysFileExists,
	FS_GetDiskPath,

	NULL,
	(void *)FS_MountArchive_Fullpath,

	FS_GetFullDiskPath,
};

int EXPORT GetFSAPI( int version, fs_api_t *api, fs_globals_t **globals, fs_interface_t *engfuncs )
{
	if( engfuncs && !FS_InitInterface( version, engfuncs ))
		return 0;

	memcpy( api, &g_api, sizeof( *api ));
	*globals = &FI;

	return FS_API_VERSION;
}
