Manipulating the Transcript

by Eric Eve

Introduction

Output from a TADS 3 game doesn't generally go straight to the screen. Instead it's buffered in something called the transcript so that it can be manipulated further before it's actually displayed (normally right at the end of the action-processing cycle on each turn). Although this can occasionally complicate things, it allows the various pieces of text output during the execution of a command to be reordered, summarized, or processed in any other way desired before actually being displayed. This allows the standard library to, for example, group implicit action announcements, so that instead of displaying:

>go south
(first unlocking the door)
(first opening the door)

The game displays the rather neater:

>go south
(first unlocking the door, then opening it)

Occasionally the interception of output text by the transcript can seem a bit of a nuisance to the unwary game author, particularly when you want to interrupt the flow of text with a dramatic pause or a special prompt for user input at a particular point, and the transcript ruins the effect. However, there are quite straightforward ways to deal with this (see the article on Some Common Input/Output Issues if you need help with them), and once you learn how to manipulate the transcript, it can be used to good effect. In particular you can use it to reorder reports that otherwise seem to be in the wrong sequence, or, more commonly, to combine multiple reports into one where this would make for a neater effect. For example:

Instead of showing "gold coin: You give the tavern keeper a gold coin" three times over when the player character gives three gold coins to the tavern keeper, you could combine these three reports into "You give the tavern keeper three gold coins."

Instead of having several lines like "red ball: Taken" in response to a TAKE ALL command, you could, if you wished, have one combined report along the lines of "You take the red ball, the blue ball, the small picture of Abraham Lincoln, the chipped tea-cup, and the silver spoon. "

Instead of the rather awkward effect you can get when, say, an actor sitting on a chair leaves the room ("Bob stands up", "Bob follows you out") you could combine the two reports into the neater "Bob stands up, gets off the stage, and follows you out")

This article will explain in a little more detail how the transcript works, and how you can achieve some useful effects with it.

Simple Ordering with reportAfter() and reportBefore()

Before going into further details of how the transcript actually works, it's worth looking at some simple ordering effects that can be achieved very easily without any detailed knowledge of the transcript.

We'll start with a simple example of reportAfter(). Suppose we have a game in which there are objects like fountains, pools, washbasins, sinks and taps (or faucets) that make other objects wet when they come in contact with them. We might cater for this by calling a custom makeWet() method on objects dipped in the fountain or put under a running tap (or faucet). This method should not only change the state of the newly-wetted object, but also announce that it has become wet. But we might want the announcement of becoming wet to follow any other description of the action, so that no matter where in the action sequence book.makeWet() method happens to get called we can be always sure of getting:

>put book in pond
You put the book in the pond. The book becomes rather wet.

And never:

>put book in pond
The book becomes rather wet. You put the book in the pond.

For reportBefore() we'll use a slightly more complex example. Suppose we have an NPC (called Bob) who's following the player character around. Suppose that we also use the occasional TravelMessage to describe the player character's travel, something like:

theatre: Room 'Theatre'
"A large stage occupies the northern end of the theatre. The exit is to
the west. "
west: TravelMessage { -> lobby "You walk out of the theatre. " }
;

The output we'd get from this is less than ideal. Without customizing anything else and just using a plain-vanilla AccompanyingState for Bob we'd get:

>w
Bob comes with you. You walk out of the theatre.

These messages look in the wrong order. They'd look even more wrong if we'd given Bob a custom AccompanyingInTravelState that provided a custom message like "Bob follows you out." instead of "Bob comes with you.".

One quick and dirty fix would be to use reportBefore() on the TravelMessage to makes its message come first:

theatre: Room 'Theatre'
"A large stage occupies the northern end of the theatre. The exit is to
the west. "
west: TravelMessage { -> landing "<<reportBefore('You walk out of the
theatre. ')>>" }
;

We'd then get:

>w
You walk out of the theatre. Bob comes with you.

This is certainly an improvement, and it illustrates the use of reportBefore(), but the implementation is a little messy unless we only have one or two TravelMessages in our game; not least it would be something of an inconvenience to have to wrap every travelDesc in a reportBefore macro like that. It's also a little inflexible. Suppose we had a second NPC in the game called Sally who sometimes acts as a TourGuide. When Sally is leading the player out of the theatre, we'd want her movements to be described first, not those of the player, so using reportBefore on the TravelMessage would then give the wrong result (not a problem if no NPC in your game will ever be a TourGuide, but worth bearing in mind otherwise).

A cleaner solution is to override TravelWithMessage to use reportBefore() only where appropriate, i.e. when there's an acommpanying actor in an AccompanyingInTravelState that's not a GuidedInTravelState:

This code may look a bit frightful, but it does what's required. Once it's included in your game you can define TravelMessages in the normal way without having to worry and have them take care of the sequence of reports automatically. The showTravelDesc() method takes advantage of the fact that Schedulable.allSchedulables already contains a list of every Actor in the game, so we can use it to see if the list contains anything that's an Actor we're interested in, i.e. an actor whose current ActorState is an AccompanyingInTravelState but not a GuidedInTravelState (which is a subclass of AccompanyingInTravelState). If so, but only if so, we need to ensure that the message displayed is wrapped in a reportBefore() macro. To do that we first need to capture the output from showTravelDesc()in a single-quoted string, which we do by wrapping the inherited method in mainOutputStream.captureOutput(). We can then display the captured string (str) via the reportBefore() macro, which ensures that it's displayed before the report of the NPC's travel.

By the way, it might have seemed simpler to use reportAfter() on the NPC's accompanying message in the sayDeparting(conn) method of his AccompanyingInTravelState, but this wouldn't work too well in practice: reportAfter really does report after everthing else, so if we used it here we'd see the report of the NPC's tagging along after the Player Character not only after the description of the Player Char's travel, but after the description of the new room they'd entered together, and that wouldn't look right at all!

As we've seen, it's sometimes possible to get the effect you want with reportAfter() or reportBefore(), but for anything more elaborate than this, it's helpful to have a deeper understanding of how the transcript works. In particular, using reportBefore in TravelWithMessage no longer works so well if the following actor has to do anything (like standing up) before following the player out of the room, since one will then get a transcript like:

>w
Bob stands up.
You walk out of the theatre. Bob comes with you.

Which is not at all what one would want. It is possible to deal with this, and towards the end of the article we'll see how; but to do that, or anything else more advanced than we've seen so far, it's necessary to gain a deeper understanding of how the transcript works. That's what we'll look at next.

How the Transcript Works

What the Transcript Is

The transcript is an object belonging to the CommandTranscript class, a subclass of OutputFilter. Its function is to collect, and at the appropriate time display, the list of reports relating to a particular action. The current transcript object can be referenced using gTranscript (a macro that expands to mainOutputStream.curTranscript).

The CommandTranscript class (and hence the gTranscript object) has a number of properties and methods. Those most likely to be of interest to game authors are:

reports_: a Vector containing a list of the current transcript's report objects. These objects are usually derived from the MessageResult class and/or the CommandReport class, although any object that defines a suitable showMessage() method would work here.

isActive: a flag that determines whether text being output is captured by this filter and stored in the reports_ list (if isActive is true) or simply passed through unaltered (if isActive is nil).

activate(): set isActive to true.

deactivate(): set isActive to nil.

addReport(report): add report to the Vector of report objects in reports_.

clearReports(): remove all the reports from reports_ (leaving it empty).

filterText(ostr, txt): if the transcript is active, turn txt into a command report and add it to reports_; otherwise pass txt through unchanged. Since a CommandTranscript is also an OutputFilter, this captures all the text output during a game while gTranscript is active.

transforms_: a list of objects of type TranscriptTransform which carry out further transformations on list of report objects in reports_ before they're finally displayed. The standard CommandTranscript lists four TranscriptTransforms here, but it's conceivable (though hardly common) that a game author might want to add others.

showReports(deact): show all the reports in list_. This method first deactivates the transcript (so that it doesn't capture its own output), then calls applyTransforms() to apply all the TranscriptTransforms in transforms_ to reports_, then calls the showMessage() method of every item in reports_. If deact is true the transcript is left deactivated, otherwise it is reactived after all the reports have been show.

summarizeAction (cond, report): A powerful method that can be used by game authors to replace multiple reports with a single summary. Its use will be described more fully below.

What the Transcript Contains — CommandReports

As mentioned above, what the transcript contains (in its reports_ property) is a Vector of CommandReport objects. To understand the transcript fully, it's helpful to know a bit more about these objects. But if you're reading this article for the first time, you might like to skip over this section fairly rapidly (or even skip straight to the next section) and come back to it when you need it for reference or deeper understanding.

The library uses CommandReports to control how the text from a command is displayed.
Reports are divided into two broad classes: "default" and "full" reports.

A "default" report is one that simply confirms that an action was performed, and provides little additional information. The library uses default reports for simple commands whose full implications should normally be obvious to a player typing such commands: take, drop, put in, and the like. The library's default reports are usually quite terse: "Taken", "Dropped", "Done".

A "full" report is one that gives the player more information than a simple confirmation. These reports typically describe either the changes to the game state caused by a command or surprising side effects of a command. For example, if the command is "push button," and pushing the button opens the door next to the button, a full report would describe the door opening.

Full reports are further divided into three subcategories by time ordering: "main," "before," and "after." "Before" and "after" reports are ordered before and after (respectively) a main report.

An action may iterate over a number of objects. For example TAKE ALL may result in an attempt to take a ball, a pen, and a table. The action may succeed with some objects but not with others; for example the command TAKE ALL may result in the ball and the pen being picked up, but not the table, which could be reported as being too heavy. By the time we near the end of the command execution cycle (in afterActionMain, for example), the transcript will contain CommandReports relating to each iteration (the TAKE action on the ball, the pen, and the table). There may well be more than one report relating to each iteration (typically there'd be a MultiObjectAnnouncement for each of the objects, followed by a DefaultCommandReport or MainCommandReport for each iteration that succeeded and a FailCommandReport for each iteration that failed).

There are various classes of CommandReport, as we'll shortly see below. Although they can vary a bit from class to class, the most CommandReports have the following properties and methods (amongst others); those shown in mauve are derived from MessageResult (from which CommandReportMessage descends, along with CommandReport):

action_: the action associated with this report (typically this will be whatever gAction was at the time the CommandReport was created).

iter_: the iteration number that was current when this report was added to the transcript. This can be used to check whether or not several reports all concern the same iteration (e.g., whether they all related to the attempt to take a table when the table was one of several targets of a TAKE ALL command).

isFailure: indicates whether the action associated with this report failed (on this particular iteration). Note that if isFailure is true the action did fail on this iteration, but that if isFailure is nil that doesn't necessarily mean the action succeeded; there may be several CommandReports associated with a particular iteration of an action, and the action failed if any one of those CommandReports has isFailure = true.

messageText_: the text this report will display when it is finally shown.

messageProp_: the message property (if any) used to generate the message stored in messageText_. If this is given as &prop then the objects involved in the command will first be checked to see if they define prop; if one of them does, then one of them will be used as the message object; if not then the standard library message object (typically playerActionMessages) will be used. Once the message object has been determined, the messageText_ will be generated by a call to messageObject.(prop)(args).

getAction(): usually just returns action_

isActionImplicit(): is true if this report concerns an implicit action.

isActionNestedIn(other): determines whether the action relating to this report is nested in the action relating to the other report. One action is nested in another if the second action is being executed as part of the first (e.g. because the first action triggers an implicitAction or calls nestedAction())

isPartOf(report): One report is considered to be part of another if they belong to the same iteration (i.e. their iter_ properties are equal) and the action relating to the first report is part of the action relating to the second. One action is considered to be part of another either if the first action is the second action, or if the original action of the first action is part of the original action of the second action. In brief, the original action is the action the actor was actually trying to carry out. For a fuller explanation see the description of the getOriginalAction method of the Action class.

setMessage(msg, [params]): sets the messageText_ property in the same way as the MessageReport constructor does, via a call to resolveMessageText.

resolveMessageText(sources, msg, [params]): If msg is given as a property, construct the message text by calling msg(params) on the appropriate message object, which may be one of the objects listed in sources if any of them define the msg property, or failing that the action message object (typically playerActionMessages). If msg is given as a string, simply use that string. Either way the resulting message is then passed through langMessageBuilder.generateMessage(msg) to expand any parameter substitutions .

construct():

As we shall see in the next section, reports are typically added to the transcript with code like gTranscript.addReport( new WhateverReportClass(params) ); the construct method of a CommandReport is thus generally called at the point when the report is added to the transcript, and is used to store or generate the information relevant to the report. The CommandReport constructor simply stores gAction in the action_ property. The CommandReportMessage constructor additionally calls the MessageResult constructor which fills the messageText_ property via a call to resolveMessageText. The CommandAnnouncement constructor fills the messageText_ property by calling its messageProp_ property (predefined on each subclass) on gLibMessages using the parameters passed to the constructor. If you define your own CommandReport class (which can be useful) you can make its construct method do whatever you deem useful.

We said above that reports are broadly defined into "default" and "main" reports, but another useful classification is into CommandReportMessages, CommandAnnouncements, and others.

CommandReportMessages are the reports that use MessageResult to construct their messages. They typically result from using the macros described in the article on Action Results. The various types of CommandReportMessage (with links to the appropriate sections in the Library Reference Manual) are:

CosmeticSpacingCommandReport: a cosmetic spacing report, usually added via the cosmeticSpacingReport(msg, params...) macro. This adds a message for internal spacing only; it is usually used to add a blank line or a paragraph break. The important feature of this type of report is that it doesn't count against any default reports: if there are default reports for the action, and no other reports, a CosmeticSpacingReport won't suppress the default reports. This is useful when internal separation is added on speculation that there might be some reports to separate, but without certainty that there will actually be any reports shown; for example, when preparing to show a list of special descriptions, we might add some spacing just in case some special descriptions will be shown, saving the trouble of checking to see if anything actually needs to be shown.

DefaultCommandReport: a default report (usually generated via defaultReport()). This will typically contain some laconic response (such as 'Taken. ') intended as a minimal acknowledgement that an action has succeeded. If there is a FullCommandReport for the same iteration of the same action, the DefaultCommandReport will be suppressed.

DefaultDescCommandReport: a default descriptive report for the current command, usually via the defaultDescReport() macro. This report will be shown unless any other report is shown for the same command. This differs from defaultReport in that we don't suppress a default description for an implied command: we only suppress a default description when there are other reports for the same command. The purpose of the default descriptive report is to generate reports that say things along the lines that there's nothing special to describe. For examples of how this might be used, see the comment attached to the defaultDescReport() macro in adv3.h

ExtraCommandReport: an extra report (added via a call to extraReport()). ExtraCommandReports do not cause DefaultCommandReports to be suppressed.

FullCommandReport: the base class for the FullCommandReport subclasses described below. The presence of a FullCommandReport will cause the suppression of any DefaultCommandReports relating to the same iteration of the same action.

AfterCommandReport: a report generated by the reportAfter() macro. AfterCommandReports are displayed after MainCommandReports.

BeforeCommandReport: a report generated by the reportBefore() macro. BeforeCommandReports are displayed before MainCommandReports.

FailCommandReport: a report indicating that a command failed (for the current iteration). These are reports added using the reportFailure() macro, and have isFailure = true. If you plan to manipulate the transcript to any extent in your game, it's a very good idea to ensure that you always use reportFailure() to report failures (rather than a double-quoted string, say), so that it's easy to identify failures in the transcript.

MainCommandReport: the most common kind of report for an action, generated either by using the mainReport() macro, or by displaying text using a double-quoted string or say() while the transcript is active.

QuestionCommandReport: an interruption for interactive input. This is used to report a prompt for more information that's needed before the command can proceed, such as a prompt for a missing object, or a disambiguation prompt.

The other main class of CommandReport that occurs quite commonly in the transcript is the CommandAnnouncement, which are used to track announcements to be made as part of an action's results. The various CommandAnnouncement subclasses are listed below:

AmbigObjectAnnouncement: We display this when the parser manages to resolve a noun phrase to an object (or objects) from an ambiguous set of possibilities, without having to ask the player for help but also without absolute certainty that the objects selected are the ones the player meant. This happens when more than enough objects are logical possibilities for selection, but some objects are more logical choices than others. The parser picks the most logical of the available options, but since other logical choices are present, the parser can't be certain that it chose the ones the player actually meant. Because of this uncertainty, we generate one of these announcements each time this happens. This report lets the player know exactly which object we chose, which will immediately alert the player when our selection is different from what they had in mind. In form, this type of announcement usually looks just like a default object announcement.

DefaultObjectAnnouncement: this announcement is displayed whenever the player leaves out a required object from a command, but the parser is able to infer which object they must have meant. The parser infers that an object was intended when a verb requires an object that the player didn't specify, and there's only one logical choice for the missing object. We announce our assumption to put it out in the open, to ensure that the player is immediately alerted if they had something else in mind. In English, this type of announcement conventionally consists of simply the name of the assumed object, in parenthesis and on a line by itself.

ImplicitActionAnnouncement: this is displayed when we perform a command implicitly, which we usually do to fulfill a precondition of an action. In English, we usually show an implied action as the verb participle phrase ("opening the door"), prefixed with "first", and enclosed in parentheses on a line by itself (hence, "(first opening the door)"). An ImplicitActionAnnouncement has a noteJustTrying() method, which sets its justTrying property to true and changes the message from "(first doing to such-and-such)" to "(first trying to do such and such)", which we use when the attempt fails. ImplicitActionAnnouncements may need careful handling when manipulating the transcript.

MultiObjectAnnouncement: When the player applies a single command to a series of objects (as in "take the book and the folder" or "take all"), we'll show one of these announcements for each object, just before we execute the command for that object. This announcement usually just shows the object's name plus suitable punctuation (in English, a colon), and helps the player see which results go with which objects. They can also help the game author see what reports go with which objects when trying to manipulate the transcript: everything from one MultiObjectAccouncement up to (but not including) the next will be the set of reports relating to one object (i.e. one iteration of the command). This set generally needs to be dealt with as a group; e.g., if we want to rearrange the order of reports, we should keep such sets of reports together, or otherwise deal with them as a group by replacing them all with a summary report of our own. Simply dispersing or rearranging the reports in a group of reports between one MultiObjectAnnouncement and the next is likely to result in chaotic output.

RemappedActionAnnouncement: Remapped action announcement. This is used when we need to mention a defaulted or disambiguated object, but the player's original input was remapped to a different action that rearranges the object roles. In these cases, rather than just announcing the defaulted object name, we announce the entire remapped action; we show the full action description because rearrangement of the object roles usually makes the standard object-only announcement confusing to read, since it doesn't naturally fit in as a continuation of what the user typed. In English, this message is usually shown with the entire verb phrase, in present participle form ("opening the door"), enclosed in parentheses and on a line by itself.

The other types of CommandReport are less common, or at least less commonly interesting to game authors wanting to manipulate the transcript. If you're just skipping quickly through this section on a first-read through, you might want to jump straight to the next section. But for the sake of completeness (and after all, you may encounter one of these less common report types and have to deal with it), we list the other kinds of CommandReports below (again with links to the appropriate section of the Library Reference Manual):

ConvBoundaryReport: A conversation begin/end report. This is a special marker we insert into the transcript to flag the boundaries of an NPC's conversational message.

ConvBeginReport: a ConvBoundaryReport that displays a <.convbegin > tag containing the interlocutor's actorID.

ConvEndReport: a ConvBoundaryReport that displays a <.convend > tag containing the interlocutor's ID, or, if s/he has one, his/her new ConvNode.

GroupSeparatorMessage: this simply displays separation between groups of messages - that is, between one set of messages associated with a single action and a set of messages associated with a different action.

InternalSeparatorMessage: this displays separation within a group of messages for a command, to visually separate the results from an implied command from the results for the enclosing command.

MarkerReport: a report boundary marker. This is a pseudo-report that doesn't display anything; its purpose is to allow a caller to identify a block of reports (the reports between two markers) for later removal or reordering.

EndOfDescReport: end-of-description marker. This serves as a marker in the transcript stream to let us know where the descriptive reports for a given action end.

FailCommandMarker: failure marker – this is a silent report that marks an action as having failed without actually generating any message text.

When the transcript is active (as it usually is) and the game displays a string using say(txt) or "txt" or some equivalant statement. The filterText() method of the current transcript then captures the text, wrapping it in a MainCommandReport and adding it to the list of report objects in the reports_ Vector.

Whenever gTranscript.addReport(report) is called; this directly adds report to the reports_ Vector.

This second method of adding reports is more common than might at first appear; it's used by all the macros that are responsible for reporting things:

At first sight, this last definition may make using mainReport() look exactly equivalent to displaying the same text with a double-quoted string, but this is not always the case. If you use mainReport(txt), you can be sure that one and only one MainCommandReport will be added to the transcript. If, however, you use a double-quoted string, under certain circumstances that string can be split over a number of MainCommandReports, specifically when the double-quoted string uses the <<>> notation. In ordinary use this doesn't matter, but it can matter quite a bit when you want to manipulate the transcript, as we shall see.

When Reports Get Displayed

Apart from one or two exceptional circumstances that need not detain us, the reports in the transcript are generally displayed once per action (which often equates to once per player turn, except where NPCs are performing actions on their own initiative).

The usual flow of events is that executeCommand() eventually calls executeAction() wrapped within the withCommandTranscript() function (for the roles of executeCommand and executeAction see the article on the Command Execution Cycle). In this context, withCommandTranscript() creates a new CommandTranscript object, installs it on the mainOutputStream (so that it becomes the gTranscript object), calls executeAction(), uninstalls the CommandTranscript object, and then runs the showReports() method of the transcript. To put that more briefly: first a new transcript object is setup, then executeAction() is called, then the transcript shows its reports.

About the last thing executeAction() does is to call doAction() on the current action. About the last thing doAction() does is to call afterActionMain() (after all the action processing has occurred), so this is a good point at which to intervene in the transcript, as we shall now go on to explore.

Manipulating the Transcript

Where to Intervene

As explained at the end of the previous section, the best point at which to intervene to manipulate the transcript is in the afterActionMain() method of the current action. However, it isn't normally necessary to override this method directly, since the library provides some convenient hooks for the purpose. To use these hooks we'd normally follow these two steps:

At some earlier point in the execution cycle make a call to gAction.callAfterAction(obj) (where obj can be any convenient object). This might typically but by no means exclusively be done in the action() part of the relevant dobjFor() or iobjFor() on one of the actions involved in the command, with self serving as the obj parameter.)

Then define an afterActionMain() method on the same obj (whatever you defined it to be) to carry out whatever transcript manipulation you want.

These two steps will hopefully become clearer through the examples we shall be looking at below.

How to Intervene

Manipulating the transcript is basically a matter of manipulating the report objects in its reports_ property. The reports_ property holds a Vector of reports, so any Vector methods may be applied to it. The reports can be reordered, or some removed and others added, or the messageText_ property of some reports tweaked so that they come out saying something different.

In order to manipulate the transcript effectively, it is very helpful to know (or know where to look up) the details of the Vector intrinsic class and to be reasonably comfortable with Anonymous Functions, since these are extremely useful in manipulating Vectors, and are also essential in the summarizeAction() method we shall look at next.

Although you can manipulate the reports_ property any way you like, the CommandTranscript class provides a summarizeAction() method which makes it relatively easy to handle certain cases. We shall now go on to look at how this method is used and give a couple of examples, before going on to look at cases where it's necessary to manipulate reports_ by other means.

The summarizeAction() method does what it says: it summarizes an action by combining a number of reports from the current transcript into a single report. For example, we could change something like this:

The summarizeAction() method doesn't do this all by itself, but it
does make it easier for game authors to produce this effect by
removing a run of consecutive reports in the transcript which meet the
author's specification and replacing them with a single report defined
by the author. Whatever kind of CommandReport it is we specify we want
summarized, summarizeAction() will also gobble up any
ImplicitActionAnnouncements, MultiObjectAnnouncements,
DefaultCommandReports and ConvBoundaryReports it finds between one
such CommandReport and the next, so that these subsidiary reports
don't get in the way of constructing our summary.

summarizeAction() is called with two arguments, cond and report, both of which must be defined by the game author as anonymous functions.

The summarizeAction() method runs through the reports for the current action, submitting each one to the 'cond' callback to see if it's of interest to the summary. For each consecutive run of two or more reports that can be summarized, it removes the reports that 'cond' accepted, along with the multiple-object announcement reports associated with them; it then inserts a new report with the message returned by the 'report' callback.

'cond' is called as cond(x), where 'x' is a report object. This callback returns true if the report can be summarized for the caller's purposes, nil if not.

'report' is called as report(vec), where 'vec' is a Vector consisting of all of the consecutive report objects that we're now summarizing. This function returns a string giving the message to use in place of the reports we're removing. This should be a summary message, standing in for the set of individual reports we're removing.

There's an important subtlety to note. If the messages you're summarizing are conversational (that is, if they're generated by TopicEntry responses), you should take care to generate the full replacement text in the 'report' part, rather than doing so in separate code that you run after summarizeAction() returns. This is important because it ensures that the Conversation Manager knows that your replacement message is part of the same conversation. If you wait until after summarizeAction() returns to generate more response text, the conversation manager won't realize that the additional text is part of the same conversation.

[Note: most of the foregoing explanation is taken from the comment in the library source.]

Reading this description in the abstract, especially for the first time, may not leave a very clear impression of how summarizeAction() should actually be used. So we'll just give a quick summary here, and then move on to some examples that will hopefully make it a bit clearer.

The steps to follow to make use of summarizeAction() are:

At some earlier convenient point, call gAction.callAfterActionMain(obj) (where obj is some appropriate object)

On the same obj as used in the call at step 1, define an an afterActionMain method().

In obj.afterActionMain() include a call to gAction.summarizeAction(cond, report).

In place of cond define an anonymous function that picks out the reports you want to summarize: e.g. {x: x.ofKind(MyCustomCommandReport)}.

In place of report define an anonymous function that prints a summary of the reports matched by cond, e.g. {vec: 'Bob accepts ' + spellint(vec.length) + ' gold coins. '}.

Note that although we have used short-form anonymous functions as examples in these steps, we could equally well have used the new function() syntax and gone on to make these functions as complicated as we liked. But now let's go on to look at some examples.

Example 1 - Combining Identical Reports

Although, as we shall see, it's not absolutely essential, it is convenient to define our own report class, since this is then easy to pick out of the transcript. We'll define a GiveReport class in a fairly general way that allows it to be used with any actor receiving any kind of object (instead of just Bob receiving gold coins) so that it could be reused in other contexts. This class will be responsible for providing the message that should be displayed if the player character gives Bob just one gold coin:

We need to define a custom matchTopic() method to ensure that the GiveTopic matches any object of the GoldCoin class. The topicResponse() method then calls gAction.callAfterActionMain to register the GiveTopic as the action on which to call afterActionMain() (the fact that this may be called more than once doesn't matter, the library will ensure that our GiveTopic is only registered once), and then adds an appropriate GiveReport to the transcript. Finally, we define afterActionMain() to look for all the GiveReports in the transcript and replace them with a single report.

The vec parameter in the second anonymous function is a Vector containing all the GiveReports identified by the first anonymous function. There'll be one of these for each coin handed over to Bob, so the length of the Vector gives the number of coins handed over. We use spellInt to spell out the number (e.g. 'three' instead of '3') and then use the pluralName of the object stored in the first report to give us the plural 'gold coins'. We could simply have used the string ' gold coins' here, but that would have made our code a little less general.

For a similar but slightly more elaborate example of this kind of thing, see the section on 'Counting the Cash' in the Getting Started guide.

Of course, giving coins to an NPC is not the only case where reports on multiple coins could be improved. It may be we'd want to expand the tidying up to other actions applying to multiple coins. For example:

We could achieve this by much the same means as before: define a TakeReport class on analogy with the GiveReport class we defined above, override the action part of dobjFor(Take) on the GoldCoin class:

And then define an afterActionMain method that uses gTranscript.summarizeAction() as above. One downside with this approach, however, is that the more standard actions we want to deal with like this, the more cumbersome it will become to define a custom MainReport class and override action methods like this. A second downside with this particular implementation is that afterActionMain will get called on every individual gold coin involved in the action, which is needlessly inefficient. So let's look at a slightly different way of doing it.

The library implementation of the Take action already adds a report to the transcript – a DefaultCommandReport added through a call to defaultReport(). So provided we can find some means of picking out the DefaultCommandReports we want, we can simply have afterActionMain summarize those DefaultCommandReports. We can do this is we modify CommandReport (and hence all its subclasses) to remember the direct object of the action it's recording:

There are two reasons for making a separate object (takeReportManager) rather than self the argument to callAfterActionMain() here. First, callAfterActionMain(self) would register each individual coin involved in the command for having its afterActionMain() method called, so that, for example, if the player were picking up twenty coins, afterActionMain() would be called twenty times, once on each coin, which is rather inefficient; callAfterActionMain() won't register the same object more than once, but there's nothing to stop it registering multiple objects of the same class. Second, if we wanted to extend this principle to other actions such as Drop, PutIn, PutOn, etc., it would be handier to have them each call their own version of afterActionMain (thus defined on a different object), rather that having one large afterActionMain that had to cope with a variety of different action.

It remains now to define our takeReportManager class and its associated afterActionMain() method:

This works fine provided the command issued by the player concerns only a quantity of gold coins. It is not quite so neat if the player issues a TAKE ALL command when there are plenty of other objects in scope, e.g.:

While this is not totally disastrous (as it might have been had our summarizeAction() method not ensured that it was only summarizing reports involving GoldCoin objects), it is slightly messy. It could be improved slightly by putting a '<.p>' before '{You/he takes} ' in the second anonymous function argument of summarizeAction(), since this would at least visually separate the 'You take six gold coins' report from the others, but it's less than ideal. Whether it would be worth fixing is a matter of judgement. In the next section we'll go on to look at how similar reports for different objects might be combined, and we'll look at fixing this then. But before we do that, we'll just take the gold coins a stage further.

If taking the gold coins is an obvious candidate for combining reports, putting them somewhere is equally so. With the default library handling we'd get:

Note that by using the {in iobj} parameter substitution we can make the same putReportManager serve for both putting in and putting on. Note also that we have to add the test x.action_ == gAction, since we only want to count the default reports for the main action (PutOn or PutIn), and not the default reports that are also generated for the implicit Take actions. With this code in place we get the much improved:

>put coins on table
(first taking the gold coin, then taking the gold coin, then taking the gold coin, then taking
the gold coin, then taking the gold coin, then taking the gold coin)
You put six gold coins on the small square table.

But it would be better still if we could combine the implicit action reports. This can be done, but it can't be done using summarizeAction, since this creates a new MainCommandReport, whereas we need to summarize the implicit action reports into a single new ImplicitActionAnnouncement. It's therefore necessary to manipulate gTranscript.reports_ directly. Basically, what we need to do is first to determine how may implicit action reports for taking gold coins there are. If there are none or only one we don't need to do anything; we only need to manipulate the transcript any further if there are more than one. If there are we need to create a new implicit action report that summarizes the others, and then remove the others from the transcript. It turns out that we then need to remove all the DefaultCommandReports relating to taking gold coins as well, since they will otherwise be displayed once the corresponding ImplicitObjectAnnouncements have been removed. Here's one way of doing all that:

This is still far from perfect, but rather than developing this implementation any further, we'll go on to the next example, since this in any way provides a more general implementation for combining reports of this kind.

Example 2 - Combining Similar Reports

Combining identical reports about doing the same thing to identical objects is one thing. It is quite another is to combine similar reports about doing the same thing to a group of different objects. For the sake of argument, let's suppose that we've got a group of objects that includes a number of gold coins, silver coins and copper coins, as well as some miscellaneous portable and non-portable items. Assuming we're starting over from scratch (and not using any of the techniques for summarizing gold coin reports discussed above, a TAKE ALL command might produce something like the following result:

For the remainder of this example, we can concentrate on developing takeReportManager.afterActionMain(). We'll start with a very simple (and not entirely adequate) implementation, that simply lists every item after "You take ":

We need to exclude the MultiObjectAnnouncements, otherwise we'll end up counting everything twice. Otherwise the most mysterious part of this code is probably this:

objectLister.makeSimpleList(vec.applyAll({x: x.dobj_}).toList)

objectLister.makeSimpleList takes a list objects and returns a single-quoted string containing a nicely-formatted list of those objects. vec.applyAll({x: x.dobj_}) returns a Vector containing the dobj_ property of every report object in vec (or, if you like, it returns the Vector that results from replacing every element of vec with its dobj_ property). Finally toList turns the Vector into a list, so we can pass it to makeSimpleList (actually, it's not strictly necessary, since the method will quite happily accept a Vector).

With this definition of afterActionMain we get:

>take all
You take the tennis ball, the old coat, the odd sock, the large red box, the wardrobe, the
small green book, the torch, the small square table, the small blue box, eight gold coins,
three silver coins, and two copper coins.

Apart from the fact that the list contains two items it shouldn't
(the wardrobe and the table) this has worked pretty well. In
particular, by leveraging the library's Lister class (via
objectLister) we've automatically got a report that separates out and
summarizes the various equivalent objects (the eight gold coins, three
silver coins, and two copper coins).

The next step is to separate out the reports of items that weren't taken. Provided the reportFailure() macro was used to report the failure of the item to be taken, their reports can easily be identified as ones for which isFailure is true. We can use this to separate the vector of reports into two, one of successes and one of failures. We can then deal with the successes as before and finally append the list of failures:

>take all
You take the tennis ball, the old coat, the odd sock, the large red box, the small green book,
the torch, the small blue box, eight gold coins, three silver coins, and two copper coins.
You canít take that. The small square table is too heavy.

This is an improvement, but it is still not quite right. For one thing, it's far from clear what 'that' refers to in "You can't take that", and the final report might read a little better in this context it is red "The small square table is too heavy to take." We can fix this by tweaking each failure report as we append it to our result string. Here we give the code of the resultant foreach loop; the rest of the code remains the same:

>take all
You take the tennis ball, the old coat, the odd sock, the large red box, the small green book,
the torch, the small blue box, eight gold coins, three silver coins, and two copper coins. You
canít take the wardrobe. The small square table is too heavy to take.

It would be possible to refine this a little further. For example, if there were several items that were too heavy to take, you might want to combine them into a single report ("The wardrobe and the small square table are too heavy to take"). But this probably isn't worthwhile. Given the number of ways a take action could fail (for example, consider the case where there's an item in a locked glass cabinet) it's probably better not to include the failure reports along with the successful ones like this. A safer approach for the more general case would be to separate out the failure reports before calling summarizeAction, have summarizeAction summarize the objects that were taken, and then let the library report the objects that weren't taken in the normal way. Not only is this safer to implement, it also makes it clearer to the player what went wrong with the items that weren't taken.

There is, however, one further refinement we should explore before leaving this example. Suppose one or more of the objects had an overidden actionDobjTake method that displayed a custom message; e.g.:

>take all
You take the tennis ball, the tennis ball, the old coat, the odd sock, the large red box, the
small green book, the torch, the small blue box, eight gold coins, three silver coins, and two
copper coins. You canít take the wardrobe. The small square table is too heavy to take.

The reason for this doubling of the tennis ball is that the transcript now contains two reports for taking the tennis ball: the default command report generated from the inherited action handling and the main command report from the custom message. The simplest way to solve this is to ensure that our Vector of taken objects contains each object only once, which we can achieve by calling the getUnique() method on this Vector. Our code then becomes:

This example could be taken a lot further to make it both more general and more robust, but we have taken it as far as we need to for the purpose of this article. For a much fuller implementation (which you can also use in your own games), see the combineReports.t extension that should be in the ../TADS 3/lib/extensions folder.

Example 3 - Combining Disparate Reports

For our final example, we'll return to Bob following the player character out of the theatre. Once again, we'll start from scratch (i.e. we'll assume we haven't used reportBefore or reportAfter anywhere to tweak the order of reports). This time, though, we'll assume that Bob starts out sitting on a chair in the theatre when the player does whatever makes Bob decide to follow him. Assuming we still have a TravelMessage on the western exit from the Theatre, the output we'd get using the standard library without any further modifications is:

>w
Bob stands up.
Bob comes with you. You walk out of the theatre.
Lobby
The main theatre auditorium is just to the east. To the north are the toilets and the
street exit is to the east.
Bob is standing here.

Quite apart from improving the output by customizing some of these messages, it would be better if the format were more like:

>w
You walk out of the theatre.
Bob stands up and comes with you.
Lobby
The main theatre auditorium is just to the east. To the north are the toilets and the
street exit is to the east.
Bob is standing here.

To do this we have to combine, not similar reports about different objects, but different reports about the same object (Bob). Since we need to manipulate the transcript to do this in any case, we may as well rearrange the reports into a better sequence at the same time.

If we go back to the original transcript, we see that Bob is mentioned three times: first when he stands up, then when he comes with you, and then when he is standing in the new location. What we need is some way of picking out the first two reports and moving them after the report of the player character walking out of the theatre, while leaving the final report (Bob in the new location) alone. But because we'd like whatever we do to be as general as posssible, we can't rely on there being two reports of what Bob does before we get to the new location (in many cases there'll only be one, and on occasion there could be more than two). Neither can we rely on there being a report of the player character's movements; not every TravelConnector through which the player character may pass will necessarily have a travelDesc.

What we can rely on is the presence of a <.roomname> tag in a command report all on its own just before the name of the new location is displayed. We can use this for two purposes: first, we can ignore all reports about Bob that come after this tag while combining all the reports about Bob that come before it; second, we can use this tag as a marker, showing us the position where we need to insert our summary report of Bob's actions.

The way to pick out the reports we need to work on is then to pick out all the reports prior to the <.roomname> tag that start with the word "Bob" (or, in the more general case, with the name of the NPC who's doing the following). Unfortunately there's a complication: the string "Bob comes with you." is actually split over four or five different command reports, which makes it extremely different to deal with. The reason for this is that the library creates this output from a double-quoted string that makes generous use of the <<>> notation, which the transcript sees as a sequence of separate reports. The obvious cure is to override AccompanyingInTravelState.sayDeparting() to use mainReport instead. A literalistic way of doing this that otherwise preserves the library behaviour is:

modify AccompanyingInTravelState
sayDeparting(conn)
{
local msg = mainOutputStream.captureOutput( {: inherited(conn) });;
/*
* strip off any leading \^ so that we can be sure of matching the NPC's name
* at the start of the report string.
*/
if(msg.startsWith('\^'))
msg = msg.substr(2);
mainReport(msg);
}
;

The main thing is to ensure that we use mainReport() to generate the sayDeparting message, to ensure that it's all contained in one report.

To make our transcript-manipulation code as general as possible we'll define it on a modified AccompanyingState in such a way as to work for any actor. As ever the first step is to register something as an object on which to call afterActionMain. In this case the object may as well be the AccompanyingState itself, and the best place to carry out the registration is probably in the beforeTravel() method (which is also the method that triggers the NPC following the PC):

The remaining step is to define the afterActionMain() method. What we're trying to do is beyond the capabilities of summarizeAction(), so we'll have to manipulate gTranscript.reports_ directly. We've already described the logic of what we need to do; we just need to translate it into TADS 3 code:

afterActionMain()
{
/*
* Check whether there is a <.roomname> tag somewhere in the
* transcript.
*/
local idx = gTranscript.reports_.indexWhich({ x: x.messageText_ ==
'<.roomname>' });
/*
* If there is no <.roomname> tag, travel failed for some reason,
* in which case we don't want to change the transcript at all.
*/
if(idx == nil)
return;
local actor = getActor;
local str = '';
local vec = new Vector(4);
local pat = new RexPattern('<NoCase>^' + actor.theName + '<Space>+');
/*
* Look through all the reports in the transcript for those whose
* message text begins with the name of our actor, followed by at
* least one space (so that if the actor's name is Rob we don't
* pick up any messages relating to Roberta, for example),
* regardless of case (so we pick up messages about 'the tall man'
* and 'The tall man').
*
* Once we get to the description of a new room, stop looking (we
* don't want to include the description of the actor arriving in
* the new location, just the actor departing the old one).
*
* Store the relevant message strings (those before the new room
* description, but not any after it) in the vector vec; at the
* same time remove these reports from the transcript (since we'll
* be replacing them with a single report below).
*/
while((idx = gTranscript.reports_.indexWhich({ x:
rexMatch(pat, x.messageText_) })) != nil)
{
if(idx > gTranscript.reports_.indexWhich({ x: x.messageText_ ==
'<.roomname>' }))
break;
vec.append(gTranscript.reports_[idx].messageText_);
gTranscript.reports_.removeElementAt(idx);
}
local len = actor.theName.length() + 1;
/*
* Now go through each of the strings in vec in turn, stripping
* off the actor's name at the start and the period/full-stop at
* the end (so that, for example 'Bob stands up. ' becomes 'stands
* up'. In searching for the position of the terminating full
* stop (period) we start beyond the end of the actor's name in
* case the actor's name contains a full stop (e.g. Prof. Smith).
*/
vec.applyAll( { cur: cur.substr(len, cur.find('.', len) - len)});
/*
* Concatanate these truncated action reports into a single string
* listing the actor's actions (e.g. 'stands up and comes with
* you'), separating the final pair of actions with 'and' and any
* previous actions with a comma. We can use stringLister (defined
* in the previous example) to do this for us.
*/
str = stringLister.makeSimpleList(vec.toList);
/*
* Put the actor's name back at the start of the string, and
* conclude the string with a full-stop and a paragraph break.
*/
str = '\^' + actor.theName + ' ' + str + '.<.p>';
/*
* Find the insertion point, which is just before the description
* of the new room.
*/
idx = gTranscript.reports_.indexWhich({ x: x.messageText_ ==
'<.roomname>' });
/*
* Insert the message we just created as a new report at the
* appropriate place, i.e. just before the new room description.
*/
gTranscript.reports_.insertAt(idx, new MessageResult(str));
}
;

With this we finally get:

>w
You walk out of the theatre.
Bob stands up and follows you out of the theatre.
Lobby
The main theatre auditorium is just to the east. To the north are the toilets and the
street exit is to the east.
Bob tags along behind.

This may seem like quite a lot of work for a relatively minor effect, but because we've achieved this effect by modifying AccompanyingState and making it general for any actor, we get it for every NPC in our game, and even when we don't need to combine any reports, we also get the reordering of reports (the NPC's movement described after that of the player character who's being followed) for every NPC in an AccompanyingState. Indeed, we could hive this modified AccompanyingState (along with the modified AccompanyingInTravelState) into a small extension which we included in all our games, at which point the effort required to tweak the transcript like this undoubtedly become worthwhile. You don't even have to copy any of this into your own code; it's all included in the smartAccompany.t extension which you can find in the ../lib/extensions folder.

There's just one more thing to consider here; the modifications we made to AccompanyingState are appropriate to when an NPC is following the player character, but less so when the player character is following the NPC (i.e., when the NPC is in a GuidedTourState). Since GuidedTourState is a subclass of AccompanyingState it's probably best to override GuidedTourState.afterActionMain() to do nothing (as the smartAccompany.t extension does).

Some Final Thoughts

Hopefully the three examples given above should serve to illustrate the more abstract discussion that preceded. These examples clearly won't cover everything you might want to do with the transcript in your game, but they should help you work out how to get the particular effects you want.

Tweaking the transcript can be a tricky business, so don't be disheartened if you can't get it right first time. Even more than in most other areas of TADS 3 programming you'll probably find there's a lot of trial and error. In particular it may not be at all obvious what set of command reports the transcript is going to contain at any particular point. If something doesn't immediately work the way you expect, or you can't work out how to get started on it, the best thing to do is probably to set a breakpoint in the Windows debugger (preferably in your afterActionMain method) and take a look at what gTranscript.reports_ contains.

If you can't use Workbench (e.g. because you're not using Windows), you'll almost certainly need to write a debugging routine that displays the contents of gTranscript.reports_, and then call it from afterActionMain. Remember, though, that any such routine must start by deactivating the transcript, otherwise it'll go into an everlasting loop as it adds reports about the transcript to the transcript!

For your convenience the extension showTranscript.t is included in the ../lib/extensions folder. If you use this extension you must also include reflect.t in your project (at least when compiling for debugging). The extension defines the function showTranscript(), which lists the contents of gTranscript.reports_ in a form which will hopefully help you see what's going on (but if you need more information, feel free to tweak the extenstion).

When debugging (or perhaps even planning how to write) your afterActionMain() routine without the aid of Workbench (or even if you do have Workbench but prefer not to use the debugger for this purpose), you can simply include a call to showTranscript() in your afterActionMain() routine to get a reasonable idea of what the transcript contains at that point. That should help you work out either what you need to do to get the effect you're after, or else what's going wrong with your latest attempt.