2 Copyright (c) 2001, Loki software, inc.
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
8 Redistributions of source code must retain the above copyright notice, this list
9 of conditions and the following disclaimer.
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.
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
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.
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).
37 // - Pak files are searched first inside the directories.
38 // - Case insensitive.
39 // - Unix-style slashes (/) (windows is backwards .. everyone knows that)
41 // Leonardo Zide (leo@lokigames.com)
48 #include <glib/gslist.h>
49 #include <glib/gdir.h>
50 #include <glib/gstrfuncs.h>
52 #include "qerplugin.h"
53 #include "idatastream.h"
55 ArchiveModules& FileSystemQ3API_getArchiveModules();
56 #include "ifilesystem.h"
58 #include "generic/callback.h"
59 #include "string/string.h"
60 #include "stream/stringstream.h"
62 #include "moduleobservers.h"
63 #include "filematch.h"
66 #define VFS_MAXDIRS 64
72 #define gamemode_get GlobalRadiant().getGameMode
76 // =============================================================================
79 Archive* OpenArchive(const char* name);
81 struct archive_entry_t
90 typedef std::list<archive_entry_t> archives_t;
92 static archives_t g_archives;
93 static char g_strDirs[VFS_MAXDIRS][PATH_MAX+1];
95 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX+1];
96 static int g_numForbiddenDirs = 0;
97 static bool g_bUsePak = true;
99 ModuleObservers g_observers;
101 // =============================================================================
104 static void AddSlash (char *str)
106 std::size_t n = strlen (str);
109 if (str[n-1] != '\\' && str[n-1] != '/')
111 globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
117 static void FixDOSName (char *src)
119 if (src == 0 || strchr(src, '\\') == 0)
122 globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
134 const _QERArchiveTable* GetArchiveTable(ArchiveModules& archiveModules, const char* ext)
136 StringOutputStream tmp(16);
137 tmp << LowerCase(ext);
138 return archiveModules.findModule(tmp.c_str());
140 static void InitPakFile (ArchiveModules& archiveModules, const char *filename)
142 const _QERArchiveTable* table = GetArchiveTable(archiveModules, path_get_extension(filename));
146 archive_entry_t entry;
147 entry.name = filename;
149 entry.archive = table->m_pfnOpenArchive(filename);
150 entry.is_pakfile = true;
151 g_archives.push_back(entry);
152 globalOutputStream() << " pak file: " << filename << "\n";
156 inline void pathlist_prepend_unique(GSList*& pathlist, char* path)
158 if(g_slist_find_custom(pathlist, path, (GCompareFunc)path_compare) == 0)
160 pathlist = g_slist_prepend(pathlist, path);
168 class DirectoryListVisitor : public Archive::Visitor
171 const char* m_directory;
173 DirectoryListVisitor(GSList*& matches, const char* directory)
174 : m_matches(matches), m_directory(directory)
176 void visit(const char* name)
178 const char* subname = path_make_relative(name, m_directory);
181 if(subname[0] == '/')
183 char* dir = g_strdup(subname);
184 char* last_char = dir + strlen(dir);
185 if(last_char != dir && *(--last_char) == '/')
187 pathlist_prepend_unique(m_matches, dir);
192 class FileListVisitor : public Archive::Visitor
195 const char* m_directory;
196 const char* m_extension;
198 FileListVisitor(GSList*& matches, const char* directory, const char* extension)
199 : m_matches(matches), m_directory(directory), m_extension(extension)
201 void visit(const char* name)
203 const char* subname = path_make_relative(name, m_directory);
206 if(subname[0] == '/')
208 if(m_extension[0] == '*' || extension_equal(path_get_extension(subname), m_extension))
209 pathlist_prepend_unique(m_matches, g_strdup (subname));
214 static GSList* GetListInternal (const char *refdir, const char *ext, bool directories, std::size_t depth)
218 ASSERT_MESSAGE(refdir[strlen(refdir) - 1] == '/', "search path does not end in '/'");
222 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);
230 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
232 FileListVisitor visitor(files, refdir, ext);
233 (*i).archive->forEachFile(Archive::VisitorFunc(visitor, Archive::eFiles, depth), refdir);
237 files = g_slist_reverse(files);
242 inline int ascii_to_upper(int c)
244 if (c >= 'a' && c <= 'z')
246 return c - ('a' - 'A');
252 This behaves identically to stricmp(a,b), except that ASCII chars
253 [\]^`_ come AFTER alphabet chars instead of before. This is because
254 it converts all alphabet chars to uppercase before comparison,
255 while stricmp converts them to lowercase.
257 static int string_compare_nocase_upper(const char* a, const char* b)
261 int c1 = ascii_to_upper(*a++);
262 int c2 = ascii_to_upper(*b++);
279 // Arnout: note - sort pakfiles in reverse order. This ensures that
280 // later pakfiles override earlier ones. This because the vfs module
281 // returns a filehandle to the first file it can find (while it should
282 // return the filehandle to the file in the most overriding pakfile, the
283 // last one in the list that is).
285 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
289 bool operator()(const CopiedString& self, const CopiedString& other) const
291 return string_compare_nocase_upper(self.c_str(), other.c_str()) > 0;
295 typedef std::set<CopiedString, PakLess> Archives;
297 // =============================================================================
300 // reads all pak files from a dir
301 void InitDirectory(const char* directory, ArchiveModules& archiveModules)
305 g_numForbiddenDirs = 0;
306 StringTokeniser st(GlobalRadiant().getGameDescriptionKeyValue("forbidden_paths"), " ");
307 for(j = 0; j < VFS_MAXDIRS; ++j)
309 const char *t = st.getToken();
312 strncpy(g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX);
313 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
314 ++g_numForbiddenDirs;
317 for(j = 0; j < g_numForbiddenDirs; ++j)
319 char* dbuf = g_strdup(directory);
320 if(*dbuf && dbuf[strlen(dbuf)-1] == '/')
321 dbuf[strlen(dbuf)-1] = 0;
322 const char *p = strrchr(dbuf, '/');
323 p = (p ? (p+1) : dbuf);
324 if(matchpattern(p, g_strForbiddenDirs[j], TRUE))
331 if(j < g_numForbiddenDirs)
333 printf("Directory %s matched by forbidden dirs, removed\n", directory);
337 if (g_numDirs == VFS_MAXDIRS)
340 strncpy(g_strDirs[g_numDirs], directory, PATH_MAX);
341 g_strDirs[g_numDirs][PATH_MAX] = '\0';
342 FixDOSName (g_strDirs[g_numDirs]);
343 AddSlash (g_strDirs[g_numDirs]);
345 const char* path = g_strDirs[g_numDirs];
350 archive_entry_t entry;
352 entry.archive = OpenArchive(path);
353 entry.is_pakfile = false;
354 g_archives.push_back(entry);
359 GDir* dir = g_dir_open (path, 0, 0);
363 globalOutputStream() << "vfs directory: " << path << "\n";
365 const char* ignore_prefix = "";
366 const char* override_prefix = "";
369 // See if we are in "sp" or "mp" mapping mode
370 const char* gamemode = gamemode_get();
372 if (strcmp (gamemode, "sp") == 0)
374 ignore_prefix = "mp_";
375 override_prefix = "sp_";
377 else if (strcmp (gamemode, "mp") == 0)
379 ignore_prefix = "sp_";
380 override_prefix = "mp_";
385 Archives archivesOverride;
388 const char* name = g_dir_read_name(dir);
392 for(j = 0; j < g_numForbiddenDirs; ++j)
394 const char *p = strrchr(name, '/');
395 p = (p ? (p+1) : name);
396 if(matchpattern(p, g_strForbiddenDirs[j], TRUE))
399 if(j < g_numForbiddenDirs)
402 const char *ext = strrchr (name, '.');
404 if(ext && !string_compare_nocase_upper(ext, ".pk3dir"))
406 if (g_numDirs == VFS_MAXDIRS)
408 snprintf(g_strDirs[g_numDirs], PATH_MAX, "%s%s/", path, name);
409 g_strDirs[g_numDirs][PATH_MAX] = '\0';
410 FixDOSName (g_strDirs[g_numDirs]);
411 AddSlash (g_strDirs[g_numDirs]);
415 archive_entry_t entry;
416 entry.name = g_strDirs[g_numDirs-1];
417 entry.archive = OpenArchive(g_strDirs[g_numDirs-1]);
418 entry.is_pakfile = false;
419 g_archives.push_back(entry);
423 if ((ext == 0) || *(++ext) == '\0' || GetArchiveTable(archiveModules, ext) == 0)
426 // using the same kludge as in engine to ensure consistency
427 if(!string_empty(ignore_prefix) && strncmp(name, ignore_prefix, strlen(ignore_prefix)) == 0)
431 if(!string_empty(override_prefix) && strncmp(name, override_prefix, strlen(override_prefix)) == 0)
433 archivesOverride.insert(name);
437 archives.insert(name);
442 // add the entries to the vfs
443 for(Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i)
445 char filename[PATH_MAX];
446 strcpy(filename, path);
447 strcat(filename, (*i).c_str());
448 InitPakFile(archiveModules, filename);
450 for(Archives::iterator i = archives.begin(); i != archives.end(); ++i)
452 char filename[PATH_MAX];
453 strcpy(filename, path);
454 strcat(filename, (*i).c_str());
455 InitPakFile(archiveModules, filename);
460 globalErrorStream() << "vfs directory not found: " << path << "\n";
465 // frees all memory that we allocated
466 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
467 // (for instance when modifying the project settings)
470 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
472 (*i).archive->release();
477 g_numForbiddenDirs = 0;
480 #define VFS_SEARCH_PAK 0x1
481 #define VFS_SEARCH_DIR 0x2
483 int GetFileCount (const char *filename, int flag)
486 char fixed[PATH_MAX+1];
488 strncpy(fixed, filename, PATH_MAX);
489 fixed[PATH_MAX] = '\0';
493 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
495 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
497 if((*i).is_pakfile && (flag & VFS_SEARCH_PAK) != 0
498 || !(*i).is_pakfile && (flag & VFS_SEARCH_DIR) != 0)
500 if((*i).archive->containsFile(fixed))
508 ArchiveFile* OpenFile(const char* filename)
510 ASSERT_MESSAGE(strchr(filename, '\\') == 0, "path contains invalid separator '\\': \"" << filename << "\"");
511 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
513 ArchiveFile* file = (*i).archive->openFile(filename);
523 ArchiveTextFile* OpenTextFile(const char* filename)
525 ASSERT_MESSAGE(strchr(filename, '\\') == 0, "path contains invalid separator '\\': \"" << filename << "\"");
526 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
528 ArchiveTextFile* file = (*i).archive->openTextFile(filename);
538 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
539 std::size_t LoadFile (const char *filename, void **bufferptr, int index)
541 char fixed[PATH_MAX+1];
543 strncpy (fixed, filename, PATH_MAX);
544 fixed[PATH_MAX] = '\0';
547 ArchiveFile* file = OpenFile(fixed);
551 *bufferptr = malloc (file->size()+1);
552 // we need to end the buffer with a 0
553 ((char*) (*bufferptr))[file->size()] = 0;
555 std::size_t length = file->getInputStream().read((InputStream::byte_type*)*bufferptr, file->size());
564 void FreeFile (void *p)
569 GSList* GetFileList (const char *dir, const char *ext, std::size_t depth)
571 return GetListInternal (dir, ext, false, depth);
574 GSList* GetDirList (const char *dir, std::size_t depth)
576 return GetListInternal (dir, 0, true, depth);
579 void ClearFileDirList (GSList **lst)
583 g_free ((*lst)->data);
584 *lst = g_slist_remove (*lst, (*lst)->data);
588 const char* FindFile(const char* relative)
590 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
592 if((*i).archive->containsFile(relative))
594 return (*i).name.c_str();
601 const char* FindPath(const char* absolute)
603 const char *best = "";
604 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
606 if(string_length((*i).name.c_str()) > string_length(best))
607 if(path_equal_n(absolute, (*i).name.c_str(), string_length((*i).name.c_str())))
608 best = (*i).name.c_str();
615 class Quake3FileSystem : public VirtualFileSystem
618 void initDirectory(const char *path)
620 InitDirectory(path, FileSystemQ3API_getArchiveModules());
624 globalOutputStream() << "filesystem initialised\n";
625 g_observers.realise();
629 g_observers.unrealise();
630 globalOutputStream() << "filesystem shutdown\n";
634 int getFileCount(const char *filename, int flags)
636 return GetFileCount(filename, flags);
638 ArchiveFile* openFile(const char* filename)
640 return OpenFile(filename);
642 ArchiveTextFile* openTextFile(const char* filename)
644 return OpenTextFile(filename);
646 std::size_t loadFile(const char *filename, void **buffer)
648 return LoadFile(filename, buffer, 0);
650 void freeFile(void *p)
655 void forEachDirectory(const char* basedir, const FileNameCallback& callback, std::size_t depth)
657 GSList* list = GetDirList(basedir, depth);
659 for(GSList* i = list; i != 0; i = g_slist_next(i))
661 callback(reinterpret_cast<const char*>((*i).data));
664 ClearFileDirList(&list);
666 void forEachFile(const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth)
668 GSList* list = GetFileList(basedir, extension, depth);
670 for(GSList* i = list; i != 0; i = g_slist_next(i))
672 const char* name = reinterpret_cast<const char*>((*i).data);
673 if(extension_equal(path_get_extension(name), extension))
679 ClearFileDirList(&list);
681 GSList* getDirList(const char *basedir)
683 return GetDirList(basedir, 1);
685 GSList* getFileList(const char *basedir, const char *extension)
687 return GetFileList(basedir, extension, 1);
689 void clearFileDirList(GSList **lst)
691 ClearFileDirList(lst);
694 const char* findFile(const char *name)
696 return FindFile(name);
698 const char* findRoot(const char *name)
700 return FindPath(name);
703 void attach(ModuleObserver& observer)
705 g_observers.attach(observer);
707 void detach(ModuleObserver& observer)
709 g_observers.detach(observer);
712 Archive* getArchive(const char* archiveName, bool pakonly)
714 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
716 if(pakonly && !(*i).is_pakfile)
719 if(path_equal((*i).name.c_str(), archiveName))
724 void forEachArchive(const ArchiveNameCallback& callback, bool pakonly, bool reverse)
727 g_archives.reverse();
729 for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
731 if(pakonly && !(*i).is_pakfile)
734 callback((*i).name.c_str());
738 g_archives.reverse();
742 Quake3FileSystem g_Quake3FileSystem;
744 void FileSystem_Init()
748 void FileSystem_Shutdown()
752 VirtualFileSystem& GetFileSystem()
754 return g_Quake3FileSystem;