view mde/setup/paths.d @ 63:66d555da083e

Moved many modules/packages to better reflect usage.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 27 Jun 2008 18:35:33 +0100
parents mde/resource/paths.d@f000d6cd0f74
children 108d123238c0
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 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.setup.paths;

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

import tango.io.Console;
import tango.io.FilePath;
import tango.sys.Environment;
//import tango.scrapple.sys.win32.Registry;     // Trouble getting this to work

/** 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)
    {
        PathView[] files = getFiles (file, readOrder);
        if (files is null)
            throw new MTFileIOException ("Unable to find the file: "~file~"[.mtt|mtb]");
        
        return new mdeReader (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)
    {
        PathView[] 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;
    }
    
    /** 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;
    }
    
    /// 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).newline;
    }
    
private:
    PathView[] getFiles (char[] filename, PRIORITY readOrder)
    in {
        assert (readOrder == PRIORITY.LOW_HIGH ||
                readOrder == PRIORITY.HIGH_LOW ||
                readOrder == PRIORITY.HIGH_ONLY );
    } body {
        PathView[] ret;
        if (readOrder == PRIORITY.LOW_HIGH) {
            for (size_t i = 0; i < pathsLen; ++i) {
                PathView file = findFile (paths[i]~filename);
                if (file !is null)
                    ret ~= file;
            }
        } else {
            for (int i = pathsLen - 1; i >= 0; --i) {
                PathView 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) {
        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) {
                // 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;

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

/** Find at least one path for each required directory.
*
* Note: the logger cannot be used yet, so only output is exception messages. */
// FIXME: use tango/sys/Environment.d
version (linux) {
    // base-path not used on posix
    void resolvePaths (char[] = null) {
        // Home directory:
        char[] HOME = Environment.get("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 (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;
    }
} else version (Windows) {
    void resolvePaths (char[] base = "./") {
        //FIXME: Get path from registry
        //FIXME: Get user path (Docs&Settings/USER/Local Settings/Application data/mde)
        //http://www.dsource.org/projects/tango/forums/topic/187
        
        // Base paths:
        PathView installPath = findPath (false, base);
        PathView staticPath = findPath (false, installPath.append("data").toString);
        PathView userPath = findPath (true, installPath.append("user").toString);   // FIXME: see above
        
        // 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;
    }
} 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".
// There are NO CHECKS that this is not exceeded.
    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. */
    PathView 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

    /** A special adapter for reading from multiple mergetag files with the same relative path to an
     * mdeDirectory simultaneously.
     */
    class mdeReader : IReader
    {
        private this (PathView[] files, DataSet ds, bool rdHeader)
        in {
            assert (files !is null, "mdeReader.this: files is null");
        } body {
            // Don't let sub-readers create their own, separate, datasets:
            if (ds is null) ds = new DataSet;
        
            foreach (file; files) {
                IReader r = makeReader (file, ds, rdHeader);
            
                readers[readersLen++] = r;
            }
        }
    
        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;
    }
}