# HG changeset patch # User Diggory Hardy # Date 1209376787 -3600 # Node ID 467c74d4804d072999f2113afd434d49444e625c # Parent f985c28c0ec9769a14d9ad470ec17bcbce325c90 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 diff -r f985c28c0ec9 -r 467c74d4804d codeDoc/jobs.txt --- 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. diff -r f985c28c0ec9 -r 467c74d4804d mde/events.d --- 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); diff -r f985c28c0ec9 -r 467c74d4804d mde/gl.d --- 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 diff -r f985c28c0ec9 -r 467c74d4804d mde/global.d --- 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 diff -r f985c28c0ec9 -r 467c74d4804d mde/gui/IWindow.d --- 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 (); } diff -r f985c28c0ec9 -r 467c74d4804d mde/gui/Widget.d --- 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 diff -r f985c28c0ec9 -r 467c74d4804d mde/gui/gui.d --- 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 + } diff -r f985c28c0ec9 -r 467c74d4804d mde/mde.d --- 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 } diff -r f985c28c0ec9 -r 467c74d4804d mde/mergetag/mtunittest.d --- 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)."); } } diff -r f985c28c0ec9 -r 467c74d4804d mde/scheduler/Init.d --- 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); diff -r f985c28c0ec9 -r 467c74d4804d mde/scheduler/InitFunctions.d --- 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"); diff -r f985c28c0ec9 -r 467c74d4804d mde/scheduler/Scheduler.d --- /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 . */ + +/** 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."); + } +} diff -r f985c28c0ec9 -r 467c74d4804d mde/scheduler/runTime.d --- 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 . */ - -/** 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 -} diff -r f985c28c0ec9 -r 467c74d4804d test/mdeTest.d --- 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;