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:

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. 

We can achieve this by using reportAfter() in the makeWet() method:

   makeWet () 
   {
      if (!isWet) 
      {
          local obj = self;
          gMessageParams(obj);
          reportAfter('{The obj/he{ become{s} rather wet. ');
          name = 'rather wet ' + name;
          initializeVocabWith('rather wet -');
      }
      isWet = true;
    }

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:

modify TravelWithMessage
    showTravelDesc()
    {        
        if(Schedulable.allSchedulables.indexWhich({x:
            x.ofKind(Actor) 
            && x.curState.ofKind(AccompanyingInTravelState) 
            && !x.curState.ofKind(GuidedInTravelState) }))
        {    
            local str = mainOutputStream.captureOutput( {: inherited });    
            reportBefore(str);
        }
        else
            inherited;
    } 
;

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:


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):

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:


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:


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):


How Reports Get Added to the Transcript

Reports are added to the current transcript in one of two ways:

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:

  1. 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.)
  2. 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.


Using summarizeAction

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:

gold coin: Bob accepts the gold coin. 
gold coin: Bob accepts the gold coin. 
gold coin: Bob accepts the gold coin. 

into this:

Bob accepts the three gold coins. 

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:

  1. At some earlier convenient point, call gAction.callAfterActionMain(obj) (where obj is some appropriate object)
  2. On the same obj as used in the call at step 1, define an an afterActionMain method().
  3. In obj.afterActionMain() include a call to gAction.summarizeAction(cond, report).
  4. In place of cond define an anonymous function that picks out the reports you want to summarize: e.g. {x: x.ofKind(MyCustomCommandReport)}.
  5. 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

Let's start with the example suggested above, namely combining multiple reports of 'Bob accepts the gold coin.' into a single report of 'Bob accepts n gold coins.'

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:

class GiveReport: MainCommandReport
    construct(receivingActor, obj)
    {
	    /* remember our object */
        obj_ = obj;
        
        gMessageParams(receivingActor, obj);
        
        /* use the inherited handling to construct the message */
        inherited('{The receivingActor/he} accepts {the obj/him}. ');
    }
    obj_ = nil
;

Assuming we have defined a suitable GoldCoin class, we can then define a GiveTopic for Bob along the following lines:

+ GiveTopic 
    matchTopic(fromActor, obj)
    {
        return obj.ofKind(GoldCoin) ? matchScore : nil;
    }
    
    topicResponse()
    {
        gAction.callAfterActionMain(self);
        gTranscript.addReport( new GiveReport(getActor, gDobj) );
        gDobj.moveInto(bob);
    }
    
    afterActionMain()
    {
        gTranscript.summarizeAction(
            {x: x.ofKind(GiveReport) },
            {vec: 'Bob accepts ' + spellInt(vec.length) + ' ' +
            vec[1].obj_.pluralName + '. '}
            );
    }
;

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:

>take coins
gold coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 

Would obviously read better as:

>take coins
You take six gold coins.

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:

class GoldCoin 'gold coin*coins' 'gold coin'
   dobjFor(Take)
   {
     action()
     {
	     gAction.callAfterActionMain(self);
	     if(!gAction.isImplicit)
	        gTranscript.addReport( new TakeReport(self) );
	     moveInto(gActor);
     }
   }
 ;

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:

modify CommandReport
    construct()
    {
        inherited();
        dobj_ = gDobj;
    }
    dobj_ = nil;
;

(We don't bother to record the indirect object here since iterating a command over a series of indirect objects is very rare; no action defined in the library does it.)

Then the only override we need to dobjFor(Take) on the GoldCoin class is to invoke callAfterActionMain():

class GoldCoin: Thing 'gold coin*coins' 'gold coin'
    dobjFor(Take)
    {
        action()
        {
            gAction.callAfterActionMain(takeReportManager);
            inherited;
        }
    }    
    isEquivalent = true
;

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:

takeReportManager: object
    afterActionMain()
    {       
        gTranscript.summarizeAction(
            { x: x.ofKind(DefaultCommandReport) && x.dobj_.ofKind(GoldCoin)},
            { vec: '{You/he} take{s} ' + spellInt(vec.length) + ' ' +
            vec[1].dobj_.pluralName + '. '} );            
    }
;

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.:

>take all
tennis ball: Taken. 
old coat: Taken. 
odd sock: Taken. 
large red box: Taken. 
small green book: Taken. 
torch: Taken. 
small square table: The small square table is too heavy. 
small blue box: Taken. You take six gold coins. 

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:

>put coins on table
gold coin:
(first taking the gold coin)
Done. 

gold coin:
(first taking the gold coin)
Done. 

gold coin:
(first taking the gold coin)
Done. 

gold coin:
(first taking the gold coin)
Done. 

gold coin:
(first taking the gold coin)
Done. 

gold coin:
(first taking the gold coin)
Done. 

We can improve this greatly using the same technique as with Take:

class GoldCoin: Thing 'gold coin*coins' 'gold coin'
    dobjFor(Take)
    {
        action()
        {
            gAction.callAfterActionMain(takeReportManager);
            inherited;
        }
    }    
    dobjFor(PutOn)
    {
        action()
        {
            gAction.callAfterActionMain(putReportManager);
            inherited;
        }
    }
    dobjFor(PutIn)
    {
        action()
        {
            gAction.callAfterActionMain(putReportManager);
            inherited;
        }
    }
    
    isEquivalent = true
;

putReportManager: object
    afterActionMain()
    {       
        gTranscript.summarizeAction(
            { x: x.ofKind(DefaultCommandReport) && x.dobj_.ofKind(GoldCoin)
            && x.action_== gAction},
            { vec: '<.p>{You/he} put{s} ' + spellInt(vec.length) + ' ' +
            vec[1].dobj_.pluralName + ' {in iobj}. '} );            
    }
;

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:

putReportManager: object
    afterActionMain()
    {    
        /* Summarize the PutIn or PutOn action for all gold coins. */
        gTranscript.summarizeAction(
            { x: x.ofKind(DefaultCommandReport) && x.dobj_.ofKind(GoldCoin)
            && x.action_==gAction},
            { vec: '<.p>{You/he} put{s} ' + spellInt(vec.length) + ' ' +
            vec[1].dobj_.pluralName + ' {in iobj}. '} );    
       
        /* 
         *   Define this function separately as we'll use it more than once; 
         *   the function identifies implicit action reports relating to 
         *   taking gold coins.
         */
        
        local impFunc = {x: x.ofKind(ImplicitActionAnnouncement) 
            && x.action_.ofKind(TakeAction)
                && x.dobj_.ofKind(GoldCoin) };

        
        /* 
         *   Count how many implicit action reports there are relating to 
         *   taking gold coins.
         */
        local impActions = gTranscript.reports_.countWhich(impFunc);
                   
        /*  We only need to do anything if there's more than one. */
        if(impActions > 1)
        {
            /* Note the location of the first relevant implicit action report */
            local firstImp = gTranscript.reports_.indexWhich(impFunc);
            
            /* Store a copy of this report */
            local rep = gTranscript.reports_[firstImp];
            
            /* 
             *   Change the text of this implicit action report to account 
             *   for all the gold coins implicitly taken
             */
            rep.messageText_ = '<./p0>\n<.assume>first taking ' +
                spellInt(impActions) + ' gold coins<./assume>\n';
            
            /*  
             *   Remove all the individual gold coin taking implicit action 
             *   reports from the transcript.
             */
            gTranscript.reports_ = gTranscript.reports_.subset({x: !impFunc(x) });
            
            /*  
             *   Add back our summary implicit action report at the location 
             *   of the first individual report we removed.
             */
            gTranscript.reports_.insertAt(firstImp, rep);
            
            /*   
             *   Remove all the DefaultCommandReports relating to taking gold
             *   coins, since they would otherwise show up now we've removed
             *   most of the implicit action reports.
             */
            gTranscript.reports_ = gTranscript.reports_.subset({
                x: !(x.ofKind(DefaultCommandReport) 
                     && x.action_.ofKind(TakeAction)
                     && x.dobj_.ofKind(GoldCoin)) } );
        }
    }
;

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:

>take all
tennis ball: Taken. 
old coat: Taken. 
odd sock: Taken. 
large red box: Taken. 
wardrobe: You canít take that. 
small green book: Taken. 
torch: Taken. 
small square table: The small square table is too heavy. 
small blue box: Taken. 
gold coin: Taken. 
silver coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 
silver coin: Taken. 
gold coin: Taken. 
gold coin: Taken. 
copper coin: Taken. 
gold coin: Taken. 
copper coin: Taken. 
silver coin: Taken. 

Over the course of this example, we'll explore how this can be combined into a single summary report listing what the player has taken and what could not be taken. We'll proceed step by step.

First, we'll need to borrow one piece of code from the previous example, namely the modification to Command Report:

modify CommandReport
    construct()
    {
        inherited();
        dobj_ = gDobj;
    }
    dobj_ = nil;
;

Then we'll start with a simple modification of Thing, in fact precisely the same modification we earlier used on the GoldCoin class:

modify Thing
    dobjFor(Take)
    {
        action()
        {
            gAction.callAfterActionMain(takeReportManager);
            inherited;
        }
    }
;

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 ":

takeReportManager: object
    afterActionMain()
    {
        gTranscript.summarizeAction(
            { x: x.action_.ofKind(TakeAction) 
            && !x.ofKind(MultiObjectAnnouncement) },
            { vec: '{You/he} take{s} ' + objectLister.makeSimpleList(
                vec.applyAll({x: x.dobj_}).toList
                ) + '. ' } );
    }
;

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:

takeReportManager: object
    afterActionMain()
    {
        gTranscript.summarizeAction(
            { x: x.action_.ofKind(TakeAction) 
            && !x.ofKind(MultiObjectAnnouncement) },
            new function (vec)
        { 
            /* Isolate the reports of failures */
            local failVec = vec.subset({x: x.isFailure});
            
            /* And then the reports of successes */
            local successVec = vec.subset({x: !x.isFailure});
            
            /* Construct a string reporting the objects we did take */
            local str = '{You/he} take{s} ' + objectLister.makeSimpleList(
                successVec.applyAll({x: x.dobj_})) + '. ';  
            
            /* 
             *   Then append a report explaining the failure of all those we 
             *   didn't
             */
            foreach(local cur in failVec)
                str += cur.messageText_;
            
            /* Return the result */
            return str;
        });           
    }
;

With this code the output becomes:

>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:

            foreach(local cur in failVec)
            {
                local mess = cur.messageText_;
                
                /* replace "too heavy" with "too heavy to take" */
                mess = mess.findReplace('too heavy', 'too heavy to take',
                                      ReplaceOnce);
                
                /* replace "that" or "those" with the name of the object */                      
                mess = rexReplace('%<(that|those)%>', mess, cur.dobj_.theName,
                                  ReplaceAll);
                str += mess;
            }

With this our output becomes what we were aiming for:

>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.:

+ tennisBall: Thing 'split tennis ball*balls' 'tennis ball'
    "This one has been split open; it's no good for playing tennis with any
    more. "
    dobjFor(Take)
    {
        action()
        {
            inherited;
            "You grab hold of the tennis ball. ";
        }
    }
;

We'd then find that the tennis ball was listed twice:

>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:

takeReportManager: object
    afterActionMain()
    {
        gTranscript.summarizeAction(
            { x: x.action_.ofKind(TakeAction) 
            && !x.ofKind(MultiObjectAnnouncement) },
            new function (vec)
        { 
            /* Isolate the reports of failures */
            local failVec = vec.subset({x: x.isFailure});
            
            /* And then the reports of successes */
            local successVec = vec.subset({x: !x.isFailure});
            
            /* 
             *   Construct a string reporting the objects we did take, 
             *   ensuring that each one is counted only once.
             */
            local str = '{You/he} take{s} ' + objectLister.makeSimpleList(
                successVec.applyAll({x: x.dobj_}).getUnique()) + '. ';  
            
            /* 
             *   Then append a report explaining the failure of all those we 
             *   didn't
             */
            foreach(local cur in failVec)
            {
                local mess = cur.messageText_;
                
                /* replace "too heavy" with "too heavy to take" */
                mess = mess.findReplace('too heavy', 'too heavy to take',
                                      ReplaceOnce);
                
                /* replace "that" or "those" with the name of the object */                      
                mess = rexReplace(thatPat, mess, cur.dobj_.theName,
                                  ReplaceAll);
                str += mess;
            }
            
            /* Return the result */
            return str;
        });           
    }
    thatPat = static new RexPattern('%<(that|those)%>')
;

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);
    }
