changeset 24:32eff0e01c05

Only locally-changed options are stored in user-config now. Log levels revised. Options sub-classes are handled more generically and can be added without changing the Options class. Options changed at run-time are tracked, and on exit merged with user options and saved. Revised log levels as set out in policies.txt and as used in code. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 27 Mar 2008 16:15:21 +0000
parents 47478557428d
children 2c28ee04a4ed
files codeDoc/jobs.txt codeDoc/options.txt codeDoc/policies.txt mde/SDL.d mde/events.d mde/i18n/I18nTranslation.d mde/input/config.d mde/input/input.d mde/input/joystick.d mde/mde.d mde/mergetag/Reader.d mde/options.d mde/resource/paths.d mde/scheduler/Init.d
diffstat 14 files changed, 257 insertions(+), 217 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Thu Mar 27 10:58:57 2008 +0000
+++ b/codeDoc/jobs.txt	Thu Mar 27 16:15:21 2008 +0000
@@ -6,8 +6,6 @@
 
 
 To do:
-*   Generalise Options subclass handling. Add local versions (store options from user path plus changed options). Add changing functions to change both sets and track whether or not to save at exit. Save from local versions. Update doc file.
-
 Also see todo.txt.
 *   Windows building/compatibility (currently partial)
 *   gdc building/compatibility (wait for tango 0.99.5 release?)
@@ -42,7 +40,3 @@
 
 
 Done (for git log message):
