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)
45 #include "globaldefs.h"
51 #include "qerplugin.h"
52 #include "idatastream.h"
54 ArchiveModules& FileSystemQ3API_getArchiveModules();
55 #include "ifilesystem.h"
57 #include "generic/callback.h"
58 #include "string/string.h"
59 #include "stream/stringstream.h"
61 #include "moduleobservers.h"
62 #include "filematch.h"
66 const int VFS_MAXDIRS = 64;
72 #define gamemode_get GlobalRadiant().getGameMode
76 // =============================================================================
79 Archive* OpenArchive( const char* name );
81 struct archive_entry_t
91 typedef std::list<archive_entry_t> archives_t;
93 static archives_t g_archives;
94 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
96 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
97 static int g_numForbiddenDirs = 0;
98 static bool g_bUsePak = true;
100 ModuleObservers g_observers;
102 // =============================================================================
105 static void AddSlash( char *str ){
106 std::size_t n = strlen( str );
108 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
109 globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
115 static void FixDOSName( char *src ){
116 if ( src == 0 || strchr( src, '\\' ) == 0 ) {
120 globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
124 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() );
137 static Archive* InitPakFile( ArchiveModules& archiveModules, const char *filename ){
138 const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
141 archive_entry_t entry;
142 entry.name = filename;
144 entry.archive = table->m_pfnOpenArchive( filename );
145 entry.is_pakfile = true;
146 g_archives.push_back( entry );
147 globalOutputStream() << "pak file: " << filename << "\n";
149 return entry.archive;
155 inline void pathlist_prepend_unique( GSList*& pathlist, char* path ){
156 if ( g_slist_find_custom( pathlist, path, (GCompareFunc)path_compare ) == 0 ) {
157 pathlist = g_slist_prepend( pathlist, path );
165 class DirectoryListVisitor : public Archive::Visitor
168 const char* m_directory;
170 DirectoryListVisitor( GSList*& matches, const char* directory )
171 : m_matches( matches ), m_directory( directory )
173 void visit( const char* name ){
174 const char* subname = path_make_relative( name, m_directory );
175 if ( subname != name ) {
176 if ( subname[0] == '/' ) {
179 char* dir = g_strdup( subname );
180 char* last_char = dir + strlen( dir );
181 if ( last_char != dir && *( --last_char ) == '/' ) {
184 pathlist_prepend_unique( m_matches, dir );
189 class FileListVisitor : public Archive::Visitor
192 const char* m_directory;
193 const char* m_extension;
195 FileListVisitor( GSList*& matches, const char* directory, const char* extension )
196 : m_matches( matches ), m_directory( directory ), m_extension( extension )
198 void visit( const char* name ){
199 const char* subname = path_make_relative( name, m_directory );
200 if ( subname != name ) {
201 if ( subname[0] == '/' ) {
204 if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
205 pathlist_prepend_unique( m_matches, g_strdup( subname ) );
211 static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
214 ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
217 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
219 DirectoryListVisitor visitor( files, refdir );
220 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
225 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
227 FileListVisitor visitor( files, refdir, ext );
228 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
232 files = g_slist_reverse( files );
237 inline int ascii_to_upper( int c ){
238 if ( c >= 'a' && c <= 'z' ) {
239 return c - ( 'a' - 'A' );
245 This behaves identically to stricmp(a,b), except that ASCII chars
246 [\]^`_ come AFTER alphabet chars instead of before. This is because
247 it converts all alphabet chars to uppercase before comparison,
248 while stricmp converts them to lowercase.
250 static int string_compare_nocase_upper( const char* a, const char* b ){
253 int c1 = ascii_to_upper( *a++ );
254 int c2 = ascii_to_upper( *b++ );
268 // Arnout: note - sort pakfiles in reverse order. This ensures that
269 // later pakfiles override earlier ones. This because the vfs module
270 // returns a filehandle to the first file it can find (while it should
271 // return the filehandle to the file in the most overriding pakfile, the
272 // last one in the list that is).
274 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
278 bool operator()( const CopiedString& self, const CopiedString& other ) const {
279 return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
283 typedef std::set<CopiedString, PakLess> Archives;
285 Archive* AddPakDir( const char* fullpath ){
286 if ( g_numDirs == VFS_MAXDIRS ) return 0;
288 globalOutputStream() << "pak directory: " << fullpath << "\n";
289 strncpy( g_strDirs[g_numDirs], fullpath, PATH_MAX );
290 g_strDirs[g_numDirs][PATH_MAX] = '\0';
294 archive_entry_t entry;
295 entry.name = fullpath;
296 entry.archive = OpenArchive( fullpath );
297 entry.is_pakfile = false;
298 g_archives.push_back( entry );
300 return entry.archive;
304 // for Daemon DPK VFS
306 Archive* AddDpkDir( const char* fullpath ){
307 return AddPakDir( fullpath );
310 struct pakfile_path_t
312 CopiedString fullpath; // full pak dir or pk3dir name
313 bool is_pakfile; // tells it is .pk3dir or .pk3 file
316 typedef std::pair<CopiedString, pakfile_path_t> PakfilePathsKV;
317 typedef std::map<CopiedString, pakfile_path_t> PakfilePaths; // key must have no extension, only name
319 static PakfilePaths g_pakfile_paths;
321 void AddDpkPak( const char* name, const char* fullpath, bool is_pakfile ){
322 pakfile_path_t pakfile_path;
323 pakfile_path.fullpath = fullpath;
324 pakfile_path.is_pakfile = is_pakfile;
325 g_pakfile_paths.insert( PakfilePathsKV( name, pakfile_path ) );
328 // takes name without ext, returns without ext
329 static const char* GetLatestDpkPakVersion( const char* name ){
330 const char* maxversion = 0;
331 const char* result = 0;
333 const char* pakversion;
334 int namelen = string_length( name );
336 for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i )
338 pakname = i->first.c_str();
339 if ( strncmp( pakname, name, namelen ) != 0 || pakname[namelen] != '_' ) continue;
340 pakversion = pakname + (namelen + 1);
341 if ( maxversion == 0 || DpkPakVersionCmp( pakversion, maxversion ) > 0 ){
342 maxversion = pakversion;
349 // release string after using
350 // Note: it also contains the version string,
351 // for …/src/map-castle_src.dpkdir/maps/castle.map
352 // it will return map-castle_src
353 static char* GetCurrentMapDpkPakName(){
359 mapname = string_clone( GlobalRadiant().getMapName() );
360 mapnamelen = string_length( mapname );
362 char pattern[] = ".dpkdir/";
363 char* end = strstr( mapname, ".dpkdir/" );
368 mapdir = strrchr( mapname, '/' );
378 result = string_clone( mapdir );
381 string_release( mapname, mapnamelen );
385 // prevent loading duplicates or circular references
386 static Archives g_loaded_dpk_paks;
388 // actual pak adding on initialise, deferred from InitDirectory
389 // Daemon DPK filesystem doesn't need load all paks it finds
390 static void LoadDpkPakWithDeps( const char* pakname ){
392 ArchiveTextFile* depsFile;
394 if (pakname == NULL) {
395 // load DEPS from game pack
396 StringOutputStream baseDirectory( 256 );
397 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
398 baseDirectory << GlobalRadiant().getGameToolsPath() << basegame << '/';
399 arc = AddDpkDir( baseDirectory.c_str() );
400 depsFile = arc->openTextFile( "DEPS" );
402 const char* und = strrchr( pakname, '_' );
404 pakname = GetLatestDpkPakVersion( pakname );
406 if ( !pakname || g_loaded_dpk_paks.find( pakname ) != g_loaded_dpk_paks.end() ) {
410 PakfilePaths::iterator i = g_pakfile_paths.find( pakname );
411 if ( i == g_pakfile_paths.end() ) {
415 if ( i->second.is_pakfile ){
416 arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() );
418 arc = AddDpkDir( i->second.fullpath.c_str() );
420 g_loaded_dpk_paks.insert( pakname );
422 depsFile = arc->openTextFile( "DEPS" );
430 TextLinesInputStream<TextInputStream> istream = depsFile->getInputStream();
435 while ( line = istream.readLine(), string_length( line.c_str() ) ) {
436 if ( !DpkReadDepsLine( line.c_str(), &p_name, &p_version ) ) continue;
438 const char* p_latest = GetLatestDpkPakVersion( p_name );
439 if ( p_latest ) LoadDpkPakWithDeps( p_latest );
441 int len = string_length( p_name ) + string_length( p_version ) + 1;
442 char* p_pakname = string_new( len );
443 sprintf( p_pakname, "%s_%s", p_name, p_version );
444 LoadDpkPakWithDeps( p_pakname );
445 string_release( p_pakname, len );
447 string_release( p_name, string_length( p_name ) );
448 if ( p_version ) string_release( p_version, string_length( p_version ) );
455 // end for Daemon DPK vfs
457 // =============================================================================
460 // reads all pak files from a dir
461 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
464 g_numForbiddenDirs = 0;
465 StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
467 for ( j = 0; j < VFS_MAXDIRS; ++j )
469 const char *t = st.getToken();
470 if ( string_empty( t ) ) {
473 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
474 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
475 ++g_numForbiddenDirs;
478 for ( j = 0; j < g_numForbiddenDirs; ++j )
480 char* dbuf = g_strdup( directory );
481 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
482 dbuf[strlen( dbuf ) - 1] = 0;
484 const char *p = strrchr( dbuf, '/' );
485 p = ( p ? ( p + 1 ) : dbuf );
486 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
493 if ( j < g_numForbiddenDirs ) {
494 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
498 if ( g_numDirs == VFS_MAXDIRS ) {
502 strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
503 g_strDirs[g_numDirs][PATH_MAX] = '\0';
504 FixDOSName( g_strDirs[g_numDirs] );
505 AddSlash( g_strDirs[g_numDirs] );
507 const char* path = g_strDirs[g_numDirs];
512 archive_entry_t entry;
514 entry.archive = OpenArchive( path );
515 entry.is_pakfile = false;
516 g_archives.push_back( entry );
521 GDir* dir = g_dir_open( path, 0, 0 );
524 globalOutputStream() << "vfs directory: " << path << "\n";
527 Archives archivesOverride;
528 const char* ignore_prefix = "";
529 const char* override_prefix = "";
530 bool is_wad_vfs, is_pak_vfs, is_pk3_vfs, is_pk4_vfs, is_dpk_vfs;
532 is_wad_vfs = !!GetArchiveTable( archiveModules, "wad" );
533 is_pak_vfs = !!GetArchiveTable( archiveModules, "pak" );
534 is_pk3_vfs = !!GetArchiveTable( archiveModules, "pk3" );
535 is_pk4_vfs = !!GetArchiveTable( archiveModules, "pk4" );
536 is_dpk_vfs = !!GetArchiveTable( archiveModules, "dpk" );
539 // See if we are in "sp" or "mp" mapping mode
540 const char* gamemode = gamemode_get();
542 if ( strcmp( gamemode, "sp" ) == 0 ) {
543 ignore_prefix = "mp_";
544 override_prefix = "sp_";
546 else if ( strcmp( gamemode, "mp" ) == 0 ) {
547 ignore_prefix = "sp_";
548 override_prefix = "mp_";
554 const char* name = g_dir_read_name( dir );
556 if ( name == nullptr ) {
560 for ( j = 0; j < g_numForbiddenDirs; ++j )
562 const char *p = strrchr( name, '/' );
563 p = ( p ? ( p + 1 ) : name );
564 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
569 if ( j < g_numForbiddenDirs ) {
573 const char *ext = strrchr( name, '.' );
574 char tmppath[PATH_MAX + 1];
576 if ( ext != nullptr ) {
577 if ( is_dpk_vfs && !string_compare_nocase_upper( ext, ".dpkdir" ) ) {
578 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
579 tmppath[PATH_MAX] = '\0';
580 FixDOSName( tmppath );
582 AddDpkPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
585 else if ( ( is_wad_vfs && !string_compare_nocase_upper( ext, ".pakdir" ) )
586 || ( is_pk3_vfs && !string_compare_nocase_upper( ext, ".pk3dir" ) )
587 || ( is_pk4_vfs && !string_compare_nocase_upper( ext, ".pk4dir" ) ) ) {
588 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
589 tmppath[PATH_MAX] = '\0';
590 FixDOSName( tmppath );
592 AddPakDir( tmppath );
596 // GetArchiveTable() needs "pk3" if ext is ".pk3"
597 if ( ( ext == nullptr ) || *( ext + 1 ) == '\0' || GetArchiveTable( archiveModules, ext + 1 ) == 0 ) {
601 // using the same kludge as in engine to ensure consistency
602 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
606 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
607 if ( !string_compare_nocase_upper( ext, ".dpk" ) ) {
609 archives.insert( name );
614 archivesOverride.insert( name );
619 archives.insert( name );
624 // add the entries to the vfs
627 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) {
628 const char* name = i->c_str();
629 const char* ext = strrchr( name, '.' );
630 if ( !string_compare_nocase_upper( ext, ".dpk" ) )
632 CopiedString name_final = CopiedString( StringRange( name, ext ) );
633 fullpath = string_new_concat( path, name );
634 AddDpkPak( name_final.c_str(), fullpath, true );
635 string_release( fullpath, string_length( fullpath ) );
641 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
643 const char* name = i->c_str();
644 const char* ext = strrchr( name, '.' );
645 if ( ( is_wad_vfs && !string_compare_nocase_upper( ext, ".wad" ) )
646 || ( is_pak_vfs && !string_compare_nocase_upper( ext, ".pak" ) )
647 || ( is_pk3_vfs && !string_compare_nocase_upper( ext, ".pk3" ) )
648 || ( is_pk4_vfs && !string_compare_nocase_upper( ext, ".pk4" ) ) ) {
649 fullpath = string_new_concat( path, i->c_str() );
650 InitPakFile( archiveModules, fullpath );
651 string_release( fullpath, string_length( fullpath ) );
655 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
657 const char* name = i->c_str();
658 const char* ext = strrchr( name, '.' );
659 if ( ( is_wad_vfs && !string_compare_nocase_upper( ext, ".wad" ) )
660 || ( is_pak_vfs && !string_compare_nocase_upper( ext, ".pak" ) )
661 || ( is_pk3_vfs && !string_compare_nocase_upper( ext, ".pk3" ) )
662 || ( is_pk4_vfs && !string_compare_nocase_upper( ext, ".pk4" ) ) ) {
663 fullpath = string_new_concat( path, i->c_str() );
664 InitPakFile( archiveModules, fullpath );
665 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_dpk_paks.clear();
694 const int VFS_SEARCH_PAK = 0x1;
695 const int 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() );
826 globalOutputStream() << "filesystem initialised\n";
827 g_observers.realise();
831 ArchiveModules& archiveModules = FileSystemQ3API_getArchiveModules();
832 bool is_dpk_vfs = !!GetArchiveTable( archiveModules, "dpk" );
836 g_loaded_dpk_paks.clear();
838 // Load DEPS from game pack
839 LoadDpkPakWithDeps( NULL );
841 // prevent VFS double start, for MapName="" and MapName="unnamed.map"
842 if ( string_length( GlobalRadiant().getMapName() ) ){
843 // load map's paks from DEPS
844 char* mappakname = GetCurrentMapDpkPakName();
845 if ( mappakname != NULL ) {
846 LoadDpkPakWithDeps( mappakname );
847 string_release( mappakname, string_length( mappakname ) );
851 g_pakfile_paths.clear();
852 g_loaded_dpk_paks.clear();
857 // like shutdown() but does not unrealise (keep map etc.)
862 // like initialise() but does not realise (keep map etc.)
864 globalOutputStream() << "filesystem refreshed\n";
868 g_observers.unrealise();
869 globalOutputStream() << "filesystem shutdown\n";
873 int getFileCount( const char *filename, int flags ){
874 return GetFileCount( filename, flags );
876 ArchiveFile* openFile( const char* filename ){
877 return OpenFile( filename );
879 ArchiveTextFile* openTextFile( const char* filename ){
880 return OpenTextFile( filename );
882 std::size_t loadFile( const char *filename, void **buffer ){
883 return LoadFile( filename, buffer, 0 );
885 void freeFile( void *p ){
889 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
890 GSList* list = GetDirList( basedir, depth );
892 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
894 callback( reinterpret_cast<const char*>( ( *i ).data ) );
897 ClearFileDirList( &list );
899 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
900 GSList* list = GetFileList( basedir, extension, depth );
902 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
904 const char* name = reinterpret_cast<const char*>( ( *i ).data );
905 if ( extension_equal( path_get_extension( name ), extension ) ) {
910 ClearFileDirList( &list );
912 GSList* getDirList( const char *basedir ){
913 return GetDirList( basedir, 1 );
915 GSList* getFileList( const char *basedir, const char *extension ){
916 return GetFileList( basedir, extension, 1 );
918 void clearFileDirList( GSList **lst ){
919 ClearFileDirList( lst );
922 const char* findFile( const char *name ){
923 return FindFile( name );
925 const char* findRoot( const char *name ){
926 return FindPath( name );
929 void attach( ModuleObserver& observer ){
930 g_observers.attach( observer );
932 void detach( ModuleObserver& observer ){
933 g_observers.detach( observer );
936 Archive* getArchive( const char* archiveName, bool pakonly ){
937 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
939 if ( pakonly && !( *i ).is_pakfile ) {
943 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
944 return ( *i ).archive;
946 else if ( path_equal( path_get_filename_start( ( *i ).name.c_str() ), archiveName ) ) {
947 return ( *i ).archive;
952 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
954 g_archives.reverse();
957 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
959 if ( pakonly && !( *i ).is_pakfile ) {
963 callback( ( *i ).name.c_str() );
967 g_archives.reverse();
973 Quake3FileSystem g_Quake3FileSystem;
975 VirtualFileSystem& GetFileSystem(){
976 return g_Quake3FileSystem;
979 void FileSystem_Init(){
982 void FileSystem_Shutdown(){