changeset 30:467c74d4804d

Major changes to the scheduler, previously only used by the main loop. Revamped Scheduler. Functions can be removed, have multiple schedules, have their scheduling changed, etc. Scheduler has a unittest. Checked all pass. Main loop scheduler moved to mde. Draw-on-demand currently disabled, simplifying this. Made mtunitest.d remove the temporary file it uses afterwards. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Mon, 28 Apr 2008 10:59:47 +0100
parents f985c28c0ec9
children baa87e68d7dc
files codeDoc/jobs.txt mde/events.d mde/gl.d mde/global.d mde/gui/IWindow.d mde/gui/Widget.d mde/gui/gui.d mde/mde.d mde/mergetag/mtunittest.d mde/scheduler/Init.d mde/scheduler/InitFunctions.d mde/scheduler/Scheduler.d mde/scheduler/runTime.d test/mdeTest.d
diffstat 14 files changed, 462 insertions(+), 277 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Sat Apr 12 14:10:13 2008 +0100
+++ b/codeDoc/jobs.txt	Mon Apr 28 10:59:47 2008 +0100
@@ -3,14 +3,22 @@
 
 
 In progress:
+Started buttonWidget (on hold)
+
 
 
-To do:
-Also see todo.txt.
-*   Windows building/compatibility (currently partial)
-*   gdc building/compatibility (wait for tango 0.99.5 release?)
-*	OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
-*	Sensitivity adjustments. From es_a_out:
+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   OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
+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   Windows building/compatibility (currently partial)
+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   Sensitivity adjustments. From es_a_out:
         /+ FIXME: revise.
         + I can't see any point using HALF_RANGE here, since it should really be used dependant on
         + the device attached, not the axis. Also what about adjusted range like X3's throttle?
@@ -37,9 +45,11 @@
         y = sign(y) * pow(abs(y), a);		// sensitivity adjustment by a +/
         myThis.axis[cast(inputID) s.pop()] = y;
         +/
+1   Mergetag binary support
 
 
 Done (for git log message):
-GUI: Implemented a GridWidget to layout several sub-widgets.
-Improved log messages about init functions.
-Moved all dynamic-library loading into a separate init stage.
\ No newline at end of file
+Revamped Scheduler. Functions can be removed, have multiple schedules, have their scheduling changed, etc.
+Scheduler has a unittest. Checked all pass.
+Main loop scheduler moved to mde. Draw-on-demand currently disabled, simplifying this.
+Made mtunitest.d remove the temporary file it uses afterwards.
--- a/mde/events.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/events.d	Mon Apr 28 10:59:47 2008 +0100
@@ -17,16 +17,16 @@
 module mde.events;
 
 import global = mde.global;
-static import mde.SDL;
+import sdl = mde.SDL;           // resizeWindow
 
 import mde.input.input;
 import mde.input.exception;
 
 import mde.scheduler.InitFunctions;
-import mde.scheduler.runTime;
 
 import derelict.sdl.events;
 
+import tango.time.Time;
 import tango.util.log.Log : Log, Logger;
 
 private Logger logger;
@@ -34,8 +34,6 @@
     logger = Log.getLogger ("mde.events");
     
     init.addFunc (&initInput, "initInput");
-
-    Scheduler.perFrame (&mde.events.pollEvents);
 }
 
 void initInput () { // init func
@@ -54,7 +52,7 @@
     }
 }
 
