# 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 8/21/2012 from TADS version 3.1.2