How to know whether a firsttime {} has triggered or not?

Hello again!

Once again I'm working on updating my SaveLoadCode library and hit a snag. Right now, my library has no way of knowing whether or not a firsttime {} otherwise {} pair has triggered and I am looking to remedy that so they don't re-trigger when a player loads their game with my library.

I've figured out that the room.beforefirstenter and room.firstenter scripts trigger off of the room.visited attribute, the text processor commands {once: } and {notfirst: } functions key off of the game.textprocessor_seen dictionary parameter, but I can't seem to find how the game knows whether afirsttime {} and otherwise {} script has already triggered?

Is there some other attribute that this information is stored under that I'm missing? Is it buried somewhere in the java code, and if so is there a way I can save/update it?

Hoping someone with better knowledge of the Quest source code can help me figure this out...


Is there some other attribute that this information is stored under that I'm missing? Is it buried somewhere in the java code, and if so is there a way I can save/update it?

It's not in the code anywhere, unfortunately. firsttime is a hard coded function, so is in the C# code, and would need a custom version of Quest to change how it works.

You can check if a firsttime block has fired – if you convert a script attribute to a string, you will see that any firsttime blocks are actually removed from the script after they fire (and otherwise blocks are replaced by their contents, without the otherwise keyword)
This change is actually made when the script attribute is prepared for saving – I'm not sure whether the change will be visible to your code after saving, or only after loading a saved game. This may differ between desktop and online versions of the player.


Interesting... Definitely sounds more efficient from a code standpoint, but unfortunately makes it harder for me...

Was thinking about this for a bit, and I think one way I could get past this with my library would be to provide new firsttime/otherwise functions that would be compatible with my library. Maybe something like...

