changeset 84:e0f1ec7fe73a

Merge plus a few tweaks.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sun, 31 Aug 2008 15:59:17 +0100
parents ac1e3fd07275 (diff) 2813ac68576f (current diff)
children 56c0ddd90193
files codeDoc/jobs.txt codeDoc/staticCtors.txt mde/font/font.d mde/gui/WidgetManager.d mde/input/Config.d mde/lookup/Options.d mde/mde.d mde/setup/init2.d
diffstat 49 files changed, 3022 insertions(+), 2759 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/file/formats-overview.txt	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,25 @@
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+File headers - these are like magic numbers, but following a Byte-Order-Mark.
+
+For mde files, they should have the form: mdeXXXNN where XXX is a three-character ASCII identifier
+for the specific file type and NN is a version number (only to be changed when the base file
+format changes, not for changes to the serialization).
+
+Identifier      File extension  File format
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
+{MT01}          .mtt            Mergetag text
+mdessi00        .ssi            Single Serialized Item
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/file/mergetag/file-format-binary.txt	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,18 @@
+Copyright © 2007-2008 Diggory Hardy
+License: GNU General Public License version 2 or later (see COPYING)
+
+
+No file format is set yet; this basically includes possibilities. The file format may or may not be compatible across platforms; if not it may just be used as a cache (i.e. open .mtt/.mtb, whichever is newest, and if it's .mtt then save a .mtb version).
+
+
+This is the file format for mergetag binary files. The unit size is a byte. Most numbers to do with the layout (i.e. not stored data) should be stored as a 32-bit uint.
+
+
+BOM  ---  a Byte Order Mark should be used to determin endianness (MT01 (or other version) in bytes, but converted to two ushorts to detect endianness?)
+
+
+File should then consist of sections:
+
+Header data including an address for the header section data if included.
+
+Sections list. Include a list of sections with identifiers and addresses, sorted by identifier and in a suitible format to easily be converted to a D hash-map. Addresses for each section should consist of both a start and an end address; the end address should be checked upon reading the section. In addition the start address must be checked against the end of file to avoid security vulnerabilities with reading other memory blocks.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/file/mergetag/file-format-requirements.txt	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,33 @@
+Copyright © 2007-2008 Diggory Hardy
+License: GNU General Public License version 2 or later (see COPYING)
+
+
+Requirements:
+
+---	Config - named entries (map associated by strings; sections by strings)
+Merging: chosing one entry over another
+
+---	Data - list of entries, each of custom compound type (e.g. list of 3-vector over reals)
+As sub blocks within a standard item (mergable: combine the blocks of multiple items).
+
++++	Global type for handling all this:
+File consists of sections.
+Each section consists of items.
+Items are sorted by ID and not by type, i.e. if two items with the same ID but different types exist, merging rules are used to choose between them.
+Items have a custom type, which can be a compound of:
++	Basic types:
+++		bool
+++		int (int+uint)
+++		real (or float or double? no.)
+++		string (char)
+++		binary (ubyte[])
++	Strings (of char, wchar or dchar)
++	Fixed-length arrays (single type)
++	Variable-length arrays (single type)
++	Fixed format tuples (multiple types which are prespecified)
++	The top-most type may be a "data list", which is identical to a variable-length array accept that merging items with identical types will combine their lists instead of choosing one over the other.
+To access an item, it should be found by ID, its type should be checked, and then it may be accessed.
+Types are specific to items. As an optimisation, a binary format may have a list of types and index them.
+
++++	Basic types:
+All D base types, including void, with support for writing strings.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/file/mergetag/file-format-text.txt	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,183 @@
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+This is the file format for mergetag text files.
+Version: 0.1 unfinalised
+
+
+The encoding should be unicode UTF-8, UTF-16 or UTF-32, and for anything other than UTF-8 must include a BOM.
+
+
+Hierarchy:
++	Sections	(special section: see header)
+++	Data Tags
+
+
+IDs:
+IDs are used for several purposes; they are UTF-8 strings. They are stored in text files as unquoted strings; escape sequences are not supported and the strings should not contain the following characters, although this is not checked: <|=>{}
+All characters between the appropriate markers are consumed into the ID, hence whitespace is meaningful.
+Multiple section or data tags with the same ID are allowed; see the "Merging rules" section.
+
+
+Outside of tags only whitespace or valid tags is allowed. Whitespace is ignored.
+The following tags are valid (see below for details):
+tag		purpose
+{...}		section identifiers
+<...>		data items
+!{...}		simple comment block
+!<...>		comment block parsed the same as <...>
+Within tags, type specifications or data items whitespace is allowed between symbols.
+
+
+Section identifier tags:
+Format: {ID}
+The ID is the section identifier/name. The ID type is DefaultData unless overriden by the code using the reader.
+A section identifier marks the beginning of a new section, extending until the next section identifier or the end of the file.
+
+
+Data item tags:
+Format: <tp|ID=dt>
+A data item with type tp, identifier ID and data dt. If the data does not fit the given type it is an error and the tag is ignored. Once split into a type string, ID and data string, the contents are passed to an addTag() function within the DataSection class which will parse tags of a recognised format and either ignore or print a warning about other tags.
+
+
+Data item tags: Type format:
+Note:
+	The type is read as a single token terminated by any of these characters:	<>|=
+	There must not be spaces within the type, e.g. "char []".
+	Of course any character other than a | terminating the token is an error.
+Format:
+	tp		a basic type
+	tp[]		a dynamic list of sub-type tp
+	t1[t2]		an associative array with key-type t2
+Possible future additions:
+	tp()		a dynamic merging list of sub-type tp (only valid as the primary type, ie <subtype()|...>, not a sub-type of a tuple or another dynamic list)
+	{t1,t2,...,tn}	a tuple with sub-types t1, t2, ..., tn
+
+Basic types (only items with a + are currently supported, items with * are in DefaultData):
+	name
+	
+	void	--- less useful type
++*	bool	--- integer types
++*	byte
++*	ubyte
++*	short
++*	ushort
++*	int
++*	uint
++*	long
++*	ulong
+	cent
+	ucent
+	
++*	binary	--- alias for ubyte[]
+	
++*	float	--- floating point types
++*	double
++*	real
+	ifloat
+	idouble
+	ireal
+	cfloat
+	cdouble
+	creal
+	
++*	char	--- single character types (actually these CANNOT support UTF8 symbols with length > 1)
+	wchar
+	dchar
++*	string	--- alias for char[] --- (DOES support UTF8)
+	wstring	--- alias for wchar[]
+	dstring	--- alias for dchar[]
+
+
+Data item tags: Data format:
+Valid chars:	[](){},+-.0-9eEixXa-fA-F '.' ".*"
+Format:
+	[d1,d2,...,dn]	data all of type t corresponding to t[]
+	(d1,d2,...,dn)	data all of type t corresponding to t()
+	{d1,d2,...,dn}	data corresponding to a type declaration of {t1,t2,...,tn}
+	d		a single data element
+
+Single data elements:
+	z		an integer number (regexp: [+-]?[0-9]+)
+	z		a floating point number (rough regexp: [+-]?[0-9]*[.]?[0-9]*(e[+-]?[0-9]+)?)
+	zi		an imaginary floating point number (z is a floating point number)
+	y+zi, y-zi	a complex number (4+0i may be written as 4, etc) (y, z are f.p.s)
+	0xz, -0xz	a hexadecimal integer z (composed of chars 0-9,a-f,A-F)
+	'c'		a char/wchar/dchar character, depending on the type specified (c may be any single character except ' or an escape sequence)
+	"string"	equivalent to ['s','t','r','i','n','g'] --- may contain the following escape sequences as defined in D: \" \' \\ \a \b \f \n \r \t \v
+	XX...XX		Binary (ubyte[]); each pair of chars is read as a hex ubyte
+	<void>		void "data" has no symbols
+
+
+Data format: Escape sequences:
+To be created and written.
+
+
+Comment tags (there are no line comments):
+Simple comment blocks:
+Format: !{...}
+This is a simple comment block, and only curly braces ({,}) are treated specially. A {, whether or not it is preceded by a !, starts an embedded comment block, and a } ends either an embedded block or the actual comment block. Note: beware commenting out anything containing curly braces which aren't in matching pairs.
+Commented data tags:
+Format: !<tp|ID=dt>
+Basically a commented out data tag. Conformance to the above spec may not be checked as strictly as normal, but the dt section is checked for strings so that a > within a string won't end the tag.
+
+
+Merging rules:
+if, when a data item is read, a data item with the same identifier
+within the same section exists in the DataSet being read into:
++	if the types are identical:
+++		if the primary type is a tp() mergeable dynamic list:
++++			the entries from the item being read are concatenated to those in the item
++++			in the DataSet
+++		else:
+++-			the item already in the DataSet takes priority and is left untouched
++	else:
++-		a warning is issued, and the data item within the DataSet is left untouched
+This allows merging some config settings in a user config file with the remaining settings in a
+complete system config file and some support for modifications overriding or adding to some data.
+
+
+Header:
+The header is a standard section which is mandatory and must be the first section. Its section identifier must start at the beginning of the file with no whitespace, declared with:
+	{MTXY}		where XY is a two digit CAPITAL HEX version number representing the mergetag format version, e.g. {MT01} .
+If these are not the first 6 characters of the file the file will not be regarded as valid.
+This formatting is very strict to allow reliable low-level parsing.
+
+
+The data tags within the header have no special meaning; any may be used such as the following:
+	<string|"Author"="...">
+	<string|"Name"="...">
+	<string|"Description"="...">
+	<string|"Program"="...">	(which program created/uses this?)
+	<*|"Version"=...>		(use any supported type)
+	<string|"Date"="YYYYMMDD">	(reverse date format; optionally "YYYYMMDDhhmmss")
+	<{u16,u8,u8}|"Date"={YYYY,MM,DD}>	(actually this type probably won't be supported by a standard section)
+	<string|"Copyright"=...>
+
+
+Example:	!THIS IS NO LONGER VALID!
+{MT01}
+{example section}
+<u32|"num"=5>
+<{u32,UTF8[]}()|"DATA"=(
+	{1,['a']},
+	{59,['w','o','r','d']},
+	{2,"strings can be written like this"} )>
+<wchar[]|"name"="This string is stored in UTF16, regardless of the file's encoding.">
+<{u32,UTF8[]}()|"DATA"=(
+	{3,"this is appended to the previous 'DATA' item"} )>
+{"section: section identifiers and tuples are not confused since tuples only occur inside <...> items"}
+<void|Empty tag= >
+!{this is a comment {containing a comment}}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/file/mergetag/issues.txt	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,20 @@
+Copyright © 2007-2008 Diggory Hardy
+License: GNU General Public License version 2 or later (see COPYING)
+
+
+This is mostly just a list of potential minor issues noticed while coding but not seen worth throwing an error about.
+
+Overall:
+
+read.d:
+	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.
+
+write.d:
+	Threading support?
+	There is currently no way to specify the base in which numbers are written (in text form).
+
+format.d:
+	No support for ulong where val > long.max.
Binary file codeDoc/file/mergetag/new-models.vym has changed
--- a/codeDoc/jobs.txt	Sat Aug 30 10:54:32 2008 +0100
+++ b/codeDoc/jobs.txt	Sun Aug 31 15:59:17 2008 +0100
@@ -18,6 +18,7 @@
 3   Scheduler for drawing only windows which need redrawing.
 3   Update scheduler as outlined in FIXME.
 3   Windows building/compatibility (currently partial) - tango/sys/win32/SpecialPath.d
+2   Why does mde.events need to be imported before mde.setup.Init to make fonts display properly? Analysis of static CTORs doesn't seem to have helped.
 2   Remove ability to scan, then load, mergetag sections. Not so necessary with section creator callback and allows "sliding window" type partial buffering.
 2   Options need a "level": simple options, for advanced users, for debugging only, etc.
 2   Command-line options for paths to by-pass normal path finding functionality.
--- a/codeDoc/mergetag/file-format-binary.txt	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-Copyright © 2007-2008 Diggory Hardy
-License: GNU General Public License version 2 or later (see COPYING)
-
-
-No file format is set yet; this basically includes possibilities. The file format may or may not be compatible across platforms; if not it may just be used as a cache (i.e. open .mtt/.mtb, whichever is newest, and if it's .mtt then save a .mtb version).
-
-
-This is the file format for mergetag binary files. The unit size is a byte. Most numbers to do with the layout (i.e. not stored data) should be stored as a 32-bit uint.
-
-
-BOM  ---  a Byte Order Mark should be used to determin endianness (MT01 (or other version) in bytes, but converted to two ushorts to detect endianness?)
-
-
-File should then consist of sections:
-
-Header data including an address for the header section data if included.
-
-Sections list. Include a list of sections with identifiers and addresses, sorted by identifier and in a suitible format to easily be converted to a D hash-map. Addresses for each section should consist of both a start and an end address; the end address should be checked upon reading the section. In addition the start address must be checked against the end of file to avoid security vulnerabilities with reading other memory blocks.
--- a/codeDoc/mergetag/file-format-requirements.txt	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-Copyright © 2007-2008 Diggory Hardy
-License: GNU General Public License version 2 or later (see COPYING)
-
-
-Requirements:
-
----	Config - named entries (map associated by strings; sections by strings)
-Merging: chosing one entry over another
-
----	Data - list of entries, each of custom compound type (e.g. list of 3-vector over reals)
-As sub blocks within a standard item (mergable: combine the blocks of multiple items).
-
-+++	Global type for handling all this:
-File consists of sections.
-Each section consists of items.
-Items are sorted by ID and not by type, i.e. if two items with the same ID but different types exist, merging rules are used to choose between them.
-Items have a custom type, which can be a compound of:
-+	Basic types:
-++		bool
-++		int (int+uint)
-++		real (or float or double? no.)
-++		string (char)
-++		binary (ubyte[])
-+	Strings (of char, wchar or dchar)
-+	Fixed-length arrays (single type)
-+	Variable-length arrays (single type)
-+	Fixed format tuples (multiple types which are prespecified)
-+	The top-most type may be a "data list", which is identical to a variable-length array accept that merging items with identical types will combine their lists instead of choosing one over the other.
-To access an item, it should be found by ID, its type should be checked, and then it may be accessed.
-Types are specific to items. As an optimisation, a binary format may have a list of types and index them.
-
-+++	Basic types:
-All D base types, including void, with support for writing strings.
--- a/codeDoc/mergetag/file-format-text.txt	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,183 +0,0 @@
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-This is the file format for mergetag text files.
-Version: 0.1 unfinalised
-
-
-The encoding should be unicode UTF-8, UTF-16 or UTF-32, and for anything other than UTF-8 must include a BOM.
-
-
-Hierarchy:
-+	Sections	(special section: see header)
-++	Data Tags
-
-
-IDs:
-IDs are used for several purposes; they are UTF-8 strings. They are stored in text files as unquoted strings; escape sequences are not supported and the strings should not contain the following characters, although this is not checked: <|=>{}
-All characters between the appropriate markers are consumed into the ID, hence whitespace is meaningful.
-Multiple section or data tags with the same ID are allowed; see the "Merging rules" section.
-
-
-Outside of tags only whitespace or valid tags is allowed. Whitespace is ignored.
-The following tags are valid (see below for details):
-tag		purpose
-{...}		section identifiers
-<...>		data items
-!{...}		simple comment block
-!<...>		comment block parsed the same as <...>
-Within tags, type specifications or data items whitespace is allowed between symbols.
-
-
-Section identifier tags:
-Format: {ID}
-The ID is the section identifier/name. The ID type is DefaultData unless overriden by the code using the reader.
-A section identifier marks the beginning of a new section, extending until the next section identifier or the end of the file.
-
-
-Data item tags:
-Format: <tp|ID=dt>
-A data item with type tp, identifier ID and data dt. If the data does not fit the given type it is an error and the tag is ignored. Once split into a type string, ID and data string, the contents are passed to an addTag() function within the DataSection class which will parse tags of a recognised format and either ignore or print a warning about other tags.
-
-
-Data item tags: Type format:
-Note:
-	The type is read as a single token terminated by any of these characters:	<>|=
-	There must not be spaces within the type, e.g. "char []".
-	Of course any character other than a | terminating the token is an error.
-Format:
-	tp		a basic type
-	tp[]		a dynamic list of sub-type tp
-	t1[t2]		an associative array with key-type t2
-Possible future additions:
-	tp()		a dynamic merging list of sub-type tp (only valid as the primary type, ie <subtype()|...>, not a sub-type of a tuple or another dynamic list)
-	{t1,t2,...,tn}	a tuple with sub-types t1, t2, ..., tn
-
-Basic types (only items with a + are currently supported, items with * are in DefaultData):
-	name
-	
-	void	--- less useful type
-+*	bool	--- integer types
-+*	byte
-+*	ubyte
-+*	short
-+*	ushort
-+*	int
-+*	uint
-+*	long
-+*	ulong
-	cent
-	ucent
-	
-+*	binary	--- alias for ubyte[]
-	
-+*	float	--- floating point types
-+*	double
-+*	real
-	ifloat
-	idouble
-	ireal
-	cfloat
-	cdouble
-	creal
-	
-+*	char	--- single character types (actually these CANNOT support UTF8 symbols with length > 1)
-	wchar
-	dchar
-+*	string	--- alias for char[] --- (DOES support UTF8)
-	wstring	--- alias for wchar[]
-	dstring	--- alias for dchar[]
-
-
-Data item tags: Data format:
-Valid chars:	[](){},+-.0-9eEixXa-fA-F '.' ".*"
-Format:
-	[d1,d2,...,dn]	data all of type t corresponding to t[]
-	(d1,d2,...,dn)	data all of type t corresponding to t()
-	{d1,d2,...,dn}	data corresponding to a type declaration of {t1,t2,...,tn}
-	d		a single data element
-
-Single data elements:
-	z		an integer number (regexp: [+-]?[0-9]+)
-	z		a floating point number (rough regexp: [+-]?[0-9]*[.]?[0-9]*(e[+-]?[0-9]+)?)
-	zi		an imaginary floating point number (z is a floating point number)
-	y+zi, y-zi	a complex number (4+0i may be written as 4, etc) (y, z are f.p.s)
-	0xz, -0xz	a hexadecimal integer z (composed of chars 0-9,a-f,A-F)
-	'c'		a char/wchar/dchar character, depending on the type specified (c may be any single character except ' or an escape sequence)
-	"string"	equivalent to ['s','t','r','i','n','g'] --- may contain the following escape sequences as defined in D: \" \' \\ \a \b \f \n \r \t \v
-	XX...XX		Binary (ubyte[]); each pair of chars is read as a hex ubyte
-	<void>		void "data" has no symbols
-
-
-Data format: Escape sequences:
-To be created and written.
-
-
-Comment tags (there are no line comments):
-Simple comment blocks:
-Format: !{...}
-This is a simple comment block, and only curly braces ({,}) are treated specially. A {, whether or not it is preceded by a !, starts an embedded comment block, and a } ends either an embedded block or the actual comment block. Note: beware commenting out anything containing curly braces which aren't in matching pairs.
-Commented data tags:
-Format: !<tp|ID=dt>
-Basically a commented out data tag. Conformance to the above spec may not be checked as strictly as normal, but the dt section is checked for strings so that a > within a string won't end the tag.
-
-
-Merging rules:
-if, when a data item is read, a data item with the same identifier
-within the same section exists in the DataSet being read into:
-+	if the types are identical:
-++		if the primary type is a tp() mergeable dynamic list:
-+++			the entries from the item being read are concatenated to those in the item
-+++			in the DataSet
-++		else:
-++-			the item already in the DataSet takes priority and is left untouched
-+	else:
-+-		a warning is issued, and the data item within the DataSet is left untouched
-This allows merging some config settings in a user config file with the remaining settings in a
-complete system config file and some support for modifications overriding or adding to some data.
-
-
-Header:
-The header is a standard section which is mandatory and must be the first section. Its section identifier must start at the beginning of the file with no whitespace, declared with:
-	{MTXY}		where XY is a two digit CAPITAL HEX version number representing the mergetag format version, e.g. {MT01} .
-If these are not the first 6 characters of the file the file will not be regarded as valid.
-This formatting is very strict to allow reliable low-level parsing.
-
-
-The data tags within the header have no special meaning; any may be used such as the following:
-	<string|"Author"="...">
-	<string|"Name"="...">
-	<string|"Description"="...">
-	<string|"Program"="...">	(which program created/uses this?)
-	<*|"Version"=...>		(use any supported type)
-	<string|"Date"="YYYYMMDD">	(reverse date format; optionally "YYYYMMDDhhmmss")
-	<{u16,u8,u8}|"Date"={YYYY,MM,DD}>	(actually this type probably won't be supported by a standard section)
-	<string|"Copyright"=...>
-
-
-Example:	!THIS IS NO LONGER VALID!
-{MT01}
-{example section}
-<u32|"num"=5>
-<{u32,UTF8[]}()|"DATA"=(
-	{1,['a']},
-	{59,['w','o','r','d']},
-	{2,"strings can be written like this"} )>
-<wchar[]|"name"="This string is stored in UTF16, regardless of the file's encoding.">
-<{u32,UTF8[]}()|"DATA"=(
-	{3,"this is appended to the previous 'DATA' item"} )>
-{"section: section identifiers and tuples are not confused since tuples only occur inside <...> items"}
-<void|Empty tag= >
-!{this is a comment {containing a comment}}
--- a/codeDoc/mergetag/issues.txt	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-Copyright © 2007-2008 Diggory Hardy
-License: GNU General Public License version 2 or later (see COPYING)
-
-
-This is mostly just a list of potential minor issues noticed while coding but not seen worth throwing an error about.
-
-Overall:
-
-read.d:
-	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.
-
-write.d:
-	Threading support?
-	There is currently no way to specify the base in which numbers are written (in text form).
-
-format.d:
-	No support for ulong where val > long.max.
Binary file codeDoc/mergetag/new-models.vym has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/staticCtors.txt	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,60 @@
+Map of what happens in static CTORs (excluding creating loggers):
+
+mde
+->  imde
+    ->  input.Input
+        ->  input.Config
+            
+
+* means shouldn't affect anything else done by static ctors
+
+imde {
+    input = new Input();
+    mainSchedule = new Scheduler;*
+}
+
+font.FontTexture.OptionsFont {*
+    fontOpts = new OptionsFont;
+    Options.addOptionsClass (fontOpts, "font");
+}
+
+gui.WidgetManager {*
+    gui = new WidgetManager ("gui");
+}
+
+input.Config.Config {*
+    loadedFiles = new TreeBag!(char[]);
+}
+
+input.Input.Input {*
+    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 ];
+}
+
+lookup.Options.OptionsMisc {*
+    miscOpts = new OptionsMisc;
+    Options.addOptionsClass (miscOpts, "misc");
+}
+
+setup.Init {?
+    Logger root = Log.root;
+    debug root.level(Logger.Trace);
+    else  root.level(Logger.Info);
+    root.add(new AppendConsole);
+}
+
+setup.init2 {*
+    init.addFunc (&initInput, "initInput");
+    init.addFunc (&guiLoad, "guiLoad");
+}
+
+setup.sdl {*
+    init.addFunc (&initSdlAndGl, "initSdlAndGl");
+}
+
+setup.sdl.OptionsVideo {*
+    vidOpts = new OptionsVideo;
+    Options.addOptionsClass (vidOpts, "video");
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/deserialize.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,631 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/**************************************************************************************************
+ * Generic deserialization templated function.
+ *
+ * Supports:
+ *  Associative arrays, dynamic arrays (with usual formatting of strings), structs, char types,
+ *  bool, int types, float types.
+ *
+ * There are also some public utility functions with their own documentation.
+ *
+ * Examples:
+ * ------------------------------------------------------------------------------------------------
+ * // Basic examples:
+ * ulong        a = deserialize!(ulong) ("20350");
+ * float        d = deserialize!(float) ("  1.2e-9 ");
+ * int[]        b = deserialize!(int[]) ("[0,1,2,3]");
+ *
+ * // String and char[] syntax:
+ * char[]       c = deserialize!(char[]) ("\"A string\"");
+ * char[]       e = deserialize!(char[]) ("['a','n','o','t','h','e','r', ' ' ,'s','t','r','i','n','g']");
+ *
+ * // These be used interchangably; here's a more complex example of an associative array:
+ * bool[char[]] f = deserialize!(bool[char[]]) ("[ \"one\":true, ['t','w','o']:false, \"three\":1, \"four\":000 ]");
+ *
+ * // There is also a special notation for ubyte[] types:
+ * // The digits following 0x must be in pairs and each specify one ubyte.
+ * assert ( deserialize!(ubyte[]) (`0x01F2AC`) == deserialize!(ubyte[]) (`[01 ,0xF2, 0xAC]`) );
+ *
+ * // There's no limit to the complexity!
+ * char[char[][][][char]][bool] z = ...; // don't expect me to write this!
+ * ------------------------------------------------------------------------------------------------
+ *
+ * Throws:
+ *      May throw a ParseException or a UnicodeException (which both extend TextException).
+ *
+ * TODO: Optimize memory allocation (if possible?). Test best sizes for initial allocations
+ * instead of merely guessing?
+ *************************************************************************************************/
+//NOTE: in case of multiple formats, make this a dummy module importing both serialize modules,
+// or put all the code here.
+module mde.file.deserialize;
+
+// tango imports
+import tango.core.Exception : TextException, UnicodeException;
+import cInt = tango.text.convert.Integer;
+import cFloat = tango.text.convert.Float;
+import Utf = tango.text.convert.Utf;
+import Util = tango.text.Util;
+
+/**
+ * Base class for deserialize exceptions.
+ */
+class ParseException : TextException
+{
+    this( char[] msg )
+    {
+        super( msg );
+    }
+}
+
+alias deserialize parseTo;      // support the old name
+
+//BEGIN deserialize templates
+
+// Associative arrays
+
+T[S] deserialize(T : T[S], S) (char[] src) {
+    src = Util.trim(src);
+    if (src.length < 2 || src[0] != '[' || src[$-1] != ']')
+        throw new ParseException ("Invalid associative array: 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] == '\\')
+                        ++i;    // escape seq.
+                    ++i;
+                }
+                // Could have an unterminated ' or " causing i >= pair.length, but:
+                // 1. Impossible: split would have thrown
+                // 2. In any case this would be caught below.
+            }
+            ++i;
+        }
+        if (i >= pair.length)
+            throw new ParseException ("Invalid associative array: encountered [ ... KEY] (missing :DATA)");
+        ret[deserialize!(S) (pair[0..i])] = deserialize!(T) (pair[i+1..$]);
+    }
+    return ret;
+}
+
+
+// Arrays
+
+T[] deserialize(T : T[]) (char[] src) {
+    src = Util.trim(src);
+    if (src.length >= 2 && src[0] == '[' && src[$-1] == ']')
+        return toArray!(T[]) (src);
+    throw new ParseException ("Invalid array: not [ ... ]");
+}
+
+// String (array special case)
+T deserialize(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 ParseException ("Invalid string: ends \\\" !");  // next char is "
+                ret[i++] = unEscapeChar (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 ParseException ("Invalid string: not quoted (\"*\") or char array (['a',...,'c'])");
+}
+// Unicode conversions for strings:
+T deserialize(T : wchar[]) (char[] src) {
+    // May throw a UnicodeException; don't bother catching and rethrowing:
+    return Utf.toString16 (deserialize!(char[]) (src));
+}
+T deserialize(T : dchar[]) (char[] src) {
+    // May throw a UnicodeException; don't bother catching and rethrowing:
+    return Utf.toString32 (deserialize!(char[]) (src));
+}
+
+// Binary (array special case)
+T deserialize(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 && src[0..2] == "0x") {
+        src = src[2..$];    // strip down to actual digits
+        
+        // Must be in pairs:
+        if (src.length % 2 == 1)
+            throw new ParseException ("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;
+    }
+    else throw new ParseException ("Invalid ubyte[]: not an array and doesn't start 0x");
+}
+
+
+// Basic types
+
+// Char
+// Assumes value is <= 127 (for valid UTF-8), since input would be invalid UTF-8 if not anyway.
+// (And we're not really interested in checking for valid unicode; char[] conversions don't either.)
+T deserialize(T : char) (char[] src) {
+    src = Util.trim(src);
+    if (src.length < 3 || src[0] != '\'' || src[$-1] != '\'')
+        throw new ParseException ("Invalid char: not 'x' or '\\x'");
+    if (src[1] != '\\') {
+        if (src.length == 3)
+            return src[1];              // Either non escaped
+        throw new ParseException ("Invalid char: too long (or non-ASCII)");
+    } else if (src.length == 4)
+        return unEscapeChar (src[2]);   // Or escaped
+    
+    throw new ParseException ("Invalid char: '\\'");
+}
+// Basic unicode convertions for wide-chars.
+T deserialize(T : wchar) (char[] src) {
+    src = Util.trim(src);
+    if (src.length < 3 || src[0] != '\'' || src[$-1] != '\'')
+        throw new ParseException ("Invalid char: not 'x' or '\\x'");
+    T[] t = Utf.toString16 (src[1..$-1]);
+    if (t.length == 1)
+        return t[0];
+    else
+        throw new ParseException ("Invalid char: not one character");
+}
+T deserialize(T : dchar) (char[] src) {
+    src = Util.trim(src);
+    if (src.length < 3 || src[0] != '\'' || src[$-1] != '\'')
+        throw new ParseException ("Invalid char: not 'x' or '\\x'");
+    T[] t = Utf.toString32 (src[1..$-1]);
+    if (t.length == 1)
+        return t[0];
+    else
+        throw new ParseException ("Invalid char: not one character");
+}
+
+// Bool
+T deserialize(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;  // skip leading zeros
+    if (src.length == pos && pos > 0)
+        return false;
+    if (src.length == pos + 1 && src[pos] == '1')
+        return true;
+    throw new ParseException ("Invalid bool: not true or false and doesn't evaluate to 0 or 1");
+}
+
+// Ints
+T deserialize(T : byte) (char[] src) {
+    return toTInt!(T) (src);
+}
+T deserialize(T : short) (char[] src) {
+    return toTInt!(T) (src);
+}
+T deserialize(T : int) (char[] src) {
+    return toTInt!(T) (src);
+}
+T deserialize(T : long) (char[] src) {
+    return toTInt!(T) (src);
+}
+T deserialize(T : ubyte) (char[] src) {
+    return toTInt!(T) (src);
+}
+T deserialize(T : ushort) (char[] src) {
+    return toTInt!(T) (src);
+}
+T deserialize(T : uint) (char[] src) {
+    return toTInt!(T) (src);
+}
+T deserialize(T : ulong) (char[] src) {
+    return toTInt!(T) (src);
+}
+debug (UnitTest) unittest {
+    assert (deserialize!(byte) ("-5") == cast(byte) -5);
+    // annoyingly, octal syntax differs from D (blame tango):
+    assert (deserialize!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]);
+}
+
+// Floats
+T deserialize(T : float) (char[] src) {
+    return toTFloat!(T) (src);
+}
+T deserialize(T : double) (char[] src) {
+    return toTFloat!(T) (src);
+}
+T deserialize(T : real) (char[] src) {
+    return toTFloat!(T) (src);
+}
+
+
+// Structs
+T deserialize(T) (char[] src) {
+    static assert (is(T == struct), "Unsupported type: "~typeof(T));
+    
+    src = Util.trim(src);
+    if (src.length < 2 || src[0] != '{' || src[$-1] != '}')
+        throw new ParseException ("Invalid struct: not { ... }");
+    
+    // cannot access elements of T.tupleof with non-const key, so use a type which can be
+    // accessed with a non-const key to store slices:
+    char[][T.tupleof.length] temp;
+    foreach (char[] pair; split (src[1..$-1])) {
+        uint i = 0;
+        while (i < pair.length) {   // advance to the ':'
+            char c = pair[i];
+            if (c == ':')
+                break;
+            // key must be an int so no need for string checks
+            ++i;
+        }
+        if (i >= pair.length)
+            throw new ParseException ("Invalid struct: encountered { ... KEY} (missing :DATA)");
+        
+        size_t k = deserialize!(size_t) (pair[0..i]);
+        // Note: could check no entry was already stored in temp.
+        temp[k] = pair[i+1..$];
+    }
+    T ret;
+    setStruct (ret, temp);
+    return ret;
+}
+//END deserialize templates
+
+//BEGIN Utility funcs
+/** Splits a string into substrings separated by '$(B ,)' with support for characters and strings
+ * containing escape sequences and for embedded arrays ($(B [...])).
+ *
+ * Params:
+ *     src A string to separate on commas. It shouldn't have enclosing brackets.
+ *
+ * Returns:
+ *     An array of substrings within src, excluding commas. Whitespace is not stripped and
+ *     empty strings may get returned.
+ *
+ * Remarks:
+ *     This function is primarily intended for as a utility function for use by the templates
+ *     parsing arrays and associative arrays, but it may be useful in other cases too. Hence the
+ *     fact no brackets are stripped from src.
+ */
+//FIXME foreach struct is more efficient
+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 ParseException ("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 (i > src.length)
+        throw new ParseException ("Unterminated quote (\' or \")");
+    
+    if (ret.length <= k)
+        ret.length = k + 1;
+    ret[k] = src[j..i];     // add final piece (i >= j)
+    return ret[0..k+1];
+}
+
+/* Templated read-int function to read (un)signed 1-4 byte integers.
+ *
+ * Actually a reimplementation of tango.text.convert.Integer toLong and parse functions.
+ */
+private TInt toTInt(TInt) (char[] src) {
+    const char[] INT_OUT_OF_RANGE = "Integer out of range";
+    bool sign;
+    uint radix, ate, ate2;
+    
+    // Trim off whitespace.
+    // NOTE: Cannot use tango.text.convert.Integer.trim to trim leading whitespace since it doesn't
+    // treat new-lines, etc. as whitespace which for our purposes is whitespace.
+    src = Util.trim (src);
+    
+    ate = cInt.trim (src, sign, radix);
+    if (ate == src.length)
+        throw new ParseException ("Invalid integer: no digits");
+    ulong val = cInt.convert (src[ate..$], radix, &ate2);
+    ate += ate2;
+    
+    if (ate < src.length)
+        throw new ParseException ("Invalid integer at marked character: \"" ~ src[0..ate] ~ "'" ~ src[ate] ~ "'" ~ src[ate+1..$] ~ "\"");
+    
+    if (val > TInt.max)
+        throw new ParseException (INT_OUT_OF_RANGE);
+    if (sign) {
+        long sval = cast(long) -val;
+        if (sval > TInt.min)
+            return cast(TInt) sval;
+        else throw new ParseException (INT_OUT_OF_RANGE);
+    }
+    return cast(TInt) val;
+}
+
+/* Basically a reimplementation of tango.text.convert.Float.toFloat which checks for
+ * whitespace before throwing an exception for overlong input. */
+private TFloat toTFloat(TFloat) (char[] src) {
+    // NOTE: As for toTInt(), this needs to strip leading as well as trailing whitespace.
+    src = Util.trim (src);
+    if (src == "")
+        throw new ParseException ("Invalid float: no digits");
+    uint ate;
+    
+    TFloat x = cFloat.parse (src, &ate);
+    return x;
+}
+
+/* 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 unEscapeChar (char c)
+{
+    // This code was generated:
+    if (c <= 'b') {
+        if (c <= '\'') {
+            if (c == '\"') {
+                return '\"';
+            } else if (c == '\'') {
+                return '\'';
+            }
+        } else {
+            if (c == '\\') {
+                return '\\';
+            } else if (c == 'a') {
+                return '\a';
+            } else if (c == 'b') {
+                return '\b';
+            }
+        }
+    } else {
+        if (c <= 'n') {
+            if (c == 'f') {
+                return '\f';
+            } else if (c == 'n') {
+                return '\n';
+            }
+        } else {
+            if (c == 'r') {
+                return '\r';
+            } else if (c == 't') {
+                return '\t';
+            } else if (c == 'v') {
+                return '\v';
+            }
+        }
+    }
+    
+    // if we haven't returned:
+    throw new ParseException ("Bad escape sequence: \\"~c);
+}
+
+// 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 ParseException ("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] = deserialize!(T) (element);
+        ++i;
+    }
+    return ret[0..i];
+}
+
+/** Set a struct's elements from an array.
+*
+* For a more generic version, see http://www.dsource.org/projects/tutorials/wiki/StructTupleof
+*/
+// NOTE: Efficiency? Do recursive calls get inlined?
+private void setStruct(S, size_t N, size_t i = 0) (ref S s, char[][N] src) {
+    static assert (is(S == struct), "Only to be used with structs.");
+    static assert (N == S.tupleof.length, "src.length != S.tupleof.length");
+    static if (i < N) {
+        if (src[i])
+            s.tupleof[i] = deserialize!(typeof(s.tupleof[i])) (src[i]);
+        setStruct!(S, N, i+1) (s, src);
+    }
+}
+//END Utility funcs
+
+debug (mdeUnitTest) {
+    import tango.util.log.Log : Log, Logger;
+    
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.file.deserialize");
+    }
+unittest {
+    // Utility
+    bool throws (void delegate() dg) {
+        bool r = false;
+        try {
+            dg();
+        } catch (Exception e) {
+            r = true;
+            logger.trace ("Exception caught: "~e.msg);
+        }
+        return r;
+    }
+    assert (!throws ({ int i = 5; }));
+    assert (throws ({ throw new Exception ("Test - this exception should be caught"); }));
+    
+    
+    // Associative arrays
+    char[][char] X = deserialize!(char[][char]) (`['a':"animal\n", 'b':['b','u','s','\n']]`);
+    char[][char] Y = ['a':cast(char[])"animal\n", 'b':['b','u','s','\n']];
+    
+    //FIXME: when the compiler's fixed: http://d.puremagic.com/issues/show_bug.cgi?id=1671
+    // 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 (compiler bug)
+    
+    assert (throws ({ deserialize!(int[int]) (`[1:1`); }));             // bad brackets
+    assert (throws ({ deserialize!(int[char[]]) (`["ab\":1]`); }));     // unterminated quote
+    assert (throws ({ deserialize!(int[char[]]) (`["abc,\a\b\c":1]`); }));    // bad escape seq.
+    assert (throws ({ deserialize!(int[char[]]) (`["abc"]`); }));       // no data
+    
+    
+    // Arrays
+    assert (deserialize!(double[]) (`[1.0,1.0e-10]`) == [1.0, 1.0e-10]);// generic array stuff
+    assert (deserialize!(double[]) (`[     ]`) == cast(double[]) []);   // empty array
+    assert (deserialize!(int[][]) (`[[1],[2,3],[]]`) == [[1],[2,3],[]]);// sub-array
+    assert (throws ({ deserialize!(int[]) (`[1,2`); }));                // bad brackets
+    assert (throws ({ deserialize!(int[][]) (`[[1]]]`); }));            // bad brackets
+    
+    // char[] and char conversions, with commas, escape sequences and multichar UTF8 characters:
+    assert (deserialize!(char[][]) (`[ ".\"", [',','\''] ,"!\b€" ]`) == [ ".\"".dup, [',','\''] ,"!\b€" ]);
+    assert (throws ({ deserialize!(char[]) ("\"\\\""); }));
+    assert (throws ({ deserialize!(char[]) (`['a'`); }));               // bad brackets
+    
+    // wchar[] and dchar[] conversions:
+    // The characters were pretty-much pulled at random from unicode tables.
+    // The last few cause some wierd (display only) effects in my editor.
+    assert (deserialize!(wchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"w);
+    assert (deserialize!(dchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"d);
+    
+    assert (deserialize!(ubyte[]) (`0x01F2aC`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]);    // ubyte[] special notation
+    assert (deserialize!(ubyte[]) (`[01 ,0xF2, 0xAC]`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]);    // ubyte[] std notation
+    assert (throws ({ deserialize!(ubyte[]) (`0x123`); }));             // digits not in pairs
+    assert (throws ({ deserialize!(ubyte[]) (`[2,5`); }));              // not [...] or 0x..
+    assert (throws ({ deserialize!(ubyte[]) (`0x123j`); }));
+    
+    
+    // char types
+    assert (deserialize!(char) ("'\\\''") == '\'');
+    assert (deserialize!(wchar) ("'X'") == 'X');
+    assert (deserialize!(dchar) ("'X'") == 'X');
+    assert (deserialize!(wchar) ("'£'") == '£');
+    assert (deserialize!(dchar) ("'£'") == '£');
+    assert (throws ({ deserialize!(char) ("'\\'"); }));
+    assert (throws ({ deserialize!(char) ("'£'"); }));        // non-ascii
+    assert (throws ({ deserialize!(char) ("''"); }));
+    assert (throws ({ deserialize!(char) ("'ab'"); }));
+    assert (throws ({ deserialize!(wchar) ("''"); }));
+    
+    
+    // bool
+    assert (deserialize!(bool[]) (`[true,false,01,00]`) == cast(bool[]) [1,0,1,0]);
+    assert (throws ({ deserialize!(bool) ("011"); }));
+    
+    
+    // ints
+    assert (deserialize!(byte) ("-5") == cast(byte) -5);
+    assert (deserialize!(int) ("-0x7FFFFFFF") == cast(int) -0x7FFF_FFFF);
+    // annoyingly, octal syntax differs from D (blame tango):
+    assert (deserialize!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]);
+    assert (throws ({ deserialize!(int) (""); }));
+    assert (throws ({ deserialize!(int) ("0x8FFFFFFF"); }));
+    assert (throws ({ deserialize!(uint) ("-1"); }));
+    assert (throws ({ deserialize!(uint) ("1a"); }));
+    
+    
+    // floats
+    assert (deserialize!(float) ("0.0") == 0.0f);
+    assert (deserialize!(double) ("-1e25") == -1e25);
+    assert (deserialize!(real) ("5.24e-269") == cast(real) 5.24e-269);
+    assert (throws ({ deserialize!(float) (""); }));
+    
+    
+    // structs
+    struct A {  int x = 5;  char y; }
+    struct B {  A a;    float b;   }
+    A a;    a.y = 'y';
+    assert (deserialize!(A) ("{ 1 : 'y' }") == a);
+    B b;    b.a = a;    b.b = 1.0f;
+    assert (deserialize!(B) (" {1:1.0,0: { 1 : 'y' } } ") == b);
+    assert (throws ({ deserialize!(A) (" 1:'x'}"); })); // bad braces
+    assert (throws ({ deserialize!(A) ("{ 1 }"); }));     // no :DATA
+    
+    
+    // unEscapeChar
+    assert (deserialize!(char[]) ("\"\\a\\b\\t\\n\\v\\f\\r\\\"\\\'\\\\\"") == "\a\b\t\n\v\f\r\"\'\\");
+    
+    logger.info ("Unittest complete.");
+}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/exception.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,48 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/// Base file exception classes
+module mde.file.exception;
+
+import mde.exception;
+
+/// Base exception for all file exceptions
+class fileException : mdeException
+{
+    char[] getSymbol () {
+        return super.getSymbol ~ ".file";
+    }
+    
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
+/** Exception for errors finding, opening, closing, reading or writing files,
+ * but not for parsing content. */
+class ioException : fileException
+{
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
+/** Exception for parsing, formatting and serializing content. */
+class parseException : fileException
+{
+    this (char[] msg) {
+        super(msg);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/DataSet.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,70 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/** This module contains the mergetag DataSet class, used for all reading and writing operations.
+ */
+module mde.file.mergetag.DataSet;
+
+// package imports
+public import mde.file.mergetag.iface.IDataSection;
+import mde.file.mergetag.DefaultData;
+
+
+/**************************************************************************************************
+ * Data class; contains a DataSection class instance for each loaded section of a file.
+ *
+ * Stored data is available for direct access via header and sec; all functions are just helper
+ * functions.
+ *
+ * Any class implementing IDataSection may be used to store data; by default a DefaultData class is
+ * used when reading a file. Another class may be used by creating the sections before reading the
+ * file or passing the reader a function to create the sections (see Reader.dataSecCreator).
+ *
+ * Could be a struct, except that structs are value types (not reference types).
+ */
+class DataSet
+{
+    DefaultData header;			/// Header section.
+    IDataSection[ID] sec;		/// Dynamic array of sections
+    
+    /// Template to return all sections of a child-class type.
+    T[ID] getSections (T : IDataSection) () {
+        T[ID] ret;
+        foreach (ID id, IDataSection s; sec) {
+            T x = cast(T) s;
+            if (x) ret[id] = x;	// if non-null
+        }
+        return ret;
+    }
+}
+
+debug (mdeUnitTest) {
+    import tango.util.log.Log : Log, Logger;
+
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.file.mergetag.DataSet");
+    }
+    
+    unittest {	// Only covers DataSet really.
+        DataSet ds = new DataSet;
+        ds.sec[cast(ID)"test"] = new DefaultData;
+        assert (ds.getSections!(DefaultData)().length == 1);
+        ds.sec[cast(ID)"test"].addTag ("char[]",cast(ID)"T"," \"ut tag 1 \" ");
+        assert (ds.getSections!(DefaultData)()[cast(ID)"test"].Arg!(char[])[cast(ID)"T"] == "ut tag 1 ");
+    
+        logger.info ("Unittest complete.");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/DefaultData.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,181 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/** This module contains the DefaultData class, and some notes possibly useful for implementing
+* other types of DataSection.
+*/
+module mde.file.mergetag.DefaultData;
+
+public import mde.file.mergetag.iface.IDataSection;
+import mde.file.serialize;
+
+
+/*************************************************************************************************
+ * Default DataSection class.
+ * 
+ * Supported types are given by dataTypes.
+ * 
+ * Currently DefaultData is only used for headers, and thus the list of supported types has been
+ * reduced to just those used in headers. Load order is HIGH_LOW, i.e. existing entries aren't
+ * overwritten.
+ *************************************************************************************************/
+/* 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).
+*
+* 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 : IDataSection
+{
+    //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;
+            ret ~= indent(indents);
+            foreach (c; consts) {
+                ret ~= "if (" ~ var ~ " == \"" ~ c ~ "\") {\n" ~
+                    //indent(indents+1) ~ varName(c) ~ "[id] = parseTo!(" ~ c ~ ") (dt);\n" ~
+                    indent(indents+1) ~ "if ((id in "~varName(c)~") is null)\n" ~
+                    indent(indents+2) ~ varName(c)~"[id] = parseTo!(" ~ c ~ ") (dt);\n" ~
+                indent(indents) ~ "} else ";
+            }
+            ret = ret[0..$-6] ~ '\n';  // remove last else
+            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 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;
+    * }
+    *
+    * type y = Arg!(type).Arg;	// example of use
+    * --------------------
+    * 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.
+    */
+    /+ All types previously supported. Most of these weren't used.
+    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[]"];
+    +/
+    const char[][] dataTypes = ["char[]", "char[][]"];
+    
+    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) {	/// Supports all types listed in dataTypes.
+        mixin (writeVars ());
+    }
+    
+    /* These make no attempt to check Arg is valid.
+    * But if the symbol doesn't exist the complier will throw an error anyway, e.g.:
+    * Error: identifier '_boolAA' is not defined
+    */
+    template ArgName (T : T[]) {
+        const char[] ArgName = ArgName!(T)~`A`;
+    }
+    template ArgName (T) {
+        const char[] ArgName = `_`~T.stringof;
+    }
+    template Arg(T) {
+        mixin(`alias `~ArgName!(T)~` Arg;`);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/Reader.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,520 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/**************************************************************************************************
+ * This module contains all reading functions, for both binary and text MergeTag files.
+ *************************************************************************************************/
+module mde.file.mergetag.Reader;
+
+// package imports
+public import mde.file.mergetag.iface.IReader;
+import mde.file.mergetag.DataSet;
+import mde.file.mergetag.DefaultData;
+import mde.file.mergetag.exception;
+import mde.file.mergetag.internal;
+
+import tango.core.Exception;
+
+// tango imports
+import tango.io.FilePath;
+import tango.io.UnicodeFile;
+import Util = tango.text.Util;
+import ConvInt = tango.text.convert.Integer;
+//import tango.util.collection.model.View : View;
+import tango.util.collection.HashSet : HashSet;
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.file.mergetag.Reader");
+}
+
+// TODO: allow compressing with zlib for both binary and text? (.mtz, .mtt, .mtb extensions)
+
+/** Make an IReader class.
+*
+* Create an appropriate reader: MTTReader or MTBReader.
+*
+* Throws:
+*  $(TABLE
+*  $(TR $(TH Exception) $(TH Thrown when))
+*  $(TR $(TD MTFileIOException) $(TD When extension given is neither mtt nor mtb))
+*  )
+*
+*/
+IReader makeReader (FilePath path, DataSet ds = null, bool rdHeader = false) {
+    if      (path.ext == "mtb") return new MTBReader (path, ds, rdHeader);
+    else if (path.ext == "mtt") return new MTTReader (path, ds, rdHeader);
+    else throw new MTFileIOException ("Invalid mergetag extension");
+}
+
+/** Resolve a file path.
+ *
+ * Tries adding both ".mtt" and ".mtb" extensions, returning whichever exists (the most recently
+ * modified if both exist), or returns null if neither exist. */
+FilePath findFile (char[] path) {
+    if (path is null) return null;
+    
+    FilePath tPath = new FilePath (path ~ ".mtt");
+    FilePath bPath = new FilePath (path ~ ".mtb");
+        
+    bool bPathExists = bPath.exists;
+        
+    if (tPath.exists) {
+        if (bPathExists) {
+                // take the latest version (roughly speaking...)
+            return (tPath.modified > bPath.modified ? tPath : bPath);
+        } else return tPath;
+    } else {
+        if (bPathExists) return bPath;
+        else return null;
+    }
+}
+
+/**
+ * Class for reading a mergetag text file.
+ * 
+ * Use as:
+ * -----------------------
+ * IReader foo;
+ * try {
+ *   foo = new MTTReader("foo.mtt");
+ *   foo.read();
+ * }
+ * catch (MTException) {}
+ * // get your data from foo.dataset.
+ * -----------------------
+ *
+ * Throws:
+ *  $(TABLE
+ *  $(TR $(TH Exception) $(TH Thrown when))
+ *  $(TR $(TD MTFileIOException) $(TD An error occurs while opening the file))
+ *  $(TR $(TD MTFileFormatException) $(TD The file doesn't start with a recognised header/version))
+ *  $(TR $(TD MTSyntaxException) $(TD A file syntax error occurs))
+ *  $(TR $(TD MTException) $(TD An unexpected error occurs))
+ *  )
+ * Note that all exceptions extend MTException and when any exception is thrown the class is
+ * rendered unusable: any subsequent calls to read will be ignored.
+ *
+ * Threading: Separate instances of Reader should be thread-safe provided access to the same
+ * dataset is synchronized; i.e. no two readers refering to the same dataset should run
+ * simultaneously. (The Reader class could be made thread-safe w.r.t. datasets, but
+ * performance-wise I doubt it would be worth it.)
+ * Do not run a single instance of Reader in multiple threads simultaneously.
+ */
+class MTTReader : IReader
+{
+//BEGIN DATA
+    /** Get or set the DataSet
+    *
+    * A container for all read data.
+    *
+    * This may be accessed from here; however it may be preferable to use an external reference
+    * (passed to the class on initialisation).
+    */
+    DataSet dataset () {	return _dataset;	}
+    void dataset (DataSet ds)	/// ditto
+    {	_dataset = ds;	}
+    
+    /** A delegate for creating new DataSections within the dataset.
+    *
+    * Allows a user-made class to be used in the DataSet instead of DefaultData (used if no
+    * dataSecCreator exists). Also allows an existing class instance to be used instead of a new
+    * one.
+    *
+    * This works by supplying a function which returns a reference to an instance of a class
+    * implementing IDataSection. The function is passed the ID of the new section and may use this
+    * to use different IDataSection classes for different sections.
+    *
+    * The function may also return null, in which case the section will be skipped. In the version
+    * of read taking a set of sections to read, the section will not be marked as read and may
+    * still be read later (assuming dataSecCreator returns non-null). However, in the version of
+    * read not taking the set argument, all sections are set as read regardless, and the section
+    * cannot be read later.
+    */
+    void dataSecCreator (IDataSection delegate (ID) dSC) {
+        _dataSecCreator = dSC;
+    }
+    
+private:
+    // Non-static symbols:
+    final char[] ErrFile;		// added after ErrInFile to do the same without the "in " bit.
+    final char[] ErrInFile;		// something like "in \"path/file.mtt\""
+    
+    final char[] fbuf;			// file is read into this
+    MTFormatVersion.VERS fileVer = MTFormatVersion.VERS.INVALID;	// Remains INVALID until set otherwise by CTOR.
+    
+    IDataSection delegate (ID) _dataSecCreator = null;   // see property setter above
+    
+    size_t endOfHeader;
+    bool allRead = false;		// true if endOfHeader == fbuf.length or read([]) has run
+    bool fatal = false;			// a fatal file error occured; don't try to recover
+    /* If the file is scanned for sections, the starting position of all sections are stored
+    * in secTable. If this is empty, either no sections exist (and endOfHeader == fbuf.length)
+    * or a section scan has not been run (read() with no section names doesn't need to do so).
+    */
+    struct SecMD {	// sec meta data
+        static SecMD opCall (size_t _pos, bool _read) {
+            SecMD ret;
+            ret.pos = _pos;
+            ret.read = _read;
+            return ret;
+        }
+        size_t pos;			// position to start reading
+        bool read;			// true if already read
+    }
+    SecMD [ID] secTable;
+    
+    DataSet _dataset;
+//END DATA
+    
+//BEGIN METHODS: CTOR / DTOR
+    /** Tries to open file path and read it into a buffer.
+     *
+     * Params:
+     * path     = The name or FilePath of the file to open.
+     *     Standard extensions are .mtt and .mtb for text and binary files respectively.
+     * ds       = If null create a new DataSet, else use existing DataSet ds and merge read
+     *     data into it.
+     * rdHeader = If true, read the header like a standard section. Doesn't read the header by
+     *     default since if it's not requested it's likely not wanted.
+     *
+     * Memory:
+     * This currently works by loading the whole file into memory at once. This should be fine most
+     * of the time, but could potentially be a problem. Changing this would mean significantly
+     * changes to the way the code works.
+     */
+    /* Ideas for implementing a partial-loading memory model:
+     * Use a conduit directly.
+     * Use a fiber to do the parsing; let it switch back when it runs out of memory.
+     * Redesign the code so it never needs to look backwards in the buffer?
+     *
+     * Major problem: reading only some sections and keeping references to other sections
+     * would no longer be possible.
+     */
+    public this (char[] path, DataSet ds = null, bool rdHeader = false) {
+        this (new FilePath (path), ds, rdHeader);
+    }
+    /** ditto */
+    public this (FilePath path, DataSet ds = null, bool rdHeader = false) {
+        // Create a dataset or use an existing one
+        if (ds !is null) _dataset = ds;
+        else _dataset = new DataSet();
+        
+        // Open & read the file
+        try {	// Supports unicode files with a BOM; defaults to UTF8 when there isn't a BOM:
+            scope file = new UnicodeFile!(char) (path, Encoding.Unknown);
+            fbuf = cast(char[]) file.read();
+        } catch (Exception e) {
+            throwMTErr ("Error reading file: " ~ e.msg, new MTFileIOException);
+        }
+        // Remember the file name so that we can report errors (somewhat) informatively:
+        ErrFile = path.path ~ path.file;
+        ErrInFile = " in \"" ~ ErrFile ~ '"';
+        
+        // Version checking & matching header section tag:
+        if (fbuf.length < 6 || fbuf[0] != '{' || fbuf[1] != 'M' || fbuf[2] != 'T' || fbuf[5] != '}')
+            throwMTErr("Not a valid MergeTag text file" ~ ErrInFile, new MTFileFormatException);
+        fileVer = MTFormatVersion.parseString (fbuf[3..5]);
+        if (fileVer == MTFormatVersion.VERS.INVALID)
+            throwMTErr("Unrecognised MergeTag version: MT" ~ fbuf[3..5] ~ ErrInFile, new MTFileFormatException);
+        
+        // Header reading/skipping:
+        if (rdHeader) {	// only bother actually reading it if it was requested
+            // If already existing, merge; else create a new DefaultData.
+            if (!_dataset.header) _dataset.header = new DefaultData;
+            endOfHeader = parseSection (6, cast(IDataSection) _dataset.header);
+        }
+        else endOfHeader = parseSection (6,null);
+    }
+//END METHODS: CTOR / DTOR
+    
+//BEGIN METHODS: PUBLIC
+    /** Scans for sections if not already done and returns a list of IDs.
+    *
+    * Won't work (will return an empty array) if all sections have already been read without
+    * scanning for sections.
+    */
+    public ID[] getSectionNames () {
+        if (fatal) return [];
+        if (!secTable.length) read([]);     // scan for sections
+        return secTable.keys;
+    }
+    
+    /** Reads (some) sections of the file into data. Note that sections will never be _read twice.
+    *
+    * To be more accurate, the file is copied into a buffer by this(). read() then parses the
+    * contents of this buffer, and stores the contents in dataset.
+    *
+    * Each section read is stored in a DataSection class. By default this is an instance of
+    * DefaultData; this can be customised (see dataSecCreator).
+    *
+    * If secSet is provided, reading is restricted to sections given in secSet, otherwise all
+    * sections are read. Sections given in secSet but not found in the file are not reported as an
+    * error. Suggested: supply a HashSet!(uint) as the View!(ID). An ArrayBag!(ID) as used is not a
+    * good choice, except that in this case it's empty.
+    *
+    * Merging:
+    * Where a section already exists in the DataSet (when either the section is given more than
+    * once in the file, or it was read from a different file by another reader) it is merged.
+    * Entries already in the DataSet take priority.
+    *
+    * Performance:
+    * Note that loading only desired sections like this still parses the sections not
+    * read (although it does not try to understand the type or data fields), so there is only a
+    * small performance advantage to this where other sections do exist in the file. There is also
+    * some overhead in only partially reading the file to keep track of where other sections are so
+    * that the entire file need not be re-read if further (or all remaining) sections are read
+    * later.
+    */
+    public void read () {
+        if (secTable.length) {
+            foreach (ID id, ref SecMD smd; secTable) {
+                if (!smd.read) {
+                    IDataSection ds = getOrCreateSec (id);
+                    parseSection (smd.pos, ds);
+                    // allRead is set true so there's no point setting smd.read = true
+                }
+            }
+        } else {					// this time we don't need to use secTable
+            for (size_t pos = endOfHeader; pos < fbuf.length;) {
+                ID id = fbufReadSecMarker (pos);
+                IDataSection ds = getOrCreateSec (id);
+                pos = parseSection (pos, ds);
+            }
+        }
+        
+        allRead = true;
+    }
+    /** ditto */
+    public void read (ID[] secSet) {
+        HashSet!(ID) hs = new HashSet!(ID);
+        foreach (id; secSet) hs.add(id);
+        read (hs);
+    }
+    /** ditto */
+    public void read (View!(ID) secSet) {
+        if (allRead || fatal) return;			// never do anything in either case
+        
+        if (secTable.length) {
+            foreach (ID id; secSet) {
+                SecMD* psmd = id in secTable;
+                if (psmd && !psmd.read) {		// may not exist
+                    IDataSection ds = getOrCreateSec (id);
+                    parseSection (psmd.pos, ds);
+                    if (ds !is null) psmd.read = true;  // getOrCreateSec may return null
+                }
+            }
+        } else {
+            for (size_t pos = endOfHeader; pos < fbuf.length;) {
+                ID id = fbufReadSecMarker (pos);
+                secTable[id] = SecMD(pos,false);	// add to table
+                if (secSet.contains(id)) {
+                    IDataSection ds = getOrCreateSec (id);
+                    pos = parseSection (pos, ds);
+                    if (ds !is null) secTable[id].read = true;
+                } else {
+                    pos = parseSection (pos, null);     // skip section
+                }
+            }
+        }
+    }
+//END METHODS: PUBLIC
+    
+//BEGIN METHODS: PRIVATE
+    /* Utility function for read
+    * Look for a section; return it if it exists otherwise create a new section:
+    *   use _dataSecCreator if it exists or just create a DefaultData if not.
+    * However if _dataSecCreator returns null don't add it to the dataset.
+    */
+    private IDataSection getOrCreateSec (ID id) {
+        IDataSection* i = id in _dataset.sec;
+        if (i) return *i;
+        else {
+            IDataSection s;
+            if (_dataSecCreator !is null) s = _dataSecCreator(id);
+            else s = new DefaultData;
+            if (s !is null) _dataset.sec[id] = s;
+            return s;
+        }
+    }
+    
+    /* Reads a section, starting from index pos, finishing at the next section marker (returning
+    the position of the start of the marker). pos should start after the section marker.
+    
+    After analysing tags, the function passes the type, ID and data to addTag.
+    
+    NOTE: from performance tests on indexing char[]'s and dereferencing char*'s, the char*'s are
+    slightly faster, but a tiny difference isn't worth the extra effort/risk of using char*'s.
+    */
+    private size_t parseSection (size_t pos, IDataSection dsec) {
+        debug scope (failure)
+                logger.trace ("MTTReader.parseSection: failure");
+        /* Searches fbuf starting from start to find one of <=>| and stops at its index.
+    
+        If quotable then be quote-aware for single and double quotes.
+        Note: there's no length restriction for the content of the quote since it could be a single
+        non-ascii UTF-8 char which would look like several chars.
+        */
+        void fbufLocateDataTagChar (ref size_t pos, bool quotable) {
+            while (true) {
+                fbufIncrement (pos);
+                
+                if ((fbuf[pos] >= '<' && fbuf[pos] <= '>') || fbuf[pos] == '|') return;
+                else if (quotable) {
+                    char c = fbuf[pos];
+                    if (c == '\'' || c == '"') {
+                        fbufIncrement(pos);
+                        while (fbuf[pos] != c) {
+                            if (fbuf[pos] == '\\') ++pos;	// escape seq.
+                            fbufIncrement(pos);
+                        }
+                    }
+                }
+            }
+        }
+        
+        // Used to ignore a tag (if it starts !< or !{ or should otherwise be ignored):
+        bool comment = false;
+        for (; pos < fbuf.length; ++pos) {
+            if (Util.isSpace(fbuf[pos])) continue;	// whitespace
+            else if (fbuf[pos] == '<') {		// data tag
+                char[] ErrDTAG = "Bad data tag format: not <type|id=data>" ~ ErrInFile;
+                
+                // Type section of tag:
+                size_t pos_s = pos + 1;
+                fbufLocateDataTagChar (pos, false);	// find end of type section
+                if (fbuf[pos] != '|') throwMTErr (ErrDTAG, new MTSyntaxException);
+                char[] type = fbuf[pos_s..pos];
+                
+                // ID section of tag:
+                pos_s = pos + 1;
+                fbufLocateDataTagChar (pos, false);	// find end of type section
+                if (fbuf[pos] != '=') throwMTErr (ErrDTAG, new MTSyntaxException);
+                ID tagID = cast(ID) fbuf[pos_s..pos];
+                
+                // Data section of tag:
+                pos_s = pos + 1;
+                fbufLocateDataTagChar (pos, true);      // find end of data section
+                if (fbuf[pos] != '>') throwMTErr (ErrDTAG, new MTSyntaxException);
+                char[] data = fbuf[pos_s..pos];
+                
+                if (!comment && dsec !is null) {
+                    type = Util.trim(type);
+                    try {
+                        dsec.addTag (type, tagID, data);
+                    }
+                    catch (TextException e) {
+                        logger.error ("TextException while reading " ~ ErrFile ~ ":");	// following a parse error
+                        logger.error (e.msg);
+                        logger.error ("Tag ignored: <"~type~"|"~tagID~"="~data~">");
+                        // No throw: tag is just ignored
+                    }
+                    catch (Exception e) {
+                        logger.error ("Unknown error occured" ~ ErrInFile ~ ':');
+                        logger.error (e.msg);
+                        throwMTErr (e.msg);             // Fatal to Reader
+                    }
+                } else comment = false;			// cancel comment status now
+            }
+            else if (fbuf[pos] == '{') {
+                if (comment) {				// simple block comment
+                    uint depth = 0;			// depth of embedded comment blocks
+                    while (true) {
+                        fbufIncrement (pos);
+                        if (fbuf[pos] == '}') {
+                            if (depth == 0) break;
+                            else --depth;
+                        } else if (fbuf[pos] == '{')
+                            ++depth;
+                    }
+                    comment = false;			// end of this comment
+                } else {
+                    return pos;				// next section coming up; we are done
+                }
+            }
+            else if (fbuf[pos] == '!') {		// possibly a comment; check next char
+                comment = true;				// starting a comment (or an error)
+                					// variable is reset at end of comment
+            } else					// must be an error
+            throwMTErr ("Invalid character (or sequence starting \"!\") outside of tag" ~ ErrInFile, new MTSyntaxException);
+        }
+        // if code execution reaches here, we're at EOF
+        // possible error: last character was ! (but don't bother checking since it's inconsequential)
+        return pos;
+    }
+    
+    /* Parses fbuf for a section marker. Already knows fbuf[pos] == '{'.
+    */
+    private ID fbufReadSecMarker (ref size_t pos) {
+        // at this point pos is whatever a parseSection run returned
+        // since we haven't hit EOF, fbuf[pos] MUST be '{' so no need to check
+        fbufIncrement(pos);
+        
+        size_t start = pos;
+        for (; pos < fbuf.length; ++pos)
+            if (fbuf[pos] == '}' || fbuf[pos] == '{') break;
+        
+        if (pos >= fbuf.length || fbuf[pos] != '}')
+            throwMTErr ("Bad section tag format: not {id}" ~ ErrInFile, new MTSyntaxException);
+        
+        ID id = cast(ID) fbuf[start..pos];
+        fbufIncrement(pos);
+        return id;
+    }
+    
+    /* Increments pos and checks it hasn't hit fbuf.length . */
+    private void fbufIncrement(ref size_t pos) {
+        ++pos;
+        if (pos >= fbuf.length) throwMTErr("Unexpected EOF" ~ ErrInFile, new MTSyntaxException);
+    }
+    
+    private void throwMTErr (char[] msg, MTException exc = new MTException) {
+        fatal = true;	// if anyone catches the error and tries to do anything --- we're dead now
+        logger.error (msg);	// report the error
+        throw exc;		// and signal our error
+    }
+//END METHODS: PRIVATE
+}
+
+
+/**
+* Class for reading a mergetag text file.
+*
+* Currently only a dummy class: a MTNotImplementedException will be thrown if created.
+*/
+class MTBReader : IReader
+{
+    public this (char[] path, DataSet ds = null, bool rdHeader = false) {
+        this (new FilePath (path), ds, rdHeader);
+    }
+    public this (PathView path, DataSet ds = null, bool rdHeader = false) {
+        throw new MTNotImplementedException;
+    }
+        
+    DataSet dataset () {                /// Get the DataSet
+        return null;
+    }
+    void dataset (DataSet) {}           /// Set the DataSet
+    
+    void dataSecCreator (IDataSection delegate (ID)) {} /// Set the dataSecCreator
+    
+    ID[] getSectionNames () {           /// Get identifiers for all sections
+        return [];
+    }
+    void read () {}                     /// Commence reading
+    void read (ID[] secSet) {}          /// ditto
+    void read (View!(ID) secSet) {}     /// ditto
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/Writer.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,279 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/**************************************************************************************************
+ * This module contains all writing functions, for both binary and text MergeTag files.
+ *
+ * Files can be written in a text or binary form; binary is faster and smaller while text allows
+ * editing with an ordinary text editor. TextWriter and BinaryWriter are the main classes, both of
+ * which implement the interface IWriter. DualWriter is another class implementing IWriter, which
+ * contains a private instance of a TextWriter and a BinaryWriter and implements all methods in the
+ * interface simply by chaining the appropriate method from each of these classes, thus performing
+ * two writes at once.
+ *
+ * Any of these three classes may be used directly, or makeWriter may be invoked to create an
+ * instance of the appropriate class.
+ *************************************************************************************************/
+ module mde.file.mergetag.Writer;
+
+// package imports
+public import mde.file.mergetag.iface.IWriter;
+import mde.file.mergetag.DataSet;
+import mde.file.mergetag.internal;
+import mde.file.mergetag.exception;
+
+// tango imports
+import tango.core.Exception;
+import tango.io.FileConduit;
+import tango.io.Buffer : Buffer, IBuffer;
+import tango.io.Print : Print;
+import convInt = tango.text.convert.Integer;
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.file.mergetag.Writer");
+}
+
+
+/** Method to create and return either a MTTWriter or a MTBWriter.
+ *
+ * Has two modes of operation: if method is FromExtension, examines the existing extension and
+ * creates a MTT/MTB writer if the extension is mtt or mtb (throwing if not).
+ *
+ * Otherwise, writing format is determined directly by method, and appropriate extensions are
+ * added to the file name without checking for an existing extension.
+ *
+ * Params:
+ *  path = File path
+ *  dataset = Dataset passed to Writer to write from (if null, must be set before write() is called)
+ *  method = $(TABLE
+ *      $(TR $(TH Value)            $(TH Writer returned)       $(TH Suffix added))
+ *      $(TR $(TD FromExtension)    $(TD MTBWriter or MTTWriter)$(TD $(I none)))
+ *      $(TR $(TD Binary)           $(TD MTBWriter)             $(TD .mtb))
+ *      $(TR $(TD Text)             $(TD MTTWriter)             $(TD .mtt))
+ *      $(TR $(TD Both)             $(TD DualWriter)            $(TD .mtb / .mtt))
+ *  )
+ *
+ * Throws:
+ *  MTFileFormatException if neither test can deduce the writing method, the supplied writing
+ *  method is invalid or the determined/supplied method is not yet implemented.
+ *
+ * Use as:
+ * -----------------------
+ * DataSet dataset; // contains data to write
+ * IWriter foo;
+ * try {
+ *   foo = makeWriter(...);
+ *   foo.write();
+ * }
+ * catch (MTException) {}
+ * -----------------------
+ * Where the makeWriter line has one of the following forms:
+ * -----------------------
+ *   foo = makeWriter("foo.mtt", dataset);
+ *   foo = makeWriter("foo", dataset, WriterMethod.Text);
+ * -----------------------
+ *
+ * Throws:
+ *  MTFileFormatException if unable to determine writing format or use requested format.
+ */
+//FIXME: separate functions for separate functionality, like in Reader?
+IWriter makeWriter (char[] path, DataSet dataset = null,
+                    WriterMethod method = WriterMethod.FromExtension) {
+    if (method == WriterMethod.FromExtension) {
+        if (path.length > 4 && path[$-4..$] == ".mtt")
+            return new MTTWriter (path, dataset);
+        else if (path.length > 4 && path[$-4..$] == ".mtb")
+            return new MTBWriter (path, dataset);
+        else {
+            logger.error ("Unable to determine writing format: text or binary");
+            throw new MTFileFormatException;
+        }
+    }
+    else {
+        if (method == WriterMethod.Binary) return new MTBWriter (path~".mtb", dataset);
+        else if (method == WriterMethod.Text) return new MTTWriter (path~".mtt", dataset);
+        else if (method == WriterMethod.Both) return new DualWriter (path, dataset);
+        else throw new MTFileFormatException;
+    }
+}
+
+
+/**
+ * Class to write a dataset to a file.
+ *
+ * Files are only actually open for writing while the write() method is running.
+ *
+ * Throws:
+ *  $(TABLE
+ *  $(TR $(TH Exception) $(TH Thrown when))
+ *  $(TR $(TD MTNoDataSetException) $(TD No dataset is available to write from))
+ *  $(TR $(TD MTFileIOException) $(TD An error occurs while attemting to write the file))
+ *  $(TR $(TD MTException) $(TD An unexpected error occurs))
+ *  )
+ * Note that all exceptions extend MTException; unlike Reader exceptions don't block further calls.
+ */
+class MTTWriter : IWriter
+{
+//BEGIN DATA
+    /// Get or set the DataSet (i.e. the container from which all data is written).
+    DataSet dataset () {	return _dataset;	}
+    void dataset (DataSet ds)	/// ditto
+    {	_dataset = ds;	}
+        
+    
+private:
+    // taken from tango.io.Console, mostly to make sure notepad can read our files:
+    version (Win32)
+        const char[] Eol = "\r\n";
+    else
+        const char[] Eol = "\n";
+
+    /* The container where data is written from. */
+    DataSet _dataset;
+    
+    char[] _path;
+//END DATA
+    
+//BEGIN CTOR / DTOR
+    /** Prepares to open file path for writing.
+    *
+    * The call doesn't actually execute any code so cannot fail (unless out of memory).
+    *
+    * Params:
+    * path = The name of the file to open.
+    *     Standard extensions are .mtt and .mtb for text and binary files respectively.
+    * dataset_ = If null create a new DataSet, else use existing DataSet *dataset_ and merge read
+    *     data into it.
+    */
+    public this (char[] path, DataSet ds = null) {
+        _path = path;
+        _dataset = ds;
+    }
+//END CTOR / DTOR
+    
+    /** Writes the header and all DataSections.
+     *
+     * Firstly writes the header unless it has already been written. Then writes all DataSections
+     * to the file. Thus if write is called more than once with or without changing the DataSet the
+     * header should be written only once. This behaviour could, for instance, be used to write
+     * multiple DataSets into one file without firstly merging them. Note that this behaviour may
+     * be changed when binary support is added.
+     */
+    public void write ()
+    {
+        if (!_dataset) throwMTErr ("write(): no Dataset available to write from!", new MTNoDataSetException ());
+        
+        try {
+            FileConduit conduit;	// actual conduit; don't use directly when there's content in the buffer
+            IBuffer buffer;		// write strings directly to this (use opCall(void[]) )
+            
+            // Open a conduit on the file:
+            conduit = new FileConduit (_path, FileConduit.WriteCreate);
+            scope(exit) conduit.close();
+            
+            buffer = new Buffer(conduit);	// And a buffer
+            scope(exit) buffer.flush();
+            
+            // Write the header:
+            buffer ("{MT" ~ MTFormatVersion.CurrentString ~ "}" ~ Eol);
+            if (_dataset.header !is null) writeSection (buffer, _dataset.header);
+        
+            // Write the rest:
+            foreach (ID id, IDataSection sec; _dataset.sec) {
+                writeSectionIdentifier (buffer, id);
+                writeSection (buffer, sec);
+            }
+        
+            buffer.flush();
+            
+        }
+        catch (IOException e) {
+            throwMTErr ("Error writing to file: " ~ e.msg, new MTFileIOException);
+        }
+        catch (Exception e) {
+            throwMTErr ("Unexpected exception when writing file: " ~ e.msg);
+        }
+    }
+        
+    private void writeSectionIdentifier (IBuffer buffer, ID id) {
+        buffer ("{" ~ cast(char[])id ~ "}" ~ Eol);
+    }
+    
+    private void writeSection (IBuffer buffer, IDataSection sec) {
+        void writeItem (char[] tp, ID id, char[] dt) {	// actually writes an item
+            buffer ("<" ~ tp ~ "|" ~ cast(char[])id ~"=" ~ dt ~ ">" ~ Eol);
+        }
+        sec.writeAll (&writeItem);
+        
+        buffer (Eol);			// blank line at end of each section
+    }
+    
+    private void throwMTErr (char[] msg, Exception exc = new MTException) {
+        logger.error (msg);		// report the error
+        throw exc;			// and signal our error
+    }
+}
+
+/*
+* Implement MTBWriter (and move both writers to own modules?).
+*/
+class MTBWriter : IWriter {
+    public this (char[] path, DataSet ds = null) {
+        throw new MTNotImplementedException;
+        
+        /+_path = path;
+        _dataset = ds;+/
+    }
+    
+    DataSet dataset () {
+        return null;
+    }
+    void dataset (DataSet) {}
+    
+    void write () {}
+}
+
+/* Basic implementation for mtt only.
+*
+*Implement std CTORs which add extensions to each filename and extra CTORs which take two filenames.
+*/
+class DualWriter : IWriter {
+    /** The individual writers.
+    *
+    * Potentially could be used directly, but there should be no need. */
+    MTTWriter mtt;
+    //MTBWriter mtb;  /** ditto */
+    
+    public this (char[] path, DataSet ds = null) {
+        mtt = new MTTWriter (path~".mtt", ds);
+    }
+    
+    DataSet dataset () {
+        return mtt.dataset;
+    }
+    void dataset (DataSet ds) {
+        mtt.dataset = ds;
+    }
+    
+    /** Write.
+    *
+    * Write text then binary, so the mtb file will be the most recent.
+    */
+    void write () {
+        mtt.write();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/exception.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,91 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/*******************************************
+ * Contains exception classes for MergeTag.
+ *
+ * Publically imports mde.exception.
+ ******************************************/
+module mde.file.mergetag.exception;
+
+public import mde.exception;
+
+/// Base MergeTag exception class.
+class MTException : mdeException {
+    char[] getSymbol () {
+        return super.getSymbol ~ ".mergetag";
+    }
+    
+    this (char[] msg) {
+        super(msg);
+    }
+    this () {   // Only called when an unexpected exception/error occurs
+        super ("Unknown exception");
+    }
+}
+
+/** Thrown on file IO errors. */
+class MTFileIOException : MTException {
+    this () {
+        super ("File IO exception");
+    }
+    this (char[] msg) {
+        super (msg);
+    }
+}
+
+/** Thrown on unknown format errors; when reading or writing and the filetype cannot be guessed. */
+class MTFileFormatException : MTException {
+    this () {
+        super ("File format exception");
+    }
+}
+
+/** Thrown on syntax errors when reading; bad tags or unexpected EOF. */
+class MTSyntaxException : MTException {
+    this () {
+        super ("Syntax exception");
+    }
+}
+
+/** Thrown by addTag (in classes implementing IDataSection) when a data parsing error occurs
+* (really just to make whoever called addTag to log a warning saying where the error occured). */
+class MTaddTagParseException : MTException {
+    this () {
+        super ("Parse exception within addTag");
+    }
+}
+
+/+
+/// Thrown by TypeView.parse on errors.
+class MTBadTypeStringException : MTException {
+    this () {}
+}
++/
+
+/// Thrown by *Writer.write.
+class MTNoDataSetException : MTException {
+    this () {
+        super ("No dataset");
+    }
+}
+
+/// Thrown when attempting to use an unimplemented part of the package
+/// Really, just until MTB stuff is implemented
+class MTNotImplementedException : MTException {
+    this () {
+        super ("Functionality not implemented!");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/iface/IDataSection.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,68 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/** This module contains the interface IDataSection used by DataSet.
+*
+* It has been given its own module to avoid cyclic dependancies and separate out the functionality
+* of mergetag.
+*
+* Also some base mergetag symbols have been moved here.
+*/
+module mde.file.mergetag.iface.IDataSection;
+
+/** Typedef for data & section indexes.
+*
+* Make it an alias, there doesn't appear to be any point having it as a typedef. */
+alias char[] ID;
+
+/**
+ * Interface for data storage classes, generally called DataSections, which contain all data-tags
+ * loaded from a single section of a file.
+ *
+ * A class implementing this may implement the addTag function to do whatever it likes with the
+ * data passed. DefaultData is one implementation which separates this data out into supported
+ * types and stores it appropriately (allowing merging with existing entries by keeping whichever
+ * tag was last loaded), while ignoring unsupported types. A different
+ * implementation could filter out the tags desired and use them directly, and ignore the rest.
+ *
+ * The mde.mergetag.parse.parseTo module provides a useful set of templated functions to
+ * convert the data accordingly. It is advised to keep the type definitions as defined in the file-
+ * format except for user-defined types, although this isn't necessary for library operation
+ * (addTag and writeAll are solely responsible for using and setting the type, ID and data fields).
+ *
+ * Another idea for a DataSection class:
+ * Use a void*[ID] variable to store all data (may also need a type var for each item).
+ * addTag should call a templated function which calls parse then casts to a void* and stores the data.
+ * Use a templated get(T)(ID) method which checks the type and casts to T.
+ */
+interface IDataSection
+{
+    /** Delegate passed to writeAll. */
+    typedef void delegate (char[],ID,char[]) ItemDelg;
+    
+    /** Handles parsing of data items for all recognised types.
+     *
+     * Should ignore unsupported types/unwanted tags.
+     *
+     * TextExceptions (thrown by parseTo/parseFrom) are caught and a warning logged; execution
+     * then continues (so the offending tag gets dropped). */
+    void addTag (char[],ID,char[]);
+    
+    /** Responsible for getting all data tags saved.
+    *
+    * writeAll should call the ItemDelg once for each tag to be saved with parameters in the same
+    * form as received by addTag (char[] type, ID id, char[] data). */
+    void writeAll (ItemDelg);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/iface/IReader.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,37 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/**
+* Interface for readers.
+*/
+module mde.file.mergetag.iface.IReader;
+
+import mde.file.mergetag.DataSet;
+
+import tango.util.collection.model.View : View;
+
+/** Interface for all mergetag readers (MTTReader etc.).
+*/
+interface IReader {
+    DataSet dataset ();                 /// Get the DataSet
+    void dataset (DataSet);             /// Set the DataSet
+    
+    void dataSecCreator (IDataSection delegate (ID));   /// Set the dataSecCreator
+    
+    ID[] getSectionNames ();            /// Get identifiers for all sections
+    void read ();                       /// Commence reading
+    void read (ID[] secSet);            /// ditto
+    void read (View!(ID) secSet);       /// ditto
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/iface/IWriter.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,48 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/**
+* Interface for writers.
+*/
+module mde.file.mergetag.iface.IWriter;
+
+import mde.file.mergetag.DataSet;
+
+
+/** Interface for all mergetag writers (MTTWriter etc.).
+*/
+interface IWriter {
+    DataSet dataset ();                 /// Get the DataSet
+    void dataset (DataSet);             /// Set the DataSet
+    
+    void write ();                      /// Commence writing
+}
+
+/**
+* Enumeration for specifying the writing method ("Params" section shows possible values).
+*
+* Params:
+* FromExtension = Determine writing format from file name extension (must be one of .mtb or .mtt).
+* Binary = Use binary mode (adds extension .mtb without checking for an existing extension).
+* Text = Use text mode (adds extension .mtt without checking for an existing extension).
+* Both = Write simultaneously in binary and text modes (with appropriate extensions added to each
+* file name.
+*/
+enum WriterMethod : byte {
+    FromExtension = -1,
+    Binary = 1,
+    Text = 2,
+    Both = 3
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/internal.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,35 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/// Contains functions/data structures used internally by mergetag.
+module mde.file.mergetag.internal;
+
+package abstract class MTFormatVersion {
+    enum VERS : ubyte {	// convenient list of all known file format versions
+        INVALID	= 0x00,
+        MT01	= 0x01,		// not yet final
+    }
+    /// The current MergeTag version
+    static const VERS Current = VERS.MT01;
+    static const char[2] CurrentString = "01";
+    
+    static VERS parseString (char[] str)
+    in {
+            assert (str.length == 2);
+    } body {
+        if (str[0] == '0' && str[1] == '1') return VERS.MT01;
+        else return VERS.INVALID;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/mdeUT.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,100 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/// This module provides a unittest for mergetag.
+module mde.file.mergetag.mdeUT;
+
+debug (mdeUnitTest) {
+    import mde.file.mergetag.Reader;
+    import mde.file.mergetag.Writer;
+    import mde.file.mergetag.DataSet;
+    import mde.file.mergetag.DefaultData;
+    import mde.file.deserialize;
+    import mde.file.serialize;
+    
+    import tango.io.FilePath;
+    import tango.util.log.Log : Log, Logger;
+    
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.file.mergetag.mdeUT");
+    }
+    
+    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";
+        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
+        
+        IWriter w = makeWriter (file, dsW, WriterMethod.Both);
+        w.write();
+        
+        // FIXME: when binary writing is supported, read both formats and check
+        IReader r = makeReader (FilePath (file~".mtt"), 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);
+                
+        IDataSection* 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`));
+        
+        // Delete the unittest file now
+        FilePath (file~".mtt").remove;
+        
+        logger.info ("Unittest complete (for DefaultData).");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/serialize.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,400 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/**************************************************************************************************
+ * Generic serialization templated function.
+ *
+ * Supports:
+ *  Associative arrays, dynamic arrays (with usual formatting of strings), structs, char types,
+ *  bool, int types, float types.
+ *
+ * Examples:
+ * ------------------------------------------------------------------------------------------------
+ * // Basic examples:
+ * Cout (serialize!(byte) (-13)).newline;                       // -13
+ * Cout (serialize!(real) (2.56e11)).newline;                   // 2.55999999999999990000e+11
+ * Cout (serialize!(double[]) ([0.0, 1.0, 2.0, 3.0])).newline;  // [0.00000000000000000,1.00000000000000000,2.00000000000000000,3.00000000000000000]
+ * Cout (serialize ([true,false,false])).newline;               // [true,false,false]
+ *
+ * // String and ubyte[] special syntaxes (always used):
+ * Cout (serialize ("A string.")).newline;                      // "A string." (including quotes)
+ * Cout (serialize (cast(ubyte[]) [5u, 0xF1u, 0x10u])).newline; // 0x05f110
+ *
+ * // Associative arrays:
+ * Cout (serialize ([-1:"negative one"[], 0:"zero", 1:"one"])).newline; // [0:"zero",1:"one",-1:"negative one"]
+ *
+ * // Structs:
+ * struct S {   int a = 5;  double[int[]] x;    }
+ * S s;
+ * Cout (serialize (s));
+ *
+ * // No limit on complexity...
+ * char[] somethingComplicated = serialize!(real[][][bool[int[][]]]) (...);
+ * ------------------------------------------------------------------------------------------------
+ *
+ * throws:
+ *      May throw a UnicodeException or an IllegalArgumentException.
+ *
+ * TODO: Optimize memory allocation (if possible?). Test best sizes for initial allocations
+ * instead of merely guessing?
+ *************************************************************************************************/
+//NOTE: in case of multiple formats, make this a dummy module importing both serialize modules,
+// or put all the code here.
+//FIXME: Optimize by using a slicing buffer. Put everything in a struct containing this buffer to
+// make it thread-safe.
+module mde.file.serialize;
+// Since serialize is never used in a module where deserialize is not used, save an import:
+public import mde.file.deserialize;
+
+// tango imports
+import tango.core.Traits;
+import tango.core.Exception : UnicodeException, IllegalArgumentException;
+import cInt = tango.text.convert.Integer;
+import cFloat = tango.text.convert.Float;
+import Utf = tango.text.convert.Utf;
+
+
+alias serialize parseFrom;      // support the old name
+
+// Formatting options, for where multiple formats are supported by the deserializer.
+
+// Output using the special binary notation (0x01F2AC instead of [01 ,0xF2, 0xAC])?
+const bool SPECIAL_BINARY_NOTATION = true;
+
+// Output binary as true / false or 1 / 0 ?
+const bool BINARY_AS_WORDS = true;
+
+
+char[] serialize(U) (U val) {
+    // Associative arrays (NOTE: cannot use is() expression)
+    static if (isAssocArrayType!(U)) {          // generic associative array
+        alias typeof(U.keys[0])     S;
+        alias typeof(U.values[0])   T;
+        char[] ret;
+        // A guess, including values themselves and [,:] elements (must be at least 2).
+        ret.length = val.length * (defLength!(T) + defLength!(S) + 2) + 2;
+        ret[0] = '[';
+        uint i = 1;
+        foreach (S k, T v; val) {
+            char[] s = serialize!(S) (k) ~ ":" ~ serialize!(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];
+    }
+    // Arrays
+    else static if (is(U S == S[]) || isStaticArrayType!(U)) {
+        alias typeof(U[0]) T;
+        
+        static if (is(T == char)) {             // string
+            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++] = escapeChar(val[t++]);	// character; increment i and t
+                }
+            }
+            ret[i++] = '"';
+            return ret[0..i];
+        }
+        else static if (is(T == wchar) || is(T == dchar)) {   // wstring or dstring
+            // May throw a UnicodeException; don't bother catching and rethrowing:
+            return serialize!(char[]) (Utf.toString (val));
+        }
+        else static if (SPECIAL_BINARY_NOTATION && is(T == ubyte)) {    // special binary notation
+            // Note: To disable the usage of this special type, set SPECIAL_BINARY_NOTATION = false.
+            static const char[16] digits = "0123456789abcdef";
+    
+            char[] ret = new char[val.length * 2 + 2];	// exact length
+            ret[0..2] = "0x";
+            uint i = 2;
+    
+            foreach (ubyte x; val) {
+                ret[i++] = digits[x >> 4];
+                ret[i++] = digits[x & 0x0F];
+            }
+            return ret;
+        }
+        else {                                  // generic array
+            char[] ret;
+        // A guess, including commas and brackets (must be at least 2)
+            ret.length = val.length * (defLength!(T) + 1) + 2;
+            ret[0] = '[';
+            uint i = 1;
+            foreach (T x; val) {
+                char[] s = serialize!(T) (x);
+                i += s.length;
+                if (i+1 >= ret.length)
+                    ret.length = ret.length * 2;	// check length
+                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];
+        }
+    }
+    // Structs
+    else static if (is(U == struct)) {
+        char[] ret;
+        // A very rough guess.
+        ret.length = val.sizeof * 4;
+        ret[0] = '{';
+        uint i = 1;
+        foreach (k, v; val.tupleof) {
+            alias typeof(v) T;
+            char[] s = serialize!(size_t) (k) ~ ":" ~ serialize!(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];
+    }
+    // Basic types
+    else static if (is(U == char)) {            // char (UTF-8 byte)
+        if (val > 127)      // outputing invalid utf-8 could corrupt the output stream
+            throw new IllegalArgumentException ("Not a valid UTF-8 character");
+        
+        // Can't return reference to static array; so making it 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] = escapeChar (val);
+            ret[3] = '\'';
+            return ret;
+        }
+    } else static if (is(U == wchar) ||
+                      is(U == dchar)) {         // wchar or dchar (UTF-16/32 single char)
+        if (val <= 127u)
+            return serialize!(char) (cast(char) val);  // ASCII
+        else {  // convert to a multi-byte UTF-8 char
+            // NOTE: suboptimal
+            char[] t,ret;
+            t = Utf.toString([val]);
+            ret.length = t.length + 2;
+            ret = '\'' ~ t ~ '\'';
+            return ret;
+        }
+    } else static if (is (U == bool)) {         // boolean
+        static if (BINARY_AS_WORDS) {
+            if (val)
+                return "true";
+            else return "false";
+        } else {
+            if (val)
+                return "1";
+            else return "0";
+        }
+    } else static if (is (U : long)) {          // any integer type, except char types and bool
+        static if (is (U == ulong))             // ulong may not be supported properly
+            if (val > cast(ulong) long.max)
+                throw new IllegalArgumentException ("No handling available for ulong where value > long.max");
+        return cInt.toString (val);
+    } else static if (is (U : real)) {          // any (real) floating point type
+        char[] ret = new char[32];              // minimum allowed by assert in format
+        return cFloat.format (ret, val, U.dig+2, 1);// from old C++ tests, U.dig+2 gives best(?) accuracy
+    }
+    // Unsupported
+    else
+        static assert (false, "Unsupported type: "~U.stringof);
+}
+
+//BEGIN Utility funcs
+/* This template provides the initial length for strings for formatting various types. These strings
+ * can be expanded; this value is intended to cover 90% of cases or so.
+ *
+ * NOTE: This template was intended to provide specialisations for different types.
+ * This one value should do reasonably well for most types.
+ */
+private {
+    template defLength(T)        { const uint defLength = 20; }
+    template defLength(T : char) { const uint defLength = 4;  }
+    template defLength(T : bool) { const uint defLength = 5;  }
+}
+private bool isEscapableChar (char c) {
+    return ((c <= '\r' && c >= '\a') || c == '\"' || c == '\'' || c == '\\');
+}
+// Throws on unsupported escape sequences; however this should never happen within serialize.
+private char escapeChar (char c) {
+    // This code was generated:
+    if (c <= '\v') {
+        if (c <= '\b') {
+            if (c == '\a') {
+                return 'a';
+            } else if (c == '\b') {
+                return 'b';
+            }
+        } else {
+            if (c == '\t') {
+                return 't';
+            } else if (c == '\n') {
+                return 'n';
+            } else if (c == '\v') {
+                return 'v';
+            }
+        }
+    } else {
+        if (c <= '\r') {
+            if (c == '\f') {
+                return 'f';
+            } else if (c == '\r') {
+                return 'r';
+            }
+        } else {
+            if (c == '\"') {
+                return '\"';
+            } else if (c == '\'') {
+                return '\'';
+            } else if (c == '\\') {
+                return '\\';
+            }
+        }
+    }
+    
+    // if we haven't returned:
+    throw new IllegalArgumentException ("Internal error (escapeChar)");
+}
+//END Utility funcs
+
+
+
+debug (mdeUnitTest) {
+    import tango.util.log.Log : Log, Logger;
+
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.file.serialize");
+    }
+unittest {
+    // Utility
+    bool throws (void delegate() dg) {
+        bool r = false;
+        try {
+            dg();
+        } catch (Exception e) {
+            r = true;
+            logger.trace ("Exception caught: "~e.msg);
+        }
+        return r;
+    }
+    assert (!throws ({ int i = 5; }));
+    assert (throws ({ throw new Exception ("Test - this exception should be caught"); }));
+    
+    // Associative arrays
+    char[] X = serialize!(char[][char]) (['a':cast(char[])"animal", 'b':['b','u','s']]);
+    char[] Y = `['a':"animal",'b':"bus"]`;
+    assert (X == Y);
+    
+    
+    // Arrays
+    // generic array stuff:
+    assert (serialize!(double[]) ([1.0, 1.0e-10]) == `[1.00000000000000000,0.10000000000000000e-09]`);
+    assert (serialize!(double[]) (cast(double[]) []) == `[]`);		// empty array
+    
+    // char[] conversions, with commas, escape sequences and multichar UTF8 characters:
+    assert (serialize!(char[][]) ([ ".\""[], [',','\''] ,"!\b€" ]) == `[".\"",",\'","!\b€"]`);
+    
+    // wchar[] and dchar[] conversions:
+    // The characters were pretty-much pulled at random from unicode tables.
+    assert (serialize!(wchar[]) ("Test string: ¶α؟अกሀ搀"w) == "\"Test string: ¶α؟अกሀ搀\"");
+    assert (serialize!(dchar[]) ("Test string: ¶α؟अกሀ搀"d) == "\"Test string: ¶α؟अกሀ搀\"");
+    
+    
+    static if (SPECIAL_BINARY_NOTATION)
+        assert (serialize!(ubyte[]) (cast(ubyte[]) [0x01, 0xF2, 0xAC]) == `0x01f2ac`);	// ubyte[] special notation
+    else
+        assert (serialize!(ubyte[]) (cast(ubyte[]) [0x01, 0xF2, 0xAC]) == `[1,242,172]`);
+    
+    
+    // Structs
+    struct Foo {    int a = 9;  char b = '\v'; float c;    }
+    struct Bar {    Foo a,b;    }
+    static Foo foo1 = { a:150, b:'8', c:17.2f}, foo2;
+    Bar bar;
+    bar.a = foo1;
+    bar.b = foo2;
+    assert (serialize(bar) == "{0:{0:150,1:'8',2:1.72000007e+01},1:{0:9,1:'\\v',2:nan}}");
+    
+    
+    // Basic Types
+    // Character types
+    assert (serialize!(char) ('\'') == "\'\\\'\'");
+    assert (serialize!(wchar) ('X') == "'X'");
+    assert (serialize!(dchar) ('X') == "'X'");
+    assert (serialize!(wchar) ('£') == "'£'");  // unicode U+00A3 i.e. a multi-byte UTF-8 char
+    assert (serialize!(dchar) ('£') == "'£'");
+    assert (throws ({ serialize!(char) ('£'); }));      // compiler converts £ to char, but it's not valid UTF-8
+    
+    // Bool
+    static if (BINARY_AS_WORDS)
+        assert (serialize(false) == "false");
+    else
+        assert (serialize(true) == "1");
+    
+    // Integers
+    assert (serialize (cast(byte) -5) == "-5");
+    assert (serialize (cast(short) -32768) == "-32768");
+    assert (serialize (-5) == "-5");
+    assert (serialize (-9223372036854775807L) == "-9223372036854775807");
+    assert (serialize (cast(ubyte) -1) == "255");
+    assert (serialize (cast(ushort) -1) == "65535");
+    assert (serialize!(uint) (-1) == "4294967295");
+    assert (serialize (cast(ulong) 0x7FFF_FFFF_FFFF_FFFFLu) == "9223372036854775807");
+    assert (serialize!(uint[]) ([0b0100u,0724,0xFa59c,0xFFFFFFFF,0]) ==
+                               "[4,468,1025436,4294967295,0]");
+    assert (throws ({
+        // ulong is not properly supported.
+        // NOTE: this is something that should really work.
+        char[] r = serialize!(ulong) (0x8FFF_FFFF_FFFF_FFFFLu);
+    }));
+    
+    // Floats
+    // These numbers are not particularly meaningful:
+    assert (serialize!(float) (0.0f) == "0.00000000");
+    assert (serialize!(double) (-1e25) == "-1.00000000000000000e+25");
+    assert (serialize!(real) (cast(real) 4.918e300) == "4.91800000000000000000e+300");
+    
+    // Escape sequences (test conversion functions)
+    assert (serialize ("\a\b\t\n\v\f\r\"\'\\") == `"\a\b\t\n\v\f\r\"\'\\"`);
+    
+    logger.info ("Unittest complete.");
+}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/ssi.d	Sun Aug 31 15:59:17 2008 +0100
@@ -0,0 +1,84 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/**************************************************************************************************
+ * mde SSI (Single Seriazied Item) format - functions to read a file into a struct and vice-versa.
+ * 
+ * This is a very simple format capable of handling any type supported by the (de)serializer.
+ *************************************************************************************************/
+module mde.file.ssi;
+
+import mde.file.exception;
+import mde.file.serialize;
+
+import tango.io.UnicodeFile;
+
+S read(S) (FilePath path) {
+    char[] buf;
+    try {
+        scope file = new UnicodeFile!(char) (path, Encoding.Unknown);   // from BOM or use UTF-8
+        buf = cast(char[]) file.read;
+    } catch (Exception e) {
+        throw new ioException ("While reading \""~path.toString~"\": "~e.msg);
+    }
+    
+    // Read header. Note: may be followed by new-line, but serializer strips white-space anyway.
+    if (buf.length < 8 || buf[0..8] != "mdessi00")
+        throw new parseException (path.toString ~ " is not a recognized mde ssi file: it doesn't start mdessi00");
+    
+    try {
+        return deserialize!(S) (buf[8..$]);
+    } catch (Exception e) {
+        throw new parseException ("Failed to read mde ssi file: "~e.msg);
+    }
+}
+
+void write(S) (FilePath path, S content) {
+    try {
+        scope file = new UnicodeFile!(char) (path, Encoding.UTF_8N);
+        file.write ("mdessi00\n"~serialize(content), true);
+    } catch (Exception e) {
+        throw new ioException ("Unable to write file "~path.toString~": "~e.msg);
+    }
+}
+
+debug (mdeUnitTest) {
+    import tango.util.log.Log : Log, Logger;
+    import tango.io.FilePath;
+    
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.file.ssi");
+    }
+    
+    unittest {
+        struct A {
+            float x;
+            dchar y;
+            long z;
+        }
+        A a;
+        a.x = 0.0f;
+        a.y = '搀';
+        a.z = (cast(long) uint.max) + 1;
+        
+        FilePath path = FilePath ("SSIUnitTest.ssi");
+        write (path, a);
+        assert (a == read!(A)(path));
+        path.remove;    // get rid of the file
+        
+        logger.info ("Unittest complete.");
+    }
+}
--- a/mde/font/font.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/font/font.d	Sun Aug 31 15:59:17 2008 +0100
@@ -21,15 +21,14 @@
 import mde.font.FontTexture;
 import mde.font.exception;
 
-import mde.mergetag.Reader;
-import mde.mergetag.DataSet;
-import mde.mergetag.exception;
+import mde.file.mergetag.Reader;
+import mde.file.mergetag.DataSet;
 import mde.setup.paths;
 
 import derelict.freetype.ft;
 import derelict.opengl.gl;
 
-import mde.mergetag.deserialize;
+import mde.file.deserialize;
 import tango.stdc.stringz;
 import Util = tango.text.Util;
 import tango.util.log.Log : Log, Logger;
@@ -59,9 +58,6 @@
         /** Load the freetype library from the file fileName. */
         private const fileName = "fonts";
         void initialize () {
-            if (!confDir.exists (fileName))
-                throw new fontException ("No font settings file (fonts.[mtt|mtb])");
-            
             if (FT_Init_FreeType (&library))
                 throw new fontException ("error initialising the FreeType library");
             
@@ -112,9 +108,10 @@
                 if (p is null)
                     throw new fontException ("No fallback font style specified");
                 fallbackName = *p;
-            }
-            catch (MTException e) {
-                throw new fontException ("Mergetag exception: "~e.msg);
+            } catch (NoFileException) {
+                throw new fontException ("No font settings file (fonts.[mtt|mtb])");
+            } catch (Exception e) {
+                throw new fontException ("Reading font settings failed: "~e.msg);
             }
             
             // Find the fallback
--- a/mde/gui/WidgetDataSet.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/gui/WidgetDataSet.d	Sun Aug 31 15:59:17 2008 +0100
@@ -30,10 +30,9 @@
 public import mde.gui.types;
 
 // For loading from file:
-import mt = mde.mergetag.DataSet;
-import mt = mde.mergetag.DefaultData;
-import mt = mde.mergetag.exception;
-import mde.mergetag.serialize;
+import mt = mde.file.mergetag.DataSet;
+import mt = mde.file.mergetag.DefaultData;
+import mde.file.serialize;
 import tango.util.log.Log : Log, Logger;
 
 private Logger logger;
--- a/mde/gui/WidgetManager.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/gui/WidgetManager.d	Sun Aug 31 15:59:17 2008 +0100
@@ -62,7 +62,11 @@
      */
     this (char[] file) {
         super(file);
-        
+    }
+    
+    // NOTE - temporarily here to allow CTOR to run safely during static this
+    // called during init
+    void init () {
         // Events we want to know about:
         imde.input.addMouseClickCallback(&clickEvent);
         imde.input.addMouseMotionCallback(&motionEvent);
@@ -187,8 +191,8 @@
 import mde.gui.widget.Ifaces;
 import mde.gui.widget.createWidget;
 
-import mde.mergetag.Reader;
-import mde.mergetag.Writer;
+import mde.file.mergetag.Reader;
+import mde.file.mergetag.Writer;
 import mde.setup.paths;
 
 /*************************************************************************************************
@@ -221,11 +225,6 @@
         if (allLoaded || (defaultDesign !is null && allDesigns == false))
             return; // test if already loaded
             
-            if (!confDir.exists (fileName)) {
-                logger.error ("Unable to load GUI: no config file!");
-                return; // not a fatal error (so long as the game can run without a GUI!)
-            }
-            
             // Set up a reader
             scope IReader reader;
         try {
@@ -266,6 +265,9 @@
                 allLoaded = true;
             } else
                 reader.read([defaultDesign]);
+        } catch (NoFileException) {
+            logger.error ("Unable to load GUI: no config file!");
+            // just return: not a fatal error (so long as the game can run without a GUI!)
         } catch (Exception e) {
             logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):");
             logger.error (e.msg);
--- a/mde/gui/widget/Floating.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/gui/widget/Floating.d	Sun Aug 31 15:59:17 2008 +0100
@@ -31,7 +31,7 @@
 
 private Logger logger;
 static this () {
-    logger = Log.getLogger ("mde.gui.widget.Window");
+    logger = Log.getLogger ("mde.gui.widget.Floating");
 }
 //FIXME - documentation
 
--- a/mde/input/Config.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/input/Config.d	Sun Aug 31 15:59:17 2008 +0100
@@ -18,10 +18,10 @@
 
 import mde.input.exception;
 
-import MT = mde.mergetag.Reader;
+import MT = mde.file.mergetag.Reader;
 import mde.setup.paths;
-import mde.mergetag.deserialize;
-debug import mde.mergetag.serialize;
+import mde.file.deserialize;
+debug import mde.file.serialize;
 
 import tango.util.log.Log : Log, Logger;
 import tango.util.collection.TreeBag : TreeBag;
@@ -116,14 +116,11 @@
     
     static Config[char[]] configs;	/// All configs loaded by load().
     private static TreeBag!(char[]) loadedFiles;	// all filenames load tried to read
-    
     private static Logger logger;
-    static this() {
-        logger = Log.getLogger ("mde.input.config.Config");
-    }
     
 //BEGIN File loading/saving code
     static this () {
+        logger = Log.getLogger ("mde.input.Config");
         loadedFiles = new TreeBag!(char[]);
     }
     
--- a/mde/lookup/Options.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/lookup/Options.d	Sun Aug 31 15:59:17 2008 +0100
@@ -14,21 +14,26 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
 /** This module handles stored options, currently all except input maps.
-*
-* The purpose of having all options centrally controlled is to allow generic handling by the GUI
-* and ease saving and loading of values. The Options class is only really designed around handling
-* small numbers of variables for now.
-*/
+ *
+ * The purpose of having all options centrally controlled is to allow generic handling by the GUI
+ * and ease saving and loading of values. The Options class is only really designed around handling
+ * small numbers of variables for now.
+ *
+ * Note: This module uses some non-spec functionality, which "works for me", but may need to be
+ * changed if it throws up problems. Specifically: templated virtual functions (Options.set, get
+ * and list), and accessing private templates from an unrelated class (Options.TName, TYPES).
+ * OptionChanges used to have a templated set member function (used by Options.set), which caused
+ * linker problems when the module wasn't compiled from scratch.
+ */
 module mde.lookup.Options;
 
 import mde.setup.paths;
 import mde.exception;
 
-import mde.mergetag.Reader;
-import mde.mergetag.Writer;
-import mde.mergetag.DataSet;
-import mde.mergetag.exception;
-import mde.mergetag.serialize;
+import mde.file.mergetag.Reader;
+import mde.file.mergetag.Writer;
+import mde.file.mergetag.DataSet;
+import mde.file.serialize;
 
 import tango.core.Exception : ArrayBoundsException;
 import tango.util.log.Log : Log, Logger;
@@ -51,8 +56,18 @@
 * Further to this, a generic class is used to store all options which have been changed, and if any
 * have been changed, is merged with options from the user conf dir and saved on exit.
 */
+/* An idea for potentially extending Options, but which doesn't seem necessary now:
+Move static code from Options to an OptionSet class, which may be sub-classed. These sub-classes
+may be hooked in to the master OptionSet class to shadow all Options classes and be notified of
+changes, and may or may not have values loaded from files during init. Change-sets could be
+rewritten to use this.
+However, only the changesets should need to be notified of each change (gui interfaces could simply
+be notified that a change occured and redraw everything; users of options can just re-take their
+values every time they use them). */
 class Options : IDataSection
 {
+    protected this() {}   /// Do not instantiate directly.
+    
     // All supported types, for generic handling via templates. It should be possible to change
     // the supported types simply by changing this list now (untested).
     template store(A...) { alias A store; }
@@ -134,10 +149,6 @@
     	*/
         private const fileName = "options";
         void load () {
-        // Check it exists (if not it should still be created on exit).
-        // Don't bother checking it's not a folder, because it could still be a block or something.
-            if (!confDir.exists (fileName)) return;
-        
             try {
                 IReader reader;
                 reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH);
@@ -148,10 +159,11 @@
                     else return null;
                 };
                 reader.read;
-            } catch (MTException e) {
-                logger.fatal ("Loading options aborted:");
-                logger.fatal (e.msg);
-                throw new optionsLoadException ("Mergetag exception (see above message)");
+            } catch (NoFileException e) {
+                // Just return. Options file will be created on exit.
+            } catch (Exception e) {
+                logger.warn ("Loading options failed: "~e.msg);
+                logger.warn ("If warning persists, delete the offending file.");        // FIXME - delete the bad file somehow
             }
         }
         void save () {
@@ -170,31 +182,43 @@
                 reader.read;
             } catch (NoFileException) {
                 // No user file exists; not an error.
-            } catch (MTException e) {
+            } catch (Exception e) {
             	// Log a message and continue, overwriting the file:
-                logger.error ("Loading options aborted:");
-                logger.error (e.msg);
+                logger.error ("Loading options aborted: " ~ e.msg);
             }
         
             try {
                 IWriter writer;
                 writer = confDir.makeMTWriter (fileName, ds);
                 writer.write();
-            } catch (MTException e) {
-                logger.error ("Saving options aborted! Reason:");
-                logger.error (e.msg);
+            } catch (Exception e) {
+                logger.error ("Saving options aborted: "~e.msg);
             }
         }
     
         private Logger logger;
         static this() {
-            logger = Log.getLogger ("mde.options");
+            logger = Log.getLogger ("mde.lookup.Options");
         }
     }
     //END Static
     
     
     //BEGIN Non-static
+    /+ NOTE: according to spec: "Templates cannot be used to add non-static members or virtual
+    functions to classes." However, this appears to work (but linking problems did occur).
+    Alternative: use mixins. From OptionsChanges:
+        // setT (used to be a template, but:
+        // Templates cannot be used to add non-static members or virtual functions to classes. )
+        template setMixin(A...) {
+            static if (A.length) {
+                const char[] setMixin = `void set`~TName!(A[0])~` (ID id, `~A[0].stringof~` x) {
+                    `~TName!(T)~`s[id] = x;
+                }
+                ` ~ setMixin!(A[1..$]);
+            } else
+                const char[] setMixin = ``;
+        }+/
     /** Set option symbol of an Options sub-class to val.
      *
      * Due to the way options are handled generically, string IDs must be used to access the options
@@ -203,13 +227,11 @@
     void set(T) (char[] symbol, T val) {
         static assert (TIsIn!(T,TYPES), "Options does not support type "~T.stringof);
         
-        mixin (`alias opts`~TName!(T)~` optsVars;`);
-        
         changed = true;     // something got set (don't bother checking this isn't what it already was)
         
         try {
-            *(optsVars[cast(ID) symbol]) = val;
-            optionChanges.set!(T) (cast(ID) symbol, val);
+            mixin (`*(opts`~TName!(T)~`[cast(ID) symbol]) = val;`);
+            mixin (`optionChanges.`~TName!(T)~`s[symbol] = val;`);
         } catch (ArrayBoundsException) {
             // log and ignore:
             logger.error ("Options.set: invalid symbol");
@@ -404,7 +426,6 @@
             } else
                 const char[] writeAllMixin = ``;
         }
-        
     }
     //END Templates
     // These store the actual values, but are never accessed directly except when initially added.
@@ -413,13 +434,6 @@
     
     this () {}
     
-    void set(T) (ID id, T x) {
-        static assert (Options.TIsIn!(T,TYPES), "Options does not support type "~T.stringof);
-        
-        mixin (`alias `~TName!(T)~`s vars;`);
-        vars[id] = x;
-    }
-    
     //BEGIN Mergetag loading/saving code
     // HIGH_LOW priority: only load symbols not currently existing
     void addTag (char[] tp, ID id, char[] dt) {
--- a/mde/lookup/Translation.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/lookup/Translation.d	Sun Aug 31 15:59:17 2008 +0100
@@ -41,10 +41,10 @@
 import mde.setup.paths;
 import mde.exception;
 
-import mde.mergetag.DataSet;
-import mde.mergetag.Reader;
-import mde.mergetag.exception;
-import mde.mergetag.deserialize;
+import mde.file.mergetag.DataSet;
+import mde.file.mergetag.Reader;
+import mde.file.mergetag.exception;
+import mde.file.deserialize;
 
 import tango.util.log.Log : Log, Logger;
 
--- a/mde/mde.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/mde.d	Sun Aug 31 15:59:17 2008 +0100
@@ -21,15 +21,19 @@
 module mde.mde;
 
 import mde.imde;                        // this module's interface for external modules
+import mde.events;      // pollEvents() // NOTE: Must be imported before Init, otherwise fonts don't display properly (why??)
 import mde.setup.Init;                  // initialization
 import mde.lookup.Options;              // pollInterval option
 import mde.scheduler.Scheduler;         // mainSchedule
-import mde.events;                      // pollEvents()
 import gl = mde.gl.draw;                // gl.draw()
 
 import tango.core.Thread : Thread;	// Thread.sleep()
 import tango.time.Clock;                // Clock.now()
 import tango.util.log.Log : Log, Logger;
+debug (mdeUnitTest) {
+    import mde.file.ssi;
+    import mde.file.mergetag.mdeUT;
+}
 
 int main(char[][] args)
 {
--- a/mde/mergetag/DataSet.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/** This module contains the mergetag DataSet class, used for all reading and writing operations.
- */
-module mde.mergetag.DataSet;
-
-// package imports
-public import mde.mergetag.iface.IDataSection;
-import mde.mergetag.DefaultData;
-import mde.mergetag.exception;
-
-
-/**************************************************************************************************
- * Data class; contains a DataSection class instance for each loaded section of a file.
- *
- * Stored data is available for direct access via header and sec; all functions are just helper
- * functions.
- *
- * Any class implementing IDataSection may be used to store data; by default a DefaultData class is
- * used when reading a file. Another class may be used by creating the sections before reading the
- * file or passing the reader a function to create the sections (see Reader.dataSecCreator).
- *
- * Could be a struct, except that structs are value types (not reference types).
- */
-class DataSet
-{
-    DefaultData header;			/// Header section.
-    IDataSection[ID] sec;		/// Dynamic array of sections
-    
-    /// Template to return all sections of a child-class type.
-    T[ID] getSections (T : IDataSection) () {
-        T[ID] ret;
-        foreach (ID id, IDataSection s; sec) {
-            T x = cast(T) s;
-            if (x) ret[id] = x;	// if non-null
-        }
-        return ret;
-    }
-}
-
-debug (mdeUnitTest) {
-    import tango.util.log.Log : Log, Logger;
-
-    private Logger logger;
-    static this() {
-        logger = Log.getLogger ("mde.mergetag.DataSet");
-    }
-    
-    unittest {	// Only covers DataSet really.
-        DataSet ds = new DataSet;
-        ds.sec[cast(ID)"test"] = new DefaultData;
-        assert (ds.getSections!(DefaultData)().length == 1);
-        ds.sec[cast(ID)"test"].addTag ("char[]",cast(ID)"T"," \"ut tag 1 \" ");
-        assert (ds.getSections!(DefaultData)()[cast(ID)"test"].Arg!(char[])[cast(ID)"T"] == "ut tag 1 ");
-    
-        logger.info ("Unittest complete.");
-    }
-}
--- a/mde/mergetag/DefaultData.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,183 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/** This module contains the DefaultData class, and some notes possibly useful for implementing
-* other types of DataSection.
-*/
-module mde.mergetag.DefaultData;
-
-public import mde.mergetag.iface.IDataSection;
-import mde.mergetag.exception;
-
-import mde.mergetag.serialize;
-
-
-/*************************************************************************************************
- * Default DataSection class.
- * 
- * Supported types are given by dataTypes.
- * 
- * Currently DefaultData is only used for headers, and thus the list of supported types has been
- * reduced to just those used in headers. Load order is HIGH_LOW, i.e. existing entries aren't
- * overwritten.
- *************************************************************************************************/
-/* 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).
-*
-* 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 : IDataSection
-{
-    //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;
-            ret ~= indent(indents);
-            foreach (c; consts) {
-                ret ~= "if (" ~ var ~ " == \"" ~ c ~ "\") {\n" ~
-                    //indent(indents+1) ~ varName(c) ~ "[id] = parseTo!(" ~ c ~ ") (dt);\n" ~
-                    indent(indents+1) ~ "if ((id in "~varName(c)~") is null)\n" ~
-                    indent(indents+2) ~ varName(c)~"[id] = parseTo!(" ~ c ~ ") (dt);\n" ~
-                indent(indents) ~ "} else ";
-            }
-            ret = ret[0..$-6] ~ '\n';  // remove last else
-            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 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;
-    * }
-    *
-    * type y = Arg!(type).Arg;	// example of use
-    * --------------------
-    * 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.
-    */
-    /+ All types previously supported. Most of these weren't used.
-    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[]"];
-    +/
-    const char[][] dataTypes = ["char[]", "char[][]"];
-    
-    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) {	/// Supports all types listed in dataTypes.
-        mixin (writeVars ());
-    }
-    
-    /* These make no attempt to check Arg is valid.
-    * But if the symbol doesn't exist the complier will throw an error anyway, e.g.:
-    * Error: identifier '_boolAA' is not defined
-    */
-    template ArgName (T : T[]) {
-        const char[] ArgName = ArgName!(T)~`A`;
-    }
-    template ArgName (T) {
-        const char[] ArgName = `_`~T.stringof;
-    }
-    template Arg(T) {
-        mixin(`alias `~ArgName!(T)~` Arg;`);
-    }
-}
--- a/mde/mergetag/Reader.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,526 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/**************************************************************************************************
- * This module contains all reading functions, for both binary and text MergeTag files.
- *************************************************************************************************/
-module mde.mergetag.Reader;
-
-// package imports
-public import mde.mergetag.iface.IReader;
-import mde.mergetag.DataSet;
-import mde.mergetag.DefaultData;
-import mde.mergetag.exception;
-import mde.mergetag.internal;
-
-import tango.core.Exception;
-
-// tango imports
-import tango.io.FilePath;
-import tango.io.UnicodeFile;
-import Util = tango.text.Util;
-import ConvInt = tango.text.convert.Integer;
-//import tango.util.collection.model.View : View;
-import tango.util.collection.HashSet : HashSet;
-import tango.util.log.Log : Log, Logger;
-
-private Logger logger;
-static this() {
-    logger = Log.getLogger ("mde.mergetag.Reader");
-}
-
-// TODO: allow compressing with zlib for both binary and text? (.mtz, .mtt, .mtb extensions)
-
-/** Make an IReader class.
-*
-* Create an appropriate reader: MTTReader or MTBReader.
-*
-* Throws:
-*  $(TABLE
-*  $(TR $(TH Exception) $(TH Thrown when))
-*  $(TR $(TD MTFileIOException) $(TD When extension given is neither mtt nor mtb))
-*  )
-*
-*/
-IReader makeReader (FilePath path, DataSet ds = null, bool rdHeader = false) {
-    if      (path.ext == "mtb") return new MTBReader (path, ds, rdHeader);
-    else if (path.ext == "mtt") return new MTTReader (path, ds, rdHeader);
-    else throw new MTFileIOException ("Invalid mergetag extension");
-}
-
-/** Resolve a file path.
- *
- * Tries adding both ".mtt" and ".mtb" extensions, returning whichever exists (the most recently
- * modified if both exist), or returns null if neither exist. */
-FilePath findFile (char[] path) {
-    if (path is null) return null;
-    
-    FilePath tPath = new FilePath (path ~ ".mtt");
-    FilePath bPath = new FilePath (path ~ ".mtb");
-        
-    bool bPathExists = bPath.exists;
-        
-    if (tPath.exists) {
-        if (bPathExists) {
-                // take the latest version (roughly speaking...)
-            return (tPath.modified > bPath.modified ? tPath : bPath);
-        } else return tPath;
-    } else {
-        if (bPathExists) return bPath;
-        else return null;
-    }
-}
-
-/**
- * Class for reading a mergetag text file.
- * 
- * Use as:
- * -----------------------
- * IReader foo;
- * try {
- *   foo = new MTTReader("foo.mtt");
- *   foo.read();
- * }
- * catch (MTException) {}
- * // get your data from foo.dataset.
- * -----------------------
- *
- * Throws:
- *  $(TABLE
- *  $(TR $(TH Exception) $(TH Thrown when))
- *  $(TR $(TD MTFileIOException) $(TD An error occurs while opening the file))
- *  $(TR $(TD MTFileFormatException) $(TD The file doesn't start with a recognised header/version))
- *  $(TR $(TD MTSyntaxException) $(TD A file syntax error occurs))
- *  $(TR $(TD MTException) $(TD An unexpected error occurs))
- *  )
- * Note that all exceptions extend MTException and when any exception is thrown the class is
- * rendered unusable: any subsequent calls to read will be ignored.
- *
- * Threading: Separate instances of Reader should be thread-safe provided access to the same
- * dataset is synchronized; i.e. no two readers refering to the same dataset should run
- * simultaneously. (The Reader class could be made thread-safe w.r.t. datasets, but
- * performance-wise I doubt it would be worth it.)
- * Do not run a single instance of Reader in multiple threads simultaneously.
- */
-class MTTReader : IReader
-{
-//BEGIN DATA
-    /** Get or set the DataSet
-    *
-    * A container for all read data.
-    *
-    * This may be accessed from here; however it may be preferable to use an external reference
-    * (passed to the class on initialisation).
-    */
-    DataSet dataset () {	return _dataset;	}
-    void dataset (DataSet ds)	/// ditto
-    {	_dataset = ds;	}
-    
-    /** A delegate for creating new DataSections within the dataset.
-    *
-    * Allows a user-made class to be used in the DataSet instead of DefaultData (used if no
-    * dataSecCreator exists). Also allows an existing class instance to be used instead of a new
-    * one.
-    *
-    * This works by supplying a function which returns a reference to an instance of a class
-    * implementing IDataSection. The function is passed the ID of the new section and may use this
-    * to use different IDataSection classes for different sections.
-    *
-    * The function may also return null, in which case the section will be skipped. In the version
-    * of read taking a set of sections to read, the section will not be marked as read and may
-    * still be read later (assuming dataSecCreator returns non-null). However, in the version of
-    * read not taking the set argument, all sections are set as read regardless, and the section
-    * cannot be read later.
-    */
-    void dataSecCreator (IDataSection delegate (ID) dSC) {
-        _dataSecCreator = dSC;
-    }
-    
-private:
-    static Logger logger;
-    
-    // Non-static symbols:
-    final char[] ErrFile;		// added after ErrInFile to do the same without the "in " bit.
-    final char[] ErrInFile;		// something like "in \"path/file.mtt\""
-    
-    final char[] fbuf;			// file is read into this
-    MTFormatVersion.VERS fileVer = MTFormatVersion.VERS.INVALID;	// Remains INVALID until set otherwise by CTOR.
-    
-    IDataSection delegate (ID) _dataSecCreator = null;   // see property setter above
-    
-    size_t endOfHeader;
-    bool allRead = false;		// true if endOfHeader == fbuf.length or read([]) has run
-    bool fatal = false;			// a fatal file error occured; don't try to recover
-    /* If the file is scanned for sections, the starting position of all sections are stored
-    * in secTable. If this is empty, either no sections exist (and endOfHeader == fbuf.length)
-    * or a section scan has not been run (read() with no section names doesn't need to do so).
-    */
-    struct SecMD {	// sec meta data
-        static SecMD opCall (size_t _pos, bool _read) {
-            SecMD ret;
-            ret.pos = _pos;
-            ret.read = _read;
-            return ret;
-        }
-        size_t pos;			// position to start reading
-        bool read;			// true if already read
-    }
-    SecMD [ID] secTable;
-    
-    DataSet _dataset;
-//END DATA
-    
-//BEGIN METHODS: CTOR / DTOR
-    static this () {
-        logger = Log.getLogger ("mde.mergetag.read.Reader");
-    }
-    
-    /** Tries to open file path and read it into a buffer.
-     *
-     * Params:
-     * path     = The name or FilePath of the file to open.
-     *     Standard extensions are .mtt and .mtb for text and binary files respectively.
-     * ds       = If null create a new DataSet, else use existing DataSet ds and merge read
-     *     data into it.
-     * rdHeader = If true, read the header like a standard section. Doesn't read the header by
-     *     default since if it's not requested it's likely not wanted.
-     *
-     * Memory:
-     * This currently works by loading the whole file into memory at once. This should be fine most
-     * of the time, but could potentially be a problem. Changing this would mean significantly
-     * changes to the way the code works.
-     */
-    /* Ideas for implementing a partial-loading memory model:
-     * Use a conduit directly.
-     * Use a fiber to do the parsing; let it switch back when it runs out of memory.
-     * Redesign the code so it never needs to look backwards in the buffer?
-     *
-     * Major problem: reading only some sections and keeping references to other sections
-     * would no longer be possible.
-     */
-    public this (char[] path, DataSet ds = null, bool rdHeader = false) {
-        this (new FilePath (path), ds, rdHeader);
-    }
-    /** ditto */
-    public this (FilePath path, DataSet ds = null, bool rdHeader = false) {
-        // Create a dataset or use an existing one
-        if (ds !is null) _dataset = ds;
-        else _dataset = new DataSet();
-        
-        // Open & read the file
-        try {	// Supports unicode files with a BOM; defaults to UTF8 when there isn't a BOM:
-            scope file = new UnicodeFile!(char) (path, Encoding.Unknown);
-            fbuf = cast(char[]) file.read();
-        } catch (Exception e) {
-            throwMTErr ("Error reading file: " ~ e.msg, new MTFileIOException);
-        }
-        // Remember the file name so that we can report errors (somewhat) informatively:
-        ErrFile = path.path ~ path.file;
-        ErrInFile = " in \"" ~ ErrFile ~ '"';
-        
-        // Version checking & matching header section tag:
-        if (fbuf.length < 6 || fbuf[0] != '{' || fbuf[1] != 'M' || fbuf[2] != 'T' || fbuf[5] != '}')
-            throwMTErr("Not a valid MergeTag text file" ~ ErrInFile, new MTFileFormatException);
-        fileVer = MTFormatVersion.parseString (fbuf[3..5]);
-        if (fileVer == MTFormatVersion.VERS.INVALID)
-            throwMTErr("Unrecognised MergeTag version: MT" ~ fbuf[3..5] ~ ErrInFile, new MTFileFormatException);
-        
-        // Header reading/skipping:
-        if (rdHeader) {	// only bother actually reading it if it was requested
-            // If already existing, merge; else create a new DefaultData.
-            if (!_dataset.header) _dataset.header = new DefaultData;
-            endOfHeader = parseSection (6, cast(IDataSection) _dataset.header);
-        }
-        else endOfHeader = parseSection (6,null);
-    }
-//END METHODS: CTOR / DTOR
-    
-//BEGIN METHODS: PUBLIC
-    /** Scans for sections if not already done and returns a list of IDs.
-    *
-    * Won't work (will return an empty array) if all sections have already been read without
-    * scanning for sections.
-    */
-    public ID[] getSectionNames () {
-        if (fatal) return [];
-        if (!secTable.length) read([]);     // scan for sections
-        return secTable.keys;
-    }
-    
-    /** Reads (some) sections of the file into data. Note that sections will never be _read twice.
-    *
-    * To be more accurate, the file is copied into a buffer by this(). read() then parses the
-    * contents of this buffer, and stores the contents in dataset.
-    *
-    * Each section read is stored in a DataSection class. By default this is an instance of
-    * DefaultData; this can be customised (see dataSecCreator).
-    *
-    * If secSet is provided, reading is restricted to sections given in secSet, otherwise all
-    * sections are read. Sections given in secSet but not found in the file are not reported as an
-    * error. Suggested: supply a HashSet!(uint) as the View!(ID). An ArrayBag!(ID) as used is not a
-    * good choice, except that in this case it's empty.
-    *
-    * Merging:
-    * Where a section already exists in the DataSet (when either the section is given more than
-    * once in the file, or it was read from a different file by another reader) it is merged.
-    * Entries already in the DataSet take priority.
-    *
-    * Performance:
-    * Note that loading only desired sections like this still parses the sections not
-    * read (although it does not try to understand the type or data fields), so there is only a
-    * small performance advantage to this where other sections do exist in the file. There is also
-    * some overhead in only partially reading the file to keep track of where other sections are so
-    * that the entire file need not be re-read if further (or all remaining) sections are read
-    * later.
-    */
-    public void read () {
-        if (secTable.length) {
-            foreach (ID id, ref SecMD smd; secTable) {
-                if (!smd.read) {
-                    IDataSection ds = getOrCreateSec (id);
-                    parseSection (smd.pos, ds);
-                    // allRead is set true so there's no point setting smd.read = true
-                }
-            }
-        } else {					// this time we don't need to use secTable
-            for (size_t pos = endOfHeader; pos < fbuf.length;) {
-                ID id = fbufReadSecMarker (pos);
-                IDataSection ds = getOrCreateSec (id);
-                pos = parseSection (pos, ds);
-            }
-        }
-        
-        allRead = true;
-    }
-    /** ditto */
-    public void read (ID[] secSet) {
-        HashSet!(ID) hs = new HashSet!(ID);
-        foreach (id; secSet) hs.add(id);
-        read (hs);
-    }
-    /** ditto */
-    public void read (View!(ID) secSet) {
-        if (allRead || fatal) return;			// never do anything in either case
-        
-        if (secTable.length) {
-            foreach (ID id; secSet) {
-                SecMD* psmd = id in secTable;
-                if (psmd && !psmd.read) {		// may not exist
-                    IDataSection ds = getOrCreateSec (id);
-                    parseSection (psmd.pos, ds);
-                    if (ds !is null) psmd.read = true;  // getOrCreateSec may return null
-                }
-            }
-        } else {
-            for (size_t pos = endOfHeader; pos < fbuf.length;) {
-                ID id = fbufReadSecMarker (pos);
-                secTable[id] = SecMD(pos,false);	// add to table
-                if (secSet.contains(id)) {
-                    IDataSection ds = getOrCreateSec (id);
-                    pos = parseSection (pos, ds);
-                    if (ds !is null) secTable[id].read = true;
-                } else {
-                    pos = parseSection (pos, null);     // skip section
-                }
-            }
-        }
-    }
-//END METHODS: PUBLIC
-    
-//BEGIN METHODS: PRIVATE
-    /* Utility function for read
-    * Look for a section; return it if it exists otherwise create a new section:
-    *   use _dataSecCreator if it exists or just create a DefaultData if not.
-    * However if _dataSecCreator returns null don't add it to the dataset.
-    */
-    private IDataSection getOrCreateSec (ID id) {
-        IDataSection* i = id in _dataset.sec;
-        if (i) return *i;
-        else {
-            IDataSection s;
-            if (_dataSecCreator !is null) s = _dataSecCreator(id);
-            else s = new DefaultData;
-            if (s !is null) _dataset.sec[id] = s;
-            return s;
-        }
-    }
-    
-    /* Reads a section, starting from index pos, finishing at the next section marker (returning
-    the position of the start of the marker). pos should start after the section marker.
-    
-    After analysing tags, the function passes the type, ID and data to addTag.
-    
-    NOTE: from performance tests on indexing char[]'s and dereferencing char*'s, the char*'s are
-    slightly faster, but a tiny difference isn't worth the extra effort/risk of using char*'s.
-    */
-    private size_t parseSection (size_t pos, IDataSection dsec) {
-        debug scope (failure)
-                logger.trace ("MTTReader.parseSection: failure");
-        /* Searches fbuf starting from start to find one of <=>| and stops at its index.
-    
-        If quotable then be quote-aware for single and double quotes.
-        Note: there's no length restriction for the content of the quote since it could be a single
-        non-ascii UTF-8 char which would look like several chars.
-        */
-        void fbufLocateDataTagChar (ref size_t pos, bool quotable) {
-            while (true) {
-                fbufIncrement (pos);
-                
-                if ((fbuf[pos] >= '<' && fbuf[pos] <= '>') || fbuf[pos] == '|') return;
-                else if (quotable) {
-                    char c = fbuf[pos];
-                    if (c == '\'' || c == '"') {
-                        fbufIncrement(pos);
-                        while (fbuf[pos] != c) {
-                            if (fbuf[pos] == '\\') ++pos;	// escape seq.
-                            fbufIncrement(pos);
-                        }
-                    }
-                }
-            }
-        }
-        
-        // Used to ignore a tag (if it starts !< or !{ or should otherwise be ignored):
-        bool comment = false;
-        for (; pos < fbuf.length; ++pos) {
-            if (Util.isSpace(fbuf[pos])) continue;	// whitespace
-            else if (fbuf[pos] == '<') {		// data tag
-                char[] ErrDTAG = "Bad data tag format: not <type|id=data>" ~ ErrInFile;
-                
-                // Type section of tag:
-                size_t pos_s = pos + 1;
-                fbufLocateDataTagChar (pos, false);	// find end of type section
-                if (fbuf[pos] != '|') throwMTErr (ErrDTAG, new MTSyntaxException);
-                char[] type = fbuf[pos_s..pos];
-                
-                // ID section of tag:
-                pos_s = pos + 1;
-                fbufLocateDataTagChar (pos, false);	// find end of type section
-                if (fbuf[pos] != '=') throwMTErr (ErrDTAG, new MTSyntaxException);
-                ID tagID = cast(ID) fbuf[pos_s..pos];
-                
-                // Data section of tag:
-                pos_s = pos + 1;
-                fbufLocateDataTagChar (pos, true);      // find end of data section
-                if (fbuf[pos] != '>') throwMTErr (ErrDTAG, new MTSyntaxException);
-                char[] data = fbuf[pos_s..pos];
-                
-                if (!comment && dsec !is null) {
-                    type = Util.trim(type);
-                    try {
-                        dsec.addTag (type, tagID, data);
-                    }
-                    catch (TextException e) {
-                        logger.error ("TextException while reading " ~ ErrFile ~ ":");	// following a parse error
-                        logger.error (e.msg);
-                        logger.error ("Tag ignored: <"~type~"|"~tagID~"="~data~">");
-                        // No throw: tag is just ignored
-                    }
-                    catch (Exception e) {
-                        logger.error ("Unknown error occured" ~ ErrInFile ~ ':');
-                        logger.error (e.msg);
-                        throwMTErr (e.msg);             // Fatal to Reader
-                    }
-                } else comment = false;			// cancel comment status now
-            }
-            else if (fbuf[pos] == '{') {
-                if (comment) {				// simple block comment
-                    uint depth = 0;			// depth of embedded comment blocks
-                    while (true) {
-                        fbufIncrement (pos);
-                        if (fbuf[pos] == '}') {
-                            if (depth == 0) break;
-                            else --depth;
-                        } else if (fbuf[pos] == '{')
-                            ++depth;
-                    }
-                    comment = false;			// end of this comment
-                } else {
-                    return pos;				// next section coming up; we are done
-                }
-            }
-            else if (fbuf[pos] == '!') {		// possibly a comment; check next char
-                comment = true;				// starting a comment (or an error)
-                					// variable is reset at end of comment
-            } else					// must be an error
-            throwMTErr ("Invalid character (or sequence starting \"!\") outside of tag" ~ ErrInFile, new MTSyntaxException);
-        }
-        // if code execution reaches here, we're at EOF
-        // possible error: last character was ! (but don't bother checking since it's inconsequential)
-        return pos;
-    }
-    
-    /* Parses fbuf for a section marker. Already knows fbuf[pos] == '{'.
-    */
-    private ID fbufReadSecMarker (ref size_t pos) {
-        // at this point pos is whatever a parseSection run returned
-        // since we haven't hit EOF, fbuf[pos] MUST be '{' so no need to check
-        fbufIncrement(pos);
-        
-        size_t start = pos;
-        for (; pos < fbuf.length; ++pos)
-            if (fbuf[pos] == '}' || fbuf[pos] == '{') break;
-        
-        if (pos >= fbuf.length || fbuf[pos] != '}')
-            throwMTErr ("Bad section tag format: not {id}" ~ ErrInFile, new MTSyntaxException);
-        
-        ID id = cast(ID) fbuf[start..pos];
-        fbufIncrement(pos);
-        return id;
-    }
-    
-    /* Increments pos and checks it hasn't hit fbuf.length . */
-    private void fbufIncrement(ref size_t pos) {
-        ++pos;
-        if (pos >= fbuf.length) throwMTErr("Unexpected EOF" ~ ErrInFile, new MTSyntaxException);
-    }
-    
-    private void throwMTErr (char[] msg, MTException exc = new MTException) {
-        fatal = true;	// if anyone catches the error and tries to do anything --- we're dead now
-        logger.error (msg);	// report the error
-        throw exc;		// and signal our error
-    }
-//END METHODS: PRIVATE
-}
-
-
-/**
-* Class for reading a mergetag text file.
-*
-* Currently only a dummy class: a MTNotImplementedException will be thrown if created.
-*/
-class MTBReader : IReader
-{
-    public this (char[] path, DataSet ds = null, bool rdHeader = false) {
-        this (new FilePath (path), ds, rdHeader);
-    }
-    public this (PathView path, DataSet ds = null, bool rdHeader = false) {
-        throw new MTNotImplementedException;
-    }
-        
-    DataSet dataset () {                /// Get the DataSet
-        return null;
-    }
-    void dataset (DataSet) {}           /// Set the DataSet
-    
-    void dataSecCreator (IDataSection delegate (ID)) {} /// Set the dataSecCreator
-    
-    ID[] getSectionNames () {           /// Get identifiers for all sections
-        return [];
-    }
-    void read () {}                     /// Commence reading
-    void read (ID[] secSet) {}          /// ditto
-    void read (View!(ID) secSet) {}     /// ditto
-}
--- a/mde/mergetag/Writer.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,279 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/**************************************************************************************************
- * This module contains all writing functions, for both binary and text MergeTag files.
- *
- * Files can be written in a text or binary form; binary is faster and smaller while text allows
- * editing with an ordinary text editor. TextWriter and BinaryWriter are the main classes, both of
- * which implement the interface IWriter. DualWriter is another class implementing IWriter, which
- * contains a private instance of a TextWriter and a BinaryWriter and implements all methods in the
- * interface simply by chaining the appropriate method from each of these classes, thus performing
- * two writes at once.
- *
- * Any of these three classes may be used directly, or makeWriter may be invoked to create an
- * instance of the appropriate class.
- *************************************************************************************************/
- module mde.mergetag.Writer;
-
-// package imports
-public import mde.mergetag.iface.IWriter;
-import mde.mergetag.DataSet;
-import mde.mergetag.internal;
-import mde.mergetag.exception;
-
-// tango imports
-import tango.core.Exception;
-import tango.io.FileConduit;
-import tango.io.Buffer : Buffer, IBuffer;
-import tango.io.Print : Print;
-import convInt = tango.text.convert.Integer;
-import tango.util.log.Log : Log, Logger;
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.mergetag.Writer");
-}
-
-
-/** Method to create and return either a MTTWriter or a MTBWriter.
- *
- * Has two modes of operation: if method is FromExtension, examines the existing extension and
- * creates a MTT/MTB writer if the extension is mtt or mtb (throwing if not).
- *
- * Otherwise, writing format is determined directly by method, and appropriate extensions are
- * added to the file name without checking for an existing extension.
- *
- * Params:
- *  path = File path
- *  dataset = Dataset passed to Writer to write from (if null, must be set before write() is called)
- *  method = $(TABLE
- *      $(TR $(TH Value)            $(TH Writer returned)       $(TH Suffix added))
- *      $(TR $(TD FromExtension)    $(TD MTBWriter or MTTWriter)$(TD $(I none)))
- *      $(TR $(TD Binary)           $(TD MTBWriter)             $(TD .mtb))
- *      $(TR $(TD Text)             $(TD MTTWriter)             $(TD .mtt))
- *      $(TR $(TD Both)             $(TD DualWriter)            $(TD .mtb / .mtt))
- *  )
- *
- * Throws:
- *  MTFileFormatException if neither test can deduce the writing method, the supplied writing
- *  method is invalid or the determined/supplied method is not yet implemented.
- *
- * Use as:
- * -----------------------
- * DataSet dataset; // contains data to write
- * IWriter foo;
- * try {
- *   foo = makeWriter(...);
- *   foo.write();
- * }
- * catch (MTException) {}
- * -----------------------
- * Where the makeWriter line has one of the following forms:
- * -----------------------
- *   foo = makeWriter("foo.mtt", dataset);
- *   foo = makeWriter("foo", dataset, WriterMethod.Text);
- * -----------------------
- *
- * Throws:
- *  MTFileFormatException if unable to determine writing format or use requested format.
- */
-//FIXME: separate functions for separate functionality, like in Reader?
-IWriter makeWriter (char[] path, DataSet dataset = null,
-                    WriterMethod method = WriterMethod.FromExtension) {
-    if (method == WriterMethod.FromExtension) {
-        if (path.length > 4 && path[$-4..$] == ".mtt")
-            return new MTTWriter (path, dataset);
-        else if (path.length > 4 && path[$-4..$] == ".mtb")
-            return new MTBWriter (path, dataset);
-        else {
-            logger.error ("Unable to determine writing format: text or binary");
-            throw new MTFileFormatException;
-        }
-    }
-    else {
-        if (method == WriterMethod.Binary) return new MTBWriter (path~".mtb", dataset);
-        else if (method == WriterMethod.Text) return new MTTWriter (path~".mtt", dataset);
-        else if (method == WriterMethod.Both) return new DualWriter (path, dataset);
-        else throw new MTFileFormatException;
-    }
-}
-
-
-/**
- * Class to write a dataset to a file.
- *
- * Files are only actually open for writing while the write() method is running.
- *
- * Throws:
- *  $(TABLE
- *  $(TR $(TH Exception) $(TH Thrown when))
- *  $(TR $(TD MTNoDataSetException) $(TD No dataset is available to write from))
- *  $(TR $(TD MTFileIOException) $(TD An error occurs while attemting to write the file))
- *  $(TR $(TD MTException) $(TD An unexpected error occurs))
- *  )
- * Note that all exceptions extend MTException; unlike Reader exceptions don't block further calls.
- */
-class MTTWriter : IWriter
-{
-//BEGIN DATA
-    /// Get or set the DataSet (i.e. the container from which all data is written).
-    DataSet dataset () {	return _dataset;	}
-    void dataset (DataSet ds)	/// ditto
-    {	_dataset = ds;	}
-        
-    
-private:
-    // taken from tango.io.Console, mostly to make sure notepad can read our files:
-    version (Win32)
-        const char[] Eol = "\r\n";
-    else
-        const char[] Eol = "\n";
-
-    /* The container where data is written from. */
-    DataSet _dataset;
-    
-    char[] _path;
-//END DATA
-    
-//BEGIN CTOR / DTOR
-    /** Prepares to open file path for writing.
-    *
-    * The call doesn't actually execute any code so cannot fail (unless out of memory).
-    *
-    * Params:
-    * path = The name of the file to open.
-    *     Standard extensions are .mtt and .mtb for text and binary files respectively.
-    * dataset_ = If null create a new DataSet, else use existing DataSet *dataset_ and merge read
-    *     data into it.
-    */
-    public this (char[] path, DataSet ds = null) {
-        _path = path;
-        _dataset = ds;
-    }
-//END CTOR / DTOR
-    
-    /** Writes the header and all DataSections.
-     *
-     * Firstly writes the header unless it has already been written. Then writes all DataSections
-     * to the file. Thus if write is called more than once with or without changing the DataSet the
-     * header should be written only once. This behaviour could, for instance, be used to write
-     * multiple DataSets into one file without firstly merging them. Note that this behaviour may
-     * be changed when binary support is added.
-     */
-    public void write ()
-    {
-        if (!_dataset) throwMTErr ("write(): no Dataset available to write from!", new MTNoDataSetException ());
-        
-        try {
-            FileConduit conduit;	// actual conduit; don't use directly when there's content in the buffer
-            IBuffer buffer;		// write strings directly to this (use opCall(void[]) )
-            
-            // Open a conduit on the file:
-            conduit = new FileConduit (_path, FileConduit.WriteCreate);
-            scope(exit) conduit.close();
-            
-            buffer = new Buffer(conduit);	// And a buffer
-            scope(exit) buffer.flush();
-            
-            // Write the header:
-            buffer ("{MT" ~ MTFormatVersion.CurrentString ~ "}" ~ Eol);
-            if (_dataset.header !is null) writeSection (buffer, _dataset.header);
-        
-            // Write the rest:
-            foreach (ID id, IDataSection sec; _dataset.sec) {
-                writeSectionIdentifier (buffer, id);
-                writeSection (buffer, sec);
-            }
-        
-            buffer.flush();
-            
-        }
-        catch (IOException e) {
-            throwMTErr ("Error writing to file: " ~ e.msg, new MTFileIOException);
-        }
-        catch (Exception e) {
-            throwMTErr ("Unexpected exception when writing file: " ~ e.msg);
-        }
-    }
-        
-    private void writeSectionIdentifier (IBuffer buffer, ID id) {
-        buffer ("{" ~ cast(char[])id ~ "}" ~ Eol);
-    }
-    
-    private void writeSection (IBuffer buffer, IDataSection sec) {
-        void writeItem (char[] tp, ID id, char[] dt) {	// actually writes an item
-            buffer ("<" ~ tp ~ "|" ~ cast(char[])id ~"=" ~ dt ~ ">" ~ Eol);
-        }
-        sec.writeAll (&writeItem);
-        
-        buffer (Eol);			// blank line at end of each section
-    }
-    
-    private void throwMTErr (char[] msg, Exception exc = new MTException) {
-        logger.error (msg);		// report the error
-        throw exc;			// and signal our error
-    }
-}
-
-/*
-* Implement MTBWriter (and move both writers to own modules?).
-*/
-class MTBWriter : IWriter {
-    public this (char[] path, DataSet ds = null) {
-        throw new MTNotImplementedException;
-        
-        /+_path = path;
-        _dataset = ds;+/
-    }
-    
-    DataSet dataset () {
-        return null;
-    }
-    void dataset (DataSet) {}
-    
-    void write () {}
-}
-
-/* Basic implementation for mtt only.
-*
-*Implement std CTORs which add extensions to each filename and extra CTORs which take two filenames.
-*/
-class DualWriter : IWriter {
-    /** The individual writers.
-    *
-    * Potentially could be used directly, but there should be no need. */
-    MTTWriter mtt;
-    //MTBWriter mtb;  /** ditto */
-    
-    public this (char[] path, DataSet ds = null) {
-        mtt = new MTTWriter (path~".mtt", ds);
-    }
-    
-    DataSet dataset () {
-        return mtt.dataset;
-    }
-    void dataset (DataSet ds) {
-        mtt.dataset = ds;
-    }
-    
-    /** Write.
-    *
-    * Write text then binary, so the mtb file will be the most recent.
-    */
-    void write () {
-        mtt.write();
-    }
-}
--- a/mde/mergetag/deserialize.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,605 +0,0 @@
-/**************************************************************************************************
- * Generic deserialization templated function.
- *
- * copyright: Copyright (c) 2007-2008 Diggory Hardy.
- *
- * author: Diggory Hardy, diggory.hardy@gmail.com
- *
- * Supports:
- *  Associative arrays, arrays (inc. strings), structs, char types, bool, int types, float types.
- *
- * There are also some public utility functions with their own documentation.
- *
- * Throws:
- * On errors, a ParseException or a UnicodeException (both extend TextException) is thrown with a
- * suitable message. No other exceptions should be thrown.
- *
- * Examples:
- * ------------------------------------------------------------------------------------------------
- * // Basic examples:
- * ulong        a = deserialize!(ulong) ("20350");
- * float        d = deserialize!(float) ("  1.2e-9 ");
- * int[]        b = deserialize!(int[]) ("[0,1,2,3]");
- *
- * // String and char[] syntax:
- * char[]       c = deserialize!(char[]) ("\"A string\"");
- * char[]       e = deserialize!(char[]) ("['a','n','o','t','h','e','r', ' ' ,'s','t','r','i','n','g']");
- *
- * // These be used interchangably; here's a more complex example of an associative array:
- * bool[char[]] f = deserialize!(bool[char[]]) ("[ \"one\":true, ['t','w','o']:false, \"three\":1, \"four\":000 ]");
- *
- * // There is also a special notation for ubyte[] types:
- * // The digits following 0x must be in pairs and each specify one ubyte.
- * assert ( deserialize!(ubyte[]) (`0x01F2AC`) == deserialize!(ubyte[]) (`[01 ,0xF2, 0xAC]`) );
- *
- * // There's no limit to the complexity!
- * char[char[][][][char]][bool] z = ...; // don't expect me to write this!
- * ------------------------------------------------------------------------------------------------
- *
- * TODO: Optimize memory allocation (if possible?). Test best sizes for initial allocations
- * instead of merely guessing?
- *************************************************************************************************/
-//NOTE: in case of multiple formats, make this a dummy module importing both serialize modules,
-// or put all the code here.
-module mde.mergetag.deserialize;
-
-// tango imports
-import tango.core.Exception : TextException, UnicodeException;
-import cInt = tango.text.convert.Integer;
-import cFloat = tango.text.convert.Float;
-import Utf = tango.text.convert.Utf;
-import Util = tango.text.Util;
-
-/**
- * Base class for deserialize exceptions.
- */
-class ParseException : TextException
-{
-    this( char[] msg )
-    {
-        super( msg );
-    }
-}
-
-alias deserialize parseTo;      // support the old name
-
-//BEGIN deserialize templates
-
-// Associative arrays
-
-T[S] deserialize(T : T[S], S) (char[] src) {
-    src = Util.trim(src);
-    if (src.length < 2 || src[0] != '[' || src[$-1] != ']')
-        throw new ParseException ("Invalid associative array: 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] == '\\')
-                        ++i;    // escape seq.
-                    ++i;
-                }
-                // Could have an unterminated ' or " causing i >= pair.length, but:
-                // 1. Impossible: split would have thrown
-                // 2. In any case this would be caught below.
-            }
-            ++i;
-        }
-        if (i >= pair.length)
-            throw new ParseException ("Invalid associative array: encountered [ ... KEY] (missing :DATA)");
-        ret[deserialize!(S) (pair[0..i])] = deserialize!(T) (pair[i+1..$]);
-    }
-    return ret;
-}
-
-
-// Arrays
-
-T[] deserialize(T : T[]) (char[] src) {
-    src = Util.trim(src);
-    if (src.length >= 2 && src[0] == '[' && src[$-1] == ']')
-        return toArray!(T[]) (src);
-    throw new ParseException ("Invalid array: not [ ... ]");
-}
-
-// String (array special case)
-T deserialize(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 ParseException ("Invalid string: ends \\\" !");  // next char is "
-                ret[i++] = unEscapeChar (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 ParseException ("Invalid string: not quoted (\"*\") or char array (['a',...,'c'])");
-}
-// Unicode conversions for strings:
-T deserialize(T : wchar[]) (char[] src) {
-    // May throw a UnicodeException; don't bother catching and rethrowing:
-    return Utf.toString16 (deserialize!(char[]) (src));
-}
-T deserialize(T : dchar[]) (char[] src) {
-    // May throw a UnicodeException; don't bother catching and rethrowing:
-    return Utf.toString32 (deserialize!(char[]) (src));
-}
-
-// Binary (array special case)
-T deserialize(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 && src[0..2] == "0x") {
-        src = src[2..$];    // strip down to actual digits
-        
-        // Must be in pairs:
-        if (src.length % 2 == 1)
-            throw new ParseException ("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;
-    }
-    else throw new ParseException ("Invalid ubyte[]: not an array and doesn't start 0x");
-}
-
-
-// Basic types
-
-// Char
-// Assumes value is <= 127 (for valid UTF-8), since input would be invalid UTF-8 if not anyway.
-// (And we're not really interested in checking for valid unicode; char[] conversions don't either.)
-T deserialize(T : char) (char[] src) {
-    src = Util.trim(src);
-    if (src.length < 3 || src[0] != '\'' || src[$-1] != '\'')
-        throw new ParseException ("Invalid char: not 'x' or '\\x'");
-    if (src[1] != '\\') {
-        if (src.length == 3)
-            return src[1];              // Either non escaped
-        throw new ParseException ("Invalid char: too long (or non-ASCII)");
-    } else if (src.length == 4)
-        return unEscapeChar (src[2]);   // Or escaped
-    
-    throw new ParseException ("Invalid char: '\\'");
-}
-// Basic unicode convertions for wide-chars.
-// Assumes value is <= 127 as does deserialize!(char).
-T deserialize(T : wchar) (char[] src) {
-    return cast(T) deserialize!(char) (src);
-}
-T deserialize(T : dchar) (char[] src) {
-    return cast(T) deserialize!(char) (src);
-}
-
-// Bool
-T deserialize(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;  // skip leading zeros
-    if (src.length == pos && pos > 0)
-        return false;
-    if (src.length == pos + 1 && src[pos] == '1')
-        return true;
-    throw new ParseException ("Invalid bool: not true or false and doesn't evaluate to 0 or 1");
-}
-
-// Ints
-T deserialize(T : byte) (char[] src) {
-    return toTInt!(T) (src);
-}
-T deserialize(T : short) (char[] src) {
-    return toTInt!(T) (src);
-}
-T deserialize(T : int) (char[] src) {
-    return toTInt!(T) (src);
-}
-T deserialize(T : long) (char[] src) {
-    return toTInt!(T) (src);
-}
-T deserialize(T : ubyte) (char[] src) {
-    return toTInt!(T) (src);
-}
-T deserialize(T : ushort) (char[] src) {
-    return toTInt!(T) (src);
-}
-T deserialize(T : uint) (char[] src) {
-    return toTInt!(T) (src);
-}
-T deserialize(T : ulong) (char[] src) {
-    return toTInt!(T) (src);
-}
-debug (UnitTest) unittest {
-    assert (deserialize!(byte) ("-5") == cast(byte) -5);
-    // annoyingly, octal syntax differs from D (blame tango):
-    assert (deserialize!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]);
-}
-
-// Floats
-T deserialize(T : float) (char[] src) {
-    return toTFloat!(T) (src);
-}
-T deserialize(T : double) (char[] src) {
-    return toTFloat!(T) (src);
-}
-T deserialize(T : real) (char[] src) {
-    return toTFloat!(T) (src);
-}
-
-
-// Structs
-T deserialize(T) (char[] src) {
-    static assert (is(T == struct), "Unsupported type: "~typeof(T));
-    
-    src = Util.trim(src);
-    if (src.length < 2 || src[0] != '{' || src[$-1] != '}')
-        throw new ParseException ("Invalid struct: not { ... }");
-    
-    // cannot access elements of T.tupleof with non-const key, so use a type which can be
-    // accessed with a non-const key to store slices:
-    char[][T.tupleof.length] temp;
-    foreach (char[] pair; split (src[1..$-1])) {
-        uint i = 0;
-        while (i < pair.length) {   // advance to the ':'
-            char c = pair[i];
-            if (c == ':')
-                break;
-            // key must be an int so no need for string checks
-            ++i;
-        }
-        if (i >= pair.length)
-            throw new ParseException ("Invalid struct: encountered { ... KEY} (missing :DATA)");
-        
-        size_t k = deserialize!(size_t) (pair[0..i]);
-        // Note: could check no entry was already stored in temp.
-        temp[k] = pair[i+1..$];
-    }
-    T ret;
-    setStruct (ret, temp);
-    return ret;
-}
-//END deserialize templates
-
-//BEGIN Utility funcs
-/** Splits a string into substrings separated by '$(B ,)' with support for characters and strings
- * containing escape sequences and for embedded arrays ($(B [...])).
- *
- * Params:
- *     src A string to separate on commas. It shouldn't have enclosing brackets.
- *
- * Returns:
- *     An array of substrings within src, excluding commas. Whitespace is not stripped and
- *     empty strings may get returned.
- *
- * Remarks:
- *     This function is primarily intended for as a utility function for use by the templates
- *     parsing arrays and associative arrays, but it may be useful in other cases too. Hence the
- *     fact no brackets are stripped from src.
- */
-//FIXME foreach struct is more efficient
-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 ParseException ("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 (i > src.length)
-        throw new ParseException ("Unterminated quote (\' or \")");
-    
-    if (ret.length <= k)
-        ret.length = k + 1;
-    ret[k] = src[j..i];     // add final piece (i >= j)
-    return ret[0..k+1];
-}
-
-/* Templated read-int function to read (un)signed 1-4 byte integers.
- *
- * Actually a reimplementation of tango.text.convert.Integer toLong and parse functions.
- */
-private TInt toTInt(TInt) (char[] src) {
-    const char[] INT_OUT_OF_RANGE = "Integer out of range";
-    bool sign;
-    uint radix, ate, ate2;
-    
-    // Trim off whitespace.
-    // NOTE: Cannot use tango.text.convert.Integer.trim to trim leading whitespace since it doesn't
-    // treat new-lines, etc. as whitespace which for our purposes is whitespace.
-    src = Util.trim (src);
-    
-    ate = cInt.trim (src, sign, radix);
-    if (ate == src.length)
-        throw new ParseException ("Invalid integer: no digits");
-    ulong val = cInt.convert (src[ate..$], radix, &ate2);
-    ate += ate2;
-    
-    if (ate < src.length)
-        throw new ParseException ("Invalid integer at marked character: \"" ~ src[0..ate] ~ "'" ~ src[ate] ~ "'" ~ src[ate+1..$] ~ "\"");
-    
-    if (val > TInt.max)
-        throw new ParseException (INT_OUT_OF_RANGE);
-    if (sign) {
-        long sval = cast(long) -val;
-        if (sval > TInt.min)
-            return cast(TInt) sval;
-        else throw new ParseException (INT_OUT_OF_RANGE);
-    }
-    return cast(TInt) val;
-}
-
-/* Basically a reimplementation of tango.text.convert.Float.toFloat which checks for
- * whitespace before throwing an exception for overlong input. */
-private TFloat toTFloat(TFloat) (char[] src) {
-    // NOTE: As for toTInt(), this needs to strip leading as well as trailing whitespace.
-    src = Util.trim (src);
-    if (src == "")
-        throw new ParseException ("Invalid float: no digits");
-    uint ate;
-    
-    TFloat x = cFloat.parse (src, &ate);
-    return x;
-}
-
-/* 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 unEscapeChar (char c)
-{
-    // This code was generated:
-    if (c <= 'b') {
-        if (c <= '\'') {
-            if (c == '\"') {
-                return '\"';
-            } else if (c == '\'') {
-                return '\'';
-            }
-        } else {
-            if (c == '\\') {
-                return '\\';
-            } else if (c == 'a') {
-                return '\a';
-            } else if (c == 'b') {
-                return '\b';
-            }
-        }
-    } else {
-        if (c <= 'n') {
-            if (c == 'f') {
-                return '\f';
-            } else if (c == 'n') {
-                return '\n';
-            }
-        } else {
-            if (c == 'r') {
-                return '\r';
-            } else if (c == 't') {
-                return '\t';
-            } else if (c == 'v') {
-                return '\v';
-            }
-        }
-    }
-    
-    // if we haven't returned:
-    throw new ParseException ("Bad escape sequence: \\"~c);
-}
-
-// 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 ParseException ("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] = deserialize!(T) (element);
-        ++i;
-    }
-    return ret[0..i];
-}
-
-/** Set a struct's elements from an array.
-*
-* For a more generic version, see http://www.dsource.org/projects/tutorials/wiki/StructTupleof
-*/
-// NOTE: Efficiency? Do recursive calls get inlined?
-private void setStruct(S, size_t N, size_t i = 0) (ref S s, char[][N] src) {
-    static assert (is(S == struct), "Only to be used with structs.");
-    static assert (N == S.tupleof.length, "src.length != S.tupleof.length");
-    static if (i < N) {
-        if (src[i])
-            s.tupleof[i] = deserialize!(typeof(s.tupleof[i])) (src[i]);
-        setStruct!(S, N, i+1) (s, src);
-    }
-}
-//END Utility funcs
-
-debug (UnitTest) {
-    import tango.util.log.Log : Log, Logger;
-    
-    private Logger logger;
-    static this() {
-        logger = Log.getLogger ("text.deserialize");
-    }
-unittest {
-    // Utility
-    bool throws (void delegate() dg) {
-        bool r = false;
-        try {
-            dg();
-        } catch (Exception e) {
-            r = true;
-            logger.info ("Exception caught: "~e.msg);
-        }
-        return r;
-    }
-    assert (!throws ({ int i = 5; }));
-    assert (throws ({ throw new Exception ("Test - this exception should be caught"); }));
-    
-    
-    // Associative arrays
-    char[][char] X = deserialize!(char[][char]) (`['a':"animal\n", 'b':['b','u','s','\n']]`);
-    char[][char] Y = ['a':cast(char[])"animal\n", 'b':['b','u','s','\n']];
-    
-    //FIXME: when the compiler's fixed: http://d.puremagic.com/issues/show_bug.cgi?id=1671
-    // 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 (compiler bug)
-    
-    assert (throws ({ deserialize!(int[int]) (`[1:1`); }));             // bad brackets
-    assert (throws ({ deserialize!(int[char[]]) (`["ab\":1]`); }));     // unterminated quote
-    assert (throws ({ deserialize!(int[char[]]) (`["abc,\a\b\c":1]`); }));    // bad escape seq.
-    assert (throws ({ deserialize!(int[char[]]) (`["abc"]`); }));       // no data
-    
-    
-    // Arrays
-    assert (deserialize!(double[]) (`[1.0,1.0e-10]`) == [1.0, 1.0e-10]);// generic array stuff
-    assert (deserialize!(double[]) (`[     ]`) == cast(double[]) []);   // empty array
-    assert (deserialize!(int[][]) (`[[1],[2,3],[]]`) == [[1],[2,3],[]]);// sub-array
-    assert (throws ({ deserialize!(int[]) (`[1,2`); }));                // bad brackets
-    assert (throws ({ deserialize!(int[][]) (`[[1]]]`); }));            // bad brackets
-    
-    // char[] and char conversions, with commas, escape sequences and multichar UTF8 characters:
-    assert (deserialize!(char[][]) (`[ ".\"", [',','\''] ,"!\b€" ]`) == [ ".\"".dup, [',','\''] ,"!\b€" ]);
-    assert (throws ({ deserialize!(char[]) ("\"\\\""); }));
-    assert (throws ({ deserialize!(char[]) (`['a'`); }));               // bad brackets
-    
-    // wchar[] and dchar[] conversions:
-    // The characters were pretty-much pulled at random from unicode tables.
-    // The last few cause some wierd (display only) effects in my editor.
-    assert (deserialize!(wchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"w);
-    assert (deserialize!(dchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"d);
-    
-    assert (deserialize!(ubyte[]) (`0x01F2aC`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]);    // ubyte[] special notation
-    assert (deserialize!(ubyte[]) (`[01 ,0xF2, 0xAC]`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]);    // ubyte[] std notation
-    assert (throws ({ deserialize!(ubyte[]) (`0x123`); }));             // digits not in pairs
-    assert (throws ({ deserialize!(ubyte[]) (`[2,5`); }));              // not [...] or 0x..
-    assert (throws ({ deserialize!(ubyte[]) (`0x123j`); }));
-    
-    
-    // char types
-    assert (deserialize!(char) ("'\\\''") == '\'');
-    assert (deserialize!(wchar) ("'X'") == 'X');
-    assert (deserialize!(dchar) ("'X'") == 'X');
-    assert (throws ({ deserialize!(char) ("'\\'"); }));
-    assert (throws ({ deserialize!(char) ("'£'"); }));        // non-ascii
-    assert (throws ({ deserialize!(char) ("''"); }));
-    assert (throws ({ deserialize!(char) ("'ab'"); }));
-    assert (throws ({ deserialize!(wchar) ("''"); }));
-    
-    
-    // bool
-    assert (deserialize!(bool[]) (`[true,false,01,00]`) == cast(bool[]) [1,0,1,0]);
-    assert (throws ({ deserialize!(bool) ("011"); }));
-    
-    
-    // ints
-    assert (deserialize!(byte) ("-5") == cast(byte) -5);
-    assert (deserialize!(int) ("-0x7FFFFFFF") == cast(int) -0x7FFF_FFFF);
-    // annoyingly, octal syntax differs from D (blame tango):
-    assert (deserialize!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]);
-    assert (throws ({ deserialize!(int) (""); }));
-    assert (throws ({ deserialize!(int) ("0x8FFFFFFF"); }));
-    assert (throws ({ deserialize!(uint) ("-1"); }));
-    assert (throws ({ deserialize!(uint) ("1a"); }));
-    
-    
-    // floats
-    assert (deserialize!(float) ("0.0") == 0.0f);
-    assert (deserialize!(double) ("-1e25") == -1e25);
-    assert (deserialize!(real) ("5.24e-269") == cast(real) 5.24e-269);
-    assert (throws ({ deserialize!(float) (""); }));
-    
-    
-    // structs
-    struct A {  int x = 5;  char y; }
-    struct B {  A a;    float b;   }
-    A a;    a.y = 'y';
-    assert (deserialize!(A) ("{ 1 : 'y' }") == a);
-    B b;    b.a = a;    b.b = 1.0f;
-    assert (deserialize!(B) (" {1:1.0,0: { 1 : 'y' } } ") == b);
-    assert (throws ({ deserialize!(A) (" 1:'x'}"); })); // bad braces
-    assert (throws ({ deserialize!(A) ("{ 1 }"); }));     // no :DATA
-    
-    
-    // unEscapeChar
-    assert (deserialize!(char[]) ("\"\\a\\b\\t\\n\\v\\f\\r\\\"\\\'\\\\\"") == "\a\b\t\n\v\f\r\"\'\\");
-    
-    logger.info ("Unittest complete.");
-}
-}
--- a/mde/mergetag/exception.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/*******************************************
- * Contains exception classes for MergeTag.
- *
- * Publically imports mde.exception.
- ******************************************/
-module mde.mergetag.exception;
-
-public import mde.exception;
-
-/// Base MergeTag exception class.
-class MTException : mdeException {
-    char[] getSymbol () {
-        return super.getSymbol ~ ".mergetag";
-    }
-    
-    this (char[] msg) {
-        super(msg);
-    }
-    this () {   // Only called when an unexpected exception/error occurs
-        super ("Unknown exception");
-    }
-}
-
-/** Thrown on file IO errors. */
-class MTFileIOException : MTException {
-    this () {
-        super ("File IO exception");
-    }
-    this (char[] msg) {
-        super (msg);
-    }
-}
-
-/** Thrown on unknown format errors; when reading or writing and the filetype cannot be guessed. */
-class MTFileFormatException : MTException {
-    this () {
-        super ("File format exception");
-    }
-}
-
-/** Thrown on syntax errors when reading; bad tags or unexpected EOF. */
-class MTSyntaxException : MTException {
-    this () {
-        super ("Syntax exception");
-    }
-}
-
-/** Thrown by addTag (in classes implementing IDataSection) when a data parsing error occurs
-* (really just to make whoever called addTag to log a warning saying where the error occured). */
-class MTaddTagParseException : MTException {
-    this () {
-        super ("Parse exception within addTag");
-    }
-}
-
-/+
-/// Thrown by TypeView.parse on errors.
-class MTBadTypeStringException : MTException {
-    this () {}
-}
-+/
-
-/// Thrown by *Writer.write.
-class MTNoDataSetException : MTException {
-    this () {
-        super ("No dataset");
-    }
-}
-
-/// Thrown when attempting to use an unimplemented part of the package
-/// Really, just until MTB stuff is implemented
-class MTNotImplementedException : MTException {
-    this () {
-        super ("Functionality not implemented!");
-    }
-}
--- a/mde/mergetag/iface/IDataSection.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/** This module contains the interface IDataSection used by DataSet.
-*
-* It has been given its own module to avoid cyclic dependancies and separate out the functionality
-* of mergetag.
-*
-* Also some base mergetag symbols have been moved here.
-*/
-module mde.mergetag.iface.IDataSection;
-
-/** Typedef for data & section indexes.
-*
-* Make it an alias, there doesn't appear to be any point having it as a typedef. */
-alias char[] ID;
-
-/**
- * Interface for data storage classes, generally called DataSections, which contain all data-tags
- * loaded from a single section of a file.
- *
- * A class implementing this may implement the addTag function to do whatever it likes with the
- * data passed. DefaultData is one implementation which separates this data out into supported
- * types and stores it appropriately (allowing merging with existing entries by keeping whichever
- * tag was last loaded), while ignoring unsupported types. A different
- * implementation could filter out the tags desired and use them directly, and ignore the rest.
- *
- * The mde.mergetag.parse.parseTo module provides a useful set of templated functions to
- * convert the data accordingly. It is advised to keep the type definitions as defined in the file-
- * format except for user-defined types, although this isn't necessary for library operation
- * (addTag and writeAll are solely responsible for using and setting the type, ID and data fields).
- *
- * Another idea for a DataSection class:
- * Use a void*[ID] variable to store all data (may also need a type var for each item).
- * addTag should call a templated function which calls parse then casts to a void* and stores the data.
- * Use a templated get(T)(ID) method which checks the type and casts to T.
- */
-interface IDataSection
-{
-    /** Delegate passed to writeAll. */
-    typedef void delegate (char[],ID,char[]) ItemDelg;
-    
-    /** Handles parsing of data items for all recognised types.
-     *
-     * Should ignore unsupported types/unwanted tags.
-     *
-     * TextExceptions (thrown by parseTo/parseFrom) are caught and a warning logged; execution
-     * then continues (so the offending tag gets dropped). */
-    void addTag (char[],ID,char[]);
-    
-    /** Responsible for getting all data tags saved.
-    *
-    * writeAll should call the ItemDelg once for each tag to be saved with parameters in the same
-    * form as received by addTag (char[] type, ID id, char[] data). */
-    void writeAll (ItemDelg);
-}
--- a/mde/mergetag/iface/IReader.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/**
-* Interface for readers.
-*/
-module mde.mergetag.iface.IReader;
-
-import mde.mergetag.DataSet;
-
-import tango.util.collection.model.View : View;
-
-/** Interface for all mergetag readers (MTTReader etc.).
-*/
-interface IReader {
-    DataSet dataset ();                 /// Get the DataSet
-    void dataset (DataSet);             /// Set the DataSet
-    
-    void dataSecCreator (IDataSection delegate (ID));   /// Set the dataSecCreator
-    
-    ID[] getSectionNames ();            /// Get identifiers for all sections
-    void read ();                       /// Commence reading
-    void read (ID[] secSet);            /// ditto
-    void read (View!(ID) secSet);       /// ditto
-}
--- a/mde/mergetag/iface/IWriter.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/**
-* Interface for writers.
-*/
-module mde.mergetag.iface.IWriter;
-
-import mde.mergetag.DataSet;
-
-
-/** Interface for all mergetag writers (MTTWriter etc.).
-*/
-interface IWriter {
-    DataSet dataset ();                 /// Get the DataSet
-    void dataset (DataSet);             /// Set the DataSet
-    
-    void write ();                      /// Commence writing
-}
-
-/**
-* Enumeration for specifying the writing method ("Params" section shows possible values).
-*
-* Params:
-* FromExtension = Determine writing format from file name extension (must be one of .mtb or .mtt).
-* Binary = Use binary mode (adds extension .mtb without checking for an existing extension).
-* Text = Use text mode (adds extension .mtt without checking for an existing extension).
-* Both = Write simultaneously in binary and text modes (with appropriate extensions added to each
-* file name.
-*/
-enum WriterMethod : byte {
-    FromExtension = -1,
-    Binary = 1,
-    Text = 2,
-    Both = 3
-}
--- a/mde/mergetag/internal.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/// Contains functions/data structures used internally by mergetag.
-module mde.mergetag.internal;
-
-package abstract class MTFormatVersion {
-    enum VERS : ubyte {	// convenient list of all known file format versions
-        INVALID	= 0x00,
-        MT01	= 0x01,		// not yet final
-    }
-    /// The current MergeTag version
-    static const VERS Current = VERS.MT01;
-    static const char[2] CurrentString = "01";
-    
-    static VERS parseString (char[] str)
-    in {
-            assert (str.length == 2);
-    } body {
-        if (str[0] == '0' && str[1] == '1') return VERS.MT01;
-        else return VERS.INVALID;
-    }
-}
--- a/mde/mergetag/mtunittest.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-This program is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation, either
-version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>. */
-
-/// This module provides a unittest for mergetag.
-module mde.mergetag.mtunittest;
-
-debug (mdeUnitTest) {
-    import mde.mergetag.Reader;
-    import mde.mergetag.Writer;
-    import mde.mergetag.DataSet;
-    import mde.mergetag.DefaultData;
-    import mde.mergetag.parse.parseTo : parseTo;
-    import mde.mergetag.parse.parseFrom : parseFrom;
-    
-    import tango.io.FilePath;
-    import tango.util.log.Log : Log, Logger;
-    
-    private Logger logger;
-    static this() {
-        logger = Log.getLogger ("mde.mergetag.mtunittest");
-    }
-    
-    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";
-        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
-        
-        IWriter w = makeWriter (file, dsW, WriterMethod.Both);
-        w.write();
-        
-        // FIXME: when binary writing is supported, read both formats and check
-        IReader r = makeReader (file~".mtt", 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);
-                
-        IDataSection* 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`));
-        
-        // Delete the unittest file now
-        FilePath (file~".mtt").remove;
-        
-        logger.info ("Unittest complete (for DefaultData).");
-    }
-}
--- a/mde/mergetag/serialize.d	Sat Aug 30 10:54:32 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,377 +0,0 @@
-/**************************************************************************************************
- * Generic serialization templated function.
- *
- * copyright: Copyright (c) 2007-2008 Diggory Hardy.
- *
- * author: Diggory Hardy, diggory.hardy@gmail.com
- *
- * Supports:
- *  Associative arrays, arrays (inc. strings), structs, char types, bool, int types, float types.
- *
- * Examples:
- * ------------------------------------------------------------------------------------------------
- * // Basic examples:
- * Cout (serialize!(byte) (-13)).newline;                       // -13
- * Cout (serialize!(real) (2.56e11)).newline;                   // 2.55999999999999990000e+11
- * Cout (serialize!(double[]) ([0.0, 1.0, 2.0, 3.0])).newline;  // [0.00000000000000000,1.00000000000000000,2.00000000000000000,3.00000000000000000]
- * Cout (serialize ([true,false,false])).newline;               // [true,false,false]
- *
- * // String and ubyte[] special syntaxes (always used):
- * Cout (serialize ("A string.")).newline;                      // "A string." (including quotes)
- * Cout (serialize (cast(ubyte[]) [5u, 0xF1u, 0x10u])).newline; // 0x05f110
- *
- * // Associative arrays:
- * Cout (serialize ([-1:"negative one"[], 0:"zero", 1:"one"])).newline; // [0:"zero",1:"one",-1:"negative one"]
- *
- * // Structs:
- * struct S {   int a = 5;  double[int[]] x;    }
- * S s;
- * Cout (serialize (s));
- *
- * // No limit on complexity...
- * char[] somethingComplicated = serialize!(real[][][bool[int[][]]]) (...);
- * ------------------------------------------------------------------------------------------------
- *
- * TODO: Optimize memory allocation (if possible?). Test best sizes for initial allocations
- * instead of merely guessing?
- *************************************************************************************************/
-//NOTE: in case of multiple formats, make this a dummy module importing both serialize modules,
-// or put all the code here.
-module mde.mergetag.serialize;
-// Since serialize is never used in a module where deserialize is not used, save an import:
-public import mde.mergetag.deserialize;
-
-// tango imports
-import tango.core.Traits;
-import tango.core.Exception : UnicodeException, IllegalArgumentException;
-import cInt = tango.text.convert.Integer;
-import cFloat = tango.text.convert.Float;
-import Utf = tango.text.convert.Utf;
-
-
-alias serialize parseFrom;      // support the old name
-
-// Formatting options, for where multiple formats are supported by the deserializer.
-
-// Output using the special binary notation (0x01F2AC instead of [01 ,0xF2, 0xAC])?
-const bool SPECIAL_BINARY_NOTATION = true;
-
-// Output binary as true / false or 1 / 0 ?
-const bool BINARY_AS_WORDS = true;
-
-
-char[] serialize(U) (U val) {
-    // Associative arrays (NOTE: cannot use is() expression)
-    static if (isAssocArrayType!(U)) {          // generic associative array
-        alias typeof(U.keys[0])     S;
-        alias typeof(U.values[0])   T;
-        char[] ret;
-        // A guess, including values themselves and [,:] elements (must be at least 2).
-        ret.length = val.length * (defLength!(T) + defLength!(S) + 2) + 2;
-        ret[0] = '[';
-        uint i = 1;
-        foreach (S k, T v; val) {
-            char[] s = serialize!(S) (k) ~ ":" ~ serialize!(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];
-    }
-    // Arrays
-    else static if (is(U S == S[]) || isStaticArrayType!(U)) {
-        alias typeof(U[0]) T;
-        
-        static if (is(T == char)) {             // string
-            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++] = escapeChar(val[t++]);	// character; increment i and t
-                }
-            }
-            ret[i++] = '"';
-            return ret[0..i];
-        }
-        else static if (is(T == wchar) || is(T == dchar)) {   // wstring or dstring
-            // May throw a UnicodeException; don't bother catching and rethrowing:
-            return serialize!(char[]) (Utf.toString (val));
-        }
-        else static if (SPECIAL_BINARY_NOTATION && is(T == ubyte)) {    // special binary notation
-            // Note: To disable the usage of this special type, set SPECIAL_BINARY_NOTATION = false.
-            static const char[16] digits = "0123456789abcdef";
-    
-            char[] ret = new char[val.length * 2 + 2];	// exact length
-            ret[0..2] = "0x";
-            uint i = 2;
-    
-            foreach (ubyte x; val) {
-                ret[i++] = digits[x >> 4];
-                ret[i++] = digits[x & 0x0F];
-            }
-            return ret;
-        }
-        else {                                  // generic array
-            char[] ret;
-        // A guess, including commas and brackets (must be at least 2)
-            ret.length = val.length * (defLength!(T) + 1) + 2;
-            ret[0] = '[';
-            uint i = 1;
-            foreach (T x; val) {
-                char[] s = serialize!(T) (x);
-                i += s.length;
-                if (i+1 >= ret.length)
-                    ret.length = ret.length * 2;	// check length
-                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];
-        }
-    }
-    // Structs
-    else static if (is(U == struct)) {
-        char[] ret;
-        // A very rough guess.
-        ret.length = val.sizeof * 4;
-        ret[0] = '{';
-        uint i = 1;
-        foreach (k, v; val.tupleof) {
-            alias typeof(v) T;
-            char[] s = serialize!(size_t) (k) ~ ":" ~ serialize!(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];
-    }
-    // Basic types
-    else static if (is(U == char)) {            // char (UTF-8 byte)
-        // 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; so making it 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] = escapeChar (val);
-            ret[3] = '\'';
-            return ret;
-        }
-    } else static if (is(U == wchar) ||
-                      is(U == dchar)) {         // wchar or dchar (UTF-16/32 single char)
-        // Note: only ascii can be converted. NOTE: convert to UTF-8 (multibyte) char?
-        if (val <= 127u)
-            return serialize!(char) (cast(char) val);  // ASCII
-        else throw new UnicodeException (
-            "Error: unicode non-ascii character cannot be converted to a single UTF-8 char", 0);
-    } else static if (is (U == bool)) {         // boolean
-        static if (BINARY_AS_WORDS) {
-            if (val)
-                return "true";
-            else return "false";
-        } else {
-            if (val)
-                return "1";
-            else return "0";
-        }
-    } else static if (is (U : long)) {          // any integer type, except char types and bool
-        static if (is (U == ulong))             // ulong may not be supported properly
-            if (val > cast(ulong) long.max)
-                throw new IllegalArgumentException ("No handling available for ulong where value > long.max");
-        return cInt.toString (val);
-    } else static if (is (U : real)) {          // any (real) floating point type
-        char[] ret = new char[32];              // minimum allowed by assert in format
-        return cFloat.format (ret, val, U.dig+2, 1);// from old C++ tests, U.dig+2 gives best(?) accuracy
-    }
-    // Unsupported
-    else
-        static assert (false, "Unsupported type: "~U.stringof);
-}
-
-//BEGIN Utility funcs
-/* This template provides the initial length for strings for formatting various types. These strings
- * can be expanded; this value is intended to cover 90% of cases or so.
- *
- * NOTE: This template was intended to provide specialisations for different types.
- * This one value should do reasonably well for most types.
- */
-private {
-    template defLength(T)        { const uint defLength = 20; }
-    template defLength(T : char) { const uint defLength = 4;  }
-    template defLength(T : bool) { const uint defLength = 5;  }
-}
-private bool isEscapableChar (char c) {
-    return ((c <= '\r' && c >= '\a') || c == '\"' || c == '\'' || c == '\\');
-}
-// Throws on unsupported escape sequences; however this should never happen within serialize.
-private char escapeChar (char c) {
-    // This code was generated:
-    if (c <= '\v') {
-        if (c <= '\b') {
-            if (c == '\a') {
-                return 'a';
-            } else if (c == '\b') {
-                return 'b';
-            }
-        } else {
-            if (c == '\t') {
-                return 't';
-            } else if (c == '\n') {
-                return 'n';
-            } else if (c == '\v') {
-                return 'v';
-            }
-        }
-    } else {
-        if (c <= '\r') {
-            if (c == '\f') {
-                return 'f';
-            } else if (c == '\r') {
-                return 'r';
-            }
-        } else {
-            if (c == '\"') {
-                return '\"';
-            } else if (c == '\'') {
-                return '\'';
-            } else if (c == '\\') {
-                return '\\';
-            }
-        }
-    }
-    
-    // if we haven't returned:
-    throw new IllegalArgumentException ("Internal error (escapeChar)");
-}
-//END Utility funcs
-
-
-
-debug (UnitTest) {
-    import tango.util.log.Log : Log, Logger;
-
-    private Logger logger;
-    static this() {
-        logger = Log.getLogger ("text.serialize");
-    }
-unittest {
-    // Utility
-    bool throws (void delegate() dg) {
-        bool r = false;
-        try {
-            dg();
-        } catch (Exception e) {
-            r = true;
-            logger.info ("Exception caught: "~e.msg);
-        }
-        return r;
-    }
-    assert (!throws ({ int i = 5; }));
-    assert (throws ({ throw new Exception ("Test - this exception should be caught"); }));
-    
-    // Associative arrays
-    char[] X = serialize!(char[][char]) (['a':cast(char[])"animal", 'b':['b','u','s']]);
-    char[] Y = `['a':"animal",'b':"bus"]`;
-    assert (X == Y);
-    
-    
-    // Arrays
-    // generic array stuff:
-    assert (serialize!(double[]) ([1.0, 1.0e-10]) == `[1.00000000000000000,0.10000000000000000e-09]`);
-    assert (serialize!(double[]) (cast(double[]) []) == `[]`);		// empty array
-    
-    // char[] conversions, with commas, escape sequences and multichar UTF8 characters:
-    assert (serialize!(char[][]) ([ ".\""[], [',','\''] ,"!\b€" ]) == `[".\"",",\'","!\b€"]`);
-    
-    // wchar[] and dchar[] conversions:
-    // The characters were pretty-much pulled at random from unicode tables.
-    assert (serialize!(wchar[]) ("Test string: ¶α؟अกሀ搀"w) == "\"Test string: ¶α؟अกሀ搀\"");
-    assert (serialize!(dchar[]) ("Test string: ¶α؟अกሀ搀"d) == "\"Test string: ¶α؟अกሀ搀\"");
-    
-    
-    static if (SPECIAL_BINARY_NOTATION)
-        assert (serialize!(ubyte[]) (cast(ubyte[]) [0x01, 0xF2, 0xAC]) == `0x01f2ac`);	// ubyte[] special notation
-    else
-        assert (serialize!(ubyte[]) (cast(ubyte[]) [0x01, 0xF2, 0xAC]) == `[1,242,172]`);
-    
-    
-    // Structs
-    struct Foo {    int a = 9;  char b = '\v'; float c;    }
-    struct Bar {    Foo a,b;    }
-    static Foo foo1 = { a:150, b:'8', c:17.2f}, foo2;
-    Bar bar;
-    bar.a = foo1;
-    bar.b = foo2;
-    assert (serialize(bar) == "{0:{0:150,1:'8',2:1.72000007e+01},1:{0:9,1:'\\v',2:nan}}");
-    
-    
-    // Basic Types
-    // Character types
-    assert (serialize!(char) ('\'') == "\'\\\'\'");
-    assert (serialize!(wchar) ('X') == "'X'");
-    assert (serialize!(dchar) ('X') == "'X'");
-    assert (throws ({ char[] r = serialize!(wchar) ('£');   /* unicode U+00A3 */ }));
-    assert (throws ({ char[] r = serialize!(dchar) ('£'); }));
-    
-    // Bool
-    static if (BINARY_AS_WORDS)
-        assert (serialize(false) == "false");
-    else
-        assert (serialize(true) == "1");
-    
-    // Integers
-    assert (serialize (cast(byte) -5) == "-5");
-    assert (serialize (cast(short) -32768) == "-32768");
-    assert (serialize (-5) == "-5");
-    assert (serialize (-9223372036854775807L) == "-9223372036854775807");
-    assert (serialize (cast(ubyte) -1) == "255");
-    assert (serialize (cast(ushort) -1) == "65535");
-    assert (serialize!(uint) (-1) == "4294967295");
-    assert (serialize (cast(ulong) 0x7FFF_FFFF_FFFF_FFFFLu) == "9223372036854775807");
-    assert (serialize!(uint[]) ([0b0100u,0724,0xFa59c,0xFFFFFFFF,0]) ==
-                               "[4,468,1025436,4294967295,0]");
-    assert (throws ({
-        // ulong is not properly supported.
-        // NOTE: this is something that should really work.
-        char[] r = serialize!(ulong) (0x8FFF_FFFF_FFFF_FFFFLu);
-    }));
-    
-    // Floats
-    // These numbers are not particularly meaningful:
-    assert (serialize!(float) (0.0f) == "0.00000000");
-    assert (serialize!(double) (-1e25) == "-1.00000000000000000e+25");
-    assert (serialize!(real) (cast(real) 4.918e300) == "4.91800000000000000000e+300");
-    
-    // Escape sequences (test conversion functions)
-    assert (serialize ("\a\b\t\n\v\f\r\"\'\\") == `"\a\b\t\n\v\f\r\"\'\\"`);
-    
-    logger.info ("Unittest complete.");
-}
-}
--- a/mde/setup/init2.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/setup/init2.d	Sun Aug 31 15:59:17 2008 +0100
@@ -52,6 +52,7 @@
 void guiLoad () {   // init func
     try {
         font.FontStyle.initialize;
+        gui.init;
         gui.loadDesign();
         cleanup.addFunc (&guiSave, "guiSave");
     } catch (Exception e) {
--- a/mde/setup/paths.d	Sat Aug 30 10:54:32 2008 +0100
+++ b/mde/setup/paths.d	Sun Aug 31 15:59:17 2008 +0100
@@ -32,10 +32,10 @@
 module mde.setup.paths;
 
 import mde.exception;
-import mde.mergetag.Reader;
-import mde.mergetag.Writer;
-import mde.mergetag.DataSet;
-import mde.mergetag.exception;
+import mde.file.mergetag.Reader;
+import mde.file.mergetag.Writer;
+import mde.file.mergetag.DataSet;
+import mde.file.mergetag.exception;
 
 import tango.io.Console;
 import tango.io.FilePath;
@@ -105,15 +105,6 @@
         return ret;
     }
     
-    /** Check whether the given file exists under any path with either .mtt or .mtb suffix. */
-    bool exists (char[] file) {
-        for (uint i = 0; i < pathsLen; ++i) {
-            if (FilePath (paths[i]~file~".mtt").exists) return true;
-            if (FilePath (paths[i]~file~".mtb").exists) return true;
-        }
-        return false;
-    }
-    
     /// Print all paths found.
     static void printPaths () {
         Cout ("Data paths found:");