changeset 53:f000d6cd0f74

Changes to paths, command line arguments and font LCD rendering. Use "./" instead of "" as default install dir on windows. Implemented a command-line argument parser. Changes to LCD filter/render-mode option handling after testing what actually happens. Changed some FontTexture messages and internals.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 05 Jun 2008 17:16:52 +0100
parents 387a80724c35
children c81342b54ef2
files .hgignore codeDoc/jobs.txt data/conf/options.mtt mde/Options.d mde/mde.d mde/resource/FontTexture.d mde/resource/font.d mde/resource/paths.d mde/scheduler/Init.d
diffstat 9 files changed, 267 insertions(+), 156 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Thu Jun 05 17:16:52 2008 +0100
@@ -0,0 +1,4 @@
+syntax: glob
+dsss.last
+bin/*
+dsss_objs/*
--- a/codeDoc/jobs.txt	Mon Jun 02 14:34:24 2008 +0100
+++ b/codeDoc/jobs.txt	Thu Jun 05 17:16:52 2008 +0100
@@ -52,5 +52,3 @@
 
 
 Done (for git log message):
-Options: impl template to ease creating Options sub-classes.
-New OptionsFont class (currently only controls render mode and hinting).
\ No newline at end of file
--- a/data/conf/options.mtt	Mon Jun 02 14:34:24 2008 +0100
+++ b/data/conf/options.mtt	Thu Jun 05 17:16:52 2008 +0100
@@ -4,11 +4,12 @@
 <bool|exitImmediately=false>
 <char[]|L10n="en-GB">
 <int|logLevel=1>
+<int|logOptions=0x1003>
 <double|pollInterval=0.01>
 
 {font}
-<int|lcdFilter=0>
-<int|renderMode=0>
+<int|lcdFilter=2>
+<int|renderMode=0x30000>
 
 {video}
 <bool|noFrame=false>
@@ -16,7 +17,7 @@
 <bool|hardware=false>
 <bool|fullscreen=false>
 <int|screenW=1280>
-<int|windowW=1272>
+<int|windowW=800>
 <int|screenH=1024>
-<int|windowH=993>
+<int|windowH=600>
 
--- a/mde/Options.d	Mon Jun 02 14:34:24 2008 +0100
+++ b/mde/Options.d	Thu Jun 05 17:16:52 2008 +0100
@@ -475,7 +475,7 @@
 /** A home for all miscellaneous options, at least for now. */
 OptionsMisc miscOpts;
 class OptionsMisc : Options {
-    mixin (impl!("bool useThreads, exitImmediately; int logLevel; double pollInterval; char[] L10n;"));
+    mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;"));
     
     static this() {
         miscOpts = new OptionsMisc;
--- a/mde/mde.d	Mon Jun 02 14:34:24 2008 +0100
+++ b/mde/mde.d	Thu Jun 05 17:16:52 2008 +0100
@@ -36,11 +36,10 @@
 import tango.time.Clock;                // Clock.now()
 import tango.util.log.Log : Log, Logger;
 
-int main()
+int main(char[][] args)
 {
     //BEGIN Initialisation
     Logger logger = Log.getLogger ("mde.mde");
-    logger.info ("Starting mde...");
     
     // Create instances now, so they can be used during init (if necessary)
     input = new Input();
@@ -48,7 +47,7 @@
     
     scope Init init;
     try {
-        init = new Init();	// initialisation
+        init = new Init(args);	// initialisation
     } catch (InitException e) {
         logger.fatal ("Initialisation failed: " ~ e.msg);
         return 1;
--- a/mde/resource/FontTexture.d	Mon Jun 02 14:34:24 2008 +0100
+++ b/mde/resource/FontTexture.d	Thu Jun 05 17:16:52 2008 +0100
@@ -32,7 +32,6 @@
 
 import Utf = tango.text.convert.Utf;
 import tango.util.log.Log : Log, Logger;
-import tango.io.Stdout;
 
 private Logger logger;
 static this () {
@@ -74,7 +73,7 @@
     void updateCache (FT_Face face, char[] str, ref TextBlock cache)
     {
         debug scope (failure)
-                logger.warn ("updateCache failed");
+                logger.error ("updateCache failed");
         
         if (cache.cacheVer == cacheVer)	// Existing cache is up-to-date
             return;
@@ -165,7 +164,7 @@
     void drawTextCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y) {
         updateCache (face, str, cache);	// update if necessary
         debug scope (failure)
-                logger.warn ("drawTextCache failed");
+                logger.error ("drawTextCache failed");
         
         glEnable (GL_TEXTURE_2D);
         glEnable(GL_BLEND);
@@ -198,7 +197,7 @@
     
     void addGlyph (FT_Face face, dchar chr) {
         debug scope (failure)
-                logger.warn ("FontTexture.addGlyph failed!");
+                logger.error ("FontTexture.addGlyph failed!");
         
         auto gi = FT_Get_Char_Index (face, chr);
         auto g = face.glyph;
@@ -208,11 +207,6 @@
             throw new fontGlyphException ("Unable to render glyph");
         
         auto b = g.bitmap;
-        if (b.pitch != b.width) {
-            char[128] tmp;
-            logger.warn (logger.format (tmp, "b.pitch is {}, b.width is {}", b.pitch, b.width));
-            //throw new fontGlyphException ("Unsupported freetype bitmap: b.pitch != b.width");
-        }
         glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
         //glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch);
         
@@ -234,7 +228,7 @@
         }
         // 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 font texture.");
+        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");
         
@@ -243,6 +237,7 @@
         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) {
@@ -357,6 +352,9 @@
         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
@@ -380,7 +378,6 @@
         Line line;
         line.yPos = nextYPos;
         line.height = attr.h * EXTRA_H;
-        Stdout.format ("Creating new line of height {}", line.height).newline;
         line.length = attr.w;
         size_t i = 0;
         while (i < lines.length && lines[i].height < line.height) ++i;
@@ -389,7 +386,6 @@
         
         attr.x = 0;	// first glyph in the line
         attr.y = line.yPos;
-        attr.texID = texID;
         return true;
     }
     
--- a/mde/resource/font.d	Mon Jun 02 14:34:24 2008 +0100
+++ b/mde/resource/font.d	Thu Jun 05 17:16:52 2008 +0100
@@ -71,18 +71,20 @@
                 logger.warn (logger.format (tmp, "Using an untested FreeType version: {}.{}.{}", maj, min, patch));
             }
             
-            // Set LCD filtering method (note: FT_Library_SetLcdFilter can still complain with
-            // FT_LcdFilter.FT_LCD_FILTER_NONE).
-            if (fontOpts.lcdFilter && FT_Library_SetLcdFilter(library, cast(FT_LcdFilter)fontOpts.lcdFilter)) {
-                // If setting failed, leave at default (disabled status). Note: it is disabled by
-                // default because the code isn't compiled in by default, to avoid patents.
-                logger.warn ("Bad/unsupported LCD filter option; disabling LCD filtering.");
+            // 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.");
-                Options.setInt ("font", "lcdFilter", FT_LcdFilter.FT_LCD_FILTER_NONE);
-            }
-            const RMF = FT_LOAD_TARGET_MONO | FT_LOAD_TARGET_LCD | FT_LOAD_TARGET_LCD_V;
-            if (fontOpts.renderMode & RMF && fontOpts.lcdFilter == 0) {
-                logger.warn ("Using LCD rendering when LCD filtering is disabled has no effect; disabling.");
+                
+                // 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);
             }
             
--- a/mde/resource/paths.d	Mon Jun 02 14:34:24 2008 +0100
+++ b/mde/resource/paths.d	Thu Jun 05 17:16:52 2008 +0100
@@ -39,8 +39,7 @@
 
 import tango.io.Console;
 import tango.io.FilePath;
-import tango.stdc.stdlib;
-import tango.stdc.stringz;
+import tango.sys.Environment;
 //import tango.scrapple.sys.win32.Registry;     // Trouble getting this to work
 
 /** Order to read files in.
@@ -115,6 +114,15 @@
         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 {
@@ -165,6 +173,14 @@
         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;
@@ -185,49 +201,56 @@
 * Note: the logger cannot be used yet, so only output is exception messages. */
 // FIXME: use tango/sys/Environment.d
 version (linux) {
-    void resolvePaths () {
+    // base-path not used on posix
+    void resolvePaths (char[] = null) {
         // Home directory:
-        char[] HOME = fromStringz (getenv (toStringz ("HOME")));
+        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");
+        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 () {
+    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, "");;
-        PathView userPath = findPath (true, "user");   // FIXME: see above
-        PathView staticPath = findPath (false, "data");
+        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 ("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:
@@ -237,86 +260,98 @@
     static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!");
 }
 
-private:
-// The maximum number of paths for any one "directory".
-// There are NO CHECKS that this is not exceeded.
-const MAX_PATHS = 3;
+/// For command line args: these paths are added if non-null, with highest priority.
+char[] extraDataPath, extraConfPath;
 
-/* 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 ...) {
-    foreach (path; paths) {
-        PathView pv = new FilePath (path);
-        if (pv.exists && pv.isFolder) return pv;    // got a valid path
-    }
-    if (create) {   // try to create a folder, using each path in turn until succesful
-        foreach (path; paths) {
-            FilePath fp = new FilePath (path);
-            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 mdeException (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;
+private {
+    class PathException : mdeException {
+        this(char[] msg) {
+            super (msg);
         }
     }
     
-    DataSet dataset () {                /// Get the DataSet
-        return readers[0].dataset;      // all readers share the same dataset
+// 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);
     }
-    void dataset (DataSet ds) {         /// Set the DataSet
-        for (uint i = 0; i < readersLen; ++i) readers[i].dataset (ds);
-    }
+//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;
+            }
+        }
     
-    void dataSecCreator (IDataSection delegate (ID) dsC) {  /// Set the dataSecCreator
-        for (uint i = 0; i < readersLen; ++i) readers[i].dataSecCreator = dsC;
-    }
+        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. */
+     *
+     * 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 () {                      /// 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;
     }
-    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	Mon Jun 02 14:34:24 2008 +0100
+++ b/mde/scheduler/Init.d	Thu Jun 05 17:16:52 2008 +0100
@@ -34,6 +34,9 @@
 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;
 
@@ -58,37 +61,12 @@
  */
 static this()
 {
-    // Find/create paths:
-    try {
-        paths.resolvePaths();
-    } catch (Exception e) {
-        // NOTE: an exception thrown here cannot be caught by main()!
-        throw new InitException ("Resolving paths failed: " ~ e.msg);
-    }
-    
-    // Set up the logger:
-    {
-        // Where logging is done to is determined at compile-time, currently just via static ifs.
-        Logger root = Log.getRootLogger();
-        
-        static if (true ) { // Log to files (first appender so root seperator messages don't show on console)
-            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 ("");
-            }
-        }
-        static if (true ) { // Log to the console
-            root.addAppender(new ConsoleAppender);
-        }
-        
-        // 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);
-    }
+    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()
 {
@@ -124,28 +102,105 @@
     * 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()
+    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's a really good idea to let the options apply to all other loading. */
+        *   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");
         
-        // Now re-set the logging level, using the value from the config file:
-        Log.getRootLogger.setLevel (cast(Log.Level) miscOpts.logLevel, true);
-        // And set this (debug option):
-        imde.run = !miscOpts.exitImmediately;
-        //END Pre-init
+	// Set up the logger:
+        Logger root;
+	try {
+            enum LOG {
+                LEVEL	= 0x10,		// mask for log level
+                CONSOLE	= 0x1001,	// log to console?
+                ROLLFILE= 0x1002	// 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:
+            Log.getRootLogger.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.");
+        }
         
-        debug logger.trace ("Init: pre-init done");
+        // 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.
@@ -160,9 +215,10 @@
             
             throw new InitException ("Loading dynamic libraries failed (see above).");
         }
+        debug logger.trace ("Init: dynamic libraries loaded");
         //END Load dynamic libraries
+        //END Pre-init
         
-        debug logger.trace ("Init: dynamic libraries loaded");
         
         //BEGIN Init (stages init2, init4)
         /* Call init functions.
@@ -293,8 +349,28 @@
             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;
+        }
     }
-    //END runStage...
     
     debug (mdeUnitTest) unittest {
         /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid