radiant: replace StringBuffer with std::string
[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         std::string tmp = ext;
133         transform(tmp.begin(), tmp.end(), tmp.begin(), ::tolower);
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 // 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(){
354         char* mapdir;
355         char* mapname;
356         int mapnamelen;
357         char* result = 0;
358
359         mapname = string_clone( GlobalRadiant().getMapName() );
360         mapnamelen = string_length( mapname );
361
362         char pattern[] = ".dpkdir/";
363         char* end = strstr( mapname, ".dpkdir/" );
364         if ( end )
365         {
366                 end[ 0 ] = '\0';
367
368                 mapdir = strrchr( mapname, '/' );
369                 if ( mapdir )
370                 {
371                         mapdir++;
372                 }
373                 else
374                 {
375                         mapdir = mapname;
376                 }
377
378                 result = string_clone( mapdir );
379         }
380
381         string_release( mapname, mapnamelen );
382         return result;
383 }
384
385 // prevent loading duplicates or circular references
386 static Archives g_loaded_dpk_paks;
387
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 ){
391         Archive* arc;
392         ArchiveTextFile* depsFile;
393
394         if (pakname == NULL) {
395                 // load DEPS from game pack
396                 std::string baseDirectory( GlobalRadiant().getGameToolsPath() );
397                 baseDirectory += GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" );
398                 baseDirectory += '/';
399                 arc = AddDpkDir( baseDirectory.c_str() );
400                 depsFile = arc->openTextFile( "DEPS" );
401         } else {
402                 const char* und = strrchr( pakname, '_' );
403                 if ( !und ) {
404                         pakname = GetLatestDpkPakVersion( pakname );
405                 }
406                 if ( !pakname || g_loaded_dpk_paks.find( pakname ) != g_loaded_dpk_paks.end() ) {
407                         return;
408                 }
409
410                 PakfilePaths::iterator i = g_pakfile_paths.find( pakname );
411                 if ( i == g_pakfile_paths.end() ) {
412                         return;
413                 }
414
415                 if ( i->second.is_pakfile ){
416                         arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() );
417                 } else {
418                         arc = AddDpkDir( i->second.fullpath.c_str() );
419                 }
420                 g_loaded_dpk_paks.insert( pakname );
421
422                 depsFile = arc->openTextFile( "DEPS" );
423         }
424
425         if ( !depsFile ) {
426                 return;
427         }
428
429         {
430                 TextLinesInputStream<TextInputStream> istream = depsFile->getInputStream();
431
432                 CopiedString line;
433                 char *p_name;
434                 char *p_version;
435                 while ( line = istream.readLine(), string_length( line.c_str() ) ) {
436                         if ( !DpkReadDepsLine( line.c_str(), &p_name, &p_version ) ) continue;
437                         if ( !p_version ) {
438                                 const char* p_latest = GetLatestDpkPakVersion( p_name );
439                                 if ( p_latest ) LoadDpkPakWithDeps( p_latest );
440                         } else {
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 );
446                         }
447                         string_release( p_name, string_length( p_name ) );
448                         if ( p_version ) string_release( p_version, string_length( p_version ) );
449                 }
450         }
451
452         depsFile->release();
453 }
454
455 // end for Daemon DPK vfs
456
457 // =============================================================================
458 // Global functions
459
460 // reads all pak files from a dir
461 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
462         int j;
463
464         g_numForbiddenDirs = 0;
465         StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
466
467         for ( j = 0; j < VFS_MAXDIRS; ++j )
468         {
469                 const char *t = st.getToken();
470                 if ( string_empty( t ) ) {
471                         break;
472                 }
473                 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
474                 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
475                 ++g_numForbiddenDirs;
476         }
477
478         for ( j = 0; j < g_numForbiddenDirs; ++j )
479         {
480                 char* dbuf = g_strdup( directory );
481                 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
482                         dbuf[strlen( dbuf ) - 1] = 0;
483                 }
484                 const char *p = strrchr( dbuf, '/' );
485                 p = ( p ? ( p + 1 ) : dbuf );
486                 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
487                         g_free( dbuf );
488                         break;
489                 }
490                 g_free( dbuf );
491         }
492
493         if ( j < g_numForbiddenDirs ) {
494                 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
495                 return;
496         }
497
498         if ( g_numDirs == VFS_MAXDIRS ) {
499                 return;
500         }
501
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] );
506
507         const char* path = g_strDirs[g_numDirs];
508
509         g_numDirs++;
510
511         {
512                 archive_entry_t entry;
513                 entry.name = path;
514                 entry.archive = OpenArchive( path );
515                 entry.is_pakfile = false;
516                 g_archives.push_back( entry );
517         }
518
519         if ( g_bUsePak ) {
520
521                 GDir* dir = g_dir_open( path, 0, 0 );
522
523                 if ( dir != NULL ) {
524                         globalOutputStream() << "vfs directory: " << path << "\n";
525
526                         Archives archives;
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;
531
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" );
537
538                         if ( is_dpk_vfs ) {
539                                 // See if we are in "sp" or "mp" mapping mode
540                                 const char* gamemode = gamemode_get();
541
542                                 if ( strcmp( gamemode, "sp" ) == 0 ) {
543                                         ignore_prefix = "mp_";
544                                         override_prefix = "sp_";
545                                 }
546                                 else if ( strcmp( gamemode, "mp" ) == 0 ) {
547                                         ignore_prefix = "sp_";
548                                         override_prefix = "mp_";
549                                 }
550                         }
551
552                         while ( true )
553                         {
554                                 const char* name = g_dir_read_name( dir );
555
556                                 if ( name == nullptr ) {
557                                         break;
558                                 }
559
560                                 for ( j = 0; j < g_numForbiddenDirs; ++j )
561                                 {
562                                         const char *p = strrchr( name, '/' );
563                                         p = ( p ? ( p + 1 ) : name );
564                                         if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
565                                                 break;
566                                         }
567                                 }
568
569                                 if ( j < g_numForbiddenDirs ) {
570                                         continue;
571                                 }
572
573                                 const char *ext = strrchr( name, '.' );
574                                 char tmppath[PATH_MAX + 1];
575
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 );
581                                                 AddSlash( tmppath );
582                                                 AddDpkPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
583                                         }
584
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 );
591                                                 AddSlash( tmppath );
592                                                 AddPakDir( tmppath );
593                                         }
594                                 }
595
596                                 // GetArchiveTable() needs "pk3" if ext is ".pk3"
597                                 if ( ( ext == nullptr ) || *( ext + 1 ) == '\0' || GetArchiveTable( archiveModules, ext + 1 ) == 0 ) {
598                                         continue;
599                                 }
600
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 ) {
603                                         continue;
604                                 }
605
606                                 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
607                                         if ( !string_compare_nocase_upper( ext, ".dpk" ) ) {
608                                                 if ( is_dpk_vfs ) {
609                                                         archives.insert( name );
610                                                         continue;
611                                                 }
612                                         }
613                                         else {
614                                                 archivesOverride.insert( name );
615                                                 continue;
616                                         }
617                                 }
618
619                                 archives.insert( name );
620                         }
621
622                         g_dir_close( dir );
623
624                         // add the entries to the vfs
625                         char* fullpath;
626                         if ( is_dpk_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" ) )
631                                         {
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 ) );
636                                         }
637                                 }
638                         }
639                         else
640                         {
641                                 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
642                                 {
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 ) );
652                                         }
653                                 }
654
655                                 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
656                                 {
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 ) );
666                                         }
667                                 }
668                         }
669                 }
670                 else
671                 {
672                         globalErrorStream() << "vfs directory not found: " << path << "\n";
673                 }
674         }
675 }
676
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)
680 void Shutdown(){
681         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
682         {
683                 ( *i ).archive->release();
684         }
685         g_archives.clear();
686
687         g_numDirs = 0;
688         g_numForbiddenDirs = 0;
689
690         g_pakfile_paths.clear();
691         g_loaded_dpk_paks.clear();
692 }
693
694 const int VFS_SEARCH_PAK = 0x1;
695 const int VFS_SEARCH_DIR = 0x2;
696
697 int GetFileCount( const char *filename, int flag ){
698         int count = 0;
699         char fixed[PATH_MAX + 1];
700
701         strncpy( fixed, filename, PATH_MAX );
702         fixed[PATH_MAX] = '\0';
703         FixDOSName( fixed );
704
705         if ( !flag ) {
706                 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
707         }
708
709         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
710         {
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 ) ) {
714                                 ++count;
715                         }
716                 }
717         }
718
719         return count;
720 }
721
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 )
725         {
726                 ArchiveFile* file = ( *i ).archive->openFile( filename );
727                 if ( file != 0 ) {
728                         return file;
729                 }
730         }
731
732         return 0;
733 }
734
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 )
738         {
739                 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
740                 if ( file != 0 ) {
741                         return file;
742                 }
743         }
744
745         return 0;
746 }
747
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];
751
752         strncpy( fixed, filename, PATH_MAX );
753         fixed[PATH_MAX] = '\0';
754         FixDOSName( fixed );
755
756         ArchiveFile* file = OpenFile( fixed );
757
758         if ( file != 0 ) {
759                 *bufferptr = malloc( file->size() + 1 );
760                 // we need to end the buffer with a 0
761                 ( (char*) ( *bufferptr ) )[file->size()] = 0;
762
763                 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
764                 file->release();
765                 return length;
766         }
767
768         *bufferptr = 0;
769         return 0;
770 }
771
772 void FreeFile( void *p ){
773         free( p );
774 }
775
776 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
777         return GetListInternal( dir, ext, false, depth );
778 }
779
780 GSList* GetDirList( const char *dir, std::size_t depth ){
781         return GetListInternal( dir, 0, true, depth );
782 }
783
784 void ClearFileDirList( GSList **lst ){
785         while ( *lst )
786         {
787                 g_free( ( *lst )->data );
788                 *lst = g_slist_remove( *lst, ( *lst )->data );
789         }
790 }
791
792 const char* FindFile( const char* relative ){
793         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
794         {
795                 if ( ( *i ).archive->containsFile( relative ) ) {
796                         return ( *i ).name.c_str();
797                 }
798         }
799
800         return "";
801 }
802
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 )
806         {
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();
810                         }
811                 }
812         }
813
814         return best;
815 }
816
817
818 class Quake3FileSystem : public VirtualFileSystem
819 {
820 public:
821 void initDirectory( const char *path ){
822         InitDirectory( path, FileSystemQ3API_getArchiveModules() );
823 }
824 void initialise(){
825         load();
826         globalOutputStream() << "filesystem initialised\n";
827         g_observers.realise();
828 }
829
830 void load(){
831         ArchiveModules& archiveModules = FileSystemQ3API_getArchiveModules();
832         bool is_dpk_vfs = !!GetArchiveTable( archiveModules, "dpk" );
833
834         if ( is_dpk_vfs ) {
835                 const char* pakname;
836                 g_loaded_dpk_paks.clear();
837
838                 // Load DEPS from game pack
839                 LoadDpkPakWithDeps( NULL );
840
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 ) );
848                         }
849                 }
850
851                 g_pakfile_paths.clear();
852                 g_loaded_dpk_paks.clear();
853         }
854 }
855
856 void clear() {
857         // like shutdown() but does not unrealise (keep map etc.)
858         Shutdown();
859 }
860
861 void refresh(){
862         // like initialise() but does not realise (keep map etc.)
863         load();
864         globalOutputStream() << "filesystem refreshed\n";
865 }
866
867 void shutdown(){
868         g_observers.unrealise();
869         globalOutputStream() << "filesystem shutdown\n";
870         Shutdown();
871 }
872
873 int getFileCount( const char *filename, int flags ){
874         return GetFileCount( filename, flags );
875 }
876 ArchiveFile* openFile( const char* filename ){
877         return OpenFile( filename );
878 }
879 ArchiveTextFile* openTextFile( const char* filename ){
880         return OpenTextFile( filename );
881 }
882 std::size_t loadFile( const char *filename, void **buffer ){
883         return LoadFile( filename, buffer, 0 );
884 }
885 void freeFile( void *p ){
886         FreeFile( p );
887 }
888
889 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
890         GSList* list = GetDirList( basedir, depth );
891
892         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
893         {
894                 callback( reinterpret_cast<const char*>( ( *i ).data ) );
895         }
896
897         ClearFileDirList( &list );
898 }
899 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
900         GSList* list = GetFileList( basedir, extension, depth );
901
902         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
903         {
904                 const char* name = reinterpret_cast<const char*>( ( *i ).data );
905                 if ( extension_equal( path_get_extension( name ), extension ) ) {
906                         callback( name );
907                 }
908         }
909
910         ClearFileDirList( &list );
911 }
912 GSList* getDirList( const char *basedir ){
913         return GetDirList( basedir, 1 );
914 }
915 GSList* getFileList( const char *basedir, const char *extension ){
916         return GetFileList( basedir, extension, 1 );
917 }
918 void clearFileDirList( GSList **lst ){
919         ClearFileDirList( lst );
920 }
921
922 const char* findFile( const char *name ){
923         return FindFile( name );
924 }
925 const char* findRoot( const char *name ){
926         return FindPath( name );
927 }
928
929 void attach( ModuleObserver& observer ){
930         g_observers.attach( observer );
931 }
932 void detach( ModuleObserver& observer ){
933         g_observers.detach( observer );
934 }
935
936 Archive* getArchive( const char* archiveName, bool pakonly ){
937         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
938         {
939                 if ( pakonly && !( *i ).is_pakfile ) {
940                         continue;
941                 }
942
943                 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
944                         return ( *i ).archive;
945                 }
946                 else if ( path_equal( path_get_filename_start( ( *i ).name.c_str() ), archiveName ) ) {
947                         return ( *i ).archive;
948                 }
949         }
950         return 0;
951 }
952 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
953         if ( reverse ) {
954                 g_archives.reverse();
955         }
956
957         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
958         {
959                 if ( pakonly && !( *i ).is_pakfile ) {
960                         continue;
961                 }
962
963                 callback( ( *i ).name.c_str() );
964         }
965
966         if ( reverse ) {
967                 g_archives.reverse();
968         }
969 }
970 };
971
972
973 Quake3FileSystem g_Quake3FileSystem;
974
975 VirtualFileSystem& GetFileSystem(){
976         return g_Quake3FileSystem;
977 }
978
979 void FileSystem_Init(){
980 }
981
982 void FileSystem_Shutdown(){
983 }