Help with dynamic time control, scheduling and calendars?

Hey im in need of some help with controlling time in Inform 7.

In my current project i have multiple choices that make the time advance more than 1 minute.

This creates a problem, as some crucial events are scheduled to start after a certain time. If the player commits to an action that takes more than 1 minute, and the time passes the scheduled time for the event, then the event doesn’t trigger? Is there any way to say that ‘this’ event triggers between these two hours? For an example, between 2PM and 4PM?

Also is there any way to make a calendar system in Inform 7? to include days and months? I ran into the same issue as before as the code didn’t change the day unless the time specifically hit 12PM…

Please help!

Thanks!

There’s an example of the time switching to days of the week in Inform 7. (See “Example: The Hang of Thursdays”). You could expand on that to have it toggle a “week” counter and when the 4th week is up, simply toggle to the next month.

Example: The Hang of Thursdays:

The Stage is a room. Rule for printing the name of the stage: say "[current weekday] [current time period]" instead. 

A weekday is a kind of value. The weekdays are Saturday, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday. The current weekday is a weekday that varies. The current weekday is Saturday. 

A time period is a kind of value. The time periods are morning, afternoon, evening, night. The current time period is a time period that varies. The current time period is afternoon. 

This is the new advance time rule: 
    if the current time period is less than night: 
        now the current time period is the time period after the current time period; 
    otherwise: 
        now the current time period is morning; 
        now the current weekday is the weekday after the current weekday. 

The new advance time rule is listed instead of the advance time rule in the turn sequence rules. 

Test me with "z / z / z / z / z". 

You’d have to add to that with some stuff like:

A month is a kind of value. The months are January, February, March, April, May, June, July, August, September, October, November, December. The current month is January.

…then modify the original code even further with a “current week” toggle and once the fourth week is up, have the month increment or loop back to January again. At the point where you have this all coded in your game you could set the current day of the week and month of the year manually with:

Now the current month is August. Now the current weekday is Monday.

I wouldn’t get too complicated like this though. Wouldn’t it be simpler to note to the player in some other way what the current day of the week is? Like off a newspaper in a newspaper stand? Or mention it at the start of a new scene? Unless you’re going for a MMORPG type game world I don’t see the need for it and even still, most MMORPGs that I know of use a simple 24 hour clock and disregard the day of the week and current month (current month being reflective based on game patches - for example “Frostfell” happens each real world December in EverQuest but only after a patch).

I did write up something for handling clocks in Tads 3. It would have to be heavily converted over to Inform 7 to get it to work there. Here’s my gameclok.t file:

#ifndef ADV3_H
#include <adv3.h>
#endif

// checks for if a variable was NOT defined from file en_us.h
// .. and if so then include the file to define that variable, method, etc.
#ifndef singleDir
#include <en_us.h>
// //#error "You can put an error here too if the variable <> something. "
#endif

    /*
     * Our gameClockAgent needs a reference pointer.
     * We could create it 'nil' like so:
     *    gameClockAgent.construct(self,nil,15000); 
     * .... but without a pointer to this object it will
     * not get registered within the global "eventManager"
     * object, in the "events_" list. 
     * So... we must create a method someplace. We'll
     * create our method and start up the agent like
     * so: 
     *   gameClockAgent.construct(self,&myDaemon,15000); 
     * Our workaround is to create an actual Thing object
     * that has some start/stop routines, and a daemon routine
     * which acts as our timer counter. 
     * 
     * To start this agent you'd do this at start-up somewhere:
     *     gameClockAgentManager.startMyDaemon();//
     *
     * Then later on to query and display the time just do this:
     *     say(gameClockAgentManager.getTimeHourMin());
     *
     */

/*
 * Note: only create one of these CromexxGameClockManager objects per game.
 * All of our CromexxClock class objects will query the global realTimeManager 
 * to find ONE of this kind of object. If you have multiples running you're
 * possibly not going to get the correct game world time.
 * 
 * Also note that if you use CromexxAlarmClock objects you need to have them
 * defined after this object as they need to refer to this object to register
 * themselves for hourly chiming.
 */
class CromexxGameClockManager: InitObject
   isInitialized = 0 // a cromexx counter we made up
   isActive = true // if nil then the daemon exists (if exists) just won't run
   myDaemonRunning = nil 
   worldHour = 19
   worldMinute = 45
   worldDay = 1   // 1st of the month - 31st usually
   worldMonth = 1  // 1-12
   worldYear = 2307 
   daemonID = nil
   alarmClockList = [] // a list of alarm clocks that would like hourly notification
   monthShortNames = ['JAN','FEB','MAR',
                      'APR','MAY','JUN', 
                      'JUL','AUG','SEP',
                      'OCT','NOV','DEC']
   monthNames = ['January','February','March',
                 'April','May','June',
                 'July','August','September',
                 'October','November','December']
   monthDays = [31,28,31,
                30,31,30,
                31,31,30,
                31,30,31]
   /* Part of the InitObject routine
    * This fires off automatically when the game starts.
    * Note that no text will ever be displayed within this
    * method as InitObjects are usually created before the
    * parser is even ready for displaying text yet.
    */  
   execute() { 
              isInitialized++; 
              startMyDaemon();
              "*** This text should not show. *** __CromexxGameClockManager class. gameclok.t";
   }
   registerAlarmClock(oclock){
         if(oclock == nil) exit;
         if(!oclock.ofKind(CromexxAlarmClock)) exit;
         if(alarmClockList== nil) exit;         
         if(alarmClockList.indexOf(oclock) != nil) exit;
         alarmClockList+= oclock;
   }
   unRegisterAlarmClock(oclock){
        if(oclock == nil) exit;
        if(!oclock.ofKind(CromexxAlarmClock)) exit;
        if(alarmClockList== nil) exit;         
        if(alarmClockList.indexOf(oclock) != nil) alarmClockList-= oclock; 
   }
   startMyDaemon(){
       if(self.myDaemonRunning != true){
            /* start daemon */ 
            daemonID = new RealTimeDaemon(self,&myGameClockDaemon,15000);// there are other ways to initialize this but this calls self & a method in self
            /* set that the daemon is running (so we can keep easier track) */
            self.myDaemonRunning = true;
       }
       /* make sure we are active */
       self.isActive = true;
   }
   stopMyDaemon(){
         // **** BEST WAY *** if you set a "daemonID" tracking var to this object
         if(daemonID != nil) daemonID.removeEvent;
         daemonID = nil;
         // **** ANOTHER WAY ****
         // eventManager.removeMatchingEvents(obj, &prop);
         // **** ANOTHER WAY ****
          /* (eventManager.events_; Schedulable.allSchedulables;) */
         // foreach(local obj in eventManager.events_){
         //       // if our obj_ is this "someObj" object, then remove the event/agent/daemon
         //       if(obj.obj_ == self){
         //            obj.removeEvent();// actually removes the event/agent/daemon
         //            self.myDaemonRunning = nil;
         //            /* "isActive = nil" is optional. We picked up this flag idea from ditch day drifter
         //             *  This variable is used only in our "someObj" object, not part of 
         //             *  the Event class (as far as I know), and is used as an emergency backup
         //             *  check in case the agent/daemon is still running or fires off one last time.
         //             *  (i.e. a "server hiccup" type prevention)
         //             */ 
         //           self.isActive = nil;
         //       }
         // }
   }  
   isNight {
      if(worldHour > 18) return true;
      if(worldHour < 7) return true;
      return nil;
   }
   /* **** Display a default Day or Night message */
   displayDayNightMsg(){
      if(isNight == true){
           "It is night time. "; 
      }else{
           "It\'s day time. ";
      }
   }    
   // returns in the format of: 'January 1, 2037' 
   getDateSimple(){
        local s0 = monthNames[worldMonth] + ' ' + toString(worldDay) + ', ' + toString(worldYear);
        return s0;        
   }
   getTimeHourMin(){
     local iHour = worldHour;
     local ampm = 'AM';
     if(iHour >= 13){
         ampm = 'PM';
         iHour = iHour - 12;
     }
     local iMinute = worldMinute;
     if(iMinute < 10){
        iMinute = '0'+toString(iMinute);
     }
     local s0 = toString(iHour) + ':' + iMinute + ' ' + ampm;
     return s0;     
   }
   getTimeHourMinSec {
     local iHour = worldHour;
     local ampm = 'AM';
     if(iHour >= 13){
         ampm = 'PM';
         iHour = iHour - 12;
     }
     local iMinute = worldMinute;
     if(iMinute < 10){ 
         iMinute = '0'+toString(iMinute);
     }
     local s0 = toString(iHour) + ':' + iMinute + ':00 ' + ampm;
     return s0;     
   } 
   // 15000 = 15 sec real time. (so 72 min. real time = 1 day virtual time; & 20 min virtual time = 1 min real time)
   // note: do NOT call it as a RealTimeEvent!!! Even though it is one (call it as a RealTimeDaemon or else your program could lock up and crash!)
   myGameClockDaemon(){
          // if the daemon is not active (even if running) we exit anyway
          if(self.isActive != true){
              exit();
          }
          // *** do stuff ***
          worldMinute += 5;// add 5 minutes to the game clock
          if(worldMinute >= 60){
               worldMinute = 0;
               worldHour++;   
               if(alarmClockList != nil){  
                  if(alarmClockList.length != 0){
                     foreach(local oclock in alarmClockList){
                          oclock.hourlyNotificationScript(worldHour);
                     }
                  }
               }
               if(worldHour >= 24){
                  worldHour = 1;
                  worldDay++;
                  if(worldDay > monthDays[worldMonth]){
                       worldDay = 1;
                       worldMonth++;
                       if(worldMonth > 12){
                             worldYear++; // *** happy new year!           
                             worldMonth = 1;                  
                       }
                  }
               }
          }
   }// end myGameClockDaemon()...
