changeset 137:9f035cd139c6

BIG commit. Major change: old Options class is gone, all content values are loaded and saved automatically. All options updated to reflect this, some changed. Content restrutured a lot: New IContent module, Content module includes more functionality. New ContentLoader module to manage content loading/saving/translation. Translation module moved to content dir and cut down to reflect current usage. File format unchanged except renames: FontOptions -> Font, VideoOptions -> Screen. Font render mode and LCD filter options are now enums. GUI loading needs to create content (and set type for enums), but doesn't save/load value. Some setup of mainSchedule moved to mde.mainLoop. Content callbacks are called on content change now. ContentLists are set up implicitly from content symbols. Not as fast but much easier! Bug-fix in the new MTTagReader. Renamed MT *Reader maker functions to avoid confusion in paths.d. New mde.setup.logger module to allow logger setup before any other module's static this().
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 07 Feb 2009 12:46:03 +0000
parents 4084f07f2c7a
children 3468e9bfded1
files codeDoc/debugCodes.txt codeDoc/ideas.txt data/L10n/en-GB.mtt data/conf/guiDemo.mtt data/conf/options.mtt examples/guiDemo.d mde/content/AStringContent.d mde/content/Content.d mde/content/ContentLoader.d mde/content/IContent.d mde/content/Items.d mde/content/Translation.d mde/content/ValueCache.d mde/content/miscContent.d mde/exception.d mde/file/mergetag/MTTagReader.d mde/file/mergetag/MTTagUnittest.d mde/file/mergetag/MTTagWriter.d mde/file/mergetag/Reader.d mde/file/mergetag/Writer.d mde/file/mergetag/mdeUT.d mde/file/paths.d mde/font/FontTexture.d mde/font/font.d mde/gui/WidgetDataSet.d mde/gui/WidgetManager.d mde/gui/widget/AChildWidget.d mde/gui/widget/AParentWidget.d mde/gui/widget/Floating.d mde/gui/widget/Ifaces.d mde/gui/widget/PopupMenu.d mde/gui/widget/contentFunctions.d mde/gui/widget/layout.d mde/gui/widget/miscContent.d mde/gui/widget/miscWidgets.d mde/imde.d mde/lookup/Options.d mde/lookup/Translation.d mde/mainLoop.d mde/mde.d mde/menus.d mde/setup/Init.d mde/setup/Screen.d mde/setup/logger.d mde/workaround2371.d
diffstat 45 files changed, 1116 insertions(+), 1301 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/debugCodes.txt	Sun Feb 01 12:36:21 2009 +0000
+++ b/codeDoc/debugCodes.txt	Sat Feb 07 12:46:03 2009 +0000
@@ -14,3 +14,8 @@
 drawGlyphCache          Draw the font texture in the upper-left corner of the screen, with a pretty background.
 mdeWidgets              Log trace messages for the creation of all widgets.
 SDLCalls		Log a message before some SDL calls.
+
+Version identifies:
+
+- code -		- purpose -
+mdeBenchmark		Runs main loop as fast as possible forcing continual drawing, and prints framerate every 5 seconds.
--- a/codeDoc/ideas.txt	Sun Feb 01 12:36:21 2009 +0000
+++ b/codeDoc/ideas.txt	Sat Feb 07 12:46:03 2009 +0000
@@ -32,3 +32,6 @@
 
 Content:
 ->  Per-content undo support?
+
+
+Extend content with a validator function/delegate, specific to each class, which takes the new value and returns it or a corrected version of it. Not so good to do it generally from Content, since setting a new value via usual method will re-trigger validator and callbacks (e.g. bad validator could cause infinite loop).
--- a/data/L10n/en-GB.mtt	Sun Feb 01 12:36:21 2009 +0000
+++ b/data/L10n/en-GB.mtt	Sat Feb 07 12:46:03 2009 +0000
@@ -1,19 +1,25 @@
 {MT01}
 !{en-GB British English}
-{imde}
+{menus}
 <entry|main={0:"File",1:"Main MDE menu"}>
-<entry|quit={0:"Quit"}>
+<entry|main.quit={0:"Quit"}>
 <entry|debug={0:"Debug",1:"Debug-mode menu of debugging functions"}>
-{Options}
-<entry|Options={0:"Options"}>
-{FontOptions}
-<entry|FontOptions={0:"Font options"}>
-<entry|lcdFilter={0:"LCD filtering",1:"Enable or disable sub-pixel rendering. Note that the FreeType library may be compiled without support due to patent issues."}>
-<entry|renderMode={0:"Font rendering mode",1:"Controls how fonts are rendered: in gray-scale, or for LCDs (with a horizontal (usual) or vertical layout, with an RGB (usual) or BGR sub-pixel mode."}>
+<entry|debug.guiDemo={0:"GUI Demo"}>
+{Font}
+<entry|lcdFilter={0:"LCD filtering",1:"Filtering used with LCD rendering."}>
+<entry|lcdFilter.none={0:"None",1:"Leaves big colour fringes."}>
+<entry|lcdFilter.default={0:"Default",1:"Reduces colour fringes but may blur fonts slightly."}>
+<entry|lcdFilter.light={0:"Light",1:"Blurs fonts less than default."}>
+<entry|mode={0:"Font render mode",1:"Controls how fonts are rendered. Your FreeType library may be compiled without support for LCD rendering due to patent issues."}>
+<entry|mode.normal={0:"Normal",1:"The default (gray) render mode"}>
+<entry|mode.light={0:"Light",1:"Rendering is the same as normal but hinting is lighter"}>
+<entry|mode.lcd={0:"LCD (RGB)",1:"For common LCD displays. LCD-rendering may not be supported."}>
+<entry|mode.lcd_v={0:"LCD (vertical, RGB)",1:"For LCDs with vertically stacked sub-pixels."}>
+<entry|mode.lcd_bgr={0:"LCD (BGR)",1:"An alternative sub-pixel ordering used in some LCD displays."}>
+<entry|mode.lcd_bgr_v={0:"LCD (vertical, BGR)"}>
 <entry|defaultFont={0:"Default font",1:"Filename of default font"}>
 <entry|defaultSize={0:"Default size",1:"Size for default font"}>
 {MiscOptions}
-<entry|MiscOptions={0:"Miscellaneous options"}>
 <entry|maxThreads={0:"Max threads",1:"Maximum number of threads to use in mde (currently only applies to init stages run in parallel)."}>
 <entry|logLevel={0:"Logging level",1:"Log messages of this level and higher."}>
 <entry|logLevel.Trace={0:"Trace"}>
@@ -23,11 +29,14 @@
 <entry|logLevel.Fatal={0:"Fatal"}>
 <entry|logLevel.None={0:"None"}>
 <entry|logOutput={0:"Logging output",1:"Output to: 0=nowhere, 1=the console, 2=a file, 3=both"}>
-<entry|L10n={0:"Localisation",1:"Specifies the language to use."}>
+<entry|logOutput.none={0:"None"}>
+<entry|logOutput.console={0:"Console"}>
+<entry|logOutput.file={0:"File"}>
+<entry|logOutput.both={0:"Console and file"}>
+<entry|l10n={0:"Localisation",1:"Specifies the language to use."}>
 <entry|pollInterval={0:"Polling interval",1:"Delay in main loop to limit CPU usage"}>
 <entry|exitImmediately={0:"Exit immediately",1:"Load files and exit immediately, without running main loop (for debugging)"}>
-{VideoOptions}
-<entry|VideoOptions={0:"Video options"}>
+{Screen}
 <entry|fullscreen={0:"Fullscreen",1:"If true use the whole screen, if false use a window."}>
 <entry|hardware={0:"Hardware",1:"Create the video surface in hardware or software memory."}>
 <entry|resizable={0:"Resizable",1:"In windowed mode, allow the window to be resized by the window manager."}>
@@ -36,8 +45,11 @@
 <entry|screenH={0:"Screen height",1:"Vertical resolution (fullscreen mode)."}>
 <entry|windowW={0:"Window width",1:"Horizontal size (windowed mode)."}>
 <entry|windowH={0:"Window height",1:"Vertical size (windowed mode)."}>
-{dynamic}
-<entry|switch={0:"Options"}>
-<entry|switch.misc={0:"Miscellaneous"}>
-<entry|switch.video={0:"Video"}>
-<entry|switch.font={0:"Font"}>
+{}
+<entry|Font={0:"Font options"}>
+<entry|MiscOptions={0:"Miscellaneous options"}>
+<entry|Screen={0:"Video options"}>
+<entry|gui.switch={0:"Options"}>
+<entry|gui.switch.misc={0:"Miscellaneous"}>
+<entry|gui.switch.video={0:"Video"}>
+<entry|gui.switch.font={0:"Font"}>
--- a/data/conf/guiDemo.mtt	Sun Feb 01 12:36:21 2009 +0000
+++ b/data/conf/guiDemo.mtt	Sat Feb 07 12:46:03 2009 +0000
@@ -7,20 +7,20 @@
 <WidgetData|bar={0:[0x4100,14,1,2],1:["menuContent","blank"]}>
 <WidgetData|blank={0:[0x2]}>
 
-<WidgetData|menuContent={0:[0x2031],1:["imde.menus","menus"]}>
+<WidgetData|menuContent={0:[0x2031],1:["menus","menus"]}>
 <WidgetData|menus={0:[0x4110,12]   ,1:["menuPopup"]}>
 <WidgetData|menuPopup={0:[0x6033,0],1:["menuList"]}>
 <WidgetData|menuList={0:[0x6030,0] ,1:["menuPopup"]}>
 
-<EnumContent|switch={0:["misc","video","font"],1:0}>
-<WidgetData|options={0:[0x2031],1:["dynamic.switch","switchL"]}>
+<EnumContent|gui.switch=["misc","video","font"]>
+<WidgetData|options={0:[0x2031],1:["gui.switch","switchL"]}>
 <WidgetData|switchL={0:[0x4100,4,2,1],1:["switchVal","switchT"]}>
 <WidgetData|switchVal={0:[0x4100,4,1,2],1:["optName","optVal"]}>
 <WidgetData|switchT={0:[0x4210],1:["optMisc","optVideo","optFont"]}>
 
-<WidgetData|optMisc={0:[0x2031],1:["Options.MiscOptions","optSec"]}>
-<WidgetData|optVideo={0:[0x2031],1:["Options.VideoOptions","optSec"]}>
-<WidgetData|optFont={0:[0x2031],1:["Options.FontOptions","optSec"]}>
+<WidgetData|optMisc={0:[0x2031],1:["MiscOptions","optSec"]}>
+<WidgetData|optVideo={0:[0x2031],1:["Screen","optSec"]}>
+<WidgetData|optFont={0:[0x2031],1:["Font","optSec"]}>
 
 !{use optBox for no description, optDBox for descriptions under entries}
 <WidgetData|optSec={0:[0x4110,0],1:["optBox"]}>
--- a/data/conf/options.mtt	Sun Feb 01 12:36:21 2009 +0000
+++ b/data/conf/options.mtt	Sat Feb 07 12:46:03 2009 +0000
@@ -2,19 +2,19 @@
 {MiscOptions}
 <int|maxThreads=1>
 <bool|exitImmediately=false>
-<char[]|L10n="en">
-<int|logLevel=1>
-<int|logOutput=3>
+<char[]|l10n="en">
+<enumVal|logLevel="Info">
+<enumVal|logOutput="Both">
 <double|pollInterval=0.01>
 
-{FontOptions}
-<int|lcdFilter=2>
-<int|renderMode=0x30000>
+{Font}
+<enumVal|lcdFilter="default">
+<enumVal|mode="lcd">
 !<char[]|defaultFont="n019003l.pfb">
 <char[]|defaultFont="DejaVuSans.ttf">
 <int|defaultSize=16>
 
-{VideoOptions}
+{Screen}
 <bool|noFrame=false>
 <bool|resizable=true>
 <bool|hardware=false>
--- a/examples/guiDemo.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/examples/guiDemo.d	Sat Feb 07 12:46:03 2009 +0000
@@ -16,11 +16,10 @@
 /** Main module for a gui demo & testing executable. */
 module examples.guiDemo;
 
-import mde.imde;                        // this module's interface for external modules
-import mde.menus;		// add default menus
-import mde.events;      // pollEvents() // NOTE: Must be imported before Init, otherwise fonts don't display properly (why??)
+import mde.imde;
+import mde.mainLoop;			// Some setup for the main loop
+import mde.menus;			// add default menus
 import mde.setup.Init;                  // initialization
-import mde.lookup.Options;              // pollInterval option
 import mde.scheduler.Scheduler;         // mainSchedule
 import mde.setup.Screen;                // Screen.draw()
 import mde.setup.InitStage;             // StageState
@@ -29,10 +28,6 @@
 import tango.core.Thread : Thread;	// Thread.sleep()
 import tango.time.Clock;                // Clock.now()
 import tango.util.log.Log : Log, Logger;
-debug (mdeUnitTest) {
-    import mde.file.ssi;
-    import mde.file.mergetag.mdeUT;
-}
 
 int main(char[][] args)
 {
@@ -57,21 +52,12 @@
     
     scope Init init = new Init(args);	// initialize mde
     
-    //BEGIN Main loop setup
-    /* Note: the main loop is currently controlled by the scheduler. This is not really ideal,
-     * since it provides no direct control of the order in which components are executed and does
-     * not allow running components simultaeneously with threads.
-     * Note: probably drawing should start at the beginning of the loop and glFlush()/swapBuffers
-     * be called at the end to optimise. */
     mainSchedule.add (SCHEDULE.DRAW, &Screen.draw).request = true;      // Draw, per event and first frame only.
-    mainSchedule.add (mainSchedule.getNewID, &mde.events.pollEvents).frame = true;
-    //END Main loop setup
     
-    double pollInterval = miscOpts.pollInterval();
     while (run) {
 	mainSchedule.execute (Clock.now());
 	
-	Thread.sleep (pollInterval);	// sleep this many seconds
+        Thread.sleep (mainInterval);	// sleep this many seconds
     }
     
     return 0;		// cleanup handled by init's DTOR
--- a/mde/content/AStringContent.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/content/AStringContent.d	Sat Feb 07 12:46:03 2009 +0000
@@ -29,32 +29,14 @@
     logger = Log.getLogger ("mde.content.AStringContent");
 }
 
