Action Results
(Adapted for use with adv3Lite by Eric Eve)
Editor's Note: This chapter of the Technical Manual covers pretty much the same ground as the corresponsing section in the Adv3Lite Library Manual, but is nevertheless worth preserving since, as it was originally written by Mike Roberts, it offers a slightly different perspective and way of explaining things that some people may find helpful. Moreover, the adv3Lite handling of action results (the various stagee that need to be defined on objects handling any given action) were closely modelled on the corresponding phases in adv3, meaning that, for the most part, it is fairly straightforward to translate the corresponding stages between the two libraries, even though there are some difference between them.
Introduction
One of the main programming tasks in writing an adventure game is handling special-case commands from the player - commands that do something special, rather than using the basic library handling. The TADS 3 library uses "object oriented programming" techniques that let you write special-case command handlers for individual objects or for entire classes of related objects, but this article assumes you already know all about that sort of thing: dobjFor(), verify(), action(), and so on. So we're not going to talk about that. Instead, this article is about how you return information from your special action handlers, to tell the library what happened and what to do next.
The handling for a specific action is divided into a series of phases: verification, preconditions, checking, and action. Since each phase serves a distinct purpose, each phase has its own kinds of results. So, you shouldn't expect to signal the same kinds of results from the "verify" phase that you would from the "check" phase. Since each phase is different, we have to consider them one at a time.
Verify
Pre-conditions
Check
Action
Report
Library Messages
Verification
The primary purpose of the verification phase - really the only purpose - is to figure out which object or objects are the most logical for a given action. This lets the parser pick the best interpretation automatically when the player's phrasing is ambiguous, so that we don't have to constantly bother the player asking for clarification.
A side benefit is that we can often use this same information to rule out some actions entirely, since our "logicalness" determination can frequently tell that an action is completely illogical. This saves us the trouble of writing any code in the other phases for handling this particular action on this particular object, since we can just rule out the object in the verify phase based on the logicalness rating we have to come up with anyway. But always keep in mind that this is just a side benefit; it's easy to slip into thinking that the point of the verify phase is to rule things out, but it's not, because we'll still have plenty more chances to rule things out later, in other phases. Any ruling-out that we can do in the verify phase is just a free bonus that saves us a little work.
What does "logical" mean, anyway?
Before we go on, we should define what "logical" means in the context of verifiers. This is really important to understand, because it's at the heart of what the verification stage is for. Here's the key: "logical" means what the player thinks it means.
So, what does "what the player thinks it means" really mean?
English, like any natural language, is full of ambiguity. It's capable of very fine precision, as you can plainly see if you read any ANSI specification document or any legal contract. But there's a high price for that kind of precision, paid in the form of verbosity and linguistic convolution - which, again, you can plainly see in any ANSI spec or any contract. There's a reason precision comes at such a great cost, and it's not that lawyers and engineers are bad writers. The real reason is that precision of the kind needed in engineering and legal documents just isn't nearly as important in everyday speech as speed and expressiveness, so natural languages evolved to express information concisely, at the cost of allowing lots of ambiguity. In everyday speech, there's so much shared context that we can communicate volumes with a few words, knowing that our shared context will fill in all of the things we leave poorly specified in our actual words.
Think about two friends talking, and one of them saying to the other, "Joe is retiring to Florida." So, who's Joe? First off, any English speaker would know it's a man's name, so we've narrowed it down to "male human named Joe"; that's still pretty darn ambiguous, but these two people talking are friends, so they have a shared idea of "Joe." They might each know multiple people named Joe, and might even have several acquaintances named Joe in common, but there will be one person that they both understand to be the one they can talk about as just "Joe." If they didn't have such an understanding, or if the speaker wanted to talk about one of the other Joes they both know, the speaker would have qualified it somehow: "Joe Smith," or "Bob's friend Joe," or "that guy from school who was always going on about his hernia problems" - the speaker would qualify it according to the shared context with the listener.
That's what we mean by "what the player thinks it means": we have to imagine the player sitting at the keyboard, having just read the moving prose you've written to describe the room in which the player character finds herself. What's in the player's mind now? What does the player mean by "Joe" here, in this location in the game, at this time?
What "logical" doesn't mean
There are a couple of important distinctions we need to draw here.
First, note that we're not interested in what the player character thinks; we're interested in what the player thinks. Whatever theory anyone has about the relationship between player and player character, there's no question that it's the player typing those commands, so our job parsing the input is to understand the meaning the player meant to convey.
Second, we're especially not interested in what the author thinks, or what the "narrator" thinks, or what the parser thinks, or what an omiscient observer who can look at all of the object properties thinks. This is a key point to understand and to keep in mind while writing your game, because it's sometimes easy to forget about the player and think instead in terms of the objective reality (so to speak) of the game world.
For example, if a particular key opens a particular door, it's easy to fall into the trap of thinking that that particular key ought to be a more logical match for UNLOCK DOOR WITH KEY than any other key. That's the "objective reality" trap: the key is in fact the correct one for the action, but that doesn't matter - the only thing that matters is whether or not the player knows this. If the player doesn't know it, then there's no reason to assume that the player means that key over any other.
(The library has a Key class that operates on exactly this principle. Initially, one key is pretty much like any other. Once the player has successfully used a particular key to open a particular door, though, the library remembers that successful combination, on the assumption that the player will also duly note and remember the combination. On future attempts, the key that the player used successfully will be considered the most likely one in cases of ambiguity.)
Another example, roughly the opposite of the previous one: there's a door. It looks perfectly ordinary, but unbeknownst to the player, it's actually a fake door, cleverly constructed to look exactly like a real door, but not openable. Should OPEN DOOR be ruled out as illogical? No, because the player has no way of knowing that the door's a fake. If the fake is so convincing that even attempting to open it would not reveal its falsity (maybe it just looks like a real door that's stuck, for example), then it would still not be illogical to try opening it again and again. Only when the player finds out that the door isn't actually a door - maybe by inspecting it more closely, or by being told by a character in the game - will OPEN DOOR stop making sense to the player.
Verifiers and game state changes: a deadly combination
A general note on writing verify handlers: as in TADS 2, it's important that a verify handler never modifies the state of the game. For example, a verify handler should never move an object from one location to another, and it should never change an object from open to closed. The reason is simple: the parser can call the same verifier several times in the course of an action, and it can call a verifier "tentatively," before deciding if the object is really involved in the action. If a verifier does anything to change game state, then it will make its change too many times, or simply at the wrong times.
How multiple results are handled
A single verification call can have several separate results, so you don't actually return results as function return values. Instead, you add the results to the current result set. Fortunately, this isn't as complicated as it sounds, because the library provides a set of macros that make it easy to add a result. Typically, a verify routine checks some conditions, adds one or two results, and might inherit the default handling from a superclass.
To determine if a command will be allowed to proceed, or to compare the logicalness of different possible object matches for a noun phrase, the library looks at the worst result of the verification. The library pays attention to the worst result because verification is essentially a negative sort of process: we're looking for problems, reasons the object is a bad choice, so naturally we go with the biggest problem we find. Note that this means that if there are no verify results at all, the parser takes this to mean that there are no problems - objects are assumed logical by default, unless a verifier says otherwise.
Because the worst result prevails in cases of multiple results, you don't have to worry that superclass code that you inherit could overrule any objections that your code might raise. If you raise an objection by adding an "illogical" result of some kind, your objection will be obeyed; the worst that can happen is that a superclass can find a worse objection, in which case that more serious problem will upstage your objection. On the other hand, this means that you can't overrule a superclass or subclass to make the object more logical; if some other code objects, there's no way to retract that objection in your code. The only way to prevent superclass code from declaring something illogical when you want to make it logical is to avoid inheriting the superclass code in the first place - which is easy, since it just means you don't put an "inherited()" call in the method.
Examples
Here are some examples of how to use verifiers.
For something that just plain makes no sense at all, we use "illogical." For example, it makes no sense to open a coffee mug:
dobjFor(Open) { verify() { illogical('{I} {can\'t} open a coffee mug! '); } }
For things that might make sense some of the time, but don't make sense right now because of the current state of an object, we use either "illogicalNow" or "illogicalAlready," the latter being for cases where the action is redundant because it tries to put the object into a state it's already in. For example, a door can be opened and closed, but if it's already open, then opening it again doesn't usually make any sense. We use "illogicalAlready" for this kind of redundant command.
dobjFor(Open) { verify() { if (isOpen) illogicalAlready('{The subj dobj} {is} already open. '); } }
We'd use illogicalNow instead of illogicalAlready if the object's state makes the command illogical, but for some reason other than redundancy. For example, we can't board an inflatable rubber raft when it's deflated.
Note that it's not necessary to do anything at all if we don't want to raise an objection, so there's no "else" in the "if" statement above. If we don't raise an objection, the parser assumes that the object is perfectly logical.
Sometimes, a particular action is possible on essentially any object, but it's more sensical on certain objects than others. For example, you could try to read almost anything, but things like books and magazines are much more likely than others to be the object of a READ action. You can handle this sort of thing using "logicalRank," which lets you indicate that an action is logical, and give different objects different relative rankings. For reading, you could give ordinary objects a low ranking - the library uses a value of 50 on its arbitrary ranking scale for this. (The default ranking for an object that doesn't provide a specific ranking is 100.) So, for ordinary objects, you could do this:
dobjFor(Read) { verify() { logicalRank(50); } }
Verifier result types in detail
Here are the macros to add verify results. These are listed in descending order of logicalness, so you can read two things into the ordering. First, an item later on the list will upstage an item earlier on the list when both items are added to the same object's verify results, because the worst result is always the prevailing one. For example, if an object's verify uses both "logicalRank" and "illogical," the overall result will be "illogical." Second, during disambiguation, once the parser has found the overall result for each possible match to a noun phrase, it will pick the object or objects with the best results, so an object with an overall result from earlier in the list will be chosen over objects with overall results later in the list.
logical. You don't usually have to bother with this one, but it's provided anyway, to make the set complete. Remember, you can't overrule an objection by making an object more logical, and everything starts out logical by default, so this result really has no effect. About the only reason to use this is if you want to add emphasis to the source code, for the benefit of a human reader looking at the code - it won't affect anything in the parser, but a human reader would be able to see that you specifically intend for the action to be logical.
logicalRank(rank). This indicates that the object is logical, and qualifies the logicalness with a ranking.
The rank argument is any number; the higher the number, the more logical the ranking. The default ranking for a regular "logical" object is 100. Other than this, the meaning of the ranking is up to you; you can use this to fine-tune the parser's automatic object chooser for particular situations where you want it to pick one object over another.
The library uses logical rankings in a few places to do this kind of fine-tuning, and when it does, it has its own conventional rank values. You don't have to use the same values, but it might be convenient to do so. The library's conventional rank values are:
- 150: an especially good fit; this object is an especially likely match for the action. For example, a book might use this for a READ command.
- 140: similar to 150, but slightly less ideal. The library uses this for cases where an object is entirely appropriate in some general way (a key being used to unlock something, for example), but isn't the most likely unique individual object (it's not the particular key known to open this particular lock, for example).
- 100: the default ranking used by the "logical" macro. This object is a good candidate, with nothing that would make it seem unlikely, but nothing that would make it seem more likely than anything else.
- 80: slightly less than perfect. This object is a good match, but with some temporary and readily correctable attributes that make it less than perfect. The library uses this for objects with unmet preconditions attached, so that it chooses objects that can already meet any preconditions over those that need work first.
- 70: slightly less than perfect, with some attributes that aren't necessarily problems, but make it seem likely that the player would probably be referring to something else. For example, we might want to favor items already being held when we read them.
- 60: same as above, for slightly more unfavorable attributes.
- 50: logical, but not especially likely. This object can be used as a match for the action, but probably isn't the best choice. This is used for cases where the object would not normally be expected to be a good choice for the action.
dangerous. This result indicates that the action is not one that a player would perform lightly, because of apparent danger in the situation. This makes the object only slightly less logical than the default "logical" result, but its main purpose is to prevent the action from being undertaken as an implied action, or as a default action.
As always, what's important is what the player perceives. If something is in fact dangerous, but the danger isn't apparent to the player, you shouldn't add a "dangerous" result.
For example, going north through the airlock door would imply OPEN AIRLOCK DOOR. It's probably pretty obvious to a player that you don't open an airlock door unless you have your spacesuit on, so you'd probably want to add a "dangerous" result to this action. This would prevent OPEN AIRLOCK DOOR from being performed as an implied action for NORTH, and it would prevent the airlock door from being chosen as the default object if the player just types OPEN. Note that "dangerous" doesn't rule out the object. If the action is explicitly and unambiguously performed on the object, then we'll allow the action to proceed despite the danger.
illogicalAlready(msg). The action is illogical, because whatever it's trying to do is already done. For example, OPEN DOOR is illogical when the door is open - but it's not always illogical, since it's perfectly logical when the door is closed.
The parameters provide a message to display to the player, to explain why the action is illogical. Within game code, you'll almost always use a simple single-quoted string here - something like this:
illogicalAlready('{The subj dobj} {is} already open. ');
Note that you can use "{xxx}" substitutions here. For an overview of how those work, see the article on message substitution parameters. Note also that when you give the message as a string, there are no additional parameters (so the params list is left empty).
Within the library itself, you'll never see single-quoted strings
in these messages. Instead, the library uses the alternative way of
specifying the message, which is to provide a property of Thing (or
some other class or object on which the verify() method is being defined)
such cannotOpenMsg
which defines the actual message to be
displayed (and which is thus easily overridden by game code that wants
to customize these messages). In the library these messages are in turn
usually defined using BMsg()
. This first it makes it easier to translate the
library to other languages, it makes it easy
to use different messages for different actors or under varying
circumstances by using a CustomMessages object. We'll see more
about using library message properties later
in the article.
illogicalNow(msg). The action is illogical on this object, due to the object's current state, but might be logical at other times. For example, BOARD RAFT is illogical when the inflatable rubber raft is deflated, but isn't always illogical.
As with illogicalAlready(), the arguments give the message to display and its parameters.
illogical(msg). The action is illogical on this object at all times. This differs from illogicalAlready and illogicalNow in that the action is always going to be illogical on this object by the very nature of the object. For example, TAKE BUILDING simply makes no sense, since a building is obviously not an object you can pick up and carry around; this isn't a momentary condition of the building, but an intrinsic aspect of its nature.
As with illogicalNow, the msg argument can be a simple single-quoted string giving the message text, or it can be a property of the class or object on which the verify method is being defined.
illogicalSelf(msg). The action is illogical because it's attempting to do something to an object using the object itself. For example, PUT BOX IN BOX.
The reason the library provides this special separate macro, rather than simply using illogical() for these cases, is that it's relatively easy for a player to inadvertantly attempt an action like this by using a plural phrase in a command. By flagging these errors specially, we make it possible for the parser to filter them out when they occur in plurals, avoiding unnecessary messages.
implausible(msg): results in a logical rank of 35 and prevents the action from going ahead. This is intended for actions that are illogical, but whose illogicality might not be quite so immediately apparent as is the case with a straightforwardly illogical action.
nonObvious. This indicates that the object is not obvious for the action. This makes the object less likely, in terms of logicalness, than any of the "illogical" results, so it'll be chosen for disambiguation purposes only after exhausting all of the other possibilities. It also prevents the action from being used implicitly, and it prevents the object from being chosen as a default.
This can be used for situations where an object is actually a good match for a command, but this isn't meant to be obvious to the player, because the object serves a hidden purpose. For example, suppose you can pick a lock with a hairpin, but this is a puzzle. You obviously can't make the hairpin illogical for UNLOCK DOOR WITH something, but if you didn't do anything at all, and there were no other key-like objects present, then a simple UNLOCK DOOR would pick the hairpin by default. nonObvious handles this: it allows the object to be used in a command when the command is explicit and unambiguous, but it ensures that the object will never be supplied as a default for the command, and that the command will never be attempted implicitly.
inaccessible(msg). The action is impossible because the object is inaccessible. This indicates that the object is in scope, but it's inaccessible to a sense that's needed for the action, such as touch or hearing. You usually won't have to use this result type in a game, since accessibility to a sense is almost always handled with a precondition. If you write your own sense-checking precondition, look at the existing ones in the library (touchObj, objVisible, objAudible, and so on) to get an idea of how to use this.
Pre-conditions
One of the things that comes up over and over again when writing command handlers is checking for certain basic requirements before performing a command. For example, before you can use a key to unlock a door, you have to be holding the key. After writing a few command handlers, it becomes clear that the same requirements tend to come up a lot for different actions. "You have to be holding something" comes up not only for UNLOCK DOOR WITH KEY, but for all sorts of other actions as well: TURN SCREW WITH SCREWDRIVER, CHOP WOOD WITH AXE, PUT BOOK ON SHELF, SHAKE CAN, THROW DART AT TARGET.
It's tempting to think that these requirements should be tested in the action itself, but in practice, that's a bad place to make these checks. The problem with checking these requirements on the action is that it doesn't allow for cases where the requirements vary. For example, you might think it's a reasonable rule to say that EAT X requires "X must be held," and so encode that rule in the EAT action. But while "X must be held" might apply to a hot dog or a sandwich, it probably wouldn't apply to a steak or a bowl of soup; for those, being able to touch the object is sufficient. It's much better to test these requirements for each object, since that makes it easy to vary the requirements accordingly. Specifying the individual requirements for every action for every object might sound incredibly tedious at first glance, but remember that TADS 3 is object-oriented, so all we'll really end up doing is writing the requirements for a base class or two, and then overriding this inherited set of requirements for special cases.
The library's approach to testing these common requirements is something called "pre-conditions." A pre-condition is a requirement that you indicate must be met before the command can proceed. If the requirement isn't met, the command isn't allowed. This means that once you specify a pre-condition for a given action, you can stop worrying about that condition; your action handlers will never have to test for it, since they'll never even be invoked if the pre-condition fails.
The pre-condition mechanism has two really powerful features. First, it lets you take a common condition that applies to all sorts of different actions, such as "object must be held," and write the code to test that condition just once, by creating a PreCondition object. Every time you need to apply that condition, you merely list the PreCondition object in the pre-condition list for the object action where you want to apply it; there's no additional code to write for the object, since the check is entirely contained in the PreCondition object. For example, if you're creating a SANDWICH object, and you want to require that the player be holding the object before eating it, you'd simply list the objHeld precondition for EAT SANDWICH:
// on the sandwich object dobjFor(Eat) { preCond = [objHeld] }
Second, a pre-condition can do more than just test that a requirement is met: a pre-condition can actually try to bring the requirement into effect automatically, using an "implied command." An implied command is simply a command that is obviously implied by the requirement; for example, "object is held" pretty obviously implies TAKE OBJECT to bring the requirement into effect. This lets your game overcome the common adventure-game annoyance of parser errors telling you what you have to do rather than just doing it; it's annoying to be told "You have to take the sandwich first," because if the parser knows this, why doesn't it just do it already?
(Note that the automatic implied command of a pre-condition is sometimes not desirable, because it would give away a puzzle, or because it would do something that the player would in all likelihood avoid because of danger. In these cases, you can avoid the implied command in one of two ways. The easiest way is not to use the pre-condition in the first place, but check for the condition explicitly somewhere else, such as in the check() routine. The other way, which is often better but a little trickier, is to use a "dangerous" or "nonObvious" verify result for the implied action.)
You attach pre-conditions to a particular object for a particular action, by using the preCond property in a dobjFor() or iobjFor() group. The preCond property simply returns a list of PreCondition objects. For example, we might add this to a screwdriver object:
iobjFor(UnscrewWith) { preCond = [touchObj] }
In many cases, you'll want to simply add a pre-condition or two to the default set of conditions inherited from a base class. To do this, simply use inherited() to pick up the base class conditions, and add in your additional conditions:
dobjFor(Read) { preCond = (inherited() + touchObj) }
In some cases, you'll need to remove a pre-condition applied in a base class. For example, the base Thing class in the library applies an objHeld pre-condition to EAT, but some objects don't need to be held to be eaten. For these kinds of objects, you can remove the objHeld condition by inheriting the default and then subtracting out the condition you don't want:
dobjFor(Eat) { preCond = (inherited() - objHeld) }
Here's a list of the pre-conditions defined in the adv3Lite library.
objHeld This object must be held by the actor before the action can go ahead; if not, the library will attempt an implicit take.
objCarried A variant of the objHeld pre-condition for use with the DROP action. The object must be held by the actor before the action can go ahead. If the object is not directly held but is in a container carried by the actor and that container's canDropContents property is true, then the library will attempt an implicit TakeFrom to allow the action to proceed.
objClosed This object must be closed before the action can go ahead (typically the action concerned will be locking). If not, the library will attempt an implicit close.
objNotWorn This object must not be worn when the action takes place (e.g. when dropping the object). It it is currently worn, the library will attempt an implicit doff.
objVisible This object must be visible and there must be enough light to see it by (e.g. to examine or read the object). If the condition is not met the library does not attempt to correct it.
objAudible This object must be audible to the actor for this action to go ahead. If the condition is not met the library does not attempt to correct it.
objSmellable This object must be smellable by the actor for this action to go ahead. If the condition is not met the library does not attempt to correct it.
objDetached This object must be detached from any other object before this action can go ahead. If it isn't, the library attempts an implicit detach command.
objUnlocked The object must be unlocked before the action (typically opening) can go ahead. This PreCondition is used in the library on the dobjFor(Open) of objects whose autoUnlock property is true. It is normally only meaningful to use objUnlocked in the dobjFor(Open) preCond list of objects with a lockability of either lockableWithoutKey or lockableWithKey. In the latter case the objUnlocked PreCondition will only attempt to unlock the object if the actor is carrying a key that might plausibly work.
actorInStagingLocation The actor must be in this object's stagingLocation before the action can proceed. If the condition is not met the library attempts to carry out the appropriate implicit actions (getting in/out/off or on various objects) to bring it about.
actorOutOfNested The actor must be in a top level room (out of any nested room) before the action can proceed. If the condition is not met the library attempts to remove the actor from the nested room(s) it's it via the appropriate implicit actions.
actorOutOfSubNested. This is used by the GetOff and GetOutOf actions to ensure that an actor is out of any enclosed nested rooms before exiting the enclosing nested room (for example, that the actor is out of a Booth situated on a Platform before getting off the Platform).
travelPermitted This checks whether travel is permitted by calling the beforeAction notifications for the actor doing the travelling, and in the case of PushTravel, for the object being pushed. This ensures that if travel is ruled out by a relevant object's beforeTravel method, the action is halted before, say, any attempt is made to open a door via an implicit action or to check any travel barriers that might be in place (experience suggests that this is generally a better order of events).
canTalkToObj The actor must be able to talk to this object before the action can proceed. If the condition is not met the library does not attempt to correct it.
containerOpen If this object is a container (i.e. if its contType is In), then it must be open before the action can proceed. If it's not open, the library will try an implicit open command. If the object's contType is not In then this preCondition has no effect. (It has to be defined this way so that this preCondition can be defined for various actions on Thing in the library).
containerInteriorVisible Similar to containerOpen
except that if the container is transparent or the actor is inside the container no implicit open will be attempted (since it shouldn't be needed). In the library this is used for the LOOK IN command.
containerInteriorAccessible Similar to containerInteriorAccessible
except that the implicit open is omitted only if the actor is inside the container. In the library this is used for indirect object on the PUT IN command.
touchObj The actor must be able to touch this object before the action can proceed. If the condition is not met the library does not attempt to correct it, unless the object is in a closed openable transparent container, in which case the library will attempt to open the container to allow the action to proceed,
The last of these PreConditions is very common. It also has a couple of additional complications, in that it queries the verifyReach(obj) and checkReach(obj) methods of the object that needs to be touched (the obj method in these methods actually refers to the actor whose trying to do the touching). verifyReach(obj) can optionally add further verify conditions (specified just as for a normal verify method). checkReach(obj) can then apply further checks; if checkReach(obj) displays anything (presumably a message saying why the object can't be touched), then the action will not be allowed to go ahead.
This incidentally highlights the point that a PreCondition has a verify stage (when it can apply further verify results) and a check stage (which is normally the stage at which it would attempt any implicit actions, though touchObj is a bit of an exception here).
You can extend the list above by defining your own PreCondition objects. If you want to enforce some set of conditions that's beyond the capabilities of the library PreCondition objects listed above, you can usually do so by creating a custom PreCondition. For details on how, see the separate article on Custom Preconditions.
Check
As we discussed back in the verify section, the verify routine is for determining how logical an action seems from the player's perspective, irrespective of whether or not the action is actually possible or meaningful within the game world. When we want to disallow an action for reasons that wouldn't be obvious to the player, we wait until the "check" method to halt the command.
The "check" routine can do one of two things: it can let the command proceed, or it can display an error message to stop the action.
If the "check" routine wants to stop the action, it can simply display a message directly, using a double-quoted string. The parser automatically considers the action to be a failure if the "check" routine displays any text (unless it also calls the noHalt() function).
Here's a sample "check" routine. This implements a piece of clothing that we don't want to allow the player to remove - it's not important to the game to let the player character undress, so we want to simply disallow it. However, it's perfectly logical from the player's perspective to try the action, so we don't want to make it illogical; this is why we must handle it in "check" rather than in "verify."
dobjFor(Doff) { check() { "It would be highly unladylike to get undressed away from the privacy of one's own bed chamber. "; } }
Action
The "action" routine is where the body of the action is carried out. If we've made it this far, we know the action is logical, that it's passed all of its pre-conditions, and that we've made it past any "check" conditions.
In addition to actually carrying out the action, this routine may generate a message to describe what happened. The easiest way to do this is to display a message directly, using a double-quoted string:
dobjFor(Open) { action() { makeOpen(true); "Okay, {the subj dobj} {is} now open. "; } }
Often, though, it will be better to display the message at the Report stage.
In addition, the library defines several macros that you can use instead of showing a message directly.
reportAfter(msg) This adds a report that'll be displayed after the Report stage (in other words, after all other reports). This could be used, for example, to display the side-effects of an action after it has been routinely reported as taking place. The msg parameter would normally be supplied as a single-quoted string.
extraReport(msg). This cas be used to display a piece of introductory text that won't suppress the default message at the Report stage. This probably isn't needed much but may occasionally needed to provided some piece of parentheic information. For example it is used by the library to announce which key it has chosen (e.g. ("(with the brass key)") in response to a LOCK or UNLOCK command:
actionReport(msg1, msg2?). This generates different reports depending on whether the action was carried out implicitly or explicity. If the action was carried out in response to an explicit request to carry out the action for which we're implementing the Action stage, e.g., TAKE VASE then msg1 will be displayed at the action stage. If, on the other hand, the action is being carried out implicitly, e.g. '(first taking the vase)' after PUT VASE ON TABLE, then msg2 will be displayed after any implicit action announcements (provided we've supplied the msg2 parameter), otherwise further message will be displayed at this point). So, for example, we might define:
dobjFor(Take) { ... action() { actionReport('You pick up the delicate vase very carefully indeed. ', 'You were, of course, very careful when you picked up the delicate vase. '); inherited(); } }
This avoids the messy output that could ensue from using a double-quoted string in the action() method, which could result in an illogical looking response such as:
>PUT VASE ON TABLE You pick up the delicate vase very carefully indeed. (first taking the vase)
It follows that we should always use actionReport()
in an action method rather than displaying text directly there if there's any chance that the action might be carried out implicitly. (Since adv3 has a mainReport()
function that's used in a similar context, in adv3Lite you can, if you wish, use mainReport()
as a synomym of actionReport()
; in adv3Lite mainReport(msg)
is simply a macro that expands to actionReport(msg)
).
Report
The purpose of the Report phase is to provide a brief default confirmation that the requested action has taken place, for example:
>TAKE TENNIS BALL Taken
The Report phase can also aggregate the reports resulting from applying the same action to a number of objects, e.g.:
>TAKE SOCK AND BALL Taken. >DROP ALL You drop the odd sock and the tennis ball. >TAKE ALL You take the old coat, the large red box, the small green book, the small blue box, the odd sock, and the tennis ball.
We supply this aggregation by using the macro gActionListStr, which by the time we reach the report stage will expand to a suitably formatted list of all the direct objects our command has just acted on. For example, a simplified version of the action and report stages for the TAKE action might be:
class Thing: Mentionable dobjFor(Take) { action() { actionMoveInto(gActor); } report() { say ('Taken. | You take <<gActionListStr>>. '); } } ;
The vertical bar | divides the brief response from the more elaborate one. The brief one is used when the player has specified which object or objects they want the command to act on (such as SOCK AND BALL) so only needs a brief acknowledgement that the command was successful. The more elaborate one is used for commands such as TAKE ALL where the player hasn't been so specific and needs to be informed what objects the command has affected.
The report routine is only run on the last of a series of objects involved in any one command (of course many commands act on only one object, in which case this is the object whose report() method will be executed). The report() phase is not run at all if either of the following conditions is true:
- The action is an implicit action.
- There's nothing left to report on, either because no objects made it to the action stage, or because the action stage has already reported on the action for every object involved in the command.
This brings us to another important point: if an action is reported on at the action stage, it will not also be reported on at the report stage for the same object.
It also brings us to the question, when should we handle displaying the outcome of an action at the action stage, and we should we leave it to the report stage? We should display our response to an action at the action stage either if (1) something exceptional happens in (or we at least want to say something out of the ordinary about) the effect of a particular action on a particular object or (2) the display of some informative text is the whole point of the action (because it's an action like EXAMINE, READ, SMELL or FEEL which requests information about a particular object rather then trying to change its state). In such cases we don't want EXAMINE FOO to be met with 'Examined' or 'You examine the foo'. Otherwise, it's normally best to let the report stage handle it.
Library Messages
This section is mostly for people interested in the internal technical details of the library, for people translating the library, and for authors of library extensions. Game authors will not usually need to know how the library messages mechanism works.
In most cases, game authors will want to write the text of their messages directly within the game code. For the library, though, interspersing messages directly within code would be a problem, because it would require library translators to edit virtually every library module, and to search laboriously through the entire library looking for messages to translate. It would also create a huge merging problem as the library evolves: for every new version of the library, a translator would have to repeat the process from scratch on every module in the library that was updated. For its own messages, then, the library needs to separate the message text from the main library code.
The library separates message text from program code via one or more of the following mechanisms (often in combination):
- Message text relating to responses to actions is usually defined of properties or methods of the objects (or rather the classes of objects) involved in the actions. For IActions and SystemActions which typically don't involve such additional objects, properties or methods of the Action object may be used instead.
- Exccept in the language specific parts of the language
(such as in english.t) these messages are defined using the
DMsg()
andBMsg()
macros (the first displays the message, the second creates a single-quoted string that can be used elsewhere; for the full story consult the section on Messages in the Adv3Lite Library Manual). - In the language-specific parts of the library some messages or message fragments may be implemented as literal text.
For example, the actual library implementation of the report
stage of dobjFor(Take)
in the advLite library is:
report() { DMsg(report take, 'Taken. | {I} {take} {1}. ', gActionListStr); }
While the verify stage with its associated messages are defined as:
dobjFor(Take) { verify() { if(!isTakeable) illogical(cannotTakeMsg); if(isDirectlyIn(gActor)) illogicalNow(alreadyHeldMsg); if(gActor.isIn(self)) illogicalNow(cannotTakeMyContainerMsg); if(gActor == self) illogicalSelf(cannotTakeSelfMsg); logical; } ... } cannotTakeMsg = BMsg(cannot take, '{The subj cobj} {is} fixed in place. ') alreadyHeldMsg = BMsg(already holding, '{I}{\'m} already holding {the dobj}. ') cannotTakeMyContainerMsg = BMsg(cannot take my container, '{I} {can\'t} take {the dobj} while {i}{\'m} {1} {him dobj}. ', objInPrep) cannotTakeSelfMsg = BMsg(cannot take self, '{I} {can} hardly take {myself}. ')
These messages can be customised either by overriding the various xxxMsg properties on classes or individual objects, or by means of a CustomMessages object.
Varying messages by actor
The adv3Lite library makes no specific provision for varying
messages by actor beyond using message substitution parameters
such as {I}
and {myself}
, which the library will replace
with the name or pronoun appropriate to the current actor, and by always using the more verbose
form of a message when the actor is not the player character. Game authors can, however,
implement greater variation of messages by actor by use of CustomMessages objects, for
example:
bobActionMessages: CustomMessages [ Msg(report take, '{I} grudgingly pick{s/ed} up {1}. '), Msg(report drop, '{I} disdainfully {put} down {1}. } ' ] active = (gActor == bob) ;