firsttime_saveload (uniqueid) {firstscript}
  if (not HasAttribute(game, "SaveLoadCode_firsttimetrigger")) {
    // Make it a string list so SaveLoadCode can more easily parse it
    game.SaveLoadCode_firsttimetrigger = NewStringList()
  }
  if (not TypeOf(uniqueid)="string") {
    uniqueid = ToString(uniqueid)
  }
  if (not ListContains(game.SaveLoadCode_firsttimetrigger, uniqueid) {
    // Then this should be the first time this is triggering
    list add (game.SaveLoadCode_firsttimetrigger, uniqueid)
    invoke(firstscript)
  }
  // Else, the script tied to this uniqueid has already triggered...
otherwise_saveload (uniqueid) {otherscript}
  if (not HasAttribute(game, "SaveLoadCode_firsttimetrigger")) {
    // Make it a string list so SaveLoadCode can more easily parse it
    game.SaveLoadCode_firsttimetrigger = NewStringList()
  }
  if (not TypeOf(uniqueid)="string") {
    uniqueid = ToString(uniqueid)
  }
  if (ListContains(game.SaveLoadCode_firsttimetrigger, uniqueid) {
    // Then the 'firsttime' paired to this 'otherwise' should have already triggered, run this otherwise
    invoke(otherscript)
  }

The users would have to CTRL+F through their code and replace any firsttime/otherwise instances with these new firsttime_saveload () and otherwise_saveload () functions, and provide each pair with a unique id in order to remain compatible, but then as long as my library saves the game.SaveLoadCode_firsttimetrigger parameter (if it exists at time of saving), then as far as I can tell it should work...

This is the most efficient solution I could come up with at this time, but I'm open to suggestions if people have other ideas.


I might suggest allowing a 'default' ID to make those functions easier to use (so game designers don't have to keep track of what IDs they already used).

For example, you could have something like:

// If there's only one argument, use it as the script as well as (a stringified form) as the ID
if (not IsDefined("firstscript")) {
  firstscript = uniqueid
}

This would work fine unless the same script is used in two firsttime functions, in which case the user can still give their own ID.
Or to reduce the size of save files (saving massive strings in the list), you could hash the script in some way.

For the otherwise function, I've not tried it but I think you might have an issue. If you have a firsttime followed immediately by an otherwise with the same ID, it will run both, because the firsttime has already run before the otherwise is reached.

You could resolve this by having two separate lists, for firsttime and otherwise IDs. That way, your otherwise function will skip the first time it is reached, separate from the firsttime function.

Alternatively, you could do it the way that the text processor else is handled: Have a global boolean, something like game.SaveLoadCode_run_otherwise which is set to true or false after running (or not) a firsttime script. Then the otherwise function could just check this – running only if the last-encountered firsttime didn't. Although this would prevent you from having an otherwise without a firsttime, possibly removing one of the advantages over the built-in system.

If you want to have otherwise work without firsttime, the (increasingly ugly) workaround might be making the list a dictionary, with its values true or false depending whether the corresponding otherwise should run. The first time a firsttime is encountered, it would set it to false. Subsequent times, it would remove it from the list and add it again as true. The firsttime function will run its script based on the presence or absence of a specific key in the dictionary; while the otherwise function would use the true/false value.

Hope that makes some kind of sense.


So I took your advice and made some alterations:

<function name="first_saveload" parameters="firstscript">
    if (not HasAttribute(game, "SaveLoadCode_firsttimetrigger")) {
      // Make it a string dictionary so SaveLoadCode can more easily parse it
      game.SaveLoadCode_firsttimetrigger = NewStringDictionary()
    }
    uniqueid = ToString(firstscript)
    if (Instr(uniqueid,"{")=1) {
      // Then uniqueid was likely saved as "{ script }" rather than "Script: script", which can lead to errors on subsequent visits. Let's change that...
      // Replace left "{" with "Script:"
      uniqueid = "Script: "+Right(uniqueid,LengthOf(uniqueid)-2)
      // Remove right "}" entirely
      uniqueid = Left(uniqueid,LengthOf(uniqueid)-2)+" "
    }
    if (not uniqueid in game.SaveLoadCode_firsttimetrigger) {
      // Then this should be the first time this is triggering
      DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "FirsttimeJustRun_TRUE")
      invoke (firstscript)
    }
    else {
      // Else, the script tied to this uniqueid has already triggered...
      if (Equal(DictionaryItem(game.SaveLoadCode_firsttimetrigger,uniqueid),"FirsttimeJustRun_TRUE")) {
        // If this is second time through, then set value to "" so its otherwise script will run...
        DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "")
      }
    }
  </function>
  <function name="other_saveload" parameters="otherscript">
    if (not HasAttribute(game, "SaveLoadCode_firsttimetrigger")) {
      // Make it a string dictionary so SaveLoadCode can more easily parse it
      game.SaveLoadCode_firsttimetrigger = NewStringDictionary()
    }
    uniqueid = ToString(otherscript)
    if (Instr(uniqueid,"{")=1) {
      // Then uniqueid was likely saved as "{ script }" rather than "Script: script", which can lead to errors on subsequent visits. Let's change that...
      // Replace left "{" with "Script:"
      uniqueid = "Script:"+Right(uniqueid,LengthOf(uniqueid)-1)
      // Remove right "}" entirely
      uniqueid = Left(uniqueid,LengthOf(uniqueid)-1)
    }
    if (uniqueid in game.SaveLoadCode_firsttimetrigger) {
      // Then the 'firsttime' paired to this 'otherwise' should have already triggered
      if (Equal(DictionaryItem(game.SaveLoadCode_firsttimetrigger,uniqueid),"")) {
        // Then the 'firsttime' script paired to this 'otherwise' didn't JUST run, so run the otherwise script...
        invoke (otherscript)
      }
      else {
        // Then the 'firsttime' script paired to this 'otherwise' JUST ran earlier in this code block. Set the dictionary value to "" so this otherwise will trigger on the NEXT pass...
        DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "")
      }
    }
    else {
      // No 'firsttime' tied to this 'otherwise' has been run. Assume one does not exist and set the dictionary trigger value so this otherwise script will trigger on the next pass.
      DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "")
    }
  </function>

