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