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