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