/* daemon.t version 1.00 (7 May 2001) Daemon priority sorting for TADS 2 Dan Schmidt 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); }