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