Wednesday, October 19, 2016

GENERIC-OBJECTS as conversation (ASK/TELL) fodder

Note: None of the code in this post adheres to the formatting recommended to me for ZIL programming so I apologize if the look of it drives anyone crazy. While debugging the problem at hand, I use an indenting system I find most readable, but if I ever release any game source, I'll try to format it as suggested. I also might write another post here at some point sharing what those format conventions are.

The ZIL manual lists two ways for setting up ASK/TELL conversations:

  1. The (EVERYTHING) syntax addition allows for verb definitions where any object is accepted, whether or not it is scope.  Like so:
    <SYNTAX ASK OBJECT ABOUT OBJECT (EVERYTHING) = V-ASK>
  2. It also mentions GENERIC-OBJECTS objects that are out of scope and can't be physically interacted with.  It specifically mentions Deadline's "new will," the missing mystery document a player might be inquiring about.  Despite not being physically available, these objects are always in scope.

Originally, I thought was a little unclear why ZIL needed both methods, as I figure that if the (EVERYTHING) syntax works, you could just stick those objects in a room no one will find them and, hey, no one will interact with them.  It occurred to me while writing this, though, that an author might want different error messages for trying to interact with tangible objects compared to idea/topic objects.  You don't want >EXAMINE FREEDOM to respond with "You don't see that here." Unless your game is kind of an ass about freedom.

Anyhow, the current ZIL/ZILF compiler doesn't support the (EVERYTHING) method, so it was recommended to me that I try to handle everything with GENERIC-OBJECTS.  I imagined this was going to be sort of a headache.  I figured I'd have to write fancy code to take GENERIC-OBJECTS out of scope when they had a real object counterpart within scope; if I had a flashlight in my game, I'd have to have a GENERIC-OBJECTS object for it for when players ask NPCs about it when it is another room, but when it is in scope, I don't want "Which flashlight do you mean, the flashlight or the flashlight?" responses.  This ended up not being a problem, but I'll get to that later.

My lack of enthusiasm to play around with all of this resulted in a several month break from ZIL code, but I got the itch back recently.  Of course, it didn't help that I initially got off on the wrong foot because I had forgotten the name of the scope thing I needed so I just quickly skimmed the ZIL model, saw another type of scope object called LOCAL-GLOBALS, and thought, ah, that must be it.  Unfortunately, LOCAL-GLOBALS objects are something else, something better used for tangible things that show up in several rooms like a river or something.

After wasting a good couple hours on that, Jesse McGrew set me off on the right GENERIC-OBJECTS path.  Right away, my disambiguation worries became a non-issue because it turns out GENERIC-OBJECTS are only parsed at a last resort so they never have the same "parser weight" as an actual object in the same room.

The only remaining issue was that while interacting with a non-present object properly responded with "You don't see that.", the message took up a turn.  Consistency among error messages is one of my many goals with any IF language I work with.

Luckily, Jesse was there to the rescue again and pointed out the "hook" where we could fix that.  He gave me this code:

First off, if one were to use this code, they'd put it before "parser.zil" is included. REPLACE-DEFINITION tells the compiler, "hey, when we get to this code, we'll use this instead" (I'm probably getting this wrong on some technical level but it's how I took it).  You can only use REPLACE-DEFINITION on certain hook routines, but as this post shows, it can be useful they're there.

The interesting thing about this code is, as you can see by the name, it runs after the command has been performed but we still have time to say, "change the command to INVENTORY" (so it doesn't take a turn).  This is another one of the things I find endearing about ZIL; an impressive amount of the game loop can be modified through code (unlike many later IF languages where more aspects of the game loop are set in stone by the game engine).

Jesse wrote the above code without testing.  It probably worked just fine right out of the box, but as far as I could read it, I thought it seemed a little wrong so I instantly started re-writing it.  It took several attempts, but I eventually got something that worked:

At this point, it occurred to Jesse that instances where the PRSO (the direct object) or PRSI (the indirect object) are 0 could run into problems, specifically with the "<IN? , PRSI ,GENERIC-OBJECTS>" check.  I figured the worry was the Vile Zero Error From Hell.  First off, out of curiosity, I wanted to see if my interpreters would catch it.  As many interpreters today straight out ignore Vile Zero Errors From Hell, finding one that even pointed it out took several tries.  Eventually, I got DOS Frotz to say, hey, something is wrong.

So yes, I had to add some code to check for 0 values.  My final code ended up looking like this:

Basically, this code says, GENERIC-OBJECTS should not take a turn when used as direct objects, and if it's an indirect object, only take up a turn if it's part of ASK/TELL.

Originally, I tried to spread out my code a bit more, but the way these DEFMAC things seem to work, they really only allow for one element.  That's why my solution basically is one conditional with several other conditionals wrapped inside it.  I asked Jesse about it, and it turns out there's another command, BIND, which allows for routines-within-routines.  Not only that, but the BIND code can have its own local variables... and not only that, but you can also give those local variables starting values, something you can't do with "AUX" variables for your random routine.

In the code:
<BIND ((A 1) (B 2) Z T W) ...>
A would have a starting value of 1 and B would have a starting value of 2.

Anyhow, here is a final version of the above routines, using BIND to make the code easier to read (hopefully):

Setting up this conversation stuff should be the last speedbump for a while, at least until I start working on QUEUEs (ZIL's version of daemons/events) although I expect that all to work fine (with just some extra design effort on my part). I may also write a menu system at some point, mainly as a distraction to keep myself from working on, gasp, ACTUAL GAME CODE.

EDIT:  Okay, it turns out I wasn't done with the above code.  I was going through all of parser.zil's verb routines making sure that applicable routines gave "You don't see that." type responses for GENERIC-OBJECTS.  I was reminded that >THINK ABOUT [object] is a default verb there, which of course should be allowed for taking up a turn.  In testing out the code, I noticed that behavior would be broken after a couple turns.  I had thought BIND treated its variables as local variables if not given a starting value, but no, they act more like global variables (so, ERR, once set as 1, would always stay 1).  Here is the fixed code: