Mercurial > projects > mde
view mde/file/paths.d @ 151:e785e98d3b78
Updated for compatibility with tango 0.99.8.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Sat, 04 Apr 2009 17:32:18 +0200 |
parents | 9f035cd139c6 |
children |
line wrap: on
line source
/* LICENSE BLOCK Part of mde: a Modular D game-oriented Engine Copyright © 2007-2008 Diggory Hardy 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, see <http://www.gnu.org/licenses/>. */ /** Resource paths module. * * Internally, most mde code using files doesn't use absolute paths but paths * relative to mde directory. Further, mergetag files are usually not read * just from one file, but are the result of merging multiple files from the * same relative paths in different directories, including the root data * directory, possibly a directory in /etc, a directory for files specific to * the user accont, and possibly other directories. * * This module transforms relative paths to absolute paths, allowing for * multiple files to be read and merged. * * The base paths from which relative files are read are slit into two groups: * data paths, and conf paths (usually the conf paths refer to a "conf" * directory within the data paths). */ /* Implementation note: * All paths are stored internally as strings, rather than as an instance of FilePath/FilePath once * the FilePath has served its immediate purpose, since it's more convenient and creating new * FilePaths for adjusted paths should be no slower than mutating existing ones. */ module mde.file.paths; import mde.exception; import mde.file.mergetag.Reader; import mde.file.mergetag.MTTagReader; import mde.file.mergetag.Writer; import mde.file.mergetag.MTTagWriter; import mde.file.mergetag.DataSet; import mde.file.mergetag.exception; import tango.io.Console; import tango.io.FilePath; version (linux) { import tango.io.FileScan; import tango.util.container.SortedMap; import tango.sys.Environment; } else version (Windows) import tango.sys.win32.SpecialPath; import tango.util.log.Log : Log, Logger; private Logger logger; static this() { logger = Log.getLogger ("mde.file.paths"); } /** Order to read files in. * * Values: HIGH_LOW, LOW_HIGH, HIGH_ONLY. */ enum PRIORITY : byte { HIGH_LOW, LOW_HIGH, HIGH_ONLY } /** This struct has one instance for each "directory". * * It is the only item within this module that you should need to interact with. * * In the case of confDir, the user path is guaranteed to exist (as highest priority path). */ struct mdeDirectory { /** Creates an MT IReader for each file (using MTMultiReader). * * Params: * file = The file path and name relative to the mdeDirectory, * without a suffix (e.g. "options"). * readOrder = Read the highest priority or lowest priority files first? * For correct merging, this should be LOW_HIGH when newly- * read items override old ones (as is the case with * DefaultData) and HIGH_LOW when the first-read items * survive. Thus override order needs to be the same for each * section, except the header which is always read with * LOW_HIGH order. Alternately, for files which shouldn't be * merged where only the highest priority file should be read, * pass HIGH_ONLY. * ds = The dataset, as for mergetag. Note: all actual readers share one dataset. * rdHeader = Read the headers for each file and merge if rdHeader == true. */ IReader makeMTReader (char[] file, PRIORITY readOrder, DataSet ds = null, bool rdHeader = false) { return new MTMultiReader (getFiles (file, readOrder), ds, rdHeader); } /** Creates an MTTagReader for each file (using MTMultiTagReader). * * Params as for makeMTReader. */ MTTagReader makeMTTagReader (char[] file, PRIORITY readOrder) { return new MTMultiTagReader (getFiles (file, readOrder)); } /** Creates an MT writer for file deciding on the best path to use. * * Params: * file = The file path and name relative to the mdeDirectory, without a suffix * (e.g. "options") * ds = The dataset, as for mergetag. */ IWriter makeMTWriter (char[] file, DataSet ds = null) { // FIXME: use highest priority writable path return getMTWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text); } /** Creates an MTTagWriter for file. */ MTTagWriter makeMTTagWriter (char[] file) { // FIXME: use highest priority writable path return getMTTagWriter (paths[pathsLen-1] ~ file); } /** Returns a string listing the file name or names (if readOrder is not * HIGH_ONLY and multiple matches are found), or an error message. Intended * for user output only. */ char[] getFileName (char[] file, PRIORITY readOrder) { FilePath[] files; try { files = getFiles (file, readOrder); } catch (NoFileException e) { return e.msg; } char[] ret = files[0].toString; foreach (f; files[1..$]) ret ~= ", " ~ f.toString; return ret; } /// Print all paths found. static void printPaths () { Cout ("Data paths found:"); dataDir.coutPaths; Cout ("\nConf paths found:"); confDir.coutPaths; Cout ("\nLog file directory:\n\t")(logDir); Cout ("\nFont directories:"); foreach (path; fontPaths) Cout ("\n\t")(path.toString); Cout.newline; } private: FilePath[] getFiles (char[] filename, PRIORITY readOrder) { FilePath[] ret; if (readOrder == PRIORITY.LOW_HIGH) { for (size_t i = 0; i < pathsLen; ++i) { FilePath file = findFile (paths[i]~filename); if (file !is null) ret ~= file; } } else { debug assert (readOrder == PRIORITY.HIGH_LOW || readOrder == PRIORITY.HIGH_ONLY ); for (int i = pathsLen - 1; i >= 0; --i) { FilePath file = findFile (paths[i]~filename); if (file !is null) ret ~= file; if (readOrder == PRIORITY.HIGH_ONLY) break; } } if (ret is null) throw new NoFileException ("Unable to find the file: "~filename~"[.mtt|mtb]"); return ret; } // Unconditionally add a path void addPath (char[] path) { assert (pathsLen < MAX_PATHS); paths[pathsLen++] = path~'/'; } // Test a path and add if is a folder. bool tryPath (char[] path, bool create = false) { assert (pathsLen < MAX_PATHS); FilePath fp = FilePath (path); if (fp.exists && fp.isFolder) { paths[pathsLen++] = path~'/'; return true; } else if (create) { try { fp.create; paths[pathsLen++] = fp.toString~'/'; return true; } catch (Exception e) { logger.error ("Creating path {} failed:" ~ e.msg, path); } } return false; } void coutPaths () { if (pathsLen) { for (size_t i = 0; i < pathsLen; ++i) Cout ("\n\t" ~ paths[i]); } else Cout (" [none]"); } // Use a static array to store all possible paths with separate length counters. // Lowest priority paths are first. char[][MAX_PATHS] paths; ubyte pathsLen = 0; } /** Get the actual path of a font file, or throw NoFileException if not found. * * Returns a C string (null terminated). */ char[] getFontPath (char[] file) { foreach (path; fontPaths) { FilePath font = path.dup.append (file); if (font.exists && font.isFile) return font.cString; } throw new NoFileException ("Unable to find font file: "~file); } /** These are the actual instances, one for each of the data and conf "directories". */ mdeDirectory dataDir, confDir; char[] logDir; /// Directory for log files FilePath[] fontPaths; /// All directories for fonts //BEGIN Path resolution /// For command line args: these paths are added if non-null, with highest priority. char[] extraDataPath, extraConfPath; /** Add an extra font directory. May be called multiple times. */ void addFontPath (char[] path) { FilePath fp = FilePath (path); if (fp.exists && fp.isFolder) fontPaths ~= fp; } /** Find at least one path for each required directory. */ debug (mdeUnitTest) { /** In unittest mode, add paths unittest/data and unittest/conf before init runs. */ static this () { dataDir.tryPath ("unittest/data"); confDir.tryPath ("unittest/conf"); if (!(dataDir.pathsLen && confDir.pathsLen)) throw new mdeException ("Fatal: unittest/data and unittest/conf don't both exist"); // Don't bother with real paths or logDir or font dir(s) for unittest. } } void resolvePaths (char[] base = ".") { size_t iFP = fontPaths.length; fontPaths.length = fontPaths.length + 4; version (linux) { // Home directory: char[] HOME = Environment.get("HOME", "."); // Installation base (should contain "data" and "conf"): FilePath staticPath = findPath (false, "/usr/share/games/mde", "/usr/local/share/games/mde", base~"/data"); // Path for user-adjusted files: FilePath userPath = findPath (true, HOME~"/.config/mde", HOME~"/.mde"); dataDir.addPath (staticPath.toString); confDir.tryPath (staticPath.toString ~ "/conf"); confDir.tryPath ("/etc/mde"); // Font paths: auto fontP1 = FilePath("/usr/share/fonts").toList; foreach (fp1; fontP1) if (fp1.isFolder) foreach (fp2; fp1.toList) if (fp2.isFolder) { if (iFP >= fontPaths.length) fontPaths.length = fontPaths.length * 2; fontPaths[iFP++] = fp2; } } else version (Windows) { //FIXME: Get base path from registry FilePath staticPath = findPath (false, base~"/data"); FilePath userPath = findPath (true, getSpecialPath(CSIDL_LOCAL_APPDATA) ~ "/mde"); dataDir.addPath (staticPath.toString); confDir.tryPath (staticPath.toString ~ "/conf"); // Font path: fontPaths ~= FilePath(getSpecialPath (CSIDL_FONTS)); } else { static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!"); } // Static data paths: dataDir.tryPath (userPath.toString ~ "/data"); if (extraDataPath) dataDir.tryPath (extraDataPath); // Configuration paths: confDir.tryPath (userPath.toString ~ "/conf", true); if (extraConfPath) confDir.tryPath (extraConfPath); if (dataDir.pathsLen==0) throw new mdeException ("Fatal: no data path found!"); if (confDir.pathsLen==0) throw new mdeException ("Fatal: no conf path found!"); for (int i = dataDir.pathsLen - 1; i >= 0; --i) { FilePath font = FilePath (dataDir.paths[i] ~ "fonts"); if (font.exists && font.isFolder) fontPaths[iFP++] = font; } fontPaths.length = iFP; // Logging path: logDir = userPath.toString; } private { class PathException : mdeException { this(char[] msg) { super (msg); } } // The maximum number of paths for any one "directory". const MAX_PATHS = 4; /* Try each path in succession, returning the first to exist and be a folder. * If none are valid and create is true, will try creating each in turn. * If still none are valid, throws. */ FilePath findPath (bool create, char[][] paths ...) { FilePath[] fps; fps.length = paths.length; foreach (i,path; paths) { FilePath pv = new FilePath (path); if (pv.exists && pv.isFolder) return pv; // got a valid path fps[i] = pv; } if (create) { // try to create a folder, using each path in turn until succesful foreach (fp; fps) { try { return fp.create; } catch (Exception e) {} } } // no valid path... char[] msg = "Unable to find"~(create ? " or create" : "")~" a required path! The following were tried:"; foreach (path; paths) msg ~= " \"" ~ path ~ '\"'; throw new PathException (msg); } } //END Path resolution /// Thrown when makeMTReader couldn't find a file. class NoFileException : MTFileIOException { this (char[] msg) { super(msg); } }