Partway through coding, I think I caught onto what you were saying in your last post. So long as no two 'firsttime's or 'otherwise's have the same script, this should work in all cases. I would love to have an optional uniqueid parameter that the user could input (similar to what you suggested in the beginning of your last post) to cover cases where their scripts are identical, but Quest seems to take offense to user-made functions having optional parameters (it would throw an error "expected 2 parameters but got 1" before I even reached the if (not IsDefined("uniqueid")) block)... Not sure if there's a way around that, but I ran into a bigger issue with this kind of implementation...

However, the current, bigger issue is that for whatever reason, Quest does not seem to register that a given uniqueid has already been entered into the dictionary on the first pass. The added lines of code that will convert the "{" to "Script:" and "}" to "" were added in an attempt to remedy the issue, but even after doing so the issue persists...

See the following test game:

<!--Saved by Quest 5.8.6836.13983-->
<asl version="580">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <game name="FirsttimeTest">
    <gameid>971c06f8-72ec-46d5-98a7-697ed6c332ec</gameid>
    <version>1.0</version>
    <firstpublished>2022</firstpublished>
  </game>
  <object name="room">
    <inherit name="editor_room" />
    <isroom />
    <enter type="script">
      first_saveload() {
        msg ("firsttime1_room1")
      }
      other_saveload() {
        msg ("otherwise1_room1")
      }
      first_saveload() {
        msg ("firsttime2_room1")
      }
      other_saveload() {
        msg ("otherwise2_room1")
      }
    </enter>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
    </object>
    <exit alias="east" to="room2">
      <inherit name="eastdirection" />
    </exit>
  </object>
  <object name="room2">
    <inherit name="editor_room" />
    <enter type="script">
      first_saveload() {
        msg ("firsttime_room2")
        first_saveload() {
          msg ("nested_firsttime_room2")
        }
      }
      other_saveload() {
        msg ("otherwise_room2")
      }
    </enter>
    <exit alias="west" to="room">
      <inherit name="westdirection" />
    </exit>
    <exit alias="south" to="room3">
      <inherit name="southdirection" />
    </exit>
  </object>
  <object name="room3">
    <inherit name="editor_room" />
    <exit alias="north" to="room2">
      <inherit name="northdirection" />
    </exit>
  </object>
  <command>
    <pattern>debug</pattern>
    <script>
      msg (ToString(game.SaveLoadCode_firsttimetrigger))
    </script>
  </command>
  <function name="first_saveload" parameters="firstscript">
    if (not HasAttribute(game, "SaveLoadCode_firsttimetrigger")) {
      // Make it a string dictionary so SaveLoadCode can more easily parse it
      game.SaveLoadCode_firsttimetrigger = NewStringDictionary()
    }
    uniqueid = ToString(firstscript)
    if (Instr(uniqueid,"{")=1) {
      // Then uniqueid was likely saved as "{ script }" rather than "Script: script", which can lead to errors on subsequent visits. Let's change that...
      // Replace left "{" with "Script:"
      uniqueid = "Script: "+Right(uniqueid,LengthOf(uniqueid)-2)
      // Remove right "}" entirely
      uniqueid = Left(uniqueid,LengthOf(uniqueid)-2)+" "
    }
    if (not uniqueid in game.SaveLoadCode_firsttimetrigger) {
      // Then this should be the first time this is triggering
      DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "FirsttimeJustRun_TRUE")
      invoke (firstscript)
    }
    else {
      // Else, the script tied to this uniqueid has already triggered...
      if (Equal(DictionaryItem(game.SaveLoadCode_firsttimetrigger,uniqueid),"FirsttimeJustRun_TRUE")) {
        // If this is second time through, then set value to "" so its otherwise script will run...
        DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "")
      }
    }
  </function>
  <function name="other_saveload" parameters="otherscript">
    if (not HasAttribute(game, "SaveLoadCode_firsttimetrigger")) {
      // Make it a string dictionary so SaveLoadCode can more easily parse it
      game.SaveLoadCode_firsttimetrigger = NewStringDictionary()
    }
    uniqueid = ToString(otherscript)
    if (Instr(uniqueid,"{")=1) {
      // Then uniqueid was likely saved as "{ script }" rather than "Script: script", which can lead to errors on subsequent visits. Let's change that...
      // Replace left "{" with "Script:"
      uniqueid = "Script:"+Right(uniqueid,LengthOf(uniqueid)-1)
      // Remove right "}" entirely
      uniqueid = Left(uniqueid,LengthOf(uniqueid)-1)
    }
    if (uniqueid in game.SaveLoadCode_firsttimetrigger) {
      // Then the 'firsttime' paired to this 'otherwise' should have already triggered
      if (Equal(DictionaryItem(game.SaveLoadCode_firsttimetrigger,uniqueid),"")) {
        // Then the 'firsttime' script paired to this 'otherwise' didn't JUST run, so run the otherwise script...
        invoke (otherscript)
      }
      else {
        // Then the 'firsttime' script paired to this 'otherwise' JUST ran earlier in this code block. Set the dictionary value to "" so this otherwise will trigger on the NEXT pass...
        DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "")
      }
    }
    else {
      // No 'firsttime' tied to this 'otherwise' has been run. Assume one does not exist and set the dictionary trigger value so this otherwise script will trigger on the next pass.
      DictionaryAdd (game.SaveLoadCode_firsttimetrigger, uniqueid, "")
    }
  </function>
