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
88 typedef std::list<archive_entry_t> archives_t;
90 static archives_t g_archives;
91 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
93 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
94 static int g_numForbiddenDirs = 0;
95 static bool g_bUsePak = true;
97 ModuleObservers g_observers;
99 // =============================================================================
102 static void AddSlash( char *str ){
103 std::size_t n = strlen( str );
105 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
106 globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
112 static void FixDOSName( char *src ){
113 if ( src == 0 || strchr( src, '\\' ) == 0 ) {
117 globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
121 if ( *src == '\\' ) {
130 const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
131 StringOutputStream tmp( 16 );
132 tmp << LowerCase( ext );
133 return archiveModules.findModule( tmp.c_str() );
135 static void InitPakFile( ArchiveModules& archiveModules, const char *filename ){
136 const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
139 archive_entry_t entry;
140 entry.name = filename;
142 entry.archive = table->m_pfnOpenArchive( filename );
143 entry.is_pakfile = true;
144 g_archives.push_back( entry );
145 globalOutputStream() << " pak file: " << filename << "\n";
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 );
159 class DirectoryListVisitor : public Archive::Visitor
162 const char* m_directory;
164 DirectoryListVisitor( GSList*& matches, const char* directory )
165 : m_matches( matches ), m_directory( directory )
167 void visit( const char* name ){
168 const char* subname = path_make_relative( name, m_directory );
169 if ( subname != name ) {
170 if ( subname[0] == '/' ) {
173 char* dir = g_strdup( subname );
174 char* last_char = dir + strlen( dir );
175 if ( last_char != dir && *( --last_char ) == '/' ) {
178 pathlist_prepend_unique( m_matches, dir );
183 class FileListVisitor : public Archive::Visitor
186 const char* m_directory;
187 const char* m_extension;
189 FileListVisitor( GSList*& matches, const char* directory, const char* extension )
190 : m_matches( matches ), m_directory( directory ), m_extension( extension )
192 void visit( const char* name ){
193 const char* subname = path_make_relative( name, m_directory );
194 if ( subname != name ) {
195 if ( subname[0] == '/' ) {
198 if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
199 pathlist_prepend_unique( m_matches, g_strdup( subname ) );
205 static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
208 ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
211 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
213 DirectoryListVisitor visitor( files, refdir );
214 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
219 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
221 FileListVisitor visitor( files, refdir, ext );
222 ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
226 files = g_slist_reverse( files );
231 inline int ascii_to_upper( int c ){
232 if ( c >= 'a' && c <= 'z' ) {
233 return c - ( 'a' - 'A' );
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.
244 static int string_compare_nocase_upper( const char* a, const char* b ){
247 int c1 = ascii_to_upper( *a++ );
248 int c2 = ascii_to_upper( *b++ );
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).
268 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
272 bool operator()( const CopiedString& self, const CopiedString& other ) const {
273 return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
277 typedef std::set<CopiedString, PakLess> Archives;
279 // =============================================================================
282 // reads all pak files from a dir
283 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
286 g_numForbiddenDirs = 0;
287 StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
288 for ( j = 0; j < VFS_MAXDIRS; ++j )
290 const char *t = st.getToken();
291 if ( string_empty( t ) ) {
294 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
295 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
296 ++g_numForbiddenDirs;
299 for ( j = 0; j < g_numForbiddenDirs; ++j )
301 char* dbuf = g_strdup( directory );
302 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
303 dbuf[strlen( dbuf ) - 1] = 0;
305 const char *p = strrchr( dbuf, '/' );
306 p = ( p ? ( p + 1 ) : dbuf );
307 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
313 if ( j < g_numForbiddenDirs ) {
314 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
318 if ( g_numDirs == VFS_MAXDIRS ) {
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] );
327 const char* path = g_strDirs[g_numDirs];
332 archive_entry_t entry;
334 entry.archive = OpenArchive( path );
335 entry.is_pakfile = false;
336 g_archives.push_back( entry );
340 GDir* dir = g_dir_open( path, 0, 0 );
343 globalOutputStream() << "vfs directory: " << path << "\n";
345 const char* ignore_prefix = "";
346 const char* override_prefix = "";
349 // See if we are in "sp" or "mp" mapping mode
350 const char* gamemode = gamemode_get();
352 if ( strcmp( gamemode, "sp" ) == 0 ) {
353 ignore_prefix = "mp_";
354 override_prefix = "sp_";
356 else if ( strcmp( gamemode, "mp" ) == 0 ) {
357 ignore_prefix = "sp_";
358 override_prefix = "mp_";
363 Archives archivesOverride;
366 const char* name = g_dir_read_name( dir );
371 for ( j = 0; j < g_numForbiddenDirs; ++j )
373 const char *p = strrchr( name, '/' );
374 p = ( p ? ( p + 1 ) : name );
375 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
379 if ( j < g_numForbiddenDirs ) {
383 const char *ext = strrchr( name, '.' );
385 if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
386 if ( g_numDirs == VFS_MAXDIRS ) {
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] );
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 );
404 if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
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 ) {
412 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
413 archivesOverride.insert( name );
417 archives.insert( name );
422 // add the entries to the vfs
423 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
425 char filename[PATH_MAX];
426 strcpy( filename, path );
427 strcat( filename, ( *i ).c_str() );
428 InitPakFile( archiveModules, filename );
430 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
432 char filename[PATH_MAX];
433 strcpy( filename, path );
434 strcat( filename, ( *i ).c_str() );
435 InitPakFile( archiveModules, filename );
440 globalErrorStream() << "vfs directory not found: " << path << "\n";
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)
449 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
451 ( *i ).archive->release();
456 g_numForbiddenDirs = 0;
459 #define VFS_SEARCH_PAK 0x1
460 #define VFS_SEARCH_DIR 0x2
462 int GetFileCount( const char *filename, int flag ){
464 char fixed[PATH_MAX + 1];
466 strncpy( fixed, filename, PATH_MAX );
467 fixed[PATH_MAX] = '\0';
471 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
474 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
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 ) ) {
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 )
491 ArchiveFile* file = ( *i ).archive->openFile( 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 )
504 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
513 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
514 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
515 char fixed[PATH_MAX + 1];
517 strncpy( fixed, filename, PATH_MAX );
518 fixed[PATH_MAX] = '\0';
521 ArchiveFile* file = OpenFile( fixed );
524 *bufferptr = malloc( file->size() + 1 );
525 // we need to end the buffer with a 0
526 ( (char*) ( *bufferptr ) )[file->size()] = 0;
528 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
537 void FreeFile( void *p ){
541 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
542 return GetListInternal( dir, ext, false, depth );
545 GSList* GetDirList( const char *dir, std::size_t depth ){
546 return GetListInternal( dir, 0, true, depth );
549 void ClearFileDirList( GSList **lst ){
552 g_free( ( *lst )->data );
553 *lst = g_slist_remove( *lst, ( *lst )->data );
557 const char* FindFile( const char* relative ){
558 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
560 if ( ( *i ).archive->containsFile( relative ) ) {
561 return ( *i ).name.c_str();
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 )
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();
583 class Quake3FileSystem : public VirtualFileSystem
586 void initDirectory( const char *path ){
587 InitDirectory( path, FileSystemQ3API_getArchiveModules() );
590 globalOutputStream() << "filesystem initialised\n";
591 g_observers.realise();
594 g_observers.unrealise();
595 globalOutputStream() << "filesystem shutdown\n";
599 int getFileCount( const char *filename, int flags ){
600 return GetFileCount( filename, flags );
602 ArchiveFile* openFile( const char* filename ){
603 return OpenFile( filename );
605 ArchiveTextFile* openTextFile( const char* filename ){
606 return OpenTextFile( filename );
608 std::size_t loadFile( const char *filename, void **buffer ){
609 return LoadFile( filename, buffer, 0 );
611 void freeFile( void *p ){
615 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
616 GSList* list = GetDirList( basedir, depth );
618 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
620 callback( reinterpret_cast<const char*>( ( *i ).data ) );
623 ClearFileDirList( &list );
625 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
626 GSList* list = GetFileList( basedir, extension, depth );
628 for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
630 const char* name = reinterpret_cast<const char*>( ( *i ).data );
631 if ( extension_equal( path_get_extension( name ), extension ) ) {
636 ClearFileDirList( &list );
638 GSList* getDirList( const char *basedir ){
639 return GetDirList( basedir, 1 );
641 GSList* getFileList( const char *basedir, const char *extension ){
642 return GetFileList( basedir, extension, 1 );
644 void clearFileDirList( GSList **lst ){
645 ClearFileDirList( lst );
648 const char* findFile( const char *name ){
649 return FindFile( name );
651 const char* findRoot( const char *name ){
652 return FindPath( name );
655 void attach( ModuleObserver& observer ){
656 g_observers.attach( observer );
658 void detach( ModuleObserver& observer ){
659 g_observers.detach( observer );
662 Archive* getArchive( const char* archiveName, bool pakonly ){
663 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
665 if ( pakonly && !( *i ).is_pakfile ) {
669 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
670 return ( *i ).archive;
675 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
677 g_archives.reverse();
680 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
682 if ( pakonly && !( *i ).is_pakfile ) {
686 callback( ( *i ).name.c_str() );
690 g_archives.reverse();
695 Quake3FileSystem g_Quake3FileSystem;
697 void FileSystem_Init(){
700 void FileSystem_Shutdown(){
703 VirtualFileSystem& GetFileSystem(){
704 return g_Quake3FileSystem;