2 Copyright (c) 2001, Loki software, inc.
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
8 Redistributions of source code must retain the above copyright notice, this list
9 of conditions and the following disclaimer.
11 Redistributions in binary form must reproduce the above copyright notice, this
12 list of conditions and the following disclaimer in the documentation and/or
13 other materials provided with the distribution.
15 Neither the name of Loki software nor the names of its contributors may be used
16 to endorse or promote products derived from this software without specific prior
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
23 DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 // - Directories should be searched in the following order: ~/.q3a/baseq3,
35 // install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
37 // - Pak files are searched first inside the directories.
38 // - Case insensitive.
39 // - Unix-style slashes (/) (windows is backwards .. everyone knows that)
41 // Leonardo Zide (leo@lokigames.com)
50 #include "qerplugin.h"
51 #include "idatastream.h"
53 ArchiveModules& FileSystemQ3API_getArchiveModules();
54 #include "ifilesystem.h"
56 #include "generic/callback.h"
57 #include "string/string.h"
58 #include "stream/stringstream.h"
60 #include "moduleobservers.h"
61 #include "filematch.h"
64 #define VFS_MAXDIRS 64
70 #define gamemode_get GlobalRadiant().getGameMode
74 // =============================================================================
77 Archive* OpenArchive( const char* name );
79 struct archive_entry_t
89 typedef std::list<archive_entry_t> archives_t;
91 static archives_t g_archives;
92 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
94 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
95 static int g_numForbiddenDirs = 0;
96 static bool g_bUsePak = true;
98 ModuleObservers g_observers;
100 // =============================================================================
103 static void AddSlash( char *str ){
104 std::size_t n = strlen( str );
106 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
107 globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
113 static void FixDOSName( char *src ){
114 if ( src == 0 || strchr( src, '\\' ) == 0 ) {
118 globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
122 if ( *src == '\\' ) {
131 const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
132 StringOutputStream tmp( 16 );
133 tmp << LowerCase( ext );
134 return archiveModules.findModule( tmp.c_str() );
136 static Archive* InitPakFile( ArchiveModules& archiveModules, const char *filename ){
137 const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
140 archive_entry_t entry;
141 entry.name = filename;
143 entry.archive = table->m_pfnOpenArchive( filename );
144 entry.is_pakfile = true;
145 g_archives.push_back( entry );
146 globalOutputStream() << " pak file: " << filename << "\n";
148 return entry.archive;
154 inline void pathlist_prepend_unique( GSList*& pathlist, char* path ){
155 if ( g_slist_find_custom( pathlist, path, (GCompareFunc)path_compare ) == 0 ) {
156 pathlist = g_slist_prepend( pathlist, path );
164 class DirectoryListVisitor : public Archive::Visitor
167 const char* m_directory;
169 DirectoryListVisitor( GSList*& matches, const char* directory )
170 : m_matches( matches ), m_directory( directory )
172 void visit( const char* name ){
173 const char* subname = path_make_relative( name, m_directory );
174 if ( subname != name ) {
175 if ( subname[0] == '/' ) {
178 char* dir = g_strdup( subname );
179 char* last_char = dir + strlen( dir );
180 if ( last_char != dir && *( --last_char ) == '/' ) {
183 pathlist_prepend_unique( m_matches, dir );
188 class FileListVisitor : public Archive::Visitor
191 const char* m_directory;
192 const char* m_extension;
194 FileListVisitor( GSList*& matches, const char* directory, const char* extension )
195 : m_matches( matches ), m_directory( directory ), m_extension( extension )
197 void visit( const char* name ){
198 const char* subname = path_make_relative( name, m_directory );
199 if ( subname != name ) {
200 if ( subname[0] == '/' ) {
203 if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
204 pathlist_prepend_unique( m_matches, g_strdup( subname ) );
210 static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
213 ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
216 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
218 DirectoryListVisitor visitor( files, refdir );
219 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
224 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
226 FileListVisitor visitor( files, refdir, ext );
227 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
231 files = g_slist_reverse( files );
236 inline int ascii_to_upper( int c ){
237 if ( c >= 'a' && c <= 'z' ) {
238 return c - ( 'a' - 'A' );
244 This behaves identically to stricmp(a,b), except that ASCII chars
245 [\]^`_ come AFTER alphabet chars instead of before. This is because
246 it converts all alphabet chars to uppercase before comparison,
247 while stricmp converts them to lowercase.
249 static int string_compare_nocase_upper( const char* a, const char* b ){
252 int c1 = ascii_to_upper( *a++ );
253 int c2 = ascii_to_upper( *b++ );
267 // Arnout: note - sort pakfiles in reverse order. This ensures that
268 // later pakfiles override earlier ones. This because the vfs module
269 // returns a filehandle to the first file it can find (while it should
270 // return the filehandle to the file in the most overriding pakfile, the
271 // last one in the list that is).
273 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
277 bool operator()( const CopiedString& self, const CopiedString& other ) const {
278 return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
282 typedef std::set<CopiedString, PakLess> Archives;
284 Archive* AddPk3Dir( const char* fullpath ){
285 if ( g_numDirs == VFS_MAXDIRS ) return 0;
287 strncpy( g_strDirs[g_numDirs], fullpath, PATH_MAX );
288 g_strDirs[g_numDirs][PATH_MAX] = '\0';
292 archive_entry_t entry;
293 entry.name = fullpath;
294 entry.archive = OpenArchive( fullpath );
295 entry.is_pakfile = false;
296 g_archives.push_back( entry );
298 return entry.archive;
304 bool IsUnvanquished(){
305 return strncmp( GlobalRadiant().getGameFile(), "unvanquished", 12 ) == 0;
308 struct pakfile_path_t
310 CopiedString fullpath; // full pak dir or pk3dir name
311 bool is_pakfile; // defines is it .pk3dir or .pk3 file
314 typedef std::pair<CopiedString, pakfile_path_t> PakfilePathsKV;
315 typedef std::map<CopiedString, pakfile_path_t> PakfilePaths; // key must have no extension, only name
317 static PakfilePaths g_pakfile_paths;
319 void AddUnvPak( const char* name, const char* fullpath, bool is_pakfile ){
320 pakfile_path_t pakfile_path;
321 pakfile_path.fullpath = fullpath;
322 pakfile_path.is_pakfile = is_pakfile;
323 g_pakfile_paths.insert( PakfilePathsKV( name, pakfile_path ) );
326 // Comparaison function for version numbers
327 // Implementation is based on dpkg's version comparison code (verrevcmp() and order())
328 // http://anonscm.debian.org/gitweb/?p=dpkg/dpkg.git;a=blob;f=lib/dpkg/version.c;hb=74946af470550a3295e00cf57eca1747215b9311
329 static int char_weight(char c){
332 else if (std::isalpha(c))
342 static int VersionCmp(const char* a, const char* b){
346 while ((*a && !std::isdigit(*a)) || (*b && !std::isdigit(*b))) {
347 int ac = char_weight(*a);
348 int bc = char_weight(*b);
362 while (std::isdigit(*a) && std::isdigit(*b)) {
369 if (std::isdigit(*a))
371 if (std::isdigit(*b))
380 // takes name without ext, returns without ext
381 static const char* GetLatestVersionOfUnvPak( const char* name ){
382 const char* maxversion = 0;
383 const char* result = 0;
385 const char* pakversion;
386 int namelen = string_length( name );
388 for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i )
390 pakname = i->first.c_str();
391 if ( strncmp( pakname, name, namelen ) != 0 || pakname[namelen] != '_' ) continue;
392 pakversion = pakname + (namelen + 1);
393 if ( maxversion == 0 || VersionCmp( pakversion, maxversion ) > 0 ){
394 maxversion = pakversion;
401 // release string after using
402 static char* GetCurrentMapPakName(){
408 mapname = string_clone( GlobalRadiant().getMapName() );
409 mapnamelen = string_length( mapname );
411 mapdir = strrchr( mapname, '/' );
414 if ( strncmp( mapdir, ".pk3dir/maps/", 13 ) == 0 ) {
416 mapdir = strrchr( mapname, '/' );
417 if ( mapdir ) mapdir++;
418 else mapdir = mapname;
419 result = string_clone( mapdir );
423 string_release( mapname, mapnamelen );
428 // prevent loading duplicates or circular references
429 static Archives g_loaded_unv_paks;
431 // actual pak adding on initialise, deferred from InitDirectory
432 // Unvanquished filesystem doesn't need load all paks it finds
433 static void LoadPakWithDeps( const char* pakname ){
434 const char* und = strrchr( pakname, '_' );
435 if ( !und ) pakname = GetLatestVersionOfUnvPak( pakname );
436 if ( !pakname || g_loaded_unv_paks.find( pakname ) != g_loaded_unv_paks.end() ) return;
438 PakfilePaths::iterator i = g_pakfile_paths.find( pakname );
439 if ( i == g_pakfile_paths.end() ) return;
442 if ( i->second.is_pakfile ){
443 arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() );
445 arc = AddPk3Dir( i->second.fullpath.c_str() );
447 g_loaded_unv_paks.insert( pakname );
449 ArchiveTextFile* depsFile = arc->openTextFile( "DEPS" );
450 if ( !depsFile ) return;
453 TextLinesInputStream<TextInputStream> istream = depsFile->getInputStream();
458 const char* p_name_end;
459 const char* p_version;
460 const char* p_version_end;
461 while ( line = istream.readLine(), string_length( line.c_str() ) ) {
463 while ( std::isspace( *c ) && *c != '\0' ) ++c;
465 while ( !std::isspace( *c ) && *c != '\0' ) ++c;
467 while ( std::isspace( *c ) && *c != '\0' ) ++c;
469 while ( !std::isspace( *c ) && *c != '\0' ) ++c;
472 if ( p_name_end - p_name == 0 ) continue;
473 if ( p_version_end - p_version == 0 ) {
474 const char* p_pakname;
475 CopiedString name_final = CopiedString( StringRange( p_name, p_name_end ) );
476 p_pakname = GetLatestVersionOfUnvPak( name_final.c_str() );
477 if ( !p_pakname ) continue;
478 LoadPakWithDeps( p_pakname );
480 int len = ( p_name_end - p_name ) + ( p_version_end - p_version ) + 1;
481 char* p_pakname = string_new( len );
482 strncpy( p_pakname, p_name, p_name_end - p_name );
483 p_pakname[ p_name_end - p_name ] = '\0';
484 strcat( p_pakname, "_" );
485 strncat( p_pakname, p_version, p_version_end - p_version );
486 LoadPakWithDeps( p_pakname );
487 string_release( p_pakname, len );
495 // end for unvanquished
497 // =============================================================================
500 // reads all pak files from a dir
501 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
504 g_numForbiddenDirs = 0;
505 StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
506 for ( j = 0; j < VFS_MAXDIRS; ++j )
508 const char *t = st.getToken();
509 if ( string_empty( t ) ) {
512 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
513 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
514 ++g_numForbiddenDirs;
517 for ( j = 0; j < g_numForbiddenDirs; ++j )
519 char* dbuf = g_strdup( directory );
520 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
521 dbuf[strlen( dbuf ) - 1] = 0;
523 const char *p = strrchr( dbuf, '/' );
524 p = ( p ? ( p + 1 ) : dbuf );
525 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
531 if ( j < g_numForbiddenDirs ) {
532 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
536 if ( g_numDirs == VFS_MAXDIRS ) {
540 strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
541 g_strDirs[g_numDirs][PATH_MAX] = '\0';
542 FixDOSName( g_strDirs[g_numDirs] );
543 AddSlash( g_strDirs[g_numDirs] );
545 const char* path = g_strDirs[g_numDirs];
550 archive_entry_t entry;
552 entry.archive = OpenArchive( path );
553 entry.is_pakfile = false;
554 g_archives.push_back( entry );
559 GDir* dir = g_dir_open( path, 0, 0 );
562 globalOutputStream() << "vfs directory: " << path << "\n";
565 unv = IsUnvanquished();
567 const char* ignore_prefix = "";
568 const char* override_prefix = "";
571 // See if we are in "sp" or "mp" mapping mode
572 const char* gamemode = gamemode_get();
574 if ( strcmp( gamemode, "sp" ) == 0 ) {
575 ignore_prefix = "mp_";
576 override_prefix = "sp_";
578 else if ( strcmp( gamemode, "mp" ) == 0 ) {
579 ignore_prefix = "sp_";
580 override_prefix = "mp_";
585 Archives archivesOverride;
588 const char* name = g_dir_read_name( dir );
593 for ( j = 0; j < g_numForbiddenDirs; ++j )
595 const char *p = strrchr( name, '/' );
596 p = ( p ? ( p + 1 ) : name );
597 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
601 if ( j < g_numForbiddenDirs ) {
605 const char *ext = strrchr( name, '.' );
606 char tmppath[PATH_MAX];
608 if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
610 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
611 tmppath[PATH_MAX] = '\0';
612 FixDOSName( tmppath );
616 AddUnvPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
618 AddPk3Dir( tmppath );
622 if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
626 // using the same kludge as in engine to ensure consistency
627 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
630 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
632 archives.insert( name );
634 archivesOverride.insert( name );
639 archives.insert( name );
644 // add the entries to the vfs
647 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) {
648 const char* name = i->c_str();
649 const char* ext = strrchr( name, '.' );
650 CopiedString name_final = CopiedString( StringRange( name, ext ) );
651 fullpath = string_new_concat( path, name );
652 AddUnvPak( name_final.c_str(), fullpath, true );
653 string_release( fullpath, string_length( fullpath ) );
656 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
658 fullpath = string_new_concat( path, i->c_str() );
659 InitPakFile( archiveModules, fullpath );
660 string_release( fullpath, string_length( fullpath ) );
662 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
664 fullpath = string_new_concat( path, i->c_str() );
665 InitPakFile( archiveModules, fullpath );
666 string_release( fullpath, string_length( fullpath ) );
672 globalErrorStream() << "vfs directory not found: " << path << "\n";
677 // frees all memory that we allocated
678 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
679 // (for instance when modifying the project settings)
681 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
683 ( *i ).archive->release();
688 g_numForbiddenDirs = 0;
690 g_pakfile_paths.clear();
691 g_loaded_unv_paks.clear();
694 #define VFS_SEARCH_PAK 0x1
695 #define VFS_SEARCH_DIR 0x2
697 int GetFileCount( const char *filename, int flag ){
699 char fixed[PATH_MAX + 1];
701 strncpy( fixed, filename, PATH_MAX );
702 fixed[PATH_MAX] = '\0';
706 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
709 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
711 if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
712 || !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
713 if ( ( *i ).archive->containsFile( fixed ) ) {
722 ArchiveFile* OpenFile( const char* filename ){
723 ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
724 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
726 ArchiveFile* file = ( *i ).archive->openFile( filename );
735 ArchiveTextFile* OpenTextFile( const char* filename ){
736 ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
737 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
739 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
748 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
749 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
750 char fixed[PATH_MAX + 1];
752 strncpy( fixed, filename, PATH_MAX );
753 fixed[PATH_MAX] = '\0';
756 ArchiveFile* file = OpenFile( fixed );
759 *bufferptr = malloc( file->size() + 1 );
760 // we need to end the buffer with a 0
761 ( (char*) ( *bufferptr ) )[file->size()] = 0;
763 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
772 void FreeFile( void *p ){
776 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
777 return GetListInternal( dir, ext, false, depth );
780 GSList* GetDirList( const char *dir, std::size_t depth ){
781 return GetListInternal( dir, 0, true, depth );
784 void ClearFileDirList( GSList **lst ){
787 g_free( ( *lst )->data );
788 *lst = g_slist_remove( *lst, ( *lst )->data );
792 const char* FindFile( const char* relative ){
793 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
795 if ( ( *i ).archive->containsFile( relative ) ) {
796 return ( *i ).name.c_str();
803 const char* FindPath( const char* absolute ){
804 const char *best = "";
805 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
807 if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
808 if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
809 best = ( *i ).name.c_str();
818 class Quake3FileSystem : public VirtualFileSystem
821 void initDirectory( const char *path ){
822 InitDirectory( path, FileSystemQ3API_getArchiveModules() );
825 if ( IsUnvanquished() ) {
827 g_loaded_unv_paks.clear();
829 pakname = GetLatestVersionOfUnvPak( "tex-common" );
830 if ( pakname ) LoadPakWithDeps( pakname );
832 pakname = GetLatestVersionOfUnvPak( "radiant" );
833 if ( pakname ) LoadPakWithDeps( pakname );
835 pakname = GetCurrentMapPakName();
836 if ( pakname && !string_empty( pakname ) ) {
837 LoadPakWithDeps( pakname );
840 g_pakfile_paths.clear();
841 g_loaded_unv_paks.clear();
844 globalOutputStream() << "filesystem initialised\n";
845 g_observers.realise();
848 g_observers.unrealise();
849 globalOutputStream() << "filesystem shutdown\n";
853 int getFileCount( const char *filename, int flags ){
854 return GetFileCount( filename, flags );
856 ArchiveFile* openFile( const char* filename ){
857 return OpenFile( filename );
859 ArchiveTextFile* openTextFile( const char* filename ){
860 return OpenTextFile( filename );
862 std::size_t loadFile( const char *filename, void **buffer ){
863 return LoadFile( filename, buffer, 0 );
865 void freeFile( void *p ){
869 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
870 GSList* list = GetDirList( basedir, depth );
872 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
874 callback( reinterpret_cast<const char*>( ( *i ).data ) );
877 ClearFileDirList( &list );
879 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
880 GSList* list = GetFileList( basedir, extension, depth );
882 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
884 const char* name = reinterpret_cast<const char*>( ( *i ).data );
885 if ( extension_equal( path_get_extension( name ), extension ) ) {
890 ClearFileDirList( &list );
892 GSList* getDirList( const char *basedir ){
893 return GetDirList( basedir, 1 );
895 GSList* getFileList( const char *basedir, const char *extension ){
896 return GetFileList( basedir, extension, 1 );
898 void clearFileDirList( GSList **lst ){
899 ClearFileDirList( lst );
902 const char* findFile( const char *name ){
903 return FindFile( name );
905 const char* findRoot( const char *name ){
906 return FindPath( name );
909 void attach( ModuleObserver& observer ){
910 g_observers.attach( observer );
912 void detach( ModuleObserver& observer ){
913 g_observers.detach( observer );
916 Archive* getArchive( const char* archiveName, bool pakonly ){
917 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
919 if ( pakonly && !( *i ).is_pakfile ) {
923 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
924 return ( *i ).archive;
929 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
931 g_archives.reverse();
934 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
936 if ( pakonly && !( *i ).is_pakfile ) {
940 callback( ( *i ).name.c_str() );
944 g_archives.reverse();
950 Quake3FileSystem g_Quake3FileSystem;
952 VirtualFileSystem& GetFileSystem(){
953 return g_Quake3FileSystem;
956 void FileSystem_Init(){
959 void FileSystem_Shutdown(){