Turn Scripts, Turn Counts, and Multiple Commands (Another suppressturnscripts Adventure!)

I have another fix for this which involves changes in the C# code, but, since the average user isn't building Quest in Visual Studio, I came up with this, too.

FOR DESKTOP USERS ONLY

We can modify the FinishTurn and ResolveNextName functions to make turn scripts and turn counts behave correctly when the player is allowed to enter multiple commands.

ResolveNextName (mod):

  <function name="ResolveNextName"><![CDATA[
    resolvedall = false
    queuetype = TypeOf(game.pov, "currentcommandvarlistqueue")
    if (queuetype = "stringlist") {
      queuelength = ListCount(game.pov.currentcommandvarlistqueue)
      if (queuelength > 0) {
        // Pop next variable off the queue
        var = StringListItem(game.pov.currentcommandvarlistqueue, 0)
        if (queuelength = 1) {
          game.pov.currentcommandvarlistqueue = null
        }
        else {
          newqueue = NewStringList()
          for (i, 1, queuelength - 1) {
            list add (newqueue, StringListItem(game.pov.currentcommandvarlistqueue, i))
          }
          game.pov.currentcommandvarlistqueue = newqueue
        }
        // Resolve variable
        value = StringDictionaryItem(game.pov.currentcommandvarlist, var)
        if (value <> "") {
          result = null
          resolvinglist = false
          // This is to resolve issue 626
          if (StartsWith(var, "objectexit")) {
            result = ResolveName(var, value, "exit")
          }
          if (result = null) {
            if (StartsWith(var, "object")) {
              if (HasScript(game.pov.currentcommandpattern, "multipleobjects")) {
                game.pov.currentcommandpendingobjectlist = NewObjectList()
                game.pov.currentcommandpendingvariable = var
                do (game.pov.currentcommandpattern, "multipleobjects")
                ResolveNameList (value, "object")
                resolvinglist = true
              }
              else {
                result = ResolveName(var, value, "object")
              }
            }
            else if (StartsWith(var, "exit")) {
              result = ResolveName(var, value, "exit")
            }
            else if (StartsWith(var, "text")) {
              result = StringDictionaryItem(game.pov.currentcommandvarlist, var)
            }
            else {
              error ("Unhandled command variable '" + var + "' - command variable names must begin with 'object', 'exit' or 'text'")
            }
          }
          // at this point, ResolveName has returned - either an object name, unresolved, or pending
          if (result = null) {
            if ((not resolvinglist) and LengthOf(GetString(game.pov, "currentcommandpendingvariable")) = 0) {
              UnresolvedCommand (value, var)
            }
          }
          else {
            AddToResolvedNames (var, result)
          }
        }
        else {
          ResolveNextName
        }
      }
      else {
        resolvedall = true
      }
    }
    else if (queuetype = "null") {
      resolvedall = true
    }
    else {
      error ("Invalid queue type")
    }
    if (resolvedall) {
      // All the objects have been resolved, so now we can actually do the command
      // TO DO: game.lastobjects should be game.pov.lastobjects
      game.lastobjects = game.pov.currentcommandresolvedobjects
      if (not DictionaryContains(game.pov.currentcommandresolvedelements, "multiple")) {
        dictionary add (game.pov.currentcommandresolvedelements, "multiple", false)
      }
      if (not GetBoolean(game.pov.currentcommandpattern, "isundo")) {
        if (LengthOf(game.pov.currentcommand) > 0) {
          start transaction (game.pov.currentcommand)
        }
      }
      if (not GetBoolean(game.pov.currentcommandpattern, "isoops")) {
        // TO DO: game.unresolved* should be game.pov.unresolved*
        game.unresolvedcommand = null
        game.unresolvedcommandvarlist = null
        game.unresolvedcommandkey = null
      }
      if (HasScript(game.pov.currentcommandpattern, "script")) {
        // This is the bit that actually runs the commands
        do (game.pov.currentcommandpattern, "script", game.pov.currentcommandresolvedelements)
        // Next 2 lines modded by KV to fix issues with multiple commands
        game.runturnscripts = true
        FinishTurn
        // END OF MOD
      }
      HandleNextCommandQueueItem
    }
  ]]></function>

FinishTurn (mod):

  <function name="FinishTurn">
    // Modded by KV to handle multiple commands correctly
    if (GetBoolean(game,"runturnscripts")) {
      if (not GetBoolean(game, "suppressturnscripts")) {
        RunTurnScripts
      }
    }
    game.runturnscripts = false
    // END OF MOD
    game.suppressturnscripts = false
    UpdateStatusAttributes
    CheckDarkness
    UpdateObjectLinks
  </function>

The example game's code:

<!--Saved by Quest 5.8.6708.15638-->
<asl version="550">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <game name="Suppressing the Turn Scripts">
    <gameid>65c36394-fcd4-4abf-89a5-4d0659cb4ef7</gameid>
    <version>0.6</version>
    <firstpublished>2018</firstpublished>
    <feature_advancedscripts />
    <turns type="int">0</turns>
    <statusattributes type="stringdictionary">
      <item>
        <key>turns</key>
        <value>Turns: !</value>
      </item>
    </statusattributes>
    <description><![CDATA[A test game.<br/><br/>Enter HINT:  no turn scripts, and no turn count<br/><br/>Enter TEST ONE: turn scripts fire once, and the turn count increases by 1<br/><br/>Enter TEST TWO:  no turn scripts, and no turn count]]></description>
    <author>KV</author>
    <suppressturnscripts type="boolean">false</suppressturnscripts>
    <multiplecommands />
    <inituserinterface type="script">
      JS.eval ("function testOne(){	setTimeout(function(){		ASLEvent('CallMeWithASL','The turn scripts fired once and the turn count increased by one.');		if (webPlayer){		  setTimeout(function(){		    scrollToEnd();		  },500);		}	},1);};")
      JS.eval ("function testTwo(){	setTimeout(function(){		ASLEvent('CallMeWithASL','This suppressed the turn scripts and the turn count.');		if (webPlayer){		  setTimeout(function(){		    scrollToEnd();		  },500);		}	},1);};")
    </inituserinterface>
  </game>
  <turnscript name="test_turnscript">
    <enabled />
    <script><![CDATA[
      msg ("<b><center><br/>I AM THE TEST TURNSCRIPT!<br/></center></b>")
    ]]></script>
  </turnscript>
  <object name="room">
    <inherit name="editor_room" />
    <description><![CDATA[<br/>Enter (or click):<br/>  {command:HINT}, {command:TEST ONE},  {command:TEST TWO},  {command:TEST ONE. TEST TWO}, or  {command:HINT. TEST TWO}]]></description>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
    </object>
    <object name="Ralph">
      <inherit name="editor_object" />
      <inherit name="namedmale" />
    </object>
  </object>
  <command name="hint">
    <pattern>hint;hints</pattern>
    <script>
      game.suppressturnscripts = true
      msg ("This story has no hints.")
    </script>
  </command>
  <command name="testone">
    <pattern>test one</pattern>
    <script>
      JS.testOne ()
    </script>
  </command>
  <command name="testtwo">
    <pattern>test two</pattern>
    <script>
      game.suppressturnscripts = true
      JS.testTwo ()
    </script>
  </command>
  <turnscript name="turn_count">
    <enabled />
    <script>
      if (not GetBoolean(game, "suppressturnscripts")) {
        IncreaseObjectCounter (game, "turns")
      }
    </script>
  </turnscript>
  <function name="ResolveNextName"><![CDATA[
    resolvedall = false
    queuetype = TypeOf(game.pov, "currentcommandvarlistqueue")
    if (queuetype = "stringlist") {
      queuelength = ListCount(game.pov.currentcommandvarlistqueue)
      if (queuelength > 0) {
        // Pop next variable off the queue
        var = StringListItem(game.pov.currentcommandvarlistqueue, 0)
        if (queuelength = 1) {
          game.pov.currentcommandvarlistqueue = null
        }
        else {
          newqueue = NewStringList()
          for (i, 1, queuelength - 1) {
            list add (newqueue, StringListItem(game.pov.currentcommandvarlistqueue, i))
          }
          game.pov.currentcommandvarlistqueue = newqueue
        }
        // Resolve variable
        value = StringDictionaryItem(game.pov.currentcommandvarlist, var)
        if (value <> "") {
          result = null
          resolvinglist = false
          // This is to resolve issue 626
          if (StartsWith(var, "objectexit")) {
            result = ResolveName(var, value, "exit")
          }
          if (result = null) {
            if (StartsWith(var, "object")) {
              if (HasScript(game.pov.currentcommandpattern, "multipleobjects")) {
                game.pov.currentcommandpendingobjectlist = NewObjectList()
                game.pov.currentcommandpendingvariable = var
                do (game.pov.currentcommandpattern, "multipleobjects")
                ResolveNameList (value, "object")
                resolvinglist = true
              }
              else {
                result = ResolveName(var, value, "object")
              }
            }
            else if (StartsWith(var, "exit")) {
              result = ResolveName(var, value, "exit")
            }
            else if (StartsWith(var, "text")) {
              result = StringDictionaryItem(game.pov.currentcommandvarlist, var)
            }
            else {
              error ("Unhandled command variable '" + var + "' - command variable names must begin with 'object', 'exit' or 'text'")
            }
          }
          // at this point, ResolveName has returned - either an object name, unresolved, or pending
          if (result = null) {
            if ((not resolvinglist) and LengthOf(GetString(game.pov, "currentcommandpendingvariable")) = 0) {
              UnresolvedCommand (value, var)
            }
          }
          else {
            AddToResolvedNames (var, result)
          }
        }
        else {
          ResolveNextName
        }
      }
      else {
        resolvedall = true
      }
    }
    else if (queuetype = "null") {
      resolvedall = true
    }
    else {
      error ("Invalid queue type")
    }
    if (resolvedall) {
      // All the objects have been resolved, so now we can actually do the command
      // TO DO: game.lastobjects should be game.pov.lastobjects
      game.lastobjects = game.pov.currentcommandresolvedobjects
      if (not DictionaryContains(game.pov.currentcommandresolvedelements, "multiple")) {
        dictionary add (game.pov.currentcommandresolvedelements, "multiple", false)
      }
      if (not GetBoolean(game.pov.currentcommandpattern, "isundo")) {
        if (LengthOf(game.pov.currentcommand) > 0) {
          start transaction (game.pov.currentcommand)
        }
      }
      if (not GetBoolean(game.pov.currentcommandpattern, "isoops")) {
        // TO DO: game.unresolved* should be game.pov.unresolved*
        game.unresolvedcommand = null
        game.unresolvedcommandvarlist = null
        game.unresolvedcommandkey = null
      }
      if (HasScript(game.pov.currentcommandpattern, "script")) {
        // This is the bit that actually runs the commands
        do (game.pov.currentcommandpattern, "script", game.pov.currentcommandresolvedelements)
        game.runturnscripts = true
        FinishTurn
      }
      HandleNextCommandQueueItem
    }
  ]]></function>
  <function name="FinishTurn">
    if (GetBoolean(game,"runturnscripts")) {
      if (not GetBoolean(game, "suppressturnscripts")) {
        RunTurnScripts
      }
    }
    game.runturnscripts = false
    game.suppressturnscripts = false
    UpdateStatusAttributes
    CheckDarkness
    UpdateObjectLinks
  </function>
  <function name="CallMeWithASL" parameters="data">
    msg (data)
  </function>
</asl>

Hmm...
If you set game.suppressturnscripts = true within a turnscript, it will suppress turnscripts for the next command executed in the current turn, or do nothing if there are no more commands this turn.

I'm not sure if there's any reason you'd ever do that, but it's certainly an unexpected behaviour.

Also, turnscripts that do things like modify the UI don't need to run once per command; and if they're interacting with JS this could break stuff unpredictably. Similarly, you only want to run turnscripts after each command, not all the other stuff in FinishTurn.

Also, if you have turnscripts doing stuff like keeping JS and Quest variables in sync, you want them to run even if a turn isn't a real turn; there should be some mechanism for making specific turnscripts "immune" to suppressturnscripts. In this case, you'd have to remove the "suppress" check from FinishTurn into RunTurnScripts.

