CMake: Make dll bundling optional
[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         const char* und = strrchr( pakname, '_' );
381         if ( !und ) pakname = GetLatestDpkPakVersion( pakname );
382         if ( !pakname || g_loaded_dpk_paks.find( pakname ) != g_loaded_dpk_paks.end() ) return;
383
384         PakfilePaths::iterator i = g_pakfile_paths.find( pakname );
385         if ( i == g_pakfile_paths.end() ) return;
386
387         Archive* arc;
388         if ( i->second.is_pakfile ){
389                 arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() );
390         } else {
391                 arc = AddDpkDir( i->second.fullpath.c_str() );
392         }
393         g_loaded_dpk_paks.insert( pakname );
394
395         ArchiveTextFile* depsFile = arc->openTextFile( "DEPS" );
396         if ( !depsFile ) return;
397
398         {
399                 TextLinesInputStream<TextInputStream> istream = depsFile->getInputStream();
400
401                 CopiedString line;
402                 char *p_name;
403                 char *p_version;
404                 while ( line = istream.readLine(), string_length( line.c_str() ) ) {
405                         if ( !DpkReadDepsLine( line.c_str(), &p_name, &p_version ) ) continue;
406                         if ( !p_version ) {
407                                 const char* p_latest = GetLatestDpkPakVersion( p_name );
408                                 if ( p_latest ) LoadDpkPakWithDeps( p_latest );
409                         } else {
410                                 int len = string_length( p_name ) + string_length( p_version ) + 1;
411                                 char* p_pakname = string_new( len );
412                                 sprintf( p_pakname, "%s_%s", p_name, p_version );
413                                 LoadDpkPakWithDeps( p_pakname );
414                                 string_release( p_pakname, len );
415                         }
416                         string_release( p_name, string_length( p_name ) );
417                         if ( p_version ) string_release( p_version, string_length( p_version ) );
418                 }
419         }
420
421         depsFile->release();
422 }
423
424 // end for Daemon DPK vfs
425
426 // =============================================================================
427 // Global functions
428
429 // reads all pak files from a dir
430 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
431         int j;
432
433         g_numForbiddenDirs = 0;
434         StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
435         for ( j = 0; j < VFS_MAXDIRS; ++j )
436         {
437                 const char *t = st.getToken();
438                 if ( string_empty( t ) ) {
439                         break;
440                 }
441                 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
442                 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
443                 ++g_numForbiddenDirs;
444         }
445
446         for ( j = 0; j < g_numForbiddenDirs; ++j )
447         {
448                 char* dbuf = g_strdup( directory );
449                 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
450                         dbuf[strlen( dbuf ) - 1] = 0;
451                 }
452                 const char *p = strrchr( dbuf, '/' );
453                 p = ( p ? ( p + 1 ) : dbuf );
454                 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
455                         g_free( dbuf );
456                         break;
457                 }
458                 g_free( dbuf );
459         }
460         if ( j < g_numForbiddenDirs ) {
461                 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
462                 return;
463         }
464
465         if ( g_numDirs == VFS_MAXDIRS ) {
466                 return;
467         }
468
469         strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
470         g_strDirs[g_numDirs][PATH_MAX] = '\0';
471         FixDOSName( g_strDirs[g_numDirs] );
472         AddSlash( g_strDirs[g_numDirs] );
473
474         const char* path = g_strDirs[g_numDirs];
475
476         g_numDirs++;
477
478         {
479                 archive_entry_t entry;
480                 entry.name = path;
481                 entry.archive = OpenArchive( path );
482                 entry.is_pakfile = false;
483                 g_archives.push_back( entry );
484         }
485
486         if ( g_bUsePak ) {
487
488                 GDir* dir = g_dir_open( path, 0, 0 );
489
490                 if ( dir != 0 ) {
491                         globalOutputStream() << "vfs directory: " << path << "\n";
492
493                         Archives archives;
494                         Archives archivesOverride;
495                         const char* ignore_prefix = "";
496                         const char* override_prefix = "";
497                         bool is_pk3_vfs, is_pk4_vfs, is_dpk_vfs;
498
499                         is_pk3_vfs = GetArchiveTable( archiveModules, "pk3" );
500                         is_pk4_vfs = GetArchiveTable( archiveModules, "pk4" );
501                         is_dpk_vfs = GetArchiveTable( archiveModules, "dpk" );
502
503                         if ( !is_dpk_vfs ) {
504                                 // See if we are in "sp" or "mp" mapping mode
505                                 const char* gamemode = gamemode_get();
506
507                                 if ( strcmp( gamemode, "sp" ) == 0 ) {
508                                         ignore_prefix = "mp_";
509                                         override_prefix = "sp_";
510                                 }
511                                 else if ( strcmp( gamemode, "mp" ) == 0 ) {
512                                         ignore_prefix = "sp_";
513                                         override_prefix = "mp_";
514                                 }
515                         }
516
517                         for (;; )
518                         {
519                                 const char* name = g_dir_read_name( dir );
520                                 if ( name == 0 ) {
521                                         break;
522                                 }
523
524                                 for ( j = 0; j < g_numForbiddenDirs; ++j )
525                                 {
526                                         const char *p = strrchr( name, '/' );
527                                         p = ( p ? ( p + 1 ) : name );
528                                         if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
529                                                 break;
530                                         }
531                                 }
532                                 if ( j < g_numForbiddenDirs ) {
533                                         continue;
534                                 }
535
536                                 const char *ext = strrchr( name, '.' );
537                                 char tmppath[PATH_MAX];
538
539                                 if ( is_dpk_vfs ) {
540                                         if ( !!ext && !string_compare_nocase_upper( ext, ".dpkdir" ) ) {
541                                                 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
542                                                 tmppath[PATH_MAX] = '\0';
543                                                 FixDOSName( tmppath );
544                                                 AddSlash( tmppath );
545                                                 AddDpkPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
546                                         }
547                                 }
548
549                                 if ( is_pk3_vfs || is_pk4_vfs ) {
550                                         if ( !!ext && ( !string_compare_nocase_upper( ext, ".pk3dir" )
551                                                 || !string_compare_nocase_upper( ext, ".pk4dir" ) ) ) {
552                                                 snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
553                                                 tmppath[PATH_MAX] = '\0';
554                                                 FixDOSName( tmppath );
555                                                 AddSlash( tmppath );
556                                                 AddPk3Dir( tmppath );
557                                         }
558                                 }
559
560                                 // GetArchiveTable() needs "pk3" if ext is ".pk3"
561                                 if ( ( ext == 0 ) || *( ext + 1 ) == '\0' || GetArchiveTable( archiveModules, ext + 1 ) == 0 ) {
562                                         continue;
563                                 }
564
565                                 // using the same kludge as in engine to ensure consistency
566                                 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
567                                         continue;
568                                 }
569                                 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
570                                         if ( !string_compare_nocase_upper( ext, ".dpk" ) ) {
571                                                 if ( is_dpk_vfs ) {
572                                                         archives.insert( name );
573                                                 }
574                                         }
575                                         else {
576                                                 archivesOverride.insert( name );
577                                         }
578                                         continue;
579                                 }
580
581                                 archives.insert( name );
582                         }
583
584                         g_dir_close( dir );
585
586                         // add the entries to the vfs
587                         char* fullpath;
588                         if ( is_dpk_vfs ) {
589                                 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) {
590                                         const char* name = i->c_str();
591                                         const char* ext = strrchr( name, '.' );
592                                         if ( !string_compare_nocase_upper( ext, ".dpk" ) ) {
593                                                 CopiedString name_final = CopiedString( StringRange( name, ext ) );
594                                                 fullpath = string_new_concat( path, name );
595                                                 AddDpkPak( name_final.c_str(), fullpath, true );
596                                                 string_release( fullpath, string_length( fullpath ) );
597                                         }
598                                 }
599                         }
600                         if ( is_pk3_vfs || is_pk4_vfs ) {
601                                 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
602                                 {
603                                         const char* name = i->c_str();
604                                         const char* ext = strrchr( name, '.' );
605                                         if ( !string_compare_nocase_upper( ext, ".pk3" )
606                                                 || !string_compare_nocase_upper( ext, ".pk4" ) ) {
607                                                 fullpath = string_new_concat( path, i->c_str() );
608                                                 InitPakFile( archiveModules, fullpath );
609                                                 string_release( fullpath, string_length( fullpath ) );
610                                         }
611                                 }
612                                 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
613                                 {
614                                         const char* name = i->c_str();
615                                         const char* ext = strrchr( name, '.' );
616                                         if ( !string_compare_nocase_upper( ext, ".pk3" )
617                                                 || !string_compare_nocase_upper( ext, ".pk4" ) ) {
618                                                 fullpath = string_new_concat( path, i->c_str() );
619                                                 InitPakFile( archiveModules, fullpath );
620                                                 string_release( fullpath, string_length( fullpath ) );
621                                         }
622                                 }
623                         }
624                 }
625                 else
626                 {
627                         globalErrorStream() << "vfs directory not found: " << path << "\n";
628                 }
629         }
630 }
631
632 // frees all memory that we allocated
633 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
634 //   (for instance when modifying the project settings)
635 void Shutdown(){
636         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
637         {
638                 ( *i ).archive->release();
639         }
640         g_archives.clear();
641
642         g_numDirs = 0;
643         g_numForbiddenDirs = 0;
644
645         g_pakfile_paths.clear();
646         g_loaded_dpk_paks.clear();
647 }
648
649 #define VFS_SEARCH_PAK 0x1
650 #define VFS_SEARCH_DIR 0x2
651
652 int GetFileCount( const char *filename, int flag ){
653         int count = 0;
654         char fixed[PATH_MAX + 1];
655
656         strncpy( fixed, filename, PATH_MAX );
657         fixed[PATH_MAX] = '\0';
658         FixDOSName( fixed );
659
660         if ( !flag ) {
661                 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
662         }
663
664         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
665         {
666                 if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
667                          || !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
668                         if ( ( *i ).archive->containsFile( fixed ) ) {
669                                 ++count;
670                         }
671                 }
672         }
673
674         return count;
675 }
676
677 ArchiveFile* OpenFile( const char* filename ){
678         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
679         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
680         {
681                 ArchiveFile* file = ( *i ).archive->openFile( filename );
682                 if ( file != 0 ) {
683                         return file;
684                 }
685         }
686
687         return 0;
688 }
689
690 ArchiveTextFile* OpenTextFile( const char* filename ){
691         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
692         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
693         {
694                 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
695                 if ( file != 0 ) {
696                         return file;
697                 }
698         }
699
700         return 0;
701 }
702
703 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
704 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
705         char fixed[PATH_MAX + 1];
706
707         strncpy( fixed, filename, PATH_MAX );
708         fixed[PATH_MAX] = '\0';
709         FixDOSName( fixed );
710
711         ArchiveFile* file = OpenFile( fixed );
712
713         if ( file != 0 ) {
714                 *bufferptr = malloc( file->size() + 1 );
715                 // we need to end the buffer with a 0
716                 ( (char*) ( *bufferptr ) )[file->size()] = 0;
717
718                 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
719                 file->release();
720                 return length;
721         }
722
723         *bufferptr = 0;
724         return 0;
725 }
726
727 void FreeFile( void *p ){
728         free( p );
729 }
730
731 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
732         return GetListInternal( dir, ext, false, depth );
733 }
734
735 GSList* GetDirList( const char *dir, std::size_t depth ){
736         return GetListInternal( dir, 0, true, depth );
737 }
738
739 void ClearFileDirList( GSList **lst ){
740         while ( *lst )
741         {
742                 g_free( ( *lst )->data );
743                 *lst = g_slist_remove( *lst, ( *lst )->data );
744         }
745 }
746
747 const char* FindFile( const char* relative ){
748         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
749         {
750                 if ( ( *i ).archive->containsFile( relative ) ) {
751                         return ( *i ).name.c_str();
752                 }
753         }
754
755         return "";
756 }
757
758 const char* FindPath( const char* absolute ){
759         const char *best = "";
760         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
761         {
762                 if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
763                         if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
764                                 best = ( *i ).name.c_str();
765                         }
766                 }
767         }
768
769         return best;
770 }
771
772
773 class Quake3FileSystem : public VirtualFileSystem
774 {
775 public:
776 void initDirectory( const char *path ){
777         InitDirectory( path, FileSystemQ3API_getArchiveModules() );
778 }
779 void initialise(){
780         load();
781         globalOutputStream() << "filesystem initialised\n";
782         g_observers.realise();
783 }
784
785 void load(){
786         ArchiveModules& archiveModules = FileSystemQ3API_getArchiveModules();
787         bool is_dpk_vfs = GetArchiveTable( archiveModules, "dpk" );
788
789         if ( is_dpk_vfs ) {
790                 const char* pakname;
791                 g_loaded_dpk_paks.clear();
792
793                 pakname = GetLatestDpkPakVersion( "tex-common" );
794                 if (pakname != NULL) {
795                         LoadDpkPakWithDeps( pakname );
796                 }
797
798                 // prevent VFS double start, for MapName="" and MapName="unnamed.map"
799                 if ( string_length( GlobalRadiant().getMapName() ) ){
800                         // load map's paks from DEPS
801                         char* mappakname = GetCurrentMapDpkPakName();
802                         if ( mappakname != NULL ) {
803                                 LoadDpkPakWithDeps( mappakname );
804                                 string_release( mappakname, string_length( mappakname ) );
805                         }
806                 }
807
808                 g_pakfile_paths.clear();
809                 g_loaded_dpk_paks.clear();
810         }
811 }
812
813 void clear() {
814         // like shutdown() but does not unrealise (keep map etc.)
815         Shutdown();
816 }
817
818 void refresh(){
819         // like initialise() but does not realise (keep map etc.)
820         load();
821         globalOutputStream() << "filesystem refreshed\n";
822 }
823
824 void shutdown(){
825         g_observers.unrealise();
826         globalOutputStream() << "filesystem shutdown\n";
827         Shutdown();
828 }
829
830 int getFileCount( const char *filename, int flags ){
831         return GetFileCount( filename, flags );
832 }
833 ArchiveFile* openFile( const char* filename ){
834         return OpenFile( filename );
835 }
836 ArchiveTextFile* openTextFile( const char* filename ){
837         return OpenTextFile( filename );
838 }
839 std::size_t loadFile( const char *filename, void **buffer ){
840         return LoadFile( filename, buffer, 0 );
841 }
842 void freeFile( void *p ){
843         FreeFile( p );
844 }
845
846 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
847         GSList* list = GetDirList( basedir, depth );
848
849         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
850         {
851                 callback( reinterpret_cast<const char*>( ( *i ).data ) );
852         }
853
854         ClearFileDirList( &list );
855 }
856 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
857         GSList* list = GetFileList( basedir, extension, depth );
858
859         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
860         {
861                 const char* name = reinterpret_cast<const char*>( ( *i ).data );
862                 if ( extension_equal( path_get_extension( name ), extension ) ) {
863                         callback( name );
864                 }
865         }
866
867         ClearFileDirList( &list );
868 }
869 GSList* getDirList( const char *basedir ){
870         return GetDirList( basedir, 1 );
871 }
872 GSList* getFileList( const char *basedir, const char *extension ){
873         return GetFileList( basedir, extension, 1 );
874 }
875 void clearFileDirList( GSList **lst ){
876         ClearFileDirList( lst );
877 }
878
879 const char* findFile( const char *name ){
880         return FindFile( name );
881 }
882 const char* findRoot( const char *name ){
883         return FindPath( name );
884 }
885
886 void attach( ModuleObserver& observer ){
887         g_observers.attach( observer );
888 }
889 void detach( ModuleObserver& observer ){
890         g_observers.detach( observer );
891 }
892
893 Archive* getArchive( const char* archiveName, bool pakonly ){
894         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
895         {
896                 if ( pakonly && !( *i ).is_pakfile ) {
897                         continue;
898                 }
899
900                 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
901                         return ( *i ).archive;
902                 }
903         }
904         return 0;
905 }
906 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
907         if ( reverse ) {
908                 g_archives.reverse();
909         }
910
911         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
912         {
913                 if ( pakonly && !( *i ).is_pakfile ) {
914                         continue;
915                 }
916
917                 callback( ( *i ).name.c_str() );
918         }
919
920         if ( reverse ) {
921                 g_archives.reverse();
922         }
923 }
924 };
925
926
927 Quake3FileSystem g_Quake3FileSystem;
928
929 VirtualFileSystem& GetFileSystem(){
930         return g_Quake3FileSystem;
931 }
932
933 void FileSystem_Init(){
934 }
935
936 void FileSystem_Shutdown(){
937 }