Mercurial > projects > mde
diff mde/file/paths.d @ 134:7ababdf97748
Moved mde.setup.paths to mde.file.paths and paths.mdeReader to mde.file.mergetag.Reader.MTMultiReader.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Thu, 29 Jan 2009 14:59:45 +0000 |
parents | mde/setup/paths.d@1b1e2297e2fc |
children | bc697a218716 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/file/paths.d Thu Jan 29 14:59:45 2009 +0000 @@ -0,0 +1,360 @@ +/* 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.Writer; +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; + +debug { + 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 reader for each file. + * + * 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) + { + FilePath[] files = getFiles (file, readOrder); + if (files is null) + throw new NoFileException ("Unable to find the file: "~file~"[.mtt|mtb]"); + + return new MTMultiReader (files, ds, rdHeader); + } + + /** 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 makeWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text); + } + + /** Returns a string listing the file name or names (if readOrder is not HIGH_ONLY and multiple + * matches are found), or "no file found". Intended for user output only. */ + char[] getFileName (char[] file, PRIORITY readOrder) + { + FilePath[] files = getFiles (file, readOrder); + if (files is null) + return "no file found"; + + 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); + version (Windows) { + Cout ("\nFont directory:\n\t")(fontDir).newline; + } else version (linux) { + Cout ("\nFont files found:"); + foreach (f,p; fontFiles) + Cout ("\n\t")(f)("\t")(p[0..$-1]); + 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 { + 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; + } + } + 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) { + // No logging avaiable yet: Use Stdout/Cout + Cout ("Creating path "~path~" failed:" ~ e.msg).newline; + } + } + 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; +} + +/** These are the actual instances, one for each of the data and conf "directories". */ +mdeDirectory dataDir, confDir; +char[] logDir; /// Directory for log files + +//BEGIN Path resolution +/** 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. + } +} +version (linux) { + SortedMap!(char[],char[]) fontFiles; // key is file name, value is CString path + /** Get the actual path of a font file, or throw NoFileException if not found. + * + * Returns a C string (null terminated). */ + char[] getFontPath (char[] file) { + char[] ret; + if (fontFiles.get (file, ret)) + return ret; + throw new NoFileException ("Unable to find font file: "~file); + } + + // base-path not used on posix + void resolvePaths (char[] base = "data") { + // Home directory: + char[] HOME = Environment.get("HOME", "."); + + // Base paths: + // Static data (must exist): + FilePath staticPath = + findPath (false, "/usr/share/games/mde", "/usr/local/share/games/mde", base); + // Config (can just use defaults if necessary, so long as we can save afterwards): + FilePath userPath = findPath (true, HOME~"/.config/mde", HOME~"/.mde"); + + // Static data paths: + dataDir.addPath (staticPath.toString); // we know this is valid anyway + dataDir.tryPath (userPath.toString ~ "/data"); + if (extraDataPath) dataDir.tryPath (extraDataPath); + if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!"); + + // Configuration paths: + confDir.tryPath (staticPath.toString ~ "/conf"); + confDir.tryPath ("/etc/mde"); + confDir.tryPath (userPath.toString ~ "/conf", true); + if (extraConfPath) confDir.tryPath (extraConfPath); + if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!"); + + // Logging path: + logDir = userPath.toString; + + // Font paths: + auto fs = new FileScan; + // Scan for directories containing truetype and type1 fonts: + fs.sweep ("/usr/share/fonts", delegate bool(FilePath fp, bool isDir) + { return isDir || fp.suffix == ".ttf" || fp.suffix == ".pfb"; }, + true); + fontFiles = new SortedMap!(char[],char[]); + foreach (fp; fs.files) + fontFiles.add (fp.file, fp.cString); // both strings should be slices of same memory + } +} else version (Windows) { + char[] fontDir; + /** Get the actual path of a font file, or throw NoFileException if not found. + * + * Returns a C string (null terminated). */ + char[] getFontPath (char[] file) { + FilePath path = new FilePath (fontDir~file); + if (path.exists && !path.isFolder) + return path.cString; + throw new NoFileException ("Unable to find font file: "~file); + } + + void resolvePaths (char[] base = "./") { + //FIXME: Get base path from registry + + // Base paths: + FilePath installPath = findPath (false, base); + FilePath staticPath = findPath (false, installPath.toString); + FilePath userPath = findPath (true, getSpecialPath(CSIDL_LOCAL_APPDATA) ~ "/mde"); + + // Static data paths: + dataDir.addPath (staticPath.toString); // we know this is valid anyway + dataDir.tryPath (userPath.toString ~ "/data"); + if (extraDataPath) dataDir.tryPath (extraDataPath); + if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!"); + + // Configuration paths: + confDir.tryPath (staticPath.toString ~ "/conf"); + confDir.tryPath (installPath.append("user").toString); + confDir.tryPath (userPath.toString ~ "/conf", true); + if (extraConfPath) confDir.tryPath (extraConfPath); + if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!"); + + // Logging path: + logDir = userPath.toString; + + // Font path: + fontDir = getSpecialPath (CSIDL_FONTS) ~ "/"; // append separator + } +} else { + static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!"); +} + +/// For command line args: these paths are added if non-null, with highest priority. +char[] extraDataPath, extraConfPath; + +private { + class PathException : mdeException { + this(char[] msg) { + super (msg); + } + } + + // The maximum number of paths for any one "directory". + const MAX_PATHS = 4; + static assert (MTMultiReader.MAX_READERS == MAX_PATHS, "MAX_PATHS not all equal"); + + /* 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); + } +} \ No newline at end of file