/*
daemon.t version 1.00 (7 May 2001)
Daemon priority sorting for TADS 2
Dan Schmidt <
[email protected]>
TADS 2 does not allow authors to specify exactly when daemons should run
relative to each other. The ill effects of this can range from
infelicitous (you lose control over the order in which some messages are
printed) to quite annoying (you don't know for sure when the turncount()
daemon will run, incrementing global.turnsofar). daemon.t allows you to
specify a priority for each daemon, so that you can set exactly the
order in which they should run.
To use it, simply start up the timeline object at the beginning of time
by inserting the following line into init():
notify (timeline, &run, 0);
and replace all your calls to notify, unnotify, set/remdaemon, and
set/remfuse with the following calls:
- add_daemon (obj, func [, priority]) starts a daemon that will execute
at the end of every turn, starting this one. If you want the daemon
to be a global function, rather than the method of an object, set obj
to nil. The priority argument is optional and defaults to
timeline.default_priority (1000 by default) if not given.
- add_fuse (obj, func, timeout [, priority]) starts a fuse; the specified
function or method will be called timeout turns from now. As with
add_daemon, the priority argument is optional, and obj may be nil to
specify a global function.
- rem_daemon (obj, func) removes the specified daemon.
- rem_fuse (obj, func) removes the specified fuse.
If any of these functions are called from within a daemon/fuse, they
will not take effect until the following turn.
The larger the priority number of a daemon or fuse, the later it runs.
This may seem kind of unintuitive, but think of it in the 'take a number
at the deli' sense; low numbers are served first.
Daemons and fuses share the same priority queue, so a daemon with
priority 10 executes before a fuse with priority 20 which executes
before a daemon with priority 30. Daemons/fuses with the same priority
happen to execute most-recently-added first, but don't count on that.
TADS' setdaemon and setfuse builtins take an extra parameter, which is
passed to the daemon or fuse function. daemon.t's add_daemon and
add_fuse functions support this behavior as well; if the func
parameter is a two-element list consisting of a symbol and a
parameter, rather than just a symbol, then the first element of the
list is taken to be the function to be called, and the second element
is taken to be the parameter to pass to the function.
If you use the two-element func option for add_daemon or add_fuse, you
must also use it when removing, as with setdaemon/setfuse.
Some examples:
add_daemon (elephant, &stomp_around);
causes elephant.stomp_around to be called every turn.
add_daemon (elephant, &stomp_around, 100);
has the same behavior, but elephant.stomp_around now executes with a
priority of 100, rather than the default.
rem_daemon (elephant, &stomp_around);
stops elephant.stomp_around from being called any more.
add_daemon (elephant. [&trumpet 3]);
causes elephant.trumpet(3) to be called every turn.
add_fuse (elephant, &go_berserk, 5, 50);
causes elephant.go_berserk to be called 5 turns from now. It will
execute with a priority of 50 (so, for example, it would execute
before the stomp_around daemon).
daemon.t will happily coexist with 'real' daemons, fuses, and
notifies, but you won't have control over when the other ones execute.
*/
#pragma C+
// A sysdaemon is a method to be called on an object every turn.
// They are kept as an explicit linked list since sorting actual
// TADS lists is a pain in the neck.
class sysdaemon: object
pri = 0 // priority: 0 max, 32767 min
obj = nil // object to have method called (or nil for function)
func = nil // method/function to call
timeout = 0 // when to call, or -1 for every turn
next = nil // next daemon on priority-ordered list
// Initialize
ini (o, f, t, p) = {
self.obj = o;
self.func = f;
self.timeout = t;
self.pri = p;
}
;
null_daemon: sysdaemon pri=32767; // End of list sentinel
head_daemon: sysdaemon pri=-1 next=null_daemon; // Beginning of list marker
timeline: object
head = head_daemon // head of our linked list of sysdaemons
default_priority = 1000 // priority if not specified
// If we're traversing the daemon list, we set self.running to true
// and save off any pending add_daemons and rem_daemons.
running = nil
pending_adds = []
pending_rems = []
add (o, f, t, p) = {
if (p <= head_daemon.pri || p >= null_daemon.pri) {
"[BUG] priority out of range while adding daemon or fuse\n";
return;
}
if (self.running) {
self.pending_adds += [[o, f, t, p]]; // save it off
} else {
local d = self.head;
local newd = new sysdaemon; // create a new sysdaemon representing this call
if (t == 0) t = -1; // to us, -1 means permanent
newd.ini (o, f, t, p);
// Insert into the sorted list
while (1) {
if (newd.pri <= d.next.pri) {
newd.next = d.next; // splice it in
d.next = newd;
break;
}
d = d.next; // move down the list
}
}
}
rem (o, f) = {
if (self.running) {
self.pending_rems += [[o, f]]; // save it off
} else {
// Delete from the sorted list
local flist = (datatype(f) == DTY_LIST); // f is list?
local d = self.head;
while (1) {
if (d.next == null_daemon) {
"[BUG] tried to remove nonexistent daemon\n";
return;
}
// '==' doesn't work for lists, thus the following annoyance
if (d.next.obj == o && flist ? (d.next.func[1] == f[1] && d.next.func[2] == f[2])
: (d.next.func == f)) {
local destroyed = d.next; // save a handle to the upcoming guy
d.next = d.next.next; // splice it out
delete destroyed; // and kill it
break;
}
d = d.next; // move down the list
}
}
}
run = {
// Traverse the sorted list, calling each daemon
local prevd = self.head; // previous daemon called
local d = prevd.next; // daemon to call
self.running = true; // mark ourselves as traversing the list
while (d != null_daemon) {
local nextd = d.next; // daemon to call next round
if (d.timeout <= 0) { // fuse expired, or it's permanent
if (d.obj) {
if (proptype(d, &func) == DTY_LIST) {
(d.obj).(d.func[1])(d.func[2]); // method call w/ arg
} else {
(d.obj).(d.func); // method call
}
} else {
if (proptype(d, &func) == DTY_LIST) {
(d.func[1])(d.func[2]); // function call w/ arg
} else {
(d.func)(); // function call
}
}
}
if (d.timeout == 0) { // fuse expired
prevd.next = d.next; // splice it out
delete d; // and kill it
} else if (d.timeout > 0) {
--d.timeout; // fuse runs down
}
prevd = d; // move down the list
d = nextd;
}
self.running = nil; // done with the list
// Now handle any adds and rems that took place while we were traversing.
{
local i;
for (i = 1; i <= length(self.pending_adds); ++i) {
local p = self.pending_adds[i];
self.add (p[1], p[2], p[3], p[4]);
}
for (i = 1; i <= length(self.pending_rems); ++i) {
local p = self.pending_rems[i];
self.rem (p[1], p[2]);
}
self.pending_adds = [];
self.pending_rems = [];
}
}
;
////////////////////////////////////////////////////////////
//
// Now follows the global function interface
add_daemon: function (obj, func, ...)
{
local pri = timeline.default_priority;
if (argcount >= 3) {
pri = getarg(3);
}
timeline.add (obj, func, 0, pri);
}
add_fuse: function (obj, func, t, ...)
{
local pri = timeline.default_priority;
if (argcount >= 4) {
pri = getarg(4);
}
timeline.add (obj, func, t, pri);
}
rem_daemon: function (obj, func)
{
timeline.rem (obj, func);
}
rem_fuse: function (obj, func)
{
timeline.rem (obj, func);
}