I'd instead change the first function to include:

      if (HasScript(game.pov.currentcommandpattern, "script")) {
        // This is the bit that actually runs the commands
        do (game.pov.currentcommandpattern, "script", game.pov.currentcommandresolvedelements)
        RunCommandTurnScripts()
      }

And then…

  <function name="RunCommandTurnScripts">
    if (IsGameRunning()) {
      if (game.menucallback = null) {
        scripts = ObjectListSort(FilterByAttribute(AllTurnScripts(), "percommand", true), "name")
        if (GetBoolean (game, "suppressturnscripts")) {
          game.commandturnscriptssuppressed = true
          game.suppressturnscripts = false
          scripts = FilterByAttribute (scripts, "always", true)
        }
        foreach (turnscript, scripts) {
          if (GetBoolean(turnscript, "enabled")) {
            inscope = false
            if (turnscript.parent = game or turnscript.parent = null) {
              inscope = true
            } else {
              if (Contains(turnscript.parent, game.pov)) {
                inscope = true
              }
            }
            if (inscope) {
              do (turnscript, "script")
            }
          }
        }
      }
    }
  </function>

  <function name="RunTurnScripts">
    if (IsGameRunning()) {
      if (game.menucallback = null) {
        scripts = ObjectListSort(FilterByNotAttribute(AllTurnScripts(), "percommand", true), "name")
        if (GetBoolean(game, "commandturnscriptssuppressed")) {
          scripts = FilterByAttribute (scripts, "always", true)
          game.commandturnscriptssuppressed = false
        }
        else if (GetBoolean(game, "suppressturnscripts")) {
          scripts = FilterByAttribute (scripts, "always", true)
        }
        game.suppressturnscripts = false
        foreach (turnscript, scripts) {
          if (GetBoolean(turnscript, "enabled")) {
            inscope = false
            if (turnscript.parent = game or turnscript.parent = null) {
              inscope = true
            } else {
              if (Contains(turnscript.parent, game.pov)) {
                inscope = true
              }
            }
            if (inscope) {
              do (turnscript, "script")
            }
          }
        }
      }
    }
  </function>

  <function name="FinishTurn">
    RunTurnScripts
    UpdateStatusAttributes
    CheckDarkness
    UpdateObjectLinks
  </function>

So now you can give a turnscript a boolean attribute/flag percommand which causes it to run after each command rather than at the end of the turn, and a flag always which causes it to ignore game.suppressturnscripts.

At the moment, suppressturnscripts suppresses both the ones after this command, and the ones after each turn. If the a turnscript sets suppressturnscripts to true, it will stop the ones at the end of the turn, and the turnscripts on the subsequent command this turn if there is one. Not sure that's any more useful behaviour, but it seems more consistent.

(And yes, I'm throwing myself into code as a way to escape rising panic over non-sales of my new book, whose pre-orders are currently low enough that Amazon will probably fail to notify people following my author page when it comes out. If I'm not coherent, just ignore me)


Also: I notice that if a command runs ShowMenu, it will suppress all turnscripts until the menu is either answered or ignored. But the same doesn't apply for Ask. This seems an odd distinction. In that case, should always scripts still run?

(Answering myself: Yes they should. If the player types "put box on table. eat poison. put apple in box." and the poison displays an "are you sure?" menu, the turnscript that handles indenting of the places/objects pane should still run)

Actually… I think rather than having RunTurnScripts check for a menu callback, we should have ShowMenu set suppressturnscripts. (Is that how it works in 5.8? I haven't checked. If so, remove the menucallback check from the code I just posted). This way, if the user wants to show a menu without suppressing turnscripts for some reason (for example, if it's a conversation menu and you want the in-game clock to increment after each line) you can just set the flag to false again after displaying the menu.


Off-topic, I always defend the forum when someone else brings this up, but it really sucks when I can't find code from older threads.

...and I know: it's all about my search terms.

[Expletive deleted]... I forgot what I was even looking for now...