-Implemented drawing a very basic gl box, and only drawing when necessary.
-
-Improvements to window resizing, and gl draws a box as a test.
-Scheduler has "on request" support to redraws only when requested by an event.
--- a/codeDoc/options.txt	Thu Mar 27 10:58:57 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-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, version 2, as published by the Free Software Foundation.
-
-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, write to the Free Software Foundation, Inc.,
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */
-
-
-Possibilities (first is current functionality):
-* Store current options, write at exit
-* Store current options and options changed during run, if any changed: merge with local options and write at exit
-* Store current options and local options, changes affect local options, write at exit if changed
-
-
-Ideas for extending options to track user-changed options separately from system options so as not to override unchanged system options.
-
-I was inteding on implementing this, until I realised the extra complexity necessary. Also the gains in functionality seem tiny and not always desireable.
-
-Extract of doc. plus example:
-
-/** Base class for handling options. This class itself will handle no options and should not be
-* instantiated, but sub-classes handle options.
-*
-* Each sub-class provides named variables for maximal-speed reading; however these cannot be used
-* for changing options due to only changed options being written to the user file.
-*
-* The static class keeps track of all Options class instances for global loading and saving.
-*
-* Details: Options sub-classes hold associative arrays of pointers to all options, with char[] id.
-* When options are changed, they also load all options from the user file to save back there.
-* When an options GUI window is opened, option names and descriptions are loaded using the id and
-* i18n.I18nTranslation (possibly not by the Options class), and possibly also system-level options
-* are loaded to allow users to revert to these.
-*/
-class Options : IDataSection
-{
-    bool example;
-    
-    bool*[char[]] optsBool;
-    bool[char[]] cgdOptsBool;
-    //bool[char[]] sysOptsBool;
-    bool changed; sysLoaded;
-    
-    this () {
-        optsBool = ["example":&example];
-    }
-    
-    ...
-}
-
-From todo.txt:
-Options:
-->  types:
-    ->  bool
-    ->  int
-    ->  char[]
-    ->  float/double/real?
-->  automated saving/loading
-    ->  track whether any options have been changed
-    ->  track all changed options (minus ones reverted to system confif) plus all options loaded from file to save to
-->  symbols (static or associative arrays?)
-    ->  read-only outside of class
-    ->  a merge of "changed" and "system"
-    ->  only part which needs to be loaded when not changing options
-->  "Changed" symbols
-    ->  constists of entries as above (changes above system config)
-    ->  only loaded when an option is first changed?
-    ->  use setter functions
-        ->  change/add/remove from "changed"
-        ->  change base symbols
-->  "System" symbols
-    ->  symbols only loaded from system-level config
-    ->  only to show what the default setting to revert to is (since otherwise when removing a user change its system value wouldn't be known until config is reloaded)
-    ->  only load when required
-->  sections
-    ->  one class per section
-    ->  classes may be derived to provide their own handling
-    ->  use separate files? Otherwise cannot efficiently load options on a per-section status with mtt files.
-->  root or static class
-    ->  parent of all sections
-    ->  handles saving and loading
--- a/codeDoc/policies.txt	Thu Mar 27 10:58:57 2008 +0000
+++ b/codeDoc/policies.txt	Thu Mar 27 16:15:21 2008 +0000
@@ -107,21 +107,19 @@
 Logging should be handled by tango's Logger class. A logger with the name of the form mde.package.module or mde.package.module.X where X is a symbol within the module should be used for each module.
 
 In general the levels should be used as follows:
-	Trace	Where required or thought highly useful for debugging, and only compiled in debugging mode.
-	Info	Sparingly, for informational purposes (e.g. when parsing a file). Should not generally be used repetitively (within loops, etc.). Not for reporting unexpected behaviour.
-	Warn	For small errors which can be overlooked, even if they MAY cause bigger problems later. I.e. something unexpected, but not necessarily a major problem, happens.
-	Error	For errors which directly:
-		• cut-short a (reasonably large) operation (e.g. reading a file).
-		• cause a significant change in program operation, but do not directly cause the program to terminate.
-	Fatal	For errors directly (i.e. definately and almost immediately) ending the program.
+	Trace	Where desired for debugging, and only compiled in debugging mode (i.e. wrap with debug).
+	Info	Sparingly, for informational purposes. Should not be used repetitively (within loops, etc.). Not for reporting unexpected behaviour or tracing program's running, except to the degree an end user may want. Also for unittests. Temporarily for reporting information to the user, until the GUI can be used instead.
+	Warn	For when something unexpected happens which is not necessarily an error (although could be).
+	Error	For anything which is definitely an error but not fatal.
+	Fatal	For errors directly (i.e. definitely) ending the program.
 
-For all levels bar trace, messages should if possible be understandable to end users, while (for warn and above) including enough information to fix the problem when it is due to data files rather than code.
+For all levels except trace, messages should if possible be understandable to end users, while (for warn and above) including enough information to fix the problem when it is due to data files rather than code.
 Thus:
 	Trace output should only be available when compiled in debug mode.
 	When run by an end-user (with info-level logging enabled),
 	• info messages normally occur and should be understandable to end users;
-	• warn messages may occur, and may or may not indicate problems;
-	• error messages indicate that something big is wrong, and if the program still runs it is unlikely to be usable as intended;
+	• warn messages may occur, and may indicate problems;
+	• error messages indicate that something is definitely wrong (even if it only has a minor effect on the program as a whole);
 	• fatal messages indicate a problem preventing the program from running.
 
 Log/exception messages can be divided into two categories: those aimed at end users or modders and those only aimed at developers. A short string, either a brief English message or just a code, should be defined in the code, which can either be translated to a full message by I18nTranslation or output directly. The code string need not be a long description since it can be looked up in the code.
@@ -129,9 +127,11 @@
 
 
 --- Exceptions ---
-Thrown errors should, where documented, be documented with a log message; an exception message may be used to produce the final log message but must be output via a log message.
+Exceptions should only be used for errors (see comment on log levels above). Thus when an exception is caught, by definition an error occured.
 
-Thrown errors should use an exception class specific to at least the package involved to enable specific catching of errors. Exception classes should be defined within a module exception.d in the package directory. Exception classes should generally follow the conventions within mde/exception.d to aid in providing reasonable error messages.
+Thrown exceptions should, where documented, be documented via the logger.
+
+Thrown exceptions should use an exception class specific to at least the package involved to enable specific catching of errors. Exception classes should be defined within a module exception.d in the package directory. Exception classes should generally follow the conventions within mde/exception.d to aid in providing reasonable error messages.
 
 
 
--- a/mde/SDL.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/SDL.d	Thu Mar 27 16:15:21 2008 +0000
@@ -41,7 +41,7 @@
 private uint flags = 0;
 
 void initSdlAndGl() {   // init2 func
-    logger.trace ("init2: initSdlAndGl() started");
+    debug logger.trace ("init2: initSdlAndGl() started");
     
     // Load SDL and GL dynamic libs
     try {
@@ -54,7 +54,7 @@
         setInitFailure ();
         return;
     }
-    logger.trace ("Derelict: loaded SDL and OpenGL");
+    debug logger.trace ("Derelict: loaded SDL and OpenGL");
     
     // Initialise SDL
     if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_JOYSTICK /+| SDL_INIT_EVENTTHREAD+/)) {
@@ -67,37 +67,37 @@
     }
     
     cleanup2.addFunc (&cleanupSDL);
