changeset 4:9a990644948c

Many changes: upgraded to tango 0.99.4, reorganised mde/input, large changes to mde/mergetag and mde/init, separated off test/MTTest.d and more. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Sun, 06 Jan 2008 17:38:51 +0000
parents 485c98ecbd91
children 76d0adc92f2e
files conf/input.mtt doc/policies.txt dsss.conf mde/events.d mde/exception.d mde/init.d mde/input/config.d mde/input/core.d.old mde/input/eventstream.d.old mde/input/exception.d mde/input/input.d mde/mde mde/mde.d mde/mergetag/dataset.d mde/mergetag/doc/file-format-text.txt mde/mergetag/exception.d mde/mergetag/old-code/dataset.d.old mde/mergetag/old-code/typeSpec.d.old mde/mergetag/read.d mde/mergetag/write.d mde/scheduler.d mde/test.d mde/text/format.d mde/text/parse.d mde/text/util.d test.mtt test/MTTest.d testw.mtt
diffstat 28 files changed, 1670 insertions(+), 356 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conf/input.mtt	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,4 @@
+{MT01}
+<uint[]|0=[1]>
+{0}
+<uint[][uint]|0=[0x20000000 : [0x100, 3] ]>
--- a/doc/policies.txt	Sat Nov 03 16:06:06 2007 +0000
+++ b/doc/policies.txt	Sun Jan 06 17:38:51 2008 +0000
@@ -1,9 +1,11 @@
 This is a collection of all coding policies for the mde engine as a whole. Policies for individual packages should be put in the individual package directory or elsewhere.
 
-These are principles, not cast-iron rules, but should generally be adhered to.
+These are principles, not cast-iron rules, which I (Diggory Hardy) generally try to adhere to. If any other programmers have better principles to apply over these rules, they may do so for their own coding providing they have a good reason (i.e. not simply wanting to be a little different).
 
 
-Coding conventions: I mostly follow those provided in the D specification. Generally indent with four spaces. Use british or american (or other) spellings as you like, but BE CONSISTANT at least within packages. I generally break lines at 100 chars to prevent overlong lines (particularly documentation); this isn't critical but provides a good guide and keeps text looking reasonable.
+Coding conventions: Mostly stick to those provided in the D specification. Generally indent with four spaces and use tabs to align comments. Aim to break long lines at around 100 chars (particularly with documentation); this isn't essential but provides a good guide and keeps text looking reasonable. With code, however, breaking lines doesn't always produce better-looking code.
+
+Spelling: Use british or american (or other) spellings as you like, but BE CONSISTANT at least for code symbols within packages. For text output to the user there need be no convention until internationalisation support is built-in. As far as internationalisation/localisation is concerned, does it make sense to translate log messages or not? (They are going to be seen by end-users, but will largely be used by developers.)
 
 
 Package design principle: use a separate package for each module of the engine. In most packages where there is only one module (file) imported by other parts of the engine, that module should have the same name as the package and be designed to have a standardised interface to the package so that the package could be replaced with another as a drop-in replacement (written with the same interface). Of course in many cases it may not be possible to swich one package for another quite this easily, but holding to this principle should at least minimise the amount of work necessary when doing so.
@@ -12,12 +14,16 @@
 Engine-wide initialisation and cleanup should be handled or invoked by mde.init.Init's CTOR and DTOR methods where this is viable.
 
 
-Logging should be handled by tango's Logger class. A logger with the name mde.package.module or mde.package.module.X where X is a symbol within the module should be used for each module. Thrown errors should be documented primarily by a log message rather than by returning a message within the exception, to keep logging consistant for both thrown errors and other messages. In general the levels should be used as follows:
+Logging should be handled by tango's Logger class. A logger with the name mde.package.module or mde.package.module.X where X is a symbol within the module should be used for each module. Thrown errors should, where documented, be documented with a log message; an exception message may be used to produce the final log message but must be output via a log message.
+
+In general the levels should be used as follows:
 	Trace	Where required or thought highly useful for debugging
-	Info	Sparingly, for informational purposes (e.g. when parsing a file). Should not be used per-frame.
-	Warn	For small errors which can be overlooked, even if they may cause bigger problems later.
-	Error	For errors which cut-short a (reasonably large) operation (e.g. reading a file), but do not directly cause the program to terminate.
+	Info	Sparingly, for informational purposes (e.g. when parsing a file). Should not generally be used repetitively (within loops, etc.).
+	Warn	For small errors which can be overlooked, even if they MAY cause bigger problems later.
+	Error	For errors which directly:
+		• cut-short a (reasonably large) operation (e.g. reading a file).
+		• cause a significant change in program operation, but do not directly cause the program to terminate.
 	Fatal	For errors directly (i.e. definately and almost immediately) ending the program.
 
 
-Thrown errors should use a class specific to at least the package involved to enable specific catching of errors. Exception classes should be defined within a module exception.d in the package directory. Exception classes should all contain a this() CTOR and possibly a this(char[] msg) CTOR.
+Thrown errors should use an exception class specific to at least the package involved to enable specific catching of errors. Exception classes should be defined within a module exception.d in the package directory. Exception classes should all contain a this() CTOR and possibly a this(char[] msg) CTOR.
--- a/dsss.conf	Sat Nov 03 16:06:06 2007 +0000
+++ b/dsss.conf	Sun Jan 06 17:38:51 2008 +0000
@@ -1,4 +1,8 @@
 [mde/mde.d]
-target=mde-exec
-[mde/mergetag]
-version = 0.1
+buildflags=-L-ldl
+target=bin/mde
+#[mde/mergetag]
+#[mde/input]
+[test/MTTest.d]
+target=bin/MTTest
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/events.d	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,27 @@
+/// Handles all events from SDL_PollEvent.
+module mde.events;
+
+import mde.scheduler;
+
+import mde.input.input;
+
+import derelict.sdl.events;
+
+static bool run = true;
+
+static this () {
+    Scheduler.perFrame (&pollEvents);
+}
+
+void pollEvents (double) {
+    SDL_Event event;
+    while (SDL_PollEvent (&event)) {
+        switch (event.type) {
+            case SDL_QUIT:
+                run = false;
+            break;
+            default:
+                Input.instance() (event);
+        }
+    }
+}
--- a/mde/exception.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/exception.d	Sun Jan 06 17:38:51 2008 +0000
@@ -10,10 +10,24 @@
  * should also be provided.
  */
 class mdeException : Exception {
+    const symbol = "mde";	/// Override in derived classes.
     this (char[] msg) {
-        super("Error: mde: " ~ msg);
+        super(symbol ~ ": " ~ msg);
     }
     this () {
         super("");	// Exception doesn't have a this() CTOR
     }
 }
+
+class initException : mdeException {
+    const override symbol = super.symbol ~ ".init";
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
+class DynamicLibraryLoadException : initException {
+    this (char[] msg) {
+        super("when loading dynamic library: " ~ msg);
+    }
+}
--- a/mde/init.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/init.d	Sun Jan 06 17:38:51 2008 +0000
@@ -5,10 +5,35 @@
  *************************************************************************************************/
 module mde.init;
 
+import mde.exception;
+
+import mde.input.input;
+
 // tango imports
-import tango.util.log.Log : Log;
+import tango.core.Thread;
+import tango.util.log.Log : Log, Logger;
 import tango.util.log.ConsoleAppender : ConsoleAppender;
 
+import derelict.sdl.sdl;
+import derelict.util.exception;
+
+/**
+ * Static CTOR
+ *
+ * This should handle a minimal amount of functionality where useful. For instance, configuring the
+ * logger here and not in Init allows unittests to use the logger.
+ */
+static this()
+{
+    // For now, just log to the console:
+    Logger root = Log.getRootLogger();
+    root.setLevel(root.Level.Trace);
+    root.addAppender(new ConsoleAppender);
+}
+static ~this()
+{
+}
+
 /**
  * Init class
  *
@@ -17,13 +42,102 @@
  */
 scope class Init
 {
+    static Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.init.Init");
+    }
+    
+    /** CTOR − initialisation
+    *
+    * Runs general initialisation code, in a threaded manner where this isn't difficult.
+    *
+    * If this fails by throwing an exception, it must run necessary cleanup first since the DTOR
+    * cannot be run. */
+    /* In a single-threaded function this could be done with:
+    * scope(failure) cleanup;
+    * This won't work with a threaded init function since any threads completing succesfully will
+    * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions
+    * must not run where the initialisation functions have failed.
+    * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct.
+    */
     this()
     {
-        // For now, just log to the console:
-        Log.getRootLogger().addAppender(new ConsoleAppender);
+        // Start all threads
+        ThreadGroup tg = new ThreadGroup;
+        tg.create(&initSDL);
+        
+        // Do some initialisation in the main thread
+        Input.instance.loadConfig (0);
+        
+        // Wait for all threads to complete
+        try {
+            tg.joinAll (true);		// rethrows any exceptions
+        }
+        catch (initException e) {	// Any problems?
+            // All cleanup-on-failure must be done here.
+            runCleanupFcts();
+            throw e;	// Rethrow. Warning: if multiple threads throw exceptions, only one gets returned.
+        }
+        
+    }
+    
+    /* Initialisation functions.
+    *
+    * These should each handle a separate area of initialisation such that these functions could
+    * be run simultaneously in separate threads. */
+    void initSDL () {
+        try {
+            // SDL Joystick, used by mde.input
+            DerelictSDL.load();
+        } catch (DerelictException de) {
+            throw new DynamicLibraryLoadException (de.msg);
+        }
+        logger.info ("Derelict: loaded SDL");
+        
+        SDL_Init (SDL_INIT_TIMER | SDL_INIT_JOYSTICK);
+        addCleanupFct (&cleanupSDL);
+        logger.info ("SDL initialised");
     }
     
     ~this()
     {
+        runCleanupFcts();
+    }
+    
+    /* Cleanup Functions.
+    *
+    * These may exist simply as something to add to the cleanup list... */
+    static void cleanupSDL () {
+        SDL_Quit();
+    }
+    
+    private static {
+        void function ()[] cleanup;		// all functions to be run for cleanup
+        // Adding cleanup functions must be synchronized; use:
+        void addCleanupFct (void function () fct) {
+            synchronized cleanup ~= fct;
+        }
+        // Clean-up fcts are run in reverse order to how they're added:
+        void runCleanupFcts () {
+            foreach_reverse (fct; cleanup) fct();
+        }
     }
 }
+
+unittest {
+    /* Fake init and cleanup. This happens before the CTOR runs so the extra Init.runCleanupFcts()
+    * call isn't going to mess things up. The extra function called by runCleanupFcts won't cause
+    * any harm either. */
+    static bool initialised = false;
+        
+    static void init () {
+        initialised = true;
+        Init.addCleanupFct (&cleanup);
+    }
+    static void cleanup () {	initialised = false;	}
+        
+    init();
+    assert (initialised);
+    Init.runCleanupFcts();
+    assert (!initialised);
+}
--- a/mde/input/config.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/input/config.d	Sun Jan 06 17:38:51 2008 +0000
@@ -1,21 +1,34 @@
 /// This module contains a class for holding configs and handles saving, loading and editing.
 module mde.input.config;
 
-// package imports
-import mde.input.core;
+debug import mde.text.format;
+
+import mde.input.exception;
+
+import mde.mergetag.read;
+import mde.text.parse;
 
-/** Struct to hold the configuration for the input system. Thus loading and switching between
+import tango.util.log.Log : Log, Logger;
+import tango.util.collection.TreeBag : TreeBag;
+
+Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.input.config");
+}
+
+/** Class to hold the configuration for the input system. Thus loading and switching between
  *  multiple configurations should be easy.
  *
- *  Note: documentation should be generated for the codes (enum : uint ...), but it's not.
+ * Class extends DataSection so that it can be loaded by mergetag easily.
  */
