Recent Changes to the TADS 3 Library
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 4/10/2008
The new class SimpleLister provides simplified interfaces for creating
formatted lists of items. This class is useful when you just want to
create a textual listing, using the standard serial-comma generation
and so on, without all the hassle of specifying a sense environment,
point of view, etc. The class also has a method that creates a text
string for a listing (rather than displaying the list directly, which
is what the base Lister normally does).
Two concrete instances of SimpleLister are provided, to further
simplify particular listings: objectLister, which can be used to
format a list of simulation objects; and stringLister, which can be
used to format a list of string values.
Commands of the form "X, Y", where both X and Y were words not in the
game's dictionary, caused a run-time error ("nil object reference").
This was due to a library bug in the code that resolves the actor
object when giving orders to an actor ("BOB, GO EAST"). This has been
corrected.
The Goal object has a new property, goalFullyDisplayed, that the hint
system automatically sets to true upon displaying the last hint in the
Goal's menu list. This can be used, for example, to close a hint for
a red herring after the player's seen the full list of hints; this
might be desirable to remove clutter from the menu, since the player
probably won't worry about an object again once it's been revealed
that it's just a red herring.
The templates for TravelMessage, NoTravelMessage, and DeadEndConnector
now accept an event list (given as a list in square brackets) in lieu
of a double-quoted message string. These classes were already mixable
(via multiple inheritance) with EventList, so this change simply makes
it more convenient to define instances when using this capability.
Top-level rooms (i.e., Room objects) didn't properly handle ENTER
commands (e.g., ENTER HALLWAY) when attempted from a separate
top-level room connected via a sense connector. The ENTER handling
for Room incorrectly assumed that only one top-level room was in scope
at a time, which isn't true when rooms have sense connections. The
handler also failed to handle the simpler situation where the actor
was in a nested room within the target room.
The Room dobjFor(Enter) handler has been improved to handle
such situations. The new handling is as follows:
- If the actor is already directly in the target room (for
example, the player types ENTER HALLWAY while already in the hall),
the behavior is as before ("You're already in the hallway").
- If the actor is indirectly in the target room (for example,
the PC is sitting in a chair in the hall, and types ENTER HALLWAY),
the command is replaced with GET OUT OF outermost nested room
containing actor within target room.
- If the actor is in a separate top-level room, and there's
a travel connector from the actor's current location to the target
room, the command is replaced with TRAVEL VIA connector.
- Otherwise, the command fails with the message &whereToGoMsg
("You'll have to say which way to go").
In the past, when NestedRoom.checkMovingActorInto() decided it had to
perform an implied action to move the actor into the nested room, the
routine assumed that the action failed if the actor didn't end up in
the nested room
and in the room's default posture. This
condition does hold for the library NestedRoom subclasses when the
defaults are inherited, but overriding either the default posture or
the implied command for a particular instance can make the condition
fail. When the condition fails, the routine infers that the implied
action failed, and terminates the whole command.
This condition - specifically, the test for posture - is overly
strict. The routine isn't documented as guaranteeing the default
posture, and in fact it never did guarantee the default posture
anyway, as the implied action was always bypassed if the actor's
location was initially correct, regardless of posture.
So, the condition has now been changed to remove the default
posture check. The implied command (if any) is now considered
successful if the actor is directly in the nested room afterward.
The Decoration class now overrides the SEARCH action so that it uses
the standard Thing handling, to be consistent with the way LOOK IN is
handled. In the past, SEARCH instead was handled by the Decoration
catch-all verb handler, which shows the generic "that's not important"
message. This led to different default Decoration behavior for SEARCH
vs LOOK IN, which was inconsistent with the library's usual default
treatment of the two actions as synonymous.
The English library's typographical output filter object
(typographicalOutputFilter) ordinarily adds an "en" space (a space
that's slightly wider than a normal inter-word space) after any
sentence-ending punctuation, such as a period or question mark. This
produces looser spacing between sentences, which some people find
easier to read. However, it had an undesirable side effect, which was
to add the same spacing after honorific abbreviations like Mr. and
Mrs. - these look exactly like sentence endings, as far as the simple
substitution rule was concerned.
The filter now specifically looks for a pre-defined set of
abbreviations, and suppresses the extra spacing when it finds one.
The abbreviations are given by the property
typographicalOutputFilter.abbreviations - to change or add to the set
of abbreviations, modify typographicalOutputFilter to change this
property. (Note that this property is only evaluated once, at program
start-up, so changing it on the fly won't have any effect. If you do
want to modify the set of abbreviations dynamically, you'll have to
instead update typographicalOutputFilter.abbrevPat to use a new
RexPattern object with the new set of abbreviations.)
DistanceConnector now allows moving objects through the connector via
moveInto - in particular, DistanceConnector now overrides
checkMoveThrough() to return a "success" indication.
In the past, DistanceConnector inherited checkMoveThrough() from
SenseConnector, which disallowed a move via moveInto() if the distance
connector was involved.
In the abstract, there's usually no good reason that an object
can't be moved through a distance connector. The only reason to
disallow it might be that the distance involved is so great that it
would take an impractical amount of time to complete the move.
More to the point, though, player commands will generally disallow
this kind of operation for reasons of reachability - a touchObj
precondition is involved for any basic command that moves an object
from one container to another (TAKE, DROP, PUT IN, PUT ON, etc), and
that precondition will fail separately because the distance object is
unreachable. Because of this, a moveInto() involving a distance
connector is almost certainly due to a programmatic (scripted)
operation, rather than a basic player command. In the past, you could
work around this by calling moveIntoForTravel() instead of moveInto(),
but this was a needless complication. This change allows moveInto()
to succeed even if a distance connector is involved.
The ShuffledEventList object has a new property, suppressRepeats, that
lets you control whether or not a given event can occur twice in a
row. This property corresponds to the suppressRepeats property of
the ShuffledList object.
By default, this is set to true, which yields the former behavior.
You can set this to nil if you don't want to suppress repeats. With
very small lists (of three or four elements), the suppression of
repeats sometimes makes the result sequence look fairly un-random,
because the no-repeats constraint reduces the number of available
permutations. If you see output for a small event list that doesn't
look random enough, you might try setting suppressRepeats to nil to
see if that produces more pleasing results.
ShuffledList now ignores suppressRepeats for a two-element list. If
the list has only two elements, and repeats aren't allowed, the list
will necessarily yield the predictable sequence A-B-A-B..., which
defeats the purpose of the shuffled list.
The Lockable class's dobjFor(Open) check() method now inherits the
default handling. Since Lockable is designed to be used as a mix-in
class, this addition ensures that any additional Open checks inherited
from other superclasses are properly invoked.
The comment pre-parser object (commentPreParser) now sets a runOrder
value that's lower than the default, so that it runs before other
pre-parsers that inherit the default runOrder. In most cases, it's
unnecessary (and even potentially problematic) to run comments through
other pre-parsers, since most are designed to parse the usual sorts of
command syntax, not the free-form text allowed in comments. Running
the comment pre-parser first effectively bypasses other pre-parsers
when a comment is encountered, since the comment pre-parser halts
further processing of the command when it detects that the command is
actually a comment.
In the past, if an actor returned nil from obeyCommand(), the parser
ignored the command for the purposes of the AGAIN command. A
subsequent AGAIN command in this case simply repeated the
previous command - the one just before the refused order. This
has been corrected; the parser now remembers a command for AGAIN even
if the actor to whom the command is directed refuses it.
Thing.setGlobalParamName() didn't correctly delete any previous name
from the global table. (This would only have been a problem if you
used the method more than once on a particular object, to change the
object's global parameter name on the fly, which seems highly
unlikely.) This has been corrected.
Released 11/2/2007
There are no library changes in this release.
Released 9/13/2007
NonObviousVerifyResult.resultRank is now defined as the same value as
IllogicalVerifyResult.resultRank. This makes the parser treat
illogical and non-obvious objects as equivalent in cases of ambiguity,
so that the parser won't choose one over the other. In the past,
non-obvious objects were ranked lower than illogical objects, which
caused the parser to choose an illogical object over a non-obvious
object in cases of ambiguity. This didn't quite make sense; for the
purposes of choosing among ambiguous objects, non-obvious objects
really should be treated as equivalent to ordinarily illogical
objects, since the point of the non-obvious designation is that the
object appears at first glance to be illogical for a given verb.
The parser now incorporates Michel Nizette's vocabLikelihood
extension. This extension lets each object set an "intrinsic
likelihood" value that affects how the parser chooses among ambiguous
objects. When the parser can't tell the difference between two
objects using the normal 'verify' procedure, it compares the
vocabLikelihood properties of the two objects, and chooses the one
with the higher value if they differ. This makes it easier to
fine-tune the disambiguation process to reduce unnecessary prompting.
SensoryEmanation.endEmanation() now resets displayCount to nil.
displayCount was documented as being automatically reset when a period
of continuous presence ended, but in the past no actual resetting was
done. We set the value to nil, rather than to 0 or 1, to make it more
obvious (when debugging, for example) that the object is not currently
in scope.
The English library now defines suitable custom objInPrep,
actorInPrep, and actorOutOfPrep values for Surface, Underside, and
RearContainer. This ensures that the default messages produce
reasonable relational descriptions when referring to the contents of
these specialized container types. (In the past, only
Surface.objInPrep was suitably customized.)
BasicOpenable.tryImplicitRemoveObstructor() now only attempts an
implied Open if the object is closed. When the object is already
open, the routine now does nothing. This change is necessary because
objects can conceivably create an obstruction even if they're already
open: once the object's open, if it's still creating an obstruction,
another implied Open would be pointless (and, in fact, could cause
an infinite loop).
The new Thing method isOrIsIn(obj) returns true if 'self' is either
inside 'obj' or equal to 'obj'. This is useful for cases where you
want to check if a given object is anywhere within another object's
containment tree, including the other object itself.
When a MultiLoc object is the point-of-view object in a sense
calculation, it now connects all of its multiple containers to the
scope in addDirectConnections(). In the past, a MultiLoc's containers
were only considered part of the scope when the point of view was
inside the MultiLoc, but it makes sense for the MultiLoc itself
to be the same way, since it inherently "sees" all of its containers.
LocateInParent is now defined as a class. (It was always meant to be,
but the definition lacked an explicit 'class' keyword in past version.)
Released 7/19/2007
Compatibility warning: The message property cannotTalkToSelf
has been renamed to cannotTalkToSelfMsg, for consistency with other
message property names. (You should search your existing game code
and update any references to the old name to use the new name
instead.)
The new gameMain property beforeRunsBeforeCheck lets you control
the relative ordering of the "check" phase and "before" notifiers.
In the past, the library always ran the "before" notifiers -
beforeAction and roomBeforeAction - before running the check()
handlers for the objects involved in the action. To ensure
compatibility, the default setting for the new property is
beforeRunsBeforeCheck = true - this causes "before" to run before
check(), as it always has.
However, in many ways, it's more logical and practical to run the
check() phase first first, and then run the "before" notifiers.
This ordering allows the "before" handlers to assume that the action
is more or less committed to running to completion, since they know
that the check() phase has already tested the action and allowed
it to proceed. The most common use of "before" handlers is to carry
out side effects of an action, so by running all of the check() tests
first, the "before" handlers can more confidently invoke their side
effects, without worrying that the action will later fail due to a
check() condition.
Now, you can never truly call an action "committed" until it's been
fully carried out, since even a "before" handler can conceivably
cancel a command (with "exit", for instance). You could set up a
situation where an action is affected by two "before" handlers, and
the one that runs later cancels the command - thus invalidating any
assumption in the first one that the command is committed. However,
this is relatively unusual, and in any case it's under your control:
if you don't use "before" handlers to cancel commands, it won't occur.
If you want to use the new alternative ordering, where "check" runs
before the "before" notifications, simply set beforeRunsBeforeCheck to
nil in your gameMain object. Some authors have tested this ordering
with existing games and found it to work well with existing code, but
be aware that you could trigger subtle changes - you might have
unknowingly created dependencies on the relative ordering of check()
and "before" that will only show up under certain conditions. We
therefore recommend using the new alternative ordering only for new
code or for projects that are relatively early in their development
cycle, to ensure thorough testing of the new ordering.
ThingState now allows tokens to be shared among multiple states
associated with the same object. In the past, state words had to be
unique among an object's states: it wasn't possible to have a set of
states such as "unpainted", "painted red", and "painted blue", because
it was invalid for "painted" to appear in two states. This sort of
sharing is now allowed.
The old rule was that a word could match an object only if the word
didn't appear in any of the object's other (i.e., non-current)
ThingState token lists. The new rule is that a word can match an
object if (a) the word appears in the object's current
ThingState token list, or (b) the word doesn't appear in
any of the object's other ThingState token lists. Condition
(a) is what's new; it allows words to be shared among states by
allowing a word to match the current state as long as it's in the
current state's list, regardless of whether it appears in other
states' lists.
In the English library, when a command triggers a chain of several
implied actions, the announcement for the implied actions now adds
the word "then" in each conjunction: "(first standing up, then
taking the box, then opening it)". In the past, "then"
appeared only in the final conjunction; most people find it reads
more naturally to use "then" in each conjunction.
The English library now uses the adjustDefaultObjectPrep() mechanism
to generate implied action announcements. This means that verbs like
SitOn and GetOutOf will use object-specific prepositions when they're
announced as implied actions: "(first sitting on the bench)" vs
"(first sitting in the car)".
The actions GetOutOf and GetOffOf now adjust the prepositions in their
generated verb phrases according to the actorOutOfPrep definition for
the direct object.
AccompanyingInTravelState.initiateTopic() now defers to the next state
object. Since the in-travel state is designed to be ephemeral, it's
unlikely that one of these would actually contain its own initiated
topics, so in almost all cases the desired topic would come from the
state that the actor will be in following the travel.
The library now defines a simple template for ShuffledList. The new
template takes a single list parameter giving the value list.
ListGroupCustom now defines groupDisplaysSublist = nil by default.
This type of group is intended mainly for cases where you want to
display a collective message to describe the group, rather than
listing its elements individually, so we usually don't want the
inclusion of a ListGroupCustom message to trigger the "long list"
format (i.e., with semicolons) in the overall list. This change
makes this behvior the default.
Non-physical CollectiveGroup objects (i.e., those with 'nil'
locations) didn't work when the associated individuals were MultiLoc
or SenseConnector objects. This has been corrected.
Actor.knowAbout() now considers an object to be known if it's
currently in sight. In the past, an object was known if it was
specifically marked as known, or it was marked as having been
previously seen. For the most part, an object that's currently
visible will also be marked as having been previously seen, since this
marking occurs whenever a Look command or travel to a new location
lists an object. However, if an object comes into scope (via
moveInto) while an actor is in a given location, the object won't be
marked as having been seen until the next Look command or subsequent
travel away from and then back to the location.
This change corrects a subtle problem that involved possessive
phrases in commands. In the past, an object that just came into scope
was excluded from matching a possessive noun phrase under certain
circumstances, such as in the response to a disambiguation question.
This should no longer occur.
In the past, the library
seemed to allow commands like PUSH TV
INTO BOX (i.e., commands to push a pushable object into a nested
room), but on closer inspection it didn't really carry them out:
instead, it carried out only the underlying basic travel command,
without actually moving the object supposedly being pushed around.
The library now rejects PUSH INTO
nested room commands with a
new message, &cannotPushObjectNested ("You can't push the TV
there").
The library rejects these commands by default because we think
it'll be relatively rare that games will need to allow them. Given
that, it didn't seem worth complicating things by adding a bunch of
new methods to provide generalized support, and we also didn't want to
introduce that much new code at the current point in the release
cycle.
However, the library does now provide a new hook that will make it
easier for games to implement PUSH INTO nested commands if they
need to. The new hook will also make it possible to implement riding
vehicles into nested rooms, which wasn't previously possible. The new
hook is a new Traveler method, travelerTravelWithin().
This new method is called on the Traveler object when a nested-room
travel command (GET IN, STAND ON, SIT ON, LIE ON, PUSH X INTO, etc) is
performed. In simple cases, the Traveler is simply the Actor
performing command. However, if the actor is riding in a Vehicle, the
Traveler is the Vehicle object; and if the command is a PUSH travel
command, such as PUSH TV INTO BOX, the Traveler is a special ephemeral
object, of class PushTraveler, created just for the command.
The library provides default implementations of the new hook method
for Actor, Vehicles, and PushTraveler. For Actor, the method simply
does the same thing that travelWithin() used to do, so existing code
will work as it did before. For Vehicle, the method calls the same
method on the Actor, so again it will work as it used to. For
PushTraveler, the method simply terminates the command with the new
message &cannotPushObjectNested.
If you want to allow pushing objects into nested rooms, you'll need
to modify PushTraveler.travelerTravelWithin() to do basically the same
thing that PushTraveler.travelerTravelTo() does. You'll probably want
to set up a new set of notification methods parallel to
beforeMovePushable() and movePushable() - you could just use those
same routines, but you'll probably want to set up new custom
nested-room versions instead, since the conditions and messages will
probably need to be different for nested rooms from those used for
room-to-room travel.
Note that you could also take advantage of the new method to allow
an actor to ride a vehicle into a nested room. The default handling
in Vehicle defers to the underlying Actor, and the actor performs the
travel as though the Vehicle were any other nested room - which means
that the actor gets out of the vehicle, then gets into the new nested
room. If you instead wanted this type of command to mean "ride
vehicle into nested room," you can override travelerTravelWithin() on
the vehicle, so that the method moves the Vehicle itself into the new
nested room. (You'll also have to set up preconditions that move the
vehicle to the proper staging locations using the same rules that are
used for actors, which could take a little work - that's the main
reason the library doesn't support it out of the box yet.)
In the past, if an NPC was using AccompanyingState to follow around
the player character, and the pair went through an AutoClosingDoor,
the door was reported as having closed twice during the turn -
once for each actor going through. AutoClosingDoor now skips the
auto-closing step if the actor going through is in an accompanying
state.
The parser's "OOPS" mechanism had a problematic interaction with
literal phrases that showed up under certain conditions. In
particular, if the player entered a command that caused some kind of
follow-up question, such as a disambiguation query ("which book do you
mean...") or missing-object prompt ("what do you want to unlock it
with?"),
and the player chose to ignore the follow-up question
and instead just enter a new command,
and the new command
contained a literal phrase ("type asdf on keypad", say), the OOPS
mechanism was overly aggressive in deciding the command contained a
typo.
The root of the problem was that follow-up questions are parsed
according to their own special grammars, which are much more limited
than the full grammar used at the main command prompt. Normally, when
the player responds with something that doesn't match one of these
special grammars, the parser assumes the player intended to bypass the
follow-up question and just enter a new command. However, if parser
manages to find a non-dictionary word in the player's input, it
invokes the OOPS mechanism to see if the player actually intended to
answer the follow-up question but made a typographical error entering
the response. The problem is that if the player entered a
literal-phrase verb, the presence of a non-dictionary word
doesn't necessarily imply a typographical error. For normal
commands (as opposed to follow-up questions), this isn't an issue,
because the parser finds the literal when matching against the full
grammar. For follow-up questions, though, the parser never noticed
the literal-phrase interpretation of the input, since it wasn't
matching against the full grammar, so it prematurely decided that the
input as mis-typed.
The OOPS mechanism now makes an extra check when presented with a
non-dictionary word in the response to a follow-up question. Before
deciding that the input really does contain a typo, the OOPS mechanism
first checks the input to see if it matches anything in the full
command-line grammar; if the parser finds a match, it assumes that the
entry was meant as a brand new command, and so it bypasses the
follow-up question and re-parses the input as a new command.
In the past, calling getOrigTokenList() on an EventAction object
caused a run-time error. This was because EventActions don't have the
token list data that the parser normally attaches when creating an
Action to represent a parsed command line, which in turn is because
these actions aren't created from command parsing but are instead
"synthetic" actions, created internally by the library for bookkeeping
purposes. (This same problem might have affected other synthetic
actions as well, but it's only known to have shown up with
EventActions.)
This has been corrected. Action.getOrigTokenList() now checks to
make sure there's a parser token list attached to the action, and if
not, the method return the parent action's token list; and if there's
no parent action, the method simply returns an empty list.
HermitActorState now sets its property limitSuggestsion to true by
default, to prevent the TOPICS command from displaying any topic
suggestions from the actor or conversation node. This is desirable
because the hermit state effectively blocks any topics defined outside
the state: the whole point of the state is that it makes the actor
essentially unresponsive to conversational overtures. So there's
generally no point in offering topic suggestions while the actor is in
the state.
In some cases, you might want to override this for a particular
hermit state object. In particular, if it's not outwardly obvious
that the actor will be unresponsive, you might still want to allow
suggestions, since the player has no way of knowing that the actor
won't respond to questions until she attempts asking them. Similarly,
if the hermit state is expected to persist for a short time only, you
might want to continue to allow suggestions so that the player knows
there are useful topics to explore when the actor becomes responsive
again.
ByeTopic now overrides impliesGreeting to suppress any implied HELLO
greeting. This means that if the player explicitly says GOODBYE to an
NPC, and no conversation is in progress, the goodbye response will be
displayed by itself, without an implied greeting exchange. In the
past, ByeTopic inherited the default 'true' value for impliesGreeting,
so saying GOODBYE to an NPC outside of a conversation caused an
implied greeting, followed immediately by the explicit goodbye
response. In most cases, this ended up looking pretty strange - not
only was the self-canceling HELLO-GOODBYE exchange odd on its face,
but in most cases the implied greeting also looked redundant from the
player's perspective, because a player tended to say GOODBYE
explicitly only when she was under the impression that some kind of
conversational interaction was already under way.
In the past, conversation-flow problems sometimes occurred if a topic
changed its "isConversational" status as a side effect of showing the
response for the topic. This was because
ActorTopicDatabase.showTopicResponse() waited until after invoking the
response to determine whether the response should affect the
conversation flow, so the status change side effect caused
showTopicResponse() to use the new setting - meant for the next
response - rather than the setting for the response it actually
showed. This has been corrected; showTopicResponse() now makes its
determination based on the isConversational status as it stands just
before invoking the response.
The finishGameMsg() function now explicitly runs the score notifier
daemon before it displays the end-of-game message. This ensures that
the usual score-change message is displayed if any points were awarded
in the course of the action that triggered the end of the game.
Without the explicit invocation, the notifier daemon wouldn't
typically get a chance to show a notification for the final point
award: the notification is normally shown just before the next command
prompt after points are awarded, and at the end of the game there's
not usually another command prompt forthcoming.
In the English library, inlineListingContentsLister didn't always
generate correct verb agreement for its prefix message for a "wide"
listing ("You are carrying tongs (which contains ...)").
This has been corrected.
In the past, searching a RoomPart nominally containing a RoomPartItem
sometimes caused a "nil object reference" run-time error. This was
due to a missing point-of-view object setting; the code where the
error occurred now applies a default POV of the command actor when
no explicit POV is set.
Container and Openable objects now apply a touchObj precondition to
the Search action. This requires that the actor be able to reach the
object in order to search it. This change is intended to better
enforce the idea that searching a container involves some actual
physical manipulation, such as shifting the contents around to look
behind or under things, momentarily removing contents to see what's
underneath, and so on. The actor at least has to be able to reach the
object to perform this kind of manipulation.
In the past, AGAIN didn't work properly when used to repeat a FOLLOW
command involving an NPC using the "guided tour" mechanism (the
TourGuide and GuidedTourState classes). This was due to a subtle
turn-timer problem with AGAIN that affected commands like FOLLOW that
use nested or replacement actions in the course of their execution.
This problem was observed in the FOLLOW/AGAIN situation, but could
also have affected other commands. The root problem has been
corrected.
Released 3/8/2007
The Follow action has always adjusted the scope list to include any
NPCs that the target actor remembers having seen leave, whether or not
they're still within sight. This allows you to FOLLOW a character
who's no longer present - the whole point of FOLLOW would be defeated
if this weren't possible, after all. However, in the past, this scope
list expansion created duplicate entries in the scope list if an actor
with a "follow memory" was also still physically present - the actor
was added to the scope list once for ordinary visibility, then again
for the "follow memory." This duplication caused problems in some
situations, since other parts of the library expect the scope list to
contain only unique entries. The Follow action now ensures that the
list remains unique after its additions.
Released 2/9/2007
There are no library changes in TADS 3.0.14.
Released 1/19/2007
The adv3 English library has a new option setting for the parser's
truncation length. The truncation length has always been customizable
in the low-level system objects that the parser uses to look up and
match vocabulary words, but the library didn't formerly provide a good
way for games to override
its truncation length setting. In
effect, the truncation length was hard-coded into the English library,
and the only way for authors to change it was to edit en_us.t; most
authors would rather not do that because of the hassle of merging
their own changes into future library updates.
The new setting is in gameMain.parserTruncLength. The default
setting is 6 characters - this is the same as the English parser's old
hard-wired setting, so existing code will behave the same as it did in
the past.
As part of this change, the new method
languageGlobals.setStringComparator() lets you change the active
string comparator dynamically. You can use this method if you want to
change the truncation length, or other comprator settings, on the fly
during the game.
The English library now allows a TAction VerbRule to specify a custom
preposition for a default object announcement, overriding the
preposition coded in the verbPhrase. This is useful for a few verbs
where the preposition tends to vary according to the direct object.
For example, SIT might use ON in some cases but IN for other cases:
"on the stool" vs "in the car."
To accomplish this, TAction.announceDefaultObject() first parses
the verbPhrase to get the standard preposition, then calls a new
method on self, adjustDefaultObjectPrep(prep, obj). 'prep' is the
default preposition from the verbPhrase, and 'obj' is the direct
object of the command. The routine returns the new preposition
string. The default implementation simply returns 'prep', but
the StandOn, SitOn, and LieOn actions override this to return the
direct object's actorInPrep.
InitiateTopics can now be located within ActorState objects. In the
past, InitiateTopics were only found when they were directly within an
Actor or a TopicGroup within an actor. The library now looks for
InitiateTopics in the current actor state object as well, which is
more consistent with other topic types.
inputManager.pauseForMore() now re-activates the transcript if it was
active before the pause. In the past, this function simply flushed
output and left the transcript deactivated. This was problematic for
certain library subsystems that depend upon the transcript being
active throughout a command. Now, the function flushes the
transcript's pending output up to the pause, then reactivates the
transcript after the pause, so that transcript capture proceeds as
normal throughout the rest of the command.
In the past, the library generally assumed that NestedRoom objects
would only be located inside either Rooms or other NestedRooms, but
never inside ordinary Things. However, in practice it's often useful
to be able to put a nested room inside something that's not itself
a nested room.
A number of small library changes should now make NestedRooms
work properly when located inside ordinary Thing objects. The
changes are mostly internal and should have no impact on existing
code, but for reference, here's what's changed:
- The methods getRoomPartLocation(), getLocTraveler(),
atmosphereList(), roomDaemon(), getDropDestination(), and
effectiveFollowLocation() have been moved to Thing, with essentially
no changes. These are the methods the library calls on containers of
nested rooms, so moving them to Thing allows any game object to
contain a nested room.
- The definitions of most of the above methods have been removed
from BasicLocation, since BasicLocation now simply inherits the Thing
definitions. The definitions of roomDaemon() and atmosphereList()
have been moved to Room instead, since those definitions were intended
for top-level rooms.
- Room now defines checkActorOutOfNested() to find and defer to
the child object containing the actor. This ensures that the check
is forwarded to the nested room containing the actor.
In addition to the changes above, NestedRoom no longer assumes that
its immediate location is the destination when an actor within the
nested room leaves the nested room. Instead, NestedRoom now
consistently uses the value given by exitDestination. Further,
NestedRoom.exitDestination itself no longer uses the immediate
container as its default setting, but instead uses the default staging
location as given by the defaultStagingLocation method.
A change in 3.0.10 introduced a parsing problem with distinguishing
nouns from plurals when the words were right at the parser's
truncation length. To be more precise, if you defined a noun that was
exactly the truncation length,
and you also defined a plural
for that noun as the noun plus "s" (or, actually, as the noun plus any
other letters), then the parser would incorrectly interpret the noun
word in player input as though it were plural.
For example, the default truncation length is 6 characters, so this
problem occurred with "button" and "buttons" defined as a noun and
plural, respectively. In this case, if you had two or more objects in
scope with vocabulary like 'red button*buttons', and the player typed
something like "x button", the parser incorrectly treated the "button"
in the player's input as though it were plural, so it applied the
Examine command iteratively to all of the buttons in scope, rather
than asking the player which singular button they intended to examine.
This problem has now been corrected.
The message definition for cannotSetToMsg was missing from the English
message list (in en_us/msg_neu.t). The message is now there.
A command of the form PUSH pushable INTO object, where
the second object was some random, non-enterable thing, yielded an
unformatted message ("{that/he dobj} {is} not something you can
enter"). The underlying problem was that the action remapping invoked
by PushTravelViaIobjAction didn't properly set up the object
parameters for the remapped verb, leaving the message system unable to
find the direct object. This has been corrected.
The Attachable class had a problem that caused a run-time error when
the player entered a command of the form DETACH
object, where
object was an Attachable and the command had no indirect
object. The problem came from a name conflict between an Attachable
method called cannotDetachMsg(obj), which took one parameter, and a
library messages property of the same name, which takes no parameters.
When the message resolution system tried to retrieve the library
message from the direct object, it invoked the zero-parameter version
of the property, which caused the run-time error due to the parameter
mismatch with the one-parameter version implemented in Attachable.
To fix this problem, the method in Attachable has been renamed to
cannotDetachMsgFor(obj). The library message has the same name as
before.
Any existing code that overrode the Attachable method will have to
adjust for the name change. That said, it seems almost impossible for
this change to affect existing code, since the very bug that we're
talking about here would have prevented the override from working
properly in the first place.
A library bug caused a run-time error ("stack overflow") on commands
like REMOVE ME or REMOVE
nested room containing me. This has
been corrected.
(The specific problem is as follows. The library assumes that a
REMOVE X command will result in the actor taking X, and so as a sanity
check calculates how much weight the actor would be holding if that
were allowed. When X is the actor or contains the actor, the
hypothetical weight check created a circular containment situation;
the stack overflow came from the library's attempt to recursively
visit all of the hypothetical contents of the actor, which is of
course an infinite loop in a circular containment situation. To avoid
this problem, the library now ignores hypotheticals that would create
circular containment. It's safe to ignore these hypothetical tests
because commands that perform them should always be disallowed anyway,
since actual circular containment is never allowed.)
A library bug caused a run-time error ("nil object reference") on
entering a command of the form
object,
unknown word,
where
object was any in-scope non-Actor object. This has
been corrected.
(The problem came about because the parser attempted to treat such
a command as though it were directed to an NPC. This is some special
handling that applies when the command has a syntax error; the point
is to let the author customize the parsing error messages for orders
given to particular actors. However, when object isn't an
actor, this is problematic, because the library assumes that it can
call certain Actor methods on the object in question. The fix is that
the library will only apply this handling in cases where the object is
actually an Actor; in other cases, it won't assume that the command
was intended as an order to an NPC, so it will simply use the default
parsing error messages.)
TADS 3.0 General Release version - Released 9/15/2006
The library didn't properly handle situations where an NPC order
involved multiple objects and a failed implied sub-action. If an
implied action failed for one of the objects involved, the implied
action was assumed to have failed for all
subsequent
objects in the multiple object list as well, even if the subsequent
implied actions actually succeeded. This resulted in self-contradictory
transcripts, where an implied action was reported as successful, but
then was followed by a message that the implied action had failed:
Bob takes the coin.
Bob must be holding the coin first.
The problem was that the implied action mechanism was incorrectly
considering the failure status for the entire top-level command when
determining if the subsequent implied actions failed. Instead, it
should have been checking the status of the implied actions
themselves. It now does this by looking for a failure report within
the implied action's reports, rather than looking for a failure
anywhere within the entire transcript.
SpaceOverlay.getWeight() now omits the object's "contents" from the
weight calculation if the contents are to be abandoned when the object
is moved. If the contents are to be abandoned, it means that they're
not actually attached to or contained within the space overlay, but
are simply colocated with it; they thus have no contribution to the
overlay's total weight. If the contents are not to be
abandoned on moving the overlay object, they're effectively attached
to it, so they do contribute to its weight as normal.
CaptureFilter is now a subclass of OutputFilter (it was formerly just
an 'object'); and SwitchableCaptureFilter is a subclass of
CaptureFilter (it also was just an 'object'). This should make no
difference functionally, as an output filter is only required to
implement the filterText() method, but is desirable anyway in that it
makes an ofKind(OutputFilter) test recognize these object types as
output filter subclasses.
Released 9/8/2006
Very slight compatibility risk: NameAsOther (and thus
NameAsParent) no longer maps the "in" names to its target object. The
"in" names are the names generated when an object is described as
contained within the NameAsOther. In the past, these were mapped to
the target object along with all of the ordinary names for the object.
However, this was the wrong behavior for ComplexComponent, which
inherits from NameAsOther, because the containment relationship
between a ComplexContainer and its contents is defined by the
container subclass mixed with ComplexComponent in the object's
superclass list,
not by the target object, which in this case
is the ComplexContainer of which the ComplexComponent is a part.
Although it's conceivable that some other applications of
NameAsOther would actually want the old behavior, it seems highly
unlikely, so we don't expect any practical compatibility list.
However, we have provided a new mix-in class, ChildNameAsOther,
that adds mappings for all of the "in" names to the target object.
So, if you have a NameAsOther that depends upon the old "in" name
mapping, just add ChildNameAsOther to the object's superclass list
(right after NameAsOther or NameAsParent), and you'll get the same
behavior as before.
In 3.0.10, the ImpByeTopic was differentiated into a couple of
subclasses to allow handling implicit goodbyes differently when
desired (see
below). However,
this change didn't handle one type of goodbye correctly, namely the
NPC-initiated goodbye, via npc.endConversation(). The change
incorrectly made it so that those goodbyes were treated the same as
explicit
player-initiated goodbyes.
Prior to the 3.0.10 change, NPC-initiated goodbyes were subsumed
into the undifferentiated "implicit goodbye" category, and they
should clearly remain in that category; they simply need to be
differentiated like the other implicit goodbyes were in the 3.0.10
change.
To this end, the new topic class ActorByeTopic has been added; this
is analogous to BoredByeTopic and LeaveByeTopic, and is used when the
NPC terminated the conversation via npc.endConversation(). In the
absence of an active ActorByeTopic, the active ImpByeTopic will be
used instead. This restores compatibility with pre-3.0.10 code (where
there was no differentiation among "implied goodbye" types, and
npc.endConversation() events were handled as implied goodbyes), while
providing a specific topic type to handle this one type of goodbyes.
The English library's "instructions" module (instruct.t) now uses HTML
markups to display typographical quotes and apostrophes ("curly
quotes") throughout the text of the standard instructions. Thanks to
Greg Boettcher for making this improvement.
The detailed-naming scheme for object announcements (see
below) introduced in 3.0.10 has
been made optional, and disabled by default. Testing reveals that the
mechanism as currently designed is too twitchy for some people's
taste, so we've disabled it by default for the time being; however,
the code is all intact, for those who want to use it as-is or tweak it
for their needs. To enable the detailed announcement naming, set
gameMain.useDistinguishersInAnnouncements to true.
The BannerWindow system had a flaw in the way it re-initialized
banners after a RESTART. The problem showed up in cases where there
were dependency orderings among the windows, so that one window's
initBannerWindow() had to call another's initBannerWindow() in order
to create the windows in the correct order. The problem didn't always
happen even in cases of ordering dependencies, since it also depended
on the arbitrary ordering of the VM's internal object lists. When it
happened, the problem manifested itself by creating extra copies of an
affected banner window at each RESTART. This has now been corrected.
In banner.t, the formerly anonymous InitObject that handles banner
initialization (and post-RESTART re-initialization) now has a name,
bannerInit. This is so that games can use "modify" and "replace" with
the object, and also so they can refer to it from the execBeforeMe
properties of other InitObjects, for initialization dependency
ordering purposes.
"Follow mode" for NPCs ("Bob, follow me") didn't work correctly when
the actor being followed moved between two top-level rooms connected
by a sight sense connector. This has been corrected. (The problem
was that the code that carried out the "follow" attempted to move the
follower using a "local travel" action - something like STAND ON STAGE
or ENTER BOOTH - any time the target actor was still in sight. The
follower now attempts local travel only if the target is still in
sight and the target is within the same top-level room;
otherwise, the follow uses a suitable full-fledged travel action.)
In Actor.actorActionFollow(), if the actor is already in "follow" mode
for the requested other actor, a message is now displayed to this
effect (alreadyFollowModeMsg: "I'm already following Bob"). This
won't be a factor by default, since the library automatically cancels
"follow" mode any time a new command is issued to an actor
before attempting to enact the new command. However, games
might want to override this auto-cancel behavior, in which case they
might encounter this situation in actorActionFollow(). In the past,
the routine did nothing at all - it didn't even show a message, so the
generally undesirable "Nothing happens" was displayed by default.
In the past, an actor that started in an InConversationState triggered
a run-time error at start-up, due to an initialization order problem
in the library. This has been corrected. (In particular, an actor's
boredAgendaItem property is now initialized via a perInstance()
definition rather than in initializeActor(). This ensures that the
property is initialized before it's needed. In the past, the order of
initializations sometimes resulted in the library trying to use the
actor's BoredAgendaItem object before it was initialized, leading to a
"nil object reference" error.)
Room's condition for remapping the GetOutOf action to the Out action
has changed slightly. In the past, this remapping was performed only
if the 'out' direction had an apparent destination, which is
only the case when the actor attempting the travel already knows the
destination (such as from past experience or at-hand information).
This condition wasn't quite right, though. Instead, the condition
should have been simply that the 'out' direction has an apparent
connector - that is, there's a visible way to travel in the
'out' direction. Room has been changed to use the new condition.
The template (in en_us/en_us.h) for DeadEndConnector now makes the
apprentDestName entry optional. This allows using the template to
define a dead-end connector that merely displays a message when
traversed, without giving it a name.
In the English library, PathPassage now limits its remapping of the
Take action to the TravelVia action to cases where the entered verb
phrase was literally "take." This prevents other phrasings, such as
"pick up path" or "get path," from being interpreted as attempts to
travel along a path.
The message sayTravelingRemotely in the English library has been
corrected to add the word "to" before the destination name.
Released 8/17/2006
Incompatibility warning: (This note concerns a change that
was made in 3.0.9 but inadvertantly omitted from the 3.0.9 release
notes. We're mentioning it now in case anyone was affected by it
and needs help adjusting their code for the change.)
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.
Minor incompatibility warning: The Lockable class now has
initiallyLocked set to true. This means that
all Lockable
objects now start out locked by default (i.e., unless your game code
specifically overrides initiallyLocked to set it to nil for a given
Lockable). In the past, Lockable didn't define initiallyLocked at all
(so it defaulted to nil), but Door and IndirectLockable defined it as
true - so some Lockables formerly started out locked by default while
others were unlocked by default. This change should be generally
beneficial, since (1) it simplifies matters by making the initial lock
status consistent across all Lockables, and (2) the vast majority of
Lockables start out locked anyway, so "locked" is the better default.
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.
Minor incompatibility warning: The method Action.callVerifyPrecond has
been renamed to callVerifyPreCond - that is, the 'c' in 'precond' is
now capitalized. This change is for better naming consistency, since
all of the other symbol names in the library that include the
substring "precond" capitalize the C. This routine is intended mostly
for internal use within the library, so it should affect little or no
existing game code.
Minor incompatibility warning: The NOTE command has been removed, and
replaced by a new comment syntax that lets the player enter a comment
by starting a command line with a punctuation-mark prefix.
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.
Minor incompatibility warning: Each actor now has its own separate
lookup table of ConvNode names. This means that you don't have to
worry about making ConvNode names unique globally - you only have to
make sure that names are unique within a single actor's set of nodes.
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.
Minor incompatibility warning: Thing.getExtraScopeItems() is now
required to return a list - nil is no longer a valid return value
for this method. Use an empty list instead of nil to indicate that
there's nothing to add to the current scope. The old nil return
code was inconsistent and unnecessary, as an empty list always
functionally meant the same thing anyway.
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 [];").
Minor incompatibility warning: Thing.getListedContentsInExamine() has
been renamed to getContentsForExamine(), and its operation changed
slightly. The old name was a bit misleading, in that the method
actually needs to return all of the visible contents of the object,
whether or not they're marked as listable. Unlistable contents need
to be included in this list so that their listable contents can be
included recursively. This is the operational change: in the past,
only listable contents were included in the returned list; now, all
visible contents are included.
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.
Minor incompatibility warning: the "equivalent object" mechanism has
changed substantially, although existing game code shouldn't be
affected unless it was modifying the equivalence mechanism itself.
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.)
In the past, when EXAMINE was applied to a Room, the Room simply
turned the command into a nested LOOK AROUND command. This has
changed slightly. Now, the Room directly does the work that LOOK
AROUND would do, without the nested command, and with the difference
that the initial line with the room name in boldface isn't included in
the output. (This is accomplished by omitting the LookRoomName flag
from the 'verbose' flags for the Actor.lookAround() call.)
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.
An object can now be associated with more than one CollectiveGroup.
This allows an object to be a member of multiple disjoint groups.
For example, you could create a group for "treasure," and a group
for "jewelry," and put some objects in both groups, while leaving
others in one group or the other.
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.
Several new classes bring functionality parallel to
RestrictedContainer to surfaces, undersides, and rear containers and
surfaces. The new classes are RestrictedSurface, RestrictedUnderside,
RestrictedRearContainer, and RestrictedRearSurface. These new classes
(along with RestrictedContainer itself) are all based on the new
mix-in class RestrictedHolder, which defines generic containment
restriction rules that can be applied to any container subtype.
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).
In the past, the default Search action handling was the same as for
LookIn. Now, the Search handling is slightly different for Container
and Openable.
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.
In the past, the Thing handlers for PUT UNDER and PUT BEHIND included
touchObj preconditions on the direct object instead of objHeld
preconditions. These have been changed to objHeld conditions; since
these verbs generally require physically moving the direct object, the
correct precondition is that the direct object be held, just as for
the other PUT verbs.
A new Action method, getEnteredVerbPhrase(), returns the action's
original verb phrase text as typed by the player. The return value is
a string giving the verb phrase in a canonical format: specifically,
the result is all in lower-case, and each noun phrase in the player's
input is replaced by a placeholder token: '(dobj)' for the direct
object, '(iobj)' for the indirect object, '(topic)' for a topic
phrase, and '(literal)' for a literal text phrase. Only the verb
phrase is included in the result - there's no target actor phrase, and
no sentence-ending punctuation included. The noun phrase replacements
apply to the
entire noun phrases, so the single placeholder
'(dobj)' could represent an entire list of direct objects. For
example, if the player types "BOB, PUT THE BOOK AND PENCIL IN THE BOX
AND GIVE IT TO ME", calling getEnteredVerbPhrase() on the PutIn action
would yield 'put (dobj) in (iobj)', and calling the method on the
GiveTo would yield 'give (dobj) to (iobj)'.
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.
The new Action method getPredicate() returns the verb phrase match
tree object that the parser resolved to the Action. The English
library uses Action objects as predicate match tree objects, so in the
English version this simply yields the original Action. However,
non-English libraries can use separate objects for verb phrase match
tree objects, so if you need any information from the verb phrase's
grammar tree (for example, if you need to call getOrigText() or
getOrigTokenList()), you should use action.getPredicate() rather than
using the action object directly. (This does not apply to the
new method getEnteredVerbPhrase() above - that method is explicitly
defined on the Action, not the grammar object. getEnteredVerbPhrase()
itself calls getPredicate() to get the grammar information, so you can
simply call getEnteredVerbPhrase() directly on the Action object.)
The library looks to a new global variable, gLibMessages, to determine
which object to use as the "library message" source. (This is the
object used for messages that don't logically relate to the current
actor; generally, these messages are replies to meta-commands, or text
fragments used to construct descriptions.)
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 the past, the Floor class replied to THROW object AT FLOOR
with the same default message ("you should just put it down instead")
in all cases. This reply is no longer used in cases where the Floor
is unreachable; instead, the standard THROW handling applies, since
the player couldn't accomplish the same thing with DROP.
RoomPart.getHitFallDestination() will now take into account any
explicit 'location' setting in the RoomPart when determining the
destination. In the past, an explicit 'location' setting was ignored,
and a valid destination could only be found if the RoomPart was in the
same top-level room as the actor. Now, if the RoomPart has an
explicit non-nil location setting, that takes precedence in
determining the destination. This is useful when creating connected
top-level locations with room parts that you want capable of serving
as THROW targets.
When throwing an object at a target, and the target happens also to be
the place where the object is described as landing when thrown at that
target (i.e., the target is the nominal drop destination of its own
the "hit-fall" destination), a new default message is used, of the
form "The projectile lands on the target." In the past, the message
was the awkward "The projectile hits the target without any obvious
effect, and falls to the target." This was most likely to occur when
the target was something like a floor or ground object, since that's
the only case where the target is likely to also be the nominal drop
destination. This change is in the routine
DropTypeThrow.standardReport().
GIVE TO, SHOW TO, and THROW TO now use different messages depending on
whether the indirect object is an actor or an inanimate object. For
ordinary Things, the message is now of the form "You can't give
anything to X"; for Actors, the message is "The X does not appear
interested." In the past, the latter message was used for all
objects, animate or not. Imputing "interest" to an inanimate object
could be read as either intentionally snide (as though the library
were sarcastically calling the player dense for trying such an
obviously stupid thing) or simply wrong (as though the library didn't
know the difference); since msg_neu is supposed to affect a neutral
narrative tone, neither alternative is desirable.
In addition, showing or giving an Actor to itself is now handled
with a separate message ("Showing X to itself would accomplishing
nothing").
REMOVE dobj now uses a separate reply if the object is already
being held: "There's nothing to remove the X from." In the past, the
command was unconditionally turned into TAKE dobj, which showed
"(from you)" as the implied indirect object part, which was a bit
awkward.
When the THROW direction command is used with a nonportable
object (as in THROW DESK EAST), the result is now the same as for
commands like MOVE and PUSH: "The desk cannot be moved."
The default response for THROW DOWN has been changed to use the same
message as for DROP when applied to an object that's not currently
being held: "You're not carrying that."
In statusLine.beginStatusLine(), in the StatusModeApi case, there was
some leftover old code ("<body bgcolor=statusbg text=statustext>")
right after the call to setColorScheme() that effectively undid the
color settings made in setColorScheme(). The old code has been deleted.
The library now uses the same default message in response to a command
to throw the player character in a direction (as in THROW ME EAST) as
it does as for THROW ME AT something.
A new Thing method, adjustThrowDestination(), gives a prospective
landing site for a thrown object a chance to redirect the landing to
another object. getHitFallDestination() now treats the result it
calculates using getDropDestination() as tentative, and calls this new
method on the tentative result object to determine the actual
destination. The default implementation of the new method in Thing
simply returns 'self', which confirms the tentative destination as the
actual destination. BulkLimiter overrides the new method to redirect
the landing site to the BulkLimiter's location's drop destination if
the thrown object would overflow the BulkLimiter's capacity.
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.
In ActorState, the beforeTravel() method no longer ends the current
conversation if the actor is merely moving between nested locations
within the same top-level location. These aren't usually true
departures that should trigger conversation termination, so the method
no longer treats them as such.
A new travel connector class, DeadEndConnector, can be used for
situations where travel through the connector is impossible, but for
reasons that can only be learned by attempting to go through the
connector. For example, this can be used for a passage that turns out
to be blocked by a cave-in that isn't visible from the passage
entrance, or in a situation where you describe the actor as wandering
around in a direction for a while but giving up and returning to the
point of origin.
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.
"Push travel" has been changed slightly to handle situations where the
object being pushed has side effects that affect the description of
the destination location. The most common situation where this arises
is when the object being pushed is (or contains) a light source, but
it could happen in many other custom-coded situations.
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 English library, the Push-Travel commands (PUSH X NORTH, PUSH X
UP RAMP, etc) now only accept single direct objects. In the past, the
grammar phrasings allowed direct object lists, but actually pushing
multiple objects into a new room is a practical impossibility, so
there's no point in accepting it in the grammar.
The mechanism for describing "local" NPC travel has been enhanced.
Local NPC travel is travel where an NPC moves from one top-level
location to another, and both top-level locations are in view of the
PC. In these cases, special handling is needed because the NPC isn't
truly arriving or departing in the usual sense; the NPC is instead
moving closer to, further away from, or laterally with respect to, the
PC.
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:
- If the PC is moving closer to the PC, the local
arrival message is displayed at the destination end of the travel.
"Closer" means that the NPC is moving from a top-level location that
doesn't contain the PC to a top-level location that does
contain the PC.
- If the PC is moving further away from the PC, the local
departure message is displayed at the origin end of the travel.
"Further away" means that the NPC is moving from the top-level
location that contains the PC to a different top-level location that
doesn't.
- If the PC is moving laterally, the remote travel
message is displayed at the destination end of the travel.
"Laterally" means that the NPC is moving between two top-level
locations, neither of which contain the PC. Note that there's no good
basis for preferring to show the message on the origin or destination
side of the travel in this case, since the generic situation is so
symmetrical, but at the same time it's necessary to choose one or the
other because we don't want two messages for this kind of travel. So,
we have to make an arbitrary choice, and the library chooses to show
the message on the destination side.
These new rules obviously require some new methods. Here's the
new arrangement of methods:
- The local arrival message is embodied in
TravelConnector.describeLocalArrival(), which by default calls
Traveler.sayArrivingLocally(), which by default calls
libMessages.sayArrivingLocally(). Note that these aren't new - this
part of the mechanism carries forward unchanged from past versions.
However, their usage has changed, in that the library in the
past called the local arrival messages in all cases of local
travel, but now calls it only in certain cases, as described
above, and calls the local-departure or remote-travel methods in other
cases that formerly all folded into the local-arrival case.
- The local departure message is embodied in
TravelConnector.describeLocalDeparture(), which by default calls
Traveler.sayDepartingLocally(), which by default calls
libMessages.sayDepartingLocally(). These methods are new.
- The remote travel message is embodied in
TravelConnector.describeRemoteTravel(), which by default calls
Traveler.sayTravelingRemotely(), which by default calls
libMessages.sayTravelingRemotely(). These methods are new.
Existing code should continue to work correctly with the new
framework, although with one caveat: if you customized a
describeLocalArrival() or sayArrivingLocally() method, you might want
to add corresponding customizations for the new local-departure and/or
remote-travel methods. If you don't, your local-arrival customization
will still work in the cases where it applies under the new rules, but
it won't be invoked in all the cases it was in the past.
AccompanyingInTravelState now describes local travel using its
standard departure message. This ensures that the correct messages
are displayed when accompanying an actor in local travel or travel
between two connected top-level locations (such as locations linked by
distance).
TravelAction.actionOfKind(cls) now properly handles the case where
'cls' is the base TravelAction class. (In the past, asking about
TravelAction itself caused a run-time error.)
The various SettingsItem objects defined in the library are now all
named objects. In the past, some of these (such as
gameMain.verboseMode) were defined as embedded objects; this made it
more difficult to modify their behavior, since it wasn't possible to
use 'modify' to alter the objects directly. The behavior should be
exactly the same as in the past; the only difference is that the
objects are now easier to customize.
The putDestMessage that was defined in defaultFloor and defaultGround
has been moved to the base Floor class instead - this message should
be common to most floor/ground type objects, not just for the default
ones in the library.
The method standing.tryMakingPosture(loc) now generates the command
STAND ON loc, rather than simply STAND as it did in the past.
This makes the behavior consistent with the sitting and lying postures.
Room can now be used as the direct object of STAND ON, SIT ON, and LIE
ON. These are handled simply by remapping the commands to the room's
"floor" object (specifically, the object returned from the room's
roomFloor method). This is in part to accommodate the change above to
standing.tryMakingPosture(), and in part to provide better automatic
handling for player commands like SIT IN YARD.
RoomPart now applies some additional disambiguation filtering in cases
where two or more top-level locations are linked by a sense connector.
When a resolve list contains multiple RoomPart objects, and some of
those RoomParts are local and some remote, RoomPart will reduce the
list to include only the local RoomParts. (A "local" object is one
within the same top-level location as the actor; a "remote" object is
one that's in a separate top-level location that's linked by a sense
connector.)
If a RoomPart has an explicit 'location' setting, that location will
no longer add the RoomPart redundantly to the location's 'contents'
list. In the past, the RoomPart would show up twice in the contents
list, because it was added once by virtue of being in the RoomParts
list, and again by virtue of its 'location' setting.
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.
RoomPart and TravelConnectorLink now have a default sightSize of
'large'. This means that it's possible to examine these objects at a
distance. Room parts are things like walls and ceilings that tend to
be large and to contain large-scale details that would be
realistically discernible at a distance - the details are typically
things like doors and windows. Similarly, TravelConnectorLink (which
you'd mainly use via its subclasses Enterable and Exitable) is for
things like building exteriors, which likewise tend to have
large-scale details.
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.
RoomPartItem now overrides useSpecialDescInRoom() and
useSpecialDescInContents() to return nil, and no longer overrides
useSpecialDesc and useInitSpecialDesc.
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.
Thing.isListedInContents now calls useSpecialDescInContents(location)
to make its determination. In the past, it called useSpecialDesc
directly; this was incorrect because it didn't properly take into
account the differentiation among contexts that the various
useSpecialDescInXxx methods provide.
The exit-list generator (exitLister.showExitsWithLister) now considers
two destination locations to be equivalent based on the destination
room object rather than the destination room name. This means that
two distinct exit locations will now be listed separately even if they
have the same name. For example, in the past, if the east and west
exits led to separate rooms that both happened to be named "the
hallway," the listing formerly read "east (or west), to the hallway",
but will now read "east, to the hallway; west, to the hallway". The
old approach of merging list entries based on name alone produced odd
results in some cases, and didn't have any obvious benefits; the new
approach should produce more predictable results.
TIAction has a new method, retryWithAmbiguousIobj(), that lets an
action handler specifically ask for disambiguation from a list of
possibilities. This is the TIAction equivalent of the TAction method
retryWithAmbiguousDobj(), and works the same way; the only difference
is that the disambiguation list applies to the indirect object rather
than to the direct object.
The TAction and TIAction methods retryWithAmbiguousDobj() and
retryWithAmbiguousIobj() can now be called before the "iteration"
phase of the command execution, specifically during
Action.beforeActionMain(), and they'll work properly: they'll ask
their disambiguation question, then apply it to the entire iteration
for the other object list. For example, if you call
retryWithAmbiguousIobj() during beforeActionMain() to prompt for a new
indirect object for a PUT IN command, the player's response will
automatically be applied to the whole direct object list if the player
specified multiple direct objects. In the past, it wasn't possible to
call these "retry" methods during beforeActionMain(), so if they were
used during commands with multiple objects in the other object slot,
the result was that the "retry" - and its question to the user - was
repeated for each object in the list.
(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.)
In the past, initializeThing() was sometimes called multiple times in
the course of dynamically creating an object (with 'new'). This
happened when creating objects based on classes that inherited from
Thing more than once (classes like this include Flashlight, Passage,
Room, Chair, Bed, Platform, NominalPlatform, Booth, and
UntakeableActor). This happened because the default constructor for
any object based on multiple base classes simply inherits each of the
base class constructors, one after the other; when more than one base
class itself inherits from Thing, this results in multiple inherited
invocations of the Thing constructor, which in the past resulted in
multiple invocations of initializeThing() for the new object.
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.
Room has a few command-handling enhancements, for rooms that have
associated vocabulary. LOOK IN room is now treated the same as
EXAMINE room; LOOK UNDER and LOOK BEHIND are refused ("You
can't look under that"); SMELL and LISTEN TO room are now
equivalent to simply SMELL and LISTEN; BOARD and ENTER are treated as
redundant ("You're already in that"); and GET OUT OF is a little
smarter about remapping, so that it only remaps to GO OUT if there
actually is an OUT direction defined for the room, and fails with an
error if not ("You'll have to say which way to go").
NestedRoom.makeStandingUp() now leaves the actor's posture unchanged
if the travel to the new location fails. It does this by noting the
original posture before the travel attempt, and checking after the
travel attempt to see if the actor is still in the starting nested
room; if so, the routine restores the saved posture. This ensures
that a failed travel attempt won't cause a posture change in the
original nested location.
NominalPlatform now overrides hideFromDefault() to always return true.
Nominal platforms are meant to be used as internal objects and not to
appear as simulation objects, so it's generally not desirable for them
to be used as defaults.
AccompanyingState had a problem that caused infinite recursion
(leading to a stack overflow) when an actor in an AccompanyingState
was explicitly moved via scriptedTravelTo(). The problem was simply
that the actor attempted to accompany itself on its own travel, and
that accompanying travel triggered a further accompanying travel, and
so on. This has been corrected: actors in accompanying travel states
now explicitly ignore their own travel for the purposes of
accompanying travel.
The library uses a separate default message in cases where an Actor is
holding an object used in ENTER, BOARD, SIT ON, etc. In the past, the
message was the rather awkward "You can't do that while the chair in
in Bob." The new message is phrased "...while Bob is holding the
chair" instead. The default message for non-Actors is unchanged.
FOLLOW caused a run-time error if the follower was holding the
NPC to be followed. This was because effectiveFollowLocation
wasn't defined for ordinary objects. This has been corrected
by defining Thing.effectiveFollowLocation to return the location's
effectiveFollowLocation, or just 'self' if the object has no
location.
There are two new subclasses of ImpByeTopic, to allow you to
differentiate between the two implicit ways of ending a conversation.
BoredByeTopic handles cases where the NPC ends the conversation
because of "boredom" (that is, inactivity in the conversation that
exceeds the NPC's attentionSpan setting), while LeaveByeTopic handles
cases where the PC simply walks away from the NPC in mid-conversation.
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.
The "boredom" mechanism in InConversationState has been changed
slightly. In the past, it was implemented directly in the takeTurn()
method of InConversationState. Now, it's handled with an AgendaItem
instead - specifically, a new subclass called BoredomAgendaItem, which
the Actor and InConversationState classes manage automatically. This
change should have no effect on existing code, since the new code
implements the same behavior as the old version. The benefit of this
change is that it makes it easier for a game to customize the boredom
behavior, since it's now part of the more general "agenda" mechanism
instead of being buried in ad hoc code inside the conversation state
class.
The suggested topic lister had a problem that showed up when several
topic suggestions were present with the same listing name, and some of
the topics were inactive while others were active. The lister
automatically removes redundant entries from suggestion lists by
removing multiple items with the same name, but in the past, the
lister sometimes incorrectly removed the active elements
instead of the inactive ones, effectively eliminating the suggestion
entirely from the list. This has been corrected: the lister now only
removes a redundant suggestion if there's another active
suggestion with the same listing name.
InitiateTopic no longer sets any pronoun antecedents when triggered.
(It did set its match object as a pronoun antecedent in the past.
This was undesirable because an InitiateTopic is triggered by internal
calculations in the game, not by anything the player has done.)
ConvAgendaItem now checks to make sure the player character is present
before declaring itself ready. This ensures that an NPC won't attempt
a conversational agenda item unless the PC is actually present to hear
what the NPC has to say. (This change is in ConvAgendaItem.isReady.)
Actor.executeAgenda() formerly marked an agenda item as done if
invoking the item threw any sort of exception. Now, this is only done
on a run-time error (a RuntimeError exception); in cases of other
exceptions, the item's doneness isn't changed. The original purpose
of the doneness change was to reduce debugging hassles in cases where
an agenda item encountered an error; in such cases, if the item wasn't
marked as done, the scheduler would end up invoking the item in an
infinite loop, because it would look perpetually ready to run.
However, doing this on all exceptions interfered with certain
non-error cases where exceptions were used to jump out of the action
handling. Limiting the caught exceptions to runtime errors should
retain most of the intended benefits while avoiding the problems with
a more general error catcher here.
Actor.initiateConversation() now handles a nil 'state' argument
slightly differently. In the past, if 'state' was nil, the actor's
state was simply left unchanged. Now, the method will switch the
actor to the state returned from the current state's
getImpliedConvState() method. In most cases, the net effect is
exactly as before - i.e., the actor's state is left unchanged -
because the default getImpliedConvState() simply returns 'self'.
However, ConversationReadyState overrides getImpliedConvState() to
return the associated InConversationState. This change makes it
easier to initiate a conversation when using an actor with
conversational states, since it will generally pick the correct
conversational state automatically.
Note that the ActorState method getImpliedConvState() is new with
this change.
Due to a bug, <.convnode> tags displayed inside npcGreetingMsg
methods weren't handled properly if the greeting was displayed due to
NPC initiation of the conversation (via npcInitiateConversation()).
This has been corrected.
ConvNode.canEndConversation() can now return a special value,
blockEndConv, that indicates that the actor said something to force
the conversation to keep going. You should always use this if you
display a message in the routine and you want to prevent the
conversation from ending. Returning blockEndConv is almost the
same as returning nil from the method, but has the additional side
effect that the caller will call noteConvAction() on the other actor,
to prevent this actor from generating any further scripted remarks on
the same turn.
When the library displays a parser error message for a command
directed to an NPC, it now shows the error message in a neutral sense
context. This ensures that the message is displayed even if the NPC
is in a remote location (this could be the case if we're talking over
the phone, for example). In the past, the parser message was
generated in the NPC's sense context, so the NPC was in a remote
location that wasn't in scope to the PC, the parser message was
suppressed, resulting in a "Nothing happens" message or the like.
When a command of the form "actor, xxx" is entered, and the portion
after the "actor" phrase is unparseable, the parser now attempts to
parse at least the "actor" part to determine if the command is being
directed to an NPC. In the past, the parser didn't do this; the actor
phrase is part of the basic sentence grammar, so if the parser failed
to match the rest of the basic sentence grammar, it didn't bother
trying to figure out if a target actor was included. To find the
target actor, the parser now makes a second attempt at parsing a
command to see if it matches a very basic sentence syntax that only
includes the target actor specification; if it can match the syntax,
and resolve the noun phrase to an in-scope actor, the parser takes
the result to be the target actor.
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.
The library now gives a replacement action zero time if it's replacing
an action that itself has zero time. This corrects some timing
anomalies (particularly with respect to NPCs) that showed up in
certain unusual cases where replaceAction() was used in the course
of an action that itself was being run as a nested action.
A new parser option lets you cancel remaining commands on the command
line when an action fails.
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.)
The Action class has a new method, checkAction(), that is called just
before the existing execAction() method. TAction and TIAction define
the new method, and perform the calls to the direct and indirect
object 'check' methods in this new method rather than in execAction(),
as was done in the past.
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.
OopsIAction is now defined as an IAction. (In the past, it was based
directly on Action, which was problematic if an OopsIAction grammar
match was used in certain contexts, such as
CommandRanking.sortByRanking.)
ObjectPreCondition now uses the same ordering as the underlying
condition.
During calls to the "verify" methods for remapped actions (i.e.,
actions mapped to different actions using remapTo), the library now
sets gAction to the remapped action. In the past, during the initial
remap testing stage, the library left gAction with the original action
that triggered the remapping. This gives game code more consistent
information during the verify phase.
VocabObject now defines the methods isOwnedBy(obj) and
getNominalOwner(). This allows player input to refer to non-physical
objects, such as topics, using possessive syntax.
The INSTRUCTIONS command is now ignored for the purposes of UNDO and
AGAIN.
The parser no longer chooses Unthings as default objects in cases
where noun phrases are missing in player input. (This is handled via
Unthing.hideFromDefault().)
The new Thing method setGlobalParamName() lets you add a global
message parameter name dynamically. In the past, there was no
straightforward way to do this; you had to manually update the message
builder's internal table of names in order to add a new name. This
method takes care of all of the necessary internal table updates.
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.
The parser now marks a plural-phrase match for a direct or indirect
object as "unclearly disambiguated" if the phrase also matches a
singular noun for an object that's also in scope. This flag makes
the parser generate an extra message to notify the player that the
parser chose an object from a potentially ambiguous set:
>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.
The event manager (the part of the library that manages fuses and
daemons) now automatically disables a daemon or fuse that throws an
exception out of its main execution method. The reason this is
important is that an infinite loop would otherwise occur in many
cases: if the object remained active, the event manager would
re-invoke it after the exception, and in most cases the re-invocation
would simply encounter the same exception, at which point the event
manager would invoke it once again, and so on ad infinitum.
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.
A new class, OneTimePromptDaemon, makes it easy to set up a prompt
daemon that only fires once. It works like an ordinary prompt daemon,
but removes itself from the active event list as soon as it fires.
This can be handy for cases where you want to defer some non-recurring
processing until just before the next command prompt.
Thanks to Greg Boettcher, the English library's default messages now
use typographical ("curly") quote marks and apostrophes. In cases of
open/close quote marks, these are generated with
<Q>...</Q> markups; in cases of apostrophes, ’
entities are used.
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.
The library now attempts to be as precise as possible when announcing
objects chosen in vague disambiguations, as defaults for missing noun
phrases, and for multiple objects to a verb. In the past, the library
simply used the base name of the object being announced, but this
didn't take into account the various things that the parser can use to
distinguish objects on input, such as lit/unlit state, location, and
owner. The library now generates these object announcements using the
same Distinguisher mechanism it uses to resolve ambiguous noun phrases
in input, ensuring that the generated names are as precise as possible
in distinguishing announced objects from others in scope.
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.
The library now suppresses vague disambiguation announcements for
objects specified with indefinite articles when the announcement
would be redundant. That is, if there's no Distinguisher that can
tell apart any of the possible matches for an object specified with
an indefinite article, the library doesn't bother making the
announcement, because it would add no useful information for the
player. For example, in the past, you might have seen an exchange
like this:
>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.
The English library can now correctly generate a plural name for an
object whose name includes an "of" phrase; for example, the library
now correctly generates the plural for "piece of paper" as "pieces of
paper". In the past, the library pluralized the last word of the
entire phrase, so the result in this example would have been "piece of
papers". The new algorithm when there's an "of" is simply to
pluralize the last word before the first "of", using the same rules as
for a phrase without "of".
In the past, the English parser treated a command of the form GIVE
adjective noun as though it meant GIVE
noun TO
adjective. This was almost never what the player actually
meant; the player almost always really meant GIVE
object TO
the current interlocutor, where
object is named
adjective noun. The parser now applies that more likely
interpretation when confronted with this syntax. The same applies to
SHOW.
(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.)
In the past, the English parser treated the word "of" as a non-weak
token when matching noun phrases of the form "x of y". This meant
that if a noun phrase consisted only of weak tokens and "of", the
parser matched it. This has changed; "of" is now ignored for the
purposes of the weak-token test, so a phrase consisting only of weak
tokens and "of" is now considered weak overall.
A bug in the English library caused run-time errors in some cases
involving TopicAction verbs. The problem was that some of the
TopicAction message generators assumed that they were the current
active verb, which isn't always the case. This has been fixed.
The English library defines the new verb phrasings ASK actor
topic and TELL actor topic; these are phrasings
for the new actions AskVague and TellVague, respectively. These are
defined entirely to provide more helpful error messages in cases where
the player enters an ASK or TELL verb without an ABOUT phrase. The
default handling for the new actions simply displays an explanation of
the proper ASK ABOUT or TELL ABOUT phrasing. Some players have been
seen to misinterpret custom phrasings that show up in SpecialTopic
prompts, such as "ask bob why", as general-purpose command phrasing,
and then attempt to use similar phrasing elsewhere in the game. This
results in "invalid command" errors from the parser, of course, but
the standard error messages were often unhelpful with this particular
kind of phrasing error. The new handlers are meant to improve on the
standard error messages for this common case.
The English library now treats the