MegaTADS
Version 1
By Krister Fundin
Introduction
MegaTADS is a collection of extensions and patches for the adv3 library. Many of these are small enough that releasing them separately would have been too much of a hassle. By bundling them together in this form, the result is also more efficient and less space-consuming. MegaTADS is structured so that it mirrors the adv3 library. All changes pertaining to the Thing class, for instance, are stored in a file called mThing.t, which is analogous to the thing.t file in adv3, and though there might be several patches to the Thing class, only a single modify statement is required.
MegaTADS is also meant to be a breeding ground for patches that might eventually make it into the standard library. The extension thus serves two additional purposes. First of all, developers get a chance to use certain patches before the next official TADS 3 version is released, and as a consequence, these patches get a chance to be tested more thoroughly before being made official. If any bugs or limitations are discovered in this way, then they can be addressed earlier, possibly making the official TADS 3 releases more stable.
To start using MegaTADS, we add the library file megatads.tl to our project. Some components may also benefit from the inclusion of the mega_en_us.h header file, but note that this must be included after en_us.h.
Turning components on and off
MegaTADS consists of many separate components, and not everyone is likely to need or even want to use all of them. One or two might cause trouble in a particular project, and it would be unfortunate if that made it necessary to stop using MegaTADS completely. Therefore, we allow individual components to be turned on and off separately. This is done by using a number of different preprocessor symbols. (These can be defined by using the -D option when compiling from the command line, or by adding them to the list of defines when using TADS Workbench.)
By default, most components are active. The only ones that aren't are those which are a bit experimental or which are known to contain bugs that might make them unusable. To turn off a specific component, we can define a symbol along the lines of MEGA_NAME_OFF, where the name identifies the component, and to turn on one of the experimental components, we can use the similar MEGA_NAME_ON symbol. Each component has a single-word name, and these are all given in the list of components which follows further on in this manual.
One more thing we can do is to turn all components off. This is done by defining the MEGA_ALL_OFF symbol. Then we can turn on only those components that we want to use. An explicit MEGA_NAME_ON overrides MEGA_ALL_OFF, and both of these override the defaults.
The following chapters detail the various components. The header for each chapter gives the name of the component and its default on/off status. If, for instance, we wanted to turn off the Unactor component, which is on by default, we would define the MEGA_UNACTOR_OFF symbol. If we wanted to turn everything except the grammar component off, then we would define MEGA_ALL_OFF along with MEGA_GRAMMAR_ON.
Index of components
Component | Short name | Default status |
Understanding things before or after an ordinary command | COMMAND | Off |
Miscellaneous grammar additions | GRAMMAR | On |
Uniform handling of interjections | INTERJECT | On |
Locational qualifiers | LOCATIONAL | On |
A patch for the Passage class | PASSAGE | On |
Extended punctuation | PUNCT | On |
SayTopics | SAYTOPIC | On |
The SpareWords class | SPARE | On |
A patch for the TIAction class | TIACTION | On |
The Unactor class | UNACTOR | On |
A generic USE command | USE | On |
Understanding things before or after an ordinary command
Short name: COMMAND
Default status: Off
With this component, it is possible to define various phrases that the player can put before or after a command without changing its meaning in any way. The point of this is mainly to allow the parser to understand more “chatty” commands, as could perhaps be expected from an unexperienced player.
A couple of phrases are already understood by default when this component is active. Commands can be pre- or suffixed with PLEASE; a few adverbs are understood and ignored, and some other words like AGAIN and ANYWAY can be put after a command.
New phrases can be added by defining a grammar statement either of the kind beforeCommandPhrase or afterCommandPhrase, as in the following example:
grammar beforeCommandPhrase: 'go' ('forth' | 'hence') 'and' : BasicProd ;
This will allow the player to type GO HENCE AND PICK UP THE RED BOOK, for instance.
By default, this extension is not active, since it contains a rather serious bug. When active, it causes the parser not to understand orders directed at NPCs unless a non-empty beforeCommandPhrase is used. (It's not clear at this point whether this represents a problem in the component itself or in the implementation of the parseTokens() method.)
Miscellaneous grammar additions
Short name: GRAMMAR
Default status: On
This component contains some additions and modifications to the standard English parser shipped with TADS 3. A brief summary of the changes follows:
- When asking for disambiguation or for a missing object, an answer such as NOTHING is now accepted and will cancel the action, generating a brief note about how a new command can be entered instead of answering the question.
- In addition to ALL THE COINS, we can now type EVERY COIN and some variations.
- The demonstratives THIS, THAT, THESE and THOSE are understood, and are also accepted as pronouns. Furthermore, THEM is now allowed as a definite article, as in GET FIVE OF THEM APPLES.
- Directional phrases have a few more synonyms: GO TO THE NORTH is accepted, as well as GO EASTWARDS and GO SOUTH-EAST with a hyphen.
- Additional synonyms and phrasings have been added to several of the standard commands, including some unorthodox constructions such as USE THE KEY FOR UNLOCKING THE DOOR.
There are also a couple of changes which aren't related to the grammar, but which still affect parsing in some cases:
- The isDirectlyIn() method of a Thing now returns true if the identity object of the thing's actual location is the same as the container asked about. In practice, this means that the contents of a ComplexContainer's various subXxx locations are considered immediate contents of the ComplexContainer itself.
- When verifying the TakeFrom action, it is now sufficient if the direct object is nominally inside the indirect object. This means that commands such as TAKE THE STONE FROM THE GROUND should work.
Uniform handling of interjections
Short name: INTERJECT
Default status: On
This component removes all the so-called conversational intransitive actions from the library and replaces them with a more uniform system. The actions in question are HelloAction, GoodbyeAction, YesAction and NoAction. The immediate effects of this new system is that a few more syntaxes are allowed for these actions. Defining an interjection will make sure that these phrasings are automatically recognized:
- BRAVO
- BOB, BRAVO
- SAY BRAVO
- SAY BRAVO TO BOB
- TELL BOB BRAVO
Without this component, only the first three forms are regognized. Also, becuase the SAY form is now treated separately, a command such as TELL BOB TO SAY YES will be parsed differently. It used to be equivalent to BOB, YES (I.E. as the player saying so to Bob), but is now treated as an order (I.E. telling Bob to say so to someone unspecified).
Note that it is a good idea to use the SayTopic component in conjuction with this one, since this allows for another two phrasings to be understood:
- SAY “BRAVO”
- SAY “BRAVO” TO BOB
Alas, actually defining a new interjection such as BRAVO is not made all that much easier by this component, so the manual won't go into this.
Locational qualifiers
Short name: LOCATIONAL
Default status: On
The TADS 3 parser already recognizes commands where objects are referred to by giving their location. We can type TAKE THE HAT WHICH IS IN THE HATBOX, for instance. This component expands on this functionality in two ways: it makes it possible to decide on an object-by-object basis whether they are in or on or under or behind other objects, and it makes it possible to add new locational qualifiers. Previously, only IN and ON and some synonyms were recognized, but this component also adds UNDER and BEHIND to the list.
Sometimes, we may want to describe an object in relation to another object while still keeping the two separate from each other. We can say that the radiator is under the window, but we don't want to make the window an Underside just because of this. Still, it would be nice if the parser could understand THE RADIATOR UNDER THE WINDOW. This component introduces a method call isNominallyInWithContType(), which we can use for this purpose. The definition of the radiator would look something like this:
radiator: Fixture isNominallyInWithContType(obj, contType) { /* for parsing purposes, we are under the window */ if (obj == window && contType == underContType) return true; else return inherited(obj, contType); } ;
For objects with contents or components, we can also decide how these can be referred to by overriding the contTypes property on the container. This is a list giving all valid containment types for this object. For ordinary Thing objects, the list includes inContType, onContType and genericContType. (The last one applies to some prepositions like AT and BY.) Containers, Surfaces, Undersides and RearContainers override this property to include their respective containment types.
Finally, we can also add completely new containment types. This involves just a few steps. We'll have to define a ContainmentType object, then add a grammar production for matching the locational phrase itself. After that, the new containment type can be used just like the other ones described above. Here's an example of a containment type in English for something which is to the left of something else:
leftContType: ContainmentType 'to the left of'; grammar locationalPrepPhrase(left): ('to' 'the' |) 'left' ('of' | 'from') : BasicProd contType = leftContType ;
Note that the template for the ContainmentType class is defined in the mega_en_us.h header file, which must be included for this code to function.
A patch for the Passage class
Short name: PASSAGE
Default status: On
This is a small patch that fixes a certain problem in the adv3 library. If two rooms are joined by a door or such, and a SenseConnector brings the other room into view when the door is open, then both sides of the door will be in scope, which confuses the parser. Sometimes, a command involving the door will be directed at the wrong side, and a message along the lines of “you can't reach the door through the door” is displayed.
When this component is active, the far side of the door (or any passage) will be removed from scope by the near side.
Extended punctuation
Short name: PUNCT
Default status: On
This component makes some minor changes to how various sorts of punctuation are handled. In summary:
- The ampersand is allowed in most places where AND was previously allowed.
- A question mark can end a sentence, just as a period or an exclamation already could.
- The parser won't be confused by seeing several punctuation marks in sequence. For instance, a command such as WAIT... won't yield an error anymore.
SayTopics
Short name: SAYTOPIC
Default status: On
This component adds a new kind of TopicEntry which can match literal text spoken to an Actor using the new SAY command. A SayTopic is defined much like other kinds of topics, except that it cannot be used with objects — only regular expressions. Here's a quick example:
+ SayTopic 'xyzzy' "<q>That's the magic word all right,</q> says Bob. " ;
This topic will be invoked if the player types SAY “XYZZY” or ANSWER XYZZY TO BOB, for instance. There's also a DefaultSayTopic, which matches any literal for which there is no specific match.
This component also works together with the interjection component, if it's active, so that interjections can be quoted.
The SpareWords class
Short name: SPARE
Default status: On
There is a potential problem that can show up if we combine dynamically created objects, the standard TADS 3 error messages for unknown words, and a perfectionist author. Let's say that our story involves frogs. We have a Frog class, and at various points we create instances of this class. The problem (if we want to think of it as a problem, which we don't have to) is that the player could type a command involving the word “frog” at a time when no frog is actually present, in which case she will by default be told that the word “frog” isn't necessary in the story. Apart from being an obvious lie, this could also give the player access to some meta-information that she's not really meant to have, I.E. the ability to determine whether or not there are any frogs in the story at a given time.
The solution to this isn't very complicated. We just have to create a VocabObject somewhere and add the word “frog” to it. The SpareWords class is meant to make this a bit easier, especially if we have many classes from which we create objects dynamically during the story. A SpareWords object can define a property called classes, which should hold a list of classes to create permanent dictionary entries from. In our Frog case, all we have to do is this:
SpareWords classes = [Frog] ;
We could add more classes to the list, in case we also had some squirrels and butterflies in our story. A bit easier, though, would be to use a common superclass for all of these, and then use the superClasses property instead:
SpareWords superClasses = [Animals] ;
This way, we can add more kinds of animals without having to worry about updating the SpareWords object.
A patch for the TIAction class
Short name: TIACTION
Default status: On
This patch fixes something which could be regarded as a problem with the TIAction class. If the player enters a command which resolves to a two-object action, but where only one noun phrase is specified, and that noun phrase refers to something which is not in scope, then the parser will sometimes ask for the missing object only to reject the command after getting the answer.
With this patch, the non-empty noun phrase is always resolved first, so that we avoid asking for a missing object when the command would be rejected anyway.
The Unactor class
Short name: UNACTOR
Default status: On
In many stories, NPCs have a tendency to move about, sometimes frequently. A person can be present at one time and have left a few turns later, possibly without the player realizing it, and what's more is that the error messages for unknown objects aren't that well suited for people with proper names:
> TALK TO BOB You see no bob here.
As a potential solution to this problem, a new class named Unactor is offered. This is a sub-class of Unthing, and it works mostly in the same way. The only differences are that the Unactor remains in scope everywhere, as long as the actor it's connected with is either known or seen, and that the notHereMsg is customized based on the same information that is kept in order to allow following an actor. There are three variations:
Bob isn't here. Bob has left. The last place you saw Bob was in the kitchen.
The first one is printed if we simply don't know where Bob is; the second one is printed during circumstances where it would normally be possible to type FOLLOW BOB, and the third one is printed if we have previously seen Bob someplace but have since gone elsewhere.
To set up an Unactor object, we can use the template which is defined in the mega_en_us.h header file. It consists of a vocabWords string followed by the target actor prefixed by an @ sign (see the example below).
Note that it may be necessary to override notHereMsg to account for other situations that arise during the course of the story. If Bob should die, then that fact ought to be reflected by this message. We may also want to individually change the three standard messages. This can be done by adding the properties unactorNotHereMsg, unactorHasLeftMsg or unactorLastSeenMsg to the Unactor object. For reference, here is an example of an Unactor that overrides all of these in some way:
Unactor 'bob/bobby' @bob notHereMsg() { if (bob.isDead) return 'Bob has past away. '; else return inherited(); } unactorNotHereMsg = 'You don\'t know where Bob is right now. ' unactorHasLeftMsg = 'Bob just left. You\'d better go after him. ' unactorLastSeenMsg(who, srcLoc) { switch (srcLoc) { case kitchen: return 'Bob? He was in the kitchen when you last saw him. '; case livingRoom: return 'As far as you know, Bob is still in the living room. '; default: return inherited(who, srcLoc); } } ;
A generic USE command
Short name: USE
Default status: On
This component defines a UseAction and a UseWithAction. On most objects, the USE command will result in a request for the player to be more specific about how to use the object in question. Some classes, though, remap the USE command to other actions, since they typically have a single, most obvious use. A button, for instance, is likely to be pushed, so the Button class remaps UseAction to PushAction. Another example is the Key class, where USE KEY ON DOOR will either lock or unlock the door with the key.