changeset 10:4c3575400769

DefaultData largely rewritten with unittest, SDL input event handling completed with unittest, changes to Init threading. Init threads now catch own exceptions. Doc: assigned some inputID devisions. Added support for remaining SDL events. Input axes' output is now stored with a short instead of a real. Input unittest written (for SDL event handling). Rewrote most of mde.mergetag.defaultdata using generic programming to generate read & write rules for all types. As a direct result, defaultdata can now write properly. DefaultData unittest written (also provides testing for mergetag read/write). Moved mde.text.parse/format to tango.scrapple.text.convert.parseTo/parseFrom with many changes. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Mon, 18 Feb 2008 11:54:56 +0000
parents 1885a9080f2a
children b940f267419e
files conf/input.mtt doc/changelog doc/input_ID_assignments doc/jobs doc/policies.txt dsss.conf mde/events.d mde/exception.d mde/global.d mde/init.d mde/input/config.d mde/input/exception.d mde/input/input.d mde/input/joystick.d mde/mde.d mde/mergetag/dataset.d mde/mergetag/defaultdata.d mde/mergetag/doc/issues.txt mde/mergetag/exception.d mde/mergetag/mtunittest.d mde/mergetag/read.d mde/mergetag/write.d mde/test.d mde/text/exception.d mde/text/format.d mde/text/parse.d mde/text/util.d test/mdeTest.d test/util.d
diffstat 29 files changed, 1033 insertions(+), 1111 deletions(-) [+]
line wrap: on
line diff
--- a/conf/input.mtt	Wed Jan 30 11:33:56 2008 +0000
+++ b/conf/input.mtt	Mon Feb 18 11:54:56 2008 +0000
@@ -1,4 +1,16 @@
 {MT01}
 <char[][]|Configs=["Std"]>
+<char[]|Test="1">
 {Default}
-<uint[][uint]|B=[0x20000000 : [0x100, 3], 0x20000001 : [0x100, 5] ]>
+<uint[][uint]|B=[ 0x8800001B : [[0x1000, 0x0]] ]>
+{UnitTest}
+<uint[][uint]|B=[
+	0x88000124 : [[0x1000, 0x00F0]],
+	0x40000003 : [[0x1000, 0x01F0]],
+	0x20002005 : [ [0x1000,0x02F0],[0x1000,0x03F0] ],
+	0x110D600C : [[0x1000,0x04F0]],
+	0x120D600C : [[0x1000,0x05F0]],
+	0x140D600C : [[0x1000,0x06F0]],
+	0x180D600C : [[0x1000,0x07F0]] ]>
+<uint[][uint]|A=[0x80001008 : [[0x1000,0x20F0]],0x130D600C : [[0x1000,0x21F0]] , 0x1C0D600C : [[0x1000,0x22F0]]]>
+<uint[][uint]|M=[ 0x88000000 : [[0x1000, 0x10F0], [0x1000, 0x11F0]], 0x40016064:[[0x1000,0x12F0]]]>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changelog	Mon Feb 18 11:54:56 2008 +0000
@@ -0,0 +1,2 @@
+See git log (run "git log" from base dir).
+Also you could look at the "jobs" file.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/input_ID_assignments	Mon Feb 18 11:54:56 2008 +0000
@@ -0,0 +1,65 @@
+Event input status and callbacks are differentiated via IDs of type Input.inputID (currently uint).
+
+Multi-user support requiers some way to make these specific to particular users; this is intended to be implemented via each user having their own Input class object; user-common callbacks are added by each user's class, thus enabling these to have extra user-specific functionality added later. Thus inputIDs don't need to contain a user ID.
+
+4 bytes are available; of this the lowest 4 bits specifies which part of the engine uses these IDs, the rest is available for the engine part to use as it wants. The reason the lowest 4 bits are used for the engine part is so that if the inputID type is changed to a different sized integer, these 4 bits remain at the end of the block (i.e. the IDs available for the subsystem are still in one block).
+
+Bit:	31		   4  3		0
+	[ engine subsystem ]  [subsystem]
+
+
+Subsystem values:
+0	Direct engine commands:
+		Main commands handled by mde.mde (may be moved), e.g. direct quit
+1	GUI/User interface:
+		Interface commands, including quit dialog box
+2	Physics system:
+		Direct control of thrusters, etc (control also possible via scripts)
+3	Scripting system:
+		Indirect control of thrusters, etc. from physics objects
+		Other game-world interaction & scripting uses
+4-F	Unassigned
+
+
+This is not enforced by the engine (except perhaps for scripts?), but simply followed.
+
+
+
+Subsystem 0:
+xx_xx_xx_00	Quitting:
+00_00_00_00		End main loop normally
+xx_xx_xx_E0	Debug dumps/commands
+xx_xx_xx_F0	Test events
+
+
+
+
+For stream functions, codes are used as follows. Only the last (lowest) two bytes are used; of this the highest 4 bits categorise the rough type of the stream function, and the next 4 bits subdivide this. The lowest byte has no meaning but is just a number.
+
+Categorisation:
+
+1*_**	Output as event
+2*_**	Adjustments, keeping same output type
+3*_**	Adjustments with a different output type
+F*_**	Debug types
+    *
+2X_**	Adjustments:
+	OR'd flags for X:
+	1	uses timers
+	2	uses other events from same button/axis/etc.
+	4	uses other events from different button/axis/etc.
+	8	no output, only sets values for use by other adjusters
+    *
+3X_**	Adjustments with different output types (e.g. axis -> button):
+	OR'd flags as for 2X_**
+
+F*_**	Debug outputs:
+	F0_00	No output, doesn't do anything
+	F1_**	Logs output, passing event on
+
+Actual codes, with config values used:
+
+1000	Standard output functions
+		Use 1 config value for output ID
+2000	Reverse value (axes only)
+		No config values
--- a/doc/jobs	Wed Jan 30 11:33:56 2008 +0000
+++ b/doc/jobs	Mon Feb 18 11:54:56 2008 +0000
@@ -1,9 +1,44 @@
 In progress:
-*	Why doesn't input.config filtering via headers "Configs" work?
 
 To do:
-*	change init threads (should catch own exceptions)
-*	finish event callback support
-*	add remaining SDL event support
+*	Write Input unittests; remove untested notes
+*	Why doesn't input.config filtering via headers "Configs" work?
 *	add options support; in particular for whether or not to use threads (and adjust Init to use this).
 *	OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
+*	Sensitivity adjustments. From es_a_out:
+        /+ FIXME: revise.
+        + I can't see any point using HALF_RANGE here, since it should really be used dependant on
+        + the device attached, not the axis. Also what about adjusted range like X3's throttle?
+        +
+        + Sensitivity: is this the right place to adjust it? For things like throttle where the
+        + ends of the interval must remain fixed, multiplying cannot be used to adjust and adjusting
+        + the curve via a power function doesn't seem to be what we want. For things where the
+        + end points needn't remain fixed, multiplying seems the right thing to do, but cannot be
+        + done here since we don't know the end points can be changed.
+        
+        real y = x;
+        uint conf = s.pop();
+        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.pop();
+        /+ 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 +/
+        myThis.axis[cast(inputID) s.pop()] = y;
+        +/
+
+Done (for git log message):
+*	Init threads now catch own exceptions.
+*	Assigned some inputID devisions.
+*	add remaining SDL event support
+*	Rewrote most of mde.mergetag.defaultdata using generic programming to generate read & write rules for all types. As a result, defaultdata can now write properly.
+*	Axis output now stored with short instead of real.
+*	Input unittest
+*       Moved mde/text to scrapple
+*	DefaultData unittest. Then commit.
--- a/doc/policies.txt	Wed Jan 30 11:33:56 2008 +0000
+++ b/doc/policies.txt	Mon Feb 18 11:54:56 2008 +0000
@@ -2,8 +2,11 @@
 
 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).
 
+A warning: I have several times changed my mind about items I've written here. So don't expect all existing code to conform, and if you feel that a principle listed is not a good idea don't think you have to conform to it.
 
-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.
+
+
+Coding conventions: Mostly stick to those provided in the D specification. Generally indent with four spaces and use tabs to align comments. Keep editor's tab-width at 8. 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.
 
 Identifiers: as for the D spec, use descriptive words for identifiers, although try to keep them from being overlong. Use capital letters to show separate words, not _s. E.g.: file, readFile; not: rdFile, read_file, readFileUsingMyMethodNow. Again, this is only really a guideline.
 
@@ -12,6 +15,7 @@
 Spelling: Use british or american (or other) spellings as you like, but BE CONSISTANT at least for code symbols within packages (i.e. if you write your own package you choose the spelling, and for comments it doesn't matter, unless it's actually refering to a symbol). 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.
 
 
@@ -21,6 +25,7 @@
 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, 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:
@@ -40,7 +45,16 @@
 	• warn messages may occur, and may or may not indicate problems;
 	• error messages indicate that something big is wrong, and if the program still runs it is unlikely to be usable as intended;
 	• fatal messages indicate a problem preventing the program from running.
-	
 
 
 Thrown errors should use an exception class specific to at least the package involved to enable specific catching of errors. Exception classes should be defined within a module exception.d in the package directory. Exception classes should generally follow the conventions within mde/exception.d to aid in providing reasonable error messages.
+
+
+
+Unittests (last block in the module where multiple unittest blocks are used) should end with:
+    logger.info ("Unittest complete.");
+No more logging should be needed, since if it fails whoever runs the unittest will know about it, and logging messages cannot be used to tell how complete the unittesting is. These messages just confirm that the unittests ran really.
+
+Unittests may be defined in their own modules or other modules. Unittests should be wrapped in
+    debug(mdeUnitTest)
+statements (these may also be used to wrap imports only needed by the unittest). Any modules containing unittests should be imported by test.mdeTest.
--- a/dsss.conf	Wed Jan 30 11:33:56 2008 +0000
+++ b/dsss.conf	Mon Feb 18 11:54:56 2008 +0000
@@ -1,8 +1,26 @@
+defaulttargets = mde/mde.d
+
+[*]
+version (Posix) {
+    buildflags=-L-ldl
+} else {
+    warn Only posix builds have been tested; elsewhere other libraries will probably need to be linked.
+}
+
 [mde/mde.d]
-buildflags=-L-ldl
-target=bin/mde
-#[mde/mergetag]
-#[mde/input]
-#[test/MTTest.d]
-#target=bin/MTTest
+version (Posix) {
+    target=bin/mde
+} else version (Windows) {
+    target=bin/mde.exe
+}
 
+[test/mdeTest.d]
+version (Posix) {
+    buildflags=-L-ldl -debug=mdeUnitTest -unittest
+    target=bin/mdeTest
+} else version (Windows) {
+    warn Extra linking probably needed!
+    buildflags=-debug=mdeUnitTest -unittest
+    target=bin/mdeTest.exe
+}
+noinstall
\ No newline at end of file
--- a/mde/events.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/events.d	Mon Feb 18 11:54:56 2008 +0000
@@ -2,8 +2,10 @@
 module mde.events;
 
 import mde.scheduler;
