view mde/setup/paths.d @ 91:4d5d53e4f881

Shared alignment for dynamic content lists - finally implemented! Lots of smaller changes too. Some debugging improvements. When multiple .mtt files are read for merging, files with invalid headers are ignored and no error is thrown so long as at least one file os valid.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 16 Oct 2008 17:43:48 +0100
parents 56c0ddd90193
children 71f0f1f83620
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/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.setup.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;
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)
    {
        FilePath[] files = getFiles (file, readOrder);
        if (files is null)
            throw new NoFileException ("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)
    {
        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).newline;
    }
    
private:
    FilePath[] getFiles (char[] filename, PRIORITY readOrder)
    {
        FilePath[] ret;
        
        debug (mdeUnitTest) {
        /* Alternate approach - just try from current directory.
        * Really just for unittests, but with the if condition it shouldn't break anything else. */
            if (pathsLen == 0) {
                FilePath file = findFile (filename);
                if (file !is null)
                    ret ~= file;
                return 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) {
        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):
        FilePath 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):
        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;
    }
} 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:
        FilePath installPath = findPath (false, base);
        FilePath staticPath = findPath (false, installPath.append("data").toString);
        FilePath 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. */
    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

    /** A special adapter for reading from multiple mergetag files with the same relative path to an
     * mdeDirectory simultaneously.
     */
    class mdeReader : IReader
    {
        private this (FilePath[] 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;
            
            Exception exc;
            foreach (file; files) {
                try {   // try reading header of each file
                    IReader r = makeReader (file, ds, rdHeader);
                    readers[readersLen++] = r;
                } catch (Exception e) {
                    exc = e;
                }
            }
            if (readersLen == 0)        // no files have valid headers
                throw exc;              // fail: re-throw last exception
        }
    
        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 (IContainer   !(ID) secSet) {      /// ditto
            for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
        }
        
        private:
            IReader[MAX_PATHS] readers;
            ubyte readersLen = 0;
            
            PRIORITY rdOrder;
    }
}

/// Thrown when makeMTReader couldn't find a file.
class NoFileException : MTFileIOException {
    this (char[] msg) {
        super(msg);
    }
}