</asl>

By using the debug command to print the contents of game.SaveLoadCode_firsttimetrigger, you can see that

  1. On the first pass of a room, it properly executes the 'firsttime' block, and does not execute the otherwise block. Before I added the code to convert "{" to "Script:", it used to always save this first uniqueid string as "{ script }". You can comment out the if (Instr(uniqueid,"{")=1) { ... } block of code in my test game, then run the debug command after starting to see what I mean...

  2. On second pass of a room, the if (not uniqueid in game.SaveLoadCode_firsttimetrigger) { block still triggers, (even though it shouldn't, as far as I can tell), entering in a seemingly identical key and triggering the 'firsttime' block a second time. Even before I added the code to convert "{" to "Script:", it used to always save this second uniqueid string as "Script: script". The otherwise block still properly triggers on the second pass, perhaps because it is tied to the first, script-like uniqueid entry?

  3. On the third pass, nothing triggers (not even the otherwise block). Not sure why even the otherwise block doesn't trigger...

  4. Then on the fourth pass or greater, the otherwise blocks continue to trigger, as-expected...

Am I hitting some kind of internal race condition here, or is there something else I'm missing?

I imagine if I made a user-entered uniqueid parameter required, I could avoid these issues potentially, but making it optional/not-required would make this much easier to implement into existing games, so I'm tempted to try to figure this out, if possible...


Quest seems to take offense to user-made functions having optional parameters (it would throw an error "expected 2 parameters but got 1"

Oh, I forgot about that.
Yeah; functions can have optional parameters only if used within an expression (so somevariable = MyFunction () can have optional parameters, but MyFunction () on its own line can't.

Sorry, that completely slipped my mind for a minute.


entering in a seemingly identical key

That's interesting; as a dictionary shouldn't be able to have two identical keys.
If you have the desktop application, could you save the game after this using Quest's built-in save function, and look at the two strings in the save file? It might be easier to compare them in a text editor, to see if one of them contains extra spaces or non-printing characters or something. Knowing what's causing this problem should make it easier to debug.

I'll try to remember to take a look at this once the current wave of panic abates.


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums