1 /*
2 Copyright (c) 2001, Loki software, inc.
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
7
8 Redistributions of source code must retain the above copyright notice, this list
9 of conditions and the following disclaimer.
10
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.
14
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
17 written permission.
18
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.
29 */
30
31 //
32 // Rules:
33 //
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).
36 //
37 // - Pak files are searched first inside the directories.
38 // - Case insensitive.
39 // - Unix-style slashes (/) (windows is backwards .. everyone knows that)
40 //
41 // Leonardo Zide (leo@lokigames.com)
42 //
43
44 #include "vfs.h"
45
46 #include <stdio.h>
47 #include <stdlib.h>
48 #include <glib.h>
49
50 #include "qerplugin.h"
51 #include "idatastream.h"
52 #include "iarchive.h"
53 ArchiveModules& FileSystemQ3API_getArchiveModules();
54 #include "ifilesystem.h"
55
56 #include "generic/callback.h"
57 #include "string/string.h"
58 #include "stream/stringstream.h"
59 #include "os/path.h"
60 #include "moduleobservers.h"
61 #include "filematch.h"
62
63
64 #define VFS_MAXDIRS 64
65
66 #if defined( WIN32 )
67 #define PATH_MAX 260
68 #endif
69
70 #define gamemode_get GlobalRadiant().getGameMode
71
72
73
74 // =============================================================================
75 // Global variables
76
77 Archive* OpenArchive( const char* name );
78
79 struct archive_entry_t
80 {
81 CopiedString name;
82 Archive* archive;
83 bool is_pakfile;
84 };
85
86 #include <list>
87
88 typedef std::list<archive_entry_t> archives_t;
89
90 static archives_t g_archives;
91 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
92 static int g_numDirs;
93 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
94 static int g_numForbiddenDirs = 0;
95 static bool g_bUsePak = true;
96
97 ModuleObservers g_observers;
98
99 // =============================================================================
100 // Static functions
101
AddSlash(char * str)102 static void AddSlash( char *str ){
103 std::size_t n = strlen( str );
104 if ( n > 0 ) {
105 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
106 globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
107 strcat( str, "/" );
108 }
109 }
110 }
111
FixDOSName(char * src)112 static void FixDOSName( char *src ){
113 if ( src == 0 || strchr( src, '\\' ) == 0 ) {
114 return;
115 }
116
117 globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
118
119 while ( *src )
120 {
121 if ( *src == '\\' ) {
122 *src = '/';
123 }
124 src++;
125 }
126 }
127
128
129
GetArchiveTable(ArchiveModules & archiveModules,const char * ext)130 const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
131 StringOutputStream tmp( 16 );
132 tmp << LowerCase( ext );
133 return archiveModules.findModule( tmp.c_str() );
134 }
InitPakFile(ArchiveModules & archiveModules,const char * filename)135 static void InitPakFile( ArchiveModules& archiveModules, const char *filename ){
136 const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
137
138 if ( table != 0 ) {
139 archive_entry_t entry;
140 entry.name = filename;
141
142 entry.archive = table->m_pfnOpenArchive( filename );
143 entry.is_pakfile = true;
144 g_archives.push_back( entry );
145 globalOutputStream() << " pak file: " << filename << "\n";
146 }
147 }
148
pathlist_prepend_unique(GSList * & pathlist,char * path)149 inline void pathlist_prepend_unique( GSList*& pathlist, char* path ){
150 if ( g_slist_find_custom( pathlist, path, (GCompareFunc)path_compare ) == 0 ) {
151 pathlist = g_slist_prepend( pathlist, path );
152 }
153 else
154 {
155 g_free( path );
156 }
157 }
158
159 class DirectoryListVisitor : public Archive::Visitor
160 {
161 GSList*& m_matches;
162 const char* m_directory;
163 public:
DirectoryListVisitor(GSList * & matches,const char * directory)164 DirectoryListVisitor( GSList*& matches, const char* directory )
165 : m_matches( matches ), m_directory( directory )
166 {}
visit(const char * name)167 void visit( const char* name ){
168 const char* subname = path_make_relative( name, m_directory );
169 if ( subname != name ) {
170 if ( subname[0] == '/' ) {
171 ++subname;
172 }
173 char* dir = g_strdup( subname );
174 char* last_char = dir + strlen( dir );
175 if ( last_char != dir && *( --last_char ) == '/' ) {
176 *last_char = '\0';
177 }
178 pathlist_prepend_unique( m_matches, dir );
179 }
180 }
181 };
182
183 class FileListVisitor : public Archive::Visitor
184 {
185 GSList*& m_matches;
186 const char* m_directory;
187 const char* m_extension;
188 public:
FileListVisitor(GSList * & matches,const char * directory,const char * extension)189 FileListVisitor( GSList*& matches, const char* directory, const char* extension )
190 : m_matches( matches ), m_directory( directory ), m_extension( extension )
191 {}
visit(const char * name)192 void visit( const char* name ){
193 const char* subname = path_make_relative( name, m_directory );
194 if ( subname != name ) {
195 if ( subname[0] == '/' ) {
196 ++subname;
197 }
198 if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
199 pathlist_prepend_unique( m_matches, g_strdup( subname ) );
200 }
201 }
202 }
203 };
204
GetListInternal(const char * refdir,const char * ext,bool directories,std::size_t depth)205 static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
206 GSList* files = 0;
207
208 ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
209
210 if ( directories ) {
211 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
212 {
213 DirectoryListVisitor visitor( files, refdir );
214 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
215 }
216 }
217 else
218 {
219 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
220 {
221 FileListVisitor visitor( files, refdir, ext );
222 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
223 }
224 }
225
226 files = g_slist_reverse( files );
227
228 return files;
229 }
230
ascii_to_upper(int c)231 inline int ascii_to_upper( int c ){
232 if ( c >= 'a' && c <= 'z' ) {
233 return c - ( 'a' - 'A' );
234 }
235 return c;
236 }
237
238 /*!
239 This behaves identically to stricmp(a,b), except that ASCII chars
240 [\]^`_ come AFTER alphabet chars instead of before. This is because
241 it converts all alphabet chars to uppercase before comparison,
242 while stricmp converts them to lowercase.
243 */
string_compare_nocase_upper(const char * a,const char * b)244 static int string_compare_nocase_upper( const char* a, const char* b ){
245 for (;; )
246 {
247 int c1 = ascii_to_upper( *a++ );
248 int c2 = ascii_to_upper( *b++ );
249
250 if ( c1 < c2 ) {
251 return -1; // a < b
252 }
253 if ( c1 > c2 ) {
254 return 1; // a > b
255 }
256 if ( c1 == 0 ) {
257 return 0; // a == b
258 }
259 }
260 }
261
262 // Arnout: note - sort pakfiles in reverse order. This ensures that
263 // later pakfiles override earlier ones. This because the vfs module
264 // returns a filehandle to the first file it can find (while it should
265 // return the filehandle to the file in the most overriding pakfile, the
266 // last one in the list that is).
267
268 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
269 class PakLess
270 {
271 public:
operator ()(const CopiedString & self,const CopiedString & other) const272 bool operator()( const CopiedString& self, const CopiedString& other ) const {
273 return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
274 }
275 };
276
277 typedef std::set<CopiedString, PakLess> Archives;
278
279 // =============================================================================
280 // Global functions
281
282 // reads all pak files from a dir
InitDirectory(const char * directory,ArchiveModules & archiveModules)283 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
284 int j;
285
286 g_numForbiddenDirs = 0;
287 StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
288 for ( j = 0; j < VFS_MAXDIRS; ++j )
289 {
290 const char *t = st.getToken();
291 if ( string_empty( t ) ) {
292 break;
293 }
294 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
295 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
296 ++g_numForbiddenDirs;
297 }
298
299 for ( j = 0; j < g_numForbiddenDirs; ++j )
300 {
301 char* dbuf = g_strdup( directory );
302 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
303 dbuf[strlen( dbuf ) - 1] = 0;
304 }
305 const char *p = strrchr( dbuf, '/' );
306 p = ( p ? ( p + 1 ) : dbuf );
307 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
308 g_free( dbuf );
309 break;
310 }
311 g_free( dbuf );
312 }
313 if ( j < g_numForbiddenDirs ) {
314 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
315 return;
316 }
317
318 if ( g_numDirs == VFS_MAXDIRS ) {
319 return;
320 }
321
322 strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
323 g_strDirs[g_numDirs][PATH_MAX] = '\0';
324 FixDOSName( g_strDirs[g_numDirs] );
325 AddSlash( g_strDirs[g_numDirs] );
326
327 const char* path = g_strDirs[g_numDirs];
328
329 g_numDirs++;
330
331 {
332 archive_entry_t entry;
333 entry.name = path;
334 entry.archive = OpenArchive( path );
335 entry.is_pakfile = false;
336 g_archives.push_back( entry );
337 }
338
339 if ( g_bUsePak ) {
340 GDir* dir = g_dir_open( path, 0, 0 );
341
342 if ( dir != 0 ) {
343 globalOutputStream() << "vfs directory: " << path << "\n";
344
345 const char* ignore_prefix = "";
346 const char* override_prefix = "";
347
348 {
349 // See if we are in "sp" or "mp" mapping mode
350 const char* gamemode = gamemode_get();
351
352 if ( strcmp( gamemode, "sp" ) == 0 ) {
353 ignore_prefix = "mp_";
354 override_prefix = "sp_";
355 }
356 else if ( strcmp( gamemode, "mp" ) == 0 ) {
357 ignore_prefix = "sp_";
358 override_prefix = "mp_";
359 }
360 }
361
362 Archives archives;
363 Archives archivesOverride;
364 for (;; )
365 {
366 const char* name = g_dir_read_name( dir );
367 if ( name == 0 ) {
368 break;
369 }
370
371 for ( j = 0; j < g_numForbiddenDirs; ++j )
372 {
373 const char *p = strrchr( name, '/' );
374 p = ( p ? ( p + 1 ) : name );
375 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
376 break;
377 }
378 }
379 if ( j < g_numForbiddenDirs ) {
380 continue;
381 }
382
383 const char *ext = strrchr( name, '.' );
384
385 if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
386 if ( g_numDirs == VFS_MAXDIRS ) {
387 continue;
388 }
389 snprintf( g_strDirs[g_numDirs], PATH_MAX, "%s%s/", path, name );
390 g_strDirs[g_numDirs][PATH_MAX] = '\0';
391 FixDOSName( g_strDirs[g_numDirs] );
392 AddSlash( g_strDirs[g_numDirs] );
393 g_numDirs++;
394
395 {
396 archive_entry_t entry;
397 entry.name = g_strDirs[g_numDirs - 1];
398 entry.archive = OpenArchive( g_strDirs[g_numDirs - 1] );
399 entry.is_pakfile = false;
400 g_archives.push_back( entry );
401 }
402 }
403
404 if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
405 continue;
406 }
407
408 // using the same kludge as in engine to ensure consistency
409 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
410 continue;
411 }
412 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
413 archivesOverride.insert( name );
414 continue;
415 }
416
417 archives.insert( name );
418 }
419
420 g_dir_close( dir );
421
422 // add the entries to the vfs
423 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
424 {
425 char filename[PATH_MAX];
426 strcpy( filename, path );
427 strcat( filename, ( *i ).c_str() );
428 InitPakFile( archiveModules, filename );
429 }
430 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
431 {
432 char filename[PATH_MAX];
433 strcpy( filename, path );
434 strcat( filename, ( *i ).c_str() );
435 InitPakFile( archiveModules, filename );
436 }
437 }
438 else
439 {
440 globalErrorStream() << "vfs directory not found: " << path << "\n";
441 }
442 }
443 }
444
445 // frees all memory that we allocated
446 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
447 // (for instance when modifying the project settings)
Shutdown()448 void Shutdown(){
449 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
450 {
451 ( *i ).archive->release();
452 }
453 g_archives.clear();
454
455 g_numDirs = 0;
456 g_numForbiddenDirs = 0;
457 }
458
459 #define VFS_SEARCH_PAK 0x1
460 #define VFS_SEARCH_DIR 0x2
461
GetFileCount(const char * filename,int flag)462 int GetFileCount( const char *filename, int flag ){
463 int count = 0;
464 char fixed[PATH_MAX + 1];
465
466 strncpy( fixed, filename, PATH_MAX );
467 fixed[PATH_MAX] = '\0';
468 FixDOSName( fixed );
469
470 if ( !flag ) {
471 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
472 }
473
474 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
475 {
476 if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
477 || !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
478 if ( ( *i ).archive->containsFile( fixed ) ) {
479 ++count;
480 }
481 }
482 }
483
484 return count;
485 }
486
OpenFile(const char * filename)487 ArchiveFile* OpenFile( const char* filename ){
488 ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
489 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
490 {
491 ArchiveFile* file = ( *i ).archive->openFile( filename );
492 if ( file != 0 ) {
493 return file;
494 }
495 }
496
497 return 0;
498 }
499
OpenTextFile(const char * filename)500 ArchiveTextFile* OpenTextFile( const char* filename ){
501 ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
502 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
503 {
504 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
505 if ( file != 0 ) {
506 return file;
507 }
508 }
509
510 return 0;
511 }
512
513 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
LoadFile(const char * filename,void ** bufferptr,int index)514 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
515 char fixed[PATH_MAX + 1];
516
517 strncpy( fixed, filename, PATH_MAX );
518 fixed[PATH_MAX] = '\0';
519 FixDOSName( fixed );
520
521 ArchiveFile* file = OpenFile( fixed );
522
523 if ( file != 0 ) {
524 *bufferptr = malloc( file->size() + 1 );
525 // we need to end the buffer with a 0
526 ( (char*) ( *bufferptr ) )[file->size()] = 0;
527
528 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
529 file->release();
530 return length;
531 }
532
533 *bufferptr = 0;
534 return 0;
535 }
536
FreeFile(void * p)537 void FreeFile( void *p ){
538 free( p );
539 }
540
GetFileList(const char * dir,const char * ext,std::size_t depth)541 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
542 return GetListInternal( dir, ext, false, depth );
543 }
544
GetDirList(const char * dir,std::size_t depth)545 GSList* GetDirList( const char *dir, std::size_t depth ){
546 return GetListInternal( dir, 0, true, depth );
547 }
548
ClearFileDirList(GSList ** lst)549 void ClearFileDirList( GSList **lst ){
550 while ( *lst )
551 {
552 g_free( ( *lst )->data );
553 *lst = g_slist_remove( *lst, ( *lst )->data );
554 }
555 }
556
FindFile(const char * relative)557 const char* FindFile( const char* relative ){
558 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
559 {
560 if ( ( *i ).archive->containsFile( relative ) ) {
561 return ( *i ).name.c_str();
562 }
563 }
564
565 return "";
566 }
567
FindPath(const char * absolute)568 const char* FindPath( const char* absolute ){
569 const char *best = "";
570 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
571 {
572 if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
573 if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
574 best = ( *i ).name.c_str();
575 }
576 }
577 }
578
579 return best;
580 }
581
582
583 class Quake3FileSystem : public VirtualFileSystem
584 {
585 public:
initDirectory(const char * path)586 void initDirectory( const char *path ){
587 InitDirectory( path, FileSystemQ3API_getArchiveModules() );
588 }
initialise()589 void initialise(){
590 globalOutputStream() << "filesystem initialised\n";
591 g_observers.realise();
592 }
shutdown()593 void shutdown(){
594 g_observers.unrealise();
595 globalOutputStream() << "filesystem shutdown\n";
596 Shutdown();
597 }
598
getFileCount(const char * filename,int flags)599 int getFileCount( const char *filename, int flags ){
600 return GetFileCount( filename, flags );
601 }
openFile(const char * filename)602 ArchiveFile* openFile( const char* filename ){
603 return OpenFile( filename );
604 }
openTextFile(const char * filename)605 ArchiveTextFile* openTextFile( const char* filename ){
606 return OpenTextFile( filename );
607 }
loadFile(const char * filename,void ** buffer)608 std::size_t loadFile( const char *filename, void **buffer ){
609 return LoadFile( filename, buffer, 0 );
610 }
freeFile(void * p)611 void freeFile( void *p ){
612 FreeFile( p );
613 }
614
forEachDirectory(const char * basedir,const FileNameCallback & callback,std::size_t depth)615 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
616 GSList* list = GetDirList( basedir, depth );
617
618 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
619 {
620 callback( reinterpret_cast<const char*>( ( *i ).data ) );
621 }
622
623 ClearFileDirList( &list );
624 }
forEachFile(const char * basedir,const char * extension,const FileNameCallback & callback,std::size_t depth)625 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
626 GSList* list = GetFileList( basedir, extension, depth );
627
628 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
629 {
630 const char* name = reinterpret_cast<const char*>( ( *i ).data );
631 if ( extension_equal( path_get_extension( name ), extension ) ) {
632 callback( name );
633 }
634 }
635
636 ClearFileDirList( &list );
637 }
getDirList(const char * basedir)638 GSList* getDirList( const char *basedir ){
639 return GetDirList( basedir, 1 );
640 }
getFileList(const char * basedir,const char * extension)641 GSList* getFileList( const char *basedir, const char *extension ){
642 return GetFileList( basedir, extension, 1 );
643 }
clearFileDirList(GSList ** lst)644 void clearFileDirList( GSList **lst ){
645 ClearFileDirList( lst );
646 }
647
findFile(const char * name)648 const char* findFile( const char *name ){
649 return FindFile( name );
650 }
findRoot(const char * name)651 const char* findRoot( const char *name ){
652 return FindPath( name );
653 }
654
attach(ModuleObserver & observer)655 void attach( ModuleObserver& observer ){
656 g_observers.attach( observer );
657 }
detach(ModuleObserver & observer)658 void detach( ModuleObserver& observer ){
659 g_observers.detach( observer );
660 }
661
getArchive(const char * archiveName,bool pakonly)662 Archive* getArchive( const char* archiveName, bool pakonly ){
663 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
664 {
665 if ( pakonly && !( *i ).is_pakfile ) {
666 continue;
667 }
668
669 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
670 return ( *i ).archive;
671 }
672 }
673 return 0;
674 }
forEachArchive(const ArchiveNameCallback & callback,bool pakonly,bool reverse)675 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
676 if ( reverse ) {
677 g_archives.reverse();
678 }
679
680 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
681 {
682 if ( pakonly && !( *i ).is_pakfile ) {
683 continue;
684 }
685
686 callback( ( *i ).name.c_str() );
687 }
688
689 if ( reverse ) {
690 g_archives.reverse();
691 }
692 }
693 };
694
695 Quake3FileSystem g_Quake3FileSystem;
696
FileSystem_Init()697 void FileSystem_Init(){
698 }
699
FileSystem_Shutdown()700 void FileSystem_Shutdown(){
701 }
702
GetFileSystem()703 VirtualFileSystem& GetFileSystem(){
704 return g_Quake3FileSystem;
705 }
706