mirror of
https://bitbucket.org/CPMADevs/cnq3
synced 2024-11-27 14:22:32 +00:00
3189 lines
81 KiB
C++
3189 lines
81 KiB
C++
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena source code is free software; you can redistribute it
|
|
and/or modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the License,
|
|
or (at your option) any later version.
|
|
|
|
Quake III Arena source code is distributed in the hope that it will be
|
|
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
|
|
#include "q_shared.h"
|
|
#include "qcommon.h"
|
|
#include "unzip.h"
|
|
|
|
/*
|
|
=============================================================================
|
|
|
|
QUAKE3 FILESYSTEM
|
|
|
|
All of Quake's data access is through a hierarchical file system, but the contents of
|
|
the file system can be transparently merged from several sources.
|
|
|
|
A "qpath" is a reference to game file data. MAX_ZPATH is 256 characters, which must include
|
|
a terminating zero. "..", "\\", and ":" are explicitly illegal in qpaths to prevent any
|
|
references outside the quake directory system.
|
|
|
|
The "base path" is the path to the directory holding all the game directories and usually
|
|
the executable. It defaults to ".", but can be overridden with a "+set fs_basepath c:\quake3"
|
|
command line to allow code debugging in a different directory. Basepath cannot
|
|
be modified at all after startup. Any files that are created (demos, screenshots,
|
|
etc) will be created reletive to the base path, so base path should usually be writable.
|
|
|
|
If a user runs the game directly from a CD, the base path would be on the CD. This
|
|
should still function correctly, but all file writes will fail (harmlessly).
|
|
|
|
The "home path" is the path used for all write access. On win32 systems we have "base path"
|
|
== "home path", but on *nix systems the base installation is usually readonly, and
|
|
"home path" points to ~/.q3a or similar
|
|
|
|
The user can also install custom mods and content in "home path", so it should be searched
|
|
along with "home path" and "cd path" for game content.
|
|
|
|
|
|
The "base game" is the directory under the paths where data comes from by default, and
|
|
can be either "baseq3" or "demoq3".
|
|
|
|
The "current game" may be the same as the base game, or it may be the name of another
|
|
directory under the paths that should be searched for files before looking in the base game.
|
|
This is the basis for addons.
|
|
|
|
Clients automatically set the game directory after receiving a gamestate from a server,
|
|
so only servers need to worry about +set fs_game.
|
|
|
|
No other directories outside of the base game and current game will ever be referenced by
|
|
filesystem functions.
|
|
|
|
To save disk space and speed loading, directory trees can be collapsed into zip files.
|
|
The files use a ".pk3" extension to prevent users from unzipping them accidentally, but
|
|
otherwise the are simply normal uncompressed zip files. A game directory can have multiple
|
|
zip files of the form "pak0.pk3", "pak1.pk3", etc. Zip files are searched in decending order
|
|
from the highest number to the lowest, and will always take precedence over the filesystem.
|
|
This allows a pk3 distributed as a patch to override all existing data.
|
|
|
|
Because we will have updated executables freely available online, there is no point to
|
|
trying to restrict demo / oem versions of the game with code changes. Demo / oem versions
|
|
should be exactly the same executables as release versions, but with different data that
|
|
automatically restricts where game media can come from to prevent add-ons from working.
|
|
|
|
If not running in restricted mode, and a file is not found in any local filesystem,
|
|
an attempt will be made to download it and save it under the base path.
|
|
|
|
File search order: when FS_FOpenFileRead gets called it will go through the fs_searchpaths
|
|
structure and stop on the first successful hit. fs_searchpaths is built with successive
|
|
calls to FS_AddGameDirectory
|
|
|
|
Additionaly, we search in several subdirectories:
|
|
current game is the current mode
|
|
base game is a variable to allow mods based on other mods
|
|
(such as baseq3 + missionpack content combination in a mod for instance)
|
|
BASEGAME is the hardcoded base game ("baseq3")
|
|
|
|
e.g. the qpath "sound/newstuff/test.wav" would be searched for in the following places:
|
|
|
|
home path + current game's zip files
|
|
home path + current game's directory
|
|
base path + current game's zip files
|
|
base path + current game's directory
|
|
cd path + current game's zip files
|
|
cd path + current game's directory
|
|
|
|
home path + base game's zip file
|
|
home path + base game's directory
|
|
base path + base game's zip file
|
|
base path + base game's directory
|
|
cd path + base game's zip file
|
|
cd path + base game's directory
|
|
|
|
home path + BASEGAME's zip file
|
|
home path + BASEGAME's directory
|
|
base path + BASEGAME's zip file
|
|
base path + BASEGAME's directory
|
|
cd path + BASEGAME's zip file
|
|
cd path + BASEGAME's directory
|
|
|
|
server download, to be written to home path + current game's directory
|
|
|
|
|
|
The filesystem can be safely shutdown and reinitialized with different
|
|
basedir / cddir / game combinations, but all other subsystems that rely on it
|
|
(sound, video) must also be forced to restart.
|
|
|
|
Because the same files are loaded by both the clip model (CM_) and renderer (TR_)
|
|
subsystems, a simple single-file caching scheme is used. The CM_ subsystems will
|
|
load the file with a request to cache. Only one file will be kept cached at a time,
|
|
so any models that are going to be referenced by both subsystems should alternate
|
|
between the CM_ load function and the ref load function.
|
|
|
|
TODO: A qpath that starts with a leading slash will always refer to the base game, even if another
|
|
game is currently active. This allows character models, skins, and sounds to be downloaded
|
|
to a common directory no matter which game is active.
|
|
|
|
How to prevent downloading zip files?
|
|
Pass pk3 file names in systeminfo, and download before FS_Restart()?
|
|
|
|
Aborting a download disconnects the client from the server.
|
|
|
|
How to mark files as downloadable? Commercial add-ons won't be downloadable.
|
|
|
|
Non-commercial downloads will want to download the entire zip file.
|
|
the game would have to be reset to actually read the zip in
|
|
|
|
Auto-update information
|
|
|
|
Path separators
|
|
|
|
Casing
|
|
|
|
separate server gamedir and client gamedir, so if the user starts
|
|
a local game after having connected to a network game, it won't stick
|
|
with the network game.
|
|
|
|
allow menu options for game selection?
|
|
|
|
Read / write config to floppy option.
|
|
|
|
Different version coexistance?
|
|
|
|
When building a pak file, make sure a q3config.cfg isn't present in it,
|
|
or configs will never get loaded from disk!
|
|
|
|
todo:
|
|
|
|
downloading (outside fs?)
|
|
game directory passing and restarting
|
|
|
|
=============================================================================
|
|
|
|
*/
|
|
|
|
|
|
#define MAX_ZPATH 256
|
|
#define MAX_SEARCH_PATHS 4096
|
|
#define MAX_FILEHASH_SIZE 1024
|
|
|
|
typedef struct fileInPack_s {
|
|
char *name; // name of the file
|
|
unsigned long pos; // file info position in zip
|
|
struct fileInPack_s* next; // next file in the hash
|
|
} fileInPack_t;
|
|
|
|
typedef struct {
|
|
char pakFilename[MAX_OSPATH]; // c:\quake3\baseq3\pak0.pk3
|
|
char pakBasename[MAX_OSPATH]; // pak0
|
|
char pakGamename[MAX_OSPATH]; // baseq3
|
|
unzFile handle; // handle to zip file
|
|
int checksum; // regular checksum
|
|
int pure_checksum; // checksum for pure
|
|
int numfiles; // number of files in pk3
|
|
int referenced; // referenced file flags
|
|
int hashSize; // hash table size (power of 2)
|
|
fileInPack_t* *hashTable; // hash table
|
|
fileInPack_t* buildBuffer; // buffer with the filenames etc.
|
|
} pack_t;
|
|
|
|
typedef struct {
|
|
char path[MAX_OSPATH]; // c:\quake3
|
|
char gamedir[MAX_OSPATH]; // baseq3
|
|
} directory_t;
|
|
|
|
typedef struct searchpath_s {
|
|
struct searchpath_s *next;
|
|
pack_t *pack; // only one of pack / dir will be non-NULL
|
|
directory_t *dir;
|
|
} searchpath_t;
|
|
|
|
static char fs_gamedir[MAX_OSPATH]; // this will be a single file name with no separators
|
|
static cvar_t* fs_debug;
|
|
static cvar_t* fs_homepath;
|
|
static cvar_t* fs_basepath;
|
|
static cvar_t* fs_basegame;
|
|
static cvar_t* fs_gamedirvar;
|
|
static searchpath_t* fs_searchpaths;
|
|
|
|
static int fs_readCount; // total bytes read
|
|
static int fs_loadCount; // total files read
|
|
static int fs_loadStack; // total files in memory
|
|
static int fs_packFiles; // total number of files in packs
|
|
|
|
static int fs_checksumFeed;
|
|
|
|
typedef union {
|
|
FILE* o;
|
|
unzFile z;
|
|
} qfile_gut;
|
|
|
|
typedef struct {
|
|
qfile_gut file;
|
|
qbool unique;
|
|
} qfile_ut;
|
|
|
|
typedef struct {
|
|
qfile_ut handleFiles;
|
|
qbool handleSync;
|
|
int baseOffset;
|
|
int fileSize;
|
|
int zipFilePos;
|
|
qbool zipFile;
|
|
char name[MAX_ZPATH];
|
|
} fileHandleData_t;
|
|
|
|
static fileHandleData_t fsh[MAX_FILE_HANDLES];
|
|
|
|
// TTimo - https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=540
|
|
// whether we did a reorder on the current search path when joining the server
|
|
static qbool fs_reordered;
|
|
|
|
// never load anything from pk3 files that are not present at the server when pure
|
|
static int fs_numServerPaks; // only non-zero when *connected to* a pure server
|
|
static int fs_serverPaks[MAX_SEARCH_PATHS]; // checksums
|
|
|
|
// only used for autodownload, to make sure the client has at least
|
|
// all the pk3 files that are referenced at the server side
|
|
static int fs_numServerReferencedPaks;
|
|
static int fs_serverReferencedPaks[MAX_SEARCH_PATHS]; // checksums
|
|
static char *fs_serverReferencedPakNames[MAX_SEARCH_PATHS]; // pk3 names
|
|
|
|
// last valid game folder used
|
|
static char lastValidBase[MAX_OSPATH];
|
|
static char lastValidGame[MAX_OSPATH];
|
|
|
|
#ifdef FS_MISSING
|
|
static FILE* missingFiles = NULL;
|
|
#endif
|
|
|
|
|
|
int Q_FileHash( const char* s, int tablesize )
|
|
{
|
|
int ch, hash = 0;
|
|
|
|
for (int i = 0; s[i]; ++i) {
|
|
ch = tolower(s[i]);
|
|
if (ch == '.')
|
|
break; // don't include extension
|
|
if (ch =='\\')
|
|
ch = '/'; // damn path names
|
|
hash += ch * (i+119);
|
|
}
|
|
|
|
return (hash & (tablesize-1));
|
|
}
|
|
|
|
|
|
qbool FS_Initialized()
|
|
{
|
|
return (fs_searchpaths != NULL);
|
|
}
|
|
|
|
|
|
int FS_LoadStack()
|
|
{
|
|
return fs_loadStack;
|
|
}
|
|
|
|
|
|
static qbool FS_PakIsPure( const pack_t* pack )
|
|
{
|
|
if (!fs_numServerPaks)
|
|
return qtrue;
|
|
|
|
for ( int i = 0; i < fs_numServerPaks; ++i ) {
|
|
// FIXME: also use hashed file names
|
|
// NOTE TTimo: a pk3 with same checksum but different name would be validated too
|
|
// I don't see this as allowing for any exploit, it would only happen if the client does manips of it's file names 'not a bug'
|
|
if ( pack->checksum == fs_serverPaks[i] ) {
|
|
return qtrue; // on the approved list
|
|
}
|
|
}
|
|
|
|
return qfalse; // not on the pure server pak list
|
|
}
|
|
|
|
|
|
static fileHandle_t FS_HandleForFile()
|
|
{
|
|
for ( int i = 1; i < MAX_FILE_HANDLES; ++i ) {
|
|
if ( fsh[i].handleFiles.file.o == NULL ) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
Com_Error( ERR_DROP, "FS_HandleForFile: none free" );
|
|
return 0;
|
|
}
|
|
|
|
|
|
static FILE* FS_FileForHandle( fileHandle_t f )
|
|
{
|
|
if ( f < 0 || f > MAX_FILE_HANDLES ) {
|
|
Com_Error( ERR_DROP, "FS_FileForHandle: out of reange" );
|
|
}
|
|
if (fsh[f].zipFile == qtrue) {
|
|
Com_Error( ERR_DROP, "FS_FileForHandle: can't get FILE on zip file" );
|
|
}
|
|
if ( ! fsh[f].handleFiles.file.o ) {
|
|
Com_Error( ERR_DROP, "FS_FileForHandle: NULL" );
|
|
}
|
|
|
|
return fsh[f].handleFiles.file.o;
|
|
}
|
|
|
|
|
|
void FS_ForceFlush( fileHandle_t f )
|
|
{
|
|
FILE* file = FS_FileForHandle(f);
|
|
setvbuf( file, NULL, _IONBF, 0 );
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
FS_filelength
|
|
|
|
If this is called on a non-unique FILE (from a pak file),
|
|
it will return the size of the pak file, not the expected
|
|
size of the file.
|
|
================
|
|
*/
|
|
int FS_filelength( fileHandle_t f ) {
|
|
int pos;
|
|
int end;
|
|
FILE* h;
|
|
|
|
h = FS_FileForHandle(f);
|
|
pos = ftell (h);
|
|
fseek (h, 0, SEEK_END);
|
|
end = ftell (h);
|
|
fseek (h, pos, SEEK_SET);
|
|
|
|
return end;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
FS_ReplaceSeparators
|
|
|
|
Fix things up differently for win/unix/mac
|
|
====================
|
|
*/
|
|
static void FS_ReplaceSeparators( char *path ) {
|
|
char *s;
|
|
|
|
for ( s = path ; *s ; s++ ) {
|
|
if ( *s == '/' || *s == '\\' ) {
|
|
*s = PATH_SEP;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
FS_BuildOSPath
|
|
|
|
Qpath may have either forward or backwards slashes
|
|
===================
|
|
*/
|
|
char *FS_BuildOSPath( const char *base, const char *game, const char *qpath ) {
|
|
char temp[MAX_OSPATH];
|
|
static char ospath[2][MAX_OSPATH];
|
|
static int toggle;
|
|
|
|
toggle ^= 1; // flip-flop to allow two returns without clash
|
|
|
|
if( !game || !game[0] ) {
|
|
game = fs_gamedir;
|
|
}
|
|
|
|
Com_sprintf( temp, sizeof(temp), "/%s/%s", game, qpath );
|
|
FS_ReplaceSeparators( temp );
|
|
Com_sprintf( ospath[toggle], sizeof( ospath[0] ), "%s%s", base, temp );
|
|
|
|
return ospath[toggle];
|
|
}
|
|
|
|
|
|
// creates any directories needed to store the given filename
|
|
|
|
static qbool FS_CreatePath( char* OSPath )
|
|
{
|
|
|
|
// make absolutely sure that it can't back up the path
|
|
// FIXME: is c: allowed???
|
|
if ( strstr( OSPath, ".." ) || strstr( OSPath, "::" ) ) {
|
|
Com_Printf( "WARNING: refusing to create relative path \"%s\"\n", OSPath );
|
|
return qfalse;
|
|
}
|
|
|
|
char* ofs;
|
|
for (ofs = OSPath+1 ; *ofs ; ofs++) {
|
|
if (*ofs == PATH_SEP) {
|
|
// create the directory
|
|
*ofs = 0;
|
|
Sys_Mkdir( OSPath );
|
|
*ofs = PATH_SEP;
|
|
}
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
FS_Remove
|
|
|
|
===========
|
|
*/
|
|
void FS_Remove( const char *osPath ) {
|
|
remove( osPath );
|
|
}
|
|
|
|
/*
|
|
===========
|
|
FS_HomeRemove
|
|
|
|
===========
|
|
*/
|
|
void FS_HomeRemove( const char *homePath ) {
|
|
remove( FS_BuildOSPath( fs_homepath->string,
|
|
fs_gamedir, homePath ) );
|
|
}
|
|
|
|
/*
|
|
================
|
|
FS_FileExists
|
|
|
|
Tests if the file exists in the current gamedir, this DOES NOT
|
|
search the paths. This is to determine if opening a file to write
|
|
(which always goes into the current gamedir) will cause any overwrites.
|
|
NOTE TTimo: this goes with FS_FOpenFileWrite for opening the file afterwards
|
|
================
|
|
*/
|
|
qbool FS_FileExists( const char* file )
|
|
{
|
|
return FS_FileExistsEx( file, qtrue );
|
|
}
|
|
|
|
qbool FS_FileExistsEx( const char* file, qbool curGameDir )
|
|
{
|
|
const char* testpath = FS_BuildOSPath( fs_homepath->string, curGameDir ? fs_gamedir : "baseq3", file );
|
|
FILE* f = fopen( testpath, "rb" );
|
|
if (f) {
|
|
fclose( f );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
/*
|
|
================
|
|
FS_SV_FileExists
|
|
|
|
Tests if the file exists
|
|
================
|
|
*/
|
|
static qbool FS_SV_FileExists( const char *file )
|
|
{
|
|
FILE *f;
|
|
char *testpath;
|
|
|
|
testpath = FS_BuildOSPath( fs_homepath->string, file, "");
|
|
testpath[strlen(testpath)-1] = '\0';
|
|
|
|
f = fopen( testpath, "rb" );
|
|
if (f) {
|
|
fclose( f );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
FS_SV_FOpenFileWrite
|
|
|
|
===========
|
|
*/
|
|
fileHandle_t FS_SV_FOpenFileWrite( const char *filename ) {
|
|
char *ospath;
|
|
fileHandle_t f;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
ospath = FS_BuildOSPath( fs_homepath->string, filename, "" );
|
|
ospath[strlen(ospath)-1] = '\0';
|
|
|
|
f = FS_HandleForFile();
|
|
fsh[f].zipFile = qfalse;
|
|
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_SV_FOpenFileWrite: %s\n", ospath );
|
|
}
|
|
|
|
if (!FS_CreatePath( ospath )) {
|
|
return 0;
|
|
}
|
|
|
|
Com_DPrintf( "writing to: %s\n", ospath );
|
|
fsh[f].handleFiles.file.o = fopen( ospath, "wb" );
|
|
|
|
Q_strncpyz( fsh[f].name, filename, sizeof( fsh[f].name ) );
|
|
|
|
fsh[f].handleSync = qfalse;
|
|
if (!fsh[f].handleFiles.file.o) {
|
|
f = 0;
|
|
}
|
|
return f;
|
|
}
|
|
|
|
static void FS_PreventSoundStutter()
|
|
{
|
|
#ifndef DEDICATED
|
|
S_ClearSoundBuffer();
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
===========
|
|
FS_SV_FOpenFileRead
|
|
search for a file somewhere below the home path, base path or cd path
|
|
we search in that order, matching FS_SV_FOpenFileRead order
|
|
===========
|
|
*/
|
|
int FS_SV_FOpenFileRead( const char *filename, fileHandle_t *fp )
|
|
{
|
|
char *ospath;
|
|
fileHandle_t f = 0;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
f = FS_HandleForFile();
|
|
fsh[f].zipFile = qfalse;
|
|
|
|
Q_strncpyz( fsh[f].name, filename, sizeof( fsh[f].name ) );
|
|
|
|
FS_PreventSoundStutter();
|
|
|
|
// search homepath
|
|
ospath = FS_BuildOSPath( fs_homepath->string, filename, "" );
|
|
// remove trailing slash
|
|
ospath[strlen(ospath)-1] = '\0';
|
|
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_SV_FOpenFileRead (fs_homepath): %s\n", ospath );
|
|
}
|
|
|
|
fsh[f].handleFiles.file.o = fopen( ospath, "rb" );
|
|
fsh[f].handleSync = qfalse;
|
|
|
|
if (!fsh[f].handleFiles.file.o)
|
|
{
|
|
// NOTE TTimo on non *nix systems, fs_homepath == fs_basepath, might want to avoid
|
|
if (Q_stricmp(fs_homepath->string,fs_basepath->string))
|
|
{
|
|
// search basepath
|
|
ospath = FS_BuildOSPath( fs_basepath->string, filename, "" );
|
|
ospath[strlen(ospath)-1] = '\0';
|
|
|
|
if ( fs_debug->integer )
|
|
{
|
|
Com_Printf( "FS_SV_FOpenFileRead (fs_basepath): %s\n", ospath );
|
|
}
|
|
|
|
fsh[f].handleFiles.file.o = fopen( ospath, "rb" );
|
|
fsh[f].handleSync = qfalse;
|
|
|
|
if ( !fsh[f].handleFiles.file.o )
|
|
{
|
|
f = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fsh[f].handleFiles.file.o) {
|
|
*fp = f;
|
|
return FS_filelength(f);
|
|
}
|
|
|
|
*fp = 0;
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
FS_SV_Rename
|
|
|
|
===========
|
|
*/
|
|
void FS_SV_Rename( const char *from, const char *to ) {
|
|
char *from_ospath, *to_ospath;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
FS_PreventSoundStutter();
|
|
|
|
from_ospath = FS_BuildOSPath( fs_homepath->string, from, "" );
|
|
to_ospath = FS_BuildOSPath( fs_homepath->string, to, "" );
|
|
from_ospath[strlen(from_ospath)-1] = '\0';
|
|
to_ospath[strlen(to_ospath)-1] = '\0';
|
|
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_SV_Rename: %s --> %s\n", from_ospath, to_ospath );
|
|
}
|
|
|
|
if (rename( from_ospath, to_ospath )) {
|
|
// Failed, try copying it and deleting the original
|
|
//FS_CopyFile( from_ospath, to_ospath );
|
|
//FS_Remove( from_ospath );
|
|
Com_Error( ERR_FATAL, "FS_SV_Rename: %s --> %s failed\n", from_ospath, to_ospath );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
FS_Rename
|
|
|
|
===========
|
|
*/
|
|
void FS_Rename( const char *from, const char *to ) {
|
|
char *from_ospath, *to_ospath;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
FS_PreventSoundStutter();
|
|
|
|
from_ospath = FS_BuildOSPath( fs_homepath->string, fs_gamedir, from );
|
|
to_ospath = FS_BuildOSPath( fs_homepath->string, fs_gamedir, to );
|
|
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_Rename: %s --> %s\n", from_ospath, to_ospath );
|
|
}
|
|
|
|
rename( from_ospath, to_ospath );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
FS_FCloseFile
|
|
|
|
If the FILE pointer is an open pak file, leave it open.
|
|
|
|
For some reason, other dll's can't just cal fclose()
|
|
on files returned by FS_FOpenFile...
|
|
==============
|
|
*/
|
|
void FS_FCloseFile( fileHandle_t f ) {
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if (fsh[f].zipFile == qtrue) {
|
|
unzCloseCurrentFile( fsh[f].handleFiles.file.z );
|
|
if ( fsh[f].handleFiles.unique ) {
|
|
unzClose( fsh[f].handleFiles.file.z );
|
|
}
|
|
Com_Memset( &fsh[f], 0, sizeof( fsh[f] ) );
|
|
return;
|
|
}
|
|
|
|
// we didn't find it as a pak, so close it as a unique file
|
|
if (fsh[f].handleFiles.file.o) {
|
|
fclose (fsh[f].handleFiles.file.o);
|
|
}
|
|
Com_Memset( &fsh[f], 0, sizeof( fsh[f] ) );
|
|
}
|
|
|
|
/*
|
|
===========
|
|
FS_FOpenFileWrite
|
|
|
|
===========
|
|
*/
|
|
fileHandle_t FS_FOpenFileWrite( const char *filename ) {
|
|
char *ospath;
|
|
fileHandle_t f;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
f = FS_HandleForFile();
|
|
fsh[f].zipFile = qfalse;
|
|
|
|
ospath = FS_BuildOSPath( fs_homepath->string, fs_gamedir, filename );
|
|
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_FOpenFileWrite: %s\n", ospath );
|
|
}
|
|
|
|
if (!FS_CreatePath( ospath )) {
|
|
return 0;
|
|
}
|
|
|
|
// enabling the following line causes a recursive function call loop
|
|
// when running with +set logfile 1 +set developer 1
|
|
//Com_DPrintf( "writing to: %s\n", ospath );
|
|
fsh[f].handleFiles.file.o = fopen( ospath, "wb" );
|
|
|
|
Q_strncpyz( fsh[f].name, filename, sizeof( fsh[f].name ) );
|
|
|
|
fsh[f].handleSync = qfalse;
|
|
if (!fsh[f].handleFiles.file.o) {
|
|
f = 0;
|
|
}
|
|
return f;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
FS_FOpenFileAppend
|
|
|
|
===========
|
|
*/
|
|
fileHandle_t FS_FOpenFileAppend( const char *filename ) {
|
|
char *ospath;
|
|
fileHandle_t f;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
f = FS_HandleForFile();
|
|
fsh[f].zipFile = qfalse;
|
|
|
|
Q_strncpyz( fsh[f].name, filename, sizeof( fsh[f].name ) );
|
|
|
|
FS_PreventSoundStutter();
|
|
|
|
ospath = FS_BuildOSPath( fs_homepath->string, fs_gamedir, filename );
|
|
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_FOpenFileAppend: %s\n", ospath );
|
|
}
|
|
|
|
if (!FS_CreatePath( ospath )) {
|
|
return 0;
|
|
}
|
|
|
|
fsh[f].handleFiles.file.o = fopen( ospath, "ab" );
|
|
fsh[f].handleSync = qfalse;
|
|
if (!fsh[f].handleFiles.file.o) {
|
|
f = 0;
|
|
}
|
|
return f;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
FS_FilenameCompare
|
|
|
|
Ignore case and seprator char distinctions
|
|
===========
|
|
*/
|
|
qbool FS_FilenameCompare( const char *s1, const char *s2 ) {
|
|
int c1, c2;
|
|
|
|
do {
|
|
c1 = *s1++;
|
|
c2 = *s2++;
|
|
|
|
if (c1 >= 'a' && c1 <= 'z') {
|
|
c1 -= ('a' - 'A');
|
|
}
|
|
if (c2 >= 'a' && c2 <= 'z') {
|
|
c2 -= ('a' - 'A');
|
|
}
|
|
|
|
if ( c1 == '\\' || c1 == ':' ) {
|
|
c1 = '/';
|
|
}
|
|
if ( c2 == '\\' || c2 == ':' ) {
|
|
c2 = '/';
|
|
}
|
|
|
|
if (c1 != c2) {
|
|
return qtrue; // strings not equal
|
|
}
|
|
} while (c1);
|
|
|
|
return qfalse; // strings are equal
|
|
}
|
|
|
|
|
|
// returns qtrue if we allow opening the file on the real file system
|
|
|
|
static qbool FS_IsPureClientReadException( const char* filename )
|
|
{
|
|
const int l = strlen( filename );
|
|
if ( l < 5 )
|
|
return qfalse;
|
|
|
|
const char* const s4 = filename + l - 4;
|
|
const char* const s5 = filename + l - 5;
|
|
return !Q_stricmp( s4, ".cfg" ) ||
|
|
!Q_stricmp( s4, ".ttf" ) ||
|
|
!Q_stricmp( s4, ".dat" ) ||
|
|
!Q_stricmp( s4, ".jpg" ) ||
|
|
!Q_stricmp( s4, ".tga" ) ||
|
|
!Q_stricmp( s4, ".png" ) ||
|
|
!Q_stricmp( s5, ".jpeg" );
|
|
}
|
|
|
|
|
|
/*
|
|
===========
|
|
FS_FOpenFileRead
|
|
|
|
Finds the file in the search path.
|
|
Returns filesize and an open FILE pointer.
|
|
Used for streaming data out of either a
|
|
separate file or a ZIP file.
|
|
===========
|
|
*/
|
|
extern qbool com_fullyInitialized;
|
|
|
|
int FS_FOpenFileRead( const char *filename, fileHandle_t *file, qbool uniqueFILE ) {
|
|
searchpath_t *search;
|
|
char *netpath;
|
|
pack_t *pak;
|
|
fileInPack_t *pakFile;
|
|
directory_t *dir;
|
|
long hash;
|
|
unz_s *zfi;
|
|
FILE *temp;
|
|
int l;
|
|
char demoExt[16];
|
|
|
|
hash = 0;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( file == NULL ) {
|
|
// just wants to see if file is there
|
|
for ( search = fs_searchpaths ; search ; search = search->next ) {
|
|
//
|
|
if ( search->pack ) {
|
|
hash = Q_FileHash( filename, search->pack->hashSize );
|
|
}
|
|
// is the element a pak file?
|
|
if ( search->pack && search->pack->hashTable[hash] ) {
|
|
// look through all the pak file elements
|
|
pak = search->pack;
|
|
pakFile = pak->hashTable[hash];
|
|
do {
|
|
// case and separator insensitive comparisons
|
|
if ( !FS_FilenameCompare( pakFile->name, filename ) ) {
|
|
// found it!
|
|
return qtrue;
|
|
}
|
|
pakFile = pakFile->next;
|
|
} while(pakFile != NULL);
|
|
} else if ( search->dir ) {
|
|
dir = search->dir;
|
|
netpath = FS_BuildOSPath( dir->path, dir->gamedir, filename );
|
|
temp = fopen (netpath, "rb");
|
|
if ( !temp ) {
|
|
continue;
|
|
}
|
|
fclose(temp);
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !filename ) {
|
|
Com_Error( ERR_FATAL, "FS_FOpenFileRead: NULL 'filename' parameter passed\n" );
|
|
}
|
|
|
|
Com_sprintf (demoExt, sizeof(demoExt), ".dm_%d",PROTOCOL_VERSION );
|
|
// qpaths are not supposed to have a leading slash
|
|
if ( filename[0] == '/' || filename[0] == '\\' ) {
|
|
filename++;
|
|
}
|
|
|
|
// make absolutely sure that it can't back up the path.
|
|
// The searchpaths do guarantee that something will always
|
|
// be prepended, so we don't need to worry about "c:" or "//limbo"
|
|
if ( strstr( filename, ".." ) || strstr( filename, "::" ) ) {
|
|
*file = 0;
|
|
return -1;
|
|
}
|
|
|
|
// make sure the q3key file is only readable by the quake3.exe at initialization
|
|
// any other time the key should only be accessed in memory using the provided functions
|
|
if( com_fullyInitialized && strstr( filename, "q3key" ) ) {
|
|
*file = 0;
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// search through the path, one element at a time
|
|
//
|
|
|
|
*file = FS_HandleForFile();
|
|
fsh[*file].handleFiles.unique = uniqueFILE;
|
|
|
|
for ( search = fs_searchpaths ; search ; search = search->next ) {
|
|
//
|
|
if ( search->pack ) {
|
|
hash = Q_FileHash( filename, search->pack->hashSize );
|
|
}
|
|
// is the element a pak file?
|
|
if ( search->pack && search->pack->hashTable[hash] ) {
|
|
// disregard if it doesn't match one of the allowed pure pak files
|
|
if ( !FS_PakIsPure(search->pack) ) {
|
|
continue;
|
|
}
|
|
|
|
// look through all the pak file elements
|
|
pak = search->pack;
|
|
pakFile = pak->hashTable[hash];
|
|
do {
|
|
// case and separator insensitive comparisons
|
|
if ( !FS_FilenameCompare( pakFile->name, filename ) ) {
|
|
// found it!
|
|
|
|
// mark the pak as having been referenced and mark specifics on cgame and ui
|
|
// shaders, txt, arena files by themselves do not count as a reference as
|
|
// these are loaded from all pk3s
|
|
// from every pk3 file..
|
|
l = strlen( filename );
|
|
if ( !(pak->referenced & FS_GENERAL_REF)) {
|
|
if ( Q_stricmp(filename + l - 7, ".shader") != 0 &&
|
|
Q_stricmp(filename + l - 4, ".txt") != 0 &&
|
|
Q_stricmp(filename + l - 4, ".cfg") != 0 &&
|
|
Q_stricmp(filename + l - 7, ".config") != 0 &&
|
|
strstr(filename, "levelshots") == NULL &&
|
|
Q_stricmp(filename + l - 4, ".bot") != 0 &&
|
|
Q_stricmp(filename + l - 6, ".arena") != 0 &&
|
|
Q_stricmp(filename + l - 5, ".menu") != 0) {
|
|
pak->referenced |= FS_GENERAL_REF;
|
|
}
|
|
}
|
|
|
|
if (!(pak->referenced & FS_QAGAME_REF) && !Q_stricmp(filename, "vm/qagame.qvm")) {
|
|
pak->referenced |= FS_QAGAME_REF;
|
|
}
|
|
if (!(pak->referenced & FS_CGAME_REF) && !Q_stricmp(filename, "vm/cgame.qvm")) {
|
|
pak->referenced |= FS_CGAME_REF;
|
|
}
|
|
if (!(pak->referenced & FS_UI_REF) && !Q_stricmp(filename, "vm/ui.qvm")) {
|
|
pak->referenced |= FS_UI_REF;
|
|
}
|
|
|
|
if ( uniqueFILE ) {
|
|
// open a new file on the pakfile
|
|
fsh[*file].handleFiles.file.z = unzReOpen (pak->pakFilename, pak->handle);
|
|
if (fsh[*file].handleFiles.file.z == NULL) {
|
|
Com_Error (ERR_FATAL, "Couldn't reopen %s", pak->pakFilename);
|
|
}
|
|
} else {
|
|
fsh[*file].handleFiles.file.z = pak->handle;
|
|
}
|
|
Q_strncpyz( fsh[*file].name, filename, sizeof( fsh[*file].name ) );
|
|
fsh[*file].zipFile = qtrue;
|
|
zfi = (unz_s *)fsh[*file].handleFiles.file.z;
|
|
// in case the file was new
|
|
temp = zfi->file;
|
|
// set the file position in the zip file (also sets the current file info)
|
|
unzSetCurrentFileInfoPosition(pak->handle, pakFile->pos);
|
|
// copy the file info into the unzip structure
|
|
Com_Memcpy( zfi, pak->handle, sizeof(unz_s) );
|
|
// we copy this back into the structure
|
|
zfi->file = temp;
|
|
// open the file in the zip
|
|
unzOpenCurrentFile( fsh[*file].handleFiles.file.z );
|
|
fsh[*file].zipFilePos = pakFile->pos;
|
|
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_FOpenFileRead: %s (found in '%s')\n",
|
|
filename, pak->pakFilename );
|
|
}
|
|
return zfi->cur_file_info.uncompressed_size;
|
|
}
|
|
pakFile = pakFile->next;
|
|
} while(pakFile != NULL);
|
|
} else if ( search->dir ) {
|
|
// check a file in the directory tree
|
|
|
|
// For mods, we ignore baseq3/q3config.cfg and baseq3/autoexec.cfg
|
|
// to avoid config pollution.
|
|
// Mod authors should package a proper default.cfg (pretty much binds only)
|
|
// in their .pk3 so that it overrides baseq3/default.cfg (it's in pak0.pk3).
|
|
if ( Q_stricmp( search->dir->gamedir, fs_gamedir ) &&
|
|
( !Q_stricmp( filename, "q3config.cfg" ) ||
|
|
!Q_stricmp( filename, "autoexec.cfg" ) ) ) {
|
|
continue;
|
|
}
|
|
|
|
// if we are running restricted, the only files we
|
|
// will allow to come from the directory are .cfg files
|
|
|
|
// FIXME TTimo I'm not sure about the fs_numServerPaks test
|
|
// if you are using FS_ReadFile to find out if a file exists,
|
|
// this test can make the search fail although the file is in the directory
|
|
// I had the problem on https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=8
|
|
// turned out I used FS_FileExists instead
|
|
if ( fs_numServerPaks && !FS_IsPureClientReadException( filename ) ) {
|
|
continue;
|
|
}
|
|
|
|
dir = search->dir;
|
|
|
|
netpath = FS_BuildOSPath( dir->path, dir->gamedir, filename );
|
|
fsh[*file].handleFiles.file.o = fopen (netpath, "rb");
|
|
if ( !fsh[*file].handleFiles.file.o ) {
|
|
continue;
|
|
}
|
|
|
|
Q_strncpyz( fsh[*file].name, filename, sizeof( fsh[*file].name ) );
|
|
fsh[*file].zipFile = qfalse;
|
|
if ( fs_debug->integer ) {
|
|
Com_Printf( "FS_FOpenFileRead: %s (found in '%s/%s')\n", filename,
|
|
dir->path, dir->gamedir );
|
|
}
|
|
|
|
return FS_filelength (*file);
|
|
}
|
|
}
|
|
|
|
Com_DPrintf ("Can't find %s\n", filename);
|
|
#ifdef FS_MISSING
|
|
if (missingFiles) {
|
|
fprintf(missingFiles, "%s\n", filename);
|
|
}
|
|
#endif
|
|
*file = 0;
|
|
return -1;
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
FS_Read
|
|
|
|
Properly handles partial reads
|
|
=================
|
|
*/
|
|
int FS_Read2( void *buffer, int len, fileHandle_t f ) {
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !f ) {
|
|
return 0;
|
|
}
|
|
|
|
return FS_Read( buffer, len, f);
|
|
}
|
|
|
|
int FS_Read( void *buffer, int len, fileHandle_t f ) {
|
|
int block, remaining;
|
|
int read;
|
|
byte *buf;
|
|
int tries;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !f ) {
|
|
return 0;
|
|
}
|
|
|
|
buf = (byte *)buffer;
|
|
fs_readCount += len;
|
|
|
|
if (fsh[f].zipFile == qfalse) {
|
|
remaining = len;
|
|
tries = 0;
|
|
while (remaining) {
|
|
block = remaining;
|
|
read = fread (buf, 1, block, fsh[f].handleFiles.file.o);
|
|
if (read == 0) {
|
|
// we might have been trying to read from a CD, which
|
|
// sometimes returns a 0 read on windows
|
|
if (!tries) {
|
|
tries = 1;
|
|
} else {
|
|
return len-remaining; //Com_Error (ERR_FATAL, "FS_Read: 0 bytes read");
|
|
}
|
|
}
|
|
|
|
if (read == -1) {
|
|
Com_Error (ERR_FATAL, "FS_Read: -1 bytes read");
|
|
}
|
|
|
|
remaining -= read;
|
|
buf += read;
|
|
}
|
|
return len;
|
|
} else {
|
|
return unzReadCurrentFile(fsh[f].handleFiles.file.z, buffer, len);
|
|
}
|
|
}
|
|
|
|
/*
|
|
=================
|
|
FS_Write
|
|
|
|
Properly handles partial writes
|
|
=================
|
|
*/
|
|
int FS_Write( const void *buffer, int len, fileHandle_t h ) {
|
|
int block, remaining;
|
|
int written;
|
|
byte *buf;
|
|
int tries;
|
|
FILE *f;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !h ) {
|
|
return 0;
|
|
}
|
|
|
|
f = FS_FileForHandle(h);
|
|
buf = (byte *)buffer;
|
|
|
|
remaining = len;
|
|
tries = 0;
|
|
while (remaining) {
|
|
block = remaining;
|
|
written = fwrite (buf, 1, block, f);
|
|
if (written == 0) {
|
|
if (!tries) {
|
|
tries = 1;
|
|
} else {
|
|
Com_Printf( "FS_Write: 0 bytes written\n" );
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (written == -1) {
|
|
Com_Printf( "FS_Write: -1 bytes written\n" );
|
|
return 0;
|
|
}
|
|
|
|
remaining -= written;
|
|
buf += written;
|
|
}
|
|
if ( fsh[h].handleSync ) {
|
|
fflush( f );
|
|
}
|
|
return len;
|
|
}
|
|
|
|
void QDECL FS_Printf( fileHandle_t h, const char *fmt, ... ) {
|
|
va_list argptr;
|
|
char msg[MAXPRINTMSG];
|
|
|
|
va_start (argptr,fmt);
|
|
Q_vsnprintf (msg, sizeof(msg), fmt, argptr);
|
|
va_end (argptr);
|
|
|
|
FS_Write(msg, strlen(msg), h);
|
|
}
|
|
|
|
|
|
#define PK3_SEEK_BUFFER_SIZE 65536
|
|
|
|
int FS_Seek( fileHandle_t f, long offset, int origin )
|
|
{
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
return -1;
|
|
}
|
|
|
|
if (fsh[f].zipFile == qtrue) {
|
|
//FIXME: this is incomplete and really, really
|
|
//crappy (but better than what was here before)
|
|
byte buffer[PK3_SEEK_BUFFER_SIZE];
|
|
int remainder = offset;
|
|
|
|
if( offset < 0 || origin == FS_SEEK_END ) {
|
|
Com_Error( ERR_FATAL, "Negative offsets and FS_SEEK_END not implemented "
|
|
"for FS_Seek on pk3 file contents\n" );
|
|
return -1;
|
|
}
|
|
|
|
switch( origin ) {
|
|
case FS_SEEK_SET:
|
|
unzSetCurrentFileInfoPosition(fsh[f].handleFiles.file.z, fsh[f].zipFilePos);
|
|
unzOpenCurrentFile(fsh[f].handleFiles.file.z);
|
|
//fallthrough
|
|
|
|
case FS_SEEK_CUR:
|
|
while( remainder > PK3_SEEK_BUFFER_SIZE ) {
|
|
FS_Read( buffer, PK3_SEEK_BUFFER_SIZE, f );
|
|
remainder -= PK3_SEEK_BUFFER_SIZE;
|
|
}
|
|
FS_Read( buffer, remainder, f );
|
|
return offset;
|
|
break;
|
|
|
|
default:
|
|
Com_Error( ERR_FATAL, "Bad origin in FS_Seek\n" );
|
|
return -1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
FILE* file = FS_FileForHandle(f);
|
|
int _origin;
|
|
|
|
switch ( origin ) {
|
|
case FS_SEEK_CUR:
|
|
_origin = SEEK_CUR;
|
|
break;
|
|
case FS_SEEK_END:
|
|
_origin = SEEK_END;
|
|
break;
|
|
case FS_SEEK_SET:
|
|
_origin = SEEK_SET;
|
|
break;
|
|
default:
|
|
_origin = SEEK_CUR;
|
|
Com_Error( ERR_FATAL, "Bad origin in FS_Seek\n" );
|
|
break;
|
|
}
|
|
|
|
return fseek( file, offset, _origin );
|
|
}
|
|
|
|
|
|
/*
|
|
======================================================================================
|
|
|
|
CONVENIENCE FUNCTIONS FOR ENTIRE FILES
|
|
|
|
======================================================================================
|
|
*/
|
|
|
|
|
|
qbool FS_FileIsInPAK( const char* filename, int* pureChecksum, int* checksum )
|
|
{
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !filename ) {
|
|
Com_Error( ERR_FATAL, "FS_FOpenFileRead: NULL 'filename' parameter passed\n" );
|
|
}
|
|
|
|
// qpaths are not supposed to have a leading slash
|
|
if ( filename[0] == '/' || filename[0] == '\\' ) {
|
|
filename++;
|
|
}
|
|
|
|
// make absolutely sure that it can't back up the path.
|
|
// The searchpaths do guarantee that something will always
|
|
// be prepended, so we don't need to worry about "c:" or "//limbo"
|
|
if ( strstr( filename, ".." ) || strstr( filename, "::" ) ) {
|
|
return qfalse;
|
|
}
|
|
|
|
// search through the path, one element at a time
|
|
|
|
for ( const searchpath_t* search = fs_searchpaths; search; search = search->next ) {
|
|
if ( !search->pack )
|
|
continue;
|
|
|
|
long hash = Q_FileHash( filename, search->pack->hashSize );
|
|
// is the element a pak file?
|
|
if ( search->pack->hashTable[hash] ) {
|
|
// disregard if it doesn't match one of the allowed pure pak files
|
|
if ( !FS_PakIsPure(search->pack) ) {
|
|
continue;
|
|
}
|
|
|
|
// look through all the pak file elements
|
|
const fileInPack_t* pakFile = search->pack->hashTable[hash];
|
|
do {
|
|
// case and separator insensitive comparisons
|
|
if ( !FS_FilenameCompare( pakFile->name, filename ) ) {
|
|
if (pureChecksum) {
|
|
*pureChecksum = search->pack->pure_checksum;
|
|
}
|
|
if (checksum) {
|
|
*checksum = search->pack->checksum;
|
|
}
|
|
return qtrue;
|
|
}
|
|
pakFile = pakFile->next;
|
|
} while (pakFile);
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
FS_ReadFile
|
|
|
|
Filename are relative to the quake search path
|
|
a null buffer will just return the file length without loading
|
|
============
|
|
*/
|
|
int FS_ReadFile( const char *qpath, void **buffer ) {
|
|
fileHandle_t h;
|
|
byte* buf;
|
|
qbool isConfig;
|
|
int len;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !qpath || !qpath[0] ) {
|
|
Com_Error( ERR_FATAL, "FS_ReadFile with empty name\n" );
|
|
}
|
|
|
|
buf = NULL; // quiet compiler warning
|
|
|
|
// if this is a .cfg file and we are playing back a journal, read
|
|
// it from the journal file
|
|
if ( strstr( qpath, ".cfg" ) ) {
|
|
isConfig = qtrue;
|
|
if ( com_journal && com_journal->integer == 2 ) {
|
|
int r;
|
|
|
|
Com_DPrintf( "Loading %s from journal file.\n", qpath );
|
|
r = FS_Read( &len, sizeof( len ), com_journalDataFile );
|
|
if ( r != sizeof( len ) ) {
|
|
if (buffer != NULL) *buffer = NULL;
|
|
return -1;
|
|
}
|
|
// if the file didn't exist when the journal was created
|
|
if (!len) {
|
|
if (buffer == NULL) {
|
|
return 1; // hack for old journal files
|
|
}
|
|
*buffer = NULL;
|
|
return -1;
|
|
}
|
|
if (buffer == NULL) {
|
|
return len;
|
|
}
|
|
|
|
buf = (byte*)Hunk_AllocateTempMemory(len+1);
|
|
*buffer = buf;
|
|
|
|
r = FS_Read( buf, len, com_journalDataFile );
|
|
if ( r != len ) {
|
|
Com_Error( ERR_FATAL, "Read from journalDataFile failed" );
|
|
}
|
|
|
|
fs_loadCount++;
|
|
fs_loadStack++;
|
|
|
|
// guarantee that it will have a trailing 0 for string operations
|
|
buf[len] = 0;
|
|
|
|
return len;
|
|
}
|
|
} else {
|
|
isConfig = qfalse;
|
|
}
|
|
|
|
// look for it in the filesystem or pack files
|
|
len = FS_FOpenFileRead( qpath, &h, qfalse );
|
|
if ( h == 0 ) {
|
|
if ( buffer ) {
|
|
*buffer = NULL;
|
|
}
|
|
// if we are journalling and it is a config file, write a zero to the journal file
|
|
if ( isConfig && com_journal && com_journal->integer == 1 ) {
|
|
Com_DPrintf( "Writing zero for %s to journal file.\n", qpath );
|
|
len = 0;
|
|
FS_Write( &len, sizeof( len ), com_journalDataFile );
|
|
FS_Flush( com_journalDataFile );
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if ( !buffer ) {
|
|
if ( isConfig && com_journal && com_journal->integer == 1 ) {
|
|
Com_DPrintf( "Writing len for %s to journal file.\n", qpath );
|
|
FS_Write( &len, sizeof( len ), com_journalDataFile );
|
|
FS_Flush( com_journalDataFile );
|
|
}
|
|
FS_FCloseFile( h);
|
|
return len;
|
|
}
|
|
|
|
fs_loadCount++;
|
|
fs_loadStack++;
|
|
|
|
buf = (byte*)Hunk_AllocateTempMemory(len+1);
|
|
*buffer = buf;
|
|
|
|
FS_Read (buf, len, h);
|
|
|
|
// guarantee that it will have a trailing 0 for string operations
|
|
buf[len] = 0;
|
|
FS_FCloseFile( h );
|
|
|
|
// if we are journalling and it is a config file, write it to the journal file
|
|
if ( isConfig && com_journal && com_journal->integer == 1 ) {
|
|
Com_DPrintf( "Writing %s to journal file.\n", qpath );
|
|
FS_Write( &len, sizeof( len ), com_journalDataFile );
|
|
FS_Write( buf, len, com_journalDataFile );
|
|
FS_Flush( com_journalDataFile );
|
|
}
|
|
return len;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
FS_FreeFile
|
|
=============
|
|
*/
|
|
void FS_FreeFile( void *buffer ) {
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
if ( !buffer ) {
|
|
Com_Error( ERR_FATAL, "FS_FreeFile( NULL )" );
|
|
}
|
|
fs_loadStack--;
|
|
|
|
Hunk_FreeTempMemory( buffer );
|
|
|
|
// if all of our temp files are free, clear all of our space
|
|
if ( fs_loadStack == 0 ) {
|
|
Hunk_ClearTempMemory();
|
|
}
|
|
}
|
|
|
|
/*
|
|
============
|
|
FS_WriteFile
|
|
|
|
Filename are reletive to the quake search path
|
|
============
|
|
*/
|
|
void FS_WriteFile( const char *qpath, const void *buffer, int size ) {
|
|
fileHandle_t f;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !qpath || !buffer ) {
|
|
Com_Error( ERR_FATAL, "FS_WriteFile: NULL parameter" );
|
|
}
|
|
|
|
f = FS_FOpenFileWrite( qpath );
|
|
if ( !f ) {
|
|
Com_Printf( "Failed to open %s\n", qpath );
|
|
return;
|
|
}
|
|
|
|
FS_Write( buffer, size, f );
|
|
|
|
FS_FCloseFile( f );
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
==========================================================================
|
|
|
|
ZIP FILE LOADING
|
|
|
|
==========================================================================
|
|
*/
|
|
|
|
/*
|
|
=================
|
|
FS_LoadZipFile
|
|
|
|
Creates a new pak_t in the search chain for the contents
|
|
of a zip file.
|
|
=================
|
|
*/
|
|
static pack_t *FS_LoadZipFile( char *zipfile, const char *basename )
|
|
{
|
|
unzFile uf;
|
|
int err;
|
|
unz_global_info gi;
|
|
char filename_inzip[MAX_ZPATH];
|
|
unz_file_info file_info;
|
|
int i, len;
|
|
long hash;
|
|
int fs_numHeaderLongs;
|
|
|
|
fs_numHeaderLongs = 0;
|
|
|
|
uf = unzOpen(zipfile);
|
|
err = unzGetGlobalInfo (uf,&gi);
|
|
|
|
if (err != UNZ_OK)
|
|
return NULL;
|
|
|
|
fs_packFiles += gi.number_entry;
|
|
|
|
len = 0;
|
|
unzGoToFirstFile(uf);
|
|
for (i = 0; i < gi.number_entry; i++)
|
|
{
|
|
err = unzGetCurrentFileInfo(uf, &file_info, filename_inzip, sizeof(filename_inzip), NULL, 0, NULL, 0);
|
|
if (err != UNZ_OK) {
|
|
break;
|
|
}
|
|
len += strlen(filename_inzip) + 1;
|
|
unzGoToNextFile(uf);
|
|
}
|
|
|
|
fileInPack_t* buildBuffer = (fileInPack_t*)Z_Malloc( (gi.number_entry * sizeof( fileInPack_t )) + len );
|
|
char* namePtr = ((char*)buildBuffer) + gi.number_entry * sizeof( fileInPack_t );
|
|
int* fs_headerLongs = (int*)Z_Malloc( ( gi.number_entry + 1 ) * sizeof(int) );
|
|
fs_headerLongs[ fs_numHeaderLongs++ ] = LittleLong( fs_checksumFeed );
|
|
|
|
// get the hash table size from the number of files in the zip
|
|
// because lots of custom pk3 files have less than 32 or 64 files
|
|
for (i = 1; i <= MAX_FILEHASH_SIZE; i <<= 1) {
|
|
if (i > gi.number_entry) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
pack_t* pack = (pack_t*)Z_Malloc( sizeof( pack_t ) + i * sizeof(fileInPack_t *) );
|
|
pack->hashSize = i;
|
|
pack->hashTable = (fileInPack_t **) (((char *) pack) + sizeof( pack_t ));
|
|
for(i = 0; i < pack->hashSize; i++) {
|
|
pack->hashTable[i] = NULL;
|
|
}
|
|
|
|
Q_strncpyz( pack->pakFilename, zipfile, sizeof( pack->pakFilename ) );
|
|
Q_strncpyz( pack->pakBasename, basename, sizeof( pack->pakBasename ) );
|
|
|
|
// strip .pk3 if needed
|
|
if ( strlen( pack->pakBasename ) > 4 && !Q_stricmp( pack->pakBasename + strlen( pack->pakBasename ) - 4, ".pk3" ) ) {
|
|
pack->pakBasename[strlen( pack->pakBasename ) - 4] = 0;
|
|
}
|
|
|
|
pack->handle = uf;
|
|
pack->numfiles = gi.number_entry;
|
|
unzGoToFirstFile(uf);
|
|
|
|
for (i = 0; i < gi.number_entry; i++)
|
|
{
|
|
err = unzGetCurrentFileInfo(uf, &file_info, filename_inzip, sizeof(filename_inzip), NULL, 0, NULL, 0);
|
|
if (err != UNZ_OK) {
|
|
break;
|
|
}
|
|
if (file_info.uncompressed_size > 0) {
|
|
fs_headerLongs[fs_numHeaderLongs++] = LittleLong(file_info.crc);
|
|
}
|
|
Q_strlwr( filename_inzip );
|
|
hash = Q_FileHash( filename_inzip, pack->hashSize );
|
|
buildBuffer[i].name = namePtr;
|
|
strcpy( buildBuffer[i].name, filename_inzip );
|
|
namePtr += strlen(filename_inzip) + 1;
|
|
// store the file position in the zip
|
|
unzGetCurrentFileInfoPosition(uf, &buildBuffer[i].pos);
|
|
//
|
|
buildBuffer[i].next = pack->hashTable[hash];
|
|
pack->hashTable[hash] = &buildBuffer[i];
|
|
unzGoToNextFile(uf);
|
|
}
|
|
|
|
pack->checksum = Com_BlockChecksum( &fs_headerLongs[ 1 ], 4 * ( fs_numHeaderLongs - 1 ) );
|
|
pack->pure_checksum = Com_BlockChecksum( fs_headerLongs, 4 * fs_numHeaderLongs );
|
|
pack->checksum = LittleLong( pack->checksum );
|
|
pack->pure_checksum = LittleLong( pack->pure_checksum );
|
|
|
|
Z_Free(fs_headerLongs);
|
|
|
|
pack->buildBuffer = buildBuffer;
|
|
return pack;
|
|
}
|
|
|
|
/*
|
|
=================================================================================
|
|
|
|
DIRECTORY SCANNING FUNCTIONS
|
|
|
|
=================================================================================
|
|
*/
|
|
|
|
#define MAX_FOUND_FILES 0x1000
|
|
|
|
static int FS_ReturnPath( const char *zname, char *zpath, int *depth ) {
|
|
int len, at, newdep;
|
|
|
|
newdep = 0;
|
|
zpath[0] = 0;
|
|
len = 0;
|
|
at = 0;
|
|
|
|
while(zname[at] != 0)
|
|
{
|
|
if (zname[at]=='/' || zname[at]=='\\') {
|
|
len = at;
|
|
newdep++;
|
|
}
|
|
at++;
|
|
}
|
|
strcpy(zpath, zname);
|
|
zpath[len] = 0;
|
|
*depth = newdep;
|
|
|
|
return len;
|
|
}
|
|
|
|
|
|
static int FS_AddFileToList( const char *name, char *list[MAX_FOUND_FILES], int nfiles )
|
|
{
|
|
int i;
|
|
|
|
if ( nfiles == MAX_FOUND_FILES - 1 ) {
|
|
return nfiles;
|
|
}
|
|
for ( i = 0 ; i < nfiles ; i++ ) {
|
|
if ( !Q_stricmp( name, list[i] ) ) {
|
|
return nfiles; // allready in list
|
|
}
|
|
}
|
|
list[nfiles] = CopyString( name );
|
|
nfiles++;
|
|
|
|
return nfiles;
|
|
}
|
|
|
|
|
|
/*
|
|
Returns a uniqued list of files that match the given criteria
|
|
from all search paths
|
|
*/
|
|
static char** FS_ListFilteredFiles( const char *path, const char *extension, const char* filter, int *numfiles, int filters )
|
|
{
|
|
int nfiles;
|
|
char *list[MAX_FOUND_FILES];
|
|
searchpath_t *search;
|
|
int i;
|
|
int pathLength;
|
|
int extensionLength;
|
|
int length, pathDepth, temp;
|
|
pack_t *pak;
|
|
fileInPack_t *buildBuffer;
|
|
char zpath[MAX_ZPATH];
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !path ) {
|
|
*numfiles = 0;
|
|
return NULL;
|
|
}
|
|
if ( !extension ) {
|
|
extension = "";
|
|
}
|
|
|
|
pathLength = strlen( path );
|
|
if ( path[pathLength-1] == '\\' || path[pathLength-1] == '/' ) {
|
|
pathLength--;
|
|
}
|
|
extensionLength = strlen( extension );
|
|
nfiles = 0;
|
|
FS_ReturnPath(path, zpath, &pathDepth);
|
|
|
|
//
|
|
// search through the path, one element at a time, adding to list
|
|
//
|
|
for (search = fs_searchpaths ; search ; search = search->next) {
|
|
// is the element a pak file?
|
|
if (search->pack && !(filters & FS_FILTER_INPAK)) {
|
|
|
|
//ZOID: If we are pure, don't search for files on paks that
|
|
// aren't on the pure list
|
|
if ( !FS_PakIsPure(search->pack) ) {
|
|
continue;
|
|
}
|
|
|
|
// look through all the pak file elements
|
|
pak = search->pack;
|
|
buildBuffer = pak->buildBuffer;
|
|
for (i = 0; i < pak->numfiles; i++) {
|
|
char *name;
|
|
int zpathLen, depth;
|
|
|
|
// check for directory match
|
|
name = buildBuffer[i].name;
|
|
//
|
|
if (filter) {
|
|
// case insensitive
|
|
if (!Com_FilterPath( filter, name ))
|
|
continue;
|
|
// unique the match
|
|
nfiles = FS_AddFileToList( name, list, nfiles );
|
|
}
|
|
else {
|
|
|
|
zpathLen = FS_ReturnPath(name, zpath, &depth);
|
|
|
|
if ( (depth-pathDepth)>2 || pathLength > zpathLen || Q_stricmpn( name, path, pathLength ) ) {
|
|
continue;
|
|
}
|
|
|
|
// check for extension match
|
|
length = strlen( name );
|
|
if ( length < extensionLength ) {
|
|
continue;
|
|
}
|
|
|
|
if ( Q_stricmp( name + length - extensionLength, extension ) ) {
|
|
continue;
|
|
}
|
|
// unique the match
|
|
|
|
temp = pathLength;
|
|
if (pathLength) {
|
|
temp++; // include the '/'
|
|
}
|
|
nfiles = FS_AddFileToList( name + temp, list, nfiles );
|
|
}
|
|
}
|
|
} else if (search->dir && !(filters & FS_FILTER_NOTINPAK)) { // scan for files in the filesystem
|
|
char *netpath;
|
|
int numSysFiles;
|
|
char **sysFiles;
|
|
char *name;
|
|
|
|
netpath = FS_BuildOSPath( search->dir->path, search->dir->gamedir, path );
|
|
sysFiles = Sys_ListFiles( netpath, extension, filter, &numSysFiles, qfalse );
|
|
for ( i = 0 ; i < numSysFiles ; i++ ) {
|
|
// unique the match
|
|
name = sysFiles[i];
|
|
nfiles = FS_AddFileToList( name, list, nfiles );
|
|
}
|
|
Sys_FreeFileList( sysFiles );
|
|
}
|
|
}
|
|
|
|
// return a copy of the list
|
|
*numfiles = nfiles;
|
|
|
|
if ( !nfiles ) {
|
|
return NULL;
|
|
}
|
|
|
|
char** listCopy = (char**)Z_Malloc( ( nfiles + 1 ) * sizeof( *listCopy ) );
|
|
for ( i = 0 ; i < nfiles ; i++ ) {
|
|
listCopy[i] = list[i];
|
|
}
|
|
listCopy[i] = NULL;
|
|
|
|
return listCopy;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
FS_ListFiles
|
|
=================
|
|
*/
|
|
char **FS_ListFiles( const char *path, const char *extension, int *numfiles ) {
|
|
return FS_ListFilteredFiles( path, extension, NULL, numfiles, 0 );
|
|
}
|
|
|
|
/*
|
|
=================
|
|
FS_FreeFileList
|
|
=================
|
|
*/
|
|
void FS_FreeFileList( char **list ) {
|
|
int i;
|
|
|
|
if ( !fs_searchpaths ) {
|
|
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
|
}
|
|
|
|
if ( !list ) {
|
|
return;
|
|
}
|
|
|
|
for ( i = 0 ; list[i] ; i++ ) {
|
|
Z_Free( list[i] );
|
|
}
|
|
|
|
Z_Free( list );
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
FS_GetFileList
|
|
================
|
|
*/
|
|
int FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ) {
|
|
int nFiles, i, nTotal, nLen;
|
|
char **pFiles = NULL;
|
|
|
|
*listbuf = 0;
|
|
nFiles = 0;
|
|
nTotal = 0;
|
|
|
|
if (Q_stricmp(path, "$modlist") == 0) {
|
|
return FS_GetModList(listbuf, bufsize);
|
|
}
|
|
|
|
pFiles = FS_ListFiles(path, extension, &nFiles);
|
|
|
|
for (i =0; i < nFiles; i++) {
|
|
nLen = strlen(pFiles[i]) + 1;
|
|
if (nTotal + nLen + 1 < bufsize) {
|
|
strcpy(listbuf, pFiles[i]);
|
|
listbuf += nLen;
|
|
nTotal += nLen;
|
|
}
|
|
else {
|
|
nFiles = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
FS_FreeFileList(pFiles);
|
|
|
|
return nFiles;
|
|
}
|
|
|
|
/*
|
|
=======================
|
|
Sys_ConcatenateFileLists
|
|
|
|
mkv: Naive implementation. Concatenates three lists into a
|
|
new list, and frees the old lists from the heap.
|
|
bk001129 - from cvs1.17 (mkv)
|
|
|
|
FIXME TTimo those two should move to common.c next to Sys_ListFiles
|
|
=======================
|
|
*/
|
|
static unsigned int Sys_CountFileList(char **list)
|
|
{
|
|
int i = 0;
|
|
|
|
if (list)
|
|
{
|
|
while (*list)
|
|
{
|
|
list++;
|
|
i++;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
static char** Sys_ConcatenateFileLists( char **list0, char **list1 )
|
|
{
|
|
int totalLength = 0;
|
|
totalLength += Sys_CountFileList(list0);
|
|
totalLength += Sys_CountFileList(list1);
|
|
|
|
char** cat, **dst, **src;
|
|
dst = cat = (char**)Z_Malloc( ( totalLength + 1 ) * sizeof( char* ) );
|
|
|
|
/* Copy over lists. */
|
|
if (list0)
|
|
{
|
|
for (src = list0; *src; src++, dst++)
|
|
*dst = *src;
|
|
}
|
|
if (list1)
|
|
{
|
|
for (src = list1; *src; src++, dst++)
|
|
*dst = *src;
|
|
}
|
|
|
|
// Terminate the list
|
|
*dst = NULL;
|
|
|
|
// Free our old lists.
|
|
// NOTE: not freeing their content, it's been merged in dst and still being used
|
|
if (list0) Z_Free( list0 );
|
|
if (list1) Z_Free( list1 );
|
|
|
|
return cat;
|
|
}
|
|
|
|
/*
|
|
================
|
|
FS_GetModList
|
|
|
|
Returns a list of mod directory names
|
|
A mod directory is a peer to baseq3 with a pk3 in it
|
|
The directories are searched in base path, cd path and home path
|
|
================
|
|
*/
|
|
int FS_GetModList( char *listbuf, int bufsize ) {
|
|
int nMods, i, j, nTotal, nLen, nPaks, nPotential, nDescLen;
|
|
char **pFiles = NULL;
|
|
char **pPaks = NULL;
|
|
char *name, *path;
|
|
char descPath[MAX_OSPATH];
|
|
fileHandle_t descHandle;
|
|
|
|
int dummy;
|
|
char **pFiles0 = NULL;
|
|
char **pFiles1 = NULL;
|
|
qbool bDrop = qfalse;
|
|
|
|
*listbuf = 0;
|
|
nMods = nPotential = nTotal = 0;
|
|
|
|
pFiles0 = Sys_ListFiles( fs_homepath->string, NULL, NULL, &dummy, qtrue );
|
|
pFiles1 = Sys_ListFiles( fs_basepath->string, NULL, NULL, &dummy, qtrue );
|
|
// it is likely that we have duplicate names now, which we will cleanup below
|
|
pFiles = Sys_ConcatenateFileLists( pFiles0, pFiles1 );
|
|
nPotential = Sys_CountFileList(pFiles);
|
|
|
|
for ( i = 0 ; i < nPotential ; i++ ) {
|
|
name = pFiles[i];
|
|
// NOTE: cleaner would involve more changes
|
|
// ignore duplicate mod directories
|
|
if (i!=0) {
|
|
bDrop = qfalse;
|
|
for(j=0; j<i; j++)
|
|
{
|
|
if (Q_stricmp(pFiles[j],name)==0) {
|
|
// this one can be dropped
|
|
bDrop = qtrue;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (bDrop) {
|
|
continue;
|
|
}
|
|
// we drop "baseq3" "." and ".."
|
|
if (Q_stricmp(name, BASEGAME) && Q_stricmpn(name, ".", 1)) {
|
|
// now we need to find some .pk3 files to validate the mod
|
|
// NOTE TTimo: (actually I'm not sure why .. what if it's a mod under developement with no .pk3?)
|
|
// we didn't keep the information when we merged the directory names, as to what OS Path it was found under
|
|
// so it could be in base path, cd path or home path
|
|
// we will try each three of them here (yes, it's a bit messy)
|
|
path = FS_BuildOSPath( fs_basepath->string, name, "" );
|
|
nPaks = 0;
|
|
pPaks = Sys_ListFiles(path, ".pk3", NULL, &nPaks, qfalse);
|
|
Sys_FreeFileList( pPaks ); // we only use Sys_ListFiles to check wether .pk3 files are present
|
|
|
|
/* try on home path */
|
|
if ( nPaks <= 0 )
|
|
{
|
|
path = FS_BuildOSPath( fs_homepath->string, name, "" );
|
|
nPaks = 0;
|
|
pPaks = Sys_ListFiles( path, ".pk3", NULL, &nPaks, qfalse );
|
|
Sys_FreeFileList( pPaks );
|
|
}
|
|
|
|
if (nPaks > 0) {
|
|
nLen = strlen(name) + 1;
|
|
// nLen is the length of the mod path
|
|
// we need to see if there is a description available
|
|
descPath[0] = '\0';
|
|
Q_strncpyz(descPath, name, sizeof(descPath));
|
|
Q_strcat(descPath, sizeof(descPath), "/description.txt");
|
|
nDescLen = FS_SV_FOpenFileRead( descPath, &descHandle );
|
|
if ( nDescLen > 0 && descHandle) {
|
|
FILE *file;
|
|
file = FS_FileForHandle(descHandle);
|
|
Com_Memset( descPath, 0, sizeof( descPath ) );
|
|
nDescLen = fread(descPath, 1, 48, file);
|
|
if (nDescLen >= 0) {
|
|
descPath[nDescLen] = '\0';
|
|
}
|
|
FS_FCloseFile(descHandle);
|
|
} else {
|
|
Q_strncpyz(descPath, name, sizeof(descPath));
|
|
}
|
|
nDescLen = strlen(descPath) + 1;
|
|
|
|
if (nTotal + nLen + 1 + nDescLen + 1 < bufsize) {
|
|
strcpy(listbuf, name);
|
|
listbuf += nLen;
|
|
strcpy(listbuf, descPath);
|
|
listbuf += nDescLen;
|
|
nTotal += nLen + nDescLen;
|
|
nMods++;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Sys_FreeFileList( pFiles );
|
|
|
|
return nMods;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
|
|
static void FS_Dir_f()
|
|
{
|
|
char **dirnames;
|
|
int ndirs;
|
|
int i;
|
|
|
|
if ( Cmd_Argc() < 2 || Cmd_Argc() > 3 ) {
|
|
Com_Printf( "usage: dir <directory> [extension]\n" );
|
|
return;
|
|
}
|
|
|
|
const char* path = Cmd_Argv( 1 );
|
|
const char* extension = (Cmd_Argc() == 2) ? "" : Cmd_Argv( 2 );
|
|
|
|
Com_Printf( "Directory of %s %s\n", path, extension );
|
|
Com_Printf( "---------------\n" );
|
|
|
|
dirnames = FS_ListFiles( path, extension, &ndirs );
|
|
|
|
for ( i = 0; i < ndirs; i++ ) {
|
|
Com_Printf( "%s\n", dirnames[i] );
|
|
}
|
|
|
|
FS_FreeFileList( dirnames );
|
|
}
|
|
|
|
|
|
static void FS_ConvertPath( char *s )
|
|
{
|
|
while (*s) {
|
|
if ( *s == '\\' || *s == ':' ) {
|
|
*s = '/';
|
|
}
|
|
s++;
|
|
}
|
|
}
|
|
|
|
|
|
// ignore case and seprator char distinctions
|
|
|
|
int FS_PathCmp( const char *s1, const char *s2 )
|
|
{
|
|
int c1, c2;
|
|
|
|
do {
|
|
c1 = *s1++;
|
|
c2 = *s2++;
|
|
|
|
if (c1 >= 'a' && c1 <= 'z') {
|
|
c1 -= ('a' - 'A');
|
|
}
|
|
if (c2 >= 'a' && c2 <= 'z') {
|
|
c2 -= ('a' - 'A');
|
|
}
|
|
|
|
if ( c1 == '\\' || c1 == ':' ) {
|
|
c1 = '/';
|
|
}
|
|
if ( c2 == '\\' || c2 == ':' ) {
|
|
c2 = '/';
|
|
}
|
|
|
|
if (c1 < c2) {
|
|
return -1;
|
|
}
|
|
if (c1 > c2) {
|
|
return 1;
|
|
}
|
|
} while (c1);
|
|
|
|
return 0; // strings are equal
|
|
}
|
|
|
|
|
|
static void FS_SortFileList(char **filelist, int numfiles)
|
|
{
|
|
int i, j, k;
|
|
|
|
char** sortedlist = (char**)Z_Malloc( ( numfiles + 1 ) * sizeof( *sortedlist ) );
|
|
sortedlist[0] = NULL;
|
|
int numsortedfiles = 0;
|
|
|
|
for (i = 0; i < numfiles; i++) {
|
|
for (j = 0; j < numsortedfiles; j++) {
|
|
if (FS_PathCmp(filelist[i], sortedlist[j]) < 0) {
|
|
break;
|
|
}
|
|
}
|
|
for (k = numsortedfiles; k > j; k--) {
|
|
sortedlist[k] = sortedlist[k-1];
|
|
}
|
|
sortedlist[j] = filelist[i];
|
|
numsortedfiles++;
|
|
}
|
|
|
|
Com_Memcpy(filelist, sortedlist, numfiles * sizeof( *filelist ) );
|
|
Z_Free(sortedlist);
|
|
}
|
|
|
|
|
|
static void FS_NewDir_f()
|
|
{
|
|
char **dirnames;
|
|
int ndirs;
|
|
int i;
|
|
|
|
if ( Cmd_Argc() < 2 ) {
|
|
Com_Printf( "usage: fdir <filter>\n" );
|
|
Com_Printf( "example: fdir *q3dm*.bsp\n");
|
|
return;
|
|
}
|
|
|
|
Com_Printf( "---------------\n" );
|
|
|
|
dirnames = FS_ListFilteredFiles( "", "", Cmd_Argv(1), &ndirs, 0 );
|
|
|
|
FS_SortFileList(dirnames, ndirs);
|
|
|
|
for ( i = 0; i < ndirs; i++ ) {
|
|
FS_ConvertPath(dirnames[i]);
|
|
Com_Printf( "%s\n", dirnames[i] );
|
|
}
|
|
|
|
Com_Printf( "%d files listed\n", ndirs );
|
|
FS_FreeFileList( dirnames );
|
|
}
|
|
|
|
|
|
static void FS_Path_f()
|
|
{
|
|
int i;
|
|
const searchpath_t* s;
|
|
|
|
Com_Printf ("Current search path:\n");
|
|
for (s = fs_searchpaths; s; s = s->next) {
|
|
if (s->pack) {
|
|
if (!fs_numServerPaks || FS_PakIsPure(s->pack)) {
|
|
Com_Printf( "OK: %s (%i files)\n", s->pack->pakFilename, s->pack->numfiles);
|
|
} else {
|
|
Com_DPrintf( "IMPURE: %s (%i files)\n", s->pack->pakFilename, s->pack->numfiles);
|
|
}
|
|
} else {
|
|
Com_Printf( "%s/%s\n", s->dir->path, s->dir->gamedir );
|
|
}
|
|
}
|
|
|
|
Com_Printf( "\n" );
|
|
for ( i = 1 ; i < MAX_FILE_HANDLES ; i++ ) {
|
|
if ( fsh[i].handleFiles.file.o ) {
|
|
Com_Printf( "handle %i: %s\n", i, fsh[i].name );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//===========================================================================
|
|
|
|
|
|
static int QDECL paksort( const void *a, const void *b ) {
|
|
char *aa, *bb;
|
|
|
|
aa = *(char **)a;
|
|
bb = *(char **)b;
|
|
|
|
return FS_PathCmp( aa, bb );
|
|
}
|
|
|
|
/*
|
|
================
|
|
FS_AddGameDirectory
|
|
|
|
Sets fs_gamedir, adds the directory to the head of the path,
|
|
then loads the zip headers
|
|
================
|
|
*/
|
|
static void FS_AddGameDirectory( const char *path, const char *dir )
|
|
{
|
|
searchpath_t *sp;
|
|
int i;
|
|
pack_t *pak;
|
|
char *pakfile;
|
|
int numfiles;
|
|
char **pakfiles;
|
|
|
|
// this fixes the case where fs_basepath is the same as fs_cdpath, which happens on full installs
|
|
// KHB preserved despite removal of fs_cdpath, just in case base/home has the same problem on *nix
|
|
for ( sp = fs_searchpaths ; sp ; sp = sp->next ) {
|
|
if ( sp->dir && !Q_stricmp(sp->dir->path, path) && !Q_stricmp(sp->dir->gamedir, dir)) {
|
|
return; // we've already got this one
|
|
}
|
|
}
|
|
|
|
Q_strncpyz( fs_gamedir, dir, sizeof( fs_gamedir ) );
|
|
|
|
// add the directory to the search path
|
|
searchpath_t* search = Z_New<searchpath_t>();
|
|
search->dir = Z_New<directory_t>();
|
|
|
|
Q_strncpyz( search->dir->path, path, sizeof( search->dir->path ) );
|
|
Q_strncpyz( search->dir->gamedir, dir, sizeof( search->dir->gamedir ) );
|
|
search->next = fs_searchpaths;
|
|
fs_searchpaths = search;
|
|
|
|
// find all pak files in this directory
|
|
pakfile = FS_BuildOSPath( path, dir, "" );
|
|
pakfile[ strlen(pakfile) - 1 ] = 0; // strip the trailing slash
|
|
|
|
pakfiles = Sys_ListFiles( pakfile, ".pk3", NULL, &numfiles, qfalse );
|
|
|
|
// sort them so that later alphabetic matches override
|
|
// earlier ones. This makes pak1.pk3 override pak0.pk3
|
|
if ( numfiles > MAX_PAKFILES ) {
|
|
numfiles = MAX_PAKFILES;
|
|
}
|
|
|
|
char *sorted[MAX_PAKFILES];
|
|
for ( i = 0 ; i < numfiles ; i++ ) {
|
|
sorted[i] = pakfiles[i];
|
|
}
|
|
|
|
qsort( sorted, numfiles, sizeof(char*), paksort );
|
|
|
|
for ( i = 0 ; i < numfiles ; i++ ) {
|
|
pakfile = FS_BuildOSPath( path, dir, sorted[i] );
|
|
if ( ( pak = FS_LoadZipFile( pakfile, sorted[i] ) ) == 0 )
|
|
continue;
|
|
// store the game name for downloading
|
|
strcpy(pak->pakGamename, dir);
|
|
|
|
search = Z_New<searchpath_t>();
|
|
search->pack = pak;
|
|
search->next = fs_searchpaths;
|
|
fs_searchpaths = search;
|
|
}
|
|
|
|
// done
|
|
Sys_FreeFileList( pakfiles );
|
|
}
|
|
|
|
|
|
qbool FS_idPak( const char* pak, const char* base )
|
|
{
|
|
for (int i = 0; i < NUM_ID_PAKS; ++i) {
|
|
if ( !FS_FilenameCompare(pak, va("%s/pak%d", base, i)) ) {
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
|
|
// check whether the string contains stuff like "../"
|
|
// to prevent directory traversal bugs
|
|
|
|
qbool FS_CheckDirTraversal(const char *checkdir)
|
|
{
|
|
return (strstr(checkdir, "../") || strstr(checkdir, "..\\"));
|
|
}
|
|
|
|
|
|
/*
|
|
dlstring == qtrue
|
|
|
|
Returns a list of pak files that we should download from the server. They all get stored
|
|
in the current gamedir and an FS_Restart will be fired up after we download them all.
|
|
|
|
The string is the format:
|
|
|
|
@remotename@localname [repeat]
|
|
|
|
static int fs_numServerReferencedPaks;
|
|
static int fs_serverReferencedPaks[MAX_SEARCH_PATHS];
|
|
static char *fs_serverReferencedPakNames[MAX_SEARCH_PATHS];
|
|
|
|
----------------
|
|
dlstring == qfalse
|
|
|
|
we are not interested in a download string format, we want something human-readable
|
|
(this is used for diagnostics while connecting to a pure server)
|
|
|
|
================
|
|
*/
|
|
qbool FS_ComparePaks( char *neededpaks, int len, qbool dlstring ) {
|
|
searchpath_t *sp;
|
|
qbool havepak, badchecksum;
|
|
char *origpos = neededpaks;
|
|
int i;
|
|
|
|
if (!fs_numServerReferencedPaks)
|
|
return qfalse; // Server didn't send any pack information along
|
|
|
|
*neededpaks = 0;
|
|
|
|
for ( i = 0 ; i < fs_numServerReferencedPaks ; i++ )
|
|
{
|
|
// Ok, see if we have this pak file
|
|
badchecksum = qfalse;
|
|
havepak = qfalse;
|
|
|
|
// never autodownload any of the id paks
|
|
if ( FS_idPak(fs_serverReferencedPakNames[i], BASEGAME) || FS_idPak(fs_serverReferencedPakNames[i], "missionpack") ) {
|
|
continue;
|
|
}
|
|
|
|
// Make sure the server cannot make us write to non-quake3 directories.
|
|
if(FS_CheckDirTraversal(fs_serverReferencedPakNames[i]))
|
|
{
|
|
Com_Printf("WARNING: Invalid download name %s\n", fs_serverReferencedPakNames[i]);
|
|
continue;
|
|
}
|
|
|
|
for ( sp = fs_searchpaths ; sp ; sp = sp->next ) {
|
|
if ( sp->pack && sp->pack->checksum == fs_serverReferencedPaks[i] ) {
|
|
havepak = qtrue; // This is it!
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !havepak && fs_serverReferencedPakNames[i] && *fs_serverReferencedPakNames[i] ) {
|
|
// Don't got it
|
|
|
|
if (dlstring)
|
|
{
|
|
// We need this to make sure we won't hit the end of the buffer or the server could
|
|
// overwrite non-pk3 files on clients by writing so much crap into neededpaks that
|
|
// Q_strcat cuts off the .pk3 extension.
|
|
|
|
origpos += strlen(origpos);
|
|
|
|
// Remote name
|
|
Q_strcat( neededpaks, len, "@");
|
|
Q_strcat( neededpaks, len, fs_serverReferencedPakNames[i] );
|
|
Q_strcat( neededpaks, len, ".pk3" );
|
|
|
|
// Local name
|
|
Q_strcat( neededpaks, len, "@");
|
|
// Do we have one with the same name?
|
|
if ( FS_SV_FileExists( va( "%s.pk3", fs_serverReferencedPakNames[i] ) ) )
|
|
{
|
|
char st[MAX_ZPATH];
|
|
// We already have one called this, we need to download it to another name
|
|
// Make something up with the checksum in it
|
|
Com_sprintf( st, sizeof( st ), "%s.%08x.pk3", fs_serverReferencedPakNames[i], fs_serverReferencedPaks[i] );
|
|
Q_strcat( neededpaks, len, st );
|
|
} else
|
|
{
|
|
Q_strcat( neededpaks, len, fs_serverReferencedPakNames[i] );
|
|
Q_strcat( neededpaks, len, ".pk3" );
|
|
}
|
|
|
|
// Find out whether it might have overflowed the buffer and don't add this file to the
|
|
// list if that is the case.
|
|
if(strlen(origpos) + (origpos - neededpaks) >= len - 1)
|
|
{
|
|
*origpos = '\0';
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Q_strcat( neededpaks, len, fs_serverReferencedPakNames[i] );
|
|
Q_strcat( neededpaks, len, ".pk3" );
|
|
// Do we have one with the same name?
|
|
if ( FS_SV_FileExists( va( "%s.pk3", fs_serverReferencedPakNames[i] ) ) )
|
|
{
|
|
Q_strcat( neededpaks, len, " (local file exists with wrong checksum)");
|
|
}
|
|
Q_strcat( neededpaks, len, "\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( *neededpaks ) {
|
|
return qtrue;
|
|
}
|
|
|
|
return qfalse; // We have them all
|
|
}
|
|
|
|
|
|
void FS_MissingPaks( unsigned int* checksums, int* checksumCount, int maxChecksums )
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < fs_numServerReferencedPaks; i++) {
|
|
// ignore official paks
|
|
if (FS_idPak(fs_serverReferencedPakNames[i], BASEGAME))
|
|
continue;
|
|
|
|
qbool gotIt = qfalse;
|
|
for (searchpath_t* sp = fs_searchpaths; sp; sp = sp->next) {
|
|
if (sp->pack && sp->pack->checksum == fs_serverReferencedPaks[i]) {
|
|
gotIt = qtrue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (gotIt)
|
|
continue;
|
|
|
|
checksums[count++] = fs_serverReferencedPaks[i];
|
|
|
|
if (count == maxChecksums) {
|
|
Com_Printf("^3WARNING: FS_MissingPaks has too many checksums for the array given (over %d)\n", maxChecksums);
|
|
break;
|
|
}
|
|
}
|
|
|
|
*checksumCount = count;
|
|
}
|
|
|
|
|
|
qbool FS_PakExists( unsigned int checksum )
|
|
{
|
|
for (searchpath_t* sp = fs_searchpaths; sp; sp = sp->next) {
|
|
if (sp->pack && sp->pack->checksum == checksum)
|
|
return qtrue;
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
static void FS_Restart_f()
|
|
{
|
|
FS_Restart(fs_checksumFeed);
|
|
}
|
|
|
|
|
|
static const cmdTableItem_t fs_cmds[] =
|
|
{
|
|
{ "path", FS_Path_f, NULL, "prints info about the current search path" },
|
|
{ "dir", FS_Dir_f, NULL, "prints an extension-filtered file list" },
|
|
{ "fdir", FS_NewDir_f, NULL, "prints a pattern-filtered file list" },
|
|
{ "fs_restart", FS_Restart_f, NULL, "restarts the file system" }
|
|
};
|
|
|
|
|
|
/*
|
|
================
|
|
FS_Shutdown
|
|
|
|
Frees all resources and closes all files
|
|
================
|
|
*/
|
|
void FS_Shutdown( qbool closemfp ) {
|
|
searchpath_t *p, *next;
|
|
int i;
|
|
|
|
for(i = 0; i < MAX_FILE_HANDLES; i++) {
|
|
if (fsh[i].fileSize) {
|
|
FS_FCloseFile(i);
|
|
}
|
|
}
|
|
|
|
// free everything
|
|
for ( p = fs_searchpaths ; p ; p = next ) {
|
|
next = p->next;
|
|
|
|
if ( p->pack ) {
|
|
unzClose(p->pack->handle);
|
|
Z_Free( p->pack->buildBuffer );
|
|
Z_Free( p->pack );
|
|
}
|
|
if ( p->dir ) {
|
|
Z_Free( p->dir );
|
|
}
|
|
Z_Free( p );
|
|
}
|
|
|
|
// any FS_ calls will now be an error until reinitialized
|
|
fs_searchpaths = NULL;
|
|
|
|
Cmd_UnregisterArray( fs_cmds );
|
|
|
|
#ifdef FS_MISSING
|
|
if (closemfp) {
|
|
fclose(missingFiles);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Com_AppendCDKey( const char *filename );
|
|
void Com_ReadCDKey( const char *filename );
|
|
|
|
/*
|
|
NOTE TTimo: the reordering that happens here is not reflected in the cvars (\cvarlist *pak*)
|
|
this can lead to misleading situations, see https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=540
|
|
*/
|
|
static void FS_ReorderPurePaks()
|
|
{
|
|
searchpath_t *s;
|
|
int i;
|
|
searchpath_t **p_insert_index, // for linked list reordering
|
|
**p_previous; // when doing the scan
|
|
|
|
// only relevant when connected to pure server
|
|
if ( !fs_numServerPaks )
|
|
return;
|
|
|
|
fs_reordered = qfalse;
|
|
|
|
p_insert_index = &fs_searchpaths; // we insert in order at the beginning of the list
|
|
for ( i = 0 ; i < fs_numServerPaks ; i++ ) {
|
|
p_previous = p_insert_index; // track the pointer-to-current-item
|
|
for (s = *p_insert_index; s; s = s->next) {
|
|
// the part of the list before p_insert_index has been sorted already
|
|
if (s->pack && fs_serverPaks[i] == s->pack->checksum) {
|
|
fs_reordered = qtrue;
|
|
// move this element to the insert list
|
|
*p_previous = s->next;
|
|
s->next = *p_insert_index;
|
|
*p_insert_index = s;
|
|
// increment insert list
|
|
p_insert_index = &s->next;
|
|
break; // iterate to next server pack
|
|
}
|
|
p_previous = &s->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void FS_Startup( const char *gameName )
|
|
{
|
|
QSUBSYSTEM_INIT_START( "FileSystem" );
|
|
|
|
//each map load increment 'fs_packFiles'!
|
|
//start cnq3: 30298 files in pk3 files
|
|
//next map: 60596 files in pk3 files
|
|
//next map: 90894 files in pk3 files
|
|
//etc
|
|
fs_packFiles = 0;
|
|
|
|
fs_debug = Cvar_Get( "fs_debug", "0", 0 );
|
|
Cvar_SetRange( "fs_debug", CVART_BOOL, NULL, NULL );
|
|
Cvar_SetHelp( "fs_debug", "prints file open/write accesses" );
|
|
fs_basepath = Cvar_Get ("fs_basepath", Sys_Cwd(), CVAR_INIT );
|
|
fs_basegame = Cvar_Get ("fs_basegame", "", CVAR_INIT );
|
|
fs_gamedirvar = Cvar_Get ("fs_game", APEXGAME, CVAR_INIT | CVAR_SYSTEMINFO );
|
|
Cvar_SetHelp( "fs_game", "name of the mod folder" );
|
|
const char* homePath = Sys_DefaultHomePath();
|
|
if (!homePath || !homePath[0])
|
|
homePath = fs_basepath->string;
|
|
fs_homepath = Cvar_Get ("fs_homepath", homePath, CVAR_INIT );
|
|
|
|
// add search path elements in reverse priority order
|
|
if (fs_basepath->string[0]) {
|
|
FS_AddGameDirectory( fs_basepath->string, gameName );
|
|
}
|
|
// fs_homepath is somewhat particular to *nix systems, only add if relevant
|
|
// NOTE: same filtering below for mods and basegame
|
|
if (fs_basepath->string[0] && Q_stricmp(fs_homepath->string,fs_basepath->string)) {
|
|
FS_AddGameDirectory ( fs_homepath->string, gameName );
|
|
}
|
|
|
|
// check for additional base game so mods can be based upon other mods
|
|
if ( fs_basegame->string[0] && !Q_stricmp( gameName, BASEGAME ) && Q_stricmp( fs_basegame->string, gameName ) ) {
|
|
if (fs_basepath->string[0]) {
|
|
FS_AddGameDirectory(fs_basepath->string, fs_basegame->string);
|
|
}
|
|
if (fs_homepath->string[0] && Q_stricmp(fs_homepath->string,fs_basepath->string)) {
|
|
FS_AddGameDirectory(fs_homepath->string, fs_basegame->string);
|
|
}
|
|
}
|
|
|
|
// check for additional game folder for mods
|
|
if ( fs_gamedirvar->string[0] && !Q_stricmp( gameName, BASEGAME ) && Q_stricmp( fs_gamedirvar->string, gameName ) ) {
|
|
if (fs_basepath->string[0]) {
|
|
FS_AddGameDirectory(fs_basepath->string, fs_gamedirvar->string);
|
|
}
|
|
if (fs_homepath->string[0] && Q_stricmp(fs_homepath->string,fs_basepath->string)) {
|
|
FS_AddGameDirectory(fs_homepath->string, fs_gamedirvar->string);
|
|
}
|
|
}
|
|
|
|
Com_ReadCDKey(BASEGAME);
|
|
if (fs_gamedirvar && fs_gamedirvar->string[0] != 0) {
|
|
Com_AppendCDKey( fs_gamedirvar->string );
|
|
}
|
|
|
|
// add our commands
|
|
Cmd_RegisterArray( fs_cmds, MODULE_COMMON );
|
|
|
|
// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=506
|
|
// reorder the pure pk3 files according to server order
|
|
FS_ReorderPurePaks();
|
|
|
|
fs_gamedirvar->modified = qfalse; // We just loaded, it's not modified
|
|
|
|
#ifdef FS_MISSING
|
|
if (missingFiles == NULL) {
|
|
missingFiles = fopen( "\\missing.txt", "ab" );
|
|
}
|
|
#endif
|
|
|
|
Com_Printf( "%d files in pk3 files\n", fs_packFiles );
|
|
QSUBSYSTEM_INIT_DONE( "FileSystem" );
|
|
}
|
|
|
|
|
|
#define DEFAULT_PAK_COUNT 9
|
|
|
|
static const int DefaultPakChecksums[DEFAULT_PAK_COUNT] = {
|
|
1566731103,
|
|
298122907,
|
|
412165236,
|
|
-1303471980,
|
|
1197932710,
|
|
-207895723,
|
|
-585902437,
|
|
908855077,
|
|
977125798
|
|
};
|
|
|
|
|
|
/*
|
|
===================
|
|
FS_CheckPaks
|
|
|
|
Checks that pak0.pk3 to pakN.pk3 are present and
|
|
their checksums correct, N being DEFAULT_PAK_COUNT
|
|
Note: If you're building a game that doesn't depend on the
|
|
Q3 media pak0-N.pk3, you'll want to remove this function
|
|
===================
|
|
*/
|
|
static void FS_CheckPaks()
|
|
{
|
|
enum defaultPakStatus_t {
|
|
DPS_FOUND = 1,
|
|
DPS_VALID = 2
|
|
};
|
|
|
|
int status[DEFAULT_PAK_COUNT] = { 0 };
|
|
int badChecksums[DEFAULT_PAK_COUNT] = { 0 };
|
|
int found = 0;
|
|
|
|
for( searchpath_t* path = fs_searchpaths; path; path = path->next ) {
|
|
for ( int i = 0; i < DEFAULT_PAK_COUNT; ++i ) {
|
|
if ( status[i] & DPS_FOUND )
|
|
continue;
|
|
|
|
if ( path->pack &&
|
|
!Q_stricmpn(path->pack->pakBasename, va("pak%d", i), MAX_OSPATH) &&
|
|
!Q_stricmpn(path->pack->pakGamename, BASEGAME, MAX_OSPATH) ) {
|
|
++found;
|
|
status[i] |= DPS_FOUND;
|
|
if ( path->pack->checksum == DefaultPakChecksums[i] )
|
|
status[i] |= DPS_VALID;
|
|
else
|
|
badChecksums[i] = path->pack->checksum;
|
|
}
|
|
}
|
|
|
|
if ( found == DEFAULT_PAK_COUNT )
|
|
break;
|
|
}
|
|
|
|
if ( found < DEFAULT_PAK_COUNT ) {
|
|
int firstMissing = 0;
|
|
for ( int i = 0; i < DEFAULT_PAK_COUNT; ++i ) {
|
|
if ( !(status[i] & DPS_FOUND) ) {
|
|
firstMissing = i;
|
|
break;
|
|
}
|
|
}
|
|
Com_Error( ERR_FATAL, "pak%d.pk3 is missing or unreadable! Check that CNQ3 is in the correct place "
|
|
"and that every file in the %s directory is present and readable.", firstMissing, BASEGAME );
|
|
return;
|
|
}
|
|
|
|
for ( int i = 0; i < DEFAULT_PAK_COUNT; ++i ) {
|
|
if ( !(status[i] & DPS_VALID) ) {
|
|
Com_Error( ERR_FATAL, "pak%d.pk3 is corrupt! Please re-copy pak%d.pk3 "
|
|
"from your legitimate Q3 CD-ROM. Got checksum %X, expected %X.",
|
|
i, i, (unsigned int)badChecksums[i], (unsigned int)DefaultPakChecksums[i] );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
FS_LoadedPakChecksums
|
|
|
|
Returns a space separated string containing the checksums of all loaded pk3 files.
|
|
Servers with sv_pure set will get this string and pass it to clients.
|
|
=====================
|
|
*/
|
|
const char *FS_LoadedPakChecksums( void ) {
|
|
static char info[BIG_INFO_STRING];
|
|
searchpath_t *search;
|
|
|
|
info[0] = 0;
|
|
|
|
for ( search = fs_searchpaths ; search ; search = search->next ) {
|
|
// is the element a pak file?
|
|
if ( !search->pack ) {
|
|
continue;
|
|
}
|
|
|
|
Q_strcat( info, sizeof( info ), va("%i ", search->pack->checksum ) );
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
FS_LoadedPakPureChecksums
|
|
|
|
Returns a space separated string containing the pure checksums of all loaded pk3 files.
|
|
Servers with sv_pure use these checksums to compare with the checksums the clients send
|
|
back to the server.
|
|
=====================
|
|
*/
|
|
const char *FS_LoadedPakPureChecksums( void ) {
|
|
static char info[BIG_INFO_STRING];
|
|
searchpath_t *search;
|
|
|
|
info[0] = 0;
|
|
|
|
for ( search = fs_searchpaths ; search ; search = search->next ) {
|
|
// is the element a pak file?
|
|
if ( !search->pack ) {
|
|
continue;
|
|
}
|
|
|
|
Q_strcat( info, sizeof( info ), va("%i ", search->pack->pure_checksum ) );
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
FS_ReferencedPakChecksums
|
|
|
|
Returns a space separated string containing the checksums of all referenced pk3 files.
|
|
The server will send this to the clients so they can check which files should be auto-downloaded.
|
|
=====================
|
|
*/
|
|
const char *FS_ReferencedPakChecksums( void ) {
|
|
static char info[BIG_INFO_STRING];
|
|
searchpath_t *search;
|
|
|
|
info[0] = 0;
|
|
|
|
for ( search = fs_searchpaths ; search ; search = search->next ) {
|
|
// is the element a pak file?
|
|
if ( search->pack ) {
|
|
if (search->pack->referenced || Q_stricmpn(search->pack->pakGamename, BASEGAME, strlen(BASEGAME))) {
|
|
Q_strcat( info, sizeof( info ), va("%i ", search->pack->checksum ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
|
|
/*
|
|
returns a space-separated string containing the pure checksums of all referenced pk3 files
|
|
servers with sv_pure set will get this string back from clients for pure validation
|
|
the string has a specific order, "cgame ui @ ref1 ref2 ref3 ..."
|
|
*/
|
|
const char* FS_ReferencedPakPureChecksums()
|
|
{
|
|
static char info[BIG_INFO_STRING];
|
|
info[0] = 0;
|
|
|
|
int numPaks = 0;
|
|
int checksum = fs_checksumFeed;
|
|
|
|
for (int nFlags = FS_CGAME_REF; nFlags; nFlags = nFlags >> 1) {
|
|
if (nFlags & FS_GENERAL_REF) {
|
|
// add a delimter between must haves and general refs
|
|
Q_strcat(info, sizeof(info), "@ ");
|
|
}
|
|
for ( const searchpath_t* search = fs_searchpaths; search; search = search->next ) {
|
|
// is the element a pak file and has it been referenced based on flag?
|
|
if ( search->pack && (search->pack->referenced & nFlags)) {
|
|
Q_strcat( info, sizeof( info ), va("%i ", search->pack->pure_checksum) );
|
|
if (nFlags & (FS_CGAME_REF | FS_UI_REF)) {
|
|
break;
|
|
}
|
|
checksum ^= search->pack->pure_checksum;
|
|
numPaks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// last checksum is the encoded number of referenced pk3s
|
|
checksum ^= numPaks;
|
|
Q_strcat( info, sizeof( info ), va("%i ", checksum ) );
|
|
|
|
return info;
|
|
}
|
|
|
|
|
|
/*
|
|
returns a space separated string containing the names of all referenced pk3 files
|
|
the server will send this to the clients so they can check which files should be auto-downloaded
|
|
*/
|
|
const char* FS_ReferencedPakNames()
|
|
{
|
|
static char info[BIG_INFO_STRING];
|
|
info[0] = 0;
|
|
|
|
// we want to return ALL pk3's from the fs_game path
|
|
// and referenced ones from baseq3
|
|
for ( const searchpath_t* search = fs_searchpaths; search; search = search->next ) {
|
|
// is the element a pak file?
|
|
if ( search->pack ) {
|
|
if (search->pack->referenced || Q_stricmpn(search->pack->pakGamename, BASEGAME, strlen(BASEGAME))) {
|
|
if (*info)
|
|
Q_strcat(info, sizeof( info ), " " );
|
|
Q_strcat( info, sizeof( info ), search->pack->pakGamename );
|
|
Q_strcat( info, sizeof( info ), "/" );
|
|
Q_strcat( info, sizeof( info ), search->pack->pakBasename );
|
|
}
|
|
}
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
FS_ClearPakReferences
|
|
=====================
|
|
*/
|
|
void FS_ClearPakReferences( int flags ) {
|
|
searchpath_t *search;
|
|
|
|
if ( !flags ) {
|
|
flags = -1;
|
|
}
|
|
for ( search = fs_searchpaths; search; search = search->next ) {
|
|
// is the element a pak file and has it been referenced?
|
|
if ( search->pack ) {
|
|
search->pack->referenced &= ~flags;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
FS_PureServerSetLoadedPaks
|
|
|
|
If the string is empty, all data sources will be allowed.
|
|
If not empty, only pk3 files that match one of the space
|
|
separated checksums will be checked for files, with the
|
|
exception of .cfg and .dat files.
|
|
=====================
|
|
*/
|
|
void FS_PureServerSetLoadedPaks( const char *pakSums ) {
|
|
Cmd_TokenizeString( pakSums );
|
|
|
|
int c = Cmd_Argc();
|
|
if ( c > MAX_SEARCH_PATHS ) {
|
|
c = MAX_SEARCH_PATHS;
|
|
}
|
|
|
|
fs_numServerPaks = c;
|
|
|
|
for ( int i = 0 ; i < c ; i++ ) {
|
|
fs_serverPaks[i] = atoi( Cmd_Argv( i ) );
|
|
}
|
|
|
|
if (fs_numServerPaks) {
|
|
Com_DPrintf( "Connected to a pure server.\n" );
|
|
}
|
|
else
|
|
{
|
|
if (fs_reordered)
|
|
{
|
|
// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=540
|
|
// force a restart to make sure the search order will be correct
|
|
Com_DPrintf( "FS search reorder is required\n" );
|
|
FS_Restart(fs_checksumFeed);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
FS_PureServerSetReferencedPaks
|
|
|
|
The checksums and names of the pk3 files referenced at the server
|
|
are sent to the client and stored here. The client will use these
|
|
checksums to see if any pk3 files need to be auto-downloaded.
|
|
=====================
|
|
*/
|
|
void FS_PureServerSetReferencedPaks( const char *pakSums, const char *pakNames ) {
|
|
int i, c, d = 0;
|
|
|
|
Cmd_TokenizeString( pakSums );
|
|
|
|
c = Cmd_Argc();
|
|
if ( c > MAX_SEARCH_PATHS ) {
|
|
c = MAX_SEARCH_PATHS;
|
|
}
|
|
|
|
for ( i = 0 ; i < c ; i++ ) {
|
|
fs_serverReferencedPaks[i] = atoi( Cmd_Argv( i ) );
|
|
}
|
|
|
|
for (i = 0 ; i < sizeof(fs_serverReferencedPakNames) / sizeof(*fs_serverReferencedPakNames); i++)
|
|
{
|
|
if(fs_serverReferencedPakNames[i])
|
|
Z_Free(fs_serverReferencedPakNames[i]);
|
|
|
|
fs_serverReferencedPakNames[i] = NULL;
|
|
}
|
|
|
|
if ( pakNames && *pakNames ) {
|
|
Cmd_TokenizeString( pakNames );
|
|
|
|
d = Cmd_Argc();
|
|
|
|
if(d > c)
|
|
d = c;
|
|
|
|
for ( i = 0 ; i < d ; i++ ) {
|
|
fs_serverReferencedPakNames[i] = CopyString( Cmd_Argv( i ) );
|
|
}
|
|
}
|
|
|
|
// ensure that there are as many checksums as there are pak names.
|
|
if(d < c)
|
|
c = d;
|
|
|
|
fs_numServerReferencedPaks = c;
|
|
}
|
|
|
|
|
|
// called only at initial startup, not when the filesystem is resetting due to a game change
|
|
|
|
void FS_InitFilesystem()
|
|
{
|
|
Com_Printf( "Working directory: %s\n", Sys_Cwd() );
|
|
|
|
// allow command line parms to override our defaults
|
|
// we have to handle this specially, because normal command
|
|
// line variable sets don't happen until after the filesystem
|
|
// has already been initialized
|
|
Com_StartupVariable( "fs_basepath" );
|
|
Com_StartupVariable( "fs_homepath" );
|
|
Com_StartupVariable( "fs_game" );
|
|
|
|
// try to start up normally
|
|
FS_Startup( BASEGAME );
|
|
|
|
FS_CheckPaks();
|
|
|
|
// if we can't find default.cfg, assume that the paths are busted
|
|
// and error out now, rather than getting an unreadable
|
|
// graphics screen when the font fails to load
|
|
if ( FS_ReadFile( "default.cfg", NULL ) <= 0 )
|
|
Com_Error( ERR_FATAL, "Couldn't load default.cfg" );
|
|
|
|
Q_strncpyz( lastValidBase, fs_basepath->string, sizeof(lastValidBase) );
|
|
Q_strncpyz( lastValidGame, fs_gamedirvar->string, sizeof(lastValidGame) );
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
FS_Restart
|
|
================
|
|
*/
|
|
void FS_Restart( int checksumFeed ) {
|
|
|
|
// free anything we currently have loaded
|
|
FS_Shutdown(qfalse);
|
|
|
|
// set the checksum feed
|
|
fs_checksumFeed = checksumFeed;
|
|
|
|
// clear pak references
|
|
FS_ClearPakReferences(0);
|
|
|
|
// try to start up normally
|
|
FS_Startup( BASEGAME );
|
|
|
|
FS_CheckPaks();
|
|
|
|
// if we can't find default.cfg, assume that the paths are
|
|
// busted and error out now, rather than getting an unreadable
|
|
// graphics screen when the font fails to load
|
|
if ( FS_ReadFile( "default.cfg", NULL ) <= 0 ) {
|
|
// this might happen when connecting to a pure server not using BASEGAME/pak0.pk3
|
|
// (for instance a TA demo server)
|
|
if (lastValidBase[0]) {
|
|
FS_PureServerSetLoadedPaks("");
|
|
Cvar_Set("fs_basepath", lastValidBase);
|
|
Cvar_Set("fs_gamedirvar", lastValidGame);
|
|
lastValidBase[0] = '\0';
|
|
lastValidGame[0] = '\0';
|
|
FS_Restart(checksumFeed);
|
|
Com_Error( ERR_DROP, "Invalid game folder\n" );
|
|
return;
|
|
}
|
|
Com_Error( ERR_FATAL, "Couldn't load default.cfg" );
|
|
}
|
|
|
|
// bk010116 - new check before safeMode
|
|
if ( Q_stricmp(fs_gamedirvar->string, lastValidGame) ) {
|
|
// skip the q3config.cfg if "safe" is on the command line
|
|
if ( !Com_SafeMode() ) {
|
|
Cbuf_AddText ("exec q3config.cfg\n");
|
|
}
|
|
}
|
|
|
|
Q_strncpyz(lastValidBase, fs_basepath->string, sizeof(lastValidBase));
|
|
Q_strncpyz(lastValidGame, fs_gamedirvar->string, sizeof(lastValidGame));
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
FS_ConditionalRestart
|
|
restart if necessary
|
|
=================
|
|
*/
|
|
void FS_ConditionalRestart( int checksumFeed ) {
|
|
if( fs_gamedirvar->modified || checksumFeed != fs_checksumFeed ) {
|
|
FS_Restart( checksumFeed );
|
|
} else if ( fs_numServerPaks && !fs_reordered ) {
|
|
FS_ReorderPurePaks();
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================================================================================
|
|
|
|
Handle based file calls for virtual machines
|
|
|
|
========================================================================================
|
|
*/
|
|
|
|
int FS_FOpenFileByMode( const char *qpath, fileHandle_t *f, fsMode_t mode ) {
|
|
int r;
|
|
qbool sync;
|
|
|
|
sync = qfalse;
|
|
|
|
switch( mode ) {
|
|
case FS_READ:
|
|
r = FS_FOpenFileRead( qpath, f, qtrue );
|
|
break;
|
|
case FS_WRITE:
|
|
*f = FS_FOpenFileWrite( qpath );
|
|
r = 0;
|
|
if (*f == 0) {
|
|
r = -1;
|
|
}
|
|
break;
|
|
case FS_APPEND_SYNC:
|
|
sync = qtrue;
|
|
case FS_APPEND:
|
|
*f = FS_FOpenFileAppend( qpath );
|
|
r = 0;
|
|
if (*f == 0) {
|
|
r = -1;
|
|
}
|
|
break;
|
|
default:
|
|
Com_Error( ERR_FATAL, "FS_FOpenFileByMode: bad mode" );
|
|
return -1;
|
|
}
|
|
|
|
if (!f) {
|
|
return r;
|
|
}
|
|
|
|
if ( *f ) {
|
|
if (fsh[*f].zipFile == qtrue) {
|
|
fsh[*f].baseOffset = unztell(fsh[*f].handleFiles.file.z);
|
|
} else {
|
|
fsh[*f].baseOffset = ftell(fsh[*f].handleFiles.file.o);
|
|
}
|
|
fsh[*f].fileSize = r;
|
|
}
|
|
fsh[*f].handleSync = sync;
|
|
|
|
return r;
|
|
}
|
|
|
|
int FS_FTell( fileHandle_t f ) {
|
|
int pos;
|
|
if (fsh[f].zipFile == qtrue) {
|
|
pos = unztell(fsh[f].handleFiles.file.z);
|
|
} else {
|
|
pos = ftell(fsh[f].handleFiles.file.o);
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
void FS_Flush( fileHandle_t f ) {
|
|
fflush(fsh[f].handleFiles.file.o);
|
|
}
|
|
|
|
void FS_FilenameCompletion( const char *dir, const char *ext, qbool stripExt,
|
|
void(*callback)(const char *s), int filters ) {
|
|
char **filenames;
|
|
int nfiles;
|
|
int i;
|
|
char filename[ MAX_STRING_CHARS ];
|
|
|
|
filenames = FS_ListFilteredFiles( dir, ext, NULL, &nfiles, filters );
|
|
|
|
FS_SortFileList( filenames, nfiles );
|
|
|
|
for( i = 0; i < nfiles; i++ ) {
|
|
FS_ConvertPath( filenames[ i ] );
|
|
Q_strncpyz( filename, filenames[ i ], MAX_STRING_CHARS );
|
|
|
|
if( stripExt ) {
|
|
COM_StripExtension(filename, filename, sizeof(filename));
|
|
}
|
|
|
|
callback( filename );
|
|
}
|
|
FS_FreeFileList( filenames );
|
|
}
|