TADS 3 Action Report Combiner

Version 4
By Krister Fundin

Introduction

This extension provides a relatively simple way of combining several consecutive action reports (messages generated by the story) into one. It works similarly to the summarizeAction() method on command transcripts, but is designed to combine several different reports instead of several similar ones (though the latter is also possible) and to combine reports from separate nested actions, such as those generated by pre-conditions.

The main purpose of the extension is to produce better-sounding prose for activities carried out by actors other than the player. For instance, two consecutive reports like these:

Bob opens the front door.

Bob leaves through the front door.

could be summarized in one sentence:

Bob opens the front door and leaves.

Note that the extension itself does not define any rules that combine standard reports from the adv3 library; it merely provides a framework for doing so. However, the samples directory contains a few working examples, including one that combines the above mentioned reports.

Basic usage

To start combining reports, we add the library file combine.tl to our project. Next, we create a CombineReportsRule object for each combination of reports that we would like to summarize.

The CombineReportsRule object must define two properties. The first one, called pattern, specifies a sequence of reports that the rules matches, and the second one, called combineReports, is the method that does the actual combining.

The pattern property should contain a list of function pointers (typically in the form of anonymous functions). Each of these functions should take a single argument — a report object — and return true if the report is of the correct type, nil if not.

A sequence of reports is matched by a rule if the first function returns true for the first report, the second function for the second report, and so forth. There can be any number of pattern items.

When a matching sequence is found, the combineReports() method is called, and the actual reports are passed as arguments. This means that if the rule has two pattern items, then combineReports() will always be called with two arguments, and should therefore be defined as taking two parameters.

The combineReports() method may return either a new report object to replace the matching sequence with, or just a message string that will be wrapped in a MainCommandReport. It can also return nil if it decides not to combine the reports after all.

Matching many similar reports

It is possible to specify that a pattern item can match more than one report. This is done by putting the multi keyword in the slot after the pattern item in question. The rule will then try to match as many consecutive reports of the given type as possible. Note that we can put several of these in the same pattern if we want to.

When combineReports() is called, the matching reports for multi pattern items are wrapped in lists. Thus, the combineReports() method of a rule with two pattern items will still be passed two arguments, even if one or both of the items matched several reports.

One thing that we may want to check, though, is that the pattern item actually did match several reports. If we want to combine two or more reports of the same kind, then we’re not interested in the case where the multi pattern item only matched a single report. If we find it to be so, then we can return nil from combineReports() to signal that we don’t really want to summarize anything.

Skippable reports

There are some reports that are generated frequently, but which are mostly irrelevant for the tasks that this extension is intended for. The most common of these reports is the command separator, which displays a paragraph break between the reports of two separate actions. Suppose that the output of the story looks like this:

Bob takes the apple.

Bob eats the apple.

If we want to combine these reports into one, then we should be aware of the fact that there are actually three consecutive reports here, not two. The actual transcript looks something like this:

  1. A DefaultCommandReport for the Take action.
  2. A CommandSepAnnouncement (I.E. a paragraph break).
  3. A DefaultCommandReport for the Eat action.

It would be tiresome to have to include these separators in every pattern that we write. Therefore, this extension has a notion of skippable reports. These are allowed to occur between the reports of a matching report sequence. When the sequence is summarized, the skippable reports are removed as well, but we don’t have to worry about matching them.

We can decide which reports to consider as skippable by overriding the canSkipReport() method of a CombineReportsRule. This method takes a report as the only argument and returns true if it should be skipped or nil if not.

Specifying precedence among rules

Sometimes one will want to control the order in which different rules are applied. This can be because one rule generates reports which can be further summarized by a second rule, or because one wants to try to combine a longer sequence first, then a shorter subset if that failed.

Precedence can be specified in the same way as for PreinitObjects and their cousins. A rule may define the execBeforeMe property as a list of rules that must be applied before it, and it may define the execAfterMe property as a list of rules that it must be applied before.

Using CustomReports

When trying to combine certain messages, we will sometimes find that the reports produced by the adv3 don’t always contain all the information that we need, and even when they do, we may have to dig uncomfortably deep in order to find it.

The best way to get around this problem is often to create a new report class which encapsulates all relevant information about a message. Then we can locate the place in the library where the original report is generated and change it to generate a report from our own class.

In the end, though, these custom reports and the rules for matching them often end up looking very similar, which is why this extension also provides two classes with which we can save a lot of typing: the CustomReport class and the CombineCustomReportsRule class.

