If you have any Lockables in existing code that you specifically intended to start out unlocked, and you didn't explicitly set initiallyLocked to nil in those objects, you'll have to do so now.
This is a list of changes to adv3, the TADS 3 library. Changes are listed in reverse chronological order (the most recent changes are first).
Released 8/17/2006
In version 3.0.9, gameMain.verboseMode was changed from a simple true/nil property to an object of class BinarySettingsItem. Any existing game code that attempted to turn verbose mode on or off by setting gameMain.verboseMode to true or nil will now encounter a run-time error the first time the player enters a travel command.
If you want to set the default verbosity mode explicitly in your game, you can't do it any more by setting gameMain.verboseMode to true or nil. Instead, you can add a line like this to a start-up routine, such as gameMain.newGame():
gameMain.verboseMode.isOn = true; // turn on verbose mode
Note that the verbosity mode is now part of the "global preferences" mechanism, so in most cases it's best for games not to change it explicitly, instead leaving it up to the player to decide on the setting. In the past, some authors liked to set a verbosity mode that they felt was most suitable for the game. Now that the player can specify a set of default preferences that they wish to apply to all games, it's better for authors not to presume to change the player's default settings without a good reason. As with any other rule, there are bound to be exceptions, so if you have a really good reason to override the player's preferences then you should feel free to do so. But if you're tempted to override the player's preferences just because you like it a particular way, you might want to reconsider.
If you have any Lockables in existing code that you specifically intended to start out unlocked, and you didn't explicitly set initiallyLocked to nil in those objects, you'll have to do so now.
The default comment prefix is now an asterisk ("*"). You can change this to any prefix string you'd like by modifying the commentPrefix property of the commentPreParser object. You can also control whether or not leading whitespace is allowed before the comment prefix (it is by default) by modifying commentPreParser's leadPat property.
Using NOTE as the comment prefix was problematic because NOTE is a common enough word that games often want to use it in an object's name, and this creates situations where a user might want to start an input line with NOTE with the intention of referring to an object, not of entering a comment. It was essentially impossible in some of these cases to reliably determine which the player meant. The new approach avoids these problems by using syntax that should be unambiguous in nearly all games.
In the past, there was a StringPreParser object in the English library that helped the NOTE command by quoting the note text in some cases. This preparser has been removed, and a new preparser has been added in its place, but this time in the general library, in misc.t. The new object is called commentPreParser, and it performs all of the comment handling itself, without the need for a separate NOTE action.
As part of this change, NoteAction and NoteIAction have been eliminated. In the unlikely event that you used 'modify' to change the behavior of these actions, you'll have to rework your code. Look at the commentPreParser object for details of the new arrangement.
This shouldn't affect existing game code, except for cases where you refer directly to conversationManager.convNodeTab. You should scan your source for occurrences of "convNodeTab" and change any that you find to "actor.convNodeTab", where "actor" is the Actor who owns the table. The most likely place for game code to refer to convNodeTab is in a "modify conversationManager" definition.
It's very unlikely that this change will affect any existing game code, since any game code that overrides this method almost certainly did so to add something to the scope. However, you do a quick search through your game code for getExtraScopeItems(), and make sure that you don't have any nil returns; if you do, simply change them to empty lists ("return [];").
This change corrects an inconsistency that occurred in cases where an object had fixed-in-place contents that themselves had contents. In the past, directly examining such an object didn't mention anything about the contents of the second-level containers, while a simple LOOK did include the inner contents. The change makes the two cases consistent.
This change should have little or no effect on existing code, since the former getListedContentsInExamine() method is an internal method that's unlikely to have been called or overridden in game code. If you overrode this routine, though, you'll need to apply the name change to your code, and make any adjustments for the slight change in the method's semantics.
In the past, equivalence was based on the superclass of the object. Two objects were equivalent if (a) they both had isEquivalent set to true, and (b) they had identical superclass lists. This approach to equivalence was occasionally problematic, particularly when using "proxy" objects to modify the behavior of objects involved in equivalence groups. In addition, the superclass approach was somewhat counterintuitive: the whole point of the equivalence mechanism is to treat objects with identical names as interchangeable, but using superclasses as the basis of the equivalence had nothing to do with the naming.
The new scheme is based on a new property called equivalenceKey, which is defined in the language module. In the English library, the default setting of this new property is the disambigName of the object. So, equivalence groups are now simply based on the basic disambiguation name of the object. This is much more consistent with the disambiguation mechanism itself, because it means that the parser decides whether objects are interchangeable using essentially the same logic that the player uses intuitively: if the game always refers to two objects by the same name, they're interchangeable.
Another advantage of the new scheme is that it's much more customizable than the old scheme. Since the basis of the equivalence decision is now distinguished as a separate property (equivalenceKey), you can control equivalence groups simply by overriding the property. You could even effectively restore the old scheme by defining equivalenceKey as getSuperclassList() - this would make the immediate superclass list of an object the basis of its equivalence grouping, producing the same behavior as in the past.
Note that if you change an object's equivalenceKey dynamically during play - or if you change its underlying disambigName property - the object's listing group won't be automatically updated. If you do make such a change and you want to update the object's listing group, just call initializeEquivalent() on the object. (Under the old scheme, there was really no way to change an object's grouping - even if you changed its name, it was still grouped based on its class, which could have caused strange results in listings. The new scheme at least allows for this kind of change, although it requires this manual step to keep the list grouping settings in sync.)
There are two reasons for this change. First, when the player explicitly types EXAMINE room name, it's somewhat redundant to include the room name in the output. Second, when the player types EXAMINE ALL or EXAMINE list of things, the standard multi-object-command output format already prefaces the results for each item in the list with the name of the item ("kitchen:"), which made the room name line especially redundant in this case.
To define multiple CollectiveGroups for a given object, use the new property 'collectiveGroups' on the objects that you want to associate with the groups. Set this property to a list with the CollectiveGroup objects to associate with the given object.
You can still use the single-valued 'collectiveGroup' property, so existing game code will continue to work unchanged. However, 'collectiveGroup' is now obsolescent, so you should not use it for new code - use 'collectiveGroups' instead. For compatibility with existing code, the library's default definition of Thing.collectiveGroups is a method that returns an empty list if collectiveGroup is nil, or a one-element list containing the collectiveGroup value if the value is not nil. Since this is slightly ineffecient, support for the old 'collectiveGroup' will probably eventually be removed, at which point the default for 'collectiveGroups' will simply be an empty list.
The new classes work just like RestrictedContainer did, and RestrictedContainer itself hasn't changed its behavior (it's been refactored slightly for the new base class, but this is just an internal change that shouldn't affect any existing code). To accommodate the new types of restriction, suitable new library "player action" messages have been added (cannotPutOnRestrictedMsg, cannotPutUnderRestrictedMsg, cannotPutBehindRestrictedMsg).
First, Openable adds an objOpen precondition for Search if the actor isn't inside the object; for LookIn, this is skipped if the object is transparent.
Second, in the Search check() method, Container requires that the object be openable or transparent to the "touch" sense, or that the actor is inside the container. LookIn is similar, but only requires that the container to be non-opaque in the "sight" sense.
The reason for these changes is that Search implies a more thorough, physical examination than LookIn does. To most people, an explicit search involves physically poking through an object's contents, while "look in" is more passive, implying just having a look inside.
There are two reasons why the method returns the canonical format rather than the full text of the entire command. First, the full text is already readily available, via gAction.getOrigText(), so there's no need for a new method to retrieve this information. Second, and more importantly, the canonical format isoaltes the verb phrase structure of the player's input, independently of any noun phrases, making it easy to determine exactly which verb phrasing the player actually used.
This method is most useful in cases where the library's verb rules define two or more different phrasings for the same Action, and you need to be able to distinguish exactly which variant the player entered. For the most part, this is unnecessary: when the library's verb rules include synonyms, it's because the different phrasings usually have exactly the same abstract meaning, hence it's enough to know which Action matched the grammar. In some cases, though, a particular verb applied to a particular object has an idiomatic meaning different from the usual meaning for other objects, and in those cases the generic synonyms often fail to be idiomatic synonyms.
For example, the library defines "get (dobj)" and "take (dobj)" as synonyms for the Take action, because GET and TAKE can almost always be used interchangeably. However, if you were defining an "aspirin" object, you might want to treat the command "take aspirin" as meaning "eat aspirin," but you would still want "get aspirin" to mean "pick up aspirin." You could handle this by overriding the dobjFor(Take) action() method on the aspirin object, comparing gAction.getEnteredVerbPhrase() to 'take (dobj)', and calling replaceAction(Eat, self) if it's a match.
In the past, the library simply used the libMessages object directly as the source of these messages. Now, the library instead uses the current value of gLibMessages as the message source. The default value of gLibMessages is libMessages, and the library never changes this value itself, so the default behavior is exactly the same as before.
The purpose of this change is to allow a game to switch to a new set of library messages dynamically during play. For example, if your game is structured into chapters with different points of view, you might want to use a distinctive writing styles for the different chapters, in which case you'd want to change all of the library messages at each chapter transition. To do this, simply set gLibMessages to refer to a new object at each point when you want to switch message sources; subsequent messages will come from your new source object. Note that it's not necessary to do this if you only want to customize messages statically (i.e., throughout the entire game); for that, you can just 'modify libMessages'.
In addition, showing or giving an Actor to itself is now handled with a separate message ("Showing X to itself would accomplishing nothing").
This change corrects a problem that occurred when a thrown object landed in a BulkLimiter that was already near capacity. In the past, the BulkLimiter applied its capacity control by effectively blocking the THROW before it happened. With the change, the THROW will be allowed to proceed, and the thrown object will land in the nearest suitable container of the original target.
Note that any game code that overrides getHitFallDestination() should be sure to call adjustThrowDestination() on its tentative return value. Games usually won't have any reason to override getHitFallDestination(), so this change shouldn't affect most existing code.
DeadEndConnector supplements FakeConnector, which is most useful for exits that look like connections but can't be traversed for reasons that are apparent before the travel is ever physically attempted - in particular, for motivational reasons ("You can't leave town without finding your missing brother"). DeadEndConnector differs from FakeConnector in that DeadEndConnector acts as though the physical travel were actually occurring. It fires all of the normal travel notifications, so any side effects that would occur on ordinary travel will also occur with a DeadEndConnector.
In the past, the object being pushed wasn't moved to its new location until after the new location had been described. The point of this sequencing was to exclude the pushed object from the new location's description, simply by keeping it out of the new location until after the description was displayed. This exclusion is desirable because it would otherwise look as though the pushed object were already in the new location on the player character's arrival, which would be confusing. However, it prevented any side effects of the pushed object's presence from being taken into account in the new location's description. If the pushed object was a light source, for example, and the new location was otherwise unlit, the new location was described as dark.
Now, the library tentatively moves the pushable object to the destination of the travel, and marks the object (via the new Actor.excludeFromLookAround mechanism) for exclusion from the location description. It then moves the actor and shows the description of the new location. Finally, it moves the pushable back to the origin location, and then moves a second time, this time for real, to the destination location.
(The reason for moving the pushable twice - first tentatively, before the travel, then again "for real," after the travel - is that the method that makes the final move is overridable and so might actually do something other than move the pushable to the travel destination. Even if it leaves the object unmoved, though, or moves it to a different final destination, the tentative first move is still a valid intermediate step. At the point we're generating the description of the new room, the player character is in the process of pushing the pushable into the new room - the PC is notionally walking along with the pushable at that stage. If the overridable final-move method wants to do something different, it can do so; it will simply have to describe the change, which it had to do in the past anyway. At that point, the PC will already be in the new location, and so will in fact have pushed the pushable this far; anything that happens in the overridable method happens after the intermediate stage where we generated the description, so any side effects of the pushable's tentative presence were valid at that point, no matter what happens afterwards.)
In the past, the local NPC travel mechanism did everything via the "local arrival" message: the NPC generated only this one special message at the destination end of the travel. The new mechanism adds two messages analogous to the local arrival message: a "local departure" message and a "remote travel" message. Here's how they're used:
These new rules obviously require some new methods. Here's the new arrangement of methods:
In addition, when a RoomPart has an explicit 'location' setting, it will now automatically add itself to that location's 'roomParts' list. This means that you don't have to manually set both properties, which saves a little work and also makes your game easier to maintain, since you won't have to remember to make coordinated changes to both settings in your source code if you change the room part later.
In cases where you create a RoomPart or an Enterable or Exitable that has fine-grained details that would be too small to see at a distance, and the object is visible from a separate top-level location linked by distance, you might want to override this to set the sightSize back to medium. When there's no distance-linked top-level location, this shouldn't be an issue, since there'd be no way to examine the object from a distance to begin with.
In the past, RoomPartItem overrode useSpecialDesc and useInitSpecialDesc (setting them to nil) in order to prevent room part items from being included in LOOK descriptions, but this had the bad side effect of preventing showSpecialDesc() from showing the initSpecialDesc. This change uses the more precise methods to select exactly where the special desc should be shown, without affecting the selection of which special desc to show.
(This new flexibility involves two supporting changes. First, the various initForMissingXxx() methods in various Action subclasses now detect that the iteration over the object list hasn't begun yet, and they simply retain the entire object list (rather than the current iteration item, as they did in the past) for the retry. Second, to support the first change, the class PreResolvedAmbigProd now accepts an entire object list instead of just a single iteration element. These are internal methods that game code is unlikely to call directly, so the only visible effect of these changes should be the new flexibility in how the "retry" methods can be used.)
Now, initializeThing() is only called once. The Thing constructor now tests a flag before calling initializeThing; it only calls the method if the flag isn't set, and it sets the flag after calling the method. This ensures that subsequent inherited constructor calls simply skip the call to initializeThing(). The constructor also skips the call to its own inherited base class constructor when the flag is set; this ensures that the vocabulary initializations in VocabObject are only invoked once per object.
BoredByeTopic and LeaveByeTopic extend the hierarchy that already existed with ByeTopic and ImpByeTopic. If there's an active ImpByeTopic and no active BoredByeTopic or LeaveByeTopic objects at the time of an implied "goodbye", the ImpByeTopic will be used for both cases (this also happens to be exactly the way the library worked in the past, before the two new subclasses were added, so this change won't disturb existing code). If there's an active BoredByeTopic as well as an active ImpByeTopic, the BoredByeTopic will be selected over the ImpByeTopic to handle "boredom" goodbyes; likewise, if there's an active LeaveByeTopic, it will be selected over an active ImpByeTopic to handle goodbyes triggered by the PC's departure.
Note that the ActorState method getImpliedConvState() is new with this change.
The main result of this change is that a few of the low-level parser message methods - askUnknownWord, specialTopicInactive, commandNotUnderstood - are now invoked on the target actor when one is present in the command. In the past, because the parser didn't even figure out that a target actor was present in these cases, these methods were always invoked on the player character actor. With this change, it's now possible to customize a NPC's responses for unknown words and command phrasings individually by NPC.
To implement this, Action.afterActionMain() checks to see if the action failed, as indicated by a 'reportFailure()' message in the course of the action handling. If the action failed, and gameMain.cancelCmdLineOnFailure is set to true, afterActionMain() throws a CancelCommandLineException. This exception is in turn caught in executeCommand(), which cancels any remaining commands on the command line and simply proceeds to the next turn.
In addition, the new playerMessages method explainCancelCommandLine() lets you display an explanation when a command line is canceled due to the failure of a command. executeCommand() invokes this new method when it handles a CancelCommandLineException if there are in fact any remaining tokens on the command line. (This check for remaining tokens skips the explanation when there's nothing left on the command line to cancel, as the cancellation obviously has no effect in such cases.) This new message method doesn't display anything by default; it's just a hook that you can use if you want to provide an explanation. Note that you'll probably want to show this explanation only once per game, rather than every time a command is canceled (you can use a flag property to do this - if the flag is nil, show the message and set the flag to true; otherwise skip the message).
The default setting for gameMain.cancelCmdLineOnFailure is nil. This provides the traditional handling, which simply continues executing any remaining commands on the command line even when an action fails.
The reason this new feature is an option rather than simply being the default new policy is that neither possibility is ideal in every case. On the one hand, continuing to execute commands after an action has failed can lead to confusing results: the failure of the earlier command can leave things in a state other than what the player was anticipating, which could change the behavior of subsequent commands. So the argument in favor of canceling remaining commands is that it avoids this possible source of player confusion by halting processing once it appears that the player's thinking is out of sync with the game's internal state. On the other hand, exactly what constitutes failure might not always be apparent to the player, so halting halfway through a command line might sometimes appear arbitrary to the player, or even buggy. The argument in favor of continuing to plow through the rest of the command line like nothing happened, then, is that it's simple and consistent. A prime virtue of any UI is predictability, so that the user has an easier time forming a working mental model of the software, and the most predictable behavior is to unconditionally execute everything on the command line. Further, given that multi-level UNDO is available by default in tads3 games, any unintended effects from extra commands can always be manually undone as soon as the player realizes what happened. The arguments on both sides are valid, so the library leaves it up to the author to set the game's policy. (It's even been suggested that this ought to be left up to the player, via a command that selects the mode, but I think this would be overkill, so for now this is just an author-controlled option. Games are free to add their own command to let the player control it, of course; it's just a matter of flipping the cancelCmdLineOnFailure setting at run-time.)
This change should have no effect on existing code, since it's a simple internal rearrangement. The benefit is that it's now possible for game code to call the 'check' phase of a verb explicitly, without also invoking the 'execute' phase.
You only need to use this method if you want to add or change a parameter name dynamically at run-time. The library still automatically initializes the message builder's tables for globalParamName settings defined at compile-time.
>show bob the rags (the piece of cloth) Bob does not appear interested.
If the parser's decision was wrong, the message will alert the player, so that the player will know to try rephrasing the command rather than being left with the impression that the intended command didn't work.
Note that this change only comes into play when a fuse/daemon throws an exception out of its main method. It won't affect a daemon/fuse that merely encounters an exception in the course of its processing, as long as the fuse/daemon catches and handles the exception.
This change is purely cosmetic, so it should require no changes to existing game code. It shouldn't even affect test scripts (where you run an input script and "diff" the output against a reference log) - by default, the log file mechanism by default generates transcript output in plain ASCII, and the curly quotes are mapped to ordinary straight quotes when rendered in plain ASCII.
To accomplish this, the announcements call upon a new Thing method, getInScopeDistinguisher(). This method looks at the Distinguisher objects associated with the object to be announced, and tries to find one that can distinguish the object to be announced from every other object in scope. If it finds one, it returns it. If it fails to find one, it returns the distinguisher that does the best partial job - that is, the one that distinguishes the object from the largest number of other in-scope objects. (The method never returns nil; in the worst case, it simply returns basicDistinguisher, which distinguishes objects based on their disambigName properties.) The announcements then use the returned Distinguisher to generate the announced name.
Note that in the simplest case, this change results in the disambigName being used in object announcements; in the past, the base name was used. In most cases, this won't cause any change in game behavior, since the disambigName for most objects is just the base name anyway.
>take a silver coin (the silver coin)
The parser was making the announcement because it had chosen arbitrarily from one of several silver coins. But since all of the possible matches were indistinguishable anyway, the announcement doesn't help the player see which one in particular was chosen. The library now suppresses the message in this case. Note, however, that if it's possible to distinguish any of the possible matches from one another, you'll still see a message. For example, if there's a silver coin and a gold coin present, and the player types TAKE A COIN, the library will announce which one (gold or silver). Similarly, if there's a silver coin on the table and another on the floor, and the player types TAKE A SILVER COIN, the library will mention the location of the one chosen: "(the silver coin on the floor)", for example.
(As part of this change, a new parser class was added: ImpliedActorNounPhraseProd. This is a subclass of EmptyNounPhraseProd that works almost the same way, but doesn't apply a grammar ranking penalty for the missing noun phrase if a default interlocutor can be identified. The single-noun-phrase grammars for GIVE and SHOW use this new class, ensuring that they're chosen over the non-prepositional two-noun-phrase phrasings whenever a default interlocutor is present.)
Released 8/29/2005
If you have existing game code that defines any ThingState objects using the old template, you'll need to change the definitions to use "+" instead of "@". Alternatively, you could put your own definition of the old ThingState template into a common header file that your game use, in which case you wouldn't have to change any of your existing object definitions; but you're probably better off just changing your code to use the new convention, since defining your own template could cause some slight confusion for other users if you should share your code in the future.
getDropDestination(obj, path) { if (obj.isReallySmall) ...; // Don't do this!!! if (obj != nil && obj.isReallySmall) ...; // Do this instead }
In addition, the eventOrder of the standard sense-change daemon is now 500, to ensure that the sense-change daemon runs after most other events. The library automatically creates the sense-change daemon at game start-up; this is the daemon that displays messages about new, ongoing, or ending sounds and odors. In the past, this daemon had default priority, so its order relative to other fuses and daemons was arbitrary. If another fuse or daemon ran after the sense-change daemon, and made some change that affected the sensory environment, a message about the change wasn't displayed until the following turn, since the chance had already passed to generate the message on the turn during which the change actually occurred. This change ensures that the sense-change daemon runs after most other events, so any changes that occur in daemons and fuses will be reflected in the sense-change update for the same turn.
If you've explicitly set eventOrder for any fuses or daemons in your game, you might need to adjust the values you set. In particular, if you set an eventOrder value between 50 and 100 for an event object to ensure that the event is processed after events that inherit the default eventOrder, you'll need to raise that value above 100. You should also consider if it's desirable for those events to run after the sense-change daemon, as they would have in the past (since the sense-change daemon used to have default priority), and if so, raise their eventOrder values to above 500. Finally, if you have an existing eventOrder that's over 500, you might want to consider whether you still want the event to run after the sense-change daemon; if not, you should lower the value to under 500.
The point-of-view actor can differ from gActor in cases where game code explicitly generates a room description from the point of view of one actor in the course of processing a command given to a different actor. In these cases, gActor will be the actor who's performing the command, and getPOVActor() will return the actor who's generating the description.
The point-of-view actor can differ from the point-of-view object (the object returned by the getPOV() function) when the actor is observing the location being described through an intermediary object. For example, if we're generating a description of a remote room because an actor is examining a closed-circuit TV monitor showing the remote location, the point-of-view actor will be the actor who's looking at the monitor, while the point-of-view object will be the camera in the remote location. Note that the POV is the camera, not the monitor: the POV is intended to be the object that's physically absorbing the light rays, or other sensory equivalents, from the surroundings being described.
This change is implemented in Thing.showSpecialDescWithInfo() and Thing.showSpecialDescInContentsWithInfo().
The exclusion that this method applies is over and above any other listing-selection mechanisms, such as obj.isListed. It's intended mostly for ephemeral exclusions that are applied other than by the object itself, since the object itself can usually more easily control its own listability via isListed and the like.
In addition, the method now calls adjustLookAroundTable() on the actor and POV objects, giving them a chance to make further adjustments to the table.
(This corrects a bug that used to occur. In the past, the library attempted to let the NPC respond, rather than generating a parser error. The problem with this approach was that the soundproof barrier stopped the PC from hearing the NPC's response, thus the library could only report that "Nothing obvious happens." Now that this is handled as a parsing error instead of a conversational response, the more helpful "Bob does not appear to hear you" is displayed.)
The new method RoomPart.isObjListedInRoomPart returns true by default if the object does not have a special description for the purposes of examining the room part - that is, if the object's useSpecialDescInRoomPart returns nil. We don't want to include the object in the miscellaneous list if it has a special description because the special description will be automatically shown - it would thus be mentioned twice if we included it in the miscellaneous list as well.
Floor overrides isObjListedInRoomPart to list an object only if it's listed normally - that is, the object's isListed returns true. This is the old behavior that applied to all room parts, but now it only applies to Floor objects. The reason we treat Floor differently is that everything that's directly in a room is by default nominally on the floor in the room, so if we didn't limit the listings like this in the case of EXAMINE FLOOR, we'd include all of the internal fixtures and components of the room, which is generally undesirable.
The main purpose of this change is that it makes it a little easier to set up Fixture-type objects that are described as being situated on walls, ceilings, and the like. Now, all that's necessary is to give the object a specialNominalRoomPartLocation - even if the object isn't normally listed in a room description, as is the case for a Fixture, we'll now list the object when we examine its room part as long as it doesn't have an explicit special description for the room part.
This method makes it much easier to handle superficial formatting variations in the player's input by centralizing the conversions to this single routine. In the past, both isValidSetting() and makeSetting() had to handle formatting variations since they received the raw player input.
Settable's default implementation of canonicalizeSetting() doesn't do anything - it just returns the original player input unchanged. NumberedDial overrides the method to convert spelled-out numbers to numeral format and to strip off any leading zeroes from an entered numeral format; LabeledDial overrides it to convert the input to the exact case of the matching entry in the list of valid labels.
The 'master switch" for the active tense is gameMain.usePastTense. Set this to true to activate past tense in library messages. The default is nil, which selects present tense.
A number of new message parameters and verbEndingXxx properties help in writing messages that can change tense. The standard library messages defined in en_us/msg_neu.t use these new features to adapt automatically to the current tense.
See the comments in en_us/en_us.t for full details.
Two new commands let the user control the global defaults: SAVE DEFAULTS saves the current settings to the configuration file, and RESTORE DEFAULTS explicitly loads the file and applies its settings. These commands both list the full set of saved settings, so that the user can easily tell exactly which settings are affected.
By default, the library uses the new global settings framework for the VERBOSE, FOOTNOTES, NOTIFY, and EXITS settings.
The new mechanism is implemented mostly in the new file settings.t. The main API is exposed through the new settingsManager object, and the command-line user interface is implemented in the new settingsUI object. The new SettingsItem class is used to encapsulate individual configuration variables. If you want to create your own extensions to the set of global default variables, simply create a SettingsItem object for each new variable you want to add. The settings framework will automatically find your new SettingsItem objects, and save and restore them as needed.
The change has two main benefits. First, it eliminates some code duplication, by replacing a few bits of code that were effectively duplicated in LiteralAction and LiteralTAction with common versions that both classes now inherit from LiteralActionBase. Second, game code can now easily determine if an action involves any sort of literal phrase, by testing gAction.ofKind(LiteralActionBase).
check() { if (condition) { reportFailure('You can\'t do that for some reason.'); exit; } }
The failCheck() routine encapsulates the display of the failure report and the termination of the action with 'exit'. Using failCheck(), you can rewrite the code above more concisely, like so:
check() { if (condition) failCheck('You can\'t do that for some reason.'); }
Thing provides a default implementation of the new isOccludedBy() method, which simply returns nil to indicate that the object is not occluded by the given Occluder.
Existing code won't be affected by this change, since any existing Occluder code already overrides isOccludedBy() to make the per-object occlusion decision. The reason for this change is that it's sometimes easier to decide about occlusion in the individual objects rather than in the Occluder; the new default Occluder.occludeObj() makes it easer to write the code that way by providing a pre-defined framework that already works that way.
In addition, these new commands are now described in the standard instructions.
RandomEventList and ShuffledEventList are now based on the new RandomFiringScript. This doesn't change their behavior; existing code based on these classes should continue to work unchanged.
The practical effect of this change is that topic words will now match properly even when there are longer and shorter versions of the same vocabulary defined for various objects. In the past, if one object was defined with a six-letter noun, and another object had a seven-letter noun with the same first six letters as the other object ("coffee" and "coffees", say), the parser filtered out the seven-letter match in favor of the six-letter match, even though both objects ought to be equally valid in global scope. With the change, both objects are now properly kept as possible resolutions when parsing a topic phrase.
Whether it's better to select the Collective or the individuals probably varies according to context, since the "right" one is the one the player intends in a particular command, and that will likely vary from one command to the next (and from one player to the next). But the design of the parser requires that we choose one interpretation or the other across the board; context sensitivity isn't an option. Even if context sensitivity were an option, we might still not have enough information to correctly read the player's mind every time. This change - matching the Collective object instead of the individuals - seems more likely to yield intuitive results more of the time than the old behavior.
ComplexContainer overrides getAllForTakeFrom() to return everything that's in scope and that's directly in (a) the sub-container or sub-surface, if the object has either, or (b) the sub-rear-surface or sub-underside, if the object doesn't have a sub-container or sub-surface; and as with Thing's version, this list is then limited to objects that aren't components of their direct containers. Note that if there's both a sub-surface and a sub-container, the method will return everything that's in either of these; and likewise with the sub-rear-surface and sub-underside.
This change is designed primarily to improve the behavior for ComplexContainers, which wasn't ideal under the old mechanism, as it only returned the complex container's sub-components, which are internal implementation objects that are meant to be invisible to the player. Clearly, in the case a complex container, it's the contents of the inner containers that are of interest in TAKE ALL FROM. This change also makes it easier to customize the TAKE ALL FROM behavior for your own special containers, since it gives the container a chance to determine what ALL means in context.
The grammar class VagueContainerNounPhraseProd, which matches locational phrases such as "all in box" and "the one in the box," also uses the new method to determine which objects are involved. This provides the same improvement for commands such as LOOK AT EVERYTHING IN THE BOX when a complex container or other special container is involved.
It's occasionally useful to be able to fine-tune precondition order because preconditions can sometimes interfere with one another. This is particularly a problem with implied actions that require the actor to be in a particular posture or location, because many of the standard preconditions can move the actor around as a side effect. For example, a doorOpen condition could execute an implied STAND as part of its implied OPEN DOOR; if another precondition requires the actor being to be sitting, its effect would be undone if the doorOpen were executed second. We can safely STAND and OPEN DOOR first, then SIT DOWN second, because we'll still satisfy the doorOpen condition even though we're no longer standing when we're done.
In addition to the new preCondOrder ordering, the default ordering has changed for two-object verbs (those with direct and indirect objects). By default, the direct object preconditions are now executed first, followed by the indirect preconditions; in the past this order was reversed. This is only the default order - the preCondOrder takes precedence. That is, the action builds a list consisting of the direct object preconditions followed by the indirect object preconditions, then sorts that list in ascending order of preCondOrder, preserving the existing relative order of objects with the same preCondOrder.
SyncEventList template ->masterObject inherited; Hint template 'hintText' [referencedGoals]?
Unthing template 'vocabWords' 'name' @location? 'notHereMsg'?;
By way of implementation, the Lockable class has a new method, lockStatusReportable, that indicates whether or not the lock status is worth mentioning in the object's current state. (This doesn't supersede lockStatusObvious, but rather complements it. lockStatusObvious still indicates whether or not the lock status is visually apparent; lockStatusReportable indicates whether it's worth mentioning from an aesthetic point of view.) Lockable defers to any mix-in class that provides the method, and Openable takes advantage of this by providing an implementation of the method that returns true if the object is closed, nil if it's open.
The relationship between BasicDoor and Door is analogous to that between BasicOpenable and Openable. BasicDoor defines the internal behavior, while Door adds handling for user commands to manipulate the door (in particular, OPEN, CLOSE, LOCK, UNLOCK).
This change doesn't affect the functionality of Door; it merely rearranges the code for Door internally by moving a few methods to the new BasicDoor base class. SecretDoor isn't much affected, either, but does now add support for remembering a recent traversal for disambiguation purposes, as Door did in the past (and still does). No changes should be needed in existing game code.
The main benefits of this change are that it makes SecretDoor behave more consistently with the Door class, and it provides a single base class for all door-like objects. In addition, the new BasicDoor class makes it easier to define custom doors with special controls, since it has all of the necessary functionality but doesn't make any assumptions about handling commands.
The purpose of this change is to make it easier to customize how the auto-closing is reported, or whether it's reported at all. If you want the door to close silently, without any mention that it closed, simply override reportAutoClose() to do nothing.
(Matchstick objects, of course, aren't affected by this change, since they can light themselves on fire independently. Matchsticks thus remain valid as default choices for BurnWith even when they're unlit.)
Similarly, NestedRoom.getExtraScopeItems() now adds the nested room to scope only when the actor is directly in the nested room. The rationale is the same as for the floor in an outer room.
A side effect of this change is that the intransitive conversational commands (HELLO, GOODBYE, YES, NO) won't try to make the PC talk to itself by default. In version 3.0.8, these commands were enhanced to choose the same default interlocutor as TALK TO would when the most recent interlocutor is no longer present. This had the undesirable effect of choosing the PC as the default target when no other actors were present. Now that TALK TO won't choose the PC as the default actor, HELLO and the rest won't do so, either.
As part of this change, the intransitive SIT DOWN and LIE DOWN phrasings no longer have [badness], so the parser will match them as full-fledged intransitive phrasings.
First, it's now possible to define a message method on a Thing that takes no arguments, even if the message normally does require arguments. This makes it more syntactically convenient to define simple message string or property-redirect methods that require some small chunk of code, but which don't need any of the argument values that are normally sent to the message method. For example, we can now write this:
notImportantMsg = (myGenericMsg)
instead of this (which was formerly required):
notImportantMsg(obj) { return myGenericMsg; }
Second, these sorts of message methods can now return nil to indicate that they don't want to provide the message after all. This is useful when an object only wants to override a library message when it's in a certain state. It's also useful when an object wants to provide the message only when that object is in a particular role in a two-object command (i.e., a command with both a direct and indirect object). Recall that when a command involves both a direct and an indirect object, the message processor asks both of the objects for an override; this new capability lets the objects be selective about providing an override according to their actual roles in the command.
For example, suppose you want your 'vase' object to override the "not a container" message for PUT IN, but only when the vase is the indirect object of the PUT IN, not when it's being put somewhere itself. To do this, you could write vase.notAContainer like so:
notAContainerMsg = (gIobj == self ? 'The vase\'s opening is too small.' : nil)
The library provides a pair of new macros that simplifies the syntax for that kind of conditional message override: dobjMsg() and iobjMsg(). These macros simply test to see if gDobj or gIobj (respectively) equal 'self'; if so, they return the argument, and if not they return nil.
(It would be even better if the library could decide based on the noun phrase itself whether the noun phrase refers to something visual or not, since the player could just as well try to EXAMINE THE SULFUROUS ODOR or TAKE THE BEEPING NOISE. Unfortunately, it's effectively impossible to do this, since the true meaning of the error message is that the parser can't determine what the noun phrase actually refers to. We don't want to say that outright, of course, since that would detract from the player's sense of immersion in the game world by calling attention to the parser and its limitations, so the best we can do is to find a heuristic that works most of the time. The addition of the per-action customization improves on the old scheme and should make incongruous cases relatively rare.)
In addition, the code now specially recognizes names that start with one-letter words (such as "X-ray" or "<q>L</q> button"), and assumes they're pronounced by saying the name of the letter. So, for example, "X-ray" turns into "an X-ray," since the "X" is pronounced as "ex."
SET is also now accepted as a single-object verb. When applied to a Thing, the library assumes the intention is SET thing ON something, and prompts for the other object. When applied to a Settable, the library assumes the intention is SET settable TO setting, and prompts for the setting.
Note that the single-object form (simply SET dobj) could be useful all by itself in some cases: SET ALARM, for example. For such cases, you can simply define a custom dobjFor(Set) for the object.
In addition, THROW obj direction (as in THROW BOOK EAST) is now accepted, via the new action ThrowDir. The default handling for ThrowDir (in Thing) is simply to display a message essentially explaining that the proper command is THROW AT: "You need to be more specific about what you want to throw the book at." If the phrasing is THROW obj DOWN or THROW DOWN obj, though, we use the same response we use for THROW AT FLOOR ("You should just put it down instead").
The English grammar uses this new criterion to mark phrasings as weak when they involve two object phrases with no prepositional marker and no qualifier word - things like GIVE BOB BOOK. These phrasings are weak because they could be interpreted as a single object with a missing phrase instead of two objects - if the wording were GIVE GREEN BOOK, for example, there's probably a GREEN BOOK and a missing object rather than two objects GREEN and BOOK. A phrasing such as GIVE BOOK TO BOB isn't weak because the prespositional marker makes the grammar fairly unambiguous; likewise, GIVE BOB THE BOOK is relatively clear because the article "the" can't normally go in the middle of a noun phrase, so most likely introduces a second noun phrase.
In addition, UntakeableActor and Person now use messages similar to those used for TAKE ("The cat won't let you," "Bob probably wouldn't like that"), since these sorts of actors are meant to exhibit volition, and in the real world would generally not allow themselves to be arbitrarily tasted.
Released 9/12/2004
Note that the new class ShipboardRoom is provided for convenience as a pre-defined combination of Shipboard and Room. This means that any rooms you previously defined with the superclass list ShipboardRoom, Room (and just changed, following the prescription above, to Shipboard, Room) can now be written simply as ShipboardRoom. This is purely for convenience; you can leave the class list as Shipboard, Room and it will behave the same way.
The new parameter gives enterConversation() more information about what triggered the conversation. This extra information is necessary because, without it, the conversation system wasn't able to properly handle topic entries that were marked as conversational but non-greeting (that is, the isConversational property is true, but impliesGreeting is nil). In the past, such entries were incorrectly treated as though they were entirely non-conversational, so they didn't enter an in-conversation state when used in a response.
Note that the same information conveyed by the old 'explicit' parameter can be inferred from the new 'entry' parameter. When 'entry' is nil, it means that the conversation entry is explicit, because it was triggered by a direct action (HELLO, etc.) rather than a TopicEntry. When 'entry' is non-nil, it means that the conversation entry is implied by the TopicEntry.
The new parameter in both cases gives the "processed" version of the player's input. The default SpecialTopic implementation now matches the processed version of the input rather than the original text the player entered.
In the English library, the processed input is the player's original input with any periods, commas, question marks, exclamation marks, colons, and semicolons removed, and with any leading "A" or "T" keyword removed.
The purpose of the change to the parameters of these methods is that it makes it simpler and more efficient to do this filtering.
Note that the punctuation filtering is new. We filter out the punctuation because it's not usually meaningful for matching special topics. For example, if the player's input contains the correct keywords to match a special topic, we usually want to match the special topic even if the player (for example) put a period or question mark at the end of the command.
The old sentence fragment format had a couple of problems. First, it was likely to be problematic for some non-English translations. Second, it was less flexible than the new full-sentence format, even in English, for the purposes of adding new "drop type" objects and of adding new messages composed using the prefix.
The parser now remembers both objects for two-object verbs, and waits to make a decision until the player actually enters a command with a pronoun. When the player types a singular pronoun (it, he, she), and the most recent command had multiple noun slots, the parser performs the standard "verify" evaluation on each potential antecedent and picks the most logical one. If more than one antecedent is equally logical, the parser simply picks one arbitrarily, and announces the choice in the usual manner. When the choice is completely arbitrary, it will always be the direct object of the two-noun command.
Some examples:
>LOCK DOOR WITH KEY Locked. >DROP IT Dropped. (the key, because it's illogical to drop the door) >UNLOCK DOOR WITH KEY (first taking the key) Unlocked. >OPEN IT Opened. (the door, because it's illogical to open the key) >LOCK DOOR WITH KEY (first closing the door) Locked. >EXAMINE IT (the door) It's a heavy wooden door.
In the last command - EXAMINE IT - the choice of the two potential antecedents is arbitrary, because it's equally logical to examine the key or the door. The parser thus chooses the direct object of the prior command, and announces the choice to alert the player (in case she was thinking "it" would refer to the key).
The new routine has several benefits. First, it provides the object being moved with the same sort of notifications that the old and new containers already receive. Since it runs along with these other notifications, it allows the same distinction as those other notifiers between moves that trigger these side effects (via calls to moveInto) and those that don't (via direct calls to baseMoveInto) Second, since the new method runs last, after the notifyRemove() and notifyInsert() notifications, it has the last chance to veto the move; this means that if it doesn't cancel the move, then the move will definitely proceed (at least to the extent that baseMoveInto() will be called). Any side effects that must be carried out only when the move will succeed can thus be safely coded here.
First, the library automatically enters all of the keywords defined by special topics into the game's dictionary. This ensures that the parser won't claim that special topic keywords are unknown if they're used out of context, as it did in the past. It was confusing when the parser claimed that special topic keywords were unknown, because the parser most certainly did know them some of the time.
Second, when the player enters a command that looks syntactically invalid, but which doesn't contain any unknown words, the parser scans recent special topics for a match. If it finds a match, the parser will show a new message: "That command can't be used right now." This old response - "The story doesn't understand that command" - was potentially confusing, since the story can recognize the exact same command at other times.
The parser scans the list of special topics most recently listed in topic inventories, using the new specialTopicHistory object. This object keeps track of a configurable number of the most recent SpecialTopic suggestions listed; set its maxEntries property, which defaults to 20, to the maximum number of recent entries to scan. The purpose of the limit is to ensure that a scan won't take too long, by limiting the scan to the specified maximum number of SpecialTopic items. The trade-off is that the parser will forget older suggestions, so they'll be met with the old "story doesn't understand" message if the player tries them. However, it seems safe to assume that the player will also forget about older suggestions, and so won't think to try them.
If you set maxEntries to nil, it essentially removes the limit to the number of entries to scan. The library won't maintain the history list at all in this case. Instead, to check an unrecognized command, the parser will simply scan all SpecialTopic objects in the entire game, whether or not they've ever been suggested in topic inventory displays. This option might be preferable for a game where the total number of items is small enough that scanning them all doesn't take an appreciable amount of time. (Or not. Some authors might not want the parser to recognize previously-unseen special topics at all - even to the extent of saying "that command can't be used right now" - because doing so could conceivably give away upcoming plot developments. That is, if the player accidentally types a command that happens to match a special topic that hasn't ever been suggested, the player could infer from the special error message that the command will do something later in the game. The chances of actually giving away anything important this way seem rather remote, but this sort of thing rubs some authors the wrong way. If you don't like the idea of using the different error message for special topics until after they've been suggested at least once, but you also don't want a limit on the number of past topics to remember, just set maxEntries to a very high number.)
In most cases, the order of processing for the matches for a plural phrase is arbitrary, so you won't need to worry about this new property. Once in a while, though, it's convenient to be able to control the order of matching. This is especially useful when objects have ordinal names (first door, second door, etc), or when objects are named by relative spatial position (top drawer, middle drawer, bottom drawer).
Note that the new property only controls the order of processing within an individual plural phrase. For example, in the command TAKE BOOKS AND MAGAZINES, the books will be sorted by their relative pluralOrder values, then the magazines will be sorted by theirs - but when the command is processed, all of the books will be taken before any of the magazines, regardless of the relative pluralOrder values. This is important because it ensures that commands are processed in the order specified by the player.
Because pluralOrder and the existing disambigPromptOrder will probably be used in the same way most of the time, disambigPromptOrder now returns the pluralOrder value by default. So, for the common case that the two properties should have the same value, you can simply set pluralOrder.
The default implementation in GameMainDef does nothing. The new method is simply a placeholder for the game's (optional) use.
This change is designed to make it easier to manage your about-box information. HTML TADS discards any previous <ABOUTBOX> tag when the screen is cleared, so it's necessary to re-display the tag whenever you clear the screen. If you write your <ABOUTBOX> tag in the new gameMain.setAboutBox() method, and you always use the new cls() function to clear the screen (rather than calling the low-level clearScreen() function directly), the library will automatically keep yout about-box active.
Some authors like to limit usage of ALL to the basic inventory verbs, either because of tradition or because they think it's a sort of "cheating" when a player tries commands like OPEN ALL or EXAMINE ALL. This feature is probably of particular interest to writers of puzzle-oriented games.
In addition to the all-or-(almost-)nothing option setting in gameMain, you can control ALL acceptance on a verb-by-verb basis. The new Action property actionAllowsAll controls whether a particular Action accepts ALL for its noun phrases. By default, the base definition in Action simply returns the value of gameMain.allVerbsAllowAll. In addition, the basic inventory verbs (Take, TakeFrom, Drop, PutIn, PutOn) override actionAllowsAll to return true. If you want to enable or disable ALL for an individual action, you can use "modify" to set the property for that action class.
The default display style is to show object announcements in bold text. When a command operates on several objects, the response can get rather lengthy; the new bold-face style for the announcements is intended to make these responses easier to read by making the boundaries of the sub-parts more visually apparent.
The new StyleTag object is called announceObjStyleTag. You can use "modify" with this object to customize the announcement style, if you want.
Note that lookAroundWithin() automatically adds LookRoomDesc to the flags if the actor has never seen the room before.
If you pass 'nil' for the 'verbose' parameter, the effect is the same as passing (LookRoomName | LookListSpecials | LookListPortables). If you pass 'true', the effect is the same as passing (LookRoomName | LookRoomDesc | LookListSpecials | LookListPortables).
The main effect of this change is that it lets you generate a room description without including the room title, which wasn't possible in the past. This can be useful for cases where the standard LOOK AROUND format isn't desirable, such as when describing the view through a sense connector to another room.
Note that none of the standard library commands ever call this new method, because the basic library doesn't provide any command to view a remote room. However, games might provide their own commands to do so, such as LOOK THROUGH KEYHOLE, and you might want to call lookAroundWithin() to generate the descriptions in these cases. Doing so will invoke roomRemoteDesc instead of roomDesc to generate the room description.
The default Thing.roomRemoteDesc implementation simply calls roomDesc. You'll probably want to override this any time you actually use it, to show a suitable description from the remote actor's point of view.
In the past, lookAroundWithin() simply removed the actor object directly, so the default behavior is the same as in the past. Separating this out into a new method lets games customize which objects are mentioned and which aren't for the particular room and point of view.
This method is called from lookAroundPov(), which formerly performed the same test directly, so the default behavior hasn't changed. This has been separated into a new method because the default condition isn't always ideal. For example, if the player's inside a closed wooden booth with a small window that looks out to the enclosing location, and the player types LOOK AROUND, we'd want to describe the interior of the booth, not the outside location. For this case, you could override isLookAroundCeiling() for the booth to return true even though the location is visible through the window.
local txt = gAction.getDobjInfo().np_.getOrigText();
Some background: In the English parser, certain grammar rules are defined specifically to match certain distinct patterns of erroneous user input. The point of matching invalid input is to recognize it and respond intelligently to it, rather than issue a blanket error message. Most of these invalid constructs are marked with "badness" levels, which tells the parser to match them only as a last resort, so that we match a valid grammar rule over one of these special error rules whenever possible.
For example, the library now displays "{You/he} close{s} the door behind {you/him}" as "You close the door behind you." The old rule would have used "behind yourself" instead, but the new exception kicks in because both the subject ("{You/he}") and the objective-case item ("{you/him}") refer to the 'actor' parameter object.
The reason the automatic reflexive substitution exists in the first place is that it allows you to write a message string that refers to multiple parameters that might end up being the same object some of the time. In the first example ("{You/he} can't eat {the dobj/him}"), the direct object and the actor might sometimes be the same, but usually aren't; since the library automatically provides the reflexive pronoun when the direct object happens to be the same as the actor, the same message string works for any combination of objects ("You can't eat yourself" vs. "You can't eat the deck chair"). However, this rule is too aggressive without the exception. The point of the exception is to give authors exact control in cases where they know in advance that the subject and objective-case object will be the same, by virtue of the fact that the message is referring to the same parameter by name in both cases. In these cases, there's no need for the library to provide reflexive pronouns conditionally, because the author knows in advance that the objects will always be the same - so the author will know whether or not the reflexive pronoun is desired, and will write it that way, or not, as desired.
In addition, in the case of LOOK BEHIND, the library uses a special message if the implicit Open was applied: "Opening the door reveals nothing unusual." This message emphasizes that we're looking through the passage, not at the space created between the door and the adjacent wall (if such a thing is applicable).
When we look behind a door that was already open, the library assumes that the player's intention is simply to look at the back side of the door, or the space between the door and the adjacent wall. In this case, the inherited Thing message ("you see nothing unusual behind the door") is used.
When a passage is closed, the inherited Thing message ("you can't see anything through (the passage)") is used instead, since a closed passage is assumed to be something opaque, such as a closed door.
In addition, the actor is now excluded in all cases from being the default direct object for PUT IN. In the past, the actor was selected as the default if there was nothing better, which occasionally led to odd results.
In the past, this precondition treated an object that was visible but not reachable as still potentially logical, but at reduced likelihood. The reasoning was that the obstruction might be easily removable via an implied action. For example, an object inside an openable glass case could be made touchable by opening the glass case, which could be performed as an implied action. However, in the case of distance, there's not usually any way to remove the obstruction automatically, so there's no point in considering the object to be logical, even at reduced likelihood. The precondition still does exactly this for objects that aren't at a distance, but now assumes that distance can't be resolved automatically.
In addition, the accompanying-travel state is now ignored when the actor is being carried. When an NPC is being carried, it will naturally move along with the actor it's accompanying, just as all of the actor's other inventory items do by virtue of being carried. We therefore don't want the NPC to separately travel to the new location, because doing so makes the actor drop the NPC.
Released 6/12/2004
(A couple of definitions might help. A "direct examination" is what happens when the player explicitly EXAMINEs the object; an "indirect examination" occurs when looking at the room, showing an inventory listing, or directly examining an enclosing object. An object is "mentioned somehow" in an indirect description if either (1) it's included in the basic generated list of portable items within the enclosing object that's being examined, as determined by the isListed property, or (2) it shows as special description, as determined by the useSpecialDesc method.)
The reasoning behind the new rules is as follows. First, on directly examining an object, its contents should almost always be mentioned; we'd want to conceal the contents only in unusual cases, so the default for contentsListedInExamine is simply 'true'. Second, when indirectly examining an object, we'd normally want its contents to be included in the description, since they're also, indirectly, the contents of the enclosing object being examined; however, when the object itself isn't mentioned in the indirect examination, then we probably don't want to mention its contents, either. Furthermore, if for some reason we've chosen to conceal the contents even when the object is directly examined, then we almost certainly want to conceal the contents in indirect examinations as well.
The rule is slightly different, and simpler, in NonPortable: contentsListed simply returns the value of contentsListedInExamine. The reason for this different default is that a NonPortable is almost always a fixed feature of its enclosing objects, and as such is almost always mentioned, to the extent it merits mentioning, as part of the custom description of the room or containing object. This is sometimes accomplished with a specialDesc, but not always, so the absence of a specialDesc isn't sufficient reason to think that the object isn't mentioned. Since a NonPortable almost always gets some kind of mention in indirect examinations, then, the default is that its contents are listed in these cases, too, as long as they'd be listed in a direct examination.
Arbitrary ordering is usually fine. However, it's nice to be able to control the order when objects have ordinals in their names ("first book," "third button," etc), to make the list order match the nominal order. For example, if you have a group of books named "first book," "second book," and "third book," you could give these objects disambigPromptOrder values of 101, 102, and 103, respectively.
Which book do you mean, the red book, the first black book, or the second black book?
>second
The parser will now interpret "second" as referring to "second black book." In the past, it took this to mean the second item in the list, which in this case is "first black book"; this is hardly ever what the player means in these cases, though.
(This change is actually a bug fix. The parser already had logic to apply this precedence order, but it didn't work properly. In particular, the "adjective ending" preference - the preference for avoiding a phrase that ends in an adjective - superseded the ordinal preference. Adjective endings are obviously not a problem for disambiguation responses, though, so this has been changed.)
Which coin do you mean, the one on the floor, or the one on the balcony?
In a related change, the parser now chooses a "local" interpretation of a locational qualifier in cases of ambiguity. For example, if the player were to answer the question above with "the one on the floor," the parser would now assume that the player is talking about the coin in the local location (i.e., within the player character's top-level enclosing room). In the past, if the balcony also had a floor, the locational qualifier would have been ambiguous.
BulkLimiter no longer defines this property at all. Instead, this functionality has been moved to Container and Surface. Furthermore, BulkLimiter.examineInterior no longer shows the lookInDesc; instead Container and Surface now show the lookInDesc as part of their action() handler for the LOOK IN command.
This is a coarser-grained mechanism than using verify() routines for individual actions, but it's more convenient when you want to exclude an object from being used as a default for a large number of actions.
(In the past, it was necessary to write a test like this instead: (gActionIs(Travel) && gAction.getDirection == northDirection). This two-part test was required because the grammar rules for NORTH, SOUTH, etc. generate the base Travel action instead of the specific-direction subclasses. The grammar rules still do this, but gActionIs() now calls a new Action method, actionOfKind(), which TravelAction overrides to encapsulate the two-part test; since TravelAction does the test for you, there's no longer any need to write it out by hand. You can still write the two-part test if you prefer, and it still works the same as it did before, but it's easier and clearer to just write gActionIs(North) and the like.)
The point of this change is to avoid annoying situations where the game automatically puts away an object that you just picked up in order to do something with it. These situations were especially annoying when they were entirely automatic (for example, the game picked up an object automatically, then put it away automatically in order to make room to pick up another object automatically, then had to go get the first object out again in order to use it). It was rare for this to happen, but it looked pretty silly when it did.
In a related change, the library now arranges the order of putting things away to ensure that if object A goes inside bag of holding B, and B goes inside another bag of holding C, then A will be put in B before B is put in C. This was almost always the result anyway due to affinity ordering, but proper nesting order is now explicitly assured.
This has been changed. There are two possible behaviors, which you can control via a global configuration property, gameMain.filterPluralMatches:
Released 5/9/2004
Possible compatibility-breaking change: The standard parser dictionary has been renamed from G_dict to cmdDict (for "command dictionary"). The dictionary is simply an object, and the new name is more consistent with other parser object names (in particular, cmdTokenizer, the standard parser tokenizer).
It's relatively uncommon for game code to refer to the main dictionary directly, but you should search your code for references to G_dict and change the name to cmdDict.
Possible compatibility-breaking change: The methods that describe an actor's location have been changed, in order to improve consistency and to simplify the code structure. These changes are mostly internal to the library, so they're unlikely to affect existing code unless you've made some rather low-level customizations.
The changes involve a lot of renaming and rerouting of internal method calls. The list of changes below is divided into groups of related changes.
Group 1: The "nominal actor container" methods have been consolidated into a single method.
Group 2: The Posture object has been taken out of the loop, as it were, for the methods that describe actors. The Posture object's role in the past was to break out these various methods into a separate version for each posture, and then further dispatch to the actor's nominal location: for example, the okayPostureChange method was split up into okayStand, okaySit, and okayLie.
The original idea was that locations might want to customize extensively by posture, but in practice the multiplicity of methods turned out to be more trouble than it was worth. With the present change, each set of Stand/Sit/Lie variations is now consolidated back into a single method, which makes the Posture object's role unnecessary, allowing it to be removed from the dispatch chain. It's still used in generating the final message, but in most cases this is now a simple matter of getting the posture's participle ("sitting", etc) from the Posture object and using it to build the message string.
The following list of changes will look daunting, but it's unlikely that existing game code will be much affected, as these methods were always meant mostly for internal use in the library.
Group 3: Actor "grouping" is now handled in BasicLocation, rather than only in NestedRoom.
Group 4: Miscellaneous minor changes to the generation of remote room actor descriptions.
The immediate practical benefit of the new method is that it makes it possible for scripted NPC travel to traverse AskConnectors. In the past, an NPC encountering an AskConnector was typically unable to proceed, since the AskConnector didn't directly link to the destinations of its underlying connectors, and the underlying connectors typically weren't linked directly as directions to the enclosing room. This made it impossible for the scripted NPC to find a suitable connector, and thus blocked the scripted travel.
The isListed property now returns, by default, the value of isListedInContents. The meanings of these properties haven't changed: isListedInContents controls whether or not the object is listed when examining the object's direct container, whereas isListedInContents controls listability for examining indirect containers, including the enclosing room. The reason for this change is that it's rare that we'd want an object to be unlisted when its direct container is examined, but still want it to be listed when enclosing containers are examined (whereas the reverse isn't entirely uncommon, since we sometimes want to mention an object only as a detail of its direct parent, but not of the broader room context).
The new property contentsListedInExamine lets you control whether or not an object's direct contents are listed when the object is examined directly. In the past, you could do this only by overriding examineListContents, but the new property makes it easier to control this behavior. The new property returns the same thing that contentsListed used to return.
The contentsListed property now returns the value of contentsListedInExamine by default. This is parallel to the change to isListed.
The new method getListedContentsInExamine() returns the list of direct contents that should be listed when the object is examined. This new method is the direct-examination equivalent of the existing getListedContents() method, and returns the same value that getListedContents() formerly returned: the subset of direct contents that appear in the sense info table passed to the method.
The getListedContents() method now simply returns the result of calling getListedContentsInExamine().
The method examineListContentsWith() now calls getListedContentsInExamine() to obtain the list of direct contents to list when examining an object. In the past, this used the 'contents' property. The method also checks the value of contentsListedInExamine, and skips the entire listing if this property is nil.
These changes are all designed to be compatible with existing code. They should simply add more flexibility and make certain behaviors easier to customize, but existing code shouldn't be affected.
This problem has been corrected: the room description generator now ensures that a MultiLoc's contents appear only once. To do this, the room description code now excludes each object that is also located in the "local" top-level room or in any connected remote room whose contents listing has already been generated.
Along the same lines, Vaporous objects are now included in EXAMINE ALL, SMELL ALL, and LISTEN TO ALL. These objects do have a visible presence, so it makes sense to include them in commands that examine everything in sight with any sense.
For particular Noise and Odor objects that you do want to participate in LISTEN TO ALL and SMELL ALL, just override hideFromAll() to return true for the desired action or actions.
Recall that, by default, Thing downgrades the disambiguation likelihood that EXAMINE refers to an object not being held. The reasoning is that an object being held is physically closer to the actor, so in a real-world situation, it would be more convenient to closely inspect an object being held than one sitting nearby. Since NonPortable objects can't be held, though, this reasoning breaks down somewhat. The change removes the disambiguation disadvantage for NonPortable objects.
Similarly, remoteInitSpecialDesc(pov) now calls distantInitSpecialDesc by default (rather than initSpecialDesc, as it did in the past).
This change is designed to accommodate players who are accustomed to the standard ASK/TELL format, and who might not realize that the special topic list shows commands meant to be entered literally, or who are just so in the habit of using ASK/TELL commands that it doesn't occur to them to leave out the "A" or "T" part. Since an "A" or "T" command pretty clearly indicates that the player's intent is to communicate with the NPC, it makes sense to check the topic the player is trying against any active special topics.
Note that the special topic parser only accepts the super-abbreviated formats - the "A" and "T" commands. This is because it seems much less likely that it would even occur to a player to try a special command with the full ASK ABOUT or TELL ABOUT phrasing. Once the command is expanded to the full phrasing, the detailed syntax of the special command suggestions should make it fairly obvious to the player that the ASK ABOUT or TELL ABOUT part isn't needed. In contrast, since the abbreviated "A" and "T" formats are ungrammatical to start with, it would seem perfectly natural to use them with arbitrary special command suggestions.
First, Thing has a new property, lookInDesc, that gives a message that's displayed by default in response to LOOK IN and SEARCH. This shows the standard default ("there's nothing unusual in it") if you don't provide a custom message. It happens frequently that a game wants to depict an object as having some interior features, but doesn't want to actually model those interior features as separate objects. The new lookInDesc makes this more convenient, since you only need to provide the custom message in this property, rather than overriding the entire dobjFor(LookIn) handling, as was necessary in the past.
Second, Decoration now treats LOOK IN as logical by default (in the past, this was handled by the catch-all routine that makes most actions respond that the object is "not important"), and applies the same logical-rank downgrade applied to EXAMINE. Correspondingly, the default lookInDesc for Decoration displays the object's "not important" message. This means that Decoration objects act by default as they did before, displaying "that isn't important" in response to LOOK IN, but can be given a custom LOOK IN response simply by setting lookInDesc, just as with any Thing.
Third, the English phrasing for finding nothing in an object specifically searched with LOOK IN or SEARCH has changed slightly, to report that there's nothing "unusual" in the object. In many cases, an object is described as having what would in the real world be interior features, but those interior features aren't modeled as actual game objects and hence don't show up by default in response to LOOK IN or SEARCH. The slight change in phrasing helps make the response at least technically defensible. It's probably still better in most cases to customize the LOOK IN response anyway, but at least the default response is a little less jarring in these cases.
Released 4/25/2004
Important: you must search for "initDesc" and replace it with "initSpecialDesc" first. This is required because the former "initExamineDesc" has been renamed to "initDesc" - so you have to make sure you change all the old "initDesc" instances before you rename "initSpecialDesc" to create new "initDesc" instances. Please do your search-and-replace operations in the order shown below:
The purpose of this change is to make the naming more consistent. The old usage - "initDesc" for a special description and "initExamineDesc" for the EXAMINE description - was inconsistent with the respective non-initial properties, "specialDesc" and "desc". The qualification was reversed: "specialDesc" was qualified as special, leaving the EXAMINE description unqualified as simply "desc", while the initial descriptions qualified the EXAMINE desription rather than the special one.
If you have any Attachable objects that define cannotAttachMsg, you must make two changes to each. First, rename cannotAttachMsg to explainCannotAttachTo. Second, rather than returning a string, display the string directly.
Note that the TopicDatabase methods findTopicResponse() and handleTopic() no longer need the topic list parameter because this information can be obtained from the new convType parameter.
These methods are mostly for internal use by the library, so the likelihood that your existing game code will be affected is small. Even so, you should scan through your code for these methods and make the appropriate adjustments.
Important: note that there are two related but independent methods called handleTopic(). There's the TopicDatabase method, which is affected by this change, and there's the separate TopicEntry method, which is not affected. When you're scanning your code, you only need to change the TopicDatabase version. It's easy to tell which is which at a glance: the TopicDatabase version takes three parameters, while the TopicEntry version only takes two. When you're scanning your code, only change the three-parameter instances.
Additional technical notes: you'll only need to read this part if you're making some rather advanced overrides to the library. Please skip this part unless you find something that you're not sure how to handle in the search-and-replace step above.
If your code contains calls to any of the affected methods, and you're wondering what to pass for the convType parameter, you should first check to see if you're receiving that value as a parameter to the calling code; if so, just pass that parameter down. This is usually easy to spot at a glance. If you have code like this:
handleTopic(self.(convType.topicListProp), actor, topic);
then you can simply change it to this:
handleTopic(actor, topic, convType, nil);
The important thing to note here is that you're already using the convType parameter to look up the topic list in the old-style call; you can simply drop the topic list lookup and pass the convType parameter itself instead (but note that it moves to the last position).
If you don't have a convType parameter passed in from your caller, you'll need to choose which one to use. Look at which topic list you're passing, and think about what conversational action is triggering your code. Then scan through the list of xxxConvType objects (askAboutConvType, tellAboutConvType, yesConvType, noConvType, etc) defined in actor.t, and choose one that (1) has the same topic list property that you're already using, defined in its topicListProp property, and (2) conceptually matches the action you're performing. If you can't find anything appropriate, you might simply need to define your own ConvType object for whatever extended action you're defining; just create one following the same pattern as the existing ones in actor.t.
You can usually just pass nil for the new fourth parameter ('path') to handleTopic(), findTopicResponse(), and ConvNode.handleConversation(). You only have to pass a non-nil value when you're implementing a search through a hierarchy of topic databases, in which case you'll have to pass a list of the inferior databases in the hierarchy. The purpose of the new parameter is to allow a higher-level database to defer to a match in a lower-level database, if one exists, so at each level you must pass the remainder of the "search path" of databases.
Recall that topic entry databases for conversation are arranged into a three-level hierarchy for each actor: at the top is the ConvNode's list of topics, at the middle level is the ActorState list, and at the bottom level is the Actor list. We find a matching topic entry by scanning each level of this hierarchy in turn, and taking the first match we find. So, if there's a ConvNode match for a topic we're looking for, we use it, even if there's a match for the same topic in the ActorState or Actor. The matchScore is irrelevant: the three-level hierarchy always trumps the matchScore ranking. The matchScore ranking is only used to choose among different matching entries at the same hierarchy level.
The new deferToEntry() mechanism allows topic entries to defer handling across hierarchy levels. Here's how the new scheme works: before we start searching the hierarchy, we start by noting the matching entry at each level. Then, we search each level from the top down as before. At each level, though, we look at the winning match, and ask it via deferToEntry() if it wants to defer to the winning match from the next level down the hierarchy. If so, then we ignore the match at that level. This means that we skip the higher-level match and go straight to the lower-level handling.
The main purpose of this addition is to allow a DefaultTopic entry to be a catch-all only for topics that aren't handled explicitly at a lower hierarchy level. For example, to create a DefaultTopic that defers to any non-default match at a lower hierarchy level, add this to the DefaultTopic object:
deferToEntry(other) { return !other.ofKind(DefaultTopic); }
This tells the topic finder that if there's any match that's not itself a DefaultTopic at a lower hierarchy level, then this default should be ignored.
The template for Room now takes the first string to be the 'roomName' property, the second to be 'destName', and the third to be the 'name' property. The 'desc' property is still in the last position.
Room template 'roomName' 'destName'? 'name'? "desc"?;
In the past, the 'name' was the first entry, and 'destName' was the second. The library took the 'roomName' to be the same as the 'name'. However, it makes more sense to do this the other way around: the 'roomName' is now the one that's explicitly defined by the game, and the 'name' setting is optional. If the 'name' setting is omitted, it's derived by default by converting the 'roomName' to lower case.
The 'roomName' is what's shown as the title of the room, in the room description and on the status line. This is usually given in "title case," meaning that each word has an initial capital letter, with the exception of minor function words: "Hall of the Ancient Kings". This is the format that games have defined all along, but in the past, they defined it using the 'name' property. The change improves matters, because it allows the room's ordinary 'name' property to be entered separately, in a format suitable for use when the name is needed in library messages: "You can't have the ice cave."
The default 'name' derivation - converting the 'roomName' string to lower case - gives decent results a lot of the time. However, in many cases, you'll want to customize the ordinary name separately. The template makes this easy by adding a slot for the 'name' property separately from the 'roomName'.
There's one additional change to the default derivations. In the past, the 'destName' didn't have a default at all. Now, the default is the room's 'theName', which is itself derived, by default, by prepending the ordinary name with 'the'. As before, the template lets you supply a separate, custom 'destName' value.
The primary design principle of a MultiLoc hasn't changed: a MultiLoc is a single object that appears in multiple locations, but which doesn't provide any sense connection among the multiple containers. A MultiLoc is a single object in the sense that the same object appears, in its physical entirety, in each of its containers.
In the past, a MultiLoc was essentially a "sense ceiling": sense information propagated from outside the MultiLoc to its interior, but didn't propagate from within the MultiLoc to its containers. This was intended to prevent the MultiLoc from propagating sense information among its containers, but it was too restrictive an implementation. For example, if a MultiLoc contained a flashlight, the flashlight's light didn't shine into the MultiLoc's locations. Similarly, if an NPC was inside a MultiLoc, the NPC couldn't see or hear anything from any of the MultiLoc's locations, and hence couldn't take part in a conversation with the PC while the PC was outside the MultiLoc.
The change is that a MultiLoc is no longer a sense ceiling; it's now more like a wall. Now, from a point of view inside the MultiLoc, all of the MultiLoc's locations (and their contents, to the extent that's appropriate) are visible. From the point of view of any of the MultiLoc's containers, the MultiLoc and its contents (as appropriate) are visible, but none of the MultiLoc's other containers are visible. Light is similarly transmitted from inside the MultiLoc to all of its containers, but not from one container to another. So, for example, if a flashlight is inside a MultiLoc, it provides light to every container of the MultiLoc; but if a flashlight is in one of a MultiLoc's containers, the light doesn't make it through the MultiLoc to its other containers.
The main effect of this change is that things located inside a MultiLoc now behave the way one would expect them to. In the past, the asymmetrical "sense ceiling" design led to some odd behavior when putting objects inside a MultiLoc: an actor within a MultiLoc couldn't carry on a conversation with someone outside, for example, because the actor inside couldn't hear out beyond the MultiLoc. With the change, MultiLoc should behave much more intuitively.
The listers now take into account the "cardinality" of each item listed, to ensure grammatical agreement in these cases. For English, there are only two degrees of cardinality that matter: one, and many. The English module therefore assigns a grammatical cardinality of 2 to items whose isPlural property is set to true, 1 for all others; this is done with the new Thing method listCardinality(lister). A lister calls this new method to determine the cardinality of the list, so that it can provide the correct information to the methods that generate the messages.
The SMELL command has been similarly enhanced.
As part of this change, the thingListenDesc and thingSmellDesc messages have been moved from libMessages to playerActionMessages, and renamed thingListenDescMsg and thingSmellDescMsg, respectively. These messages are now shown as default description messages, necessitating the move to playerActionMessages.
The new class, OccludingConnector, is a mix-in that you can combine with SenseConnector to create a partially occluded sense path. Put OccludingConnector before SenseConnector in the class list. You specify which objects to occlude from view by defining the method occludeObj(obj, sense, pov). 'obj' is an object to test for occlusion; 'sense' is the sense being calculated; and 'pov' is the point of view, which is usually the actor performing the command. Your method must return true if the connector occludes the object, nil if not.
To implement the window in our example above, we'd do something like this:
OccludingConnector, SenseConnector, Fixture 'window' 'window' "It's a small window. " connectorMaterial = glass locationList = [roomA, roomB] occludeObj(obj, sense, pov) { /* from roomA, we can't see what's in the bookcase in roomB */ return (pov.isIn(roomA) && obj.isIn(bookcase)); } ;
OccludingConnector uses a new mechanism that can be used for more extensive customization of the sense path calculation than OccludingConnector itself enables. During each sense path calculation, the library first builds a table of all of the objects connected by containment to the point of view. (This table contains all of the objects that can possibly be sensed, because sense paths are based entirely on containment relationships.) The library then calls clearSenseInfo() on each item to initialize it for the path calculation.
In clearSenseInfo(), an object can register itself for an additional notification at the end of the sense calculation. To register for the notification, override clearSenseInfo(), inherit the default code, and append 'self' to the notification vector:
clearSenseInfo() { inherited(); senseTmp.notifyList.append(self); }
Now that the object is registered, it will be notified at the end of the calculation, after the sense path to each object is fully calculated, but before the final table of sense paths is constructed. The notification method is called finishSensePath(objs, sense). 'objs' is a LookupTable whose keys are the objects connected by containment to the point of view; 'sense' is the Sense object of the calculation.
You can do anything you want to the sense table in finishSensePath(). The sense path information for each object is stored in the object's tmpXxx_ properties - tmpTrans_, tmpAmbient_, tmpObstructor_, etc. To change an object's sense path, simply change those properties. (OccludingConnector simply sets each occluded object's path transparency, in tmpTrans_ and tmpTransWithin_, to nil to indicate that the object cannot be seen.) You can make additional objects visible by adding them to the lookup table and setting their tmpXxx_ properties to the settings you want them to have.
Note that the notification step is required for performance reasons. The library could call the notification method on every object, which would simplify things by eliminating the need for the extra line of code to register. However, very few objects are likely to require this extra notification, and since sense path calculations are performed very frequently, the library doesn't want to call the notification method blindly on all objects. So, to avoid adding the notification overhead to every object, the library requires the rare objects that require the notification to register themselves.
First, a new type of description has been added: the "remote" description. There are three new remote description methods: remoteDesc, for the EXAMINE description; remoteSpecialDesc, for the special description; and remoteInitSpecialDesc, for the remote initial special description. These new methods are used to provide the examination and special descriptions when the object is viewed from a separate top-level location. For example, if two top-level rooms are connected by a window, so that an actor in one room can see objects in the other room through the window, the remote description is used to describe objects in the other room from the actor's perspective.
Second, when choosing a special description (as part of a room or contents listing), the logic is now as follows:
In the past, the sightSize was taken into account in selecting the type of special description to show: the specialDesc was shown if the object had a large visual size, regardless of the transparency of the sense path. Now, the sightSize isn't considered at all. The reason for the change is that the distantDesc and obscuredDesc are almost always custom messages per object, so they will naturally take into account the visual size of the object; considering the visual size added an unnecessary extra dimension.
Note how the new remoteSpecialDesc fits into the selection rules: the remote special description takes precedence over the obscured and distant descriptions. That is, if the object being described is in a separate top-level room from the actor doing the looking, the remoteSpecialDesc is used, even if the object is also visually obscured or at a distance.
Third, when choosing an examine description (when the object is explicitly examined), the logic is now as follows:
In the past, the 'desc' was used any time the visual details were visible, even if the object was distant or obscured and even if the object had a custom distantDesc or obscuredDesc. The change is that the distantDesc or obscuredDesc will essentially "override" the ordinary 'desc' when custom definitions are provided, regardless of the visual size of the object. The reason for this change is to keep the EXAMINE description selection rules consistent with the special description rules; the rules are essentially parallel in the two cases. The EXAMINE selection rules are slightly more complicated because of the need to generate suitable defaults when a custom obscured/distant description is not defined: by considering first whether or not a custom description is available, we can decide to show the custom distant/obscured description (if one is available), the default ordinary description (if no custom description is available and the details are visible), or a default distant/obscured description (in any other case).
Note that the defaultObscuredDesc and defaultDistantDesc methods are new. The default implementations simply display the same default library messages that the default obscuredDesc and distantDesc implementations displayed in past versions.
The logic for deciding whether or not to use the initial special description and initial examine description for an object has been refactored slightly. The refactoring yields the same default effects as before, so existing game code won't be affected, but the changes provide better control for overriding the default behavior.
First, the new Thing method isInInitState determines whether or not to use the initial special and examine descriptions. By default, this method returns true if the object has never been moved, nil if the object has been moved. That is, the object is considered to be in its initial state, for the purposes of descriptions, until the first time the object is moved by an actor in the game.
Second, the new Thing method useInitExamineDesc() indicates whether or not the initExamineDesc should be used when examining the object. By default, this returns true if the object is in its initial state, as indicated by isInInitState, and the object defines a non-nil initExamineDesc. Since isInInitState by default returns true if the object has never been moved, and nil otherwise, the default behavior is the same as before, so existing games will not be affected by this change.
Third, the existing Thing method useInitDesc() looks at isInInitState to make its determination. Since isInInitState by default returns true if the object has never been moved, this has the same effect as in the past.
These changes make it easier to change the "never moved" logic that determines whether or not to use initial descriptions, and also allows you to control the use of the initial special and examine descriptions independently of one another. For example, if you want the initial examine description to be used one time only, rather than to be based on whether the object has been moved, you could override useInitExamineDesc() to base its determination on 'described' rather than on 'moved'.
To generate the new remote room contents listings, the room describer first determines the set of top-level rooms with items that are visible from the point of view of the actor doing the looking. The room describer shows the contents of the local room (the room containing the point of view) first, using the same procedure it always has. Next, the describer runs through the set of other top-level rooms with visible contents; for each one, it generates a separate remote contents listing.
Each remote room's contents listing is displayed using essentially the same procedure used to generate the local room's contents listing. However, instead of using the lister defined by the local room's roomContentsLister property (which is set to the roomLister object by default), each remote listing is generated using a lister returned by the new method remoteRoomContentsLister(other). This method is called on the point-of-view room, and 'other' is the remote room whose contents are to be listed. By default, this returns a lister that uses the remote room's inRoomName to generate the description.
You can customize the listing of portable items visible in remote locations by returning a custom lister from remoteRoomContentsLister(). Note that the new class CustomRoomLister makes this easy, if all you want to do is to customize the prefix and suffix messages displayed in the listing:
remoteRoomContentsLister(other) { return new CustomRoomLister('Through the window, {you/he} see{s}', '.'); }
Note that remoteRoomContentsLister(other) is called on the local room - the room containing the actor doing the looking - and 'other' is the remote room. This arrangement lets you customize the listing in the point-of-view room, which makes sense in that the overall description is of the point-of-view room.
To accomplish this change, Actor now overrides remoteSpecialDesc and distantSpecialDesc. On LOOK AROUND, the room calls the appropriate one of these to describe the actor when the actor is at a distance. (The normal selection procedure is used: the remoteSpecialDesc is used if the NPC is in a separate top-level location from the point-of-view actor, otherwise distantSpecialDesc is used if the NPC is at a distance.) The new Actor implementations of remoteSpecialDesc and distantSpecialDesc invoke the corresponding new ActorState methods of the same names. These new ActorState methods in turn call back to the new Actor method actorThereDesc (both the distant and remote methods call this one new method). The new actorThereDesc calls the new method roomRemoteActorDesc on the actor's location. Thing provides a default implementation of this new methods, which simply calls the same thing on its the location if there's a location that's visible from the point of view, or shows a default library message if not. The default library message method is also called roomRemoteActorDesc. This uses the location's inRoomName to generate the description.
This sequence of calls lets you hook in and customize the actor's distant special description at whatever level is most convenient - at the actor itself, at the ActorState, or at the containing room. In most cases, it will probably be most convenient to customize this in the room, to display a message customized to the appearance of the room from a distance. To do this, you'd put a method like this on the room:
roomRemoteActorDesc(actor) { "\^<<actor.nameIs>> over at the north end of the courtyard. "; }
Note that the actor to describe is provided as a parameter, and that the method is responsible for displaying the complete message describing the actor's presence.
For certain vehicles, it might still be desirable for OUT to mean GET OUT. Fortunately, it's easy to override the default behavior on a case-by-case basis - just override a vehicle's 'out' property to return nestedRoomOut:
car: Vehicle out = nestedRoomOut // ...other properties... ;
First, when an NPC departs from or arrives at a remote location, and the other end of the NPC's travel is out of sight, the library adds the name of the remote location to the arrival or departure message. For example: "Bob leaves to the east from the alley," or "Bob comes down the stairs to the north end of the courtyard." The name of the location is taken from the destName property of the NPC's location. The location name is only shown when the location is remote; when an NPC enters or leaves the PC's outermost room, the traditional messages ("Bob leaves to the east," etc.) are shown. The addition of the location name to the message helps clarify that the NPC is moving around nearby and within sight, but isn't directly at the PC's location.
Second, when an NPC starts and finishes its journey within sight of the PC, the library omits the departure message entirely, and displays a new type of arrival message. When the entire journey is within the PC's field of view, we don't wish to describe the departure and arrival as two separate steps, since the whole journey can be described as a single operation; this is what the new type of arrival message does. The new message is given by the new TravelConnector method describeLocalArrival(), which the traveler calls from its describeNpcArrival method when it determines that the origin is visible to the PC. TravelConnector.describeLocalArrival() in turn calls the new Traveler method sayArrivingLocally(), and the default implementation of this new method simply displays the library message given by libMessages.sayArrivingLocally(). The library message displays a message of the form "Bob enters the alley." Note that it might sometimes be desirable to customize the travel connector's describeLocalArrival() method: you could say things like "Bob climbs the stairs up to the loft," or "Bob squeezes through the window into the kitchen," for example.
ComplexContainer also overrides beforeMovePushable(), passing the notification down to any SpaceOverlay components it has. This corrects the same problem for a ComplexContainer that's also a TravelPushable, and which has SpaceOverlay components.
>take box Under the box is a rusty can. Taken.
There are two changes to the way this listing is generated.
First, the listing is no longer just a generic LOOK UNDER (or equivalent). Instead, it uses a new lister, given by the abandonContentsLister property of the SpaceOverlay object. Underside uses undersideAbandonContentsLister by default, and RearSurface and RearContainer use rearAbandonContentsLister. These new listers use more specific language to describe the revelation, of the form "Moving the box reveals a rusty can underneath" (or "behind").
Second, the SpaceOverlay takes care to move the listing so that it follows the main report from the command that triggered the listing. This ensures that the main report remains close to the command line, which is required if the terse phrasing of the default reports is to make sense.
There's one exception, though: if the actor performing the command ends up in a different location at the end of the command, the listing is not moved to the end of the report. Instead, the listing simply stays at the beginning of the command. This exception is important because travel usually triggers a description of the new location, and once we're in the new location, the revelation of the SpaceOverlay's contents will seem out of place. It's better in these cases to leave the contents listing at the start of the reports.
With these two changes, the old listing above is transformed into this:
>take box Taken. Moving the box reveals a rusty can underneath.
First, the new SpaceOverlay property 'neverListOnMove' lets you suppress the default listing; just set this property to true, and no listing will ever be generated when the overlay object is moved. If the contents are to be abandoned, the abanonment will still happen, but no automatic mention will be made of it. Your code is responsible in these cases for generating any desired listing.
Second, the new method listContentsForMove() generates the revelation of the contents in the default format. SpaceOverlay calls this method itself to generate the automatic message when appropriate. If you set neverListOnMove to true in order to suppress the default revelation listing, you can call listContentsForMove() at the appropriate juncture in your own code to generate a substitute listing. Of course, you could instead completely customize the listing with your own method; but if you only want to change where the listing appears, without changing its contents, listContentsForMove() makes that easy.
Note also that you can customize the standard listing format used by providing your own lister, and setting the SpaceOverlay object's abandonContentsLister to refer to your custom lister.
If you set 'isSticky' to true for a node, the node will stay active if the NPC shows a response that doesn't set a new node. In other words, the "default next node" for any response while the sticky node is active is the sticky node itself, rather than nil.
Sticky nodes are useful in cases where you want an actor to drive a conversation along a particular thread, but you still want to allow the player to digress by talking about other topics. As long as a digression doesn't explicitly set a new node, the sticky node will remain active.
This change makes it easier to program catch-all goodbye messages, while still allowing differentiation when desired. To create a catch-all message that handles both explicit and implicit goodbyes, just create a ByeTopic. If you want to differentiate, create one of each: the ByeTopic will be chosen for the explicit GOODBYE commands, since the ImpByeTopic won't match those; and the ImpByeTopic will be chosen for implied goodbye, because both will match, but the ImpByeTopic's higher matchScore will ensure that it's selected over the ByeTopic.
Along the same lines, HelloGoodbyeTopicObj now matches implied as well as explicit goodbyes. (In the past, it handled implied and explicit greetings, but only explicit goodbyes.)
First, the new libMessages method silentImplicitAction() can be used in place of the standard announceImplicitAction() to generate the announcement. silentImplicitAction() simply shows no message. To use it, perform the implied action with a call like this:
tryImplicitActionMsg(&silentImplicitAction, Open, self);
Second, the lister that aggregates a set of implicit actions into a single announcement message now accepts the empty messages generated by silentImplicitAction(). This ensures that silent implied actions can be mixed with announced implied actions in a single command, and a sensible aggregate announcement will be displayed.
In the past, the parser unconditionally set the pronoun antecedent after a two-object command to the second-resolved object, which in many such verbs is the direct object. So, after PUT VASE ON IT, the pronoun "it" previously referred to the vase. Due to this change, though, the explicit use of the word "it" directly within the command causes meaning of "it" to be left unchanged by the command. This change doesn't affect the behavior when no pronoun is used in the command, so PUT VASE ON TABLE still causes "it" to refer to the vase on the subsequent command.
>look at bob He's wearing a red sweater. >bill, examine What do you want Bill to look at? >his book The book looks old and dusty; the title on the cover is faded to the point of being unreadable.
In the past, the parser would have taken the "his" in the interactive response to refer to Bob, since Bob would have been the pronoun antecedent up until that point (by virtue of having been mentioned in the previous command). With this change, the parser takes "his" to refer to Bill. This fits with the phrasing of the prompt text, and also with the fact that Bill was mentioned more recently in a command (specifically, in the incomplete "examine" command).
Similarly, reflexive third-person pronouns (HIMSELF, HERSELF, ITSELF, THEMSELVES) in interactive responses are taken as referring to the target actor, when the gender matches.
In the past, the parser treated exact matches as stronger than truncated matches for singular definite noun phrases (the most common kind of noun phrase: TAKE BOOK, OPEN THE BOX). Any time the parser matched some names exactly and other names only after truncation for the same user input, the parser kept only the exact matches, filtering out the truncated matches. This rule hasn't been changed; what's changed is that it has now been extended to every kind of noun phrase, including indefinite phrases (TAKE A BOOK) and plural phrases (TAKE BOOKS).
In some cases, it's convenient to define a word that's grammatically a plural under the 'noun' property, because a single game object encompasses what looks to the user like a set of something: a bookcase might define "shelves" as a noun, for example, since the individual shelves aren't modeled as separate objects. This change ensures that if other objects are in scope that define the same word under the 'plural' property, the bookcase will still match the word "shelves" when the noun phrase is resolved.
This change doesn't actually make much difference most of the time, but it is frequently significant in "topic" phrases (as in ASK ABOUT). Topic phrases have very wide scope, so a particular word can match numerous objects from all over the game. In these cases, the old scheme that included only 'plural' matches was overly exclusive, causing the parser to omit objects from the topic resolution that the author would at first glance have expected to be included. The new more inclusive matching should reduce the potential for confusing match exclusions in these cases.
Normally, when the last word of a noun phrase can match one or more objects in scope that define the word under the 'noun' property, the parser will ignore any additional in-scope objects that match the same word under the 'adjective' property. For example, if there's a desk in scope that defines 'desk' as a noun, and there's also a desk drawer that defines 'desk' as an adjective, the parser will interpreter LOOK AT DESK as referring to the desk, not to the drawer. However, if there's a red book present, but there's no object in scope that defines 'red' as a noun, the parser takes READ RED as referring to the red book, even though BOOK was left unstated. This combination of rules gives the player the convenience of abbreviating the name of an object to a unique adjective, while avoiding spurious ambiguity in cases like the desk-and-drawer exmaple.
In cases of "global" scope, though, this strategy isn't as beneficial. When resolving a topic phrase, objects from all over the game have to be considered "in scope" even though they're not physically present, as it's perfectly reasonable for the player to ASK ABOUT something that was seen earlier in the game but isn't physically present at the time of the question. As a result, the elimination of adjective bindings for a word can cause subtle problems that are very difficult to track down, and which sometimes can't be easily fixed without creating other problems. Two objects could happen to share the same vocabulary words, in one case as an adjective and in the other case as a noun, even though the objects are completely unrelated. Thus, the existence of one object can change the way another object's vocabulary words are treated when matching a topic phrase.
The English parser now treats topic resolution differently. When resolving a topic phrase, the parser no longer makes the strict noun/adjective distinction. Instead, the parser considers both kinds of vocabulary to match. So, if the player types ASK BOB ABOUT DESK in our example with the drawer, both the desk and the drawer are now considered possible topic matches.
This change has the potential to create a different sort of problem, in that it could include too many possible matches for a topic phrase. However, this is a much easier problem to deal with than the old one, and in most cases won't even be noticeable as a problem. In the first place, topic phrases don't require disambiguation in the normal sense; the parser never asks the player which object was intended, so the more inclusive matching won't degrade the user interface by generating more disambiguation queries. Second, it's much easier to understand why an extra object is matching a given vocabulary word than to understand why a given object is failing to match. A failure to match indicates that some other object is defining the same word under another part of speech, but it doesn't tell you which object or where it's defined; when too many objects match, in contrast, you can easily see (using the debugger, for example) which extra objects are matching, and make any desired vocabulary adjustments. Third, in the vast majority of cases, the extra matches are completely harmless and not even noticeable. Topics are generally handled in conversations, and conversations have to explicitly include responses for all of the topics they want to match. If an extra object or two show up in vocabulary matching for a given conversational command, it won't usually matter, because those unexpected objects won't have topic matches in the conversation anyway and will thus simply be ignored. In cases where you want a conversation to distinguish among objects with similar vocabulary, you can add the necessary distinguishing vocabulary to the objects just as you would in cases of ordinary ambiguity, and you can tweak the topic list (using match scores, for example) to control which response is used for which vocabulary. In the rare cases where it's not straightforward to fine-tune the matching using object vocabulary, you can always resort to using regular expressions as the topic match criteria.
In the past, this situation sometimes had undesirable results. In most cases, the two matches to the same object were collapsed into a single match, but the "match flags" from the two matches were combined - so both matches were marked as truncated. Depending on what other objects were in scope at the time, this had the effect of eliminating the object from consideration as a resolution of the noun phrase: even though it had an exact match in the dictionary, the resolver saw it as a truncated match because of the combination of the two sets of match flags. This was especially likely to affect topic phrases (ASK ABOUT, for example), because the global scope made it much more likely that other objects would have exact matches for the same vocabulary.
The parser is now smarter about the way it combines these sorts of redundant matches. Instead of simply OR'ing together the match flags, the parser now specifically keeps only the "strongest" match in these cases. In our example, the parser sees that there's a truncated dictionary match (WINDOWSILL) and an exact match (WINDOW) for the same object, so it now decides to consider the match to be exact.
This can be overridden to consider, for example, loose keys held in the actor's pocket. To prevent the keyring from automatically grabbing any keys when it's taken, simply override this method to return an empty list. Note that you don't need to bother limiting the returned list to include only valid keys, since the caller does this by considering only objects for which isMyKey() returns true.
Released 3/14/2004
Possible compatibility-breaking change: The property formerly named 'vocabWords_' has been renamed to 'vocabWords' (the change is that the underscore formerly at the end of the name has been removed). The underscore suffix is conventionally used in the library to indicate an internal library property that isn't normally for use by game code, which obviously isn't appropriate in this case, as this is the property that games usually initialize to set an object's vocabulary words.
You should search for instances of 'vocabWords_' in your existing code, and replace them with 'vocabWords'. Most existing game code will probably have few, if any, instances of this property name, since most object definitions in existing game code probably use the standard library templates to set this property's value without giving its name. Even so, you should search through your code and update any mentions to use the new name.
AskTellGiveShowTopic is based on the new class TopicOrThingMatchTopic, which can be subclassed to create other combinations of other subsets of these four verbs (or to add topic entries for new custom game-defined verbs). This new class combines the features of the "Thing" and "Topic" base classes for topic entries.
The main reason you might want to generate a topic inventory automatically on activating a node is that the node contains one or more obscure topics that players will be unlikely to guess on their own. If it's unlikely that the player would think to ask just the right thing at just the right time, a topic inventory can help guide them through the conversation by showing the possibilities. As always, only the topics you specifically mark as suggested (with SuggestedAskTopic, etc) will be listed in the inventory. When possible, it's probably better from a game-design perspective to lead players into asking the right questions more subtly, by putting the right cues in the conversation itself; when this is impractical, the topic inventory is a usable substitute.
The other direction still applies, though: when a pronoun refers to a CollectiveGroup object, the parser puts the group's associated individuals into consideration for resolving the pronoun.
The problem manifested as strange behavior when the contained object was removed from the sub-container (via TAKE or the like). In particular, because the object's 'location' was still set to the ComplexContainer, the object wasn't properly removed from the sub-container's contents list, so the object appeared to still be in the sub-container as well as in its new location.
First, if a real-time event itself stopped and asked for input from the user (by reading a new command line, for example), the transcript capturing prevented the text displayed by the event from showing up at all until after the user entered something on the new command line. It was possible to work around this by deactivating the transcript prior to showing any output in the real-time event, but this is no longer necessary.
Second, even if a real-time event didn't display anything, its mere silent firing caused an extra blank line to show up immediately after the user next pressed the Enter key to finish a command line.
Both of these problems are now corrected, so there's no need for real-time events to do anything special with respect to output formatting or command-line input.
Released 3/6/2004
In the past, the game code was responsible for defining two entrypoints: main() and mainRestore(). The template games generated by Workbench's "New Project" command defined these two functions as one-liners that called mainCommon(), where the real action took place.
In the new arrangement, the library now invokes methods of a new object, gameMain, which each game must define. For your convenience, the library defines a class, GameMainDef, that you can use as the base class of your gameMain object. This base class defines suitable defaults for the required methods, so you only have to override what you need to customize. Overall, this change should result in much less required startup code for each game.
If you use GameMainDef as the base class of your gameMain object, you're only required to define one property in it:
In addition, there are a couple of optional optional methods that you can use for customization:
Most games will find it sufficient to define only initialPlayerChar and showIntro(), and simply inherit the default handling for everything else from GameMainDef. However, there are several additional methods that you can override if you want to do something special.
If your existing game code uses the Workbench-generated template, or you follow the same pattern, then rewriting your code to fit the new design is relatively straightforward. Just follow these steps.
gameMain: GameMainDef initialPlayerChar = me showIntro() { // TO DO! } ;
That's it - if you were using the standard Workbench template for mainCommon(), you're now finished.
If you made certain kinds of changes to the template main(), mainRestore(), or mainCommon() that Workbench produced when you originally created your game, you might have to do some additional work. These extra changes are uncommon, but check this list to make sure they don't apply to you:
The properties moved are:
The change is that prepositions in a verb phrase are now grouped inside the parentheses of the "(what)" placeholders for the objects, if the prepositions go with the noun phrases. For example, the LookIn action's verbPhrase string has changed from 'look/looking in (what)' to 'look/looking (in what)'. The difference is subtle: by moving the preposition 'in' so that it's inside the parentheses with the direct object's "(what)" placeholder, we're telling the library that the 'in' is part of the direct object noun phrase. Contrast this with the verbPhrase for Doff, which is now 'take/taking off (what)': this tells us that the preposition 'off' is part of the verb structure, not part of the direct object.
How can you tell which way it goes? There are two rules you should use to determine this.
First, if the preposition separates the direct and indirect objects, such as the IN in PUT X IN Y, then it always goes inside the indirect object phrase, hence 'put (what) (in what)'.
Second, for any other preposition, use the "it test": try writing the verb using 'it' in place of 'what', and ask whether it sounds right for the preposition to go before or after the 'it'. For example, for the Doff action, TAKE IT OFF sounds right, and TAKE OFF IT is obviously wrong. This means that the preposition 'off' belongs in the verb phrase, hence 'take off (what)'. In contrast, for the LookIn action, LOOK IN IT sounds right, not LOOK IT IN, so the preposition goes with the direct object, hence 'look (in what)'. If the "it test" indicates that the preposition goes before the 'it', then the preposition is part of the direct object and thus goes inside the parentheses of the "(what)"; if the preposition goes after the 'it', then the preposition is part of the verb and goes outside the parentheses.
Prepositions that go with the verb are uncommon in two-object verbs, but they do occur. An example from the library is ConsultAbout, one form of which uses the verbPhrase 'look/looking up (what) (in what)'. We'd write LOOK IT UP IN IT, so the preposition 'up' goes after the direct object pronoun and is thus part of the verb, while the preposition 'in' goes before the indirect object pronoun and is thus part of the indirect object. In case there's any ambiguity about whether the 'up' goes with the verb or with the indirect object, try the un-it test: we'd write LOOK UP TOPIC IN BOOK, so clearly the 'up' is not part of the indirect object, but is just part of the verb.
Existing code will need to be scanned for occurrences of TextList, StopTextList, RandomTextList, ShuffledTextList, and SyncTextList. Replace these names with CyclicEventList, StopEventList, RandomEventList, ShuffledEventList, and SyncEventList, respectively. (Note that you can simply do a global search-and-replace to change to suffix TextList to EventList, except that you must change occurrences the TextList when it occurs as an entire word to CyclicEventList.)
In addition, the following property names must be changed:
If you really don't want to change your existing code, you can always use #define to create macros with the substitutions. You really should change your code if possible, though, since using the old names is likely to create confusion as the old names recede from your memory and from the documentation.
First, the greetingList property is no longer used; instead, the state now looks for a HelloTopic entry and uses its response. In addition, you can differentiate between explicit HELLO commands and implied greetings generated by other conversational commands (ASK TO, for example) by creating separate HelloTopic and ImpHelloTopic objects. In most cases, it's not necessary to distinguish the two cases, so you can simply use a single HelloTopic to cover both cases.
Second, the enterFromByeMsg and enterFromConvMsg methods have been removed, along with the enterFromByeList and enterFromConvList properties. Instead, the state now looks for a ByeTopic or ImpByeTopic entry, depending on whether the conversation is ending due to an explicit GOODBYE command or due to an automatic ending (due to the other actor walking away, or due to an inactivity timeout) and uses its response.
You should scan your existing game code for any greetingList, enterFromByeMsg, enterFromByeList, enterFromConvMsg, and enterFromConvList properties, and change them to HelloTopic, ByeTopic, and ImpByeTopic as appropriate.
Note that both the HELLO and GOODBYE topic entries go in the ConversationReadyState, not in the InConversationState. This is because these handlers typically describe the transition to and from the "ready" state. A single in-conversation state could work with several "ready" states, so putting the hello/goodbye topic entries in the in-conversation state wouldn't allow any differentiation among the several "ready" state transitions. Keeping the hello/goodbye entries in the "ready" states themselves makes this differentiation easy. For example, this allows a "goodbye" message to say something like "Bob goes back to sweeping the porch."
In general terms, these changes are designed to concentrate the handling of all conversational actions along a single path. In the past, each type of conversational command was handled by its own cluster of methods, so the Actor and ActorState had a set of parallel methods for HELLO, GOODBYE, YES, NO, ASK ABOUT, ASK FOR, TELL ABOUT, SHOW TO, and GIVE TO. This old design was intended to make it easy to override these handlers in isolation, but experience has since shown that it's much more useful to be able to override all of the handlers in concert instead. The new design therefore consolidates all of these different conversational actions into a single method that's parameterized with the type of action. There are still several methods because of the sequence of processing points through the Actor and ActorState, but there are no longer several clusters of methods: each of the old clusters is now a single method.
An additional benefit of these changes is that HELLO and GOODBYE are now handled through the topic database like everything else. This has two useful effects. First, it means that a DefaultAnyTopic entry will now respond to a HELLO or GOODBYE along with everything else. This is highly desirable in most cases, because these defaults are usually meant to convey a blanket response to all conversational overtures. Second, it means that it's now a bit easier to customize HELLO and GOODBYE responses: just use HelloTopic, ByeTopic, and HelloByeTopic objects as desired.
Advice: Because there are a lot of details to these changes, we suggest you scan your code for mentions of the affected methods, to see if your code is affected at all. If your code doesn't define or call any of these methods, you shouldn't be affected. The methods are:
Now to the specific changes.
Actor.sayToActor() has some changes to its parameters. The 'prop', 'propArgs', and 'topicListProp' parameters have been dropped, and the new 'topic' and 'convType' parameters have been added. 'topic' is a special object representing the topic; the library defines the singletons helloTopicObj, byeTopicObj, yesTopicObj, and noTopicObj for the corresponding library actions. 'convType' is an object of type ConvType describing the type of action being performed.
The new helloTopicObj and byeTopicObj singletons have been added, as mentioned above. These are used as special topic placeholders for the HELLO and GOODBYE commands, respectively.
The new ConvType class has been added, and singleton instances for all of the standard library conversation actions (HELLO, GOODBYE, YES, NO, ASK ABOUT, ASK FOR, etc.) have been defined.
The 'topicListProp' and 'handler' parameters of ActorState.handleConversation() has been dropped, and the new 'convType' parameter has been added. 'convType' is a ConvType object representing the conversation type.
The 'topicListProp' parameter of ConvNode.handleConversation() has been replaced with a new 'convType' parameter, which is a ConvType object describing the conversation type.
Actor now provides a handleConversation() method. The ActorState will invoke this method by default when the ActorState's own handleConversation() method doesn't handle the action. This new Actor method is in lieu of the former cluster of per-command handlers in Actor: yesnoFrom, answerQuestion, hearAbout, beShown, beGiven, answerRequestFor.
All of the per-command handlers in Actor and ActorState have been removed: yesNoFrom, answerQuestion, hearAbout, beShown, beGiven, answerRequestFor. These are replaced by the handleConversation() method in Actor and ActorState, as mentioned above.
The new Actor method defaultConvResponse() is a general handler for showing the default response when a conversational action isn't otherwise handled (that is, it's not handled by any topic database entry, and the ActorState doesn't want to handle it specially). This new method provides an easy way to show the same response for every conversational action - just override this one method, and you can show a common response for all conversation commands. By default, this method routes looks at the convType parameter to determine the conversation type, and invokes the Actor method that provides the default response for that particular conversation type, using the same default response methods used in the past (defaultGreetingResponse, defaultGoodbyeResponse, etc).
The ActorState object can now optionally define any of the default response handlers that Actor can define (defaultGreetingResponse, defaultAskResponse, etc). When these are defined, they'll be called when the state object doesn't provide a suitable TopicEntry in its topic database. Note that these effectively override the Actor's topic database and default response handlers for a given type of action. The order of handling for a conversational action is now ConvNode, ActorState TopicEntry list, ActorState default response method, Actor TopicEntry list, and finally Actor default response method.
You can use the new message customization scheme any time the library generates a standard response message using code like this:
dobjFor(Open) { verify { illogical(&cannotOpenMsg); } }
Any time you see library code like that, you can override the default response message for that action on an individual object simply by defining the message property in your object:
coconut: Thing cannotOpenMsg = '{You/he} would need something sharp to do that. ' ;
In addition to illogical(), this also works with illogicalNow(), inaccessible(), defaultReport(), defaultDescReport(), extraReport(), mainReport() reportBefore(), reportAfter(), reportFailure(), and any other verify or action reporting routines.
Here's how this works. In the past, when a verify, check, or action routine generated a message using a message property, the library looked up the message in the current actor's "action message object," which is usually playerActionMessages when the actor is the player character, and npcActionMessages otherwise. Now, the library still looks there, but only after checking the individual objects involved in the command to see if any of them define the message property.
The library looks first at the direct object, then at the indirect object. If you were to create your own custom verbs with three or more object slots, such as PUT COIN IN SLOT WITH TWEEZERS, the library would automatically continue on to those extra objects as well. If the library finds the message property in any of these objects, it stops and uses that object as the source of the message; if it can't find the message property among these objects, the library simply falls back on the standard message object (playerActionMessage or whatever).
Important: As part of this change, all of the library message properties have been renamed to end in "Msg". This affects every message property, so if you've created your own "action message object," or you've used 'modify' to change playerActionMessages and/or npcActionMessages, you'll have to do some extensive searching and replacing to add the "Msg" suffix to every message property name. Sorry about this; the proposed change was put to the TADS 3 mailing list, and no one objected. The naming change isn't gratuitous. The reason for the name change is that it should greatly reduce the chances of collisions between message properties and properties defined for internal use by an object. The library itself formerly had a number of these collisions, so it was necessary to rename at least those properties; using the naming convention consistently for all of the message properties will help ensure that games don't inadvertantly introduce their own name collisions.
There's one last detail to mention. An object can override a message property with another message property. For example, the Vaporous object in the library uses this feature:
notWithIntangibleMsg = ¬WithVaporousMsg
When a message property in an object points directly to another property, the library takes this as an indirection to another library message from the action message object. This feature is mostly for the library's benefit, since library objects are required to get all of their messages from the action message object (to ensure that the library can be translated without rewriting entire object definitions).
Existing game code that overrides or calls getState or curState for a Script object (including any EventList subclass) will need to be changed to use the new name. You should scan your source code for occurrences of these names, and rename them as needed. Note that you should not rename curState properties that pertain to Actor objects, since Actor.curState has not been renamed.
In most cases, game code won't have any reason to override or access these script properties at all, so most game code should be unaffected by this change. Game code usually just defines instances of the library EventList subclasses using templates, and such code won't be affected by this change.
The direction of a relationship is specified by the new Attachable method isMajorItemFor(obj). By default, this method always simply returns nil, which means that there are no "major" items by default, which makes all attachment relationships symmetrical by default. If you wish, you can override isMajorItemFor() so that it returns true in some cases. When A.isMajorItemFor(B) returns true, the relationship will always be described such that B is said to be attached to A: examining A will yield "a B is attached to the A," while examining B will show "the B is attached to an A."
A few new methods have been added to support the new feature; these are mostly for internal use, but could potentially be used by a game to fine-tune the way attachments are listed. The new method isListedAsAttachedTo(obj) lets the object indicate whether or not 'obj' is listed among the things 'self' is attached to; by default, this returns true if 'obj' isn't permanently attached and 'self' isn't the "major" item for 'obj'. The new method isListedAsMajorFor(obj) is essentially the major-list counterpart: it indicates whether or not 'obj' is listed among the things attached to 'self' when 'self' is described. By default, this method returns true if 'self' is the "major" item for 'obj', and obj.isListedAsAttachedTo(self) returns true (that is, 'obj' thinks it should be listed as attached to 'self'). Finally, majorAttachmentLister returns the lister to use for the items attached to 'self' for which 'self' is the "major" item in the relationship; by default, this uses a MajorAttachmentLister instance.
In addition to letting you set up "under" and "behind" relationships among objects initially, these new classes support the PUT UNDER and PUT BEHIND commands to let actors add new contents under and behind the objects. (The PUT BEHIND command is also new in this release.) An Underside can have new objects added under it with PUT UNDER, and a RearContainer or RearSurface can have new contents added with PUT BEHIND. These commands can optionally be disallowed for a given Underside or RearContainer/RearSurface: override the properties allowPutUnder and allowPutBehind, respectively. The new classes derive from BulkLimiter, so you can use the usual BulkLimiter properties to control the individual and total bulk allowed under and behind the objects.
ComplexContainer has been extended to support these new classes. The new ComplexContainer property subUnderside can be set to an Underside object representing the space under or bottom surface of the complex container; the new property subRear can be set to a RearContainer or RearSurface representing the space behind or back surface of the complex container. PUT BEHIND and LOOK BEHIND commands on the complex container are routed to the subUnderside; PUT UNDER and LOOK UNDER are routed to the subRear; and both subcomponents are included in the regular LOOK AT display.
These additions were adapted from work originally done by Eric Eve.
+ washingMachine: ComplexContainer 'washing machine' 'washing machine' subContainer: ComplexComponent, Container { /* etc */ } subSurface: ComplexComponent, Surface { /* etc */ } ; ++ Thing 'blanket' 'blanket' subLocation = &subContainer ; ++ Container 'laundry basket' 'laundry basket' subLocation = &subSurface ;
The blanket and the laundry basket are nominally directly inside the washing machine itself, according to the "+" syntax, but their 'subLocation' settings ensure that they end up in the desired component sub-containers during initialization.
Note that 'subLocation' is only intended for initialization, so the library automatically sets subLocation to nil for each object right after the initial setting is used. This helps avoid any unpleasant surprises should the object be moved into a different ComplexContainer later on. When you're moving objects around on the fly in your program code, there's no reason to use subLocation at all; instead, just specify the appropriate component as the explicit destination of the moveInto: towel.moveInto(washingMachine.subSurface), for example.
In addition, the object formerly called openingLister has been renamed to openableOpeningLister.
CommandTopic works like any other TopicEntry object, so you can put these in ConvNode, ActorState, and Actor topic databases to provide responses under the specific conditions you need to match.
As part of this change, ActorState.obeyCommand() now invokes handleConversation() to look for a response to the command, specifying the Action object as the topic and 'commandConvType' as the conversation type object. This looks as usual in the ConvNode, ActorState, and Actor conversation topic databases for a CommandTopic to handle the response, or for a DefaultAnyTopic if there's no CommandTopic.
First, when building the sense information table, the library now calls a new method, addToSenseInfoTable, on each object connected by containment to the source object. This method by default does what the library has always done: it evaluates the temporary sense properties set up by the containment path traversal, and adds a SenseInfo object to the table based on the sense properties. An objects can now override this method to add a table entry based on a different sensory status.
Second, when finding the containment paths from one object to another, the library calls the new method specialPathFrom on the target object if (and only if) no ordinary containment path can be found. This method can supply its own custom containment path. This allows an object to exist outside of the normal containment hierarchy but still get a chance to represent its connection to the normal containment hierarchy as it sees fit.
This change makes it much easier to work with CollectiveGroup objects, because it makes a CollectiveGroup as visible, audible, etc. as any of its individuals.
Note that this change doesn't affect CollectiveGroup objects that have a normal location. These objects already participated in the sensory model in the normal manner, and will continue to do so. This change only affects group objects with no location.
In the past, the 'verify', 'check', and 'action' methods for the Default handlers were called in addition to any verb-specific versions of the methods. For example, if an object defined a dobjFor(Default) with an action() method, and the player entered an OPEN command on the object, the Default action handler ran, and then the dobjFor(Open) action() method also ran. In most cases, this made no difference, because the base Thing class doesn't even define action() or check() handlers for most verbs, since it disallows most verbs in the verify() stage.
With this change, the Default handlers now run instead of the verb-specific handlers, when the Default handlers run at all. The rules about when Default handlers override verb-specific handlers, and vice versa, haven't changed. A Default handler still overrides any verb-specific handler inherited from a base class, and any verb-specific handler defined in the same class as the Default handler or in any subclass still overrides that Default handler.
Note that the relative order of the All and Default handlers has been reversed: the All handler now always runs first. This makes the sequence of methods proceed from most general to most specific.
Note that this doesn't apply if 'exit' is used within a nested implied action; this only applies if 'exit' is used within the precondition's checkPreCondition() method itself.
The message specifier can be one of several standard, pre-defined objects, or it can simply be a string to display. The pre-defined object ftDeath, ftVictory, ftFailure, and ftGameOver display messages for death of the player character ("YOU HAVE DIED"), victory ("YOU HAVE WON"), failure ("YOU HAVE FAILED"), and simply "game over", respectively. Alternatively, you can make up your own messages for unconventional cases ('YOU HAVE SUFFERED A FATE WORSE THAN DEATH', say) simply by specifying a string. Your string will be displayed surrounded by "***" sequences to yield the conventional formatting, or with other sequences that might vary by language. You can also pass nil as the message specifier, in which case no message at all is displayed.
If your game has complex scoring that makes it difficult or impossible to state a maximum possible score, or if you simply don't want to give this information to the player, simply set maxScore to nil in your gameMain object. The library uses nil as the default because there's no reason for the library to assume that a game will have a particular maximum score (the library formerly used a default value of 100, but this was overly presumptuous).
If you explicitly set the maxScore property in your gameMain object, the library will not automatically compute the maximum score. Your explicit maxScore setting always overrides the computed value.
To take advantage of the new capabilities, simply define the new 'points' property for each Achievement object you create. Then, rather than calling addToScore() or addToScoreOnce(), both of which take a parameter specifying the number of points to award, call the new Achievement methods awardPoints() or awardPointsOnce() instead. For example, suppose you have some existing code that looks like this:
vase: Thing handleBreakage() { // ... do the real work here... scoreMarker.addToScoreOnce(10); } scoreMarker: Achievement { "breaking the base" } ;
This code defines a nested object called 'scoreMarker' to describe the scoring item, and the 'handleBreakage' method awards the Achievement object, assigning it 10 points in the score. Using the new style, you'd change the code above to look like this instead:
vase: Thing handleBreakage() { // ... do the real work here... scoreMarker.awardPoints(); } scoreMarker: Achievement { +10 "breaking the base" } ;The "+10" in the nested Achievement definition assigns the object a value of 10 in its 'points' property (using a template defined in adv3.h), indicating that the item is worth 10 points in the score. Rather than specifying this in the code that awards the Achievement, we define it as part of the Achievement object itself. The code that awards the points doesn't need to specify the number of points; it just calls the awardPoints() method of the Achievement object, which awards the points defined in the object. This makes the code easier to read, since you can see the description of the scoring item and the number of points it's worth in one place.
To take advantage of the library's automatic computation of the maximum possible score, you have to follow a few rules:
If you follow these rules, the library will accurately compute the maximum score value, so you don't need to bother figuring out how many points are in the game yourself, and you don't need to worry about initializing libScore.maxScore in your game's start-up code.
If your game can't follow the rules above (because you have alternative solutions to puzzles that assign different scores, for example, or because your game has alternative paths with different maximum scores), you'll need to figure the maximum score manually, and set the property 'maxScore' to this value in your 'gameMain' object. Explicitly setting gameMain.maxScore in your code will override the library's automatic computation.
This change also adds a new Thing method examineListContentsWith(). This is simply a service method that performs the bulk of what Thing.examineListContents() did before, but is parameterized by a lister to use to generate the listing.
AskConnector makes this easy. Simply define the room's 'north' as an AskConnector object, and set the AskConnector instance's property 'travelAction' to 'GoThroughAction'. The connector, when invoked for travel, will try running the GoThroughAction, but with a missing direct object; this will make the parser ask the player which door to use with the usual prompt for a missing object: "What do you want to go through?"
If the direction has a specific set of possible objects, as in our example of two doors leading north, you can further refine the parser's question by specifying the 'travelObjs' property. Set this to a list of the possible direct objects for the action. If you set this property, you should also set 'travelObjsPhrase' to a string giving the noun phrase to use in disambiguous questions: in our example, we might set this to 'door', so that the question becomes "Which door do you mean...?".
First, TravelPushable.movePushable() now uses moveIntoForTravel() to move the object, rather than moveInto(). This corrects a problem that occurred when the starting and destination locations were connected by a sense connection (such as a distance connector).
Second, movePushable() and describeMovePushable() each now take an additional argument giving the connector being traversed.
Third, the new method beforeMovePushable() gives the pushable object a chance to do any necessary work just before the travel takes place. This is especially useful for adding a message describing anything special that happens when pushing the object out of its current location. By default, this routine does nothing.
Now, the directional message is the default in the base TravelConnector class. If there's a direction property linked to the connector from the origin (for a departure) or destination (for an arrival), the base TravelConnector messages will now show the directional version of the message. The generic, non-directional message will only be shown when no directional link can be found. Note that this doesn't affect the more specific types of connectors and their custom messages, so stairs will still say "Bob goes up the stairs," doors will still say "Bob leaves through the wooden door," and so on.
This change is necessary to allow actors to hold other actors, and to allow intermediate containers within rooms (that is, to allow a Room to hold an arbitrary Thing subclass, which in turn holds a NestedRoom). Actor has methods with the names getTraveler() and getPushTraveler(), but these methods have a different purpose than the old BasicLocation methods: the Actor methods get the traveler when the actor is initiating the travel, and the old BasicLocation methods get the actual traveler when a traveler within the location initiates travel. The use of the same name created a conflict between these different purposes, so one or the other set had to be renamed. In addition, limiting these methods to BasicLocation prevented ordinary Things from holding actors who could travel; adding them to Thing corrects this.
Stairways were the only library objects that exposed the problem, but it was possible to define your own objects with the same bad behavior. In general, the problem occurred whenever a two-object push-travel action (PUSH x UP y, PUSH x THROUGH y, PUSH x INTO y, etc.) was attempted, and the indirect object's handler for the corresponding travel action (ClimbUp, GoThrough, Enter, etc.) was defined using asDobjFor(), and the handler for the target action of the asDobjFor() was itself defined using a remapTo().
The problem has been fixed, so any two-object push-travel action should now be handled equivalently to the corresponding one-object (directional) push-travel action on the same travel connector.
The new property allowedPostures contains a list of the postures that the object allows. By default, Chair allows sitting and standing, and Bed and Platform allow sitting, standing, and lying. You can override allowedPostures for an individual Chair, Bed, or Platform object to add or remove allowed postures. When a posture isn't in the allowed list, it will be ruled as "illogical" in the verification stage.
In addition, the new property obviousPostures contains a list of the postures that are "obvious" for the object; these are the postures that are most natural for the object. The obvious postures are enumerated separately from the allowed postures to control defaulting: if a posture is allowed but not obvious for an object, then the corresponding command will be ruled as "nonObvious" in the verify stage, ensuring that the command will only be accepted if stated explicitly (that is, the player will be able to perform the command, but the parser won't automatically guess the command based on incomplete information from the player). By default, it's obvious to sit on a chair, to sit or lie on a bed, and to sit, lie, or stand on a platform.
The three separate classes are still three separate classes for a couple of reasons. First, each one has a primary, "natural" posture associated with it: sitting on a chair, lying on a bed, standing on a platform. This primary posture is the one that the library will assume for implied actions and incomplete commands. Second, Platform differs from the other two in that objects dropped while standing on a platform land on the platform, whereas objects dropped while on a chair or bed land in the enclosing room.
Note that a posture that isn't allowed by allowedPostures will be treated as illogical. If you want to create an object for which a given posture makes physical sense, but isn't allowed for some other reason (character motivation constraints, for example), you should include the posture in allowedPostures and then disallow the corresponding command using the check() routine.
The base NestedRoom now checks, in the GetOutOf action's check() handler, that there is a non-nil exitDestination. If there's not a valid exit destination, the check() handler calls the new method cannotMoveActorOutOf() to display an appropriate message, then terminates the command with 'exit'.
The existing FloorlessRoom class has been retained, but is now simply a convenience class that combines Floorless and Room. This provides the same behavior as the old FloorlessRoom class, so existing code should not be affected.
The reason for dropping the extra announcements is that they don't really tell the player anything useful, so they're just unnecessary verbosity. Because of the recursive relationship of the implied action in these cases, the first action is nested in the second, which is nested in the third, and so on; so the second and subsequent actions all failed as a direct result of the first one failing (in our example, the OPEN fails because it requires the UNLOCK to succeed, so when the UNLOCK fails the OPEN must also fail). This means that we never actually get around to trying the second and subsequent actions; when the first action fails, we give up on trying any of the others. The only thing they tell the player is why the failed action was attempted at all (in our example, the implied OPEN explains why we're attempting the implied UNLOCK). But the rationale in these cases is almost always obvious to the player, so even this isn't a very good reason to keep the extra announcements.
This behavior can be controlled via implicitAnnouncementGrouper.keepAllFailures. The property is nil by default; to list the entire stack of failures for these cases, change it to true.
Instead, a Lockable now tests a new method, autoUnlockOnOpen(); if this returns true, then OPEN implies UNLOCK, otherwise it does not. This means that, if autoUnlockOnOpen returns nil, and the object is locked, then an OPEN command will simply fail with an error message ("the door seems to be locked"), and the player will have to enter an explicit UNLOCK command.
The new autoUnlockOnOpen() method is defined as follows:
Some authors might always prefer the automatic UNLOCK to forcing a player to type a separate UNLOCK command for objects with non-obvious locking status. Which way you prefer is a matter of taste. On the one hand, the extra UNLOCK command is a little annoying to players, and is exactly the sort of thing the precondition mechanism was created to avoid. On the other hand, the automatic UNLOCK is weird when the lock status isn't apparent, because until the OPEN fails, we have no way of knowing that the object was locked in the first place and thus no reason to try the implied UNLOCK; it's an instance of the parser revealing game-state information that the player character isn't supposed to know. It also could be seen as actually detracting from playability by making the game do too much automatically, taking away a certain amount of control from the player (the player could think: "I didn't even know that door was locked; if I had, I wouldn't have tried to unlock it right now.")
By default, the library implements the more realistic but also more tedious behavior, requiring a separate UNLOCK to OPEN a locked object whose lock status isn't obvious. If you prefer to make UNLOCK automatic on OPEN for a given object, simply override autoOpenOnUnlock() to return true unconditionally for that object; if you prefer to make UNLOCK automatic for all objects, modify Lockable to make autoOpenOnUnlock() return true in all cases.
>go north (first trying to unlock the iron door) You need a key to unlock the door.
Note, however, that if the actor knows which key to use, and one of the known keys is in sight, then the command will be allowed to proceed implicitly. In these cases, it won't be necessary to ask for the key, since the actor's knowledge of the key allows it to be supplied automatically as a default.
An additional improvement in the new FueledLightSource class is that the fuel source is no longer assumed to be the object itself. This lets you model the fuel source as a separate object; for example, you could use a separate battery object as the fuel source for a flashlight, allowing a character in the game to swap out a dead battery for a new one. Simply override the 'fuelSource' property of your FueledLightSource object to specify the object that provides the fuel. By default, the fuel source is still 'self', so Candle objects in existing source code won't need any changes.
ActorState.takeTurn() now uses this information to consider the NPC to have finished its turn if the ConvNode added a conversational message. In the past, merely having a non-nil ConvNode was enough to make takeTurn() consider the turn to be taken entirely by the ConvNode; now, the turn is only considered finished if the ConvNode actually wanted to say something. This allows ordinary actor-state background scripts to continue running even when a ConvNode is active, as long as the ConvNode doesn't generate any NPC conversation messages.
Note that readiness is more important than agendaOrder. Suppose that AgendaItem A has a lower agendaItem value, and thus goes earlier in the list, than AgendaItem B. Despite this list order, if B is ready to run and A is not, B will be executed.
In a related change, SpecialTopic now inherits from SuggestedTopicTree, rather than SuggestedTopic as it did in the past. This ensures that a special topic will be suggested in the topic inventory when any of its AltTopic children are active.
+ Thing 'big large huge gigantic giant book/tome/volume/ text/manuscript' 'huge tome' // etc ;
Note the line break in the middle of the list of nouns. This introduces an extra space into the string, which is now perfectly okay.
The Thing and CollectiveGroup objects use this new method to ensure that a collective group's filtering is applied consistently for each command, whether the command uses a full noun phrase or a pronoun to refer to the groupable objects.
In a related change, the parser's method of choosing a facet as the antecedent of a pronoun has changed slightly. First, the parser now considers all in-scope facets when an antecedent has facets, even if the base antecedent object is itself in scope. Second, the parser chooses which in-scope facet to use based on the same criteria as above: visibility, then touchability. This ensures that when multiple facets are in scope (because of sense connectors, for example), the most readily visible and reachable one will be selected.
In practice, this means that you can simply use a string in any nested action context where a literal is required, and you can use a simple Thing or Topic object in any context where a topic is required. Examples that formerly required special coding, but now work in the obvious fashion:
In the past, the library tested in a few places to see if a given property or method was inherited from a given library base class. This was almost the same test that overrides() performs, but didn't work properly in cases where a library base class had been modified by a game or library extension; when 'modify' is used to extend a class, the defining class for methods defined in the original (pre-modification) version of the class is the original version, not the new version created by 'modify', which takes on the original name. To allow for 'modify' usage, all of the tests the library used to make along these lines have now been replaced with calls to overrides() instead.
(This is simply an internal change to eliminate some unnecessary extra source code that's been disabled for a long time anyway. When the sense-cache mechanism was first introduced, it was made optional, so that any games that had problems with the caching could turn it off. The code has been active by default for a long time now, and I'm not aware of anyone who has found it necessary or desirable to disable it, so there no longer seems to be any value in the added complexity of having both options. Removing the conditional option simplifies the source code by removing the effectively dead code for the option of disabling the sense cache.)
Released November 15, 2003
// OLD WAY!!! + TopicEntry @lighthouse "It's very tall..."; ++ AltTopic "Not really..." isActive=(...); +++ AltTopic "Well, maybe..." isActive=(...); ++++ AltTopic "Okay, it is..." isActive=(...);
...you'd now instead write it like this:
// the new way + TopicEntry @lighthouse "It's very tall..."; ++ AltTopic "Not really..." isActive=(...); ++ AltTopic "Well, maybe..." isActive=(...); ++ AltTopic "Okay, it is..." isActive=(...);
The only change you need to make is to remove the additional nesting from the second and subsequent AltTopic in each group - simply put all of the AltTopics in a group at the same '+' nesting level.
The old nesting scheme had a tendency to get unwieldy whenever a single topic had more than one or two alternatives. The new scheme makes game code a little cleaner when defining large AltTopic lists.
For example, you might want to set up a travel connector that leads out of a vehicle, in which case you'd want the actor within the vehicle rather than the vehicle to be the traveler when the connector is traversed. In these cases, you'd override the vehicle's getTraveler() method to check the connector, returning the actor rather than the vehicle when the outbound connector is the one being traversed.
In addition, for greater generality, the "actor" argument to BasicLocation.getTraveler() (and subclass overrides) has been renamed to indicate that it now takes a traveler rather than merely an actor. Vehicle.getTraveler() takes advantage of this: it now recursively asks its container for the traveler, passing itself (rather than the traveler within itself) as the proposed traveler. These changes allows for situations where an actor is inside a vehicle that's inside another vehicle, for example.
Finally, Vehicle.getTraveler() now makes an additional check before returning itself as the traveler: if the connector allows the traveler within the vehicle to pass (as indicated by the connector's canTravelerPass() method), but the connector doesn't allow the vehicle itself to pass, then getTraveler() simply returns the original traveler rather than trying to make the vehicle travel. This makes it easy to set up an outbound connector from within a vehicle - simply set up the connector's canTravelerPass() to exclude the vehicle itself, and a traveler within the vehicle will automatically leave the vehicle behind on traversing the connector.
The TravelConnector method connectorBack() now takes a Traveler object rather than an Actor as its first argument.
The TravelConnector method fixedSource() now takes a Traveler instead of an Actor as its second argument.
The Thing method getTravelConnector() now accepts a nil 'actor' argument. When the actor is nil, it means that the caller is interested in the structure of the map independently of any actor's present ability to perceive that structure. This is useful in some cases, such as auto-mapping, where we want to know what the map is actually like rather than merely what it looks like to a given actor at a given time.
The internal handling of the various PushTravel action subclasses now uses only the 'verify' stage of the action handling for the indirect object, when there's an indirect object at all. For example, PUSH BOX THROUGH DOOR only calls the door's dobjFor(GoThrough) 'verify' handler, not any of its other handlers. This change reflects the fact that the action of a push-travel is always carried out by a nested TravelVia with the special PushTraveler object, hence the only need we have for the indirect object handlers at all during the initial PushTravel handling is for disambiguation via the 'verify' routine. The old implementation was problematic when the indirect object remapped decomposed verb, because it incorrectly remapped the entire action before the proper nested action could be invoked. This change corrects the problem, allowing the PushTravel varieties to work correctly for any combination of objects.
In the Traveler class, describeDeparture() and describeArrival() now show their messages in a visual sense context for the traveler, if the player character isn't involved in the travel. This ensures that the travel is properly described (or properly not described) in situations where the traveler is visible to the player character but the motivating NPC isn't, or vice versa, such as when an NPC is inside an opaque vehicle.
The new class was introduced for two reasons. First, it consolidates the behavior common to all non-portable objects, eliminating a small amount of redundancy in the former arrangement. Second, and much more importantly, it roots all of library's non-portable object classes in a single base class. This means that code can reliably test an object for unportability by checking obj.ofKind(NonPortable). In the past, it was necessary to write (obj.ofKind(Immovable) || obj.ofKind(Fixture)); not only is that test more cumbersome, but its very form is suggestive of the potential for additional "||" clauses in the future. The new arrangement formalizes in the library a single root class for all unportable objects, ensuring a clean way for the library and library extensions to add any future specialized unportables without breaking existing game code.
Note that this change does not break existing game code that uses the "||" test; other than the new common base class, the inheritance structure for Fixture and Immovable and their subclasses has not changed. Even so, it wouldn't be a bad idea to replace any "||" tests in your existing code with the simpler ofKind(NonPortable) test, because this will ensure that your code takes advantage of the insulation from future changes that the new class affords.
The Attachable class provides default handlers for AttachTo, Detach, DetachFrom, and TakeFrom, but it's relatively easy to define additional, customized action handlers in terms of these default actions using the standard 'remapTo' mechanism. For example, if you were defining a rope, you could define a TieTo action, and then map TieTo to AttachTo in your rope object.
The new classes PermanentAttachment and PermanentAttachmentChild are subclasses of Attachable for cases where you describe objects as attached in the story text, but you don't want to allow the objects to be detached from one another. Describing objects as attached tends to invite a player to try to detach them; these classes lets the objects provide customized responses to the player's attempts, without actually allowing the objects to come apart. (These classes aren't intended for mere components; the Component class is adequate for that. PermanentAttachment is for situations such as a ball attached with a string to a paddle, where essentially separate objects are conspicuously attached to one another.)
The traditional way of programming these sorts of objects was to create the object with a 'nil' initial location, then use moveInto() to move the object into the game upon the triggering event. PresentLater provides an alternative. To use PresentLater, you set up the object in its eventual location, as though it were going to be there from the start, but you add PresentLater at the start of the object's superclass list. During pre-initialization, the library will remember the object's starting location, and then move the object to 'nil', effectively removing it from the game world. Later, when you want to the object to appear, simply call makePresent() on the object, and it will automatically move to the eventual location that was noted during start-up.
The advantage of using PresentLater is that you define the object's eventual location as part of the object itself, as though it were an ordinary object; this means that you don't have to specify its eventual location as part of some method (in a moveInto call) elsewhere in the source code. Another advantage is that you can use the makePresentByKey() method to bring a whole set of related objects into the game world at once: you can give each PresentLater object a "key" property, which is just an arbitrary value you choose to identify a group of objects, and then bring all objects that have that key into the game world at once.
Similarly, the Floor object's SitOn and LieOn handlers now apply a precondition requiring the actor, rather than the traveler, to be in the room directly containing the Floor object.
The parser uses getFacets() to resolve an out-of-scope pronoun antecedent, if possible. If the player uses a pronoun, and the antecedent is out of scope, the parser calls the antecedent's getFacets() method, and looks for a facet that's in scope; if it finds an in-scope facet, the parser uses that facet as the replacement antecedent. For example, if you type OPEN DOOR, GO THROUGH IT, THEN CLOSE IT, the parser will now correctly resolve the second "it" to the door. In the past, the second "it" wasn't resolvable, because it referred to the side of the door that's in the other room and thus out of scope after the travel. Using the new facet list capability, the parser can now resolve the second "it" to the side of the door that is in scope.
(If more than one new facet of the object is in scope in these cases, the pronoun resolver will take the one that's most readily visible - that is, the one that has the most "transparent" sense path in the actor's visual senses. If all of the in-scope facets are equally visible, the resolver just picks one arbitrarily.)
By default, VocabObject.getFacets() simply returns an empty list. The Door and Passage classes override the method to return a list containing the linked object representing the other side of the door or passage, if there is one. The new MultiFaceted object returns the list of facet objects that it synthesizes; each facet instance of a MultiFaceted does the same thing.
You can customize getFacets() whenever you use multiple objects with a shared parser identity. One common case where you might want to do this is "object transmutation," where you substitute one object for another in order to effect a radical change in the setting. For example, you might have one object representing a wooden dining table, and another representing the pile of splintered wood that an enraged chef turns it into after the guests insult his cooking. In this case, you could override getFacets() in the table so that it returns the pile-of-splintered-wood object.
Along the same lines, the DynamicMultiLoc class has been removed, and its functionality has been rolled into MultiLoc. In particular, the reInitializeLocation() method is now part of the base MultiLoc class.
If you have any AutoMultiLoc or DynamicMultiLoc objects in your existing game code, you should simply change the AutoMultiLoc or DynamicMultiLoc superclass to MultiLoc. The objects should then behave the same as they did before.
MultiInstance is good when you want to duplicate a ubiquitous background object in multiple locations: trees in a forest, the sun in outdoor locations, crowds in the street. MultiFaceted is for large objects that span many locations, such as rivers or long ropes. MultiLoc isn't ideal for these sorts of cases because the sense system treats a MultiLoc as a single object that's entirely in all of its locations at once; this means, for example, that if it's lit in one place, it's lit everyplace.
MultiInstance and MultiFaceted work mostly like MultiLoc from the programming perspective. Internally, though, rather than appearing itself in each of its locations, a MultiInstance or MultiFaceted creates a separate "instance" object for each of its locations. Each instance is an ordinary singly-located object, so the library creates one per location. The instance objects are instances of a template that you define as part of the MultiInstance or MultiFaceted object.
The new classes present the MultiLoc multi-location interface, so you can add and subtract locations as needed, using the usual routines: moveInto(), moveIntoAdd(), moveOutOf(). You can also set the initial set of locations as you would for a MultiLoc: you can set locationList to the list of initial locations, or you can define initialLocationClass, isInitiallyIn, and/or buildLocationList.
The instances of a MultiInstance or MultiFaceted are all essentially identical, so these classes are only good for relatively homogeneous objects. These classes aren't appropriate for cases where you want to present distinctive aspects of an object, such as the different sides of a large building, or distinctive individual objects, such as different people in a crowd. When the individual parts are distinct, you're best off just creating separate objects for the different instances. The new classes are mainly for convenience in cases where you'd otherwise have to repeat an identical object definition in several locations.
For example, PUSH COIN WITH STICK would be a good place to use iobjTouchObj as a direct object precondition: the actor is manipulating the stick directly, so the actor has to be able to touch the stick, but the coin only needs to be reachable indirectly with the stick. Another example: PLUG CORD INTO OUTLET requires direct manipulation of the cord, but only the cord needs to touch the outlet, so this would be a good case for dobjTouchObj as a precondition on the indirect object.
In the class Thing, the default direct object handlers for the actions MoveWith, TurnWith, ScrewWith, and UnscrewWith now use the iobjTouchObj condition rather than touchObj condition. These verbs all generally model interactions where the direct object only needs to be reachable through the indirect object. The new class Attachable uses dobjTouchObj as a precondition on the indirect object.
To enable the new iobjTouchObj condition, the TouchObjCondition class can now handle a yet-unresolved source object during the verification stage. When the source object is nil, the pre-condition simply skips its verification stage. This allows a TouchObjCondition to be applied across objects for a two-object action: for example, it allows you to apply a condition to the direct object that the indirect object can touch it.
These conditions use the new Thing method tryMovingObjInto(obj), which tries to move the given object 'obj' into 'self'. This method is customized in Room, Container, Surface, and Actor to generate appropriate implied commands for those classes, and you can override it in your own objects as needed.
The new conversation-ending code endConvActor indicates that we're ending the conversation of the actor's volition.
It's not always possible for setTopicPronoun() to guess about an antecedent for a topic phrase match, because TopicEntry instances can match more than one game object, and topic phrases by their nature can refer to more objects than are present visually. The new method will set a pronoun antecedent for the topic if the topic phrase in the player's input yields one object when "intersected" with the TopicEntry's 'matchObj' list. The method first looks only at in-scope objects, then looks to the "likely" list, but only if there are no in-scope matches. If the intersection yields more than one object, the library doesn't set a pronoun antecedent at all, since the match is ambiguous.
Since this heuristic procedure can't always decide on an antecedent, you might occasionally want to set the antecedent explicitly in a particular TopicEntry. You can do this by overriding the TopicEntry instance's setTopicPronoun(fromActor,topic), in which you'd call fromActor.setPronounObj(obj), where 'obj' is the game object you want to set as the antecedent.
A separate problem prevented an undo savepoint from being created for an "again" command repeated a command directed to another character. Since no savepoint was created, typing "undo" after such an "again" command took back not only the "again" turn but the turn before it as well. The savepoint is now created properly.
As part of this change, the ActorTopicEntry class, which was introduced in the refactoring of the TopicEntry class introduced in 3.0.6l, has been eliminated. On further consideration, the bifurcation of TopicEntry into actor-associated and non-actor-associated subtypes wasn't very clean, since it implied a similar split for some of the other classes that work with TopicEntry, notably AltTopic and DefaultTopic. This was too unwieldy.
So, ActorTopicEntry has been removed. In its place, the "topic owner" abstraction, which was introduced in 3.0.6l, has been used throughout the base TopicEntry class to replace the need for an associated actor. This means that the base TopicEntry can do everything that it did before (and everything that ActorTopicEntry did before), but without any assumption that there's an associated actor. The getActor() method is still present, but it's now strictly for the convenience of game code; the library no longer assumes that topic entries to be associated with actors. The TopicEntry.getActor() method simply returns the topic owner if it's of class Actor, otherwise nil.
These changes should have no impact on existing game code, since the ActorTopicEntry was intended as an internal class structure only.
Note that these changes are designed in such a way that objects based on both Openable and Container (either by mixing the two superclasses, or by using the library class OpenableContainer) should be unaffected. Objects based on Container or BasicContainer, without any Openable mix-in, should now behave more logically, in that they won't assume anything about being openable.
This mechanism is now replaced with something a little more subtle, which solves the same problem without any loss of convenience for the player. Instead of letting the original OPEN command fail on encountering a door that's locked but not previously known to be locked, the library now inserts a "testing" action into the sequence ahead of the implied UNLOCK. The "testing" action represents the actor's first attempt to open the lockable, as in attempting to turn a locked doorknob. This first phase fails, but instead of letting the rest of the command fail, we consider the actor to have immediately learned from the first phase that the object is locked, so we proceed directly to an implied UNLOCK action. The result looks like this:
>go north (first trying the door and finding it locked, unlocking it, then opening it)
Note that the lockStatusObvious property of the former scheme still applies. If lockStatusObvious is true, then the extra "testing" phase is omitted, since we can tell just looking at the object that it's locked. Note also that the "testing" phase is only used when the object is actually locked; when it's not locked, the OPEN command succeeds, so there's no need to split the OPEN action into the two phases.
An example of a non-proper but qualified name is "your book." This isn't the proper name of the book, but the possessive pronoun makes it fully qualified, making it incorrect to add an article when using the name in a sentence.
In practice, at the moment, "qualified" and "definite" have the same effect, which is to omit any articles when the name is used in a sentence. The new property is intended to separate the two concepts in case there's a need to distinguish them in the game or in future library changes.
Note that an ampersand is treated as alphabetic, not as a punctuation mark. This has two implications that might be counterintuitive, at least to the extent that ampersands look to most people like punctuation marks. (In fact, they're not punctuation marks at all; they're actually ligatures of "et," Latin for "and," although the glyph has become so stylized in modern typefaces that it's hard to see that unless you know what to look for.) First, an ampersand is not a token separator, so a sequence like "T&V" will be read as a single token. Second, when an ampersand is surrounded by whitespace (or other token separators), so that it does count as a separate token, it'll be parsed as a "word" token, not as punctuation. For example, "Bob & Sons" is read as three "word" tokens. This means that you can use a lone "&" as an adjective or noun in defining an object's vocabulary.
As part of this change, the Action routine that was formerly called getReflexiveBinding has been renamed to getAnaphoricBinding. The new name better reflects the more generalized function, since it can be used for other kinds of anaphoric bindings besides reflexives. This is an internal method that is highly unlikely to be used in any game code, so this change should be transparent.
("Anaphor" is the linguistic term for any sort of reference to an earlier noun phrase in a sentence; a reflexive is a particular kind of anaphor that re-uses a noun phrase from an earlier "slot" in a predicate to fill another slot in the same predicate, such as HIMSELF in ASK BOB ABOUT HIMSELF. The reason reflexives exist in English and other languages is that they're unambiguously anaphoric: in ASK BOB ABOUT HIMSELF, HIMSELF can only refer to Bob, whereas the HIM in ASK BOB ABOUT HIM almost certainly refers to someone other than Bob from a previous sentence or clause. The name change in the method reflects the fact that there are other kinds of anaphoric constructs besides reflexives; in ASK BOB ABOUT HIS BOOK, the HIS refers anaphorically to BOB, but it's not reflexive.)
This problem showed up by making an object invisible from its own interior when the object had a non-transparent fill medium, and the light coming in from outside wasn't bright enough to penetrate the fill medium twice. For example, a transparent booth filled with attenuating fog, with external normal illumination (brightness 3), was not visible from its interior because of the double traversal of the fog. Such a booth is now visible from its interior.
Released October 4, 2003
First, the main interface changes:
Note that the "stat" parameter has been removed from the two "set" methods. This parameter was essentially superfluous, since it's rare to the point of non-existence for a game to want to mark something as un-seen or unknown after it's been previously seen or known; the new methods elide this parameter and act like the old methods acted with the parameter set to true.
You have a choice of how to update your existing game code to the new interfaces. The first option is to do the search-and-replace operations in the list above throughout your game's source code; this is the recommended option, because it'll make your code consistent with future documentation. The second option is to modify the VocabObject class to reinstate the old methods, defining them in terms of the new ones:
modify VocabObject seenBy(actor) { return actor.hasSeen(self); } setSeenBy(actor, flag) { actor.setHasSeen(self); } isKnownBy(actor) { return actor.knowsAbout(self); } setKnownBy(actor, flag) { actor.setKnowsAbout(self); } ;
Second, how do the changes facilitate separate NPC knowledge tracking? The change here is that the Thing and Topic 'seen' and 'isKnown' properties that underlay the old system are still used, but are now only defaults for tracking the seen/known information. The actual properties used to track the information are determined on an actor-by-actor basis, using the new Actor.seenProp and Actor.knownProp properties.
By default, Actor defines seenProp as &seen, and knownProp as &isKnown. This means that, by default, there's only one "global" set of knowledge. To track an NPC's knowledge individually, simply set seenProp and/or knownProp to new properties unique to that actor. For example:
bob: Person // ... other definitions... seenProp = &bobSeen knownProp = &bobKnown ;
This specifies that 'bob' will individually track its knowledge, separately from any other actors. When you call bob.setKnowsAbout(obj), the library will examine obj.bobKnown and obj.bobSeen, because those are the properties that track bob's knowledge. Note that this individual tracking is completely automatic once you define seenProp and knownProp, since the Actor methods hasSeen, setHasSeen, knowsAbout, and setKnowsAbout all use seenProp and/or knownProp to do their work.
Note that because the default values in Actor for seenProp and knownProp are (respectively) &seen and &isKnown, everything works roughly the same way it did before if you don't override these properties. In particular, if you don't care about tracking individual NPC knowledge, which typical games probably won't, you only have to worry about 'seen' and 'isKnown' as before. So, if you want to initialize an object as pre-known, before the start of the game, simply set isKnown=true for that object as in the past.
Third, a minor enhancement: the GIVE and SHOW iobjFor() handlers in Actor now automatically mark the object being shown, and its visible contents, as having been seen by the actor being shown the object. For games that track NPC knowledge separately, this will ensure that the target of a GIVE or SHOW takes note of having seen the object in question.
In support of the new Consultable class, the TopicDatabase and TopicEntry classes have been refactored a bit. In particular, these two classes no longer assume that they're associated with an actor, and no longer assume that they're involved in conversation. The parts of the former implementations that made assumptions about actor or conversation associations have been moved into a pair of new subclasses, ActorTopicDatabase and ActorTopicEntry. The existing classes that were based directly on TopicDatabase and TopicEntry are now based on the new ActorXxx subclasses instead. For almost all existing game code, this change should be entirely transparent, because the final subclasses that most games use still have the same names and work the same way they did before; the only changes are some internal reshuffling to abstract out most of the functionality into the new base classes.
In addition, the TopicResolver class has a new subclass, ConvTopicResolver, that's used for the conversational actions (ASK ABOUT, TELL ABOUT). The base TopicResolver doesn't differentiate at all among topics; it simply considers everything to be an equally good match. The ConvTopicResolver differentiates topics into three sublists, as it did in the past: objects that are in physical scope or are known to the actor performing the command; objects that are "likely" topics, as indicated by the performing actor's isLikelyTopic(); and everything else.
Similarly, when the direct object is missing from a CONSULT ABOUT command, the default is the last object consulted by the same actor, as long as that object is still visible. The new Actor property lastConsulted keeps track of the last object consulted; the new Actor method noteConsultation() sets this property. The Consultable class calls gActor.noteConsultation(self) in its ConsultAbout action() handler. This saves the player some typing, since they can leave out the object to be consulted when they're looking up a series of topics in the same consultable (they can type simply LOOK UP BOB, for example).
This change should have little or no impact on existing games.
Normally, when you create a tree of AltTopic alternatives, you can make each AltTopic a separate suggestion. Each alternative is effectively an independent topic for suggestion purposes, so asking about one doesn't satisfy the PC's curiosity about any of the other alternatives. This means that the game might make the same suggestion several times, as the different alternatives become active.
In many cases, it's better to treat the whole group of alternatives as a single suggestion, so that the suggestion is only made once (or only as many times as desired) for the whole set. This is what SuggestedTopicTree is for. To use this class, simply add SuggestedTopicTree to the superclass list for the main TopicEntry (that is, the TopicEntry at the root of the tree: the one containing all of the AltTopic alternatives).
Note that the new TopicEntry property altTalkCount keeps track of the number of invocations for an entire alternative group. This property is kept in the outermost TopicEntry for an AltTopic group, and is incremented each time the outermost TopicEntry or any of its AltTopic objects is invoked (via ASK ABOUT, TELL ABOUT, etc). SuggestedTopicTree uses this new counter to determine if the PC's curiosity has been satisfied for the set of alternatives as a group.
This change allows the conversation manager to defer setting the new default ConvNode in the responding actor until after the response has been finished. This ensures that a ConvNode's noteActive() method will not be invoked in response to a <.convstay> tag. In the past, the conversation manager immediately transitioned the actor to the default next ConvNode at the start of showing the response, so if the response contained a <.convstay> tag, this caused a transition back to the starting ConvNode, and thus a call to noteActive() on the ConvNode object. The new scheme ensures that the actor's ConvNode won't change at all when processing a <.convstay> tag.
For naming consistency, the Lister formerly named keyringListingContentsLister has been renamed to keyringInlineContentsLister. The old name was a bit odd and didn't properly call attention to the "in-line" aspect.
The new Thing property suppressAutoSeen can be used to suppress the library's automatic "seen" marking. By default, the library automatically marks every visible object as having been seen whenever a LOOK AROUND command is performed. The library also automatically marks as seen every visible object within a container when the container is explicitly examined (with EXAMINE, LOOK IN, or OPEN, for example).
suppressAutoSeen is nil by default. If you set it to true for an object, then the library will not mark the object as having been seen in any of the standard cases, even when the object is visible. The game must mark the object as having been seen by explicitly calling setSeenBy(), if and when desired.
>north You must open the door first. >open it
The new Goal properites openWhenRevealed and closeWhenRevealed let you tie a hint to a <.reveal> tag. You can set these properties to (single-quoted) strings giving <.reveal> tags to test.
Isolating the conditions in separate methods makes it easier to use 'modify Goal' to add new custom open/close sub-conditions. For example, suppose you wanted to add a pair of new custom Goal properties that open and close hint topics based on a Thing being moved for the first time. You could do this like so:
modify Goal openWhenMoved = nil closeWhenMoved = nil openWhen = (inherited || (openWhenMoved != nil && openWhenMoved.moved)) closeWhen = (inherited || (closeWhenMoved != nil && closeWhenMoved.moved)) ;
Along the same lines, the new library functions spellIntOrdinal(n) and spellIntOrdinalExt(n, flags) return fully spelled-out ordinals ('first', 'second', 'third', etc). The 'Ext' version takes the same bit-flag values as spellIntExt().
In order to allow throwing at MultiLoc objects, the interface to a Thing method, getDropDestination, has been altered slightly. This method now takes an additional parameter giving the "sense path" that was used in the operation that's seeking the drop destination. The new sense path parameter is a list in the same format returned by Thing.selectPathTo. This path information allows getDropDestination to determine whence the MultiLoc object was approached - that is, it allows a MutliLoc object to find out which of its containers or containment peers was last traversed to reach the object. This path can be nil, but when it's supplied, it tells us how we're approaching the drop destination.
Note that this change slightly affects the interfaces to several Thing methods: getHitFallDestination, throwTargetHitWith, stopThrowViaPath, throwViaPath, and throwTargetCatch. In all of these routines, the former 'prvCont' parameter is now replaced with the 'path' parameter. Similarly, the processThrow method passes the path rather than the previous container.
First, the tokenizer no longer treats an abbreviation as a single token that includes the period, but rather as two separate tokens: one for the word itself, and a separate token for the period. The period is entered not with the ordinary punctuation token class, but with the new 'tokAbbrPeriod' class. Second, wherever ordinary noun tokens were allowed in the grammar, the grammar now instead accepts the new subproduction 'nounWord'. This new production matches an ordinary noun token, or a noun token followed by a tokAbbrPeriod token. Third, and similarly, the existing 'adjWord' production now accepts an adjective token followed by a tokAbbrPeriod token. Fourth, the vocabulary initialization (VocabObject.initializeVocabWith, which parses the vocabWords_ string) now enters each token that ends in a period with the period, as it did, but also without the period. So, for example, the vocabWords_ string 'main st.' will create an adjective 'main', a noun 'st.', and a second noun 'st' (in other words, 'st' is entered in the dictionary both with and without a trailing period).
This corrects a problem that could have occurred with the old scheme if the same word was used abbreviated and unabbreviated. For example, if 's.' was used as an abbreviated word (for a string like 's. main st.', for example), and 's' was also entered in the dictionary as a verb (which it is by default, for the South action) the command "s." was not properly handled. The problem was that the tokenizer would see that "s." was entered in the dictionary with the period, so it tokenized it with the period. This precluded interpreting it as two tokens, 's' and '.', so the 's' was not matched to the verb, hence the command was rejected as not understood.
A useful side effect of this change is that each abbreviation is now automatically entered in the dictionary without its period. This should be helpful to players who choose to avoid the extra typing of entering the periods in abbreviations.
To enable this change, the parser includes a new grammar production, EmptyTopicPhrase. This new production represents a topic phrase that requires an interactive response from the player.
Another similar, though more subtle, oversight made it impossible to use askForDobj() to convert a TAction into a TIAction. This is an unusual scenario, because the usual way to perform this conversion would be through askForIobj(): it's almost always the indirect object that's missing in these cases. However, there are cases where a missing direct object is possible; for example, we might want a full-fledged SPRAY verb that takes only one object, as in SPRAY AIR FRESHENER, but also have a two-object form for things like SPRAY SINK WITH DISINFECTANT, in which it's the direct object that's missing from the two-object form when we type SPRAY DISINFECTANT. (This particular example is a bit contrived, since we could just as well have defined the verb as SPRAY DISINFECTANT ON SINK, but even so, we don't want to be forced to find such a rephrasing.) In these cases, askForDobj() can now be used. When a TAction is retried as a TIAction with askForDobj(), the direct object of the TAction now becomes the indirect object of the TIAction; this is plainly the only way it can be, since we know the direct object is missing by virtue of the fact that we're asking for it.
In addition, the FireSource class now declares it illogical to burn an object using itself as the fire source. It's common for a Candle to also be a FireSource; this change prevents such an object from being chosen as its own default fire source in a command like LIGHT CANDLE, which would cause an infinite loop as we tried to light the candle with itself, which would require first lighting the candle, which would default to lighting it with itself again, and so on.
The FireSource and Matchstick classes have also been fine-tuned to work together a little better. The FireSource class now specifies a logicalRank of 150 when used as the indirect object of BurnWith. The Matchstick class uses a logical rank of 160 when lit and 140 when not lit. This makes a matchstick an especially logical choice for starting a fire under all conditions, but makes a non-matchstick an even better choice when it's already burning. A lit match trumps everything (160), next best is an already-lit non-matchstick FireSource (150), and when there's nothing else around that's burning, a matchstick will still be a good choice (140). This ensures that we don't light a new match when there's another fire source readily at hand, but at the same time ensures that we'll default to using a match when no better choice is available.
Internally, the announcement generator distinguishes between failed actions and actions interrupted for interactive input, so it's a fairly easy matter to customize the types of messages separately. To customize the "asking" case separately from the "trying" case, customize the askingImpCtx object; in particular, override its buildImplicitAnnouncement() method and its useInfPhrase property as needed. The default in the library is to use the same message style for both cases.
Note that the parser can only exclude synonym remappings when they're remapped using the remapTo (or maybeRemapTo) mechanism. Remappings that are done using replaceAction() or the like can't be detected as synonyms, so they won't be excluded. Remappings that aren't simple synonyms won't be excluded, since remapping to different verbs or word orders changes the meaning of the command enough that the EXCEPT list shouldn't exclude the new objects. For example, if OPEN DOOR remaps to PUSH BUTTON, because a button is used to operate a mechanical door, OPEN ALL BUT BUTTON would not exclude the remapping to PUSH BUTTON, since we didn't say not to PUSH the button, only not to OPEN the button.
Released August 17, 2003
The SuggestedTopic method isSuggestionActive() uses this new method to hide suggestions that aren't currently matchable. This provides an important benefit for the most common cases: topics won't be suggested until the player character knows about them. This avoids showing suggestions for things the player shouldn't even have heard of yet.
To facilitate this change, isSuggestionActive() now takes an additional argument, giving the list of in-scope objects. This avoids the need to repeatedly compute the scope list when scanning a large list of suggested topic objects, which is important because the scope calculation is complex and can be time-consuming.
This change requires new names for several TopicEntry properties: isKnown is now isActive; checkIsKnown is now checkIsActive; and topicGroupKnown is now topicGroupActive.
Game code that uses TopicEntry objects will have to rename isKnown to isActive in the object definitions. The other renamed properties might have to be changed as well, of course, but these are less likely to appear in game code.
The change is that TravelConnector.actorTravelPreCond(actor) now returns a new precondition, actorTravelReady, instead of actorStanding. The actorTravelReady precondition abstracts the travel condition by letting the actor's immediate location handle it. In particular, it calls three new BasicLocation methods to carry out the precondition: isActorTravelReady(conn), tryMakingTravelReady(conn), and notTravelReadyMsg. By default, these methods do exactly what actorStanding does: they require the actor to be standing up before travel. However, a location can override any or all of these. For example, a particular nested room might want to use a command other than "stand up" to get the actor out of the nested room in preparation for travel; the nested room could override tryMakingActorTravelReady() in this case to carry out the desired implied command.
Note that the travel connector to be traversed is an argument to the first two of the new methods. This allows the actor's current location to impose different requirements on travel via different connectors. For example, it might be necessary to climb down from a ladder before leaving via any connector except for a window high up on the wall, which can only be reached when on the ladder.
Note that this new method does not have any effect on the execution of the command for the current object; in particular, it doesn't interrupt the execution flow the way 'exit' and the like do. Instead, this method simply sets a flag in the current Action object telling it to ignore any additional objects it would otherwise iterate over. If you want to jump out of the execution cycle in addition to canceling further iteration, simply use 'exit' (or one of the similar signals) after calling this method. Note also that this method obviously can't cancel the iteration for any prior objects in the iteration.
To enable this change, the interface to the libMessages method announceImplicitAction() has changed slightly. The method now takes a "context" parameter, which is an object of class ImplicitAnnouncementContext The context contains information on the format of the message to be generated.
Also, the implicitGroupTransform internally scans for failure messages that apply to implied action announcements, and rewrites the implied action announcements into the new format when failures are noted.
Finally, the implicitAnnouncementGrouper object implementation in the English module does some additional grouping, to render a series of "trying" messages as readably as possible. When a series of consecutive "trying" messages appears, the grouper combines them under a single "trying" phrase, as in "first trying to unlock the door and then open it."
First, the new method getInfPhrase() returns a string giving the full action description in infinitive form. This returns a phrase such as "open the box" or "unlock the door with the key". The verb is always in the infinitive form, but note that the English infinitive complementizer, "to", is not part of the returned phrase; this is because an infinitive is used without a complementizer in certain contexts in English, such as with auxiliary verbs (such as "might" or "should").
Second, the new method getVerbPhrase(inf, ctx) returns the full verb phrase in either the infinitive form ("open the box") or the present participle form ("opening the box"). In English, the two forms are equivalent except for the verb ending, so the code to generate both forms can be easily consolidated into a single routine. The subclasses of Action override getVerbPhrase() as needed to provide appropriate phrasings. The context object 'ctx' is optional; if it's not nil, it should be an instance of the new class GetVerbPhraseContext. If the context object is provided, the routine uses it to keep track of pronoun antecedents for the verb phrase; the returned verb phrase will use a pronoun if one of its objects matches the current antecedent, and the routine will set the antecedent in the context for the next verb phrase that uses the same context. The context is useful if you're generating a series of verb phrases that you're going to string together into a single sentence; by using pronouns to refer to repeated objects, the sentence will be shorter and will read more naturally.
Third, the implementation of getParticiplePhrase() now simply calls getVerbPhrase() in all cases.
Fourth, the routine formerly called verbInf() has been renamed to getQuestionInf(), to better describe its purpose. This routine doesn't return the full infinitive form of the verb, but rather returns an infinitive form specifically for an interrogative in which one of the verb's objects is the interrogative's unknown. The old name was always misleading, but the addition of getInfPhrase() would have made the name even more confusing because of the similarity of the names.
Whenever an object is to be dropped into the enclosing room, the DROP and THROW commands first find the drop destination using the existing getDropDestination() method. Then, they invoke the new receiveDrop(obj, desc) method on the drop destination object. In most cases, the drop location is the enclosing room, so the enclosing room's receiveDrop() method is usually the one that will be invoked.
The receiveDrop() method is responsible for carrying out the effects of dropping the object, including any game-state changes (such as moving the dropped to its new location) and reporting the results of the action.
In most cases, the effects won't vary according to which command was used to discard the object. This is the reason that there's only one receiveDrop() method, rather than separate methods for DROP, THROW, and any library extension or game-specific verbs. By using a common routine to handle all means of discarding an object, the game only has to implement any special handling once.
However, the message will almost always vary according to the command, for the simple reason that the message needs to acknowledge what the actor did to discard the item in addition to the effects of dropping it. This is the main reason the 'desc' argument is provided: it's a special "drop descriptor" object that lets you find out what command was used. You could also look at gAction to find the Action being performed, but the descriptor object makes your job a lot easier by providing some convenience methods for building report messages.
The descriptor object is of class DropType. This class has two important methods. First, standardReport() displays the "normal" report for the action. For DROP, this is simply "Dropped." For THROW, this is a message of the form "The ball hits the desk, and falls to the floor." You can use the standard report when your receiveDrop() method doesn't do anything out of the ordinary and thus doesn't require a special report. Second, getReportPrefix() returns a string that gives the start of a sentence describing the action. The returned string is a complete main clause for a sentence -- but it has no ending punctuation, so you can add more to the sentence if you like. The main clause will always be written so that the object being dropped will be the logical antecedent for any pronouns in subsequent text, which lets you tack on a string like this: ", and it falls to the floor."
Rather than building a message from the report prefix provided by the descriptor, you are free to build a completely custom message, if you prefer. To do this, you could use 'modify' to add your own method to each DropType subclass in the library, and then call this method from your receiveDrop(). Alternatively, you could use ofKind() to check what kind of DropType descriptor you got, and show custom messages for the ones you recognize.
Released August 2, 2003
In addition, the ScriptEvent class has been eliminated. Instead, simply use Script objects in an event list. So, when an object appears in an event list, the EventList class now invokes the object's doScript() method. This makes it easy to build event lists recursively.
The new EventList subclasses CyclicEventList and StopEventList provide new pre-defined options for the behavior of an event list after visiting all elements. The new subclass SyncEventList provides a list that synchronizes its state with another list.
The TextList, StopTextList, and SyncTextList classes are now simple subclasses of CyclicEventList, StopEventList, and SyncEventList, respectively.
A new EventList method, scriptDone(), is invoked by doScript() after it processes the script's current step. By default, this method simply invokes advanceState() to advance the event list to its next state. Subclasses can override scriptDone() so that it does not advance the script's state, or does something else in addition.
The new EventList subclass ExternalEventList overrides scriptDone() so that the method does nothing. This makes it easy to create an event list that is driven externally; that is, the event list doesn't advance its state when doScript() is invoked, but only advances its state in response to some external processing that calls advanceState() directly on the event list object.
This change makes the precondition handling a little more intelligent. When the player doesn't know the status beforehand, an Open command will simply fail with the discovery that the object is locked. This usually makes more sense than attempting to unlock the object automatically with an implied Unlock command, because if the object isn't already known to be locked, there's no reason to try to unlock it. When the status is known, however, it makes sense to perform the implied Unlock.
Objects whose lock status is visibly apparent can be marked as such with the lockStatusObvious property. This property is nil by default; an object whose lock status can be visibly observed should set this to true. When this property is set, the lock status is always known, since we'll assume that actors will simply look at the lock and observe the status. In addition, when this property is set, we'll add the locked/unlocked status at the end of the object's 'Examine' description (we'll add "It's currently locked" or "It's currently unlocked").
The Passage travel connector class now uses the other side's container's roomLocation as the default destination of the passage, rather than the other side's immediate container, as it did in the past. This makes it much easier to create passages that model non-trivial containment relationships, such as holes in walls, since an intermediate container (between the Passage and the enclosing Room or NestedRoom) will no longer create any confusion in performing the travel.
If the travel connector has a non-nil location, the TravelConnector class's connectorTravelPreCond method adds a precondition that the connector must be touchable. This ensures that characters won't be allowed to traverse connectors that are visible but on the other side of a window, for example, or connectors that are out of reach.
This change is generally desirable because we usually want arrival by travel connector to be self-contained; we don't want it to appear as though the character walked to the new location and only then performed a SIT ON THE CHAIR command (say). It's worth noting, however, that any side effects of the usual way of getting into the new location's posture will be skipped. Therefore, when linking a travel connector into a nested room with a non-standing posture, you'll need to consider any side effects you'd want to trigger, and trigger them explicitly on the travel connector itself.
To handle this new class, TravelMessageHandler and its subclasses have a new pair of methods, sayArrivingViaPath() and sayDepartingViaPath(), for generating travel messages related to paths.
In addition, the class's beforeAction() method, which responded to the initiating actor's travel, has been changed to a beforeTravel() method. This takes advantage of the more precise kind of notification offered by beforeTravel(). In particular, using this method avoids unnecessary attempts to accompany an actor who attempts a travel command that doesn't actually result in travel (because the connector doesn't go anywhere, for example).
To facilitate this change and make it easier to override an actor's per-turn processing, the main handling for an actor's turn is now in a new method, executeActorTurn(). The executeTurn() method simply sets up the action environment and then invokes executeActorTurn(). In most cases, subclasses that need to specialize the per-turn processing should override executeActorTurn() rather than executeTurn(), since this will let the override run in the action environment.
bob: Person // ... other definitions ... dobjFor(AttackWith) actorStateDobjFor(AttackWith) ;
This sets up the "bob" Actor object so that the handling for AttackWith, with bob as the direct object, will be delegated to bob's current ActorState object. Second, just write a normal dobjFor() handler in each of the ActorState objects associated with bob:
+ bobFighting: ActorState dobjFor(AttackWith) { action() { "Bob puts up a fight..."; } } ; + bobCowering: ActorState dobjFor(AttackWith) { action() { "Bob just hides in the corner..."; } } ;
When isConversational is nil for the response object that's found for a conversation command, a ConversationReadyState will not enter its in-conversation, but will simply show the response and remain in the conversation-ready state. This means that there will be no "greeting" exchange for these responses.
In related changes, ConvNode.endConversation() and ActorState.endConversation() now take 'reason' codes rather than 'explicit' flags. This gives the methods full details on why the termination is occurring.
Each AgendaItem object has a method called isReady, which indicates whether or not the item is ready to execute. An actor will only execute an agenda item once it's ready. The invokeItem method contains the code that actually carries out the agenda item's action.
By default, an agenda item is removed from the actor's agenda list when it's executed. However, if the item's stayInList property is true, then the item will not be removed on execution. This property lets you handle agenda items which the actor will try repeatedly, until some other code determines that the goal has been achieved and removes the item from the actor's agenda.
You must nest AgendaItem objects within their actor, using the "+" notation. This associates the AgendaItem objects with the actor, but it doesn't add them to the actor's agenda list. You must explicitly add agenda items to the actor's agenda list by calling the actor's addToAgenda() method. Agenda items must be explicitly added because an actor's motivation typically will change dynamically as the game's events unfold.
To accommodate this change, the default takeTurn() method in ActorState now preforms more processing. If the actor has an active ConvNode, then this takes precedence over any other default processing for the actor's takeTurn() method. If the actor hasn't engaged in conversation on the same turn, we invoke the ConvNode to continue the conversation under NPC control; otherwise, we do nothing more. If there's no ConvNode, we process the actor's "agenda." If there's no ConvNode, and the actor has no agenda items that are ready to execute, and the ActorState inherits from Script, then we invoke the script. This structure means that a stateful conversation takes precedence over everything, and an agenda item takes precedence over state-level background activity.
Similarly, ConversationReadyState now makes the same check before going through a greeting. If the initating actor can't talk to the target actor, the enterConversation() method says so and uses 'exit' to terminate the command. This prevents strange situations from arising commands that don't require the canTalkToObj precondition. (SHOW TO works this way, because the actors don't necessarily have to be able to talk to each other to use SHOW TO; we could show a note to an NPC on the other side of a sound-proof window, for example.)
getActor().initiateTopic(getActor().location);
Along the same lines, ActorState.showGreetingMsg has been removed, and replaced with Actor.defaultGreetingResponse. The method has been moved to Actor because of the change to greetingsFrom, and the method has been renamed for consistency with the similar pattern for the other conversation command default message methods (for ASK, TELL, etc) in Actor.
Along the same lines, ActorState.goodbyeFrom now delegates to a method of the same name in Actor by default. A new Actor method, defaultGoodbyeResponse, displays the default message, for consistency with the naming of the similar methods for other conversation commands.
The purpose of this new feature is to make it easy to define extra vocabulary for an object that the parser accepts for an object without creating any new ambiguity. This comes up in situations where you simply want to create additional synonyms for an object, as well as in cases where an object is most strongly identified in terms of its relationship to something else. For example, you might have a location that contains a couple of doors, one to a house and the other to a shed. You could use adjectives ("house door" and "shed door") to differentiate the doors, but it might be more natural to use a prepositional phrasing, such as "the door of the house" and "the door of the shed." If you also have a house and a shed object, though, this phrasing would create unwanted ambiguity with those objects. This is where weak tokens come in. If you define "house" and "shed" as weak tokens for the respective door objects, the parser will never take those words alone to refer to the doors, so there will be no ambiguity with the house and shed objects themselves; but the parser will still allow "house" and "shed" to refer to the doors, as long as they're used in combination with the strong token "door".
Weak tokens are defined per-object (they're not global). Each object that has weak tokens keeps a list of its weak tokens in its 'weakTokens' property. In the basic VocabObject.matchName() implementation, the object checks the noun phrase to see if it consists entirely of weak tokens, and rejects the match if so.
When you define an object, you can define weak tokens in one of two ways. If you define 'noun' and 'adjective' (etc.) properties directly, simply define a 'weakTokens' property containing a list of strings giving the object's weak tokens. If you use the vocabulary initialization string, simply enclose each weak token in parentheses. For example:
+ Door 'front door/(house)' 'front door of house' "The door is badly weathered. " ;
Instead of allowing any adjective to be quoted, the parser now only matches quoted strings that are specifically designated as "literal adjectives" in an object's vocabulary. To designate an adjective as a literal adjective, you can either enclose the adjective in double-quotes in the vocabulary initializer string, or you can explicitly define the word using the 'literalAdjective' part-of-speech property instead of the normal 'adjective' part-of-speech. For example, you could define an elevator button for the lobby level like so:
+ Button '"L" button' 'L button' "It's a button labeled <q>L.</q> ";
This change makes the parser stricter about which words it accepts as quoted adjectives, but it also allows the parser to be more flexible about requiring the quotes. In particular, the parser now accepts all of the following forms for the button defined above: "L" button, button "L", L button, and button L. In the past, the parser was unable to accept the last of these, because without the quotes, it wasn't able to tell that the unquoted "L" was meant as a literal adjective. Now that the "L" is defined in the dictionary as a literal adjective, it's no longer necessary for the player to quote the word for the parser to recognize it as a literal adjective, so the parser is able to accept the form with the literal adjective following the noun.
Note that this change also involves a subtle change to the special "*" vocabulary wildcard token. In the past, if the string '"*"' (that is, an asterisk enclosed in double-quotes) appeared as an 'adjective' dictionary entry for an object, then that object matched any quoted string used as an adjective in player input. Now, this effect is obtained by using the string '*' as a 'literalAdjective' dictionary entry for an object. This change has no effect at all on the way you write vocabulary initializer strings, since you still enclose the asterisk in quotes in that string; however, if you're defining a 'literalAdjective' list directly for an object, you now must drop the quotes and add a word entry consisting of simply the asterisk.
This new method is especially useful for adding a "summary" of an action that involves multiple objects. Each individual object's action handler (an action() method in a dobjFor() block, for example) can register a handler object to receive notification when the overall command is finished. A given object can be registered only once - redundant registrations are simply ignored - so each individual iterated object doesn't have to worry about whether or not other iterated objects will register the same handler. Then, at the end of the action, the handler will be invoked; it can determine what happened in the action, and do something based on the end result. For example, the handler could scan the transcript (gTranscript) for certain types of reports, and add a summary message based on the reports, or could even replace some of the individual reports with a summary. The handler could also take addition action based on the overall end results; for example, in a GIVE TO command, a handler could look at the full set of objects successfully given, and decide that the combination is sufficient to allow the recipient to give the donor something in return.
gold coin: Bob accepts the gold coin. gold coin: Bob accepts the gold coin. gold coin: Bob accepts the gold coin.
into something like this:
Bob accepts the three gold coins.
This sort of summary isn't straightforward to generate in general, and the library makes no attempt to apply it generically. In specific cases where you can control the range of possible results, though, this can be a powerful way to improve the game's output by describing an iterated action as though it were a single logical unit.
To implement these changes, NounPhraseProd has a new method, getVerifyKeepers(), that the disambiguation filtering methods call to reduce the list. The default NounPhraseProd definition of the method does the traditional filtering that the disambiguation filter did, which keeps just the most logical subset of the results. AllPluralProd overrides this method to keep everything in the list, and QuantifiedPluralProd overrides the method to select a subset with the desired number of entries.
The CollectiveGroup object by default no longer matches a noun phrase that specifies a quantity ("take five coins" or "take both coins"; or any singular phrase, since a singular phrase specifies a quantity of one). Collective groups are designed to stand in for a completely collection of in-scope individuals, not for arbitrary subsets of the individuals, so when a quantity is specified we must fall back on iterating over a selected subset of the individuals.
To implement this change, the filterResolveList() method now takes two additional parameters: one giving the quantity specified in the noun phrase, if any, and another giving the role played by the object (DirectObject, IndirectObject, etc). When the noun phrase specifies a quantity, the new parameter will be set to an integer giving the quantity specified. If the noun phrase is unspecific about the quantity (as in "take coins" or "get all coins"), then the quantity parameter will be nil.
Note that the isCollectiveAction() method now takes an additional parameter as well, giving the role in the action played by the object being resolved (DirectObject, IndirectObject, etc). This allows differentiating the handling based on both the action and the role played in the action.
Custom CollectiveGroup objects can represent specific quantities of objects, from the player's perspective, if desired. For example, a game might want to create a CollectiveGroup that represents a quantity of money, rather than dealing with the individual coins making up the quantity. To do this, the CollectiveGroup object must override the filterResolveList() method, and must set the "quant_" element of its own ResolveInfo entry to an integer giving the number of grammatical objects it represents. The CollectiveGroup must keep track of what it's representing somehow; the best way to do this is to create a new instance of the CollectiveGroup itself, and store a property in the new instance giving the quantity represented by the collective. Note that using CollectiveGroup objects in this manner is tricky; you'll need to code action handlers for the custom CollectiveGroup object so that they correctly take into the account the quantity represented by the group object.
The purpose of this change is to ensure that ownership will be used as a distinguishing feature whenever possible, before the parser falls back on location. The old combined distinguisher could only use ownership in the limited case where the owner and immediate location were the same, because it couldn't otherwise be sure that it would be able to distinguish multiple objects with the same owner but different locations. By separating the ownership and location distinguishers, we first try to identify objects purely by ownership; when this fails, we fall back on the old approach of identifying by immediate location in preference to owner.
To support this change, the English methods for owner-or-location names (aNameOwnerLoc, theNameOwnerLoc, countNameOwnerLoc) now take a parameter indicating whether to use ownership or location as the first priority in generating the name. When ownership takes priority, these methods will show the name with a possessive form of the owner regardless of the containment relationship between the object and its owner.
The new Goal property openWhenDescribed lets you specify that the goal should be opened when the referenced object is described (usually with EXAMINE). This is good for goals where the existence of a puzzle isn't made apparent until the full description of an object is viewed. The new property closeWhenDescribed correspondingly closes the goal when the referenced object is described.
The firstCommandPhrase(withActor) rule now uses the new singleNounOnly production to match the actor phrase. This eliminates the structural ambiguity of certain types of invalid input that were able to cause unbounded memory consumption with the old rule.
Along similar lines, TAction.initResolver() now calls cacheScopeList() to initialize its cached scope list, rather than doing so directly. This allows TAction subclasses to customize the scope rules in exactly the same way that subclasses of Resolver can customize scope. Likewise, TopicResolver.filterAmbiguousNounPhrase() now calls objInScope() rather than accessing the "scope_" list directly.
In the past, an OOPS command itself was run through the normal pre-parsing steps, but the new command resulting from applying the OOPS replacement text was not pre-parsed. This resulted in incorrect behavior in certain certain rare cases. One noticeable example was when a SpecialTopic was active. Because SpecialTopics rely on pre-parsing to match their special command templates, and because the corrected text after the OOPS wasn't pre-parsed, it was effectively impossible to use OOPS to correct a typo in a command intended to match a SpecialTopic. The new OOPS handling ensures that pre-parsing is run as normal on the new command resulting from applying an OOPS correction.
Released June 15, 2003
In addition, the new ConvNode method npcGreetingMsg is defined to display the initial conversational exchange in an NPC-initiated conversation. Any ConvNode used in an initiateConversation() call must either override npcGreetingMsg to define a greeting message, or define an npcGreetingList property as a TextList containing a list of greeting messages.
If you don't override npcContinueMsg or npcContinueList, there will be no NPC-initiated continuation message. If you do provide a continuation message, then the ConvNode stays active by default. You can use the <.convnode> tag within the continuation message's text to switch to a new ConvNode, just as in a topic response message.
In addition, the callWithSenseContext() function has been changed to eliminate the argument list parameter; instead, callers should use anonymous functions when the code to be invoked requires arguments. (This is the way the rest of the library already used the function in most cases anyway, so this change simply cleans up some old code that's no longer needed. Most games won't need to call this function directly, so this is unlikely to affect any existing game code.)
Released June 7, 2003
WARNING! Due to the extensive scope of these new features, there's a good chance that they'll undergo some changes as we gain experience with them. Be aware that game code that uses these features might have to be changed to accommodate library changes in future updates.
A new class, ActorState, makes it easier to create characters that exhibit multiple behaviors. The Actor class has been changed to work with ActorState; most of the methods involved in describing an actor's physical state and in interacting with an actor are now delegated from the Actor class to the ActorState object. The idea is that most of the parts of an actor that need to vary according to the actor's behavior have been moved to the new ActorState object; this means that adding a new behavior to an actor is a matter of defining a new ActorState object for the actor.
Several subclasses of ActorState have also been created. These include ConversationReadyState and InConversationState, which are used to make an actor explicitly enter, maintain, and exit a conversation; HermitActorState, for times when an actor is unresponsive; and AccompanyingState and AccompanyingInTravelState, which allow an NPC to travel with another character as part of the character's own turns.
A new "topic database" system makes it easier to manage conversational interactions, by creating individual objects that represent responses to ASK, TELL, GIVE, SHOW, and other interactions. The TopicDatabase class is used to store an actor's responses; Actor and ActorState inherit from TopicDatabase, so you don't normally create instances of this class directly. The TopicEntry class represents a response; several subclasses, including AltTopic, AskTopic, TellTopic, AskTellTopic, GiveTopic, ShowTopic, GiveShowTopic, YesTopic, NoTopic, DefaultTopic, and SpecialTopic are used to create the individual responses.
The conversationManager object and the ConvNode class are used to manage "threaded" conversations. A threaded conversation is one where an actor keeps track of what has been said before, and responds according to the context of the conversation.
The SuggestedTopic class and its subclasses let you maintain a "topic inventory," which is a mechanism that automatically suggests topics to the player. This is an optional mechanism that you can use in various ways; you can use it as a sort of hint system, and you can use it to guide threaded conversations.
These notifications are more precise than using beforeAction() and afterAction() with the TravelVia pseudo-action, because these actions are only called when travel is actually occurring. TravelVia will fire notifications even when travel isn't actually possible.
A beforeTravel() method can veto the travel action using "exit". The notification is invoked before the travel is actually performed, and even before a description of the departure is produced.
The method Actor.trackFollowInfo() is now invoked from Actor.beforeTravel() rather than directly from traveler.travelerTravelTo(), as it was in the past. This change makes the follow-tracking a lot less "special," in that it uses the general before/after travel notification system rather than a separate, purpose-built notification system. Note that this change means that any actor object that overrides beforeTravel() must inherit the base class implementation if the actor wants to be able to follow other actors.
First, BasicLocation.roomTravelPreCond() no longer does anything; the method is still present so that rooms can override it as desired, but it doesn't do anything by default. In the past, this method unconditionally added a precondition requiring that the traveler be in the outermost room; we don't want to apply that condition, since the required starting location of the traveler depends on the outbound connector, not the room.
Second, TravelConnector.connectorTravelPreCond() now adds a requirement that the traveler be in the connector's location, if the connector has a location. This ends up having the same effect as BasicLocation.roomTravelPreCond() in cases where explicit connectors, such as doors and passages, are situated in the top-level room.
Third, RoomAutoConnector.connectorTravelPreCond() overrides the inherited version, and instead requires that the traveler be in the "appropriate" starting location. This provides the same effect as the old BasicLocation.roomTravelPreCond(), but with an enhancement. The "appropriate" location is defined as the nearest enclosing location (enclosing the traveler) that has a directional connection ("north = xxx", etc) leading to the room. In most cases, rooms are connected directly to one another only at the top level; in such cases, the directional connection will be found in the traveler's outermost room, so the effect will be exactly as it was with the old system. But here's the enhancement: in cases where the directional connector is defined leading out of a nested room, this will correctly find the nested room as the starting location.
Note that NestedRoom objects cannot by default be used as direct targets of directional connectors, because they're not based on RoomConnector. It seems highly unlikely that it would ever be useful to connect a nested room directly to a directional connector (i.e., "east = wardrobe") - some kind of explicit connector (a door, or a passage, or something) seems desirable in almost every case, just from the perspective of game geography. In the event that a direct directional connector is required, though, simply include RoomAutoConnector in the superclass list for your specific nested room.
Fourth, the methods travelerArriving() and travelerLeaving() have been moved from Room to BasicLocation. This produces the proper travel messages for any travel between top-level locations, even when the travel starts or ends in a nested location.
Note that if all goes well, there won't be any mention of the action for the player character (because the successful nested action will result in a default report, which will be suppressed due to the non-default message for the room description); so the new posture will simply be reflected in the description of the new location. Generally, all should always go well in these cases, since the actor will already be located within the location and merely has to change posture. If you want to explain why the posture change is necessary, you can override travelerArriving() for the location, display an explanation, then inherit the default. An appropriate message would be something like "The ceiling is so low you have to lie down."
To allow for connectors that are affected by traversal (for example, a connector that randomizes its destination each time it's used), the new method noteTraversal() must be called whenever the connector is actually traversed. This method can change the connector state as needed for the traversal. Note that game code should generally never need to call noteTraversal(), as the library dobjFor(TravelVia) action implementations do this automatically as needed. This method is provided for games to override as needed.
The purpose of this check is to ensure that the command has some sort of handling. If the game adds a new verb, but never defines any handling at all for the new verb for some object, then without this check, there would be no handling at all if the player attempted to apply the verb to that object. The old check tested for the existence of an action() handler, but this was overly conservative, since an object could have fully handled the verb in its verify() or check() handler. The new test allows for this by allowing the action to proceed if any sort of handler (check, verify, or action) is defined for an object involved in the command.
>inventory You are carrying a music box, a gold coin, and thirty silver coins, and you're wearing a watch and a helmet. >inventory You are carrying a cardboard box (which contains several LP's, a bunch of photographs, and some letters), a rubber chicken, two pieces of the Magic Goblet of Forblotsk, and a bowl of chowder. You're wearing a pair of flippers, a wool jacket, and a red wig.
The list length is determined by capturing the output and counting the phrase separators. The English library counts commas, semicolons, the word "and", and right parentheses to determine the phrase count.
This change slightly affects the Actor class (showInventoryWith now takes only one lister object), and extensively affects the InventoryLister class and the WearingLister class and their subclasses (the English-specific implementations in msg_neu.t). Games shouldn't be affected unless they modified these classes.
If you want the old behavior, where the items being worn and the items being carried were always shown as separate sentences, simply set DividedInventoryLister.singleSentenceMaxNouns to zero. If you want to always use a single sentence no matter what, set the property to some absurdly high number - 30000, say.
If you'd prefer the more traditional behavior that shows the items being worn marked "(being worn)" and mixed in with the rest of the inventory in a single listing, simply change Actor.inventoryLister to refer to actorSingleInventoryLister.
The parser resolves a third-person reflexive pronoun by referring back to the resolved object list for the other noun phrase in the action. Verbs taking only one noun phrase don't accept these, as they make no sense: "open itself" isn't meaningful. In the basic TIAction, the order of noun phrase resolution can vary by verb, and the order of resolution isn't required to match the order of the nouns in the phrase; in English, at least, a reflexive pronoun in this type of construction is always anaphoric (i.e., it always refers to a phrase earlier in the sentence). This means that the TIAction resolver could find itself trying to resolve the reflexive phrase first, before it knows the resolution of the phrase to which the reflexive refers. To cope with this situation, the resolver notes when this occurs, provides an empty list for the initial resolution of the reflexive, and then goes back and re-resolves the reflexive after resolving the other noun phrase.
The parser does not call this method when the action is "conversational," as indicated by the isConversational() method no the action. Conversational methods are not considered to involve an order to the target actor. The actual physical action of a conversational action simply consists of the issuing actor saying something to the target actor, so even though these actions are phrased as though they're orders to the target actor, they're really carried out by the issuing actor, and thus don't require acceptance by the target actor.
The default implementation of this method on Actor calls the corresponding method on the atcor's current state object. The basic state object implementation simply refuses the command, so the default behavior is the same as it was in the past.
First, a new property, firstStrings, can be set to a list of strings to show sequentially before starting the shuffled strings. This can be useful in cases where you have some meaningful information to convey initially, but once those messages have been displayed, you want to fall back on randomized messages for atmosphere and variety. The firstStrings list is shown only once, in sequential order. Once the firstStrings list is exhausted, these strings are never shown again.
Second, the main shuffled list in textStrings can now be shown sequentially the first time through, if desired. Set the property shuffleFirst to nil (it's true by default) if you don't want the list shuffled the first time through. Since the strings in the textStrings list are intended to be shown in random order, in most cases it won't matter to the author what order is used, and in this sense the order in which the strings are actually defined is as random as any other order. In some cases, it might actually be desirable to have the strings come out in a certain order the first time through; this lets you refer back to an earlier message in a later message, for example, with assurance that the player will have always seen the earlier message first. After the first time through the list, the list is always shuffled and shown again in random order.
Third, the class now takes care to ensure that a message is never repeated consecutively. Repeats are normally avoided naturally by the shuffling: every item is shown once before anything is repeated. But a consecutive repeat was still possible in one special case, which is immediately after a shuffle. Because the order of one shuffle is independent of the order of the next shuffle, it was possible for the last element of the previous shuffle to be the same as the first element of the next shuffle. The class now suppresses this case by checking each new shuffle to make sure its first element doesn't match the last element of the previous shuffle, and choosing again when necessary. This change is intended to increase the apparent randomness by ensuring that the same string is never shown twice in a row.
REPLAY has two mutually exclusive options. REPLAY QUIET plays back the script without showing any output while the script is running. REPLAY NONSTOP plays back the script without pausing for MORE prompts.
Because REPLAY is fully redundant with the old "@" syntax, but much friendlier, the "@" syntax has been removed.
Released April 12, 2003
The parser now keeps track, during the verification process, of any remapped objects. The default picker looks at this remapping information before deciding on a default. If there are any objects among the possible defaults that are to be remapped, the default picker will discard any that are redundant due to the remappings. For the OPEN JAR/OPEN LID example, the parser would see that OPEN LID turns into OPEN JAR, and it would thus discard the lid from the list of possible defaults, since the jar is already in the list. This would leave us with just one possibility, so the parser would be able to apply use it as the default.
The parser will only eliminate a remapped object as redundant when the remapping matches the object, action, and role of another object in the list of possible defaults. If a remapping changes the verb, the remapped object will only match another object if it's also remapped to that same new verb. The verb has to match because the command could otherwise have a different effect, and thus the two actions on the same object wouldn't be redundant with one another.
north asExit(down)
This change is intended to make asExit more consistent with the similarly-named asDobjFor() and related macros.
A new mechanism in the Actor class makes it relatively easy to set it up so one or more non-player characters accompany the player character on travel. This kind of group travel is especially good for things like a sidekick character who goes everywhere the player does, and for tour guides or other escorts who are showing the player character where to go.
This mechanism is similar to the "follow" mechanism, which makes one actor attempt to follow another on each turn, but it improves considerably on regular following by customizing the messages. Normal following is a little awkward for explicit group travel of the sort that one wants with sidekicks and escorts, because the messages are so generic; the NPC almost seems to be wandering around on its own and just coincidentally showing up where the PC is. This new "accompanying" mechanism accomplishes much the same thing, but smooths out the messages a bit. First, rather than having the NPC trailing along after the fact, the new system sends accompanying NPC's on ahead; this means that the NPC's don't just wander in later as they do with normal following. Second, the message for the NPC's pre-departure is customized to make it clear that the NPC isn't departing, but is coming along with you. Third, on arriving in the new location, there's a customization hook for describing the NPC's presence in the new location specially for the group travel; this is important because it gives us a place to mention that the actor starts doing whatever it is the actor normally does in the new location. The overall effect is that each group travel action is made to look like a single, integrated action, rather than two generic and unrelated actor travel actions.
NPC's can also be set to accompany NPC's with the same mechanism, but it's much less interesting for that, since the main value of the new mechanism is that it improves the messages for PC-plus-NPC group travel. For NPC-plus-NPC group travel, this new mechanism has no particular advantage over the the simpler "follow" mechanism.
Setting up accompanying travel is relatively straightforward. You have to specify the conditions under which the group travel occurs, and you should customize two messages related to the travel. You set this all up by overriding methods on the NPC who's going to follow the lead actor (usually the PC). The methods to override are as follows.
First, override the accompanyTravel() method so that it returns true under the conditions where you want the accompanying travel to occur. This method is called each time your NPC is present and sees another actor attempt a travel action (it's called during your NPC's beforeAction when the action is a TravelVia). If you want your actor to accompany the PC everywhere, simply return true when the action actor is the PC. The method also has access to the TravelConnector involved in the travel, so you selectively accompany an actor for some destinations but not others, if you wish.
Warning: the next two parts are likely to be modified soon. I'd recommend against writing any code that makes customizations using these features right now.
Second, define an accompanyDesc method for your actor. This method is used instead of the usual actorHereDesc method to describe your actor in the new location immediately after the group travel is finished. It's usually desirable to use a special message to describe the actor right after the accompanying travel, because you usually want to convey that the actor is just arriving - not that the actor is walking in separately from the lead actor, but that the actor is arriving at the same time as the lead actor. If you do override accompanyingDesc, you should override accompanyingListWith to return an empty list ([]).
Third, you can optionally provide a special TravelMessageHandler for your actor by overriding getAccompanyingTravelMessageHandler. By default, this routine provides a message handler that uses messages like "Bob goes with you" instead of the usual "Bob leaves to the east" to describe the departure of your actor. The normal departure message isn't appropriate, because the actor isn't just leaving, it's leaving *with* the lead actor. The default "Bob goes with you" is better, but you might still want to override this handler to provide even more customized messages: "Bob escorts you to the east," or "You drag Bob with you," or whatever makes sense for the specific situation.
Finally, note that you can use accompanying travel and the regular following mechanism together. Regular following can be a useful fallback for cases you don't want to customize. Regular following occurs after the fact, because it occurs on an NPC's turn when the NPC sees that the actor it's tasked to follow is no longer present. Accompanying travel, in contrast, happens on the same turn as the lead actor's travel. This means that regular following is essentially overridden by accompanying travel, since it happens first.
For the purposes of matching vocabulary in the dictionary, quoted adjectives are simply treated as though the quotes weren't present. So, "QU" TILE is treated exactly the same as QU TILE. This means you don't have to worry about the quotes when defining your vocabulary words.
There is an additional bit of special treatment for quoted strings. The special vocabulary word '"*"' (that is, an asterisk within double-quote characters) serves as a wildcard for any quoted string. If an object defines this special wildcard as an adjective among its vocabulary words, then that object will match any quoted string as an adjective. This is the equivalent of the '#' wildcard string for numeric adjectives.
String-as-adjective phrasing is implemented using the new production literalAdjPhrase. This new production is now used wherever numberPhrase was formerly used in the role of an adjective in noun phrase rules. literalAdjPhrase matches numbers, '#' number phrases, and quoted strings.
The asDobjFor() approach had the drawback that the action synonyms didn't ever generate TravelVia actions proper, but simply called the TravelVia handlers internally; so, for example, beforeAction() routines wouldn't ever see a TravelVia action being performed. The new remapping approach is better because it means that every possible action on these objects that involves travel will go through an actual TravelVia action. This allows beforeAction() and similar routines to be assured that they can catch all travel actions by looking for TravelVia alone, eliminating the need to worry about synonym actions that could cause travel.
Released March 23, 2003
Another new class, BasicOpenable, provides the basic state management for openable objects (which can optionally be linked in pairs to maintain the same status across the pair) but doesn't provide any of the verb handling of Openable. This can be used for objects that want to maintain open/closed state using the usual method names, but which don't respond to direct player open/close commands.
IMPORTANT: Objects based on Openable and Lockable must not initialize their open status with isOpen or their locked status with isLocked. Instead, initialize the status with initiallyOpen and initiallyLocked, respectively. Also, be sure you never set isOpen or isLocked directly; instead, call makeOpen() and makeLocked() to effect these status changes.
In ShipboardDirection.defaultConnector, if the source location or any container has 'isShipboard' set to true, then we now use noTravel as the default connector rather than noShipTravel, as the latter is meant to convey that shipboard directions make no sense in non-shipboard locations.
Added a ShipboardRoom mix-in class that defines isShipboard to true.
Along the same lines, Door.connectorTravelPreCond() now calls a separate method, getDoorOpenPreCond(), to obtain the door-is-open precondition object. By default, this returns a doorOpen precondition (wrapped with an ObjectPreCondition for 'self'), so the default behavior hasn't changed.
>stand on platform Okay, you are now standing on the platform. Bill stands on the platform. Bill follows you onto the platform.
The redundant "Bill follows you" has been eliminated.
>sit on red chair (first standing on the red platform) (first getting off of the blue platform) (first standing) Okay, you're now sitting on the red chair.
To the uninitiated, this looks backwards: shouldn't we stand up, then get off the blue platform, then get on the red platform, and then sit on the red chair? Of course, that's what's actually happening, and in its own way the transcript above reflects this: the second "first" applies to the first "first": it's saying "before you can stand on the red platform, you first have to get off the blue platform." Likewise, the third "first" applies to the second "first," to say "before you can stand on the red platform, you have to stand up first." In other words, the implied reports are nested recursively. The recursive structure isn't represented visually, though, so it's not evident whether one of the later "firsts" refers to the preceding "first" or back to the original command line. It would have been possible to represent the recursive structure visually, using indentation or something like that, but this probably wouldn't have made most people very happy; most non-programmers aren't accustomed to thinking in terms of recursion and stacks and tree views, and even programmers might well have found this kind of presentation to be too obviously mechanistic.
To improve the situation, the transcript processor now features a new transform, called implicitGroupTransform, that rearranges these recursive listings into an order that should be entirely straightforward to anyone, programmer or not. The new transform also consolidates these lists into a much more concise and readable format. The transformer does two things. First, it "unstacks" recursive lists of implied action announcements to put them into the chronological order in which the commands were actually performed; the transcript has always kept track internally of which announcement is tied to which action, so the new transformer can merely inspect these records to determine the action relationships and use this information to unwind the stack. Second, the transformer combines each run of adjacent implied action announcements into a single announcement, showing a list of the actions performed. The result is that the example above now looks like this:
>sit on red chair (first standing, getting off of the blue platform, then standing on the red platform) Okay, you're now sitting on the red chair.
This new format should be easier for players to understand, since it shows implied actions in the order in which they're actually carried out, eliminating any need to be aware of the recursive goal-seeking work that the system is doing internally. Hopefully, players will also find it more readable and less obviously mechanistic than the old format.
3.0.6e was released March 16, 2003
Note that the literal is always in the IndirectObject role, regardless of whichLiteral, which only specifies the grammatical role for message generation purposes.
Likewise, assign the IndirectObject role to the topic in TopicAction. This will allow remapTo to be used with a TopicAction.
In particular, the message role only affects the way the object is used in generating messages based on the verb, such as "what do you want to open it with?".
To do this, add a verb rule for TYPE ON <object>, without any literal phrase. For most objects, fail in verification; but when verification passes, in the action, ask for a missing literal phrase. (This has the additional benefit that it makes it easy to create objects that allow generic typing on them, as in TYPE ON COMPUTER, for situations where the game doesn't want to make the player type anything specific but still wants to allow the generic act of typing on the object.)
>ASK BOB ABOUT what do you want to ask him about? >BILL "Ah, yes, , very interesting..."
(The problem is that we're not propagating getOrigText() from the empty topic phrase match down to the underlying replacement match.)
>ASK BOB what do you want to ask him about? >ABOUT BILL "Ah, yes, about bill, very interesting..."
(The problem is that we need an aboutTopicPhrase production to parse responses to ABOUT WHAT questions.)
Remove the special-cased plural filtering in AllPluralProd and DefinitePluralProd. Move this logic instead into Collective's filterResolveList() implementation.
Add a new Thing property, collectiveGroup, that allows an ordinary object to be associated with a CollectiveGroup object. In CollectiveGroup's filterResolveList() method, choose to keep either the individuals (the ordinary Thing objects associated via their collectiveGroup properties with a CollectiveGroup object) or the CollectiveGroup in the resolution list, but not both. By default, make the selection based on action; subclasses can override as desired to use other criteria.
CollectiveGroup can be used to create things like a "money" object to represent a set of coins and bills, so that a command like "look at money" can respond with a single description of all of the money present, rather than iterating over all of the coins and bills individually.
This will allow the traditional ordering, with actor "I am here" messages placed after the rest of the room description, while keeping the rest of the special descriptions grouped with the room's main description. Most special descriptions belong with the main room description because they're meant to be extensions of the room description. Some special descriptions are not; they're meant to describe more ephemeral status information, so work better when grouped with the other status-like messages. Actors in particular fall into the latter category, since actors are meant to seem autonomous, not parts of the rooms they occupy.
Override showSpecialDescInContents in Actor, so that we can show the type of description we previously showed using nestedActorLister.
Remove nestedActorLister and NestedRoom.examineNestedRoomActors().
The conversion of sentence-ending punctuation compensates for the elimination of the double-space insertion in the VM-level formatter. It also improves matters over the old way by making the treatment of sentence-ending punctuation customizable: the game can replace the filter method with its own custom conversions. This allows customization not only for stylistic variation but for language-specific conventions as well.
This would require a new set of VM-level functions in the tads-io set:
In addition, add a LogConsole library class to simplify operations with the log console. This class can be quite simple; it's just an OutputStream subclass that implements the writeFromStream method to call logConsoleSay().
(The mapPushTravelHandlers() macro essentially wants to decompose the two-object action into two single-object actions, which isn't safe with the regular asDobjFor() mapping. The difference with iobjAsDobjFor() is that this new routine temporarily makes the indirect object take on the direct object role, so that the target dobj handler sees the proper object in the direct object slot.)
This change will allow disambigName to be overridden without requiring all of the xxxDisambigName's to be overridden at the same time, because the xxxDisambigName's will by default apply the standard algorithms to the modified disambigName. At the same time, though, this has the virtue of the original implementation that, in the common case where disambigName is not overridden, any overrides to theName, aName, and countName will be used for the corresponding xxxDisambigName's. Furthermore, since these are all still separate methods, objects can still separately override each xxxDisambigName as needed for cases where the disambigName is customized and the customized name requires overriding the normal article algorithms.
While we're at it, add pluralDisambigName, using the same logic.
First, remove the special-case tokenizer handling for "'s" words that appear in the dictionary, so that we handle all "'s" words uniformly in the tokenizer: all "'s" suffixes are treated as separate tokens.
Second, never add "'s" words to the dictionary in initializeVocab(). Instead, define a new dictionary property, adjApostS; when we see a "'s" word in a vocabulary list, remove the "'s" suffix and add only the prefix to the dictionary, but add it as an adjApostS instead of as an adjective. (For simplicity, don't bother with nounApostS at all; allow only adjectives as apostrophe-s words.)
Third, add a new grammar production, adjWord, that accepts either a single adjective or an adjApostS followed by an apostrophe-s token.
Fourth, where appropriate, change 'adjective->' grammar rules to use 'adjWord->' instead.
These changes correct the problem that adding a literal "'s" word to an object's vocabulary prevented that word from being used as a true possessive phrase in the grammar. The old tokenizer rule was that a word that appeared with an explicit "'s" suffix in the dictionary was kept intact, rather than split into separate tokens for the root word and the "'s" suffix. Since the word was kept intact as a single token, it couldn't match the grammar rules for possessive phrases, which only match the separated form. These changes allow the same word to be used in either context, since everything is treated the same way in the tokenizer; even if a word is used as a literal vocabulary word with a "'s", it'll still be split up during tokenization. Even with this change, we can still match explicit "'s" vocabulary words, thanks to the adjWord grammar.
Note this has one small potential impact on existing games: if an object explicitly defines its own 'adjective' property (rather than using the automatic library initialization), and a word defined in the adjective list ends in apostrophe-s, then that word must be stripped of the apostrophe-s suffix, removed from the 'adjective' list, and added to a new 'adjApostS' list instead. Similarly, if any vocabulary words that end in apostrophe-s are dynamically added with G_dict.addWord(), the apostrophe-s words should be stripped of the suffix and added under the '&adjApostS' property instead of the '&adjective' property.
Add a subclass, Heavy, for objects that are immovable because they're very heavy. This is suitable for things like large furniture and big boulders.
In addition, especially with random atmospheric messages, it can get tedious to see the same messages over and over if you spend a lot of time in the same area. It's therefore often the case that we want frequent atmospheric messages when the player first gets to a location, but then we want the frequency to drop dramatically after the player has spent more than a few turns there. To make this easy to handle, add a couple of new properties: 'messageReduceAfter' is the number of times that we want to see messages at the initial frequency, and 'messageReduceTo' is a new value for 'messagePercent' that we'll apply after we've generated the messages 'messageReduceAfter' times. Make these nil by default, which means that there is never a reduction in the frequency. Authors can use these properties to generate random atmosphere messages at high frequency at first, but then drop the frequency to just an occasional message after the player has been in the location long enough to get the idea.
Split off a new method from each of Traveler.describeDeparture and describeArrival: describeNpcDeparture/Arrival, which is called when an NPC (or a traveler not involving the PC) is doing the travel. This will simplify overriding the messages for vehicles and other special travelers when desired, since the overriding method won't have to make all of the checks to see if it's the PC doing the travel.
ShuffledList: the basic shuffled selection class. Keeps a list of values, and returns a randomly selected element on demand.
ShuffledIntegerList: a specialization of ShuffledList that returns a randomly selected integer from a given range.
ShuffledTextList: a subclass of RandomTextList that makes its selection using shuffling rather than independent random selection.
openMsg, closedMsg, lockedMsg, unlockedMsg, onMsg, offMsg
statusStanding, statusSitting, statusLying, statusStandingOn, statusSittingOn, statusLyingOn
>close bag; x all in bag red ball: You cannot see that. [etc]
We're incorrectly resolving the contents of an object for an "all in x" phrase, even when the contents can't be seen. We must filter the resolved objects to include only the visible contents.
askDisambig should provide an open <.parser> tag if and only if askAgain is false: this will let the mode flow in from the preceding re-prompt message from noMatchDisambig or disambigOrdinalOutOfRange. askDisambig should always close the <.parser> tag.
>take coin Which coin do you mean, a gold coin, a silver coin, or a copper coin? >second Taken. [actually takes a gold coin, since more than one was present]
First, get rid of dobjForwardTo, iobjForwardTo, remapTIAction, dobjRemapTI, iobjRemapTI, dobjRemapTIReverse, and iobjRemapTIReverse.
Second, add a new mechanism that combines the old forwarding and remapping schemes into a single new system. Use syntax like so:
desk: Fixed dobjFor(Open) remapTo(Open, drawer) dobjFor(Close) remapTo(Close, drawer) ;
The first definition above maps the Open action, when applied to the desk, to a replacement action of Open applied to the drawer instead. The second does the same thing for Close. This replaces the old dobjForwardTo/iobjForwardTo scheme, so we no longer need forwarding at all. The difference with the new remapTo scheme is that we use a replacement action for the remapping - the remap for Open above is essentially like using replaceAction(Open, drawer) for the action definition of Open on the desk.
Further, consider this syntax:
lid: Fixed dobjFor(Push) remapTo(Open, jar) dobjFor(Pull) remapTo(Close, jar) ;
Here we've remapped not only the object (as we formerly did with forwarding), but also the action. The first definition above says to replace "push lid" with the new action "open jar."
Next, consider this:
ninjaThrowingStar: Weapon iobjFor(AttackWith) remapTo(ThrowAt, self, DirectObject) ;
In this case, we're doing what the old dobjRemapTIReverse did, but
with considerably clearer syntax. We're saying that when we resolve
the indirect object of "attack with" and find that it's the throwing
star, we should remap the action to "throw
The class should treat all of its immediate contents as components, so it should be based on Thing rather than Container or Surface. It should have two additional properties: one for a secret object that serves as the internal container, and another for a secret object that serves as the internal surface. Commands like "put in" and "look in" should be redirected to the internal container, if one is defined; "put on" and the like to the internal surface, if present. The contents lister must specifically show the contents of the secret internal container and surface as though they were contained directly by the outer object.
>look in stove (First opening the stove) Opening the stove reveals a loaf of bread. The stove contains a loaf of bread.
It would be good to remove this redundancy: if we're opening something implicitly for a look-in command, we shouldn't bother showing what the opening reveals. (We can handle this in Openable's Open action handler: if the action is implied, and the parent action is LookIn with the same direct object, we can suppress the revelation message.)
>look in small alcove You see nothing in it. A trophy is visible...
(Perhaps if we special description items, and nothing listed, we should display no message at all.)
>put coin in bag (first opening the bag) Opening the bag reveals a keyring. Done.
Perhaps we should put that "done" on a new line - no paragraph separator, just a new line. (In general, perhaps when there's a non-default response from an implied command, we should add an "implied command terminator separator," which by default would simply be a newline.)
>open door with key You see no door with key here.
In particular, it might be nice to recognize "<nounPhrase> <prep> <nounPhrase>" constructs specifically, and flag them as "command not understood" errors instead of "object not seen" errors. This could be handled by adding a "badness" production for np-prep-np structures for the prepositions commonly used in command phrases; by making this a badness match, we'll match it as a last resort when we can't find a valid verb phrase structure.
Likewise for Distant and Intangible.
>sit on darker chair >sit on chair Which one? >darker
This is allowed, but shouldn't be - the reply should be "you're already sitting on the darker chair." (The problem is that we verify the lighter chair as okay, and we remember that we verified something as okay but don't bother remembering what; we need to track which objects we verify as okay individually.)
>sit on chair >look ... You contain a keyring ...
(We shouldn't mention your contents in that manner. The problem is that Actor inherits contentsListed from Fixed; Actor should override contentsListed and set it to nil, since an actor's contents shouldn't be listed in the ordinary fashion in a room description.)
Add a new Thing property, contentsListedSeparately, that allows an object to control whether its contents are listed in the new in-line style, or as the traditional separate list. Make this nil by default.
>out Out of what? >e You can't get out of that (the east wall).
It would be nice to treat such responses as new commands. We could probably adopt the heuristic that we give the new command interpretation priority over the noun phrase interpretation of a reply to this prompt. Anything that looks like both a syntactically valid noun phrase and a syntactically valid command probably matches the noun phrase syntax only because it's extremely abbreviated; the chances of the verb phrase match being an accident are somewhat less. In addition, it should be less confusing to the user to treat the response as a new command when a noun phrase was intended than vice versa, and it should be fairly obvious how to force a noun phrase interpretation (adding "the" would do the trick, for example).
>i You are carrying a duffel bag (which contains a pen cup). The pen cup contains four pens.
Right now, we're not adding that bit about the pens, because we think we're done with the top-level list, as we've already listed everything: we scan the duffel, and decide that we don't need to recurse into it, since it's listed and has in-line listed contents. What we'd need to do is add another recursive descent to look for everything that we listed at the second (or deeper) level that has contentsListedSeparately.
>e >put cup in desk >undo
(Takes back turns through "e". The problem is that we're marking a remapped action as a nested action; it's not really nested, because the original action is completely replaced and will never reach the execution phase. Do not mark remapped actions as nested.)
tags wherever literal quotes are used, rather than ASCII-34 quotes.
>put ball in asdf Which ball do you mean, the red ball, or the green ball? >red The story doesn't know the word 'asdf'. >oops box Which ball do you mean, the red ball, or the green ball?
The issue is that OOPS retries the entire command with an updated token list, and as a result, the disambiguation work from the first attempt at resolving the objects is lost for the second iteration. The reparsing is necessary, since the corrected typo could change the entire meaning of the command.
One possible solution: add a resolution pass specifically to catch unknown words. Don't do any other resolution on this pass - just deal with unknown words. This would ensure that we process OOPS before we ask any disambiguation questions.
Another possibility: keep a set of answers to questions, and the conditions under which the questions were asked. Clear the list each time we start a new parsing from scratch, but let the list survive if we reparse a token list. Whenever we're about to ask a question, look to see if the same question has been asked with the same conditions, and if so, reuse the same response.
Should this 'take blue book' or should it ask...
Which book do you mean, the blue one, or the green one?
Right now we use the literal tokens from the original command, so we
ask the disambiguation question again. This is exactly what we want
in cases with indistinguishables:
But in cases where we have distinguishable objects, we probably want
the resolved objects, not the words.
We probably want to distinguish this way: if we had indefinites or
indistinguishables, resolve again from the original text. Otherwise,
use the original resolution.
You see five silver coins here.
>take coin
Taken.
>g
Taken.
>take coin Which coin, a gold coin, or a silver coin? >coin Which coin, the silver coin, or the gold coin?
The second time around, the cardinality of the full list should not change, so we should be asking the same question as the first time.
As part of this, we need to find a way to say what object blocks a sense path opaquely, so that we can say why we can't touch something that we can see. There are two main cases: containers are in the way, and connectors are in the way.
Examples of specialized bags of holding: wallet, key ring, purse, duffel bag.
>take both coins Which two (of five gold coins or three silver coins) do you mean? >two gold You don't see that many coins here.
The problem is that we disambiguate with the reduced match list.
>take coin Which coin, gold, silver, or copper? >all but copper
no worky - might even be a gram prod bug, since we seem to have a weird firstToken/lastToken value in these cases
>take gold coin, gold coin gold coin: Taken. gold coin: You're already carrying the gold coin.
When we have multiple equivalent items in scope, and we mention several items in a list, we should pick separate equivalent instances for each mention.
This should apply to disambiguation responses, too:
>take coin Which coin do you mean, a gold coin or a silver coin? >gold, gold
>i You are carrying a paper bag. The paper bag contains a key. >drop key (First taking the key) Dropped.
>get coin Which coin, a gold coin, a copper coin, or a silver coin? >coin Which coin, a copper coin, a silver coin, or a gold coin?
The re-ordering on the second round is weird - we should keep the order stable if we can.
>get ball Which ball, red or green? >blue You don't see that here.
The error shouldn't be "you don't see that here" - it should be more like "that's not one of the choices you were offered." However, it would be better to widen the scope again to the real scope, rather than failing.
When establishing a new context, we check to see if the generator is in scope in the given sense. If not, we hide messages; if so, we show messages.
A nested context should actually be considered a context switch, because we don't want an enclosing hiding context to hide an enclosed showing context. For example, if we are generating messages with sight scope for Lloyd, and we have a nested context where Lloyd is saying something and so we now are in hearing scope for Lloyd, we want the nested context to show its messages if in fact Lloyd is in hearing scope even if Lloyd is not in sight scope (maybe the player character and Lloyd are in contact over a walkie-talkie).
We should see bob leaving on his third queued move, but we don't for some reason.
>bob, n. get silver coin. >n
>get large red ball, small red ball and drop them [works] >get asdf red ball, small red ball and drop them Don't know the word 'asdf' >oops large ...you see no drop them here
Why does it pick out the right interpretation normally but can't on a token reparse?
>ask about watch (asking Bob)
Most default messages are meant to be tagged onto the end of the command line, but when we're adding a direct object to a two-object verb, the added object goes in the middle of the command and hence the default phrase doesn't sound right when tagged onto the end of the command line. By adding the participle form of the verb, we make it clear that the default message stands alone as a new sentence (or sentence fragment, anyway) and is not meant as a missing continuation of the original command text.
The idea is that if we have two objects that are both ranked the same way on one axis but are distinguishable on another axis, we want to consider the axis on which they're distinguishable. For example, if we type "put silver coin in jar," and one jar is open and the other is closed, we want to pick the one that's open. At the same time, they'll both be ranked as equivalently logical on the basis that neither is held by the actor. We don't want the being-held status to override the open status, but we likewise don't want the open status to override the being-held status if both are open but only one is being held. The solution is to ignore the equivalent being-held status when both objects have this same value, and to ignore the equivalent open status when both have the same value, but still consider these attributes when they differ.
iobjFor(AttachTo) asIobjFor(PutOn)
>dig in dirt What do you want to dig in it with? >with shovel
>ask (Bob) What do you want to ask him about?
>ask (asking Bob) What do you want to ask him about? >watch (asking Bob)
>ask (asking Bob)
It would be better as simply
>ask (Bob)
>go through passage >again
This somehow keeps the old passage in scope. Must need to check scope again for 'again'.
(The problem is that the last action is hanging on to its cached resolver scope lists. We simply need to drop the old resolvers so that we start the new command with new resolvers.)
for (pass = 1 ; pass <= 2 ; ++pass) run all verifiers for each precondition run this precondition, allowing implicit commands on pass 1 ONLY if no implicit commands were executed break
This would ensure that side effects of an implicit command are re-tested through previous preconditions. Each precondition would only be allowed to run its implicit command on pass 1 because this would prevent infinite loops from conflicting preconditions - on pass 2, if the condition isn't met by now, we'll assume it's not going to be, so we'll simply fail the command.
(trying each key on the keyring, you find that the iron key unlocks the door)
The problem is that the objHeld precondition is applying a likelihood of 80 because the iron key isn't held, and the held keys are getting 100's. Maybe we need to reduce the likelihood of any other keys or keyrings when the known key is present, but this seems like a poor special-case hack for a problem that is likely to arise in other contexts.
To solve this, add a 'priority' to logical results. Use low priority for precondition rankings, because these are inherently of lower importance than unique attributes of the objects themselves.
>move me to What do you want to move it to?
Could we substitute "yourself" for "it" somehow?
>get key's jar Which key's do you mean...
That should just be 'which key do you mean', not 'which KEY'S'
>bob's unknown word ''s'
>get key Which key do you mean... >get bob's key The word ''s' is not necessary...
The problem would seem to be that we don't handle 's in disambig responses and treat it as an unknown word.
>look under me You see nothing unusual under you.
We should make that second 'you' into 'yourself' instead. In general, can we change an objective case use of an object already used in the nominative should to a reflexive instead?
>bob, x key Which key? >quit This command cannot be directed to another character...
>lock door (first closing the iron door) (with the iron key) Locked.
The defaulted indirect object is misleading. This should instead be:
>lock door (with the iron key) (first closing the iron door) Locked.
The problem comes from the fact that "Lock" has an objClosed precondition for keyed lockable; "Lock" doesn't need this precondition, because "LockWith" has it. Removing the precondition will get the ordering right.
>type hello on typewri
>get iron key You take the key and attach it to the keyring >get keys brass key: You take and attach... rusty key: You take and attach... iron key: Taken.
It's irritating that the last one is taken off the keyring. Should we leave items out of plurals when we have multiple logical items of different logicalness? (This isn't a completely academic problem, since you could in practice have a couple of new keys in a room that you want to pick up, without detaching keys you already have on the keyring. One easy way to solve this would be to use a different command for detaching from the keyring - require 'take key off keyring', and say Illogical('if you want to take the key off the keyring, say so'); but this could be irritating when you just want to take the key off the keyring.
>follow bob Bob is right here. >bob, u You see no bob here.
>bob, n >g
We're not checking to see if the actor is still in scope.
>n. open door. >close door and sit down--> endless loop - weird problem with token indices for 'sit down' part of grammar tree. The token indices for the second predicate are for the entire command, strangely, so we think we need to parse the entire phrase again and again.
This has something to do with the empty dobj noun phrase. Same thing happens with 'close door and open' (which also has an empty dobj), but not with 'close door and look' (which takes no dobj) or 'close door and sit down on floor' (which has a non-empty dobj).
Make use of this information in 'verify' routines for the
earlier-resolved objects when applicable. For example, for "take
Make "listen to x" and "smell x" defer to the associated objects.
For scoping, a sound or smell doesn't have to place the main object in scope, but rather can just put the intangible sensory emanation in scope. For example, if an alarm clock is buzzing, the 'buzzing sound' object would be in scope but not the alarm clock itself. (On the other hand, some sounds, like a ringing phone, might be sufficiently distinctive as to place the main object in scope. But for those cases we can simply associate the sound/smell with the main object.)
>listen to phone It's ringing. ==> phone.soundDesc >listen to ring [phone visible] It's coming from the phone. ==> sound.soundWithSource >listen to ring [phone not visible] It sounds like a phone. ==> sound.soundWithoutSource >look [phone visible] ... The phone is ringing. ==> sound.soundHereWithSource >look [phone not visible] ... You can hear what sounds like a phone ringing. ==> sound.soundHereWithoutSource
The obstructor can be easily found with gActor.findOpaqueObstructor(sight, obj) (where 'obj' is the noise or odor object). Once the obstructor is found, we'll have to generate an appropriate message, which will require a new method parallel to obs.cannotReachObject(obj) - perhaps we could call it obs.describeSoundObstructor(obj) etc - which would display "The ringing seems to be coming from inside the box" and the like.
This entire mechanism would not be used when the obstructor itself cannot be seen, such as in the dark.
>look ... You can hear what sounds like a phone ringing. >listen to ring It sounds like a phone. >x phone You don't see any phone.
It would be better if that last line were:
>x phone You can hear a phone ringing, but you can't see it.
To deal with this, adjust the touchObj and objVisible preconditions so that it provides better feedback for an object that can be heard but not seen.
>take ball Which ball, the red ball, or the green ball? >third There weren't that many choices - did you mean the red ball, or the green ball? >orange That wasn't one of the choices - did you mean the red ball, or the green ball?
However, if the response looks like a valid response but doesn't give us a match, and it also looks syntactically like a valid new command, treat it as a new command.
Text lists should be linkable to a common counter object, so that the lists are synchronized on the same counter. This lets you have separate lists for each room, but keep the timeline in each list the same.
It should be possible to associate a text list object with a room instead of coding the room's text list in-line in the room.
>take coin Which coin, silver or gold?
We should probably keep the copper one in the list. In particular, we probably shouldn't remove an item from consideration for disambiguation just because it's less likely - once we've determined that the noun phrase is ambiguous, we should offer all logical matches, even when they're less likely.
>take coinc Which coin...? >take tricyc The word 'tricyc' is not necessary in this story. >take tricyc Taken.
For some reason, truncated words are treated as misspellings when they appear in disambiguation responses.
(Experimental code has been added, conditionally compiled based on the SENSE_CACHE macro. We'll leave it on for the time being to get some experience with it to see how well it works.)
You are carrying three matches (one lit), two flashlights (one providing light).
Add a macro (gMessageParams) to simplify the syntax for adding new message parameters.
>bob, get ball Which ball? >z Bob waits...
The "z" should have a default target of the player character, not Bob.
Symptoms fixed: "ask bob and bill about box" -> "you see no bob and bill here"; "give coins to bob and bill" -> "you see no coins to here".
modify grammar production(tag):: ; replace grammar production(tag): : ;
To make this workable, the "production(tag)" names of grammar match objects must be unique; change the compiler to enforce uniqueness of these names.
Also, change VerbRule to include a tag: VerbRule(tag). This will allow modify/replace to be used with VerbRule(tag) definitions to replace library verb grammar rules.
To do this, add a new pair of properties to TIAction: remapDobjProp and remapIobjProp; define these properties using the same template style as the rest of the properties in DefineTIAction. Add a new remapTIAction() macro that can be used from within a remap() method to remap a command.
Note that only the first-resolved object can be remapped, because the whole point is to effect the remapping before resolving the second-resolved object, so that the second-resolved object can be resolved using the rules of the new action rather than of the original action.
Use angle-bracket notation, even though we don't use it for individual tokens: <nounM nounN nounF>
Add redirectTravelIn, etc, which do show isConnectorApparent = true.
NOTE: This change affects all of the notifier, filter, monitor, and capturer function interfaces. All of these formerly global functions are now methods of the OutputStream class.
We should test the scope list as a second pass rather than allowing the full scope on the first pass. So, we should try first as we do now, limiting scope to the narrowed ("logical") list; then, if that fails, we should try again with the full scope list.
dobjForwardTo(Open, drawer)
This is used in place of a dobjFor().
(This was from Phil Lewis, who suggested "reroute" as the name and also suggested a slightly more general format: dobjReroute(drawer, Open). It might also be useful to be able to specify the verb to forward separately, so that you could handle a verb with both a different verb and different target object, but the action is probably the same in most cases, so it seems more convenient to be able to omit it.)
To do this, return a CommandResults object describing the command reports list from newAction(), nestedAction(), and so on.
result = withCommandReportsClass( MyCommandReportList, {: nestedActorAction(bob, SitOn, chair) });
>light match (the match)
when we have one match we're holding and another in the matchbook: we choose the one we're holding over the one in the matchbook because of the must-be-holding precondition to lighting a match, but mentioning which one we're choosing is weird because it doesn't tell us anything.
>drop match Which match, one of your matches, or the match in the matchbook? >my match Which match, one of your matches, or the match in the matchbook?
...etc. The problem is that we're taking "my match" to mean any match in my possession for parsing purposes, whereas we differentiate holding from indirectly owned. Ideally, we'd prefer to treat "my match" as "the match I'm holding directly" when it's ambiguous.
Which candle to you mean, the lit candle or the unlit candle? -but not- Which wax do you mean, the unlit candle or the seal? -which should just be Which wax do you mean, the candle or the seal?
Use this mechanism to create a base class for light sources. We can use this for things like matches and candles. The disambig name for an object is "lit x" or "unlit x", so we add the adjective "lit" or "unlit" according to state. Check the adjective in parseName.
Use this same mechanism to specify by owner when owner is a distinguishing factor - "which gas mask do you mean, yours, or teeterwaller's?"
To do this, associate with each object a list of "distinguisher" objects. A candle might include the "lit" distinguisher object, and probably all objects would include the "owner" distinguisher. We'd make a list of all of the distinguishables in common to the set of equivalents, then run through the common set. For each one, we'd ask the distinguisher if the list of equivalents we have is distinguishable via this distinguisher. The "lit" distinguisher would return true if the objects were all in different "lit" states, and the "owner" distinguisher would return true if all had different owners. We'd stop at the first distinguisher that can tell all of the objects apart. The distinguisher would give us the method to call in each of the objects to list its distinguishing name - the "lit" distinguisher would call the litName property, for example, which would display "the lit candle" or "the unlit candle"; the "owner" distinguisher would display "yours" or "teeterwaller's".
The response would have to be handled by the normal disambiguation response mechanism, so the objects would have to conspire with the distinguisher to have the proper adjectives. This is easy, though, because the objects are the ones displaying the adjectives in the first place when they show the disambiguation list.
If we find no distinguisher that can tell all of the items apart, we'd look for one that can tell at least some of the items apart, and phrase it like this: "the lit candle, or one of the unlit candles". If they select one of the ones indistinguishable with this distinguisher, we'd iterate, and maybe pick it up with a separate distinguisher next time.
If none of the distinguishers can tell any of the objects apart, we'd simply choose one arbitrarily like we do now.
>take candle Which candle do you mean, the lit candle, or one of the unlit candles? >unlit Which do you mean, your candle, or one of Bob's? >bob's (arbitrarily picking one of bob's several unlit candles) Taken.
Note that for ownership distinctions, need to offer a way to refer to unowned objects:
>get candle Which candle do you mean, Bob's, or the other one? -or- Which candle do you mean, Bob's, or another one? [responses would include...] >another / another one / another gold coin / one of the others / one of the other gold coins / other / the other / the other one / the other gold coin / other gold coin
dobjRemapTI(OpenWith, UnlockWith) dobjRemapTIReverse(FillWith, PutIn) dobjRemapTIReverse(ThrowAt, AttackWith)
>i You are carrying three matches (one lit), and a matchbook (which contains two matches). >x match Which match do you mean, an unlit match, or the lit match? >an unlit match (the match) The match is an ordinary match.
It would be better not to make an arbitrary choice here, unless they explicitly said "any unlit match". We should have another go at asking for detail in this case.
PO Box: Taken. red ball: Taken. iron key: (First putting the red ball in the duffel bag to make room) Taken. blue test booklet: Taken. brass key: (First putting the red book in the duffel bag to make room) Taken. gold coin: Taken. gold coin: Taken.
Obvious exits lead north, back to the east, and south to the den. ...north; east, back to the living room; and south, to the den.
>bob, north Bob opens the door. Bob departs through the door.
>bob s Bob must open the door before he can do that. >bob, open door Bob must unlock the door before he can do that.
For special descriptions, add a "room part location" property - this is a purely advisory property used only for finding contents of the floor and so on. When we "x floor", show special descriptions only for objects whose roomPartLocation is the floor of interest.
>fill bucket (dipping the bucket into the river) >fill bucket (pouring the coffee into the bucket)
This is important because the normal announcements are worded with the intention of being tacked onto the end of what the player typed; when the action is remapped, the action we're actually executing is different from what the player typed, so phrase fragments that are intended to be added to what the player typed are no long applicable.