changeset 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 960206198cbd
children cc3763817b8a
files mde/Options.d mde/events.d mde/font/FontTexture.d mde/font/exception.d mde/font/font.d mde/gl/draw.d mde/gui/Gui.d mde/gui/widget/TextWidget.d mde/i18n/I18nTranslation.d mde/input/Config.d mde/input/Input.d mde/lookup/Options.d mde/lookup/Translation.d mde/mde.d mde/resource/FontTexture.d mde/resource/exception.d mde/resource/font.d mde/resource/paths.d mde/scheduler/Init.d mde/scheduler/Scheduler.d mde/scheduler/exception.d mde/scheduler/init2.d mde/scheduler/initFunctions.d mde/sdl.d mde/setup/Init.d mde/setup/exception.d mde/setup/init2.d mde/setup/initFunctions.d mde/setup/paths.d mde/setup/sdl.d mde/types/Colour.d mde/types/basic.d
diffstat 32 files changed, 2812 insertions(+), 2808 deletions(-) [+]
line wrap: on
line diff
--- a/mde/Options.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,484 +0,0 @@
-/* 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/>. */
-
-/** This module handles stored options, currently all except input maps.
-*
-* The purpose of having all options centrally controlled is to allow generic handling by the GUI
-* and ease saving and loading of values. The Options class is only really designed around handling
-* small numbers of variables for now.
-*/
-module mde.Options;
-
-import mde.exception;
-
-import mde.mergetag.Reader;
-import mde.mergetag.Writer;
-import mde.mergetag.DataSet;
-import mde.mergetag.exception;
-import mde.resource.paths;
-
-import tango.scrapple.text.convert.parseTo : parseTo;
-import tango.scrapple.text.convert.parseFrom : parseFrom;
-
-import tango.core.Exception : ArrayBoundsException;
-import tango.util.log.Log : Log, Logger;
-
-/** Base class for handling options.
-*
-* This class itself handles no options and should not be instantiated, but provides a sub-classable
-* base for generic options handling. Also, the static portion of this class tracks sub-class
-* instances and provides loading and saving methods.
-*
-* Each sub-class provides named variables for maximal-speed reading. Local sub-class references
-* should be used for reading variables, and via the addOptionsClass() hook will be loaded from
-* files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will
-* not be saved; use, for example, Options.setBool(...). Use an example like OptionsMisc as a
-* template for creating a new Options sub-class.
-*
-* Details: Options sub-classes hold associative arrays of pointers to all option variables, with a
-* char[] id. This list is used for saving, loading and to provide generic GUI options screens. The
-* built-in support in Options is only for bool, int and char[] types (a float type may get added).
-* Further to this, a generic class is used to store all options which have been changed, and if any
-* have been changed, is merged with options from the user conf dir and saved on exit.
-*/
-class Options : IDataSection
-{
-    // No actual options are stored by this class. However, much of the infrastructure is
-    // present since it need not be redefined in sub-classes.
-    
-    // The "pointer lists":
-    protected bool*  [ID]   optsBool;
-    protected int*   [ID]   optsInt;
-    protected double*[ID]   optsDouble;
-    protected char[]*[ID]   optsCharA;
-    
-    //BEGIN Mergetag loading/saving code
-    void addTag (char[] tp, ID id, char[] dt) {
-        if (tp == "bool") {
-            bool** p = id in optsBool;
-            if (p !is null) **p = parseTo!(bool) (dt);
-        } else if (tp == "char[]") {
-            char[]** p = id in optsCharA;
-            if (p !is null) **p = parseTo!(char[]) (dt);
-        } else if (tp == "double") {
-            double** p = id in optsDouble;
-            if (p !is null) **p = parseTo!(double) (dt);
-        } else if (tp == "int") {
-            int** p = id in optsInt;
-            if (p !is null) **p = parseTo!(int) (dt);
-        }
-    }
-
-    void writeAll (ItemDelg dlg) {
-        foreach (ID id, bool*   val; optsBool)  dlg ("bool"  , id, parseFrom!(bool  ) (*val));
-        foreach (ID id, char[]* val; optsCharA) dlg ("char[]", id, parseFrom!(char[]) (*val));
-        foreach (ID id, double* val; optsDouble)dlg ("double", id, parseFrom!(double) (*val));
-        foreach (ID id, int*    val; optsInt)   dlg ("int"   , id, parseFrom!(int   ) (*val));
-    }
-    //END Mergetag loading/saving code
-    
-    //BEGIN Static
-    /** Add an options sub-class to the list for loading and saving.
-    *
-    * Call from static this() (before Init calls load()). */
-    static void addOptionsClass (Options c, char[] i)
-    in {    // Trap a couple of potential coding errors:
-        assert (c !is null);    // Instance must be created before calling addOptionsClass
-        assert (((cast(ID) i) in subClasses) is null);  // Don't allow a silent replacement
-    } body {
-        subClasses[cast(ID) i] = c;
-        subClassChanges[cast(ID) i] = new OptionsGeneric;
-    }
-    
-    /** Set option symbol of Options class subClass to val.
-    *
-    * Due to the way options are handled generically, string IDs must be used to access the options
-    * via hash-maps, which is a little slower than direct access but necessary since the option
-    * must be changed in two separate places. */
-    private static const ERR_MSG = "Options.setXXX called with incorrect parameters!";
-    static void setBool (char[] subClass, char[] symbol, bool val) {
-        changed = true;     // something got set (don't bother checking this isn't what it already was)
-        
-        try {
-            *(subClasses[cast(ID) subClass].optsBool[cast(ID) symbol]) = val;
-            subClassChanges[cast(ID) subClass].setBool (cast(ID) symbol, val);
-        } catch (ArrayBoundsException) {
-            // log and ignore:
-            logger.error (ERR_MSG);
-        }
-    }
-    static void setInt (char[] subClass, char[] symbol, int val) {
-        changed = true;     // something got set (don't bother checking this isn't what it already was)
-        
-        try {
-            *(subClasses[cast(ID) subClass].optsInt[cast(ID) symbol]) = val;
-            subClassChanges[cast(ID) subClass].setInt (cast(ID) symbol, val);
-        } catch (ArrayBoundsException) {
-            // log and ignore:
-            logger.error (ERR_MSG);
-        }
-    }
-    static void setDouble (char[] subClass, char[] symbol, double val) {
-        changed = true;     // something got set (don't bother checking this isn't what it already was)
-        
-        try {
-            *(subClasses[cast(ID) subClass].optsDouble[cast(ID) symbol]) = val;
-            subClassChanges[cast(ID) subClass].setDouble (cast(ID) symbol, val);
-        } catch (ArrayBoundsException) {
-            // log and ignore:
-            logger.error (ERR_MSG);
-        }
-    }
-    static void setCharA (char[] subClass, char[] symbol, char[] val) {
-        changed = true;     // something got set (don't bother checking this isn't what it already was)
-        
-        try {
-            *(subClasses[cast(ID) subClass].optsCharA[cast(ID) symbol]) = val;
-            subClassChanges[cast(ID) subClass].setCharA (cast(ID) symbol, val);
-        } catch (ArrayBoundsException) {
-            // log and ignore:
-            logger.error (ERR_MSG);
-        }
-    }
-    
-    // Track all sections for saving/loading/other generic handling.
-    static Options[ID] subClasses;
-    static OptionsGeneric[ID] subClassChanges;
-    static bool changed = false;    // any changes at all, i.e. do we need to save?
-    
-    /* Load/save options from file.
-    *
-    * If the file doesn't exist, no reading is attempted (options are left at default values).
-    */
-    private static const fileName = "options";
-    private static const MT_LOAD_EXC = "Loading options aborted:";
-    static void load () {
-        // Check it exists (if not it should still be created on exit).
-        // Don't bother checking it's not a folder, because it could still be a block or something.
-        if (!confDir.exists (fileName)) return;
-        
-        try {
-            IReader reader;
-            reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH);
-            reader.dataSecCreator = delegate IDataSection(ID id) {
-                /* Recognise each defined section, and return null for unrecognised sections. */
-                Options* p = id in subClasses;
-                if (p !is null) return *p;
-                else return null;
-            };
-            reader.read;
-        } catch (MTException e) {
-            logger.fatal (MT_LOAD_EXC);
-            logger.fatal (e.msg);
-            throw new optionsLoadException ("Mergetag exception (see above message)");
-        }
-    }
-    static void save () {
-        if (!changed) return;   // no changes to save
-        
-        DataSet ds = new DataSet();
-        foreach (id, sec; subClassChanges) ds.sec[id] = sec;
-        
-        // Read locally-stored options
-        try {
-            IReader reader;
-            reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds);
-            reader.dataSecCreator = delegate IDataSection(ID id) {
-                return null;    // All recognised sections are already in the dataset.
-            };
-            reader.read;
-        } catch (MTFileIOException) {
-            // File either didn't exist or couldn't be opened.
-            // Presuming the former, this is not a problem.
-        } catch (MTException e) {
-            // Log a message and continue, overwriting the file:
-            logger.error (MT_LOAD_EXC);
-            logger.error (e.msg);
-        }
-        
-        try {
-            IWriter writer;
-            writer = confDir.makeMTWriter (fileName, ds);
-            writer.write();
-        } catch (MTException e) {
-            logger.error ("Saving options aborted! Reason:");
-            logger.error (e.msg);
-        }
-    }
-    
-    private static Logger logger;
-    static this() {
-        logger = Log.getLogger ("mde.options");
-    }
-    //END Static
-    
-    //BEGIN Templates
-    private {
-        // Return index of first comma, or halts if not found.
-        template cIndex(char[] A) {
-            static if (A.length == 0)
-                static assert (false, "Error in implementation");
-            else static if (A[0] == ',')
-                const size_t cIndex = 0;
-            else
-                const size_t cIndex = 1 + cIndex!(A[1..$]);
-        }
-        // Return index of first semi-colon, or halts if not found.
-        template scIndex(char[] A) {
-            static if (A.length == 0)
-                static assert (false, "Error: no trailing semi-colon");
-            else static if (A[0] == ';')
-                const size_t scIndex = 0;
-            else
-                const size_t scIndex = 1 + scIndex!(A[1..$]);
-        }
-        // Look for "type symbols;" in A and return symbols as a comma separated list of names
-        // (even if type is encountered more than once). Output may contain spaces and, if
-        // non-empty, will contain a trailing comma. Assumes scIndex always returns less than A.$.
-        template parseT(char[] type, char[] A) {
-            static if (A.length < type.length + 1)	// end of input, no match
-                const char[] parseT = "";
-            else static if (A[0] == ' ')		// leading whitespace: skip
-                const char[] parseT = parseT!(type, A[1..$]);
-            else static if (A[0..type.length] == type && A[type.length] == ' ')	// match
-                const char[] parseT = A[type.length+1 .. scIndex!(A)] ~ "," ~
-                        parseT!(type, A[scIndex!(A)+1 .. $]);
-            else					// no match
-                const char[] parseT = parseT!(type, A[scIndex!(A)+1 .. $]);
-        }
-        // May have a trailing comma. Assumes cIndex always returns less than A.$.
-        template aaVars(char[] A) {
-            static if (A.length == 0)
-                const char[] aaVars = "";
-            else static if (A[0] == ' ')
-                const char[] aaVars = aaVars!(A[1..$]);
-            else
-                const char[] aaVars = "\""~A[0..cIndex!(A)]~"\"[]:&"~A[0..cIndex!(A)] ~ "," ~
-                        aaVars!(A[cIndex!(A)+1..$]);
-        }
-        // strip Trailing Comma
-        template sTC(char[] A) {
-            static if (A.length && A[$-1] == ',')
-                const char[] sTC = A[0..$-1];
-            else
-                const char[] sTC = A;
-        }
-        // if string is empty (other than space) return null, otherwise enclose: [A]
-        template listOrNull(char[] A) {
-            static if (A.length == 0)
-                const char[] listOrNull = "null";
-            else static if (A[0] == ' ')
-                const char[] listOrNull = listOrNull!(A[1..$]);
-            else
-                const char[] listOrNull = "["~A~"]";
-        }
-    } protected {
-        /** Produces the implementation code to go in the constuctor. */
-        template aaDefs(char[] A) {
-            const char[] aaDefs =
-                    "optsBool = "  ~ listOrNull!(sTC!(aaVars!(parseT!("bool"  , A)))) ~ ";\n" ~
-                    "optsInt = "   ~ listOrNull!(sTC!(aaVars!(parseT!("int"   , A)))) ~ ";\n" ~
-                    "optsDouble = "~ listOrNull!(sTC!(aaVars!(parseT!("double", A)))) ~ ";\n" ~
-                    "optsCharA = " ~ listOrNull!(sTC!(aaVars!(parseT!("char[]", A)))) ~ ";\n";
-        }
-        /+/** Produces the implementation code to go in the static constuctor. */
-        template optClassAdd(char[] symb) {
-            const char[] optClassAdd = symb ~ "=new "~classinfo(this).name~";\n";//Options.addOptionsClass("~symb~", );\n";
-        }+/
-        /** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
-         *
-         * Where type is one of bool, int, double, char[]. E.g.
-         * ---
-         * mixin (impl ("bool a, b; int i;"));
-         * ---
-         *
-         * In case this() needs to be customized, mixin(impl!(A)) is equivalent to:
-         * ---
-         * mixin (A~"\nthis(){\n"~aaDefs!(A)~"}");
-         * ---
-         *
-         * Notes: Only use space as whitespace (no new-lines or tabs). Make sure to add a trailing
-         * semi-colon (;) or you'll get told off! :D
-         *
-         * In general errors aren't reported well. Trial with pragma (msg, impl!(...)); if
-         * necessary.
-         *
-         * Extending: mixins could also be used for the static this() {...} or even the whole
-         * class, but doing so would rather decrease readability of any implementation. */
-        template impl(char[] A /+, char[] symb+/) {
-            const char[] impl = A~"\nthis(){\n"~aaDefs!(A)~"}";
-            // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}"
-        }
-    }
-    /+/** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
-     *
-     * E.g.
-     * ---
-     * mixin (impl ("bool a, b; int i;"));
-     * ---
-     * The parser isn't highly accurate. */
-    // Try using templates instead? See std.metastrings
-    static char[] impl (char[] A) {
-        char[] bools;
-        char[] ints;
-        
-        while (A.length) {
-            // Trim whitespace
-            {
-                size_t i = 0;
-                while (i < A.length && (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)))
-                    ++i;
-                A = A[i..$];
-            }
-            if (A.length == 0) break;
-            
-            char[] type;
-            for (size_t i = 0; i < A.length; ++i) {
-                if (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)) {
-                    type = A[0..i];
-                    A = A[i+1..$];
-                    break;
-                }
-            }
-            
-            char[] symbols;
-            for (size_t i = 0; i < A.length; ++i) {
-                if (A[i] == ';') {
-                    symbols = A[0..i];
-                    A = A[i+1..$];
-                    break;
-                }
-            }
-            
-            if (type == "bool") {
-                if (bools.length)
-                    bools = bools ~ "," ~ symbols;
-                else
-                    bools = symbols;
-            }
-            else if (type == "int") {
-                if (ints.length)
-                    ints = ints ~ "," ~ symbols;
-                else
-                    ints = symbols;
-            }
-            else {
-                // Unfortunately, we cannot output non-const strings (even though func is compile-time)
-                // We also cannot use pragma(msg) because the message gets printed even if the code isn't run.
-                //pragma(msg, "Warning: impl failed to parse whole input string");
-                // Cannot use Cout / logger either.
-                break;
-            }
-        }
-        
-        char[] ret;
-        if (bools.length)
-            ret = "bool "~bools~";\n";
-        if (ints.length)
-            ret = ret ~ "int "~ints~";\n";
-        
-        
-        
-        return ret;
-    }+/
-    //END Templates
-}
-
-/* Special class to store all locally changed options, whatever the section. */
-class OptionsGeneric : Options {
-    // These store the actual values, but are never accessed directly except when initially added.
-    // optsX store pointers to each item added along with the ID and are used for access.
-    bool[] bools;
-    int[] ints;
-    double[] doubles;
-    char[][] strings;
-    
-    this () {}
-    
-    void setBool (ID id, bool x) {
-        bool** p = id in optsBool;
-        if (p !is null) **p = x;
-        else {
-            bools ~= x;
-            optsBool[id] = &bools[$-1];
-        }
-    }
-    void setInt (ID id, int x) {
-        int** p = id in optsInt;
-        if (p !is null) **p = x;
-        else {
-            ints ~= x;
-            optsInt[id] = &ints[$-1];
-        }
-    }
-    void setDouble (ID id, double x) {
-        double** p = id in optsDouble;
-        if (p !is null) **p = x;
-        else {
-            doubles ~= x;
-            optsDouble[id] = &doubles[$-1];
-        }
-    }
-    void setCharA (ID id, char[] x) {
-        char[]** p = id in optsCharA;
-        if (p !is null) **p = x;
-        else {
-            strings ~= x;
-            optsCharA[id] = &strings[$-1];
-        }
-    }
-    
-    //BEGIN Mergetag loading/saving code
-    // Reverse priority: only load symbols not currently existing
-    void addTag (char[] tp, ID id, char[] dt) {
-        if (tp == "bool") {
-            if ((id in optsBool) is null) {
-                bools ~= parseTo!(bool) (dt);
-                optsBool[id] = &bools[$-1];
-            }
-        } else if (tp == "char[]") {
-            if ((id in optsCharA) is null) {
-                strings ~= parseTo!(char[]) (dt);
-                optsCharA[id] = &strings[$-1];
-            }
-            char[]** p = id in optsCharA;
-            if (p !is null) **p = parseTo!(char[]) (dt);
-        } else if (tp == "int") {
-            if ((id in optsInt) is null) {
-                ints ~= parseTo!(int) (dt);
-                optsInt[id] = &ints[$-1];
-            }
-        }
-    }
-    //END Mergetag loading/saving code
-}
-
-/* NOTE: These Options classes use templates to ease inserting contents.
-*
-* Each entry has an I18nTranslation entry; see data/L10n/ClassName.mtt for descriptions.
-*
-* To create a new class, just copy and paste anywhere and adjust.
-*/
-
-/** A home for all miscellaneous options, at least for now. */
-OptionsMisc miscOpts;
-class OptionsMisc : Options {
-    mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;"));
-    
-    static this() {
-        miscOpts = new OptionsMisc;
-        Options.addOptionsClass (miscOpts, "misc");
-    }
-}
--- a/mde/events.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/events.d	Fri Jun 27 18:35:33 2008 +0100
@@ -13,11 +13,14 @@
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
-/// Handles all events from SDL_PollEvent.
+/** Handling for all events from SDL_PollEvent.
+ *
+ * Handles some events, including a quit-request and window resizing, and passes the rest on to the
+ * input system. */
 module mde.events;
 
 import imde = mde.imde;
-import sdl = mde.sdl;           // resizeWindow
+import sdl = mde.setup.sdl;	// resizeWindow
 
 import mde.input.Input;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/font/FontTexture.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,495 @@
