-/*\r
-Copyright (c) 2001, Loki software, inc.\r
-All rights reserved.\r
-\r
-Redistribution and use in source and binary forms, with or without modification, \r
-are permitted provided that the following conditions are met:\r
-\r
-Redistributions of source code must retain the above copyright notice, this list \r
-of conditions and the following disclaimer.\r
-\r
-Redistributions in binary form must reproduce the above copyright notice, this\r
-list of conditions and the following disclaimer in the documentation and/or\r
-other materials provided with the distribution.\r
-\r
-Neither the name of Loki software nor the names of its contributors may be used \r
-to endorse or promote products derived from this software without specific prior \r
-written permission. \r
-\r
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' \r
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE \r
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE \r
-DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY \r
-DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \r
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; \r
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON \r
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT \r
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS \r
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \r
-*/\r
-\r
-//\r
-// Rules:\r
-//\r
-// - Directories should be searched in the following order: ~/.q3a/baseq3,\r
-// install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).\r
-//\r
-// - Pak files are searched first inside the directories.\r
-// - Case insensitive.\r
-// - Unix-style slashes (/) (windows is backwards .. everyone knows that)\r
-//\r
-// Leonardo Zide (leo@lokigames.com)\r
-//\r
-\r
-#include <glib.h>\r
-#include <stdio.h>\r
-\r
-#if defined (__linux__) || defined (__APPLE__)\r
- #include <dirent.h>\r
- #include <unistd.h>\r
-#else\r
- #include <wtypes.h>\r
- #include <io.h>\r
- #define R_OK 04\r
- #define S_ISDIR(mode) (mode & _S_IFDIR)\r
-#endif\r
-\r
-// TTimo: String functions\r
-// see http://www.qeradiant.com/faq/index.cgi?file=175\r
-#include "str.h"\r
-\r
-#include <stdlib.h>\r
-#include <sys/stat.h>\r
-\r
-#include "vfspk3.h"\r
-#include "vfs.h"\r
-#include "unzip-vfspk3.h"\r
-\r
-typedef struct\r
-{\r
- char* name;\r
- unz_s zipinfo;\r
- unzFile zipfile;\r
- guint32 size;\r
-} VFS_PAKFILE;\r
-\r
-// =============================================================================\r
-// Global variables\r
-\r
-static GSList* g_unzFiles;\r
-static GSList* g_pakFiles;\r
-static char g_strDirs[VFS_MAXDIRS][PATH_MAX];\r
-static int g_numDirs;\r
-static bool g_bUsePak = true;\r
-\r
-// =============================================================================\r
-// Static functions\r
-\r
-static void vfsAddSlash (char *str)\r
-{\r
- int n = strlen (str);\r
- if (n > 0)\r
- {\r
- if (str[n-1] != '\\' && str[n-1] != '/')\r
- strcat (str, "/");\r
- }\r
-}\r
-\r
-static void vfsFixDOSName (char *src)\r
-{\r
- if (src == NULL)\r
- return;\r
-\r
- while (*src)\r
- {\r
- if (*src == '\\')\r
- *src = '/';\r
- src++;\r
- }\r
-}\r
-\r
-static void vfsInitPakFile (const char *filename)\r
-{\r
- unz_global_info gi;\r
- unzFile uf;\r
- guint32 i;\r
- int err;\r
-\r
- uf = unzOpen (filename);\r
- if (uf == NULL)\r
- {\r
- g_FuncTable.m_pfnSysFPrintf(SYS_WRN, " failed to init pak file %s\n", filename);\r
- return;\r
- }\r
- g_FuncTable.m_pfnSysPrintf(" pak file: %s\n", filename);\r
-\r
- g_unzFiles = g_slist_append (g_unzFiles, uf);\r
-\r
- err = unzGetGlobalInfo (uf,&gi);\r
- if (err != UNZ_OK)\r
- return;\r
- unzGoToFirstFile(uf);\r
-\r
- for (i = 0; i < gi.number_entry; i++)\r
- {\r
- char filename_inzip[NAME_MAX];\r
- unz_file_info file_info;\r
- VFS_PAKFILE* file;\r
-\r
- err = unzGetCurrentFileInfo (uf, &file_info, filename_inzip, sizeof(filename_inzip), NULL, 0, NULL, 0);\r
- if (err != UNZ_OK)\r
- break;\r
-\r
- file = (VFS_PAKFILE*)g_malloc (sizeof (VFS_PAKFILE));\r
- g_pakFiles = g_slist_append (g_pakFiles, file);\r
-\r
- vfsFixDOSName (filename_inzip);\r
- g_strdown (filename_inzip);\r
-\r
- file->name = g_strdup (filename_inzip);\r
- file->size = file_info.uncompressed_size;\r
- file->zipfile = uf;\r
- memcpy (&file->zipinfo, uf, sizeof (unz_s));\r
-\r
- if ((i+1) < gi.number_entry)\r
- {\r
- err = unzGoToNextFile(uf);\r
- if (err!=UNZ_OK)\r
- break;\r
- }\r
- }\r
-}\r
-\r
-static GSList* vfsGetListInternal (const char *refdir, const char *ext, bool directories)\r
-{\r
- GSList *lst, *lst_aux, *files = NULL;\r
- char dirname[NAME_MAX], extension[NAME_MAX], filename[NAME_MAX];\r
- char basedir[NAME_MAX];\r
- int dirlen;\r
- char *ptr;\r
- char *dirlist;\r
- struct stat st;\r
- GDir *diskdir;\r
- int i;\r
-\r
- if (refdir != NULL)\r
- {\r
- strcpy (dirname, refdir);\r
- g_strdown (dirname);\r
- vfsFixDOSName (dirname);\r
- vfsAddSlash (dirname);\r
- } else\r
- dirname[0] = '\0';\r
- dirlen = strlen (dirname);\r
-\r
- if (ext != NULL)\r
- strcpy (extension, ext);\r
- else\r
- extension[0] = '\0';\r
- g_strdown (extension);\r
-\r
- for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))\r
- {\r
- VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;\r
- gboolean found = FALSE;\r
- ptr = file->name;\r
-\r
- // check that the file name begins with dirname\r
- for (i = 0; (*ptr && i < dirlen); i++, ptr++)\r
- if (*ptr != dirname[i])\r
- break;\r
-\r
- if (i != dirlen)\r
- continue;\r
-\r
- if (directories)\r
- {\r
- char *sep = strchr (ptr, '/');\r
- if (sep == NULL)\r
- continue;\r
-\r
- i = sep-ptr;\r
-\r
- // check for duplicates\r
- for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux))\r
- if (strncmp ((char*)lst_aux->data, ptr, i) == 0)\r
- {\r
- found = TRUE;\r
- break;\r
- }\r
-\r
- if (!found)\r
- {\r
- char *name = g_strndup (ptr, i+1);\r
- name[i] = '\0';\r
- files = g_slist_append (files, name);\r
- }\r
- } else\r
- {\r
- // check extension\r
- char *ptr_ext = strrchr (ptr, '.');\r
- if ((ext != NULL) && ((ptr_ext == NULL) || (strcmp (ptr_ext+1, extension) != 0)))\r
- continue;\r
-\r
- // check for duplicates\r
- for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux))\r
- if (strcmp ((char*)lst_aux->data, ptr) == 0)\r
- {\r
- found = TRUE;\r
- break;\r
- }\r
-\r
- if (!found)\r
- files = g_slist_append (files, g_strdup (ptr));\r
- }\r
- }\r
-\r
- for (i = 0; i < g_numDirs; i++)\r
- {\r
- strcpy (basedir, g_strDirs[i]);\r
- strcat (basedir, dirname);\r
-\r
- diskdir = g_dir_open (basedir, 0, NULL);\r
-\r
- if (diskdir != NULL)\r
- {\r
- while (1)\r
- {\r
- const char* name = g_dir_read_name(diskdir);\r
- if(name == NULL)\r
- break;\r
-\r
- if (directories && (name[0] == '.'))\r
- continue;\r
-\r
- sprintf (filename, "%s%s", basedir, name);\r
- stat (filename, &st);\r
-\r
- if ((S_ISDIR (st.st_mode) != 0) != directories)\r
- continue;\r
-\r
- gboolean found = FALSE;\r
-\r
- dirlist = g_strdup(name);\r
-\r
- g_strdown (dirlist);\r
-\r
- char *ptr_ext = strrchr (dirlist, '.');\r
- if(ext == NULL\r
- || (ext != NULL && ptr_ext != NULL && ptr_ext[0] != '\0' && strcmp (ptr_ext+1, extension) == 0))\r
- {\r
-\r
- // check for duplicates\r
- for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux))\r
- if (strcmp ((char*)lst_aux->data, dirlist) == 0)\r
- {\r
- found = TRUE;\r
- break;\r
- }\r
-\r
- if (!found)\r
- files = g_slist_append (files, g_strdup (dirlist));\r
- }\r
-\r
- g_free(dirlist);\r
- }\r
- g_dir_close (diskdir);\r
- }\r
- }\r
-\r
- return files;\r
-}\r
-\r
-/*!\r
-This behaves identically to -stricmp(a,b), except that ASCII chars\r
-[\]^`_ come AFTER alphabet chars instead of before. This is because\r
-it effectively converts all alphabet chars to uppercase before comparison,\r
-while stricmp converts them to lowercase.\r
-*/\r
-//!\todo Analyse the code in rtcw/q3 to see how it behaves.\r
-static int vfsPakSort (const void *a, const void *b)\r
-{\r
- char *s1, *s2;\r
- int c1, c2;\r
-\r
- s1 = (char*)a;\r
- s2 = (char*)b;\r
-\r
- do {\r
- c1 = *s1++;\r
- c2 = *s2++;\r
-\r
- if (c1 >= 'a' && c1 <= 'z')\r
- {\r
- c1 -= ('a' - 'A');\r
- }\r
- if (c2 >= 'a' && c2 <= 'z')\r
- {\r
- c2 -= ('a' - 'A');\r
- }\r
-\r
- if ( c1 == '\\' || c1 == ':' )\r
- {\r
- c1 = '/';\r
- }\r
- if ( c2 == '\\' || c2 == ':' )\r
- {\r
- c2 = '/';\r
- }\r
- \r
- // Arnout: note - sort pakfiles in reverse order. This ensures that\r
- // later pakfiles override earlier ones. This because the vfs module\r
- // returns a filehandle to the first file it can find (while it should\r
- // return the filehandle to the file in the most overriding pakfile, the\r
- // last one in the list that is).\r
- if (c1 < c2)\r
- {\r
- //return -1; // strings not equal\r
- return 1; // strings not equal\r
- }\r
- if (c1 > c2)\r
- {\r
- //return 1;\r
- return -1;\r
- }\r
- } while (c1);\r
- \r
- return 0; // strings are equal\r
-}\r
-\r
-// =============================================================================\r
-// Global functions\r
-\r
-// reads all pak files from a dir\r
-/*!\r
-The gamemode hacks in here will do undefined things with files called zz_*.\r
-This is simple to fix by cleaning up the hacks, but may be better left alone\r
-if the engine code does the same thing.\r
-*/\r
-void vfsInitDirectory (const char *path)\r
-{\r
- char filename[PATH_MAX];\r
- GDir *dir;\r
- GSList *dirlistptr, *dirlist = NULL;\r
- int iGameMode; // 0: no filtering 1: SP 2: MP\r
-\r
- if (g_numDirs == (VFS_MAXDIRS-1))\r
- return;\r
-\r
- // See if we are in "sp" or "mp" mapping mode\r
- const char* gamemode = g_FuncTable.m_pfnReadProjectKey("gamemode");\r
-\r
- if (gamemode)\r
- {\r
- if (strcmp (gamemode, "sp") == 0)\r
- iGameMode = 1;\r
- else if (strcmp (gamemode, "mp") == 0)\r
- iGameMode = 2;\r
- else\r
- iGameMode = 0; \r
- } else\r
- iGameMode = 0;\r
-\r
- strcpy (g_strDirs[g_numDirs], path);\r
- vfsFixDOSName (g_strDirs[g_numDirs]);\r
- vfsAddSlash (g_strDirs[g_numDirs]);\r
- g_numDirs++;\r
-\r
- if (g_bUsePak)\r
- {\r
- dir = g_dir_open (path, 0, NULL);\r
-\r
- if (dir != NULL)\r
- {\r
- g_FuncTable.m_pfnSysPrintf("vfs directory: %s\n", path);\r
-\r
- for(;;)\r
- {\r
- const char* name = g_dir_read_name(dir);\r
- if(name == NULL)\r
- break;\r
-\r
- char *ext = (char*)strrchr(name, '.');\r
- if ((ext == NULL) || (strcasecmp (ext, ".pk3") != 0))\r
- continue;\r
-\r
- char* direntry = g_strdup(name);\r
-\r
- // using the same kludge as in engine to ensure consistency\r
- switch (iGameMode)\r
- {\r
- case 1: // SP\r
- if (strncmp(direntry,"sp_",3) == 0)\r
- memcpy(direntry,"zz",2);\r
- break;\r
- case 2: // MP\r
- if (strncmp(direntry,"mp_",3) == 0)\r
- memcpy(direntry,"zz",2);\r
- break;\r
- }\r
-\r
- dirlist = g_slist_append (dirlist, direntry);\r
- }\r
-\r
- g_dir_close (dir);\r
-\r
- // sort them\r
- dirlist = g_slist_sort (dirlist, vfsPakSort);\r
-\r
- // add the entries to the vfs and free the list\r
- while (dirlist)\r
- {\r
- GSList *cur = dirlist;\r
- char* name = (char*)cur->data;\r
-\r
- switch (iGameMode)\r
- {\r
- case 1: // SP\r
- if (strncmp(name,"mp_",3) == 0)\r
- {\r
- g_free (name);\r
- dirlist = g_slist_remove (cur, name);\r
- continue;\r
- } else if (strncmp(name,"zz_",3) == 0)\r
- memcpy(name,"sp",2);\r
- break;\r
- case 2: // MP\r
- if (strncmp(name,"sp_",3) == 0)\r
- {\r
- g_free (name);\r
- dirlist = g_slist_remove (cur, name);\r
- continue;\r
- } else if (strncmp(name,"zz_",3) == 0)\r
- memcpy(name,"mp",2);\r
- break;\r
- }\r
-\r
- sprintf (filename, "%s/%s", path, name);\r
- vfsInitPakFile (filename);\r
-\r
- g_free (name);\r
- dirlist = g_slist_remove (cur, name);\r
- }\r
- }\r
- else\r
- g_FuncTable.m_pfnSysFPrintf(SYS_WRN, "vfs directory not found: %s\n", path);\r
- }\r
-}\r
-\r
-// frees all memory that we allocated\r
-// FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?\r
-// (for instance when modifying the project settings)\r
-void vfsShutdown ()\r
-{\r
- while (g_unzFiles)\r
- {\r
- unzClose ((unzFile)g_unzFiles->data);\r
- g_unzFiles = g_slist_remove (g_unzFiles, g_unzFiles->data);\r
- }\r
-\r
- // avoid dangling pointer operation (makes BC hangry)\r
- GSList *cur = g_pakFiles;\r
- GSList *next = cur;\r
- while (next)\r
- {\r
- cur = next;\r
- VFS_PAKFILE* file = (VFS_PAKFILE*)cur->data;\r
- g_free (file->name);\r
- g_free (file);\r
- next = g_slist_remove (cur, file);\r
- }\r
- g_pakFiles = NULL;\r
-}\r
-\r
-void vfsFreeFile (void *p)\r
-{\r
- g_free(p);\r
-}\r
-\r
-GSList* vfsGetFileList (const char *dir, const char *ext)\r
-{\r
- return vfsGetListInternal (dir, ext, false);\r
-}\r
-\r
-GSList* vfsGetDirList (const char *dir)\r
-{\r
- return vfsGetListInternal (dir, NULL, true);\r
-}\r
-\r
-void vfsClearFileDirList (GSList **lst)\r
-{\r
- while (*lst)\r
- {\r
- g_free ((*lst)->data);\r
- *lst = g_slist_remove (*lst, (*lst)->data);\r
- }\r
-}\r
-\r
-int vfsGetFileCount (const char *filename, int flag)\r
-{\r
- int i, count = 0;\r
- char fixed[NAME_MAX], tmp[NAME_MAX];\r
- GSList *lst;\r
-\r
- strcpy (fixed, filename);\r
- vfsFixDOSName (fixed);\r
- g_strdown (fixed);\r
-\r
- if (!flag || (flag & VFS_SEARCH_PAK))\r
- {\r
- for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))\r
- {\r
- VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;\r
- \r
- if (strcmp (file->name, fixed) == 0)\r
- count++;\r
- }\r
- }\r
-\r
- if (!flag || (flag & VFS_SEARCH_DIR))\r
- {\r
- for (i = 0; i < g_numDirs; i++)\r
- {\r
- strcpy (tmp, g_strDirs[i]);\r
- strcat (tmp, fixed);\r
- if (access (tmp, R_OK) == 0)\r
- count++;\r
- }\r
- }\r
-\r
- return count;\r
-}\r
-\r
-// open a full path file\r
-int vfsLoadFullPathFile (const char *filename, void **bufferptr)\r
-{\r
- FILE *f;\r
- long len;\r
-\r
- f = fopen (filename, "rb");\r
- if (f == NULL)\r
- return -1;\r
-\r
- fseek (f, 0, SEEK_END);\r
- len = ftell (f);\r
- rewind (f);\r
-\r
- *bufferptr = g_malloc (len+1);\r
- if (*bufferptr == NULL)\r
- return -1;\r
-\r
- fread (*bufferptr, 1, len, f);\r
- fclose (f);\r
-\r
- // we need to end the buffer with a 0\r
- ((char*) (*bufferptr))[len] = 0;\r
-\r
- return len;\r
-}\r
-\r
-// NOTE: when loading a file, you have to allocate one extra byte and set it to \0\r
-int vfsLoadFile (const char *filename, void **bufferptr, int index)\r
-{\r
- int i, count = 0;\r
- char tmp[NAME_MAX], fixed[NAME_MAX];\r
- GSList *lst;\r
-\r
- *bufferptr = NULL;\r
- strcpy (fixed, filename);\r
- vfsFixDOSName (fixed);\r
- g_strdown (fixed);\r
-\r
- for (i = 0; i < g_numDirs; i++)\r
- {\r
- strcpy (tmp, g_strDirs[i]);\r
- strcat (tmp, filename);\r
- if (access (tmp, R_OK) == 0)\r
- {\r
- if (count == index)\r
- {\r
- return vfsLoadFullPathFile(tmp,bufferptr);\r
- }\r
-\r
- count++;\r
- }\r
- }\r
-\r
- for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))\r
- {\r
- VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;\r
-\r
- if (strcmp (file->name, fixed) != 0)\r
- continue;\r
-\r
- if (count == index)\r
- {\r
- memcpy (file->zipfile, &file->zipinfo, sizeof (unz_s));\r
-\r
- if (unzOpenCurrentFile (file->zipfile) != UNZ_OK)\r
- return -1;\r
-\r
- *bufferptr = g_malloc (file->size+1);\r
- // we need to end the buffer with a 0\r
- ((char*) (*bufferptr))[file->size] = 0;\r
-\r
- i = unzReadCurrentFile (file->zipfile , *bufferptr, file->size);\r
- unzCloseCurrentFile (file->zipfile); \r
- if (i > 0)\r
- return file->size;\r
- else\r
- return -1;\r
- }\r
-\r
- count++;\r
- }\r
-\r
- return -1;\r
-}\r
-\r
-//#ifdef _DEBUG\r
-#if 1\r
- #define DBG_RLTPATH\r
-#endif\r
-\r
-/*!\r
-\param shorten will try to match against the short version\r
-http://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=144\r
-recent switch back to short path names in project settings has broken some stuff\r
-with shorten == true, we will convert in to short version before looking for root\r
-FIXME WAAA .. the stuff below is much more simple on linux .. add appropriate #ifdef\r
-*/\r
-char* vfsExtractRelativePath_short(const char *in, bool shorten)\r
-{\r
- int i;\r
- char l_in[PATH_MAX];\r
- char check[PATH_MAX];\r
- static char out[PATH_MAX];\r
- out[0] = 0;\r
-\r
-#ifdef DBG_RLTPATH\r
- Sys_Printf("vfsExtractRelativePath: %s\n", in);\r
-#endif\r
-\r
-#ifdef _WIN32 \r
- if (shorten)\r
- {\r
- // make it short\r
- if (GetShortPathName(in, l_in, PATH_MAX) == 0)\r
- {\r
-#ifdef DBG_RLTPATH\r
- Sys_Printf("GetShortPathName failed\n");\r
-#endif\r
- return NULL;\r
- }\r
- }\r
- else\r
- {\r
- strcpy(l_in,in);\r
- }\r
- vfsCleanFileName(l_in);\r
-#else\r
- strcpy(l_in, in);\r
- vfsCleanFileName(l_in);\r
-#endif // ifdef WIN32 \r
-\r
-\r
-#ifdef DBG_RLTPATH\r
- Sys_Printf("cleaned path: %s\n", l_in);\r
-#endif\r
-\r
- for (i = 0; i < g_numDirs; i++)\r
- {\r
- strcpy(check,g_strDirs[i]);\r
- vfsCleanFileName(check);\r
-#ifdef DBG_RLTPATH\r
- Sys_Printf("Matching against %s\n", check);\r
-#endif\r
-\r
- // try to find a match\r
- if (strstr(l_in, check))\r
- {\r
- strcpy(out,l_in+strlen(check)+1);\r
- break;\r
- }\r
-\r
- }\r
- if (out[0]!=0)\r
- {\r
-#ifdef DBG_RLTPATH\r
- Sys_Printf("vfsExtractRelativePath: success\n");\r
-#endif\r
- return out;\r
- }\r
-#ifdef DBG_RLTPATH\r
- Sys_Printf("vfsExtractRelativePath: failed\n");\r
-#endif\r
- return NULL;\r
-}\r
-\r
-\r
-// FIXME TTimo: this and the above should be merged at some point\r
-char* vfsExtractRelativePath(const char *in)\r
-{\r
- static char out[PATH_MAX];\r
- unsigned int i, count;\r
- char *chunk, *backup = NULL; // those point to out stuff\r
- char *ret = vfsExtractRelativePath_short(in, false);\r
- if (!ret)\r
- {\r
-#ifdef DBG_RLTPATH\r
- Sys_Printf("trying with a short version\n");\r
-#endif\r
- ret = vfsExtractRelativePath_short(in, true);\r
- if (ret)\r
- {\r
- // ok, but we have a relative short version now\r
- // hack the long relative version out of here\r
- count = 0;\r
- for(i=0;i<strlen(ret);i++)\r
- {\r
- if (ret[i]=='/')\r
- count++;\r
- }\r
- // this is the clean, not short version\r
- strcpy(out, in);\r
- vfsCleanFileName(out);\r
- for(i=0;i<=count;i++)\r
- {\r
- chunk = strrchr(out, '/');\r
- if (backup)\r
- backup[0] = '/';\r
- chunk[0] = '\0';\r
- backup = chunk;\r
- }\r
- return chunk+1;\r
- }\r
- }\r
- return ret;\r
-}\r
-\r
-void vfsCleanFileName(char *in)\r
-{\r
- strlwr(in);\r
- vfsFixDOSName(in);\r
- int n = strlen(in);\r
- if (in[n-1] == '/')\r
- in[n-1] = '\0';\r
-}\r
-\r
-// HYDRA: this now searches VFS/PAK files in addition to the filesystem\r
-// if FLAG is unspecified then ONLY dirs are searched.\r
-// PAK's are searched before DIRs to mimic engine behaviour\r
-// index is ignored when searching PAK files.\r
-// see ifilesystem.h\r
-char* vfsGetFullPath(const char *in, int index, int flag)\r
-{\r
- int count = 0;\r
- static char out[PATH_MAX];\r
- char tmp[NAME_MAX];\r
- int i;\r
-\r
- if (flag & VFS_SEARCH_PAK)\r
- {\r
- char fixed[NAME_MAX];\r
- GSList *lst;\r
-\r
- strcpy (fixed, in);\r
- vfsFixDOSName (fixed);\r
- g_strdown (fixed);\r
-\r
- for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))\r
- {\r
- VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;\r
-\r
- char *ptr,*lastptr;\r
- lastptr = file->name;\r
-\r
- while (ptr = strchr(lastptr,'/'))\r
- lastptr = ptr+1;\r
-\r
- if (strcmp (lastptr, fixed) == 0)\r
- {\r
- strncpy(out,file->name,PATH_MAX);\r
- return out;\r
- }\r
- }\r
-\r
- }\r
-\r
- if (!flag || (flag & VFS_SEARCH_DIR))\r
- {\r
- for (i = 0; i < g_numDirs; i++)\r
- {\r
- strcpy (tmp, g_strDirs[i]);\r
- strcat (tmp, in);\r
- if (access (tmp, R_OK) == 0)\r
- {\r
- if (count == index)\r
- {\r
- strcpy (out, tmp);\r
- return out;\r
- }\r
- count++;\r
- }\r
- }\r
- }\r
- return NULL;\r
-}\r
-\r
-\r
-// TODO TTimo on linux the base prompt is ~/.q3a/<fs_game>\r
-// given the file dialog, we could push the strFSBasePath and ~/.q3a into the directory shortcuts\r
-// FIXME TTimo is this really a VFS functionality?\r
-// actually .. this should be the decision of the core isn't it?\r
-// or .. add an API so that the base prompt can be set during VFS init\r
-const char* vfsBasePromptPath()\r
-{\r
-#ifdef _WIN32 \r
- static char* path = "C:";\r
-#else\r
- static char* path = "/";\r
-#endif\r
- return path;\r
-}\r
-\r
+/*
+ Copyright (c) 2001, Loki software, inc.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this list
+ of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+ Neither the name of Loki software nor the names of its contributors may be used
+ to endorse or promote products derived from this software without specific prior
+ written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+//
+// Rules:
+//
+// - Directories should be searched in the following order: ~/.q3a/baseq3,
+// install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
+//
+// - Pak files are searched first inside the directories.
+// - Case insensitive.
+// - Unix-style slashes (/) (windows is backwards .. everyone knows that)
+//
+// Leonardo Zide (leo@lokigames.com)
+//
+
+#include "vfs.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <glib/gslist.h>
+#include <glib/gdir.h>
+#include <glib/gstrfuncs.h>
+
+#include "qerplugin.h"
+#include "idatastream.h"
+#include "iarchive.h"
+ArchiveModules& FileSystemQ3API_getArchiveModules();
+#include "ifilesystem.h"
+
+#include "generic/callback.h"
+#include "string/string.h"
+#include "stream/stringstream.h"
+#include "os/path.h"
+#include "moduleobservers.h"
+#include "filematch.h"
+
+
+#define VFS_MAXDIRS 64
+
+#if defined( WIN32 )
+#define PATH_MAX 260
+#endif
+
+#define gamemode_get GlobalRadiant().getGameMode
+
+
+
+// =============================================================================
+// Global variables
+
+Archive* OpenArchive( const char* name );
+
+struct archive_entry_t
+{
+ CopiedString name;
+ Archive* archive;
+ bool is_pakfile;
+};
+
+#include <list>
+
+typedef std::list<archive_entry_t> archives_t;
+
+static archives_t g_archives;
+static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
+static int g_numDirs;
+static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
+static int g_numForbiddenDirs = 0;
+static bool g_bUsePak = true;
+
+ModuleObservers g_observers;
+
+// =============================================================================
+// Static functions
+
+static void AddSlash( char *str ){
+ std::size_t n = strlen( str );
+ if ( n > 0 ) {
+ if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
+ globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
+ strcat( str, "/" );
+ }
+ }
+}
+
+static void FixDOSName( char *src ){
+ if ( src == 0 || strchr( src, '\\' ) == 0 ) {
+ return;
+ }
+
+ globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
+
+ while ( *src )
+ {
+ if ( *src == '\\' ) {
+ *src = '/';
+ }
+ src++;
+ }
+}
+
+
+
+const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
+ StringOutputStream tmp( 16 );
+ tmp << LowerCase( ext );
+ return archiveModules.findModule( tmp.c_str() );
+}
+static void InitPakFile( ArchiveModules& archiveModules, const char *filename ){
+ const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
+
+ if ( table != 0 ) {
+ archive_entry_t entry;
+ entry.name = filename;
+
+ entry.archive = table->m_pfnOpenArchive( filename );
+ entry.is_pakfile = true;
+ g_archives.push_back( entry );
+ globalOutputStream() << " pak file: " << filename << "\n";
+ }
+}
+
+inline void pathlist_prepend_unique( GSList*& pathlist, char* path ){
+ if ( g_slist_find_custom( pathlist, path, (GCompareFunc)path_compare ) == 0 ) {
+ pathlist = g_slist_prepend( pathlist, path );
+ }
+ else
+ {
+ g_free( path );
+ }
+}
+
+class DirectoryListVisitor : public Archive::Visitor
+{
+GSList*& m_matches;
+const char* m_directory;
+public:
+DirectoryListVisitor( GSList*& matches, const char* directory )
+ : m_matches( matches ), m_directory( directory )
+{}
+void visit( const char* name ){
+ const char* subname = path_make_relative( name, m_directory );
+ if ( subname != name ) {
+ if ( subname[0] == '/' ) {
+ ++subname;
+ }
+ char* dir = g_strdup( subname );
+ char* last_char = dir + strlen( dir );
+ if ( last_char != dir && *( --last_char ) == '/' ) {
+ *last_char = '\0';
+ }
+ pathlist_prepend_unique( m_matches, dir );
+ }
+}
+};
+
+class FileListVisitor : public Archive::Visitor
+{
+GSList*& m_matches;
+const char* m_directory;
+const char* m_extension;
+public:
+FileListVisitor( GSList*& matches, const char* directory, const char* extension )
+ : m_matches( matches ), m_directory( directory ), m_extension( extension )
+{}
+void visit( const char* name ){
+ const char* subname = path_make_relative( name, m_directory );
+ if ( subname != name ) {
+ if ( subname[0] == '/' ) {
+ ++subname;
+ }
+ if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
+ pathlist_prepend_unique( m_matches, g_strdup( subname ) );
+ }
+ }
+}
+};
+
+static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
+ GSList* files = 0;
+
+ ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
+
+ if ( directories ) {
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ DirectoryListVisitor visitor( files, refdir );
+ ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
+ }
+ }
+ else
+ {
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ FileListVisitor visitor( files, refdir, ext );
+ ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
+ }
+ }
+
+ files = g_slist_reverse( files );
+
+ return files;
+}
+
+inline int ascii_to_upper( int c ){
+ if ( c >= 'a' && c <= 'z' ) {
+ return c - ( 'a' - 'A' );
+ }
+ return c;
+}
+
+/*!
+ This behaves identically to stricmp(a,b), except that ASCII chars
+ [\]^`_ come AFTER alphabet chars instead of before. This is because
+ it converts all alphabet chars to uppercase before comparison,
+ while stricmp converts them to lowercase.
+ */
+static int string_compare_nocase_upper( const char* a, const char* b ){
+ for (;; )
+ {
+ int c1 = ascii_to_upper( *a++ );
+ int c2 = ascii_to_upper( *b++ );
+
+ if ( c1 < c2 ) {
+ return -1; // a < b
+ }
+ if ( c1 > c2 ) {
+ return 1; // a > b
+ }
+ if ( c1 == 0 ) {
+ return 0; // a == b
+ }
+ }
+}
+
+// Arnout: note - sort pakfiles in reverse order. This ensures that
+// later pakfiles override earlier ones. This because the vfs module
+// returns a filehandle to the first file it can find (while it should
+// return the filehandle to the file in the most overriding pakfile, the
+// last one in the list that is).
+
+//!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
+class PakLess
+{
+public:
+bool operator()( const CopiedString& self, const CopiedString& other ) const {
+ return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
+}
+};
+
+typedef std::set<CopiedString, PakLess> Archives;
+
+// =============================================================================
+// Global functions
+
+// reads all pak files from a dir
+void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
+ int j;
+
+ g_numForbiddenDirs = 0;
+ StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
+ for ( j = 0; j < VFS_MAXDIRS; ++j )
+ {
+ const char *t = st.getToken();
+ if ( string_empty( t ) ) {
+ break;
+ }
+ strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
+ g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
+ ++g_numForbiddenDirs;
+ }
+
+ for ( j = 0; j < g_numForbiddenDirs; ++j )
+ {
+ char* dbuf = g_strdup( directory );
+ if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
+ dbuf[strlen( dbuf ) - 1] = 0;
+ }
+ const char *p = strrchr( dbuf, '/' );
+ p = ( p ? ( p + 1 ) : dbuf );
+ if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
+ g_free( dbuf );
+ break;
+ }
+ g_free( dbuf );
+ }
+ if ( j < g_numForbiddenDirs ) {
+ printf( "Directory %s matched by forbidden dirs, removed\n", directory );
+ return;
+ }
+
+ if ( g_numDirs == VFS_MAXDIRS ) {
+ return;
+ }
+
+ strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
+ g_strDirs[g_numDirs][PATH_MAX] = '\0';
+ FixDOSName( g_strDirs[g_numDirs] );
+ AddSlash( g_strDirs[g_numDirs] );
+
+ const char* path = g_strDirs[g_numDirs];
+
+ g_numDirs++;
+
+ {
+ archive_entry_t entry;
+ entry.name = path;
+ entry.archive = OpenArchive( path );
+ entry.is_pakfile = false;
+ g_archives.push_back( entry );
+ }
+
+ if ( g_bUsePak ) {
+ GDir* dir = g_dir_open( path, 0, 0 );
+
+ if ( dir != 0 ) {
+ globalOutputStream() << "vfs directory: " << path << "\n";
+
+ const char* ignore_prefix = "";
+ const char* override_prefix = "";
+
+ {
+ // See if we are in "sp" or "mp" mapping mode
+ const char* gamemode = gamemode_get();
+
+ if ( strcmp( gamemode, "sp" ) == 0 ) {
+ ignore_prefix = "mp_";
+ override_prefix = "sp_";
+ }
+ else if ( strcmp( gamemode, "mp" ) == 0 ) {
+ ignore_prefix = "sp_";
+ override_prefix = "mp_";
+ }
+ }
+
+ Archives archives;
+ Archives archivesOverride;
+ for (;; )
+ {
+ const char* name = g_dir_read_name( dir );
+ if ( name == 0 ) {
+ break;
+ }
+
+ for ( j = 0; j < g_numForbiddenDirs; ++j )
+ {
+ const char *p = strrchr( name, '/' );
+ p = ( p ? ( p + 1 ) : name );
+ if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
+ break;
+ }
+ }
+ if ( j < g_numForbiddenDirs ) {
+ continue;
+ }
+
+ const char *ext = strrchr( name, '.' );
+
+ if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
+ if ( g_numDirs == VFS_MAXDIRS ) {
+ continue;
+ }
+ snprintf( g_strDirs[g_numDirs], PATH_MAX, "%s%s/", path, name );
+ g_strDirs[g_numDirs][PATH_MAX] = '\0';
+ FixDOSName( g_strDirs[g_numDirs] );
+ AddSlash( g_strDirs[g_numDirs] );
+ g_numDirs++;
+
+ {
+ archive_entry_t entry;
+ entry.name = g_strDirs[g_numDirs - 1];
+ entry.archive = OpenArchive( g_strDirs[g_numDirs - 1] );
+ entry.is_pakfile = false;
+ g_archives.push_back( entry );
+ }
+ }
+
+ if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
+ continue;
+ }
+
+ // using the same kludge as in engine to ensure consistency
+ if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
+ continue;
+ }
+ if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
+ archivesOverride.insert( name );
+ continue;
+ }
+
+ archives.insert( name );
+ }
+
+ g_dir_close( dir );
+
+ // add the entries to the vfs
+ for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
+ {
+ char filename[PATH_MAX];
+ strcpy( filename, path );
+ strcat( filename, ( *i ).c_str() );
+ InitPakFile( archiveModules, filename );
+ }
+ for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
+ {
+ char filename[PATH_MAX];
+ strcpy( filename, path );
+ strcat( filename, ( *i ).c_str() );
+ InitPakFile( archiveModules, filename );
+ }
+ }
+ else
+ {
+ globalErrorStream() << "vfs directory not found: " << path << "\n";
+ }
+ }
+}
+
+// frees all memory that we allocated
+// FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
+// (for instance when modifying the project settings)
+void Shutdown(){
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ ( *i ).archive->release();
+ }
+ g_archives.clear();
+
+ g_numDirs = 0;
+ g_numForbiddenDirs = 0;
+}
+
+#define VFS_SEARCH_PAK 0x1
+#define VFS_SEARCH_DIR 0x2
+
+int GetFileCount( const char *filename, int flag ){
+ int count = 0;
+ char fixed[PATH_MAX + 1];
+
+ strncpy( fixed, filename, PATH_MAX );
+ fixed[PATH_MAX] = '\0';
+ FixDOSName( fixed );
+
+ if ( !flag ) {
+ flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
+ }
+
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
+ || !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
+ if ( ( *i ).archive->containsFile( fixed ) ) {
+ ++count;
+ }
+ }
+ }
+
+ return count;
+}
+
+ArchiveFile* OpenFile( const char* filename ){
+ ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ ArchiveFile* file = ( *i ).archive->openFile( filename );
+ if ( file != 0 ) {
+ return file;
+ }
+ }
+
+ return 0;
+}
+
+ArchiveTextFile* OpenTextFile( const char* filename ){
+ ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
+ if ( file != 0 ) {
+ return file;
+ }
+ }
+
+ return 0;
+}
+
+// NOTE: when loading a file, you have to allocate one extra byte and set it to \0
+std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
+ char fixed[PATH_MAX + 1];
+
+ strncpy( fixed, filename, PATH_MAX );
+ fixed[PATH_MAX] = '\0';
+ FixDOSName( fixed );
+
+ ArchiveFile* file = OpenFile( fixed );
+
+ if ( file != 0 ) {
+ *bufferptr = malloc( file->size() + 1 );
+ // we need to end the buffer with a 0
+ ( (char*) ( *bufferptr ) )[file->size()] = 0;
+
+ std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
+ file->release();
+ return length;
+ }
+
+ *bufferptr = 0;
+ return 0;
+}
+
+void FreeFile( void *p ){
+ free( p );
+}
+
+GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
+ return GetListInternal( dir, ext, false, depth );
+}
+
+GSList* GetDirList( const char *dir, std::size_t depth ){
+ return GetListInternal( dir, 0, true, depth );
+}
+
+void ClearFileDirList( GSList **lst ){
+ while ( *lst )
+ {
+ g_free( ( *lst )->data );
+ *lst = g_slist_remove( *lst, ( *lst )->data );
+ }
+}
+
+const char* FindFile( const char* relative ){
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ if ( ( *i ).archive->containsFile( relative ) ) {
+ return ( *i ).name.c_str();
+ }
+ }
+
+ return "";
+}
+
+const char* FindPath( const char* absolute ){
+ const char *best = "";
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
+ if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
+ best = ( *i ).name.c_str();
+ }
+ }
+ }
+
+ return best;
+}
+
+
+class Quake3FileSystem : public VirtualFileSystem
+{
+public:
+void initDirectory( const char *path ){
+ InitDirectory( path, FileSystemQ3API_getArchiveModules() );
+}
+void initialise(){
+ globalOutputStream() << "filesystem initialised\n";
+ g_observers.realise();
+}
+void shutdown(){
+ g_observers.unrealise();
+ globalOutputStream() << "filesystem shutdown\n";
+ Shutdown();
+}
+
+int getFileCount( const char *filename, int flags ){
+ return GetFileCount( filename, flags );
+}
+ArchiveFile* openFile( const char* filename ){
+ return OpenFile( filename );
+}
+ArchiveTextFile* openTextFile( const char* filename ){
+ return OpenTextFile( filename );
+}
+std::size_t loadFile( const char *filename, void **buffer ){
+ return LoadFile( filename, buffer, 0 );
+}
+void freeFile( void *p ){
+ FreeFile( p );
+}
+
+void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
+ GSList* list = GetDirList( basedir, depth );
+
+ for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
+ {
+ callback( reinterpret_cast<const char*>( ( *i ).data ) );
+ }
+
+ ClearFileDirList( &list );
+}
+void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
+ GSList* list = GetFileList( basedir, extension, depth );
+
+ for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
+ {
+ const char* name = reinterpret_cast<const char*>( ( *i ).data );
+ if ( extension_equal( path_get_extension( name ), extension ) ) {
+ callback( name );
+ }
+ }
+
+ ClearFileDirList( &list );
+}
+GSList* getDirList( const char *basedir ){
+ return GetDirList( basedir, 1 );
+}
+GSList* getFileList( const char *basedir, const char *extension ){
+ return GetFileList( basedir, extension, 1 );
+}
+void clearFileDirList( GSList **lst ){
+ ClearFileDirList( lst );
+}
+
+const char* findFile( const char *name ){
+ return FindFile( name );
+}
+const char* findRoot( const char *name ){
+ return FindPath( name );
+}
+
+void attach( ModuleObserver& observer ){
+ g_observers.attach( observer );
+}
+void detach( ModuleObserver& observer ){
+ g_observers.detach( observer );
+}
+
+Archive* getArchive( const char* archiveName, bool pakonly ){
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ if ( pakonly && !( *i ).is_pakfile ) {
+ continue;
+ }
+
+ if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
+ return ( *i ).archive;
+ }
+ }
+ return 0;
+}
+void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
+ if ( reverse ) {
+ g_archives.reverse();
+ }
+
+ for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
+ {
+ if ( pakonly && !( *i ).is_pakfile ) {
+ continue;
+ }
+
+ callback( ( *i ).name.c_str() );
+ }
+
+ if ( reverse ) {
+ g_archives.reverse();
+ }
+}
+};
+
+Quake3FileSystem g_Quake3FileSystem;
+
+void FileSystem_Init(){
+}
+
+void FileSystem_Shutdown(){
+}
+
+VirtualFileSystem& GetFileSystem(){
+ return g_Quake3FileSystem;
+}