;


/*
 *  Our simple gameClockAgentManager... starts up automatically on start.
 *  You do not need to start this object's daemon as it's of the InitObject
 *  class and we start the daemon automatically through that.
 */
gameClockAgentManager: CromexxGameClockManager;

// *** start it somewhere else... 
// gameClockAgent.construct(self,nil,15000);    // 300000 milliseconds = 300 seconds;  (self,nil,tempC); 
// *** OR.. within an object that starts it...
// gameClockAgentManager.startMyDaemon();

/* Now we need the CromexxClock class to use in the game world to display the time
 *
 * Instead of hard coding a link to the actual instanced object, we'll
 * query the realTimeManager and see if we can find an event that
 * matches the CromexxGameClockManager class, and if so we'll store that
 * value in our private "oCromexxClockManager_" variable. Then we call 
 * that in our Read action and do a "getTimeHourMin()" check, and display
 * that back to the player. We display that time after the normal readDesc.
 *
 * Note that we'll want to keep 'timepiece' in our vocabWords list so that
 * our "Time" verb (in crom_wrd.t) can match up a generic "get time" command
 * and search for the nearest logical timepiece.
 */
class CromexxClock: Readable, Thing  
  vocabWords = 'timepiece'
  // this is an internal variable, do not set it directly
  oCromexxClockManager_ = nil
  dobjFor(Examine){
       preCond=[objVisible]
       verify(){ inherited; }
       check(){ inherited; }
       action(){ inherited; 
          // if player, do a "read" action too. Note that "nested" will not count as another turn
          if(gActor==gPlayerChar) nestedActorAction(gActor,Read,self);
       }
  }
  /*
   * Display game world time after doing a normal readDesc
   */
  dobjFor(Read){
       preCond=[objVisible]
       verify(){ inherited; }
       check(){            
            oCromexxClockManager_ = nil;
            // make sure there is a global CromexxGameClockManager to query 
            // note that we do not use the vector in "eventManager.events_" to
            // query this. We use "realTimeManager.events_" ...
            if(realTimeManager.events_ != nil){         
                foreach(local cur in realTimeManager.events_){
                   if((cur.obj_.ofKind(CromexxGameClockManager)) &&
                      (cur.prop_ == &myGameClockDaemon)){
                         oCromexxClockManager_ = cur.obj_;
                   }
                }
            }
            local ooo = self;
            gMessageParams(ooo);
            local s0 = '\^{subj ooo}{the/he} seems to be stopped or broken. ';  
            if(oCromexxClockManager_ == nil){ 
                 if(gActor==gPlayerChar) say(s0);
                 exit;
            } 
            inherited;
       }
       action(){
            inherited; 
            local obj = self;
            gMessageParams(obj);
            local s0 = 'The current time is ' + oCromexxClockManager_.getTimeHourMin() + ' (in game world time). ';
            if(gActor==gPlayerChar) say(s0);
       }       
  }