+/* 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/>. */
+
+/** Font caching system.
+ *
+ * This module also serves as the internals to the font module and shouldn't be used except through
+ * the font module. The two modules could be combined, at a cost to readability.
+ *
+ * Three types of coordinates get used in the system: FreeType coordinates for each glyph, texture
+ * coordinates, and OpenGL's model/world coordinates (for rendering). The freetype and texture
+ * coords are cartesian (i.e. y increases upwards), although largely this is too abstract to
+ * matter. However, for the model/world coords, y increases downwards. */
+module mde.font.FontTexture;
+
+import mde.types.Colour;
+import mde.lookup.Options;
+import mde.font.exception;
+
+import derelict.freetype.ft;
+import derelict.opengl.gl;
+
+import Utf = tango.text.convert.Utf;
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.font.FontTexture");
+}
+
+static const int dimW = 256, dimH = 256;	// Texture size
+const wFactor = 1f / dimW;
+const hFactor = 1f / dimH;
+
+/** A FontTexture is basically a cache of all font glyphs rendered so far.
+  *
+  * This class should be limited to code for rendering to (and otherwise handling) textures and
+  * rendering fonts to the screen.
+  *
+  * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */
+class FontTexture
+{
+    this () {}
+    ~this () {
+        foreach (t; tex) {
+            glDeleteTextures (1, &(t.texID));
+        }
+    }
+    
+    // Call if font(s) have been changed and glyphs must be recached.
+    void clear () {
+        foreach (t; tex) {
+            glDeleteTextures (1, &(t.texID));
+        }
+        cachedGlyphs = null;
+        ++cacheVer;
+    }
+    
+    
+    /** Cache informatation for rendering a block of text.
+     *
+     * Recognises '\r', '\n' and "\r\n" as end-of-line markers. */
+    void updateCache (FT_Face face, char[] str, ref TextBlock cache)
+    {
+        debug scope (failure)
+                logger.error ("updateCache failed");
+        
+        if (cache.cacheVer == cacheVer)	// Existing cache is up-to-date
+            return;
+        
+        cache.cacheVer = cacheVer;
+        
+        /* Convert the string to an array of character codes (which is equivalent to decoding UTF8
+        * to UTF32 since no character code is ever > dchar.max). */
+        static dchar[] chrs;	// keep memory for future calls (NOTE: change for threading)
+        chrs = Utf.toString32 (str, chrs);
+        
+        // Allocate space.
+        // Since end-of-line chars get excluded, will often be slightly larger than necessary.
+        cache.chars.length = chrs.length;
+        cache.chars.length = 0;
+        
+        int lineSep = face.size.metrics.height >> 6;
+        bool hasKerning = (FT_HAS_KERNING (face) != 0);
+        int y = 0;
+        CharCache cc;		// struct; reused for each character
+        
+        for (size_t i = 0; i < chrs.length; ++i)
+        {
+            // First, find yMax for the current line.
+            int yMax = 0;	// Maximal glyph height above baseline.
+            for (size_t j = i; j < chrs.length; ++j)
+            {
+                if (chrs[j] == '\n' || chrs[j] == '\r')	// end of line
+                    break;
+                
+                GlyphAttribs* ga = chrs[j] in cachedGlyphs;
+                if (ga is null) {			// Not cached
+                    addGlyph (face, chrs[j]);		// so render it
+                    ga = chrs[j] in cachedGlyphs;	// get the ref of the copy we've stored
+                    assert (ga !is null, "ga is null: 1");
+                }
+                
+                if (ga.top > yMax)
+                    yMax = ga.top;
+            }
+            y += yMax;
+            
+            // Now for the current line:
+            int x = 0;	// x pos for next glyph
+            uint gi_prev = 0;	// previous glyph index (needed for kerning)
+            for (; i < chrs.length; ++i)
+            {
+                // If end-of-line, break to find yMax for next line.
+                if (chrs.length >= i+2 && chrs[i..i+2] == "\r\n"d) {
+                    ++i;
+                    break;
+                }
+                if (chrs[i] == '\n' || chrs[i] == '\r') {
+                    break;
+                }
+                
+                cc.ga = chrs[i] in cachedGlyphs;
+                assert (cc.ga !is null, "ga is null: 2");
+                
+                // Kerning
+                if (hasKerning && (gi_prev != 0)) {
+                    FT_Vector delta;
+                    FT_Get_Kerning (face, gi_prev, cc.ga.index, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta);
+                    x += delta.x >> 6;
+                }
+                
+                // ga.left component: adding this slightly improves glyph layout. Note that the
+                // left-most glyph on a line may not start right on the edge, but this looks best.
+                cc.xPos = x + cc.ga.left;
+                cc.yPos = y - cc.ga.top;
+                x += cc.ga.advanceX;
+                
+                cache.chars ~= cc;
+                
+                // Update rect total size. Top and left coords should be zero, so make width and
+                // height maximal x and y coordinates.
+                if (cc.xPos + cc.ga.w > cache.w)
+                    cache.w = cc.xPos + cc.ga.w;
+                if (cc.yPos + cc.ga.h > cache.h)
+                    cache.h = cc.yPos + cc.ga.h;
+            }
+            // Now increment i and continue with the next line if there is one.
+            y += lineSep - yMax;
+        }
+    }
+    
+    /** Render a block of text using a cache. Updates the cache if necessary.
+     *
+     * Params:
+     *  face =	Current typeface pointer; must be passed from font.d (only needed if the cache is
+     *  	invalid)
+     *  str =	Text to render (only needed if the cache is invalid)
+     *  cache =	Cache used to speed up CPU-side rendering code
+     *  x =	Smaller x-coordinate of position
+     *  y =	Smaller y-coordinate of position
+     *  col =	Text colour (note: currently limited to black or white) */
+    void drawCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col ) {
+        updateCache (face, str, cache);	// update if necessary
+        debug scope (failure)
+                logger.error ("drawTextCache failed");
+        
+        // opaque (GL_DECAL would be equivalent)
+        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+        
+        drawCacheImpl (cache, x, y, col);
+    }
+    /** A variation of drawCache, for transparent text.
+     *
+     * Instead of passing the alpha value(s) as arguments, set the openGL colour prior to calling:
+     * ---
+     * glColor3f (.5f, .5f, .5f);	// set alpha to half
+     * drawCacheA (face, ...);
+     * 
+     * glColor3ub (0, 255, 127);	// alpha 0 for red, 1 for green, half for blue
+     * drawCacheA (face, ...);
+     * ---
+     *
+     * The overhead of the transparency is minimal. */
+    void drawCacheA (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col/+ = Colour.WHITE+/) {
+        updateCache (face, str, cache);	// update if necessary
+        debug scope (failure)
+                logger.error ("drawTextCache failed");
+        
+        // transparency alpha
+        // alpha is current colour, per component
+        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
+        
+        drawCacheImpl (cache, x, y, col);
+    }
+   
+    private void drawCacheImpl (ref TextBlock cache, int x, int y, Colour col) {
+        if (DerelictGL.availableVersion() >= GLVersion.Version14) {
+            glBlendFunc (GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR);
+            glBlendColor(col.r, col.g, col.b, 1f);	// text colour
+        } else
+            glBlendFunc (col.nearestGLConst, GL_ONE_MINUS_SRC_COLOR);
+        
+        glEnable (GL_TEXTURE_2D);
+        glEnable(GL_BLEND);
+        
+        foreach (chr; cache.chars) {
+            GlyphAttribs* ga = chr.ga;
+            
+            glBindTexture(GL_TEXTURE_2D, ga.texID);
+            
+            int x1 = x + chr.xPos;
+            int y1 = y + chr.yPos;
+            int x2 = x1 + ga.w;
+            int y2 = y1 + ga.h;
+            float tx1 = ga.x * wFactor;
+            float ty1 = ga.y * hFactor;
+            float tx2 = (ga.x + ga.w) * wFactor;
+            float ty2 = (ga.y + ga.h) * hFactor;
+           
+            glBegin (GL_QUADS);
+            glTexCoord2f (tx1, ty1);	glVertex2i (x1, y1);
+            glTexCoord2f (tx2, ty1);	glVertex2i (x2, y1);
+            glTexCoord2f (tx2, ty2);	glVertex2i (x2, y2);
+            glTexCoord2f (tx1, ty2);	glVertex2i (x1, y2);
+            glEnd ();
+        }
+        
+        glDisable(GL_BLEND);
+    }
+    
+    void addGlyph (FT_Face face, dchar chr) {
+        debug scope (failure)
+                logger.error ("FontTexture.addGlyph failed!");
+        
+        auto gi = FT_Get_Char_Index (face, chr);
+        auto g = face.glyph;
+        
+        // Use renderMode from options, masking bits which are allowable:
+        if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | (fontOpts.renderMode & 0xF0000)))
+            throw new fontGlyphException ("Unable to render glyph");
+        
+        auto b = g.bitmap;
+        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+        //glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch);
+        
+        GlyphAttribs ga;
+        ga.w		= b.width;
+        ga.h		= b.rows;
+        ga.left		= g.bitmap_left;
+        ga.top		= g.bitmap_top;
+        ga.advanceX	= g.advance.x >> 6;
+        ga.index	= gi;
+        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD)
+            ga.w /= 3;
+        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V)
+            ga.h /= 3;
+        
+        foreach (ref t; tex) {
+            if (t.addGlyph (ga))
+                goto gotTexSpace;
+        }
+        // if here, no existing texture had the room for the glyph so create a new texture
+        // NOTE: check if using more than one texture impacts performance due to texture switching
+        logger.info ("Creating a new font texture.");
+        tex ~= TexPacker.create();
+        assert (tex[$-1].addGlyph (ga), "Failed to fit glyph in a new texture but addGlyph didn't throw");
+        
+        gotTexSpace:
+        glBindTexture(GL_TEXTURE_2D, ga.texID);
+        GLenum format;
+        ubyte[] buffer;	// use our own pointer, since for LCD modes we need to perform a conversion
+        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256) {
+            assert (b.pitch == b.width, "Have assumed b.pitch == b.width for gray glyphs.");
+            buffer = b.buffer[0..b.pitch*b.rows];
+            format = GL_LUMINANCE;
+        } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) {
+            // NOTE: Can't seem to get OpenGL to read freetype's RGB buffers properly, so convent.
+            /* NOTE: Sub-pixel rendering probably also needs filtering. I haven't tried, since it's
+             * disabled in my build of the library. For a tutorial on the filtering, see:
+             * http://dmedia.dprogramming.com/?n=Tutorials.TextRendering1 */
+            buffer = new ubyte[b.width*b.rows];
+            for (uint i = 0; i < b.rows; ++i)
+                for (uint j = 0; j < b.width; j+= 3)
+            {
+                buffer[i*b.width + j + 0] = b.buffer[i*b.pitch + j + 0];
+                buffer[i*b.width + j + 1] = b.buffer[i*b.pitch + j + 1];
+                buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2];
+            }
+            
+            format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB;
+        } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) {
+            // NOTE: Notes above apply. Only in this case converting the buffers seems essential.
+            buffer = new ubyte[b.width*b.rows];
+            for (uint i = 0; i < b.rows; ++i)
+                for (uint j = 0; j < b.width; ++j)
+            {
+                // i/3 is the "real" row, b.width*3 is our width (with subpixels), j is column,
+                // i%3 is sub-pixel (R/G/B). i/3*3 necessary to round to multiple of 3
+                buffer[i/3*b.width*3 + 3*j + i%3] = b.buffer[i*b.pitch + j];
+            }
+            
+            format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB;
+        } else
+            throw new fontGlyphException ("Unsupported freetype bitmap format");
+        
+        glTexSubImage2D(GL_TEXTURE_2D, 0,
+                        ga.x, ga.y,
+                        ga.w, ga.h,
+                        format, GL_UNSIGNED_BYTE,
+                        cast(void*) buffer.ptr);
+        
+        cachedGlyphs[chr] = ga;
+    }
+    
+    // Draw the first glyph cache texture in the upper-left corner of the screen.
+    debug (drawGlyphCache) void drawTexture () {
+        if (tex.length == 0) return;
+        glEnable (GL_TEXTURE_2D);
+        glBindTexture(GL_TEXTURE_2D, tex[0].texID);
+        glEnable(GL_BLEND);
+        glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR);
+        float[4] Cc = [ 1.0f, 1f, 1f, 1f ];
+        glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, Cc.ptr);
+        glColor3f (1f, 0f, 0f);
+        
+        glBegin (GL_QUADS);
+        glTexCoord2f (0f, 0f);	glVertex2i (0, 0);
+        glTexCoord2f (1f, 0f);	glVertex2i (dimW, 0);
+        glTexCoord2f (1f, 1f);	glVertex2i (dimW, dimH);
+        glTexCoord2f (0f, 1f);	glVertex2i (0, dimH);
+        glEnd ();
+        
+        glDisable(GL_BLEND);
+    }
+    
+private:
+    TexPacker[] tex;	// contains the gl texture id and packing data
+    
+    GlyphAttribs[dchar]	cachedGlyphs;
+    int cacheVer = 0;	// version of cache, used to make sure TextBlock caches are current.
+}
+
+// Use LinePacker for our texture packer:
+alias LinePacker TexPacker;
+
+/** Represents one gl texture; packs glyphs into lines. */
+struct LinePacker
+{
+    // create a new texture
+    static LinePacker create () {
+        LinePacker p;
+        //FIXME: why do I get a blank texture when binding to non-0?
+        //glGenTextures (1, &(p.texID));
+        p.texID = 0;
+        
+        // add a pretty background to the texture
+        debug (drawGlyphCache) {
+            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+            glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
+            ubyte[3][dimH][dimW] testTex;
+            for (size_t i = 0; i < dimW; ++i)
+                for (size_t j = 0; j < dimH; ++j)
+            {
+                testTex[i][j][0] = cast(ubyte) (i + j);
+                testTex[i][j][1] = cast(ubyte) i;
+                testTex[i][j][2] = cast(ubyte) j;
+            }
+            void* ptr = testTex.ptr;
+        } else
+            const void* ptr = null;
+        
+        // Create a texture without initialising values.
+        glBindTexture(GL_TEXTURE_2D, p.texID);
+        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
+                     dimW, dimH, 0,
+                     GL_RGB, GL_UNSIGNED_BYTE, ptr);
+        return p;
+    }
+    
+    /** Find space for a glyph of size attr.w, attr.h within the texture.
+     *
+     * Throws: fontGlyphException if glyph dimensions are larger than the texture.
+     *
+     * Returns false if unable to fit the glyph into the texture, true if successful. If
+     * successful, attr's x and y are set to suitible positions such that the rect given by attr's
+     * x, y, w & h is a valid subregion of the texture. */
+    bool addGlyph (ref GlyphAttribs attr) {
+        if (attr.w > dimW || attr.h > dimH)
+            throw new fontGlyphException ("Glyph too large to fit texture!");
+        
+        attr.texID = texID;		// Set now. Possibly reset if new texture is needed.
+        if (attr.w == 0) return true;	// 0 sized glyph; x and y are unimportant.
+        
+        bool cantFitExtraLine = nextYPos + attr.h >= dimH;
+        foreach (ref line; lines) {
+            if (line.length + attr.w <= dimW &&		// if sufficient length and
+                line.height >= attr.h &&		// sufficient height and
+                (cantFitExtraLine ||			// either there's not room for another line
+                line.height <= attr.h * WASTE_H))	// or we're not wasting much vertical space
+            {						// then use this line
+                attr.x = line.length;
+                attr.y = line.yPos;
+                attr.texID = texID;
+                line.length += attr.w;
+                return true;
+            }
+        }
+        // If we didn't return, we didn't use an existing line.
+        if (cantFitExtraLine)				// run out of room
+            return false;
+        
+        // Still room: add a new line. The new line has the largest yPos (furthest down texture),
+        // but the lines array must remain ordered by line height (lowest to heighest).
+        Line line;
+        line.yPos = nextYPos;
+        line.height = attr.h * EXTRA_H;
+        line.length = attr.w;
+        size_t i = 0;
+        while (i < lines.length && lines[i].height < line.height) ++i;
+        lines = lines[0..i] ~ line ~ lines[i..$];	// keep lines sorted by height
+        nextYPos += line.height;
+        
+        attr.x = 0;	// first glyph in the line
+        attr.y = line.yPos;
+        return true;
+    }
+    
+    // Publically accessible data:
+    uint texID;				// OpenGL texture identifier (for BindTexture)
+    
+private:
+    const WASTE_H = 1.3;
+    const EXTRA_H = 1;	// can be float/double, just experimenting with 1
+    struct Line {
+        int yPos;	// y position (xPos is always 0)
+        int height;
+        int length;
+    }
+    Line[] lines;
+    int nextYPos = 0;	// y position for next created line (0 for first line)
+}
+
+// this bit of renderMode, if set, means read glyph as BGR not RGB when using LCD rendering
+enum { RENDER_LCD_BGR = 1 << 30 }
+OptionsFont fontOpts;
+class OptionsFont : Options {
+    /* renderMode should be FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, FT_LOAD_TARGET_LCD or
+     * FT_LOAD_TARGET_LCD_V, possibly with bit 31 set (see RENDER_LCD_BGR).
+     * FT_LOAD_TARGET_MONO is unsupported.
+     *
+     * lcdFilter should come from enum FT_LcdFilter:
+     * FT_LCD_FILTER_NONE = 0, FT_LCD_FILTER_DEFAULT = 1, FT_LCD_FILTER_LIGHT = 2 */
+    mixin (impl!("int renderMode, lcdFilter;"));
+    
+    static this() {
+        fontOpts = new OptionsFont;
+        Options.addOptionsClass (fontOpts, "font");
+    }
+}
+
+struct GlyphAttribs {
+    int x, y;	// position within texture
+    int w, h;	// bitmap size
+    
+    int left, top;	// bitmap_left, bitmap_top fields
+    int advanceX;	// horizontal advance distance
+    uint index;		// glyph index (within face)
+    
+    uint texID;	// gl tex identifier
+}
+
+/** Cached information for drawing a block of text.
+ *
+ * Struct should be stored externally and updated via references. */
+struct TextBlock {
+    CharCache[] chars;	// All chars. They hold x & y pos. info, so don't need to know about lines.
+    int cacheVer = -1;	// this is checked on access, and must equal for cache to be valid.
+    int w, h;		/// Size of the block. Likely the only fields of use outside the library.
+}
+struct CharCache {
+    GlyphAttribs* ga;	// character
+    int xPos, yPos;	// x,y position
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/font/exception.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,47 @@
+/* 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/>. */
+
+/// Contains font exceptions
+module mde.font.exception;
+import mde.exception;
+
+/// Thrown when initialisation fails
+class fontException : mdeException {
+    char[] getSymbol () {
+        return super.getSymbol ~ ".font";
+    }
+    
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
+/// Thrown when loading a freetype font fails.
+class fontLoadException : fontException {
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
+/// Thrown when problems occur with glyphs (rendering, etc.)
+class fontGlyphException : fontException {
+    char[] getSymbol () {
+        return super.getSymbol ~ ".glyph";
+    }
+    
+    this (char[] msg) {
+        super(msg);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/font/font.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,302 @@
+/* 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/>. */
+
+/// Sets up freetype (in a basic way).
+module mde.font.font;
+
+public import mde.types.Colour;
+import mde.lookup.Options;
+import mde.font.FontTexture;
+import mde.font.exception;
+
+import mde.mergetag.Reader;
+import mde.mergetag.DataSet;
+import mde.mergetag.exception;
+import mde.setup.paths;
+
+import derelict.freetype.ft;
+import derelict.opengl.gl;
+
+import tango.scrapple.text.convert.parseTo : parseTo;
+import tango.stdc.stringz;
+import Util = tango.text.Util;
+import tango.util.log.Log : Log, Logger;
+
+// "Publically import" this symbol:
+alias mde.font.FontTexture.TextBlock TextBlock;
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.font.font");
+}
+
+/** FontStyle class.
+ *
+ * Particular to a font and size, and any other effects like bold/italic if ever implemented.
+ * 
+ * Note: it is not currently intended to be thread-safe. */
+class FontStyle : IDataSection
+{
+    //BEGIN Static: manager
+    static {
+        debug (drawGlyphCache) void drawTexture() {
+            if (fontTex !is null)
+                fontTex.drawTexture;
+        }
+        
+        /** Load the freetype library. */
+        void initialize () {
+            if (!confDir.exists (fileName))
+                throw new fontException ("No font settings file (fonts.[mtt|mtb])");
+            
+            if (FT_Init_FreeType (&library))
+                throw new fontException ("error initialising the FreeType library");
+            
+            // Check version
+            FT_Int maj, min, patch;
+            FT_Library_Version (library, &maj, &min, &patch);
+            if (maj != 2 || min != 3) {
+                char[128] tmp;
+                logger.warn (logger.format (tmp, "Using an untested FreeType version: {}.{}.{}", maj, min, patch));
+            }
+            
+            // Set LCD filtering method if LCD rendering is enabled.
+            const RMF = FT_LOAD_TARGET_LCD | FT_LOAD_TARGET_LCD_V;
+            if (fontOpts.renderMode & RMF &&
+                FT_Library_SetLcdFilter(library, cast(FT_LcdFilter)fontOpts.lcdFilter)) {
+                /* An error occurred, presumably because LCD rendering support
+                * is not compiled into the library. */
+                logger.warn ("Bad/unsupported LCD filter option; disabling LCD font rendering.");
+                logger.warn ("Your FreeType 2 library may compiled without support for LCD/sub-pixel rendering.");
+                
+                // Reset the default filter (in case an invalid value was set in config files).
+                Options.setInt ("font", "lcdFilter", FT_LcdFilter.FT_LCD_FILTER_DEFAULT);
+                
+                /* If no support for LCD filtering, then LCD rendering only emulates NORMAL with 3
+                 * times wider glyphs. So disable and save the extra work. */
+                Options.setInt ("font", "renderMode", FT_LOAD_TARGET_NORMAL);
+            }
+            
+            /* Load font settings
+             *
+             * Each mergetag section corresponds to a font; each is loaded whether used or not
+             * (however the actual font files are only loaded on use). A fallback id must be
+             * provided in the header which must match a loaded font name; if a non-existant font
+             * is requested a warning will be logged and this font returned. */
+            char[] fallbackName;
+            try {
+                IReader reader;
+                reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH, null, true);
+                reader.dataSecCreator = delegate IDataSection(ID id) {
+                    auto f = new FontStyle;
+                    fonts[id] = f;
+                    return f;
+                };
+                reader.read;
+                
+                // get fallback name
+                char[]* p = "fallback" in reader.dataset.header.Arg!(char[]).Arg;
+                if (p is null)
+                    throw new fontException ("No fallback font style specified");
+                fallbackName = *p;
+            }
+            catch (MTException e) {
+                throw new fontException ("Mergetag exception: "~e.msg);
+            }
+            
+            // Find the fallback
+            FontStyle* p = fallbackName in fonts;
+            if (p is null)
+                throw new fontException ("Fallback font style specified is not found");
+            fallback = *p;
+            // Load the fallback now, to ensure it's available.
+            // Also note that get() doesn't make sure the fallback is loaded before returning it.
+            fallback.load;
+        }
+        private const fileName = "fonts";
+        
+        //FIXME: don't use GC for FontStyle resources
+        /** Cleanup: delete all fonts. */
+        void cleanup () {
+            FT_Done_FreeType (library);
+        }
+        
+        /** Get a FontStyle instance, for a section in the fonts.mtt file.
+          *
+          * Also loads the font if it's not already loaded, so the first call may take some time.
+          *
+          * Uses fallback font-style if the desired style isn't known about or fails to load, so
+          * this function should never fail or throw, in theory (unless out of memory). The
+          * fallback should already be loaded. */
+        FontStyle get(char[] name) {
+            FontStyle* p = name in fonts;
+            if (p is null) {
+                logger.warn ("Font style "~name~" requested but not found; reverting to the fallback style.");
+                fonts[name] = fallback;	// set to prevent another warning getting logged
+                return fallback;
+            }
+            // Got it, but we need to make sure it's loaded:
+            try {
+                p.load;
+            } catch (Exception e) {
+                logger.warn ("Font style "~name~" failed to load; reverting to the fallback style.");
+                return fallback;
+            }
+            return *p;
+        }
+        
+    private:
+        FT_Library	library;
+        FontTexture	fontTex;
+        FontStyle[ID]	fonts;		// all font styles known about; not necessarily loaded
+        FontStyle	fallback;	// used when requested font isn't in fonts
+    }
+    //END Static
+    
+    this() {}
+    
+    //BEGIN Mergetag code
+    //NOTE: would it be better not to use a new mergetag file for this?
+    //FIXME: revise when gui can set options
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "char[]") {
+            if (id == "path")
+                path = parseTo!(char[]) (dt);
+        }
+        else if (tp == "int") {
+            if (id == "size")
+                size = parseTo!(int) (dt);
+        }
+    }
+    void writeAll (ItemDelg) {}		// no writing the config for now
+    //END Mergetag code
+    
+    /** Load the font file.
+     *
+     * Even if the same font is used at multiple sizes, multiple copies of FT_Face are used.
+     * Sharing an FT_Face would require calling FT_Set_Pixel_Sizes each time a glyph is rendered or
+     * swapping the size information (face.size)? */
+    void load ()
+    in {
+        assert (library !is null, "font: library is null");
+    } body {
+        if (FT_New_Face (library, toStringz(path), 0, &face))
+            throw new fontLoadException ("Unable to read font: "~path);
+        
+        if (!FT_IS_SCALABLE (face))
+            throw new fontLoadException ("Currently no support for non-scalable fonts (which " ~
+                    path ~ " is). Please report if you want to see support.");
+        /* The following will need to be addressed when adding support for non-scalables:
+         *	Use of face.size.metrics.height property.
+         */
+        
+        if (FT_Set_Pixel_Sizes (face, 0,size))
+            throw new fontLoadException ("Unable to set pixel size");
+        
+        // Create if necessary:
+        if (fontTex is null)
+            fontTex = new FontTexture;
+    }
+    
+    /** Update a TextBlock cache, as used by the textBlock function.
+     *
+     * The only use of this is to get the text block's size ahead of rendering, via TextBlock's w
+     * and h properties.
+     *
+     * This function will only actually update the cache if it is invalid, caused either by the
+     * font being changed or if cache.cacheVer < 0. */
+    void updateBlock (char[] str, ref TextBlock cache) {
+        try {
+            fontTex.updateCache (face, str, cache);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
+        }
+    }
+    
+    /** Draw a block of text (may inlcude new-lines).
+     *
+     * The text block is drawn with top-left corner at x,y. To put the text's baseline at a given
+     * y coordinate would require some changes. Line height is currently variable, depending on the
+     * highest glyph in the line (should probably be fixed: FIXME).
+     *
+     * Specify the text's colour with col; currently this is only Colour.WHITE or Colour.BLACK
+     * (FIXME). FIXME: add alpha support.
+     *
+     * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as
+     * the cache argument. This is the recommended method, although for one-time calls when you
+     * don't need to know the size, the other version of textBlock may be used.
+     * ---------------------------------
+     * char[] str;
+     * TextBlock strCache;
+     * textBlock (x, y, str, strCache);
+     * ---------------------------------
+     * The TextBlock cache will be updated as necessary. Besides the initial update, this will only
+     * be if the font changes, or it is manually invalidated. This can be done by setting the
+     * TextBlock's cacheVer property to -1, which should be done if str is changed.
+     *
+     * The TextBlock's w and h properties are set to the size (in pixels) of the text block; other
+     * than this cache only serves as a small optimisation. However, the only way to get the size
+     * of a text block is to use a TextBlock cache and update it, either with this function or with
+     * the updateBlock function. */
+    void textBlock (int x, int y, char[] str, ref TextBlock cache, Colour col) {
+        try {
+            fontTex.drawCache (face, str, cache, x, y, col);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
+        }
+    }
+    /** ditto */
+    void textBlock (int x, int y, char[] str, Colour col) {
+        try {
+            // Using the cache method for one-time use is slightly less than optimal, but doing so
+            // isn't really recommended anyway (and maintaining two versions of fontTex.drawText
+            // would be horrible).
+            TextBlock cache;
+            fontTex.drawCache (face, str, cache, x, y, col);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
+        }
+    }
+    
+    /** A variation of textBlock for transparency.
+     *
+     * Set the alpha by calling glColor*() first. See FontTexture.drawCacheA()'s documentation for
+     * details. */
+    void textBlockA (int x, int y, char[] str, ref TextBlock cache, Colour col) {
+        try {
+            fontTex.drawCacheA (face, str, cache, x, y, col);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
+        }
+    }
+    
+    /** The font-specified vertical distance between the baseline of consecutive lines. */
+    int getLineSeparation () {
+        return face.size.metrics.height >> 6;
+    }
+    
+    ~this () {
+        FT_Done_Face (face);
+    }
+    
+private:
+    char[]	path;	// path to font file
+    int		size;	// font size
+    
+    FT_Face	face;
+}
+
+/+class OptionsFont : Options {
+    alias store!(+/
\ No newline at end of file
--- a/mde/gl/draw.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/gl/draw.d	Fri Jun 27 18:35:33 2008 +0100
@@ -26,7 +26,7 @@
 import tango.time.Time;     // TimeSpan (type only; unused)
 import tango.util.log.Log : Log, Logger;
 
-import mde.resource.font;
+import mde.font.font;
 private Logger logger;
 static this () {
     logger = Log.getLogger ("mde.gl.draw");
--- a/mde/gui/Gui.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/gui/Gui.d	Fri Jun 27 18:35:33 2008 +0100
@@ -37,7 +37,7 @@
 import mt = mde.mergetag.exception;
 import mde.mergetag.Reader;
 import mde.mergetag.Writer;
-import mde.resource.paths;
+import mde.setup.paths;
 
 import tango.util.log.Log : Log, Logger;
 
--- a/mde/gui/widget/TextWidget.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/gui/widget/TextWidget.d	Fri Jun 27 18:35:33 2008 +0100
@@ -24,7 +24,7 @@
 import mde.gui.renderer.IRenderer;
 import mde.gui.content.Content;
 
-import mde.resource.font;
+import mde.font.font;
 
 import tango.io.Stdout;
 
--- a/mde/i18n/I18nTranslation.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,242 +0,0 @@
-/* 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/>. */
-/** I18nTranslation − internationalization module for translating strings
-*
-* The idea behind this module is a class which, when asked to load symbols for a particular module/
-* package/part of the program, will load internationalized names and optional descriptions for each
-* symbol needing translation. No support for non-left-to-right scripts is currently planned, and
-* this module is currently limited to translations, although support for different date formats,
-* etc. could potentially be added later.
-*
-* Code symbols are used as identifiers for each name and its optional description. The code symbol
-* will be used as a fallback in the case no entry exists, however it is not intended to provide the
-* string for the default language (a "translation" should be used for the default language).
-*
-* Each locale may specify dependant locales/sections which will be loaded and merged in to the
-* database, to cover for symbols with a missing entry. Sections are loaded in the order specified,
-* with each section's sub-dependancies loaded before continuing with the next top-level dependancy.
-* A list of loaded sections is used to prevent any locale/section from being loaded twice, and thus
-* allow circular dependancies.
-*
-* In order that translated strings get updated correctly to reflect changes, each entry carries a
-* version number. If, for any entry, a translation exists with a higher version number, that entry
-* is out of date. A tool should be made for checking for out of date entries to take advantage of
-* this feature. Of course, out of date entries are still valid for use.
-*/
-module mde.i18n.I18nTranslation;
-
-import mde.options;
-import mde.exception;
-
-import mde.mergetag.DataSet;
-import mde.mergetag.Reader;
-import mde.mergetag.exception;
-import mde.resource.paths;
-
-import tango.util.log.Log : Log, Logger;
-import tango.scrapple.text.convert.parseTo;
-
-/** The translation class
-*
-* See module description for details.
-*
-* Encoding used is UTF-8.
-*/
-class I18nTranslation : IDataSection
-{
-    final char[] name;      /// The module/package/... which the instance is for
-    final char[] L10n;      /// The localization loaded (e.g. en-GB)
-    
-    /** Get the translation for the given identifier, and optionally the description.
-    * If no entry exists, the identifier will be returned. */
-    char[] getEntry (char[] id, out char[] description) {
-        Entry* p = id in entries;
-        if (p) {    // FIXME: check: a SEGFAULT?
-            description = p.desc;
-            return p.str;
-        } else
-            return id;
-    }
-        /** ditto */
-    char[] getEntry (char[] id) {
-        Entry* p = id in entries;
-        if (p !is null) {    // FIXME: check: a SEGFAULT?
-            return p.str;
-        } else
-            return id;
-    }
-    
-    /** Load the translation for the requested module/package/...
-    *
-    * Options (mde.options) must have been loaded before this is run.
-    *
-    * Params:
-    *   name The module/package/... to load strings for.
-    *
-    * Throws:
-    * If no localization exists for this name and the current locale (or any locale to be chain-
-    * loaded), or an error occurs while loading the database, a L10nLoadException will be thrown.
-    */
-    static I18nTranslation load (char[] name)
-    {
-        bool[ID] loadedSecs;        // set of all locales/sections loaded; used to prevent circular loading
-        ID[] secsToLoad             // locales/sections to load (dependancies may be added)
-        = [cast(ID) miscOpts.L10n];  // start by loading the current locale
-        
-        I18nTranslation transl = new I18nTranslation (name, miscOpts.L10n);
-        
-        IReader reader;
-        try {
-            reader = dataDir.makeMTReader ("L10n/"~name, PRIORITY.HIGH_LOW);
-            /* Note: we don't want to load every translation section depended on to its own class
-            * instance, since we want to merge them. So make every mergetag section use the same
-            * instance. */
-            reader.dataSecCreator = delegate IDataSection(ID) {
-                return transl;
-            };
-        
-            while (secsToLoad.length) {                 // while we have sections left to load
-                ID sec = secsToLoad[0];                 // take current section
-                secsToLoad = secsToLoad[1..$];          // and remove from list
-                                
-                if (sec in loadedSecs) continue;        // skip if it's already been loaded
-                loadedSecs[sec] = true;
-                
-                reader.read ([sec]);                    // Do the actual loading
-                
-                // Add dependancies to front of list:
-                secsToLoad = transl.depends ~ secsToLoad;
-            }
-            // When loop finishes, we're done loading.
-        } catch (MTException e) {
-            // If an error occurs, log a message but continue (strings will just be untranslated)
-            logger.error ("Mergetag exception occurred:");
-            logger.error (e.msg);
-        }
-        
-        delete transl.depends;      // Free memory
-        transl.depends = [];
-        
-        return transl;
-    }
-    
-    static this() {
-        logger = Log.getLogger ("mde.input.i18n.I18nTranslation");
-    }
-    
-    /* Mergetag functionality.
-    *
-    * Merge tags in to entries, prefering existing values.
-    * Replace depends.
-    *
-    * User-defined type "entry":
-    *   first two element is string and must exist
-    *   second element is description and is optional
-    *   third element is version and is optional
-    *   no limit on number of elements to allow future extensions
-    */
-    void addTag (char[] tp, ID id, char[] dt) {
-        if (tp == "entry") {
-            char[][] fields = split (stripBrackets (dt));
-            
-            if (fields.length < 1) {
-                // This tag is invalid, but this fact doesn't need to be reported elsewhere:
-                logger.error ("For name "~name~", L10n "~L10n~": tag with ID "~cast(char[])id~" has no data");
-                return;
-            }
-            // If the tag already exists, don't replace it
-            if (cast(char[]) id in entries) return;
-            
-            Entry entry;
-            entry.str = parseTo!(char[]) (fields[0]);
-            
-            if (fields.length >= 2)
-                entry.desc = parseTo!(char[]) (fields[1]);
-            
-            entries[cast(char[]) id] = entry;
-        } else if (tp == "char[][]") {
-            if (id == cast(ID)"depends") depends = cast(ID[]) parseTo!(char[][]) (dt);
-        }
-    }
-    
-    // This class is read-only and has no need of being saved.
-    void writeAll (ItemDelg) {}
-    
-private:
-    /* Sets name and L10n.
-    *
-    * Also ensures only load() can create instances. */
-    this (char[] n, char[] l) {
-        name = n;
-        L10n = l;
-    }
-    
-    //BEGIN Data
-    static Logger logger;
-    
-    /* This struct is used to store each translation entry.
-    *
-    * Note that although each entry also has a version field, this is not loaded for general use.
-    */
-    struct Entry {
-        char[] str;         // The translated string
-        char[] desc;        // An optional description
-    }
-    
-    Entry[char[]] entries;  // all entries
-    
-    ID[] depends;           // dependancy sections (only used while loading)
-    //END Data
-    
-    debug (mdeUnitTest) unittest {
-        /* Relies on file: conf/L10n/i18nUnitTest.mtt
-        * Contents:
-        *********
-        {MT01}
-        {test-1}
-        <entry|Str1=["Test 1"]>
-        <char[][]|depends=["test-2"]>
-        {test-2}
-        <entry|Str1=["Test 2"]>
-        <entry|Str2=["Test 3","Description",bogus,"entries",56]>
-        *********/
-        
-        // Hack a specific locale...
-        // Also to allow unittest to run without init.
-        char[] currentL10n = miscOpts.L10n;
-        miscOpts.L10n = "test-1";
-        
-        I18nTranslation transl = load ("i18nUnitTest");
-        
-        // Simple get-string, check dependancy's entry doesn't override
-        assert (transl.getEntry ("Str1") == "Test 1");
-        
-        // Entry included from dependancy with description
-        char[] desc;
-        assert (transl.getEntry ("Str2", desc) == "Test 3");
-        assert (desc == "Description");
-        
-        // No entry: fallback to identifier string
-        assert (transl.getEntry ("Str3") == "Str3");
-        
-        // No checks for version info since it's not functionality of this module.
-        // Only check extra entries are allowed but ignored.
-        
-        // Restore
-        miscOpts.L10n = currentL10n;
-        
-        logger.info ("Unittest complete.");
-    }
-}
--- a/mde/input/Config.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/input/Config.d	Fri Jun 27 18:35:33 2008 +0100
@@ -21,7 +21,7 @@
 import mde.input.exception;
 
 import MT = mde.mergetag.Reader;
-import mde.resource.paths;
+import mde.setup.paths;
 import tango.scrapple.text.convert.parseTo : parseTo;
 
 import tango.util.log.Log : Log, Logger;
--- a/mde/input/Input.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/input/Input.d	Fri Jun 27 18:35:33 2008 +0100
@@ -366,7 +366,7 @@
         es_a_fcts = [ ES_A.OUT : &es_a_out, ES_A.REVERSE : &es_a_reverse ];
         es_m_fcts = [ ES_M.OUT : &es_m_out ];
         
-        logger = Log.getLogger ("mde.input.input.Input");
+        logger = Log.getLogger ("mde.input.Input");
     }
     
     struct RelPair {	// for mouse/joystick ball motion
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/lookup/Options.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,486 @@
+/* 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/>. */
+
+/** This module handles stored options, currently all except input maps.
+*
+* The purpose of having all options centrally controlled is to allow generic handling by the GUI
+* and ease saving and loading of values. The Options class is only really designed around handling
+* small numbers of variables for now.
+*/
+module mde.lookup.Options;
+
+import mde.exception;
+
+import mde.mergetag.Reader;
+import mde.mergetag.Writer;
+import mde.mergetag.DataSet;
+import mde.mergetag.exception;
+import mde.setup.paths;
+
+import tango.scrapple.text.convert.parseTo : parseTo;
+import tango.scrapple.text.convert.parseFrom : parseFrom;
+
+import tango.core.Exception : ArrayBoundsException;
+import tango.util.log.Log : Log, Logger;
+
+/** Base class for handling options.
+*
+* This class itself handles no options and should not be instantiated, but provides a sub-classable
+* base for generic options handling. Also, the static portion of this class tracks sub-class
+* instances and provides loading and saving methods.
+*
+* Each sub-class provides named variables for maximal-speed reading. Local sub-class references
+* should be used for reading variables, and via the addOptionsClass() hook will be loaded from
+* files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will
+* not be saved; use, for example, Options.setBool(...). Use an example like OptionsMisc as a
+* template for creating a new Options sub-class.
+*
+* Details: Options sub-classes hold associative arrays of pointers to all option variables, with a
+* char[] id. This list is used for saving, loading and to provide generic GUI options screens. The
+* built-in support in Options is only for bool, int and char[] types (a float type may get added).
+* Further to this, a generic class is used to store all options which have been changed, and if any
+* have been changed, is merged with options from the user conf dir and saved on exit.
+*/
+class Options : IDataSection
+{
+    // No actual options are stored by this class. However, much of the infrastructure is
+    // present since it need not be redefined in sub-classes.
+    
+    // The "pointer lists":
+    protected bool*  [ID]   optsBool;
+    protected int*   [ID]   optsInt;
+    protected double*[ID]   optsDouble;
+    protected char[]*[ID]   optsCharA;
+    
+    //BEGIN Mergetag loading/saving code
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "bool") {
+            bool** p = id in optsBool;
+            if (p !is null) **p = parseTo!(bool) (dt);
+        } else if (tp == "char[]") {
+            char[]** p = id in optsCharA;
+            if (p !is null) **p = parseTo!(char[]) (dt);
+        } else if (tp == "double") {
+            double** p = id in optsDouble;
+            if (p !is null) **p = parseTo!(double) (dt);
+        } else if (tp == "int") {
+            int** p = id in optsInt;
+            if (p !is null) **p = parseTo!(int) (dt);
+        }
+    }
+
+    void writeAll (ItemDelg dlg) {
+        foreach (ID id, bool*   val; optsBool)  dlg ("bool"  , id, parseFrom!(bool  ) (*val));
+        foreach (ID id, char[]* val; optsCharA) dlg ("char[]", id, parseFrom!(char[]) (*val));
+        foreach (ID id, double* val; optsDouble)dlg ("double", id, parseFrom!(double) (*val));
+        foreach (ID id, int*    val; optsInt)   dlg ("int"   , id, parseFrom!(int   ) (*val));
+    }
+    //END Mergetag loading/saving code
+    
+    //BEGIN Static
+    /** Add an options sub-class to the list for loading and saving.
+    *
+    * Call from static this() (before Init calls load()). */
+    static void addOptionsClass (Options c, char[] i)
+    in {    // Trap a couple of potential coding errors:
+        assert (c !is null);    // Instance must be created before calling addOptionsClass
+        assert (((cast(ID) i) in subClasses) is null);  // Don't allow a silent replacement
+    } body {
+        subClasses[cast(ID) i] = c;
+        subClassChanges[cast(ID) i] = new OptionsGeneric;
+    }
+    
+    /** Set option symbol of Options class subClass to val.
+    *
+    * Due to the way options are handled generically, string IDs must be used to access the options
+    * via hash-maps, which is a little slower than direct access but necessary since the option
+    * must be changed in two separate places. */
+    private static const ERR_MSG = "Options.setXXX called with incorrect parameters!";
+    static void setBool (char[] subClass, char[] symbol, bool val) {
+        changed = true;     // something got set (don't bother checking this isn't what it already was)
+        
+        try {
+            *(subClasses[cast(ID) subClass].optsBool[cast(ID) symbol]) = val;
+            subClassChanges[cast(ID) subClass].setBool (cast(ID) symbol, val);
+        } catch (ArrayBoundsException) {
+            // log and ignore:
+            logger.error (ERR_MSG);
+        }
+    }
+    static void setInt (char[] subClass, char[] symbol, int val) {
+        changed = true;     // something got set (don't bother checking this isn't what it already was)
+        
+        try {
+            *(subClasses[cast(ID) subClass].optsInt[cast(ID) symbol]) = val;
+            subClassChanges[cast(ID) subClass].setInt (cast(ID) symbol, val);
+        } catch (ArrayBoundsException) {
+            // log and ignore:
+            logger.error (ERR_MSG);
+        }
+    }
+    static void setDouble (char[] subClass, char[] symbol, double val) {
+        changed = true;     // something got set (don't bother checking this isn't what it already was)
+        
+        try {
+            *(subClasses[cast(ID) subClass].optsDouble[cast(ID) symbol]) = val;
+            subClassChanges[cast(ID) subClass].setDouble (cast(ID) symbol, val);
+        } catch (ArrayBoundsException) {
+            // log and ignore:
+            logger.error (ERR_MSG);
+        }
+    }
+    static void setCharA (char[] subClass, char[] symbol, char[] val) {
+        changed = true;     // something got set (don't bother checking this isn't what it already was)
+        
+        try {
+            *(subClasses[cast(ID) subClass].optsCharA[cast(ID) symbol]) = val;
+            subClassChanges[cast(ID) subClass].setCharA (cast(ID) symbol, val);
+        } catch (ArrayBoundsException) {
+            // log and ignore:
+            logger.error (ERR_MSG);
+        }
+    }
+    
+    // Track all sections for saving/loading/other generic handling.
+    static Options[ID] subClasses;
+    static OptionsGeneric[ID] subClassChanges;
+    static bool changed = false;    // any changes at all, i.e. do we need to save?
+    
+    /* Load/save options from file.
+    *
+    * If the file doesn't exist, no reading is attempted (options are left at default values).
+    */
+    private static const fileName = "options";
+    private static const MT_LOAD_EXC = "Loading options aborted:";
+    static void load () {
+        // Check it exists (if not it should still be created on exit).
+        // Don't bother checking it's not a folder, because it could still be a block or something.
+        if (!confDir.exists (fileName)) return;
+        
+        try {
+            IReader reader;
+            reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH);
+            reader.dataSecCreator = delegate IDataSection(ID id) {
+                /* Recognise each defined section, and return null for unrecognised sections. */
+                Options* p = id in subClasses;
+                if (p !is null) return *p;
+                else return null;
+            };
+            reader.read;
+        } catch (MTException e) {
+            logger.fatal (MT_LOAD_EXC);
+            logger.fatal (e.msg);
+            throw new optionsLoadException ("Mergetag exception (see above message)");
+        }
+    }
+    static void save () {
+        if (!changed) return;   // no changes to save
+        
+        DataSet ds = new DataSet();
+        foreach (id, sec; subClassChanges) ds.sec[id] = sec;
+        
+        // Read locally-stored options
+        try {
+            IReader reader;
+            reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds);
+            reader.dataSecCreator = delegate IDataSection(ID id) {
+                return null;    // All recognised sections are already in the dataset.
+            };
+            reader.read;
+        } catch (MTFileIOException) {
+            // File either didn't exist or couldn't be opened.
+            // Presuming the former, this is not a problem.
+        } catch (MTException e) {
+            // Log a message and continue, overwriting the file:
+            logger.error (MT_LOAD_EXC);
+            logger.error (e.msg);
+        }
+        
+        try {
+            IWriter writer;
+            writer = confDir.makeMTWriter (fileName, ds);
+            writer.write();
+        } catch (MTException e) {
+            logger.error ("Saving options aborted! Reason:");
+            logger.error (e.msg);
+        }
+    }
+    
+    private static Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.options");
+    }
+    //END Static
+    
+    //BEGIN Templates
+    private {
+        // Return index of first comma, or halts if not found.
+        template cIndex(char[] A) {
+            static if (A.length == 0)
+                static assert (false, "Error in implementation");
+            else static if (A[0] == ',')
+                const size_t cIndex = 0;
+            else
+                const size_t cIndex = 1 + cIndex!(A[1..$]);
+        }
+        // Return index of first semi-colon, or halts if not found.
+        template scIndex(char[] A) {
+            static if (A.length == 0)
+                static assert (false, "Error: no trailing semi-colon");
+            else static if (A[0] == ';')
+                const size_t scIndex = 0;
+            else
+                const size_t scIndex = 1 + scIndex!(A[1..$]);
+        }
+        // Look for "type symbols;" in A and return symbols as a comma separated list of names
+        // (even if type is encountered more than once). Output may contain spaces and, if
+        // non-empty, will contain a trailing comma. Assumes scIndex always returns less than A.$.
+        template parseT(char[] type, char[] A) {
+            static if (A.length < type.length + 1)	// end of input, no match
+                const char[] parseT = "";
+            else static if (A[0] == ' ')		// leading whitespace: skip
+                const char[] parseT = parseT!(type, A[1..$]);
+            else static if (A[0..type.length] == type && A[type.length] == ' ')	// match
+                const char[] parseT = A[type.length+1 .. scIndex!(A)] ~ "," ~
+                        parseT!(type, A[scIndex!(A)+1 .. $]);
+            else					// no match
+                const char[] parseT = parseT!(type, A[scIndex!(A)+1 .. $]);
+        }
+        // May have a trailing comma. Assumes cIndex always returns less than A.$.
+        template aaVars(char[] A) {
+            static if (A.length == 0)
+                const char[] aaVars = "";
+            else static if (A[0] == ' ')
+                const char[] aaVars = aaVars!(A[1..$]);
+            else
+                const char[] aaVars = "\""~A[0..cIndex!(A)]~"\"[]:&"~A[0..cIndex!(A)] ~ "," ~
+                        aaVars!(A[cIndex!(A)+1..$]);
+        }
+        // strip Trailing Comma
+        template sTC(char[] A) {
+            static if (A.length && A[$-1] == ',')
+                const char[] sTC = A[0..$-1];
+            else
+                const char[] sTC = A;
+        }
+        // if string is empty (other than space) return null, otherwise enclose: [A]
+        template listOrNull(char[] A) {
+            static if (A.length == 0)
+                const char[] listOrNull = "null";
+            else static if (A[0] == ' ')
+                const char[] listOrNull = listOrNull!(A[1..$]);
+            else
+                const char[] listOrNull = "["~A~"]";
+        }
+    } protected {
+        /** Produces the implementation code to go in the constuctor. */
+        template aaDefs(char[] A) {
+            const char[] aaDefs =
+                    "optsBool = "  ~ listOrNull!(sTC!(aaVars!(parseT!("bool"  , A)))) ~ ";\n" ~
+                    "optsInt = "   ~ listOrNull!(sTC!(aaVars!(parseT!("int"   , A)))) ~ ";\n" ~
+                    "optsDouble = "~ listOrNull!(sTC!(aaVars!(parseT!("double", A)))) ~ ";\n" ~
+                    "optsCharA = " ~ listOrNull!(sTC!(aaVars!(parseT!("char[]", A)))) ~ ";\n";
+        }
+        /+/** Produces the implementation code to go in the static constuctor. */
+        template optClassAdd(char[] symb) {
+            const char[] optClassAdd = symb ~ "=new "~classinfo(this).name~";\n";//Options.addOptionsClass("~symb~", );\n";
+        }+/
+        /** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
+         *
+         * Where type is one of bool, int, double, char[]. E.g.
+         * ---
+         * mixin (impl ("bool a, b; int i;"));
+         * ---
+         *
+         * In case this() needs to be customized, mixin(impl!(A)) is equivalent to:
+         * ---
+         * mixin (A~"\nthis(){\n"~aaDefs!(A)~"}");
+         * ---
+         *
+         * Notes: Only use space as whitespace (no new-lines or tabs). Make sure to add a trailing
+         * semi-colon (;) or you'll get told off! :D
+         *
+         * In general errors aren't reported well. Trial with pragma (msg, impl!(...)); if
+         * necessary.
+         *
+         * Extending: mixins could also be used for the static this() {...} or even the whole
+         * class, but doing so would rather decrease readability of any implementation. */
+        template impl(char[] A /+, char[] symb+/) {
+            const char[] impl = A~"\nthis(){\n"~aaDefs!(A)~"}";
+            // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}"
+        }
+    }
+    /+/** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
+     *
+     * E.g.
+     * ---
+     * mixin (impl ("bool a, b; int i;"));
+     * ---
+     * The parser isn't highly accurate. */
+    // Try using templates instead? See std.metastrings
+    static char[] impl (char[] A) {
+        char[] bools;
+        char[] ints;
+        
+        while (A.length) {
+            // Trim whitespace
+            {
+                size_t i = 0;
+                while (i < A.length && (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)))
+                    ++i;
+                A = A[i..$];
+            }
+            if (A.length == 0) break;
+            
+            char[] type;
+            for (size_t i = 0; i < A.length; ++i) {
+                if (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)) {
+                    type = A[0..i];
+                    A = A[i+1..$];
+                    break;
+                }
+            }
+            
+            char[] symbols;
+            for (size_t i = 0; i < A.length; ++i) {
+                if (A[i] == ';') {
+                    symbols = A[0..i];
+                    A = A[i+1..$];
+                    break;
+                }
+            }
+            
+            if (type == "bool") {
+                if (bools.length)
+                    bools = bools ~ "," ~ symbols;
+                else
+                    bools = symbols;
+            }
+            else if (type == "int") {
+                if (ints.length)
+                    ints = ints ~ "," ~ symbols;
+                else
+                    ints = symbols;
+            }
+            else {
+                // Unfortunately, we cannot output non-const strings (even though func is compile-time)
+                // We also cannot use pragma(msg) because the message gets printed even if the code isn't run.
+                //pragma(msg, "Warning: impl failed to parse whole input string");
+                // Cannot use Cout / logger either.
+                break;
+            }
+        }
+        
+        char[] ret;
+        if (bools.length)
+            ret = "bool "~bools~";\n";
+        if (ints.length)
+            ret = ret ~ "int "~ints~";\n";
+        
+        
+        
+        return ret;
+    }+/
+    //END Templates
+}
+
+/* Special class to store all locally changed options, whatever the section. */
+class OptionsGeneric : Options {
+    // These store the actual values, but are never accessed directly except when initially added.
+    // optsX store pointers to each item added along with the ID and are used for access.
+    bool[] bools;
+    int[] ints;
+    double[] doubles;
+    char[][] strings;
+    
+    this () {}
+    
+    void setBool (ID id, bool x) {
+        bool** p = id in optsBool;
+        if (p !is null) **p = x;
+        else {
+            bools ~= x;
+            optsBool[id] = &bools[$-1];
+        }
+    }
+    void setInt (ID id, int x) {
+        int** p = id in optsInt;
+        if (p !is null) **p = x;
+        else {
+            ints ~= x;
+            optsInt[id] = &ints[$-1];
+        }
+    }
+    void setDouble (ID id, double x) {
+        double** p = id in optsDouble;
+        if (p !is null) **p = x;
+        else {
+            doubles ~= x;
+            optsDouble[id] = &doubles[$-1];
+        }
+    }
+    void setCharA (ID id, char[] x) {
+        char[]** p = id in optsCharA;
+        if (p !is null) **p = x;
+        else {
+            strings ~= x;
+            optsCharA[id] = &strings[$-1];
+        }
+    }
+    
+    //BEGIN Mergetag loading/saving code
+    // Reverse priority: only load symbols not currently existing
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "bool") {
+            if ((id in optsBool) is null) {
+                bools ~= parseTo!(bool) (dt);
+                optsBool[id] = &bools[$-1];
+            }
+        } else if (tp == "char[]") {
+            if ((id in optsCharA) is null) {
+                strings ~= parseTo!(char[]) (dt);
+                optsCharA[id] = &strings[$-1];
+            }
+            char[]** p = id in optsCharA;
+            if (p !is null) **p = parseTo!(char[]) (dt);
+        } else if (tp == "int") {
+            if ((id in optsInt) is null) {
+                ints ~= parseTo!(int) (dt);
+                optsInt[id] = &ints[$-1];
+            }
+        }
+    }
+    //END Mergetag loading/saving code
+}
+
+/* NOTE: Options sub-classes are expected to use a template to ease inserting contents and
+* hide some of the "backend" functionality. Use impl as below, or read the documentation for impl.
+*
+* Each entry should have a Translation entry with humanized names and descriptions in
+* data/L10n/ClassName.mtt
+*
+* To create a new Options sub-class, just copy, paste and adjust.
+*/
+
+/** A home for all miscellaneous options, at least for now. */
+OptionsMisc miscOpts;
+class OptionsMisc : Options {
+    mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;"));
+    
+    static this() {
+        miscOpts = new OptionsMisc;
+        Options.addOptionsClass (miscOpts, "misc");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/lookup/Translation.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,242 @@
+/* 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/>. */
+/** Translation − internationalization module for translating strings
+*
+* The idea behind this module is a class which, when asked to load symbols for a particular module/
+* package/part of the program, will load internationalized names and optional descriptions for each
+* symbol needing translation. No support for non-left-to-right scripts is currently planned, and
+* this module is currently limited to translations, although support for different date formats,
+* etc. could potentially be added later.
+*
+* Code symbols are used as identifiers for each name and its optional description. The code symbol
+* will be used as a fallback in the case no entry exists, however it is not intended to provide the
+* string for the default language (a "translation" should be used for the default language).
+*
+* Each locale may specify dependant locales/sections which will be loaded and merged in to the
+* database, to cover for symbols with a missing entry. Sections are loaded in the order specified,
+* with each section's sub-dependancies loaded before continuing with the next top-level dependancy.
+* A list of loaded sections is used to prevent any locale/section from being loaded twice, and thus
+* allow circular dependancies.
+*
+* In order that translated strings get updated correctly to reflect changes, each entry carries a
+* version number. If, for any entry, a translation exists with a higher version number, that entry
+* is out of date. A tool should be made for checking for out of date entries to take advantage of
+* this feature. Of course, out of date entries are still valid for use.
+*/
+module mde.lookup.Translation;
+
+import mde.options;
+import mde.exception;
+
+import mde.mergetag.DataSet;
+import mde.mergetag.Reader;
+import mde.mergetag.exception;
+import mde.resource.paths;
+
+import tango.util.log.Log : Log, Logger;
+import tango.scrapple.text.convert.parseTo;
+
+/** The translation class
+*
+* See module description for details.
+*
+* Encoding used is UTF-8.
+*/
+class Translation : IDataSection
+{
+    final char[] name;      /// The module/package/... which the instance is for
+    final char[] L10n;      /// The localization loaded (e.g. en-GB)
+    
+    /** Get the translation for the given identifier, and optionally the description.
+    * If no entry exists, the identifier will be returned. */
+    char[] getEntry (char[] id, out char[] description) {
+        Entry* p = id in entries;
+        if (p) {    // FIXME: check: a SEGFAULT?
+            description = p.desc;
+            return p.str;
+        } else
+            return id;
+    }
+        /** ditto */
+    char[] getEntry (char[] id) {
+        Entry* p = id in entries;
+        if (p !is null) {    // FIXME: check: a SEGFAULT?
+            return p.str;
+        } else
+            return id;
+    }
+    
+    /** Load the translation for the requested module/package/...
+    *
+    * Options (mde.options) must have been loaded before this is run.
+    *
+    * Params:
+    *   name The module/package/... to load strings for.
+    *
+    * Throws:
+    * If no localization exists for this name and the current locale (or any locale to be chain-
+    * loaded), or an error occurs while loading the database, a L10nLoadException will be thrown.
+    */
+    static Translation load (char[] name)
+    {
+        bool[ID] loadedSecs;        // set of all locales/sections loaded; used to prevent circular loading
+        ID[] secsToLoad             // locales/sections to load (dependancies may be added)
+        = [cast(ID) miscOpts.L10n];  // start by loading the current locale
+        
+        Translation transl = new Translation (name, miscOpts.L10n);
+        
+        IReader reader;
+        try {
+            reader = dataDir.makeMTReader ("L10n/"~name, PRIORITY.HIGH_LOW);
+            /* Note: we don't want to load every translation section depended on to its own class
+            * instance, since we want to merge them. So make every mergetag section use the same
+            * instance. */
+            reader.dataSecCreator = delegate IDataSection(ID) {
+                return transl;
+            };
+        
+            while (secsToLoad.length) {                 // while we have sections left to load
+                ID sec = secsToLoad[0];                 // take current section
+                secsToLoad = secsToLoad[1..$];          // and remove from list
+                                
+                if (sec in loadedSecs) continue;        // skip if it's already been loaded
+                loadedSecs[sec] = true;
+                
+                reader.read ([sec]);                    // Do the actual loading
+                
+                // Add dependancies to front of list:
+                secsToLoad = transl.depends ~ secsToLoad;
+            }
+            // When loop finishes, we're done loading.
+        } catch (MTException e) {
+            // If an error occurs, log a message but continue (strings will just be untranslated)
+            logger.error ("Mergetag exception occurred:");
+            logger.error (e.msg);
+        }
+        
+        delete transl.depends;      // Free memory
+        transl.depends = [];
+        
+        return transl;
+    }
+    
+    static this() {
+        logger = Log.getLogger ("mde.lookup.Translation");
+    }
+    
+    /* Mergetag functionality.
+    *
+    * Merge tags in to entries, prefering existing values.
+    * Replace depends.
+    *
+    * User-defined type "entry":
+    *   first two element is string and must exist
+    *   second element is description and is optional
+    *   third element is version and is optional
+    *   no limit on number of elements to allow future extensions
+    */
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "entry") {
+            char[][] fields = split (stripBrackets (dt));
+            
+            if (fields.length < 1) {
+                // This tag is invalid, but this fact doesn't need to be reported elsewhere:
+                logger.error ("For name "~name~", L10n "~L10n~": tag with ID "~cast(char[])id~" has no data");
+                return;
+            }
+            // If the tag already exists, don't replace it
+            if (cast(char[]) id in entries) return;
+            
+            Entry entry;
+            entry.str = parseTo!(char[]) (fields[0]);
+            
+            if (fields.length >= 2)
+                entry.desc = parseTo!(char[]) (fields[1]);
+            
+            entries[cast(char[]) id] = entry;
+        } else if (tp == "char[][]") {
+            if (id == cast(ID)"depends") depends = cast(ID[]) parseTo!(char[][]) (dt);
+        }
+    }
+    
+    // This class is read-only and has no need of being saved.
+    void writeAll (ItemDelg) {}
+    
+private:
+    /* Sets name and L10n.
+    *
+    * Also ensures only load() can create instances. */
+    this (char[] n, char[] l) {
+        name = n;
+        L10n = l;
+    }
+    
+    //BEGIN Data
+    static Logger logger;
+    
+    /* This struct is used to store each translation entry.
+    *
+    * Note that although each entry also has a version field, this is not loaded for general use.
+    */
+    struct Entry {
+        char[] str;         // The translated string
+        char[] desc;        // An optional description
+    }
+    
+    Entry[char[]] entries;  // all entries
+    
+    ID[] depends;           // dependancy sections (only used while loading)
+    //END Data
+    
+    debug (mdeUnitTest) unittest {
+        /* Relies on file: conf/L10n/i18nUnitTest.mtt
+        * Contents:
+        *********
+        {MT01}
+        {test-1}
+        <entry|Str1=["Test 1"]>
+        <char[][]|depends=["test-2"]>
+        {test-2}
+        <entry|Str1=["Test 2"]>
+        <entry|Str2=["Test 3","Description",bogus,"entries",56]>
+        *********/
+        
+        // Hack a specific locale...
+        // Also to allow unittest to run without init.
+        char[] currentL10n = miscOpts.L10n;
+        miscOpts.L10n = "test-1";
+        
+        Translation transl = load ("i18nUnitTest");
+        
+        // Simple get-string, check dependancy's entry doesn't override
+        assert (transl.getEntry ("Str1") == "Test 1");
+        
+        // Entry included from dependancy with description
+        char[] desc;
+        assert (transl.getEntry ("Str2", desc) == "Test 3");
+        assert (desc == "Description");
+        
+        // No entry: fallback to identifier string
+        assert (transl.getEntry ("Str3") == "Str3");
+        
+        // No checks for version info since it's not functionality of this module.
+        // Only check extra entries are allowed but ignored.
+        
+        // Restore
+        miscOpts.L10n = currentL10n;
+        
+        logger.info ("Unittest complete.");
+    }
+}
--- a/mde/mde.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/mde.d	Fri Jun 27 18:35:33 2008 +0100
@@ -23,14 +23,14 @@
 
 import mde.imde;                        // this module's interface for external modules
 import mde.events;                      // pollEvents
