/* * FS.java * Copyright (C) 2003 * * $Id: FS.java,v 1.12 2004-11-10 20:41:40 cawe Exp $ */ /* Copyright (C) 1997-2001 Id Software, Inc. 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. */ package jake2.qcommon; import jake2.Globals; import jake2.game.Cmd; import jake2.game.cvar_t; import jake2.sys.Sys; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.*; import javax.imageio.stream.FileImageInputStream; /** * FS * * @author cwei */ public final class FS extends Globals { /* * ================================================== * * QUAKE FILESYSTEM * * ================================================== */ public static class packfile_t { static final int SIZE = 64; static final int NAME_SIZE = 56; String name; // char name[56] int filepos, filelen; public String toString() { return name + " [ length: " + filelen + " pos: " + filepos + " ]"; } } public static class pack_t { String filename; RandomAccessFile handle; ByteBuffer backbuffer; int numfiles; Hashtable files; // with packfile_t entries } public static String fs_gamedir; private static String fs_userdir; public static cvar_t fs_basedir; public static cvar_t fs_cddir; public static cvar_t fs_gamedirvar; public static class filelink_t { String from; int fromlength; String to; } // with filelink_t entries public static List fs_links = new LinkedList(); public static class searchpath_t { String filename; pack_t pack; // only one of filename or pack will be used searchpath_t next; } public static searchpath_t fs_searchpaths; // without gamedirs public static searchpath_t fs_base_searchpaths; /* * All of Quake's data access is through a hierchal file system, but the * contents of the file system can be transparently merged from several * sources. * * The "base directory" is the path to the directory holding the quake.exe * and all game directories. The sys_* files pass this to host_init in * quakeparms_t->basedir. This can be overridden with the "-basedir" command * line parm to allow code debugging in a different directory. The base * directory is only used during filesystem initialization. * * The "game directory" is the first tree on the search path and directory * that all generated files (savegames, screenshots, demos, config files) * will be saved to. This can be overridden with the "-game" command line * parameter. The game directory can never be changed while quake is * executing. This is a precacution against having a malicious server * instruct clients to write files over areas they shouldn't. * */ /* * CreatePath * * Creates any directories needed to store the given filename. */ public static void CreatePath(String path) { int index = path.lastIndexOf('/'); // -1 if not found and 0 means write to root if (index > 0) { File f = new File(path.substring(0, index)); if (!f.mkdirs() && !f.isDirectory()) { Com.Printf("can't create path \"" + path + '"' + "\n"); } } } /* * FCloseFile * * For some reason, other dll's can't just call fclose() on files returned * by FS_FOpenFile... */ public static void FCloseFile(RandomAccessFile file) throws IOException { file.close(); } public static void FCloseFile(InputStream stream) throws IOException { stream.close(); } public static int FileLength(String filename) { searchpath_t search; String netpath; pack_t pak; int i; filelink_t link; file_from_pak = 0; // check for links first for (Iterator it = fs_links.iterator(); it.hasNext();) { link = (filelink_t) it.next(); if (filename.regionMatches(0, link.from, 0, link.fromlength)) { netpath = link.to + filename.substring(link.fromlength); File file = new File(netpath); if (file.canRead()) { Com.DPrintf("link file: " + netpath + '\n'); return (int) file.length(); } return -1; } } // search through the path, one element at a time for (search = fs_searchpaths; search != null; search = search.next) { // is the element a pak file? if (search.pack != null) { // look through all the pak file elements pak = search.pack; filename = filename.toLowerCase(); packfile_t entry = (packfile_t) pak.files.get(filename); if (entry != null) { // found it! file_from_pak = 1; Com.DPrintf("PackFile: " + pak.filename + " : " + filename + '\n'); // open a new file on the pakfile File file = new File(pak.filename); if (!file.canRead()) { Com.Error(Globals.ERR_FATAL, "Couldn't reopen " + pak.filename); } return entry.filelen; } } else { // check a file in the directory tree netpath = search.filename + '/' + filename; File file = new File(netpath); if (!file.canRead()) continue; Com.DPrintf("FindFile: " + netpath + '\n'); return (int) file.length(); } } Com.DPrintf("FindFile: can't find " + filename + '\n'); return -1; } public static int file_from_pak = 0; /* * FOpenFile * * Finds the file in the search path. returns a RadomAccesFile. Used for * streaming data out of either a pak file or a seperate file. */ public static RandomAccessFile FOpenFile(String filename) throws IOException { searchpath_t search; String netpath; pack_t pak; int i; filelink_t link; File file = null; file_from_pak = 0; // check for links first for (Iterator it = fs_links.iterator(); it.hasNext();) { link = (filelink_t) it.next(); // if (!strncmp (filename, link->from, link->fromlength)) if (filename.regionMatches(0, link.from, 0, link.fromlength)) { netpath = link.to + filename.substring(link.fromlength); file = new File(netpath); if (file.canRead()) { //Com.DPrintf ("link file: " + netpath +'\n'); return new RandomAccessFile(file, "r"); } return null; } } // // search through the path, one element at a time // for (search = fs_searchpaths; search != null; search = search.next) { // is the element a pak file? if (search.pack != null) { // look through all the pak file elements pak = search.pack; filename = filename.toLowerCase(); packfile_t entry = (packfile_t) pak.files.get(filename); if (entry != null) { // found it! file_from_pak = 1; //Com.DPrintf ("PackFile: " + pak.filename + " : " + // filename + '\n'); file = new File(pak.filename); if (!file.canRead()) Com.Error(Globals.ERR_FATAL, "Couldn't reopen " + pak.filename); if (pak.handle == null || !pak.handle.getFD().valid()) { // hold the pakfile handle open pak.handle = new RandomAccessFile(pak.filename, "r"); } // open a new file on the pakfile RandomAccessFile raf = new RandomAccessFile(file, "r"); raf.seek(entry.filepos); return raf; } } else { // check a file in the directory tree netpath = search.filename + '/' + filename; file = new File(netpath); if (!file.canRead()) continue; //Com.DPrintf("FindFile: " + netpath +'\n'); return new RandomAccessFile(file, "r"); } } //Com.DPrintf ("FindFile: can't find " + filename + '\n'); return null; } // read in blocks of 64k public static final int MAX_READ = 0x10000; /** * Read * * Properly handles partial reads */ public static void Read(byte[] buffer, int len, RandomAccessFile f) { int block, remaining; int offset = 0; int read = 0; boolean tries = true; // read in chunks for progress bar remaining = len; while (remaining != 0) { block = Math.min(remaining, MAX_READ); try { read = f.read(buffer, offset, block); } catch (IOException e) { Com.Error(Globals.ERR_FATAL, e.toString()); } if (read == 0) { Com.Error(Globals.ERR_FATAL, "FS_Read: 0 bytes read"); } else if (read == -1) { Com.Error(Globals.ERR_FATAL, "FS_Read: -1 bytes read"); } // // do some progress bar thing here... // remaining -= read; offset += read; } } /* * LoadFile * * Filename are reletive to the quake search path a null buffer will just * return the file content as byte[] */ public static byte[] LoadFile(String path) { RandomAccessFile file; byte[] buf = null; int len = 0; // TODO hack for bad strings (fuck \0) int index = path.indexOf('\0'); if (index != -1) path = path.substring(0, index); // look for it in the filesystem or pack files len = FileLength(path); if (len < 1) return null; try { file = FOpenFile(path); //Read(buf = new byte[len], len, h); buf = new byte[len]; file.readFully(buf); file.close(); } catch (IOException e) { Com.Error(Globals.ERR_FATAL, e.toString()); } return buf; } /* * LoadMappedFile * * Filename are reletive to the quake search path a null buffer will just * return the file content as ByteBuffer (memory mapped) */ public static ByteBuffer LoadMappedFile(String filename) { searchpath_t search; String netpath; pack_t pak; int i; filelink_t link; File file = null; int fileLength = 0; FileChannel channel = null; FileInputStream input = null; ByteBuffer buffer = null; file_from_pak = 0; try { // check for links first for (Iterator it = fs_links.iterator(); it.hasNext();) { link = (filelink_t) it.next(); if (filename.regionMatches(0, link.from, 0, link.fromlength)) { netpath = link.to + filename.substring(link.fromlength); file = new File(netpath); if (file.canRead()) { input = new FileInputStream(file); channel = input.getChannel(); fileLength = (int) channel.size(); buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength); input.close(); return buffer; } return null; } } // // search through the path, one element at a time // for (search = fs_searchpaths; search != null; search = search.next) { // is the element a pak file? if (search.pack != null) { // look through all the pak file elements pak = search.pack; filename = filename.toLowerCase(); packfile_t entry = (packfile_t) pak.files.get(filename); if (entry != null) { // found it! file_from_pak = 1; //Com.DPrintf ("PackFile: " + pak.filename + " : " + // filename + '\n'); file = new File(pak.filename); if (!file.canRead()) Com.Error(Globals.ERR_FATAL, "Couldn't reopen " + pak.filename); if (pak.handle == null || !pak.handle.getFD().valid()) { // hold the pakfile handle open pak.handle = new RandomAccessFile(pak.filename, "r"); } // open a new file on the pakfile if (pak.backbuffer == null) { channel = pak.handle.getChannel(); pak.backbuffer = channel.map( FileChannel.MapMode.READ_ONLY, 0, pak.handle.length()); channel.close(); } pak.backbuffer.position(entry.filepos); buffer = pak.backbuffer.slice(); buffer.limit(entry.filelen); return buffer; } } else { // check a file in the directory tree netpath = search.filename + '/' + filename; file = new File(netpath); if (!file.canRead()) continue; //Com.DPrintf("FindFile: " + netpath +'\n'); input = new FileInputStream(file); channel = input.getChannel(); fileLength = (int) channel.size(); buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength); input.close(); return buffer; } } } catch (Exception e) { } try { if (input != null) input.close(); else if (channel != null && channel.isOpen()) channel.close(); } catch (IOException ioe) { } return null; } /* * FreeFile */ public static void FreeFile(byte[] buffer) { buffer = null; } static final int IDPAKHEADER = (('K' << 24) + ('C' << 16) + ('A' << 8) + 'P'); static class dpackheader_t { int ident; // IDPAKHEADER int dirofs; int dirlen; } static final int MAX_FILES_IN_PACK = 4096; // buffer for C-Strings char[56] static byte[] tmpText = new byte[packfile_t.NAME_SIZE]; /* * LoadPackFile * * 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 pack files. */ static pack_t LoadPackFile(String packfile) { dpackheader_t header; Hashtable newfiles; int numpackfiles = 0; pack_t pack = null; RandomAccessFile file; FileImageInputStream packhandle; // unsigned checksum; // try { packhandle = new FileImageInputStream(file = new RandomAccessFile( packfile, "r")); packhandle.setByteOrder(ByteOrder.LITTLE_ENDIAN); if (packhandle.length() < 1) return null; // header = new dpackheader_t(); header.ident = packhandle.readInt(); header.dirofs = packhandle.readInt(); header.dirlen = packhandle.readInt(); if (header.ident != IDPAKHEADER) Com.Error(Globals.ERR_FATAL, packfile + " is not a packfile"); numpackfiles = header.dirlen / packfile_t.SIZE; if (numpackfiles > MAX_FILES_IN_PACK) Com.Error(Globals.ERR_FATAL, packfile + " has " + numpackfiles + " files"); newfiles = new Hashtable(numpackfiles); packhandle.seek(header.dirofs); // parse the directory packfile_t entry = null; for (int i = 0; i < numpackfiles; i++) { packhandle.readFully(tmpText); entry = new packfile_t(); entry.name = new String(tmpText).trim(); entry.filepos = packhandle.readInt(); entry.filelen = packhandle.readInt(); newfiles.put(entry.name.toLowerCase(), entry); } } catch (IOException e) { Com.DPrintf(e.getMessage() + '\n'); return null; } pack = new pack_t(); pack.filename = new String(packfile); pack.handle = file; pack.numfiles = numpackfiles; pack.files = newfiles; Com.Printf("Added packfile " + packfile + " (" + numpackfiles + " files)\n"); return pack; } /* * AddGameDirectory * * Sets fs_gamedir, adds the directory to the head of the path, then loads * and adds pak1.pak pak2.pak ... */ static void AddGameDirectory(String dir) { int i; searchpath_t search; pack_t pak; String pakfile; fs_gamedir = new String(dir); // // add the directory to the search path // ensure fs_userdir is first in searchpath search = new searchpath_t(); search.filename = new String(dir); if (fs_searchpaths != null) { search.next = fs_searchpaths.next; fs_searchpaths.next = search; } else { fs_searchpaths = search; } // // add any pak files in the format pak0.pak pak1.pak, ... // for (i = 0; i < 10; i++) { pakfile = dir + "/pak" + i + ".pak"; if (!(new File(pakfile).canRead())) continue; pak = LoadPackFile(pakfile); if (pak == null) continue; search = new searchpath_t(); search.pack = pak; search.filename = ""; search.next = fs_searchpaths; fs_searchpaths = search; } } /* * Gamedir * * Called to find where to write a file (demos, savegames, etc) * this is modified to /.jake2 */ public static String Gamedir() { return (fs_userdir != null) ? fs_userdir : Globals.BASEDIRNAME; } /* * BaseGamedir * * Called to find where to write a downloaded file */ public static String BaseGamedir() { return (fs_gamedir != null) ? fs_gamedir : Globals.BASEDIRNAME; } /* * ExecAutoexec */ public static void ExecAutoexec() { String dir; String name; dir = Cvar.VariableString("gamedir"); if (dir != null && dir.length() > 0) { name = fs_basedir.string + '/' + dir + "/autoexec.cfg"; } else { name = fs_basedir.string + '/' + Globals.BASEDIRNAME + "/autoexec.cfg"; } int canthave = Globals.SFF_SUBDIR | Globals.SFF_HIDDEN | Globals.SFF_SYSTEM; if (Sys.FindAll(name, 0, canthave) != null) { Cbuf.AddText("exec autoexec.cfg\n"); } } /* * SetGamedir * * Sets the gamedir and path to a different directory. */ public static void SetGamedir(String dir) { searchpath_t next; if (dir.indexOf("..") != -1 || dir.indexOf("/") != -1 || dir.indexOf("\\") != -1 || dir.indexOf(":") != -1) { 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.pack != null) { try { fs_searchpaths.pack.handle.close(); } catch (IOException e) { Com.DPrintf(e.getMessage() + '\n'); } // clear the hashtable fs_searchpaths.pack.files.clear(); fs_searchpaths.pack.files = null; fs_searchpaths.pack = null; } next = fs_searchpaths.next; fs_searchpaths = null; fs_searchpaths = next; } // // flush all data, so it will be forced to reload // if ((Globals.dedicated != null) && (Globals.dedicated.value == 0.0f)) Cbuf.AddText("vid_restart\nsnd_restart\n"); fs_gamedir = fs_basedir.string + '/' + dir; if (dir.equals(Globals.BASEDIRNAME) || (dir.length() == 0)) { Cvar.FullSet("gamedir", "", CVAR_SERVERINFO | CVAR_NOSET); Cvar.FullSet("game", "", CVAR_LATCH | CVAR_SERVERINFO); } else { Cvar.FullSet("gamedir", dir, CVAR_SERVERINFO | CVAR_NOSET); if (fs_cddir.string != null && fs_cddir.string.length() > 0) AddGameDirectory(fs_cddir.string + '/' + dir); AddGameDirectory(fs_basedir.string + '/' + dir); } } /* * Link_f * * Creates a filelink_t */ public static void Link_f() { filelink_t entry = null; if (Cmd.Argc() != 3) { Com.Printf("USAGE: link \n"); return; } // see if the link already exists for (Iterator it = fs_links.iterator(); it.hasNext();) { entry = (filelink_t) it.next(); if (entry.from.equals(Cmd.Argv(1))) { if (Cmd.Argv(2).length() < 1) { // delete it it.remove(); return; } entry.to = new String(Cmd.Argv(2)); return; } } // create a new link if the is not empty if (Cmd.Argv(2).length() > 0) { entry = new filelink_t(); entry.from = new String(Cmd.Argv(1)); entry.fromlength = entry.from.length(); entry.to = new String(Cmd.Argv(2)); fs_links.add(entry); } } /* * ListFiles */ public static String[] ListFiles(String findname, int musthave, int canthave) { String[] list = null; File[] files = Sys.FindAll(findname, musthave, canthave); if (files != null) { list = new String[files.length]; for (int i = 0; i < files.length; i++) { list[i] = files[i].getPath(); } } return list; } /* * Dir_f */ public static void Dir_f() { String path = null; String findname = null; String wildcard = "*.*"; String[] dirnames; if (Cmd.Argc() != 1) { wildcard = Cmd.Argv(1); } while ((path = NextPath(path)) != null) { String tmp = findname; findname = path + '/' + wildcard; if (tmp != null) tmp.replaceAll("\\\\", "/"); Com.Printf("Directory of " + findname + '\n'); Com.Printf("----\n"); dirnames = ListFiles(findname, 0, 0); if (dirnames != null) { int index = 0; for (int i = 0; i < dirnames.length; i++) { if ((index = dirnames[i].lastIndexOf('/')) > 0) { Com.Printf(dirnames[i].substring(index + 1, dirnames[i] .length()) + '\n'); } else { Com.Printf(dirnames[i] + '\n'); } } } Com.Printf("\n"); } } /* * Path_f */ public static void Path_f() { searchpath_t s; filelink_t link; Com.Printf("Current search path:\n"); for (s = fs_searchpaths; s != null; s = s.next) { if (s == fs_base_searchpaths) Com.Printf("----------\n"); if (s.pack != null) Com.Printf(s.pack.filename + " (" + s.pack.numfiles + " files)\n"); else Com.Printf(s.filename + '\n'); } Com.Printf("\nLinks:\n"); for (Iterator it = fs_links.iterator(); it.hasNext();) { link = (filelink_t) it.next(); Com.Printf(link.from + " : " + link.to + '\n'); } } /* * NextPath * * Allows enumerating all of the directories in the search path */ public static String NextPath(String prevpath) { searchpath_t s; String prev; if (prevpath == null || prevpath.length() == 0) return fs_gamedir; prev = fs_gamedir; for (s = fs_searchpaths; s != null; s = s.next) { if (s.pack != null) continue; if (prevpath == prev) return s.filename; prev = s.filename; } return null; } /* * InitFilesystem */ public static void InitFilesystem() { Cmd.AddCommand("path", new xcommand_t() { public void execute() { Path_f(); } }); Cmd.AddCommand("link", new xcommand_t() { public void execute() { Link_f(); } }); Cmd.AddCommand("dir", new xcommand_t() { public void execute() { Dir_f(); } }); fs_userdir = System.getProperty("user.home") + "/.jake2"; FS.CreatePath(fs_userdir + "/"); FS.AddGameDirectory(fs_userdir); // // basedir // allows the game to run from outside the data tree // fs_basedir = Cvar.Get("basedir", ".", CVAR_NOSET); // // cddir // Logically concatenates the cddir after the basedir for // allows the game to run from outside the data tree // setCDDir(); // // start up with baseq2 by default // AddGameDirectory(fs_basedir.string + '/' + Globals.BASEDIRNAME); // any set gamedirs will be freed up to here markBaseSearchPaths(); // check for game override fs_gamedirvar = Cvar.Get("game", "", CVAR_LATCH | CVAR_SERVERINFO); if (fs_gamedirvar.string.length() > 0) SetGamedir(fs_gamedirvar.string); } /** * set baseq2 directory */ static void setCDDir() { fs_cddir = Cvar.Get("cddir", "", CVAR_ARCHIVE); if (fs_cddir.string.length() > 0) AddGameDirectory(fs_cddir.string); } static void markBaseSearchPaths() { // any set gamedirs will be freed up to here fs_base_searchpaths = fs_searchpaths; } // RAFAEL /* * Developer_searchpath */ public static int Developer_searchpath(int who) { int ch; // PMM - warning removal // char *start; searchpath_t s; if (who == 1) // xatrix ch = 'x'; else if (who == 2) ch = 'r'; for (s = fs_searchpaths; s != null; s = s.next) { if (s.filename.indexOf("xatrix") != -1) return 1; if (s.filename.indexOf("rogue") != -1) return 2; } return 0; } }