You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2805 lines
64 KiB
2805 lines
64 KiB
/* |
|
filesystem.c - game filesystem based on DP fs |
|
Copyright (C) 2007 Uncle Mike |
|
|
|
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/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> |
|
#if XASH_LINUX |
|
#include <sys/inotify.h> |
|
#endif |
|
#include "port.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; |
|
searchpath_t *fs_searchpaths = NULL; // chain |
|
char fs_rodir[MAX_SYSPATH]; |
|
char fs_rootdir[MAX_SYSPATH]; |
|
char fs_writedir[MAX_SYSPATH]; // path that game allows to overwrite, delete and rename files (and create new of course) |
|
|
|
static searchpath_t fs_directpath; // static direct path |
|
static char fs_basedir[MAX_SYSPATH]; // base game directory |
|
static char fs_gamedir[MAX_SYSPATH]; // game current directory |
|
#if !XASH_WIN32 |
|
static qboolean fs_caseinsensitive = true; // try to search missing files |
|
#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, 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++; |
|
} |
|
|
|
static 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; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// convert names to lowercase because windows 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 ); |
|
} |
|
} |
|
|
|
void listdirectory( stringlist_t *list, const char *path, qboolean lowercase ) |
|
{ |
|
int i; |
|
signed char *c; |
|
#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 |
|
|
|
============================================================================= |
|
*/ |
|
|
|
/* |
|
================== |
|
FS_FixFileCase |
|
|
|
emulate WIN32 FS behaviour when opening local file |
|
================== |
|
*/ |
|
const char *FS_FixFileCase( const char *path ) |
|
{ |
|
#if defined __DOS__ & !defined __WATCOM_LFN__ |
|
// not fix, but convert to 8.3 CAPS and rotate slashes |
|
// it is still recommended to package game data |
|
static char out[PATH_MAX]; |
|
int i = 0; |
|
int last = 0; |
|
while(*path) |
|
{ |
|
char c = *path++; |
|
|
|
if(c == '/') c = '\\'; |
|
else c = toupper(c); |
|
out[i++] = c; |
|
if(c == '\\' || c == '.') |
|
{ |
|
if( i - last > 8 ) |
|
{ |
|
char *l = &out[last]; |
|
l[6] = '~'; |
|
l[7] = '1'; |
|
l[8] = c; |
|
i = last + 9; |
|
} |
|
last = i; |
|
} |
|
} |
|
|
|
out[i] = 0; |
|
return out; |
|
#elif !XASH_WIN32 && !XASH_IOS // assume case insensitive |
|
DIR *dir; |
|
struct dirent *entry; |
|
char dirpath[PATH_MAX], *filename; |
|
static char fixedpath[PATH_MAX]; |
|
|
|
if( !fs_caseinsensitive ) |
|
return path; |
|
|
|
if( path[0] != '/' ) |
|
Q_snprintf( dirpath, sizeof( dirpath ), "./%s", path ); |
|
else Q_strncpy( dirpath, path, PATH_MAX ); |
|
|
|
filename = Q_strrchr( dirpath, '/' ); |
|
|
|
if( filename ) |
|
*filename++ = '\0'; |
|
else |
|
{ |
|
filename = (char*)path; |
|
Q_strcpy( dirpath, "."); |
|
} |
|
|
|
/* android has too slow directory scanning, |
|
so drop out some not useful cases */ |
|
if( filename - dirpath > 4 ) |
|
{ |
|
char *point; |
|
// too many wad textures |
|
if( !Q_stricmp( filename - 5, ".wad") ) |
|
return path; |
|
point = Q_strchr( filename, '.' ); |
|
if( point ) |
|
{ |
|
if( !Q_strcmp( point, ".mip") || !Q_strcmp( point, ".dds" ) || !Q_strcmp( point, ".ent" ) ) |
|
return path; |
|
if( filename[0] == '{' ) |
|
return path; |
|
} |
|
} |
|
|
|
//Con_Reportf( "FS_FixFileCase: %s\n", path ); |
|
|
|
if( !( dir = opendir( dirpath ) ) ) |
|
return path; |
|
|
|
while( ( entry = readdir( dir ) ) ) |
|
{ |
|
if( Q_stricmp( entry->d_name, filename ) ) |
|
continue; |
|
|
|
Q_snprintf( fixedpath, sizeof( fixedpath ), "%s/%s", dirpath, entry->d_name ); |
|
|
|
//Con_Reportf( "FS_FixFileCase: %s %s %s\n", dirpath, filename, entry->d_name ); |
|
|
|
path = fixedpath; |
|
|
|
break; |
|
} |
|
|
|
closedir( dir ); |
|
#endif |
|
return path; |
|
} |
|
|
|
#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; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
/* |
|
================ |
|
FS_AddArchive_Fullpath |
|
================ |
|
*/ |
|
static qboolean FS_AddArchive_Fullpath( const char *file, qboolean *already_loaded, int flags ) |
|
{ |
|
const char *ext = COM_FileExtension( file ); |
|
|
|
if( !Q_stricmp( ext, "pk3" ) ) |
|
return FS_AddZip_Fullpath( file, already_loaded, flags ); |
|
else if ( !Q_stricmp( ext, "pak" )) |
|
return FS_AddPak_Fullpath( file, already_loaded, flags ); |
|
|
|
// skip wads, this function only meant to be used for extras |
|
return false; |
|
} |
|
|
|
/* |
|
================ |
|
FS_AddGameDirectory |
|
|
|
Sets fs_writedir, 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 ) |
|
{ |
|
stringlist_t list; |
|
searchpath_t *search; |
|
string fullpath; |
|
int i; |
|
|
|
if( !FBitSet( flags, FS_NOWRITE_PATH )) |
|
Q_strncpy( fs_writedir, dir, sizeof( fs_writedir )); |
|
stringlistinit( &list ); |
|
listdirectory( &list, dir, false ); |
|
stringlistsort( &list ); |
|
|
|
// add any PAK package in the directory |
|
for( i = 0; i < list.numstrings; i++ ) |
|
{ |
|
if( !Q_stricmp( COM_FileExtension( list.strings[i] ), "pak" )) |
|
{ |
|
Q_sprintf( fullpath, "%s%s", dir, list.strings[i] ); |
|
FS_AddPak_Fullpath( fullpath, NULL, flags ); |
|
} |
|
} |
|
|
|
// add any Zip package in the directory |
|
for( i = 0; i < list.numstrings; i++ ) |
|
{ |
|
if( !Q_stricmp( COM_FileExtension( list.strings[i] ), "pk3" ) ) |
|
{ |
|
Q_sprintf( fullpath, "%s%s", dir, list.strings[i] ); |
|
FS_AddZip_Fullpath( fullpath, NULL, flags ); |
|
} |
|
} |
|
|
|
FS_AllowDirectPaths( true ); |
|
|
|
// add any WAD package in the directory |
|
for( i = 0; i < list.numstrings; i++ ) |
|
{ |
|
if( !Q_stricmp( COM_FileExtension( list.strings[i] ), "wad" )) |
|
{ |
|
Q_sprintf( fullpath, "%s%s", dir, list.strings[i] ); |
|
FS_AddWad_Fullpath( fullpath, NULL, flags ); |
|
} |
|
} |
|
|
|
stringlistfreecontents( &list ); |
|
FS_AllowDirectPaths( false ); |
|
|
|
// add the directory to the search path |
|
// (unpacked files have the priority over packed files) |
|
FS_AddDir_Fullpath( dir, NULL, flags ); |
|
} |
|
|
|
/* |
|
================ |
|
FS_ClearSearchPath |
|
================ |
|
*/ |
|
void FS_ClearSearchPath( void ) |
|
{ |
|
while( fs_searchpaths ) |
|
{ |
|
searchpath_t *search = fs_searchpaths; |
|
|
|
if( !search ) break; |
|
|
|
if( FBitSet( search->flags, FS_STATIC_PATH )) |
|
{ |
|
// skip read-only pathes |
|
if( search->next ) |
|
fs_searchpaths = search->next->next; |
|
else break; |
|
} |
|
else fs_searchpaths = search->next; |
|
|
|
search->pfnClose( search ); |
|
|
|
Mem_Free( search ); |
|
} |
|
} |
|
|
|
/* |
|
==================== |
|
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. |
|
==================== |
|
*/ |
|
int FS_CheckNastyPath (const char *path, qboolean isgamedir) |
|
{ |
|
// 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 |
|
|
|
// all: forbid trailing slash on gamedir |
|
if( isgamedir && path[Q_strlen(path)-1] == '/' ) return 2; |
|
|
|
// 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] ); |
|
} |
|
} |
|
|
|
FS_Print( f, "\n\n\n" ); |
|
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 = 900; // 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 )); |
|
|
|
// try to normalize filename only for liblist.gam |
|
// from hl_i?86.so to hl.so |
|
if( !isGameInfo ) |
|
{ |
|
char *p; |
|
COM_StripExtension( GameInfo->game_dll_linux ); |
|
|
|
p = Q_strrchr( GameInfo->game_dll_linux, '_' ); |
|
|
|
if( p && Q_stricmpext( "_i?86", p )) |
|
{ |
|
*p = 0; |
|
} |
|
|
|
COM_DefaultExtension( GameInfo->game_dll_linux, "."OS_LIB_EXT ); |
|
} |
|
|
|
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" ); |
|
} |
|
else if( !Q_stricmp( token, "type" )) |
|
{ |
|
pfile = COM_ParseFile( pfile, token, sizeof( token )); |
|
|
|
if( !isGameInfo && !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( !isGameInfo && !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, "edicts" )) |
|
{ |
|
pfile = COM_ParseFile( pfile, token, sizeof( token )); |
|
GameInfo->max_edicts = 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 ); |
|
} |
|
// valid for both |
|
else if( !Q_stricmp( token, "nomodels" )) |
|
{ |
|
pfile = COM_ParseFile( pfile, token, sizeof( token )); |
|
GameInfo->nomodels = Q_atoi( token ); |
|
} |
|
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 ); |
|
} |
|
// 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( 4096, Q_atoi( token ), 32768 ); |
|
} |
|
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 ); |
|
} |
|
else if( !Q_stricmp( token, "render_picbutton_text" )) |
|
{ |
|
pfile = COM_ParseFile( pfile, token, sizeof( token )); |
|
GameInfo->render_picbutton_text = Q_atoi( token ); |
|
} |
|
else if( !Q_stricmp( token, "internal_vgui_support" )) |
|
{ |
|
pfile = COM_ParseFile( pfile, token, sizeof( token )); |
|
GameInfo->internal_vgui_support = Q_atoi( token ); |
|
} |
|
} |
|
} |
|
|
|
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 |
|
if( !FS_SysFolderExists( va( "%s"PATH_SPLITTER"%s", fs_rootdir, GameInfo->falldir ))) |
|
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_CheckForGameDir |
|
================ |
|
*/ |
|
static qboolean FS_CheckForGameDir( const char *gamedir ) |
|
{ |
|
// if directory contain config.cfg it's 100% gamedir |
|
if( FS_FileExists( va( "%s/config.cfg", gamedir ), false )) |
|
return true; |
|
|
|
// if directory contain progs.dat it's 100% gamedir |
|
if( FS_FileExists( va( "%s/progs.dat", gamedir ), false )) |
|
return true; |
|
|
|
// quake mods probably always archived but can missed config.cfg before first running |
|
if( FS_FileExists( va( "%s/pak0.pak", gamedir ), false )) |
|
return true; |
|
|
|
// NOTE; adds here some additional checks if you wished |
|
|
|
return false; |
|
} |
|
|
|
/* |
|
================ |
|
FS_ParseGameInfo |
|
================ |
|
*/ |
|
static qboolean FS_ParseGameInfo( const char *gamedir, gameinfo_t *GameInfo ) |
|
{ |
|
string liblist_path, gameinfo_path; |
|
string default_gameinfo_path; |
|
gameinfo_t tmpGameInfo; |
|
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 filepath_ro, liblist_ro; |
|
fs_offset_t roLibListTime, roGameInfoTime, rwGameInfoTime; |
|
|
|
FS_AllowDirectPaths( true ); |
|
|
|
Q_snprintf( filepath_ro, sizeof( filepath_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( filepath_ro ); |
|
rwGameInfoTime = FS_SysFileTime( gameinfo_path ); |
|
|
|
if( roLibListTime > rwGameInfoTime ) |
|
{ |
|
haveUpdate = FS_ConvertGameInfo( gamedir, gameinfo_path, liblist_ro ); |
|
} |
|
else if( roGameInfoTime > rwGameInfoTime ) |
|
{ |
|
char *afile_ro = (char *)FS_LoadDirectFile( filepath_ro, NULL ); |
|
|
|
if( afile_ro ) |
|
{ |
|
gameinfo_t gi; |
|
|
|
haveUpdate = true; |
|
|
|
FS_InitGameInfo( &gi, gamedir ); |
|
FS_ParseGenericGameInfo( &gi, afile_ro, true ); |
|
FS_WriteGameInfo( gameinfo_path, &gi ); |
|
Mem_Free( afile_ro ); |
|
} |
|
} |
|
|
|
FS_AllowDirectPaths( false ); |
|
} |
|
|
|
// if user change liblist.gam update the gameinfo.txt |
|
if( 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_CheckForGameDir( gamedir ) || !Q_stricmp( fs_gamedir, gamedir )) && !FS_FileExists( gameinfo_path, false )) |
|
{ |
|
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 |
|
|
|
if( FS_ReadGameInfo( gameinfo_path, gamedir, GameInfo )) |
|
return true; |
|
return false; |
|
} |
|
|
|
/* |
|
================ |
|
FS_AddGameHierarchy |
|
================ |
|
*/ |
|
void FS_AddGameHierarchy( const char *dir, uint flags ) |
|
{ |
|
int i; |
|
qboolean isGameDir = flags & FS_GAMEDIR_PATH; |
|
|
|
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_strnicmp( FI.games[i]->gamefolder, dir, 64 )) |
|
{ |
|
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 ); |
|
FS_AddGameDirectory( va( "%s/%s/", fs_rodir, dir ), newFlags ); |
|
FS_AllowDirectPaths( false ); |
|
} |
|
|
|
if( isGameDir ) |
|
FS_AddGameDirectory( va( "%s/downloaded/", dir ), FS_NOWRITE_PATH | FS_CUSTOM_PATH ); |
|
FS_AddGameDirectory( va( "%s/", dir ), flags ); |
|
if( isGameDir ) |
|
FS_AddGameDirectory( va( "%s/custom/", dir ), 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(); |
|
|
|
#if XASH_IOS |
|
{ |
|
FS_AddPak_Fullpath( va( "%sextras.pak", SDL_GetBasePath() ), NULL, extrasFlags ); |
|
FS_AddPak_Fullpath( va( "%sextras_%s.pak", SDL_GetBasePath(), GI->gamefolder ), NULL, extrasFlags ); |
|
} |
|
#else |
|
str = getenv( "XASH3D_EXTRAS_PAK1" ); |
|
if( COM_CheckString( str )) |
|
FS_AddArchive_Fullpath( str, NULL, extrasFlags ); |
|
|
|
str = getenv( "XASH3D_EXTRAS_PAK2" ); |
|
if( COM_CheckString( str )) |
|
FS_AddArchive_Fullpath( str, NULL, extrasFlags ); |
|
#endif |
|
|
|
|
|
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_strcpy( fs_gamedir, rootfolder ); |
|
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; |
|
} |
|
|
|
/* |
|
================== |
|
FS_FindLibrary |
|
|
|
search for library, assume index is valid |
|
================== |
|
*/ |
|
static qboolean FS_FindLibrary( const char *dllname, qboolean directpath, fs_dllinfo_t *dllInfo ) |
|
{ |
|
searchpath_t *search; |
|
int index, start = 0, i, len; |
|
|
|
fs_ext_path = directpath; |
|
|
|
// check for bad exports |
|
if( !COM_CheckString( dllname )) |
|
return false; |
|
|
|
// HACKHACK remove absoulte path to valve folder |
|
if( !Q_strnicmp( dllname, "..\\valve\\", 9 ) || !Q_strnicmp( dllname, "../valve/", 9 )) |
|
start += 9; |
|
|
|
// 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 ); // apply ext if forget |
|
|
|
search = FS_FindFile( dllInfo->shortPath, &index, false ); |
|
|
|
if( !search && !directpath ) |
|
{ |
|
fs_ext_path = false; |
|
|
|
// trying check also 'bin' folder for indirect paths |
|
Q_strncpy( dllInfo->shortPath, dllname, sizeof( dllInfo->shortPath )); |
|
search = FS_FindFile( dllInfo->shortPath, &index, false ); |
|
if( !search ) return false; // unable to find |
|
} |
|
|
|
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; |
|
} |
|
|
|
poolhandle_t _Mem_AllocPool( const char *name, const char *filename, int fileline ) |
|
{ |
|
return (poolhandle_t)0xDEADC0DE; |
|
} |
|
|
|
void _Mem_FreePool( poolhandle_t *poolptr, const char *filename, int fileline ) |
|
{ |
|
// stub |
|
} |
|
|
|
void* _Mem_Alloc( poolhandle_t poolptr, size_t size, qboolean clear, const char *filename, int fileline ) |
|
{ |
|
if( clear ) return calloc( 1, size ); |
|
return malloc( size ); |
|
} |
|
|
|
void* _Mem_Realloc( poolhandle_t poolptr, void *memptr, size_t size, qboolean clear, const char *filename, int fileline ) |
|
{ |
|
return realloc( memptr, size ); |
|
} |
|
|
|
void _Mem_Free( void *data, const char *filename, int fileline ) |
|
{ |
|
free( data ); |
|
} |
|
|
|
void _Con_Printf( const char *fmt, ... ) |
|
{ |
|
va_list ap; |
|
|
|
va_start( ap, fmt ); |
|
vprintf( fmt, ap ); |
|
va_end( ap ); |
|
} |
|
|
|
void _Sys_Error( const char *fmt, ... ) |
|
{ |
|
va_list ap; |
|
|
|
va_start( ap, fmt ); |
|
vfprintf( stderr, fmt, ap ); |
|
va_end( ap ); |
|
|
|
exit( 1 ); |
|
} |
|
|
|
|
|
/* |
|
================ |
|
FS_Init |
|
================ |
|
*/ |
|
qboolean FS_InitStdio( qboolean caseinsensitive, const char *rootdir, const char *basedir, const char *gamedir, const char *rodir ) |
|
{ |
|
stringlist_t dirs; |
|
qboolean hasBaseDir = false; |
|
qboolean hasGameDir = false; |
|
int i; |
|
|
|
FS_InitMemory(); |
|
#if !XASH_WIN32 |
|
fs_caseinsensitive = caseinsensitive; |
|
#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, false ); |
|
stringlistsort( &dirs ); |
|
|
|
for( i = 0; i < dirs.numstrings; i++ ) |
|
{ |
|
char *roPath = va( "%s" PATH_SPLITTER "%s" PATH_SPLITTER, fs_rodir, dirs.strings[i] ); |
|
char *rwPath = va( "%s" PATH_SPLITTER "%s" PATH_SPLITTER, fs_rootdir, dirs.strings[i] ); |
|
|
|
// check if it's a directory |
|
if( !FS_SysFolderExists( roPath )) |
|
continue; |
|
|
|
// no need to check folders here, FS_CreatePath will not fail |
|
FS_CreatePath( rwPath ); |
|
} |
|
|
|
stringlistfreecontents( &dirs ); |
|
} |
|
|
|
// validate directories |
|
stringlistinit( &dirs ); |
|
listdirectory( &dirs, "./", false ); |
|
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 |
|
FS_AddGameDirectory( "./", FS_STATIC_PATH ); |
|
|
|
for( i = 0; i < dirs.numstrings; i++ ) |
|
{ |
|
if( !FS_SysFolderExists( dirs.strings[i] ) || ( !Q_strcmp( dirs.strings[i], ".." ) && !fs_ext_path )) |
|
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 ) |
|
{ |
|
struct stat buf; |
|
|
|
if( stat( filename, &buf ) == -1 ) |
|
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 ) |
|
{ |
|
const char *ffilepath = FS_FixFileCase( filepath ); |
|
if( ffilepath != filepath ) |
|
file->handle = open( ffilepath, mod|opt, 0666 ); |
|
if( file->handle >= 0 ) |
|
FS_BackupFileName( file, ffilepath, mod|opt ); |
|
} |
|
else |
|
FS_BackupFileName( file, filepath, mod|opt ); |
|
#endif |
|
|
|
if( file->handle < 0 ) |
|
{ |
|
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; |
|
} |
|
|
|
/* |
|
================== |
|
FS_SysFileExists |
|
|
|
Look for a file in the filesystem only |
|
================== |
|
*/ |
|
qboolean FS_SysFileExists( const char *path, qboolean caseinsensitive ) |
|
{ |
|
#if XASH_WIN32 |
|
int desc; |
|
|
|
if(( desc = open( path, O_RDONLY|O_BINARY )) < 0 ) |
|
return false; |
|
|
|
close( desc ); |
|
return true; |
|
#elif XASH_POSIX |
|
int ret; |
|
struct stat buf; |
|
|
|
ret = stat( path, &buf ); |
|
|
|
// speedup custom path search |
|
if( caseinsensitive && ( ret < 0 ) ) |
|
{ |
|
const char *fpath = FS_FixFileCase( path ); |
|
if( fpath != path ) |
|
ret = stat( fpath, &buf ); |
|
} |
|
|
|
if( ret < 0 ) |
|
return false; |
|
|
|
return S_ISREG( buf.st_mode ); |
|
#else |
|
#error |
|
#endif |
|
} |
|
|
|
/* |
|
================== |
|
FS_SetCurrentDirectory |
|
|
|
Sets current directory, path should be in UTF-8 encoding |
|
================== |
|
*/ |
|
int FS_SetCurrentDirectory( const char *path ) |
|
{ |
|
#if XASH_WIN32 |
|
return SetCurrentDirectoryW( FS_PathToWideChar(path) ); |
|
#elif XASH_POSIX |
|
return !chdir( path ); |
|
#else |
|
#error |
|
#endif |
|
} |
|
|
|
/* |
|
================== |
|
FS_SysFolderExists |
|
|
|
Look for a existing folder |
|
================== |
|
*/ |
|
qboolean FS_SysFolderExists( const char *path ) |
|
{ |
|
#if XASH_WIN32 |
|
DWORD dwFlags = GetFileAttributes( path ); |
|
|
|
return ( dwFlags != -1 ) && ( dwFlags & FILE_ATTRIBUTE_DIRECTORY ); |
|
#elif XASH_POSIX |
|
struct stat buf; |
|
|
|
if( stat( path, &buf ) < 0 ) |
|
return false; |
|
|
|
return S_ISDIR( buf.st_mode ); |
|
#else |
|
#error |
|
#endif |
|
} |
|
|
|
/* |
|
==================== |
|
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, 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 ); |
|
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" PATH_SPLITTER, fs_rootdir ); |
|
Q_snprintf( netpath, sizeof( netpath ), "%s%s", dirpath, name ); |
|
if( FS_SysFileExists( netpath, true )) |
|
{ |
|
FS_InitDirectorySearchpath( &fs_directpath, dirpath, 0 ); |
|
|
|
if( index != NULL ) |
|
*index = -1; |
|
return &fs_directpath; |
|
} |
|
} |
|
|
|
if( index != NULL ) |
|
*index = -1; |
|
|
|
return NULL; |
|
} |
|
|
|
|
|
/* |
|
=========== |
|
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; |
|
int pack_ind; |
|
|
|
search = FS_FindFile( filename, &pack_ind, gamedironly ); |
|
|
|
// not found? |
|
if( search == NULL ) |
|
return NULL; |
|
|
|
return search->pfnOpenFile( search, filename, 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 ) |
|
{ |
|
// some stupid 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, false )) |
|
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 |
|
Q_sprintf( real_path, "%s/%s", fs_writedir, filepath ); |
|
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 count, done; |
|
fs_offset_t nb; |
|
|
|
// 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 += ((fs_offset_t)buffersize > count ) ? 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 > (fs_offset_t)buffersize ) |
|
count = (fs_offset_t)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 > (fs_offset_t)sizeof( file->buff )) |
|
count = (fs_offset_t)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_vsprintf( tempbuff, 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, byte 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, byte *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, (byte)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 ) |
|
{ |
|
file_t *file; |
|
byte *buf = NULL; |
|
fs_offset_t filesize = 0; |
|
|
|
file = FS_Open( path, "rb", gamedironly ); |
|
|
|
if( file ) |
|
{ |
|
filesize = file->real_length; |
|
|
|
buf = (byte *)Mem_Malloc( fs_mempool, filesize + 1 ); |
|
buf[filesize] = '\0'; |
|
FS_Read( file, buf, filesize ); |
|
FS_Close( file ); |
|
} |
|
else |
|
{ |
|
buf = FS_LoadWADFile( path, &filesize, gamedironly ); |
|
|
|
if( !buf ) |
|
buf = FS_LoadZIPFile( path, &filesize, gamedironly ); |
|
|
|
} |
|
|
|
if( filesizeptr ) |
|
*filesizeptr = filesize; |
|
|
|
return buf; |
|
} |
|
|
|
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 ) |
|
{ |
|
if( FS_FindFile( filename, NULL, gamedironly )) |
|
return true; |
|
return false; |
|
} |
|
|
|
/* |
|
================== |
|
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 ) |
|
{ |
|
searchpath_t *search; |
|
|
|
search = FS_FindFile( name, NULL, gamedironly ); |
|
|
|
if( search ) |
|
{ |
|
if( search->type != SEARCHPATH_PLAIN ) // file in pack or wad |
|
return NULL; |
|
return va( "%s%s", search->filename, name ); |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
/* |
|
================== |
|
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; |
|
int pack_ind; |
|
|
|
search = FS_FindFile( filename, &pack_ind, gamedironly ); |
|
if( !search ) return -1; // doesn't exist |
|
|
|
return search->pfnFileTime( search, filename ); |
|
} |
|
|
|
/* |
|
================== |
|
FS_Rename |
|
|
|
rename specified file from gamefolder |
|
================== |
|
*/ |
|
qboolean FS_Rename( const char *oldname, const char *newname ) |
|
{ |
|
char oldpath[MAX_SYSPATH], newpath[MAX_SYSPATH]; |
|
qboolean iRet; |
|
|
|
if( !oldname || !newname || !*oldname || !*newname ) |
|
return false; |
|
|
|
Q_snprintf( oldpath, sizeof( oldpath ), "%s%s", fs_writedir, oldname ); |
|
Q_snprintf( newpath, sizeof( newpath ), "%s%s", fs_writedir, newname ); |
|
|
|
COM_FixSlashes( oldpath ); |
|
COM_FixSlashes( newpath ); |
|
|
|
iRet = rename( oldpath, newpath ); |
|
|
|
return (iRet == 0); |
|
} |
|
|
|
/* |
|
================== |
|
FS_Delete |
|
|
|
delete specified file from gamefolder |
|
================== |
|
*/ |
|
qboolean GAME_EXPORT FS_Delete( const char *path ) |
|
{ |
|
char real_path[MAX_SYSPATH]; |
|
qboolean iRet; |
|
|
|
if( !path || !*path ) |
|
return false; |
|
|
|
Q_snprintf( real_path, sizeof( real_path ), "%s%s", fs_writedir, path ); |
|
COM_FixSlashes( real_path ); |
|
iRet = remove( real_path ); |
|
|
|
return (iRet == 0); |
|
} |
|
|
|
/* |
|
================== |
|
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 |
|
}; |
|
|
|
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" ); |
|
} |
|
|
|
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, |
|
}; |
|
|
|
int EXPORT GetFSAPI( int version, fs_api_t *api, fs_globals_t **globals, fs_interface_t *engfuncs ) |
|
{ |
|
if( !FS_InitInterface( version, engfuncs )) |
|
return 0; |
|
|
|
memcpy( api, &g_api, sizeof( *api )); |
|
*globals = &FI; |
|
|
|
return FS_API_VERSION; |
|
}
|
|
|