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