+import global = mde.global;
 
 import mde.input.input;
+import mde.input.exception;
 
 import derelict.sdl.events;
 
@@ -14,8 +16,6 @@
     logger = Log.getLogger ("mde.events");
 }
 
-static bool run = true;
-
 void addEventsSchedule () {
     Scheduler.perFrame (&pollEvents);
 }
@@ -26,10 +26,15 @@
         switch (event.type) {
             case SDL_QUIT:
                 logger.info ("Quit requested");
-                run = false;
+                global.run = false;
                 break;
             default:
-                input (event);
+                try {
+                    global.input (event);
+                } catch (InputClassException e) {
+                    logger.warn ("Caught input exception; event will be ignored. Exception was:");
+                    logger.warn (e.msg);
+                }
         }
     }
 }
--- a/mde/exception.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/exception.d	Mon Feb 18 11:54:56 2008 +0000
@@ -4,15 +4,21 @@
 /** Base class for all mde Exceptions.
  *
  * All packages should have their own base exception type extending this one, and for each package
- * level a CTOR taking a message should pass the message to the super. The const string symbol
+ * level a CTOR taking a message should pass the message to the super.
+ * A CTOR not taking a message and calling the super without a parameter may also be provided.
+ *
+ * The static string symbol
  * should be overriden as below so that it ends up as something like "mde.file" or
- * "mde.pkg.file.Class" describing where the exception was thrown.
- * A CTOR not taking a message and calling the super without a parameter may also be provided.
+ * "mde.pkg.file.Class" describing where the exception was thrown. (Since only methods overload
+ * correctly, symbol is made static and an overloadable method is used to access the correct symbol.)
  */
 class mdeException : Exception {
-    const symbol = "mde";	/// Override in derived classes to name the module where the error occured.
+    static const symbol = "mde";	/// Override in derived classes to name the module where the error occured.
+    char[] getSymbol () {
+        return symbol;
+    }
     this (char[] msg) {
-        super(symbol ~ ": " ~ msg);
+        super(getSymbol() ~ ": " ~ msg);
     }
     this () {			// No supplied error message.
         super(symbol);
@@ -20,8 +26,37 @@
 }
 
 class initException : mdeException {
-    const override symbol = super.symbol ~ ".init";
+    // NOTE: if symbol is final, it can't be modified in the static this(), but as const it can
+    static const char[] symbol;
+    static this () {	symbol = super.symbol ~ ".init";	}
+    char[] getSymbol () {
+        return symbol;
+    }
+    
     this (char[] msg) {
         super(msg);
     }
 }
+
+debug (mdeUnitTest) {
+    import tango.util.log.Log : Log, Logger;
+
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.events");
+    }
+    
+    unittest {
+        // Check message prepending works correctly.
+        mdeException mE = new initException("");
+        assert (mE.getSymbol() == "mde.init", mE.getSymbol());
+        try {
+            throw new initException ("ABC");
+            assert (false);
+        } catch (Exception e) {
+            assert (e.msg == "mde.init: ABC", e.msg);
+        }
+    
+        logger.info ("Unittest complete.");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/global.d	Mon Feb 18 11:54:56 2008 +0000
@@ -0,0 +1,15 @@
+/** This module is for global objects.
+*
+* It's existance may only be temporary until objects are used in other ways; use of global objects
+* is generally disliked.
+*
+* I suggest importing via a rename or static import, so on use it's obvious where the objects used
+* are declared.
+*/
+module mde.global;
+
+import mde.input.input;
+
+bool run = true;	// mail loop continues if this is true
+
+Input input;		// Input instance. When multiple users are allowed instances will be per-user.
--- a/mde/init.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/init.d	Mon Feb 18 11:54:56 2008 +0000
@@ -5,14 +5,16 @@
  *************************************************************************************************/
 module mde.init;
 
-public import mde.exception;
+import mde.exception;
 
 import mde.events;
+import global = mde.global;
 import mde.input.input;
 import mde.input.joystick;
 
 // tango imports
 import tango.core.Thread;
+import tango.core.Exception;
 import tango.util.log.Log : Log, Logger;
 import tango.util.log.ConsoleAppender : ConsoleAppender;
 import tango.stdc.stringz : fromUtf8z;
@@ -20,6 +22,8 @@
 import derelict.sdl.sdl;
 import derelict.util.exception;
 
+private Logger logger;
+
 /**
  * Static CTOR
  *
@@ -28,10 +32,15 @@
  */
 static this()
 {
-    // For now, just log to the console:
-    Logger root = Log.getRootLogger();
-    root.setLevel(root.Level.Trace);
-    root.addAppender(new ConsoleAppender);
+    debug(mdeUnitTest) {}   // For unittests, the logger is set up by test.mdeTest
+    else {
+        // For now, just log to the console:
+        Logger root = Log.getRootLogger();
+        root.setLevel(root.Level.Trace);
+        root.addAppender(new ConsoleAppender);
+    }
+    
+    logger = Log.getLogger ("mde.init");
 }
 static ~this()
 {
@@ -70,14 +79,14 @@
         * be run simultaneously in separate threads. */
         
         bool initFailure = false;	// Set true if something goes wrong and we need to abort.
-        void setFailure () {		// For synchronization, although may be unnecessary
+        void setFailure () {		// For synchronization, although shouldn't be necessary
             synchronized initFailure = true;
         }
         void delegate() [] initFuncs = [
         delegate void() {
+            logger = Log.getLogger ("mde.init.Init.SDL");
             // Inits SDL and related stuff (joystick).
             try {
-                // SDL Joystick, used by mde.input
                 DerelictSDL.load();
             } catch (DerelictException de) {
                 logger.fatal ("Loading dynamic library failed:");
@@ -111,7 +120,7 @@
         ThreadGroup tg = new ThreadGroup;	// can't fail since it does nothing (tango 0.99.4) (unless out of memory − anyway should be safe to throw at this point)
         try {	// creating/starting threads can fail
             foreach (func; initFuncs) tg.create(func);
-        } catch (Exception e) {			// should be a ThreadException, but catch all Exceptions
+        } catch (ThreadException e) {		// to do with threading; try without threads
             logger.warn ("Caught exception while trying to create threads:");
             logger.warn (e.msg);
             logger.warn ("Will continue in a non-threaded manner.");
@@ -120,7 +129,8 @@
         }
         
         // Do some initialisation in the main thread
-        input.loadConfig ();		// (may also create instance)
+        global.input = new Input();
+        global.input.loadConfig ();		// (may also create instance)
         addEventsSchedule ();
         
         // Wait for all threads to complete.
@@ -180,7 +190,7 @@
     }
 }
 
-unittest {
+debug (mdeUnitTest) 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. */
@@ -196,4 +206,6 @@
     assert (initialised);
     Init.runCleanupFcts();
     assert (!initialised);
+
+    logger.info ("Unittest complete.");
 }
--- a/mde/input/config.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/input/config.d	Mon Feb 18 11:54:56 2008 +0000
@@ -1,12 +1,12 @@
 /// This module contains a class for holding configs and handles saving, loading and editing.
 module mde.input.config;
 
-debug import mde.text.format;
+debug import tango.scrapple.text.convert.parseFrom : parseFrom;
 
-public import mde.input.exception;
+import mde.input.exception;
 
 import MT = mde.mergetag.read;
-import mde.text.parse;
+import tango.scrapple.text.convert.parseTo : parseTo;
 
 import tango.util.log.Log : Log, Logger;
 import tango.util.collection.TreeBag : TreeBag;
@@ -28,30 +28,38 @@
      *
      *  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 _B.SDLKEY) | x in button. Keyboard events are SDL-specific since the codes may differ for other
-     *  libraries.
+     *  $(_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
-     *  U,L,D,R positions and up and down events sent to the appropriate outputs.
+     *  For joystick hat events, a motion is converted into up and down events on separate U,L,D,R
+     *  positions and up and down events sent to the appropriate outputs (effectively making four
+     *  buttons, some pairs of which can be pressed simultaneously). For output to axes, see
+     *  A.JOYHAT_* .
      */
     enum B : uint {
-        KEY		= 0x8000_0000u,		/// 0x8000_0000u
-        SDLKEY		= 0x8800_0000u,		/// 0x8800_0000u
-        MOUSE		= 0x4000_0000u,		/// 0x4000_0000u
-        JOYBUTTON	= 0x2000_0000u,		/// 0x2000_0000u
-        JOYHAT		= 0x1000_0000u,		/// 0x1000_0000u
-        JOYHAT_U	= 0x1800_0000u,		/// 0x1800_0000u
-        JOYHAT_D	= 0x1400_0000u,		/// 0x1400_0000u
-        JOYHAT_L	= 0x1200_0000u,		/// 0x1200_0000u
-        JOYHAT_R	= 0x1100_0000u,		/// 0x1100_0000u
+        KEY		= 0x8000_0000u,		/// 0x8000_0000
+        SDLKEY		= 0x8800_0000u,		/// 0x8800_0000
+        MOUSE		= 0x4000_0000u,		/// 0x4000_0000
+        JOYBUTTON	= 0x2000_0000u,		/// 0x2000_0000
+        JOYHAT		= 0x1000_0000u,		/// 0x1000_0000
+        JOYHAT_U	= 0x1800_0000u,		/// 0x1800_0000
+        JOYHAT_D	= 0x1400_0000u,		/// 0x1400_0000
+        JOYHAT_L	= 0x1200_0000u,		/// 0x1200_0000
+        JOYHAT_R	= 0x1100_0000u,		/// 0x1100_0000
     }
     
     /** Axis event type bit-codes
      *
-     *  Well, SDL only supports one type of axis now, but this could be extended in the future.
+     *  SDL only supports one type of axis now, but this could be extended in the future.
+     *
+     *  This can also be used to make joystick hats output to two axes (and can be used in
+     *  conjuction with B.JOYHAT_* to output as buttons as well).
     */
     enum A : uint {
-        JOYAXIS		= 0x8000_0000u,		/// 0x8000_0000u
+        JOYAXIS		= 0x8000_0000u,		/// 0x8000_0000
+        JOYHAT		= 0x1000_0000u,		/// 0x1000_0000
+        JOYHAT_LR	= 0x1300_0000u,		/// 0x1300_0000
+        JOYHAT_UD	= 0x1C00_0000u,		/// 0x1C00_0000
     }
     
     /** Mouse & Joystick ball event type bit-codes
@@ -59,29 +67,40 @@
      *  Currently, mouse input only comes from the window manager: the code is exactly M.WMMOUSE.
      */
     enum M : uint {
-        MOUSE		= 0x8000_0000u,		/// 0x8000_0000u
-        WMMOUSE		= 0x8800_0000u,		/// 0x8800_0000u
-        JOYBALL		= 0x4000_0000u,		/// 0x4000_0000u
+        MOUSE		= 0x8000_0000u,		/// 0x8000_0000
+        WMMOUSE		= 0x8800_0000u,		/// 0x8800_0000
+        JOYBALL		= 0x4000_0000u,		/// 0x4000_0000
     }
     
     /** Output queues: the core of the input configuration.
     *
-    *  button, axis and mouse each have their own index specifications. This is split into two parts:
+    *  These are all mapped with a uint key; upon any event outQueues are looked for in the
+    *  appropriate associative array with a key as follows.
+    *
+    *  The index 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.
     *
-    *  For B.SDLKEY, the last three bytes are for the SDL keysym.
+    *  For type B.SDLKEY, the last three bytes are for the SDL keysym.
     *  For B.MOUSE, B.JOY*, A.JOY* & M.JOY*, the last three bytes are split into two sets of 12
     *  bits (with masks 0x00FF_F000 and 0x0000_0FFF), the higher of which specifies the device
     *  (which mouse or joystick), and the lower of which specifies the button/axis/ball.
     *
-    *  The code for mouse motion is currently only M.WMMOUSE. If/when multiple mice are supported
-    *  new codes will be defined.
+    *  For all three types of output, the outQueues are used as follows. The first value in the queue is
+    *  read, and a function is called with the event details dependant on this; most of the time an
+    *  output function is called directly. Other functions may be used, however, to allow further
+    *  functionality such as modifier keys, timed keys, and key sequences.
+    *
+    *  The output functions all have code 0x100; these read a single item from the outQueue which
+    *  is the inputID the event outputs to (i.e. any callbacks at that ID called and status set for
+    *  that ID).
+    *
     */
-    outQueue[uint] button;
-    outQueue[uint] axis;	/// ditto
-    outQueue[uint] mouse;	/// ditto
+    outQueue[][uint] button;
+    outQueue[][uint] axis;	/// ditto
+    outQueue[][uint] relMotion;	/// ditto
     
+    // FIXME:
     char[] name;		/// Name for user to save this under.
     uint[] inheritants;		/// Other profiles to inherit.
     
@@ -111,7 +130,9 @@
             const MT.ID CONFIGS = cast(MT.ID)"Configs";
             MT.ID[] file_configs;	// active config sections (may not exist)
             MT.ID[]* file_configs_p = cast(MT.ID[]*) (CONFIGS in file.dataset.header._charAA);
-            
+            debug foreach (i,d; file.dataset.header._charAA) logger.trace ("ID: "~cast(char[])i);
+            debug foreach (i,d; file.dataset.header._charA) logger.trace ("ID: "~cast(char[])i);
+                        
             if (file_configs_p)	file.read(*file_configs_p);	// restrict to this set IF a restriction was given
             else		file.read();			// otherwise read all
         }
@@ -132,7 +153,10 @@
             char tmp[128] = void;
             logger.trace (logger.format (tmp, "Loaded {} config sections.", configs.length));
             foreach (id, cfg; configs) {
-                logger.trace ("Section "~id~": " ~ format!(uint[][uint])(cfg.button));
+                logger.trace ("Section " ~ id ~
+                ":\n\tbutton:\t\t" ~ parseFrom!(uint[][][uint])(cfg.button) ~
+                "\n\taxis:\t\t" ~ parseFrom!(uint[][][uint])(cfg.axis) ~
+                "\n\trelMotion:\t" ~ parseFrom!(uint[][][uint])(cfg.relMotion) );
             }
         }
     }
@@ -145,12 +169,12 @@
     
     void addTag (char[] tp, MT.ID id, char[] dt) {
         if (tp == "uint[][uint]") {
-            if (id == QUEUE.BUTTON) button = cast(outQueue[uint]) parse!(uint[][uint]) (dt);
-            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);
+            if (id == QUEUE.BUTTON) button = cast(outQueue[][uint]) parseTo!(uint[][][uint]) (dt);
+            else if (id == QUEUE.AXIS) axis = cast(outQueue[][uint]) parseTo!(uint[][][uint]) (dt);
+            else if (id == QUEUE.MOUSE) relMotion = cast(outQueue[][uint]) parseTo!(uint[][][uint]) (dt);
             else logger.warn ("Unexpected tag encountered with ID " ~ cast(char[])id);
         } // FIXME: add support for name and inheritants.
-        else throw new MT.MTUnknownTypeException ("Input Config: only uint[][uint] type supported");
+        else throw new MT.MTUnknownTypeException ("Input Config: only uint[][][uint] type supported");
     }
     void writeAll (ItemDelg) {
         // FIXME
--- a/mde/input/exception.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/input/exception.d	Mon Feb 18 11:54:56 2008 +0000
@@ -4,7 +4,12 @@
 
 /// Base Input exception class.
 class inputException : mdeException {
-    const override symbol = super.symbol ~ ".input";
+    static const char[] symbol;
+    static this () {	symbol = super.symbol ~ ".input";	}
+    char[] getSymbol () {
+        return symbol;
+    }
+    
     this (char[] msg) {
         super(msg);
     }
@@ -12,7 +17,12 @@
 }
 
 class InputClassException : inputException {
-    const override symbol = super.symbol ~ ".input.Input";
+    static const char[] symbol;
+    static this () {	symbol = super.symbol ~ ".input.Input";	}
+    char[] getSymbol () {
+        return symbol;
+    }
+    
     this (char[] msg) {
         super(msg);
     }
@@ -20,7 +30,12 @@
 }
 
 class ConfigLoadException : inputException {
-    const override symbol = super.symbol ~ ".config.Config";
+    static const char[] symbol;
+    static this () {	symbol = super.symbol ~ ".config.Config";	}
+    char[] getSymbol () {
+        return symbol;
+    }
+    
     this (char[] msg) {
         super(msg);
     }
--- a/mde/input/input.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/input/input.d	Mon Feb 18 11:54:56 2008 +0000
@@ -6,53 +6,52 @@
 
 // package imports
 import mde.input.config;
-public import mde.input.exception;
+import mde.input.exception;
 
 // sdl imports
 import derelict.sdl.events;
+import derelict.sdl.types;	// only SDL_PRESSED
+import derelict.sdl.joystick;	// SDL_HAT_*
 
 import tango.util.log.Log : Log, Logger;
 
-public Input input;			/// Global instance of input.
-static this() {
-    input = new Input();
-}
-
 /// Class encapsulating all input functionality.
 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;
+    alias void delegate(inputID, short)		AxisCallback;
+    alias void delegate(inputID, real,real)	RelMotionCallback;
+    alias void delegate(ushort, ushort, ubyte, bool)	MouseClickCallback;
 
     /** Get key status at this ID.
     *
     * Returns: value (true = down, false = up) or false if no value at this ID. */
     bool getButton (inputID id) {
-        assert (this is input);
         bool* retp = 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 = id in axis;
+    * Returns: value (short; range -32767 .. 32767) or 0 if no value at this ID. */
+    short getAxis (inputID id) {
+        short* retp = id in axis;
         if (retp) return *retp;
+        else return 0;
+    }
+    /** Get axis status at this ID.
+    *
+    * Returns: value (real; range roughly -1.0 .. 1.0) or 0 if no value at this ID. */
+    real getAxis1 (inputID id) {
+        short* retp = id in axis;
+        if (retp) return (*retp) * 3.0518509475997192e-05;
         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).
+            
+    /** Get the relative motion of the mouse or a joystick ball (since last frameReset() call).
     *
     * Future: Converts to a real via sensitivity settings (defaults may be set and overriden per item).
     *
@@ -61,12 +60,20 @@
     * 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 = id in axis_rel;
+    void getRelMotion (inputID id, out real x = 0.0, out real y = 0.0) {
+        RelPair* rp = id in relMotion;
         if (rp) {
             x = rp.x;	y = rp.y;
         }
     }
+    /** Get mouse pointer position in screen coordinates.
+    *
+    * Window managers only support one mouse, so there will only be one screen coordinate.
+    * Unlike nearly everything else, this is not configurable.
+    */
+    void getMouseScreenPos (out uint x, out uint y) {
+        x = mouse_x;	y = mouse_y;
+    }
     // /// Is this modifier on?
     //bool modifierStatus (inputID id);
 
@@ -75,56 +82,167 @@
     * Delegate receives event status.
     */
     void addButtonCallback (inputID id, ButtonCallback dg) {
-        buttonCallbacks[id] = dg;
+        buttonCallbacks[id] ~= dg;
     }
 
     /** Adds a callback delegate for axis events with this ID.
     *
-    * Delegate receives event status.
+    * Delegate receives event status (as per what getAxis returns).
     */
     void addAxisCallback (inputID id, AxisCallback dg) {
-        axisCallbacks[id] = 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
+    * Delegate receives event status. As the name suggests, this is relative motion not screen
+    * position, with sensitivity adjustments applied.
+    *
+    * (A separate callback for mouse screen 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;
+    void addRelMotionCallback (inputID id, RelMotionCallback dg) {
+        relMotionCallbacks[id] ~= dg;
+    }
+    
+    /** Adds a callback delegate for all mouse clicks & releases.
+    *
+    * Delegate recieves x,y screen position (at time of click/release), button index (1 for left,
+    * 2 for middle, 3 for right, 4/5 for wheel, etc.), and whether the button was pressed or
+    * released (true if pressed).
+    *
+    * The point of this over a standard button callback is firstly to avoid mouse configuration for
+    * the GUI, and secondly to give the pointer position at the time of the event, not the time the
+    * callback gets called.
+    */
+    void addMouseClickCallback (MouseClickCallback dg) {
+        mouseClickCallbacks ~= 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.
+    *
+    * May throw InputClassExceptions (on configuration errors). Catching the exception and continuing should
+    * be fine.
     */
     bool opCall (ref SDL_Event event) {
-        assert (this is input);
         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;
-            
-            /+
+            // Keyboard events:
             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;
+                outQueue[]* p = (Config.B.SDLKEY | event.key.keysym.sym) in config.button;
+                if (p) foreach (outQueue q; *p) {
+                    bEvent (this, event.key.state == SDL_PRESSED, readOutQueue(q));
+                }
+                break;
+            
+            // Mouse events:
+            case SDL_MOUSEBUTTONDOWN:
+            case SDL_MOUSEBUTTONUP:
+                // Mouse clicks:
+                foreach (dg; mouseClickCallbacks)
+                    dg (event.button.x, event.button.y, event.button.button, event.button.state == SDL_PRESSED);
+                
+                // Button events:
+                outQueue[]* p = (Config.B.MOUSE | event.button.button) in config.button;
+                if (p) foreach (outQueue q; *p) {
+                    bEvent (this, event.button.state == SDL_PRESSED, readOutQueue(q));
+                }
+                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));
-            +/
+                // Screen coordinates (set here since relMotion is also used for joystick balls):
+                mouse_x = event.motion.x;
+                mouse_y = event.motion.y;
+                
+                // Relative motion:
+                outQueue[]* p = (Config.M.WMMOUSE) in config.relMotion;
+                if (p) foreach (outQueue q; *p) {
+                    mEvent (this, event.motion.xrel, event.motion.yrel, readOutQueue(q));
+                }
+                break;
+            
+            // Joystick events:
+            case SDL_JOYBUTTONDOWN:
+            case SDL_JOYBUTTONUP:
+                outQueue[]* p = (Config.B.JOYBUTTON | (event.jbutton.which << 12) | event.jbutton.button) in config.button;
+                if (p) foreach (outQueue q; *p) {
+                    bEvent (this, event.jbutton.state == SDL_PRESSED, readOutQueue(q));
+                }
+                break;
+            
+            case SDL_JOYAXISMOTION:
+                outQueue[]* p = (Config.A.JOYAXIS | (event.jaxis.which << 12) | event.jaxis.axis) in config.axis;
+                if (p) foreach (outQueue q; *p) {
+                    aEvent (this, event.jaxis.value, readOutQueue(q));
+                }
+                break;
+            
+            case SDL_JOYBALLMOTION:	// NOTE: untested
+                outQueue[]* p = (Config.M.JOYBALL | (event.jball.which << 12) | event.jball.ball) in config.relMotion;
+                if (p) foreach (outQueue q; *p) {
+                    mEvent (this, event.jball.xrel, event.jball.yrel, readOutQueue(q));
+                }
+                break;
+            
+            case SDL_JOYHATMOTION:	// NOTE: untested
+                static ubyte[uint] oldJHatVals;		// necessary to store this to know which "axis" changed
+        
+                uint index = (event.jhat.which << 12) | event.jhat.hat;
+                ubyte* oVal_p = index in oldJHatVals;
+                ubyte oldJHatVal = (oVal_p) ? *oVal_p : SDL_HAT_CENTERED;
+                
+                // Carry out functionality for an axis.
+                void hatExamine (ubyte neg, ubyte pos, Config.B neg_b, Config.B pos_b, Config.A axis) {
+                    // Check if there's any change on this axis (if not, nothing to do):
+                    ubyte filter = neg | pos;
+                    if ((oldJHatVal & filter) != (event.jhat.value & filter)) {
+                        // Now we know this axis changed position, so can unset old value and set
+                        // new value (if not centre).
+                        
+                        // Cancel old button status:
+                        if (oldJHatVal & neg) {
+                            outQueue[]* p = (neg_b | index) in config.button;
+                            if (p) foreach (outQueue q; *p) bEvent (this, false, readOutQueue(q));
+                        } else if (oldJHatVal & pos) {
+                            outQueue[]* p = (pos_b | index) in config.button;
+                            if (p) foreach (outQueue q; *p) bEvent (this, false, readOutQueue(q));
+                        }
+                        
+                        // Set new button status and position:
+                        short position = 0;
+                        if (event.jhat.value & neg) {
+                            position = -32767;
+                            outQueue[]* p = (neg_b | index) in config.button;
+                            if (p) foreach (outQueue q; *p) bEvent (this, true, readOutQueue(q));
+                        } else if (event.jhat.value & pos) {
+                            position = 32767;
+                            outQueue[]* p = (pos_b | index) in config.button;
+                            if (p) foreach (outQueue q; *p) bEvent (this, true, readOutQueue(q));
+                        }
+                        
+                        // New axis event:
+                        outQueue[]* p = (axis | index) in config.axis;
+                        if (p) foreach (outQueue q; *p) aEvent (this, position, readOutQueue(q));
+                    }
+                }
+                
+                // Now run the code for each axis:
+                hatExamine (SDL_HAT_UP, SDL_HAT_DOWN,
+                            Config.B.JOYHAT_U, Config.B.JOYHAT_D, Config.A.JOYHAT_UD);
+                hatExamine (SDL_HAT_LEFT, SDL_HAT_RIGHT,
+                            Config.B.JOYHAT_L, Config.B.JOYHAT_R, Config.A.JOYHAT_LR);
+                
+                oldJHatVals[index] = event.jhat.value;
+                break;
+            
+            // Other events:
             default:
-                return false;
+                return false;	// event not used
         }
-        return true;
+        return true;		// event used
     }
     
     /** Resets relative movement of mice / joystick balls to zero.
@@ -133,7 +251,7 @@
     * been read (e.g. just before updating the input).
     */
     void frameReset () {
-        foreach (rp; axis_rel) {
+        foreach (rp; relMotion) {
             rp.x = rp.y = 0.0;
         }
     }
@@ -142,21 +260,23 @@
     *
     * Returns: true if the requested config id wasn't found.
     */
-    bool loadConfig () {
+    bool loadConfig (char[] profile = "Default") {
         Config.load("conf/input.mtt");	// FIXME: filename
-        Config* c_p = "Default" in Config.configs;
+        Config* c_p = profile in Config.configs;
         if (c_p) {
             config = *c_p;
             return false;
         }
-        debug logger.warn ("Config \"Default\" not found.");
+        debug logger.warn ("Config \""~profile~"\" not found.");
         return true;
     }
     
 private:
     // Static constructor for event stream (fills es_*_fcts tables).
     static this () {
-        es_b_fcts = [ ES_B_OUT : &es_b_out ];
+        es_b_fcts = [ ES_B.OUT : &es_b_out ];
+        es_a_fcts = [ ES_A.OUT : &es_a_out, ES_A.REVERSE : &es_a_reverse ];
+        es_m_fcts = [ ES_M.OUT : &es_m_out ];
         
         logger = Log.getLogger ("mde.input.input.Input");
     }
@@ -175,15 +295,16 @@
     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
+    short[inputID] axis;		// Table of axes states
+    ushort mouse_x, mouse_y;		// Current screen coords of the window manager 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
+    RelPair[inputID] relMotion;		// 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;
+    ButtonCallback[][inputID]	buttonCallbacks;
+    AxisCallback[][inputID]	axisCallbacks;
+    RelMotionCallback[][inputID] relMotionCallbacks;
+    MouseClickCallback[]	mouseClickCallbacks;
         
     //BEGIN Event stream functionality
     /* This section contains functions called on an event, which may modify the event (adjuster
@@ -191,7 +312,7 @@
     *
     * 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
+    * Adjusters should call one of the xEvent() 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.
@@ -229,11 +350,19 @@
     alias void function (Input, short, readOutQueue) ES_A_Func;
     alias void function (Input, 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,
+    /* These are the codes allowing the config to specify event functions.
+    *
+    * They are organised as defined in doc/input_ID_assignments.
+    */
+    enum ES_B : uint {
+        OUT	= 0x1000u,
+    }
+    enum ES_A : uint {
+        OUT	= 0x1000u,
+        REVERSE	= 0x2000u,
+    }
+    enum ES_M : uint {
+        OUT	= 0x1000u,
     }
     //END ES Definitions
     
@@ -246,23 +375,25 @@
     //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.
-    const EVCONF_ERR = "Input: Invalid configuration: bad event function code";
-    void bEventOut (bool b, readOutQueue s)
+    // They must be static to allow calling from event functions.
+    const EVCONF_ERR = "Invalid configuration: bad event function code";
+    static void bEvent (Input myThis, bool b, readOutQueue s)
     {
         ES_B_Func* func = (s.pop() in es_b_fcts);
-        if (func != null) (*func)(this, b, s);
+        if (func != null) (*func)(myThis, b, s);
         else throw new InputClassException (EVCONF_ERR);
     }
-    void aEventOut (short x, readOutQueue s)
+    static void aEvent (Input myThis, short x, readOutQueue s)
     {
         ES_A_Func* func = (s.pop() in es_a_fcts);
-        if (func != null) (*func)(this, x, s);
+        if (func != null) (*func)(myThis, x, s);
         else throw new InputClassException (EVCONF_ERR);
     }
-    void mEventOut (short x, short y, readOutQueue s)
+    // Only handles relative output since position-on-screen is not stored with an ID:
+    static void mEvent (Input myThis, short x, short y, readOutQueue s)
     {
         ES_M_Func* func = (s.pop() in es_m_fcts);
-        if (func != null) (*func)(this, x, y, s);
+        if (func != null) (*func)(myThis, x, y, s);
         else throw new InputClassException (EVCONF_ERR);
     }
     
@@ -273,41 +404,170 @@
     // Simple output function
     static void es_b_out (Input myThis, bool b, readOutQueue s) {
         inputID id = cast(inputID) s.pop();
-        
+                
         myThis.button[id] = b;
         
-        ButtonCallback* cb_p = id in myThis.buttonCallbacks;
-        if (cb_p) (*cb_p) (id, b);
+        ButtonCallback[]* cb_p = id in myThis.buttonCallbacks;
+        if (cb_p) foreach (cb; *cb_p) cb (id, b);
     }
     // Adjuster to check modifier keys
-    void es_b_modifier (Input myThis, bool b, readOutQueue s);
-
-    /* Simple output function
+    static void es_b_modifier (Input myThis, bool b, readOutQueue s);
 
-    Adds 1-2 items on the stack.
-    */
-    void es_a_out (Input myThis, short x, readOutQueue s) {
-        real y = x;
-        uint conf = s.pop();
-        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.pop();
-        /+ 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 +/
-        myThis.axis[cast(inputID) s.pop()] = y;
+    // Simple output function
+    static void es_a_out (Input myThis, short x, readOutQueue s) {
+        inputID id = cast(inputID) s.pop();
+        
+        myThis.axis[id] = x;
+        
+        AxisCallback[]* cb_p = id in myThis.axisCallbacks;
+        if (cb_p) foreach (cb; *cb_p) cb (id, x);
+    }
+    
+    // Just reverses an axis's value
+    static void es_a_reverse (Input myThis, short x, readOutQueue s) {
+        aEvent (myThis, -x, s);
     }
 
     // Simple output function
-    void es_m_out (Input myThis, short x, short y, readOutQueue s) {
-        myThis.axis_rel[cast(inputID) s.pop()] = RelPair(x,y);
+    static void es_m_out (Input myThis, short x, short y, readOutQueue s) {
+        inputID id = cast(inputID) s.pop();
+        
+        myThis.relMotion[id] = RelPair(x,y);
+        
+        RelMotionCallback[]* cb_p = id in myThis.relMotionCallbacks;
+        if (cb_p) foreach (cb; *cb_p) cb (id, x,y);
     }
     //END ES Functions
     //END Event stream functionality
+    
+    
+    /* This unittest covers input.config.Config and input.input.Input for some events of all types.
+    * It is not bullet-proof, and does not cover the steam-functions other than the direct output
+    * ones (largely because these may get added at any time and tested via input tests).
+    *
+    * What it does test:
+    *	Events of all types.
+    *	Callbacks of all types.
+    *	Status set from events for all types (button,axis,relMotion).
+    *
+    * It relies on config loaded from a file.
+    */
+    debug (mdeUnitTest) unittest {
+        Input ut = new Input();
+        ut.loadConfig ("UnitTest");
+            
+        int[6] counters;	// counters for callbacks
+        // Should become: [2,2,0,2,1,1]
+            
+        //BEGIN Set up some callbacks
+        ut.addButtonCallback (0x03F0, delegate void(inputID id, bool status) {
+            assert (status == !counters[0]);	// true first call, false next
+            counters[0] += 1;
+        });
+        ut.addButtonCallback (0x06F0, delegate void(inputID id, bool status) {
+            assert (status == !counters[1]);	// true first call, false next
+            counters[1] += 1;
+        });
+        ut.addButtonCallback (0x07F0, delegate void(inputID id, bool status) {
+            counters[2] += 1;
+        });
+        ut.addAxisCallback (0x22F0, delegate void(inputID id, short x) {
+            assert (x == (counters[3] ? 0 : 32767));
+            counters[3] += 1;
+        });
+        ut.addRelMotionCallback (0x11F0, delegate void(inputID id, real x, real y) {
+            assert (x == 14.0);
+            assert (y == -1.0);
+            counters[4] += 1;
+        });
+        ut.addMouseClickCallback (delegate void(ushort x, ushort y, ubyte b, bool s) {
+            assert (x == 291);
+            assert (y == 10010);
+            assert (b == 3);
+            assert (s == false);
+            counters[5] += 1;
+        });
+        //END Set up some callbacks
+                        
+        //BEGIN Post a lot of events
+        SDL_Event e;
+            
+        // F11 down:
+        e.type = SDL_KEYDOWN;
+        e.key.state = SDL_PRESSED;
+        e.key.keysym.sym = 292;	// SDLK_F11
+        ut(e);
+        // Right mouse button up:
+        e.type = SDL_MOUSEBUTTONUP;
+        e.button.button = 3;	// SDL_BUTTON_RIGHT
+        e.button.state = SDL_RELEASED;
+        e.button.x = 291;
+        e.button.y = 10010;
+        ut(e);
+        // Mouse motion:
+        e.type = SDL_MOUSEMOTION;
+        e.motion.x = 63;
+        e.motion.y = 44;
+        e.motion.xrel = 14;
+        e.motion.yrel = -1;
+        ut(e);
+        // Joystick 2 button 5 down:
+        e.type = SDL_JOYBUTTONDOWN;
+        e.jbutton.which = 2;
+        e.jbutton.button = 5;
+        e.jbutton.state = SDL_PRESSED;
+        ut(e);
+        // Same button released:
+        e.jbutton.state = SDL_RELEASED;
+        ut(e);
+        // Joystick 1 axis 8 motion:
+        e.type = SDL_JOYAXISMOTION;
+        e.jaxis.which = 1;
+        e.jaxis.axis = 8;
+        e.jaxis.value = 32767;
+        ut(e);
+        // Joystick 22 ball 100 motion:
+        e.type = SDL_JOYBALLMOTION;
+        e.jball.which = 22;
+        e.jball.ball = 100;
+        e.jball.xrel = -21;
+        e.jball.yrel = 1024;
+        ut(e);
+        // Joystick 214 hat 12 DOWN-RIGHT:
+        e.type = SDL_JOYHATMOTION;
+        e.jhat.which = 214;
+        e.jhat.hat = 12;
+        e.jhat.value = SDL_HAT_RIGHTDOWN;
+        ut(e);
+        // Same hat LEFT:
+        e.jhat.value = SDL_HAT_LEFT;
+        ut(e);
+        //END Post a lot of events
+            
+        //BEGIN Check states
+        assert (ut.getButton(0xF0) == true);
+        assert (ut.getButton(0x1F0) == false);
+        assert (ut.getButton(0x2F0) == false);
+        assert (ut.getButton(0x4F0) == false);
+        assert (ut.getButton(0x5F0) == true);
+        assert (ut.getButton(0x6F0) == false);
+        assert (ut.getButton(0x7F0) == false);
+            
+        assert (ut.getAxis(0x20F0) == 32767);
+        assert (ut.getAxis(0x21F0) == -32767);
+        assert (ut.getAxis1(0x22F0) == 0.0);
+            
+        real s,t;
+        ut.getRelMotion(0x10F0, s,t);
+        assert (s == 14.0);
+        assert (t == -1.0);
+        ut.getRelMotion(0x12F0, s,t);
+        assert (s == -21.0);
+        assert (t == 1024.0);
+                                                
+        assert (counters == [2,2,0,2,1,1]);
+        //END Check states
+        
+        logger.info ("Unittest complete.");
+    }
 }
--- a/mde/input/joystick.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/input/joystick.d	Mon Feb 18 11:54:56 2008 +0000
@@ -9,7 +9,7 @@
 
 private Logger logger;
 static this() {
-    logger = Log.getLogger ("mde.input.config");
+    logger = Log.getLogger ("mde.input.joystick");
 }
 private SDL_Joystick*[] joysticks;	// pointers to all joystick structs, whether successfully opened or not
 
--- a/mde/mde.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/mde.d	Mon Feb 18 11:54:56 2008 +0000
@@ -6,15 +6,16 @@
 
 // Package imports
 import mde.init;
+import global = mde.global;
 import mde.events;
 import mde.scheduler;
-//import test = mde.test;
+import mde.exception;
 
 import mde.input.input;
 
 // External library imports
-import tango.core.Thread;
-import tango.io.Stdout;
+import tango.core.Thread : Thread;	// for sleep
+//import tango.io.Stdout;
 import tango.time.Clock;
 import tango.util.log.Log : Log, Logger;
 
@@ -30,25 +31,19 @@
     } catch (initException e) {
         logger.fatal ("Initialisation failed; error was:");
         logger.fatal (e.msg);
-        Stdout (e.msg);
         return 1;
     }
     
-    input.addButtonCallback (cast(Input.inputID) 5u, delegate void(Input.inputID i, bool b) {
-        Stdout ("Event: ")(i)(" changed to: ")(b).newline;
+    global.input.addButtonCallback (cast(Input.inputID) 0x0u, delegate void(Input.inputID i, bool b) {
+        if (b) {
+            logger.info ("Quiting...");
+            global.run = false;
+        }
     } );
     
-    bool oldb = false;
-    while (run)
-    /+for (ulong t = 0; t < 100; ++t)+/ {
+    while (global.run) {
         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.050);	// sleep this many seconds
     }
     
--- a/mde/mergetag/dataset.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/mergetag/dataset.d	Mon Feb 18 11:54:56 2008 +0000
@@ -1,11 +1,20 @@
-/// This module contains the mergetag DataSet class together with an interface for DataSections.
+/** This module contains the mergetag DataSet class together with an interface for DataSections.
+ *
+ * It basically forms the "glue" upon which most of mergetag works, hence gets publically imported
+ * by most mergetag modules.
+ */
 module mde.mergetag.dataset;
 
 // package imports
-public import mde.mergetag.exception;
-public import mde.mergetag.defaultdata;	// used in DataSet so it should be publically imported
+import mde.mergetag.exception;
+import mde.mergetag.defaultdata;	// used in DataSet so it should be publically imported
+
+import tango.util.log.Log : Log, Logger;
 
-//import tango.util.log.Log : Log, Logger;
+private Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.mergetag.dataset");
+}
 
 /** Typedef for data & section indexes (can be changed to ulong if necessary.) */
 typedef char[] ID;
@@ -92,10 +101,12 @@
     void writeAll (ItemDelg);	/// TBD
 }
 
-unittest {	// Only covers DataSet really.
+debug (mdeUnitTest) unittest {	// Only covers DataSet really.
     DataSet ds = new DataSet;
-    ds.sec[1] = new DefaultData;
+    ds.sec[cast(ID)"test"] = new DefaultData;
     assert (ds.getSections!(DefaultData)().length == 1);
-    ds.sec[1].addTag ("int",0," -543 ");
-    assert (ds.getSections!(DefaultData)()[1]._int[0] == -543);
+    ds.sec[cast(ID)"test"].addTag ("int",cast(ID)"T"," -543 ");
+    assert (ds.getSections!(DefaultData)()[cast(ID)"test"]._int[cast(ID)"T"] == -543);
+    
+    logger.info ("Unittest complete.");
 }
--- a/mde/mergetag/defaultdata.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/mergetag/defaultdata.d	Mon Feb 18 11:54:56 2008 +0000
@@ -3,154 +3,146 @@
 module mde.mergetag.defaultdata;
 
 public import mde.mergetag.dataset;
+import mde.mergetag.exception;
 
-import mde.text.util;
-import mde.text.parse : parse;
-import mde.text.format : format;
+import tango.scrapple.text.convert.parseTo : parseTo;
+import tango.scrapple.text.convert.parseFrom : parseFrom;
 
 /**
 * Default DataSection class.
 *
-* Supports all the basic types currently supported and array versions of
-* each (except no arrays of binary, but arrays of strings are supported).
-* Doesn't support custom types, but inheriting classes may add support.
-*/
-/* Note: I wrote this comment when the code looked rather worse. It's still partially applicable though.
+* Supports most of the basic types supported by D (excluding cent/ucent and imaginary/complex
+* types) and array versions of each of these types, plus arrays of strings.
 *
-* 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.
+* Extending the class to support more types, even custom types, shouldn't be particularly difficult
+* provided mde.text.parseTo and mde.text.parseFrom are extended to support the new types.
+*/
+/* The implementation now uses a fair bit of generic programming. Adjusting the types supported
+* should be as simple as adjusting the list dataTypes, and possibly implemting new conversions in
+* parseFrom and parseTo if you add new types (e.g. for cent or imaginary/complex types, or user types).
 *
-* I really don't like having to do things like this, but it provides a lot of benefits such as no
-* need to store types and no need to check an argument's type for every access (this could be done
-* by throwing errors, but then an incorrect (perhaps hand-edited) data file could cause a lot of
-* errors to be thrown).
+* There shouldn't really be any need to adjust the implementation, except perhaps to add new
+* functions to the class (such as another type of output where the delegate used in writeAll isn't
+* enough).
 */
 class DefaultData : DataSection
 {
-    //BEGIN DATA
+    //BEGIN META
+    /* These functions are used to generate code. Compile-time functions rather than templates are
+    * used because they are easier to write and understand. Mixins are used to compile the resultant
+    * code. Must be declared before used since forward references aren't supported for compile-time
+    * functions. */
+    
+    // Generate the correct name for each variable type.
+    static char[] varName (char[] type) {
+        char[] append = "";
+        while (type.length >= 2 && type[$-2..$] == "[]") {
+            type = type[0..$-2];
+            append ~= "A";
+        }
+        return "_" ~ type ~ append;
+    }
+    
+    // Int-to-string converter, which may not be efficient but will run at compile time.
+    static char[] int2str (uint i) {
+        char[] ret;
+        const digits = "0123456789";
+        if (i == 0) ret = "0";
+        else for (; i > 0; i /= 10) ret = digits[i%10] ~ ret;
+        return ret;
+    }
+    
+    // Generate the code for variable declarations.
+    static char[] declerations (char[][] types) {
+        char[] ret = "";
+        foreach (char[] type; types) ret ~= type ~ "[ID]\t" ~ varName(type) ~ ";\n";
+        return ret;
+    }
+    
+    // Purely to add indentation. Could just return "" without affecting functionality.
+    static char[] indent (uint i) {
+        char[] ret;
+        for (; i > 0; --i) ret ~= "  ";
+        // This is not executable at compile time:
+        //ret.length = i * 4;		// number of characters for each indentation
+        //ret[] = ' ';		// character to indent with
+        return ret;
+    }
+    
+    /* Generates a binary search algorithm.
+    *
+    * Currently this is tailored to it's particular use (addTag). */
+    static char[] binarySearch (char[] var, char[][] consts, int indents = 0) {
+        if (consts.length > 3) {
+            return indent(indents) ~ "if (" ~ var ~ " <= \"" ~ consts[$/2 - 1] ~ "\") {\n" ~
+                binarySearch (var, consts[0 .. $/2], indents + 1) ~
+            indent(indents) ~ "} else {\n" ~
+                binarySearch (var, consts[$/2 .. $], indents + 1) ~
+            indent(indents) ~ "}\n";
+        } else {
+            char[] ret;
+            foreach (c; consts) {
+                ret ~= indent(indents) ~ "if (" ~ var ~ " == \"" ~ c ~ "\") {\n" ~
+                    indent(indents+1) ~ varName(c) ~ "[id] = parseTo!(" ~ c ~ ") (dt);\n" ~
+                indent(indents) ~ "} else ";
+            }
+            ret ~= "{\n" ~ indent(indents+1) ~ "throw new MTUnknownTypeException;\n" ~ indent(indents) ~ "}\n";
+            return ret;
+        }
+    }
+    
+    // Generates the code to write data members (writeAll).
+    static char[] writeVars () {
+        char[] code = "";
+        foreach (i,type; dataTypes) {
+            code ~= "foreach (id, dt; " ~ varName(type) ~ ") itemdlg (dataTypes[" ~ int2str(i) ~ "], id, parseFrom!(" ~ type ~ ")(dt));\n";
+        }
+        return code;
+    }
+    //END META
+    
     /** Data Members
     *
-    * These names are available for direct access.
+    * These types are all stored directly, as below, are available for direct access. The variable
+    * names are created dynamically at compile-time based on the dataTypes list.
+    * ------------------
+    * int[ID] _int;		// name is type prefixed by _
+    * char[][ID] _charA;	// [] is replaced by A
+    * ------------------
     *
     * An alternative access method is to use the provided templates:
     * --------------------
     * template Arg(T) {
     *     alias Name Arg;
     * }
-    * --------------------
     *
-    * Use with a mixin or directly:
+    * type y = Arg!(type).Arg;	// example of use
     * --------------------
-    * mixin Arg!(type);
-    * auto x = Arg;
-    *
-    * type y = Arg!(type).Arg;
-    * --------------------
-    * Note: trying to use Arg!(type) to implicitly refer to Arg!(type).Arg causes compiler errors due to
-    * --- alias Name Arg; ---
-    * actually being a mixin.
+    * Note: trying to use Arg!(type) to implicitly refer to Arg!(type).Arg causes compiler errors
+    * due to the "alias Name Arg;" statement actually being a mixin.
     */
-     
-    bool	[ID]	_bool;
-    byte	[ID]	_byte;		/// ditto
-    short	[ID]	_short;		/// ditto
-    int		[ID]	_int;		/// ditto
-    long	[ID]	_long;		/// ditto
-    ubyte	[ID]	_ubyte;		/// ditto
-    ushort	[ID]	_ushort;	/// ditto
-    uint	[ID]	_uint;		/// ditto
-    ulong	[ID]	_ulong;		/// ditto
-    
-    char	[ID]	_char;		/// ditto
-    
-    float	[ID]	_float;		/// ditto
-    double	[ID]	_double;	/// ditto
-    real	[ID]	_real;		/// ditto
-    
-    bool[]	[ID]	_boolA;		/// ditto
-    byte[]	[ID]	_byteA;		/// ditto
-    short[]	[ID]	_shortA;	/// ditto
-    int[]	[ID]	_intA;		/// ditto
-    long[]	[ID]	_longA;		/// ditto
-    ubyte[]	[ID]	_ubyteA;	/// ditto
-    ushort[]	[ID]	_ushortA;	/// ditto
-    uint[]	[ID]	_uintA;		/// ditto
-    ulong[]	[ID]	_ulongA;	/// ditto
-    
-    char[]	[ID]	_charA;		/// ditto
-    
-    float[]	[ID]	_floatA;	/// ditto
-    double[]	[ID]	_doubleA;	/// ditto
-    real[]	[ID]	_realA;		/// ditto
-    
-    char[][]	[ID]	_charAA;	/// ditto
-    
-    /** Alias names */
-    alias	_ubyteA	_binary;
-    alias	_charA	_string;	/// ditto
-    alias	_charAA	_stringA;	/// ditto
-    //END DATA
+    const char[][] dataTypes = ["bool","bool[]",
+                                "byte","byte[]",
+                                "char","char[]","char[][]",
+                                "double","double[]",
+                                "float","float[]",
+                                "int","int[]",
+                                "long","long[]",
+                                "real","real[]",
+                                "short","short[]",
+                                "ubyte","ubyte[]",
+                                "uint","uint[]",
+                                "ulong","ulong[]",
+                                "ushort","ushort[]"];
     
-    void addTag (char[] tp, ID id, char[] dt) {	/// Supports all standard types.
-        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[])
-                ~ 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;` );
-            }
-        }
-        // try-catch block removed (caught by read)
-    }
-    private template addTag_elifIsType_add(T) {
-        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);
+    mixin (declerations (dataTypes));	// Declare all the variables.
+    
+    void addTag (char[] type, ID id, char[] dt) {	/// Supports all types listed in dataTypes.
+        mixin (binarySearch ("type", dataTypes));
     }
     
-    void writeAll (ItemDelg itemdlg) {
-        foreach (id, dt; _charA) itemdlg ("char[]", id, format!(char[])(dt));
+    void writeAll (ItemDelg itemdlg) {	/// Supports all types listed in dataTypes.
+        mixin (writeVars ());
     }
     
     /* These make no attempt to check Arg is valid.
@@ -166,24 +158,3 @@
         mixin(`alias `~ArgString~` Arg;`);
     }
 }
-
-/+class DynamicData : DataSection
-{
-void*[TypeInfo] data;
-    
-}+/
-
-/+
-class TemplateData : DataSection
-{
-void addTag (char[] tp, ID id, char[] dt) {
-// runtime deduction of tp and aliasing?
-// CANNOT add data at runtime though.
-}
-// will this work? no idea.
-// templates can't be used to add non-static elements, so use a static array at index: this
-template Data(T,TemplateData* p) {
-static T[ID][TemplateData*] Data;
-}
-}
-+/
--- a/mde/mergetag/doc/issues.txt	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/mergetag/doc/issues.txt	Mon Feb 18 11:54:56 2008 +0000
@@ -3,7 +3,7 @@
 Overall:
 
 read.d:
-	Allow only partially loading a file.
+	Support partially loading a file.
 	parseSection(): as mentioned at end of function
 	formatting errors could be more informative; in particular say where the error is
 	No binary support.
@@ -13,7 +13,6 @@
 	There is currently no way to specify the base in which numbers are written (in text form).
 
 parse.d:
-	No support for escaped quotes in strings during tokenisation (by tango.text.Util.quotes).
 	Doesn't support cent/ucent.
 
 format.d:
--- a/mde/mergetag/exception.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/mergetag/exception.d	Mon Feb 18 11:54:56 2008 +0000
@@ -9,7 +9,12 @@
 
 /// Base MergeTag exception class.
 class MTException : mdeException {
-    const override symbol = super.symbol ~ ".mergetag";
+    static const char[] symbol;
+    static this() {	symbol = super.symbol ~ ".mergetag";	}
+    char[] getSymbol () {
+        return symbol;
+    }
+    
     this (char[] msg) {
         super(msg);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/mergetag/mtunittest.d	Mon Feb 18 11:54:56 2008 +0000
@@ -0,0 +1,81 @@
+/// This module provides a unittest for mergetag.
+module mde.mergetag.mtunittest;
+
+import mde.mergetag.read;
+import mde.mergetag.write;
+import mde.mergetag.defaultdata;
+
+import tango.scrapple.text.convert.parseTo : parseTo;
+import tango.scrapple.text.convert.parseFrom : parseFrom;
+
+import tango.util.log.Log : Log, Logger;
+
+debug (mdeUnitTest) {
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.mergetag.unittest");
+    }
+
+    unittest {
+        /* This does a basic write-out and read-in test for each type with its default value.
+        * Thus it provides some basic testing for the whole mergetag package. */
+        
+        const file = "unittest.mtt";
+        const ID UT_ID = cast (ID) "mdeUT";
+        const headInfo = "mde Unit Test";
+                
+        DataSet dsW = new DataSet();
+        
+        dsW.header = new DefaultData();
+        dsW.header._charA[UT_ID] = headInfo;
+                
+        DefaultData secW = new DefaultData();
+        dsW.sec[UT_ID] = secW;
+        
+        static char[] genUTCode () {
+            char[] ret;
+            foreach (type; DefaultData.dataTypes) {
+                ret ~= `secW.`~DefaultData.varName(type)~`[UT_ID] = (`~type~`).init;`;
+            }
+            return ret;
+        }
+        mixin (genUTCode());	// Add an entry to dd for each type
+        
+        // FIXME: if/when binary writing is available, write in both formats
+        IWriter w = makeWriter (file, dsW);
+        w.write();
+        
+        Reader r = new Reader (file, null, true);
+        r.read();
+        
+        DataSet dsR = r.dataset;
+        assert (dsR !is null);
+        
+        assert (dsR.header !is null);
+        char[]* p = UT_ID in dsW.header._charA;
+        assert (p);
+        assert (*p == headInfo);
+                
+        DataSection* sec_p = (UT_ID in dsR.sec);
+        assert (sec_p);
+        DefaultData secR = cast(DefaultData) *sec_p;
+        assert (secR !is null);
+        
+        // FIXME: when comparing associative arrays works, use that. In the mean-time, format!() should work.
+        static char[] genCheckCode (char[] dd1, char[] dd2) {
+            const char[] failureMsg = "Assertion failed for type; values: ";
+            char[] ret;
+            foreach (type; DefaultData.dataTypes) {
+                char[] tName = DefaultData.varName(type);
+                ret ~= `char[] `~tName~`Val1 = parseFrom!(`~type~`[char[]]) (cast(`~type~`[char[]]) `~dd1~`.`~tName~`);
+char[] `~tName~`Val2 = parseFrom!(`~type~`[char[]]) (cast(`~type~`[char[]]) `~dd2~`.`~tName~`);
+assert (`~tName~`Val1 == `~tName~`Val2, "Assertion failed for type `~type~`; values: "~`~tName~`Val1~", "~`~tName~`Val2 );
+`;
+            }
+            return ret;
+        }
+        mixin (genCheckCode (`secW`,`secR`));
+    
+        logger.info ("Unittest complete (for DefaultData).");
+    }
+}
--- a/mde/mergetag/read.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/mergetag/read.d	Mon Feb 18 11:54:56 2008 +0000
@@ -8,8 +8,10 @@
 
 // package imports
 public import mde.mergetag.dataset;
+import mde.mergetag.defaultdata;
+import mde.mergetag.exception;
 
-import mde.text.exception;
+import tango.core.Exception;
 
 // tango imports
 import tango.io.UnicodeFile;
@@ -328,7 +330,7 @@
                     try {
                         dsec.addTag (type, tagID, data);
                     }
-                    catch (textParseException e) {
+                    catch (TextException e) {
                         logger.warn ("While reading " ~ ErrFile ~ ":");	// following a parse error
                         logger.warn (e.msg);
                     }
@@ -336,8 +338,10 @@
                         logger.warn ("Unsupported type \"" ~ type ~ "\" " ~ ErrInFile /*~ ":"*/);
                         //logger.warn (e.msg); needless; the above includes enough details.
                     }
-                    catch (Object e) {
-                        logger.error ("Unknown error occured" ~ ErrInFile);
+                    catch (Exception e) {
+                        logger.error ("Unknown error occured" ~ ErrInFile ~ ':');
+                        logger.error (e.msg);
+                        throw e;                        // Fatal to Reader
                     }
                 } else comment = false;			// cancel comment status now
             }
@@ -398,9 +402,8 @@
     }
 //END METHODS: PRIVATE
     
-    /+ A unittest here is really not practical since a file must be read from. Suggestion: Involve
-    + both reading and writing functions in a single unittest for the entire package mergetag.
-    + This is just here to point anyone looking in the right direction...
+    /+ A unittest here is really not practical since a file must be read from.
+    + A unittest is included in defaultdata.d .
     unittest {}
     +/
 }
--- a/mde/mergetag/write.d	Wed Jan 30 11:33:56 2008 +0000
+++ b/mde/mergetag/write.d	Mon Feb 18 11:54:56 2008 +0000
@@ -14,7 +14,8 @@
 module mde.mergetag.write;
 
 // package imports
-import mde.mergetag.dataset;
+public import mde.mergetag.dataset;
+import mde.mergetag.exception;
 
 // tango imports
 import tango.core.Exception;
@@ -153,7 +154,7 @@
         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();
@@ -163,7 +164,7 @@
             
             // Write the header:
             buffer ("{MT" ~ MTFormatVersion.CurrentString ~ "}" ~ Eol);
-            if (_dataset.header) writeSection (buffer, _dataset.header);
+            if (_dataset.header !is null) writeSection (buffer, _dataset.header);
         
             // Write the rest:
             foreach (ID id, DataSection sec; _dataset.sec) {
@@ -183,12 +184,12 @@
     }
         
     private void writeSectionIdentifier (IBuffer buffer, ID id) {
-        buffer ("{" ~ id ~ "}" ~ Eol);
+        buffer ("{" ~ cast(char[])id ~ "}" ~ Eol);
     }
     
     private void writeSection (IBuffer buffer, DataSection sec) {
         void writeItem (char[] tp, ID id, char[] dt) {	// actually writes an item
-            buffer ("<" ~ tp ~ "|" ~ id ~"=" ~ dt ~ ">" ~ Eol);
+            buffer ("<" ~ tp ~ "|" ~ cast(char[])id ~"=" ~ dt ~ ">" ~ Eol);
         }
         sec.writeAll (&writeItem);
         
--- a/mde/test.d	Wed Jan 30 11:33:56 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/// Contains some functions for testing stuff.
-module mde.test;
-
-import MT = mde.mergetag.dataset;
-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) {
-            printLabel (i);
-            Stdout (x).newline;
-        }
-        foreach (ID i, int[] x; dd._intA) {
-            printLabel (i);
-            foreach (int y; x)
-                Stdout (y)(' ');
-            Stdout.newline;
-        }
-        foreach (ID i, ubyte[] x; dd._binary) {
-            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) {
-            printLabel (i);
-            Stdout (x).newline;
-        }
-        foreach (ID i, char[] x; dd._string) {
-            printLabel (i);
-            Stdout (x).newline;
-        }
-    }
-}
-void printLabel (ID i) {
-    Stdout (i)(":\t");
-}
-
-class DataPrinter : DataSection
-{
-    this (ID id) {
-        Stdout ("New section (")(id)(").").newline;
-    }
-    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/exception.d	Wed Jan 30 11:33:56 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-/*******************************************************************
- * Contains exception classes for Text (a collection of text utils).
- *
- * Publically imports mde.exception.
- ******************************************************************/
-module mde.text.exception;
-
-public import mde.exception;
-
-/// Base Text exception class.
-class textException : mdeException {
-    const override symbol = super.symbol ~ ".text";
-    this (char[] msg) {
-        super(msg);
-    }
-    this () {}
-}
-
-/** Thrown by the parse module on any error.
- */
-class textParseException : textException {
-    const override symbol = super.symbol ~ ".parse";
-    this (char[] msg) {
-        super(msg);
-    }
-    this () {}
-}
-/** Thrown by the format module on any error.
-*/
-class textFormatException : textException {
-    const override symbol = super.symbol ~ ".format";
-    this (char[] msg) {
-        super(msg);
-    }
-    this () {}
-}
--- a/mde/text/format.d	Wed Jan 30 11:33:56 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,266 +0,0 @@
-/**************************************************************************************************
- * This contains templates for converting various data-types to a char[].
- *
- * 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.
- * It also supports arrays of any supported type (including of other arrays) and has special
- * handling for strings (char[]) and binary (ubyte[]) data-types.
- * -----------------------------
- * char[] format(T) (T value);
- * -----------------------------
- *
- * There are also a few utility functions defined; the public ones have their own documentation.
- *
- * On errors, a warning is logged and an TextConvertException is thrown. No other exceptions should
- * be thrown and none thrown from functions used outside this module.
- *************************************************************************************************/
-module mde.text.format;
-// TODO: write unittests; check strings generate quotes.
-
-// package imports
-public import mde.text.exception;
-
-// tango imports
-import cInt = tango.text.convert.Integer;
-import cFloat = tango.text.convert.Float;
-import Utf = tango.text.convert.Utf;
-import Util = tango.text.Util;
-/+
-import tango.util.log.Log : Log, Logger;
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.text.format");
-}+/
-
-//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[] ret;
-    ret.length = val.length * defLength!(T) + 2;
-    ret[0] = '[';
-    uint i = 1;
-    foreach (T x; val) {
-        char[] s = format!(T) (x);
-        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];
-}
-char[] format(T : dchar[]) (T val) {
-    return format (toUtf8 (val));
-}
-char[] format(T : wchar[]) (T val) {
-    return format (toUtf8 (val));
-}
-char[] format(T : char[]) (T val) {
-    char[] ret = new char[val.length * 2 + 2];	// Initial storage. This should ALWAYS be enough.
-    ret[0] = '"';
-    uint i = 1;
-    for (uint t = 0; t < val.length;) {
-        // process a block of non-escapable characters
-        uint s = t;
-        while (t < val.length && !isEscapableChar(val[t]))
-            ++t;	// skip all non-escapable chars
-        uint j = i + t - s;
-        ret[i..j] = val[s..t];	// copy a block
-        i = j;
-        // process a block of escapable charaters
-        while (t < val.length && isEscapableChar(val[t])) {
-            ret[i++] = '\\';				// backslash; increment i
-            ret[i++] = replaceEscapableChar(val[t++]);	// character; increment i and t
-        }
-    }
-    ret[i++] = '"';
-    return ret[0..i];
-}
-char[] format(T : ubyte[]) (T val) {
-    static const char[16] digits = "0123456789abcdef";
-    
-    char[] ret = new char[val.length * 2];	// exact length
-    uint i = 0;
-    foreach (ubyte x; val) {
-        ret[i++] = digits[x >> 4];
-        ret[i++] = digits[x & 0x0F];
-    }
-    return ret;
-}
-unittest {
-    // generic array stuff:
-    assert (format!(double[]) ([1.0, 1.0e-10]) == `[1.00000000000000000,0.10000000000000000e-09]`);
-    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";
-char[] format(T : dchar) (T val) {
-    if (val <= 127u) return format (cast(char) val);	// this char can be converted
-    throw new textFormatException (WIDE_CHAR_ERROR);
-}
-char[] format(T : wchar) (T val) {
-    if (val <= 127u) return format (cast(char) val);	// this char can be converted
-    throw new textFormatException (WIDE_CHAR_ERROR);
-}
-char[] format(T : char) (T val) {
-    // Note: if (val > 127) "is invalid UTF-8 single char"
-    // However we don't know what this is for, in particular if it will be recombined with other chars later
-    
-    // Can't return reference to static array; making dynamic is cheaper than copying.
-    char[] ret = new char[4];	// max length for an escaped char
-    ret[0] = '\'';
-    
-    if (!isEscapableChar (val)) {
-        ret[1] = val;
-        ret[2] = '\'';
-        return ret[0..3];
-    } else {
-        ret[1] = '\\';
-        ret[2] = replaceEscapableChar (val);
-        ret[3] = '\'';
-        return ret;
-    }
-    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);
-}
-char[] format(T : short) (T val) {
-    return formatLong (val);
-}
-char[] format(T : int) (T val) {
-    return formatLong (val);
-}
-char[] format(T : long) (T val) {
-    return formatLong (val);
-}
-char[] format(T : ubyte) (T val) {
-    return formatLong (val);
-}
-char[] format(T : ushort) (T val) {
-    return formatLong (val);
-}
-char[] format(T : uint) (T val) {
-    return formatLong (val);
-}
-char[] format(T : ulong) (T val) {
-    if (val > cast(ulong) long.max) throw new textFormatException ("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]) == "[4,468,1025436,4294967295,0]");
-}
-
-// Old calculation (not used):
-// t.dig+2+4+3	// should be sufficient length (mant + (neg, dot, e, exp neg) + exp (3,4,5 for float,double,real resp.))
-char[] format(T : float) (T val) {
-    char[] ret = new char[32];	// minimum allowed by assert in format
-    return cFloat.format (ret, val, T.dig+2, 1);	// from old C++ tests, T.dig+2 gives best(?) accuracy
-}
-char[] format(T : double) (T val) {
-    char[] ret = new char[32];
-    return cFloat.format (ret, val, T.dig+2, 1);
-}
-char[] format(T : real) (T val) {
-    char[] ret = new char[32];
-    return cFloat.format (ret, val, T.dig+2, 1);
-}
-unittest {
-    // NOTE: these numbers are not particularly meaningful.
-    assert (format!(float) (0.0f) == "0.00000000");
-    assert (format!(double) (-1e25) == "-1.00000000000000000e+25");
-    assert (format!(real) (cast(real) 4.918e300) == "4.91800000000000000000e+300");
-}
-//END Convert templates
-
-//BEGIN Length templates
-/* This template provides the initial length for strings for formatting various types. These strings
- * can be expanded; this value should cover 90% of cases or so.
- * FIXME: provide more specialisations (or not?)
- */
-private {
-    template defLength(T) {	const uint defLength = 20;	}
-}
-//END Length templates
-
-//BEGIN Utility funcs
-private char[] formatLong (long val) {
-    try return cInt.toString (val, cInt.Style.Signed, cInt.Flags.Throw);
-    catch (Exception e) throw new textFormatException (e.msg);
-}
-private bool isEscapableChar (char c) {
-    return ((c <= '\r' && c >= '\a') || c == '\"' || c == '\'' || c == '\\');
-}
-// Warning: this DOES NOT check c is escapable
-private char replaceEscapableChar (char c) {
-    static char[char] escCharsRev;	// reversed escChars
-    static bool escCharsRevFilled;	// will be initialised false
-    
-    if (!escCharsRevFilled) {	// only do this once
-        // map of all supported escape sequences
-        escCharsRev = ['"' : '"', '\'' : '\'',
-                       '\\' : '\\', '\a' : 'a',
-                       '\b' : 'b', '\f' : 'f',
-                       '\n' : 'n', '\r' : 'r',
-                       '\t' : 't', '\v' : 'v'];
-        escCharsRevFilled = true;
-    }
-    
-    return escCharsRev[c];
-}
-
-unittest {
-    // all utility functions should be well-enough used not to need testing
-}
-//END Utility funcs
--- a/mde/text/parse.d	Wed Jan 30 11:33:56 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,362 +0,0 @@
-/**************************************************************************************************
- * This contains templates for converting a char[] to various data-types.
- *
- * 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 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 textParseException is thrown with a suitable message. No other exceptions should
- * be thrown and none thrown from functions used outside this module.
- *************************************************************************************************/
-module mde.text.parse;
-
-// package imports
-public import mde.text.exception;
-import mde.text.util : postTrim;
-
-// tango imports
-import cInt = tango.text.convert.Integer;
-import cFloat = tango.text.convert.Float;
-import Util = tango.text.Util;
-debug {
-    import tango.util.log.Log : Log, Logger;
-
-    private Logger logger;
-}
-static this () {
-    debug logger = Log.getLogger ("mde.text.parse");
-}
-
-//BEGIN parse templates
-// Associative arrays
-const char[] AA_ERR = "Invalid associative array: ";
-T[S] parse(T : T[S], S) (char[] src) {
-    src = Util.trim(src);
-    if (src.length < 2 || src[0] != '[' || src[$-1] != ']')
-        throw new textParseException (AA_ERR ~ "not [ ... ]");	// bad braces.
-    
-    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] == '\\') {
-                        if (i+2 >= pair.length) throw new textParseException (AA_ERR ~ "unfinished escape sequence within string/char");
-                        ++i;	// escape seq.
-                    }
-                    ++i;
-                }
-                if (i == pair.length) {
-                    debug logger.warn ("Pair is: " ~ pair);
-                    throw new textParseException (AA_ERR ~ "encountered [ ... KEY] (missing :DATA)");
-                }
-            }
-            ++i;
-        }
-        if (i == pair.length) {
-            throw new textParseException (AA_ERR ~ "encountered [ ... KEY:] (missing DATA)");
-        }
-        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);
-    throw new textParseException ("Invalid array: not [x, ..., z]");
-}
-T parse(T : char[]) (char[] src) {
-    src = Util.trim(src);
-    if (src.length >= 2 && src[0] == '"' && src[$-1] == '"') {
-        src = src[1..$-1];
-        T ret;
-        ret.length = src.length;	// maximum length; retract to actual length later
-        uint i = 0;
-        for (uint t = 0; t < src.length;) {
-            // process a block of non-escaped characters
-            uint s = t;
-            while (t < src.length && src[t] != '\\') ++t;	// non-escaped characters
-            uint j = i + t - s;
-            ret[i..j] = src[s..t];	// copy a block
-            i = j;
-            
-            // process a block of escaped characters
-            while (t < src.length && src[t] == '\\') {
-                t++;
-                if (t == src.length) throw new textParseException ("Invalid 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);
-    throw new textParseException ("Invalid string: not quoted (\"*\") or char array (['a',...,'c'])");
-}
-T parse(T : ubyte[]) (char[] src) {
-    src = Util.trim(src);
-    // Standard case:
-    if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return toArray!(T) (src);
-    // Special case: sequence of hex digits, each pair of which is a ubyte
-    if (src.length % 2 == 1) throw new textParseException ("Invalid binary: odd number of chars");
-    T ret;
-    ret.length = src.length / 2;	// exact
-    for (uint i, pos; pos + 1 < src.length; ++i) {
-        ubyte x = readHexChar(src, pos) << 4;
-        x |= readHexChar(src, pos);
-        ret[i] = x;
-    }
-    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] != '\'')
-        throw new textParseException ("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:
-    /+ This was caused by a bug. Shouldn't occur now normally.
-    if (src[1] == '\\' && src.length == 3) throw new textParseException (`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) throw new textParseException ("Invalid char: too long (non-ASCII UTF-8 characters cannot be read as a single character)");
-    throw new textParseException ("Invalid char: too long");
-}
-// unittest covered above
-
-T parse(T : bool) (char[] src) {
-    src = Util.trim(src);
-    if (src == "true") return true;
-    if (src == "false") return false;
-    uint pos;
-    while (src.length > pos && src[pos] == '0') ++pos;	// strip leading zeros
-    if (src.length == pos && pos > 0) return false;
-    if (src.length == pos + 1 && src[pos] == '1') return true;
-    throw new textParseException ("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);
-}
-T parse(T : short) (char[] src) {
-    return toTInt!(T) (src);
-}
-T parse(T : int) (char[] src) {
-    return toTInt!(T) (src);
-}
-T parse(T : long) (char[] src) {
-    return toTInt!(T) (src);
-}
-T parse(T : ubyte) (char[] src) {
-    return toTInt!(T) (src);
-}
-T parse(T : ushort) (char[] src) {
-    return toTInt!(T) (src);
-}
-T parse(T : uint) (char[] src) {
-    return toTInt!(T) (src);
-}
-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);
-}
-T parse(T : double) (char[] src) {
-    return toTFloat!(T) (src);
-}
-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
-/** Templated read-int function to read (un)signed 1-4 byte integers.
- *
- * Actually a reimplementation of tango.text.convert.Integer toLong and parse functions.
- */
-TInt toTInt(TInt) (char[] src) {
-    const char[] INT_OUT_OF_RANGE = "Integer out of range";
-    bool sign;
-    uint radix, ate, ate2;
-    
-    ate = cInt.trim (src, sign, radix);
-    if (ate == src.length) throw new textParseException ("Invalid integer: no digits");
-    ulong val = cInt.convert (src[ate..$], radix, &ate2);
-    ate += ate2;
-    
-    while (ate < src.length) {
-        if (src[ate] == ' ' || src[ate] == '\t') ++ate;
-        else throw new textParseException ("Invalid integer");
-    }
-    
-    if (val > TInt.max) throw new textParseException (INT_OUT_OF_RANGE);
-    if (sign) {
-        long sval = cast(long) -val;
-        if (sval > TInt.min) return cast(TInt) sval;
-        else throw new textParseException (INT_OUT_OF_RANGE);
-    }
-    return cast(TInt) val;
-}
-
-/** 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. */
-TFloat toTFloat(TFloat) (char[] src) {
-    src = postTrim (src);
-    if (src == "") throw new textParseException ("Invalid float: no digits");
-    uint ate;
-
-    TFloat x = cFloat.parse (src, &ate);
-    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 throw new textParseException ("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;
-    }
-    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
- * subset of those supported by D: \" \' \\ \a \b \f \n \r \t \v
- */
-private char replaceEscapedChar (char c)
-{
-    static char[char] escChars;
-    static bool escCharsFilled;	// will be initialised false
-    
-    if (!escCharsFilled) {
-        // 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;
-    }
-    
-    char* r = c in escChars;
-    if (r != null) return *r;
-    
-    throw new textParseException ("Invalid escape sequence: \\"~c);	// we didn't return, so something failed
-}
-
-// Reads one hex char: [0-9A-Fa-f]. Otherwise throws an exception. Doesn't check src.length.
-private ubyte readHexChar (char[] src, inout uint pos) {
-    ubyte x;
-    if (src[pos] >= '0' && src[pos] <= '9') x = src[pos] - '0';
-    else if (src[pos] >= 'A' && src[pos] <= 'F') x = src[pos] - 'A' + 10;
-    else if (src[pos] >= 'a' && src[pos] <= 'f') x = src[pos] - 'a' + 10;
-    else throw new textParseException ("Invalid hex digit.");
-    ++pos;
-    return x;
-}
-
-// 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; split(src[1..$-1])) {
-        if (i == ret.length) ret.length = ret.length * 2;
-        ret[i] = parse!(T) (element);
-        ++i;
-    }
-    return ret[0..i];
-}
-
-unittest {
-    // all utility functions should be well-enough used not to need testing
-}
-//END Utility funcs
--- a/mde/text/util.d	Wed Jan 30 11:33:56 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-/***************************************************************
- * 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;
-    }
-    return str[0..i];
-}
-unittest {
-    assert (postTrim(" 	a6 05 	   ".dup) == " 	a6 05".dup);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/mdeTest.d	Mon Feb 18 11:54:56 2008 +0000
@@ -0,0 +1,31 @@
+/** A module to run all mde unittests and potentially to perform other tests.
+*/
+module test.mdeTest;
+
+// Define this to run unittests:
+debug=mdeUnitTest;
+
+// This module should import all mde modules containing unittests:
+import mde.input.input;
+import mde.mergetag.dataset;
+import mde.mergetag.mtunittest;
+import mde.exception;
+import mde.init;
+
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+
+static this()
+{
+    // Set up console logging:
+    Logger root = Log.getRootLogger();
+    root.setLevel(root.Level.Trace);
+    root.addAppender(new ConsoleAppender);
+    
+    logger = Log.getLogger ("test.mdeTest");
+}
+
+void main() {
+    logger.info ("All unittests complete.");
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/util.d	Mon Feb 18 11:54:56 2008 +0000
@@ -0,0 +1,73 @@
+/// Contains some functions for testing stuff.
+module test.util;
+
+import MT = mde.mergetag.dataset;
+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) {
+            printLabel (i);
+            Stdout (x).newline;
+        }
+        foreach (ID i, int[] x; dd._intA) {
+            printLabel (i);
+            foreach (int y; x)
+                Stdout (y)(' ');
+            Stdout.newline;
+        }
+        foreach (ID i, ubyte[] x; dd._binary) {
+            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) {
+            printLabel (i);
+            Stdout (x).newline;
+        }
+        foreach (ID i, char[] x; dd._string) {
+            printLabel (i);
+            Stdout (x).newline;
+        }
+    }
+}
+void printLabel (ID i) {
+    Stdout (i)(":\t");
+}
+
+class DataPrinter : DataSection
+{
+    this (ID id) {
+        Stdout ("New section (")(id)(").").newline;
+    }
+    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) {}
+}