Oh yeah! You (mrangel) had code to override scripts during play. It created a dictionary (if nonexistent) and added the script(s) to it, so we could easily do things in the same vein of var clearScreenBak = clearScreen;function clearScreen(){clearScreenBak();addTextAndScroll('Thank you for clearing the screen! (Oh... I guess this text just defeated the purpose, huh?') with Quest scripts.


I've done variations on that a few times; but it's ugly and doesn't work with functions ):


doesn't work with functions

The "F" word...


I think rather than having RunTurnScripts check for a menu callback, we should have ShowMenu set suppressturnscripts. (Is that how it works in 5.8? I haven't checked.

I don't think it works this way.


Silly example comes to mind:

You can see: A table (on which there is a plate (on which there is bacon))

==> get bacon
You take it. It's hot. Would you like to eat it before it cools down?

Eat bacon?

  1. Yes
  2. Yes!
  3. YES!

==>

You've just picked up the bacon. It's in the inventory pane now. But it's still got &nbsp;&nbsp;&nbsp;&nbsp; at the start of its listalias, because the menu prevented turnscripts from running.

So, in the last blocks of code I posted, I'd say remove both if (game.menucallback = null) { checks, and either:

  1. Change if (GetBoolean(game, "suppressturnscripts")) to if (GetBoolean(game, "suppressturnscripts") or HasScript(game, "menucallback"))

OR

  1. Add game.suppressturnscripts = true to the ShowMenu function.

Awesome!

Thank you!


Hmm… I'm assuming that the turnscript created by SetTurnTimeout should not have always set; but should it have percommand?

I'd say no, so as not to unexpectedly change the behaviour of games when Quest is updated. But maybe there should be a parallel function:

  <function name="SetCommandTimeout" parameters="commandcount, script">
    name = GetUniqueElementName("turnscript")
    SetTurnTimeoutID (commandcount, name, script)
    turnscript = GetObject (name)
    if (not turnscript = null) {
      turnscript.percommand = true
    }
  </function>

I notice that if a command runs ShowMenu, it will suppress all turnscripts until the menu is either answered or ignored. But the same doesn't apply for Ask. This seems an odd distinction. In that case, should always scripts still run?

I really wish I'd have seen this bit! (My fault for perusing when I should be reading!)

Fix:

 <function name="ShowMenuResponse" parameters="option">
    if (game.menucallback = null) {
      error ("Unexpected menu response")
    }
    else {
      parameters = NewStringDictionary()
      dictionary add (parameters, "result", UnescapeQuotes(option))
      script = game.menucallback
      ClearMenu
      if (not GetBoolean(game, "disambiguating")) {
        game.runturnscripts = true
      }
      game.disambiguating = false
      invoke (script, parameters)
    }
  </function>
  <function name="ResolveNameFromList" parameters="variable, value, objtype, scope, secondaryscope" type="object"><![CDATA[
    value = Trim(LCase(value))
    fullmatches = NewObjectList()
    partialmatches = NewObjectList()
    foreach (obj, scope) {
      name = LCase(GetDisplayAlias(obj))
      CompareNames (name, value, obj, fullmatches, partialmatches)
      if (obj.alt <> null) {
        foreach (altname, obj.alt) {
          CompareNames (LCase(altname), value, obj, fullmatches, partialmatches)
        }
      }
    }
    // allow referring to objects from the previous command by gender or article
    if (objtype = "object" and game.lastobjects <> null) {
      foreach (obj, game.lastobjects) {
        CompareNames (LCase(obj.article), value, obj, fullmatches, partialmatches)
        CompareNames (LCase(obj.gender), value, obj, fullmatches, partialmatches)
      }
    }
    // Also check the secondary scope, but only if we have not found anything yet
    if (ListCount(fullmatches) = 0 and ListCount(partialmatches) = 0 and not secondaryscope = null) {
      foreach (obj, secondaryscope) {
        name = LCase(GetDisplayAlias(obj))
        CompareNames (name, value, obj, fullmatches, partialmatches)
        if (obj.alt <> null) {
          foreach (altname, obj.alt) {
            CompareNames (LCase(altname), value, obj, fullmatches, partialmatches)
          }
        }
      }
    }
    if (ListCount(fullmatches) = 1) {
      return (ListItem(fullmatches, 0))
    }
    else if (ListCount(fullmatches) = 0 and ListCount(partialmatches) = 1) {
      return (ListItem(partialmatches, 0))
    }
    else if (ListCount(fullmatches) + ListCount(partialmatches) = 0) {
      return (null)
    }
    else {
      game.disambiguating = true
      candidates = ListCompact(ListCombine(fullmatches, partialmatches))
      if (LengthOf(variable) > 0) {
        // single object command, so after showing the menu, add the object to game.pov.currentcommandresolvedelements
        game.pov.currentcommandpendingvariable = variable
        ShowMenu (DynamicTemplate("DisambiguateMenu", value), candidates, true) {
          varname = game.pov.currentcommandpendingvariable
          game.pov.currentcommandpendingvariable = null
          if (result <> null) {
            AddToResolvedNames (varname, GetObject(result))
          }
        }
      }
      else {
        // multi-object command, so after showing the menu, add the object to the list
        game.pov.currentcommandmultiobjectpending = true
        ShowMenu (DynamicTemplate("DisambiguateMenu", value), candidates, true) {
          if (result <> null) {
            list add (game.pov.currentcommandpendingobjectlist, GetObject(result))
            ResolveNextNameListItem
          }
        }
      }
      return (null)
    }
  ]]></function>

PS

FinishTurn is working differently in Quest 5.8.

It was called from WorldModel.cs, but now it's called from ResolveNextName (and now ShowMenuResponse shall call it, too, unless game.disambiguating is true).


Ah, ignore my previous suggestions then.


XanMag is awesome! (I generally only mention this when he removes SPAM, but he's pretty much all-round awesome!)


The worst part is that the spammer has used up a perfectly good username. Some day, a guy named Martin Tolley who GM's table-top role playing games is going to come along to sign up. He's in for a real disappointment!


FinishTurn is working differently in Quest 5.8.

Looked over it again; and I still think this is a really bad idea. I stand by my earlier suggestion: give turnscripts a percommand flag, and have those turnscripts called after each command. Moving the call to FinishCommand seems to have no additional benefits over this method, and introduces a whole swarm of bugs.

FinishTurn does a few different things, including running turnscripts. Running some turnscripts after every action is preferable.

However, optimal behaviour would be for UpdateStatusAttributes, CheckDarkness, and UpdateObjectLinks should be guaranteed to run each time Quest sends a bundle of output to the browser, whether that's from a command, an unresolvedcommand script, or an ASLEvent. This is why FinishTurn is called from the core.

FinishTurn should be called from the core code, because that is the only way to guarantee it is the last thing to run after all scripts have terminated, regardless of what those scripts are. It is only some turnscripts that should be moved.


Yeah, I've dealt with a few issues concerning this since it was added to the beta build, but the wrinkles seem to be ironed out now.


I originally brought it up because multiple commands only triggered FinishTurn once, and I found a way to make it work differently.

Within hours, I found a way to make it all work correctly without modifying the hard-coded scripts, but I think Pixie prefers bypassing those hard-coded scripts if possible, in an attempt to make Quest less dependent on the C# code. (I am not speaking on behalf of Pixie, mind you; this is mere conjecture.)

Anyway, the only issues I've seen were when someone had a pre 5.8 work-in-progress loaded in the editor. ( And these games all had modified functions from the core library (such as FinishTurn).)


These are the functions which have been modified (I don't think I've missed any):

  <function name="FinishTurn">
    if (GetBoolean(game,"runturnscripts")) {
      if (not GetBoolean(game, "suppressturnscripts")) {
        RunTurnScripts
      }
    }
    game.runturnscripts = false
    game.suppressturnscripts = false
    UpdateStatusAttributes
    CheckDarkness
    UpdateObjectLinks
  </function>  
  <function name="StartGame">
    <![CDATA[
    StartTurnOutputSection
    if (game.showtitle) {
      JS.StartOutputSection ("title")
      PrintCentered ("<span style=\"font-size:260%\">" + game.gamename + "</span>")
      if (game.subtitle <> null) {
        if (LengthOf(game.subtitle) > 0) {
          PrintCentered ("<span style=\"font-size:130%\">" + game.subtitle + "</span>")
        }
      }
      if (game.author <> null) {
        if (LengthOf(game.author) > 0) {
          PrintCentered ("<br/><span style=\"font-size:140%\">[By] " + game.author + "</span>")
        }
      }
      msg ("<div style=\"margin-top:20px\"></div>")
      JS.EndOutputSection ("title")
    }
    if (game.pov = null) {
      playerObject = GetObject("player")
      if (playerObject = null) {
        if (ListCount(AllObjects()) > 0) {
          firstRoom = ObjectListItem(AllObjects(), 0)
        }
        else {
          create ("room")
          firstRoom = room
        }
        create ("player")
        player.parent = firstRoom
      }
      game.pov = player
    }
    else {
      InitPOV (null, game.pov)
    }
    InitStatusAttributes
    UpdateStatusAttributes
    InitVerbsList
    if (HasScript(game, "start")) do (game, "start")
    foreach (obj, AllObjects()) {
      if (HasScript(obj, "_initialise_")) do (obj, "_initialise_")
    }
    UpdateStatusAttributes
    UpdateObjectLinks
    on ready {
      if (game.gridmap) {
        Grid_DrawPlayerInRoom (game.pov.parent)
      }
      if (game.displayroomdescriptiononstart) {
        OnEnterRoom (null)
      }
      UpdateStatusAttributes
      UpdateObjectLinks
    }
    // Added by KV to use the old JS clearScreen if the transcript is disabled
    if (GetBoolean(game, "notranscript")){
      JS.eval("transcriptEnabled = false;")
    }
    game.runturnscripts = false
    FinishTurn
    ]]>
  </function>
 <function name="HandleCommand" parameters="command, metadata">
    <![CDATA[
    handled = false
    if (game.menucallback <> null) {
      if (HandleMenuTextResponse(command)) {
        handled = true
      }
      else {
        if (game.menuallowcancel) {
          ClearMenu
        }
        else {
          handled = true
        }
      }
    }
    if (not handled) {
      StartTurnOutputSection
      if (StartsWith (command, "*")) {
        // Modified by KV to bypass turn scripts and turn counts, and to print "Noted."
        game.suppressturnscripts = true
        msg ("")
        msg (SafeXML (command))
        msg("Noted.")
	// Added for Quest 5.8    - KV
	FinishTurn
      }
      else {    
        shownlink = false
        if (game.echocommand) {
          if (metadata <> null and game.enablehyperlinks and game.echohyperlinks) {
            foreach (key, metadata) {
              if (EndsWith(command, key)) {
                objectname = StringDictionaryItem(metadata, key)
                object = GetObject(objectname)
                if (object <> null) {
                  msg ("")
                  msg ("&gt; " + Left(command, LengthOf(command) - LengthOf(key)) + "{object:" + object.name + "}" )
                  shownlink = true
                }
              }
            }
          }
          if (not shownlink) {
            msg ("")
            OutputTextRaw ("&gt; " + SafeXML(command))
          }
        }
        if (game.command_newline) {
          msg ("")
        }
        game.pov.commandmetadata = metadata
        if (game.multiplecommands){		
          commands = Split(command, ".")
          if (ListCount(commands) = 1) {
            game.pov.commandqueue = null
            HandleSingleCommand (Trim(command))
          }
          else {
            game.pov.commandqueue = commands
            HandleNextCommandQueueItem
          }
		    }
        else {
          game.pov.commandqueue = null
          HandleSingleCommand (Trim(command))	
        }		
      }
    }
    ]]>
  </function>

 <function name="ResolveNameFromList" parameters="variable, value, objtype, scope, secondaryscope" type="object">
    <![CDATA[
    value = Trim(LCase(value))
    fullmatches = NewObjectList()
    partialmatches = NewObjectList()
    
    foreach (obj, scope) {
      name = LCase(GetDisplayAlias(obj))
      CompareNames (name, value, obj, fullmatches, partialmatches)
      if (obj.alt <> null) {
        foreach (altname, obj.alt) {
          CompareNames (LCase(altname), value, obj, fullmatches, partialmatches)
        }
      }
    }
    
    // allow referring to objects from the previous command by gender or article
    
    if (objtype = "object" and game.lastobjects <> null) {
      foreach (obj, game.lastobjects) {
        CompareNames (LCase(obj.article), value, obj, fullmatches, partialmatches)
        CompareNames (LCase(obj.gender), value, obj, fullmatches, partialmatches)
      }
    }

    // Also check the secondary scope, but only if we have not found anything yet
    
    if (ListCount(fullmatches) = 0 and ListCount(partialmatches) = 0 and not secondaryscope = null) {
      foreach (obj, secondaryscope) {
        name = LCase(GetDisplayAlias(obj))
        CompareNames (name, value, obj, fullmatches, partialmatches)
        if (obj.alt <> null) {
          foreach (altname, obj.alt) {
            CompareNames (LCase(altname), value, obj, fullmatches, partialmatches)
          }
        }
      }
    }    
    
    if (ListCount(fullmatches) = 1) {
      return (ListItem(fullmatches, 0))
    }
    else if (ListCount(fullmatches) = 0 and ListCount(partialmatches) = 1) {
      return (ListItem(partialmatches, 0))
    }
    else if (ListCount(fullmatches) + ListCount(partialmatches) = 0) {
      return (null)
    }
    else {
      // Added this line to resolve issue with new FinishTurn setup in 580
      game.disambiguating = true
      candidates = ListCompact(ListCombine(fullmatches, partialmatches))
      
      if (LengthOf(variable) > 0) {
        // single object command, so after showing the menu, add the object to game.pov.currentcommandresolvedelements
        game.pov.currentcommandpendingvariable = variable
      
        ShowMenu(DynamicTemplate("DisambiguateMenu", value), candidates, true) {
          varname = game.pov.currentcommandpendingvariable
          game.pov.currentcommandpendingvariable = null
          if (result <> null) {
            AddToResolvedNames(varname, GetObject(result))
          }
        }
      }
      else {
        // multi-object command, so after showing the menu, add the object to the list
        
        game.pov.currentcommandmultiobjectpending = true
        
        ShowMenu(DynamicTemplate("DisambiguateMenu", value), candidates, true) {
          if (result <> null) {
            list add (game.pov.currentcommandpendingobjectlist, GetObject(result))
            ResolveNextNameListItem
          }
        }        
      }
      
      return (null)
    }
    ]]>
  </function>
  <function name="ResolveNextName">
        <![CDATA[
    resolvedall = false
    queuetype = TypeOf(game.pov, "currentcommandvarlistqueue")
    if (queuetype = "stringlist") {
      queuelength = ListCount(game.pov.currentcommandvarlistqueue)
      if (queuelength > 0) {
        // Pop next variable off the queue
        var = StringListItem(game.pov.currentcommandvarlistqueue, 0)
        if (queuelength = 1) {
          game.pov.currentcommandvarlistqueue = null
        }
        else {
          newqueue = NewStringList()
          for (i, 1, queuelength - 1) {
            list add (newqueue, StringListItem(game.pov.currentcommandvarlistqueue, i))
          }
          game.pov.currentcommandvarlistqueue = newqueue
        }
        // Resolve variable
        value = StringDictionaryItem(game.pov.currentcommandvarlist, var)
        if (value <> "") {
          result = null
          resolvinglist = false
          // This is to resolve issue 626
          if (StartsWith(var, "objectexit")) {
            result = ResolveName(var, value, "exit")
          }
          if (result = null) {
            if (StartsWith(var, "object")) {
              if (GetBoolean(game.pov.currentcommandpattern, "allow_all")) {
                scope = FilterByAttribute(GetScope("object", "", "object"), "scenery", false)
                game.pov.currentcommandpendingobjectscope = ListExclude(scope, FilterByAttribute(scope, "not_all", true))
                game.pov.currentcommandpendingvariable = var
                ResolveNameList (value, "object")
                resolvinglist = true
              }
              else if (HasScript(game.pov.currentcommandpattern, "multipleobjects")) {
                game.pov.currentcommandpendingobjectlist = NewObjectList()
                game.pov.currentcommandpendingvariable = var
                do (game.pov.currentcommandpattern, "multipleobjects")
                ResolveNameList (value, "object")
                resolvinglist = true
              }
              else {
                result = ResolveName(var, value, "object")
              }
            }
            else if (StartsWith(var, "exit")) {
              result = ResolveName(var, value, "exit")
            }
            else if (StartsWith(var, "text")) {
              result = StringDictionaryItem(game.pov.currentcommandvarlist, var)
            }
            else {
              error ("Unhandled command variable '" + var + "' - command variable names must begin with 'object', 'exit' or 'text'")
            }
          }
          // at this point, ResolveName has returned - either an object name, unresolved, or pending
          if (result = null) {
            if ((not resolvinglist) and LengthOf(GetString(game.pov, "currentcommandpendingvariable")) = 0) {
              UnresolvedCommand (value, var)
            }
          }
          else {
            AddToResolvedNames (var, result)
          }
        }
        else {
          ResolveNextName
        }
      }
      else {
        resolvedall = true
      }
    }
    else if (queuetype = "null") {
      resolvedall = true
    }
    else {
      error ("Invalid queue type")
    }
    if (resolvedall) {
      // All the objects have been resolved, so now we can actually do the command
      // TO DO: game.lastobjects should be game.pov.lastobjects
      game.lastobjects = game.pov.currentcommandresolvedobjects
      if (not DictionaryContains(game.pov.currentcommandresolvedelements, "multiple")) {
        dictionary add (game.pov.currentcommandresolvedelements, "multiple", false)
      }
      if (not GetBoolean(game.pov.currentcommandpattern, "isundo")) {
        if (LengthOf(game.pov.currentcommand) > 0) {
          start transaction (game.pov.currentcommand)
        }
      }
      if (not GetBoolean(game.pov.currentcommandpattern, "isoops")) {
        // TO DO: game.unresolved* should be game.pov.unresolved*
        game.unresolvedcommand = null
        game.unresolvedcommandvarlist = null
        game.unresolvedcommandkey = null
      }
      if (HasScript(game.pov.currentcommandpattern, "script")) {
        // This is the bit that actually runs the commands
        do (game.pov.currentcommandpattern, "script", game.pov.currentcommandresolvedelements)
      }
      //
      //Setting game.runturnscripts to true to run turn scripts after ShowMenu , show menu, ask, or Ask.
      //This works in conjuction with FinishTurn, which has also been modified as of Quest 5.8.
      //- KV, 2018/05/25
      game.runturnscripts = true
      FinishTurn
      HandleNextCommandQueueItem
    }
  ]]></function>
  <function name="ShowMenuResponse" parameters="option">
    if (game.menucallback = null) {
      error ("Unexpected menu response")
    }
    else {
      parameters = NewStringDictionary()
      dictionary add (parameters, "result", UnescapeQuotes(option))
      script = game.menucallback
      ClearMenu
      // Added by KV to handle the new FinishTurn setup in 580
      if (not GetBoolean(game, "disambiguating")) {
        game.runturnscripts = true
      }
      game.disambiguating = false
      invoke (script, parameters)
      FinishTurn
    }
  </function>

This is a big change, and it makes me nervous, too. I think it's going to be okay, though. I'm pretty sure Pixie has tested the [expletive deleted] out of it, and I've tried to break it every way I can think of (which, admittedly, isn't saying much).


I know you can't test this, mrangel.

Give me monkey wrench suggestions, and I'll throw them in there. If this can be broken, I'm sure Pixie would very much appreciate it if we break it while it's still in beta.


This is the method I came up with which does not involve any changes to the hard-coded stuff:

https://github.com/KVonGit/QuestStuff/wiki/Turn-Scripts-Turn-Counts-and-Multiple-Commands


I'm not thinking so much about bugs (but the big one that comes to mind is javascript timeouts), as from a design perspective.

Under the current version, turnscripts are run once per batch of data between server and client. If you want to make them run once per command, you have to override ResolveNextName.

With the call to FinishTurn moved, turnscripts run once per command. If you have one that you only want to run once per data-batch… that looks like it would be more difficult. There's a reason it was done in C#.

Giving the player the ability to run turnscripts for every command, that's a good thing. But not replacing existing behaviour. Anyone whose game uses turnscripts will find them behaving differently after upgrading; and I don't see any benefit to justify that.

Sorry; didn't mean to rant. It just seems weird to make a change that alters the behaviour users might be expecting, reduces efficiency, and increases complexity… I can't see the upside.


javascript timeouts

What is an example of this? (I've been awake far too long to think clearly on this one. Not being argumentative at all.)


Under the current version, turnscripts are run once per batch of data between server and client. If you want to make them run once per command, you have to override ResolveNextName.

Yep, and FinishTurn. That's all I did in the code in that last link I posted, which is actually my preferred version in retrospect, but it does appear to work fine either way, so...

...and, again, admittedly, I was the one who came up with the changes to the hard-coded functions.


I've helped 3 people with issues after updating to the current beta, and it only took a few minutes a piece, and 2 of those were using one of my libraries that was overriding FinishTurn as well as suppressing the turn scripts unnecessarily in one bit of code.


With the call to FinishTurn moved, turnscripts run once per command. If you have one that you only want to run once per data-batch… that looks like it would be more difficult. There's a reason it was done in C#.

It's definitely more difficult. (See all the code I posted above.)


Anyone whose game uses turnscripts will find them behaving differently after upgrading; and I don't see any benefit to justify that.

They shouldn't unless they have one of these functions overridden:

FinishTurn

StartGame

HandleCommand

ResolveNameFromList

ResolveNextName

ShowMenuResponse

That rules all the online authors out, because they can't override functions anyway.

...and (this is just my feeling on this subject) anyone who has overridden these function in the first place should be knowledgeable enough to adjust their mod to work after the upgrade.

Everyone seems to disagree with me here, but I don't upgrade an application I'm using unless a) I finish my current project(s) first, or b) I am fully prepared to deal with all the issues my old code creates in the new "environment".


Also, the C# code is checking the ASL version before deciding whether or not it calls TryFinishTurn. If the ASL version is less than 580, it works the old way. So, published games are totally safe.


Sorry; didn't mean to rant. It just seems weird to make a change that alters the behaviour users might be expecting, reduces efficiency, and increases complexity… I can't see the upside.

I think your input is valued, mrangel. And I didn't think you were ranting.

...and I hope you didn't think I was arguing or anything. I'm just listing the stuff I know about all this.

It's all due to me (I'm pretty sure). First, I was always crying about an ASLEvent firing the turn scripts an extra time. Then, I started whining about the multiple commands only firing one turn script. Then, I was all like, "hey, Pixie. Check out this code. It makes stuff work like we'd expect."


