events.t

documentation
#charset "us-ascii"

/* 
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
 *   
 *   TADS 3 Library: events
 *   
 *   This module defines the event framework.  An event is a programmed
 *   operation that occurs at a particular point in the game; an event can
 *   be turn-based, in which case it occurs after a given number of turns
 *   has elapsed, or it can occur in real time, which means that it occurs
 *   after a particular interval of time has elapsed.  
 */

#include "adv3.h"
#include <dict.h>
#include <gramprod.h>


/* ------------------------------------------------------------------------ */
/*
 *   Run the main scheduling loop.  This continues until we encounter an
 *   end-of-file error reading from the console, or a QuitException is
 *   thrown to terminate the game.  
 */
runScheduler()
{
    /* keep going until we quit the game */
    for (;;)
    {
        /* catch the exceptions that terminate the game */
        try
        {
            /* start with an empty list of schedulable items */
            local vec = new Vector(10);

            /* find the lowest time at which something is ready to run */
            local minTime = nil;
            foreach (local cur in Schedulable.allSchedulables)
            {
                /* get this item's next eligible run time */
                local curTime = cur.getNextRunTime();

                /* 
                 *   if it's not nil, and it's equal to or below the
                 *   lowest we've seen so far, note it 
                 */
                if (curTime != nil && (minTime == nil || curTime <= minTime))
                {
                    /* 
                     *   if this is different from the current minimum
                     *   schedulable time, clear out the list of
                     *   schedulables, because the list keeps track of the
                     *   items at the lowest time only 
                     */
                    if (minTime != nil && curTime < minTime)
                        vec.removeRange(1, vec.length());

                    /* add this item to the list */
                    vec.append(cur);

                    /* note the new lowest schedulable time */
                    minTime = curTime;
                }
            }

            /* 
             *   if nothing's ready to run, the game is over by default,
             *   since we cannot escape this state - we can't ourselves
             *   change anything's run time, so if nothing's ready to run
             *   now, we won't be able to change that, and so nothing will
             *   ever be ready to run 
             */
            if (minTime == nil)
            {
                "\b[Error: nothing is available for scheduling -
                terminating]\b";
                return;
            }

            /* 
             *   Advance the global turn counter by the amount of game
             *   clock time we're consuming now.  
             */
            libGlobal.totalTurns += minTime - Schedulable.gameClockTime;

            /* 
             *   advance the game clock to the minimum run time - nothing
             *   interesting happens in game time until then, so we can
             *   skip straight ahead to this time 
             */
            Schedulable.gameClockTime = minTime;

            /* calculate the schedule order for each item */
            vec.forEach({x: x.calcScheduleOrder()});

            /*
             *   We have a list of everything schedulable at the current
             *   game clock time.  Sort the list in ascending scheduling
             *   order, so that the higher priority items come first in
             *   the list.  
             */
            vec = vec.sort(
                SortAsc, {a, b: a.scheduleOrder - b.scheduleOrder});

            /*
             *   Run through the list and run each item.  Keep running
             *   each item as long as it's ready to run - that is, as long
             *   as its schedulable time equals the game clock time.  
             */
        vecLoop:
            foreach (local cur in vec)
            {
                /* run this item for as long as it's ready to run */
                while (cur.getNextRunTime() == minTime)
                {
                    try
                    {
                        /* 
                         *   execute this item - if it doesn't want to be
                         *   called again without considering other
                         *   objects, stop looping and refigure the
                         *   scheduling order from scratch 
                         */
                        if (!cur.executeTurn())
                            break vecLoop;
                    }
                    catch (Exception exc)
                    {
                        /*
                         *   The scheduled operation threw an exception.
                         *   If the schedulable's next run time didn't get
                         *   updated, then the same schedulable will be
                         *   considered ready to run again immediately on
                         *   the next time through the loop.  It's quite
                         *   possible in this case that we'll simply repeat
                         *   the operation that threw the exception and get
                         *   right back here again.  If this happens, it
                         *   will effectively starve all of the other
                         *   schedulables.  To ensure that other
                         *   schedulables get a chance to run before we try
                         *   this erroneous operation again, advance its
                         *   next run time by one unit if it hasn't already
                         *   been advanced. 
                         */
                        if (cur.getNextRunTime() == minTime)
                            cur.incNextRunTime(1);

                        /* re-throw the exception */
                        throw exc;
                    }
                }
            }
        }
        catch (EndOfFileException eofExc)
        {
            /* end of file reading command input - we're done */
            return;
        }
        catch (QuittingException quitExc)
        {
            /* explicitly quitting - we're done */
            return;
        }
        catch (RestartSignal rsSig)
        {
            /* 
             *   Restarting - re-throw the signal for handling in the
             *   system startup code.  Note that we explicitly catch this
             *   signal, only to rethrow it, because we'd otherwise flag it
             *   as an unhandled error in the catch-all Exception handler.
             */
            throw rsSig;
        }
        catch (RuntimeError rtErr)
        {
            /* if this is a debugger error of some kind, re-throw it */
            if (rtErr.isDebuggerSignal)
                throw rtErr;
            
            /* display the error, but keep going */
            "\b[<<rtErr.displayException()>>]\b";
        }
        catch (TerminateCommandException tce)
        {
            /* 
             *   Aborted command - ignore it.  This is most like to occur
             *   when a fuse, daemon, or the like tries to terminate itself
             *   with this exception, thinking it's operating in a normal
             *   command execution environment.  As a convenience, simply
             *   ignore these exceptions so that any code can use them to
             *   abort everything and return to the main scheduling loop. 
             */
        }
        catch (ExitSignal es)
        {
            /* ignore this, just as we ignore TerminateCommandException */
        }
        catch (ExitActionSignal eas)
        {
            /* ignore this, just as we ignore TerminateCommandException */
        }
        catch (Exception exc)
        {
            /* some other unhandled exception - display it and keep going */
            "\b[Unhandled exception: <<exc.displayException()>>]\b";
        }
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   An item that can be scheduled for time-based notifications.  The main
 *   scheduler loop in runScheduler() operates on objects of this class.
 *   
 *   Note that we build a list of all Schedulable instances during
 *   pre-initialization.  If any Schedulable objects are dynamically
 *   created, they must be added to the list explicitly after creation in
 *   order for the event manager to schedule them for execution.  The
 *   default constructor does this automatically, so subclasses can simply
 *   inherit our constructor to be added to the master list.  
 */
class Schedulable: object
    /* construction - add myself to the Schedulable list */
    construct()
    {
        /* 
         *   Add myself to the master list of Schedulable instances.  Note
         *   that we must update the list in the Schedulable class itself. 
         */
        Schedulable.allSchedulables += self;
    }

    /*
     *   Get the next time (on the game clock) at which I'm eligible for
     *   execution.  We won't receive any scheduling notifications until
     *   this time.  If this object doesn't want any scheduling
     *   notifications, return nil.  
     */
    getNextRunTime() { return nextRunTime; }

    /* advance my next run time by the given number of clock units */
    incNextRunTime(amt)
    {
        if (nextRunTime != nil)
            nextRunTime += amt;
    }

    /*
     *   Notify this object that its scheduled run time has arrived.  This
     *   should perform the scheduled task.  If the scheduled task takes
     *   any game time, the object's internal next run time should be
     *   updated accordingly.
     *   
     *   The scheduler will invoke this method of the same object
     *   repeatedly for as long as its nextRunTime remains unchanged AND
     *   this method returns true.  If the object's scheduling priority
     *   changes relative to other schedulable objects, it should return
     *   nil here to tell the scheduler to recalculate scheduling
     *   priorities.  
     */
    executeTurn() { return true; }

    /*
     *   Scheduling order.  This determines which item goes first when
     *   multiple items are schedulable at the same time (i.e., they all
     *   have the same getNextRunTime() values).  The item with the lowest
     *   number here goes first.
     *   
     *   This should never be evaluated except immediately after a call to
     *   calcScheduleOrder.  
     */
    scheduleOrder = 100

    /*
     *   Calculate the scheduling order, returning the order value and
     *   storing it in our property scheduleOrder.  This is used to
     *   calculate and cache the value prior to sorting a list of
     *   schedulable items.  We use this two-step approach (first
     *   calculate, then sort) so that we avoid repeatedly evaluating a
     *   complex calculation, if indeed there is a complex calculation to
     *   perform.
     *   
     *   By default, we assume that the schedule order is static, so we
     *   simply leave our scheduleOrder property unchanged and return its
     *   present value.  
     */
    calcScheduleOrder() { return scheduleOrder; }

    /* my next running time, in game clock time */
    nextRunTime = nil

    /*
     *   A class variable giving the current game clock time.  This is a
     *   class variable because there's only one global game clock.  The
     *   game clock starts at zero and increments in game time units; a
     *   game time unit is the arbitrary quantum of time for our event
     *   scheduling system.  
     */
    gameClockTime = 0

    /*
     *   A list of all of the Schedulable objects in the game.  We set this
     *   up during pre-initialization; if any Schedulable instances are
     *   created dynamically, they must be explicitly added to this list
     *   after creation.  
     */
    allSchedulables = nil
;

/*
 *   Pre-initializer: build the master list of Schedulable instances 
 */
PreinitObject
    /*
     *   Execute preinitialization.  Build a list of all of the schedulable
     *   objects in the game, so that we can scan this list quickly during
     *   play.  
     */
    execute()
    {
        local vec;
        
        /* set up an empty vector to hold the schedulable objects */
        vec = new Vector(32);

        /* add all of the Schedulable instances to the vector */
        forEachInstance(Schedulable, {s: vec.append(s)});

        /* save the list of Schedulable instances as an ordinary list */
        Schedulable.allSchedulables = vec.toList();
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Basic Event Manager.  This is a common base class for the game-time
 *   and real-time event managers.  This class handles the details of
 *   managing the event queue; the subclasses must define the specifics of
 *   event timing. 
 */
class BasicEventManager: object
    /* add an event */
    addEvent(event)
    {
        /* append the event to our list */
        events_.append(event);
    }

    /* remove an event */
    removeEvent(event)
    {
        /* remove the event from our list */
        events_.removeElement(event);
    }

    /* 
     *   Remove events matching the given object and property combination.
     *   We remove all events that match both the object and property
     *   (events matching only the object or only the property are not
     *   affected).
     *   
     *   This is provided mostly as a convenience for cases where an event
     *   is known to be uniquely identifiable by its object and property
     *   values; this saves the caller the trouble of keeping track of the
     *   Event object created when the event was first registered.
     *   
     *   When a particular object/property combination might be used in
     *   several different events, it's better to keep a reference to the
     *   Event object representing each event, and use removeEvent() to
     *   remove the specific Event object of interest.
     *   
     *   Returns true if we find any matching events, nil if not.  
     */
    removeMatchingEvents(obj, prop)
    {
        local found;
        
        /* 
         *   Scan our list, and remove each event matching the parameters.
         *   Note that it's safe to remove things from a vector that we're
         *   iterating with foreach(), since foreach() makes a safe copy
         *   of the vector for the iteration. 
         */
        found = nil;
        foreach (local cur in events_)
        {
            /* if this one matches, remove it */
            if (cur.eventMatches(obj, prop))
            {
                /* remove the event */
                removeEvent(cur);

                /* note that we found a match */
                found = true;
            }
        }

        /* return our 'found' indication */
        return found;
    }

    /* 
     *   Remove the current event - this is provided for convenience so
     *   that an event can cancel itself in the course of its execution.
     *   
     *   Note that this has no effect on the current event execution -
     *   this simply prevents the event from receiving additional
     *   notifications in the future.  
     */
    removeCurrentEvent()
    {
        /* remove the currently active event from our list */
        removeEvent(curEvent_);
    }

    /* event list - each instance must initialize this to a vector */
    // events_ = nil
;

/*
 *   Event Manager.  This is a schedulable object that keeps track of
 *   fuses and daemons, and schedules their execution.  
 */
eventManager: BasicEventManager, Schedulable
    /*
     *   Use a scheduling order of 1000 to ensure we go after all actors.
     *   By default, actors use scheduling orders in the range 100 to 400,
     *   so our order of 1000 ensures that fuses and daemons run after all
     *   characters on a given turn.  
     */
    scheduleOrder = 1000

    /*
     *   Get the next run time.  We'll find the lowest run time of our
     *   fuses and daemons and return that.  
     */
    getNextRunTime()
    {
        local minTime;
        
        /* 
         *   run through our list of events, and find the event that is
         *   scheduled to run at the lowest game clock time 
         */
        minTime = nil;
        foreach (local cur in events_)
        {
            local curTime;
            
            /* get this item's scheduled run time */
            curTime = cur.getNextRunTime();

            /* if it's not nil and it's the lowest so far, remember it */
            if (curTime != nil && (minTime == nil || curTime < minTime))
                minTime = curTime;
        }

        /* return the minimum time we found */
        return minTime;
    }

    /*
     *   Execute a turn.  We'll execute each fuse and each daemon that is
     *   currently schedulable.  
     */
    executeTurn()
    {
        local lst;
        
        /* 
         *   build a list of all of our events with the current game clock
         *   time - these are the events that are currently schedulable 
         */
        lst = events_.subset({x: x.getNextRunTime()
                                 == Schedulable.gameClockTime});

        /* execute the items in this list */
        executeList(lst);

        /* no change in scheduling priorities */
        return true;
    }

    /*
     *   Execute a command prompt turn.  We'll execute each
     *   per-command-prompt daemon. 
     */
    executePrompt()
    {
        /* execute all of the per-command-prompt daemons */
        executeList(events_.subset({x: x.isPromptDaemon}));
    }

    /*
     *   internal service routine - execute the fuses and daemons in the
     *   given list, in eventOrder priority order 
     */
    executeList(lst)
    {
        /* sort the list in ascending event order */
        lst = lst.toList()
              .sort(SortAsc, {a, b: a.eventOrder - b.eventOrder});

        /* run through the list and execute each item ready to run */
        foreach (local cur in lst)
        {
            /* remember our old active event, then establish the new one */
            local oldEvent = curEvent_;
            curEvent_ = cur;

            /* make sure we restore things on the way out */
            try
            {
                local pc;
                
                /* have the player character note the pre-event conditions */
                pc = gPlayerChar;
                pc.noteConditionsBefore();
                
                /* cancel any sense caching currently in effect */
                libGlobal.disableSenseCache();

                /* execute the event */
                cur.executeEvent();

                /* 
                 *   if the player character is the same as it was, ask
                 *   the player character to note any change in conditions 
                 */
                if (gPlayerChar == pc)
                    pc.noteConditionsAfter();
            }
            catch (Exception exc)
            {
                /* 
                 *   If an event throws an exception out of its handler,
                 *   remove the event from the active list.  If we were to
                 *   leave it active, we'd go back and execute the same
                 *   event again the next time we look for something to
                 *   schedule, and that would in turn probably just
                 *   encounter the same exception - so we'd be stuck in an
                 *   infinite loop executing this erroneous code.  To
                 *   ensure that we don't get stuck, remove the event. 
                 */
                removeCurrentEvent();

                /* re-throw the exception */
                throw exc;
            }
            finally
            {
                /* restore the enclosing current event */
                curEvent_ = oldEvent;
            }
        }
    }

    /* our list of fuses and daemons */
    events_ = static new Vector(20)

    /* the event currently being executed */
    curEvent_ = nil
;

/*
 *   Pseudo-action subclass to represent the action environment while
 *   processing a daemon, fuse, or other event. 
 */
class EventAction: Action
    /* 
     *   event actions are internal system actions; they don't consume
     *   additional turns themselves, since they run between player turns 
     */
    actionTime = 0;
;

/*
 *   A basic event, for game-time and real-time events.
 */
class BasicEvent: object
    /* construction */
    construct(obj, prop)
    {
        /* remember the object and property to call at execution */
        obj_ = obj;
        prop_ = prop;
    }

    /* 
     *   Execute the event.  This must be overridden by the subclass to
     *   perform the appropriate operation when executed.  In particular,
     *   the subclass must reschedule or unschedule the event, as
     *   appropriate. 
     */
    executeEvent() { }

    /* does this event match the given object/property combination? */
    eventMatches(obj, prop) { return obj == obj_ && prop == prop_; }

    /* 
     *   Call our underlying method.  This is an internal routine intended
     *   for use by the executeEvent() implementations.  
     */
    callMethod()
    {
        /* 
         *   invoke the method in our sensory context, and in a simulated
         *   action environment 
         */
        withActionEnv(EventAction, gPlayerChar,
            {: callWithSenseContext(source_, sense_,
                                    {: obj_.(self.prop_)() }) });
    }

    /* the object and property we invoke */
    obj_ = nil
    prop_ = nil

    /* 
     *   The sensory context of the event.  When the event fires, we'll
     *   execute its method in this sensory context, so that any messages
     *   generated will be displayed only if the player character can
     *   sense the source object in the given sense.
     *   
     *   By default, these are nil, which means that the event's messages
     *   will be displayed (or, at least, they won't be suppressed because
     *   of the sensory context).  
     */
    source_ = nil
    sense_ = nil
;

/*
 *   Base class for fuses and daemons 
 */
class Event: BasicEvent
    /* our next run time, in game clock time */
    getNextRunTime() { return nextRunTime; }

    /* delay our scheduled run time by the given number of turns */
    delayEvent(turns) { nextRunTime += turns; }

    /* remove this event from the event manager */
    removeEvent() { eventManager.removeEvent(self); }

    /* 
     *   Event order - this establishes the order we run relative to other
     *   events scheduled to run at the same game clock time.  Lowest
     *   number goes first.  By default, we provide an event order of 100,
     *   which should leave plenty of room for custom events before and
     *   after default events.  
     */
    eventOrder = 100

    /* creation */
    construct(obj, prop)
    {
        /* inherit default handling */
        inherited(obj, prop);

        /* add myself to the event manager's active event list */
        eventManager.addEvent(self);
    }

    /* 
     *   our next execution time, expressed in game clock time; by
     *   default, we'll set this to nil, which means that we are not
     *   scheduled to execute at all 
     */
    nextRunTime = nil

    /* by default, we're not a per-command-prompt daemon */
    isPromptDaemon = nil
;

/*
 *   Fuse.  A fuse is an event that fires once at a given time in the
 *   future.  Once a fuse is executed, it is removed from further
 *   scheduling.  
 */
class Fuse: Event
    /* 
     *   Creation.  'turns' is the number of turns in the future at which
     *   the fuse is executed; if turns is 0, the fuse will be executed on
     *   the current turn.  
     */
    construct(obj, prop, turns)
    {
        /* inherit the base class constructor */
        inherited(obj, prop);

        /* 
         *   set my scheduled time to the current game clock time plus the
         *   number of turns into the future 
         */
        nextRunTime = Schedulable.gameClockTime + turns;
    }

    /* execute the fuse */
    executeEvent()
    {
        /* call my method */
        callMethod();

        /* a fuse fires only once, so remove myself from further scheduling */
        eventManager.removeEvent(self);
    }
;

/*
 *   Sensory-context-sensitive fuse - this is a fuse with an explicit
 *   sensory context.  We'll run the fuse in its sense context, so any
 *   messages generated will be visible only if the given source object is
 *   reachable by the player character in the given sense.
 *   
 *   Conceptually, the source object is considered the source of any
 *   messages that the fuse generates, and the messages pertain to the
 *   given sense; so if the player character cannot sense the source
 *   object in the given sense, the messages should not be displayed.  For
 *   example, if the fuse will describe the noise made by an alarm clock
 *   when the alarm goes off, the source object would be the alarm clock
 *   and the sense would be sound; this way, if the player character isn't
 *   in hearing range of the alarm clock when the alarm goes off, we won't
 *   display messages about the alarm noise.  
 */
class SenseFuse: Fuse
    construct(obj, prop, turns, source, sense)
    {
        /* inherit the base constructor */
        inherited(obj, prop, turns);

        /* remember our sensory context */
        source_ = source;
        sense_ = sense;
    }
;

/*
 *   Daemon.  A daemon is an event that fires repeatedly at given
 *   intervals.  When a daemon is executed, it is scheduled again for
 *   execution after its interval elapses again.  
 */
class Daemon: Event
    /*
     *   Creation.  'interval' is the number of turns between invocations
     *   of the daemon; this should be at least 1, which causes the daemon
     *   to be invoked on each turn.  The first execution will be
     *   (interval-1) turns in the future - so if interval is 1, the
     *   daemon will first be executed on the current turn, and if
     *   interval is 2, the daemon will be executed on the next turn.  
     */
    construct(obj, prop, interval)
    {
        /* inherit the base class constructor */
        inherited(obj, prop);

        /* 
         *   an interval of less than 1 is meaningless, so make sure it's
         *   at least 1 
         */
        if (interval < 1)
            interval = 1;

        /* remember my interval */
        interval_ = interval;

        /* 
         *   set my initial execution time, in game clock time - add one
         *   less than the interval to the current game clock time, so
         *   that we count the current turn as yet to elapse for the
         *   purposes of the interval before the daemon's first execution 
         */
        nextRunTime = Schedulable.gameClockTime + interval - 1;
    }
    
    /* execute the daemon */
    executeEvent()
    {
        /* call my method */
        callMethod();

        /* advance our next run time by our interval */
        nextRunTime += interval_;
    }

    /* our execution interval, in turns */
    interval_ = 1
;

/*
 *   Sensory-context-sensitive daemon - this is a daemon with an explicit
 *   sensory context.  This is the daemon counterpart of SenseFuse.  
 */
class SenseDaemon: Daemon
    construct(obj, prop, interval, source, sense)
    {
        /* inherit the base constructor */
        inherited(obj, prop, interval);

        /* remember our sensory context */
        source_ = source;
        sense_ = sense;
    }
;

/*
 *   Command Prompt Daemon.  This is a special type of daemon that
 *   executes not according to the game clock, but rather once per command
 *   prompt.  The system executes all of these daemons just before each
 *   time it prompts for a command line.  
 */
class PromptDaemon: Event
    /* execute the daemon */
    executeEvent()
    {
        /* 
         *   call my method - there's nothing else to do for this type of
         *   daemon, since our scheduling is not affected by the game
         *   clock 
         */
        callMethod();
    }

    /* flag: we are a special per-command-prompt daemon */
    isPromptDaemon = true
;

/*
 *   A one-time-only prompt daemon is a regular command prompt daemon,
 *   except that it fires only once.  After it fires once, the daemon
 *   automatically deactivates itself, so that it won't fire again.
 *   
 *   Prompt daemons are occasionally useful for non-recurring processing,
 *   when you want to defer some bit of code until a "safe" time between
 *   turns.  In these cases, the regular PromptDaemon is inconvenient to
 *   use because it automatically recurs.  This subclass is handy for these
 *   cases, since it lets you schedule some bit of processing for a single
 *   deferred execution.
 *   
 *   One special situation where one-time prompt daemons can be handy is in
 *   triggering conversational events - such as initiating a conversation -
 *   at the very beginning of the game.  Initiating a conversation can only
 *   be done from within an action context, but no action context is in
 *   effect during the game's initialization.  An easy way to deal with
 *   this is to create a one-time prompt daemon during initialization, and
 *   then trigger the event from the daemon's callback method.  The prompt
 *   daemon will set up a daemon action environment just before the first
 *   command prompt is displayed, at which point the callback will be able
 *   to trigger the event as though it were in ordinary action handler
 *   code.  
 */
class OneTimePromptDaemon: PromptDaemon
    executeEvent()
    {
        /* execute as normal */
        inherited();

        /* remove myself from the event list, so that I don't fire again */
        removeEvent();
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Real-Time Event Manager.  This object manages all of the game's
 *   real-time events, which are events that occur according to elapsed
 *   real-world time. 
 */
realTimeManager: BasicEventManager, InitObject
    /*
     *   Get the elapsed game time at which the next real-time event is
     *   scheduled.  This returns a value which can be compared to that
     *   returned by getElapsedTime(): if this value is less than or equal
     *   to the value from getElapsedTime(), then the next event is reay
     *   for immediate execution; otherwise, the result of subtracting
     *   getElapsedTime() from our return value gives the number of
     *   milliseconds until the next event is schedulable.
     *   
     *   Note that we don't calculate the delta to the next event time,
     *   but instead return the absolute time, because the caller might
     *   need to perform extra processing before using our return value.
     *   If we returned a delta, that extra processing time wouldn't be
     *   figured into the caller's determination of event schedulability.
     *   
     *   If we return nil, it means that there are no scheduled real-time
     *   events.  
     */
    getNextEventTime()
    {
        local tMin;
        
        /* 
         *   run through our event list and find the event with the lowest
         *   scheduled run time 
         */
        tMin = nil;
        foreach (local cur in events_)
        {
            local tCur;

            /* get the current item's time */
            tCur = cur.getEventTime();
            
            /* 
             *   if this one has a valid time, and we don't have a valid
             *   time yet or this one is sooner than the soonest one we've
             *   seen so far, note this one as the soonest so far 
             */
            if (tMin == nil
                || (tCur != nil && tCur < tMin))
            {
                /* this is the soonest so far */
                tMin = tCur;
            }
        }

        /* return the soonest event so far */
        return tMin;
    }

    /*
     *   Run any real-time events that are ready to execute, then return
     *   the next event time.  The return value has the same meaning as
     *   that of getNextEventTime(). 
     */
    executeEvents()
    {
        local tMin;
        
        /* 
         *   Keep checking as long as we find anything to execute.  Each
         *   time we execute an event, we might consume enough time that
         *   an item earlier in our queue that we originally dismissed as
         *   unready has become ready to run.  
         */
        for (;;)
        {
            local foundEvent;

            /* we haven't yet run anything on this pass */
            foundEvent = nil;

            /* we haven't found anything schedulable on this pass yet */
            tMin = nil;
            
            /* run each event whose time is already here */
            foreach (local cur in events_)
            {
                local tCur;

                /* 
                 *   If this event has a non-nil time, and its time is
                 *   less than or equal to the current system clock time,
                 *   run this event.  All event times are in terms of the
                 *   game elapsed time.
                 *   
                 *   If this event isn't schedulable, at least check to
                 *   see if it's the soonest schedulable event so far.  
                 */
                tCur = cur.getEventTime();
                if (tCur != nil && tCur <= getElapsedTime())
                {
                    /* cancel any sense caching currently in effect */
                    libGlobal.disableSenseCache();

                    /* execute this event */
                    cur.executeEvent();

                    /* note that we executed something */
                    foundEvent = true;
                }
                else if (tMin == nil
                         || (tCur != nil && tCur < tMin))
                {
                    /* it's the soonest event so far */
                    tMin = tCur;
                }
            }

            /* if we didn't execute anything on this pass, stop scanning */
            if (!foundEvent)
                break;
        }

        /* return the time of the next event */
        return tMin;
    }

    /*
     *   Get the current game elapsed time.  This is the number of
     *   milliseconds that has elapsed since the game was started,
     *   counting only the continuous execution time.  When the game is
     *   saved, we save the elapsed time at that point; when the game is
     *   later restored, we project that saved time backwards from the
     *   current real-world time at restoration to get the real-world time
     *   where the game would have started if it had actually been played
     *   continuously in one session.  
     */
    getElapsedTime()
    {
        /* 
         *   return the current system real-time counter minus the virtual
         *   starting time 
         */
        return getTime(GetTimeTicks) - startingTime;
    }

    /*
     *   Set the current game elapsed time.  This can be used to freeze
     *   the real-time clock - a caller can note the elapsed game time at
     *   one point by calling getElapsedTime(), and then pass the same
     *   value to this routine to ensure that no real time can effectively
     *   pass between the two calls. 
     */
    setElapsedTime(t)
    {
        /* 
         *   set the virtual starting time to the current system real-time
         *   counter minus the given game elapsed time 
         */
        startingTime = getTime(GetTimeTicks) - t;
    }

    /*
     *   The imaginary real-world time of the starting point of the game,
     *   treating the game as having been played from the start in one
     *   continous session.  Whenever we restore a saved game, we project
     *   backwards from the current real-world time at restoration by the
     *   amount of continuous elapsed time in the saved game to find the
     *   point at which the game would have started if it had been played
     *   continuously in one session up to the restored point.
     *   
     *   We set a static initial value for this, using the interpreter's
     *   real-time clock value at compilation time.  This ensures that
     *   we'll have a meaningful time base if any real-time events are
     *   created during pre-initialization.  This static value will only be
     *   in effect during preinit; we're an InitObject, so our execute()
     *   method will be invoked at run-time start-up, and at that point
     *   we'll reset the zero point to the actual run-time start time.  
     */
    startingTime = static getTime(GetTimeTicks)

    /* 
     *   Initialize at run-time startup.  We want to set the zero point as
     *   the time when the player actually started playing the game (any
     *   time we spent in pre-initialization doesn't count on the real-time
     *   clock, since it's not part of the game per se).  
     */
    execute()
    {
        /* 
         *   note the real-time starting point of the game, so we can
         *   calculate the elapsed game time later 
         */
        startingTime = getTime(GetTimeTicks);
    }

    /* 
     *   save the elapsed time so far - this is called just before we save
     *   a game so that we can pick up where we left off on the elapsed
     *   time clock when we restore the saved game 
     */
    saveElapsedTime()
    {
        /* remember the elapsed time so far */
        elapsedTimeAtSave = getElapsedTime();
    }

    /*
     *   Restore the elapsed time - this is called just after we restore a
     *   game.  We'll project the saved elapsed time backwards to figure
     *   the imaginary starting time the game would have had if it had
     *   been played in one continuous session rather than being saved and
     *   restored. 
     */
    restoreElapsedTime()
    {
        /* 
         *   project backwards from the current time by the saved elapsed
         *   time to get the virtual starting point that will give us the
         *   same current elapsed time on the system real-time clock 
         */
        startingTime = getTime(GetTimeTicks) - elapsedTimeAtSave;
    }

    /* our event list */
    events_ = static new Vector(20)

    /* the event currently being executed */
    curEvent_ = nil

    /* 
     *   saved elapsed time - we use this to figure the virtual starting
     *   time when we restore a saved game 
     */
    elapsedTimeAtSave = 0
;

/*
 *   Real-time manager: pre-save notification receiver.  When we're about
 *   to save the game, we'll note the current elapsed game time, so that
 *   when we later restore the game, we can figure the virtual starting
 *   point that will give us the same effective elapsed time on the system
 *   real-time clock.  
 */
PreSaveObject
    execute()
    {
        /* 
         *   remember the elapsed time at the point we saved the game, so
         *   that we can restore it later 
         */
        realTimeManager.saveElapsedTime();
    }
;

/*
 *   Real-time manager: post-restore notification receiver.  Immediately
 *   after we restore a game, we'll tell the real-time manager to refigure
 *   the virtual starting point of the game based on the saved elapsed
 *   time. 
 */
PostRestoreObject
    execute()
    {
        /* figure the new virtual starting time */
        realTimeManager.restoreElapsedTime();
    }
;

/*
 *   Real-Time Event.  This is an event that occurs according to elapsed
 *   wall-clock time in the real world.  
 */
class RealTimeEvent: BasicEvent
    /*
     *   Get the elapsed real time at which this event is triggered.  This
     *   is a time value in terms of realTimeManager.getElapsedTime().  
     */
    getEventTime()
    {
        /* by default, simply return our eventTime value */
        return eventTime;
    }

    /* construction */
    construct(obj, prop)
    {
        /* inherit default handling */
        inherited(obj, prop);

        /* add myself to the real-time event manager's active list */
        realTimeManager.addEvent(self);
    }

    /* remove this event from the real-time event manager */
    removeEvent() { realTimeManager.removeEvent(self); }

    /* our scheduled event time */
    eventTime = 0
;

/*
 *   Real-time fuse.  This is an event that fires once at a specified
 *   elapsed time into the game. 
 */
class RealTimeFuse: RealTimeEvent
    /*
     *   Creation.  'delta' is the amount of real time (in milliseconds)
     *   that should elapse before the fuse is executed.  If 'delta' is
     *   zero or negative, the fuse will be schedulable immediately.  
     */
    construct(obj, prop, delta)
    {
        /* inherit default handling */
        inherited(obj, prop);

        /* 
         *   set my scheduled time to the current game elapsed time plus
         *   the delta - this will give us the time in terms of elapsed
         *   game time at which we'll be executed 
         */
        eventTime = realTimeManager.getElapsedTime() + delta;
    }

    /* execute the fuse */
    executeEvent()
    {
        /* call my method */
        callMethod();

        /* a fuse fires only once, so remove myself from further scheduling */
        realTimeManager.removeEvent(self);
    }
;

/*
 *   Sensory-context-sensitive real-time fuse.  This is a real-time fuse
 *   with an explicit sensory context. 
 */
class RealTimeSenseFuse: RealTimeFuse
    construct(obj, prop, delta, source, sense)
    {
        /* inherit the base constructor */
        inherited(obj, prop, delta);

        /* remember our sensory context */
        source_ = source;
        sense_ = sense;
    }
;

/*
 *   Real-time daemon.  This is an event that occurs repeatedly at given
 *   real-time intervals.  When a daemon is executed, it is scheduled
 *   again for execution after its real-time interval elapses again.  The
 *   daemon's first execution will occur one interval from the time at
 *   which the daemon is created.
 *   
 *   If a daemon is executed late (because other, more pressing tasks had
 *   to be completed first, or because the user was busy editing a command
 *   line and the local platform doesn't support real-time command
 *   interruptions), the interval is applied to the time the daemon
 *   actually executed, not to the originally scheduled execution time.
 *   For example, if the daemon is scheduled to run once every minute, but
 *   can't run at all for five minutes because of command editing on a
 *   non-interrupting platform, once it actually does run, it won't run
 *   again for (at least) another minute after that.  This means that the
 *   daemon will not run five times all at once when it's finally allowed
 *   to run - there's no making up for lost time.  
 */
class RealTimeDaemon: RealTimeEvent
    /*
     *   Creation.  'interval' is the number of milliseconds between
     *   invocations.  
     */
    construct(obj, prop, interval)
    {
        /* inherit the base constructor */
        inherited(obj, prop);

        /* remember my interval */
        interval_ = interval;

        /* 
         *   figure my initial execution time - wait for one complete
         *   interval from the current time 
         */
        eventTime = realTimeManager.getElapsedTime() + interval;
    }

    /* execute the daemon */
    executeEvent()
    {
        /* call my method */
        callMethod();

        /* 
         *   Reschedule for next time.  To ensure that we keep to our
         *   long-term schedule, reschedule based on our original schedule
         *   time rather than the current clock time; that way, if there
         *   was a delay after our original scheduled time in firing us,
         *   we'll make up for it by shortening the interval until the
         *   next firing.  If that would make us already schedulable, then
         *   our interval must be so short we can't keep up with it; in
         *   that case, add the interval to the current clock time. 
         */
        eventTime += interval_;
        if (realTimeManager.getElapsedTime() < eventTime)
            eventTime = realTimeManager.getElapsedTime() + interval_;
    }

    /* my execution interval, in milliseconds */
    interval_ = 1
;

/*
 *   Sensory-context-sensitive real-time daemon - this is a real-time
 *   daemon with an explicit sensory context.  This is the daemon
 *   counterpart of RealTimeSenseFuse.  
 */
class RealTimeSenseDaemon: RealTimeDaemon
    construct(obj, prop, interval, source, sense)
    {
        /* inherit the base constructor */
        inherited(obj, prop, interval);

        /* remember our sensory context */
        source_ = source;
        sense_ = sense;
    }
;

TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3