A CustomReport has two components: a single superclass and a list of arguments. The meaning of the argument list depends on the superclass, and taken together, these two contain all the information necessary in order to generate the message of the report. For instance, a message about putting an object on top of another could be represented by this report:

PutOnReport, actor, object, destination.

Here we have first the type of the message, and then the details that may vary from one instance to another. The actual arguments could be something like bob, apple, kitchenTable.

Setting up a new CustomReport class is very simple. It needs only one method: getMessage(). When an instance of the report class is created, the arguments which are sent to the constructor are passed on to this method, which should return a string giving the message of the report based on the arguments in question. The PutOnReport above might look like this:

class PutOnReport: DefaultCustomReport
    getMessage(who, what, where)
    {
        gMessageParams(who, what, where);

        return '{The who/he} put{s} {the what/him} on
            {the where/him}. ';
    }
;

Note that we used the DefaultCustomReport class here. A regular CustomReport is otherwise assumed to be a main command report.

To actually generate one of these reports, we can use the customReport() macro, in which case we must include combine.h from that source file. The syntax is as follows:

customReport(PutOnReport, bob, apple, kitchenTable);

Matching CustomReports

Now that we know how to create CustomReports, it’s time to start combining them. We could stick with the usual methods, of course. The arguments are stored in a property called args_, so we could look at them directly if we wanted to. A much better way, though, is to use a CombineCustomReportsRule.

The time it takes to type in that name is soon compensated for by the ease with which we can write sequence patterns for these rules. Since all CustomReports look the same — they have a class and an argument list — we can use a specialized way of matching them, based on the canonical order of the arguments.

In a CombineCustomReportsRule, a pattern item is a list instead of a function. The list should look exactly like the definition of the report it is to match: first the class, then the arguments. The list items repesenting the arguments can be of several types. The most useful type is the string.

A string value as an argument item means that the corresponding argument from the report should be extracted and remembered for later use, but only if the same string hasn’t been encountered before. If it has, then the rule checks if the previous argument had the same value as the current one. If not, then the report isn’t matched. This means that we now have an almost trivial way of ensuring that different arguments from different reports share the same value.

An argument item can also be a string wrapped in a list. As with a plain string, this means that the corresponding arguments should be remembered, but they are not required to be the same. Instead, they are collected in a list. This allows us to use the multi keyword in these rules.

Furthermore, an argument item can be nil, in which case any value is accepted (but not remembered) for the corresponding argument, and if it’s anything else than these special types mentioned here, the rule simply checks if it’s equal to the argument, and rejects the report if not.

As a simple example of CustomReport matching, we can create a pattern for matching one or more messages about putting objects in a container followed by a message about closing the same container:

pattern =
[
    [PutInReport, 'who', ['objs'], 'container'], multi,
    [CloseReport, 'who', 'container']
]

This pattern automatically ensures that the actor and the container are the same for all the reports, including all the ones matched by the multi item.

The last thing to take care of is the combineReports() method. When using these rules, there is another method that is easier to use. It’s called combineCustomReports() and works the same way, except that the arguments are different. Instead of being passed the matching reports, this method is passed the values of all the strings used in the pattern, in order of initial appearance. Therefore, the above pattern would cause three arguments to be passed to combineCustomReports(), namely the value of who, the list of all the values of objs and finally the value of container.

Tracing reports

Though we may know how to combine reports in theory, we also have to track down the particular reports that we want to combine. To make this job easier, this extension provides a special debugging mode called report tracing. To use this mode, the file reflect.t from the TADS 3 system library must be part of our project.

During a debugging session, report tracing can be activated by typing “report-debug” at the command prompt. The same command will deactivate it again.

When in report tracing mode, reports will not simply be displayed. Instead, a brief summary of each report is printed. The summary starts with the class of the report, and for some reports, this is all. For command reports, the action that generated the report is also displayed, and for standard library messages, there is usually a message property as well. Finally, the message text is printed.

The output of the report tracing mode is designed to look a bit like a chain of object definitions. The properties action_, messageProp_ and messageText_ of a report will be set up in the way displayed by the trace, and can thus be used to form a pattern for a rule.

At the end of each trace, there is a line that says “*** end ***”. In some cases (E.G. when a daemon prints something or an actor is talking to the player), there can be several traces in a single turn. We’ll be able to see this, because after the ending line, there are more report summaries and then another ending line. The important thing about this is that these blocks of reports are completely separate from each other: the first block is displayed before the second block is generated. Therefore, we cannot combine reports from two different blocks.