do not crash on missing pak dependency
[xonotic/netradiant.git] / plugins / vfspk3 / vfs.cpp
1 /*
2    Copyright (c) 2001, Loki software, inc.
3    All rights reserved.
4
5    Redistribution and use in source and binary forms, with or without modification,
6    are permitted provided that the following conditions are met:
7
8    Redistributions of source code must retain the above copyright notice, this list
9    of conditions and the following disclaimer.
10
11    Redistributions in binary form must reproduce the above copyright notice, this
12    list of conditions and the following disclaimer in the documentation and/or
13    other materials provided with the distribution.
14
15    Neither the name of Loki software nor the names of its contributors may be used
16    to endorse or promote products derived from this software without specific prior
17    written permission.
18
19    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22    DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
23    DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 //
32 // Rules:
33 //
34 // - Directories should be searched in the following order: ~/.q3a/baseq3,
35 //   install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
36 //
37 // - Pak files are searched first inside the directories.
38 // - Case insensitive.
39 // - Unix-style slashes (/) (windows is backwards .. everyone knows that)
40 //
41 // Leonardo Zide (leo@lokigames.com)
42 //
43
44 #include "vfs.h"
45
46 #include <stdio.h>
47 #include <stdlib.h>
48 #include <glib.h>
49
50 #include "qerplugin.h"
51 #include "idatastream.h"
52 #include "iarchive.h"
53 ArchiveModules& FileSystemQ3API_getArchiveModules();
54 #include "ifilesystem.h"
55
56 #include "generic/callback.h"
57 #include "string/string.h"
58 #include "stream/stringstream.h"
59 #include "os/path.h"
60 #include "moduleobservers.h"
61 #include "filematch.h"
62
63
64 #define VFS_MAXDIRS 64
65
66 #if defined( WIN32 )
67 #define PATH_MAX 260
68 #endif
69
70 #define gamemode_get GlobalRadiant().getGameMode
71
72
73
74 // =============================================================================
75 // Global variables
76
77 Archive* OpenArchive( const char* name );
78
79 struct archive_entry_t
80 {
81         CopiedString name;
82         Archive* archive;
83         bool is_pakfile;
84 };
85
86 #include <list>
87 #include <map>
88
89 typedef std::list<archive_entry_t> archives_t;
90
91 static archives_t g_archives;
92 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
93 static int g_numDirs;
94 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
95 static int g_numForbiddenDirs = 0;
96 static bool g_bUsePak = true;
97
98 ModuleObservers g_observers;
99
100 // =============================================================================
101 // Static functions
102
103 static void AddSlash( char *str ){
104         std::size_t n = strlen( str );
105         if ( n > 0 ) {
106                 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
107                         globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
108                         strcat( str, "/" );
109                 }
110         }
111 }
112
113 static void FixDOSName( char *src ){
114         if ( src == 0 || strchr( src, '\\' ) == 0 ) {
115                 return;
116         }
117
118         globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
119
120         while ( *src )
121         {
122                 if ( *src == '\\' ) {
123                         *src = '/';
124                 }
125                 src++;
126         }
127 }
128
129
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 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 unvanquished
303
304 bool IsUnvanquished(){
305         return strncmp( GlobalRadiant().getGameFile(), "unvanquished", 12 ) == 0;
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 AddUnvPak( 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 // Comparaison function for version numbers
327 // Implementation is based on dpkg's version comparison code (verrevcmp() and order())
328 // http://anonscm.debian.org/gitweb/?p=dpkg/dpkg.git;a=blob;f=lib/dpkg/version.c;hb=74946af470550a3295e00cf57eca1747215b9311
329 static int char_weight(char c){
330         if (std::isdigit(c))
331                 return 0;
332         else if (std::isalpha(c))
333                 return c;
334         else if (c == '~')
335                 return -1;
336         else if (c)
337                 return c + 256;
338         else
339                 return 0;
340 }
341
342 static int VersionCmp(const char* a, const char* b){
343         while (*a || *b) {
344                 int firstDiff = 0;
345
346                 while ((*a && !std::isdigit(*a)) || (*b && !std::isdigit(*b))) {
347                         int ac = char_weight(*a);
348                         int bc = char_weight(*b);
349
350                         if (ac != bc)
351                                 return ac - bc;
352
353                         a++;
354                         b++;
355                 }
356
357                 while (*a == '0')
358                         a++;
359                 while (*b == '0')
360                         b++;
361
362                 while (std::isdigit(*a) && std::isdigit(*b)) {
363                         if (firstDiff == 0)
364                                 firstDiff = *a - *b;
365                         a++;
366                         b++;
367                 }
368
369                 if (std::isdigit(*a))
370                         return 1;
371                 if (std::isdigit(*b))
372                         return -1;
373                 if (firstDiff)
374                         return firstDiff;
375         }
376
377         return false;
378 }
379
380 // takes name without ext, returns without ext
381 static const char* GetLatestVersionOfUnvPak( const char* name ){
382         const char* maxversion = 0;
383         const char* result = 0;
384         const char* pakname;
385         const char* pakversion;
386         int namelen = string_length( name );
387
388         for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i )
389         {
390                 pakname = i->first.c_str();
391                 if ( strncmp( pakname, name, namelen ) != 0 || pakname[namelen] != '_' ) continue;
392                 pakversion = pakname + (namelen + 1);
393                 if ( maxversion == 0 || VersionCmp( pakversion, maxversion ) > 0 ){
394                         maxversion = pakversion;
395                         result = pakname;
396                 }
397         }
398         return result;
399 }
400
401 // release string after using
402 static char* GetCurrentMapPakName(){
403         char* mapdir;
404         char* mapname;
405         int mapnamelen;
406         char* result = 0;
407
408         mapname = string_clone( GlobalRadiant().getMapName() );
409         mapnamelen = string_length( mapname );
410
411         mapdir = strrchr( mapname, '/' );
412         if ( mapdir ) {
413                 mapdir -= 12;
414                 if ( strncmp( mapdir, ".pk3dir/maps/", 13 ) == 0 ) {
415                         *mapdir = '\0';
416                         mapdir = strrchr( mapname, '/' );
417                         if ( mapdir ) mapdir++;
418                         else mapdir = mapname;
419                         result = string_clone( mapdir );
420                 }
421         }
422
423         string_release( mapname, mapnamelen );
424         return result;
425
426 }
427
428 // prevent loading duplicates or circular references
429 static Archives g_loaded_unv_paks;
430
431 // actual pak adding on initialise, deferred from InitDirectory
432 // Unvanquished filesystem doesn't need load all paks it finds
433 static void LoadPakWithDeps( const char* pakname ){
434         const char* und = strrchr( pakname, '_' );
435         if ( !und ) pakname = GetLatestVersionOfUnvPak( pakname );
436         if ( !pakname || g_loaded_unv_paks.find( pakname ) != g_loaded_unv_paks.end() ) return;
437
438         PakfilePaths::iterator i = g_pakfile_paths.find( pakname );
439         if ( i == g_pakfile_paths.end() ) return;
440
441         Archive* arc;
442         if ( i->second.is_pakfile ){
443                 arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() );
444         } else {
445                 arc = AddPk3Dir( i->second.fullpath.c_str() );
446         }
447         g_loaded_unv_paks.insert( pakname );
448
449         ArchiveTextFile* depsFile = arc->openTextFile( "DEPS" );
450         if ( !depsFile ) return;
451
452         {
453                 TextLinesInputStream<TextInputStream> istream = depsFile->getInputStream();
454
455                 CopiedString line;
456                 const char* c;
457                 const char* p_name;
458                 const char* p_name_end;
459                 const char* p_version;
460                 const char* p_version_end;
461                 while ( line = istream.readLine(), string_length( line.c_str() ) ) {
462                         c = line.c_str();
463                         while ( std::isspace( *c ) && *c != '\0' ) ++c;
464                         p_name = c;
465                         while ( !std::isspace( *c ) && *c != '\0' ) ++c;
466                         p_name_end = c;
467                         while ( std::isspace( *c ) && *c != '\0' ) ++c;
468                         p_version = c;
469                         while ( !std::isspace( *c ) && *c != '\0' ) ++c;
470                         p_version_end = c;
471
472                         if ( p_name_end - p_name == 0 ) continue;
473                         if ( p_version_end - p_version == 0 ) {
474                                 const char* p_pakname;
475                                 CopiedString name_final = CopiedString( StringRange( p_name, p_name_end ) );
476                                 p_pakname = GetLatestVersionOfUnvPak( name_final.c_str() );
477                                 if ( p_pakname != NULL ) {
478                                         LoadPakWithDeps( p_pakname );
479                                 }
480                         } else {
481                                 int len = ( p_name_end - p_name ) + ( p_version_end - p_version ) + 1;
482                                 char* p_pakname = string_new( len );
483                                 strncpy( p_pakname, p_name, p_name_end - p_name );
484                                 p_pakname[ p_name_end - p_name ] = '\0';
485                                 strcat( p_pakname, "_" );
486                                 strncat( p_pakname, p_version, p_version_end - p_version );
487                                 LoadPakWithDeps( p_pakname );
488                                 string_release( p_pakname, len );
489                         }
490                 }
491         }
492
493         depsFile->release();
494 }
495
496 // end for unvanquished
497
498 // =============================================================================
499 // Global functions
500
501 // reads all pak files from a dir
502 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
503         int j;
504
505         g_numForbiddenDirs = 0;
506         StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
507         for ( j = 0; j < VFS_MAXDIRS; ++j )
508         {
509                 const char *t = st.getToken();
510                 if ( string_empty( t ) ) {
511                         break;
512                 }
513                 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
514                 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
515                 ++g_numForbiddenDirs;
516         }
517
518         for ( j = 0; j < g_numForbiddenDirs; ++j )
519         {
520                 char* dbuf = g_strdup( directory );
521                 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
522                         dbuf[strlen( dbuf ) - 1] = 0;
523                 }
524                 const char *p = strrchr( dbuf, '/' );
525                 p = ( p ? ( p + 1 ) : dbuf );
526                 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
527                         g_free( dbuf );
528                         break;
529                 }
530                 g_free( dbuf );
531         }
532         if ( j < g_numForbiddenDirs ) {
533                 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
534                 return;
535         }
536
537         if ( g_numDirs == VFS_MAXDIRS ) {
538                 return;
539         }
540
541         strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
542         g_strDirs[g_numDirs][PATH_MAX] = '\0';
543         FixDOSName( g_strDirs[g_numDirs] );
544         AddSlash( g_strDirs[g_numDirs] );
545
546         const char* path = g_strDirs[g_numDirs];
547
548         g_numDirs++;
549
550         {
551                 archive_entry_t entry;
552                 entry.name = path;
553                 entry.archive = OpenArchive( path );
554                 entry.is_pakfile = false;
555                 g_archives.push_back( entry );
556         }
557
558         if ( g_bUsePak ) {
559
560                 GDir* dir = g_dir_open( path, 0, 0 );
561
562                 if ( dir != 0 ) {
563                         globalOutputStream() << "vfs directory: " << path << "\n";
564
565                         bool unv;
566                         unv = IsUnvanquished();
567
568                         const char* ignore_prefix = "";
569                         const char* override_prefix = "";
570
571                         if ( !unv ) {
572                                 // See if we are in "sp" or "mp" mapping mode
573                                 const char* gamemode = gamemode_get();
574
575                                 if ( strcmp( gamemode, "sp" ) == 0 ) {
576                                         ignore_prefix = "mp_";
577                                         override_prefix = "sp_";
578                                 }
579                                 else if ( strcmp( gamemode, "mp" ) == 0 ) {
580                                         ignore_prefix = "sp_";
581                                         override_prefix = "mp_";
582                                 }
583                         }
584
585                         Archives archives;
586                         Archives archivesOverride;
587                         for (;; )
588                         {
589                                 const char* name = g_dir_read_name( dir );
590                                 if ( name == 0 ) {
591                                         break;
592                                 }
593
594                                 for ( j = 0; j < g_numForbiddenDirs; ++j )
595                                 {
596                                         const char *p = strrchr( name, '/' );
597                                         p = ( p ? ( p + 1 ) : name );
598                                         if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
599                                                 break;
600                                         }
601                                 }
602                                 if ( j < g_numForbiddenDirs ) {
603                                         continue;
604                                 }
605
606                                 const char *ext = strrchr( name, '.' );
607                                 char tmppath[PATH_MAX];
608
609                                 if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
610
611                                         snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
612                                         tmppath[PATH_MAX] = '\0';
613                                         FixDOSName( tmppath );
614                                         AddSlash( tmppath );
615
616                                         if ( unv ) {
617                                                 AddUnvPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
618                                         } else {
619                                                 AddPk3Dir( tmppath );
620                                         }
621                                 }
622
623                                 if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
624                                         continue;
625                                 }
626
627                                 // using the same kludge as in engine to ensure consistency
628                                 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
629                                         continue;
630                                 }
631                                 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
632                                         if ( unv ) {
633                                                 archives.insert( name );
634                                         } else {
635                                                 archivesOverride.insert( name );
636                                         }
637                                         continue;
638                                 }
639
640                                 archives.insert( name );
641                         }
642
643                         g_dir_close( dir );
644
645                         // add the entries to the vfs
646                         char* fullpath;
647                         if ( unv ) {
648                                 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) {
649                                         const char* name = i->c_str();
650                                         const char* ext = strrchr( name, '.' );
651                                         CopiedString name_final = CopiedString( StringRange( name, ext ) );
652                                         fullpath = string_new_concat( path, name );
653                                         AddUnvPak( name_final.c_str(), fullpath, true );
654                                         string_release( fullpath, string_length( fullpath ) );
655                                 }
656                         } else {
657                                 for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
658                                 {
659                                         fullpath = string_new_concat( path, i->c_str() );
660                                         InitPakFile( archiveModules, fullpath );
661                                         string_release( fullpath, string_length( fullpath ) );
662                                 }
663                                 for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
664                                 {
665                                         fullpath = string_new_concat( path, i->c_str() );
666                                         InitPakFile( archiveModules, fullpath );
667                                         string_release( fullpath, string_length( fullpath ) );
668                                 }
669                         }
670                 }
671                 else
672                 {
673                         globalErrorStream() << "vfs directory not found: " << path << "\n";
674                 }
675         }
676 }
677
678 // frees all memory that we allocated
679 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
680 //   (for instance when modifying the project settings)
681 void Shutdown(){
682         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
683         {
684                 ( *i ).archive->release();
685         }
686         g_archives.clear();
687
688         g_numDirs = 0;
689         g_numForbiddenDirs = 0;
690
691         g_pakfile_paths.clear();
692         g_loaded_unv_paks.clear();
693 }
694
695 #define VFS_SEARCH_PAK 0x1
696 #define VFS_SEARCH_DIR 0x2
697
698 int GetFileCount( const char *filename, int flag ){
699         int count = 0;
700         char fixed[PATH_MAX + 1];
701
702         strncpy( fixed, filename, PATH_MAX );
703         fixed[PATH_MAX] = '\0';
704         FixDOSName( fixed );
705
706         if ( !flag ) {
707                 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
708         }
709
710         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
711         {
712                 if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
713                          || !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
714                         if ( ( *i ).archive->containsFile( fixed ) ) {
715                                 ++count;
716                         }
717                 }
718         }
719
720         return count;
721 }
722
723 ArchiveFile* OpenFile( const char* filename ){
724         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
725         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
726         {
727                 ArchiveFile* file = ( *i ).archive->openFile( filename );
728                 if ( file != 0 ) {
729                         return file;
730                 }
731         }
732
733         return 0;
734 }
735
736 ArchiveTextFile* OpenTextFile( const char* filename ){
737         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
738         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
739         {
740                 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
741                 if ( file != 0 ) {
742                         return file;
743                 }
744         }
745
746         return 0;
747 }
748
749 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
750 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
751         char fixed[PATH_MAX + 1];
752
753         strncpy( fixed, filename, PATH_MAX );
754         fixed[PATH_MAX] = '\0';
755         FixDOSName( fixed );
756
757         ArchiveFile* file = OpenFile( fixed );
758
759         if ( file != 0 ) {
760                 *bufferptr = malloc( file->size() + 1 );
761                 // we need to end the buffer with a 0
762                 ( (char*) ( *bufferptr ) )[file->size()] = 0;
763
764                 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
765                 file->release();
766                 return length;
767         }
768
769         *bufferptr = 0;
770         return 0;
771 }
772
773 void FreeFile( void *p ){
774         free( p );
775 }
776
777 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
778         return GetListInternal( dir, ext, false, depth );
779 }
780
781 GSList* GetDirList( const char *dir, std::size_t depth ){
782         return GetListInternal( dir, 0, true, depth );
783 }
784
785 void ClearFileDirList( GSList **lst ){
786         while ( *lst )
787         {
788                 g_free( ( *lst )->data );
789                 *lst = g_slist_remove( *lst, ( *lst )->data );
790         }
791 }
792
793 const char* FindFile( const char* relative ){
794         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
795         {
796                 if ( ( *i ).archive->containsFile( relative ) ) {
797                         return ( *i ).name.c_str();
798                 }
799         }
800
801         return "";
802 }
803
804 const char* FindPath( const char* absolute ){
805         const char *best = "";
806         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
807         {
808                 if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
809                         if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
810                                 best = ( *i ).name.c_str();
811                         }
812                 }
813         }
814
815         return best;
816 }
817
818
819 class Quake3FileSystem : public VirtualFileSystem
820 {
821 public:
822 void initDirectory( const char *path ){
823         InitDirectory( path, FileSystemQ3API_getArchiveModules() );
824 }
825 void initialise(){
826         if ( IsUnvanquished() ) {
827                 const char* pakname;
828                 g_loaded_unv_paks.clear();
829
830                 pakname = GetLatestVersionOfUnvPak( "radiant" );
831                 if (pakname != NULL) {
832                         LoadPakWithDeps( pakname );
833                 }
834
835                 // prevent VFS double start, for MapName="" and MapName="unnamed.map"
836                 if ( string_length( GlobalRadiant().getMapName() ) ){
837                         // map's tex-* paks have precedence over any other tex-* paks
838                         char* mappakname = GetCurrentMapPakName();
839                         if ( mappakname != NULL ) {
840                                 LoadPakWithDeps( mappakname );
841                                 string_release( mappakname, string_length( mappakname ) );
842                         }
843
844                         for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i ) {
845                                 if ( strncmp( i->first.c_str(), "tex-", 4 ) != 0 ) continue;
846                                 // firstly load latest version of pak
847                                 const char *paknamever = i->first.c_str();
848                                 const char *c = strchr( paknamever, '_' );
849                                 char *paknameonly;
850                                 if ( c ) paknameonly = string_clone_range( StringRange( paknamever, c ) );
851                                 pakname = GetLatestVersionOfUnvPak( paknameonly );
852                                 if (pakname != NULL) {
853                                         LoadPakWithDeps( pakname );
854                                 }
855                                 if ( c ) string_release( paknameonly, string_length( paknameonly ) );
856                                 // then load this specific version
857                                 LoadPakWithDeps( paknamever );
858                         }
859                 }
860
861                 g_pakfile_paths.clear();
862                 g_loaded_unv_paks.clear();
863         }
864
865         globalOutputStream() << "filesystem initialised\n";
866         g_observers.realise();
867 }
868 void shutdown(){
869         g_observers.unrealise();
870         globalOutputStream() << "filesystem shutdown\n";
871         Shutdown();
872 }
873
874 int getFileCount( const char *filename, int flags ){
875         return GetFileCount( filename, flags );
876 }
877 ArchiveFile* openFile( const char* filename ){
878         return OpenFile( filename );
879 }
880 ArchiveTextFile* openTextFile( const char* filename ){
881         return OpenTextFile( filename );
882 }
883 std::size_t loadFile( const char *filename, void **buffer ){
884         return LoadFile( filename, buffer, 0 );
885 }
886 void freeFile( void *p ){
887         FreeFile( p );
888 }
889
890 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
891         GSList* list = GetDirList( basedir, depth );
892
893         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
894         {
895                 callback( reinterpret_cast<const char*>( ( *i ).data ) );
896         }
897
898         ClearFileDirList( &list );
899 }
900 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
901         GSList* list = GetFileList( basedir, extension, depth );
902
903         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
904         {
905                 const char* name = reinterpret_cast<const char*>( ( *i ).data );
906                 if ( extension_equal( path_get_extension( name ), extension ) ) {
907                         callback( name );
908                 }
909         }
910
911         ClearFileDirList( &list );
912 }
913 GSList* getDirList( const char *basedir ){
914         return GetDirList( basedir, 1 );
915 }
916 GSList* getFileList( const char *basedir, const char *extension ){
917         return GetFileList( basedir, extension, 1 );
918 }
919 void clearFileDirList( GSList **lst ){
920         ClearFileDirList( lst );
921 }
922
923 const char* findFile( const char *name ){
924         return FindFile( name );
925 }
926 const char* findRoot( const char *name ){
927         return FindPath( name );
928 }
929
930 void attach( ModuleObserver& observer ){
931         g_observers.attach( observer );
932 }
933 void detach( ModuleObserver& observer ){
934         g_observers.detach( observer );
935 }
936
937 Archive* getArchive( const char* archiveName, bool pakonly ){
938         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
939         {
940                 if ( pakonly && !( *i ).is_pakfile ) {
941                         continue;
942                 }
943
944                 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
945                         return ( *i ).archive;
946                 }
947         }
948         return 0;
949 }
950 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
951         if ( reverse ) {
952                 g_archives.reverse();
953         }
954
955         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
956         {
957                 if ( pakonly && !( *i ).is_pakfile ) {
958                         continue;
959                 }
960
961                 callback( ( *i ).name.c_str() );
962         }
963
964         if ( reverse ) {
965                 g_archives.reverse();
966         }
967 }
968 };
969
970
971 Quake3FileSystem g_Quake3FileSystem;
972
973 VirtualFileSystem& GetFileSystem(){
974         return g_Quake3FileSystem;
975 }
976
977 void FileSystem_Init(){
978 }
979
980 void FileSystem_Shutdown(){
981 }