view mde/file/paths.d @ 151:e785e98d3b78

Updated for compatibility with tango 0.99.8.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 04 Apr 2009 17:32:18 +0200
parents 9f035cd139c6
children
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, 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.MTTagReader;
import mde.file.mergetag.Writer;
import mde.file.mergetag.MTTagWriter;
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;

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 IReader for each file (using MTMultiReader).
    *
    * 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) {
        return new MTMultiReader (getFiles (file, readOrder), ds, rdHeader);
    }
    
    /** Creates an MTTagReader for each file (using MTMultiTagReader).
     *
     * Params as for makeMTReader.
     */
    MTTagReader makeMTTagReader (char[] file, PRIORITY readOrder) {
        return new MTMultiTagReader (getFiles (file, readOrder));
    }
    
    /** 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 getMTWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text);
    }
    
    /** Creates an MTTagWriter for file. */
    MTTagWriter makeMTTagWriter (char[] file)
    {
        // FIXME: use highest priority writable path
        return getMTTagWriter (paths[pathsLen-1] ~ file);
    }
    
    /** Returns a string listing the file name or names (if readOrder is not 
     * HIGH_ONLY and multiple matches are found), or an error message. Intended
     * for user output only. */
    char[] getFileName (char[] file, PRIORITY readOrder)
    {
        FilePath[] files;
        try {
            files = getFiles (file, readOrder);
        } catch (NoFileException e) {
            return e.msg;
        }
        
        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);
        Cout ("\nFont directories:");
        foreach (path; fontPaths)
            Cout ("\n\t")(path.toString);
        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 {
            debug 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;
            }
        }
        if (ret is null)
            throw new NoFileException ("Unable to find the file: "~filename~"[.mtt|mtb]");
        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) {
                logger.error ("Creating path {} failed:" ~ e.msg, path);
            }
        }
        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;
}

/** Get the actual path of a font file, or throw NoFileException if not found.
 *
 * Returns a C string (null terminated). */
char[] getFontPath (char[] file) {
    foreach (path; fontPaths) {
    	FilePath font = path.dup.append (file);
    	if (font.exists && font.isFile)
            return font.cString;
    }
    throw new NoFileException ("Unable to find font file: "~file);
}

/** These are the actual instances, one for each of the data and conf "directories". */
mdeDirectory dataDir, confDir;
char[] logDir;		/// Directory for log files
FilePath[] fontPaths;	/// All directories for fonts

//BEGIN Path resolution
/// For command line args: these paths are added if non-null, with highest priority.
char[] extraDataPath, extraConfPath;

/** Add an extra font directory. May be called multiple times. */
void addFontPath (char[] path) {
    FilePath fp = FilePath (path);
    if (fp.exists && fp.isFolder)
        fontPaths ~= fp;
}

/** 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.
    }
}
void resolvePaths (char[] base = ".") {
    size_t iFP = fontPaths.length;
    fontPaths.length = fontPaths.length + 4;
    
    version (linux) {
        // Home directory:
        char[] HOME = Environment.get("HOME", ".");
        
        // Installation base (should contain "data" and "conf"):
        FilePath staticPath = findPath (false,
                                        "/usr/share/games/mde",
                                        "/usr/local/share/games/mde",
                                        base~"/data");
        // Path for user-adjusted files:
        FilePath userPath = findPath (true, HOME~"/.config/mde", HOME~"/.mde");
        
        dataDir.addPath (staticPath.toString);
        confDir.tryPath (staticPath.toString ~ "/conf");
        confDir.tryPath ("/etc/mde");
        
	// Font paths:
        auto fontP1 = FilePath("/usr/share/fonts").toList;
        foreach (fp1; fontP1)
            if (fp1.isFolder)
                foreach (fp2; fp1.toList)
                    if (fp2.isFolder) {
                        if (iFP >= fontPaths.length)
                            fontPaths.length = fontPaths.length * 2;
                        fontPaths[iFP++] = fp2;
                    }
    } else version (Windows) {
        //FIXME: Get base path from registry
        
        FilePath staticPath = findPath (false, base~"/data");
	FilePath userPath = findPath (true, getSpecialPath(CSIDL_LOCAL_APPDATA) ~ "/mde");
        
        dataDir.addPath (staticPath.toString);
        confDir.tryPath (staticPath.toString ~ "/conf");
	
	// Font path:
	fontPaths ~= FilePath(getSpecialPath (CSIDL_FONTS));
    } else {
    	static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!");
    }
    
    // Static data paths:
    dataDir.tryPath (userPath.toString ~ "/data");
    if (extraDataPath) dataDir.tryPath (extraDataPath);
    
    // Configuration paths:
    confDir.tryPath (userPath.toString ~ "/conf", true);
    if (extraConfPath) confDir.tryPath (extraConfPath);
    
    if (dataDir.pathsLen==0) throw new mdeException ("Fatal: no data path found!");
    if (confDir.pathsLen==0) throw new mdeException ("Fatal: no conf path found!");
    
    for (int i = dataDir.pathsLen - 1; i >= 0; --i) {
        FilePath font = FilePath (dataDir.paths[i] ~ "fonts");
        if (font.exists && font.isFolder)
            fontPaths[iFP++] = font;
    }
    fontPaths.length = iFP;
    
    // Logging path:
    logDir = userPath.toString;
}

private {
    class PathException : mdeException {
        this(char[] msg) {
            super (msg);
        }
    }
    
    // The maximum number of paths for any one "directory".
    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

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