changeset 89:97e6dce08037

Solved some/removed some obsolete jobs/FIXMEs (excluding from gui code). General cleanup.
author Diggory Hardy <diggory.hardy@gmail.com>
date Mon, 29 Sep 2008 18:27:17 +0100
parents 01f4f5f1acc9
children b525ff28774b
files codeDoc/jobs.txt data/L10n/OptionsMisc.mtt data/conf/options.mtt mde/file/deserialize.d mde/file/mergetag/mdeUT.d mde/file/serialize.d mde/font/FontTexture.d mde/font/font.d mde/gl/texture.d mde/input/Input.d mde/input/joystick.d mde/lookup/Options.d mde/mde.d mde/setup/Init.d
diffstat 14 files changed, 131 insertions(+), 247 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Mon Sep 29 12:09:44 2008 +0100
+++ b/codeDoc/jobs.txt	Mon Sep 29 18:27:17 2008 +0100
@@ -13,23 +13,16 @@
 
 To do (importance 0-5: 0 pointless, 1 no obvious impact now, 2 todo sometime, 3 useful, 4 important, 5 urgent):
 Also see todo.txt and FIXME/NOTE comment marks.
-4   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. Could be to do with order init functions are run in? Crash in threaded mode.
-4   Try to correlate names of option sections more. (i.e. symbol name, class name, name of i18n translation file)
-4   Not guaranteed to catch up-click ending callback! Appears not to be a problem...
-4   OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
+3   Try to correlate names of option sections more. (i.e. symbol name, class name, name of i18n translation file)
 3   Use of dtors - don't rely on them? Or what happens when init throws during creation - relying on undefined behaviour.
-3   Fonts from Options. Get yMax for font not all glyphs on line?
-3   glBindTexture not working with non-0 index (??)
-3   on-event draw support (mde.events and GUI need to tell mde.mde)
-3   Scheduler for drawing only windows which need redrawing.
-3   Update scheduler as outlined in FIXME.
+3   glBindTexture not working with non-0 index - perhaps use a higher level graphics library at some point.
 3   Windows building/compatibility (currently partial) - tango/sys/win32/SpecialPath.d
-2   Remove ability to scan, then load, mergetag sections. Not so necessary with section creator callback and allows "sliding window" type partial buffering.
+2   Remove ability to scan, then load, mergetag sections. Not so necessary with section creator callback and allows "sliding window" type partial buffering. Also remove dataset and force use of section creator callback?
 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.
 2   Consider replacing byte/short types with int type
 2   File loading from compressed archives
-2   gdc building/compatibility (wait for tango 0.99.5 release?)
+2   gdc compatibility - now due to SDL?
 2   Sensitivity adjustments. From es_a_out:
         /+ FIXME: revise.
         + I can't see any point using HALF_RANGE here, since it should really be used dependant on
@@ -57,7 +50,7 @@
         y = sign(y) * pow(abs(y), a);		// sensitivity adjustment by a +/
         myThis.axis[cast(inputID) s.pop()] = y;
         +/
-1   Mergetag binary support
+1   Mergetag binary support / other file formats.
 
 
 Done (for mercurial log message):
--- a/data/L10n/OptionsMisc.mtt	Mon Sep 29 12:09:44 2008 +0100
+++ b/data/L10n/OptionsMisc.mtt	Mon Sep 29 18:27:17 2008 +0100
@@ -1,6 +1,6 @@
 {MT01}
 {en-GB}
-<entry|useThreads={0:"Use threads",1:"Global option for threading in mde."}>
+<entry|maxThreads={0:"Max threads",1:"Maximum number of threads to use in mde (currently only applies to init stages run in parallel)."}>
 <entry|logLevel={0:"Logging level",1:"Controls which messages are logged, from 0=trace to 6=none (default: 1=info)."}>
 <entry|L10n={0:"Localisation",1:"Specifies the language to use."}>
 <entry|pollInterval={0:"Polling interval",1:"Delay in main loop to limit CPU usage"}>
--- a/data/conf/options.mtt	Mon Sep 29 12:09:44 2008 +0100
+++ b/data/conf/options.mtt	Mon Sep 29 18:27:17 2008 +0100
@@ -1,6 +1,6 @@
 {MT01}
 {misc}