First, I was always crying about an ASLEvent firing the turn scripts an extra time.

Yeah. Imagine I've got a turnscript that counts how many turns the player has taken.
I've got some clicky-button UI stuff that uses ASLEvent to do something that doesn't count as a turn. So, I made that function set an attribute that causes the turn counter to skip the next increment. It works as intended.

Then I load my game in an updated version of quest. Now, ASLEvent doesn't fire a turnscript. So the player clicks my fancy button, and it works fine, but the next command they type doesn't count as a turn.

I could come up with a similar example for the multiple commands thing.

That is not ideal. It doesn't matter that I've not overridden any of the core functions. If the behaviour of a language feature changes, it will require users to change their code around it. And I might not notice, because I'd already tested the countdown code.

(I can't check if turnscripts fire after calling ASLEvent in this new version, but it doesn't look like they will)

Sometimes, it might be worth causing a flag day. But you have to think if it's worth it.
I've asked a few times now, but maybe you missed it. Is there any benefit to moving the call to FinishTurn? It really looks like change for the sake of change.


I've asked a few times now, but maybe you missed it. Is there any benefit to moving the call to FinishTurn?

No, I didn't miss it. I listed the events which led to these changes. I mentioned that the changes make me nervous, and that I prefer my second bit of code (which doesn't alter the hard-coded stuff). Then, I posted the link to that code.

