view mde/resource/paths.d @ 24:32eff0e01c05

Only locally-changed options are stored in user-config now. Log levels revised. Options sub-classes are handled more generically and can be added without changing the Options class. Options changed at run-time are tracked, and on exit merged with user options and saved. Revised log levels as set out in policies.txt and as used in code. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 27 Mar 2008 16:15:21 +0000
parents 56a42ec95024
children 611f7b9063c6
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, version 2, as published by the Free Software Foundation.

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.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */

/** Resource paths module.
*
* Internally to mde code other than code dealing directly with files and this module, paths are
* relative to the mde directory. This module transforms those paths to absolute paths.
*
* Additionally, the intention is to look for all files in two directories: the installation (i.e.
* main data) directory and a user directory (for user-specific configuration). Besides exposing
* both paths and checking in which valid files exist, this module provides some extra mergetag
* functionality to simplify correct reading and writing.
*
* Currently the paths are found as follows: (see codeDoc/paths.txt)
*/
/* Implementation note:
* All paths are stored internally as strings, rather than as an instance of FilePath/PathView 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.resource.paths;

import mde.exception;
import mde.mergetag.Reader;
import mde.mergetag.Writer;
import mde.mergetag.DataSet;
import mde.mergetag.exception;

import tango.io.FilePath;
import tango.util.log.Log : Log, Logger;
import tango.stdc.stdlib;
import tango.stdc.stringz;

/** 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)
    {
        if (readOrder == PRIORITY.HIGH_ONLY) return makeReader (paths[pathsLen-1] ~ file, ds, rdHeader);
        else return new mdeReader (file, readOrder, ds, rdHeader, paths);
    }
    
    /** 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);
    }
    
    /** Check whether the given file exists under any path with either .mtt or .mtb suffix. */
    bool exists (char[] file) {
        for (uint i = 0; i < pathsLen; ++i) {
            if (FilePath (paths[i]~file~".mtt").exists) return true;
            if (FilePath (paths[i]~file~".mtb").exists) return true;
        }
        return false;
    }
    
private:
    
    // Unconditionally add a path
    void addPath (char[] path) {
        paths[pathsLen++] = path~'/';
    }
    
    // Test a path and add if is a folder.
    bool tryPath (char[] path, bool create = false) {
        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 "~path~" failed:");
                logger.error (e.msg);
            }
        }
        return false;
    }
    
    // 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;

//BEGIN Path resolution
static this() {
    logger = Log.getLogger ("mde.resource.paths");
}

// These are used several times:
const DATA = "/data";
const CONF = "/conf";

version (linux) {
    void resolvePaths () {
        // Home directory:
        char[] HOME = fromStringz (getenv (toStringz ("HOME")));
        
        // Base paths:
        // Static data (must exist):
        PathView staticPath = findPath (false, "/usr/share/games/mde", "/usr/local/share/games/mde", "data");
        // Config (can just use defaults if necessary, so long as we can save afterwards):
        PathView 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 (!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 (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!");
        
        // Logging path:
        logDir = userPath.toString;
    }
} else version (Windows) {
    void resolvePaths () {
        static assert (false, "No registry code");
        
        // Base paths:
        char[] userPath = `...`;
        char[] installPath = `registryInstallPath or "."`;
        char[] staticPath = findPath (installPath ~ DATA);
        
        // Static data paths:
        dataDir.addPath (staticPath.toString);   // we know this is valid anyway
        dataDir.tryPath (userPath.toString ~ DATA);
        if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!");
                
        // Configuration paths:
        confDir.tryPath (staticPath.toString ~ CONF);
        bool sysConf = confDir.tryPath (installPath ~ CONF);
        confDir.tryPath (userPath.toString ~ CONF, !sysConf);   // create if no system conf dir
        if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!");
        
        // Logging path:
        logDir = userPath;
    }
} else {
    static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!");
}

private:
// The maximum number of paths for any one "directory".
// There are NO CHECKS that this is not exceeded.
const MAX_PATHS = 3;

Logger logger;

/* 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. */
PathView findPath (bool create, char[][] paths ...) {
    foreach (path; paths) {
        PathView pv = new FilePath (path);
        if (pv.exists && pv.isFolder) return pv;    // got a valid path
    }
    if (create) {   // try to create a folder, using each path in turn until succesful
        foreach (path; paths) {
            PathView pv = new FilePath (path);
            try {
                return pv;
            }
            catch (Exception e) {}
        }
    }
    // no valid path...
    logger.fatal ("Unable to find"~(create ? " or create" : "")~" a required path! The following were tried:");
    foreach (path; paths) logger.fatal ('\t' ~ path);
    throw new mdeException ("Unable to resolve a required path (see log for details).");
}
//END Path resolution

/** A special adapter for reading from multiple mergetag files with the same relative path to an
* mdeDirectory simultaneously.
*/
class mdeReader : IReader
{
    private this (char[] file, PRIORITY readOrder, DataSet ds, bool rdHeader, char[][MAX_PATHS] paths)
    in { assert (readOrder == PRIORITY.LOW_HIGH || readOrder == PRIORITY.HIGH_LOW); }
    body {
        rdOrder = readOrder;
        if (ds is null) ds = new DataSet;
        
        foreach (path; paths) {
            try {
                IReader r = makeReader (path~file, ds, rdHeader);
                
                readers[readersLen++] = r;
            }
            catch (MTFileIOException) {}    // Ignore errors regarding no file for now.
        }
        
        if (readersLen == 0) {          // totally failed to find any valid files
            throw new MTFileIOException ("Unable to find the file: "~file[1..$]~"[.mtt|mtb]");
        }
        
        // This is simply the easiest way of adjusting the reading order:
        if (readOrder == PRIORITY.HIGH_LOW) readers[0..readersLen].reverse;
    }
    
    DataSet dataset () {                /// Get the DataSet
        return readers[0].dataset;      // all readers share the same dataset
    }
    void dataset (DataSet ds) {         /// Set the DataSet
        for (uint i = 0; i < readersLen; ++i) readers[i].dataset (ds);
    }
    
    void dataSecCreator (IDataSection delegate (ID) dsC) {  /// Set the dataSecCreator
        for (uint i = 0; i < readersLen; ++i) readers[i].dataSecCreator = dsC;
    }
    
    /** Get identifiers for all sections.
    *
    * Note: the identifiers from all sections in all files are just strung together, starting with
    * the highest-priority file. */
    ID[] getSectionNames () {
        ID[] names;
        for (int i = readersLen-1; i >= 0; --i) names ~= readers[i].getSectionNames;
        return names;
    }
    void read () {                      /// Commence reading
        for (uint i = 0; i < readersLen; ++i) readers[i].read();
    }
    void read (ID[] secSet) {           /// ditto
        for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
    }
    void read (View!(ID) secSet) {      /// ditto
        for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
    }
        
    private:
    IReader[MAX_PATHS] readers;
    ubyte readersLen = 0;
    
    PRIORITY rdOrder;
}