;

Although in practice we'd probably go for something less elaborate:

modify AccompanyingInTravelState
    sayDeparting(conn)
    {
        local msg = getActor.theName + ' comes with you. ';
        mainReport(msg);
    }
;

In any case it would be more stylish to give Bob a customized AccompanyingInTravelState, with suitable custom messages:

+ bobFollowing: AccompanyingState
     getAccompanyingTravelState(traveler, connector)
    {
        
        return new BobAccompanyingInTravelState(
            getActor(), gActor, getActor().curState);
    }
;

class BobAccompanyingInTravelState: AccompanyingInTravelState
    sayDeparting(conn)
    {
        gMessageParams(conn);
        local msg = '{The bob/he} follows ';
        if(conn == theatre.west)
            msg += 'you out of the theatre. ';
        else if(conn.ofKind(StairwayDown))
            msg += 'you down {the conn/him}. ';
        else if(conn.ofKind(ThroughPassage))
            msg += 'you through {the conn/him}. ';
        else   
            msg += 'along behind you. ';
        mainReport(msg);
    }
    
    specialDesc = "{The bob/he} tags along behind. "
;

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):

modify AccompanyingState
    beforeTravel(traveler, connector)
    {
        gAction.callAfterActionMain(self); 
        inherited(traveler, connector);
    }

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.