view mde/resource/paths.d @ 17:5f90774ea1ef

Applied the GNU GPL v2 to mde. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 15 Mar 2008 15:14:25 +0000
parents 4608be19ebe2
children 56a42ec95024
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. */
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
    void tryPath (char[] path) {
        PathView pv = FilePath (path);
        if (pv.exists && pv.isFolder) {
            paths[pathsLen++] = path~'/';
            logger.info ("Path "~path~" is writable: " ~ (pv.isWritable ? "yes" : "no"));
        }
    }
    
    // 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;

//BEGIN Path resolution
static this() {
    logger = Log.getLogger ("mde.resource.paths");
    
    // NOTE: May fail. Currently I think I'll just let the exception halt execution.
    resolvePaths();
    if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!");
    if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!");
}

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 two exist and be a folder. Throws if none are. */
char[] findPath (char[][] paths ...) {
    foreach (path; paths) {
        PathView pv = new FilePath (path);
        if (pv.exists && pv.isFolder) return pv.toString;    // got a valid path
    }
    // no valid path...
    logger.fatal ("Unable to resolve 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).");
}

// 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:
        char[] staticPath = findPath ("/usr/share/games/mde", "/usr/local/share/games/mde", "data");
        char[] userPath = findPath (HOME~"/.config/mde", HOME~"/.mde");
        
        // Static data paths:
        dataDir.addPath (staticPath);      // we know this is valid anyway
        dataDir.tryPath (userPath ~ DATA);
        
        // Configuration paths:
        confDir.tryPath (staticPath ~ CONF);
        confDir.tryPath ("/etc/mde");
        confDir.tryPath (userPath ~ CONF);
    }
} 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[dataLength++] = staticPath;   // we know this is valid anyway
        dataDir.tryPath (userPath ~ DATA);
        
        // Configuration paths:
        confDir.tryPath (staticPath.toString ~ CONF);
        confDir.tryPath (installPath ~ CONF);
        confDir.tryPath (userPath.toString ~ CONF);
    }
} else {
    static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!");
}
//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;
}