]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - plugins/vfspk3/vfs.cpp
3806ae60b5d909a4e55e9f957ab7f635129043c2
[xonotic/netradiant.git] / plugins / vfspk3 / vfs.cpp
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 #include "globaldefs.h"
46
47 #include <stdio.h>
48 #include <stdlib.h>
49 #include <glib.h>
50
51 #include "qerplugin.h"
52 #include "idatastream.h"
53 #include "iarchive.h"
54 ArchiveModules& FileSystemQ3API_getArchiveModules();
55 #include "ifilesystem.h"
56
57 #include "generic/callback.h"
58 #include "string/string.h"
59 #include "stream/stringstream.h"
60 #include "os/path.h"
61 #include "moduleobservers.h"
62 #include "filematch.h"
63 #include "dpkdeps.h"
64
65
66 const int VFS_MAXDIRS = 64;
67
68 #if GDEF_OS_WINDOWS
69 #define PATH_MAX 260
70 #endif
71
72 #define gamemode_get GlobalRadiant().getGameMode
73
74
75
76 // =============================================================================
77 // Global variables
78
79 Archive* OpenArchive( const char* name );
80
81 struct archive_entry_t
82 {
83         CopiedString name;
84         Archive* archive;
85         bool is_pakfile;
86 };
87
88 #include <list>
89 #include <map>
90
91 typedef std::list<archive_entry_t> archives_t;
92
93 static archives_t g_archives;
94 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
95 static int g_numDirs;
96 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
97 static int g_numForbiddenDirs = 0;
98 static bool g_bUsePak = true;
99
100 ModuleObservers g_observers;
101
102 // =============================================================================
103 // Static functions
104
105 static void AddSlash( char *str ){
106         std::size_t n = strlen( str );
107         if ( n > 0 ) {
108                 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
109                         globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
110                         strcat( str, "/" );
111                 }
112         }
113 }
114
115 static void FixDOSName( char *src ){
116         if ( src == 0 || strchr( src, '\\' ) == 0 ) {
117                 return;
118         }
119
120         globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
121
122         while ( *src )
123         {
124                 if ( *src == '\\' ) {
125                         *src = '/';
126                 }
127                 src++;
128         }
129 }
130
131 const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
132         StringOutputStream tmp( 16 );
133         tmp << LowerCase( ext );
134         return archiveModules.findModule( tmp.c_str() );
135 }
136
137 static Archive* InitPakFile( ArchiveModules& archiveModules, const char *filename ){
138         const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
139
140         if ( table != 0 ) {
141                 archive_entry_t entry;
142                 entry.name = filename;
143
144                 entry.archive = table->m_pfnOpenArchive( filename );
145                 entry.is_pakfile = true;
146                 g_archives.push_back( entry );
147                 globalOutputStream() << "pak file: " << filename << "\n";
148
149                 return entry.archive;
150         }
151
152         return 0;
153 }
154
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 );
158         }
159         else
160         {
161                 g_free( path );
162         }
163 }
164
165 class DirectoryListVisitor : public Archive::Visitor
166 {
167 GSList*& m_matches;
168 const char* m_directory;
169 public:
170 DirectoryListVisitor( GSList*& matches, const char* directory )
171         : m_matches( matches ), m_directory( directory )
172 {}
173 void visit( const char* name ){
174         const char* subname = path_make_relative( name, m_directory );
175         if ( subname != name ) {
176                 if ( subname[0] == '/' ) {
177                         ++subname;
178                 }
179                 char* dir = g_strdup( subname );
180                 char* last_char = dir + strlen( dir );
181                 if ( last_char != dir && *( --last_char ) == '/' ) {
182                         *last_char = '\0';
183                 }
184                 pathlist_prepend_unique( m_matches, dir );
185         }
186 }
187 };
188
189 class FileListVisitor : public Archive::Visitor
190 {
191 GSList*& m_matches;
192 const char* m_directory;
193 const char* m_extension;
194 public:
195 FileListVisitor( GSList*& matches, const char* directory, const char* extension )
196         : m_matches( matches ), m_directory( directory ), m_extension( extension )
197 {}
198 void visit( const char* name ){
199         const char* subname = path_make_relative( name, m_directory );
200         if ( subname != name ) {
201                 if ( subname[0] == '/' ) {
202                         ++subname;
203                 }
204                 if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
205                         pathlist_prepend_unique( m_matches, g_strdup( subname ) );
206                 }
207         }
208 }
209 };
210
211 static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
212         GSList* files = 0;
213
214         ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
215
216         if ( directories ) {
217                 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
218                 {
219                         DirectoryListVisitor visitor( files, refdir );
220                         ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
221                 }
222         }
223         else
224         {
225                 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
226                 {
227                         FileListVisitor visitor( files, refdir, ext );
228                         ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
229                 }
230         }
231
232         files = g_slist_reverse( files );
233
234         return files;
235 }
236
237 inline int ascii_to_upper( int c ){
238         if ( c >= 'a' && c <= 'z' ) {
239                 return c - ( 'a' - 'A' );
240         }
241         return c;
242 }
243
244 /*!
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.
249  */
250 static int string_compare_nocase_upper( const char* a, const char* b ){
251         for (;; )
252         {
253                 int c1 = ascii_to_upper( *a++ );
254                 int c2 = ascii_to_upper( *b++ );
255
256                 if ( c1 < c2 ) {
257                         return -1; // a < b
258                 }
259                 if ( c1 > c2 ) {
260                         return 1; // a > b
261                 }
262                 if ( c1 == 0 ) {
263                         return 0; // a == b
264                 }
265         }
266 }
267
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).
273
274 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
275 class PakLess
276 {
277 public:
278 bool operator()( const CopiedString& self, const CopiedString& other ) const {
279         return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
280 }
281 };
282
283 typedef std::set<CopiedString, PakLess> Archives;
284
285 Archive* AddPakDir( const char* fullpath ){
286         if ( g_numDirs == VFS_MAXDIRS ) return 0;
287
288         globalOutputStream() << "pak directory: " << fullpath << "\n";
289         strncpy( g_strDirs[g_numDirs], fullpath, PATH_MAX );
290         g_strDirs[g_numDirs][PATH_MAX] = '\0';
291         g_numDirs++;
292
293         {
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 );
299
300                 return entry.archive;
301         }
302 }
303
304 // for Daemon DPK VFS
305
306 Archive* AddDpkDir( const char* fullpath ){
307         return AddPakDir( fullpath );
308 }
309
310 struct pakfile_path_t
311 {
312         CopiedString fullpath;  // full pak dir or pk3dir name
313         bool is_pakfile;  // tells it is .pk3dir or .pk3 file
314 };
315
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
318
319 static PakfilePaths g_pakfile_paths;
320
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 ) );
326 }
327
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;
332         const char* pakname;
333         const char* pakversion;
334         int namelen = string_length( name );
335
336         for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i )
337         {
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;
343                         result = pakname;
344                 }
345         }
346         return result;
347 }
348
349 // release string after using
350 static char* GetCurrentMapDpkPakName(){
351         char* mapdir;
352         char* mapname;
353         int mapnamelen;
354         char* result = 0;
355
356         mapname = string_clone( GlobalRadiant().getMapName() );
357         mapnamelen = string_length( mapname );
358
359         mapdir = strrchr( mapname, '/' );
360         if ( mapdir ) {
361                 mapdir -= 12;
362                 if ( strncmp( mapdir, ".dpkdir/maps/", 13 ) == 0 ) {
363                         *mapdir = '\0';
364                         mapdir = strrchr( mapname, '/' );
365                         if ( mapdir ) mapdir++;
366                         else mapdir = mapname;
367                         result = string_clone( mapdir );
368                 }
369         }
370
371         string_release( mapname, mapnamelen );
372         return result;
373
374 }
375
376 // prevent loading duplicates or circular references
377 static Archives g_loaded_dpk_paks;
378
379 // actual pak adding on initialise, deferred from InitDirectory
380 // Daemon DPK filesystem doesn't need load all paks it finds
381 static void LoadDpkPakWithDeps( const char* pakname ){
382         Archive* arc;
383         ArchiveTextFile* depsFile;
384
385         if (pakname == NULL) {
386                 // load DEPS from game pack
387                 StringOutputStream baseDirectory( 256 );
388                 const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
389                 baseDirectory << GlobalRadiant().getGameToolsPath() << basegame << '/';
390                 arc = AddDpkDir( baseDirectory.c_str() );
391                 depsFile = arc->openTextFile( "DEPS" );
392         } else {
393                 const char* und = strrchr( pakname, '_' );
394                 if ( !und ) {
395                         pakname = GetLatestDpkPakVersion( pakname );
396                 }
397                 if ( !pakname || g_loaded_dpk_paks.find( pakname ) != g_loaded_dpk_paks.end() ) {
398                         return;
399                 }
400
401                 PakfilePaths::iterator i = g_pakfile_paths.find( pakname );
402                 if ( i == g_pakfile_paths.end() ) {
403                         return;
404                 }
405
406                 if ( i->second.is_pakfile ){
407                         arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() );
408                 } else {
409                         arc = AddDpkDir( i->second.fullpath.c_str() );
410                 }
411                 g_loaded_dpk_paks.insert( pakname );
412
413                 depsFile = arc->openTextFile( "DEPS" );
414         }
415
416         if ( !depsFile ) {
417                 return;
418         }
419
420         {
421                 TextLinesInputStream<TextInputStream> istream = depsFile->getInputStream();
422
423                 CopiedString line;
424                 char *p_name;
425                 char *p_version;
426                 while ( line = istream.readLine(), string_length( line.c_str() ) ) {
427                         if ( !DpkReadDepsLine( line.c_str(), &p_name, &p_version ) ) continue;
428                         if ( !p_version ) {
429                                 const char* p_latest = GetLatestDpkPakVersion( p_name );
430                                 if ( p_latest ) LoadDpkPakWithDeps( p_latest );
431                         } else {
432                                 int len = string_length( p_name ) + string_length( p_version ) + 1;
433                                 char* p_pakname = string_new( len );
434                                 sprintf( p_pakname, "%s_%s", p_name, p_version );
435                                 LoadDpkPakWithDeps( p_pakname );
436                                 string_release( p_pakname, len );
437                         }
438                         string_release( p_name, string_length( p_name ) );
439                         if ( p_version ) string_release( p_version, string_length( p_version ) );
440                 }
441         }
442
443         depsFile->release();
444 }
445
446 // end for Daemon DPK vfs
447
448 // =============================================================================
449 // Global functions
450
451 // reads all pak files from a dir
452 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
453         int j;
454
455         g_numForbiddenDirs = 0;
456         StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
457
458         for ( j = 0; j < VFS_MAXDIRS; ++j )
459         {
460                 const char *t = st.getToken();
461                 if ( string_empty( t ) ) {
462                         break;
463                 }
464                 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
465                 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
466                 ++g_numForbiddenDirs;
467         }
468
469         for ( j = 0; j < g_numForbiddenDirs; ++j )
470         {
471                 char* dbuf = g_strdup( directory );
472                 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
473                         dbuf[strlen( dbuf ) - 1] = 0;
474                 }
475                 const char *p = strrchr( dbuf, '/' );
476                 p = ( p ? ( p + 1 ) : dbuf );
477                 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
478                         g_free( dbuf );
479                         break;
480                 }
481                 g_free( dbuf );
482         }
483
484         if ( j < g_numForbiddenDirs ) {
485                 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
486                 return;
487         }
488
489         if ( g_numDirs == VFS_MAXDIRS ) {
490                 return;
491         }
492
493         strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
494         g_strDirs[g_numDirs][PATH_MAX] = '\0';
495         FixDOSName( g_strDirs[g_numDirs] );
496         AddSlash( g_strDirs[g_numDirs] );
497
498         const char* path = g_strDirs[g_numDirs];
499
500         g_numDirs++;
501
502         {
503                 archive_entry_t entry;
504                 entry.name = path;
505                 entry.archive = OpenArchive( path );
506                 entry.is_pakfile = false;
507                 g_archives.push_back( entry );
508         }
509
510         if ( g_bUsePak ) {
511
512                 GDir* dir = g_dir_open( path, 0, 0 );
513
514                 if ( dir != NULL ) {
515                         globalOutputStream() << "vfs directory: " << path << "\n";
516
517                         Archives archives;
518                         Archives archivesOverride;
519                         const char* ignore_prefix = "";
520                         const char* override_prefix = "";
521                         bool is_wad_vfs, is_pak_vfs, is_pk3_vfs, is_pk4_vfs, is_dpk_vfs;
522
523                         is_wad_vfs = !!GetArchiveTable( archiveModules, "wad" );
524                         is_pak_vfs = !!GetArchiveTable( archiveModules, "pak" );
525                         is_pk3_vfs = !!GetArchiveTable( archiveModules, "pk3" );
526                         is_pk4_vfs = !!GetArchiveTable( archiveModules, "pk4" );
527                         is_dpk_vfs = !!GetArchiveTable( archiveModules, "dpk" );
528
529                         if ( is_dpk_vfs ) {
530                                 // See if we are in "sp" or "mp" mapping mode
531                                 const char* gamemode = gamemode_get();
532
533                                 if ( strcmp( gamemode, "sp" ) == 0 ) {
534                                         ignore_prefix = "mp_";
535                                         override_prefix = "sp_";
536                                 }
537                                 else if ( strcmp( gamemode, "mp" ) == 0 ) {
538                                         ignore_prefix = "sp_";
539                                         override_prefix = "mp_";
540                                 }
541                         }
542
543                         while ( true )
544                         {
545                                 const char* name = g_dir_read_name( dir );
546
547                                 if ( name == nullptr ) {
548                                         break;
549                                 }
550
551                                 for ( j = 0; j < g_numForbiddenDirs; ++j )
552                                 {
553                                         const char *p = strrchr( name, '/' );
554                                         p = ( p ? ( p + 1 ) : name );
555                                         if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
556                                                 break;
557                                         }
558                                 }
559
560                                 if ( j < g_numForbiddenDirs ) {
561                                         continue;
562                                 }
563
564                                 const char *ext = strrchr( name, '.' );
565                                 char tmppath[PATH_MAX];
566
567                                 if ( ext != nullptr ) {
568                                         if ( is_dpk_vfs && !string_compare_nocase_upper( ext, ".dpkdir" ) ) {
569                                                 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
570                                                 tmppath[PATH_MAX] = '\0';
571                                                 FixDOSName( tmppath );
572                                                 AddSlash( tmppath );
573                                                 AddDpkPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
574                                         }
575
576                                         else if ( ( is_wad_vfs && !string_compare_nocase_upper( ext, ".pakdir" ) )
577                                                 || ( is_pk3_vfs && !string_compare_nocase_upper( ext, ".pk3dir" ) )
578                                                 || ( is_pk4_vfs && !string_compare_nocase_upper( ext, ".pk4dir" ) ) ) {
579                                                 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
580                                                 tmppath[PATH_MAX] = '\0';
581                                                 FixDOSName( tmppath );
582                                                 AddSlash( tmppath );
583                                                 AddPakDir( tmppath );
584                                         }
585                                 }
586
587                                 // GetArchiveTable() needs "pk3" if ext is ".pk3"
588                                 if ( ( ext == nullptr ) || *( ext + 1 ) == '\0' || GetArchiveTable( archiveModules, ext + 1 ) == 0 ) {
589                                         continue;
590                                 }
591
592                                 // using the same kludge as in engine to ensure consistency
593                                 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
594                                         continue;
595                                 }
596
597                                 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
598                                         if ( !string_compare_nocase_upper( ext, ".dpk" ) ) {
599                                                 if ( is_dpk_vfs ) {
600                                                         archives.insert( name );
601                                                         continue;
602                                                 }
603                                         }
604                                         else {
605                                                 archivesOverride.insert( name );
606                                                 continue;
607                                         }
608                                 }
609
610                                 archives.insert( name );
611                         }
612
613                         g_dir_close( dir );
614
615                         // add the entries to the vfs
616                         char* fullpath;
617                         if ( is_dpk_vfs ) {
618                                 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) {
619                                         const char* name = i->c_str();
620                                         const char* ext = strrchr( name, '.' );
621                                         if ( !string_compare_nocase_upper( ext, ".dpk" ) )
622                                         {
623                                                 CopiedString name_final = CopiedString( StringRange( name, ext ) );
624                                                 fullpath = string_new_concat( path, name );
625                                                 AddDpkPak( name_final.c_str(), fullpath, true );
626                                                 string_release( fullpath, string_length( fullpath ) );
627                                         }
628                                 }
629                         }
630                         else
631                         {
632                                 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
633                                 {
634                                         const char* name = i->c_str();
635                                         const char* ext = strrchr( name, '.' );
636                                         if ( ( is_wad_vfs && !string_compare_nocase_upper( ext, ".wad" ) )
637                                                 || ( is_pak_vfs && !string_compare_nocase_upper( ext, ".pak" ) )
638                                                 || ( is_pk3_vfs && !string_compare_nocase_upper( ext, ".pk3" ) )
639                                                 || ( is_pk4_vfs && !string_compare_nocase_upper( ext, ".pk4" ) ) ) {
640                                                 fullpath = string_new_concat( path, i->c_str() );
641                                                 InitPakFile( archiveModules, fullpath );
642                                                 string_release( fullpath, string_length( fullpath ) );
643                                         }
644                                 }
645
646                                 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
647                                 {
648                                         const char* name = i->c_str();
649                                         const char* ext = strrchr( name, '.' );
650                                         if ( ( is_wad_vfs && !string_compare_nocase_upper( ext, ".wad" ) )
651                                                 || ( is_pak_vfs && !string_compare_nocase_upper( ext, ".pak" ) )
652                                                 || ( is_pk3_vfs && !string_compare_nocase_upper( ext, ".pk3" ) )
653                                                 || ( is_pk4_vfs && !string_compare_nocase_upper( ext, ".pk4" ) ) ) {
654                                                 fullpath = string_new_concat( path, i->c_str() );
655                                                 InitPakFile( archiveModules, fullpath );
656                                                 string_release( fullpath, string_length( fullpath ) );
657                                         }
658                                 }
659                         }
660                 }
661                 else
662                 {
663                         globalErrorStream() << "vfs directory not found: " << path << "\n";
664                 }
665         }
666 }
667
668 // frees all memory that we allocated
669 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
670 //   (for instance when modifying the project settings)
671 void Shutdown(){
672         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
673         {
674                 ( *i ).archive->release();
675         }
676         g_archives.clear();
677
678         g_numDirs = 0;
679         g_numForbiddenDirs = 0;
680
681         g_pakfile_paths.clear();
682         g_loaded_dpk_paks.clear();
683 }
684
685 const int VFS_SEARCH_PAK = 0x1;
686 const int VFS_SEARCH_DIR = 0x2;
687
688 int GetFileCount( const char *filename, int flag ){
689         int count = 0;
690         char fixed[PATH_MAX + 1];
691
692         strncpy( fixed, filename, PATH_MAX );
693         fixed[PATH_MAX] = '\0';
694         FixDOSName( fixed );
695
696         if ( !flag ) {
697                 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
698         }
699
700         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
701         {
702                 if ( (( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0)
703                          || (!( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0) ) {
704                         if ( ( *i ).archive->containsFile( fixed ) ) {
705                                 ++count;
706                         }
707                 }
708         }
709
710         return count;
711 }
712
713 ArchiveFile* OpenFile( const char* filename ){
714         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
715         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
716         {
717                 ArchiveFile* file = ( *i ).archive->openFile( filename );
718                 if ( file != 0 ) {
719                         return file;
720                 }
721         }
722
723         return 0;
724 }
725
726 ArchiveTextFile* OpenTextFile( const char* filename ){
727         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
728         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
729         {
730                 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
731                 if ( file != 0 ) {
732                         return file;
733                 }
734         }
735
736         return 0;
737 }
738
739 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
740 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
741         char fixed[PATH_MAX + 1];
742
743         strncpy( fixed, filename, PATH_MAX );
744         fixed[PATH_MAX] = '\0';
745         FixDOSName( fixed );
746
747         ArchiveFile* file = OpenFile( fixed );
748
749         if ( file != 0 ) {
750                 *bufferptr = malloc( file->size() + 1 );
751                 // we need to end the buffer with a 0
752                 ( (char*) ( *bufferptr ) )[file->size()] = 0;
753
754                 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
755                 file->release();
756                 return length;
757         }
758
759         *bufferptr = 0;
760         return 0;
761 }
762
763 void FreeFile( void *p ){
764         free( p );
765 }
766
767 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
768         return GetListInternal( dir, ext, false, depth );
769 }
770
771 GSList* GetDirList( const char *dir, std::size_t depth ){
772         return GetListInternal( dir, 0, true, depth );
773 }
774
775 void ClearFileDirList( GSList **lst ){
776         while ( *lst )
777         {
778                 g_free( ( *lst )->data );
779                 *lst = g_slist_remove( *lst, ( *lst )->data );
780         }
781 }
782
783 const char* FindFile( const char* relative ){
784         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
785         {
786                 if ( ( *i ).archive->containsFile( relative ) ) {
787                         return ( *i ).name.c_str();
788                 }
789         }
790
791         return "";
792 }
793
794 const char* FindPath( const char* absolute ){
795         const char *best = "";
796         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
797         {
798                 if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
799                         if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
800                                 best = ( *i ).name.c_str();
801                         }
802                 }
803         }
804
805         return best;
806 }
807
808
809 class Quake3FileSystem : public VirtualFileSystem
810 {
811 public:
812 void initDirectory( const char *path ){
813         InitDirectory( path, FileSystemQ3API_getArchiveModules() );
814 }
815 void initialise(){
816         load();
817         globalOutputStream() << "filesystem initialised\n";
818         g_observers.realise();
819 }
820
821 void load(){
822         ArchiveModules& archiveModules = FileSystemQ3API_getArchiveModules();
823         bool is_dpk_vfs = !!GetArchiveTable( archiveModules, "dpk" );
824
825         if ( is_dpk_vfs ) {
826                 const char* pakname;
827                 g_loaded_dpk_paks.clear();
828
829                 // Load DEPS from game pack
830                 LoadDpkPakWithDeps( NULL );
831
832                 // prevent VFS double start, for MapName="" and MapName="unnamed.map"
833                 if ( string_length( GlobalRadiant().getMapName() ) ){
834                         // load map's paks from DEPS
835                         char* mappakname = GetCurrentMapDpkPakName();
836                         if ( mappakname != NULL ) {
837                                 LoadDpkPakWithDeps( mappakname );
838                                 string_release( mappakname, string_length( mappakname ) );
839                         }
840                 }
841
842                 g_pakfile_paths.clear();
843                 g_loaded_dpk_paks.clear();
844         }
845 }
846
847 void clear() {
848         // like shutdown() but does not unrealise (keep map etc.)
849         Shutdown();
850 }
851
852 void refresh(){
853         // like initialise() but does not realise (keep map etc.)
854         load();
855         globalOutputStream() << "filesystem refreshed\n";
856 }
857
858 void shutdown(){
859         g_observers.unrealise();
860         globalOutputStream() << "filesystem shutdown\n";
861         Shutdown();
862 }
863
864 int getFileCount( const char *filename, int flags ){
865         return GetFileCount( filename, flags );
866 }
867 ArchiveFile* openFile( const char* filename ){
868         return OpenFile( filename );
869 }
870 ArchiveTextFile* openTextFile( const char* filename ){
871         return OpenTextFile( filename );
872 }
873 std::size_t loadFile( const char *filename, void **buffer ){
874         return LoadFile( filename, buffer, 0 );
875 }
876 void freeFile( void *p ){
877         FreeFile( p );
878 }
879
880 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
881         GSList* list = GetDirList( basedir, depth );
882
883         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
884         {
885                 callback( reinterpret_cast<const char*>( ( *i ).data ) );
886         }
887
888         ClearFileDirList( &list );
889 }
890 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
891         GSList* list = GetFileList( basedir, extension, depth );
892
893         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
894         {
895                 const char* name = reinterpret_cast<const char*>( ( *i ).data );
896                 if ( extension_equal( path_get_extension( name ), extension ) ) {
897                         callback( name );
898                 }
899         }
900
901         ClearFileDirList( &list );
902 }
903 GSList* getDirList( const char *basedir ){
904         return GetDirList( basedir, 1 );
905 }
906 GSList* getFileList( const char *basedir, const char *extension ){
907         return GetFileList( basedir, extension, 1 );
908 }
909 void clearFileDirList( GSList **lst ){
910         ClearFileDirList( lst );
911 }
912
913 const char* findFile( const char *name ){
914         return FindFile( name );
915 }
916 const char* findRoot( const char *name ){
917         return FindPath( name );
918 }
919
920 void attach( ModuleObserver& observer ){
921         g_observers.attach( observer );
922 }
923 void detach( ModuleObserver& observer ){
924         g_observers.detach( observer );
925 }
926
927 Archive* getArchive( const char* archiveName, bool pakonly ){
928         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
929         {
930                 if ( pakonly && !( *i ).is_pakfile ) {
931                         continue;
932                 }
933
934                 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
935                         return ( *i ).archive;
936                 }
937         }
938         return 0;
939 }
940 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
941         if ( reverse ) {
942                 g_archives.reverse();
943         }
944
945         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
946         {
947                 if ( pakonly && !( *i ).is_pakfile ) {
948                         continue;
949                 }
950
951                 callback( ( *i ).name.c_str() );
952         }
953
954         if ( reverse ) {
955                 g_archives.reverse();
956         }
957 }
958 };
959
960
961 Quake3FileSystem g_Quake3FileSystem;
962
963 VirtualFileSystem& GetFileSystem(){
964         return g_Quake3FileSystem;
965 }
966
967 void FileSystem_Init(){
968 }
969
970 void FileSystem_Shutdown(){
971 }