/* * Copyright(c) 1997-2001 Id Software, Inc. * Copyright(c) 2002 The Quakeforge Project. * Copyright(c) 2006 Quetoo. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or(at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include #include #include "qcommon.h" #define PAK_HEADER (('K'<<24) + ('C'<<16) + ('A'<<8) + 'P') #define MAX_FILES_IN_PACK 4096 /* The .pak files are just a linear collapse of a directory tree */ typedef struct { char name[56]; int filepos, filelen; } pakfile_t; typedef struct { int ident; // == IDPAKHEADER int dirofs; int dirlen; } pakheader_t; typedef struct pak_s { char filename[MAX_OSPATH]; FILE *handle; int numfiles; pakfile_t *files; } pak_t; char fs_gamedir[MAX_OSPATH]; cvar_t *fs_basedir; cvar_t *fs_gamedirvar; typedef struct filelink_s { struct filelink_s *next; char *from; int fromlength; char *to; } filelink_t; filelink_t *fs_links; typedef struct searchpath_s { char filename[MAX_OSPATH]; pak_t *pak; // only one of filename / pak will be used struct searchpath_s *next; } searchpath_t; searchpath_t *fs_searchpaths; searchpath_t *fs_base_searchpaths; // without gamedirs /* FS_FileLength */ int FS_FileLength(FILE *f){ int pos; int end; pos = ftell(f); fseek(f, 0, SEEK_END); end = ftell(f); fseek(f, pos, SEEK_SET); return end; } /* FS_CreatePath Creates any directories needed to store the given filename */ void FS_CreatePath(char *path){ char *ofs; for(ofs = path + 1; *ofs; ofs++){ if(*ofs == '/'){ // create the directory *ofs = 0; Sys_Mkdir(path); *ofs = '/'; } } } /* FS_FCloseFile For some reason, other dll's can't just cal fclose() on files returned by FS_FOpenFile... */ void FS_FCloseFile(FILE *f){ fclose(f); } /* FS_FOpenFile Finds the file in the search path. returns filesize and an open FILE * Used for streaming data out of either a pak file or a seperate file. */ int FS_FOpenFile(char *filename, FILE **file){ searchpath_t *search; char netpath[MAX_OSPATH]; struct stat sbuf; pak_t *pak; int i; filelink_t *link; // check for links first for(link = fs_links; link; link = link->next){ if(!strncmp(filename, link->from, link->fromlength)){ Com_sprintf(netpath, sizeof(netpath), "%s%s", link->to, filename + link->fromlength); *file = fopen(netpath, "rb"); if(*file){ return FS_FileLength(*file); } return -1; } } // search through the path, one element at a time for(search = fs_searchpaths; search; search = search->next){ // is the element a pak file? if(search->pak){ // look through all the pak file elements pak = search->pak; for(i = 0; i < pak->numfiles; i++) if(!Q_stricmp(pak->files[i].name, filename)){ // found it! // open a new file on the pakfile *file = fopen(pak->filename, "rb"); if(!*file) Com_Error(ERR_FATAL, "Couldn't reopen %s", pak->filename); fseek(*file, pak->files[i].filepos, SEEK_SET); return pak->files[i].filelen; } } else { // check a file in the directory tree Com_sprintf(netpath, sizeof(netpath), "%s/%s", search->filename, filename); if(stat(netpath, &sbuf) == -1 || !S_ISREG(sbuf.st_mode)) continue; if((*file = fopen(netpath, "rb")) == NULL) continue; return FS_FileLength(*file); } } Com_DPrintf("FS_FOpenFile: can't find %s\n", filename); *file = NULL; return -1; } /* FS_ReadFile Properly handles partial reads */ #define MAX_READ 0x10000 // read in blocks of 64k void FS_Read(void *buffer, int len, FILE *f){ int block, remaining; int read; byte *buf; int tries; buf = (byte *)buffer; // read in chunks for progress bar remaining = len; tries = 0; while(remaining){ block = remaining; if(block > MAX_READ) block = MAX_READ; read = fread(buf, 1, block, f); if(read < 1) //read failed, exit Com_Error(ERR_FATAL, "FS_Read: %d bytes read", read); remaining -= read; buf += read; } } /* FS_LoadFile Filename are reletive to the quake search path a null buffer will just return the file length without loading */ int FS_LoadFile(char *path, void **buffer){ FILE *h; byte *buf; int len; buf = NULL; // quiet compiler warning // look for it in the filesystem or pak files len = FS_FOpenFile(path, &h); if(!h){ if(buffer) *buffer = NULL; return -1; } if(!buffer){ fclose(h); return len; } buf = Z_Malloc(len); *buffer = buf; FS_Read(buf, len, h); fclose(h); return len; } /* FS_FreeFile */ void FS_FreeFile(void *buffer){ Z_Free(buffer); } /* FS_LoadPakFile Takes an explicit(not game tree related) path to a pak file. Loads the header and directory, adding the files at the beginning of the list so they override previous pak files. */ pak_t *FS_LoadPakFile(char *pakfile){ pakheader_t header; int numpakfiles; pakfile_t *newfiles = 0; int i; pak_t *pak; FILE *pakhandle; pakfile_t info[MAX_FILES_IN_PACK]; pakhandle = fopen(pakfile, "rb"); if(!pakhandle) return NULL; fread(&header, 1, sizeof(header), pakhandle); if(LittleLong(header.ident) != PAK_HEADER) Com_Error(ERR_FATAL, "%s is not a pak file.", pakfile); header.dirofs = LittleLong(header.dirofs); header.dirlen = LittleLong(header.dirlen); numpakfiles = header.dirlen / sizeof(pakfile_t); if(numpakfiles > MAX_FILES_IN_PACK) Com_Error(ERR_FATAL, "%s has %i files.", pakfile, numpakfiles); newfiles = Z_Malloc(numpakfiles * sizeof(pakfile_t)); fseek(pakhandle, header.dirofs, SEEK_SET); fread(info, 1, header.dirlen, pakhandle); // parse the directory for(i = 0; i < numpakfiles; ++i){ strcpy(newfiles[i].name, info[i].name); newfiles[i].filepos = LittleLong(info[i].filepos); newfiles[i].filelen = LittleLong(info[i].filelen); } pak = Z_Malloc(sizeof(pak_t)); strcpy(pak->filename, pakfile); pak->handle = pakhandle; pak->numfiles = numpakfiles; pak->files = newfiles; Com_Printf("Added %s: %i files.\n", pakfile, numpakfiles); return pak; } /* dotpak Returns 1 if d appears to be a 3rd party pak file, 0 otherwise. */ int dotpak(const struct dirent *d){ if(!strstr(d->d_name, ".pak")) return 0; // not a pak if(COM_GlobMatchStar("pak*.pak", (char *)d->d_name)) return 0; // pak0-pak9 are already done return 1; } /* FS_AddGameDirectory Adds the directory to the head of the path, then loads and adds pak1.pak pak2.pak ... */ void FS_AddGameDirectory(char *dir){ int i; searchpath_t *search; pak_t *pak; char pakfile[MAX_OSPATH]; struct dirent **contents; // add the base directory to the search path search = Z_Malloc(sizeof(searchpath_t)); strncpy(search->filename, dir, sizeof(search->filename) - 1); search->filename[sizeof(search->filename) - 1] = 0; search->next = fs_searchpaths; fs_searchpaths = search; // add pak0 through pak9 to search path for(i = 0; i < 10; i++){ Com_sprintf(pakfile, sizeof(pakfile), "%s/pak%i.pak", dir, i); pak = FS_LoadPakFile(pakfile); if(!pak) continue; search = Z_Malloc(sizeof(searchpath_t)); search->pak = pak; search->next = fs_searchpaths; fs_searchpaths = search; } // and lastly add all other paks if((i = scandir(dir, &contents, dotpak, alphasort)) < 0) return; // no other pak files in dir while(i--){ Com_sprintf(pakfile, sizeof(pakfile), "%s/%s", dir, contents[i]->d_name); pak = FS_LoadPakFile(pakfile); if(!pak) continue; search = Z_Malloc(sizeof(searchpath_t)); search->pak = pak; search->next = fs_searchpaths; fs_searchpaths = search; free(contents[i]); } free(contents); } /* FS_AddHomeAsGameDirectory Use ~/.quake2/dir as fs_gamedir on Unix systems */ void FS_AddHomeAsGameDirectory(char *dir){ #ifndef _WIN32 char gdir[MAX_OSPATH]; char *homedir = getenv("HOME"); if(homedir){ int len = snprintf(gdir, sizeof(gdir), "%s/.quake2/%s/", homedir, dir); Com_Printf("Using %s for writing.\n", gdir); FS_CreatePath(gdir); if((len > 0) &&(len < sizeof(gdir)) &&(gdir[len - 1] == '/')) gdir[len - 1] = 0; strncpy(fs_gamedir, gdir, sizeof(fs_gamedir) - 1); fs_gamedir[sizeof(fs_gamedir) - 1] = 0; FS_AddGameDirectory(gdir); } #endif } /* FS_Gamedir Called to find where to write a file (demos, savegames, etc) */ char *FS_Gamedir(void){ return fs_gamedir; } /* FS_ExecAutoexec Execs the local autoexec.cfg for the current gamedir. This is a function call rather than simply stuffing "exec autoexec.cfg" because we do not wish to use baseq2/autoexec.cfg for all mods. */ void FS_ExecAutoexec(void){ char name [MAX_QPATH]; searchpath_t *s, *end; // don't look in baseq2 if gamedir is set if(fs_searchpaths == fs_base_searchpaths) end = NULL; else end = fs_base_searchpaths; // search through all the paths for an autoexec.cfg file for(s = fs_searchpaths; s != end; s = s->next){ if(s->pak) continue; Com_sprintf(name, sizeof(name), "%s/autoexec.cfg", s->filename); if(Sys_FindFirst(name)){ Cbuf_AddText("exec autoexec.cfg\n"); Sys_FindClose(); break; } Sys_FindClose(); } } /* FS_SetGamedir Sets the gamedir and path to a different directory. */ void FS_SetGamedir(char *dir){ searchpath_t *next; if(strstr(dir, "..") || strstr(dir, "/") || strstr(dir, "\\") || strstr(dir, ":")){ Com_Printf("Gamedir should be a single filename, not a path\n"); return; } // free up any current game dir info while(fs_searchpaths != fs_base_searchpaths){ if(fs_searchpaths->pak){ fclose(fs_searchpaths->pak->handle); Z_Free(fs_searchpaths->pak->files); Z_Free(fs_searchpaths->pak); } next = fs_searchpaths->next; Z_Free(fs_searchpaths); fs_searchpaths = next; } // now add new entries for if(!strcmp(dir, BASEDIRNAME) || (*dir == 0)){ Cvar_FullSet("gamedir", "", CVAR_SERVERINFO | CVAR_NOSET); Cvar_FullSet("game", "", CVAR_LATCH | CVAR_SERVERINFO); } else { Cvar_FullSet("gamedir", dir, CVAR_SERVERINFO | CVAR_NOSET); FS_AddGameDirectory(va(PKGLIBDIR"/%s", dir)); FS_AddGameDirectory(va(PKGDATADIR"/%s", dir)); FS_AddHomeAsGameDirectory(dir); } } /* FS_Link_f Creates a filelink_t */ void FS_Link_f(void){ filelink_t *l, **prev; char *from; if(Cmd_Argc() != 3){ Com_Printf("Usage: link \n"); return; } from = Cmd_Argv(1); // jail link sources to gamedir if(strstr(from, "..") || *from == '/' || *from == '.'){ Com_Printf("Illegal path name %s\n", from); return; } // see if the link already exists prev = &fs_links; for(l = fs_links; l; l = l->next){ if(!strcmp(l->from, Cmd_Argv(1))){ Z_Free(l->to); if(!strlen(Cmd_Argv(2))){ // delete it *prev = l->next; Z_Free(l->from); Z_Free(l); return; } l->to = CopyString(Cmd_Argv(2)); return; } prev = &l->next; } // create a new link l = Z_Malloc(sizeof(*l)); l->next = fs_links; fs_links = l; l->from = CopyString(Cmd_Argv(1)); l->fromlength = strlen(l->from); l->to = CopyString(Cmd_Argv(2)); } /* ** FS_ListFiles */ char **FS_ListFiles(char *findname, int *numfiles){ char *s; int nfiles = 0; char **list = 0; s = Sys_FindFirst(findname); while(s){ if(s[strlen(s) - 1] != '.') nfiles++; s = Sys_FindNext(); } Sys_FindClose(); if(!nfiles) return NULL; nfiles++; // add space for a guard *numfiles = nfiles; list = malloc(sizeof(char *) * nfiles); memset(list, 0, sizeof(char *) * nfiles); s = Sys_FindFirst(findname); nfiles = 0; while(s){ if(s[strlen(s) - 1] != '.'){ list[nfiles] = strdup(s); nfiles++; } s = Sys_FindNext(); } Sys_FindClose(); return list; } /* ** FS_Dir_f */ void FS_Dir_f(void){ char *path = NULL; char findname[MAX_STRING_CHARS]; char wildcard[MAX_STRING_CHARS] = "*.*"; char **dirnames; int ndirs; if(Cmd_Argc() != 1){ strcpy(wildcard, Cmd_Argv(1)); } while((path = FS_NextPath(path)) != NULL){ char * tmp = findname; Com_sprintf(findname, sizeof(findname), "%s/%s", path, wildcard); while(*tmp != 0){ if(*tmp == '\\') *tmp = '/'; tmp++; } Com_Printf("Directory of %s\n", findname); Com_Printf("----\n"); if((dirnames = FS_ListFiles(findname, &ndirs)) != 0){ int i; for(i = 0; i < ndirs - 1; i++){ if(strrchr(dirnames[i], '/')) Com_Printf("%s\n", strrchr(dirnames[i], '/') + 1); else Com_Printf("%s\n", dirnames[i]); free(dirnames[i]); } free(dirnames); } Com_Printf("\n"); }; } /* FS_Path_f */ void FS_Path_f(void){ searchpath_t *s; filelink_t *l; Com_Printf("Current search path:\n"); for(s = fs_searchpaths; s; s = s->next){ if(s == fs_base_searchpaths) Com_Printf("----------\n"); if(s->pak) Com_Printf("%s(%i files)\n", s->pak->filename, s->pak->numfiles); else Com_Printf("%s\n", s->filename); } Com_Printf("\nLinks:\n"); for(l = fs_links; l; l = l->next) Com_Printf("%s : %s\n", l->from, l->to); } /* FS_NextPath Allows enumerating all of the directories in the search path */ char *FS_NextPath(char *prevpath){ searchpath_t *s; char *prev; prev = NULL; // fs_gamedir is the first directory in the searchpath for(s = fs_searchpaths; s; s = s->next){ if(s->pak) continue; if(prevpath == NULL) return s->filename; if(prevpath == prev) return s->filename; prev = s->filename; } return NULL; } /* FS_InitFilesystem */ void FS_InitFilesystem(void){ char bd[MAX_OSPATH]; Cmd_AddCommand("path", FS_Path_f); Cmd_AddCommand("link", FS_Link_f); Cmd_AddCommand("dir", FS_Dir_f); // basedir // allows the game to run from outside the data tree fs_basedir = Cvar_Get("basedir", ".", CVAR_NOSET); //resolve basedir if(fs_basedir->string[0] && strcmp(fs_basedir->string, ".")){ snprintf(bd, MAX_OSPATH, "%s/%s", fs_basedir->string, BASEDIRNAME); FS_AddGameDirectory(bd); } else strncpy(bd, PKGDATADIR, MAX_OSPATH); if(!getenv("QUAKE2_HOME")) // let mods know where to perform file io setenv("QUAKE2_HOME", PKGDATADIR, 0); // add baseq2 to search path FS_AddGameDirectory(PKGLIBDIR"/"BASEDIRNAME); FS_AddGameDirectory(PKGDATADIR"/"BASEDIRNAME); // then add a '.quake2/baseq2' directory in home directory by default FS_AddHomeAsGameDirectory(BASEDIRNAME); // any set gamedirs will be freed up to here fs_base_searchpaths = fs_searchpaths; // check for game override fs_gamedirvar = Cvar_Get("game", "", CVAR_LATCH | CVAR_SERVERINFO); if(fs_gamedirvar->string[0]) FS_SetGamedir(fs_gamedirvar->string); }