events.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library: events
*
* This module defines the event framework. An event is a programmed
* operation that occurs at a particular point in the game; an event can
* be turn-based, in which case it occurs after a given number of turns
* has elapsed, or it can occur in real time, which means that it occurs
* after a particular interval of time has elapsed.
*/
#include "adv3.h"
#include <dict.h>
#include <gramprod.h>
/* ------------------------------------------------------------------------ */
/*
* Run the main scheduling loop. This continues until we encounter an
* end-of-file error reading from the console, or a QuitException is
* thrown to terminate the game.
*/
runScheduler()
{
/* keep going until we quit the game */
for (;;)
{
/* catch the exceptions that terminate the game */
try
{
/* start with an empty list of schedulable items */
local vec = new Vector(10);
/* find the lowest time at which something is ready to run */
local minTime = nil;
foreach (local cur in Schedulable.allSchedulables)
{
/* get this item's next eligible run time */
local curTime = cur.getNextRunTime();
/*
* if it's not nil, and it's equal to or below the
* lowest we've seen so far, note it
*/
if (curTime != nil && (minTime == nil || curTime <= minTime))
{
/*
* if this is different from the current minimum
* schedulable time, clear out the list of
* schedulables, because the list keeps track of the
* items at the lowest time only
*/
if (minTime != nil && curTime < minTime)
vec.removeRange(1, vec.length());
/* add this item to the list */
vec.append(cur);
/* note the new lowest schedulable time */
minTime = curTime;
}
}
/*
* if nothing's ready to run, the game is over by default,
* since we cannot escape this state - we can't ourselves
* change anything's run time, so if nothing's ready to run
* now, we won't be able to change that, and so nothing will
* ever be ready to run
*/
if (minTime == nil)
{
"\b[Error: nothing is available for scheduling -
terminating]\b";
return;
}
/*
* Advance the global turn counter by the amount of game
* clock time we're consuming now.
*/
libGlobal.totalTurns += minTime - Schedulable.gameClockTime;
/*
* advance the game clock to the minimum run time - nothing
* interesting happens in game time until then, so we can
* skip straight ahead to this time
*/
Schedulable.gameClockTime = minTime;
/* calculate the schedule order for each item */
vec.forEach({x: x.calcScheduleOrder()});
/*
* We have a list of everything schedulable at the current
* game clock time. Sort the list in ascending scheduling
* order, so that the higher priority items come first in
* the list.
*/
vec = vec.sort(
SortAsc, {a, b: a.scheduleOrder - b.scheduleOrder});
/*
* Run through the list and run each item. Keep running
* each item as long as it's ready to run - that is, as long
* as its schedulable time equals the game clock time.
*/
vecLoop:
foreach (local cur in vec)
{
/* run this item for as long as it's ready to run */
while (cur.getNextRunTime() == minTime)
{
try
{
/*
* execute this item - if it doesn't want to be
* called again without considering other
* objects, stop looping and refigure the
* scheduling order from scratch
*/
if (!cur.executeTurn())
break vecLoop;
}
catch (Exception exc)
{
/*
* The scheduled operation threw an exception.
* If the schedulable's next run time didn't get
* updated, then the same schedulable will be
* considered ready to run again immediately on
* the next time through the loop. It's quite
* possible in this case that we'll simply repeat
* the operation that threw the exception and get
* right back here again. If this happens, it
* will effectively starve all of the other
* schedulables. To ensure that other
* schedulables get a chance to run before we try
* this erroneous operation again, advance its
* next run time by one unit if it hasn't already
* been advanced.
*/
if (cur.getNextRunTime() == minTime)
cur.incNextRunTime(1);
/* re-throw the exception */
throw exc;
}
}
}
}
catch (EndOfFileException eofExc)
{
/* end of file reading command input - we're done */
return;
}
catch (QuittingException quitExc)
{
/* explicitly quitting - we're done */
return;
}
catch (RestartSignal rsSig)
{
/*
* Restarting - re-throw the signal for handling in the
* system startup code. Note that we explicitly catch this
* signal, only to rethrow it, because we'd otherwise flag it
* as an unhandled error in the catch-all Exception handler.
*/
throw rsSig;
}
catch (RuntimeError rtErr)
{
/* if this is a debugger error of some kind, re-throw it */
if (rtErr.isDebuggerSignal)
throw rtErr;
/* display the error, but keep going */
"\b[<<rtErr.displayException()>>]\b";
}
catch (TerminateCommandException tce)
{
/*
* Aborted command - ignore it. This is most like to occur
* when a fuse, daemon, or the like tries to terminate itself
* with this exception, thinking it's operating in a normal
* command execution environment. As a convenience, simply
* ignore these exceptions so that any code can use them to
* abort everything and return to the main scheduling loop.
*/
}
catch (ExitSignal es)
{
/* ignore this, just as we ignore TerminateCommandException */
}
catch (ExitActionSignal eas)
{
/* ignore this, just as we ignore TerminateCommandException */
}
catch (Exception exc)
{
/* some other unhandled exception - display it and keep going */
"\b[Unhandled exception: <<exc.displayException()>>]\b";
}
}
}
/* ------------------------------------------------------------------------ */
/*
* An item that can be scheduled for time-based notifications. The main
* scheduler loop in runScheduler() operates on objects of this class.
*
* Note that we build a list of all Schedulable instances during
* pre-initialization. If any Schedulable objects are dynamically
* created, they must be added to the list explicitly after creation in
* order for the event manager to schedule them for execution. The
* default constructor does this automatically, so subclasses can simply
* inherit our constructor to be added to the master list.
*/
class Schedulable: object
/* construction - add myself to the Schedulable list */
construct()
{
/*
* Add myself to the master list of Schedulable instances. Note
* that we must update the list in the Schedulable class itself.
*/
Schedulable.allSchedulables += self;
}
/*
* Get the next time (on the game clock) at which I'm eligible for
* execution. We won't receive any scheduling notifications until
* this time. If this object doesn't want any scheduling
* notifications, return nil.
*/
getNextRunTime() { return nextRunTime; }
/* advance my next run time by the given number of clock units */
incNextRunTime(amt)
{
if (nextRunTime != nil)
nextRunTime += amt;
}
/*
* Notify this object that its scheduled run time has arrived. This
* should perform the scheduled task. If the scheduled task takes
* any game time, the object's internal next run time should be
* updated accordingly.
*
* The scheduler will invoke this method of the same object
* repeatedly for as long as its nextRunTime remains unchanged AND
* this method returns true. If the object's scheduling priority
* changes relative to other schedulable objects, it should return
* nil here to tell the scheduler to recalculate scheduling
* priorities.
*/
executeTurn() { return true; }
/*
* Scheduling order. This determines which item goes first when
* multiple items are schedulable at the same time (i.e., they all
* have the same getNextRunTime() values). The item with the lowest
* number here goes first.
*
* This should never be evaluated except immediately after a call to
* calcScheduleOrder.
*/
scheduleOrder = 100
/*
* Calculate the scheduling order, returning the order value and
* storing it in our property scheduleOrder. This is used to
* calculate and cache the value prior to sorting a list of
* schedulable items. We use this two-step approach (first
* calculate, then sort) so that we avoid repeatedly evaluating a
* complex calculation, if indeed there is a complex calculation to
* perform.
*
* By default, we assume that the schedule order is static, so we
* simply leave our scheduleOrder property unchanged and return its
* present value.
*/
calcScheduleOrder() { return scheduleOrder; }
/* my next running time, in game clock time */
nextRunTime = nil
/*
* A class variable giving the current game clock time. This is a
* class variable because there's only one global game clock. The
* game clock starts at zero and increments in game time units; a
* game time unit is the arbitrary quantum of time for our event
* scheduling system.
*/
gameClockTime = 0
/*
* A list of all of the Schedulable objects in the game. We set this
* up during pre-initialization; if any Schedulable instances are
* created dynamically, they must be explicitly added to this list
* after creation.
*/
allSchedulables = nil
;
/*
* Pre-initializer: build the master list of Schedulable instances
*/
PreinitObject
/*
* Execute preinitialization. Build a list of all of the schedulable
* objects in the game, so that we can scan this list quickly during
* play.
*/
execute()
{
local vec;
/* set up an empty vector to hold the schedulable objects */
vec = new Vector(32);
/* add all of the Schedulable instances to the vector */
forEachInstance(Schedulable, {s: vec.append(s)});
/* save the list of Schedulable instances as an ordinary list */
Schedulable.allSchedulables = vec.toList();
}
;
/* ------------------------------------------------------------------------ */
/*
* Basic Event Manager. This is a common base class for the game-time
* and real-time event managers. This class handles the details of
* managing the event queue; the subclasses must define the specifics of
* event timing.
*/
class BasicEventManager: object
/* add an event */
addEvent(event)
{
/* append the event to our list */
events_.append(event);
}
/* remove an event */
removeEvent(event)
{
/* remove the event from our list */
events_.removeElement(event);
}
/*
* Remove events matching the given object and property combination.
* We remove all events that match both the object and property
* (events matching only the object or only the property are not
* affected).
*
* This is provided mostly as a convenience for cases where an event
* is known to be uniquely identifiable by its object and property
* values; this saves the caller the trouble of keeping track of the
* Event object created when the event was first registered.
*
* When a particular object/property combination might be used in
* several different events, it's better to keep a reference to the
* Event object representing each event, and use removeEvent() to
* remove the specific Event object of interest.
*
* Returns true if we find any matching events, nil if not.
*/
removeMatchingEvents(obj, prop)
{
local found;
/*
* Scan our list, and remove each event matching the parameters.
* Note that it's safe to remove things from a vector that we're
* iterating with foreach(), since foreach() makes a safe copy
* of the vector for the iteration.
*/
found = nil;
foreach (local cur in events_)
{
/* if this one matches, remove it */
if (cur.eventMatches(obj, prop))
{
/* remove the event */
removeEvent(cur);
/* note that we found a match */
found = true;
}
}
/* return our 'found' indication */
return found;
}
/*
* Remove the current event - this is provided for convenience so
* that an event can cancel itself in the course of its execution.
*
* Note that this has no effect on the current event execution -
* this simply prevents the event from receiving additional
* notifications in the future.
*/
removeCurrentEvent()
{
/* remove the currently active event from our list */
removeEvent(curEvent_);
}
/* event list - each instance must initialize this to a vector */
// events_ = nil
;
/*
* Event Manager. This is a schedulable object that keeps track of
* fuses and daemons, and schedules their execution.
*/
eventManager: BasicEventManager, Schedulable
/*
* Use a scheduling order of 1000 to ensure we go after all actors.
* By default, actors use scheduling orders in the range 100 to 400,
* so our order of 1000 ensures that fuses and daemons run after all
* characters on a given turn.
*/
scheduleOrder = 1000
/*
* Get the next run time. We'll find the lowest run time of our
* fuses and daemons and return that.
*/
getNextRunTime()
{
local minTime;
/*
* run through our list of events, and find the event that is
* scheduled to run at the lowest game clock time
*/
minTime = nil;
foreach (local cur in events_)
{
local curTime;
/* get this item's scheduled run time */
curTime = cur.getNextRunTime();
/* if it's not nil and it's the lowest so far, remember it */
if (curTime != nil && (minTime == nil || curTime < minTime))
minTime = curTime;
}
/* return the minimum time we found */
return minTime;
}
/*
* Execute a turn. We'll execute each fuse and each daemon that is
* currently schedulable.
*/
executeTurn()
{
local lst;
/*
* build a list of all of our events with the current game clock
* time - these are the events that are currently schedulable
*/
lst = events_.subset({x: x.getNextRunTime()
== Schedulable.gameClockTime});
/* execute the items in this list */
executeList(lst);
/* no change in scheduling priorities */
return true;
}
/*
* Execute a command prompt turn. We'll execute each
* per-command-prompt daemon.
*/
executePrompt()
{
/* execute all of the per-command-prompt daemons */
executeList(events_.subset({x: x.isPromptDaemon}));
}
/*
* internal service routine - execute the fuses and daemons in the
* given list, in eventOrder priority order
*/
executeList(lst)
{
/* sort the list in ascending event order */
lst = lst.toList()
.sort(SortAsc, {a, b: a.eventOrder - b.eventOrder});
/* run through the list and execute each item ready to run */
foreach (local cur in lst)
{
/* remember our old active event, then establish the new one */
local oldEvent = curEvent_;
curEvent_ = cur;
/* make sure we restore things on the way out */
try
{
local pc;
/* have the player character note the pre-event conditions */
pc = gPlayerChar;
pc.noteConditionsBefore();
/* cancel any sense caching currently in effect */
libGlobal.disableSenseCache();
/* execute the event */
cur.executeEvent();
/*
* if the player character is the same as it was, ask
* the player character to note any change in conditions
*/
if (gPlayerChar == pc)
pc.noteConditionsAfter();
}
catch (Exception exc)
{
/*
* If an event throws an exception out of its handler,
* remove the event from the active list. If we were to
* leave it active, we'd go back and execute the same
* event again the next time we look for something to
* schedule, and that would in turn probably just
* encounter the same exception - so we'd be stuck in an
* infinite loop executing this erroneous code. To
* ensure that we don't get stuck, remove the event.
*/
removeCurrentEvent();
/* re-throw the exception */
throw exc;
}
finally
{
/* restore the enclosing current event */
curEvent_ = oldEvent;
}
}
}
/* our list of fuses and daemons */
events_ = static new Vector(20)
/* the event currently being executed */
curEvent_ = nil
;
/*
* Pseudo-action subclass to represent the action environment while
* processing a daemon, fuse, or other event.
*/
class EventAction: Action
/*
* event actions are internal system actions; they don't consume
* additional turns themselves, since they run between player turns
*/
actionTime = 0;
;
/*
* A basic event, for game-time and real-time events.
*/
class BasicEvent: object
/* construction */
construct(obj, prop)
{
/* remember the object and property to call at execution */
obj_ = obj;
prop_ = prop;
}
/*
* Execute the event. This must be overridden by the subclass to
* perform the appropriate operation when executed. In particular,
* the subclass must reschedule or unschedule the event, as
* appropriate.
*/
executeEvent() { }
/* does this event match the given object/property combination? */
eventMatches(obj, prop) { return obj == obj_ && prop == prop_; }
/*
* Call our underlying method. This is an internal routine intended
* for use by the executeEvent() implementations.
*/
callMethod()
{
/*
* invoke the method in our sensory context, and in a simulated
* action environment
*/
withActionEnv(EventAction, gPlayerChar,
{: callWithSenseContext(source_, sense_,
{: obj_.(self.prop_)() }) });
}
/* the object and property we invoke */
obj_ = nil
prop_ = nil
/*
* The sensory context of the event. When the event fires, we'll
* execute its method in this sensory context, so that any messages
* generated will be displayed only if the player character can
* sense the source object in the given sense.
*
* By default, these are nil, which means that the event's messages
* will be displayed (or, at least, they won't be suppressed because
* of the sensory context).
*/
source_ = nil
sense_ = nil
;
/*
* Base class for fuses and daemons
*/
class Event: BasicEvent
/* our next run time, in game clock time */
getNextRunTime() { return nextRunTime; }
/* delay our scheduled run time by the given number of turns */
delayEvent(turns) { nextRunTime += turns; }
/* remove this event from the event manager */
removeEvent() { eventManager.removeEvent(self); }
/*
* Event order - this establishes the order we run relative to other
* events scheduled to run at the same game clock time. Lowest
* number goes first. By default, we provide an event order of 100,
* which should leave plenty of room for custom events before and
* after default events.
*/
eventOrder = 100
/* creation */
construct(obj, prop)
{
/* inherit default handling */
inherited(obj, prop);
/* add myself to the event manager's active event list */
eventManager.addEvent(self);
}
/*
* our next execution time, expressed in game clock time; by
* default, we'll set this to nil, which means that we are not
* scheduled to execute at all
*/
nextRunTime = nil
/* by default, we're not a per-command-prompt daemon */
isPromptDaemon = nil
;
/*
* Fuse. A fuse is an event that fires once at a given time in the
* future. Once a fuse is executed, it is removed from further
* scheduling.
*/
class Fuse: Event
/*
* Creation. 'turns' is the number of turns in the future at which
* the fuse is executed; if turns is 0, the fuse will be executed on
* the current turn.
*/
construct(obj, prop, turns)
{
/* inherit the base class constructor */
inherited(obj, prop);
/*
* set my scheduled time to the current game clock time plus the
* number of turns into the future
*/
nextRunTime = Schedulable.gameClockTime + turns;
}
/* execute the fuse */
executeEvent()
{
/* call my method */
callMethod();
/* a fuse fires only once, so remove myself from further scheduling */
eventManager.removeEvent(self);
}
;
/*
* Sensory-context-sensitive fuse - this is a fuse with an explicit
* sensory context. We'll run the fuse in its sense context, so any
* messages generated will be visible only if the given source object is
* reachable by the player character in the given sense.
*
* Conceptually, the source object is considered the source of any
* messages that the fuse generates, and the messages pertain to the
* given sense; so if the player character cannot sense the source
* object in the given sense, the messages should not be displayed. For
* example, if the fuse will describe the noise made by an alarm clock
* when the alarm goes off, the source object would be the alarm clock
* and the sense would be sound; this way, if the player character isn't
* in hearing range of the alarm clock when the alarm goes off, we won't
* display messages about the alarm noise.
*/
class SenseFuse: Fuse
construct(obj, prop, turns, source, sense)
{
/* inherit the base constructor */
inherited(obj, prop, turns);
/* remember our sensory context */
source_ = source;
sense_ = sense;
}
;
/*
* Daemon. A daemon is an event that fires repeatedly at given
* intervals. When a daemon is executed, it is scheduled again for
* execution after its interval elapses again.
*/
class Daemon: Event
/*
* Creation. 'interval' is the number of turns between invocations
* of the daemon; this should be at least 1, which causes the daemon
* to be invoked on each turn. The first execution will be
* (interval-1) turns in the future - so if interval is 1, the
* daemon will first be executed on the current turn, and if
* interval is 2, the daemon will be executed on the next turn.
*/
construct(obj, prop, interval)
{
/* inherit the base class constructor */
inherited(obj, prop);
/*
* an interval of less than 1 is meaningless, so make sure it's
* at least 1
*/
if (interval < 1)
interval = 1;
/* remember my interval */
interval_ = interval;
/*
* set my initial execution time, in game clock time - add one
* less than the interval to the current game clock time, so
* that we count the current turn as yet to elapse for the
* purposes of the interval before the daemon's first execution
*/
nextRunTime = Schedulable.gameClockTime + interval - 1;
}
/* execute the daemon */
executeEvent()
{
/* call my method */
callMethod();
/* advance our next run time by our interval */
nextRunTime += interval_;
}
/* our execution interval, in turns */
interval_ = 1
;
/*
* Sensory-context-sensitive daemon - this is a daemon with an explicit
* sensory context. This is the daemon counterpart of SenseFuse.
*/
class SenseDaemon: Daemon
construct(obj, prop, interval, source, sense)
{
/* inherit the base constructor */
inherited(obj, prop, interval);
/* remember our sensory context */
source_ = source;
sense_ = sense;
}
;
/*
* Command Prompt Daemon. This is a special type of daemon that
* executes not according to the game clock, but rather once per command
* prompt. The system executes all of these daemons just before each
* time it prompts for a command line.
*/
class PromptDaemon: Event
/* execute the daemon */
executeEvent()
{
/*
* call my method - there's nothing else to do for this type of
* daemon, since our scheduling is not affected by the game
* clock
*/
callMethod();
}
/* flag: we are a special per-command-prompt daemon */
isPromptDaemon = true
;
/*
* A one-time-only prompt daemon is a regular command prompt daemon,
* except that it fires only once. After it fires once, the daemon
* automatically deactivates itself, so that it won't fire again.
*
* Prompt daemons are occasionally useful for non-recurring processing,
* when you want to defer some bit of code until a "safe" time between
* turns. In these cases, the regular PromptDaemon is inconvenient to
* use because it automatically recurs. This subclass is handy for these
* cases, since it lets you schedule some bit of processing for a single
* deferred execution.
*
* One special situation where one-time prompt daemons can be handy is in
* triggering conversational events - such as initiating a conversation -
* at the very beginning of the game. Initiating a conversation can only
* be done from within an action context, but no action context is in
* effect during the game's initialization. An easy way to deal with
* this is to create a one-time prompt daemon during initialization, and
* then trigger the event from the daemon's callback method. The prompt
* daemon will set up a daemon action environment just before the first
* command prompt is displayed, at which point the callback will be able
* to trigger the event as though it were in ordinary action handler
* code.
*/
class OneTimePromptDaemon: PromptDaemon
executeEvent()
{
/* execute as normal */
inherited();
/* remove myself from the event list, so that I don't fire again */
removeEvent();
}
;
/* ------------------------------------------------------------------------ */
/*
* Real-Time Event Manager. This object manages all of the game's
* real-time events, which are events that occur according to elapsed
* real-world time.
*/
realTimeManager: BasicEventManager, InitObject
/*
* Get the elapsed game time at which the next real-time event is
* scheduled. This returns a value which can be compared to that
* returned by getElapsedTime(): if this value is less than or equal
* to the value from getElapsedTime(), then the next event is reay
* for immediate execution; otherwise, the result of subtracting
* getElapsedTime() from our return value gives the number of
* milliseconds until the next event is schedulable.
*
* Note that we don't calculate the delta to the next event time,
* but instead return the absolute time, because the caller might
* need to perform extra processing before using our return value.
* If we returned a delta, that extra processing time wouldn't be
* figured into the caller's determination of event schedulability.
*
* If we return nil, it means that there are no scheduled real-time
* events.
*/
getNextEventTime()
{
local tMin;
/*
* run through our event list and find the event with the lowest
* scheduled run time
*/
tMin = nil;
foreach (local cur in events_)
{
local tCur;
/* get the current item's time */
tCur = cur.getEventTime();
/*
* if this one has a valid time, and we don't have a valid
* time yet or this one is sooner than the soonest one we've
* seen so far, note this one as the soonest so far
*/
if (tMin == nil
|| (tCur != nil && tCur < tMin))
{
/* this is the soonest so far */
tMin = tCur;
}
}
/* return the soonest event so far */
return tMin;
}
/*
* Run any real-time events that are ready to execute, then return
* the next event time. The return value has the same meaning as
* that of getNextEventTime().
*/
executeEvents()
{
local tMin;
/*
* Keep checking as long as we find anything to execute. Each
* time we execute an event, we might consume enough time that
* an item earlier in our queue that we originally dismissed as
* unready has become ready to run.
*/
for (;;)
{
local foundEvent;
/* we haven't yet run anything on this pass */
foundEvent = nil;
/* we haven't found anything schedulable on this pass yet */
tMin = nil;
/* run each event whose time is already here */
foreach (local cur in events_)
{
local tCur;
/*
* If this event has a non-nil time, and its time is
* less than or equal to the current system clock time,
* run this event. All event times are in terms of the
* game elapsed time.
*
* If this event isn't schedulable, at least check to
* see if it's the soonest schedulable event so far.
*/
tCur = cur.getEventTime();
if (tCur != nil && tCur <= getElapsedTime())
{
/* cancel any sense caching currently in effect */
libGlobal.disableSenseCache();
/* execute this event */
cur.executeEvent();
/* note that we executed something */
foundEvent = true;
}
else if (tMin == nil
|| (tCur != nil && tCur < tMin))
{
/* it's the soonest event so far */
tMin = tCur;
}
}
/* if we didn't execute anything on this pass, stop scanning */
if (!foundEvent)
break;
}
/* return the time of the next event */
return tMin;
}
/*
* Get the current game elapsed time. This is the number of
* milliseconds that has elapsed since the game was started,
* counting only the continuous execution time. When the game is
* saved, we save the elapsed time at that point; when the game is
* later restored, we project that saved time backwards from the
* current real-world time at restoration to get the real-world time
* where the game would have started if it had actually been played
* continuously in one session.
*/
getElapsedTime()
{
/*
* return the current system real-time counter minus the virtual
* starting time
*/
return getTime(GetTimeTicks) - startingTime;
}
/*
* Set the current game elapsed time. This can be used to freeze
* the real-time clock - a caller can note the elapsed game time at
* one point by calling getElapsedTime(), and then pass the same
* value to this routine to ensure that no real time can effectively
* pass between the two calls.
*/
setElapsedTime(t)
{
/*
* set the virtual starting time to the current system real-time
* counter minus the given game elapsed time
*/
startingTime = getTime(GetTimeTicks) - t;
}
/*
* The imaginary real-world time of the starting point of the game,
* treating the game as having been played from the start in one
* continous session. Whenever we restore a saved game, we project
* backwards from the current real-world time at restoration by the
* amount of continuous elapsed time in the saved game to find the
* point at which the game would have started if it had been played
* continuously in one session up to the restored point.
*
* We set a static initial value for this, using the interpreter's
* real-time clock value at compilation time. This ensures that
* we'll have a meaningful time base if any real-time events are
* created during pre-initialization. This static value will only be
* in effect during preinit; we're an InitObject, so our execute()
* method will be invoked at run-time start-up, and at that point
* we'll reset the zero point to the actual run-time start time.
*/
startingTime = static getTime(GetTimeTicks)
/*
* Initialize at run-time startup. We want to set the zero point as
* the time when the player actually started playing the game (any
* time we spent in pre-initialization doesn't count on the real-time
* clock, since it's not part of the game per se).
*/
execute()
{
/*
* note the real-time starting point of the game, so we can
* calculate the elapsed game time later
*/
startingTime = getTime(GetTimeTicks);
}
/*
* save the elapsed time so far - this is called just before we save
* a game so that we can pick up where we left off on the elapsed
* time clock when we restore the saved game
*/
saveElapsedTime()
{
/* remember the elapsed time so far */
elapsedTimeAtSave = getElapsedTime();
}
/*
* Restore the elapsed time - this is called just after we restore a
* game. We'll project the saved elapsed time backwards to figure
* the imaginary starting time the game would have had if it had
* been played in one continuous session rather than being saved and
* restored.
*/
restoreElapsedTime()
{
/*
* project backwards from the current time by the saved elapsed
* time to get the virtual starting point that will give us the
* same current elapsed time on the system real-time clock
*/
startingTime = getTime(GetTimeTicks) - elapsedTimeAtSave;
}
/* our event list */
events_ = static new Vector(20)
/* the event currently being executed */
curEvent_ = nil
/*
* saved elapsed time - we use this to figure the virtual starting
* time when we restore a saved game
*/
elapsedTimeAtSave = 0
;
/*
* Real-time manager: pre-save notification receiver. When we're about
* to save the game, we'll note the current elapsed game time, so that
* when we later restore the game, we can figure the virtual starting
* point that will give us the same effective elapsed time on the system
* real-time clock.
*/
PreSaveObject
execute()
{
/*
* remember the elapsed time at the point we saved the game, so
* that we can restore it later
*/
realTimeManager.saveElapsedTime();
}
;
/*
* Real-time manager: post-restore notification receiver. Immediately
* after we restore a game, we'll tell the real-time manager to refigure
* the virtual starting point of the game based on the saved elapsed
* time.
*/
PostRestoreObject
execute()
{
/* figure the new virtual starting time */
realTimeManager.restoreElapsedTime();
}
;
/*
* Real-Time Event. This is an event that occurs according to elapsed
* wall-clock time in the real world.
*/
class RealTimeEvent: BasicEvent
/*
* Get the elapsed real time at which this event is triggered. This
* is a time value in terms of realTimeManager.getElapsedTime().
*/
getEventTime()
{
/* by default, simply return our eventTime value */
return eventTime;
}
/* construction */
construct(obj, prop)
{
/* inherit default handling */
inherited(obj, prop);
/* add myself to the real-time event manager's active list */
realTimeManager.addEvent(self);
}
/* remove this event from the real-time event manager */
removeEvent() { realTimeManager.removeEvent(self); }
/* our scheduled event time */
eventTime = 0
;
/*
* Real-time fuse. This is an event that fires once at a specified
* elapsed time into the game.
*/
class RealTimeFuse: RealTimeEvent
/*
* Creation. 'delta' is the amount of real time (in milliseconds)
* that should elapse before the fuse is executed. If 'delta' is
* zero or negative, the fuse will be schedulable immediately.
*/
construct(obj, prop, delta)
{
/* inherit default handling */
inherited(obj, prop);
/*
* set my scheduled time to the current game elapsed time plus
* the delta - this will give us the time in terms of elapsed
* game time at which we'll be executed
*/
eventTime = realTimeManager.getElapsedTime() + delta;
}
/* execute the fuse */
executeEvent()
{
/* call my method */
callMethod();
/* a fuse fires only once, so remove myself from further scheduling */
realTimeManager.removeEvent(self);
}
;
/*
* Sensory-context-sensitive real-time fuse. This is a real-time fuse
* with an explicit sensory context.
*/
class RealTimeSenseFuse: RealTimeFuse
construct(obj, prop, delta, source, sense)
{
/* inherit the base constructor */
inherited(obj, prop, delta);
/* remember our sensory context */
source_ = source;
sense_ = sense;
}
;
/*
* Real-time daemon. This is an event that occurs repeatedly at given
* real-time intervals. When a daemon is executed, it is scheduled
* again for execution after its real-time interval elapses again. The
* daemon's first execution will occur one interval from the time at
* which the daemon is created.
*
* If a daemon is executed late (because other, more pressing tasks had
* to be completed first, or because the user was busy editing a command
* line and the local platform doesn't support real-time command
* interruptions), the interval is applied to the time the daemon
* actually executed, not to the originally scheduled execution time.
* For example, if the daemon is scheduled to run once every minute, but
* can't run at all for five minutes because of command editing on a
* non-interrupting platform, once it actually does run, it won't run
* again for (at least) another minute after that. This means that the
* daemon will not run five times all at once when it's finally allowed
* to run - there's no making up for lost time.
*/
class RealTimeDaemon: RealTimeEvent
/*
* Creation. 'interval' is the number of milliseconds between
* invocations.
*/
construct(obj, prop, interval)
{
/* inherit the base constructor */
inherited(obj, prop);
/* remember my interval */
interval_ = interval;
/*
* figure my initial execution time - wait for one complete
* interval from the current time
*/
eventTime = realTimeManager.getElapsedTime() + interval;
}
/* execute the daemon */
executeEvent()
{
/* call my method */
callMethod();
/*
* Reschedule for next time. To ensure that we keep to our
* long-term schedule, reschedule based on our original schedule
* time rather than the current clock time; that way, if there
* was a delay after our original scheduled time in firing us,
* we'll make up for it by shortening the interval until the
* next firing. If that would make us already schedulable, then
* our interval must be so short we can't keep up with it; in
* that case, add the interval to the current clock time.
*/
eventTime += interval_;
if (realTimeManager.getElapsedTime() < eventTime)
eventTime = realTimeManager.getElapsedTime() + interval_;
}
/* my execution interval, in milliseconds */
interval_ = 1
;
/*
* Sensory-context-sensitive real-time daemon - this is a real-time
* daemon with an explicit sensory context. This is the daemon
* counterpart of RealTimeSenseFuse.
*/
class RealTimeSenseDaemon: RealTimeDaemon
construct(obj, prop, interval, source, sense)
{
/* inherit the base constructor */
inherited(obj, prop, interval);
/* remember our sensory context */
source_ = source;
sense_ = sense;
}
;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3