view mde/file/paths.d @ 137:9f035cd139c6

BIG commit. Major change: old Options class is gone, all content values are loaded and saved automatically. All options updated to reflect this, some changed. Content restrutured a lot: New IContent module, Content module includes more functionality. New ContentLoader module to manage content loading/saving/translation. Translation module moved to content dir and cut down to reflect current usage. File format unchanged except renames: FontOptions -> Font, VideoOptions -> Screen. Font render mode and LCD filter options are now enums. GUI loading needs to create content (and set type for enums), but doesn't save/load value. Some setup of mainSchedule moved to mde.mainLoop. Content callbacks are called on content change now. ContentLists are set up implicitly from content symbols. Not as fast but much easier! Bug-fix in the new MTTagReader. Renamed MT *Reader maker functions to avoid confusion in paths.d. New mde.setup.logger module to allow logger setup before any other module's static this().
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 07 Feb 2009 12:46:03 +0000
parents 4084f07f2c7a
children e785e98d3b78
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;

debug {
    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) {
                // 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;
}

/** 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) throw new mdeException ("Fatal: no data path found!");
    if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!");
    
    for (int i = dataDir.pathsLen - 1; i >= 0; --i) {
        FilePath font = FilePath (dataDir.paths[i] ~ "fonts");
        Cout ("considering: ")(font.toString).newline;
        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);
    }
}