Initial commit.
This commit is contained in:
605
code/qcommon/files_common.cpp
Normal file
605
code/qcommon/files_common.cpp
Normal file
@@ -0,0 +1,605 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* name: files.c
|
||||
*
|
||||
* desc: handle based filesystem for Quake III Arena
|
||||
*
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#include "../game/q_shared.h"
|
||||
#include "qcommon.h"
|
||||
#include "files.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_QPATH is 64 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 relative to the base path, so base path should usually be writable.
|
||||
|
||||
The "cd path" is the path to an alternate hierarchy that will be searched if a file
|
||||
is not located in the base path. A user can do a partial install that copies some
|
||||
data to a base path created on their hard drive and leave the rest on the cd. Files
|
||||
are never writen to the cd path. It defaults to a value set by the installer, like
|
||||
"e:\quake3", but it can be overridden with "+set ds_cdpath g:\quake3".
|
||||
|
||||
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 "base game" is the directory under the paths where data comes from by default, and
|
||||
can be either "base" or "demo".
|
||||
|
||||
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 "asset0.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.
|
||||
|
||||
After the paths are initialized, quake will look for the product.txt file. If not
|
||||
found and verified, the game will run in restricted mode. In restricted mode, only
|
||||
files contained in demo/asset0.pk3 will be available for loading, and only if the zip header is
|
||||
verified to not have been modified. A single exception is made for jaconfig.cfg. Files
|
||||
can still be written out in restricted mode, so screenshots and demos are allowed.
|
||||
Restricted mode can be tested by setting "+set fs_restrict 1" on the command line, even
|
||||
if there is a valid product.txt under the basepath or cdpath.
|
||||
|
||||
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.
|
||||
|
||||
If the "fs_copyfiles" cvar is set to 1, then every time a file is sourced from the cd
|
||||
path, it will be copied over to the base path. This is a development aid to help build
|
||||
test releases and to copy working sets over slow network links.
|
||||
(If set to 2, copying will only take place if the two filetimes are NOT EQUAL)
|
||||
|
||||
|
||||
The qpath "sound/newstuff/test.wav" would be searched for in the following places:
|
||||
|
||||
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
|
||||
base path + base game's zip files
|
||||
base path + base game's directory
|
||||
cd path + base game's zip files
|
||||
cd path + base game's directory
|
||||
server download, to be written to base 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 jaconfig.cfg isn't present in it,
|
||||
or configs will never get loaded from disk!
|
||||
|
||||
todo:
|
||||
|
||||
downloading (outside fs?)
|
||||
game directory passing and restarting
|
||||
|
||||
=============================================================================
|
||||
|
||||
*/
|
||||
|
||||
// if this is defined, the executable positively won't work with any paks other
|
||||
// than the demo pak, even if productid is present. This is only used for our
|
||||
// last demo release to prevent the mac and linux users from using the demo
|
||||
// executable with the production windows pak before the mac/linux products
|
||||
// hit the shelves a little later
|
||||
//#define PRE_RELEASE_DEMO
|
||||
|
||||
|
||||
char fs_gamedir[MAX_OSPATH]; // this will be a single file name with no separators
|
||||
cvar_t *fs_debug;
|
||||
cvar_t *fs_basepath;
|
||||
cvar_t *fs_cdpath;
|
||||
cvar_t *fs_copyfiles;
|
||||
cvar_t *fs_gamedirvar;
|
||||
cvar_t *fs_restrict;
|
||||
searchpath_t *fs_searchpaths;
|
||||
int fs_readCount; // total bytes read
|
||||
int fs_loadCount; // total files read
|
||||
int fs_packFiles; // total number of files in packs
|
||||
|
||||
qboolean initialized = qfalse;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
fileHandleData_t fsh[MAX_FILE_HANDLES];
|
||||
|
||||
void FS_CheckInit(void)
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
==============
|
||||
FS_Initialized
|
||||
==============
|
||||
*/
|
||||
|
||||
qboolean FS_Initialized() {
|
||||
return (qboolean)(fs_searchpaths != NULL);
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileHandle_t FS_HandleForFile(void) {
|
||||
int i;
|
||||
|
||||
for ( i = 1 ; i < MAX_FILE_HANDLES ; i++ ) {
|
||||
#ifdef _XBOX
|
||||
if ( !fsh[i].used ) {
|
||||
#else
|
||||
if ( fsh[i].handleFiles.file.o == NULL ) {
|
||||
#endif
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
Com_Printf( "FS_HandleForFile: all handles taken:\n" );
|
||||
for ( i = 1 ; i < MAX_FILE_HANDLES ; i++ ) {
|
||||
Com_Printf( "%d. %s\n", i, fsh[i].name);
|
||||
}
|
||||
Com_Error( ERR_DROP, "FS_HandleForFile: none free" );
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
====================
|
||||
FS_ReplaceSeparators
|
||||
|
||||
Fix things up differently for win/unix/mac
|
||||
====================
|
||||
*/
|
||||
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 *qpath )
|
||||
{
|
||||
char temp[MAX_OSPATH];
|
||||
static char ospath[2][MAX_OSPATH];
|
||||
static int toggle;
|
||||
|
||||
toggle ^= 1; // flip-flop to allow two returns without clash
|
||||
|
||||
Com_sprintf( temp, sizeof(temp), "/%s/%s", fs_gamedirvar->string, qpath );
|
||||
|
||||
FS_ReplaceSeparators( temp );
|
||||
Com_sprintf( ospath[toggle], sizeof( ospath[0] ), "%s%s",
|
||||
fs_basepath->string, temp );
|
||||
|
||||
return ospath[toggle];
|
||||
}
|
||||
|
||||
char *FS_BuildOSPathUnMapped( 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
|
||||
|
||||
Com_sprintf( temp, sizeof(temp), "/%s/%s", fs_gamedirvar->string, qpath );
|
||||
|
||||
FS_ReplaceSeparators( temp );
|
||||
Com_sprintf( ospath[toggle], sizeof( ospath[0] ), "%s%s",
|
||||
"d:", temp );
|
||||
|
||||
return ospath[toggle];
|
||||
}
|
||||
|
||||
#ifndef _XBOX
|
||||
char *FS_BuildOSPath( const char *base, const char *game, const char *qpath ) {
|
||||
char temp[MAX_OSPATH];
|
||||
static char ospath[4][MAX_OSPATH];
|
||||
static int toggle;
|
||||
|
||||
toggle = (++toggle)&3; // allows four returns without clash (increased from 2 during fs_copyfiles 2 enhancement)
|
||||
|
||||
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];
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
============
|
||||
FS_CreatePath
|
||||
|
||||
Creates any directories needed to store the given filename
|
||||
============
|
||||
*/
|
||||
void FS_CreatePath (char *OSPath) {
|
||||
char *ofs;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
strlwr(OSPath);
|
||||
|
||||
for (ofs = OSPath+1 ; *ofs ; ofs++) {
|
||||
if (*ofs == PATH_SEP) {
|
||||
// create the directory
|
||||
*ofs = 0;
|
||||
Sys_Mkdir (OSPath);
|
||||
*ofs = PATH_SEP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
===========
|
||||
FS_SV_FOpenFileRead
|
||||
|
||||
===========
|
||||
*/
|
||||
int FS_SV_FOpenFileRead( const char *filename, fileHandle_t *fp ) {
|
||||
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 ) );
|
||||
|
||||
// don't let sound stutter
|
||||
S_ClearSoundBuffer();
|
||||
|
||||
#ifdef _XBOX
|
||||
ospath = FS_BuildOSPath( filename );
|
||||
#else
|
||||
ospath = FS_BuildOSPath( fs_basepath->string, filename, "" );
|
||||
#endif
|
||||
// remove trailing slash
|
||||
ospath[strlen(ospath)-1] = '\0';
|
||||
|
||||
if ( fs_debug->integer ) {
|
||||
Com_Printf( "FS_SV_FOpenFileRead: %s\n", ospath );
|
||||
}
|
||||
|
||||
fsh[f].handleFiles.file.o = fopen( ospath, "rb" );
|
||||
fsh[f].handleSync = qfalse;
|
||||
if (!fsh[f].handleFiles.file.o) {
|
||||
f = 0;
|
||||
}
|
||||
|
||||
*fp = f;
|
||||
if (f) {
|
||||
return FS_filelength(f);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
===========
|
||||
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 ) );
|
||||
|
||||
// don't let sound stutter
|
||||
S_ClearSoundBuffer();
|
||||
|
||||
#ifdef _XBOX
|
||||
ospath = FS_BuildOSPath( filename );
|
||||
#else
|
||||
ospath = FS_BuildOSPath( fs_basepath->string, fs_gamedir, filename );
|
||||
#endif
|
||||
|
||||
if ( fs_debug->integer ) {
|
||||
Com_Printf( "FS_FOpenFileAppend: %s\n", ospath );
|
||||
}
|
||||
|
||||
FS_CreatePath( ospath );
|
||||
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
|
||||
===========
|
||||
*/
|
||||
qboolean FS_FilenameCompare( const char *s1, const char *s2 ) {
|
||||
int c1, c2;
|
||||
|
||||
do {
|
||||
c1 = *s1++;
|
||||
c2 = *s2++;
|
||||
|
||||
if ( Q_islower(c1) ) {
|
||||
c1 -= ('a' - 'A');
|
||||
}
|
||||
if ( Q_islower(c2) ) {
|
||||
c2 -= ('a' - 'A');
|
||||
}
|
||||
|
||||
if ( c1 == '\\' || c1 == ':' ) {
|
||||
c1 = '/';
|
||||
}
|
||||
if ( c2 == '\\' || c2 == ':' ) {
|
||||
c2 = '/';
|
||||
}
|
||||
|
||||
if (c1 != c2) {
|
||||
return -1; // strings not equal
|
||||
}
|
||||
} while (c1);
|
||||
|
||||
return 0; // strings are equal
|
||||
}
|
||||
|
||||
|
||||
#define MAXPRINTMSG 4096
|
||||
void QDECL FS_Printf( fileHandle_t h, const char *fmt, ... ) {
|
||||
va_list argptr;
|
||||
char msg[MAXPRINTMSG];
|
||||
|
||||
va_start (argptr,fmt);
|
||||
vsprintf (msg,fmt,argptr);
|
||||
va_end (argptr);
|
||||
|
||||
FS_Write(msg, strlen(msg), h);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
============
|
||||
FS_WriteFile
|
||||
|
||||
Filename are relative 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 );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
================
|
||||
FS_Shutdown
|
||||
|
||||
Frees all resources and closes all files
|
||||
================
|
||||
*/
|
||||
void FS_Shutdown( void ) {
|
||||
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 ) {
|
||||
#ifndef _XBOX
|
||||
unzClose(p->pack->handle);
|
||||
#endif
|
||||
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_RemoveCommand( "path" );
|
||||
Cmd_RemoveCommand( "dir" );
|
||||
Cmd_RemoveCommand( "touchFile" );
|
||||
|
||||
initialized = qfalse;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
================
|
||||
FS_InitFilesystem
|
||||
|
||||
Called only at inital startup, not when the filesystem
|
||||
is resetting due to a game change
|
||||
================
|
||||
*/
|
||||
void FS_InitFilesystem( void ) {
|
||||
// allow command line parms to override our defaults
|
||||
// we don't have to specially handle this, because normal command
|
||||
// line variable sets happen before the filesystem
|
||||
// has been initialized
|
||||
//
|
||||
// UPDATE: BTO (VV)
|
||||
// we have to specially handle this, because normal command
|
||||
// line variable sets don't happen until after the filesystem
|
||||
// has already been initialized
|
||||
Com_StartupVariable( "fs_cdpath" );
|
||||
Com_StartupVariable( "fs_basepath" );
|
||||
Com_StartupVariable( "fs_game" );
|
||||
Com_StartupVariable( "fs_copyfiles" );
|
||||
Com_StartupVariable( "fs_restrict" );
|
||||
|
||||
// try to start up normally
|
||||
FS_Startup( BASEGAME );
|
||||
initialized = qtrue;
|
||||
|
||||
// see if we are going to allow add-ons
|
||||
FS_SetRestrictions();
|
||||
|
||||
// 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" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void FS_Flush( fileHandle_t f ) {
|
||||
fflush(fsh[f].handleFiles.file.o);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user