-import mde.Options;                     // pollInterval option
+import mde.lookup.Options;              // pollInterval option
 
 import gl = mde.gl.draw;                // gl.draw()
 import mde.input.Input;                 // new Input()
 
-import mde.scheduler.Init;
+import mde.setup.Init;
 import mde.scheduler.Scheduler;         // Scheduler.run()
-import mde.scheduler.exception;         // InitException
+import mde.setup.exception;             // InitException
 
 import tango.core.Thread : Thread;	// Thread.sleep()
 import tango.time.Clock;                // Clock.now()
--- a/mde/resource/FontTexture.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,495 +0,0 @@
-/* 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/>. */
-
-/** Font caching system.
- *
- * This module also serves as the internals to the font module and shouldn't be used except through
- * the font module. The two modules could be combined, at a cost to readability.
- *
- * Three types of coordinates get used in the system: FreeType coordinates for each glyph, texture
- * coordinates, and OpenGL's model/world coordinates (for rendering). The freetype and texture
- * coords are cartesian (i.e. y increases upwards), although largely this is too abstract to
- * matter. However, for the model/world coords, y increases downwards. */
-module mde.resource.FontTexture;
-
-import mde.types.basic;	// Colour
-import mde.Options;
-import mde.resource.exception;
-
-import derelict.freetype.ft;
-import derelict.opengl.gl;
-
-import Utf = tango.text.convert.Utf;
-import tango.util.log.Log : Log, Logger;
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.resource.FontTexture");
-}
-
-static const int dimW = 256, dimH = 256;	// Texture size
-const wFactor = 1f / dimW;
-const hFactor = 1f / dimH;
-
-/** A FontTexture is basically a cache of all font glyphs rendered so far.
-  *
-  * This class should be limited to code for rendering to (and otherwise handling) textures and
-  * rendering fonts to the screen.
-  *
-  * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */
-class FontTexture
-{
-    this () {}
-    ~this () {
-        foreach (t; tex) {
-            glDeleteTextures (1, &(t.texID));
-        }
-    }
-    
-    // Call if font(s) have been changed and glyphs must be recached.
-    void clear () {
-        foreach (t; tex) {
-            glDeleteTextures (1, &(t.texID));
-        }
-        cachedGlyphs = null;
-        ++cacheVer;
-    }
-    
-    
-    /** Cache informatation for rendering a block of text.
-     *
-     * Recognises '\r', '\n' and "\r\n" as end-of-line markers. */
-    void updateCache (FT_Face face, char[] str, ref TextBlock cache)
-    {
-        debug scope (failure)
-                logger.error ("updateCache failed");
-        
-        if (cache.cacheVer == cacheVer)	// Existing cache is up-to-date
-            return;
-        
-        cache.cacheVer = cacheVer;
-        
-        /* Convert the string to an array of character codes (which is equivalent to decoding UTF8
-        * to UTF32 since no character code is ever > dchar.max). */
-        static dchar[] chrs;	// keep memory for future calls (NOTE: change for threading)
-        chrs = Utf.toString32 (str, chrs);
-        
-        // Allocate space.
-        // Since end-of-line chars get excluded, will often be slightly larger than necessary.
-        cache.chars.length = chrs.length;
-        cache.chars.length = 0;
-        
-        int lineSep = face.size.metrics.height >> 6;
-        bool hasKerning = (FT_HAS_KERNING (face) != 0);
-        int y = 0;
-        CharCache cc;		// struct; reused for each character
-        
-        for (size_t i = 0; i < chrs.length; ++i)
-        {
-            // First, find yMax for the current line.
-            int yMax = 0;	// Maximal glyph height above baseline.
-            for (size_t j = i; j < chrs.length; ++j)
-            {
-                if (chrs[j] == '\n' || chrs[j] == '\r')	// end of line
-                    break;
-                
-                GlyphAttribs* ga = chrs[j] in cachedGlyphs;
-                if (ga is null) {			// Not cached
-                    addGlyph (face, chrs[j]);		// so render it
-                    ga = chrs[j] in cachedGlyphs;	// get the ref of the copy we've stored
-                    assert (ga !is null, "ga is null: 1");
-                }
-                
-                if (ga.top > yMax)
-                    yMax = ga.top;
-            }
-            y += yMax;
-            
-            // Now for the current line:
-            int x = 0;	// x pos for next glyph
-            uint gi_prev = 0;	// previous glyph index (needed for kerning)
-            for (; i < chrs.length; ++i)
-            {
-                // If end-of-line, break to find yMax for next line.
-                if (chrs.length >= i+2 && chrs[i..i+2] == "\r\n"d) {
-                    ++i;
-                    break;
-                }
-                if (chrs[i] == '\n' || chrs[i] == '\r') {
-                    break;
-                }
-                
-                cc.ga = chrs[i] in cachedGlyphs;
-                assert (cc.ga !is null, "ga is null: 2");
-                
-                // Kerning
-                if (hasKerning && (gi_prev != 0)) {
-                    FT_Vector delta;
-                    FT_Get_Kerning (face, gi_prev, cc.ga.index, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta);
-                    x += delta.x >> 6;
-                }
-                
-                // ga.left component: adding this slightly improves glyph layout. Note that the
-                // left-most glyph on a line may not start right on the edge, but this looks best.
-                cc.xPos = x + cc.ga.left;
-                cc.yPos = y - cc.ga.top;
-                x += cc.ga.advanceX;
-                
-                cache.chars ~= cc;
-                
-                // Update rect total size. Top and left coords should be zero, so make width and
-                // height maximal x and y coordinates.
-                if (cc.xPos + cc.ga.w > cache.w)
-                    cache.w = cc.xPos + cc.ga.w;
-                if (cc.yPos + cc.ga.h > cache.h)
-                    cache.h = cc.yPos + cc.ga.h;
-            }
-            // Now increment i and continue with the next line if there is one.
-            y += lineSep - yMax;
-        }
-    }
-    
-    /** Render a block of text using a cache. Updates the cache if necessary.
-     *
-     * Params:
-     *  face =	Current typeface pointer; must be passed from font.d (only needed if the cache is
-     *  	invalid)
-     *  str =	Text to render (only needed if the cache is invalid)
-     *  cache =	Cache used to speed up CPU-side rendering code
-     *  x =	Smaller x-coordinate of position
-     *  y =	Smaller y-coordinate of position
-     *  col =	Text colour (note: currently limited to black or white) */
-    void drawCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col ) {
-        updateCache (face, str, cache);	// update if necessary
-        debug scope (failure)
-                logger.error ("drawTextCache failed");
-        
-        // opaque (GL_DECAL would be equivalent)
-        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
-        
-        drawCacheImpl (cache, x, y, col);
-    }
-    /** A variation of drawCache, for transparent text.
-     *
-     * Instead of passing the alpha value(s) as arguments, set the openGL colour prior to calling:
-     * ---
-     * glColor3f (.5f, .5f, .5f);	// set alpha to half
-     * drawCacheA (face, ...);
-     * 
-     * glColor3ub (0, 255, 127);	// alpha 0 for red, 1 for green, half for blue
-     * drawCacheA (face, ...);
-     * ---
-     *
-     * The overhead of the transparency is minimal. */
-    void drawCacheA (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col/+ = Colour.WHITE+/) {
-        updateCache (face, str, cache);	// update if necessary
-        debug scope (failure)
-                logger.error ("drawTextCache failed");
-        
-        // transparency alpha
-        // alpha is current colour, per component
-        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-        
-        drawCacheImpl (cache, x, y, col);
-    }
-   
-    private void drawCacheImpl (ref TextBlock cache, int x, int y, Colour col) {
-        if (DerelictGL.availableVersion() >= GLVersion.Version14) {
-            glBlendFunc (GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR);
-            glBlendColor(col.r, col.g, col.b, 1f);	// text colour
-        } else
-            glBlendFunc (col.nearestGLConst, GL_ONE_MINUS_SRC_COLOR);
-        
-        glEnable (GL_TEXTURE_2D);
-        glEnable(GL_BLEND);
-        
-        foreach (chr; cache.chars) {
-            GlyphAttribs* ga = chr.ga;
-            
-            glBindTexture(GL_TEXTURE_2D, ga.texID);
-            
-            int x1 = x + chr.xPos;
-            int y1 = y + chr.yPos;
-            int x2 = x1 + ga.w;
-            int y2 = y1 + ga.h;
-            float tx1 = ga.x * wFactor;
-            float ty1 = ga.y * hFactor;
-            float tx2 = (ga.x + ga.w) * wFactor;
-            float ty2 = (ga.y + ga.h) * hFactor;
-           
-            glBegin (GL_QUADS);
-            glTexCoord2f (tx1, ty1);	glVertex2i (x1, y1);
-            glTexCoord2f (tx2, ty1);	glVertex2i (x2, y1);
-            glTexCoord2f (tx2, ty2);	glVertex2i (x2, y2);
-            glTexCoord2f (tx1, ty2);	glVertex2i (x1, y2);
-            glEnd ();
-        }
-        
-        glDisable(GL_BLEND);
-    }
-    
-    void addGlyph (FT_Face face, dchar chr) {
-        debug scope (failure)
-                logger.error ("FontTexture.addGlyph failed!");
-        
-        auto gi = FT_Get_Char_Index (face, chr);
-        auto g = face.glyph;
-        
-        // Use renderMode from options, masking bits which are allowable:
-        if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | (fontOpts.renderMode & 0xF0000)))
-            throw new fontGlyphException ("Unable to render glyph");
-        
-        auto b = g.bitmap;
-        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-        //glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch);
-        
-        GlyphAttribs ga;
-        ga.w		= b.width;
-        ga.h		= b.rows;
-        ga.left		= g.bitmap_left;
-        ga.top		= g.bitmap_top;
-        ga.advanceX	= g.advance.x >> 6;
-        ga.index	= gi;
-        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD)
-            ga.w /= 3;
-        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V)
-            ga.h /= 3;
-        
-        foreach (ref t; tex) {
-            if (t.addGlyph (ga))
-                goto gotTexSpace;
-        }
-        // if here, no existing texture had the room for the glyph so create a new texture
-        // NOTE: check if using more than one texture impacts performance due to texture switching
-        logger.info ("Creating a new font texture.");
-        tex ~= TexPacker.create();
-        assert (tex[$-1].addGlyph (ga), "Failed to fit glyph in a new texture but addGlyph didn't throw");
-        
-        gotTexSpace:
-        glBindTexture(GL_TEXTURE_2D, ga.texID);
-        GLenum format;
-        ubyte[] buffer;	// use our own pointer, since for LCD modes we need to perform a conversion
-        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256) {
-            assert (b.pitch == b.width, "Have assumed b.pitch == b.width for gray glyphs.");
-            buffer = b.buffer[0..b.pitch*b.rows];
-            format = GL_LUMINANCE;
-        } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) {
-            // NOTE: Can't seem to get OpenGL to read freetype's RGB buffers properly, so convent.
-            /* NOTE: Sub-pixel rendering probably also needs filtering. I haven't tried, since it's
-             * disabled in my build of the library. For a tutorial on the filtering, see:
-             * http://dmedia.dprogramming.com/?n=Tutorials.TextRendering1 */
-            buffer = new ubyte[b.width*b.rows];
-            for (uint i = 0; i < b.rows; ++i)
-                for (uint j = 0; j < b.width; j+= 3)
-            {
-                buffer[i*b.width + j + 0] = b.buffer[i*b.pitch + j + 0];
-                buffer[i*b.width + j + 1] = b.buffer[i*b.pitch + j + 1];
-                buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2];
-            }
-            
-            format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB;
-        } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) {
-            // NOTE: Notes above apply. Only in this case converting the buffers seems essential.
-            buffer = new ubyte[b.width*b.rows];
-            for (uint i = 0; i < b.rows; ++i)
-                for (uint j = 0; j < b.width; ++j)
-            {
-                // i/3 is the "real" row, b.width*3 is our width (with subpixels), j is column,
-                // i%3 is sub-pixel (R/G/B). i/3*3 necessary to round to multiple of 3
-                buffer[i/3*b.width*3 + 3*j + i%3] = b.buffer[i*b.pitch + j];
-            }
-            
-            format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB;
-        } else
-            throw new fontGlyphException ("Unsupported freetype bitmap format");
-        
-        glTexSubImage2D(GL_TEXTURE_2D, 0,
-                        ga.x, ga.y,
-                        ga.w, ga.h,
-                        format, GL_UNSIGNED_BYTE,
-                        cast(void*) buffer.ptr);
-        
-        cachedGlyphs[chr] = ga;
-    }
-    
-    // Draw the first glyph cache texture in the upper-left corner of the screen.
-    debug (drawGlyphCache) void drawTexture () {
-        if (tex.length == 0) return;
-        glEnable (GL_TEXTURE_2D);
-        glBindTexture(GL_TEXTURE_2D, tex[0].texID);
-        glEnable(GL_BLEND);
-        glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR);
-        float[4] Cc = [ 1.0f, 1f, 1f, 1f ];
-        glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, Cc.ptr);
-        glColor3f (1f, 0f, 0f);
-        
-        glBegin (GL_QUADS);
-        glTexCoord2f (0f, 0f);	glVertex2i (0, 0);
-        glTexCoord2f (1f, 0f);	glVertex2i (dimW, 0);
-        glTexCoord2f (1f, 1f);	glVertex2i (dimW, dimH);
-        glTexCoord2f (0f, 1f);	glVertex2i (0, dimH);
-        glEnd ();
-        
-        glDisable(GL_BLEND);
-    }
-    
-private:
-    TexPacker[] tex;	// contains the gl texture id and packing data
-    
-    GlyphAttribs[dchar]	cachedGlyphs;
-    int cacheVer = 0;	// version of cache, used to make sure TextBlock caches are current.
-}
-
-// Use LinePacker for our texture packer:
-alias LinePacker TexPacker;
-
-/** Represents one gl texture; packs glyphs into lines. */
-struct LinePacker
-{
-    // create a new texture
-    static LinePacker create () {
-        LinePacker p;
-        //FIXME: why do I get a blank texture when binding to non-0?
-        //glGenTextures (1, &(p.texID));
-        p.texID = 0;
-        
-        // add a pretty background to the texture
-        debug (drawGlyphCache) {
-            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-            glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
-            ubyte[3][dimH][dimW] testTex;
-            for (size_t i = 0; i < dimW; ++i)
-                for (size_t j = 0; j < dimH; ++j)
-            {
-                testTex[i][j][0] = cast(ubyte) (i + j);
-                testTex[i][j][1] = cast(ubyte) i;
-                testTex[i][j][2] = cast(ubyte) j;
-            }
-            void* ptr = testTex.ptr;
-        } else
-            const void* ptr = null;
-        
-        // Create a texture without initialising values.
-        glBindTexture(GL_TEXTURE_2D, p.texID);
-        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
-                     dimW, dimH, 0,
-                     GL_RGB, GL_UNSIGNED_BYTE, ptr);
-        return p;
-    }
-    
-    /** Find space for a glyph of size attr.w, attr.h within the texture.
-     *
-     * Throws: fontGlyphException if glyph dimensions are larger than the texture.
-     *
-     * Returns false if unable to fit the glyph into the texture, true if successful. If
-     * successful, attr's x and y are set to suitible positions such that the rect given by attr's
-     * x, y, w & h is a valid subregion of the texture. */
-    bool addGlyph (ref GlyphAttribs attr) {
-        if (attr.w > dimW || attr.h > dimH)
-            throw new fontGlyphException ("Glyph too large to fit texture!");
-        
-        attr.texID = texID;		// Set now. Possibly reset if new texture is needed.
-        if (attr.w == 0) return true;	// 0 sized glyph; x and y are unimportant.
-        
-        bool cantFitExtraLine = nextYPos + attr.h >= dimH;
-        foreach (ref line; lines) {
-            if (line.length + attr.w <= dimW &&		// if sufficient length and
-                line.height >= attr.h &&		// sufficient height and
-                (cantFitExtraLine ||			// either there's not room for another line
-                line.height <= attr.h * WASTE_H))	// or we're not wasting much vertical space
-            {						// then use this line
-                attr.x = line.length;
-                attr.y = line.yPos;
-                attr.texID = texID;
-                line.length += attr.w;
-                return true;
-            }
-        }
-        // If we didn't return, we didn't use an existing line.
-        if (cantFitExtraLine)				// run out of room
-            return false;
-        
-        // Still room: add a new line. The new line has the largest yPos (furthest down texture),
-        // but the lines array must remain ordered by line height (lowest to heighest).
-        Line line;
-        line.yPos = nextYPos;
-        line.height = attr.h * EXTRA_H;
-        line.length = attr.w;
-        size_t i = 0;
-        while (i < lines.length && lines[i].height < line.height) ++i;
-        lines = lines[0..i] ~ line ~ lines[i..$];	// keep lines sorted by height
-        nextYPos += line.height;
-        
-        attr.x = 0;	// first glyph in the line
-        attr.y = line.yPos;
-        return true;
-    }
-    
-    // Publically accessible data:
-    uint texID;				// OpenGL texture identifier (for BindTexture)
-    
-private:
-    const WASTE_H = 1.3;
-    const EXTRA_H = 1;	// can be float/double, just experimenting with 1
-    struct Line {
-        int yPos;	// y position (xPos is always 0)
-        int height;
-        int length;
-    }
-    Line[] lines;
-    int nextYPos = 0;	// y position for next created line (0 for first line)
-}
-
-// this bit of renderMode, if set, means read glyph as BGR not RGB when using LCD rendering
-enum { RENDER_LCD_BGR = 1 << 30 }
-OptionsFont fontOpts;
-class OptionsFont : Options {
-    /* renderMode should be FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, FT_LOAD_TARGET_LCD or
-     * FT_LOAD_TARGET_LCD_V, possibly with bit 31 set (see RENDER_LCD_BGR).
-     * FT_LOAD_TARGET_MONO is unsupported.
-     *
-     * lcdFilter should come from enum FT_LcdFilter:
-     * FT_LCD_FILTER_NONE = 0, FT_LCD_FILTER_DEFAULT = 1, FT_LCD_FILTER_LIGHT = 2 */
-    mixin (impl!("int renderMode, lcdFilter;"));
-    
-    static this() {
-        fontOpts = new OptionsFont;
-        Options.addOptionsClass (fontOpts, "font");
-    }
-}
-
-struct GlyphAttribs {
-    int x, y;	// position within texture
-    int w, h;	// bitmap size
-    
-    int left, top;	// bitmap_left, bitmap_top fields
-    int advanceX;	// horizontal advance distance
-    uint index;		// glyph index (within face)
-    
-    uint texID;	// gl tex identifier
-}
-
-/** Cached information for drawing a block of text.
- *
- * Struct should be stored externally and updated via references. */
-struct TextBlock {
-    CharCache[] chars;	// All chars. They hold x & y pos. info, so don't need to know about lines.
-    int cacheVer = -1;	// this is checked on access, and must equal for cache to be valid.
-    int w, h;		/// Size of the block. Likely the only fields of use outside the library.
-}
-struct CharCache {
-    GlyphAttribs* ga;	// character
-    int xPos, yPos;	// x,y position
-}
--- a/mde/resource/exception.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-/* 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/>. */
-
-/// Contains resource exceptions
-module mde.resource.exception;
-import mde.exception;
-
-/// Thrown when initialisation fails
-class fontException : mdeException {
-    char[] getSymbol () {
-        return super.getSymbol ~ ".resource.font";
-    }
-    
-    this (char[] msg) {
-        super(msg);
-    }
-}
-
-/// Thrown when loading a freetype font fails.
-class fontLoadException : fontException {
-    this (char[] msg) {
-        super(msg);
-    }
-}
-
-/// Thrown when problems occur with glyphs (rendering, etc.)
-class fontGlyphException : fontException {
-    char[] getSymbol () {
-        return super.getSymbol ~ ".glyph";
-    }
-    
-    this (char[] msg) {
-        super(msg);
-    }
-}
--- a/mde/resource/font.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,302 +0,0 @@
-/* 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/>. */
-
-/// Sets up freetype (in a basic way).
-module mde.resource.font;
-
-public import mde.types.basic;	// Colour
-import mde.Options;
-import mde.resource.FontTexture;
-import mde.resource.exception;
-
-import mde.mergetag.Reader;
-import mde.mergetag.DataSet;
-import mde.mergetag.exception;
-import mde.resource.paths;
-
-import derelict.freetype.ft;
-import derelict.opengl.gl;
-
-import tango.scrapple.text.convert.parseTo : parseTo;
-import tango.stdc.stringz;
-import Util = tango.text.Util;
-import tango.util.log.Log : Log, Logger;
-
-// "Publically import" this symbol:
-alias mde.resource.FontTexture.TextBlock TextBlock;
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.resource.font");
-}
-
-/** FontStyle class.
- *
- * Particular to a font and size, and any other effects like bold/italic if ever implemented.
- * 
- * Note: it is not currently intended to be thread-safe. */
-class FontStyle : IDataSection
-{
-    //BEGIN Static: manager
-    static {
-        debug (drawGlyphCache) void drawTexture() {
-            if (fontTex !is null)
-                fontTex.drawTexture;
-        }
-        
-        /** Load the freetype library. */
-        void initialize () {
-            if (!confDir.exists (fileName))
-                throw new fontException ("No font settings file (fonts.[mtt|mtb])");
-            
-            if (FT_Init_FreeType (&library))
-                throw new fontException ("error initialising the FreeType library");
-            
-            // Check version
-            FT_Int maj, min, patch;
-            FT_Library_Version (library, &maj, &min, &patch);
-            if (maj != 2 || min != 3) {
-                char[128] tmp;
-                logger.warn (logger.format (tmp, "Using an untested FreeType version: {}.{}.{}", maj, min, patch));
-            }
-            
-            // Set LCD filtering method if LCD rendering is enabled.
-            const RMF = FT_LOAD_TARGET_LCD | FT_LOAD_TARGET_LCD_V;
-            if (fontOpts.renderMode & RMF &&
-                FT_Library_SetLcdFilter(library, cast(FT_LcdFilter)fontOpts.lcdFilter)) {
-                /* An error occurred, presumably because LCD rendering support
-                * is not compiled into the library. */
-                logger.warn ("Bad/unsupported LCD filter option; disabling LCD font rendering.");
-                logger.warn ("Your FreeType 2 library may compiled without support for LCD/sub-pixel rendering.");
-                
-                // Reset the default filter (in case an invalid value was set in config files).
-                Options.setInt ("font", "lcdFilter", FT_LcdFilter.FT_LCD_FILTER_DEFAULT);
-                
-                /* If no support for LCD filtering, then LCD rendering only emulates NORMAL with 3
-                 * times wider glyphs. So disable and save the extra work. */
-                Options.setInt ("font", "renderMode", FT_LOAD_TARGET_NORMAL);
-            }
-            
-            /* Load font settings
-             *
-             * Each mergetag section corresponds to a font; each is loaded whether used or not
-             * (however the actual font files are only loaded on use). A fallback id must be
-             * provided in the header which must match a loaded font name; if a non-existant font
-             * is requested a warning will be logged and this font returned. */
-            char[] fallbackName;
-            try {
-                IReader reader;
-                reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH, null, true);
-                reader.dataSecCreator = delegate IDataSection(ID id) {
-                    auto f = new FontStyle;
-                    fonts[id] = f;
-                    return f;
-                };
-                reader.read;
-                
-                // get fallback name
-                char[]* p = "fallback" in reader.dataset.header.Arg!(char[]).Arg;
-                if (p is null)
-                    throw new fontException ("No fallback font style specified");
-                fallbackName = *p;
-            }
-            catch (MTException e) {
-                throw new fontException ("Mergetag exception: "~e.msg);
-            }
-            
-            // Find the fallback
-            FontStyle* p = fallbackName in fonts;
-            if (p is null)
-                throw new fontException ("Fallback font style specified is not found");
-            fallback = *p;
-            // Load the fallback now, to ensure it's available.
-            // Also note that get() doesn't make sure the fallback is loaded before returning it.
-            fallback.load;
-        }
-        private const fileName = "fonts";
-        
-        //FIXME: don't use GC for FontStyle resources
-        /** Cleanup: delete all fonts. */
-        void cleanup () {
-            FT_Done_FreeType (library);
-        }
-        
-        /** Get a FontStyle instance, for a section in the fonts.mtt file.
-          *
-          * Also loads the font if it's not already loaded, so the first call may take some time.
-          *
-          * Uses fallback font-style if the desired style isn't known about or fails to load, so
-          * this function should never fail or throw, in theory (unless out of memory). The
-          * fallback should already be loaded. */
-        FontStyle get(char[] name) {
-            FontStyle* p = name in fonts;
-            if (p is null) {
-                logger.warn ("Font style "~name~" requested but not found; reverting to the fallback style.");
-                fonts[name] = fallback;	// set to prevent another warning getting logged
-                return fallback;
-            }
-            // Got it, but we need to make sure it's loaded:
-            try {
-                p.load;
-            } catch (Exception e) {
-                logger.warn ("Font style "~name~" failed to load; reverting to the fallback style.");
-                return fallback;
-            }
-            return *p;
-        }
-        
-    private:
-        FT_Library	library;
-        FontTexture	fontTex;
-        FontStyle[ID]	fonts;		// all font styles known about; not necessarily loaded
-        FontStyle	fallback;	// used when requested font isn't in fonts
-    }
-    //END Static
-    
-    this() {}
-    
-    //BEGIN Mergetag code
-    //NOTE: would it be better not to use a new mergetag file for this?
-    //FIXME: revise when gui can set options
-    void addTag (char[] tp, ID id, char[] dt) {
-        if (tp == "char[]") {
-            if (id == "path")
-                path = parseTo!(char[]) (dt);
-        }
-        else if (tp == "int") {
-            if (id == "size")
-                size = parseTo!(int) (dt);
-        }
-    }
-    void writeAll (ItemDelg) {}		// no writing the config for now
-    //END Mergetag code
-    
-    /** Load the font file.
-     *
-     * Even if the same font is used at multiple sizes, multiple copies of FT_Face are used.
-     * Sharing an FT_Face would require calling FT_Set_Pixel_Sizes each time a glyph is rendered or
-     * swapping the size information (face.size)? */
-    void load ()
-    in {
-        assert (library !is null, "font: library is null");
-    } body {
-        if (FT_New_Face (library, toStringz(path), 0, &face))
-            throw new fontLoadException ("Unable to read font: "~path);
-        
-        if (!FT_IS_SCALABLE (face))
-            throw new fontLoadException ("Currently no support for non-scalable fonts (which " ~
-                    path ~ " is). Please report if you want to see support.");
-        /* The following will need to be addressed when adding support for non-scalables:
-         *	Use of face.size.metrics.height property.
-         */
-        
-        if (FT_Set_Pixel_Sizes (face, 0,size))
-            throw new fontLoadException ("Unable to set pixel size");
-        
-        // Create if necessary:
-        if (fontTex is null)
-            fontTex = new FontTexture;
-    }
-    
-    /** Update a TextBlock cache, as used by the textBlock function.
-     *
-     * The only use of this is to get the text block's size ahead of rendering, via TextBlock's w
-     * and h properties.
-     *
-     * This function will only actually update the cache if it is invalid, caused either by the
-     * font being changed or if cache.cacheVer < 0. */
-    void updateBlock (char[] str, ref TextBlock cache) {
-        try {
-            fontTex.updateCache (face, str, cache);
-        } catch (Exception e) {
-            logger.warn ("Exception while drawing text: "~e.msg);
-        }
-    }
-    
-    /** Draw a block of text (may inlcude new-lines).
-     *
-     * The text block is drawn with top-left corner at x,y. To put the text's baseline at a given
-     * y coordinate would require some changes. Line height is currently variable, depending on the
-     * highest glyph in the line (should probably be fixed: FIXME).
-     *
-     * Specify the text's colour with col; currently this is only Colour.WHITE or Colour.BLACK
-     * (FIXME). FIXME: add alpha support.
-     *
-     * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as
-     * the cache argument. This is the recommended method, although for one-time calls when you
-     * don't need to know the size, the other version of textBlock may be used.
-     * ---------------------------------
-     * char[] str;
-     * TextBlock strCache;
-     * textBlock (x, y, str, strCache);
-     * ---------------------------------
-     * The TextBlock cache will be updated as necessary. Besides the initial update, this will only
-     * be if the font changes, or it is manually invalidated. This can be done by setting the
-     * TextBlock's cacheVer property to -1, which should be done if str is changed.
-     *
-     * The TextBlock's w and h properties are set to the size (in pixels) of the text block; other
-     * than this cache only serves as a small optimisation. However, the only way to get the size
-     * of a text block is to use a TextBlock cache and update it, either with this function or with
-     * the updateBlock function. */
-    void textBlock (int x, int y, char[] str, ref TextBlock cache, Colour col) {
-        try {
-            fontTex.drawCache (face, str, cache, x, y, col);
-        } catch (Exception e) {
-            logger.warn ("Exception while drawing text: "~e.msg);
-        }
-    }
-    /** ditto */
-    void textBlock (int x, int y, char[] str, Colour col) {
-        try {
-            // Using the cache method for one-time use is slightly less than optimal, but doing so
-            // isn't really recommended anyway (and maintaining two versions of fontTex.drawText
-            // would be horrible).
-            TextBlock cache;
-            fontTex.drawCache (face, str, cache, x, y, col);
-        } catch (Exception e) {
-            logger.warn ("Exception while drawing text: "~e.msg);
-        }
-    }
-    
-    /** A variation of textBlock for transparency.
-     *
-     * Set the alpha by calling glColor*() first. See FontTexture.drawCacheA()'s documentation for
-     * details. */
-    void textBlockA (int x, int y, char[] str, ref TextBlock cache, Colour col) {
-        try {
-            fontTex.drawCacheA (face, str, cache, x, y, col);
-        } catch (Exception e) {
-            logger.warn ("Exception while drawing text: "~e.msg);
-        }
-    }
-    
-    /** The font-specified vertical distance between the baseline of consecutive lines. */
-    int getLineSeparation () {
-        return face.size.metrics.height >> 6;
-    }
-    
-    ~this () {
-        FT_Done_Face (face);
-    }
-    
-private:
-    char[]	path;	// path to font file
-    int		size;	// font size
-    
-    FT_Face	face;
-}
-
-/+class OptionsFont : Options {
-    alias store!(+/
\ No newline at end of file
--- a/mde/resource/paths.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,357 +0,0 @@
-/* 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.resource.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 {
-            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;
-    }
-}
\ No newline at end of file
--- a/mde/scheduler/Init.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,407 +0,0 @@
-/* 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/>. */
-
-/**************************************************************************************************
- * Initialisation setup and exit cleanup module.
- *
- * This module provides an infrastructure for handling much of the initialisation and
- * deinitialisation of the program. It does not, however, provide much of the (de)initialisation
- * code; with the exception of that for the logger.
- *************************************************************************************************/
-module mde.scheduler.Init;
-
-import mde.scheduler.init2;     // This module is responsible for setting up some init functions.
-import mde.scheduler.initFunctions;
-import mde.scheduler.exception;
-
-import mde.Options;
-import paths = mde.resource.paths;
-import mde.exception;
-
-// tango imports
-import tango.core.Thread;
-import tango.core.Exception;
-import tango.stdc.stringz : fromStringz;
-import tango.io.Console;	// for printing command-line usage
-import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock;	// output date in log file
-import tango.util.Arguments;
-import tango.util.log.Log : Log, Logger;
-import tango.util.log.ConsoleAppender : ConsoleAppender;
-
-//version = SwitchAppender;
-version (SwitchAppender) {  // My own variation, currently just a test
-    import tango.util.log.SwitchingFileAppender : SwitchingFileAppender;
-} else {
-    import tango.util.log.RollingFileAppender : RollingFileAppender;
-}
-
-// Derelict imports
-import derelict.opengl.gl;
-import derelict.sdl.sdl;
-//import derelict.sdl.image;	Was to be used... for now isn't
-import derelict.freetype.ft;
-import derelict.util.exception;
-
-/**
- * Static CTOR
- *
- * This should handle a minimal amount of functionality where useful. For instance, configuring the
- * logger here and not in Init allows unittests to use the logger.
- */
-static this()
-{
-    Logger root = Log.getRootLogger();
-    // Set the level here, but set it again once options have been loaded:
-    debug root.setLevel(root.Level.Trace);
-    else root.setLevel(root.Level.Info);
-    // Temporarily log to the console (until we've found paths and loaded options):
-    root.addAppender(new ConsoleAppender);
-}
-static ~this()
-{
-}
-
-/**
- * Init class
- *
- * A scope class created at beginning of the program and destroyed at the end; thus the CTOR
- * handles program initialisation and the DTOR handles program cleanup.
- */
-scope class Init
-{
-    //private bool failure = false;       // set true on failure during init, so that clean
-    private static Logger logger;
-    static this() {
-        logger = Log.getLogger ("mde.scheduler.Init.Init");
-    }
-    
-    /** this() − initialisation
-    *
-    * Runs general initialisation code, in a threaded manner where this isn't difficult.
-    * If any init fails, cleanup is still handled by ~this().
-    *
-    * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library
-    * loading (load any dynamic libraries first, so that if loading fails nothing else need be
-    * done). 3. Init functions (threaded functions handling the rest of initialisation).
-    */
-    /* In a single-threaded function this could be done with:
-    * scope(failure) cleanup;
-    * This won't work with a threaded init function since any threads completing succesfully will
-    * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions
-    * must not run where the initialisation functions have failed.
-    * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct.
-    */
-    this(char[][] cmdArgs)
-    {
-        debug logger.trace ("Init: starting");
-        
-        //BEGIN Pre-init (stage init0)
-        //FIXME: warn on invalid arguments, including base-path on non-Windows
-        // But Arguments doesn't support this (in tango 0.99.6 and in r3563).
-        Arguments args;
-        try {
-            args = new Arguments();
-            args.define("base-path").parameters(1);
-            args.define("data-path").parameters(1,-1);
-            args.define("conf-path").parameters(1,-1);
-            args.define("paths");
-            args.define("q").aliases(["quick-exit"]);
-            args.define("help").aliases(["h"]);
-            args.parse(cmdArgs);
-            if (args.contains("help"))	// lazy way to print help
-                throw new InitException ("Help requested");	// and stop
-        } catch (Exception e) {
-            printUsage(cmdArgs[0]);
-            throw new InitException ("Parsing arguments failed: "~e.msg);
-        }
-        
-	// Find/create paths:
-	try {
-            if (args.contains("data-path"))
-                paths.extraDataPath = args["data-path"];
-            if (args.contains("conf-path"))
-                paths.extraConfPath = args["conf-path"];
-            if (args.contains("base-path"))
-                paths.resolvePaths (args["base-path"]);
-            else
-                paths.resolvePaths();
-	} catch (Exception e) {
-            throw new InitException ("Resolving paths failed: " ~ e.msg);
-	}
-        if (args.contains("paths")) {
-            paths.mdeDirectory.printPaths;
-            throw new InitException ("Paths requested");	// lazy way to stop
-        }
-        debug logger.trace ("Init: resolved paths successfully");
-    
-        /* Load options now. Don't load in a thread since:
-        *   Loading should be fast & most work is probably disk access
-        *   It enables logging to be controlled by options
-        *   It's a really good idea to let the options apply to all other loading */
-        try {
-            Options.load();
-        } catch (optionsLoadException e) {
-            throw new InitException ("Loading options failed: " ~ e.msg);
-        }
-        debug logger.trace ("Init: loaded options successfully");
-        
-	// Set up the logger:
-        Logger root;
-	try {
-            enum LOG {
-                LEVEL	= 0xF,		// mask for log level
-                CONSOLE	= 0x1000,	// log to console?
-                ROLLFILE= 0x2000	// use Rolling/Switching File Appender?
-            }
-            
-	    // Where logging is done to is determined at compile-time, currently just via static ifs.
-            root = Log.getRootLogger();
-	    root.clearAppenders;	// we may no longer want to log to the console
-	    
-            // Now re-set the logging level, using the value from the config file:
-            root.setLevel (cast(Log.Level) (miscOpts.logOptions & LOG.LEVEL), true);
-            
-            // Log to a file (first appender so root seperator messages don't show on console):
-            if (miscOpts.logOptions & LOG.ROLLFILE) {
-                version (SwitchAppender) {
-                    root.addAppender (new SwitchingFileAppender (paths.logDir~"/log-.txt", 5));
-                } else {
-                // Use 2 log files with a maximum size of 1 MB:
-                    root.addAppender (new RollingFileAppender (paths.logDir~"/log-.txt", 2, 1024*1024));
-                    root.info (""); // some kind of separation between runs
-                    root.info ("");
-                }
-            } else if (!(miscOpts.logOptions & LOG.CONSOLE)) {
-                // make sure at least one logger is enabled
-                Options.setInt ("misc", "logOptions", miscOpts.logOptions | LOG.CONSOLE);
-            }
-            if (miscOpts.logOptions & LOG.CONSOLE) {	// Log to the console
-                root.addAppender(new ConsoleAppender);
-            }
-            logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now));
-        } catch (Exception e) {
-            // Presumably it was only adding a file appender which failed; set up a new console
-            // logger and if that fails let the exception kill the program.
-            root.clearAppenders;
-            root.addAppender (new ConsoleAppender);
-            logger.warn ("Exception while setting up the logger; logging to the console instead.");
-        }
-        
-        // a debugging option:
-        imde.run = !args.contains("q") && !miscOpts.exitImmediately;
-        debug logger.trace ("Init: applied pre-init options");
-        
-        //BEGIN Load dynamic libraries
-        /* Can be done by init functions but much neater to do here.
-        * Also means that init functions aren't run if a library fails to load. */
-        try {
-            DerelictSDL.load();
-            // SDLImage was going to be used... for now it isn't because of gl texturing problems
-            //DerelictSDLImage.load();
-            DerelictGL.load();
-            DerelictFT.load();
-        } catch (DerelictException de) {
-            logger.fatal ("Loading dynamic library failed:");
-            logger.fatal (de.msg);
-            
-            throw new InitException ("Loading dynamic libraries failed (see above).");
-        }
-        debug logger.trace ("Init: dynamic libraries loaded");
-        //END Load dynamic libraries
-        //END Pre-init
-        
-        
-        //BEGIN Init (stages init2, init4)
-        /* Call init functions.
-        *
-        * Current method is to try using threads, and on failure assume no threads were actually
-        * created and run functions in a non-threaded manner. */
-        
-        try {
-            if (runStageThreaded (init)) runStageForward (init);
-        }
-        catch (InitStageException) {    // This init stage failed.
-            // FIXME: check DTOR still runs
-            throw new InitException ("An init function failed (see above message(s))");
-        }
-        //END Init
-        
-        debug logger.trace ("Init: done");
-    }
-    
-    /** DTOR - runs cleanup functions. */
-    ~this()
-    {
-        debug logger.trace ("Cleanup: starting");
-        
-        Options.save(); // save options... do so here for now
-        
-        // General cleanup:
-        try {
-            if (runStageThreaded (cleanup)) runStageReverse (cleanup);
-        }
-        catch (InitStageException) {
-            // Nothing else to do but report:
-            logger.error ("One or more cleanup functions failed!");
-        }
-        
-        debug logger.trace ("Cleanup: done");
-    }
-    
-    
-    //BEGIN runStage...
-    private static {
-        /* The following three functions, runStage*, each run all functions in a stage in some order,
-        * catching any exceptions thrown by the functions (although this isn't guaranteed for threads),
-        * and throw an InitStageException on initFailure. */
-    
-        const LOG_IF_MSG = "Init function ";
-        const LOG_CF_MSG = "Cleanup function ";
-        const LOG_F_START = " - running";
-        const LOG_F_END = " - completed";
-        const LOG_F_BAD = " - failed";
-        const LOG_F_FAIL = " - failed: ";
-        /* Runs all functions consecutively, first-to-last.
-        * If any function fails, halts immediately. */
-        void runStageForward (InitStage s) {
-            foreach (func; s.funcs) {
-                if (initFailure) break;
-                try {
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
-                    func.func();
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
-                } catch (Exception e) {
-                    logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~
-                            ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
-                
-                    setInitFailure();
-                }
-            }
-            
-            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
-        }
-        /* Runs all functions consecutively, last-to-first.
-        * If any function fails, continue until all have been run. */
-        void runStageReverse (InitStage s) {
-            foreach_reverse (func; s.funcs) {
-                try {
-                    debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START);
-                    func.func();
-                    debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
-                } catch (Exception e) {
-                    logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~
-                            ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
-                
-                    setInitFailure();
-                }
-            }
-            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
-        }
-        /* Tries running functions in a threaded way. Returns false if successful, true if not but
-        * functions should be run without threads. */
-        bool runStageThreaded (InitStage s) {
-            if (!miscOpts.useThreads) return true;  // Use unthreaded route instead
-        
-            ThreadGroup tg;
-            try {                           // creating/starting threads could fail
-                tg = new ThreadGroup;
-                foreach (func; s.funcs) {   // Start all threads
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
-                    tg.create(func.func);
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
-                }
-            } catch (ThreadException e) {   // Problem with threading; try without threads
-                logger.error ("Caught ThreadException while trying to create threads:");
-                logger.error (e.msg);
-                logger.info ("Will disable threads and continue, assuming no threads were created.");
-            
-                Options.setBool("misc", "useThreads", false);   // Disable threads entirely
-                return true;                // Try again without threads
-            }
-        
-            /* Wait for all threads to complete.
-            *
-            * If something went wrong, we still need to do this before cleaning up.
-            */
-            foreach (t; tg) {
-                try {
-                    t.join (true);
-                } catch (Exception e) {
-                    // Relying on catching exceptions thrown by other threads is a bad idea.
-                    // Hence all threads should catch their own exceptions and return safely.
-                
-                    logger.fatal ("Unhandled exception from Init function:");
-                    logger.fatal (e.msg);
-                
-                    setInitFailure ();        // abort (but join other threads first)
-                }
-            }
-            
-            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
-            return false;                   // Done successfully
-        }
-    //END runStage...
-        
-        void printUsage (char[] progName) {
-            Cout ("mde [no version]").newline;
-            Cout ("Usage:").newline;
-            Cout (progName ~ ` [options]`).newline;
-            version(Windows)
-                    Cout (
-`  --base-path path	Use path as the base (install) path (Windows only). It
-			should contain the "data" directory.`).newline;
-            Cout (
-`  --data-path path(s)	Add path(s) as a potential location for data files.
-			First path argument becomes the preffered location to
-			load data files from.
-  --conf-path path(s)	Add path(s) as a potential location for config files.
-			Configuration in the first path given take highest
-			priority.
-  --paths		Print all paths found and exit.
-  --quick-exit, -q	Exit immediately, without entering main loop.
-  --help, -h		Print this message.`).newline;
-        }
-    }
-    
-    debug (mdeUnitTest) unittest {
-        /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid
-        * messing other init/cleanup up. */
-        static InitStage initUT, cleanupUT;
-        
-        static bool initialised = false;
-        static void cleanupFunc1 () {
-            initialised = false;
-        }
-        static void cleanupFunc2 () {
-            assert (initialised == true);
-        }
-                
-        static void initFunc () {
-            initialised = true;
-            cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1");
-            cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2");
-        }
-        
-        initUT.addFunc (&initFunc, "UT init");
-        
-        runStageForward (initUT);
-        assert (initialised);
-        
-        runStageReverse (cleanupUT);
-        assert (!initialised);
-
-        logger.info ("Unittest complete.");
-    }
-}
--- a/mde/scheduler/Scheduler.d	Fri Jun 27 17:19:46 2008 +0100
+++ b/mde/scheduler/Scheduler.d	Fri Jun 27 18:35:33 2008 +0100
@@ -13,8 +13,10 @@
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
-/** Scheduler
-*/
+/** A fairly generic scheduler.
+ *
+ * This class implements most functionality a generic scheduler might want, however currently it
+ * doesn't any uses where equivalent functionality couldn't be achived very easily anyway. */
 module mde.scheduler.Scheduler;
 
 public import tango.time.Time;
