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.
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 ):
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?
- Yes
- Yes!
- YES!
==>
You've just picked up the bacon. It's in the inventory pane now. But it's still got
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:
if (GetBoolean(game, "suppressturnscripts"))
to if (GetBoolean(game, "suppressturnscripts") or HasScript(game, "menucallback"))
OR
game.suppressturnscripts = true
to the ShowMenu function.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>
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 ("> " + Left(command, LengthOf(command) - LengthOf(key)) + "{object:" + object.name + "}" )
shownlink = true
}
}
}
}
if (not shownlink) {
msg ("")
OutputTextRaw ("> " + 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'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?)
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...
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.
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 ASLEvent
to 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.
We're going to need Pixie's two cents.
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.
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.
This does not effect published games, only games being edited in 5.8 which were created before 5.8.