view mde/scheduler/Scheduler.d @ 85:56c0ddd90193

Intermediate commit (not stable). Changes to init system.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 11 Sep 2008 11:33:51 +0100
parents 66d555da083e
children 2a364c7d82c9
line wrap: on
line source

/* 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/>. */

/** A fairly generic scheduler.
 *
 * This class implements most functionality a generic scheduler might want, however currently it
 * doesn't any uses where equivalent functionality couldn't be achived very easily anyway. */
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.fromMillis (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 () {
        if (funcs.length == 0) return 0xF000_0000;  // otherwise would get an out-of-bounds error
        // Take the last used ID and add one, making sure it's at least 0xF000_0000.
        // Don't bother checking if it's out of bounds since there's 2^28 available IDs.
        ID i = funcs.keys[$-1] + 1;
        if (i < 0xF000_0000) i = 0xF000_0000;
        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.fromMillis(1);// non-zero so we can check zero after first call
        void perInt (TimeSpan i) {  interval = i;    }
        s.add(2,&perInt).set(false, TimeSpan.fromMillis(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.fromMillis (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.fromMillis (5);
        s.execute (t);
        assert (ctr1 == 3);         // as requested
        assert (interval == TimeSpan.fromMillis (10));  // perInt should get called (just!)
        
        s.request(2);               // request this
        
        t += TimeSpan.fromMillis (8);
        s.execute (t);
        assert (ctr1 == 3);         // inc1 shouldn't run
        assert (interval == TimeSpan.fromMillis (8));  // perInt was requested
        
        t += TimeSpan.fromMillis (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.fromMillis (8));
        
        logger.info ("Unittest complete.");
    }
}