;

/* Class: CromexxWatch
 *
 * This is like CromexxClock only you need to
 * actually be holding the watch to read or examine it (for getting the time).
 * Note that to use this object properly for a literal watch object
 * you should blend it in with class CromexxClothing or AdvancedCromexxClothing
 * and set the "clothLevel = 17" (for watch). 
 * 
 * Example use:
 *     + woomansWatch001: CromexxWatch, CromexxClothing 
 *          vocabWords = '(lady\'s) watch'  
 *          name = 'lady\'s watch'
 *          desc = "This is a lady\'s watch with a pink leather band. "
 *          girl_only = true
 *          clothLevel = 17
 *          // note that class CromexxWatch displays the time value after the readDesc
 *          readDesc = "You read the time on the watch. "
 *     ;
 *
 * Note that we'll want to keep 'timepiece' in our vocabWords list so that
 * our "Time" verb (in crom_wrd.t) can match up a generic "get time" command
 * and search for the nearest logical timepiece.
 */
class CromexxWatch: CromexxClock
  vocabWords = 'timepiece'
  dobjFor(Read){
       preCond=[objHeld,objVisible]
       verify(){ inherited; }
       check(){ inherited; }
       action(){ inherited; }
  }
  dobjFor(Examine){
       preCond=[objHeld,objVisible]
       verify(){ inherited; }
       check(){ inherited; }
       action(){ inherited; }
  }
;

/* class: CromexxAlarmClock
 * Use this for cuckoo clocks, alarm clocks, grandfather clocks, etc.
 * 
 * If you set "iRunOnlyWhenPlayerHere" to true, then you must have a way
 * to re-activate the daemon. We can do this by adding a CromAlarmClockTrigger
 * to a room object. So when the player enters the room the trigger finds and
 * sets this clock's daemon back to running again.
 */ 
class CromexxAlarmClock: CromexxClock, InitObject
   vocabWords = 'timepiece'
   // internal variable
   oCromexxClockManager_ = nil
   iRunOnlyWhenPlayerHere = nil
   iAlarmOnTheHour = true  // by default we'll have an alarm go off on the hour
   alarmText {
            local ooo = self;
            gMessageParams(ooo);
            local s0 = '\^{subj ooo}{the/he} rings as its alarm goes off. ';
            say(s0); 
   }   
   execute() {
     findSetWorldClockManager();
     oCromexxClockManager_.registerAlarmClock(self);       
   }
   // game world clock calls this on the hour if we're registered
   hourlyNotificationScript(val){
        if(gPlayerChar.location == self.location){
             if(iAlarmOnTheHour==true) alarmText;
        }
   }
   findSetWorldClockManager(){
           // reset local pointer and find our world game clock manager
           oCromexxClockManager_ = nil;
           if(realTimeManager.events_ != nil){         
                foreach(local cur in realTimeManager.events_){
                   if((cur.obj_.ofKind(CromexxGameClockManager)) &&
                      (cur.prop_ == &myGameClockDaemon)){
                         oCromexxClockManager_ = cur.obj_;
                   }
                }
           }
   } 
;