-void pollEvents (double) {
+void pollEvents (TimeSpan) {
     SDL_Event event;
     while (SDL_PollEvent (&event)) {
         switch (event.type) {
@@ -63,13 +61,13 @@
                 global.run = false;
                 break;
             case SDL_VIDEORESIZE:
-                mde.SDL.resizeWindow (event.resize.w, event.resize.h);
-                Scheduler.requestUpdate(RF_KEYS.DRAW);
+                sdl.resizeWindow (event.resize.w, event.resize.h);
+                //global.scheduler.request(global.SCHEDULE.DRAW);
                 break;
-            case SDL_ACTIVEEVENT:
+            /+case SDL_ACTIVEEVENT:
             case SDL_VIDEOEXPOSE:
-                Scheduler.requestUpdate(RF_KEYS.DRAW);
-                break;
+                //global.scheduler.request(global.SCHEDULE.DRAW);
+                break;+/
             default:
                 try {
                     global.input (event);
--- a/mde/gl.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/gl.d	Mon Apr 28 10:59:47 2008 +0100
@@ -18,14 +18,13 @@
 * Everything here is really intended as makeshift code to enable GUI development. */
 module mde.gl;
 
-import mde.scheduler.runTime;
+import global = mde.global;
+import mde.gui.gui;
 
 import derelict.sdl.sdl;
 import derelict.opengl.gl;
 
-static this () {
-    Scheduler.perRequest (RF_KEYS.DRAW, &mde.gl.draw);
-}
+import tango.time.Time;     // TimeSpan (type only; unused)
 
 //BEGIN GL & window setup
 void glSetup () {
@@ -65,19 +64,12 @@
 
 //BEGIN Drawing loop
 // Temporary draw function
-void draw () {
+void draw (TimeSpan) {
     glClear(GL_COLOR_BUFFER_BIT);
     
-    foreach (func; drawCallbacks)
-        func();
+    gui.draw ();
     
     glFlush();
     SDL_GL_SwapBuffers();
 }
-
-alias void delegate() DrawingFunc;
-void addDrawCallback (DrawingFunc f) {
-    drawCallbacks ~= f;
-}
-private DrawingFunc[] drawCallbacks;
 //END Drawing loop
--- a/mde/global.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/global.d	Mon Apr 28 10:59:47 2008 +0100
@@ -24,6 +24,12 @@
 module mde.global;
 
 import mde.input.input;
+import mde.scheduler.Scheduler;
+
+/** Some enums used by per request functions. */
+enum SCHEDULE : Scheduler.ID {
+    DRAW
+};
 
 bool run = true;	// main loop continues if this is true
 
--- a/mde/gui/IWindow.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/gui/IWindow.d	Mon Apr 28 10:59:47 2008 +0100
@@ -30,5 +30,9 @@
     * Returns the widget with the given ID from the Window's widget list. If the widget hasn't yet
     * been created, creates it using the Window's widget creation data (throws on error; don't
     * catch the exception). */
-    Widget getWidget (widgetID i);
+    IWidget getWidget (widgetID i);
+    
+    /** Called by a sub-widget when a redraw is necessary (since drawing may sometimes be done on
+    * event. */
+    void requestRedraw ();
 }
--- a/mde/gui/Widget.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/gui/Widget.d	Mon Apr 28 10:59:47 2008 +0100
@@ -22,34 +22,61 @@
 import gl = mde.gl;
 import mde.scheduler.InitFunctions;
 
-/** Interface for widgets (may become a class).
+//BEGIN Iface and createWidget
+/** Interface for widgets (may become a base class).
+*
+* A widget is a region of a GUI window which handles rendering and user-interaction for itself
+* and is able to communicate with it's window and parent/child widgets as necessary.
 *
-* Variable loading/saving efficiency and code-reuse need to be revised later!
-* Give each Widget an int[] of data which it should check in this() and throw if bad?
+* A widget's constructor should have this prototype:
+* ----------------------------------
+* this (IWindow window, int[] data);
+* ----------------------------------
+* Where window is the parent window and data is an array of initialisation data. The method should
+* throw a WidgetDataException (created without parameters) if the data has wrong length or is
+* otherwise invalid.
 */
-interface Widget
+//FIXME: check code reuse later!
+interface IWidget
 {
-    /* this() should look like this:
-    this (IWindow window, int[] data)
-    * where:
-    *   window is the parent window (only needed for getting sub-widgets, hence no need to store)
-    *   data is the widget creation data, stripped of the widget type (see createWidget).
-    */
-    
     /** Draw, starting from given x and y.
     *
-    * Maybe replace later with drawClipped, especially for cases where only part of the widget is
-    * visible behind a scrolling window or hidden window. */
+    * Maybe later enforce clipping of all sub-widget drawing, particularly for cases where only
+    * part of the widget is visible: scroll bars or a hidden window. */
     void draw (int x, int y);
     
-    /** Calculate the size of the widget, taking into account child-widgets.
+    /** Calculate the minimum size the widget could be shrunk to, taking into account
+    * child-widgets. */
+    void getMinimumSize (out int w, out int h);
+    
+    /** Get the current size of the widget.
     *
-    * Later will work out how to make this more flexible. */
-    void getSize (out int w, out int h);
+    * On the first call (during loading), this may be a value saved as part of the config or
+    * something else (e.g. revert to getMinimumSize). */
+    void getCurrentSize (out int w, out int h);
+}
+
+/// Widget types. Start high so they can be reordered easily later.
+enum WIDGET_TYPES : int {
+    BOX = 1001, GRID
 }
 
+/** Create a widget of type data[0] (see enum WIDGET_TYPES) for _window window, with initialisation
+* data [1..$]. */
+IWidget createWidget (IWindow window, int[] data) {
+    if (data.length < 1) throw new WidgetDataException ("No widget data");
+    int type = data[0];     // type is first element of data
+    data = data[1..$];      // the rest is passed to the Widget
+    
+    if (type == WIDGET_TYPES.BOX) return new BoxWidget (window, data);
+    else if (type == WIDGET_TYPES.GRID) return new GridWidget (window, data);
+    else throw new WidgetDataException ("Bad widget type");
+}
+//END Iface and createWidget
+
+//BEGIN Widgets
 /// Draws a box. That's it.
-class BoxWidget : Widget
+class BoxWidget : IWidget
 {
     int w, h;   // size
     
@@ -65,24 +92,28 @@
         gl.drawBox (x,x+w, y,y+h);
     }
     
-    void getSize (out int w, out int h) {
+    void getMinimumSize (out int w, out int h) {
+        w = h = 0;  // box has no content
+    }
+    void getCurrentSize (out int w, out int h) {
         w = this.w;
         h = this.h;
     }
 }
 
 /// Encapsulates a grid of Widgets
-class GridWidget : Widget
+class GridWidget : IWidget
 {
-    const PADDING = 1;  // padding between rows/cols
-    const BORDER = 1;  // border width
+    //NOTE: maybe remove the padding and have each widget include a border? Or vice-versa (no borders on widgets)?
+    const PADDING = 3;  // padding between rows/cols
+    const BORDER = 8;   // border width
     int w, h;           // size
     int rows, cols;     // number of cells in grid
     int[] rowH;         // row height (highest widget in the row)
     int[] colW;         // column width (widest widget)
     int[] rowY;         // cumulative rowH[i-1] + BORDER/PADDING
     int[] colX;         // cumulative colW[i-1] + BORDER/PADDING
-    Widget[] subWidgets;// all widgets in the grid (by row):
+    IWidget[] subWidgets;   // all widgets in the grid (by row):
     /* SubWidget order:    [ 2 3 ]
     *                      [ 0 1 ] */
     
@@ -94,19 +125,34 @@
         
         // Get all sub-widgets
         if (data.length != 2 + rows * cols) throw new WidgetDataException;
+        subWidgets.length = rows*cols;
+        foreach (i, inout subWidget; subWidgets) {
+            subWidget = window.getWidget (data[i+2]);
+        }
+        
+        getMinimumSize (w,h);   // Calculate the size (current size is not saved)
+    }
+    
+    void draw (int x, int y) {
+        gl.setColor (1.0f, 0.6f, 0.0f);
+        gl.drawBox (x,x+w, y,y+h);
+        
+        foreach (i,widget; subWidgets) {
+            widget.draw (x + colX[i % cols], y + rowY[i / cols]);
+        }
+    }
+    
+    /** Also recalculates row/column widths. */
+    void getMinimumSize (out int w, out int h) {
         if (rows*cols == 0) {    // special case
             w = h = 2*BORDER;
             return;
         }
-        subWidgets.length = rows*cols;
-        for (uint i = 2; i < data.length; ++i) {
-            subWidgets[i-2] = window.getWidget (data[i]);
-        }
         
         // Find the sizes of all subWidgets
         int[] widgetW = new int[subWidgets.length]; // dimensions
         int[] widgetH = new int[subWidgets.length];
-        foreach (i,widget; subWidgets) widget.getSize (widgetW[i],widgetH[i]);
+        foreach (i,widget; subWidgets) widget.getCurrentSize (widgetW[i],widgetH[i]);
         
         // Find row heights and column widths (non cumulative)
         rowH.length = rows;
@@ -134,33 +180,43 @@
         }
         w = cum + BORDER - PADDING;     // total width
     }
+    void getCurrentSize (out int wC, out int hC) {
+        wC = w;
+        hC = h;
+    }
+}
+/+ On hold until after next commit
+/// First interactible widget
+class ButtonWidget : IWidget
+{
+    const BORDER = 5;   // border width
+    int w, h;           // size
+    bool pushed = false;// true if button is pushed in
+    
+    this (IWindow, int[] data) {
+        if (data.length != 2) throw new WidgetDataException;
+        
+        w = data[0] + 2*BORDER;
+        h = data[1] + 2*BORDER;
+    }
     
     void draw (int x, int y) {
-        gl.setColor (1.0f, 0.6f, 0.0f);
+        if (pushed)
+            gl.setColor (1f, 0f, 1f);
+        else
+            gl.setColor (.6f, 0f, .6f);
         gl.drawBox (x,x+w, y,y+h);
-        
-        foreach (i,widget; subWidgets) {
-            widget.draw (x + colX[i % cols], y + rowY[i / cols]);
-        }
     }
     
-    void getSize (out int w, out int h) {
+    void getMinimumSize (out int w, out int h) {
+        w = this.w; // button is not resizable
+        h = this.h;
+    }
+    void getCurrentSize (out int w, out int h) {
         w = this.w;
         h = this.h;
     }
-}
-
-// Widget types. Start high so they can be reordered easily later.
-enum WIDGET_TYPES : int {
-    BOX = 1001, GRID
+    
 }
-
-Widget createWidget (IWindow window, int[] data) {
-    if (data.length < 1) throw new WidgetDataException ("No widget data");
-    int type = data[0];     // type is first element of data
-    data = data[1..$];      // the rest is passed to the Widget
-    
-    if (type == WIDGET_TYPES.BOX) return new BoxWidget (window, data);
-    else if (type == WIDGET_TYPES.GRID) return new GridWidget (window, data);
-    else throw new WidgetDataException ("Bad widget type");
-}
++/
+//END Widgets
--- a/mde/gui/gui.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/gui/gui.d	Mon Apr 28 10:59:47 2008 +0100
@@ -30,17 +30,27 @@
 import tango.scrapple.text.convert.parseTo : parseTo;
 import tango.scrapple.text.convert.parseFrom : parseFrom;
 
+import tango.util.log.Log : Log, Logger;
+
 private Logger logger;
 static this () {
     logger = Log.getLogger ("mde.gui.gui");
     
-    init.addFunc (&GUI.load, "GUI.load");
+    init.addFunc (&loadGUI, "loadGUI");
 }
 
+GUI gui;    // Currently just one instance; handle differently later.
+// Wrap gui.load, since init doesn't handle delegates
+// (do it this way since GUI handling will eventually be changed)
+void loadGUI () {
+    gui.load();
+}
+
+/** A GUI handles a bunch of windows, all to be drawn to the same device. */
 struct GUI {
-static:
-    private const fileName = "gui";
+    /** Load all windows from the file gui. */
     void load() {
+        static const fileName = "gui";
         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!)
@@ -65,20 +75,32 @@
         windows.length = 0;
         foreach (sec; reader.dataset.sec) {
             Window w = cast(Window) sec;
-            if (w !is null) {                       // extra safety
-                windows ~= w;
-                try {
-                    w.finalise();
-                    
-                    gl.addDrawCallback (&w.draw);
-                } catch (WindowLoadException e) {
-                    logger.error ("Window failed to load: " ~ e.msg);
-                }
+            debug if (w is null) {
+                logger.error (__FILE__ ~ "(GUI.load): code error (w is null)");
+                continue;
+            }
+            try {
+                //logger.trace ("1");
+                int x;
+                w.finalise();
+                x = 6;
+                windows ~= w;       // only add if load successful
+            } catch (WindowLoadException e) {
+                logger.error ("Window failed to load: " ~ e.msg);
             }
         }
     }
     
-private:
+    /** Draw each window.
+    *
+    * Currently no concept of how to draw overlapping windows, or how to not bother drawing windows
+    * which don't need redrawing. */
+    void draw() {
+        foreach (w; windows)
+            w.draw();
+    }
+    
+    private:
     Window[] windows;
 }
 
@@ -95,40 +117,50 @@
 class Window : mt.IDataSection, IWindow
 {
     alias int widgetID;     // Widget ID type. Each ID is unique under this window. Type is int since this is the widget data type.
-    private int[][widgetID] widgetData;     // Data for all widgets under this window.
-    private Widget[widgetID] widgets;       // List of all widgets under this window (created on demand).
     
-    Widget widget;                  // The primary widget in this window.
-    int x,y;                        // Window position
-    int w,h;                        // Window size (calculated from Widgets)
-    
-    const BORDER_WIDTH = 8;         // Temporary way to handle window decorations
-    
-    
-    // Call after loading is finished to setup the window and confirm that it's valid.
+    /** Call after loading is finished to setup the window and confirm that it's valid.
+    *
+    * Throws: WindowLoadException. Do not use the instance in this case! */
     void finalise () {
-        // Create the widget, throwing on error:
+        // Check data was loaded:
+        if (widgetData is null) throw new WindowLoadException ("No widget data");
+        
+        // Create the primary widget (and indirectly all sub-widgets), throwing on error:
         widget = getWidget (0);     // primary widget always has ID 0.
-        widget.getSize (w,h);       // Find the initial size
+        
+        widgetData = null;          // data is no longer needed: allow GC to collect (cannot safely delete)
+        
+        widget.getCurrentSize (w,h);// Find the initial size
         w += BORDER_WIDTH * 2;      // Adjust for border
         h += BORDER_WIDTH * 2;
     }
     
-    Widget getWidget (widgetID i) {
+    /** Get/create a widget by ID.
+    *
+    * Should $(I only) be called internally and by sub-widgets! */
+    IWidget getWidget (widgetID i)
+    in {
+        // widgetData is normally left to be garbage collected after widgets have been created:
+        assert (widgetData !is null, "getWidget: widgetData is null");
+    } body {
         // See if it's already been created:
-        Widget* p = i in widgets;
+        IWidget* p = i in widgets;
         if (p !is null) return *p;  // yes
         else {                      // no
             int[]* d = i in widgetData;
             if (d is null) throw new WindowLoadException ("Widget not found");
             
             // Throws WidgetDataException (a WindowLoadException) if bad data:
-            Widget w = createWidget (this, *d);
+            IWidget w = createWidget (this, *d);
             widgets[i] = w;
             return w;
         }
     }
     
+    void requestRedraw () {
+    //FIXME
+    }
+    
     void draw () {
         //BEGIN Window border/back
         gl.setColor (0.0f, 0.0f, 0.5f);
@@ -156,4 +188,15 @@
     void writeAll (ItemDelg dlg) {
     }
     //END Mergetag code
+    
+    private:
+    int[][widgetID] widgetData;     // Data for all widgets under this window (deleted after loading)
+    IWidget[widgetID] widgets;      // List of all widgets under this window (created on demand).
+    
+    IWidget widget;                 // The primary widget in this window.
+    int x,y;                        // Window position
+    int w,h;                        // Window size (calculated from Widgets)
+    
+    const BORDER_WIDTH = 8;         // Temporary way to handle window decorations
+
 }
--- a/mde/mde.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/mde.d	Mon Apr 28 10:59:47 2008 +0100
@@ -22,12 +22,11 @@
 // Comment to show use, where only used minimally:
 
 import global = mde.global;             // global.run
-import mde.SDL;                         // unused (but must be linked in)
-import mde.events;                      // unused (but must be linked in)
-import mde.gui.gui;                     // unused (but must be linked in)
+import gl = mde.gl;                     // gl.draw
+import mde.events;                      // pollEvents
 
 import mde.scheduler.Init;
-import mde.scheduler.runTime;           // Scheduler.run()
+import mde.scheduler.Scheduler;         // Scheduler.run()
 import mde.scheduler.exception;         // InitException
 
 import tango.core.Thread : Thread;	// Thread.sleep()
@@ -49,8 +48,15 @@
     }
     //END Initialisation
     
+    //BEGIN Main loop setup
+    Scheduler scheduler = new Scheduler;    // main loop's scheduler
+    
+    scheduler.add (scheduler.getNewID, &mde.events.pollEvents).frame = true;
+    scheduler.add (global.SCHEDULE.DRAW, &gl.draw).frame = true;    // draw. for now, every frame.
+    //END Main loop setup
+    
     while (global.run) {
-        Scheduler.run (Clock.now());
+        scheduler.execute (Clock.now());
         
         Thread.sleep (0.050);	// sleep this many seconds
     }
--- a/mde/mergetag/mtunittest.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/mergetag/mtunittest.d	Mon Apr 28 10:59:47 2008 +0100
@@ -16,22 +16,23 @@
 /// This module provides a unittest for mergetag.
 module mde.mergetag.mtunittest;
 
-import mde.mergetag.Reader;
-import mde.mergetag.Writer;
-import mde.mergetag.DataSet;
-import mde.mergetag.DefaultData;
-
-import tango.scrapple.text.convert.parseTo : parseTo;
-import tango.scrapple.text.convert.parseFrom : parseFrom;
-
-import tango.util.log.Log : Log, Logger;
-
 debug (mdeUnitTest) {
+    import mde.mergetag.Reader;
+    import mde.mergetag.Writer;
+    import mde.mergetag.DataSet;
+    import mde.mergetag.DefaultData;
+    
+    import tango.scrapple.text.convert.parseTo : parseTo;
+    import tango.scrapple.text.convert.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. */
@@ -91,7 +92,10 @@
             return ret;
         }
         mixin (genCheckCode (`secW`,`secR`));
-    
+        
+        // Delete the unittest file now
+        FilePath (file~".mtt").remove;
+        
         logger.info ("Unittest complete (for DefaultData).");
     }
 }
--- a/mde/scheduler/Init.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/scheduler/Init.d	Mon Apr 28 10:59:47 2008 +0100
@@ -46,7 +46,7 @@
 // Derelict imports
 import derelict.opengl.gl;
 import derelict.sdl.sdl;
-//import derelict.freetype.ft;
+import derelict.freetype.ft;
 import derelict.util.exception;
 
 /**
@@ -150,13 +150,12 @@
         try {
             DerelictSDL.load();
             DerelictGL.load();
-            //DerelictFT.load();
+            DerelictFT.load();
         } catch (DerelictException de) {
             logger.fatal ("Loading dynamic library failed:");
             logger.fatal (de.msg);
             
             throw new InitException ("Loading dynamic libraries failed (see above).");
-            return;
         }
         //END Load dynamic libraries
         
@@ -308,11 +307,11 @@
                 
         static void initFunc () {
             initialised = true;
-            cleanupUT.addFunc (&cleanupFunc1);
-            cleanupUT.addFunc (&cleanupFunc2);
+            cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1");
+            cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2");
         }
         
-        initUT.addFunc (&initFunc);
+        initUT.addFunc (&initFunc, "UT init");
         
         runStageForward (initUT);
         assert (initialised);
--- a/mde/scheduler/InitFunctions.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/mde/scheduler/InitFunctions.d	Mon Apr 28 10:59:47 2008 +0100
@@ -23,8 +23,6 @@
 * "out cleanupFunc[]". */
 module mde.scheduler.InitFunctions;
 
-static import mde.gl;
-
 import tango.util.log.Log : Log, Logger;
 static this() {
     logger = Log.getLogger ("mde.scheduler.InitFunctions");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/scheduler/Scheduler.d	Mon Apr 28 10:59:47 2008 +0100
@@ -0,0 +1,211 @@
+/* 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/>. */
+
+/** Scheduler
+*/
+module mde.scheduler.Scheduler;
+
+public import tango.time.Time;
+
+debug {
+    import tango.util.log.Log : Log, Logger;
+    private Logger logger;
+}
+static this() {
+    debug logger = Log.getLogger ("mde.scheduler.Scheduler");
+}
+
+/// This class can run scheduled functions per frame, per time interval and per request.
+class Scheduler
+{
+    /** The type of function callback to be added to the scheduler.
+    *
+    * The parameter time gives the time since the function was last called, or zero on the first
+    * run. */
+    alias void function (TimeSpan time) scheduleFct;
+    alias void delegate (TimeSpan time) scheduleDlg;    /// ditto
+    
+    alias uint ID;      /// This is the type of identifier used by add/get/remove/request
+    
+    /** The struct used to store the function and scheduling information */
+    class ScheduleFunc {
+        /// Set the function this represents
+        this (scheduleDlg f) {
+            fct = f;
+        }
+        
+        /** Quick way to set scheduling.
+        *
+        * Function will be scheduled each _frame if frame is true, or when requested, or each
+        * interval if interval is positive. */
+        ScheduleFunc set (bool frame, TimeSpan interval) {
+            this.frame = frame;
+            if (interval < TimeSpan.zero) interval = TimeSpan.zero;
+            this.interval = interval;
+            
+            return this;
+        }
+        
+        bool frame;         /// Call function each time execute() runs
+        bool request;       /// Call function the next time execute() runs
+        TimeSpan interval;  /// Call the function when the last call was longer than interval ago
+        
+        package:
+        scheduleDlg fct = null; // function to call
+        Time lastCall = zero;   // time of last call; zero at start (special case)
+    }
+    
+    /** Add a function to be scheduled.
+    *
+    * This function should have a unique identifier, which can be used with get/remove/request.
+    * The identifier can be supplied or generated by getNewID().
+    *
+    * Use the returned pointer to set the scheduling, e.g.:
+    * -----
+    * scheduler.add(scheduler.getNewID, myFunction).set(false, TimeSpan.millis (10));
+    * scheduler.get(15).frame = true;
+    */
+    ScheduleFunc add (ID id, scheduleFct func) {
+        // Convert to a delegate. Maybe someday implicit casts will work...
+        scheduleDlg d;
+        d.funcptr = func;
+        return add (id, d);
+    }
+    /** ditto */
+    ScheduleFunc add (ID id, scheduleDlg func)
+    in {
+        debug if ((id in funcs) !is null) logger.error ("Duplicate ID used!");
+    } body {
+        ScheduleFunc sf = new ScheduleFunc (func);
+        funcs[id] = sf; // add
+        return sf;      // and return for chain-calling
+    }
+    
+    /** Get function with ID id. */
+    ScheduleFunc get (ID id) {
+        try {
+            return funcs[id];
+        } catch (Exception) {
+            debug logger.error ("get(): ID does not exist!");
+        }
+    }
+    
+    /** Remove function with ID id. */
+    void remove (ID id) {
+        try {
+            funcs.remove(id);
+        } catch (Exception) {
+            debug logger.error ("remove(): ID does not exist!");
+        }
+    }
+    
+    /** Request that function with ID id is called next time execute() runs. */
+    void request (ID id) {
+        get(id).request = true;
+    }
+    
+    /** Generate an ID. All generated IDs are >= 0xF000_0000 to provide plenty of room for other
+    * IDs. */
+    ID getNewID () {
+        // For now use a very simple method to find a vacant ID: iterate
+        for (ID i = 0xF000_0000; i < ID.max; ++i)
+            if ((i in funcs) is null)
+                return i;
+    }
+    
+    /** This function should get called by the main loop, once per frame.
+    *
+    * Params:
+    *   time = the current sim-time (tango.time.Time.Time); all time evaluations will use this
+    *   all = skip normal tests and call all functions (still cancelling requests and updating call
+    *       times for correct running next time execute() is called with all = false)
+    */
+    void execute (Time time, bool all = false) {
+        foreach (func; funcs) {
+            // The interval since the function was last run. In order to be correct for more
+            // complex cases, it must be calculated per function.
+            TimeSpan interval;
+            
+            // Per frame/request:
+            if (func.frame || func.request || all) {
+                if (func.lastCall == zero) interval = TimeSpan.zero;    // first call
+                else interval = (time - func.lastCall);
+                func.fct (interval);        // call
+                
+                func.request = false;       // cancel regardless of last value
+                func.lastCall = time;
+            }
+            // Per-interval functions:
+            else if ((func.interval != TimeSpan.zero) &&        // has a per-interval schedule
+                    (time >= (func.lastCall + func.interval)))  // time to call again
+            {
+                if (func.lastCall == zero) interval = TimeSpan.zero;    // first call
+                else interval = (time - func.lastCall);
+                func.fct (interval);        // call
+                
+                func.lastCall = time;
+            }
+        }
+    }
+    
+    private:
+    static const Time zero = Time(0L);
+    Time lastTime = zero;
+    ScheduleFunc[ID] funcs;
+    
+    debug (mdeUnitTest) unittest {
+        Scheduler s = new Scheduler;
+        
+        int ctr1 = 0;
+        void inc1 (TimeSpan) {  ++ctr1; }
+        s.add(1,&inc1).frame = true;
+        
+        TimeSpan interval = TimeSpan.millis(1);// non-zero so we can check zero after first call
+        void perInt (TimeSpan i) {  interval = i;    }
+        s.add(2,&perInt).set(false, TimeSpan.millis(10));
+        
+        Time t = Time.epoch1970;    // starting time (value isn't important)
+        s.execute (t);
+        assert (ctr1 == 1);         // called once
+        assert (interval == TimeSpan.zero); // initial interval
+        
+        t += TimeSpan.millis (5);   // 5ms later...
+        s.execute (t);
+        assert (ctr1 == 2);
+        assert (interval == TimeSpan.zero); // perInt shouldn't get called
+        
+        s.get(1).frame = false;     // don't call per-frame anymore
+        s.get(1).request = true;    // but request next call
+        
+        t += TimeSpan.millis (5);
+        s.execute (t);
+        assert (ctr1 == 3);         // as requested
+        assert (interval == TimeSpan.millis (10));  // perInt should get called (just!)
+        
+        s.request(2);               // request this
+        
+        t += TimeSpan.millis (8);
+        s.execute (t);
+        assert (ctr1 == 3);         // inc1 shouldn't run
+        assert (interval == TimeSpan.millis (8));  // perInt was requested
+        
+        t += TimeSpan.millis (4);
+        s.execute (t);
+        // check perInt's last call-time was updated by the request, so it doesn't get run now:
+        assert (interval == TimeSpan.millis (8));
+        
+        logger.info ("Unittest complete.");
+    }
+}
--- a/mde/scheduler/runTime.d	Sat Apr 12 14:10:13 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +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/>. */
-
-/** Scheduler
-*/
-module mde.scheduler.runTime;
-
-import tango.time.Time;
-
-debug {
-    import tango.util.log.Log : Log, Logger;
-    private Logger logger;
-}
-static this() {
-    debug logger = Log.getLogger ("mde.scheduler.runTime");
-}
-
-/** Some enums used by per request functions. */
-enum RF_KEYS : uint { DRAW };
-
-// NOTE: Currently has no support for removing functions. To fix, assign ID and store fct pointers
-// in an associative array, returning the ID [on adding fct pointer].
-// NOTE: support delegates or not?
-/// This class can run scheduled functions per frame or every t seconds (sim-time).
-abstract class Scheduler
-{
-    /** The type of function pointer to be passed to the scheduler.
-    *
-    * The double $(I time) parameter gives the number of (sim) seconds since the function was last
-    * called, or zero on the first run. */
-    alias void function (double time) scheduleFct;
-    
-    /** Add a function to be called per frame. */
-    static void perFrame (scheduleFct fct) {
-        frameFcts ~= fct;
-    }
-    
-    /** Add a function to be called per t secs or n 100-nano-sec intevals.
-    *
-    * Since the scheduler cannot guarantee a maximum time between calls, the interval at which
-    * functions are called is always greater than or equal to the inverval specified here. Of
-    * course, the actual inteval is given when the function is run.
-    */
-    static void perTime (double t, scheduleFct fct) {
-        perTime (TimeSpan.interval(t), fct);
-    }
-    /** ditto */
-    static void perTime (TimeSpan n, scheduleFct fct)
-    in { assert (n > TimeSpan (0L)); }
-    body {
-        timeFcts ~= new TimeFct (fct, n);
-    }
-    
-    /** Add a function to be called per requested update.
-    *
-    * A bool parameter is stored locally describing whether or not the function needs recalling,
-    * and is set true upon creation and when requestUpdate is called with the same key. The
-    * function is then called by scheduler's run() whenever this bool variable is true.
-    */
-    static void perRequest (uint key, void function() fct)
-    in {
-        debug if ((key in requestFcts) is null)
-            logger.warn ("perRequest: replacing existing function with same key!");
-    }
-    body {
-        requestFcts[key] = fct;
-        requestFctsUpdate[key] = true;
-    }
-    
-    /** Request an update to request function key. */
-    static void requestUpdate (uint key) {
-        // Note: check the value for this key actually exists
-        bool* p = key in requestFctsUpdate;
-        if (p) *p = true;
-        else debug logger.warn ("requestUpdate called with invalid key");
-    }
-    
-    /** This function should get called by the main loop, once per frame.
-    *
-    * The parameter time should be the current sim-time, using the tango.core.Types.Time enum; all
-    * time evaluations will use this.
-    */
-    static void run (Time time) {
-        double interval;
-        
-        // Call all per-frame functions:
-        if (lastTime == Time (0L)) interval = 0.0;		// 0 interval for first loop
-        else interval = (time-lastTime).interval();
-        
-        foreach (fct; frameFcts) fct(interval);
-        
-        // Call all per-interval functions:
-        foreach (fct; timeFcts) if (time >= fct.nextCall) {
-            if (fct.nextCall == Time (0L)) interval = 0.0;	// 0 interval for first call
-            else interval = (time - (fct.nextCall - fct.interval)).interval();
-            fct.nextCall = time + fct.interval;		// when to call next
-            
-            fct.fct (interval);				// call
-        }
-        
-        // Call all per-request functions:
-        foreach (key, fct; requestFcts) {
-            if (requestFctsUpdate[key]) {
-                fct();
-                requestFctsUpdate[key] = false;
-            }
-        }
-    }
-    
-    /* Holds details for functions called per time interval.
-    * Needs to be a reference type, and using a class is easiest for this. */
-    private static class TimeFct {
-        scheduleFct fct;			// function to call
-        
-        TimeSpan interval;			// interval to call at
-        // Storing nextCall is more efficient than storing lastCall since only this number has to
-        // be compared to time every frame where fct is not called:
-        Time nextCall = Time (0L);
-        
-        this (scheduleFct f, TimeSpan t) {
-            fct = f;
-            interval = t;
-        }
-    }
-    
-    private static Time lastTime = Time (0L);
-    private static scheduleFct[] frameFcts;
-    private static TimeFct[] timeFcts;
-    private static void function()[uint] requestFcts;
-    private static bool[uint] requestFctsUpdate;    // associated with requestFcts
-}
--- a/test/mdeTest.d	Sat Apr 12 14:10:13 2008 +0100
+++ b/test/mdeTest.d	Mon Apr 28 10:59:47 2008 +0100
@@ -28,6 +28,7 @@
 import mde.mergetag.mtunittest;
 import mde.exception;
 import mde.scheduler.Init;
+import mde.scheduler.Scheduler;
 import mde.i18n.I18nTranslation;
 
 import tango.util.log.Log : Log, Logger;