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 != NULL ) {
478 LoadPakWithDeps( p_pakname );
481 int len = ( p_name_end - p_name ) + ( p_version_end - p_version ) + 1;
482 char* p_pakname = string_new( len );
483 strncpy( p_pakname, p_name, p_name_end - p_name );
484 p_pakname[ p_name_end - p_name ] = '\0';
485 strcat( p_pakname, "_" );
486 strncat( p_pakname, p_version, p_version_end - p_version );
487 LoadPakWithDeps( p_pakname );
488 string_release( p_pakname, len );
496 // end for unvanquished
498 // =============================================================================
501 // reads all pak files from a dir
502 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
505 g_numForbiddenDirs = 0;
506 StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
507 for ( j = 0; j < VFS_MAXDIRS; ++j )
509 const char *t = st.getToken();
510 if ( string_empty( t ) ) {
513 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
514 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
515 ++g_numForbiddenDirs;
518 for ( j = 0; j < g_numForbiddenDirs; ++j )
520 char* dbuf = g_strdup( directory );
521 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
522 dbuf[strlen( dbuf ) - 1] = 0;
524 const char *p = strrchr( dbuf, '/' );
525 p = ( p ? ( p + 1 ) : dbuf );
526 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
532 if ( j < g_numForbiddenDirs ) {
533 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
537 if ( g_numDirs == VFS_MAXDIRS ) {
541 strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
542 g_strDirs[g_numDirs][PATH_MAX] = '\0';
543 FixDOSName( g_strDirs[g_numDirs] );
544 AddSlash( g_strDirs[g_numDirs] );
546 const char* path = g_strDirs[g_numDirs];
551 archive_entry_t entry;
553 entry.archive = OpenArchive( path );
554 entry.is_pakfile = false;
555 g_archives.push_back( entry );
560 GDir* dir = g_dir_open( path, 0, 0 );
563 globalOutputStream() << "vfs directory: " << path << "\n";
566 unv = IsUnvanquished();
568 const char* ignore_prefix = "";
569 const char* override_prefix = "";
572 // See if we are in "sp" or "mp" mapping mode
573 const char* gamemode = gamemode_get();
575 if ( strcmp( gamemode, "sp" ) == 0 ) {
576 ignore_prefix = "mp_";
577 override_prefix = "sp_";
579 else if ( strcmp( gamemode, "mp" ) == 0 ) {
580 ignore_prefix = "sp_";
581 override_prefix = "mp_";
586 Archives archivesOverride;
589 const char* name = g_dir_read_name( dir );
594 for ( j = 0; j < g_numForbiddenDirs; ++j )
596 const char *p = strrchr( name, '/' );
597 p = ( p ? ( p + 1 ) : name );
598 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
602 if ( j < g_numForbiddenDirs ) {
606 const char *ext = strrchr( name, '.' );
607 char tmppath[PATH_MAX];
609 if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
611 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
612 tmppath[PATH_MAX] = '\0';
613 FixDOSName( tmppath );
617 AddUnvPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
619 AddPk3Dir( tmppath );
623 if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
627 // using the same kludge as in engine to ensure consistency
628 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
631 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
633 archives.insert( name );
635 archivesOverride.insert( name );
640 archives.insert( name );
645 // add the entries to the vfs
648 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) {
649 const char* name = i->c_str();
650 const char* ext = strrchr( name, '.' );
651 CopiedString name_final = CopiedString( StringRange( name, ext ) );
652 fullpath = string_new_concat( path, name );
653 AddUnvPak( name_final.c_str(), fullpath, true );
654 string_release( fullpath, string_length( fullpath ) );
657 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
659 fullpath = string_new_concat( path, i->c_str() );
660 InitPakFile( archiveModules, fullpath );
661 string_release( fullpath, string_length( fullpath ) );
663 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
665 fullpath = string_new_concat( path, i->c_str() );
666 InitPakFile( archiveModules, fullpath );
667 string_release( fullpath, string_length( fullpath ) );
673 globalErrorStream() << "vfs directory not found: " << path << "\n";
678 // frees all memory that we allocated
679 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
680 // (for instance when modifying the project settings)
682 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
684 ( *i ).archive->release();
689 g_numForbiddenDirs = 0;
691 g_pakfile_paths.clear();
692 g_loaded_unv_paks.clear();
695 #define VFS_SEARCH_PAK 0x1
696 #define VFS_SEARCH_DIR 0x2
698 int GetFileCount( const char *filename, int flag ){
700 char fixed[PATH_MAX + 1];
702 strncpy( fixed, filename, PATH_MAX );
703 fixed[PATH_MAX] = '\0';
707 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
710 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
712 if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
713 || !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
714 if ( ( *i ).archive->containsFile( fixed ) ) {
723 ArchiveFile* OpenFile( const char* filename ){
724 ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
725 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
727 ArchiveFile* file = ( *i ).archive->openFile( filename );
736 ArchiveTextFile* OpenTextFile( const char* filename ){
737 ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
738 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
740 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
749 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
750 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
751 char fixed[PATH_MAX + 1];
753 strncpy( fixed, filename, PATH_MAX );
754 fixed[PATH_MAX] = '\0';
757 ArchiveFile* file = OpenFile( fixed );
760 *bufferptr = malloc( file->size() + 1 );
761 // we need to end the buffer with a 0
762 ( (char*) ( *bufferptr ) )[file->size()] = 0;
764 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
773 void FreeFile( void *p ){
777 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
778 return GetListInternal( dir, ext, false, depth );
781 GSList* GetDirList( const char *dir, std::size_t depth ){
782 return GetListInternal( dir, 0, true, depth );
785 void ClearFileDirList( GSList **lst ){
788 g_free( ( *lst )->data );
789 *lst = g_slist_remove( *lst, ( *lst )->data );
793 const char* FindFile( const char* relative ){
794 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
796 if ( ( *i ).archive->containsFile( relative ) ) {
797 return ( *i ).name.c_str();
804 const char* FindPath( const char* absolute ){
805 const char *best = "";
806 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
808 if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
809 if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
810 best = ( *i ).name.c_str();
819 class Quake3FileSystem : public VirtualFileSystem
822 void initDirectory( const char *path ){
823 InitDirectory( path, FileSystemQ3API_getArchiveModules() );
826 if ( IsUnvanquished() ) {
828 g_loaded_unv_paks.clear();
830 pakname = GetLatestVersionOfUnvPak( "radiant" );
831 if (pakname != NULL) {
832 LoadPakWithDeps( pakname );
835 // prevent VFS double start, for MapName="" and MapName="unnamed.map"
836 if ( string_length( GlobalRadiant().getMapName() ) ){
837 // map's tex-* paks have precedence over any other tex-* paks
838 char* mappakname = GetCurrentMapPakName();
839 if ( mappakname != NULL ) {
840 LoadPakWithDeps( mappakname );
841 string_release( mappakname, string_length( mappakname ) );
844 for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i ) {
845 if ( strncmp( i->first.c_str(), "tex-", 4 ) != 0 ) continue;
846 // firstly load latest version of pak
847 const char *paknamever = i->first.c_str();
848 const char *c = strchr( paknamever, '_' );
850 if ( c ) paknameonly = string_clone_range( StringRange( paknamever, c ) );
851 pakname = GetLatestVersionOfUnvPak( paknameonly );
852 if (pakname != NULL) {
853 LoadPakWithDeps( pakname );
855 if ( c ) string_release( paknameonly, string_length( paknameonly ) );
856 // then load this specific version
857 LoadPakWithDeps( paknamever );
861 g_pakfile_paths.clear();
862 g_loaded_unv_paks.clear();
865 globalOutputStream() << "filesystem initialised\n";
866 g_observers.realise();
869 g_observers.unrealise();
870 globalOutputStream() << "filesystem shutdown\n";
874 int getFileCount( const char *filename, int flags ){
875 return GetFileCount( filename, flags );
877 ArchiveFile* openFile( const char* filename ){
878 return OpenFile( filename );
880 ArchiveTextFile* openTextFile( const char* filename ){
881 return OpenTextFile( filename );
883 std::size_t loadFile( const char *filename, void **buffer ){
884 return LoadFile( filename, buffer, 0 );
886 void freeFile( void *p ){
890 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
891 GSList* list = GetDirList( basedir, depth );
893 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
895 callback( reinterpret_cast<const char*>( ( *i ).data ) );
898 ClearFileDirList( &list );
900 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
901 GSList* list = GetFileList( basedir, extension, depth );
903 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
905 const char* name = reinterpret_cast<const char*>( ( *i ).data );
906 if ( extension_equal( path_get_extension( name ), extension ) ) {
911 ClearFileDirList( &list );
913 GSList* getDirList( const char *basedir ){
914 return GetDirList( basedir, 1 );
916 GSList* getFileList( const char *basedir, const char *extension ){
917 return GetFileList( basedir, extension, 1 );
919 void clearFileDirList( GSList **lst ){
920 ClearFileDirList( lst );
923 const char* findFile( const char *name ){
924 return FindFile( name );
926 const char* findRoot( const char *name ){
927 return FindPath( name );
930 void attach( ModuleObserver& observer ){
931 g_observers.attach( observer );
933 void detach( ModuleObserver& observer ){
934 g_observers.detach( observer );
937 Archive* getArchive( const char* archiveName, bool pakonly ){
938 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
940 if ( pakonly && !( *i ).is_pakfile ) {
944 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
945 return ( *i ).archive;
950 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
952 g_archives.reverse();
955 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
957 if ( pakonly && !( *i ).is_pakfile ) {
961 callback( ( *i ).name.c_str() );
965 g_archives.reverse();
971 Quake3FileSystem g_Quake3FileSystem;
973 VirtualFileSystem& GetFileSystem(){
974 return g_Quake3FileSystem;
977 void FileSystem_Init(){
980 void FileSystem_Shutdown(){