-    logger.trace ("SDL initialised");
+    debug logger.trace ("SDL initialised");
     
     // Must be called after SDL has been initialised, so cannot be a separate Init function.
     openJoysticks ();                   // after SDL init
     cleanup2.addFunc (&closeJoysticks);
 
-    logger.trace ("init2: initSdlAndGl() finished");
+    debug logger.trace ("init2: initSdlAndGl() finished");
 }
 
 version = MDE_OPENGL;
 
 void setupWindow() {    // init4 func
-    logger.trace ("init4: setupWindow() started");
+    debug logger.trace ("init4: setupWindow() started");
     
     // Window creation flags and size
     /* NOTE: I'm getting an API mismatch error from the nvidia driver when using OpenGL,
     * thus I've temporarily disabled it. */
     version (MDE_OPENGL) flags = SDL_OPENGL;
-    if (Options.video.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
+    if (vidOpts.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
     else flags |= SDL_SWSURFACE;
     int w, h;
-    if (Options.video.fullscreen) {
+    if (vidOpts.fullscreen) {
         flags |= SDL_FULLSCREEN;
-        w = Options.video.screenW;
-        h = Options.video.screenH;
+        w = vidOpts.screenW;
+        h = vidOpts.screenH;
     }
     else {
-        if (Options.video.resizable) flags |= SDL_RESIZABLE;
-        if (Options.video.noFrame) flags |= SDL_NOFRAME;
-        w = Options.video.windowW;
-        h = Options.video.windowH;
+        if (vidOpts.resizable) flags |= SDL_RESIZABLE;
+        if (vidOpts.noFrame) flags |= SDL_NOFRAME;
+        w = vidOpts.windowW;
+        h = vidOpts.windowH;
     }
     
     version (MDE_OPENGL) {
@@ -129,16 +129,16 @@
     SDL_WM_SetCaption (toStringz ("mde"), null);
     // SDL_WM_GrabInput (use later)
     
-    logger.trace ("init4: setupWindow() finished");
+    debug logger.trace ("init4: setupWindow() finished");
 }
 
 void resizeWindow (int w, int h) {
-    if (Options.video.fullscreen) {
-        Options.video.screenW = w;
-        Options.video.screenH = h;
+    if (vidOpts.fullscreen) {
+        Options.setInt ("video", "screenW", w);
+        Options.setInt ("video", "screenH", h);
     } else {
-        Options.video.windowW = w;
-        Options.video.windowH = h;
+        Options.setInt ("video", "windowW", w);
+        Options.setInt ("video", "windowH", h);
     }
     
     if (SDL_SetVideoMode (w, h, 32, flags) is null) {
@@ -154,9 +154,9 @@
 }
 
 void cleanupSDL () {    // cleanup2 func
-    logger.trace ("cleanup2: cleanupSDL() started");
+    debug logger.trace ("cleanup2: cleanupSDL() started");
     SDL_Quit();
-    logger.trace ("cleanup2: cleanupSDL() finished");
+    debug logger.trace ("cleanup2: cleanupSDL() finished");
 }
 
     /+ Load of info-printing stuff (currently doesn't have a use)
@@ -184,3 +184,27 @@
     }
     }
     +/
+
+
+/** All video options. */
+OptionsVideo vidOpts;
+class OptionsVideo : Options {
+    alias store!("fullscreen","hardware","resizable","noFrame") BOOL;
+    alias store!("screenW","screenH","windowW","windowH") INT;
+    //alias store!() CHARA;
+    
+    mixin (decBool!(BOOL.a));
+    mixin (decInt!(INT.a));
+    //mixin (decCharA!(CHARA.a));
+    
+    this () {
+        mixin (aaBool!(BOOL.a));
+        mixin (aaInt!(INT.a));
+        //mixin (aaCharA!(CHARA.a));
+    }
+    
+    static this() {
+        vidOpts = new OptionsVideo;
+        Options.addOptionsClass (vidOpts, "video");
+    }
+}
--- a/mde/events.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/events.d	Thu Mar 27 16:15:21 2008 +0000
@@ -39,7 +39,7 @@
 }
 
 void initInput () { // init2 func
-    logger.trace ("init2: initInput() started");
+    debug logger.trace ("init2: initInput() started");
     
     try {
         global.input = new Input();
@@ -48,7 +48,7 @@
         setInitFailure ();                // must clean up properly
     }
     
-    logger.trace ("init2: initInput() finished");
+    debug logger.trace ("init2: initInput() finished");
 }
 
 void pollEvents (double) {
@@ -71,8 +71,8 @@
                 try {
                     global.input (event);
                 } catch (InputClassException e) {
-                    logger.warn ("Caught input exception; event will be ignored. Exception was:");
-                    logger.warn (e.msg);
+                    logger.error ("Caught input exception; event will be ignored. Exception was:");
+                    logger.error (e.msg);
                 }
         }
     }
--- a/mde/i18n/I18nTranslation.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/i18n/I18nTranslation.d	Thu Mar 27 16:15:21 2008 +0000
@@ -94,9 +94,9 @@
     {
         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) Options.misc.L10n];  // start by loading the current locale
+        = [cast(ID) miscOpts.L10n];  // start by loading the current locale
         
-        I18nTranslation transl = new I18nTranslation (name, Options.misc.L10n);
+        I18nTranslation transl = new I18nTranslation (name, miscOpts.L10n);
         
         IReader reader;
         try {
@@ -153,8 +153,8 @@
             char[][] fields = split (stripBrackets (dt));
             
             if (fields.length < 1) {
-                // This tag is invalid, but since we don't want execution to halt just log a warning:
-                logger.warn ("For name "~name~", L10n "~L10n~": tag with ID "~cast(char[])id~" has no data");
+                // 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
@@ -202,9 +202,6 @@
     //END Data
     
     debug (mdeUnitTest) unittest {
-        // This gets used before it is normally created (test incase this changes).
-        if (Options.misc is null) Options.misc = new OptionsMisc;
-        
         /* Relies on file: conf/L10n/i18nUnitTest.mtt
         * Contents:
         *********
@@ -219,8 +216,8 @@
         
         // Hack a specific locale...
         // Also to allow unittest to run without init.
-        char[] currentL10n = Options.misc.L10n;
-        Options.misc.L10n = "test-1";
+        char[] currentL10n = miscOpts.L10n;
+        miscOpts.L10n = "test-1";
         
         I18nTranslation transl = load ("i18nUnitTest");
         
@@ -239,7 +236,7 @@
         // Only check extra entries are allowed but ignored.
         
         // Restore
-        Options.misc.L10n = currentL10n;
+        miscOpts.L10n = currentL10n;
         
         logger.info ("Unittest complete.");
     }
--- a/mde/input/config.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/input/config.d	Thu Mar 27 16:15:21 2008 +0000
@@ -155,7 +155,7 @@
             else		file.read();			// otherwise read all
         }
         catch (MT.MTException) {
-            logger.error ("Unable to load configs from: " ~ filename);
+            logger.fatal ("Unable to load configs from: " ~ filename);
             throw new ConfigLoadException;
         }
         
@@ -164,7 +164,7 @@
         foreach (i, sec; file.dataset.sec) {
             Config c = cast(Config) sec;
             if (c) configs[i] = c;		// Check, because we don't want null entries in configs
-            else debug logger.warn ("Ended up with DataSection of wrong type; this should never happen.");
+            else debug logger.error ("Ended up with DataSection of wrong type; this should never happen.");
         }
         
         debug (MDE_CONFIG_DUMP) {
--- a/mde/input/input.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/input/input.d	Thu Mar 27 16:15:21 2008 +0000
@@ -301,7 +301,7 @@
             config = *c_p;
             return false;
         }
-        logger.warn ("Config profile \""~profile~"\" not found: input won't work unless a valid profile is loaded!");
+        logger.error ("Config profile \""~profile~"\" not found: input won't work unless a valid profile is loaded!");
         return true;
     }
     
--- a/mde/input/joystick.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/input/joystick.d	Thu Mar 27 16:15:21 2008 +0000
@@ -41,7 +41,7 @@
     
     for (int i = 0; i < joysticks.length; ++i) {
         if ((joysticks[i] = SDL_JoystickOpen (i)) is null) {	// null on failure
-            logger.warn (logger.format (tmp, "Unable to open joystick {} via SDL", i));
+            logger.error (logger.format (tmp, "Unable to open joystick {} via SDL", i));
         }
     }
     
--- a/mde/mde.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/mde.d	Thu Mar 27 16:15:21 2008 +0000
@@ -22,7 +22,6 @@
 // Package imports
 import global = mde.global;
 import mde.events;
-import mde.i18n.I18nTranslation;        // greeting message
 import mde.exception;
 
 import mde.SDL; // This module is ONLY imported because otherwise it wouldn't be compiled in
@@ -62,10 +61,6 @@
     } );
     //END Initialisation
     
-    /* Log a greeting message. Just a little test really, but it can stay until i18n finds a proper use. */
-    I18nTranslation transl = I18nTranslation.load ("mde");
-    logger.info (transl.getEntry ("greeting"));
-    
     while (global.run) {
         Scheduler.run (Clock.now());
         
--- a/mde/mergetag/Reader.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/mergetag/Reader.d	Thu Mar 27 16:15:21 2008 +0000
@@ -235,7 +235,7 @@
         // Remember the file name so that we can report errors (somewhat) informatively:
         ErrFile = path.path ~ path.file;
         ErrInFile = " in \"" ~ ErrFile ~ '"';
-                
+        
         // Version checking & matching header section tag:
         if (fbuf.length < 6 || fbuf[0] != '{' || fbuf[1] != 'M' || fbuf[2] != 'T' || fbuf[5] != '}')
             throwMTErr("Not a valid MergeTag text file" ~ ErrInFile, new MTFileFormatException);
@@ -431,8 +431,8 @@
                         dsec.addTag (type, tagID, data);
                     }
                     catch (TextException e) {
-                        logger.warn ("TextException while reading " ~ ErrFile ~ ":");	// following a parse error
-                        logger.warn (e.msg);
+                        logger.error ("TextException while reading " ~ ErrFile ~ ":");	// following a parse error
+                        logger.error (e.msg);
                     }
                     catch (Exception e) {
                         logger.error ("Unknown error occured" ~ ErrInFile ~ ':');
--- a/mde/options.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/options.d	Thu Mar 27 16:15:21 2008 +0000
@@ -32,19 +32,25 @@
 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 will handle no options and should not be
-* instantiated, but sub-classes handle options.
+/** Base class for handling options.
 *
-* Each sub-class provides named variables for maximal-speed reading & writing. Sub-class references
-* are stored by name, and will be available after pre-init has run (DO NOT access before this).
+* 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.
 *
-* The static class keeps track of all Options class instances for global loading and saving.
+* 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(...).
 *
 * 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
 {
@@ -75,56 +81,122 @@
         foreach (ID id, int*    val; optsInt)   dlg ("int"   , id, parseFrom!(int   ) (*val));
     }
     //END Mergetag loading/saving code
-        
+    
     //BEGIN Static
-    // Each individual section
-    static OptionsMisc  misc;
-    static OptionsVideo video;
+    /** 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 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 () {
-        // Create all uncreated sections now, so that if we return early they are still created.
-        if (misc is null) misc = new OptionsMisc;
-        if (video is null) video = new OptionsVideo;
-                
         // 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;
         
-        IReader reader;
         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. */
-                
-                if (id == cast(ID) "misc") return misc;
-                else if (id == cast(ID) "video") return video;
+                Options* p = id in subClasses;
+                if (p !is null) return *p;
                 else return null;
             };
             reader.read;
         } catch (MTException e) {
-            logger.error ("Mergetag exception occurred:");
-            logger.error (e.msg);
+            logger.fatal (MT_LOAD_EXC);
+            logger.fatal (e.msg);
             throw new optionsLoadException ("Loading aborted: mergetag exception");
         }
     }
     static void save () {
+        if (!changed) return;   // no changes to save
+        
         DataSet ds = new DataSet();
-        ds.sec[cast(ID) "misc"] = misc;
-        ds.sec[cast(ID) "video"] = video;
+        foreach (id, sec; subClassChanges) ds.sec[id] = sec;
         
-        IWriter writer;
+        // 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 ("Mergetag exception occurred; saving aborted:");
+            logger.error ("Saving options aborted! Reason:");
             logger.error (e.msg);
-            //FIXME: currently nothing done besides logging the error
         }
     }
     
@@ -167,14 +239,75 @@
     //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;
+    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 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, copy and paste, and update Options.
+* 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 {
     alias store!("useThreads") BOOL;
     alias store!("logLevel") INT;
@@ -189,21 +322,9 @@
         mixin (aaInt!(INT.a));
         mixin (aaCharA!(CHARA.a));
     }
-}
-
-/** All video options. */
-class OptionsVideo : Options {
-    alias store!("fullscreen","hardware","resizable","noFrame") BOOL;
-    alias store!("screenW","screenH","windowW","windowH") INT;
-    //alias store!() CHARA;
     
-    mixin (decBool!(BOOL.a));
-    mixin (decInt!(INT.a));
-    //mixin (decCharA!(CHARA.a));
-    
-    this () {
-        mixin (aaBool!(BOOL.a));
-        mixin (aaInt!(INT.a));
-        //mixin (aaCharA!(CHARA.a));
+    static this() {
+        miscOpts = new OptionsMisc;
+        Options.addOptionsClass (miscOpts, "misc");
     }
 }
--- a/mde/resource/paths.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/resource/paths.d	Thu Mar 27 16:15:21 2008 +0000
@@ -49,7 +49,9 @@
 
 /** This struct has one instance for each "directory".
 *
-* It is the only item within this module that you should need to interact with. */
+* 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.
@@ -114,8 +116,8 @@
                 paths[pathsLen++] = fp.toString~'/';
                 return true;
             } catch (Exception e) {
-                logger.warn ("Creating path "~path~" failed:");
-                logger.warn (e.msg);
+                logger.error ("Creating path "~path~" failed:");
+                logger.error (e.msg);
             }
         }
         return false;
