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