-struct Config
+class Config : DataSection
 {
+    alias uint[] outQueue;		// This is the type for the out queue config data.
     /** Button event type bit-codes
      *
      *  These bitcodes are OR'd to the identifier code for the input device, to indicate which type
      *  of input they are for. E.g. when a key event is recieved with code x, look up
-     *  B.SDLKEY | x in b. Keyboard events are SDL-specific since the codes may differ for other
+     *  $(_B _B.SDLKEY) | x in button. Keyboard events are SDL-specific since the codes may differ for other
      *  libraries.
      *
      *  For joystick hat events, a motion should be converted into up and down events on separate
@@ -51,9 +64,9 @@
         JOYBALL		= 0x4000_0000u,		/// 0x4000_0000u
     }
     
-    /** Output queues --- the core of the input configuration.
+    /** Output queues: the core of the input configuration.
     *
-    *  b, axis and mouse each have their own index specifications. This is split into two parts:
+    *  button, axis and mouse each have their own index specifications. This is split into two parts:
     *  the first byte specifies the type of input (given by the above enums), and the last three
     *  bytes define where the input comes from.
     *
@@ -65,7 +78,77 @@
     *  The code for mouse motion is currently only M.WMMOUSE. If/when multiple mice are supported
     *  new codes will be defined.
     */
-    outQueue[uint] b;
-    outQueue[uint] axis;    /// ditto
-    outQueue[uint] mouse;    /// ditto
+    outQueue[uint] button;
+    outQueue[uint] axis;	/// ditto
+    outQueue[uint] mouse;	/// ditto
+    
+    char[] name;		/// Name for user to save this under.
+    uint[] inheritants;		/// Other profiles to inherit.
+    
+    // FIXME: using uint IDs really isn't nice...
+    static Config[uint] configs;	/// All configs loaded by load().
+    private static TreeBag!(char[]) loadedFiles;	// all filenames load tried to read
+    
+//BEGIN File loading/saving code
+    static this () {
+        loadedFiles = new TreeBag!(char[]);
+    }
+    
+    // Load all configs from a file.
+    static void load (char[] filename) {
+        if (loadedFiles.contains (filename)) return;	// forget it; already done that
+        loadedFiles.add (filename);
+        Reader file;
+        try {
+            file = new Reader(filename, null, true);	// open and read header
+            // TODO: also load user-config file
+            
+            file.dataSecCreator =
+            	function DataSection (ID) {	return new Config;	};
+            
+            enum : ID { CONFIGS }
+            ID[] configs;	// active config sections (may not exist)
+            uint[]* configs_p = CONFIGS in file.dataset.header._uintA;
+            
+            if (configs_p)	file.read(cast(ID[]) *configs_p);	// restrict to this set IF a restriction was given
+            else		file.read();		// otherwise read all
+        }
+        catch (MTException) {
+            logger.error ("Unable to load configs from: " ~ filename);
+            throw new ConfigLoadException;
+        }
+        // FIXME: don't override configs if not empty
+        configs = cast (Config[uint]) file.dataset.sec;
+        // NOTE: this is in some ways dangerous (assuming all DataSections are Configs), but they should be.
+        debug {
+            char tmp[128] = void;
+            logger.info (logger.format (tmp, "Loaded {} config sections.", configs.length));
+            foreach (id, cfg; configs) {
+                logger.trace ("Section "~format!(uint)(id)~": " ~ format!(uint[][uint])(cfg.button));
+            }
+        }
+    }
+    
+    private enum QUEUE : ID { BUTTON, AXIS, MOUSE }
+    private this() {}	// Private since this class should only be created from here.
+    
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "uint[][uint]") {
+            if (id == QUEUE.BUTTON) {
+                button = cast(outQueue[uint]) parse!(uint[][uint]) (dt);
+                debug logger.trace ("Added button config: " ~ format!(uint[][uint])(button));
+            }
+            else if (id == QUEUE.AXIS) axis = cast(outQueue[uint]) parse!(uint[][uint]) (dt);
+            else if (id == QUEUE.MOUSE) mouse = cast(outQueue[uint]) parse!(uint[][uint]) (dt);
+            else {
+                char[80] tmp;
+                logger.info (logger.format(tmp, "Unexpected tag encountered with ID {}", id));
+            }
+        } // FIXME: add support for name and inheritants.
+        else throw new MTUnknownTypeException ("Input Config: only uint[][uint] type supported");
+    }
+    void writeAll (ItemDelg) {
+        // FIXME
+    }
+//END File loading/saving code
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/input/core.d.old	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,36 @@
+/// This module contains the core (i.e. common part) of the input system.
+module mde.input.core;
+
+import mde.input.config;
+
+typedef uint inputID;
+struct RelPair {	// for mouse/joystick ball motion
+	real x, y;
+	static RelPair opCall (real a, real b) {
+		RelPair ret;
+		ret.x = a;	ret.y = b;
+		return ret;
+	}
+}
+
+/* Note: We really want an array, not a stack. We cannot edit these lists, so we can either
+* copy the stack or just iterate through it as an array.
+*/
+typedef uint[] outQueue;	/// This is the type for the out queue config data.
+struct readOutQueue {		/// A convenient structure for reading an outQueue item by item.
+    private outQueue _q;	// the queue, stored by reference to the original
+    private uint p = 0;		// current read position (start at beginning)
+    
+    static readOutQueue opCall (outQueue q) {	/// Static constructor
+        readOutQueue ret;
+        ret._q = q;
+        return ret;
+    }
+    uint next () {		/// Get the next element. Throws an exception if there isn't another.
+        if (p >= _q.length)
+            throw new InputException ("Input: Invalid configuration: incomplete config stack");
+        uint ret = _q[p];
+        ++p;
+        return ret;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/input/eventstream.d.old	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,90 @@
+/** This module contains functions called on an event, which may modify the event (adjuster
+ * functions), and finally output to one (or more) of the state tables (the event stream).
+ *
+ * Adjuster and other event functions should have a format to fit the ES_X_Func types, for X is B
+ * (button event), A (axis event) or M (mouse relative motion event or joystick ball event).
+ * Adjusters should call one of the xEventOut() functions with their output and the remainder of
+ * the readOutQueue.
+ *
+ * To control which adjusters get called and pass parameters, a stack is used.
+ */
+module mde.input.eventstream;
+
+// package imports
+import mde.input.core;
+
+/// Module constructor fills es_*_fcts tables and rehashes them.
+static this () {
+    es_b_fcts = [ ES_B_OUT : &es_b_out ];
+}
+
+/// These aliases are for pointers to the event functions.
+alias void function (bool, readOutQueue) ES_B_Func;
+alias void function (short, readOutQueue) ES_A_Func;			/// ditto
+alias void function (short, short, readOutQueue) ES_M_Func;	/// ditto
+
+/// These are the tables for looking up which event function to call.
+static ES_B_Func[uint] es_b_fcts;
+static ES_A_Func[uint] es_a_fcts;	/// ditto
+static ES_M_Func[uint] es_m_fcts;	/// ditto
+
+/// These are the codes allowing the config to specify event functions:
+enum : uint {
+    ES_B_OUT	= 0x0000_0100u,
+    ES_A_OUT	= 0x0000_0200u,
+    ES_M_OUT	= 0x0000_0300u,
+}
+
+/** These functions pass an event to the appropriate event function (adjuster or output func). */
+void bEventOut (bool b, readOutQueue s)
+{
+	ES_B_Func* func = (s.next() in es_b_fcts);
+	if (func != null) (*func)(b,s);
+	else throw new InputException ("Input: Invalid configuration: bad event function code");
+}
+void aEventOut (short x, readOutQueue s)	/// ditto
+{
+	ES_A_Func* func = (s.next() in es_a_fcts);
+	if (func != null) (*func)(x,s);
+	else throw new InputException ("Input: Invalid configuration: bad event function code");
+}
+void mEventOut (short x, short y, readOutQueue s)	/// ditto
+{
+	ES_M_Func* func = (s.next() in es_m_fcts);
+	if (func != null) (*func)(x,y,s);
+	else throw new InputException ("Input: Invalid configuration: bad event function code");
+}
+
+/// Simple output function
+void es_b_out (bool b, readOutQueue s) {
+	current.button[cast(inputID) s.next()] = b;
+}
+/// Adjuster to check modifier keys
+void es_b_modifier (bool b, readOutQueue s);
+
+/** Simple output function
+
+Adds 1-2 items on the stack.
+*/
+void es_a_out (short x, readOutQueue s) {
+	real y = x;
+	uint conf = s.next();
+        enum : uint {
+            HALF_RANGE	= 0x8000_0000u,
+            SENSITIVITY	= 0x0080_0000u,
+        }
+        // Convert ranges into standard intervals (with or without reverse values)
+	if (conf & HALF_RANGE) y = (y + 32767.0) * 1.5259254737998596e-05;	// range  0.0 - 1.0
+	else y *= 3.0518509475997192e-05;					// range -1.0 - 1.0
+	real a;
+	if (conf & SENSITIVITY) a = s.next();
+        /+ When a global sensitivity is available (possibly only use if it's enabled)...
+        else a = axis.sensitivity;
+	y = sign(y) * pow(abs(y), a);		// sensitivity adjustment by a +/
+	current.axis[cast(inputID) s.next()] = y;
+}
+
+/// Simple output function
+void es_m_out (short x, short y, readOutQueue s) {
+	current.axis_rel[cast(inputID) s.next()] = RelPair(x,y);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/input/exception.d	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,16 @@
+module mde.input.exception;
+
+import mde.exception;
+
+/// Base Input exception class.
+class InputException : mdeException {
+    const override symbol = super.symbol ~ ".input.input.Input";
+    this (char[] msg) {
+        super(msg);
+    }
+    this () {}
+}
+
+class ConfigLoadException : InputException {
+    this () {}
+}
--- a/mde/input/input.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/input/input.d	Sun Jan 06 17:38:51 2008 +0000
@@ -1,61 +1,296 @@
-/** This module contains the interface to the input system; it should be the only module of the
- *	input package imported from outside this package.
+/**
+ * This module contains the interface to the input system; it should be the only module of the
+ * input package imported from outside this package.
  */
 module mde.input.input;
 
 // package imports
-import mde.input.core;
 import mde.input.config;
-import mde.input.eventstream;
+import mde.input.exception;
 
 // sdl imports
 import derelict.sdl.events;
 
-/// Get key status at this ID
-bool button (uint id) {
-	return b_tbl[cast(index_t) id];
-}
-/// Get axis status at this ID (range -1.0 .. 1.0)
-real axis (uint id) {
-	return axis_tbl[cast(index_t) id];
-}
-/**	Get mouse pointer position in screen coordinates.
- *	Window managers only support one mouse, so there will only be one screen coordinate.
- *	Unlike everything else, this is not configurable.
- */
-void mouseScreenPos (uint x, uint y) {
-	x = mouse_x;	y = mouse_y;
+class Input
+{
+    /// Typedef for all indexes (type is uint).
+    typedef uint				inputID;
+    alias void delegate(inputID, bool)		ButtonCallback;
+    alias void delegate(inputID, real)		AxisCallback;
+    alias void delegate(inputID, real,real)	MouseCallback;
+        
+    /// Stores a static instance of Input for most usage.
+    static Input instance () {
+        static Input instance;
+    
+        if (instance is null) instance = new Input;
+        return instance;
+    }
+
+    /** Get key status at this ID.
+    *
+    * Returns: value (true = down, false = up) or false if no value at this ID. */
+    bool getButton (inputID id) {
+        bool* retp = cast(inputID) id in button;
+        if (retp) return *retp;
+        else return false;
+    }
+    /** Get axis status at this ID.
+    *
+    * Returns: value (range -1.0 .. 1.0) or 0.0 if no value at this ID. */
+    real getAxis (inputID id) {
+        real* retp = cast(inputID) id in axis;
+        if (retp) return *retp;
+        else return 0.0;
+    }
+    /** Get mouse pointer position in screen coordinates.
+    *
+    * Window managers only support one mouse, so there will only be one screen coordinate.
+    * Unlike everything else, this is not configurable.
+    */
+    void mouseScreenPos (out uint x, out uint y) {
+        x = mouse_x;	y = mouse_y;
+    }
+    /** Get relative mouse position (also for joystick balls).
+    *
+    * Future: Converts to a real via sensitivity settings (defaults may be set and overriden per item).
+    *
+    * To avoid confusion over the ID here, the idea is for the input-layer upward to support
+    * multiple mice, in case future platforms do.
+    * Also joystick balls (supported by SDL) can be used in the same way as a mouse for relative
+    * positions.
+    */
+    void mouseRelativePos (inputID id, out real x = 0.0, out real y = 0.0) {
+        RelPair* rp = cast(inputID) id in axis_rel;
+        if (rp) {
+            x = rp.x;	y = rp.y;
+        }
+    }
+    // /// Is this modifier on?
+    //bool modifierStatus (inputID id);
+
+    /** Adds a callback delegate for key events (both DOWN and UP) with this ID.
+    *
+    * Delegate receives event status.
+    */
+    void addButtonCallback (inputID id, ButtonCallback dg) {
+        buttonCallbacks[id] = dg;
+    }
+
+    /** Adds a callback delegate for axis events with this ID.
+    *
+    * Delegate receives event status.
+    */
+    void addAxisCallback (inputID id, AxisCallback dg) {
+        axisCallbacks[id] = dg;
+    }
+
+    /** Adds a callback delegate for mouse motion/joystick ball events with this ID.
+    *
+    * Delegate receives event status. (A separate callback for mouse pointer position changes is not
+    * necessary since this will be triggered by the same event - use mouseScreenPos from within the
+    * function to get new screen coordinates.)
+    */
+    void addMouseCallback (inputID id, MouseCallback dg) {
+        mouseCallbacks[id] = dg;
+    }
+
+    /** Feed an SDL_Event struct (only uses if it's a key, mouse or joystick event).
+    *
+    * Other types of event functions may be added. Returns true if the event was used, false if not.
+    */
+    bool opCall (ref SDL_Event event) {
+        switch (event.type) {
+            case SDL_JOYBUTTONDOWN:
+            case SDL_JOYBUTTONUP:
+                outQueue* p = (Config.B.JOYBUTTON | (event.jbutton.which << 12) | event.jbutton.button) in config.button;
+                if (p) bEventOut (event.jbutton.state == 0x1, readOutQueue(*p));
+            break;
+        
+            /+
+            case SDL_KEYDOWN:
+            case SDL_KEYUP:
+            outQueue* p = (Config.B.SDLKEY | event.key.keysym.sym) in config.button;
+            if (p) eventstream.bEventOut (event.key.state == SDL_PRESSED, readOutQueue(*p));
+            break;
+            case SDL_MOUSEMOTION:
+            mouse_x = event.motion.x;
+            mouse_y = event.motion.y;
+            outQueue* p = (Config.M.WMMOUSE) in config.mouse;
+            if (p) eventstream.mEventOut (event.motion.xrel, event.motion.yrel, readOutQueue(*p));
+            +/
+            default:
+            return false;
+        }
+        return true;
+    }
+    
+    /** Resets relative movement of mice / joystick balls to zero.
+    *
+    * Should probably be called once-per-frame if these are used.
+    */
+    void frameReset () {
+        foreach (rp; axis_rel) {
+            rp.x = rp.y = 0.0;
+        }
+    }
+    
+    /** Loads all configs, activating the requested id.
+    *
+    * Returns: true if the requested config id wasn't found.
+    */
+    bool loadConfig (uint id) {
+        Config.load("conf/input.mtt");	// FIXME: filename
+        Config* c_p = id in Config.configs;
+        if (c_p) {
+            config = *c_p;
+            return false;
+            logger.info ("Succesfully loaded config.");
+        }
+        return true;
+    }
+    
+private:
+    // Static constructor for event stream (fills es_*_fcts tables).
+    static this () {
+        es_b_fcts = [ ES_B_OUT : &es_b_out ];
+    }
+    
+    struct RelPair {	// for mouse/joystick ball motion
+        real x, y;
+        static RelPair opCall (real a, real b) {
+            RelPair ret;
+            ret.x = a;	ret.y = b;
+            return ret;
+        }
+    }
+    
+    Config config;			// Configuration
+    
+    bool[inputID] button;		// Table of button states
+    real[inputID] axis;			// Table of axes states
+    ushort mouse_x, mouse_y;		// Current screen coords of the mouse
+    // FIXME: might need a bit of work... at any rate defining a default ID.
+    RelPair[inputID] axis_rel;		// Table of relative mouse / joystick ball motions
+    
+    // FIXME: these need to be more like multimaps, supporting multiple dgs (also some means of removal?)
+    ButtonCallback[inputID] buttonCallbacks;
+    AxisCallback[inputID] axisCallbacks;
+    MouseCallback[inputID] mouseCallbacks;
+        
+    //BEGIN Event stream functionality
+    /* This section contains functions called on an event, which may modify the event (adjuster
+    * functions), and finally output to one (or more) of the state tables (the event stream).
+    *
+    * Adjuster and other event functions should have a format to fit the ES_X_Func types, for X is B
+    * (button event), A (axis event) or M (mouse relative motion event or joystick ball event).
+    * Adjusters should call one of the xEventOut() functions with their output and the remainder of
+    * the readOutQueue.
+    *
+    * To control which adjusters get called and pass parameters, a stack of sorts is used: outQueue.
+    */
+    //BEGIN ES Definitions
+    /* Note: We really want an array, not a stack. We cannot edit the lists, so we can either
+    * copy to a stack or just iterate through it as an array.
+    */
+    alias Config.outQueue outQueue;
+    struct readOutQueue {		// A convenient structure for reading an outQueue item by item.
+        private Config.outQueue _q;		// the queue, stored by reference to the original
+        private uint p = 0;		// current read position (start at beginning)
+    
+        static readOutQueue opCall (Config.outQueue q) {	// Static constructor
+            readOutQueue ret;
+            ret._q = q;
+            return ret;
+        }
+        uint next () {			// Get the next element. Throws an exception if there isn't another.
+            if (p >= _q.length)
+                throw new InputException ("Input: Invalid configuration: incomplete config stack");
+            uint ret = _q[p];
+            ++p;
+            return ret;
+        }
+    }
+    
+    // These aliases are for pointers to the event functions.
+    alias void function (bool, readOutQueue) ES_B_Func;
+    alias void function (short, readOutQueue) ES_A_Func;
+    alias void function (short, short, readOutQueue) ES_M_Func;
+    
+    // These are the codes allowing the config to specify event functions:
+    enum : uint {
+        ES_B_OUT	= 0x0000_0100u,
+        ES_A_OUT	= 0x0000_0200u,
+        ES_M_OUT	= 0x0000_0300u,
+    }
+    //END ES Definitions
+    
+    // ES Data:
+    // These are the tables for looking up which event function to call.
+    static ES_B_Func[uint] es_b_fcts;
+    static ES_A_Func[uint] es_a_fcts;
+    static ES_M_Func[uint] es_m_fcts;
+    
+    //BEGIN ES Functions
+    // These 3 functions pass an event to the appropriate event function (adjuster or output func).
+    // They are used to start and continue an event stream.
+    void bEventOut (bool b, readOutQueue s)
+    {
+        ES_B_Func* func = (s.next() in es_b_fcts);
+        if (func != null) (*func)(b,s);
+        else throw new InputException ("Input: Invalid configuration: bad event function code");
+    }
+    void aEventOut (short x, readOutQueue s)
+    {
+        ES_A_Func* func = (s.next() in es_a_fcts);
+        if (func != null) (*func)(x,s);
+        else throw new InputException ("Input: Invalid configuration: bad event function code");
+    }
+    void mEventOut (short x, short y, readOutQueue s)
+    {
+        ES_M_Func* func = (s.next() in es_m_fcts);
+        if (func != null) (*func)(x,y,s);
+        else throw new InputException ("Input: Invalid configuration: bad event function code");
+    }
+    
+    // The remaining functions are the stream functions, for adjusting and outputting an event.
+    
+    // Simple output function
+    void es_b_out (bool b, readOutQueue s) {
+        inputID id = cast(inputID) s.next();
+        button[id] = b;
+        ButtonCallback* cb_p = id in buttonCallbacks;
+        if (cb_p) (*cb_p) (id, b);
+    }
+    // Adjuster to check modifier keys
+    void es_b_modifier (bool b, readOutQueue s);
+
+    /* Simple output function
+
+    Adds 1-2 items on the stack.
+    */
+    void es_a_out (short x, readOutQueue s) {
+        real y = x;
+        uint conf = s.next();
+        enum : uint {
+            HALF_RANGE	= 0x8000_0000u,
+            SENSITIVITY	= 0x0080_0000u,
+        }
+        // Convert ranges into standard intervals (with or without reverse values)
+        if (conf & HALF_RANGE) y = (y + 32767.0) * 1.5259254737998596e-05;	// range  0.0 - 1.0
+        else y *= 3.0518509475997192e-05;					// range -1.0 - 1.0
+        real a;
+        if (conf & SENSITIVITY) a = s.next();
+        /+ When a global sensitivity is available (possibly only use if it's enabled)...
+        else a = axis.sensitivity;
+        y = sign(y) * pow(abs(y), a);		// sensitivity adjustment by a +/
+        axis[cast(inputID) s.next()] = y;
+    }
+
+    // Simple output function
+    void es_m_out (short x, short y, readOutQueue s) {
+        axis_rel[cast(inputID) s.next()] = RelPair(x,y);
+    }
+    //END ES Functions
+    //END Event stream functionality
 }
-/** Get relative mouse position (also for joystick balls).
- *
- *	Converts to a real via sensitivity settings (defaults may be set and overriden per item).
- *
- *	To avoid confusion over the ID here, the idea is for the input-layer upward to support
- *	multiple mice, even though it's unlikely for the input system itself to support them. Also
- *	joystick balls (supported by SDL) can be used in the same way as a mouse for relative
- *	positions. Thus this must be configured only on one-mouse systems.
- */
-void mouseRelativePos (out real x, out real y, uint id) {
-	RelPair rp = axis_rel_tbl[cast(index_t) id];
-	x = rp.x;	y = rp.y;
-}
-/// As it says. Optional.
-bool modifierStatus (uint id);
-
-/// Adds a callback delegate for key with this ID for both DOWN and UP events.
-/// Passes current status.
-void addKeyCallback (void delegate(bool) dg);
-
-/// Similar function for axis events.
-void addAxisCallback (void delegate(real) dg);
-
-/** Similar function for mouse/joystick ball motion.
- *	Last parameter is true if it's for the window-manager mouse (use mouseScreenPos to get
- *	mouse screen position).
- */
-void addMouseCallback (void delegate(real,real,bool) dg);
-
-/** Feed an SDL_Event struct (only uses if it's a key, mouse or joystick event).
- *	Other types of event functions may be added.
- */
-void SDLEvent (SDL_Event event);
Binary file mde/mde has changed
--- a/mde/mde.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/mde.d	Sun Jan 06 17:38:51 2008 +0000
@@ -4,33 +4,56 @@
  */
 module mde.mde;
 
-// External library imports
-import tango.io.Stdout;
-
 // Package imports
 import mde.init;
-import test = mde.test;
+import mde.events;
+import mde.scheduler;
+//import test = mde.test;
 
 import mde.input.input;
 
 import mde.mergetag.read;
 
+// External library imports
+import tango.core.Thread;
+import tango.io.Stdout;
+import tango.time.Clock;
+import tango.util.log.Log : Log, Logger;
+
+import derelict.sdl.sdl;
+
 int main()
 {
-    scope init = new Init();	// initialisation
+    Logger logger = Log.getLogger ("mde.mde");
     
-    Reader MTread;
+    scope Init init;
     try {
-        MTread = new Reader ("test.mtt", null, true);
-        static DataSection dataPrinter (ID id) {	return new test.DataPrinter (id);	}
-        MTread.dataSecCreator = &dataPrinter;
-        MTread.read();
-    } catch (Exception e) {
-        Stdout (e.msg).newline;
+        init = new Init();	// initialisation
+    } catch (initException e) {
+        logger.fatal ("Initialisation failed; error was:");
+        logger.fatal (e.msg);
+        Stdout (e.msg);
+        return 1;
     }
-    //Stdout ("Data read from file:").newline;
-    //test.printDataSet (MTread.dataset);
-    return 0;
+    
+    Input input = Input.instance();
+    input.addButtonCallback (cast(Input.inputID) 3u, delegate void(Input.inputID i, bool b) {
+        Stdout ("Event: ")(i)(" changed to: ")(b).newline;
+    } );
+    bool oldb = false;
     
-    // cleanup handled by init's DTOR
+    /+while (run)+/
+    for (ulong t = 0; t < 100; ++t) {
+        Scheduler.run (Clock.now());
+        
+        bool b = input.getButton (cast(Input.inputID) 3u);
+        if (b != oldb) {
+            oldb = b;
+            Stdout ("Button 3 changed to: ")(b).newline;
+        }
+        
+        Thread.sleep (0.010);	// 10 ms
+    }
+    
+    return 0;		// cleanup handled by init's DTOR
 }
--- a/mde/mergetag/dataset.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/mergetag/dataset.d	Sun Jan 06 17:38:51 2008 +0000
@@ -6,7 +6,8 @@
 
 // other mde imports
 import mde.text.util;
-import mde.text.parse;
+import mde.text.parse : parse;
+import mde.text.format : format;
 import mde.text.exception : TextParseException;
 
 // tango imports
@@ -62,7 +63,7 @@
  */
 class DataSet
 {
-    DataSection header;			/// Header
+    DefaultData header;			/// Header
     DataSection[ID] sec;		/// Dynamic array of sections
     
     /// Return a reference to the indexed item
@@ -100,12 +101,16 @@
  */
 interface DataSection
 {
-    /** Handles parsing of data items.
+    typedef void delegate (char[],ID,char[]) ItemDelg;
+    /** Handles parsing of data items for all recognised types.
      *
-     * Should throw an MTUnknownTypeException for unsupported types, after logging to logger.
-     */
-    void addTag (TypeInfo,ID,char[]);
-    //void writeAll (Print!(char));	/// TBD
+     * Should throw an MTUnknownTypeException for unsupported types with a short explanation of the
+     * form: "Package ClassName: supported types" or similar.
+     *
+     * Only MTUnknownTypeException and MTaddTagParseException exceptions are caught by a reader
+     * calling addTag. */
+    void addTag (char[],ID,char[]);
+    void writeAll (ItemDelg);	/// TBD
 }
 
 /**
@@ -113,8 +118,11 @@
  *
  * Supports all the basic types currently supported and array versions of
  * each (except no arrays of binary or string types; these are already arrays).
+ * Doesn't support custom types, but inheriting classes may add support.
  */
-/* Due to a failure to use generic programming techniques for most of this (maybe because it's not
+/* Note: I wrote this comment when the code looked rather worse. It's still partially applicable though.
+ *
+ * Due to a failure to use generic programming techniques for most of this (maybe because it's not
  * possible or maybe just because I don't know how to use templates properly) a lot of this code is
  * really horrible and has to refer to EVERY data member.
  * Be really careful if you add any items to this class.
@@ -187,51 +195,66 @@
     alias	_charA	_string;	/// ditto
     //END DATA
     
-    void addTag (TypeInfo ti, ID id, char[] dt) {	/// for adding tags
+    void addTag (char[] tp, ID id, char[] dt) {	/// Supports all standard types.
         try {
-            // crazy way of only writing one parameter on each line:
-            mixin ( `if (ti == typeid(bool)) addTag_add!(bool) (id, dt);`
-            	~ addTag_elifIsType_add!(byte)
-            	~ addTag_elifIsType_add!(short)
-            	~ addTag_elifIsType_add!(int)
-            	~ addTag_elifIsType_add!(long)
-            	~ addTag_elifIsType_add!(ubyte)
-            	~ addTag_elifIsType_add!(ushort)
-            	~ addTag_elifIsType_add!(uint)
-            	~ addTag_elifIsType_add!(ulong)
-            	~ addTag_elifIsType_add!(char)
-            	~ addTag_elifIsType_add!(float)
-            	~ addTag_elifIsType_add!(double)
-            	~ addTag_elifIsType_add!(real)
-            	~ addTag_elifIsType_add!(bool[])
-            	~ addTag_elifIsType_add!(byte[])
-            	~ addTag_elifIsType_add!(short[])
-            	~ addTag_elifIsType_add!(int[])
-            	~ addTag_elifIsType_add!(long[])
-            	~ addTag_elifIsType_add!(ubyte[])
-            	~ addTag_elifIsType_add!(ushort[])
-            	~ addTag_elifIsType_add!(uint[])
-            	~ addTag_elifIsType_add!(ulong[])
-            	~ addTag_elifIsType_add!(char[])
-            	~ addTag_elifIsType_add!(float[])
-            	~ addTag_elifIsType_add!(double[])
-                ~ addTag_elifIsType_add!(real[])
-            );
+            if (tp.length == 0) throw new MTUnknownTypeException;
+            // split list up a bit for performance:
+            if (tp[0] < 'l') {
+                if (tp[0] < 'd') {
+                    mixin ( `if (tp == "binary") addTag_add!(ubyte[]) (id, dt);`
+                    ~ addTag_elifIsType_add!(bool)
+                    ~ addTag_elifIsType_add!(bool[])
+                    ~ addTag_elifIsType_add!(byte)
+                    ~ addTag_elifIsType_add!(byte[])
+                    ~ addTag_elifIsType_add!(char)
+                    ~ addTag_elifIsType_add!(char[])
+                    ~ `else throw new MTUnknownTypeException;` );
+                } else {
+                    mixin ( `if (tp == "double") addTag_add!(double) (id, dt);`
+                    ~ addTag_elifIsType_add!(double[])
+                    ~ addTag_elifIsType_add!(float)
+                    ~ addTag_elifIsType_add!(float[])
+                    ~ addTag_elifIsType_add!(int)
+                    ~ addTag_elifIsType_add!(int[])
+                    ~ `else throw new MTUnknownTypeException;` );
+                }
+            } else {
+                if (tp[0] < 'u') {
+                    mixin ( `if (tp == "long") addTag_add!(long) (id, dt);`
+                    ~ addTag_elifIsType_add!(long[])
+                    ~ addTag_elifIsType_add!(real)
+                    ~ addTag_elifIsType_add!(real[])
+                    ~ addTag_elifIsType_add!(short)
+                    ~ addTag_elifIsType_add!(short[])
+                    ~ `else if (tp == "string") addTag_add!(char[]) (id, dt);`
+                    ~ `else throw new MTUnknownTypeException;` );
+                } else {
+                    mixin ( `if (tp == "ubyte") addTag_add!(ubyte) (id, dt);`
+                    ~ addTag_elifIsType_add!(ubyte[])
+                    ~ addTag_elifIsType_add!(ushort)
+                    ~ addTag_elifIsType_add!(ushort[])
+                    ~ addTag_elifIsType_add!(uint)
+                    ~ addTag_elifIsType_add!(uint[])
+                    ~ addTag_elifIsType_add!(ulong)
+                    ~ addTag_elifIsType_add!(ulong[])
+                    ~ `else throw new MTUnknownTypeException;` );
+                }
+            }
         } catch (TextParseException) {
-            // Just ignore it. A warning's already been logged.
+            throw new MTaddTagParseException;
         }
     }
     private template addTag_elifIsType_add(T) {
-        const addTag_elifIsType_add = `else if (ti == typeid(`~T.stringof~`)) addTag_add!(`~T.stringof~`) (id, dt);` ;
+        const addTag_elifIsType_add =
+            `else if (tp == "`~T.stringof~`")`
+            	`addTag_add!(`~T.stringof~`) (id, dt);` ;
     }
     private void addTag_add(T) (ID id, char[] dt) {
         Arg!(T).Arg[id] = parse!(T) (dt);
     }
     
-    void writeTags (out TextTag[] ret) {
-        //ret.length = Arg!().length + ...;
-        
-        
+    void writeAll (ItemDelg itemdlg) {
+        foreach (id, dt; _charA) itemdlg ("char[]", id, format!(char[])(dt));
     }
     
     /* These make no attempt to check Arg is valid.
@@ -248,6 +271,12 @@
     }
 }
 
+/+class DynamicData : DataSection
+{
+    void*[TypeInfo] data;
+    
+}+/
+
 /+
 class TemplateData : DataSection
 {
@@ -262,3 +291,11 @@
     }
 }
 +/
+
+unittest {	// Only covers DataSet really.
+    DataSet ds = new DataSet;
+    ds.sec[1] = new DefaultData;
+    assert (ds.getSections!(DefaultData)().length == 1);
+    ds.sec[1].addTag ("int",0," -543 ");
+    assert (ds.getSections!(DefaultData)()[1]._int[0] == -543);
+}
--- a/mde/mergetag/doc/file-format-text.txt	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/mergetag/doc/file-format-text.txt	Sun Jan 06 17:38:51 2008 +0000
@@ -51,32 +51,33 @@
 Format:
 	tp		a basic type
 	tp[]		a dynamic list of sub-type tp
+	t1[t2]		an associative array with key-type t2
 Possible future additions:
 	tp()		a dynamic merging list of sub-type tp (only valid as the primary type, ie
         		<subtype()|...>, not a sub-type of a tuple or another dynamic list)
 	{t1,t2,...,tn}	a tuple with sub-types t1, t2, ..., tn
 
-Basic types (only items with a + are currently supported):
+Basic types (only items with a + are currently supported, items with * are in DefaultData):
 	name
 	
 	void	--- less useful type
-+	bool	--- integer types
-+	byte
-+	ubyte
-+	short
-+	ushort
-+	int
-+	uint
-+	long
-+	ulong
++*	bool	--- integer types
++*	byte
++*	ubyte
++*	short
++*	ushort
++*	int
++*	uint
++*	long
++*	ulong
 	cent
 	ucent
 	
-+	binary	--- alias for ubyte[]
++*	binary	--- alias for ubyte[]
 	
-+	float	--- floating point types
-+	double
-+	real
++*	float	--- floating point types
++*	double
++*	real
 	ifloat
 	idouble
 	ireal
@@ -84,10 +85,10 @@
 	cdouble
 	creal
 	
-+	char	--- single character types (actually these CANNOT support UTF8 symbols with length > 1)
++*	char	--- single character types (actually these CANNOT support UTF8 symbols with length > 1)
 	wchar
 	dchar
-+	string	--- alias for char[] --- (DOES support UTF8)
++*	string	--- alias for char[] --- (DOES support UTF8)
 	wstring	--- alias for wchar[]
 	dstring	--- alias for dchar[]
 
@@ -168,7 +169,7 @@
 	<string|"Copyright"=...>
 
 
-Example:
+Example:	!THIS IS NO LONGER VALID!
 {MT01}
 {example section}
 <u32|"num"=5>
--- a/mde/mergetag/exception.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/mergetag/exception.d	Sun Jan 06 17:38:51 2008 +0000
@@ -20,21 +20,46 @@
     this () {}
 }
 
-/** Thrown onunknown format errors; when reading or writing and the filetype cannot be guessed. */
+/** Thrown on unknown format errors; when reading or writing and the filetype cannot be guessed. */
 class MTFileFormatException : MTException {
     this () {}
 }
 
 /** Thrown when a string ID is parsed but cannot be found in the lookup table, hence cannot be used
  * as an ID. It should always be caught and handled gracefully (by ignoring the tag or section
- * involved).
- */
+ * involved). */
 class MTStringIDException : MTException {
     this () {}
 }
 
-/** Thrown by classes implementing DataSection when addTag is called with an unrecognised type string.
- */
+/** Thrown by addTag (in classes implementing dataset.DataSection) when a tag is read with an
+ * unrecognised type field. */
 class MTUnknownTypeException : MTException {
     this () {}
+    this (char[] msg) {
+        super (msg);
+    }
 }
+
+/** Thrown by addTag (in classes implementing dataset.DataSection) when a data parsing error occurs
+* (really just to make whoever called addTag to log a warning saying where the error occured). */
+class MTaddTagParseException : MTException {
+    this () {}
+}
+
+/// Thrown by TypeView.parse on errors.
+class MTBadTypeStringException : MTException {
+    this () {}
+}
+
+/// Thrown by TextWriter.indexTable (only in debug mode).
+class MTBadIDStringException : MTException {
+    this () {}
+}
+
+/// Thrown by *Writer.write.
+class MTNoDataSetException : MTException {
+    this (char[] msg) {
+        super(msg);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/mergetag/old-code/dataset.d.old	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,149 @@
+/// This file contains text removed from dataset.d, which might possibly still be useful.
+
+/**
+  Class for exceptions of type "Incorrect Type" thrown when trying to read an item. Identical use to class Exception, but provides an easy way to handle only exceptions of this type.
+*/
+class IncorrectType : Exception {
+	this (char[] s) {
+		super(s);
+	}
+}
+
+/**
+  Data class; contains a Data member for each loaded section of a file.
+  
+  Could be a struct, except structs are value types (not reference).
+*/
+class DataSet
+{
+/+	Data[SecID] sec;		/// Dynamic array of section data +/
+	void*[char[]][char[]] data;
+	
+	void*[char[]] opIndex(char[] i) {
+		return data[i];
+	}
+	void*[char[]][char[]] opSlice() {
+		return data[];
+	}
+	void*[char[]][char[]] opSlice(char[] i, char[] j) {
+		return data[i,j];
+	}
+}
+
+struct Item {
+	enum Type : ubyte {
+		_void = 0,		// initial type
+		tuple, dynlist,
+		_bool, _byte, _ubyte, _short, _ushort, _int, _uint, _long, _ulong, _cent, _ucent,
+		_char, _wchar, _dchar;
+		_float, _double, _real,
+		_ifloat, _idouble, _ireal,
+		_cfloat, _cdouble, _creal
+	}
+	static char[][26] typeName = ["void","tuple",];
+	Type type;
+	new union {
+		Tuple	tuple;
+		DynList	dynlist;
+//		DynMerge dynmerge; merging lists are stored as dynamic lists
+		bool	_bool;
+		byte	_byte;
+		ubyte	_ubyte;
+		short	_short;
+		ushort	_ushort;
+		int	_int;
+		uint	_uint;
+		long	_long;
+		ulong	_ulong;
+		cent	_cent;
+		ucent	_ucent;
+		char	_char;
+		wchar	_wchar;
+		dchar	_dchar;
+		float	_float;
+		double	_double;
+		real	_real;
+		ifloat	_ifloat;
+		idouble	_idouble;
+		ireal	_ireal;
+		cfloat	_cfloat;
+		cdouble	_cdouble;
+		creal	_creal;
+	}
+	
+	/** Functions to get data
+	
+	Each function will, if the element is of the appropriate type, return the element; if the type
+	is incorrect it will throw an error.
+	*/
+	bool _bool () {
+		if (type != _bool) throw new IncorrectType("Incorrect type when trying to read: tried to read as bool when item had type " ~ typeName[type]);
+		return _bool;
+	}
+	int _int () {	/// ditto
+		if (type != _int) throw new IncorrectType("Incorrect type when trying to read: tried to read as int");
+		return _int;
+	}
+	uint _uint () {	/// ditto
+		if (type != _uint) throw new IncorrectType("Incorrect type when trying to read: tried to read as uint");
+		return _uint;
+	}
+}
+
+struct DynList
+{
+	
+}
+
+class Data
+{
+	// added & accessed soley by templates
+	private (uint,void*)[Index]	_gd;		// generic data
+}
+
+// Externally, types are given as a string:
+typedef char[]	typeIDstr;
+	private:
+// Internally, types are given by a uint for performance:
+typedef uint	typeID;
+typeID[typeIDstr] typeIDTable;	// used to look up type typeID
+
+// This (belongs in read.d) contains a table of reading functions for all supported types. Do similarly for writing.
+(void function (Data, char[]))[typeID] genericReader;
+
+// Template function for creating a function to read a new type and adding it to genericReader:
+/+ don't actually use this without specialization
+void addSupport (T) () {
+	// create a function to read this type from a string and add it into Data.genericData as type void*; put a function pointer into generic Reader
+	// do same for write support
+	// create a reader function, accessible by the user of the library, for accessing elements/converting to the proper type
+}+/
+
+/**
+  Get data of the appropriate type.
+  
+  The function performs a check that the data is of the appropriate type and throws an exception if
+  not.
+  
+  Note: can be called as d.get!($(I type))(i).
+*/
+get(T : int) (Data d, Index i) {
+	return cast(T) *d._gd[i];
+}
+
+// add support for basic types (for all basic types):
+void addSupport (T : int) () {
+	T read_int (char[]);
+	void* get_voidp (T d) {
+		return cast(void*) &d;
+	}
+}
+
+void addSupport (T : T[]) () {	// for any array type
+	// reader: split input and call appropriate fct to convert sub-strings to type T
+	// writer: use appropriate fct to convert to substrings; concat into "[val1,val2,...,valn]" format
+	// access: store as void* to T[] and something like this:
+	T[] get(Data d, Index i) {	// but cannot overload by return-type!
+		return cast(T[]) genericData[i]
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/mergetag/old-code/typeSpec.d.old	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,103 @@
+/+ I wrote this and then realised it's not worth using, it being easier and faster to match type
+strings without this. This module does enable spaces to be in appropriate places, but so what...
+
+module typeSpec;
+
+import mde.mergetag.exception;
+
+import Util = tango.text.Util;
+
+/**
+ * Struct to view a type specifier string.
+ */
+struct TypeView
+{
+    /// Type of stored type specifier (NULL, BASIC, ARRAY, ASSOCIATIVE_ARRAY).
+    enum SPEC_TYPE : ubyte {
+        NULL, BASIC, ARRAY, ASSOCIATIVE_ARRAY
+    }
+    
+    SPEC_TYPE spec_type;		/// Current specifier type.
+    union {
+        char[] type_name;		/// For BASIC types is the type name.
+        TypeView* type_array_value;	/// For ARRAY and ASSOCIATIVE_ARRAY types, this is the value-type.
+    }
+    TypeView* type_array_key;	/// For ASSOCIATIVE_ARRAY types, this is the key-type;
+    
+    /** Construct and return a TypeView from a source string.
+     *
+     * Such a string should have one of the following forms:
+     *	TYPE_NAME:	[_a-zA-Z][_a-zA-Z0-9]* (regexp)
+     *	ARRAY:		TYPE_NAME[]
+     *	ASSOC_ARRAY:	TYPE_NAME[TYPE_NAME]
+     *
+     * Throws MTBadTypeStringException on bad type specifier strings.
+     */
+    static TypeSpec parse (char[] src) {
+        bool isNameChar (char c) {
+            return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c = '_';
+        }
+        void throwErr () {
+            logger.warn ("Bad type string");
+            throw new MTBadTypeStringException ();
+        }
+        
+        src = Util.trim(src);
+        if (!src.length) throwErr;
+        
+        TypeView ret;
+        if (src[$-1] == ']') {	// ARRAY or ASSOCIATIVE_ARRAY type
+            if (src.length < 2) throwErr;
+            
+            if (src[$-2] == '[') {	// ARRAY type
+                ret.spec_type = SPEC_TYPE.ARRAY;		// this is an array of...
+                ret.type_array_value = parse (src[0..$-2]);	// whatever this is
+            }
+            else {			// ASSOCIATIVE_ARRAY type
+                // look backwards for matching '[' bracket:
+                uint i = src.length-2;
+                uint recursion_depth = 0;
+                while (true) {
+                    char c = src[i];
+                    
+                    if (c == ']') ++recursion_depth;
+                    if (c == '[') {
+                        if (recursion_depth) --recursion_depth;
+                        else break;
+                    }
+                    if (i == 0) throwErr;	// no matching bracket!
+                    else --i;
+                }
+                
+                ret.spec_type = SPEC_TYPE.ASSOCIATIVE_ARRAY;
+                ret.type_array_value = parse (src[0..i]);
+                ret.type_array_key = parse (src[i+1..$-1]);
+            }
+        }
+        else {	// BASIC type
+            char c = src[0];
+            if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c = '_'))
+                throwErr;
+            for (uint i=1; i < src.length; ++i) {
+                if (!isNameChar(src[i])) throwErr;
+            }
+            
+            ret.spec_type = SPEC_TYPE.BASIC;
+            ret.type_name = src;
+        }
+        return ret;
+    }
+}
+
+unittest {
+    TypeView tv = TypeView.parse (` type1 [] [ Type2 [] ] [] `);
+    assert (tv.spec_type == SPEC_TYPE.ARRAY);
+    assert (tv.type_array_value.spec_type == SPEC_TYPE.ASSOCIATIVE_ARRAY);
+    assert (tv.type_array_value.type_array_value.spec_type == SPEC_TYPE.ARRAY);
+    assert (tv.type_array_value.type_array_value.type_array_value.spec_type == SPEC_TYPE.BASIC);
+    assert (tv.type_array_value.type_array_value.type_array_value.type_name == `type1`);
+    assert (tv.type_array_value.type_array_key.spec_type == SPEC_TYPE.ARRAY);
+    assert (tv.type_array_value.type_array_key.type_array_value.spec_type == SPEC_TYPE.BASIC);
+    assert (tv.type_array_value.type_array_key.type_array_value.type_name == `Type2`);
+}
++/
\ No newline at end of file
--- a/mde/mergetag/read.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/mergetag/read.d	Sun Jan 06 17:38:51 2008 +0000
@@ -8,7 +8,7 @@
 
 // package imports
 public import mde.mergetag.dataset;
-import mde.mergetag.exception;
+public import mde.mergetag.exception;
 
 // tango imports
 import tango.io.UnicodeFile;
@@ -16,6 +16,7 @@
 import ConvInt = tango.text.convert.Integer;
 import tango.util.collection.model.View : View;
 import tango.util.collection.ArrayBag : ArrayBag;
+import tango.util.collection.HashSet : HashSet;
 import tango.util.log.Log : Log, Logger;
 
 // TODO: allow compressing with zlib for both binary and text? (.mtz, .mtt, .mtb extensions)
@@ -28,10 +29,21 @@
  * 
  * Use as:
  * -----------------------
- * Reader foo("foo.mtt");
- * foo.read();
+ * Reader foo;
+ * try {
+ *   foo = new Reader("foo.mtt");
+ *   foo.read();
+ * }
+ * catch (MTException) {}
  * // get your data from foo.dataset.
  * -----------------------
+ *
+ * Only a child-class of MTException will ever be thrown, currently MTFileIOException if the file
+ * could not be read or MTFileFormatException on any error when parsing the file.
+ *
+ * Threading: Reader should be thread-safe provided access to the same dataset is synchronized;
+ * i.e. no two readers refering to the same dataset should run simultaneously. (The Reader class
+ * could be made thread-safe w.r.t. datasets, but performance-wise I doubt it would be worth it.)
  */
 class Reader
 {
@@ -62,23 +74,10 @@
     DataSection function (ID) dataSecCreator = null;
     
 private:
-    // Static symbols:
     typedef void delegate (TypeInfo,ID,char[]) readDelg;	// Delegate for accepting tags.
     
-    static bool initialised = false;
-    static TypeInfo[char[]] typeTable;
     static Logger logger;
     
-    // Error messages as const variables. Could be loaded from files to support other languages?
-    static const char[] ERR_FILEREAD = "Error reading file: ";
-    static const char[] ERR_MTHEAD = "Not a valid MergeTag text file";
-    static const char[] ERR_MTVER = "Unrecognised MergeTag version: MT";
-    static const char[] ERR_EOF = "Unexpected EOF";
-    static const char[] ERR_STAG = "Bad section tag format: not {id}";
-    static const char[] ERR_DTAG = "Bad data tag format: not <type|id=data>";
-    static const char[] ERR_CHAR = "Invalid character (or sequence starting \"!\") outside of tag";
-    static const char[] ERR_IDINT = "Tag has invalid integer ID: not a valid uint value";
-    
     // Non-static symbols:
     final char[] ErrInFile;		// something like "in \"path/file.mtt\""
     
@@ -106,43 +105,8 @@
 //END DATA
     
 //BEGIN METHODS: CTOR / DTOR
-    // Could be a static this(), but this way it's only called if the class is used.
-    private void init () {
-        init_addType!(bool);
-        init_addType!(byte);
-        init_addType!(short);
-        init_addType!(int);
-        init_addType!(long);
-        init_addType!(ubyte);
-        init_addType!(ushort);
-        init_addType!(uint);
-        init_addType!(ulong);
-        init_addType!(char);
-        init_addType!(float);
-        init_addType!(double);
-        init_addType!(real);
-        init_addType!(bool[]);
-        init_addType!(byte[]);
-        init_addType!(short[]);
-        init_addType!(int[]);
-        init_addType!(long[]);
-        init_addType!(ubyte[]);
-        init_addType!(ushort[]);
-        init_addType!(uint[]);
-        init_addType!(ulong[]);
-        init_addType!(char[]);
-        init_addType!(float[]);
-        init_addType!(double[]);
-        init_addType!(real[]);
-        // aliases:
-        typeTable["string"] = typeid(char[]);
-        typeTable["binary"] = typeid(ubyte[]);
-        
+    static this () {
         logger = Log.getLogger ("mde.mergetag.read.Reader");
-        initialised = true;
-    }
-    private static void init_addType(T) () {
-        typeTable[T.stringof] = typeid(T);
     }
     
     /** Tries to open file path and read it into a buffer.
@@ -173,8 +137,6 @@
     }
     /** ditto */
     public this (PathView path, DataSet* dataset_ = null, bool rdHeader = false) {
-        if (!initialised) init();	// on-demand static this()
-        
         // Create a dataset or use an existing one
         if (dataset_) dataset = *dataset_;
         else dataset = new DataSet();
@@ -184,22 +146,23 @@
             scope file = new UnicodeFile!(char) (path, Encoding.Unknown);
             fbuf = cast(char[]) file.read();
         } catch (Exception e) {
-            throwMTErr (ERR_FILEREAD ~ e.msg, new MTFileIOException);
+            throwMTErr ("Error reading file: " ~ e.msg, new MTFileIOException);
         }
         // Remember the file name so that we can report errors (somewhat) informatively:
         ErrInFile = " in \"" ~ path.path ~ path.file ~ '"';
         
         // Version checking & matching header section tag:
         if (fbuf.length < 6 || fbuf[0] != '{' || fbuf[1] != 'M' || fbuf[2] != 'T' || fbuf[5] != '}')
-            throwMTErr(ERR_MTHEAD ~ ErrInFile, new MTFileFormatException);
+            throwMTErr("Not a valid MergeTag text file" ~ ErrInFile, new MTFileFormatException);
         fileVer = MTFormatVersion.parseString (fbuf[3..5]);
         if (fileVer == MTFormatVersion.VERS.INVALID)
-            throwMTErr(ERR_MTVER ~ fbuf[3..5] ~ ErrInFile, new MTFileFormatException);
+            throwMTErr("Unrecognised MergeTag version: MT" ~ fbuf[3..5] ~ ErrInFile, new MTFileFormatException);
         
         // Header reading/skipping:
         if (rdHeader) {	// only bother actually reading it if it was requested
-            dataset.header = new DefaultData;
-            endOfHeader = parseSection (6,&dataset.header.addTag);
+            // If already existing, merge.
+            if (!dataset.header) dataset.header = new DefaultData;
+            endOfHeader = parseSection (6, cast(DataSection*) &dataset.header);
         }
         else endOfHeader = parseSection (6,null);
     }
@@ -251,7 +214,12 @@
     * that the entire file need not be re-read if further (or all remaining) sections are read
     * later.
     */
-    public void read (View!(ID) secSet = new ArrayBag!(ID)) {
+    public void read (ID[] secSet) {
+        HashSet!(ID) hs;
+        foreach (id; secSet) hs.add(id);
+        read (hs);
+    }
+    public void read (View!(ID) secSet = new ArrayBag!(ID)) {	/** ditto */
         if (allRead || fatal) return;			// never do anything in either case
         if (secSet.size) {
             if (secTable.length) {
@@ -259,7 +227,7 @@
                     SecMD* psmd = id in secTable;
                     if (psmd && !psmd.read) {			// may not exist
                         DataSection ds = getOrCreateSec (id);
-                        parseSection (psmd.pos, &ds.addTag);
+                        parseSection (psmd.pos, &ds);
                         psmd.read = true;
                     }
                 }
@@ -270,7 +238,7 @@
                         secTable[id] = SecMD(pos,false);	// add to table
                         if (secSet.contains(id)) {
                             DataSection ds = getOrCreateSec (id);
-                            pos = parseSection (pos, &ds.addTag);
+                            pos = parseSection (pos, &ds);
                             secTable[id].read = true;
                         }
                     } catch (MTStringIDException) {	// don't do any of the stuff above
@@ -283,7 +251,7 @@
                 foreach (ID id, ref SecMD smd; secTable) {
                     if (!smd.read) {
                         DataSection ds = getOrCreateSec (id);
-                        parseSection (smd.pos, &ds.addTag);
+                        parseSection (smd.pos, &ds);
                         smd.read = true;
                     }
                 }
@@ -292,7 +260,7 @@
                     try {
                         ID id = fbufReadSecMarker (pos);
                         DataSection ds = getOrCreateSec (id);
-                        pos = parseSection (pos, &ds.addTag);
+                        pos = parseSection (pos, &ds);
                     } catch (MTStringIDException) {
                         pos = parseSection (pos, null);	// just skip the section
                     }
@@ -312,11 +280,12 @@
     NOTE: from performance tests on indexing char[]'s and dereferencing char*'s, the char*'s are
     slightly faster, but a tiny difference isn't worth the extra effort/risk of using char*'s.
     */
-    private uint parseSection (uint pos, readDelg addTag) {
+    private uint parseSection (uint pos, DataSection* dsec) {
         bool comment = false;				// preceding char was !
         for (; pos < fbuf.length; ++pos) {
             if (Util.isSpace(fbuf[pos])) continue;	// whitespace
             else if (fbuf[pos] == '<') {		// data tag
+                const char[] ERR_DTAG = "Bad data tag format: not <type|id=data>";
                 char[] type, data;
                 ID tagID;
                 
@@ -341,10 +310,18 @@
                 if (fbuf[pos] != '>') throwMTErr (ERR_DTAG ~ ErrInFile);
                 data = fbuf[pos_s..pos];
                 
-                if (!comment && addTag != null) {
-                    TypeInfo* ti_p = Util.trim(type) in typeTable;
-                    if (ti_p) addTag (*ti_p, tagID, data);
-                    else logger.warn ("Type not supported: " ~ type);
+                if (!comment && dsec != null) {
+                    type = Util.trim(type);
+                    try {
+                        dsec.addTag (type, tagID, data);
+                    }
+                    catch (MTaddTagParseException) {
+                        logger.warn("Above error occured" ~ ErrInFile);	// following a parse error
+                    }
+                    catch (MTUnknownTypeException e) {
+                        logger.warn ("Unsupported type \"" ~ type ~ "\" " ~ ErrInFile /*~ ":"*/);
+                        //logger.warn (e.msg); needless; the above includes enough details.
+                    }
                 } else comment = false;			// cancel comment status now
             }
             else if (fbuf[pos] == '{') {
@@ -367,7 +344,7 @@
                 comment = true;				// starting a comment (or an error)
                 					// variable is reset at end of comment
             } else					// must be an error
-                throwMTErr (ERR_CHAR ~ ErrInFile);
+                throwMTErr ("Invalid character (or sequence starting \"!\") outside of tag" ~ ErrInFile);
         }
         // if code execution reaches here, we're at EOF
         // possible error: last character was ! (but don't bother checking since it's inconsequential)
@@ -390,7 +367,7 @@
         // since we haven't hit EOF, fbuf[pos] MUST be '{' so no need to check
         fbufIncrement(pos);
         ID id = fbufReadID (pos);
-        if (fbuf[pos] != '}') throwMTErr (ERR_STAG ~ ErrInFile);
+        if (fbuf[pos] != '}') throwMTErr ("Bad section tag format: not {id}" ~ ErrInFile);
         fbufIncrement(pos);
         return id;
     }
@@ -412,7 +389,7 @@
         } else {
             uint ate;
             long x = ConvInt.parse (fbuf[pos..$], 0, &ate);
-            if (x < 0L || x > 0xFFFF_FFFFL) throwMTErr (ERR_IDINT ~ ErrInFile);
+            if (x < 0L || x > 0xFFFF_FFFFL) throwMTErr ("Tag has invalid integer ID: not a valid uint value" ~ ErrInFile);
             pos += ate;					// this is where ConvInt.parse stopped
             while (Util.isSpace(fbuf[pos])) fbufIncrement(pos);	// skip any space
             return cast(ID) x;
@@ -429,14 +406,13 @@
         for (; pos < fbuf.length; ++pos) {
             if ((fbuf[pos] >= '<' && fbuf[pos] <= '>') || fbuf[pos] == '|') return;
             else if (quotable) {
-                if (fbuf[pos] == '\'') {
-                    do {
+                char c = fbuf[pos];
+                if (c == '\'' || c == '"') {
+                    ++pos;
+                    while (fbuf[pos] != c) {
+                        if (fbuf[pos] == '\\') ++pos;	// escape seq.
                         fbufIncrement(pos);
-                    } while (fbuf[pos] != '\'')
-                } else if (fbuf[pos] == '"') {
-                    do {
-                        fbufIncrement(pos);
-                    } while (fbuf[pos] != '"')
+                    } 
                 }
             }
         }
@@ -444,10 +420,10 @@
     /* Increments pos and checks it hasn't hit fbuf.length . */
     private void fbufIncrement(inout uint pos) {
         ++pos;
-        if (pos >= fbuf.length) throwMTErr(ERR_EOF ~ ErrInFile);
+        if (pos >= fbuf.length) throwMTErr("Unexpected EOF" ~ ErrInFile);
     }
     
-    private void throwMTErr (char[] msg, Exception exc = new MTException) {
+    private void throwMTErr (char[] msg, MTException exc = new MTFileFormatException) {
         fatal = true;	// if anyone catches the error and tries to do anything --- we're dead now
         logger.error (msg);	// report the error
         throw exc;		// and signal our error
--- a/mde/mergetag/write.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/mergetag/write.d	Sun Jan 06 17:38:51 2008 +0000
@@ -18,10 +18,11 @@
 import mde.mergetag.exception;
 
 // tango imports
+import tango.core.Exception;
 import tango.io.FileConduit;
 import tango.io.Buffer : Buffer, IBuffer;
-import tango.text.convert.Layout : Layout;
 import tango.io.Print : Print;
+import convInt = tango.text.convert.Integer;
 import tango.util.log.Log : Log, Logger;
 
 Logger logger;
@@ -51,11 +52,11 @@
  *
  * An exception is thrown if neither test can deduce the writing method.
  */
-IWriter makeWriter (char[] path, DataSet dataset, WriterMethod method = WriterMethod.Unspecified) {
+IWriter makeWriter (char[] path, DataSet dataset = null, WriterMethod method = WriterMethod.Unspecified) {
     return makeWriter (new FilePath (path), dataset, method);
 }
 /** ditto */
-IWriter makeWriter (PathView path, DataSet dataset, WriterMethod method = WriterMethod.Unspecified) {
+IWriter makeWriter (PathView path, DataSet dataset = null, WriterMethod method = WriterMethod.Unspecified) {
     void throwMTErr (char[] msg, Exception exc = new MTException) {
         logger.error (msg);
         throw exc;
@@ -72,7 +73,12 @@
 }
 
 /// Interface for methods and data necessarily available in TextWriter and/or BinaryWriter.
+// FIXME: rename MTWriter or so.
 interface IWriter {
+    /// Get or set the DataSet.
+    DataSet dataset ();
+    void dataset (DataSet);	/// ditto
+    
     /** Only used in a TextWriter; see TextWriter.indexTable().
      *
      * Note that other implementors implement this as a blank function and will not throw an error.
@@ -91,22 +97,43 @@
 /**
  * Class to write a dataset to a file.
  *
- * Is a scope class, since the file is kept open until ~this() runs.
+ * Files are only actually open for writing while the write() method is running.
  */
 scope class TextWriter : IWriter
 {
 //BEGIN DATA
-    /** The container where data is written from.
-     */
-    DataSet dataset;
-    
+    /// Get or set the DataSet.
+    DataSet dataset () {	return dataset;	}
+    void dataset (DataSet ds)	/// ditto
+    {	dataset = ds;	}
+        
     
     /** A table, which if created, allows items in a text file to be written with a string ID.
     *
     * If any ID (for a section or tag) to be written is found in this table, the corresponding
     * string is written instead.
+    *
+    * In debug mode, logs a warning if any of the given strings are invalid (but never throws).
     */
     void indexTable (char[][ID] iT) {
+        debug {		// check validity (no unescaped " symbols or unfinished escape sequence)
+            foreach (str; iT) {
+                for (uint i = 0; i < str.length; ++i) {
+                    auto ERR_STRING = "While preparing to write " ~ path.toUtf8() ~ ":";
+                    if (str[i] == '"') {
+                        logger.warn (ERR_STRING);
+                        logger.warn ("In given indexTable: unescaped \" char. Likely to cause errors when reading!");
+                    }
+                    if (str[i] == '\\') {
+                        ++i;
+                        if (i == str.length) {
+                            logger.warn (ERR_STRING);
+                            logger.warn ("In given indexTable: trailing \\ (half-escape sequence). Likely to cause errors when reading!");
+                        }
+                    }
+                }
+            }
+        }
         _indexTable = iT;
     }
     
@@ -117,15 +144,12 @@
     else
         const char[] Eol = "\n";
 
-    bool fatal = false;		// fatal error occured: don't attempt anything else
-    bool fileOpen = false;	// file needs to be closed on exit
-    bool writtenHeader = false;	// The header MUST be written exactly once at the beginning of the file.
+    /* The container where data is written from. */
+    DataSet _dataset;
+    
+    PathView path;
     
     char[][ID] _indexTable;	// see indexTable() doc for use.
-        
-    FileConduit conduit;	// actual conduit; don't use directly when there's content in the buffer
-    IBuffer buffer;		// write strings directly to this (use opCall(void[]) )
-    Print!(char) format;	// formats output to buffer
 //END DATA
     
 //BEGIN CTOR / DTOR
@@ -137,72 +161,87 @@
     * dataset_ = If null create a new DataSet, else use existing DataSet *dataset_ and merge read
     *     data into it.
     */
-    public this (char[] path, DataSet dataset_) {
-        this (new FilePath (path), dataset_);
+    public this (char[] _path, DataSet ds = null) {
+        this (new FilePath (_path), ds);
     }
     /** ditto */
-    public this (PathView path, DataSet dataset_) {
-        try {	// open a conduit on the file
-            conduit = new FileConduit (path, FileConduit.WriteCreate);
-            buffer = new Buffer(conduit);
-            format = new Print!(char) (new Layout!(char), buffer);
-            fileOpen = true;
-        } catch (Exception e) {
-            throwMTErr ("Error opening file: " ~ e.msg);
-        }
-    }	// OK, all set to start writing.
-    
-    ~this () {	// close file on exit
-        if (fileOpen) {
-            buffer.flush();
-            conduit.close();
-        }
+    public this (PathView _path, DataSet ds = null) {
+        path = _path;
+        _dataset = ds;
     }
 //END CTOR / DTOR
     
     /** Writes the header and all DataSections.
      *
-     * Firstly writes the header unless it has already been read. Then writes all DataSections
-     * to the file. Thus write is called more than once with or without changing the DataSet the
+     * Firstly writes the header unless it has already been written. Then writes all DataSections
+     * to the file. Thus if write is called more than once with or without changing the DataSet the
      * header should be written only once. This behaviour could, for instance, be used to write
      * multiple DataSets into one file without firstly merging them. Note that this behaviour may
      * be changed when binary support is added.
+     *
+     * Throws:
+     *	MTNoDataSetException if the dataset is null,
+     *	MTFileIOException if a file IO error occurs,
+     *	MTException on any other exception (unexpected).
      */
     public void write ()
     {
-        // Write the header:
-        if (!writtenHeader) {
-            buffer ("{MT")(MTFormatVersion.CurrentString)("}")(Eol);
-            writtenHeader = true;
-        }
-        writeSection (dataset.header);
+        if (!_dataset) throw new MTNoDataSetException ("write(): Dataset needed to write from!");
+        
+        try {
+            FileConduit conduit;	// actual conduit; don't use directly when there's content in the buffer
+            IBuffer buffer;		// write strings directly to this (use opCall(void[]) )
+        
+            // Open a conduit on the file:
+            conduit = new FileConduit (path, FileConduit.WriteCreate);
+            scope(exit) conduit.close();
+            
+            buffer = new Buffer(conduit);	// And a buffer
+            scope(exit) buffer.flush();
+            
+            // Write the header:
+            buffer ("{MT" ~ MTFormatVersion.CurrentString ~ "}" ~ Eol);
+            if (_dataset.header) writeSection (buffer, _dataset.header);
         
-        // Write the rest:
-        foreach (ID id, DataSection sec; dataset.sec) {
-            writeSectionIdentifier (id);
-            writeSection (sec);
+            // Write the rest:
+            foreach (ID id, DataSection sec; _dataset.sec) {
+                writeSectionIdentifier (buffer, id);
+                writeSection (buffer, sec);
+            }
+        
+            buffer.flush();
+            
+        }
+        catch (IOException e) {
+            throwMTErr ("Error writing to file: " ~ e.msg, new MTFileIOException);
         }
+        catch (Exception e) {
+            throwMTErr ("Unexpected exception when writing file: " ~ e.msg);
+        }
+    }
         
-        buffer.flush();
+    private void writeSectionIdentifier (IBuffer buffer, ID id) {
+        buffer ("{");
+        char[]* s = id in _indexTable;	// look for a string ID
+        if (s) buffer ("\"" ~ *s ~ "\"");	// write a string ID
+        else buffer (convInt.toUtf8(id));	// write a numeric ID
+        buffer ("}" ~ Eol);
     }
     
-    private void writeSectionIdentifier (ID id) {
-        buffer ("{");
-        char[]* p = id in _indexTable;	// look for a string ID
-        if (p) buffer ("\"")(*p)("\"");	// write a string ID
-        else format (cast(uint) id);	// write a numeric ID
-        buffer ("}")(Eol);
-    }
-    
-    private void writeSection (DataSection sec) {
-        //FIXME
-        // convert TypeInfo ti using ti.toUtf8 ?
+    private void writeSection (IBuffer buffer, DataSection sec) {
+        void writeItem (char[] tp, ID id, char[] dt) {	// actually writes an item
+            buffer ("<" ~ tp ~ "|");
+            char[]* s = id in _indexTable;
+            if (s) buffer ("\"" ~ *s ~ "\"");
+            else buffer (convInt.toUtf8(id));
+            buffer ("=" ~ dt ~ ">" ~ Eol);
+        }
+        sec.writeAll (&writeItem);
         
         buffer (Eol);			// blank line at end of each section
     }
     
     private void throwMTErr (char[] msg, Exception exc = new MTException) {
-        fatal = true;			// if anyone catches the error and tries to do anything --- we're dead now
         logger.error (msg);		// report the error
         throw exc;			// and signal our error
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/scheduler.d	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,84 @@
+/** Scheduler
+*/
+module mde.scheduler;
+
+import tango.time.Time;
+
+// NOTE: Currently has no support for removing functions. To fix, assign ID and store fct pointers
+// in an associative array, returning the ID [on adding fct pointer].
+// FIXME: support delegates or not?
+/// This class can run scheduled functions per frame or every t seconds (sim-time).
+class Scheduler
+{
+    /** The type of function pointer to be passed to the scheduler.
+    *
+    * The double $(I time) parameter gives the number of (sim) seconds since the function was last
+    * called, or zero on the first run. */
+    alias void function (double time) scheduleFct;
+    
+    /** Add a function to be called per frame. */
+    static void perFrame (scheduleFct fct) {
+        frameFcts ~= fct;
+    }
+    
+    /** Add a function to be called per t secs or n 100-nano-sec intevals.
+    *
+    * Since the scheduler cannot guarantee a maximum time between calls, the interval at which
+    * functions are called is always greater than or equal to the inverval specified here. Of
+    * course, the actual inteval is given when the function is run.
+    */
+    static void perTime (double t, scheduleFct fct) {
+        perTime (TimeSpan.interval(t), fct);
+    }
+    /** ditto */
+    static void perTime (TimeSpan n, scheduleFct fct)
+    in { assert (n > TimeSpan (0L)); }
+    body {
+        timeFcts ~= TimeFct (fct, n);
+    }
+    
+    /** This function should get called by the main loop, once per frame.
+    *
+    * The parameter time should be the current sim-time, using the tango.core.Types.Time enum; all
+    * time evaluations will use this.
+    */
+    static void run (Time time) {
+        double interval;
+        
+        // Call all per-frame functions:
+        if (lastTime == Time (0L)) interval = 0.0;		// 0 interval for first loop
+        else interval = (time-lastTime).interval();
+        
+        foreach (fct; frameFcts) fct(interval);
+        
+        // Call all per-interval functions:
+        foreach (fct; timeFcts) if (time >= fct.nextCall) {
+            if (fct.nextCall == Time (0L)) interval = 0.0;	// 0 interval for first call
+            else interval = (time - (fct.nextCall - fct.interval)).interval();
+            fct.nextCall = time + fct.interval;		// when to call next
+            
+            fct.fct (interval);				// call
+        }
+    }
+    
+    /* Holds details for functions called per time interval. */
+    private struct TimeFct {
+        scheduleFct fct;			// function to call
+        
+        TimeSpan interval;			// interval to call at
+        // Storing nextCall is more efficient than storing lastCall since only this number has to
+        // be compared to time every frame where fct is not called:
+        Time nextCall = Time (0L);
+        
+        static TimeFct opCall (scheduleFct f, TimeSpan t) {	// static CTOR
+            TimeFct ret;
+            ret.fct = f;
+            ret.interval = t;
+            return ret;
+        }
+    }
+    
+    private static Time lastTime = Time (0L);
+    private static scheduleFct[] frameFcts;
+    private static TimeFct[] timeFcts;
+}
--- a/mde/test.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/test.d	Sun Jan 06 17:38:51 2008 +0000
@@ -5,34 +5,58 @@
 import mde.text.parse;
 
 import tango.io.Stdout;
+import Int = tango.text.convert.Integer;
+
+void MTtest () {
+    Reader MTread;
+    try {
+        MTread = new Reader ("test.mtt", null, true);
+        static DataSection dataPrinter (ID id) {	return new test.DataPrinter (id);	}
+        MTread.dataSecCreator = &dataPrinter;
+        MTread.read();
+    } catch (Exception e) {
+        Stdout (e.msg).newline;
+    }
+    //Stdout ("Data read from file:").newline;
+    //test.printDataSet (MTread.dataset);
+}
 
 /// Prints $(I some) of the dataset.
 void printDataSet (DataSet ds) {
     foreach (ID sec_id, DefaultData dd; ds.getSections!(DefaultData)()) {
         Stdout ("Section:  ")(cast(uint) sec_id).newline;
         foreach (ID i, int x; dd._int) {
-            Stdout (cast(uint) i)('\t')(x).newline;
+            printLabel (i);
+            Stdout (x).newline;
         }
         foreach (ID i, int[] x; dd._intA) {
-            Stdout (cast(uint) i);
+            printLabel (i);
             foreach (int y; x)
-                Stdout ('\t')(y);
+                Stdout (y)(' ');
             Stdout.newline;
         }
         foreach (ID i, ubyte[] x; dd._binary) {
-            Stdout (cast(uint) i);
-            foreach (ubyte y; x)
-                Stdout ('\t')(y);
+            printLabel (i);
+            char[2] fmt;
+            foreach (ubyte y; x) {
+                Int.format(fmt,y,Int.Style.HexUpper,Int.Flags.Zero);
+                Stdout (fmt~' ');
+            }
             Stdout.newline;
         }
         foreach (ID i, char x; dd._char) {
-            Stdout (cast(uint) i)('\t')(x).newline;
+            printLabel (i);
+            Stdout (x).newline;
         }
         foreach (ID i, char[] x; dd._string) {
-            Stdout (cast(uint) i)('\t')(x).newline;
+            printLabel (i);
+            Stdout (x).newline;
         }
     }
 }
+void printLabel (ID i) {
+    Stdout (cast(uint) i)(":\t");
+}
 
 class DataPrinter : DataSection
 {
@@ -42,4 +66,8 @@
     void addTag (TypeInfo ti, ID id, char[] dt) {
         Stdout ("\tData item (")(id)("):\t")(ti)("\t")(dt).newline;
     }
+    void addTag (char[] tp, ID id, char[] dt) {
+        Stdout ("\tData item (")(id)("):\t")(tp)("\t")(dt).newline;
+    }
+    void writeAll (ItemDelg) {}
 }
--- a/mde/text/format.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/text/format.d	Sun Jan 06 17:38:51 2008 +0000
@@ -1,8 +1,9 @@
 /**************************************************************************************************
  * This contains templates for converting various data-types to a char[].
  *
- * Copyright (c) 2007 Diggory Hardy.
- * Licensed under the Academic Free License version 3.0
+ * Copyright: Copyright © 2007 Diggory Hardy.
+ * Authors: Diggory Hardy, diggory.hardy@gmail.com
+ * License: Licensed under the Academic Free License version 3.0
  *
  * This module basically implements the following templated function for $(B most) basic D types:
  * bool, byte, short, int, long, ubyte, ushort, uint, ulong, float, double, real, char.
@@ -18,6 +19,7 @@
  * be thrown and none thrown from functions used outside this module.
  *************************************************************************************************/
 module mde.text.format;
+// TODO: write unittests; check strings generate quotes.
 
 // package imports
 import mde.text.exception;
@@ -37,20 +39,46 @@
 //BEGIN Convert templates
 /* Idea: could extend format with a second parameter, containing flags for things like base to output.
  * Unnecessary for mergetag though.
- */
+*/
+
+// Associative arrays
+char[] format(T : T[S], S) (T[S] val) {
+    char[] ret;
+    ret.length = val.length * (defLength!(T) + defLength!(S)) + 2;
+    ret[0] = '[';
+    uint i = 1;
+    foreach (S k, T v; val) {
+        char[] s = format!(S) (k) ~ ":" ~ format!(T) (v);
+        i += s.length;
+        if (i+1 >= ret.length) ret.length = ret.length * 2;	// check.
+        ret[i-s.length .. i] = s;
+        ret[i++] = ',';
+    }
+    if (i == 1) ++i;	// special case - not overwriting a comma
+    ret[i-1] = ']';	// replaces last comma
+    return ret[0..i];
+}
+unittest {
+    char[] X = format!(char[][char]) (['a':cast(char[])"animal", 'b':['b','u','s']]);
+    char[] Y = `['a':"animal",'b':["bus"]]`;
+    assert (X == Y);
+}
+
 // Arrays
 char[] format(T : T[]) (T[] val) {
-    char[val.length * defLength!(T)] ret = void;
+    char[] ret;
+    ret.length = val.length * defLength!(T) + 2;
     ret[0] = '[';
-    uint i = 0;
+    uint i = 1;
     foreach (T x; val) {
         char[] s = format!(T) (x);
         i += s.length;
-        if (i >= ret.length) ret.length = ret.length * 2;	// check.
+        if (i+1 >= ret.length) ret.length = ret.length * 2;	// check.
         ret[i-s.length .. i] = s;
-        ret[i] = ',';
+        ret[i++] = ',';
     }
-    ret[i++] = ']';	// replaces last comma
+    if (i == 1) ++i;	// special case - not overwriting a comma
+    ret[i-1] = ']';	// replaces last comma
     return ret[0..i];
 }
 char[] format(T : dchar[]) (T val) {
@@ -60,7 +88,7 @@
     return format (toUtf8 (val));
 }
 char[] format(T : char[]) (T val) {
-    char[val.length * 2 + 2] ret = void;	// Initial storage. This should ALWAYS be enough.
+    char[] ret;	ret.length = val.length * 2 + 2;	// Initial storage. This should ALWAYS be enough.
     ret[0] = '"';
     uint i = 0;
     for (uint t = 0; t < val.length;) {
@@ -91,6 +119,15 @@
     }
     return ret;
 }
+unittest {
+    assert (format!(double[]) ([1.0, 1.0e-10]) == `[1,1e-10]`);		// generic array stuff
+    assert (format!(double[]) (cast(double[]) []) == `[]`);		// empty array
+    
+    // char[] conversions, with commas, escape sequences and multichar UTF8 characters:
+    assert (format!(char[][]) ([ ".\""[], [',','\''] ,"!\b€" ]) == `[".\"",",\'","!\b€"]`);
+    
+    assert (format!(ubyte[]) (cast(ubyte[]) [0x01, 0xF2, 0xAC]) == `01F2AC`);	// ubyte[] special notation
+}
 
 // Support for outputting a wide char... I reccomend against trying to output these though.
 const char[] WIDE_CHAR_ERROR = "Error: unicode non-ascii character cannot be converted to a single UTF-8 char";
@@ -121,11 +158,15 @@
     }
     assert (false);
 }
+unittest {
+    assert (format!(char) ('\'') == '\'');
+}
 
 char[] format(T : bool) (T val) {
     if (T) return "true";
     else return "false";
 }
+// too simple to need a unittest
 
 char[] format(T : byte) (T val) {
     return formatLong (val);
@@ -152,6 +193,11 @@
     if (val > cast(ulong) long.max) throwException ("No handling available for ulong where value > long.max");
     return formatLong (val);
 }
+unittest {
+    assert (format!(byte) (cast(byte) -5)) == "-5";
+    // annoyingly, octal syntax differs from D (blame tango):
+    assert (format!(uint[]) ([0b0100u,0724,0xFa59c,0xFFFFFFFF,0]) == "[0b100,0o724,0xFa59c,0xFFFFFFFF,0]");
+}
 
 char[] format(T : float) (T val) {
     // t.dig+2+4+3	// should be sufficient length (mant + (neg, dot, e, exp neg) + exp (3,4,5 for float,double,real resp.))
@@ -166,6 +212,11 @@
     char[32] ret;
     return cFloat.format (ret, val, T.dig+2, true);
 }
+unittest {
+    assert (format!(float) (0.0f) == "0");
+    assert (format!(double) (-1e25) == "-1e25");
+    assert (format!(real) (cast(real) 5.24e-269) == "5.24e-269");
+}
 //END Convert templates
 
 //BEGIN Length templates
@@ -180,7 +231,7 @@
 
 //BEGIN Utility funcs
 private char[] formatLong (long val) {
-    try return cInt.toUtf8 (val, cInt.Style.Unsigned, cInt.Flags.Throw);
+    try return cInt.toString (val, cInt.Style.Unsigned, cInt.Flags.Throw);
     catch (Exception e) throwException (e.msg);
 }
 private bool isEscapableChar (char c) {
@@ -193,16 +244,11 @@
     
     if (!escCharsRevFilled) {	// only do this once
         // map of all supported escape sequences
-        escCharsRev['"'] = '"';
-        escCharsRev['\''] = '\'';
-        escCharsRev['\\'] = '\\';
-        escCharsRev['\a'] = 'a';
-        escCharsRev['\b'] = 'b';
-        escCharsRev['\f'] = 'f';
-        escCharsRev['\n'] = 'n';
-        escCharsRev['\r'] = 'r';
-        escCharsRev['\t'] = 't';
-        escCharsRev['\v'] = 'v';
+        escCharsRev = ['"' : '"', '\'' : '\'',
+                       '\\' : '\\', '\a' : 'a',
+                       '\b' : 'b', '\f' : 'f',
+                       '\n' : 'n', '\r' : 'r',
+                       '\t' : 't', '\v' : 'v'];
         escCharsRevFilled = true;
     }
     
@@ -213,4 +259,8 @@
     logger.warn (msg);			// only small errors are trapped here
     throw new TextFormatException ();
 }
+
+unittest {
+    // all utility functions should be well-enough used not to need testing
+}
 //END Utility funcs
--- a/mde/text/parse.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/text/parse.d	Sun Jan 06 17:38:51 2008 +0000
@@ -1,17 +1,23 @@
 /**************************************************************************************************
  * This contains templates for converting a char[] to various data-types.
  *
- * Copyright (c) 2007 Diggory Hardy.
- * Licensed under the Academic Free License version 3.0
+ * Authors: Diggory Hardy, diggory.hardy@gmail.com
+ * Copyright: Copyright © 2007 Diggory Hardy.
+ * License: Licensed under the Academic Free License version 3.0
  *
  * This module basically implements the following templated function for $(B most) basic D types:
  * bool, byte, short, int, long, ubyte, ushort, uint, ulong, float, double, real, char.
- * It also supports arrays of any supported type (including of other arrays) and has special
- * handling for strings (char[]) and binary (ubyte[]) data-types.
+ * It also supports arrays and associative arrays of any supported type (including of other arrays)
+ * and has special handling for strings (char[]) and binary (ubyte[]) data-types.
  * -----------------------------
  * T parse(T) (char[] source);
  * -----------------------------
  *
+ * The syntax is mostly the same used by D without any prefixes/suffixes (except 0x, 0b & 0o base
+ * specifiers). The following escape sequences are supported for strings and characters: \' \" \\
+ * \a \b \f \n \r \t \v . Associative array literals use the same syntax as D, described here:
+ * $(LINK http://www.digitalmars.com/d/expression.html#AssocArrayLiteral).
+ *
  * There are also a few utility functions defined; the public ones have their own documentation.
  *
  * On errors, a warning is logged and an TextParseException is thrown. No other exceptions should
@@ -21,6 +27,7 @@
 
 // package imports
 import mde.text.exception;
+import mde.text.util : postTrim;
 
 // tango imports
 import cInt = tango.text.convert.Integer;
@@ -34,11 +41,54 @@
 }
 
 //BEGIN parse templates
+// Associative arrays
+T[S] parse(T : T[S], S) (char[] src) {
+    src = Util.trim(src);
+    if (src.length < 2 || src[0] != '[' || src[$-1] != ']')
+        throwException ("Invalid associative array: not [a:x, ..., c:z]");
+    
+    T[S] ret;
+    foreach (char[] pair; split (src[1..$-1])) {
+        uint i = 0;
+        while (i < pair.length) {	// advance to the ':'
+            char c = pair[i];
+            if (c == ':') break;
+            if (c == '\'' || c == '"') {	// string or character
+                ++i;
+                while (i < pair.length && pair[i] != c) {
+                    if (pair[i] == '\\') ++i;	// escape seq.
+                    ++i;
+                }	// Doesn't throw if no terminal quote at end of pair (in this case an error is thrown anyway)
+            }
+            ++i;
+        }
+        if (i == pair.length) {
+            debug logger.trace ("In pair: " ~ pair);
+            throwException ("Invalid key:value pair in associative array literal");
+        }
+        debug logger.trace ("pair is: " ~ pair[0..i] ~ " : " ~ pair[i+1..$]);
+        ret[parse!(S) (pair[0..i])] = parse!(T) (pair[i+1..$]);
+    }
+    return ret;
+}
+unittest {
+    char[][char] X = parse!(char[][char]) (`['a':"animal", 'b':['b','u','s']]`);
+    char[][char] Y = ['a':cast(char[])"animal", 'b':['b','u','s']];
+    
+    //FIXME: when the compiler's fixed...
+    // just assert (X == Y)
+    assert (X.length == Y.length);
+    assert (X.keys == Y.keys);
+    assert (X.values == Y.values);
+    //X.rehash; Y.rehash;	// doesn't make a difference
+    //assert (X == Y);		// fails
+}
+
 // Arrays
 T[] parse(T : T[]) (char[] src) {
     src = Util.trim(src);
     if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return toArray!(T[]) (src);
-    throwException ("Invalid array: not [., ..., .]");
+    throwException ("Invalid array: not [x, ..., z]");
 }
 T parse(T : char[]) (char[] src) {
     src = Util.trim(src);
@@ -58,14 +108,14 @@
             // process a block of escaped characters
             while (t < src.length && src[t] == '\\') {
                 t++;
-                if (t == src.length) throwException (`Warning: \" in string! There's currently no support for this during tokenising. Thus your input's probably been garbled!`);	// next char is "
+                if (t == src.length) throwException (`Warning: string ends \" !`);	// next char is "
                 ret[i++] = replaceEscapedChar (src[t++]);	// throws if it's invalid
             }
         }
         return ret[0..i];
     }
     else if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return toArray!(T) (src);
-    throwException ("Invalid string: not quoted (\"*\") or char array (['.',...,'.'])");
+    throwException ("Invalid string: not quoted (\"*\") or char array (['a',...,'c'])");
 }
 T parse(T : ubyte[]) (char[] src) {
     src = Util.trim(src);
@@ -82,20 +132,32 @@
     }
     return ret;
 }
+unittest {
+    assert (parse!(double[]) (`[1.0,1.0e-10]`) == [1.0, 1.0e-10]);	// generic array stuff
+    assert (parse!(double[]) (`[	]`) == cast(double[]) []);	// empty array
+    
+    // char[] and char conversions, with commas, escape sequences and multichar UTF8 characters:
+    assert (parse!(char[][]) (`[ ".\"", [',','\''] ,"!\b€" ]`) == [ ".\"".dup, [',','\''] ,"!\b€" ]);
+    
+    assert (parse!(ubyte[]) (`01F2AC`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]);	// ubyte[] special notation
+    assert (parse!(ubyte[]) (`[01 ,0xF2, 0xAC]`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]);	// ubyte[] std notation
+}
 
 T parse(T : char) (char[] src) {
     src = Util.trim(src);
     if (src.length < 3 || src[0] != '\'' || src[$-1] != '\'')
-        throwException ("Invalid char: not quoted (\'*\')");
+        throwException ("Invalid char: not quoted ('c')");
     if (src[1] != '\\' && src.length == 3) return src[1];	// Either non escaped
     if (src.length == 4) return replaceEscapedChar (src[2]);	// Or escaped
     
     // Report various errors; warnings for likely and difficult to tell cases:
-    if (src[1] == '\\' && src.length == 3) throwException (`Warning: \' in char! There's currently no support for this during tokenising. Thus your input's probably been garbled!`);	// next char is "
+    /+ This was caused by a bug. Shouldn't occur now normally.
+    if (src[1] == '\\' && src.length == 3) throwException (`Warning: \' in char! There's currently no support for this during tokenising. Thus your input's probably been garbled!`);	// next char is ' +/
     // Warn in case it's a multibyte UTF-8 character:
     if (src[1] & 0xC0u) throwException ("Invalid char: too long (non-ASCII UTF-8 characters cannot be read as a single character)");
     throwException ("Invalid char: too long");
 }
+// unittest covered above
 
 T parse(T : bool) (char[] src) {
     src = Util.trim(src);
@@ -107,6 +169,9 @@
     if (src.length == pos + 1 && src[pos] == '1') return true;
     throwException ("Invalid bool: not true or false and doesn't evaluate to 0 or 1");
 }
+unittest {
+    assert (parse!(bool[]) (`[true,false,01,00]`) == cast(bool[]) [1,0,1,0]);
+}
 
 T parse(T : byte) (char[] src) {
     return toTInt!(T) (src);
@@ -132,6 +197,11 @@
 T parse(T : ulong) (char[] src) {
     return toTInt!(T) (src);
 }
+unittest {
+    assert (parse!(byte) ("-5") == cast(byte) -5);
+    // annoyingly, octal syntax differs from D (blame tango):
+    assert (parse!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]);
+}
 
 T parse(T : float) (char[] src) {
     return toTFloat!(T) (src);
@@ -142,6 +212,11 @@
 T parse(T : real) (char[] src) {
     return toTFloat!(T) (src);
 }
+unittest {
+    assert (parse!(float) ("0.0") == 0.0f);
+    assert (parse!(double) ("-1e25") == -1e25);
+    assert (parse!(real) ("5.24e-269") == cast(real) 5.24e-269);
+}
 //END parse templates
 
 //BEGIN Utility funcs
@@ -155,6 +230,7 @@
     uint radix, ate, ate2;
     
     ate = cInt.trim (src, sign, radix);
+    if (ate == src.length) throwException ("Invalid integer: no digits");
     ulong val = cInt.convert (src[ate..$], radix, &ate2);
     ate += ate2;
     
@@ -174,17 +250,54 @@
 
 /** Basically a reimplementation of tango.text.convert.Float.toFloat which checks for trailing
  * whitespace before throwing an exception for overlong input and throws my exception class
- * when it does.
- */
+ * when it does. */
 TFloat toTFloat(TFloat) (char[] src) {
+    src = postTrim (src);
+    if (src == "") throwException ("Invalid float: no digits");
     uint ate;
 
     TFloat x = cFloat.parse (src, &ate);
-    while (ate < src.length) {
-        if (src[ate] == ' ' || src[ate] == '\t') ++ate;
-        else throwException ("Invalid number");
+    return x;
+}
+
+/** Splits a string into substrings separated by '$(B ,)' with support for characters and strings
+ * containing escape sequences and for embedded arrays ($(B [...])).
+ *
+ * Empty strings may get returned. */
+char[][] split (char[] src) {
+    src = Util.trim (src);
+    if (src == "") return [];		// empty array: no elements when no data
+    
+    uint depth = 0;			// surface depth (embedded arrays)
+    char[][] ret;
+    ret.length = src.length / 3;	// unlikely to need a longer array
+    uint k = 0;				// current split piece
+    uint i = 0, j = 0;			// current read location, start of current piece
+    
+    while (i < src.length) {
+        char c = src[i];
+        if (c == '\'' || c == '"') {	// string or character
+            ++i;
+            while (i < src.length && src[i] != c) {
+                if (src[i] == '\\') ++i;	// escape seq.
+                ++i;
+            }	// Doesn't throw if no terminal quote at end of src, but this should be caught later.
+        }
+        else if (c == '[') ++depth;
+        else if (c == ']') {
+            if (depth) --depth;
+            else throwException ("Invalid array literal: closes before end of data item.");
+        }
+        else if (c == ',' && depth == 0) {		// only if not an embedded array
+            if (ret.length <= k) ret.length = ret.length * 2;
+            ret[k++] = src[j..i];	// add this piece and increment k
+            j = i + 1;
+        }
+        ++i;
     }
-    return x;
+    if (ret.length <= k) ret.length = k + 1;
+    ret[k] = src[j..i];		// add final piece (i >= j)
+    return ret[0..k+1];
 }
 
 /* Throws an exception on invalid escape sequences. Supported escape sequences are the following
@@ -196,17 +309,12 @@
     static bool escCharsFilled;	// will be initialised false
     
     if (!escCharsFilled) {
-        // map of all supported escape sequences
-        escChars['"'] = '"';
-        escChars['\''] = '\'';
-        escChars['\\'] = '\\';
-        escChars['a'] = '\a';
-        escChars['b'] = '\b';
-        escChars['f'] = '\f';
-        escChars['n'] = '\n';
-        escChars['r'] = '\r';
-        escChars['t'] = '\t';
-        escChars['v'] = '\v';
+        // map of all supported escape sequences (cannot be static?)
+        escChars = ['"'  : '"', '\'' : '\'',
+                    '\\' : '\\', 'a' : '\a',
+                    'b'  : '\b', 'f' : '\f',
+                    'n'  : '\n', 'r' : '\r',
+                    't'  : '\t', 'v' : '\v'];
         escCharsFilled = true;
     }
     
@@ -228,10 +336,11 @@
 }
 
 // Generic array reader
+// Assumes input is of form "[xxxxx]" (i.e. first and last chars are '[', ']' and length >= 2).
 private T[] toArray(T : T[]) (char[] src) {
     T[] ret = new T[16];	// avoid unnecessary allocations
     uint i = 0;
-    foreach (char[] element; Util.quotes (src[1..$-1],",")) {
+    foreach (char[] element; split(src[1..$-1])) {
         if (i == ret.length) ret.length = ret.length * 2;
         ret[i] = parse!(T) (element);
         ++i;
@@ -243,4 +352,8 @@
     logger.warn (msg);			// only small errors are trapped here
     throw new TextParseException ();
 }
+
+unittest {
+    // all utility functions should be well-enough used not to need testing
+}
 //END Utility funcs
--- a/mde/text/util.d	Sat Nov 03 16:06:06 2007 +0000
+++ b/mde/text/util.d	Sun Jan 06 17:38:51 2008 +0000
@@ -1,14 +1,23 @@
-/*******************************************************
+/***************************************************************
  * A collection of text utility functions.
  *
- ******************************************************/
+ * Authors: Diggory Hardy, diggory.hardy@gmail.com
+ * Copyright: Copyright © 2007 Diggory Hardy.
+ * License: Licensed under the Academic Free License version 3.0
+ **************************************************************/
 module mde.text.util;
 
+/** Trim space and tab chars from the end of a string.
+ *
+ * No static arrays OK?? */
 T postTrim(T) (T str) {
     uint i = str.length;
     for (uint j; i > 0; i = j) {
         j = i - 1;
-        if (str[j] != ' ' || str[j] != '\t') break;
+        if (str[j] != ' ' && str[j] != '\t') break;
     }
     return str[0..i];
 }
+unittest {
+    assert (postTrim(" 	a6 05 	   ".dup) == " 	a6 05".dup);
+}
--- a/test.mtt	Sat Nov 03 16:06:06 2007 +0000
+++ b/test.mtt	Sun Jan 06 17:38:51 2008 +0000
@@ -12,9 +12,9 @@
 <ubyte[]|2= [2,0,250,5]>
 <binary|3=00102030405060708090A0B0C0D0E0F0>
 {2}!{Chars and strings}
-<char|1= ' '>
+<char|1= '.'>
 <char | 2 ='\a'>
 <string|1= "A	sequence of\tcharacters:\v1²€ç⋅−+↙↔↘,↕">
-<string|2= [ 's','t' ,'r'	,	'i' , 'n' , 'g', ' ' , '2' ]>
+<string|2= [ '"', 's','t' ,'r'	,	'i' , 'n' , 'g', ' ' , '2', '\'' ]>
 {11}
 <int|5=6>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/MTTest.d	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,37 @@
+/// Eventually translate to mergetag/unittest.d
+module MTTest;
+
+import mde.mergetag.read;
+import mde.mergetag.write;
+import mde.test;
+
+import tango.io.Stdout;
+import tango.util.log.Log : Log;
+import tango.util.log.ConsoleAppender : ConsoleAppender;
+
+static this()
+{
+    Log.getRootLogger().addAppender(new ConsoleAppender);
+}
+
+void main() {
+    Reader reader;
+    try {
+        reader = new Reader("test.mtt");
+        reader.read();
+    }
+    catch (MTException e) {
+        Stdout ("Read exception: ")(e.msg).newline;
+        return;
+    }
+    
+    try {
+        IWriter writer = makeWriter ("testw.mtt", reader.dataset);
+        writer.write();
+    }
+    catch (MTException e) {
+        Stdout ("Write exception: ")(e.msg).newline;
+    }
+    
+    printDataSet (reader.dataset);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/testw.mtt	Sun Jan 06 17:38:51 2008 +0000
@@ -0,0 +1,11 @@
+{MT01}
+{1}
+
+{2}
+<char[]|1=A\tsequence of\tcharacters:\v1²€ç⋅−+↙↔↘,↕">
+<char[]|2=\"string 2\'">
+
+{11}
+
+{13}
+