The only benefit of which I am aware could be attained by changing ResolveNextName and FinishTurn, and nothing else, as I've posted (and as you pointed out).

Does Pixie know something I don't, though? Probably so.


...and I'm going to check out the ASLEvent thing, but I'm pretty sure you are correct. It no longer runs the turn scripts, and this was one of the goals (for me anyway).


Random question:

What's the difference between breaking TAKE and DROP to handle scope differently and possibly breaking ASLEvent to handle multiple commands? Besides ASLEvent calls being used much less by authors?

(Remember that I am not lobbying for the recent changes. Nor am I arguing. Nor am I saying two wrongs make a right. Nor am I saying either of these changes were wrong. I'm just asking.)


With the changes to the hard-coded functions, a call to ASLEvent no longer triggers the turn scripts.

This is how I expect it to behave, unless the event calls something which handles a command. (I have admitted I'm a crazy person on numerous occasions, though.)


I keep saying this, and I know I'm the minority, but:

I don't believe anyone should update any game-creation software without expecting to have to change the old code in their game. This is true for Inform, TADS, or anything. (And any software, really.)


I'm not saying that these specific changes are worth the trouble. And, again, I got nervous about it and tried to back out of these changes, but Pixie has been running all sorts of tests, and he seems to prefer the way things work with the changes. (I'm sure he'll chime in once he sees these recent posts.)


Here's a small example game and the output in 5.7.2 then 5.8:

<!--Saved by Quest 5.8.6724.15602-->
<asl version="580">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <game name="hkg;fj">
    <gameid>72653bf5-68c4-44bb-967f-ecac9b40e3e0</gameid>
    <version>1.0</version>
    <firstpublished>2018</firstpublished>
  </game>
  <object name="room">
    <inherit name="editor_room" />
    <isroom />
    <beforeenter type="script">
    </beforeenter>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
    </object>
  </object>
  <turnscript name="tester">
    <enabled />
    <script>
      msg ("TURN SCRIPT")
    </script>
  </turnscript>
  <command name="tst">
    <pattern>test</pattern>
    <script>
      JS.eval ("ASLEvent('SayHello', 'hello');")
    </script>
  </command>
  <function name="SayHello" parameters="txt">
    msg (txt)
  </function>
</asl>

Quest 5.7.2

You are in a room.

> test
TURN SCRIPT
hello
TURN SCRIPT

> test
TURN SCRIPT
hello
TURN SCRIPT

Quest 5.8

You are in a room.

> test
TURN SCRIPT
hello

> test
TURN SCRIPT
hello

If we stick with the current changes, a few people will have to edit some scripts which call ASLEvent if they open an existing version 550 game in Quest 5.8.

If we don't make these changes to the C# code, a few authors will be saved from having to edit a few scripts, but every author who creates a game afterwards will still have to deal with the fact that an ASLEvent calls turn scripts an extra time.

Either way, folks doing crazy stuff with ASLEvent will have to do extra work, which we all signed up for (knowingly or not) when we scripted said crazy stuff. And we especially invite problems when working on older code after we've updated our game-creation software.


What's the difference between breaking TAKE and DROP to handle scope differently and possibly breaking ASLEvent to handle multiple commands? Besides ASLEvent calls being used much less by authors?

I would say that you should never break existing behaviour. If a feature of the engine, or some function, works a particular way in a release version, it should continue to work that way in all future versions. Maybe that's because I've spent too much time in software engineering lectures.
If you're adding an extra parameter to a function, it should be optional. Calling the function without that parameter should give the same behaviour it did before.
If you're changing turnscripts so that the user gets the option of running them once-per-turn or once-per-command, then the default option (what happens if you import a game from a previous version) should be the way the previous version behaved.

(What's the thing with breaking take/drop? Have I missed something?)


You know what?

Someone could easily add 1 line (maybe 3 lines) of code to Quest 5.8 to make a call to ASLEvent run the turn scripts like it always has...

(I'm slow sometimes.)


The TAKE/DROP thing happened with the last two updates (5.7.1 and 5.7.2), but only to those of us who had overridden TAKE and DROP.

...and I just thought of making ALSEvent calls handle turn scripts the same way, but we had our posts crossed up again.


For those of you just joining us, mrangel and I are not arguing, nor are we really debating. We are merely sharing perspectives.

(For the record, I admit that his perspective is usually the more logical.)


This is what handles a call to ASLEvent in the end:

public void SendEvent(string eventName, string param)
        {
            Element handler;
            m_elements.TryGetValue(ElementType.Function, eventName, out handler);

            if (handler == null)
            {
                Print(string.Format("Error - no handler for event '{0}'", eventName));
                return;
            }

            Parameters parameters = new Parameters();
            parameters.Add((string)handler.Fields[FieldDefinitions.ParamNames][0], param);

            RunProcedure(eventName, parameters, false);
            if (Version >= WorldModelVersion.v540)
            {
                if (Version < WorldModelVersion.v580)
                {
                    TryFinishTurn();
                }
                if (State != GameState.Finished)
                {
                    UpdateLists();
                }
                SendNextTimerRequest();
            }
        }

It just feels like...

  1. Move this function out of the C# code.
  2. ????
  3. Profit!

I just wish I could see the benefit.

...and I just thought of making ALSEvent calls handle turn scripts the same way, but we had our posts crossed up again.

Another thought comes to mind. When a timer script triggers, is FinishTurn called after it? I can't work out when that happens, unless it's still being called from the C# code.


Is the C# code not what's keeping Quest from running under Linux or Mac? (I don't know the answer. I'm seriously asking.)


When a timer script triggers, is FinishTurn called after it?

Testing now...


A timer doesn't trigger turn scripts in either version of Quest.


Interesting :p That's going to confuse me now.

Oh... FinishTurn calls UpdateStatusAttributes; but it doesn't need to, because the C# code calls UpdateStatusAttributes right after it anyway.
That fixes some of the potential issues; but is a little counterintuitive.


That's going to confuse me now.

Heh. If I had a nickel for every time I've said that over the past week...


So... Do we really want each call to ASLEventto fire the turn scripts?

I mean, I don't.

...but I'm all for taking a vote on it (especially since I am in no way in charge of anything [insert evil grin here]).


So... Do we really want each call to ASLEvent to fire the turn scripts?

If you say 'yes', it's going to make some scripts a lot more complex, and be a pain for some users; but most of those users have likely already been looking for a way to make it work. Not ideal.

If you say 'no', it's going to make some scripts more complex, and be a pain for some users. Including some whose code previously worked, or who already had implemented a workaround for the other problem. Not ideal; but probably would have been a better solution if it had been done that originally.

We want it to fire some of the turnscripts (for example, your UpdateContentsInLists turnscript should be run). So, add a flag to control it; a checkbox on the turnscript tab in the editor. And make the default fit the previous version's behaviour, so that people upgrading the editor don't find their games suddenly changing behaviour.


Not ideal; but probably would have been a better solution if it had been done that originally.

Exactly.

Once upon an update, Quest was updated to handle WEAR and REMOVE, so the required code was added, which caused everyone with custom WEAR and REMOVE stuff in a game they were editing to do some extra work when editing in an upgraded editor.

This was fine, because everyone agreed that Quest should handle those commands by default in the first place. I view the ASLEvent thing the same way.


We want it to fire some of the turnscripts... So, add a flag to control it; a checkbox on the turnscript tab in the editor. And make the default fit the previous version's behaviour, so that people upgrading the editor don't find their games suddenly changing behaviour.

We pretty much already had this going on, sans the checkbox, before these changes. (Setting game.suppressturnscripts to true, then checking for that in FinishTurn.)


people upgrading the editor don't find their games suddenly changing behaviour

This will only effect games being edited in 5.8, not published games being played in 5.8.


THOUGHTS

  1. We're going to need Pixie's two cents.

  2. Web users cannot modify FinishTurn to suppress turn scripts when calling an ASLEvent, but they can call FinishTurn from any ASLEvent to control what happens with this new setup.

  3. Any fixes which were previously applied to try to control the turn scripts when calling an ASLEvent would probably be sloppy scripts which only decremented the turn count (not rolling back changes applied by any turn scripts), unless the author had the desktop version of Quest and modified FinishTurn and/or RunTurnScripts while they were at it.

  4. This does not effect published games, only games being edited in 5.8 which were created before 5.8.


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

Support

Forums