@@ -142,31 +144,26 @@
 
 version (linux) {
     void resolvePaths () {
-        logger.trace ("1");
         // Home directory:
         char[] HOME = fromStringz (getenv (toStringz ("HOME")));
         
-        logger.trace ("3");
         // 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");
         
-        logger.trace ("5");
         // Static data paths:
         dataDir.addPath (staticPath.toString);      // we know this is valid anyway
         dataDir.tryPath (userPath.toString ~ DATA);
         if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!");
         
-        logger.trace ("7");
         // Configuration paths:
         confDir.tryPath (staticPath.toString ~ CONF);
-        bool sysConf = confDir.tryPath ("/etc/mde");
-        confDir.tryPath (userPath.toString ~ CONF, !sysConf);
+        confDir.tryPath ("/etc/mde");
+        confDir.tryPath (userPath.toString ~ CONF, true);
         if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!");
         
-        logger.trace ("9");
         // Logging path:
         logDir = userPath.toString;
     }
--- a/mde/scheduler/Init.d	Thu Mar 27 10:58:57 2008 +0000
+++ b/mde/scheduler/Init.d	Thu Mar 27 16:15:21 2008 +0000
@@ -111,7 +111,7 @@
     */
     this()
     {
-        logger.trace ("Init: starting");
+        debug logger.trace ("Init: starting");
         
         //BEGIN Pre-init (stage init0)
         /* Load options now. Don't load in a thread since:
@@ -125,7 +125,7 @@
         }
         
         // Now re-set the logging level:
-        Log.getRootLogger.setLevel (cast(Log.Level) Options.misc.logLevel, true);  // set the stored log level
+        Log.getRootLogger.setLevel (cast(Log.Level) miscOpts.logLevel, true);  // set the stored log level
         //END Pre-init
         
         
@@ -138,11 +138,11 @@
         // init2
         cleanupStages ~= &cleanup2;     // add appropriate cleanup stage
         try {
-            logger.trace ("Init: init2");
+            debug logger.trace ("Init: init2");
             if (runStageThreaded (init2)) runStageForward (init2);
         }
         catch (InitStageException) {    // This init stage failed.
-            logger.trace ("Init: init2 failed");
+            debug logger.trace ("Init: init2 failed");
             runCleanupStages();
             throw new InitException ("Initialisation failed during stage init2");
         }
@@ -150,23 +150,23 @@
         // init4
         cleanupStages ~= &cleanup4;     // add appropriate cleanup stage
         try {
-            logger.trace ("Init: init4");
+            debug logger.trace ("Init: init4");
             if (runStageThreaded (init4)) runStageForward (init4);
         }
         catch (InitStageException) {    // This init stage failed.
-            logger.trace ("Init: init4 failed");
+            debug logger.trace ("Init: init4 failed");
             runCleanupStages();
             throw new InitException ("Initialisation failed during stage init4");
         }
         //END Init
         
-        logger.trace ("Init: done");
+        debug logger.trace ("Init: done");
     }
     
     /** DTOR - runs cleanup functions. */
     ~this()
     {
-        logger.trace ("Cleanup: starting");
+        debug logger.trace ("Cleanup: starting");
         
         // cleanup1:
         Options.save(); // save options... do so here for now
@@ -174,7 +174,7 @@
         // cleanup2, 4:
         runCleanupStages();
         
-        logger.trace ("Cleanup: done");
+        debug logger.trace ("Cleanup: done");
     }
     
     
@@ -220,18 +220,18 @@
         /* 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 (!Options.misc.useThreads) return true;  // Use unthreaded route instead
+            if (!miscOpts.useThreads) return true;  // Use unthreaded route instead
         
             ThreadGroup tg;
             try {                           // creating/starting threads could fail
                 tg = new ThreadGroup;
                 foreach (func; s.funcs) tg.create(func);  // Start all threads
             } catch (ThreadException e) {   // Problem with threading; try without threads
-                logger.warn ("Caught ThreadException while trying to create threads:");
-                logger.warn (e.msg);
-                logger.warn ("Will continue in a non-threaded manner.");
+                logger.error ("Caught ThreadException while trying to create threads:");
+                logger.error (e.msg);
+                logger.info ("Will disable threads and continue.");
             
-                Options.misc.useThreads = false;    // Disable threads entirely
+                Options.setBool("misc", "useThreads", false);   // Disable threads entirely
                 return true;                // Try again without threads
             }