Paranoia2/utils/common/basefs.cpp
2020-08-31 19:50:41 +03:00

1598 lines
37 KiB
C++

/*
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 <windows.h>
#include <io.h>
#include <fcntl.h>
#include "cmdlib.h"
#include "mathlib.h"
#include "stringlib.h"
#include "filesystem.h"
#include "wfile.h"
#include "bspfile.h"
#define FILE_COPY_SIZE (1024 * 1024)
#define FILE_BUFF_SIZE (65535)
#define MAX_SYSPATH 1024 // system filepath
/*
========================================================================
PAK FILES
The .pak files are just a linear collapse of a directory tree
========================================================================
*/
// header
#define IDPACKV1HEADER (('K'<<24)+('C'<<16)+('A'<<8)+'P') // little-endian "PACK"
#define MAX_FILES_IN_PACK 65536 // pak
typedef struct
{
int ident;
int dirofs;
int dirlen;
} dpackheader_t;
typedef struct
{
char name[56]; // total 64 bytes
int filepos;
int filelen;
} dpackfile_t;
// filesystem flags
#define FS_GAMEDIR_PATH 1 // just a marker for gamedir path
struct file_s
{
int handle; // file descriptor
long real_length; // uncompressed file size (for files opened in "read" mode)
long position; // current position in the file
long offset; // offset into the package (0 if external file)
int ungetc; // single stored character from ungetc, cleared to EOF when read contents buffer
time_t filetime; // pak, wad or real filetime
long buff_ind, buff_len; // buffer current index and length
byte buff[FILE_BUFF_SIZE]; // intermediate buffer
};
typedef struct pack_s
{
char filename[256];
int handle;
int numfiles;
time_t filetime; // common for all packed files
dpackfile_t *files;
} pack_t;
typedef struct searchpath_s
{
char filename[256];
pack_t *pack;
wfile_t *wad;
int flags;
struct searchpath_s *next;
} searchpath_t;
searchpath_t *fs_searchpaths = NULL; // chain
searchpath_t fs_directpath; // static direct path
char fs_rootdir[MAX_SYSPATH]; // engine root directory
char fs_basedir[MAX_SYSPATH]; // base directory of game
char fs_gamedir[MAX_SYSPATH]; // game current directory
char fs_falldir[MAX_SYSPATH]; // game falling directory
bool fs_ext_path = false; // attempt to read\write from ./ or ../ pathes
static searchpath_t *FS_FindFile( const char *name, int *index, bool gamedironly );
static dpackfile_t* FS_AddFileToPack( const char* name, pack_t *pack, long offset, long size );
static byte *W_LoadFile( const char *path, size_t *filesizeptr, bool gamedironly );
long FS_FileTime( const char *filename, bool gamedironly );
static void FS_Purge( file_t* file );
void FS_AllowDirectPaths( bool enable )
{
fs_ext_path = enable;
}
/*
=============================================================================
OTHER PRIVATE FUNCTIONS
=============================================================================
*/
/*
====================
FS_AddFileToPack
Add a file to the list of files contained into a package
====================
*/
static dpackfile_t *FS_AddFileToPack( const char *name, pack_t *pack, long offset, long size )
{
int left, right, middle;
dpackfile_t *pfile;
// look for the slot we should put that file into (binary search)
left = 0;
right = pack->numfiles - 1;
while( left <= right )
{
int diff;
middle = (left + right) / 2;
diff = Q_stricmp( pack->files[middle].name, name );
// If we found the file, there's a problem
if( !diff ) MsgDev( D_WARN, "Package %s contains the file %s several times\n", pack->filename, name );
// If we're too far in the list
if( diff > 0 ) right = middle - 1;
else left = middle + 1;
}
// We have to move the right of the list by one slot to free the one we need
pfile = &pack->files[left];
memmove( pfile + 1, pfile, (pack->numfiles - left) * sizeof( *pfile ));
pack->numfiles++;
Q_strncpy( pfile->name, name, sizeof( pfile->name ));
pfile->filepos = offset;
pfile->filelen = size;
return pfile;
}
/*
=================
FS_LoadPackPAK
Takes an explicit (not game tree related) path to a pak file.
Loads the header and directory, adding the files at the beginning
of the list so they override previous pack files.
=================
*/
pack_t *FS_LoadPackPAK( const char *packfile, int *error )
{
dpackheader_t header;
int packhandle;
int i, numpackfiles;
pack_t *pack;
dpackfile_t *info;
packhandle = open( packfile, O_RDONLY|O_BINARY );
if( packhandle < 0 )
{
MsgDev( D_NOTE, "%s couldn't open\n", packfile );
if( error ) *error = PAK_LOAD_COULDNT_OPEN;
return NULL;
}
read( packhandle, (void *)&header, sizeof( header ));
if( header.ident != IDPACKV1HEADER )
{
MsgDev( D_NOTE, "%s is not a packfile. Ignored.\n", packfile );
if( error ) *error = PAK_LOAD_BAD_HEADER;
close( packhandle );
return NULL;
}
if( header.dirlen % sizeof( dpackfile_t ))
{
MsgDev( D_ERROR, "%s has an invalid directory size. Ignored.\n", packfile );
if( error ) *error = PAK_LOAD_BAD_FOLDERS;
close( packhandle );
return NULL;
}
numpackfiles = header.dirlen / sizeof( dpackfile_t );
if( numpackfiles > MAX_FILES_IN_PACK )
{
MsgDev( D_ERROR, "%s has too many files ( %i ). Ignored.\n", packfile, numpackfiles );
if( error ) *error = PAK_LOAD_TOO_MANY_FILES;
close( packhandle );
return NULL;
}
if( numpackfiles <= 0 )
{
MsgDev( D_NOTE, "%s has no files. Ignored.\n", packfile );
if( error ) *error = PAK_LOAD_NO_FILES;
close( packhandle );
return NULL;
}
info = (dpackfile_t *)Mem_Alloc( sizeof( *info ) * numpackfiles, C_FILESYSTEM );
lseek( packhandle, header.dirofs, SEEK_SET );
if( header.dirlen != read( packhandle, (void *)info, header.dirlen ))
{
MsgDev( D_NOTE, "%s is an incomplete PAK, not loading\n", packfile );
if( error ) *error = PAK_LOAD_CORRUPTED;
close( packhandle );
Mem_Free( info, C_FILESYSTEM );
return NULL;
}
pack = (pack_t *)Mem_Alloc( sizeof( pack_t ), C_FILESYSTEM );
Q_strncpy( pack->filename, packfile, sizeof( pack->filename ));
pack->files = (dpackfile_t *)Mem_Alloc( numpackfiles * sizeof( dpackfile_t ), C_FILESYSTEM );
pack->filetime = COM_FileTime( packfile );
pack->handle = packhandle;
pack->numfiles = 0;
// parse the directory
for( i = 0; i < numpackfiles; i++ )
FS_AddFileToPack( info[i].name, pack, info[i].filepos, info[i].filelen );
if( error ) *error = PAK_LOAD_OK;
Mem_Free( info, C_FILESYSTEM );
return pack;
}
/*
====================
FS_AddWad_Fullpath
====================
*/
static bool FS_AddWad_Fullpath( const char *wadfile, bool *already_loaded, int flags )
{
searchpath_t *search;
wfile_t *wad = NULL;
const char *ext = COM_FileExtension( wadfile );
int errorcode = WAD_LOAD_COULDNT_OPEN;
for( search = fs_searchpaths; search; search = search->next )
{
if( search->wad && !Q_stricmp( search->wad->filename, wadfile ))
{
if( already_loaded ) *already_loaded = true;
return true; // already loaded
}
}
if( already_loaded ) *already_loaded = false;
if( !Q_stricmp( ext, "wad" )) wad = W_Open( wadfile, "rb", &errorcode, fs_ext_path );
else MsgDev( D_ERROR, "\"%s\" doesn't have a wad extension\n", wadfile );
if( wad )
{
search = (searchpath_t *)Mem_Alloc( sizeof( searchpath_t ), C_FILESYSTEM );
search->wad = wad;
search->next = fs_searchpaths;
search->flags |= flags;
fs_searchpaths = search;
MsgDev( D_REPORT, "Adding wadfile: %s (%i files)\n", wadfile, wad->numlumps );
return true;
}
else
{
if( errorcode != WAD_LOAD_NO_FILES )
MsgDev( D_ERROR, "FS_AddWad_Fullpath: unable to load wad \"%s\"\n", wadfile );
return false;
}
}
/*
================
FS_AddPak_Fullpath
Adds the given pack to the search path.
The pack type is autodetected by the file extension.
Returns true if the file was successfully added to the
search path or if it was already included.
If keep_plain_dirs is set, the pack will be added AFTER the first sequence of
plain directories.
================
*/
static bool FS_AddPak_Fullpath( const char *pakfile, bool *already_loaded, int flags )
{
searchpath_t *search;
pack_t *pak = NULL;
const char *ext = COM_FileExtension( pakfile );
int i, errorcode = PAK_LOAD_COULDNT_OPEN;
for( search = fs_searchpaths; search; search = search->next )
{
if( search->pack && !Q_stricmp( search->pack->filename, pakfile ))
{
if( already_loaded ) *already_loaded = true;
return true; // already loaded
}
}
if( already_loaded ) *already_loaded = false;
if( !Q_stricmp( ext, "pak" )) pak = FS_LoadPackPAK( pakfile, &errorcode );
else MsgDev( D_ERROR, "\"%s\" does not have a pack extension\n", pakfile );
if( pak )
{
string fullpath;
search = (searchpath_t *)Mem_Alloc( sizeof( searchpath_t ), C_FILESYSTEM );
search->pack = pak;
search->next = fs_searchpaths;
search->flags |= flags;
fs_searchpaths = search;
MsgDev( D_REPORT, "Adding pakfile: %s (%i files)\n", pakfile, pak->numfiles );
// time to add in search list all the wads that contains in current pakfile (if do)
for( i = 0; i < pak->numfiles; i++ )
{
if( !Q_stricmp( COM_FileExtension( pak->files[i].name ), "wad" ))
{
Q_sprintf( fullpath, "%s/%s", pakfile, pak->files[i].name );
FS_AddWad_Fullpath( fullpath, NULL, flags );
}
}
return true;
}
else
{
if( errorcode != PAK_LOAD_NO_FILES )
MsgDev( D_ERROR, "FS_AddPak_Fullpath: unable to load pak \"%s\"\n", pakfile );
return false;
}
}
/*
================
FS_AddGameDirectory
Sets fs_gamedir, adds the directory to the head of the path,
then loads and adds pak1.pak pak2.pak ...
================
*/
void FS_AddGameDirectory( const char *dir, int flags )
{
stringlist_t list;
searchpath_t *search;
char fullpath[256];
int i;
stringlistinit( &list );
listdirectory( &list, dir, true );
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 );
}
}
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)
search = (searchpath_t *)Mem_Alloc( sizeof( searchpath_t ), C_FILESYSTEM );
Q_strncpy( search->filename, dir, sizeof ( search->filename ));
search->next = fs_searchpaths;
search->flags = flags;
fs_searchpaths = search;
}
/*
================
FS_AddGameHierarchy
================
*/
void FS_AddGameHierarchy( const char *dir, int flags )
{
// Add the common game directory
if( dir && *dir ) FS_AddGameDirectory( va( "%s/%s/", fs_rootdir, dir ), flags );
}
/*
================
FS_ClearSearchPath
================
*/
void FS_ClearSearchPath( void )
{
while( fs_searchpaths )
{
searchpath_t *search = fs_searchpaths;
if( !search ) break;
fs_searchpaths = search->next;
if( search->pack )
{
if( search->pack->files )
Mem_Free( search->pack->files, C_FILESYSTEM );
Mem_Free( search->pack, C_FILESYSTEM );
}
if( search->wad )
W_Close( search->wad );
Mem_Free( search, C_FILESYSTEM );
}
fs_searchpaths = NULL;
}
/*
================
FS_ParseLiblistGam
================
*/
static bool FS_ParseLiblistGam( const char *filename, const char *gamedir )
{
char *afile, *pfile;
char token[256];
afile = (char *)FS_LoadFile( filename, NULL, false );
if( !afile ) return false;
Q_strncpy( fs_basedir, "valve", sizeof( fs_basedir ));
COM_FileBase( gamedir, fs_gamedir );
pfile = afile;
while(( pfile = COM_ParseFile( pfile, token )) != NULL )
{
if( !Q_stricmp( token, "gamedir" ))
{
pfile = COM_ParseFile( pfile, token );
if( Q_stricmp( token, fs_basedir ) || Q_stricmp( token, fs_gamedir ))
Q_strncpy( fs_gamedir, token, sizeof( fs_gamedir ));
}
if( !Q_stricmp( token, "fallback_dir" ))
{
pfile = COM_ParseFile( pfile, token );
if( Q_stricmp( token, fs_basedir ) || Q_stricmp( token, fs_falldir ))
Q_strncpy( fs_falldir, token, sizeof( fs_falldir ));
}
}
if( afile != NULL )
Mem_Free( afile, C_FILESYSTEM );
return true;
}
/*
================
FS_ParseGameInfo
================
*/
static bool FS_ParseGameInfo( const char *filename, const char *gamedir )
{
char *afile, *pfile;
char token[256];
afile = (char *)FS_LoadFile( filename, NULL, false );
if( !afile ) return false;
// setup default values
COM_FileBase( gamedir, fs_gamedir );
pfile = afile;
while(( pfile = COM_ParseFile( pfile, token )) != NULL )
{
if( !Q_stricmp( token, "basedir" ))
{
pfile = COM_ParseFile( pfile, token );
if( Q_stricmp( token, fs_basedir ) || Q_stricmp( token, fs_gamedir ))
Q_strncpy( fs_basedir, token, sizeof( fs_basedir ));
}
else if( !Q_stricmp( token, "fallback_dir" ))
{
pfile = COM_ParseFile( pfile, token );
if( Q_stricmp( token, fs_basedir ) || Q_stricmp( token, fs_falldir ))
Q_strncpy( fs_falldir, token, sizeof( fs_falldir ));
}
else if( !Q_stricmp( token, "gamedir" ))
{
pfile = COM_ParseFile( pfile, token );
if( Q_stricmp( token, fs_basedir ) || Q_stricmp( token, fs_gamedir ))
Q_strncpy( fs_gamedir, token, sizeof( fs_gamedir ));
}
}
if( afile != NULL )
Mem_Free( afile, C_FILESYSTEM );
return true;
}
void FS_ReadGameInfo( const char *gamedir )
{
char liblist[256], gameinfo[256];
Q_strncpy( gameinfo, "gameinfo.txt", sizeof( gameinfo ));
Q_strncpy( liblist, "liblist.gam", sizeof( liblist ));
// if user change liblist.gam update the gameinfo.txt
if( FS_FileTime( liblist, false ) > FS_FileTime( gameinfo, false ))
FS_ParseLiblistGam( liblist, gamedir );
else FS_ParseGameInfo( gameinfo, gamedir );
}
/*
================
FS_Rescan
================
*/
void FS_Rescan( void )
{
FS_ClearSearchPath();
if( Q_stricmp( fs_basedir, fs_gamedir ))
FS_AddGameHierarchy( fs_basedir, 0 );
if( Q_stricmp( fs_basedir, fs_falldir ) && Q_stricmp( fs_gamedir, fs_falldir ))
FS_AddGameHierarchy( fs_falldir, 0 );
FS_AddGameHierarchy( fs_gamedir, FS_GAMEDIR_PATH );
}
/*
================
FS_Init
================
*/
void FS_Init( const char *source )
{
char workdir[_MAX_PATH];
char mapdir[_MAX_PATH];
char hullfile[_MAX_PATH];
char mapfile[_MAX_PATH];
char *pathend;
fs_searchpaths = NULL;
// skip the unneeded separator
if( source[0] == '.' && ( source[1] == '/' || source[1] == '\\' ))
Q_snprintf( mapdir, sizeof( mapdir ), "%s.map", source + 2 );
else Q_snprintf( mapdir, sizeof( mapdir ), "%s.map", source );
// create full path to a map
Q_strncpy( workdir, COM_ExpandArg( mapdir ), sizeof( workdir ));
// search for 'maps' in path
pathend = Q_stristr( workdir, "maps" );
if( !pathend )
{
MsgDev( D_ERROR, "FS_Init: couldn't init game directory!\n" );
return;
}
// create gamedir path
Q_strncpy( fs_rootdir, workdir, pathend - workdir );
int length = Q_strlen( fs_rootdir );
if( fs_rootdir[length-1] == '.' && ( fs_rootdir[length-2] == '/' || fs_rootdir[length-2] == '\\' ))
fs_rootdir[length-2] = '\0';
MsgDev( D_REPORT, "workdir: %s\n", fs_rootdir );
Q_snprintf( hullfile, sizeof( hullfile ), "%s\\hulls.txt", fs_rootdir );
FS_AddGameDirectory( va( "%s\\", fs_rootdir ), 0 );
FS_ReadGameInfo( fs_rootdir );
MsgDev( D_REPORT, "gamedir: %s, basedir %s, falldir %s\n", fs_gamedir, fs_basedir, fs_falldir );
COM_ExtractFilePath( fs_rootdir, fs_rootdir );
COM_FileBase( source, mapfile );
if( !Q_strcmp( fs_rootdir, "" ))
Q_strncpy( fs_rootdir, "..\\", sizeof( fs_rootdir ));
MsgDev( D_INFO, "rootdir %s\n", fs_rootdir ); // for debug
MsgDev( D_INFO, "source: %s.map\n\n", mapfile ); // for debug
FS_Rescan(); // create new filesystem
MsgDev( D_REPORT, "FS_Init: done\n" );
searchpath_t *s;
MsgDev( D_REPORT, "Current search path:\n" );
for( s = fs_searchpaths; s; s = s->next )
{
if( s->pack ) MsgDev( D_REPORT, "%s (%i files)\n", s->pack->filename, s->pack->numfiles );
else if( s->wad ) MsgDev( D_REPORT, "%s (%i files)\n", s->wad->filename, s->wad->numlumps );
else MsgDev( D_REPORT, "%s\n", s->filename );
}
if( COM_FileExists( hullfile ))
CheckHullFile( hullfile );
}
/*
================
FS_Shutdown
================
*/
void FS_Shutdown( void )
{
FS_ClearSearchPath();
}
/*
====================
FS_SysOpen
Internal function used to create a file_t and open the relevant non-packed file on disk
====================
*/
static 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:
MsgDev( D_ERROR, "FS_SysOpen(%s, %s): invalid mode\n", filepath, mode );
return NULL;
}
for( ind = 1; mode[ind] != '\0'; ind++ )
{
switch( mode[ind] )
{
case '+':
mod = O_RDWR;
break;
case 'b':
opt |= O_BINARY;
break;
default:
MsgDev( D_ERROR, "FS_SysOpen: %s: unknown char in mode (%c)\n", filepath, mode, mode[ind] );
break;
}
}
file = (file_t *)Mem_Alloc( sizeof( *file ), C_FILESYSTEM );
file->filetime = COM_FileTime( filepath );
file->ungetc = EOF;
file->handle = open( filepath, mod|opt, 0666 );
if( file->handle < 0 )
{
Mem_Free( file, C_FILESYSTEM );
return NULL;
}
file->real_length = lseek( file->handle, 0, SEEK_END );
// For files opened in append mode, we start at the end of the file
if( mod & O_APPEND ) file->position = file->real_length;
else lseek( file->handle, 0, SEEK_SET );
return file;
}
/*
===========
FS_OpenPackedFile
Open a packed file using its package file descriptor
===========
*/
file_t *FS_OpenPackedFile( pack_t *pack, int pack_ind )
{
dpackfile_t *pfile;
int dup_handle;
file_t *file;
pfile = &pack->files[pack_ind];
if( lseek( pack->handle, pfile->filepos, SEEK_SET ) == -1 )
return NULL;
dup_handle = dup( pack->handle );
if( dup_handle < 0 )
return NULL;
file = (file_t *)Mem_Alloc( sizeof( *file ), C_FILESYSTEM );
file->handle = dup_handle;
file->real_length = pfile->filelen;
file->offset = pfile->filepos;
file->position = 0;
file->ungetc = EOF;
return file;
}
/*
====================
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
====================
*/
static searchpath_t *FS_FindFile( const char *name, int *index, bool gamedironly )
{
searchpath_t *search;
if( !COM_CheckString( name ))
return NULL;
// search through the path, one element at a time
for( search = fs_searchpaths; search; search = search->next )
{
if( gamedironly & !FBitSet( search->flags, FS_GAMEDIR_PATH ))
continue;
// is the element a pak file?
if( search->pack )
{
int left, right, middle;
pack_t *pak;
pak = search->pack;
// look for the file (binary search)
left = 0;
right = pak->numfiles - 1;
while( left <= right )
{
int diff;
middle = (left + right) / 2;
diff = Q_stricmp( pak->files[middle].name, name );
// Found it
if( !diff )
{
if( index ) *index = middle;
return search;
}
// if we're too far in the list
if( diff > 0 )
right = middle - 1;
else left = middle + 1;
}
}
else if( search->wad )
{
dlumpinfo_t *lump;
char type = W_TypeFromExt( name );
bool anywadname = true;
string wadname, wadfolder;
string shortname;
// quick reject by filetype
if( type == TYP_NONE ) continue;
COM_ExtractFilePath( name, wadname );
wadfolder[0] = '\0';
if( Q_strlen( wadname ))
{
COM_FileBase( wadname, wadname );
Q_strncpy( wadfolder, wadname, sizeof( wadfolder ));
COM_DefaultExtension( wadname, ".wad" );
anywadname = false;
}
// make wadname from wad fullpath
COM_FileBase( search->wad->filename, shortname );
COM_DefaultExtension( shortname, ".wad" );
// quick reject by wadname
if( !anywadname && Q_stricmp( wadname, shortname ))
continue;
// NOTE: we can't using long names for wad,
// because we using original wad names[16];
COM_FileBase( name, shortname );
lump = W_FindLump( search->wad, shortname, type );
if( lump )
{
if( index )
*index = lump - search->wad->lumps;
return search;
}
}
else
{
char netpath[MAX_SYSPATH];
Q_sprintf( netpath, "%s%s", search->filename, name );
if( COM_FileExists( netpath ))
{
if( index != NULL ) *index = -1;
return search;
}
}
}
if( fs_ext_path )
{
// clear searchpath
search = &fs_directpath;
memset( search, 0, sizeof( searchpath_t ));
if( COM_FileExists( name ))
{
if( index != NULL )
*index = -1;
return search;
}
}
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, bool gamedironly )
{
searchpath_t *search;
int pack_ind;
search = FS_FindFile( filename, &pack_ind, gamedironly );
// not found?
if( search == NULL )
return NULL;
if( search->pack )
return FS_OpenPackedFile( search->pack, pack_ind );
else if( search->wad )
return NULL; // let W_LoadFile get lump correctly
else if( pack_ind < 0 )
{
char path [MAX_SYSPATH];
// found in the filesystem?
Q_sprintf( path, "%s%s", search->filename, filename );
return FS_SysOpen( path, mode );
}
return NULL;
}
/*
=============================================================================
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, bool 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 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", filepath );
COM_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;
if( close( file->handle ))
return EOF;
Mem_Free( file, C_FILESYSTEM );
return 0;
}
/*
====================
FS_Read
Read up to "buffersize" bytes from a file
====================
*/
long FS_Read( file_t *file, void *buffer, size_t buffersize )
{
long count, done;
long 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 += ((long)buffersize > count ) ? count : (long)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
// 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 > (long)buffersize )
count = (long)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 > (long)sizeof( file->buff ))
count = (long)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 = (long)buffersize > file->buff_len ? file->buff_len : (long)buffersize;
memcpy( &((byte *)buffer)[done], file->buff, count );
file->buff_ind = count;
done += count;
}
}
return done;
}
/*
====================
FS_Seek
Move the position index in a file
====================
*/
int FS_Seek( file_t *file, long 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;
}
// 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
====================
*/
long FS_Tell( file_t *file )
{
if( !file ) return 0;
return file->position - file->buff_len + file->buff_ind;
}
/*
====================
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_Eof
indicates at reached end of file
====================
*/
bool 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
====================
*/
void FS_Purge( file_t *file )
{
file->buff_len = 0;
file->buff_ind = 0;
file->ungetc = EOF;
}
/*
====================
FS_Write
Write "datasize" bytes into a file
====================
*/
long FS_Write( file_t *file, const void *data, size_t datasize )
{
long 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, (long)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_LoadFile
Filename are relative to the xash directory.
Always appends a 0 byte.
============
*/
byte *FS_LoadFile( const char *path, size_t *filesizeptr, bool gamedironly )
{
file_t *file;
byte *buf = NULL;
size_t filesize = 0;
file = FS_Open( path, "rb", gamedironly );
if( file )
{
filesize = file->real_length;
buf = (byte *)Mem_Alloc( filesize + 1, C_FILESYSTEM );
buf[filesize] = '\0';
FS_Read( file, buf, filesize );
FS_Close( file );
}
else
{
buf = W_LoadFile( path, &filesize, gamedironly );
}
if( filesizeptr )
*filesizeptr = filesize;
return buf;
}
/*
==================
FS_FileLength
return size of file in bytes
==================
*/
long FS_FileLength( file_t *f )
{
if( !f ) return 0;
return f->real_length;
}
/*
=============================================================================
OTHERS PUBLIC FUNCTIONS
=============================================================================
*/
/*
==================
FS_FileExists
Look for a file in the packages and in the filesystem
==================
*/
bool FS_FileExists( const char *filename, bool gamedironly )
{
if( FS_FindFile( filename, NULL, gamedironly ))
return true;
return false;
}
/*
==================
FS_FileTime
return time of creation file in seconds
==================
*/
long FS_FileTime( const char *filename, bool gamedironly )
{
searchpath_t *search;
int pack_ind;
search = FS_FindFile( filename, &pack_ind, gamedironly );
if( !search ) return -1; // doesn't exist
if( search->pack ) // grab pack filetime
return search->pack->filetime;
else if( search->wad ) // grab wad filetime
return search->wad->filetime;
else if( pack_ind < 0 )
{
char path [MAX_SYSPATH];
// found in the filesystem?
Q_sprintf( path, "%s%s", search->filename, filename );
return COM_FileTime( path );
}
return -1; // doesn't exist
}
/*
===========
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;
pack_t *pak;
wfile_t *wad;
int i, basepathlength, numfiles, numchars;
int resultlistindex, dirlistindex;
const char *slash, *backslash, *colon, *separator;
string netpath, temp;
stringlist_t resultlist;
stringlist_t dirlist;
char *basepath;
for( i = 0; pattern[i] == '.' || pattern[i] == ':' || pattern[i] == '/' || pattern[i] == '\\'; i++ );
if( i > 0 )
{
MsgDev( D_INFO, "FS_Search: don't use punctuation at the beginning of a search pattern!\n");
return NULL;
}
stringlistinit( &resultlist );
stringlistinit( &dirlist );
slash = Q_strrchr( pattern, '/' );
backslash = Q_strrchr( pattern, '\\' );
colon = Q_strrchr( pattern, ':' );
separator = max( slash, backslash );
separator = max( separator, colon );
basepathlength = separator ? (separator + 1 - pattern) : 0;
basepath = (char *)Mem_Alloc( basepathlength + 1, C_FILESYSTEM );
if( basepathlength ) memcpy( basepath, pattern, basepathlength );
basepath[basepathlength] = 0;
// search through the path, one element at a time
for( searchpath = fs_searchpaths; searchpath; searchpath = searchpath->next )
{
if( gamedironly && !FBitSet( searchpath->flags, FS_GAMEDIR_PATH ))
continue;
// is the element a pak file?
if( searchpath->pack )
{
// look through all the pak file elements
pak = searchpath->pack;
for( i = 0; i < pak->numfiles; i++ )
{
Q_strncpy( temp, pak->files[i].name, sizeof( temp ));
while( temp[0] )
{
if( matchpattern( temp, (char *)pattern, true ))
{
for( resultlistindex = 0; resultlistindex < resultlist.numstrings; resultlistindex++ )
{
if( !Q_strcmp( resultlist.strings[resultlistindex], temp ))
break;
}
if( resultlistindex == resultlist.numstrings )
stringlistappend( &resultlist, temp );
}
// strip off one path element at a time until empty
// this way directories are added to the listing if they match the pattern
slash = Q_strrchr( temp, '/' );
backslash = Q_strrchr( temp, '\\' );
colon = Q_strrchr( temp, ':' );
separator = temp;
if( separator < slash )
separator = slash;
if( separator < backslash )
separator = backslash;
if( separator < colon )
separator = colon;
*((char *)separator) = 0;
}
}
}
else if( searchpath->wad )
{
string wadpattern, wadname, temp2;
char type = W_TypeFromExt( pattern );
qboolean anywadname = true;
string wadfolder;
// quick reject by filetype
if( type == TYP_NONE ) continue;
COM_ExtractFilePath( pattern, wadname );
COM_FileBase( pattern, wadpattern );
wadfolder[0] = '\0';
if( Q_strlen( wadname ))
{
COM_FileBase( wadname, wadname );
Q_strncpy( wadfolder, wadname, sizeof( wadfolder ));
COM_DefaultExtension( wadname, ".wad" );
anywadname = false;
}
// make wadname from wad fullpath
COM_FileBase( searchpath->wad->filename, temp2 );
COM_DefaultExtension( temp2, ".wad" );
// quick reject by wadname
if( !anywadname && Q_stricmp( wadname, temp2 ))
continue;
// look through all the wad file elements
wad = searchpath->wad;
for( i = 0; i < wad->numlumps; i++ )
{
// if type not matching, we already have no chance ...
if( type != TYP_ANY && wad->lumps[i].type != type )
continue;
// build the lumpname with image suffix (if present)
Q_snprintf( temp, sizeof( temp ), "%s%s", wad->lumps[i].name, wad_hints[wad->lumps[i].img_type].ext );
while( temp[0] )
{
if( matchpattern( temp, wadpattern, true ))
{
for( resultlistindex = 0; resultlistindex < resultlist.numstrings; resultlistindex++ )
{
if( !Q_strcmp( resultlist.strings[resultlistindex], temp ))
break;
}
if( resultlistindex == resultlist.numstrings )
{
// build path: wadname/lumpname.ext
Q_snprintf( temp2, sizeof(temp2), "%s/%s", wadfolder, temp );
COM_DefaultExtension( temp2, va(".%s", W_ExtFromType( wad->lumps[i].type )));
stringlistappend( &resultlist, temp2 );
}
}
// strip off one path element at a time until empty
// this way directories are added to the listing if they match the pattern
slash = Q_strrchr( temp, '/' );
backslash = Q_strrchr( temp, '\\' );
colon = Q_strrchr( temp, ':' );
separator = temp;
if( separator < slash )
separator = slash;
if( separator < backslash )
separator = backslash;
if( separator < colon )
separator = colon;
*((char *)separator) = 0;
}
}
}
else
{
// get a directory listing and look at each name
Q_sprintf( netpath, "%s%s", searchpath->filename, basepath );
stringlistinit( &dirlist );
listdirectory( &dirlist, netpath );
for( dirlistindex = 0; dirlistindex < dirlist.numstrings; dirlistindex++ )
{
Q_sprintf( temp, "%s%s", basepath, dirlist.strings[dirlistindex] );
if( matchpattern( temp, (char *)pattern, true ))
{
for( resultlistindex = 0; resultlistindex < resultlist.numstrings; resultlistindex++ )
{
if( !Q_strcmp( resultlist.strings[resultlistindex], temp ))
break;
}
if( resultlistindex == resultlist.numstrings )
stringlistappend( &resultlist, temp );
}
}
stringlistfreecontents( &dirlist );
}
}
if( resultlist.numstrings )
{
stringlistsort( &resultlist );
numfiles = resultlist.numstrings;
numchars = 0;
for( resultlistindex = 0; resultlistindex < resultlist.numstrings; resultlistindex++ )
numchars += (int)Q_strlen( resultlist.strings[resultlistindex]) + 1;
search = (search_t *)Mem_Alloc( sizeof( search_t ) + numchars + numfiles * sizeof( char* ), C_FILESYSTEM );
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( resultlistindex = 0; resultlistindex < resultlist.numstrings; resultlistindex++ )
{
size_t textlen;
search->filenames[numfiles] = search->filenamesbuffer + numchars;
textlen = Q_strlen(resultlist.strings[resultlistindex]) + 1;
memcpy( search->filenames[numfiles], resultlist.strings[resultlistindex], textlen );
numfiles++;
numchars += (int)textlen;
}
}
stringlistfreecontents( &resultlist );
Mem_Free( basepath, C_FILESYSTEM );
return search;
}
/*
=============================================================================
FILESYSTEM IMPLEMENTATION
=============================================================================
*/
/*
===========
W_LoadFile
loading lump into the tmp buffer
===========
*/
static byte *W_LoadFile( const char *path, size_t *lumpsizeptr, bool gamedironly )
{
searchpath_t *search;
int index;
search = FS_FindFile( path, &index, gamedironly );
if( search && search->wad )
return W_ReadLump( search->wad, &search->wad->lumps[index], lumpsizeptr );
return NULL;
}