-<int|numThreads=2>
+<int|maxThreads=4>
 <bool|exitImmediately=false>
 <char[]|L10n="en-GB">
 <int|logOptions=0x3000>
--- a/mde/file/deserialize.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/file/deserialize.d	Mon Sep 29 18:27:17 2008 +0100
@@ -84,7 +84,7 @@
         throw new ParseException ("Invalid associative array: not [ ... ]");  // bad braces.
     
     T[S] ret;
-    foreach (char[] pair; split (src[1..$-1])) {
+    foreach (char[] pair; Split (src[1..$-1])) {
         uint i = 0;
         while (i < pair.length) {   // advance to the ':'
             char c = pair[i];
@@ -97,7 +97,7 @@
                     ++i;
                 }
                 // Could have an unterminated ' or " causing i >= pair.length, but:
-                // 1. Impossible: split would have thrown
+                // 1. Impossible: Split would have thrown
                 // 2. In any case this would be caught below.
             }
             ++i;
@@ -296,7 +296,7 @@
     // 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])) {
+    foreach (char[] pair; Split (src[1..$-1])) {
         uint i = 0;
         while (i < pair.length) {   // advance to the ':'
             char c = pair[i];
@@ -322,61 +322,71 @@
 /** 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.
+ * ---
+ * foreach (element; Split(src))
+ *     ...
+ * ---
+ * Where src is 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.
+ * Output elements are substrings of src separated by commas, excluding the commas.
+ * Not all 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
+ *     This struct is primarily intended for as a utility 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
+struct Split {
+    static Split opCall (char[] source) {
+        Split ret;
+        ret.src = Util.trim (source);
+        return ret;
+    }
+    
+    int opApply(int delegate(ref char[]) dg)
+    {
+        if (src == "")
+            return 0;
+        
+        int result = 0;
+        
+        uint depth = 0;         // surface depth (embedded arrays)
+        size_t i = 0, j = 0;    // current read location, start of current piece
     
-    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
+        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
+                char[] t = src[j..i];
+                result = dg(t);   // add this piece and increment k
+                if (result)
+                    return result;
+                j = i + 1;
+            }
             ++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 \")");
+        
+        char[] t = src[j..i];
+        result = dg(t);     // add final piece (i >= j)
+        return result;
     }
-    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];
+    char[] src;
 }
 
 /* Templated read-int function to read (un)signed 1-4 byte integers.
@@ -486,7 +496,7 @@
 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])) {
+    foreach (char[] element; Split(src[1..$-1])) {
         if (i == ret.length) ret.length = ret.length * 2;
         ret[i] = deserialize!(T) (element);
         ++i;
--- a/mde/file/mergetag/mdeUT.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/file/mergetag/mdeUT.d	Mon Sep 29 18:27:17 2008 +0100
@@ -60,7 +60,7 @@
         IWriter w = makeWriter (file, dsW, WriterMethod.Both);
         w.write();
         
-        // FIXME: when binary writing is supported, read both formats and check
+        // FIXME (unittest): when binary writing is supported, read both formats and check
         IReader r = makeReader (FilePath (file~".mtt"), null, true);
         r.read();
         
@@ -77,7 +77,7 @@
         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.
+        // FIXME (unittest): 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;
--- a/mde/file/serialize.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/file/serialize.d	Mon Sep 29 18:27:17 2008 +0100
@@ -52,8 +52,8 @@
  *************************************************************************************************/
 //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.
+//FIXME: Optimize by using a slicing buffer. Put everything in a struct containing this buffer or
+// pass buffer to functions 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;
--- a/mde/font/FontTexture.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/font/FontTexture.d	Mon Sep 29 18:27:17 2008 +0100
@@ -51,9 +51,6 @@
   * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */
 class FontTexture
 {
-    this () {}
-    ~this () {}
-    
     // Call if font(s) have been changed and glyphs must be recached.
     void clear () {
         foreach (t; tex) {
@@ -89,33 +86,15 @@
         
         int lineSep = face.size.metrics.height >> 6;
         bool hasKerning = (FT_HAS_KERNING (face) != 0);
-        int y = 0;
+        int y = face.size.metrics.ascender >> 6;
         CharCache cc;		// struct; reused for each character
         
         for (size_t i = 0; i < chrs.length; ++i)
         {
-            // First, find yMax for the current line.
-            int yMax = 0;	// Maximal glyph height above baseline.
-            for (size_t j = i; j < chrs.length; ++j)
-            {
-                if (chrs[j] == '\n' || chrs[j] == '\r')	// end of line
-                    break;
-                
-                GlyphAttribs* ga = chrs[j] in cachedGlyphs;
-                if (ga is null) {			// Not cached
-                    addGlyph (face, chrs[j]);		// so render it
-                    ga = chrs[j] in cachedGlyphs;	// get the ref of the copy we've stored
-                    assert (ga !is null, "ga is null: 1");
-                }
-                
-                if (ga.top > yMax)
-                    yMax = ga.top;
-            }
-            y += yMax;
-            
-            // Now for the current line:
+            // For each line:
             int x = 0;	// x pos for next glyph
             uint gi_prev = 0;	// previous glyph index (needed for kerning)
+            
             for (; i < chrs.length; ++i)
             {
                 // If end-of-line, break to find yMax for next line.
@@ -128,7 +107,11 @@
                 }
                 
                 cc.ga = chrs[i] in cachedGlyphs;
-                assert (cc.ga !is null, "ga is null: 2");
+                if (cc.ga is null) {                    // Not cached
+                    addGlyph (face, chrs[i]);           // so render it
+                    cc.ga = chrs[i] in cachedGlyphs;    // get the ref of the copy we've stored
+                    assert (cc.ga !is null, "Unable to cache glyph!");
+                }
                 
                 // Kerning
                 if (hasKerning && (gi_prev != 0)) {
@@ -149,11 +132,12 @@
                 // height maximal x and y coordinates.
                 if (cc.xPos + cc.ga.w > cache.w)
                     cache.w = cc.xPos + cc.ga.w;
-                if (cc.yPos + cc.ga.h > cache.h)
-                    cache.h = cc.yPos + cc.ga.h;
+                /+if (cc.yPos + cc.ga.h > cache.h)
+                    cache.h = cc.yPos + cc.ga.h;+/
             }
             // Now increment i and continue with the next line if there is one.
-            y += lineSep - yMax;
+            y += lineSep;
+            cache.h += lineSep;
         }
     }
     
@@ -356,7 +340,7 @@
     // create a new texture
     static LinePacker create () {
         LinePacker p;
-        //FIXME: why do I get a blank texture when binding to non-0?
+        //FIXME (gl): why do I get a blank texture when binding to non-0?
         //glGenTextures (1, &(p.texID));
         p.texID = 0;
         
--- a/mde/font/font.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/font/font.d	Mon Sep 29 18:27:17 2008 +0100
@@ -127,12 +127,11 @@
             return StageState.ACTIVE;
         }
         
-        //FIXME: don't use GC for FontStyle resources
         /** Cleanup: delete all fonts. */
         StageState cleanup () {
             // Clear loaded fonts (each has an FT_Face object needing to be freed):
             foreach (fs; fonts)
-                delete fs;
+                fs.freeFace;
             
             FT_Done_FreeType (library); // free the library
             
@@ -237,11 +236,11 @@
     /** Draw a block of text (may inlcude new-lines).
      *
      * The text block is drawn with top-left corner at x,y. To put the text's baseline at a given
-     * y coordinate would require some changes. Line height is currently variable, depending on the
-     * highest glyph in the line (should probably be fixed: FIXME).
+     * y coordinate would require some changes. Line height is fixed based on largest glyph.
+     * Due to hinter, glyphs are not guaranteed to lie within the "bounding box" defined by cache.
+     * Can be changed to test size of each glyph if necessary.
      *
-     * Specify the text's colour with col; currently this is only Colour.WHITE or Colour.BLACK
-     * (FIXME). FIXME: add alpha support.
+     * Specify the text's colour with col. Use textBlockA() instead  for transparent text.
      *
      * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as
      * the cache argument. This is the recommended method, although for one-time calls when you
@@ -261,7 +260,7 @@
      * the updateBlock function. */
     void textBlock (int x, int y, char[] str, ref TextBlock cache, Colour col)
     in {
-        debug assert (face, "FontStyle: face is null");
+        assert (face, "FontStyle: face is null");
     } body {
         try {
             fontTex.drawCache (face, str, cache, x, y, col);
@@ -272,7 +271,7 @@
     /** ditto */
     void textBlock (int x, int y, char[] str, Colour col)
     in {
-        debug assert (face, "FontStyle: face is null");
+        assert (face, "FontStyle: face is null");
     } body {
         try {
             // Using the cache method for one-time use is slightly less than optimal, but doing so
@@ -291,7 +290,7 @@
      * details. */
     void textBlockA (int x, int y, char[] str, ref TextBlock cache, Colour col) 
     in {
-        debug assert (face, "FontStyle: face is null");
+        assert (face, "FontStyle: face is null");
     } body {
         try {
             fontTex.drawCacheA (face, str, cache, x, y, col);
@@ -301,12 +300,16 @@
     }
     
     /** The font-specified vertical distance between the baseline of consecutive lines. */
-    int getLineSeparation () {
+    int getLineSeparation ()
+    in {
+        assert (face, "FontStyle: face is null");
+    } body {
         return face.size.metrics.height >> 6;
     }
     
-    ~this () {
+    void freeFace () {
         FT_Done_Face (face);
+        face = null;    // functions using face use assertions on face to check its validity.
     }
     
 private:
--- a/mde/gl/texture.d	Mon Sep 29 12:09:44 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +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 is unused and untested. The main reason being using glBindTexture to use multiple
-textures isn't working for me.
----
-
-/** Simple module for loading images.
-*/
-module mde.resource.image;
-
-import mde.resource.exception;
-
-import tango.std.stringz;
-
-import derelict.sdl.sdl;
-import derelict.sdl.image;
-import derelict.opengl.gl;
-
-/// The usual texture....
-alias Texture!(GL_TEXTURE_2D) Texture2D;
-
-/** Represents a texture. */
-class Texture(TARGET)
-{
-    static assert (TARGET == GL_TEXTURE_1D ||
-            TARGET == GL_TEXTURE_2D ||
-            TARGET == GL_TEXTURE_3D ||
-                    TARGET == GL_TEXTURE_CUBE_MAP);
-    
-    /** Create a new Texture.
-     *
-     * The Texture is not associated with an OpenGL texture name until load is called.
-     *
-     * Params:
-     *  components = The number of components per pixel, 3 (RGB) or 4 (RGBA), or 0 in which case
-     *  	components is derived from the image loaded.
-     */
-    this (ubyte components) {
-        components_ = components;
-    }
-    /// Free the texture ID.
-    ~this () {
-        glDeleteTextures(1, &texID);
-    }
-    
-    /// Bind as current opengl texture.
-    void bind () {
-        assert (texID != 0, "No texture loaded yet!");
-        glBindTexture (TARGET, texID);
-    }
-    
-    /// Load from a file
-    void load (char[] file) {
-        static assert (TARGET == GL_TEXTURE_2D);	// no other support
-        
-        assert (texID == 0, "Texture already loaded.");
-        glGenTextures (1, &texID);
-        bind;
-        
-        SDL_Surface* image;
-        image = IMG_Load (toStringz(file));
-        if (image is null)
-            throw new ImageException ("Unable to load "~file);
-        // SDL_Surfaces sometimes need locking... really just for spec compliance:
-        assert (!SDL_MUSTLOCK(image), "Didn't expect to have to lock a surface loaded from a file; no locks used!");
-        
-        GLenum format;
-        if (image.format.BytesPerPixel == 3)
-            format = GL_RGB;
-        else if (image.format.BytesPerPixel == 4)
-            format = GL_RGBA;
-        else
-            throw new ImageException ("Only 8-bit-per-channel RGB/RGBA images are supported");
-        
-        // Assume format is RGB(A); i.e. don't bother checking what
-        // image.format.[RGBA]mask/shift/loss are.
-        
-        if (components_ == 0)
-            components_ = image.format.BytesPerPixel;
-        
-        if (image.pitch != image.w)
-            throw new ImageException ("pitch != width; this is unsupported");
-        
-        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-        glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
-        glTexImage2D(TARGET, 0, components_,
-                     w, h, 0,
-                     format, GL_UNSIGNED_BYTE, image.pixels);
-        
-        SDL_FreeSurface (image);
-    }
-    
-    int width () {	return w;	}
-    int height () {	return h;	}
-    /// 3 for RGB, 4 for RGBA, 0 if no image loaded and format will be derived from the image.
-    ubyte components () {	return components_;	}
-    
-    private {
-        int w, h;		// size
-        ubyte components_ = 0;	// 3 for RGB, 4 for RGBA
-        uint texID = 0;
-    }
-}
-
----
-+/
\ No newline at end of file
--- a/mde/input/Input.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/input/Input.d	Mon Sep 29 18:27:17 2008 +0100
@@ -389,7 +389,7 @@
     ushort mouse_x, mouse_y;		// Current screen coords of the window manager mouse
     RelPair[inputID] relMotion;		// Table of relative mouse / joystick ball motions
     
-    // FIXME: currently no means of removal
+    // NOTE: currently no means of removal
     ButtonCallback[][inputID]	buttonCallbacks;
     AxisCallback[][inputID]	axisCallbacks;
     RelMotionCallback[][inputID] relMotionCallbacks;
--- a/mde/input/joystick.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/input/joystick.d	Mon Sep 29 18:27:17 2008 +0100
@@ -53,7 +53,7 @@
 /// Cleanup fct.
 StageState closeJoysticks () {
     foreach (js; joysticks) {
-        // FIXME: This sometimes causes a SIGSEGV (Address boundary error) when init fails.
+        // NOTE: This sometimes causes a SIGSEGV (Address boundary error) when init fails.
         if(js !is null) SDL_JoystickClose(js);	// only close if successfully opened
     }
     return StageState.INACTIVE;
--- a/mde/lookup/Options.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/lookup/Options.d	Mon Sep 29 18:27:17 2008 +0100
@@ -37,6 +37,10 @@
 
 import tango.core.Exception : ArrayBoundsException;
 import tango.util.log.Log : Log, Logger;
+private Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.lookup.Options");
+}
 
 /** Base class for handling options.
 *
@@ -50,6 +54,10 @@
 * not be saved; instead use set(), for example, miscOpts.set!(char[])("L10n","en-GB"). Use an
 * example like OptionsMisc as a template for creating a new Options sub-class.
 *
+* Optionally, overload the validate() function. This is called after loading, allowing conditions
+* to be enforced on variables. Use set!()() to change the variables. If an exception is thrown,
+* init will abort and the executable won't start.
+*
 * Details: Options sub-classes hold associative arrays of pointers to all option variables, with a
 * char[] id. This list is used for saving, loading and to provide generic GUI options screens. The
 * built-in support in Options is only for bool, int and char[] types (a float type may get added).
@@ -165,6 +173,8 @@
                 logger.warn ("Loading options failed: "~e.msg);
                 logger.warn ("If warning persists, delete the offending file.");        // FIXME - delete the bad file somehow
             }
+            foreach (opts; subClasses)
+                opts.validate;  // post-load checking of variables
         }
         void save () {
             if (!changed) return;   // no changes to save
@@ -198,11 +208,6 @@
                 logger.error ("Saving options aborted: "~e.msg);
             }
         }
-    
-        private Logger logger;
-        static this() {
-            logger = Log.getLogger ("mde.lookup.Options");
-        }
     }
     //END Static
     
@@ -266,6 +271,9 @@
         return optsVars.keys;
     }
     
+    /// Variable validate function. This implementation does nothing.
+    void validate() {}
+    
     protected {
         OptionChanges optionChanges;	// all changes to options (for saving)
     	
@@ -460,7 +468,17 @@
 /** A home for all miscellaneous options, at least for now. */
 OptionsMisc miscOpts;
 class OptionsMisc : Options {
-    mixin (impl!("bool exitImmediately; int numThreads, logOptions; double pollInterval; char[] L10n, a,b,c;"));
+    mixin (impl!("bool exitImmediately; int maxThreads, logOptions; double pollInterval; char[] L10n, a,b,c,g,z;"));
+    
+    void validate() {
+        // Try to enforce sensible values, whilst being reasonably flexible:
+        if (miscOpts.maxThreads < 1 || miscOpts.maxThreads > 64) {
+            logger.warn ("maxThreads must be in the range 1-64. Defaulting to 4.");
+            miscOpts.set!(int)("maxThreads", 4);
+        }
+        if (pollInterval !<= 1.0 || pollInterval !>= 0.0)
+            set!(double) ("pollInterval", 0.01);
+    }
     
     static this() {
         miscOpts = new OptionsMisc;
--- a/mde/mde.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/mde.d	Mon Sep 29 18:27:17 2008 +0100
@@ -21,11 +21,11 @@
 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.setup.Screen;                // Screen.draw()
+import mde.events;                      // pollEvents()
 import mde.lookup.Options;              // pollInterval option
 import mde.scheduler.Scheduler;         // mainSchedule
-import mde.setup.Screen;                // Screen.draw()
 
 import tango.core.Thread : Thread;	// Thread.sleep()
 import tango.time.Clock;                // Clock.now()
@@ -69,10 +69,6 @@
     // Note: must create the drawable after init, since it uses font (initialized in init).
     Screen.addDrawable (new SimpleDrawable);    // a drawable to print a message.
     
-    // Make sure pollInterval has a sane value. FIXME: get Options class to enforce range
-    if (miscOpts.pollInterval !<= 1.0 || miscOpts.pollInterval !>= 0.0)
-        miscOpts.set!(double) ("pollInterval", 0.01);
-    
     //BEGIN Main loop setup
     /* Note: the main loop is currently controlled by the scheduler. This is not really ideal,
      * since it provides no direct control of the order in which components are executed and does
--- a/mde/setup/Init.d	Mon Sep 29 12:09:44 2008 +0100
+++ b/mde/setup/Init.d	Mon Sep 29 18:27:17 2008 +0100
@@ -213,8 +213,6 @@
         foreach (key,stage_p; stages)
             foreach (name; stage_p.depends)
                 stages[name].rdepends ~= key;
-        if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64)        // limit to a sensible number of threads
-            miscOpts.set!(int)("numThreads", 4);        // FIXME enforce limit in Options
         
         runStages!(true);       // startup delegates
         
@@ -258,7 +256,8 @@
             }
         }
         auto toRunIt = toRun.iterator;
-        int numWorking = miscOpts.numThreads;
+        // Counts number of active threads, and before threads are started is number to use:
+        size_t numWorking = (toRun.size < miscOpts.maxThreads) ? toRun.size : miscOpts.maxThreads;
         enum STATE {    WORKING = 0,    DONE = 1,       ABORT = 2 }
         STATE doneInit = STATE.WORKING;
         Mutex toRunM = new Mutex;       // synchronization on toRun, numWorking
@@ -359,10 +358,10 @@
             return;
         }
         
-        // Start miscOpts.NumThreads - 1 threads:
+        // Start min(miscOpts.maxThreads,toRun.size)-1 threads:
         try {
             ThreadGroup g = new ThreadGroup;
-            for (int i = miscOpts.numThreads; i > 1; --i)
+            for (size_t i = numWorking; i > 1; --i)
                 g.create (&initThreadFct);
             initThreadFct();    // also run in current thread
             g.joinAll (false);  // don't rethrow exceptions - there SHOULD NOT be any
@@ -446,8 +445,8 @@
         foreach (key,stage_p; stages)
             foreach (name; stage_p.depends)
                 stages[name].rdepends ~= key;
-        if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64)        // limit to a sensible number of threads
-            miscOpts.set!(int)("numThreads", 4);        // FIXME enforce limit in Options
+        auto realNumThreads = miscOpts.numThreads;
+        miscOpts.set!(int)("numThreads", 4);    // force 4 threads for unittest
         
         
         // Run the above.
@@ -486,6 +485,7 @@
         assert (stages[toStageName("stg3")].state == cast(StageState)7);        // set by the exception
         
         stages = realInit;      // restore the real init stages
+        miscOpts.set!(int)("numThreads", realNumThreads);
         logger.info ("Unittest complete.");
     }
 }