/* ***** we put this verb in file: crom_wrt.t ***************
 * // note that this will only work if all clocks & watches have 
 * // the vocabWord of 'timepiece' in their vocabWord list.
 * DefineIAction(Time)
 *  execAction(){    
 *        libGlobal.totalTurns--;                  
 *        local tokList = Tokenizer.tokenize('examine timepiece');
 *        executeCommand(gActor,gActor,tokList,nil);// nil = start of sentence (true/nil)         
 *  }
 *;
 *VerbRule(Time)
 * ('read') ('the') 'time' | 
 * ('read') 'time' | 
 * ('get') ('the') 'time' | 
 * ('get') 'time' | 
 * ('x') 'time' | 
 * ('examine') 'time' | 
 * ('x') ('the') 'time' | 
 * ('examine') ('the') 'time' | 
 * ('get') 'time' |
 * 'time' 
 * : TimeAction
 * verbPhrase = 'time'
 *;
 *
 ********************************************** ***********/

The extension Weather by Ish McGravin uses a table-based calendar. The extension needs to be altered to work with the latest version of Inform, however.

@WDchelonis

The hang of thursdays is what i initially also used except that is only for day controlling. What i really need is day/night cycle that doesn’t always assume 1 turn = 1 minute. I found this example:

[code]“Uptempo”

The fast time rule is listed before the timed events rule in the turn sequence rules.

The advance time rule is not listed in the turn sequence rules.

This is the fast time rule:
increment the turn count;
increase the time of day by 15 minutes.

When play begins: now the right hand status line is “[time of day]”.

The Temporal Hot Spot is a room.

At 9:30 AM:
say “Two turtles run by, almost too fast to see.”

At 9:37 AM:
say “A snail blitzes past.”

At 9:42 AM:
say “The grass grows.”

At 9:50 AM:
say “Several flowers burst open.”

Test me with “z / z / z / z”.
[/code]

This works, but only once, then it breaks.

@ianb

very nice, also something that i was looking for , except like i mentioned before it also always assumes that 1 turn = 1 minute and never assumes that a turn can vary in how much time that turn takes.

There have been some threads before on the problem that if you have an action which advances time by more than one minute then events specified to occur at a given time don’t occur unless the time matches exactly.

If the action takes a few minutes (like 10) then you could quietly iterate over each minute - that way the game time will visit every minute so events still occur. See the Inform 7 Recipe Book §6.11. Waiting, Sleeping.

There is a code example on the forum - read succeeding comments too for implementation notes.

There is a thread on a timed event only working the first time.

I’m sorry if i came off as a lazy asshole now that you found this already asked on the forum. I only google it.

Actually advancing turns instead of time seems to create a way more realistic feel to my rpg project, so i think i will work around that (used the inform 7 example). The two other sources you gave i simply couldn’t get to work. The timed event seems to not really work the way they got it to work, and the other post about the work durations, they simply don’t post all of their code and only mention to use the exstension Text Capture by Eric Eve, but never how they use it. I liked the work durations solution, and would use it if i knew how to implement it.

edit: forgot to mention that if you implement the work duration rule you get call error in the engine. Thats what i meant with implementing it.

Also thanks for the help!

Is there a way that the “z” key can be used automatically, thus without physically pressing it, but a command that automate it to pass time once?

Like after the player does something else, you want to make them wait, so an extra turn passes? You can just add “try waiting” into an appropriate rule to force the player to take the waiting action, same as if they pressed z. But if you’ve got any kind of timed events or every turn rules or anything like that, this is likely to lead to player confusion and bugs, so whatever you’re trying to do, there’s probably a different, better way go about it!

Currently I have a person in room1 when the player enters.

When the player leaves and goes to room2 the person from room1 is moved to room1 with this code

After waiting in the Unified Command Room when the player can see Joel:
	say "Lets go to the Strategy Meeting Room. says Joel.";
	now Joel is in the Strategy Meeting Room.

But before that can happen, I need to physically press “wait” or “z” for the time to pass before Joel will be visible in room2 and not visible in room1 anymore.

I do not want to press the “wait” or “z” button myself but want it to happen automatically when say for instance the player leave the room.

Hope you understand what I need.

Automation of the “wait” or “z” key.

Got it - see my post in the other thread, which will help get you to the result you want without timey wimey shenanigans.

1 Like

Thanks will check it out

I would also recommend using

Try Joel going south

instead of directly moving the character. That way you get messages like “Joel arrives from the north. “

4 Likes