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