1#charset "us-ascii"
2
3/* Copyright (c) 2000, 2002 Michael J. Roberts.  All Rights Reserved. */
4/*
5 *   TADS 3 Library: events
6 *
7 *   This module defines the event framework.  An event is a programmed
8 *   operation that occurs at a particular point in the game; an event can
9 *   be turn-based, in which case it occurs after a given number of turns
10 *   has elapsed, or it can occur in real time, which means that it occurs
11 *   after a particular interval of time has elapsed.
12 */
13
14#include "adv3.h"
15#include <dict.h>
16#include <gramprod.h>
17
18
19/* ------------------------------------------------------------------------ */
20/*
21 *   Run the main scheduling loop.  This continues until we encounter an
22 *   end-of-file error reading from the console, or a QuitException is
23 *   thrown to terminate the game.
24 */
25runScheduler()
26{
27    /* keep going until we quit the game */
28    for (;;)
29    {
30        /* catch the exceptions that terminate the game */
31        try
32        {
33            local minTime;
34            local vec;
35
36            /* start with an empty list of schedulable items */
37            vec = new Vector(10);
38
39            /* find the lowest time at which something is ready to run */
40            minTime = nil;
41            foreach (local cur in Schedulable.allSchedulables)
42            {
43                local curTime;
44
45                /* get this item's next eligible run time */
46                curTime = cur.getNextRunTime();
47
48                /*
49                 *   if it's not nil, and it's equal to or below the
50                 *   lowest we've seen so far, note it
51                 */
52                if (curTime != nil && (minTime == nil || curTime <= minTime))
53                {
54                    /*
55                     *   if this is different from the current minimum
56                     *   schedulable time, clear out the list of
57                     *   schedulables, because the list keeps track of the
58                     *   items at the lowest time only
59                     */
60                    if (minTime != nil && curTime < minTime)
61                        vec.removeRange(1, vec.length());
62
63                    /* add this item to the list */
64                    vec.append(cur);
65
66                    /* note the new lowest schedulable time */
67                    minTime = curTime;
68                }
69            }
70
71            /*
72             *   if nothing's ready to run, the game is over by default,
73             *   since we cannot escape this state - we can't ourselves
74             *   change anything's run time, so if nothing's ready to run
75             *   now, we won't be able to change that, and so nothing will
76             *   ever be ready to run
77             */
78            if (minTime == nil)
79            {
80                "\b[Error: nothing is available for scheduling -
81                terminating]\b";
82                return;
83            }
84
85            /*
86             *   advance the global turn counter by the amount of game
87             *   clock time we're consuming now
88             */
89            libGlobal.totalTurns += minTime - Schedulable.gameClockTime;
90
91            /*
92             *   advance the game clock to the minimum run time - nothing
93             *   interesting happens in game time until then, so we can
94             *   skip straight ahead to this time
95             */
96            Schedulable.gameClockTime = minTime;
97
98            /* calculate the schedule order for each item */
99            vec.forEach({x: x.calcScheduleOrder()});
100
101            /*
102             *   We have a list of everything schedulable at the current
103             *   game clock time.  Sort the list in ascending scheduling
104             *   order, so that the higher priority items come first in
105             *   the list.
106             */
107            vec = vec.sort(
108                SortAsc, {a, b: a.scheduleOrder - b.scheduleOrder});
109
110            /*
111             *   Run through the list and run each item.  Keep running
112             *   each item as long as it's ready to run - that is, as long
113             *   as its schedulable time equals the game clock time.
114             */
115        vecLoop:
116            foreach (local cur in vec)
117            {
118                /* run this item for as long as it's ready to run */
119                while (cur.getNextRunTime() == minTime)
120                {
121                    try
122                    {
123                        /*
124                         *   execute this item - if it doesn't want to be
125                         *   called again without considering other
126                         *   objects, stop looping and refigure the
127                         *   scheduling order from scratch
128                         */
129                        if (!cur.executeTurn())
130                            break vecLoop;
131                    }
132                    catch (Exception exc)
133                    {
134                        /*
135                         *   The scheduled operation threw an exception.
136                         *   If the schedulable's next run time didn't get
137                         *   updated, then the same schedulable will be
138                         *   considered ready to run again immediately on
139                         *   the next time through the loop.  It's quite
140                         *   possible in this case that we'll simply repeat
141                         *   the operation that threw the exception and get
142                         *   right back here again.  If this happens, it
143                         *   will effectively starve all of the other
144                         *   schedulables.  To ensure that other
145                         *   schedulables get a chance to run before we try
146                         *   this erroneous operation again, advance its
147                         *   next run time by one unit if it hasn't already
148                         *   been advanced.
149                         */
150                        if (cur.getNextRunTime() == minTime)
151                            cur.incNextRunTime(1);
152
153                        /* re-throw the exception */
154                        throw exc;
155                    }
156                }
157            }
158        }
159        catch (EndOfFileException eofExc)
160        {
161            /* end of file reading command input - we're done */
162            return;
163        }
164        catch (QuittingException quitExc)
165        {
166            /* explicitly quitting - we're done */
167            return;
168        }
169        catch (RestartSignal rsSig)
170        {
171            /*
172             *   Restarting - re-throw the signal for handling in the
173             *   system startup code.  Note that we explicitly catch this
174             *   signal, only to rethrow it, because we'd otherwise flag it
175             *   as an unhandled error in the catch-all Exception handler.
176             */
177            throw rsSig;
178        }
179        catch (RuntimeError rtErr)
180        {
181            /* if this is a debugger error of some kind, re-throw it */
182            if (rtErr.isDebuggerSignal)
183                throw rtErr;
184
185            /* display the error, but keep going */
186            "\b[<<rtErr.displayException()>>]\b";
187        }
188        catch (TerminateCommandException tce)
189        {
190            /*
191             *   Aborted command - ignore it.  This is most like to occur
192             *   when a fuse, daemon, or the like tries to terminate itself
193             *   with this exception, thinking it's operating in a normal
194             *   command execution environment.  As a convenience, simply
195             *   ignore these exceptions so that any code can use them to
196             *   abort everything and return to the main scheduling loop.
197             */
198        }
199        catch (ExitSignal es)
200        {
201            /* ignore this, just as we ignore TerminateCommandException */
202        }
203        catch (ExitActionSignal eas)
204        {
205            /* ignore this, just as we ignore TerminateCommandException */
206        }
207        catch (Exception exc)
208        {
209            /* some other unhandled exception - display it and keep going */
210            "\b[Unhandled exception: <<exc.displayException()>>]\b";
211        }
212    }
213}
214
215/* ------------------------------------------------------------------------ */
216/*
217 *   An item that can be scheduled for time-based notifications.  The main
218 *   scheduler loop in runScheduler() operates on objects of this class.
219 *
220 *   Note that we build a list of all Schedulable instances during
221 *   pre-initialization.  If any Schedulable objects are dynamically
222 *   created, they must be added to the list explicitly after creation in
223 *   order for the event manager to schedule them for execution.  The
224 *   default constructor does this automatically, so subclasses can simply
225 *   inherit our constructor to be added to the master list.
226 */
227class Schedulable: object
228    /* construction - add myself to the Schedulable list */
229    construct()
230    {
231        /*
232         *   Add myself to the master list of Schedulable instances.  Note
233         *   that we must update the list in the Schedulable class itself.
234         */
235        Schedulable.allSchedulables += self;
236    }
237
238    /*
239     *   Get the next time (on the game clock) at which I'm eligible for
240     *   execution.  We won't receive any scheduling notifications until
241     *   this time.  If this object doesn't want any scheduling
242     *   notifications, return nil.
243     */
244    getNextRunTime() { return nextRunTime; }
245
246    /* advance my next run time by the given number of clock units */
247    incNextRunTime(amt)
248    {
249        if (nextRunTime != nil)
250            nextRunTime += amt;
251    }
252
253    /*
254     *   Notify this object that its scheduled run time has arrived.  This
255     *   should perform the scheduled task.  If the scheduled task takes
256     *   any game time, the object's internal next run time should be
257     *   updated accordingly.
258     *
259     *   The scheduler will invoke this method of the same object
260     *   repeatedly for as long as its nextRunTime remains unchanged AND
261     *   this method returns true.  If the object's scheduling priority
262     *   changes relative to other schedulable objects, it should return
263     *   nil here to tell the scheduler to recalculate scheduling
264     *   priorities.
265     */
266    executeTurn() { return true; }
267
268    /*
269     *   Scheduling order.  This determines which item goes first when
270     *   multiple items are schedulable at the same time (i.e., they all
271     *   have the same getNextRunTime() values).  The item with the lowest
272     *   number here goes first.
273     *
274     *   This should never be evaluated except immediately after a call to
275     *   calcScheduleOrder.
276     */
277    scheduleOrder = 100
278
279    /*
280     *   Calculate the scheduling order, returning the order value and
281     *   storing it in our property scheduleOrder.  This is used to
282     *   calculate and cache the value prior to sorting a list of
283     *   schedulable items.  We use this two-step approach (first
284     *   calculate, then sort) so that we avoid repeatedly evaluating a
285     *   complex calculation, if indeed there is a complex calculation to
286     *   perform.
287     *
288     *   By default, we assume that the schedule order is static, so we
289     *   simply leave our scheduleOrder property unchanged and return its
290     *   present value.
291     */
292    calcScheduleOrder() { return scheduleOrder; }
293
294    /* my next running time, in game clock time */
295    nextRunTime = nil
296
297    /*
298     *   A class variable giving the current game clock time.  This is a
299     *   class variable because there's only one global game clock.  The
300     *   game clock starts at zero and increments in game time units; a
301     *   game time unit is the arbitrary quantum of time for our event
302     *   scheduling system.
303     */
304    gameClockTime = 0
305
306    /*
307     *   A list of all of the Schedulable objects in the game.  We set this
308     *   up during pre-initialization; if any Schedulable instances are
309     *   created dynamically, they must be explicitly added to this list
310     *   after creation.
311     */
312    allSchedulables = nil
313;
314
315/*
316 *   Pre-initializer: build the master list of Schedulable instances
317 */
318PreinitObject
319    /*
320     *   Execute preinitialization.  Build a list of all of the schedulable
321     *   objects in the game, so that we can scan this list quickly during
322     *   play.
323     */
324    execute()
325    {
326        local vec;
327
328        /* set up an empty vector to hold the schedulable objects */
329        vec = new Vector(32);
330
331        /* add all of the Schedulable instances to the vector */
332        forEachInstance(Schedulable, {s: vec.append(s)});
333
334        /* save the list of Schedulable instances as an ordinary list */
335        Schedulable.allSchedulables = vec.toList();
336    }
337;
338
339/* ------------------------------------------------------------------------ */
340/*
341 *   Basic Event Manager.  This is a common base class for the game-time
342 *   and real-time event managers.  This class handles the details of
343 *   managing the event queue; the subclasses must define the specifics of
344 *   event timing.
345 */
346class BasicEventManager: object
347    /* add an event */
348    addEvent(event)
349    {
350        /* append the event to our list */
351        events_.append(event);
352    }
353
354    /* remove an event */
355    removeEvent(event)
356    {
357        /* remove the event from our list */
358        events_.removeElement(event);
359    }
360
361    /*
362     *   Remove events matching the given object and property combination.
363     *   We remove all events that match both the object and property
364     *   (events matching only the object or only the property are not
365     *   affected).
366     *
367     *   This is provided mostly as a convenience for cases where an event
368     *   is known to be uniquely identifiable by its object and property
369     *   values; this saves the caller the trouble of keeping track of the
370     *   Event object created when the event was first registered.
371     *
372     *   When a particular object/property combination might be used in
373     *   several different events, it's better to keep a reference to the
374     *   Event object representing each event, and use removeEvent() to
375     *   remove the specific Event object of interest.
376     *
377     *   Returns true if we find any matching events, nil if not.
378     */
379    removeMatchingEvents(obj, prop)
380    {
381        local found;
382
383        /*
384         *   Scan our list, and remove each event matching the parameters.
385         *   Note that it's safe to remove things from a vector that we're
386         *   iterating with foreach(), since foreach() makes a safe copy
387         *   of the vector for the iteration.
388         */
389        found = nil;
390        foreach (local cur in events_)
391        {
392            /* if this one matches, remove it */
393            if (cur.eventMatches(obj, prop))
394            {
395                /* remove the event */
396                removeEvent(cur);
397
398                /* note that we found a match */
399                found = true;
400            }
401        }
402
403        /* return our 'found' indication */
404        return found;
405    }
406
407    /*
408     *   Remove the current event - this is provided for convenience so
409     *   that an event can cancel itself in the course of its execution.
410     *
411     *   Note that this has no effect on the current event execution -
412     *   this simply prevents the event from receiving additional
413     *   notifications in the future.
414     */
415    removeCurrentEvent()
416    {
417        /* remove the currently active event from our list */
418        removeEvent(curEvent_);
419    }
420
421    /* event list - each instance must initialize this to a vector */
422    // events_ = nil
423;
424
425/*
426 *   Event Manager.  This is a schedulable object that keeps track of
427 *   fuses and daemons, and schedules their execution.
428 */
429eventManager: BasicEventManager, Schedulable
430    /*
431     *   Use a scheduling order of 1000 to ensure we go after all actors.
432     *   By default, actors use scheduling orders in the range 100 to 400,
433     *   so our order of 1000 ensures that fuses and daemons run after all
434     *   characters on a given turn.
435     */
436    scheduleOrder = 1000
437
438    /*
439     *   Get the next run time.  We'll find the lowest run time of our
440     *   fuses and daemons and return that.
441     */
442    getNextRunTime()
443    {
444        local minTime;
445
446        /*
447         *   run through our list of events, and find the event that is
448         *   scheduled to run at the lowest game clock time
449         */
450        minTime = nil;
451        foreach (local cur in events_)
452        {
453            local curTime;
454
455            /* get this item's scheduled run time */
456            curTime = cur.getNextRunTime();
457
458            /* if it's not nil and it's the lowest so far, remember it */
459            if (curTime != nil && (minTime == nil || curTime < minTime))
460                minTime = curTime;
461        }
462
463        /* return the minimum time we found */
464        return minTime;
465    }
466
467    /*
468     *   Execute a turn.  We'll execute each fuse and each daemon that is
469     *   currently schedulable.
470     */
471    executeTurn()
472    {
473        local lst;
474
475        /*
476         *   build a list of all of our events with the current game clock
477         *   time - these are the events that are currently schedulable
478         */
479        lst = events_.subset({x: x.getNextRunTime()
480                                 == Schedulable.gameClockTime});
481
482        /* execute the items in this list */
483        executeList(lst);
484
485        /* no change in scheduling priorities */
486        return true;
487    }
488
489    /*
490     *   Execute a command prompt turn.  We'll execute each
491     *   per-command-prompt daemon.
492     */
493    executePrompt()
494    {
495        /* execute all of the per-command-prompt daemons */
496        executeList(events_.subset({x: x.isPromptDaemon}));
497    }
498
499    /*
500     *   internal service routine - execute the fuses and daemons in the
501     *   given list, in eventOrder priority order
502     */
503    executeList(lst)
504    {
505        /* sort the list in ascending event order */
506        lst = lst.toList()
507              .sort(SortAsc, {a, b: a.eventOrder - b.eventOrder});
508
509        /* run through the list and execute each item ready to run */
510        foreach (local cur in lst)
511        {
512            /* remember our old active event, then establish the new one */
513            local oldEvent = curEvent_;
514            curEvent_ = cur;
515
516            /* make sure we restore things on the way out */
517            try
518            {
519                local pc;
520
521                /* have the player character note the pre-event conditions */
522                pc = gPlayerChar;
523                pc.noteConditionsBefore();
524
525                /* cancel any sense caching currently in effect */
526                libGlobal.disableSenseCache();
527
528                /* execute the event */
529                cur.executeEvent();
530
531                /*
532                 *   if the player character is the same as it was, ask
533                 *   the player character to note any change in conditions
534                 */
535                if (gPlayerChar == pc)
536                    pc.noteConditionsAfter();
537            }
538            finally
539            {
540                /* restore the enclosing current event */
541                curEvent_ = oldEvent;
542            }
543        }
544    }
545
546    /* our list of fuses and daemons */
547    events_ = static new Vector(20)
548
549    /* the event currently being executed */
550    curEvent_ = nil
551;
552
553/*
554 *   Pseudo-action subclass to represent the action environment while
555 *   processing a daemon, fuse, or other event.
556 */
557class EventAction: Action
558;
559
560/*
561 *   A basic event, for game-time and real-time events.
562 */
563class BasicEvent: object
564    /* construction */
565    construct(obj, prop)
566    {
567        /* remember the object and property to call at execution */
568        obj_ = obj;
569        prop_ = prop;
570    }
571
572    /*
573     *   Execute the event.  This must be overridden by the subclass to
574     *   perform the appropriate operation when executed.  In particular,
575     *   the subclass must reschedule or unschedule the event, as
576     *   appropriate.
577     */
578    executeEvent() { }
579
580    /* does this event match the given object/property combination? */
581    eventMatches(obj, prop) { return obj == obj_ && prop == prop_; }
582
583    /*
584     *   Call our underlying method.  This is an internal routine intended
585     *   for use by the executeEvent() implementations.
586     */
587    callMethod()
588    {
589        /*
590         *   invoke the method in our sensory context, and in a simulated
591         *   action environment
592         */
593        withActionEnv(EventAction, gPlayerChar,
594            {: callWithSenseContext(source_, sense_,
595                                    {: obj_.(self.prop_)() }) });
596    }
597
598    /* the object and property we invoke */
599    obj_ = nil
600    prop_ = nil
601
602    /*
603     *   The sensory context of the event.  When the event fires, we'll
604     *   execute its method in this sensory context, so that any messages
605     *   generated will be displayed only if the player character can
606     *   sense the source object in the given sense.
607     *
608     *   By default, these are nil, which means that the event's messages
609     *   will be displayed (or, at least, they won't be suppressed because
610     *   of the sensory context).
611     */
612    source_ = nil
613    sense_ = nil
614;
615
616/*
617 *   Base class for fuses and daemons
618 */
619class Event: BasicEvent
620    /* our next run time, in game clock time */
621    getNextRunTime() { return nextRunTime; }
622
623    /* delay our scheduled run time by the given number of turns */
624    delayEvent(turns) { nextRunTime += turns; }
625
626    /* remove this event from the event manager */
627    removeEvent() { eventManager.removeEvent(self); }
628
629    /*
630     *   Event order - this establishes the order we run relative to other
631     *   events scheduled to run at the same game clock time.  Lowest
632     *   number goes first.  By default, we provide an event order of 50,
633     *   which should leave plenty of room for custom events before and
634     *   after default events.
635     */
636    eventOrder = 50
637
638    /* creation */
639    construct(obj, prop)
640    {
641        /* inherit default handling */
642        inherited(obj, prop);
643
644        /* add myself to the event manager's active event list */
645        eventManager.addEvent(self);
646    }
647
648    /*
649     *   our next execution time, expressed in game clock time; by
650     *   default, we'll set this to nil, which means that we are not
651     *   scheduled to execute at all
652     */
653    nextRunTime = nil
654
655    /* by default, we're not a per-command-prompt daemon */
656    isPromptDaemon = nil
657;
658
659/*
660 *   Fuse.  A fuse is an event that fires once at a given time in the
661 *   future.  Once a fuse is executed, it is removed from further
662 *   scheduling.
663 */
664class Fuse: Event
665    /*
666     *   Creation.  'turns' is the number of turns in the future at which
667     *   the fuse is executed; if turns is 0, the fuse will be executed on
668     *   the current turn.
669     */
670    construct(obj, prop, turns)
671    {
672        /* inherit the base class constructor */
673        inherited(obj, prop);
674
675        /*
676         *   set my scheduled time to the current game clock time plus the
677         *   number of turns into the future
678         */
679        nextRunTime = Schedulable.gameClockTime + turns;
680    }
681
682    /* execute the fuse */
683    executeEvent()
684    {
685        /* call my method */
686        callMethod();
687
688        /* a fuse fires only once, so remove myself from further scheduling */
689        eventManager.removeEvent(self);
690    }
691;
692
693/*
694 *   Sensory-context-sensitive fuse - this is a fuse with an explicit
695 *   sensory context.  We'll run the fuse in its sense context, so any
696 *   messages generated will be visible only if the given source object is
697 *   reachable by the player character in the given sense.
698 *
699 *   Conceptually, the source object is considered the source of any
700 *   messages that the fuse generates, and the messages pertain to the
701 *   given sense; so if the player character cannot sense the source
702 *   object in the given sense, the messages should not be displayed.  For
703 *   example, if the fuse will describe the noise made by an alarm clock
704 *   when the alarm goes off, the source object would be the alarm clock
705 *   and the sense would be sound; this way, if the player character isn't
706 *   in hearing range of the alarm clock when the alarm goes off, we won't
707 *   display messages about the alarm noise.
708 */
709class SenseFuse: Fuse
710    construct(obj, prop, turns, source, sense)
711    {
712        /* inherit the base constructor */
713        inherited(obj, prop, turns);
714
715        /* remember our sensory context */
716        source_ = source;
717        sense_ = sense;
718    }
719;
720
721/*
722 *   Daemon.  A daemon is an event that fires repeatedly at given
723 *   intervals.  When a daemon is executed, it is scheduled again for
724 *   execution after its interval elapses again.
725 */
726class Daemon: Event
727    /*
728     *   Creation.  'interval' is the number of turns between invocations
729     *   of the daemon; this should be at least 1, which causes the daemon
730     *   to be invoked on each turn.  The first execution will be
731     *   (interval-1) turns in the future - so if interval is 1, the
732     *   daemon will first be executed on the current turn, and if
733     *   interval is 2, the daemon will be executed on the next turn.
734     */
735    construct(obj, prop, interval)
736    {
737        /* inherit the base class constructor */
738        inherited(obj, prop);
739
740        /*
741         *   an interval of less than 1 is meaningless, so make sure it's
742         *   at least 1
743         */
744        if (interval < 1)
745            interval = 1;
746
747        /* remember my interval */
748        interval_ = interval;
749
750        /*
751         *   set my initial execution time, in game clock time - add one
752         *   less than the interval to the current game clock time, so
753         *   that we count the current turn as yet to elapse for the
754         *   purposes of the interval before the daemon's first execution
755         */
756        nextRunTime = Schedulable.gameClockTime + interval - 1;
757    }
758
759    /* execute the daemon */
760    executeEvent()
761    {
762        /* call my method */
763        callMethod();
764
765        /* advance our next run time by our interval */
766        nextRunTime += interval_;
767    }
768
769    /* our execution interval, in turns */
770    interval_ = 1
771;
772
773/*
774 *   Sensory-context-sensitive daemon - this is a daemon with an explicit
775 *   sensory context.  This is the daemon counterpart of SenseFuse.
776 */
777class SenseDaemon: Daemon
778    construct(obj, prop, interval, source, sense)
779    {
780        /* inherit the base constructor */
781        inherited(obj, prop, interval);
782
783        /* remember our sensory context */
784        source_ = source;
785        sense_ = sense;
786    }
787;
788
789/*
790 *   Command Prompt Daemon.  This is a special type of daemon that
791 *   executes not according to the game clock, but rather once per command
792 *   prompt.  The system executes all of these daemons just before each
793 *   time it prompts for a command line.
794 */
795class PromptDaemon: Event
796    /* execute the daemon */
797    executeEvent()
798    {
799        /*
800         *   call my method - there's nothing else to do for this type of
801         *   daemon, since our scheduling is not affected by the game
802         *   clock
803         */
804        callMethod();
805    }
806
807    /* flag: we are a special per-command-prompt daemon */
808    isPromptDaemon = true
809;
810
811/* ------------------------------------------------------------------------ */
812/*
813 *   Real-Time Event Manager.  This object manages all of the game's
814 *   real-time events, which are events that occur according to elapsed
815 *   real-world time.
816 */
817realTimeManager: BasicEventManager, InitObject
818    /*
819     *   Get the elapsed game time at which the next real-time event is
820     *   scheduled.  This returns a value which can be compared to that
821     *   returned by getElapsedTime(): if this value is less than or equal
822     *   to the value from getElapsedTime(), then the next event is reay
823     *   for immediate execution; otherwise, the result of subtracting
824     *   getElapsedTime() from our return value gives the number of
825     *   milliseconds until the next event is schedulable.
826     *
827     *   Note that we don't calculate the delta to the next event time,
828     *   but instead return the absolute time, because the caller might
829     *   need to perform extra processing before using our return value.
830     *   If we returned a delta, that extra processing time wouldn't be
831     *   figured into the caller's determination of event schedulability.
832     *
833     *   If we return nil, it means that there are no scheduled real-time
834     *   events.
835     */
836    getNextEventTime()
837    {
838        local tMin;
839
840        /*
841         *   run through our event list and find the event with the lowest
842         *   scheduled run time
843         */
844        tMin = nil;
845        foreach (local cur in events_)
846        {
847            local tCur;
848
849            /* get the current item's time */
850            tCur = cur.getEventTime();
851
852            /*
853             *   if this one has a valid time, and we don't have a valid
854             *   time yet or this one is sooner than the soonest one we've
855             *   seen so far, note this one as the soonest so far
856             */
857            if (tMin == nil
858                || (tCur != nil && tCur < tMin))
859            {
860                /* this is the soonest so far */
861                tMin = tCur;
862            }
863        }
864
865        /* return the soonest event so far */
866        return tMin;
867    }
868
869    /*
870     *   Run any real-time events that are ready to execute, then return
871     *   the next event time.  The return value has the same meaning as
872     *   that of getNextEventTime().
873     */
874    executeEvents()
875    {
876        local tMin;
877
878        /*
879         *   Keep checking as long as we find anything to execute.  Each
880         *   time we execute an event, we might consume enough time that
881         *   an item earlier in our queue that we originally dismissed as
882         *   unready has become ready to run.
883         */
884        for (;;)
885        {
886            local foundEvent;
887
888            /* we haven't yet run anything on this pass */
889            foundEvent = nil;
890
891            /* we haven't found anything schedulable on this pass yet */
892            tMin = nil;
893
894            /* run each event whose time is already here */
895            foreach (local cur in events_)
896            {
897                local tCur;
898
899                /*
900                 *   If this event has a non-nil time, and its time is
901                 *   less than or equal to the current system clock time,
902                 *   run this event.  All event times are in terms of the
903                 *   game elapsed time.
904                 *
905                 *   If this event isn't schedulable, at least check to
906                 *   see if it's the soonest schedulable event so far.
907                 */
908                tCur = cur.getEventTime();
909                if (tCur != nil && tCur <= getElapsedTime())
910                {
911                    /* cancel any sense caching currently in effect */
912                    libGlobal.disableSenseCache();
913
914                    /* execute this event */
915                    cur.executeEvent();
916
917                    /* note that we executed something */
918                    foundEvent = true;
919                }
920                else if (tMin == nil
921                         || (tCur != nil && tCur < tMin))
922                {
923                    /* it's the soonest event so far */
924                    tMin = tCur;
925                }
926            }
927
928            /* if we didn't execute anything on this pass, stop scanning */
929            if (!foundEvent)
930                break;
931        }
932
933        /* return the time of the next event */
934        return tMin;
935    }
936
937    /*
938     *   Get the current game elapsed time.  This is the number of
939     *   milliseconds that has elapsed since the game was started,
940     *   counting only the continuous execution time.  When the game is
941     *   saved, we save the elapsed time at that point; when the game is
942     *   later restored, we project that saved time backwards from the
943     *   current real-world time at restoration to get the real-world time
944     *   where the game would have started if it had actually been played
945     *   continuously in one session.
946     */
947    getElapsedTime()
948    {
949        /*
950         *   return the current system real-time counter minus the virtual
951         *   starting time
952         */
953        return getTime(GetTimeTicks) - startingTime;
954    }
955
956    /*
957     *   Set the current game elapsed time.  This can be used to freeze
958     *   the real-time clock - a caller can note the elapsed game time at
959     *   one point by calling getElapsedTime(), and then pass the same
960     *   value to this routine to ensure that no real time can effectively
961     *   pass between the two calls.
962     */
963    setElapsedTime(t)
964    {
965        /*
966         *   set the virtual starting time to the current system real-time
967         *   counter minus the given game elapsed time
968         */
969        startingTime = getTime(GetTimeTicks) - t;
970    }
971
972    /*
973     *   The imaginary real-world time of the starting point of the game,
974     *   treating the game as having been played from the start in one
975     *   continous session.  Whenever we restore a saved game, we project
976     *   backwards from the current real-world time at restoration by the
977     *   amount of continuous elapsed time in the saved game to find the
978     *   point at which the game would have started if it had been played
979     *   continuously in one session up to the restored point.
980     *
981     *   We set a static initial value for this, using the interpreter's
982     *   real-time clock value at compilation time.  This ensures that
983     *   we'll have a meaningful time base if any real-time events are
984     *   created during pre-initialization.  This static value will only be
985     *   in effect during preinit; we're an InitObject, so our execute()
986     *   method will be invoked at run-time start-up, and at that point
987     *   we'll reset the zero point to the actual run-time start time.
988     */
989    startingTime = static getTime(GetTimeTicks)
990
991    /*
992     *   Initialize at run-time startup.  We want to set the zero point as
993     *   the time when the player actually started playing the game (any
994     *   time we spent in pre-initialization doesn't count on the real-time
995     *   clock, since it's not part of the game per se).
996     */
997    execute()
998    {
999        /*
1000         *   note the real-time starting point of the game, so we can
1001         *   calculate the elapsed game time later
1002         */
1003        startingTime = getTime(GetTimeTicks);
1004    }
1005
1006    /*
1007     *   save the elapsed time so far - this is called just before we save
1008     *   a game so that we can pick up where we left off on the elapsed
1009     *   time clock when we restore the saved game
1010     */
1011    saveElapsedTime()
1012    {
1013        /* remember the elapsed time so far */
1014        elapsedTimeAtSave = getElapsedTime();
1015    }
1016
1017    /*
1018     *   Restore the elapsed time - this is called just after we restore a
1019     *   game.  We'll project the saved elapsed time backwards to figure
1020     *   the imaginary starting time the game would have had if it had
1021     *   been played in one continuous session rather than being saved and
1022     *   restored.
1023     */
1024    restoreElapsedTime()
1025    {
1026        /*
1027         *   project backwards from the current time by the saved elapsed
1028         *   time to get the virtual starting point that will give us the
1029         *   same current elapsed time on the system real-time clock
1030         */
1031        startingTime = getTime(GetTimeTicks) - elapsedTimeAtSave;
1032    }
1033
1034    /* our event list */
1035    events_ = static new Vector(20)
1036
1037    /* the event currently being executed */
1038    curEvent_ = nil
1039
1040    /*
1041     *   saved elapsed time - we use this to figure the virtual starting
1042     *   time when we restore a saved game
1043     */
1044    elapsedTimeAtSave = 0
1045;
1046
1047/*
1048 *   Real-time manager: pre-save notification receiver.  When we're about
1049 *   to save the game, we'll note the current elapsed game time, so that
1050 *   when we later restore the game, we can figure the virtual starting
1051 *   point that will give us the same effective elapsed time on the system
1052 *   real-time clock.
1053 */
1054PreSaveObject
1055    execute()
1056    {
1057        /*
1058         *   remember the elapsed time at the point we saved the game, so
1059         *   that we can restore it later
1060         */
1061        realTimeManager.saveElapsedTime();
1062    }
1063;
1064
1065/*
1066 *   Real-time manager: post-restore notification receiver.  Immediately
1067 *   after we restore a game, we'll tell the real-time manager to refigure
1068 *   the virtual starting point of the game based on the saved elapsed
1069 *   time.
1070 */
1071PostRestoreObject
1072    execute()
1073    {
1074        /* figure the new virtual starting time */
1075        realTimeManager.restoreElapsedTime();
1076    }
1077;
1078
1079/*
1080 *   Real-Time Event.  This is an event that occurs according to elapsed
1081 *   wall-clock time in the real world.
1082 */
1083class RealTimeEvent: BasicEvent
1084    /*
1085     *   Get the elapsed real time at which this event is triggered.  This
1086     *   is a time value in terms of realTimeManager.getElapsedTime().
1087     */
1088    getEventTime()
1089    {
1090        /* by default, simply return our eventTime value */
1091        return eventTime;
1092    }
1093
1094    /* construction */
1095    construct(obj, prop)
1096    {
1097        /* inherit default handling */
1098        inherited(obj, prop);
1099
1100        /* add myself to the real-time event manager's active list */
1101        realTimeManager.addEvent(self);
1102    }
1103
1104    /* remove this event from the real-time event manager */
1105    removeEvent() { realTimeManager.removeEvent(self); }
1106
1107    /* our scheduled event time */
1108    eventTime = 0
1109;
1110
1111/*
1112 *   Real-time fuse.  This is an event that fires once at a specified
1113 *   elapsed time into the game.
1114 */
1115class RealTimeFuse: RealTimeEvent
1116    /*
1117     *   Creation.  'delta' is the amount of real time (in milliseconds)
1118     *   that should elapse before the fuse is executed.  If 'delta' is
1119     *   zero or negative, the fuse will be schedulable immediately.
1120     */
1121    construct(obj, prop, delta)
1122    {
1123        /* inherit default handling */
1124        inherited(obj, prop);
1125
1126        /*
1127         *   set my scheduled time to the current game elapsed time plus
1128         *   the delta - this will give us the time in terms of elapsed
1129         *   game time at which we'll be executed
1130         */
1131        eventTime = realTimeManager.getElapsedTime() + delta;
1132    }
1133
1134    /* execute the fuse */
1135    executeEvent()
1136    {
1137        /* call my method */
1138        callMethod();
1139
1140        /* a fuse fires only once, so remove myself from further scheduling */
1141        realTimeManager.removeEvent(self);
1142    }
1143;
1144
1145/*
1146 *   Sensory-context-sensitive real-time fuse.  This is a real-time fuse
1147 *   with an explicit sensory context.
1148 */
1149class RealTimeSenseFuse: RealTimeFuse
1150    construct(obj, prop, delta, source, sense)
1151    {
1152        /* inherit the base constructor */
1153        inherited(obj, prop, delta);
1154
1155        /* remember our sensory context */
1156        source_ = source;
1157        sense_ = sense;
1158    }
1159;
1160
1161/*
1162 *   Real-time daemon.  This is an event that occurs repeatedly at given
1163 *   real-time intervals.  When a daemon is executed, it is scheduled
1164 *   again for execution after its real-time interval elapses again.  The
1165 *   daemon's first execution will occur one interval from the time at
1166 *   which the daemon is created.
1167 *
1168 *   If a daemon is executed late (because other, more pressing tasks had
1169 *   to be completed first, or because the user was busy editing a command
1170 *   line and the local platform doesn't support real-time command
1171 *   interruptions), the interval is applied to the time the daemon
1172 *   actually executed, not to the originally scheduled execution time.
1173 *   For example, if the daemon is scheduled to run once every minute, but
1174 *   can't run at all for five minutes because of command editing on a
1175 *   non-interrupting platform, once it actually does run, it won't run
1176 *   again for (at least) another minute after that.  This means that the
1177 *   daemon will not run five times all at once when it's finally allowed
1178 *   to run - there's no making up for lost time.
1179 */
1180class RealTimeDaemon: RealTimeEvent
1181    /*
1182     *   Creation.  'interval' is the number of milliseconds between
1183     *   invocations.
1184     */
1185    construct(obj, prop, interval)
1186    {
1187        /* inherit the base constructor */
1188        inherited(obj, prop);
1189
1190        /* remember my interval */
1191        interval_ = interval;
1192
1193        /*
1194         *   figure my initial execution time - wait for one complete
1195         *   interval from the current time
1196         */
1197        eventTime = realTimeManager.getElapsedTime() + interval;
1198    }
1199
1200    /* execute the daemon */
1201    executeEvent()
1202    {
1203        /* call my method */
1204        callMethod();
1205
1206        /*
1207         *   Reschedule for next time.  To ensure that we keep to our
1208         *   long-term schedule, reschedule based on our original schedule
1209         *   time rather than the current clock time; that way, if there
1210         *   was a delay after our original scheduled time in firing us,
1211         *   we'll make up for it by shortening the interval until the
1212         *   next firing.  If that would make us already schedulable, then
1213         *   our interval must be so short we can't keep up with it; in
1214         *   that case, add the interval to the current clock time.
1215         */
1216        eventTime += interval_;
1217        if (realTimeManager.getElapsedTime() < eventTime)
1218            eventTime = realTimeManager.getElapsedTime() + interval_;
1219    }
1220
1221    /* my execution interval, in milliseconds */
1222    interval_ = 1
1223;
1224
1225/*
1226 *   Sensory-context-sensitive real-time daemon - this is a real-time
1227 *   daemon with an explicit sensory context.  This is the daemon
1228 *   counterpart of RealTimeSenseFuse.
1229 */
1230class RealTimeSenseDaemon: RealTimeDaemon
1231    construct(obj, prop, interval, source, sense)
1232    {
1233        /* inherit the base constructor */
1234        inherited(obj, prop, interval);
1235
1236        /* remember our sensory context */
1237        source_ = source;
1238        sense_ = sense;
1239    }
1240;
1241
1242