--- a/mde/scheduler/exception.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-/* 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/>. */
-
-/// Contains the exception classes for Init.
-module mde.scheduler.exception;
-
-import mde.exception;
-
-/// Thrown when Init fails.
-class InitException : mdeException {
-    char[] getSymbol () {
-        return super.getSymbol ~ ".Init";
-    }
-    
-    this (char[] msg) {
-        super(msg);
-    }
-}
-
-/// Thrown when an init stage fails.
-class InitStageException : InitException {
-    this () {
-        super("");
-    }
-}
--- a/mde/scheduler/init2.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/* 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/>. */
-
-/** This module is the start of implementing the following:
-*
-* Idea: change import direction so this module adds all init functions. All init functions are
-* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail
-* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g.
-* "out cleanupFunc[]".
-*
-* This should make it much easier to tell what actually happens during init and to order init such
-* that dependencies are honoured.
-*
-* Currently some external modules depend on InitFunctions, while some are set up from here. Once
-* all are set up from here, the Init* modules can be rearranged. */
-/* Idea: go back to init being controlled from elsewhere. Add a function to wait for another init
- * function to complete (threaded; might need to be done differently for non-threaded). */
-module mde.scheduler.init2;
-
-import mde.scheduler.initFunctions;
-
-import tango.util.log.Log : Log, Logger;
-
-// Modules requiring init code running:
-import imde = mde.imde;
-import mde.gui.Gui;
-import mde.input.Input;
-import font = mde.resource.font;
-
-// NOTE: error reporting needs a revision
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.scheduler.Init2");
-    
-    init.addFunc (&initInput, "initInput");
-    init.addFunc (&guiLoad, "guiLoad");
-}
-
-void guiLoad () {   // init func
-    try {
-        font.FontStyle.initialize;
-        gui.load (GUI);
-        cleanup.addFunc (&guiSave, "guiSave");
-    } catch (Exception e) {
-        logger.fatal ("guiLoad failed: " ~ e.msg);
-        setInitFailure;
-    }
-}
-void guiSave () {   // cleanup func
-    try {
-        gui.save (GUI);
-    } catch (Exception e) {
-        logger.fatal ("guiSave failed: " ~ e.msg);
-        setInitFailure;
-    }
-}
-private const GUI = "gui";
-
-void initInput () { // init func
-    try {
-        imde.input.loadConfig ();         // (may also create instance)
-        
-        // Quit on escape. NOTE: quit via SDL_QUIT event is handled completely independently!
-        imde.input.addButtonCallback (cast(Input.inputID) 0x0u, delegate void(Input.inputID i, bool b) {
-            if (b) {
-                logger.info ("Quiting...");
-                imde.run = false;
-            }
-        } );
-    } catch (Exception e) {
-        logger.fatal ("initInput failed: " ~ e.msg);
-        setInitFailure;
-    }
-}
-
-/+ Potential wrapper function:
-// Template to call function, catching exceptions:
-void wrap(alias Func) () {
-    try {
-        Func();
-    } catch (Exception e) {
-        logger.fatal (FAIL_MSG);
-        logger.fatal (e.msg);
-        setInitFailure;
-    }
-}
-private const FAIL_MSG = "Unexpected exception caught:";
-+/
--- a/mde/scheduler/initFunctions.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/* 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/>. */
-
-/** This module is responsible for calling all init functions.
-*
-* It is also responsible for setting up all scheduled functions for now.
-
-* Idea: change import direction so this module adds all init functions. All init functions are
-* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail
-* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g.
-* "out cleanupFunc[]". */
-module mde.scheduler.initFunctions;
-
-/+ unused
-import tango.util.log.Log : Log, Logger;
-static this() {
-    logger = Log.getLogger ("mde.scheduler.InitFunctions");
-}+/
-
-void setInitFailure () {    /// Call to indicate failure in an init function
-    initFailure = true;
-}
-
-/** Represents all functions to be called for a particular init stage. */
-struct InitStage
-{
-    struct InitFunction {
-        void delegate() func;       // the actual function
-        char[] name;                // it's name;
-    }
-    
-    /** Add a function to be called during this init stage.
-    *
-    * Called in order added when not threaded (reverse order for cleanup).
-    *
-    * Exceptions should never be thrown, since each function may run as a thread, and catching
-    * thread exceptions is not guaranteed to work. Log a message, call setFailure() and return
-    * instead. */
-    void addFunc (void delegate() f, char[] name) {
-        InitFunction s;
-        s.func = f;
-        s.name = name;
-        funcs ~= s;
-    }
-    void addFunc (void function() f, char[] name) { /// ditto
-        InitFunction s;
-        s.func.funcptr = f;
-        s.name = name;
-        funcs ~= s;
-    }
-    
-    InitFunction[] funcs = [];
-}
-
-InitStage init;     // all functions called during init (all should be thread-safe)
-//FIXME: implement:
-InitStage save;     // all functions to be called to save data (possible to run more than once)
-InitStage cleanup;  // all functions called during cleanup (all should be thread-safe)
-
-package:
-bool initFailure = false;   // set on failure (throwing through threads isn't a good idea)
-
-private:
-//Logger logger;
--- a/mde/sdl.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,195 +0,0 @@
-/* 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/>. */
-
-/** Just a temporary place to put SDL Init and Video stuff.
-*/
-module mde.sdl;
-
-import mde.scheduler.initFunctions;
-import mde.input.joystick;
-import mde.Options;
-import mde.gl.basic;
-import imde = mde.imde;
-
-import tango.util.log.Log : Log, Logger;
-import tango.stdc.stringz;
-
-import derelict.sdl.sdl;
-import derelict.opengl.gl;	// for loading a later gl version
-import derelict.util.exception;
-
-private Logger logger;
-static this() {
-    logger = Log.getLogger ("mde.SDL");
-    
-    init.addFunc (&initSdlAndGl, "initSdlAndGl");
-}
-
-private uint flags = 0;
-
-void initSdlAndGl() {   // init func
-    // Initialise SDL
-    if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_JOYSTICK /+| SDL_INIT_EVENTTHREAD+/)) {
-        logger.fatal ("SDL initialisation failed:");
-        char* msg = SDL_GetError ();
-        logger.fatal (msg ? fromStringz(msg) : "no reason available");
-        
-        setInitFailure ();
-        return;
-    }
-    
-    debug logger.trace ("SDL initialised");
-    
-    // Must be called after SDL has been initialised, so cannot be a separate Init function.
-    openJoysticks ();                   // after SDL init
-    cleanup.addFunc (&cleanupSDL, "cleanupSDL");
-    
-    setupWindow();
-}
-
-void setupWindow() {    // indirect init func (depends on initSdlAndGl)
-    // Window creation flags and size
-    flags = SDL_OPENGL;
-    if (vidOpts.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
-    else flags |= SDL_SWSURFACE;
-    int w, h;
-    if (vidOpts.fullscreen) {
-        flags |= SDL_FULLSCREEN;
-        w = vidOpts.screenW;
-        h = vidOpts.screenH;
-    }
-    else {
-        if (vidOpts.resizable) flags |= SDL_RESIZABLE;
-        if (vidOpts.noFrame) flags |= SDL_NOFRAME;
-        w = vidOpts.windowW;
-        h = vidOpts.windowH;
-    }
-    
-    // OpenGL attributes
-    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,    5);
-    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,  6);
-    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,   5);
-    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,  16);
-    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
-    
-    // Open a window
-    debug logger.trace ("Opening a window (this can crash if the libraries are messed up)");
-    if (SDL_SetVideoMode (w, h, 32, flags) is null) {
-        logger.fatal ("Unable to set video mode:");
-        char* msg = SDL_GetError ();
-        logger.fatal (msg ? fromStringz(msg) : "no reason available");
-        
-        // Print a load of info:
-        logger.info ("Available video modes:");
-        char[128] tmp;
-        SDL_Rect** modes = SDL_ListModes (null, SDL_FULLSCREEN);
-        if (modes is null) logger.info ("None!");
-        else if (modes is cast(SDL_Rect**) -1) logger.info ("All modes are available");
-        else {
-            for (uint i = 0; modes[i] !is null; ++i) {
-                logger.info (logger.format (tmp, "\t{}x{}", modes[i].w, modes[i].h));
-            }
-        }
-    
-        SDL_VideoInfo* vi = SDL_GetVideoInfo ();
-        if (vi !is null) {
-            logger.info ("Video info:");
-            logger.info ("Hardware surface support: "~ (vi.flags & SDL_HWSURFACE ? "yes" : "no"));
-            logger.info (logger.format (tmp, "Video memory: {}", vi.video_mem));
-    
-            if (vi.vfmt !is null) {
-                logger.info ("Best video mode:");
-                logger.info (logger.format (tmp, "Bits per pixel: {}", vi.vfmt.BitsPerPixel));
-            }
-        }
-        
-        setInitFailure ();
-        return;
-    }
-    
-    /* Now (must be done after GL context is created) we can try to load later version.
-     * The initial loading provides opengl 1.1 features.
-     *
-     * 1.4 is now used for glBlendColor (coloured text).
-     *
-     * Currently the latest version used is 1.3; adjust this as necessary. However, before using
-     * features from any OpenGL version > 1.1 a check must be made on what was loaded by calling
-     * DerelictGL.availableVersion(). Note that availableVersion() could be used instead to load
-     * the highest supported version but this way we know what we're getting.
-     */
-    if (DerelictGL.availableVersion < GLVersion.Version13) {
-        logger.fatal ("Required at least OpenGL 1.3");
-        setInitFailure;
-        return;
-    }
-    /+try {
-        DerelictGL.loadVersions(GLVersion.Version14);
-    } catch (SharedLibProcLoadException e) {
-        logger.warn ("Loading OpenGL version 1.4 failed:");
-        logger.warn (e.msg);
-        
-        //NOTE: might be worth guaranteeing a minimal version to save later checks?
-        /+ Do this if you want the program to abort:
-        setInitFailure ();
-        return;
-        +/
-    }+/
-    
-    // OpenGL stuff:
-    glSetup();
-    setProjection (w, h);
-    
-    // Window-manager settings
-    SDL_WM_SetCaption (toStringz ("mde"), null);
-    // SDL_WM_GrabInput (use later)
-}
-
-void resizeWindow (int w, int h) {
-    if (vidOpts.fullscreen) {
-        Options.setInt ("video", "screenW", w);
-        Options.setInt ("video", "screenH", h);
-    } else {
-        Options.setInt ("video", "windowW", w);
-        Options.setInt ("video", "windowH", h);
-    }
-    
-    if (SDL_SetVideoMode (w, h, 32, flags) is null) {
-        logger.fatal ("Unable to reset video mode:");
-        char* msg = SDL_GetError ();
-        logger.fatal (msg ? fromStringz(msg) : "no reason available");
-        
-        imde.run = false;
-    }
-    
-    // Reset the projection and viewport
-    setProjection (w, h);
-}
-
-void cleanupSDL () {    // cleanup func
-    closeJoysticks();
-    SDL_Quit();
-}
-
-
-/** All video options. */
-OptionsVideo vidOpts;
-class OptionsVideo : Options {
-    mixin (impl!("bool fullscreen,hardware,resizable,noFrame; int screenW,screenH,windowW,windowH;"));
-    
-    static this() {
-        vidOpts = new OptionsVideo;
-        Options.addOptionsClass (vidOpts, "video");
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/Init.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,407 @@
+/* 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/>. */
+
+/**************************************************************************************************
+ * Initialisation setup and exit cleanup module.
+ *
+ * This module provides an infrastructure for handling much of the initialisation and
+ * deinitialisation of the program. It does not, however, provide much of the (de)initialisation
+ * code; with the exception of that for the logger.
+ *************************************************************************************************/
+module mde.setup.Init;
+
+import mde.setup.init2;     // This module is responsible for setting up some init functions.
+import mde.setup.initFunctions;
+import mde.setup.exception;
+
+import mde.lookup.Options;
+import paths = mde.setup.paths;
+import mde.exception;
+
+// tango imports
+import tango.core.Thread;
+import tango.core.Exception;
+import tango.stdc.stringz : fromStringz;
+import tango.io.Console;	// for printing command-line usage
+import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock;	// output date in log file
+import tango.util.Arguments;
+import tango.util.log.Log : Log, Logger;
+import tango.util.log.ConsoleAppender : ConsoleAppender;
+
+//version = SwitchAppender;
+version (SwitchAppender) {  // My own variation, currently just a test
+    import tango.util.log.SwitchingFileAppender : SwitchingFileAppender;
+} else {
+    import tango.util.log.RollingFileAppender : RollingFileAppender;
+}
+
+// Derelict imports
+import derelict.opengl.gl;
+import derelict.sdl.sdl;
+//import derelict.sdl.image;	Was to be used... for now isn't
+import derelict.freetype.ft;
+import derelict.util.exception;
+
+/**
+ * Static CTOR
+ *
+ * This should handle a minimal amount of functionality where useful. For instance, configuring the
+ * logger here and not in Init allows unittests to use the logger.
+ */
+static this()
+{
+    Logger root = Log.getRootLogger();
+    // Set the level here, but set it again once options have been loaded:
+    debug root.setLevel(root.Level.Trace);
+    else root.setLevel(root.Level.Info);
+    // Temporarily log to the console (until we've found paths and loaded options):
+    root.addAppender(new ConsoleAppender);
+}
+static ~this()
+{
+}
+
+/**
+ * Init class
+ *
+ * A scope class created at beginning of the program and destroyed at the end; thus the CTOR
+ * handles program initialisation and the DTOR handles program cleanup.
+ */
+scope class Init
+{
+    //private bool failure = false;       // set true on failure during init, so that clean
+    private static Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.setup.Init");
+    }
+    
+    /** this() − initialisation
+    *
+    * Runs general initialisation code, in a threaded manner where this isn't difficult.
+    * If any init fails, cleanup is still handled by ~this().
+    *
+    * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library
+    * loading (load any dynamic libraries first, so that if loading fails nothing else need be
+    * done). 3. Init functions (threaded functions handling the rest of initialisation).
+    */
+    /* In a single-threaded function this could be done with:
+    * scope(failure) cleanup;
+    * This won't work with a threaded init function since any threads completing succesfully will
+    * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions
+    * must not run where the initialisation functions have failed.
+    * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct.
+    */
+    this(char[][] cmdArgs)
+    {
+        debug logger.trace ("Init: starting");
+        
+        //BEGIN Pre-init (stage init0)
+        //FIXME: warn on invalid arguments, including base-path on non-Windows
+        // But Arguments doesn't support this (in tango 0.99.6 and in r3563).
+        Arguments args;
+        try {
+            args = new Arguments();
+            args.define("base-path").parameters(1);
+            args.define("data-path").parameters(1,-1);
+            args.define("conf-path").parameters(1,-1);
+            args.define("paths");
+            args.define("q").aliases(["quick-exit"]);
+            args.define("help").aliases(["h"]);
+            args.parse(cmdArgs);
+            if (args.contains("help"))	// lazy way to print help
+                throw new InitException ("Help requested");	// and stop
+        } catch (Exception e) {
+            printUsage(cmdArgs[0]);
+            throw new InitException ("Parsing arguments failed: "~e.msg);
+        }
+        
+	// Find/create paths:
+	try {
+            if (args.contains("data-path"))
+                paths.extraDataPath = args["data-path"];
+            if (args.contains("conf-path"))
+                paths.extraConfPath = args["conf-path"];
+            if (args.contains("base-path"))
+                paths.resolvePaths (args["base-path"]);
+            else
+                paths.resolvePaths();
+	} catch (Exception e) {
+            throw new InitException ("Resolving paths failed: " ~ e.msg);
+	}
+        if (args.contains("paths")) {
+            paths.mdeDirectory.printPaths;
+            throw new InitException ("Paths requested");	// lazy way to stop
+        }
+        debug logger.trace ("Init: resolved paths successfully");
+    
+        /* Load options now. Don't load in a thread since:
+        *   Loading should be fast & most work is probably disk access
+        *   It enables logging to be controlled by options
+        *   It's a really good idea to let the options apply to all other loading */
+        try {
+            Options.load();
+        } catch (optionsLoadException e) {
+            throw new InitException ("Loading options failed: " ~ e.msg);
+        }
+        debug logger.trace ("Init: loaded options successfully");
+        
+	// Set up the logger:
+        Logger root;
+	try {
+            enum LOG {
+                LEVEL	= 0xF,		// mask for log level
+                CONSOLE	= 0x1000,	// log to console?
+                ROLLFILE= 0x2000	// use Rolling/Switching File Appender?
+            }
+            
+	    // Where logging is done to is determined at compile-time, currently just via static ifs.
+            root = Log.getRootLogger();
+	    root.clearAppenders;	// we may no longer want to log to the console
+	    
+            // Now re-set the logging level, using the value from the config file:
+            root.setLevel (cast(Log.Level) (miscOpts.logOptions & LOG.LEVEL), true);
+            
+            // Log to a file (first appender so root seperator messages don't show on console):
+            if (miscOpts.logOptions & LOG.ROLLFILE) {
+                version (SwitchAppender) {
+                    root.addAppender (new SwitchingFileAppender (paths.logDir~"/log-.txt", 5));
+                } else {
+                // Use 2 log files with a maximum size of 1 MB:
+                    root.addAppender (new RollingFileAppender (paths.logDir~"/log-.txt", 2, 1024*1024));
+                    root.info (""); // some kind of separation between runs
+                    root.info ("");
+                }
+            } else if (!(miscOpts.logOptions & LOG.CONSOLE)) {
+                // make sure at least one logger is enabled
+                Options.setInt ("misc", "logOptions", miscOpts.logOptions | LOG.CONSOLE);
+            }
+            if (miscOpts.logOptions & LOG.CONSOLE) {	// Log to the console
+                root.addAppender(new ConsoleAppender);
+            }
+            logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now));
+        } catch (Exception e) {
+            // Presumably it was only adding a file appender which failed; set up a new console
+            // logger and if that fails let the exception kill the program.
+            root.clearAppenders;
+            root.addAppender (new ConsoleAppender);
+            logger.warn ("Exception while setting up the logger; logging to the console instead.");
+        }
+        
+        // a debugging option:
+        imde.run = !args.contains("q") && !miscOpts.exitImmediately;
+        debug logger.trace ("Init: applied pre-init options");
+        
+        //BEGIN Load dynamic libraries
+        /* Can be done by init functions but much neater to do here.
+        * Also means that init functions aren't run if a library fails to load. */
+        try {
+            DerelictSDL.load();
+            // SDLImage was going to be used... for now it isn't because of gl texturing problems
+            //DerelictSDLImage.load();
+            DerelictGL.load();
+            DerelictFT.load();
+        } catch (DerelictException de) {
+            logger.fatal ("Loading dynamic library failed:");
+            logger.fatal (de.msg);
+            
+            throw new InitException ("Loading dynamic libraries failed (see above).");
+        }
+        debug logger.trace ("Init: dynamic libraries loaded");
+        //END Load dynamic libraries
+        //END Pre-init
+        
+        
+        //BEGIN Init (stages init2, init4)
+        /* Call init functions.
+        *
+        * Current method is to try using threads, and on failure assume no threads were actually
+        * created and run functions in a non-threaded manner. */
+        
+        try {
+            if (runStageThreaded (init)) runStageForward (init);
+        }
+        catch (InitStageException) {    // This init stage failed.
+            // FIXME: check DTOR still runs
+            throw new InitException ("An init function failed (see above message(s))");
+        }
+        //END Init
+        
+        debug logger.trace ("Init: done");
+    }
+    
+    /** DTOR - runs cleanup functions. */
+    ~this()
+    {
+        debug logger.trace ("Cleanup: starting");
+        
+        Options.save(); // save options... do so here for now
+        
+        // General cleanup:
+        try {
+            if (runStageThreaded (cleanup)) runStageReverse (cleanup);
+        }
+        catch (InitStageException) {
+            // Nothing else to do but report:
+            logger.error ("One or more cleanup functions failed!");
+        }
+        
+        debug logger.trace ("Cleanup: done");
+    }
+    
+    
+    //BEGIN runStage...
+    private static {
+        /* The following three functions, runStage*, each run all functions in a stage in some order,
+        * catching any exceptions thrown by the functions (although this isn't guaranteed for threads),
+        * and throw an InitStageException on initFailure. */
+    
+        const LOG_IF_MSG = "Init function ";
+        const LOG_CF_MSG = "Cleanup function ";
+        const LOG_F_START = " - running";
+        const LOG_F_END = " - completed";
+        const LOG_F_BAD = " - failed";
+        const LOG_F_FAIL = " - failed: ";
+        /* Runs all functions consecutively, first-to-last.
+        * If any function fails, halts immediately. */
+        void runStageForward (InitStage s) {
+            foreach (func; s.funcs) {
+                if (initFailure) break;
+                try {
+                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
+                    func.func();
+                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
+                } catch (Exception e) {
+                    logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~
+                            ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
+                
+                    setInitFailure();
+                }
+            }
+            
+            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
+        }
+        /* Runs all functions consecutively, last-to-first.
+        * If any function fails, continue until all have been run. */
+        void runStageReverse (InitStage s) {
+            foreach_reverse (func; s.funcs) {
+                try {
+                    debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START);
+                    func.func();
+                    debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
+                } catch (Exception e) {
+                    logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~
+                            ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
+                
+                    setInitFailure();
+                }
+            }
+            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
+        }
+        /* Tries running functions in a threaded way. Returns false if successful, true if not but
+        * functions should be run without threads. */
+        bool runStageThreaded (InitStage s) {
+            if (!miscOpts.useThreads) return true;  // Use unthreaded route instead
+        
+            ThreadGroup tg;
+            try {                           // creating/starting threads could fail
+                tg = new ThreadGroup;
+                foreach (func; s.funcs) {   // Start all threads
+                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
+                    tg.create(func.func);
+                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
+                }
+            } catch (ThreadException e) {   // Problem with threading; try without threads
+                logger.error ("Caught ThreadException while trying to create threads:");
+                logger.error (e.msg);
+                logger.info ("Will disable threads and continue, assuming no threads were created.");
+            
+                Options.setBool("misc", "useThreads", false);   // Disable threads entirely
+                return true;                // Try again without threads
+            }
+        
+            /* Wait for all threads to complete.
+            *
+            * If something went wrong, we still need to do this before cleaning up.
+            */
+            foreach (t; tg) {
+                try {
+                    t.join (true);
+                } catch (Exception e) {
+                    // Relying on catching exceptions thrown by other threads is a bad idea.
+                    // Hence all threads should catch their own exceptions and return safely.
+                
+                    logger.fatal ("Unhandled exception from Init function:");
+                    logger.fatal (e.msg);
+                
+                    setInitFailure ();        // abort (but join other threads first)
+                }
+            }
+            
+            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
+            return false;                   // Done successfully
+        }
+    //END runStage...
+        
+        void printUsage (char[] progName) {
+            Cout ("mde [no version]").newline;
+            Cout ("Usage:").newline;
+            Cout (progName ~ ` [options]`).newline;
+            version(Windows)
+                    Cout (
+`  --base-path path	Use path as the base (install) path (Windows only). It
+			should contain the "data" directory.`).newline;
+            Cout (
+`  --data-path path(s)	Add path(s) as a potential location for data files.
+			First path argument becomes the preffered location to
+			load data files from.
+  --conf-path path(s)	Add path(s) as a potential location for config files.
+			Configuration in the first path given take highest
+			priority.
+  --paths		Print all paths found and exit.
+  --quick-exit, -q	Exit immediately, without entering main loop.
+  --help, -h		Print this message.`).newline;
+        }
+    }
+    
+    debug (mdeUnitTest) unittest {
+        /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid
+        * messing other init/cleanup up. */
+        static InitStage initUT, cleanupUT;
+        
+        static bool initialised = false;
+        static void cleanupFunc1 () {
+            initialised = false;
+        }
+        static void cleanupFunc2 () {
+            assert (initialised == true);
+        }
+                
+        static void initFunc () {
+            initialised = true;
+            cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1");
+            cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2");
+        }
+        
+        initUT.addFunc (&initFunc, "UT init");
+        
+        runStageForward (initUT);
+        assert (initialised);
+        
+        runStageReverse (cleanupUT);
+        assert (!initialised);
+
+        logger.info ("Unittest complete.");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/exception.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,37 @@
+/* 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/>. */
+
+/// Contains the exception classes for Init.
+module mde.setup.exception;
+
+import mde.exception;
+
+/// Thrown when Init fails.
+class InitException : mdeException {
+    char[] getSymbol () {
+        return super.getSymbol ~ ".setup.Init";
+    }
+    
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
+/// Thrown when an init stage fails.
+class InitStageException : InitException {
+    this () {
+        super("");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/init2.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,101 @@
+/* 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/>. */
+
+/** This module is the start of implementing the following:
+*
+* Idea: change import direction so this module adds all init functions. All init functions are
+* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail
+* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g.
+* "out cleanupFunc[]".
+*
+* This should make it much easier to tell what actually happens during init and to order init such
+* that dependencies are honoured.
+*
+* Currently some external modules depend on InitFunctions, while some are set up from here. Once
+* all are set up from here, the Init* modules can be rearranged. */
+/* Idea: go back to init being controlled from elsewhere. Add a function to wait for another init
+ * function to complete (threaded; might need to be done differently for non-threaded). */
+module mde.setup.init2;
+
+import mde.setup.initFunctions;
+
+import tango.util.log.Log : Log, Logger;
+
+// Modules requiring init code running:
+import imde = mde.imde;
+import mde.gui.Gui;
+import mde.input.Input;
+import font = mde.font.font;
+
+// NOTE: error reporting needs a revision
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.setup.init2");
+    
+    init.addFunc (&initInput, "initInput");
+    init.addFunc (&guiLoad, "guiLoad");
+}
+
+void guiLoad () {   // init func
+    try {
+        font.FontStyle.initialize;
+        gui.load (GUI);
+        cleanup.addFunc (&guiSave, "guiSave");
+    } catch (Exception e) {
+        logger.fatal ("guiLoad failed: " ~ e.msg);
+        setInitFailure;
+    }
+}
+void guiSave () {   // cleanup func
+    try {
+        gui.save (GUI);
+    } catch (Exception e) {
+        logger.fatal ("guiSave failed: " ~ e.msg);
+        setInitFailure;
+    }
+}
+private const GUI = "gui";
+
+void initInput () { // init func
+    try {
+        imde.input.loadConfig ();         // (may also create instance)
+        
+        // Quit on escape. NOTE: quit via SDL_QUIT event is handled completely independently!
+        imde.input.addButtonCallback (cast(Input.inputID) 0x0u, delegate void(Input.inputID i, bool b) {
+            if (b) {
+                logger.info ("Quiting...");
+                imde.run = false;
+            }
+        } );
+    } catch (Exception e) {
+        logger.fatal ("initInput failed: " ~ e.msg);
+        setInitFailure;
+    }
+}
+
+/+ Potential wrapper function:
+// Template to call function, catching exceptions:
+void wrap(alias Func) () {
+    try {
+        Func();
+    } catch (Exception e) {
+        logger.fatal (FAIL_MSG);
+        logger.fatal (e.msg);
+        setInitFailure;
+    }
+}
+private const FAIL_MSG = "Unexpected exception caught:";
++/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/initFunctions.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,75 @@
+/* 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/>. */
+
+/** This module is responsible for calling all init functions.
+*
+* It is also responsible for setting up all scheduled functions for now.
+
+* Idea: change import direction so this module adds all init functions. All init functions are
+* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail
+* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g.
+* "out cleanupFunc[]". */
+module mde.setup.initFunctions;
+
+/+ unused
+import tango.util.log.Log : Log, Logger;
+static this() {
+    logger = Log.getLogger ("mde.setup.initFunctions");
+}
+private Logger logger;
++/
+
+void setInitFailure () {    /// Call to indicate failure in an init function
+    initFailure = true;
+}
+
+/** Represents all functions to be called for a particular init stage. */
+struct InitStage
+{
+    struct InitFunction {
+        void delegate() func;       // the actual function
+        char[] name;                // it's name;
+    }
+    
+    /** Add a function to be called during this init stage.
+    *
+    * Called in order added when not threaded (reverse order for cleanup).
+    *
+    * Exceptions should never be thrown, since each function may run as a thread, and catching
+    * thread exceptions is not guaranteed to work. Log a message, call setFailure() and return
+    * instead. */
+    void addFunc (void delegate() f, char[] name) {
+        InitFunction s;
+        s.func = f;
+        s.name = name;
+        funcs ~= s;
+    }
+    void addFunc (void function() f, char[] name) { /// ditto
+        InitFunction s;
+        s.func.funcptr = f;
+        s.name = name;
+        funcs ~= s;
+    }
+    
+    InitFunction[] funcs = [];
+}
+
+InitStage init;     // all functions called during init (all should be thread-safe)
+//FIXME: implement:
+InitStage save;     // all functions to be called to save data (possible to run more than once)
+InitStage cleanup;  // all functions called during cleanup (all should be thread-safe)
+
+package:
+bool initFailure = false;   // set on failure (throwing through threads isn't a good idea)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/paths.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,358 @@
+/* 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;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/sdl.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,195 @@
+/* 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/>. */
+
+/** Just a temporary place to put SDL Init and Video stuff.
+*/
+module mde.setup.sdl;
+
+import mde.setup.initFunctions;
+import mde.input.joystick;
+import mde.lookup.Options;
+import mde.gl.basic;
+import imde = mde.imde;
+
+import tango.util.log.Log : Log, Logger;
+import tango.stdc.stringz;
+
+import derelict.sdl.sdl;
+import derelict.opengl.gl;	// for loading a later gl version
+import derelict.util.exception;
+
+private Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.setup.sdl");
+    
+    init.addFunc (&initSdlAndGl, "initSdlAndGl");
+}
+
+private uint flags = 0;
+
+void initSdlAndGl() {   // init func
+    // Initialise SDL
+    if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_JOYSTICK /+| SDL_INIT_EVENTTHREAD+/)) {
+        logger.fatal ("SDL initialisation failed:");
+        char* msg = SDL_GetError ();
+        logger.fatal (msg ? fromStringz(msg) : "no reason available");
+        
+        setInitFailure ();
+        return;
+    }
+    
+    debug logger.trace ("SDL initialised");
+    
+    // Must be called after SDL has been initialised, so cannot be a separate Init function.
+    openJoysticks ();                   // after SDL init
+    cleanup.addFunc (&cleanupSDL, "cleanupSDL");
+    
+    setupWindow();
+}
+
+void setupWindow() {    // indirect init func (depends on initSdlAndGl)
+    // Window creation flags and size
+    flags = SDL_OPENGL;
+    if (vidOpts.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
+    else flags |= SDL_SWSURFACE;
+    int w, h;
+    if (vidOpts.fullscreen) {
+        flags |= SDL_FULLSCREEN;
+        w = vidOpts.screenW;
+        h = vidOpts.screenH;
+    }
+    else {
+        if (vidOpts.resizable) flags |= SDL_RESIZABLE;
+        if (vidOpts.noFrame) flags |= SDL_NOFRAME;
+        w = vidOpts.windowW;
+        h = vidOpts.windowH;
+    }
+    
+    // OpenGL attributes
+    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,    5);
+    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,  6);
+    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,   5);
+    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,  16);
+    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
+    
+    // Open a window
+    debug logger.trace ("Opening a window (this can crash if the libraries are messed up)");
+    if (SDL_SetVideoMode (w, h, 32, flags) is null) {
+        logger.fatal ("Unable to set video mode:");
+        char* msg = SDL_GetError ();
+        logger.fatal (msg ? fromStringz(msg) : "no reason available");
+        
+        // Print a load of info:
+        logger.info ("Available video modes:");
+        char[128] tmp;
+        SDL_Rect** modes = SDL_ListModes (null, SDL_FULLSCREEN);
+        if (modes is null) logger.info ("None!");
+        else if (modes is cast(SDL_Rect**) -1) logger.info ("All modes are available");
+        else {
+            for (uint i = 0; modes[i] !is null; ++i) {
+                logger.info (logger.format (tmp, "\t{}x{}", modes[i].w, modes[i].h));
+            }
+        }
+    
+        SDL_VideoInfo* vi = SDL_GetVideoInfo ();
+        if (vi !is null) {
+            logger.info ("Video info:");
+            logger.info ("Hardware surface support: "~ (vi.flags & SDL_HWSURFACE ? "yes" : "no"));
+            logger.info (logger.format (tmp, "Video memory: {}", vi.video_mem));
+    
+            if (vi.vfmt !is null) {
+                logger.info ("Best video mode:");
+                logger.info (logger.format (tmp, "Bits per pixel: {}", vi.vfmt.BitsPerPixel));
+            }
+        }
+        
+        setInitFailure ();
+        return;
+    }
+    
+    /* Now (must be done after GL context is created) we can try to load later version.
+     * The initial loading provides opengl 1.1 features.
+     *
+     * 1.4 is now used for glBlendColor (coloured text).
+     *
+     * Currently the latest version used is 1.3; adjust this as necessary. However, before using
+     * features from any OpenGL version > 1.1 a check must be made on what was loaded by calling
+     * DerelictGL.availableVersion(). Note that availableVersion() could be used instead to load
+     * the highest supported version but this way we know what we're getting.
+     */
+    if (DerelictGL.availableVersion < GLVersion.Version13) {
+        logger.fatal ("Required at least OpenGL 1.3");
+        setInitFailure;
+        return;
+    }
+    /+try {
+        DerelictGL.loadVersions(GLVersion.Version14);
+    } catch (SharedLibProcLoadException e) {
+        logger.warn ("Loading OpenGL version 1.4 failed:");
+        logger.warn (e.msg);
+        
+        //NOTE: might be worth guaranteeing a minimal version to save later checks?
+        /+ Do this if you want the program to abort:
+        setInitFailure ();
+        return;
+        +/
+    }+/
+    
+    // OpenGL stuff:
+    glSetup();
+    setProjection (w, h);
+    
+    // Window-manager settings
+    SDL_WM_SetCaption (toStringz ("mde"), null);
+    // SDL_WM_GrabInput (use later)
+}
+
+void resizeWindow (int w, int h) {
+    if (vidOpts.fullscreen) {
+        Options.setInt ("video", "screenW", w);
+        Options.setInt ("video", "screenH", h);
+    } else {
+        Options.setInt ("video", "windowW", w);
+        Options.setInt ("video", "windowH", h);
+    }
+    
+    if (SDL_SetVideoMode (w, h, 32, flags) is null) {
+        logger.fatal ("Unable to reset video mode:");
+        char* msg = SDL_GetError ();
+        logger.fatal (msg ? fromStringz(msg) : "no reason available");
+        
+        imde.run = false;
+    }
+    
+    // Reset the projection and viewport
+    setProjection (w, h);
+}
+
+void cleanupSDL () {    // cleanup func
+    closeJoysticks();
+    SDL_Quit();
+}
+
+
+/** All video options. */
+OptionsVideo vidOpts;
+class OptionsVideo : Options {
+    mixin (impl!("bool fullscreen,hardware,resizable,noFrame; int screenW,screenH,windowW,windowH;"));
+    
+    static this() {
+        vidOpts = new OptionsVideo;
+        Options.addOptionsClass (vidOpts, "video");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/types/Colour.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,50 @@
+/* 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/>. */
+
+/** Contains a basic colour type. */
+module mde.types.Colour;
+
+/// Represent a colour using clamped floats
+struct Colour {
+    /// Returns GL_ONE if total value is nearer white than black, else GL_ZERO.
+    uint nearestGLConst () {
+        return r+g+b >= 1.5f ? 1u : 0u;
+    }
+    
+    float r,g,b;	/// values
+    
+    static {
+        /// Predefined colours
+        const Colour WHITE = { r:1f, g:1f, b:1f };
+        const Colour BLACK = { r:0f, g:0f, b:0f };	/// ditto
+        
+        /// Construct from floats (doesn't clamp, but GL does when values are passed)
+        Colour opCall (float r, float g, float b) {
+            Colour c;
+            c.r = r;
+            c.g = g;
+            c.b = b;
+            return c;
+        }
+        /// Construct from ubytes
+        Colour opCall (ubyte r, ubyte g, ubyte b) {
+            Colour c;
+            c.r = cast(float) r / 255f;
+            c.g = cast(float) g / 255f;
+            c.b = cast(float) b / 255f;
+            return c;
+        }
+    }
+}
--- a/mde/types/basic.d	Fri Jun 27 17:19:46 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/* 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/>. */
-
-/** Contains basic types used by mde. Some may be moved to other modules. */
-module mde.types.basic;
-
-//FIXME: remove import and change types or not?
-import derelict.opengl.gltypes;
-
-/// Represent a colour using clamped floats
-struct Colour {
-    /// Returns GL_ONE if total value is nearer white than black, else GL_ZERO.
-    GLenum nearestGLConst () {
-        return r+g+b >= 1.5f ? GL_ONE : GL_ZERO;
-    }
-    
-    GLclampf r,g,b;	/// values
-    
-    static {
-        /// Predefined colours
-        const Colour WHITE = { r:1f, g:1f, b:1f };
-        const Colour BLACK = { r:0f, g:0f, b:0f };	/// ditto
-        
-        /// Construct from floats (doesn't clamp, but GL does when values are passed)
-        Colour opCall (float r, float g, float b) {
-            Colour c;
-            c.r = r;
-            c.g = g;
-            c.b = b;
-            return c;
-        }
-        /// Construct from ubytes
-        Colour opCall (ubyte r, ubyte g, ubyte b) {
-            Colour c;
-            c.r = cast(float) r / 255f;
-            c.g = cast(float) g / 255f;
-            c.b = cast(float) b / 255f;
-            return c;
-        }
-    }
-}