-// Used by Options
-template ContentN(T) {
-    static if (is(T == bool)) {
-	const char[] ContentN = "BoolContent";
-    } else static if (is(T == int)) {
-	const char[] ContentN = "IntContent";
-    } else static if (is(T == double)) {
-	const char[] ContentN = "DoubleContent";
-    } else static if (is(T == char[])) {
-	const char[] ContentN = "StringContent";
-    } else
-	static assert (false, "No Content of type "~T.stringof);
-}
-
-/** Base class for content containing a simple value editable as text. All content types used by
- * Options extend this class.
+/** Base class for content containing a simple value editable as text.
  *
- * All derived classes should have the following functions:
- * ---
- *  char[] endEdit ();	// Should convert sv and assign to self, then call endEvent
- * // Used by Options:
- *  BoolContent changeCb (void delegate (char[] symbol,T value) cb);	// The callback used by Options
- *  void assignNoCb (T val);	// assign val, but without calling callbacks
- * ---
- * On any assignation (by this, assignNoCb, opAssign) the value should be converted to a string and
- * assigned to sv, and pos should be clamped to [0,sv.length] (i.e. enforce pos <= sv.length). */
+ * Derived classes should implement endEdit to convert sv and assign its value
+ * to self, then call endEvent.
+ *
+ * On assignation by this or opAssign the value should be converted to a string
+ * and assigned to sv, and pos should be clamped to [0,sv.length] (i.e. enforce
+ * pos <= sv.length). */
 abstract class AStringContent : Content
 {
     protected this (char[] symbol) {
@@ -150,23 +132,24 @@
 class BoolContent : AStringContent
 {
     /** Create a content with _symbol name symbol. */
-    this (char[] symbol, bool val = false) {
-	assignNoCb (val);
+    this (char[] symbol) {
+        auto valp = symbol in changed.boolData;
+        if (valp)
+            v = *valp;
+        sv = v ? "true" : "false";
 	super (symbol);
     }
-    // for use by EnumValueContent
-    protected this (char[] symbol, int dummy) {
-        super (symbol);
-    }
     
-    void assignNoCb (bool val) {
+    // Assign without adding change to save changeset
+    void assignNoCng (bool val) {
 	v = val;
 	sv = v ? "true" : "false";
 	if (pos > sv.length) pos = sv.length;
+        endEvent;
     }
     void opAssign (bool val) {
-	assignNoCb (val);
-	endEvent;
+	assignNoCng (val);
+        endCng;
     }
     bool opCall () {
         return v;
@@ -177,9 +160,15 @@
 	v = sv && (sv[0] == 't' || sv[0] == 'T' || sv[0] == '1');
         sv = v ? "true" : "false";
         endEvent;
+        endCng;
 	return sv;
     }
     
+    // Add change to changeset
+    void endCng () {
+        changed.boolData[symbol] = v;
+    }
+    
 protected:
     bool v;
 }
@@ -187,18 +176,21 @@
 /** Text content. */
 class StringContent : AStringContent
 {
-    this (char[] symbol, char[] val = null) {
-        v = val;
-	super (symbol);
+    this (char[] symbol) {
+        auto valp = symbol in changed.charAData;
+        if (valp)
+            v = *valp;
+        super (symbol);
     }
     
-    void assignNoCb (char[] val) {
+    void assignNoCng (char[] val) {
 	v = val;
 	if (pos > sv.length) pos = sv.length;
+        endEvent;
     }
     void opAssign (char[] val) {
-	assignNoCb (val);
-	endEvent;
+	assignNoCng (val);
+        endCng;
     }
     char[] opCall () {
         return v;
@@ -207,9 +199,14 @@
     
     override char[] endEdit () {
 	endEvent;
+        endCng;
 	return sv;
     }
     
+    void endCng () {
+        changed.charAData[symbol] = v;
+    }
+    
 protected:
     alias sv v;		// don't need separate v and sv in this case
 }
@@ -218,19 +215,23 @@
 class IntContent : AStringContent
 {
     /** Create a content with _symbol name symbol. */
-    this (char[] symbol, int val = 0) {
-        assignNoCb (val);
+    this (char[] symbol) {
+        auto valp = symbol in changed.intData;
+        if (valp)
+            v = *valp;
+        sv = Int.toString (v);
 	super (symbol);
     }
     
-    void assignNoCb (int val) {
+    void assignNoCng (int val) {
 	v = val;
 	sv = Int.toString (v);
 	if (pos > sv.length) pos = sv.length;
+        endEvent;
     }
     void opAssign (int val) {
-	assignNoCb (val);
-	endEvent;
+	assignNoCng (val);
+	endCng;
     }
     int opCall () {
         return v;
@@ -245,30 +246,39 @@
         }
         sv = Int.toString (v);
         endEvent;
+        endCng;
 	return sv;
     }
     
+    void endCng () {
+        changed.intData[symbol] = v;
+    }
+    
 protected:
-    int v = 0;	// must be a valid value for EnumContent (i.e. 0)
+    int v;
 }
 
 /** Double content. */
 class DoubleContent : AStringContent
 {
     /** Create a content with _symbol name symbol. */
-    this (char[] symbol, double val = 0) {
-        assignNoCb (val);
+    this (char[] symbol) {
+        auto valp = symbol in changed.doubleData;
+        if (valp)
+            v = *valp;
+        sv = Float.toString (v, 8, 4);
 	super (symbol);
     }
     
-    void assignNoCb (double val) {
+    void assignNoCng (double val) {
 	v = val;
 	sv = Float.toString (v, 8, 4);
 	if (pos > sv.length) pos = sv.length;
+        endEvent;
     }
     void opAssign (double val) {
-	assignNoCb (val);
-	endEvent;
+	assignNoCng (val);
+        endCng;
     }
     double opCall () {
         return v;
@@ -283,17 +293,20 @@
         }
         sv = Float.toString (v, 8, 4);
         endEvent;
+        endCng;
 	return sv;
     }
     
+    void endCng () {
+        changed.doubleData[symbol] = v;
+    }
+    
 protected:
     double v;
 }
 
-/** A content representing an enumeration.
- *
- * Extends IntContent for Options support. */
-class EnumContent : IntContent, IContentList
+/** A content representing an enumeration. */
+class EnumContent : AStringContent, IContentList
 {
     /** CTOR.
     *
@@ -301,32 +314,43 @@
     *	enumSymbols = Symbol names for each
     *	val = which value is active; must be in [0,length-1]
     */
-    this (char[] symbol, char[][] enumSymbols, int val = 0) {
+    this (char[] symbol, char[][] enumSymbols) {
+        super (symbol);
         this.enumSymbols = enumSymbols;
         enums.length = enumSymbols.length;
         char[] symPeriod = symbol~'.';
         foreach (i, ref e; enums) {
             e = new EnumValueContent (this, i, symPeriod~enumSymbols[i]);
         }
-        super (symbol, val);	// calls assignNoCb
-    }
-    
-    /** Create from a EnumCStruct. */
-    this (char[] symbol, EnumCStruct data) {
-        this (symbol, data.symbols, data.value);
+        enums[v].assignFromParent (true);
+        sv = enums[v].name_;
+        // Re-set the value if a saved value is found:
+        auto valp = symbol in changed.enumValData;
+        if (valp)
+            assignNoCng = *valp;
     }
     
-    /** Return an EnumCStruct, which could be used to recreate this Content. */
-    //NOTE: save EnumCStruct directly?
-    EnumCStruct structOf () {
-        EnumCStruct r;
-        r.symbols = enumSymbols;
-        r.value = v;
-        return r;
+    void opAssign (size_t val) {
+        assignNoCng (val);
+        endCng;
     }
-    
-    override void assignNoCb (int val) {	// called by children (via opAssign)
-        if (val >= enums.length || val < 0) {
+    // Assign by enum symbol name (for ContentLoader)
+    void assignNoCng (char[] enumSym) {
+        foreach (i,e; enumSymbols) {
+            if (e == enumSym) {
+                assignNoCng (i);
+                return;
+            }
+        }
+        logger.warn ("EnumContent {} assigned invalid enumeration: {}; valid: {}", symbol, enumSym, enumSymbols);
+    }
+    size_t opCall () {
+        //debug logger.trace ("EnumContent {} returning value: {} ({})",symbol, enumSymbols[v], v);
+        return v;
+    }
+    alias opCall opCast;
+    void assignNoCng (size_t val) {
+        if (val >= enums.length) {
 	    logger.error ("EnumContent "~name_~" assigned invalid value; keeping value: "~sv);
 	    return;
 	}
@@ -337,19 +361,11 @@
         if (pos > sv.length) pos = sv.length;
         endEvent;
     }
-    // Change if true, assert current value if false
-    void childAssign (int val) {
-        debug assert (0 <= val && val < enums.length, "cA out of bounds");
-        if (enums[val].v)
-            assignNoCb (val);
-        else
-            enums[val].assignFromParent (v == val);
-    }
     
     override char[] endEdit () {
 	foreach (i,e; enums)
 	    if (sv == e.name_) {
-		v = i;
+		assignNoCng (i);
 		goto break1;
 	    }
 	
@@ -358,40 +374,56 @@
 	if (pos > sv.length) pos = sv.length;
 	
 	break1:
-	endEvent;
+	endCng;
 	return sv;
     }
     
+    void endCng () {
+        changed.enumValData[symbol] = enumSymbols[v];
+    }
+
     override Content[] list () {
         return enums;
     }
     
-    struct EnumCStruct {
-        char[][] symbols;
-        int value;
+    // Interface functions that don't make sense for an emuneration:
+    override void append (Content) {}
+    
+protected:
+    // Called by child; change this if true, assert current value if false
+    void childAssign (size_t val) {
+        debug assert (val < enums.length, "cA out of bounds");
+        if (enums[val].v)
+            this = val;
+        else
+            enums[val].assignFromParent (v == val);
     }
     
-protected:
+    size_t v;			// value (i.e. enums[v] is value)
     EnumValueContent[] enums;
     char[][] enumSymbols;	// saved for getStructOf
     
-    /** Special version of BoolContent for each enumeration to update the parent Enum. */
+    /** Special version of BoolContent for each enumeration to update the
+     * parent Enum.
+     * 
+     * Also should not save its value, since the parent stores the value. */
     private class EnumValueContent : BoolContent {
         /** New enumeration of parent with index num. */
         this (EnumContent parent, size_t num, char[] symbol) {
             this.parent = parent;
             i = num;
-            super (symbol, 0);	// parent sets value (if true); we shouldn't
-            sv = "false";	// except for this
+            super (symbol);
         }
         
-        override void assignNoCb (bool val) {
+        override void assignNoCng (bool val) {
             v = val;
             parent.childAssign (i);
         }
+        override void opAssign (bool val) {
+            assignNoCng (val);
+        }
         void assignFromParent (bool val) {	// don't call back to parent
-            super.assignNoCb (val);
-            endEvent;
+            super.assignNoCng (val);
         }
         
         override char[] endEdit () {
--- a/mde/content/Content.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/content/Content.d	Sat Feb 07 12:46:03 2009 +0000
@@ -13,50 +13,40 @@
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
-/*************************************************************************************************
+/******************************************************************************
  * The content system − interfaces and base Content class.
- *************************************************************************************************/
+ *****************************************************************************/
 module mde.content.Content;
 
-import util = mde.util;
+public import mde.content.IContent;
+import mde.content.Translation;	// loading strings
+import mde.content.ValueCache;	// saving/loading content values
+import util = mde.util;		// fp -> dlg
+import mde.exception;
+import mde.setup.logger;
 
-debug {
     import tango.util.log.Log : Log, Logger;
     private Logger logger;
     static this () {
         logger = Log.getLogger ("mde.gui.content.Content");
     }
-}
-
-/** IContent − interface for all Content classes.
- *
- * Very little code uses IContent (except for passing opaquely). */
-interface IContent
-{
-    /** Generically return strings.
-    *
-    * This serves two purposes: generically returning a string of/related to the content (i == 0),
-    * and returning associated descriptors. Functions should adhere to (or add to) this table.
-    *
-    *  $(TABLE
-    *  $(TR $(TH i) $(TH returns))
-    *  $(TR $(TD 0) $(TD value))
-    *  $(TR $(TD 1) $(TD Translated name or null))
-    *  $(TR $(TD 2) $(TD Translated description or null))
-    *  $(TR $(TD other) $(TD null))
-    *  ) */
-    char[] toString (uint i);
-}
 
 /** Content lists. Impemented by EnumContent as well as ContentList. */
 interface IContentList : IContent
 {
     /** Return all sub-contents. */
     Content[] list ();
+    
+    /** Append x to the content list.
+     *
+     * Done automatically in creation, from symbol name, so don't call
+     * externally. */
+    void append (Content x);
 }
 
 
-/** The base for most or all content classes.
+/******************************************************************************
+ * The base for most or all content classes.
  *
  * Includes generic callback support, toString implementation and symbol access.
  * 
@@ -66,20 +56,56 @@
  *  void opAssign (T val);	// assign val, calling callbacks
  *  T opCall ();		// return value
  *  alias opCall opCast;
- * --- */
+ *  void endEvent ();		// extend to set new value in changed (if Content has a value)
+ * ---
+ *****************************************************************************/
 class Content : IContent
 {
-    this (char[] symbol) {
-	this.symbol = symbol;
-	name_ = symbol;		// provide a temporary name
+    protected this (char[] symbol) {
+        //debug logger.trace ("Creating a {}: {}", this, symbol);
+        if (symbol in allContent)
+            throw new ContentException ("Multiple content with symbol "~ symbol);
+        allContent[symbol] = this;
+        this.symbol = symbol;
+        
+        // Name:
+        auto l10nP = symbol in translations;
+	if (l10nP)
+            name (*l10nP);
+        else
+            name_ = symbol;	// provide a temporary name
+        
+        // Add to a parent list:
+	ptrdiff_t i = symbol.length-1;
+	while (i >= 0 && symbol[i] != '.')
+	    --i;
+        IContentList parent;
+        if (i <= 0) {
+            if (symbol.length == 0)
+            	return;		// special case: is tree (content tree root)
+            parent = tree;	// otherwise, use tree as root
+        } else {
+            char[] parentSym = symbol[0..i];
+            Content* parObj = parentSym in allContent;
+            if (parObj) {	// existing object
+            	parent = cast(IContentList) *parObj;
+            	if (parent is null) {	// it's not an IContentList!
+                    logger.warn ("{} is not an IContentList, but a child, {}, exists!", parentSym, symbol);
+                    return;
+            	}
+            } else		// no existing object
+            	parent = new ContentList (parentSym);
+        }
+        parent.append (this);
     }
     
-    void name (char[] name, char[] desc = null) {
-	name_ = name;
-	desc_ = desc;
+    void name (Translation.Entry e) {
+	name_ = e.name;
+	desc_ = e.desc;
     }
-    
-    /** Add a callback. Callbacks are called in the order added. */
+   
+    /** Add a callback. Callbacks are called on a change or event, in the order
+     * added. */
     Content addCallback (void delegate (Content) cb) {
 	this.cb ~= cb;
 	return this;
@@ -97,14 +123,86 @@
 	: null;
     }
     
-    /// End of an event, e.g. a button release or end of an edit (calls callbacks).
+    /** End of an event, e.g. a button release or end of an edit (calls callbacks).
+     *
+     * Content holding a value should override this, setting its new value in
+     * changed as well as calling callbacks, e.g.:
+     * ---
+     * Content.changed.boolData[symbol] = val;
+     * super();
+     * --- */
     void endEvent () {
 	foreach (dg; cb)
 	    dg (this);
     }
     
-    final char[] symbol;	// Symbol name for this content
 protected:
-    char[] name_, desc_;	// name and description
+    char[] symbol;
+    char[] name_, desc_;	// translated name and description
     void delegate (Content) cb[];
+    
+public static:
+    /** Get Content with _symbol name symbol from the list of all content, or
+     * null if no such Content exists. */
+    Content get (char[] symbol) {
+        auto p = symbol in allContent;
+        if (p)
+            return *p;
+        logger.warn ("Content {} does not exist",symbol);
+        return null;
+    }
+    
+    // Would use tango.container.HashMap, but mde.workaround2371 doesn't work here.
+    Content[char[]] allContent;	// all content hashed by symbol name
+    ContentList tree;	// tree of all content.
+    
+    static this () {
+        tree = new ContentList ("");
+    }
+    
+package static:
+    // Kept to set the value of other content as it is created:
+    ValueCache loaded;	// loaded values not matching any content created yet
+    // The values in changed are what is written to file when saving:
+    ValueCache changed;	// changed values and values from local config file
+    
+    Translation.Entry[char[]] translations;
 }
+
+/** A generic way to handle a list of type IContent. */
+class ContentList : Content, IContentList
+{
+    this (char[] symbol) {
+	super (symbol);
+    }
+    
+    override Content[] list () {
+	return list_;
+    }
+    
+    override void append (Content x) {
+        list_ ~= x;
+    }
+    
+protected:
+    final Content[] list_;
+}
+
+/** Created on errors to display and log a message. */
+class ErrorContent : Content
+{
+    this (char[] symbol, char[] msg) {
+	super (symbol);
+        this.msg = msg;
+        logger.error (symbol ~ ": " ~ msg);
+    }
+    
+    override char[] toString (uint i) {
+	return i == 0 ? msg
+	     : i == 1 ? name_
+	     : null;
+    }
+    
+protected:
+    char[] msg;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/ContentLoader.d	Sat Feb 07 12:46:03 2009 +0000
@@ -0,0 +1,121 @@
+/* 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/>. */
+
+/******************************************************************************
+ * ContentLoader handles saving and loading of all Content values, as
+ * well as translation (loading Contents' name and desc strings from file).
+ * 
+ * In other words, the values of Content classes automatically persist across
+ * runs (provided save() and load() are called appropriately).
+ * If content is created before calling load() its value is set by load(); if
+ * it is created after, its value is set during its creation.
+ *****************************************************************************/
+module mde.content.ContentLoader;
+
+import mde.content.AStringContent;
+import mde.content.ValueCache;
+import mde.file.paths;	// PRIORITY enum
+import mde.content.Translation;
+
+debug {
+import tango.util.log.Log : Log, Logger;
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.gui.content.ContentLoader");
+}
+}
+
+/// Namespace for the module's functionality
+struct ContentLoader {
+    private static {	// Templates
+        template ContentN(T) {
+            static if (is(T == bool)) {
+                const char[] ContentN = "BoolContent";
+            } else static if (is(T == int)) {
+                const char[] ContentN = "IntContent";
+            } else static if (is(T == double)) {
+                const char[] ContentN = "DoubleContent";
+            } else static if (is(T == char[])) {
+                const char[] ContentN = "StringContent";
+            } else static if (is(T == enumVal)) {
+                const char[] ContentN = "EnumContent";
+            } else
+                static assert (false, "No Content of type "~T.stringof);
+        }
+        
+        // For setting existing content; mixed in in Content.load().
+        template setContentMixin(A...) {
+            static if (A.length)
+                const char[] setContentMixin =
+                `foreach (id,val; `~ValueCache.TName!(A[0])~`Data) {
+                    auto contP = id in Content.allContent;
+                    if (contP) {
+                        `~ContentN!(A[0])~` cont = cast(`~ContentN!(A[0])~`) *contP;
+                        if (cont)
+                            cont.assignNoCng (val);
+                    }
+                } ` ~ setContentMixin!(A[1..$]);
+            else
+                const char[] setContentMixin = ``;
+        }
+    }
+static:
+    /** Loads all stored content, to an existing Content or a buffer where new
+     * content can look up its value.
+     * 
+     * This would normally include all options, except perhaps key bindings.
+     * 
+     * Should only be called once. */
+    void load () {
+    	Content.loaded.loadData (PRIORITY.HIGH_LOW);
+        // Add locally-stored options into changes so they don't get lost when writing
+        Content.changed.loadData (PRIORITY.HIGH_ONLY);
+        with (Content.loaded) {
+            mixin (setContentMixin!(ValueCache.TYPES));
+        }
+    }
+    
+    /** Saves all changed content.
+     *
+     * May be called multiple times. */
+    void save () {
+        Content.changed.saveData;
+    }
+    
+private:
+    /* Load translations for all content.
+     * Hooked as a change callback for l10n. */
+    void translate (Content) {
+        logger.info ("loading translations...");
+        Translation trl = Translation.get (l10n());
+        
+	// Translate existing content:
+        foreach (symb, cont; Content.allContent) {
+            auto p = symb in trl.entries;
+            if (p)
+            	cont.name (*p);
+        }
+        
+        // Allow translation of new content:
+        Content.translations = trl.entries;
+        logger.info ("loaded translations");
+    }
+    
+    StringContent l10n;
+    static this () {
+        l10n = new StringContent ("MiscOptions.l10n");
+        l10n.addCallback (&translate);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/IContent.d	Sat Feb 07 12:46:03 2009 +0000
@@ -0,0 +1,44 @@
+/* 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/>. */
+
+/******************************************************************************
+ * The content system − interfaces.
+ * 
+ * The IContentList interface is defined in the Content module to allow it to
+ * use a Content[] type (since arrays of classes cannot have their type cast
+ * directly, at least not to/from an interface).
+ *****************************************************************************/
+module mde.content.IContent;
+
+/** IContent − interface for all Content classes.
+ *
+ * Very little code uses IContent (except for passing opaquely). */
+interface IContent
+{
+    /** Generically return strings.
+    *
+    * This serves two purposes: generically returning a string of/related to the content (i == 0),
+    * and returning associated descriptors. Functions should adhere to (or add to) this table.
+    *
+    *  $(TABLE
+    *  $(TR $(TH i) $(TH returns))
+    *  $(TR $(TD 0) $(TD value))
+    *  $(TR $(TD 1) $(TD Translated name or null))
+    *  $(TR $(TD 2) $(TD Translated description or null))
+    *  $(TR $(TD other) $(TD null))
+    *  ) */
+    char[] toString (uint i);
+}
+
--- a/mde/content/Items.d	Sun Feb 01 12:36:21 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,165 +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/>. */
-
-/**************************************************************************************************
- * A generic way to access content items. Also loads translations on-demand.
- *************************************************************************************************/
-module mde.content.Items;
-
-import mde.content.miscContent;
-import mde.gui.exception;
-
-import imde = mde.imde;
-import mde.lookup.Options;
-import mde.lookup.Translation;
-
-debug {
-    import tango.util.log.Log : Log, Logger;
-    private Logger logger;
-    static this () {
-	logger = Log.getLogger ("mde.gui.content.Items");
-    }
-}
-
-    /** Get a specific content item.
-     *
-     * loadTranslation() $(B must) be called before this function.
-     *
-     * E.g. get ("Options.MiscOptions.L10n") returns miscOpts.L10n,
-     * Items.get ("Options.MiscOptions") returns a ContentList of all misc options. */
-    Content get (char[] item) {
-	assert (currentL10n is miscOpts.L10n(), "must call loadTranslation (code error)");
-	char[] orig = item;	// item is modified by head()
-        
-	char[] h = head (item);
-	if (h == "Options") {
-	    if (item is null)
-		return Options.allContentList;
-	    
-	    h = head (item);
-	    auto p = h in Options.optionsClasses;
-	    if (p) {
-		if (item == null)
-		    return p.contentList;
-		
-		auto q = (h = head (item)) in p.content;
-		if (q && item is null)	// enforce item is an exact match
-		    return *q;
-	    }
-	} else if (h == "imde") {
-	    h = head (item);
-	    if (h == "menus" && item is null)
-		return imde.menus;
-	} else if (h == "dynamic") {
-            auto i = head (item) in items;
-            if (i) return *i;
-        }
-        
-	return new ErrorContent ("Error: bad content specifier", orig);
-    }
-    
-    /** Creates some content on first run (required by get()).
-     *
-     * If the correct translation strings are not loaded, this loads them. */
-    void loadTranslation () {
-	if (currentL10n is miscOpts.L10n()) return;
-	
-	// Create Option classes' ContentLists if necessary:
-	if (Options.allContentList is null) {
-	    Content[] list;
-	    list.length = Options.optionsClasses.length;
-	    size_t i;
-	    foreach (n,opts; Options.optionsClasses) {
-		opts.contentList = new ContentList (n, opts.content);
-		list[i++] = opts.contentList;
-	    }
-	    Options.allContentList = new ContentList ("Options", list);
-	}
-	
-        Translation trl;
-        Translation.Entry trle;
-        
-	// Translate Options:
-	with (Options.allContentList) {
-	    trle = Translation.get (symbol).getStruct (symbol);
-	    name (trle.name, trle.desc);
-	}
-	foreach (n,opts; Options.optionsClasses) {
-	    trl = Translation.get (n);
-	    trle = trl.getStruct (n);
-	    opts.contentList.name (trle.name, trle.desc);
-	    foreach (s, v; opts.content) {
-		trle = trl.getStruct (s);
-		v.name (trle.name, trle.desc);
-		IContentList cl = cast(IContentList) v;
-		if (cl) {
-		    foreach (i,c; cl.list) {
-			trle = trl.getStruct (c.symbol);
-			c.name (trle.name, trle.desc);
-		    }
-		}
-	    }
-	}
-	
-	// Translate imde:
-        trl = Translation.get ("imde");
-	trle = trl.getStruct ("menus");
-	imde.menus.name (trle.name, trle.desc);
-        
-        // Translate dynamic content:
-        if (items.length) {
-            trl = Translation.get ("dynamic");
-            foreach (n,item; items) {
-                trle = trl.getStruct (n);
-                item.name (trle.name, trle.desc);
-                IContentList cl = cast(IContentList) item;
-                if (cl) {
-                    foreach (i,c; cl.list) {
-                        trle = trl.getStruct (c.symbol);
-                        c.name (trle.name, trle.desc);
-                    }
-                }
-            }
-        }
-	
-	currentL10n = miscOpts.L10n();
-    }
-    
-    /** Add content c with name c.symbol, so that translations are loaded for it and it can be
-     * returned by get ("dynamic."~c.symbol). */
-    Content addContent (Content c) {
-        items[c.symbol] = c;
-        return c;
-    }
-    
-private:
-    // NOTE: possibly add all content to this list. Lookups would be faster.
-    Content[char[]] items;	// dynamically added content
-    
-    /** Takes the string "head.tail" where tail may contain '.' but head does not, returns "head",
-     * with str set to "tail". */
-    char[] head (ref char[] str) {
-	size_t i = 0;
-	while (i < str.length && str[i] != '.')
-	    ++i;
-	char[] ret = str[0..i];
-	if (i == str.length)
-	    str = null;
-	else
-	    str = str[i+1..$];
-	return ret;
-    }
-    
-    char[] currentL10n;	// Strings will be reloaded if this is not miscOpts.L10n().
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/Translation.d	Sat Feb 07 12:46:03 2009 +0000
@@ -0,0 +1,202 @@
+/* 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
+ *
+ * Loads a set of names and optionally descriptions intended to provide a user-
+ * friendly localised name to symbols defined in code or data files.
+ *
+ * Each locale may specify dependant locales which will be loaded and merged,
+ * so that, for instance, variants of a language need not define common strings
+ * in all variants. Dependencies are loaded in the order specified, including
+ * all dependencies of first locale before any dependencies of other locales.
+ * Circular dependencies are allowed.
+ *
+ * Entries may be extended to include a version number, intended to indicate
+ * translations which may need updating to reflect a changed meaning. A
+ * translation tool is needed to handle this really. */
+module mde.lookup.Translation;
+
+import mde.file.paths;
+import mde.exception;
+
+import mde.file.mergetag.MTTagReader;
+import mde.file.deserialize;
+
+import tango.util.log.Log : Log, Logger;
+
+/** Loads all translation strings for current locale.
+ *
+ * See module description for details.
+ *
+ * Encoding used is UTF-8. */
+struct Translation
+{
+    /** Load the translation for the requested locale.
+    *
+    * Params:
+    *   name = The locale to load strings for.
+    */
+    static {
+	/** Get Translation instance for locale l10n, loading if not already
+	 *  loaded. Only keeps one locale loaded.
+	 * 
+	 * These are loaded from data/L10n/locale.mtt where locale is l10n and
+	 * files for dependant locales given in the header tag
+	 * <char[][]|depends=[...]>.
+	 *
+	 * On errors, a message is logged and the function continues, likely
+	 * resulting in some symbol names being untranslated. */
+	Translation get (char[] l10n) {
+	    if (loadedL10n != l10n) {
+		loadedL10n = l10n;
+		debug logger.trace ("Loading L10n: "~loadedL10n);
+                transl.load (l10n);
+	    }
+            return transl;
+	}
+	private Translation transl;
+        private char[] loadedL10n;
+        private Logger logger;
+        static this () {
+            logger = Log.lookup ("mde.lookup.Translation");
+        }
+    }
+    
+    private void load (char[] l10n) {
+        char[][] files = [loadedL10n];	// initial locale plus dependencies
+        size_t read = 0;	// index in files of next file to read
+        while (read < files.length) {
+            logger.trace ("reading file {}", files[read]);
+            try {
+                MTTagReader reader = dataDir.makeMTTagReader ("L10n/"~files[read], PRIORITY.HIGH_LOW);
+                bool isSecTag;
+                while (reader.readTag (isSecTag)) {
+                    if (isSecTag) break;	// end of header
+                    if (reader.tagID == "depends" && reader.tagType == "char[][]") {
+                        // append, making sure no duplicates are inserted
+                        char[][] deps = deserialize!(char[][]) (reader.tagData);
+                        f1:
+                        foreach (dep; deps) {
+                            foreach (f; files)
+                                if (f == dep)
+                                    continue f1;
+                            files ~= dep;
+                        }
+                    }
+                }
+                char[] symPrefix;
+                do {
+                    if (isSecTag) {
+                        if (reader.section.length == 0) {
+                            symPrefix = "";
+                            continue;
+                        }
+                        symPrefix = reader.section ~ '.';
+                    } else {
+                        if (reader.tagType == "entry") {
+                            char[] sym = symPrefix ~ reader.tagID;
+                            // If the tag already exists, don't replace it
+                            if (sym in entries) continue;
+                            
+                            Entry entry = deserialize!(Entry) (reader.tagData);
+                            if (entry.name is null) {   // This tag is invalid; ignore it
+                                logger.error ("In L10n/{}.mtt: tag {} has no name", files[read], sym);
+                                continue;
+                            }
+                            entries[sym] = entry;
+                        }
+                    }
+                } while (reader.readTag (isSecTag))
+                ++read;
+            } catch (Exception e) {
+                logger.error ("Exception loading translation: {}", e.msg);
+            }
+        }
+    }
+    
+    /+ Getters for entries... not wanted now.
+    alias entry opCall;	    /// Convenience alias
+    
+    /** Get the translation for the given identifier.
+    * If no entry exists, the identifier will be returned.
+    *
+    * Optionally, the description can be returned. */
+    char[] entry (char[] id) {
+        Entry* p = id in entries;
+        if (p) {
+            return p.name;
+        } else {
+            return id;
+        }
+    }
+    /** ditto */
+    char[] entry (char[] id, out char[] description) {
+        Entry* p = id in entries;
+        if (p) {
+            description = p.desc;
+            return p.name;
+        } else {
+            return id;
+        }
+    }
+    
+    /** Alternative usage: return a Translation.Entry struct. */
+    Entry getStruct (char[] id) {
+        Entry* p = id in entries;
+        if (p) {
+            return *p;
+        } else {
+            logger.warn ("Unable to find translation for: {}", id);
+            Entry ret;
+            ret.name = id;
+            return ret;
+        }
+    } +/
+    
+    /** This struct is used to store each translation name and description pair.
+     *
+     * Entries may also have a version field, but this is only needed for
+     * writing/updating translations. */
+    struct Entry {
+        char[] name;        // The translated string
+        char[] desc;        // An optional description
+    }
+    
+    Entry[char[]] entries;  // all entries
+    
+    debug (mdeUnitTest) unittest {
+        // Relies on files in unittest/data/L10n: locale-x.mtt for x in 1..4
+        
+	// Struct tests
+	Translation t = get ("locale-1");
+	Entry e = t.getStruct ("section-2.entry-1");
+	assert (e.name == "Test 1");
+	assert (e.desc == "Description");
+        e = t.getStruct ("section-2.entry-2");
+        assert (e.name == "Test 2");
+	assert (e.desc is null);
+	
+	// Dependency tests. Priority should be 1,2,3,4 (entries should come from first file in list that they appear in).
+	char[] d = "1";
+        assert (t.entry ("section-1.file-1", d) == "locale-1");
+	assert (d is null);
+        assert (t.entry ("section-1.file-2", d) == "locale-2");
+	assert (d == "desc2");
+        assert (t.entry ("section-1.file-3") == "locale-3");
+        assert (t.entry ("section-1.file-4") == "locale-4");
+	
+        logger.info ("Unittest complete.");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/ValueCache.d	Sat Feb 07 12:46:03 2009 +0000
@@ -0,0 +1,159 @@
+/* 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 used to load and save content values to/from a struct.
+ *****************************************************************************/
+module mde.content.ValueCache;
+
+import mde.file.paths;
+import mde.file.mergetag.MTTagReader;
+import mde.file.mergetag.MTTagWriter;
+import mde.file.serialize;
+
+import tango.util.log.Log : Log, Logger;
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.gui.content.ValueCache");
+}
+
+typedef char[] enumVal;	// use a char[] to save enum values (it's more resiliant to change of the enum type)
+
+struct ValueCache {
+    //BEGIN Templates
+    package static {
+	// All supported content types for generic saving and loading:
+	template store(A...) { alias A store; }
+	alias store!(bool, int, double, char[], enumVal) TYPES;
+	
+	// Get name of a type. Basically just stringof, but special handling for arrays.
+        // Use TName!(T) for a valid symbol name, and T.stringof for a type.
+        template TName(T : T[]) {
+            const char[] TName = TName!(T) ~ "A";
+        }
+        template TName(T) {
+            const char[] TName = T.stringof;
+        }
+        template Vars(A...) {
+            static if (A.length) {
+                static if (is (A[0] == enumVal))
+                    const char[] Vars = `char[][char[]] `~TName!(A[0])~`Data;` ~ Vars!(A[1..$]);
+                else
+                    const char[] Vars = A[0].stringof~`[char[]] `~TName!(A[0])~`Data;` ~ Vars!(A[1..$]);
+            } else
+                const char[] Vars = ``;
+        }
+        
+        // For reading tags
+        template readTagMixin(T, A...) {
+            const char[] ifBlock = `if (reader.tagType == "`~T.stringof~`") {
+                auto p = symBuf[0..symEnd] in `~TName!(T)~`Data;
+            	if (p is null)
+                    `~TName!(T)~`Data[symBuf[0..symEnd].dup] = deserialize!(typeof(`~TName!(T)~`Data.values[0])) (reader.tagData);
+            }`;
+            static if (A.length)
+                const char[] readTagMixin = ifBlock~` else `~readTagMixin!(A).readTagMixin;
+            else
+                const char[] readTagMixin = ifBlock;
+        }
+        
+        // For writing tags
+        template writeTagMixin(A...) {
+            static if (A.length)
+                const char[] writeTagMixin =
+                `typeStr = "`~A[0].stringof~`";
+                foreach (sym,val; `~TName!(A[0])~`Data) {
+                    MTTagStrings tag;
+                    tag.type = typeStr;
+                    tag.data = serialize (val);
+                    // Take section prefix as MT section
+                    size_t dot;
+                    while (dot < sym.length && sym[dot] != '.') ++dot;
+                    if (dot == sym.length) {	// no prefix; use ""
+                    	dot = 0;
+                    	tag.id = sym;
+                    } else
+                    	tag.id = sym[dot+1..$];
+                    secs[sym[0..dot]] ~= tag;
+        	}` ~ writeTagMixin!(A[1..$]);
+            else
+                const char[] writeTagMixin = ``;
+        }
+    }
+    //END Templates
+    
+    mixin (Vars!(TYPES));	// variable associative arrays
+    
+    // Load content values from mergetag file(s) conf/options.mt[tb]
+    // Use HIGH_LOW priority to load all data, HIGH_ONLY for just user data
+    void loadData (PRIORITY priority) {
+        try {
+            MTTagReader reader = confDir.makeMTTagReader ("options", priority);
+            bool isSecTag;
+            while (reader.readTag (isSecTag) && !isSecTag) {}	// skip header
+            char[] symBuf = new char[64];
+            size_t sym2, symEnd;	// start of second symbol in symBuf, end of symbol
+            do {
+                //debug logger.trace ("tag: <{}|{}={}> sec: {} ({})", reader.tagType, reader.tagID, reader.tagData, reader.section, isSecTag);
+                if (isSecTag) {
+                    sym2 = reader.section.length;
+                    if (sym2 > 0) {	// named section; prefix symbol by "section."
+                        if (symBuf.length <= sym2)
+                            symBuf.length = sym2 * 2;
+                        symBuf[0..sym2] = reader.section;
+                        symBuf[sym2++] = '.';
+                    }
+                } else {
+                    symEnd = reader.tagID.length;
+                    if (symBuf.length - sym2 < symEnd)
+                        symBuf.length = symBuf.length + symEnd * 2;
+                    symEnd += sym2;
+                    symBuf[sym2..symEnd] = reader.tagID;
+                    mixin (readTagMixin!(TYPES).readTagMixin);
+                }
+            } while (reader.readTag (isSecTag))
+        } catch (NoFileException e) {
+            // Just return. Options file will be created on exit.
+        } catch (Exception e) {
+            logger.warn ("Loading options failed: "~e.msg);
+            logger.warn ("If warning persists, delete the offending file.");
+        }
+    }
+    
+    // Save stored values to the user file
+    void saveData () {
+        try {
+            MTTagWriter writer = confDir.makeMTTagWriter ("options.mtt");
+            
+            // First sort tags by section:
+            struct MTTagStrings {
+                char[] type, id, data;
+            }
+            MTTagStrings[][char[]] secs;
+            char[] typeStr;
+            mixin (writeTagMixin!(TYPES));
+            
+            // Now write sections:
+            foreach (secName,sec; secs) {
+            	writer.sectionTag (secName);
+            	foreach (tag; sec)
+                    writer.dataTag (tag.type, tag.id, tag.data);
+            }
+            writer.close;
+        } catch (Exception e) {
+            logger.error ("Saving options failed: "~e.msg);
+        }
+    }
+}
--- a/mde/content/miscContent.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/content/miscContent.d	Sat Feb 07 12:46:03 2009 +0000
@@ -20,64 +20,13 @@
 
 public import mde.content.Content;
 
+/+
     import tango.util.log.Log : Log, Logger;
     private Logger logger;
     static this () {
         logger = Log.getLogger ("mde.content.miscContent");
     }
-
-/** A generic way to handle a list of type IContent. */
-class ContentList : Content, IContentList
-{
-    this (char[] symbol, Content[] list = null) {
-	list_ = list;
-	super (symbol);
-    }
-    this (char[] symbol, Content[char[]] l) {
-	list_.length = l.length;
-	size_t i;
-	foreach (c; l)
-	    list_[i++] = c;
-	super (symbol);
-    }
-    
-    override Content[] list () {
-	return list_;
-    }
-    
-    ContentList append (Content x) {
-        size_t l = list_.length;
-        list_.length = l + 1;
-        list_[l] = x;
-        return this;
-    }
-    ContentList append (Content[] x) {
-        list_ ~= x;
-        return this;
-    }
-    
-protected:
-    final Content[] list_;
-}
-
-/** Created on errors to display and log a message. */
-class ErrorContent : Content
-{
-    this (char[] name, char[] msg) {
-	super (name);
-        this.msg = msg;
-        logger.error (name ~ ": " ~ msg);
-    }
-    
-    override char[] toString (uint i) {
-	return i == 0 ? msg
-	     : i == 1 ? name_
-	     : null;
-    }
-    
-protected:
-    char[] msg;
-}
++/
 
 /** A Content with no value but able to pass on an event.
 *
--- a/mde/exception.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/exception.d	Sat Feb 07 12:46:03 2009 +0000
@@ -13,7 +13,9 @@
 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 base class for all mde exceptions plus some exception classes.
+/******************************************************************************
+ * Contains the base class for all mde exceptions plus some exception classes.
+ *****************************************************************************/
 module mde.exception;
 
 /** Base class for all mde Exceptions.
@@ -45,9 +47,9 @@
 }
 
 /// Thrown when loading options fails.
-class optionsLoadException : mdeException {
+class ContentException : mdeException {
     char[] getSymbol () {
-        return super.getSymbol ~ ".options";
+        return super.getSymbol ~ ".content";
     }
     
     this (char[] msg) {
@@ -55,27 +57,6 @@
     }
 }
 
-/// Thrown when loading strings for the requested name and current locale fails.
-class L10nLoadException : mdeException {
-    char[] getSymbol () {
-        return super.getSymbol ~ ".i18n.I18nTranslation";
-    }
-    
-    this (char[] msg) {
-        super(msg);
-    }
-}
-
-/// Thrown when an image fails to load or cannot be loaded to a texture (unsupported format?).
-class ImageException : mdeException {
-    char[] getSymbol () {
-        return super.getSymbol ~ ".gl.texture";
-    }
-    this (char[] msg) {
-        super (msg);
-    }
-}
-
 
 debug (mdeUnitTest) {
     import tango.util.log.Log : Log, Logger;
@@ -87,8 +68,8 @@
     
     unittest {
         // Check message prepending works correctly.
-        mdeException mE = new optionsLoadException("");
-        assert (mE.getSymbol() == "mde.options", mE.getSymbol());
+        mdeException mE = new ContentException("");
+        assert (mE.getSymbol() == "mde.content", mE.getSymbol());
         try {
             throw new mdeException ("ABC");
             assert (false);
--- a/mde/file/mergetag/MTTagReader.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/file/mergetag/MTTagReader.d	Sat Feb 07 12:46:03 2009 +0000
@@ -42,7 +42,7 @@
 *  )
 *
 */
-MTTagReader makeMTTagReader (FilePath path) {
+MTTagReader getMTTagReader (FilePath path) {
     if      (path.ext == "mtb") return new MTBTagReader (path.toString);
     else if (path.ext == "mtt") return new MTTTagReader (path.toString);
     else throw new MTFileIOException ("Invalid mergetag extension");
@@ -117,6 +117,9 @@
         // Version checking & matching header section tag:
         if (checkHeader(fbuf) != MTFormat.MT01)
             throwMTErr("Not a valid (known) MergeTag text file" ~ ErrInFile, new MTFileFormatException);
+        
+        pos = 6;
+        section = CurrentVersionString;
     }
     
     public bool readTag (out bool sectionTag) {
@@ -197,14 +200,14 @@
                 if (comment) {				// simple block comment
                     comment = false;			// end of this comment
                 } else {
-                    section = cast(char[]) fbuf[start..pos];
+                    section = cast(char[]) fbuf[start..pos-1];
                     sectionTag = true;
                     return true;
                 }
             }
             else if (fbuf[pos] == '!') {		// possibly a comment; check next char
-                comment = true;				// starting a comment (or an error)
-                					// variable is reset at end of comment
+                comment = true;				// starting a comment
+                ++pos;
             } else					// must be an error
             throwMTErr ("Invalid character '"~fbuf[pos..pos+1]~"' (or sequence starting \"!\") outside of tag" ~ ErrInFile, new MTSyntaxException);
         }
@@ -252,7 +255,7 @@
         Exception exc;
         foreach (file; files) {
             try {   // try reading each file
-                MTTagReader r = makeMTTagReader (file);
+                MTTagReader r = getMTTagReader (file);
                 readers ~= r;
             } catch (Exception e) {
                 exc = e;
--- a/mde/file/mergetag/MTTagUnittest.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/file/mergetag/MTTagUnittest.d	Sat Feb 07 12:46:03 2009 +0000
@@ -39,7 +39,7 @@
         static S tag3 = { type:"t3", id:"i3", data:"\" a string \""};
         static S tag4 = { type:"t1", id:"i1", data:"5.-98"};
         
-        MTTagWriter w = makeMTTagWriter (file.toString);
+        MTTagWriter w = getMTTagWriter (file.toString);
         w.dataTag (tag2.type, tag2.id, tag2.data);
         w.sectionTag ("one");
         w.dataTag (tag1.type, tag1.id, tag1.data);
@@ -48,17 +48,20 @@
         w.writeTag ("one", tag4.type, tag4.id, tag4.data);
         w.close;
         
-        MTTagReader r = makeMTTagReader (file);
+        MTTagReader r = getMTTagReader (file);
         bool isSecTag;
         while (r.readTag (isSecTag)) {
             if (isSecTag) continue;
             if (r.tagID == tag1.id) {
+                assert (r.section == "one");
                 assert (r.tagType == tag1.type, r.tagID);
                 assert (r.tagData == tag1.data || r.tagData == tag4.data, r.tagID);
             } else if (r.tagID == tag2.id) {
+                assert (r.section == "MT01");
                 assert (r.tagType == tag2.type, r.tagID);
                 assert (r.tagData == tag2.data, r.tagID);
             } else if (r.tagID == tag3.id) {
+                assert (r.section == "three");
                 assert (r.tagType == tag3.type, r.tagID);
                 assert (r.tagData == tag3.data, r.tagID);
             } else assert (false, "extra tag: "~r.tagID);
--- a/mde/file/mergetag/MTTagWriter.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/file/mergetag/MTTagWriter.d	Sat Feb 07 12:46:03 2009 +0000
@@ -30,7 +30,7 @@
     logger = Log.getLogger ("mde.file.mergetag.MTTagWriter");
 }
 
-MTTagWriter makeMTTagWriter (char[] path) {
+MTTagWriter getMTTagWriter (char[] path) {
     if (path.length > 4 && path[$-4..$] == ".mtt")
         return new MTTTagWriter (path);
     else if (path.length > 4 && path[$-4..$] == ".mtb")
--- a/mde/file/mergetag/Reader.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/file/mergetag/Reader.d	Sat Feb 07 12:46:03 2009 +0000
@@ -54,7 +54,7 @@
 *  )
 *
 */
-IReader makeReader (FilePath path, DataSet ds = null, bool rdHeader = false) {
+IReader getMTReader (FilePath path, DataSet ds = null, bool rdHeader = false) {
     if      (path.ext == "mtb") return new MTBReader (path.toString, ds, rdHeader);
     else if (path.ext == "mtt") return new MTTReader (path.toString, ds, rdHeader);
     else throw new MTFileIOException ("Invalid mergetag extension");
@@ -525,7 +525,7 @@
         Exception exc;
         foreach (file; files) {
             try {   // try reading header of each file
-                IReader r = makeReader (file, ds, rdHeader);
+                IReader r = getMTReader (file, ds, rdHeader);
                 readers ~= r;
             } catch (Exception e) {
                 exc = e;
--- a/mde/file/mergetag/Writer.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/file/mergetag/Writer.d	Sat Feb 07 12:46:03 2009 +0000
@@ -89,7 +89,7 @@
  *  MTFileFormatException if unable to determine writing format or use requested format.
  */
 //FIXME: separate functions for separate functionality, like in Reader?
-IWriter makeWriter (char[] path, DataSet dataset = null,
+IWriter getMTWriter (char[] path, DataSet dataset = null,
                     WriterMethod method = WriterMethod.FromExtension) {
     if (method == WriterMethod.FromExtension) {
         if (path.length > 4 && path[$-4..$] == ".mtt")
--- a/mde/file/mergetag/mdeUT.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/file/mergetag/mdeUT.d	Sat Feb 07 12:46:03 2009 +0000
@@ -57,11 +57,11 @@
         }
         mixin (genUTCode());	// Add an entry to dd for each type
         
-        IWriter w = makeWriter (file, dsW, WriterMethod.Both);
+        IWriter w = getMTWriter (file, dsW, WriterMethod.Both);
         w.write();
         
         // FIXME (unittest): when binary writing is supported, read both formats and check
-        IReader r = makeReader (FilePath (file~".mtt"), null, true);
+        IReader r = getMTReader (FilePath (file~".mtt"), null, true);
         r.read();
         
         DataSet dsR = r.dataset;
--- a/mde/file/paths.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/file/paths.d	Sat Feb 07 12:46:03 2009 +0000
@@ -110,14 +110,14 @@
     IWriter makeMTWriter (char[] file, DataSet ds = null)
     {
         // FIXME: use highest priority writable path
-        return makeWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text);
+        return getMTWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text);
     }
     
     /** Creates an MTTagWriter for file. */
-    MTTagWriter makeMTWriter (char[] file)
+    MTTagWriter makeMTTagWriter (char[] file)
     {
         // FIXME: use highest priority writable path
-        return makeMTTagWriter (paths[pathsLen-1] ~ file);
+        return getMTTagWriter (paths[pathsLen-1] ~ file);
     }
     
     /** Returns a string listing the file name or names (if readOrder is not 
@@ -163,7 +163,7 @@
                     ret ~= file;
             }
         } else {
-            assert (readOrder == PRIORITY.HIGH_LOW ||
+            debug assert (readOrder == PRIORITY.HIGH_LOW ||
                     readOrder == PRIORITY.HIGH_ONLY );
             
             for (int i = pathsLen - 1; i >= 0; --i) {
--- a/mde/font/FontTexture.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/font/FontTexture.d	Sat Feb 07 12:46:03 2009 +0000
@@ -25,7 +25,7 @@
 module mde.font.FontTexture;
 
 import mde.types.Colour;
-import mde.lookup.Options;
+import mde.content.AStringContent;
 import mde.font.exception;
 
 import derelict.freetype.ft;
@@ -240,8 +240,7 @@
         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)))
+        if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | fontOpts.modeFlag))
             throw new fontGlyphException ("Unable to render glyph");
         
         auto b = g.bitmap;
@@ -292,7 +291,7 @@
                 buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2];
             }
             
-            format = (fontOpts.renderMode() & RENDER_LCD_BGR) ? GL_BGR : GL_RGB;
+            format = (fontOpts.mode() >= 4) ? GL_BGR : GL_RGB;
         } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) {
             // NOTE: Notes above apply. But in this case converting the buffers seems essential.
             buffer = new ubyte[b.width*b.rows];
@@ -304,7 +303,7 @@
                 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;
+            format = (fontOpts.mode() >= 4) ? GL_BGR : GL_RGB;
         } else
             throw new fontGlyphException ("Unsupported freetype bitmap format");
         
@@ -445,26 +444,33 @@
     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 }
-FontOptions fontOpts;
-class FontOptions : Options {
-    /* renderMode have one of the following values, possibly with bit 31 set (see RENDER_LCD_BGR):
-     * FT_LOAD_TARGET_NORMAL    (0x00000)
-     * FT_LOAD_TARGET_LIGHT     (0x10000)
-     * FT_LOAD_TARGET_LCD       (0x30000)
-     * FT_LOAD_TARGET_LCD_V     (0x40000)
-     * The mode FT_LOAD_TARGET_MONO (0x20000) 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, defaultSize; char[] defaultFont;"));
-    
-    void validate() {
-    }
+struct fontOpts {
+static:
+    EnumContent mode;
+    int modeFlag;
+    /* lcdFilter should come from enum FT_LcdFilter:
+    * FT_LCD_FILTER_NONE (0), FT_LCD_FILTER_DEFAULT (1), FT_LCD_FILTER_LIGHT (2) */
+    EnumContent lcdFilter;
+    IntContent defaultSize;
+    StringContent defaultFont;
     
     static this() {
-        fontOpts = new FontOptions ("FontOptions");
+        mode = new EnumContent ("Font.mode", ["normal"[],"light","lcd","lcd_v","lcd_bgr","lcd_bgr_v"]);
+        lcdFilter = new EnumContent ("Font.lcdFilter", ["none"[],"default","light"]);
+        defaultSize = new IntContent ("Font.defaultSize");
+        defaultFont = new StringContent ("Font.defaultFont");
+        mode.addCallback (delegate void(Content) {
+           /* modeFlag should have one of the following values:
+            * FT_LOAD_TARGET_NORMAL    (0x00000)
+            * FT_LOAD_TARGET_LIGHT     (0x10000)
+            * FT_LOAD_TARGET_LCD       (0x30000)
+            * FT_LOAD_TARGET_LCD_V     (0x40000)
+            * The mode FT_LOAD_TARGET_MONO (0x20000) is unsupported. */
+            modeFlag = fontOpts.mode();
+            if (modeFlag == 2 || modeFlag == 4) modeFlag = 0x30000;
+            else if (modeFlag == 3 || modeFlag == 5) modeFlag = 0x40000;
+            else modeFlag <<= 16;
+        });
     }
 }
 
--- a/mde/font/font.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/font/font.d	Sat Feb 07 12:46:03 2009 +0000
@@ -17,7 +17,6 @@
 module mde.font.font;
 
 public import mde.types.Colour;
-import mde.lookup.Options;
 import mde.font.FontTexture;
 import mde.font.exception;
 
@@ -66,8 +65,7 @@
             }
             
             // Set LCD filtering method if LCD rendering is enabled.
-            const RMF = FT_LOAD_TARGET_LCD | FT_LOAD_TARGET_LCD_V;
-            if (fontOpts.renderMode() & RMF &&
+            if (fontOpts.mode() & 2 &&
                 FT_Library_SetLcdFilter(library, cast(FT_LcdFilter)fontOpts.lcdFilter())) {
                 /* An error occurred, presumably because LCD rendering support
                 * is not compiled into the library. */
@@ -79,7 +77,7 @@
                 
                 /* If no support for LCD filtering, then LCD rendering only emulates NORMAL with 3
                  * times wider glyphs. So disable and save the extra work. */
-                fontOpts.renderMode = FT_LOAD_TARGET_NORMAL;
+                fontOpts.mode = 0;
             }
             
             // Load the fallback font; if it throws let exception abort program
--- a/mde/gui/WidgetDataSet.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/WidgetDataSet.d	Sat Feb 07 12:46:03 2009 +0000
@@ -29,7 +29,6 @@
 
 public import mde.gui.types;
 import mde.content.AStringContent;
-import Items = mde.content.Items;
 
 // For loading from file:
 import mt = mde.file.mergetag.DataSet;
@@ -43,9 +42,9 @@
     logger = Log.getLogger ("mde.gui.WidgetDataSet");
 }
 
-/*************************************************************************************************
+/******************************************************************************
  * Contains data for all widgets in a GUI.
- *************************************************************************************************/
+ *****************************************************************************/
 class WidgetDataSet : mt.IDataSection
 {
     //BEGIN Mergetag code
@@ -55,11 +54,9 @@
             widgetData[id] = deserialize!(WidgetData) (dt);
         } else if (tp == "WDims" && (id in dimData) is null) {
             dimData[id] = cast(wdims) deserialize!(int[]) (dt);
-        } else if (tp == "EnumContent" && (id in enumContent) is null) {
-            // Add dynamic content. NOTE: could confict with content from another design/section.
-            EnumContent a = new EnumContent (id, deserialize!(EnumContent.EnumCStruct) (dt));
-            enumContent[id] = a;
-            Items.addContent (a);
+        } else if (tp == "EnumContent" && (id in Content.allContent) is null) {
+            // Add dynamic content.
+            new EnumContent (id, deserialize!(char[][]) (dt));
         }
     }
     // Only WidgetDataChanges is used for writing.
@@ -85,7 +82,6 @@
 protected:
     WidgetData[widgetID] widgetData;    // Per-widget data
     wdims[widgetID] dimData;            // Per-widget sizes
-    EnumContent[char[]] enumContent;
 }
 
 /*************************************************************************************************
@@ -112,8 +108,6 @@
             dlg ("WidgetData", id, serialize!(WidgetData) (data));
         foreach (id,dim; dimData)
             dlg ("WDims", id, serialize!(int[]) (cast(int[]) dim));
-        foreach (id,c; base.enumContent)
-            dlg ("EnumContent", id, serialize (c.structOf));
     }
     //END Mergetag code
     
@@ -132,7 +126,7 @@
     
     /** Do any changes exist? True if no changes have been stored. */
     bool noChanges () {
-        return widgetData.length == 0 && dimData.length == 0 && enumContent.length == 0;
+        return widgetData.length == 0 && dimData.length == 0;
     }
     
     protected WidgetDataSet base;
--- a/mde/gui/WidgetManager.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/WidgetManager.d	Sat Feb 07 12:46:03 2009 +0000
@@ -29,9 +29,7 @@
 import mde.gui.exception;
 
 import imde = mde.imde;
-import mde.lookup.Options;	// miscOpts.L10n callback
 import mde.content.Content;
-import Items = mde.content.Items;	// loadTranslation
 debug import mde.content.miscContent;	// Debug menu
 
 import mt = mde.file.mergetag.DataSet;
@@ -73,15 +71,16 @@
     protected this (char[] file) {
         mutex = new Mutex;  // Used on functions intended to be called from outside the gui package.
         fileName = file;
-        miscOpts.L10n.addCallback (&reloadStrings);
         
         clickCallbacks = new typeof(clickCallbacks);
         motionCallbacks = new typeof(motionCallbacks);
         
-        debug {
-            auto lWS = new EventContent ("logWidgetSize");
+        auto p = "MiscOptions.l10n" in Content.allContent;
+        assert (p, "MiscOptions.l10n not created!");
+        p.addCallback (&reloadStrings);
+        debug {	// add a debug-mode menu
+            auto lWS = new EventContent ("menus.debug."~file~".logWidgetSize");
             lWS.addCallback (&logWidgetSize);
-            imde.menus.append (new ContentList ("debug", [cast(Content)lWS]));
         }
     }
     
@@ -142,8 +141,6 @@
             logger.error (e.msg);
             throw new GuiException ("Failure parsing config file");
         }
-	
-        Items.loadTranslation ();
     }
     
     /** Load the gui from some design.
@@ -252,14 +249,18 @@
         }
     }
     
-    /** Called when translation strings have been reloaded. */
+    /** A change callback on MiscOptions.l10n content to update widgets.
+     *
+     * Relies on another callback reloading translations to content first! */
     protected void reloadStrings (Content) {
-	Items.loadTranslation;
-	child.setup (++setupN, 2);
-	child.setWidth  (w, -1);
-	child.setHeight (h, -1);
-	child.setPosition (0,0);
-	requestRedraw;
+        synchronized(mutex) {
+            if (child is null) return;
+            child.setup (++setupN, 2);
+            child.setWidth  (w, -1);
+            child.setHeight (h, -1);
+            child.setPosition (0,0);
+            requestRedraw;
+        }
     }
     
     // These methods are only intended for use within the gui package.
@@ -372,7 +373,7 @@
             logger.error ("Error creating widget: {}; creating a debug widget instead.", e.msg);
         }
     
-        return new DebugWidget (this, this, id, data);
+        return new DebugWidget (this, this, id, data, content);
     }
     
     override WidgetData widgetData (widgetID id) {
@@ -483,7 +484,7 @@
     // blank: 0x1
     FixedBlank		= 0x1,
     SizableBlank	= 0x2,
-    Debug		= 0xF,
+    Debug		= TAKES_CONTENT | 0xF,
     
     // popup widgets: 0x10
     PopupMenu		= TAKES_CONTENT | 0x11,
@@ -514,9 +515,9 @@
 const char[][] WIDGETS = [
         "FixedBlank",
         "SizableBlank",
-        "Debug",
 	"TextLabel",
 	"addContent",
+        "Debug",
 	"PopupMenu",
 	"ContentLabel",
         "DisplayContent",
--- a/mde/gui/widget/AChildWidget.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/AChildWidget.d	Sat Feb 07 12:46:03 2009 +0000
@@ -22,7 +22,7 @@
 module mde.gui.widget.AChildWidget;
 
 public import mde.gui.widget.Ifaces;
-import mde.content.Content;
+import mde.content.IContent;
 import mde.gui.exception;
 
 debug {
--- a/mde/gui/widget/AParentWidget.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/AParentWidget.d	Sat Feb 07 12:46:03 2009 +0000
@@ -23,7 +23,7 @@
 
 public import mde.gui.widget.AChildWidget;
 import mde.gui.exception;
-import mde.content.Content;
+import mde.content.IContent;
 
 debug {
     import tango.util.log.Log : Log, Logger;
--- a/mde/gui/widget/Floating.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/Floating.d	Sat Feb 07 12:46:03 2009 +0000
@@ -18,7 +18,7 @@
 
 import mde.gui.widget.AParentWidget;
 import mde.gui.exception;
-import mde.content.Content;
+import mde.content.IContent;
 
 import tango.util.log.Log : Log, Logger;
 
--- a/mde/gui/widget/Ifaces.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/Ifaces.d	Sat Feb 07 12:46:03 2009 +0000
@@ -31,7 +31,7 @@
 
 public import mde.gui.types;
 public import mde.gui.renderer.IRenderer;
-import mde.content.Content;
+import mde.content.IContent;
 
 
 /******************************************************************************
--- a/mde/gui/widget/PopupMenu.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/PopupMenu.d	Sat Feb 07 12:46:03 2009 +0000
@@ -20,7 +20,7 @@
 
 import mde.gui.widget.AParentWidget;
 
-import mde.content.miscContent;
+import mde.content.Content;
 import mde.gui.exception;
 
 debug {
--- a/mde/gui/widget/contentFunctions.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/contentFunctions.d	Sat Feb 07 12:46:03 2009 +0000
@@ -26,8 +26,6 @@
 import mde.gui.widget.miscContent;
 
 import mde.content.AStringContent;
-import mde.content.miscContent;
-import Items = mde.content.Items;
 
 /******************************************************************************
  * A function which uses Items.get (data.strings[0]) to get a content and
@@ -36,7 +34,7 @@
  *****************************************************************************/
 IChildWidget addContent (IWidgetManager mgr, IParentWidget parent, widgetID, WidgetData data, IContent) {
     if (data.strings.length != 2) throw new WidgetDataException;
-    return mgr.makeWidget (parent, data.strings[1], Items.get (data.strings[0]));
+    return mgr.makeWidget (parent, data.strings[1], Content.get (data.strings[0]));
 }
 
 /******************************************************************************
@@ -58,7 +56,8 @@
     }
     if (cast(IContentList) c)
         return new ContentListWidget(mgr,parent,id,data,c);
-    if (cast(EventContent) c)
+    // Normally only EventContents are used for buttons, but any Content can be:
+    if (cast(Content) c)
         return new ButtonContentWidget(mgr,parent,id,data,c);
     // generic uneditable option
     return new DisplayContentWidget(mgr,parent,id,data,c);
--- a/mde/gui/widget/layout.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/layout.d	Sat Feb 07 12:46:03 2009 +0000
@@ -19,7 +19,7 @@
 import mde.gui.widget.AParentWidget;
 import mde.gui.exception;
 
-import mde.content.miscContent;
+import mde.content.Content;
 
 import tango.util.container.HashMap;
 
--- a/mde/gui/widget/miscContent.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/miscContent.d	Sat Feb 07 12:46:03 2009 +0000
@@ -22,7 +22,6 @@
 import mde.gui.exception;
 
 import mde.content.AStringContent;
-import mde.content.miscContent;
 
 debug {
     import tango.util.log.Log : Log, Logger;
@@ -60,7 +59,7 @@
 class ButtonContentWidget : AButtonWidget
 {
     this (IWidgetManager mgr, IParentWidget parent, widgetID id, WidgetData, IContent c) {
-	content = cast(EventContent) c;
+	content = cast(Content) c;
         if (content is null) throw new ContentException (this);
         adapter = mgr.renderer.getAdapter ();
         super (mgr, parent, id);
@@ -88,7 +87,7 @@
     
 protected:
     IRenderer.TextAdapter adapter;
-    EventContent content;
+    Content content;
     int index;
 }
 
--- a/mde/gui/widget/miscWidgets.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/gui/widget/miscWidgets.d	Sat Feb 07 12:46:03 2009 +0000
@@ -19,6 +19,7 @@
 import mde.gui.widget.AChildWidget;
 import mde.gui.exception;
 import mde.gui.renderer.IRenderer;
+import mde.content.IContent;
 
 import tango.util.log.Log : Log, Logger;
 
@@ -60,9 +61,9 @@
 /// A debug widget. Essentially as SizableBlankWidget but doesn't mind any amount of data and prints it.
 class DebugWidget : SizableWidget
 {
-    this (IWidgetManager mgr, IParentWidget parent, widgetID id, WidgetData data) {
+    this (IWidgetManager mgr, IParentWidget parent, widgetID id, WidgetData data, IContent c) {
         super (mgr, parent, id);
-        logger.warn ("Debug widget ({}); parameters: ints = {}, strings = {}", id, data.ints, data.strings);
+        logger.warn ("Debug widget ({}); parameters: ints = {}, strings = {}; content: {}", id, data.ints, data.strings, c);
     }
     
     override void draw () {
--- a/mde/imde.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/imde.d	Sat Feb 07 12:46:03 2009 +0000
@@ -20,21 +20,17 @@
 module mde.imde;
 
 import mde.scheduler.Scheduler;
-import mde.content.miscContent;
 
 static this () {
     // Make available to all importing modules:
     mainSchedule = new Scheduler;
-    menus = new ContentList ("menu");
 }
 
-ContentList menus;	/// Root of all menus; import mde.menus to add entries
-
-Scheduler mainSchedule; /// The schedule used by the main loop.
-
 /** Some enums used by per request scheduled functions. */
 enum SCHEDULE : Scheduler.ID {
     DRAW
 };
 
-bool run = true;	// main loop continues if this is true
+Scheduler mainSchedule; /// The schedule used by the main loop.
+
+bool run = true;	/// The main loop continues if this is true.
--- a/mde/lookup/Options.d	Sun Feb 01 12:36:21 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,469 +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 loading and saving of, and allows generic access to named option variables
- * of a simple type (see Options.TYPES). */
-module mde.lookup.Options;
-
-import mde.file.paths;
-import mde.exception;
-
-public import mde.content.AStringContent;
-import mde.content.miscContent;	// ContentLists used by content.Items
-
-import mde.file.mergetag.Reader;
-import mde.file.mergetag.Writer;
-import mde.file.mergetag.DataSet;
-import mde.file.serialize;
-
-import tango.core.Exception : ArrayBoundsException;
-import tango.util.log.Log : Log, Logger;
-private Logger logger;
-static this() {
-    logger = Log.getLogger ("mde.lookup.Options");
-}
-
-/*************************************************************************************************
- * This class and the OptionChanges class contain all the functionality.
- * 
- * Options are stored in derived class instances, tracked by the static portion of Options. Each
- * value is stored in a ValueContent class, whose value can be accessed with opCall, opCast and
- * opAssign. These class objects can be given callbacks called whenever their value is changed.
- * 
- * Public static methods allow getting the list of tracked sub-class instances, and loading and saving
- * option values. A public non-static method allows generic access to option variables.
- * 
- * Generic access to Options is of most use to a gui, allowing Options to be edited generically.
- * 
- * The easiest way to use Options is to use an existing sub-class as a template, e.g. MiscOptions.
- *************************************************************************************************/
-class Options : IDataSection
-{
-    /** Do not instantiate directly; use a sub-class.
-     *
-     * CTOR adds any created instance to the list of classes tracked statically for loading/saving
-     * and generic access.
-     * 
-     * Normally instances are created by a static CTOR. */
-    protected this(char[] name)
-    in {
-	assert (((cast(ID) name) in subClasses) is null);  // Don't allow a silent replacement
-    } body {
-	subClasses[cast(ID) name] = this;
-    }
-    
-    //BEGIN Templates: internal
-    package {
-	// All supported types, for generic handling via templates. It should be possible to change
-	// the supported types simply by changing this list.
-	template store(A...) { alias A store; }
-	alias store!(bool, int, double, char[]) TYPES;   // types handled
-	
-	// Get name of a type. Basically just stringof, but special handling for arrays.
-        // Use TName!(T) for a valid symbol name, and T.stringof for a type.
-        template TName(T : T[]) {
-            const char[] TName = TName!(T) ~ "A";
-        }
-        template TName(T) {
-            const char[] TName = T.stringof;
-        }
-    }
-    private {
-        // True if type is one of A
-        template TIsIn(T, A...) {
-            static if (A.length) {
-                static if (is(T == A[0]))
-                    const bool TIsIn = true;
-                else
-                    const bool TIsIn = TIsIn!(T,A[1..$]);
-            } else
-                const bool TIsIn = false;	// no more possibilities
-        }
-        
-        // For addTag
-        template addTagMixin(T, A...) {
-            const char[] ifBlock = `if (tp == "`~T.stringof~`") {
-    auto p = id in opts;
-    if (p) {
-        auto q = cast(`~ContentN!(T)~`) (*p);
-        if (q) q.assignNoCb = parseTo!(`~T.stringof~`) (dt);
-    }
-}`;
-            static if (A.length)
-                const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin;
-            else
-                const char[] addTagMixin = ifBlock;
-        }
-    }
-    //END Templates: internal
-    
-    
-    //BEGIN Static
-    static {
-	/** Get the hash map of Options classes. READ-ONLY. */
-	Options[ID] optionsClasses () {
-	    return subClasses;
-	}
-	
-	// Track all sections for saving/loading/other generic handling.
-        private Options[ID] subClasses;
-        private bool changed = false;	// any changes at all, i.e. do we need to save?
-	
-	ContentList allContentList;	/// Initially null; created by mde.content.Items on use.
-	
-    	/* Load/save options from file.
-         *
-         * If the file doesn't exist, no reading is attempted (options are left at default values).
-    	*/
-        private const fileName = "options";
-        void load () {
-            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 (NoFileException e) {
-                // Just return. Options file will be created on exit.
-            } catch (Exception e) {
-                logger.warn ("Loading options failed: "~e.msg);
-                logger.warn ("If warning persists, delete the offending file.");        // FIXME - delete the bad file somehow
-            }
-            foreach (opts; subClasses)
-                opts.validate;  // post-load checking of variables
-        }
-        void save () {
-            if (!changed) return;   // no changes to save
-            debug logger.trace ("Saving options...");
-            
-            DataSet ds = new DataSet();
-            foreach (id, subOpts; subClasses)
-                ds.sec[id] = subOpts.optionChanges;
-            
-            // Read locally-stored options
-            try {
-                IReader reader;
-                reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds);
-                reader.dataSecCreator = delegate IDataSection(ID id) {
-                    debug logger.warn ("New section appearing in options.mtt during save (ignored & overwritten): "~id);
-                    return null;    // All recognised sections are already in the dataset.
-                };
-                reader.read;
-            } catch (NoFileException) {
-                // No user file exists; not an error.
-            } catch (Exception e) {
-            	// Log a message and continue, overwriting the file:
-                logger.error ("Loading options aborted: " ~ e.msg);
-            }
-        
-            try {
-                IWriter writer;
-                writer = confDir.makeMTWriter (fileName, ds);
-                writer.write();
-            } catch (Exception e) {
-                logger.error ("Saving options aborted: "~e.msg);
-            }
-        }
-    }
-    //END Static
-    
-    
-    //BEGIN Non-static
-    /// Get all Options stored with a ValueContent.
-    Content[char[]] content() {
-        return opts;
-    }
-    
-    /** Variable validate function, called when options are loaded from file.
-     *
-     * This can be overridden to enforce limits on option variables, etc. */
-    protected void validate() {}
-    
-    /** All content in a ContentList. Initially null; mde.content.Items creates this and loads the
-    * translation strings of all sub-content upon first request involving this Options instance. */
-    ContentList contentList;
-    
-    protected {
-        OptionChanges optionChanges;	// all changes to options (for saving)
-	Content[char[]] opts;	// generic list of option values
-    }
-    
-    //BEGIN Mergetag loading/saving code
-    void addTag (char[] tp, ID id, char[] dt) {
-        mixin(addTagMixin!(TYPES).addTagMixin);
-    }
-    // Only OptionChanges writes stuff
-    void writeAll (ItemDelg dlg) {}
-    //END Mergetag loading/saving code
-    //END Non-static
-    
-    
-    //BEGIN Templates: impl & optionsThis
-    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 will have a
-         trailing comma unless no match was found in which case an empty string is returned.
-         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 .. $]);
-        }
-        // 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 B is empty return an empty string otherswise return what's below:
-        template catOrNothing(char[] A,char[] B) {
-            static if (B.length)
-                const char[] catOrNothing = A~` `~sTC!(B)~";\n";
-            else
-                const char[] catOrNothing = ``;
-        }
-        // foreach decl...
-        template createContents(T, char[] A) {
-            static if (A.length == 0)
-                const char[] createContents = "";
-            else static if (A[0] == ' ')
-                const char[] createContents = createContents!(T,A[1..$]);
-            else
-                const char[] createContents = "opts[\""~A[0..cIndex!(A)]~"\"] = (" ~ A[0..cIndex!(A)]~ " = new "~ContentN!(T)~" (\""~A[0..cIndex!(A)]~"\")).addCallback (&optionChanges.set);\n"~
-                createContents!(T,A[cIndex!(A)+1..$]);
-        }
-        // for recursing on TYPES
-        template optionsThisInternal(char[] A, B...) {
-            static if (B.length) {
-                const char[] optionsThisInternal = createContents!(B[0],parseT!(B[0].stringof,A))~
-		    optionsThisInternal!(A,B[1..$]);
-	    } else
-                const char[] optionsThisInternal = ``;
-        }
-        template declValsInternal(char[] A, B...) {
-            static if (B.length) {
-                const char[] declValsInternal = catOrNothing!(ContentN!(B[0]),parseT!(B[0].stringof,A)) ~ declValsInternal!(A,B[1..$]);
-            } else
-                const char[] declValsInternal = ``;
-        }
-    } protected {
-        /** Declares the values.
-         *
-         * Basic types are replaced with a ValueContent class to keep the option synchronized and
-         * generalize use. */
-        template declVals(char[] A) {
-            const char[] declVals = declValsInternal!(A, TYPES);
-        }
-        /** Produces the implementation code to go in the constuctor. */
-        template optionsThis(char[] A) {
-            const char[] optionsThis =
-                    "optionChanges = new OptionChanges;\n" ~
-                    "super (name);\n" ~
-                    optionsThisInternal!(A,TYPES);
-        }
-        /+ Needs too many custom parameters to be worth it? Plus makes class less readable.
-        /** 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 (declVals!(A)~`
-	this (char[] name) {
-	`~optionsThis!(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 = declVals!(A)~"\nthis(char[] name){\n"~optionsThis!(A)~"}";
-            // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}"
-        }
-    }
-    //END Templates: impl & optionsThis
-}
-
-/*************************************************************************************************
- * Special class to store all locally changed options.
- * 
- * This allows only changed options and those already stored in the user directory to be saved, so
- * that other options can be merged in from a global directory, allowing any options not locally
- * set to be changed globally.
- *************************************************************************************************/
-class OptionChanges : IDataSection
-{
-    //BEGIN Templates
-    private {
-        alias Options.TName TName;
-        alias Options.TYPES TYPES;
-        template Vars(A...) {
-            static if (A.length) {
-                const char[] Vars = A[0].stringof~`[ID] `~TName!(A[0])~`s;` ~ Vars!(A[1..$]);
-            } else
-                const char[] Vars = ``;
-        }
-        // For addTag
-        template addTagMixin(T, A...) {
-            const char[] ifBlock = `if (tp == "`~T.stringof~`") {
-                if ((id in `~TName!(T)~`s) is null)
-                    `~TName!(T)~`s[id] = parseTo!(`~T.stringof~`) (dt);
-            }`;
-            static if (A.length)
-                const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin;
-            else
-                const char[] addTagMixin = ifBlock;
-        }
-        // For writeAll
-        template writeAllMixin(A...) {
-            static if (A.length) {
-                const char[] writeAllMixin =
-                `foreach (id, val; `~TName!(A[0])~`s)
-                    dlg ("`~A[0].stringof~`", id, serialize (val));
-                ` ~ writeAllMixin!(A[1..$]);
-            } else
-                const char[] writeAllMixin = ``;
-        }
-    }
-    //END Templates
-    // 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.
-    mixin(Vars!(TYPES));
-    
-    void set (Content c) {
-	union U {
-	    BoolContent bc;
-	    StringContent sc;
-	    IntContent ic;
-	    DoubleContent dc;
-	}
-	U u;
-	if ((u.bc = cast(BoolContent) c) !is null)
-	    bools[u.bc.symbol] = u.bc();
-	else if ((u.sc = cast(StringContent) c) !is null)
-	    charAs[u.sc.symbol] = u.sc();
-	else if ((u.ic = cast(IntContent) c) !is null)
-	    ints[u.ic.symbol] = u.ic();
-	else if ((u.dc = cast(DoubleContent) c) !is null)
-	    doubles[u.dc.symbol] = u.dc();
-	Options.changed = true;
-    }
-	
-    this () {}
-    
-    //BEGIN Mergetag loading/saving code
-    // HIGH_LOW priority: only load symbols not currently existing
-    void addTag (char[] tp, ID id, char[] dt) {
-        mixin (addTagMixin!(TYPES).addTagMixin);
-    }
-    void writeAll (ItemDelg dlg) {
-        mixin(writeAllMixin!(TYPES));
-    }
-    //END Mergetag loading/saving code
-}
-
-/** A home for all miscellaneous options.
- *
- * Also a template for deriving Options; comments explain what does what.
- * 
- * Translation strings for the options are looked for in data/L10n/SectionName.mtt where
- * this ("SectionName") names the instance. */
-MiscOptions miscOpts;
-class MiscOptions : Options {
-    /* The key step is to mixin impl.
-    The syntax is just as simple variables are declared, which is how these options used to be
-    stored. Now they're enclosed in ValueContent classes; e.g. "char[] L10n;" is replaced with
-    "TextContent L10n;". The pragma statement can be uncommented to see what code gets injected
-    (note: pragma () gets called each time the module is imported as well as when it's compiled).
-    impl creates a this() function; if you want to include your own CTOR see impl's ddoc. */
-    const A = "bool exitImmediately; int maxThreads, logLevel, logOutput; double pollInterval; char[] L10n;";
-    //pragma (msg, impl!(A));
-    //mixin (impl!(A));
-    BoolContent exitImmediately;
-    IntContent maxThreads, logOutput;
-    EnumContent logLevel;
-    DoubleContent pollInterval;
-    StringContent L10n;
-    
-    this(char[] name){
-	optionChanges = new OptionChanges;
-	super (name);
-	opts["exitImmediately"] = (exitImmediately = new BoolContent ("exitImmediately")).addCallback (&optionChanges.set);
-	opts["maxThreads"] = (maxThreads = new IntContent ("maxThreads")).addCallback (&optionChanges.set);
-	opts["logLevel"] = (logLevel = new EnumContent ("logLevel", ["Trace", "Info", "Warn", "Error", "Fatal", "None"])).addCallback (&optionChanges.set);
-	opts["logOutput"] = (logOutput = new IntContent ("logOutput")).addCallback (&optionChanges.set);
-	opts["pollInterval"] = (pollInterval = new DoubleContent ("pollInterval")).addCallback (&optionChanges.set);
-	opts["L10n"] = (L10n = new StringContent ("L10n")).addCallback (&optionChanges.set);
-    }
-    
-    // Overriding validate allows limits to be enforced on variables at load time. Currently
-    // there's no infrastructure for enforcing limits when options are set at run-time.
-    override void validate() {
-        // Try to enforce sensible values, whilst being reasonably flexible:
-        if (maxThreads() < 1 || maxThreads() > 64) {
-            logger.warn ("maxThreads must be in the range 1-64. Defaulting to 4.");
-            maxThreads = 4;
-        }
-        if (pollInterval() !<= 0.1 || pollInterval() !>= 0.0)
-            pollInterval = 0.01;
-    }
-    
-    // A static CTOR is a good place to create the instance (it must be created before init runs).
-    static this() {
-	// Adds instance to Options's tracking; the string is the section name in the config files.
-	miscOpts = new MiscOptions ("MiscOptions");
-    }
-}
--- a/mde/lookup/Translation.d	Sun Feb 01 12:36:21 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,245 +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/>. */
-/** 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.lookup.Options;
-import mde.file.paths;
-import mde.exception;
-
-import mde.file.mergetag.DataSet;
-import mde.file.mergetag.Reader;
-import mde.file.mergetag.exception;
-import mde.file.deserialize;
-
-import tango.util.log.Log : Log, Logger;
-
-/** The translation class
-*
-* See module description for details.
-*
-* Encoding used is UTF-8.
-*/
-class Translation : IDataSection
-{
-    /** 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 {
-	/** Get Translation instance for _section section.
-	 *
-	 * On the first call, loads all Translation sections.
-	 * 
-	 * These are loaded from data/L10n/locale.mtt where locale is the current locale, as well
-	 * as any files named for locales specified in the header tag <char[][]|depends=[...]>.
-	 *
-	 * On errors, a message is logged and the function continues, likely resulting in some
-	 * symbol names being untranslated. */
-	Translation get (char[] section) {
-	    if (sections is null || loadedL10n !is miscOpts.L10n()) {
-		if (sections.length) {	// Clear entries hash-map, but re-use classes
-		    foreach (s; sections)
-			s.entries = null;
-		}
-		loadedL10n = miscOpts.L10n();
-		debug logger.trace ("Loading L10n: "~loadedL10n);
-		char[][] files = [loadedL10n];	// initial locale plus dependencies
-		size_t read = 0;	// index in files of next file to read
-		while (read < files.length) {
-		    try {
-			IReader reader = dataDir.makeMTReader ("L10n/"~files[read++], PRIORITY.HIGH_LOW, null, true);
-			assert (reader.dataset.header);
-			auto dp = "depends" in reader.dataset.header._charAA;
-			if (dp) {	// append, making sure no duplicates are inserted
-			    f1: foreach (dep; *dp) {
-				foreach (f; files)
-				    if (f == dep)
-					continue f1;
-				files ~= dep;
-			    }
-			}
-			reader.dataSecCreator = delegate IDataSection(ID sec) {
-			    auto p = sec in sections;
-			    if (p) return *p;
-			    return new Translation (sec);
-			};
-			reader.read;
-		    } catch (MTException e) {
-			logger.error ("Mergetag exception occurred:");
-			logger.error (e.msg);
-		    }
-		}
-	    }
-	    auto p = section in sections;
-	    if (p is null) {
-		logger.warn ("Section "~section ~ " not found in files; strings will not be translated.");
-		return new Translation (section);
-	    }
-	    return *p;
-	}
-	private Translation[char[]] sections;
-	private char[] loadedL10n;	// reload if different
-    }
-    
-    final char[] section;	// ONLY used to log an error message
-    
-    alias entry opCall;	    /// Convenience alias
-    
-    /** Get the translation for the given identifier.
-    * If no entry exists, the identifier will be returned.
-    *
-    * Optionally, the description can be returned. */
-    char[] entry (char[] id) {
-        Entry* p = id in entries;
-        if (p) {
-            return p.name;
-        } else {
-            return id;
-        }
-    }
-    /** ditto */
-    char[] entry (char[] id, out char[] description) {
-        Entry* p = id in entries;
-        if (p) {
-            description = p.desc;
-            return p.name;
-        } else {
-            return id;
-        }
-    }
-    
-    /** Alternative usage: return a Translation.Entry struct. */
-    Entry getStruct (char[] id) {
-        Entry* p = id in entries;
-        if (p) {
-            return *p;
-        } else {
-            logger.warn ("Unable to find translation for {} in {}", id, section);
-            Entry ret;
-            ret.name = id;
-            return ret;
-        }
-    }
-    
-    static this() {
-        logger = Log.getLogger ("mde.lookup.Translation");
-    }
-    
-    /* Mergetag functionality.
-     *
-     * Merge tags in to entries, prefering existing values. */
-    void addTag (char[] tp, ID id, char[] dt) {
-        if (tp == "entry") {
-            // If the tag already exists, don't replace it
-            if (cast(char[]) id in entries) return;
-            
-            Entry entry = deserialize!(Entry) (dt);
-            if (entry.name is null) {   // This tag is invalid; ignore it
-                logger.error ("In L10n/*.mtt section "~section~": tag with ID "~cast(char[])id~" has no data");
-                return;
-            }
-            entries[cast(char[]) id] = entry;
-        }
-    }
-    
-    // This class is read-only and has no need of being saved.
-    void writeAll (ItemDelg) {}
-    
-    /** 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[] name;        // The translated string
-        char[] desc;        // An optional description
-    }
-    
-private:
-    /* Sets name and ensures only Translation's static functions can create instances. */
-    this (char[] n) {
-        section = n;
-	sections[n] = this;
-    }
-    
-    //BEGIN Data
-    static Logger logger;
-    
-    Entry[char[]] entries;  // all entries
-    //END Data
-    
-    debug (mdeUnitTest) unittest {
-        // Relies on files in unittest/data/L10n: locale-x.mtt for x in 1..4
-        
-        // Hack a specific locale. Also to allow unittest to run without init.
-        StringContent realL10n = miscOpts.L10n;
-        miscOpts.L10n = new StringContent ("L10n", "locale-1");
-        
-	// Struct tests
-	Translation t = get ("section-2");
-	Entry e = t.getStruct ("entry-1");
-	assert (e.name == "Test 1");
-	assert (e.desc == "Description");
-	e = t.getStruct ("entry-2");
-        assert (e.name == "Test 2");
-	assert (e.desc is null);
-	
-	// Dependency tests. Priority should be 1,2,3,4 (entries should come from first file in list that they appear in).
-	t = get ("section-1");
-	char[] d = "1";
-	assert (t.entry ("file-1", d) == "locale-1");
-	assert (d is null);
-	assert (t.entry ("file-2", d) == "locale-2");
-	assert (d == "desc2");
-	assert (t.entry ("file-3") == "locale-3");
-	assert (t.entry ("file-4") == "locale-4");
-	
-        // Restore
-	delete miscOpts.L10n;
-        miscOpts.L10n = realL10n;
-	sections = null;
-        
-        logger.info ("Unittest complete.");
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/mainLoop.d	Sat Feb 07 12:46:03 2009 +0000
@@ -0,0 +1,59 @@
+/* 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/>. */
+
+/******************************************************************************
+ * Adds some basic functionality to the main loop.
+ *****************************************************************************/
+module mde.mainLoop;
+
+import mde.imde;
+import mde.scheduler.Scheduler;
+import mde.events;
+import mde.content.AStringContent;	// pollInterval option
+
+static this () {
+    // Make available to all importing modules:
+    mainSchedule = new Scheduler;
+    mainSchedule.add (mainSchedule.getNewID, &mde.events.pollEvents).frame = true;
+    // Polling interval of main loop; use old name to save renaming:
+    pollInterval = new DoubleContent ("MiscOptions.pollInterval");
+    pollInterval.addCallback (delegate void(Content) {
+        if (pollInterval() !<= 0.1 || pollInterval() !>= 0.0)
+            pollInterval = 0.01;
+        mainInterval = pollInterval();
+    });
+        
+    version (mdeBenchmark) {
+        // Print fps every 5 seconds:
+        void frameRateTest (TimeSpan elapsed) {
+            static TimeSpan total;
+            static size_t num;
+            total += elapsed;
+            ++num;
+            if (total.seconds > 5) {
+                logger.info ("{0} frames in {1:f3} seconds: {2:f3} fps", num, total.interval, num/total.interval);
+                total = TimeSpan.zero;
+                num = 0;
+            }
+            mainSchedule.request (SCHEDULE.DRAW);	// force draw every frame
+            mainInterval = 0.0;				// force zero wait
+        }
+        mainSchedule.add (mainSchedule.getNewID, &frameRateTest).frame = true;
+    }
+
+}
+
+DoubleContent pollInterval;
+double mainInterval;	/// Polling interval of main loop (kept in sync with pollInterval)
--- a/mde/mde.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/mde.d	Sat Feb 07 12:46:03 2009 +0000
@@ -20,11 +20,10 @@
  */
 module mde.mde;
 
-import mde.imde;                        // this module's interface for external modules
+import mde.imde;
+import mde.mainLoop;			// Some setup for the main loop
 import mde.setup.Init;                  // initialization
 import mde.setup.Screen;                // Screen.draw()
-import mde.events;                      // pollEvents()
-import mde.lookup.Options;              // pollInterval option
 import mde.scheduler.Scheduler;         // mainSchedule
 
 import tango.core.Thread : Thread;	// Thread.sleep()
@@ -75,20 +74,21 @@
     Screen.addDrawable (new SimpleDrawable);    // a drawable to print a message.
     
     //BEGIN Main loop setup
-    /* Note: the main loop is currently controlled by the scheduler. This is not really ideal,
-     * since it provides no direct control of the order in which components are executed and does
-     * not allow running components simultaeneously with threads.
-     * Note: probably drawing should start at the beginning of the loop and glFlush()/swapBuffers
-     * be called at the end to optimise. */
+    /* Note: the main loop is currently controlled by the scheduler. This is
+     * not really ideal, since it provides no direct control of the order in
+     * which components are executed and does not allow running components
+     * simultaeneously with threads. However, it does allow easy run-on-request
+     * functionality.
+     * Note: probably drawing should start at the beginning of the loop and
+     * glFlush()/swapBuffers be called at the end to optimise. */
     mainSchedule.add (SCHEDULE.DRAW, &Screen.draw).request = true;      // Draw, per event and first frame only.
-    mainSchedule.add (mainSchedule.getNewID, &mde.events.pollEvents).frame = true;
+    // Further setup is done by mde.mainLoop.
     //END Main loop setup
     
-    double pollInterval = miscOpts.pollInterval();
     while (run) {
         mainSchedule.execute (Clock.now());
         
-        Thread.sleep (pollInterval);	// sleep this many seconds
+        Thread.sleep (mainInterval);	// sleep this many seconds
     }
     
     return 0;		// cleanup handled by init's DTOR
--- a/mde/menus.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/menus.d	Sat Feb 07 12:46:03 2009 +0000
@@ -31,15 +31,15 @@
 }
 
 static this () {
-    auto quit = new EventContent("quit");
+    auto quit = new EventContent("menus.main.quit");
     quit.addCallback ((Content){
         debug logger.trace ("Quit (from menu)");
         run = false;
     });
-    auto main = new ContentList ("main", [cast(Content)quit]);
     debug {
-        main.append (new ContentList ("sm1", [cast(Content) new EventContent ("a"), new EventContent ("b")]));
-        main.append (new ContentList ("sm2", [cast(Content) new EventContent ("c"), new EventContent ("d")]));
+        new EventContent ("menus.main.a");
+        new EventContent ("menus.main.b");
+        new EventContent ("menus.main.submenu.c");
+        new EventContent ("menus.main.submenu.d");
     }
-    menus.append (main);
 }
--- a/mde/setup/Init.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/setup/Init.d	Sat Feb 07 12:46:03 2009 +0000
@@ -42,8 +42,10 @@
 
 import mde.setup.InitStage;     // Controls external delegates run by init
 import mde.setup.exception;
+import mde.setup.logger;
 
-import mde.lookup.Options;
+import mde.content.AStringContent;
+import mde.content.ContentLoader;
 import paths = mde.file.paths;
 import mde.exception;           // optionsLoadException
 import imde = mde.imde;
@@ -70,30 +72,31 @@
 import derelict.util.exception;
 
 
-/**************************************************************************************************
+/******************************************************************************
  * 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.
- *************************************************************************************************/
+ * 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
 {
     static this() {
-        // Set up the logger temporarily (until pre-init):
-        Logger root = Log.root;
-        debug root.level(Logger.Trace);
-        else  root.level(Logger.Info);
-        root.add(new AppendConsole);
-        
         logger = Log.getLogger ("mde.setup.Init");
+        exitImmediately = new BoolContent ("MiscOptions.exitImmediately");
+        maxThreads = new IntContent ("MiscOptions.maxThreads");
+        logLevel = new EnumContent ("MiscOptions.logLevel",
+                                    ["Trace", "Info", "Warn", "Error", "Fatal", "None"]);
+        logOutput = new EnumContent ("MiscOptions.logOutput",
+                                     ["none", "console", "file", "both"]);
     }
     
     /** this() − pre-init and init */
     this(char[][] cmdArgs)
     {
-        /******************************************************************************************
+        /**********************************************************************
          * Pre-init - init code written in this module.
-         *****************************************************************************************/
+         *********************************************************************/
         debug logger.trace ("Init: starting pre-init");
         //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).
@@ -142,9 +145,9 @@
         *   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);
+            ContentLoader.load();
+        } catch (Exception e) {
+            throw new InitException ("Loading options (content values) failed: " ~ e.msg);
         }
         debug logger.trace ("Init: loaded options successfully");
         
@@ -157,15 +160,15 @@
 	    
             // Now re-set the logging level and add callback to set on change:
             setLogLevel ();
-            miscOpts.logLevel.addCallback (&setLogLevel);
+            logLevel.addCallback (&setLogLevel);
             
-            if (miscOpts.logOutput() & 2) {     // first appender so root seperator messages don't show on console
+            if (logOutput() & 2) {     // first appender so root seperator messages don't show on console
                 // Use 2 log files with a maximum size of 16kiB:
                 root.add (new AppendFiles (paths.logDir~"/log-.txt", 2, 16*1024));
                 root.append (Level.None, ""); // some kind of separation between runs
                 root.append (Level.None, "");
             }
-            if (miscOpts.logOutput() & 1)
+            if (logOutput() & 1)
                 root.add(new AppendConsole);
             logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now));
         } catch (Exception e) {
@@ -177,7 +180,7 @@
         }
         
         // a debugging option:
-        imde.run = !args.contains("q") && !miscOpts.exitImmediately();
+        imde.run = !args.contains("q") && !exitImmediately();
         debug logger.trace ("Init: applied pre-init options");
         
         //BEGIN Load dynamic libraries
@@ -199,9 +202,9 @@
         //END Load dynamic libraries
         
         
-        /******************************************************************************************
+        /**********************************************************************
          * Init − where init code from external modules gets hooked in.
-         *****************************************************************************************/
+         *********************************************************************/
         debug logger.trace ("Init: done pre-init, starting init stages");
         
         // Calculate reverse dependencies of stages:
@@ -221,7 +224,7 @@
         
         runStages!(false);      // cleanup delegates
         
-        Options.save(); // save options... do so here for now
+        ContentLoader.save();	// save options before exiting
         
         debug logger.trace ("Cleanup: done");
     }
@@ -251,7 +254,7 @@
             }
         }
         // Counts number of active threads, and before threads are started is number to use:
-        size_t numWorking = (toRun.size < miscOpts.maxThreads()) ? toRun.size : miscOpts.maxThreads();
+        size_t numWorking = (toRun.size < maxThreads()) ? toRun.size : maxThreads();
         enum STATE {    WORKING = 0,    DONE = 1,       ABORT = 2 }
         STATE doneInit = STATE.WORKING;
         Mutex toRunM = new Mutex;       // synchronization on toRun, numWorking
@@ -377,7 +380,7 @@
         } catch (ThreadException e) {
             logger.error ("Exception while using threads: "~e.msg);
             logger.error ("Disabling threads and attempting to continue.");
-            miscOpts.maxThreads = 1;    // count includes current thread
+            maxThreads = 1;    // count includes current thread
             auto x = new InitStageThread (0);
             x.initThreadFct();                            // try with just this thread
         }       // any other exception will be caught in main() and abort program
@@ -392,10 +395,13 @@
     
     private static {
         Logger logger;
+        BoolContent exitImmediately;
+        IntContent maxThreads;
+        EnumContent logLevel, logOutput;
         
-        // Callback on miscOpts.logLevel
+        // Callback on logLevel
         void setLogLevel (Content = null) {
-            Log.root.level (miscOpts.logOutput() == 0 ? Level.None : cast(Level) miscOpts.logLevel(), true);
+            Log.root.level (logOutput() == 0 ? Level.None : cast(Level) logLevel(), true);
         }
         
         void printUsage (char[] progName) {
@@ -460,8 +466,8 @@
         foreach (key,stage_p; stages)
             foreach (name; stage_p.depends)
                 stages[name].rdepends ~= key;
-        IntContent realMaxThreads = miscOpts.maxThreads;
-        miscOpts.maxThreads = new IntContent ("maxThreads", 4);	// force up to 4 threads for unittest
+        int realMaxThreads = maxThreads();
+        maxThreads = 4;	// force up to 4 threads for unittest
         
         logger.level(Logger.Info);              // hide a lot of trace messages
         logger.info ("You should see some messages about InitStages not run/failing:");
@@ -501,8 +507,7 @@
         assert (stages[toStageName("stg3")].state == cast(StageState)7);        // set by the exception
         
         stages = realInit;      // restore the real init stages
-	delete miscOpts.maxThreads;
-        miscOpts.maxThreads = realMaxThreads;
+        maxThreads = realMaxThreads;
         logger.info ("Unittest complete.");
     }
 }
--- a/mde/setup/Screen.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/setup/Screen.d	Sat Feb 07 12:46:03 2009 +0000
@@ -19,7 +19,7 @@
 module mde.setup.Screen;
 
 import mde.setup.exception;
-import mde.lookup.Options;
+import mde.content.AStringContent;
 import imde = mde.imde;
 debug (drawGlyphCache) import mde.font.font;
 
@@ -47,11 +47,6 @@
         void draw ();
     }
     
-    /** All video options. */
-    class VideoOptions : Options {
-        mixin (impl!("bool fullscreen,hardware,resizable,noFrame; int screenW,screenH,windowW,windowH;"));
-    }
-    
 static:
     /** Init function to initialize SDL. */
     StageState init () {      // init func
@@ -77,19 +72,19 @@
         //BEGIN Create window and initialize OpenGL
         // Window creation flags and size
         flags = SDL_OPENGL;
-        if (videoOpts.hardware()) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
+        if (hardware()) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
         else flags |= SDL_SWSURFACE;
         int w, h;
-        if (videoOpts.fullscreen()) {
+        if (fullscreen()) {
             flags |= SDL_FULLSCREEN;
-            w = videoOpts.screenW();
-            h = videoOpts.screenH();
+            w = screenW();
+            h = screenH();
         }
         else {
-            if (videoOpts.resizable()) flags |= SDL_RESIZABLE;
-            if (videoOpts.noFrame()) flags |= SDL_NOFRAME;
-            w = videoOpts.windowW();
-            h = videoOpts.windowH();
+            if (resizable()) flags |= SDL_RESIZABLE;
+            if (noFrame()) flags |= SDL_NOFRAME;
+            w = windowW();
+            h = windowH();
         }
         
         // OpenGL attributes
@@ -156,12 +151,12 @@
     /** Called when a resize event occurs (when the window manager resizes the window). */
     void resizeEvent (int w, int h) {
         // Save new size to config
-        if (videoOpts.fullscreen()) {       // probably resizeEvent only called when not fullscreen
-            videoOpts.screenW = w;
-            videoOpts.screenH = h;
+        if (fullscreen()) {       // probably resizeEvent only called when not fullscreen
+            screenW = w;
+            screenH = h;
         } else {
-            videoOpts.windowW = w;
-            videoOpts.windowH = h;
+            windowW = w;
+            windowH = h;
         }
         
         if (setWindow (w,h))
@@ -264,7 +259,14 @@
     static this() {
         logger = Log.getLogger ("mde.setup.Screen");
         
-        videoOpts = new VideoOptions ("VideoOptions");
+        fullscreen = new BoolContent ("Screen.fullscreen");
+        hardware = new BoolContent ("Screen.hardware");
+        resizable = new BoolContent ("Screen.resizable");
+        noFrame = new BoolContent ("Screen.noFrame");
+        screenW = new IntContent ("Screen.screenW");
+        screenH = new IntContent ("Screen.screenH");
+        windowW = new IntContent ("Screen.windowW");
+        windowH = new IntContent ("Screen.windowH");
     }
     
     // DATA:
@@ -272,5 +274,8 @@
     uint flags = 0;
     IDrawable[] drawables;
     Logger logger;
-    VideoOptions videoOpts;
+    
+    // Option values:
+    BoolContent fullscreen, hardware, resizable, noFrame;
+    IntContent screenW,screenH, windowW,windowH;
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/logger.d	Sat Feb 07 12:46:03 2009 +0000
@@ -0,0 +1,33 @@
+/* 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 the logger in its static this().
+ * 
+ * Any module finding the logger is not set up early enough should import this
+ * module. This module should not import any other mde modules.
+ *****************************************************************************/
+module mde.setup.logger;
+
+import tango.util.log.Log;
+import tango.util.log.AppendConsole;
+
+static this () {
+    // Set up the logger temporarily (until pre-init):
+    Logger root = Log.root;
+    debug root.level(Logger.Trace);
+    else  root.level(Logger.Info);
+    root.add(new AppendConsole);
+}
--- a/mde/workaround2371.d	Sun Feb 01 12:36:21 2009 +0000
+++ b/mde/workaround2371.d	Sat Feb 07 12:46:03 2009 +0000
@@ -18,6 +18,6 @@
  *************************************************************************************************/
 module mde.workaround2371;
 
-public import tango.util.container.HashSet;
+import tango.util.container.HashSet;
 
 alias HashSet!(char[]) myStringHS;