vfs: support .pk3dir like in darkplaces engine - directories auto-added to the search...
[divverent/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/gslist.h>
49 #include <glib/gdir.h>
50 #include <glib/gstrfuncs.h>
51
52 #include "qerplugin.h"
53 #include "idatastream.h"
54 #include "iarchive.h"
55 ArchiveModules& FileSystemQ3API_getArchiveModules();
56 #include "ifilesystem.h"
57
58 #include "generic/callback.h"
59 #include "string/string.h"
60 #include "stream/stringstream.h"
61 #include "os/path.h"
62 #include "moduleobservers.h"
63
64
65 #define VFS_MAXDIRS 8
66
67 #if defined(WIN32)
68 #define PATH_MAX 260
69 #endif
70
71 #define gamemode_get GlobalRadiant().getGameMode
72
73
74
75 // =============================================================================
76 // Global variables
77
78 Archive* OpenArchive(const char* name);
79
80 struct archive_entry_t
81 {
82   CopiedString name;
83   Archive* archive;
84   bool is_pakfile;
85 };
86
87 #include <list>
88
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 bool    g_bUsePak = true;
95
96 ModuleObservers g_observers;
97
98 // =============================================================================
99 // Static functions
100
101 static void AddSlash (char *str)
102 {
103   std::size_t n = strlen (str);
104   if (n > 0)
105   {
106     if (str[n-1] != '\\' && str[n-1] != '/')
107     {
108       globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
109       strcat (str, "/");
110     }
111   }
112 }
113
114 static void FixDOSName (char *src)
115 {
116   if (src == 0 || strchr(src, '\\') == 0)
117     return;
118
119   globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
120
121   while (*src)
122   {
123     if (*src == '\\')
124       *src = '/';
125     src++;
126   }
127 }
128
129
130
131 const _QERArchiveTable* GetArchiveTable(ArchiveModules& archiveModules, const char* ext)
132 {
133   StringOutputStream tmp(16);
134   tmp << LowerCase(ext);
135   return archiveModules.findModule(tmp.c_str());
136 }
137 static void InitPakFile (ArchiveModules& archiveModules, const char *filename)
138 {
139   const _QERArchiveTable* table = GetArchiveTable(archiveModules, path_get_extension(filename));
140
141   if(table != 0)
142   {
143     archive_entry_t entry;
144     entry.name = filename;
145
146     entry.archive = table->m_pfnOpenArchive(filename);
147     entry.is_pakfile = true;
148     g_archives.push_back(entry);
149     globalOutputStream() << "  pak file: " << filename << "\n";
150   }
151 }
152
153 inline void pathlist_prepend_unique(GSList*& pathlist, char* path)
154 {
155   if(g_slist_find_custom(pathlist, path, (GCompareFunc)path_compare) == 0)
156   {
157     pathlist = g_slist_prepend(pathlist, path);
158   }
159   else
160   {
161     g_free(path);
162   }
163 }
164
165 class DirectoryListVisitor : public Archive::Visitor
166 {
167   GSList*& m_matches;
168   const char* m_directory;
169 public:
170   DirectoryListVisitor(GSList*& matches, const char* directory)
171     : m_matches(matches), m_directory(directory)
172   {}
173   void visit(const char* name)
174   {
175     const char* subname = path_make_relative(name, m_directory);
176     if(subname != name)
177     {
178       if(subname[0] == '/')
179         ++subname;
180       char* dir = g_strdup(subname);
181       char* last_char = dir + strlen(dir);
182       if(last_char != dir && *(--last_char) == '/')
183         *last_char = '\0';
184       pathlist_prepend_unique(m_matches, dir);
185     }
186   }
187 };
188
189 class FileListVisitor : public Archive::Visitor
190 {
191   GSList*& m_matches;
192   const char* m_directory;
193   const char* m_extension;
194 public:
195   FileListVisitor(GSList*& matches, const char* directory, const char* extension)
196     : m_matches(matches), m_directory(directory), m_extension(extension)
197   {}
198   void visit(const char* name)
199   {
200     const char* subname = path_make_relative(name, m_directory);
201     if(subname != name)
202     {
203       if(subname[0] == '/')
204         ++subname;
205       if(m_extension[0] == '*' || extension_equal(path_get_extension(subname), m_extension))
206         pathlist_prepend_unique(m_matches, g_strdup (subname));
207     }
208   }
209 };
210     
211 static GSList* GetListInternal (const char *refdir, const char *ext, bool directories, std::size_t depth)
212 {
213   GSList* files = 0;
214
215   ASSERT_MESSAGE(refdir[strlen(refdir) - 1] == '/', "search path does not end in '/'");
216
217   if(directories)
218   {
219     for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
220     {
221       DirectoryListVisitor visitor(files, refdir);
222       (*i).archive->forEachFile(Archive::VisitorFunc(visitor, Archive::eDirectories, depth), refdir);
223     }
224   }
225   else
226   {
227     for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
228     {
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         {
243                 return c - ('a' - 'A');
244         }
245   return c;
246 }
247
248 /*!
249 This behaves identically to stricmp(a,b), except that ASCII chars
250 [\]^`_ come AFTER alphabet chars instead of before. This is because
251 it converts all alphabet chars to uppercase before comparison,
252 while stricmp converts them to lowercase.
253 */
254 static int string_compare_nocase_upper(const char* a, const char* b)
255 {
256         for(;;)
257   {
258                 int c1 = ascii_to_upper(*a++);
259                 int c2 = ascii_to_upper(*b++);
260
261                 if (c1 < c2)
262                 {
263                         return -1; // a < b
264                 }
265                 if (c1 > c2)
266                 {
267                         return 1; // a > b
268                 }
269     if(c1 == 0)
270     {
271       return 0; // a == b
272     }
273         }       
274 }
275
276 // Arnout: note - sort pakfiles in reverse order. This ensures that
277 // later pakfiles override earlier ones. This because the vfs module
278 // returns a filehandle to the first file it can find (while it should
279 // return the filehandle to the file in the most overriding pakfile, the
280 // last one in the list that is).
281
282 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
283 class PakLess
284 {
285 public:
286   bool operator()(const CopiedString& self, const CopiedString& other) const
287   {
288     return string_compare_nocase_upper(self.c_str(), other.c_str()) > 0;
289   }
290 };
291
292 typedef std::set<CopiedString, PakLess> Archives;
293
294 // =============================================================================
295 // Global functions
296
297 // reads all pak files from a dir
298 void InitDirectory(const char* directory, ArchiveModules& archiveModules)
299 {
300   if (g_numDirs == (VFS_MAXDIRS-1))
301     return;
302
303   strncpy(g_strDirs[g_numDirs], directory, PATH_MAX);
304   g_strDirs[g_numDirs][PATH_MAX] = '\0';
305   FixDOSName (g_strDirs[g_numDirs]);
306   AddSlash (g_strDirs[g_numDirs]);
307
308   const char* path = g_strDirs[g_numDirs];
309   
310   g_numDirs++;
311
312   {
313     archive_entry_t entry;
314     entry.name = path;
315     entry.archive = OpenArchive(path);
316     entry.is_pakfile = false;
317     g_archives.push_back(entry);
318   }
319
320   if (g_bUsePak)
321   {
322     GDir* dir = g_dir_open (path, 0, 0);
323
324     if (dir != 0)
325     {
326                         globalOutputStream() << "vfs directory: " << path << "\n";
327
328       const char* ignore_prefix = "";
329       const char* override_prefix = "";
330
331       {
332         // See if we are in "sp" or "mp" mapping mode
333         const char* gamemode = gamemode_get();
334
335                     if (strcmp (gamemode, "sp") == 0)
336         {
337                                   ignore_prefix = "mp_";
338           override_prefix = "sp_";
339         }
340                     else if (strcmp (gamemode, "mp") == 0)
341         {
342                                   ignore_prefix = "sp_";
343           override_prefix = "mp_";
344         }
345       }
346
347       Archives archives;
348       Archives archivesOverride;
349       for(;;)
350       {
351         const char* name = g_dir_read_name(dir);
352         if(name == 0)
353           break;
354
355         const char *ext = strrchr (name, '.');
356
357         if(ext && !string_compare_nocase_upper(ext, ".pk3dir"))
358         {
359           if (g_numDirs == (VFS_MAXDIRS-1))
360             continue;
361           snprintf(g_strDirs[g_numDirs], PATH_MAX, "%s%s/", path, name);
362           g_strDirs[g_numDirs][PATH_MAX] = '\0';
363           FixDOSName (g_strDirs[g_numDirs]);
364           AddSlash (g_strDirs[g_numDirs]);
365           g_numDirs++;
366
367           {
368             archive_entry_t entry;
369             entry.name = g_strDirs[g_numDirs-1];
370             entry.archive = OpenArchive(g_strDirs[g_numDirs-1]);
371             entry.is_pakfile = false;
372             g_archives.push_back(entry);
373           }
374         }
375
376         if ((ext == 0) || *(++ext) == '\0' || GetArchiveTable(archiveModules, ext) == 0)
377           continue;
378
379         // using the same kludge as in engine to ensure consistency
380                                 if(!string_empty(ignore_prefix) && strncmp(name, ignore_prefix, strlen(ignore_prefix)) == 0)
381                                 {
382                                         continue;
383                                 }
384                                 if(!string_empty(override_prefix) && strncmp(name, override_prefix, strlen(override_prefix)) == 0)
385         {
386           archivesOverride.insert(name);
387                                         continue;
388         }
389
390         archives.insert(name);
391       }
392
393       g_dir_close (dir);
394
395                         // add the entries to the vfs
396       for(Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i)
397                         {
398         char filename[PATH_MAX];
399         strcpy(filename, path);
400         strcat(filename, (*i).c_str());
401         InitPakFile(archiveModules, filename);
402                         }
403       for(Archives::iterator i = archives.begin(); i != archives.end(); ++i)
404                         {
405         char filename[PATH_MAX];
406         strcpy(filename, path);
407         strcat(filename, (*i).c_str());
408         InitPakFile(archiveModules, filename);
409                         }
410     }
411     else
412     {
413       globalErrorStream() << "vfs directory not found: " << path << "\n";
414     }
415   }
416 }
417
418 // frees all memory that we allocated
419 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
420 //   (for instance when modifying the project settings)
421 void Shutdown()
422 {
423   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
424   {
425     (*i).archive->release();
426   }
427   g_archives.clear();
428
429   g_numDirs = 0;
430 }
431
432 #define VFS_SEARCH_PAK 0x1
433 #define VFS_SEARCH_DIR 0x2
434
435 int GetFileCount (const char *filename, int flag)
436 {
437   int count = 0;
438   char fixed[PATH_MAX+1];
439
440   strncpy(fixed, filename, PATH_MAX);
441   fixed[PATH_MAX] = '\0';
442   FixDOSName (fixed);
443
444   if(!flag)
445     flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
446
447   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
448   {
449     if((*i).is_pakfile && (flag & VFS_SEARCH_PAK) != 0
450       || !(*i).is_pakfile && (flag & VFS_SEARCH_DIR) != 0)
451     {
452       if((*i).archive->containsFile(fixed))
453         ++count;
454     }
455   }
456
457   return count;
458 }
459
460 ArchiveFile* OpenFile(const char* filename)
461 {
462   ASSERT_MESSAGE(strchr(filename, '\\') == 0, "path contains invalid separator '\\': \"" << filename << "\""); 
463   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
464   {
465     ArchiveFile* file = (*i).archive->openFile(filename);
466     if(file != 0)
467     {
468       return file;
469     }
470   }
471
472   return 0;
473 }
474
475 ArchiveTextFile* OpenTextFile(const char* filename)
476 {
477   ASSERT_MESSAGE(strchr(filename, '\\') == 0, "path contains invalid separator '\\': \"" << filename << "\""); 
478   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
479   {
480     ArchiveTextFile* file = (*i).archive->openTextFile(filename);
481     if(file != 0)
482     {
483       return file;
484     }
485   }
486
487   return 0;
488 }
489
490 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
491 std::size_t LoadFile (const char *filename, void **bufferptr, int index)
492 {
493   char fixed[PATH_MAX+1];
494
495   strncpy (fixed, filename, PATH_MAX);
496   fixed[PATH_MAX] = '\0';
497   FixDOSName (fixed);
498
499   ArchiveFile* file = OpenFile(fixed);
500   
501   if(file != 0)
502   {
503     *bufferptr = malloc (file->size()+1);
504     // we need to end the buffer with a 0
505     ((char*) (*bufferptr))[file->size()] = 0;
506
507     std::size_t length = file->getInputStream().read((InputStream::byte_type*)*bufferptr, file->size());
508     file->release();
509     return length;
510   }
511
512   *bufferptr = 0;
513   return 0;
514 }
515
516 void FreeFile (void *p)
517 {
518   free(p);
519 }
520
521 GSList* GetFileList (const char *dir, const char *ext, std::size_t depth)
522 {
523   return GetListInternal (dir, ext, false, depth);
524 }
525
526 GSList* GetDirList (const char *dir, std::size_t depth)
527 {
528   return GetListInternal (dir, 0, true, depth);
529 }
530
531 void ClearFileDirList (GSList **lst)
532 {
533   while (*lst)
534   {
535     g_free ((*lst)->data);
536     *lst = g_slist_remove (*lst, (*lst)->data);
537   }
538 }
539     
540 const char* FindFile(const char* relative)
541 {
542   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
543   {
544     if((*i).archive->containsFile(relative))
545     {
546       return (*i).name.c_str();
547     }
548   }
549
550   return "";
551 }
552
553 const char* FindPath(const char* absolute)
554 {
555   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
556   {
557     if(path_equal_n(absolute, (*i).name.c_str(), string_length((*i).name.c_str())))
558     {
559       return (*i).name.c_str();
560     }
561   }
562
563   return "";
564 }
565
566
567 class Quake3FileSystem : public VirtualFileSystem
568 {
569 public:
570   void initDirectory(const char *path)
571   {
572     InitDirectory(path, FileSystemQ3API_getArchiveModules());
573   }
574   void initialise()
575   {
576     globalOutputStream() << "filesystem initialised\n";
577     g_observers.realise();
578   }
579   void shutdown()
580   {
581     g_observers.unrealise();
582     globalOutputStream() << "filesystem shutdown\n";
583     Shutdown();
584   }
585
586   int getFileCount(const char *filename, int flags)
587   {
588     return GetFileCount(filename, flags);
589   }
590   ArchiveFile* openFile(const char* filename)
591   {
592     return OpenFile(filename);
593   }
594   ArchiveTextFile* openTextFile(const char* filename)
595   {
596     return OpenTextFile(filename);
597   }
598   std::size_t loadFile(const char *filename, void **buffer)
599   {
600     return LoadFile(filename, buffer, 0);
601   }
602   void freeFile(void *p)
603   {
604     FreeFile(p);
605   }
606
607   void forEachDirectory(const char* basedir, const FileNameCallback& callback, std::size_t depth)
608   {
609     GSList* list = GetDirList(basedir, depth);
610
611     for(GSList* i = list; i != 0; i = g_slist_next(i))
612     {
613       callback(reinterpret_cast<const char*>((*i).data));
614     }
615
616     ClearFileDirList(&list);
617   }
618   void forEachFile(const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth)
619   {
620     GSList* list = GetFileList(basedir, extension, depth);
621
622     for(GSList* i = list; i != 0; i = g_slist_next(i))
623     {
624       const char* name = reinterpret_cast<const char*>((*i).data);
625       if(extension_equal(path_get_extension(name), extension))
626       {
627         callback(name);
628       }
629     }
630
631     ClearFileDirList(&list);
632   }
633   GSList* getDirList(const char *basedir)
634   {
635     return GetDirList(basedir, 1);
636   }
637   GSList* getFileList(const char *basedir, const char *extension)
638   {
639     return GetFileList(basedir, extension, 1);
640   }
641   void clearFileDirList(GSList **lst)
642   {
643     ClearFileDirList(lst);
644   }
645
646   const char* findFile(const char *name)
647   {
648     return FindFile(name);
649   }
650   const char* findRoot(const char *name)
651   {
652     return FindPath(name);
653   }
654
655   void attach(ModuleObserver& observer)
656   {
657     g_observers.attach(observer);
658   }
659   void detach(ModuleObserver& observer)
660   {
661     g_observers.detach(observer);
662   }
663
664   Archive* getArchive(const char* archiveName)
665   {
666     for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
667     {
668       if((*i).is_pakfile)
669       {
670         if(path_equal((*i).name.c_str(), archiveName))
671         {
672           return (*i).archive;
673         }
674       }
675     }
676     return 0;
677   }
678   void forEachArchive(const ArchiveNameCallback& callback)
679   {
680     for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
681     {
682       if((*i).is_pakfile)
683       {
684         callback((*i).name.c_str());
685       }
686     }
687   }
688 };
689
690 Quake3FileSystem g_Quake3FileSystem;
691
692 void FileSystem_Init()
693 {
694 }
695
696 void FileSystem_Shutdown()
697 {
698 }
699
700 VirtualFileSystem& GetFileSystem()
701 {
702   return g_Quake3FileSystem;
703 }