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( "radiant" );
830 if ( pakname ) LoadPakWithDeps( pakname );
832 // prevent VFS double start, for MapName="" and MapName="unnamed.map"
833 if ( string_length( GlobalRadiant().getMapName() ) ){
834 // map's tex-* paks have precedence over any other tex-* paks
835 char* mappakname = GetCurrentMapPakName();
837 LoadPakWithDeps( mappakname );
838 string_release( mappakname, string_length( mappakname ) );
841 for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i ) {
842 if ( strncmp( i->first.c_str(), "tex-", 4 ) != 0 ) continue;
843 // firstly load latest version of pak
844 const char *paknamever = i->first.c_str();
845 const char *c = strchr( paknamever, '_' );
847 if ( c ) paknameonly = string_clone_range( StringRange( paknamever, c ) );
848 pakname = GetLatestVersionOfUnvPak( paknameonly );
849 LoadPakWithDeps( pakname );
850 if ( c ) string_release( paknameonly, string_length( paknameonly ) );
851 // then load this specific version
852 LoadPakWithDeps( paknamever );
856 g_pakfile_paths.clear();
857 g_loaded_unv_paks.clear();
860 globalOutputStream() << "filesystem initialised\n";
861 g_observers.realise();
864 g_observers.unrealise();
865 globalOutputStream() << "filesystem shutdown\n";
869 int getFileCount( const char *filename, int flags ){
870 return GetFileCount( filename, flags );
872 ArchiveFile* openFile( const char* filename ){
873 return OpenFile( filename );
875 ArchiveTextFile* openTextFile( const char* filename ){
876 return OpenTextFile( filename );
878 std::size_t loadFile( const char *filename, void **buffer ){
879 return LoadFile( filename, buffer, 0 );
881 void freeFile( void *p ){
885 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
886 GSList* list = GetDirList( basedir, depth );
888 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
890 callback( reinterpret_cast<const char*>( ( *i ).data ) );
893 ClearFileDirList( &list );
895 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
896 GSList* list = GetFileList( basedir, extension, depth );
898 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
900 const char* name = reinterpret_cast<const char*>( ( *i ).data );
901 if ( extension_equal( path_get_extension( name ), extension ) ) {
906 ClearFileDirList( &list );
908 GSList* getDirList( const char *basedir ){
909 return GetDirList( basedir, 1 );
911 GSList* getFileList( const char *basedir, const char *extension ){
912 return GetFileList( basedir, extension, 1 );
914 void clearFileDirList( GSList **lst ){
915 ClearFileDirList( lst );
918 const char* findFile( const char *name ){
919 return FindFile( name );
921 const char* findRoot( const char *name ){
922 return FindPath( name );
925 void attach( ModuleObserver& observer ){
926 g_observers.attach( observer );
928 void detach( ModuleObserver& observer ){
929 g_observers.detach( observer );
932 Archive* getArchive( const char* archiveName, bool pakonly ){
933 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
935 if ( pakonly && !( *i ).is_pakfile ) {
939 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
940 return ( *i ).archive;
945 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
947 g_archives.reverse();
950 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
952 if ( pakonly && !( *i ).is_pakfile ) {
956 callback( ( *i ).name.c_str() );
960 g_archives.reverse();
966 Quake3FileSystem g_Quake3FileSystem;
968 VirtualFileSystem& GetFileSystem(){
969 return g_Quake3FileSystem;
972 void FileSystem_Init(){
975 void FileSystem_Shutdown(){