report.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library: command reports
*
* This module defines the "command report" classes, which the command
* execution engine uses to keep track of the status of a command.
*/
#include "adv3.h"
/* ------------------------------------------------------------------------ */
/*
* Command report objects. The library uses these to control how the
* text from a command is displayed. Game code can also use report
* objects to show and control command results, but this isn't usually
* necessary; game code can usually simply display messages directly.
*
* 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.
*
* Note that a full report is warranted any time a command describes
* anything beyond a simple confirmation. In our door-opening button
* example, opening the door by pushing the button always warrants a
* full report, even if the player has already seen the effects of the
* button a hundred times before, and even if the button is labeled
* "push to open door." It doesn't matter whether or not the
* consequences of the command ought to be obvious to the player; what
* matters is that the command warrants a report beyond a simple
* confirmation. Any time a report is more than a simple confirmation,
* it is a full report, no matter how obvious to the player the effects
* of the action.
*
* 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.
*/
class CommandReport: object
construct()
{
/*
* remember the action with which we're associated, unless a
* subclass already specifically set the action
*/
if (action_ == nil)
action_ = gAction;
}
/* get/set my action */
getAction() { return action_; }
setAction(action) { action_ = action; }
/* check to see if my action is implicit */
isActionImplicit() { return action_ != nil && action_.isImplicit; }
/* check to see if my action is nested in the other report's action */
isActionNestedIn(other)
{
return (action_ != nil
&& other.getAction() != nil
&& action_.isNestedIn(other.getAction()));
}
/*
* Flag: if this property is true, this report indicates a failure.
* By default, a report does not indicate failure.
*/
isFailure = nil
/*
* Flag: if this property is true, this report indicates an
* interruption for interactive input.
*/
isQuestion = nil
/* iteration number current when we were added to the transcript */
iter_ = nil
/* the action I'm associated with */
action_ = nil
/*
* Am I part of the same action as the given report? Returns true if
* this action is part of the same iteration and part of the same
* action as the other report.
*/
isPartOf(report)
{
/*
* if I don't have an action, or the other report doesn't have an
* action, we're not related
*/
if (action_ == nil || report.action_ == nil)
return nil;
/* if our iterations don't match, we're not related */
if (iter_ != report.iter_)
return nil;
/* check if I'm part of the other report's action */
return action_.isPartOf(report.action_);
}
;
/*
* Group separator. 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.
*/
class GroupSeparatorMessage: CommandReport
construct(report)
{
/* use the same action and iteration as the given report */
action_ = report.getAction();
iter_ = report.iter_;
}
/* show the normal command results separator */
showMessage() { say(gLibMessages.complexResultsSeparator); }
;
/*
* Internal separator. 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.
*/
class InternalSeparatorMessage: CommandReport
construct(report)
{
/* use the same action and iteration as the given report */
action_ = report.getAction();
iter_ = report.iter_;
}
/* show the normal command results separator */
showMessage() { say(gLibMessages.internalResultsSeparator); }
;
/*
* 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.
*/
class MarkerReport: CommandReport
showMessage() { }
;
/*
* 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.
*/
class EndOfDescReport: MarkerReport
;
/*
* Simple MessageResult-based command report
*/
class CommandReportMessage: CommandReport, MessageResult
construct([params])
{
/* invoke our base class constructors */
inherited CommandReport();
inherited MessageResult(params...);
}
;
/*
* default report
*/
class DefaultCommandReport: CommandReportMessage
;
/*
* extra information report
*/
class ExtraCommandReport: CommandReportMessage
;
/*
* default descriptive report
*/
class DefaultDescCommandReport: CommandReportMessage
;
/*
* cosmetic spacing report
*/
class CosmeticSpacingCommandReport: CommandReportMessage
;
/*
* base class for all "full" reports
*/
class FullCommandReport: CommandReportMessage
/*
* a full report has a sequence number that tells us where the
* report goes relative to the others - the higher this number, the
* later the report goes
*/
seqNum = nil
;
/*
* "before" report - these come before the main report
*/
class BeforeCommandReport: FullCommandReport
seqNum = 1
;
/*
* main report
*/
class MainCommandReport: FullCommandReport
seqNum = 2
;
/*
* failure report
*/
class FailCommandReport: FullCommandReport
seqNum = 2
isFailure = true
;
/*
* failure marker - this is a silent report that marks an action as
* having failed without actually generating any message text
*/
class FailCommandMarker: MarkerReport
isFailure = true
;
/*
* "after" report - these come after the main report
*/
class AfterCommandReport: FullCommandReport
seqNum = 3
;
/*
* 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.
*/
class QuestionCommandReport: MainCommandReport
isQuestion = true;
;
/*
* 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.
*/
class ConvBoundaryReport: CommandReport
construct(id) { actorID = id; }
/* the actor's ID number, as assigned by the ConversationManager */
actorID = nil
;
class ConvBeginReport: ConvBoundaryReport
showMessage() { say('<.convbegin ' + actorID + '>'); }
;
class ConvEndReport: ConvBoundaryReport
construct(id, node)
{
inherited(id);
defConvNode = node;
}
showMessage()
{
if (actorID != nil)
say('<.convend ' + actorID + ' '
+ (defConvNode == nil ? '' : defConvNode) + '>');
}
/* the default new ConvNode for the actor */
defConvNode = nil
;
/* ------------------------------------------------------------------------ */
/*
* Announcements. We use these to track announcements to be made as
* part of an action's results.
*/
class CommandAnnouncement: CommandReport
construct([params])
{
/* inherit default handling */
inherited();
/* remember our text */
messageText_ = getMessageText(params...);
}
/*
* Get our message text. By default, we simply get the gLibMessages
* message given by the property.
*/
getMessageText([params])
{
/* get the library message */
return gLibMessages.(messageProp_)(params...);
}
/*
* Show our message. Our default implementation shows the library
* message given by our messageProp_ property, using the parameters
* we stored in our constructor.
*/
showMessage()
{
/* call gLibMessages to show our message */
say(messageText_);
}
/* our gLibMessages property */
messageProp_ = nil
/* our message text */
messageText_ = ''
;
/*
* Multiple-object announcement. 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.
*/
class MultiObjectAnnouncement: CommandAnnouncement
construct(preCalcMsg, obj, whichObj, action)
{
/* do the inherited work */
inherited(obj, whichObj, action);
/*
* if we have a pre-calculated message, use it instead of the
* message we just generated - this lets the caller explicitly
* set the message as desired
*/
if (preCalcMsg != nil)
messageText_ = preCalcMsg;
}
/* show the announceMultiActionObject message */
messageProp_ = &announceMultiActionObject
;
/*
* Default object announcement. We display this announcement 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. In cases where the object role involves a prepositional
* phrase in the verb structure, we generally show the preposition before
* the object name. This format usually reads intuitively, by combining
* with the text just above of the player's own command:
*
*. >open
*. (the door>
*. You try opening the door, but it seems to be locked.
*.
*. >unlock the door
* (with the key)
*/
class DefaultObjectAnnouncement: CommandAnnouncement
construct(obj, whichObj, action, allResolved)
{
/* remember our object */
obj_ = obj;
/* remember the message parameters */
whichObj_ = whichObj;
allResolved_ = allResolved;
/* remember my action */
action_ = action;
/* inherit default handling */
inherited();
}
/* get our message text */
getMessageText()
{
/* get the announcement message from our object */
return obj_.announceDefaultObject(whichObj_, action_, allResolved_);
}
/* our defaulted object */
obj_ = nil
/* our message parameters */
whichObj_ = nil
allResolved_ = nil
;
/*
* Ambiguous object announcement. 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.
*/
class AmbigObjectAnnouncement: CommandAnnouncement
/* show the announceAmbigObject announcement */
messageProp_ = &announceAmbigActionObject
;
/*
* 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.
*/
class RemappedActionAnnouncement: CommandAnnouncement
construct()
{
/* use the action as the message parameter */
inherited(gAction);
}
messageProp_ = &announceRemappedAction
;
/*
* Each language module must define a class called
* ImplicitAnnouncementContext, and three instances of the class, for use
* by the generic library. The language module can define other
* instances of the context class as needed. We minimally need the
* following instances to be defined by the language module:
*
* standardImpCtx: this is the standard context, which indicates that we
* want the default format for the implicit action announcement.
*
* tryingImpCtx: this is the "trying" context, which indicates that we
* want the announcement to phrase the action to indicate that we're only
* trying the action, not actually performing it. We use this when the
* implicit action has failed, in which case we want our announcement to
* say that we're merely attempting the action; the announcement
* shouldn't imply that the action has actually been performed.
*
* askingImpCtx: this is the "asking" context, which indicates that the
* action was interrupted with an interactive question, such as a prompt
* for a missing direct object.
*
* We leave it up to the language module to define the class and these
* two instances. This lets the language module represent the context
* types any way it likes.
*/
/*
* Implicit action announcement. 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)").
*/
class ImplicitActionAnnouncement: CommandAnnouncement
construct(action, msg)
{
/* use the given message property */
messageProp_ = msg;
/*
* Inherit default. The first message parameter is the action;
* the second is our standard implicit action context object,
* indicating that we want the normal context.
*/
inherited(action, standardImpCtx);
}
/*
* Make this announcement silent. This eliminates any announcement
* for this action, but makes it otherwise behave like a normal
* implied action.
*/
makeSilent()
{
/* clear my message text */
messageText_ = '';
/*
* use the silent announcement message if we have to regenerate
* our text for another context
*/
messageProp_ = &silentImplicitAction;
}
/*
* Note that the action we're attempting is merely an attempt that
* failed. This will change our report to indicate that we're only
* trying the action, rather than suggesting that we actually carried
* it out.
*/
noteJustTrying()
{
/* note that we're just trying the action */
justTrying = true;
/* change our message to the "trying" form */
messageText_ = getMessageText(getAction(), tryingImpCtx);
}
/*
* Note that the action we're attempting is incomplete, as it was
* interupted for interactive input (such as asking for a missing
* object).
*/
noteQuestion()
{
/* note that the action was interrupted with a question */
justAsking = true;
/* change our message to the "asking" form */
messageText_ = getMessageText(getAction(), askingImpCtx);
}
/*
* Flag: we're just attempting the action; this is set when we
* determine that the implicit action has failed, in which case we
* want an announcement indicating that we're merely attempting the
* action, not actually performing it. Presume that we're actually
* going to perform the action; the action can change this if
* necessary.
*/
justTrying = nil
/* flag: the action was interrupted with an interactive question */
justAsking = nil
;
class CommandSepAnnouncement: CommandAnnouncement
construct()
{
/* we're not associated with an iteration or action */
action_ = nil;
iter_ = 0;
}
showMessage()
{
/* show a command separator */
"<.commandsep>";
}
;
/* ------------------------------------------------------------------------ */
/*
* Command Transcript. This is a "semantic transcript" of the results of
* a command. This provides a list of CommandReport objects describing
* the results of the command.
*/
class CommandTranscript: OutputFilter
construct()
{
/* set up a vector to hold the reports */
reports_ = new Vector(5);
}
/*
* flag: the command has failed (i.e., at least one failure report
* has been generated)
*/
isFailure = nil
/*
* Note that the current action has failed. This is equivalent to
* adding a reportFailure() message to the transcript.
*/
noteFailure()
{
/* add an empty reportFailure message */
reportFailure('');
}
/*
* Did the given action fail? This scans the transcript to determine
* if there are any failure messages associated with the given
* action.
*/
actionFailed(action)
{
/*
* scan the transcript for failure messages that are associated
* with the given action
*/
return reports_.indexWhich(
{x: x.isPartOf(action) && x.isFailure}) != nil;
}
/*
* flag: I'm active; when this is nil, we'll pass text through our
* filter routine unchanged
*/
isActive = true
/*
* Summarize the current action's reports. This allows a caller to
* turn a series of iterated reports into a single report for the
* entire action. 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.
*
* This function 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, we'll remove the reports that
* 'cond' accepted, and we'll remove the multiple-object announcement
* reports associated with them, and we'll insert 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.
*/
summarizeAction(cond, report)
{
local vec = new Vector(8);
local rpt = reports_;
local cnt = rpt.length();
local i;
/* find the first report for the current action */
for (i = 1 ; i <= cnt && rpt[i].getAction() != gAction ; ++i) ;
/* iterate over the transcript for the current action */
for ( ; ; ++i)
{
local ok;
/* presume we won't find a summarizable item */
ok = nil;
/* if we're still in range, check what we have */
if (i <= cnt)
{
/* get the current item */
local cur = rpt[i];
/* if this one is of interest, note it in the vector */
if (cond(cur))
{
/* add it to our vector of summarizable reports */
vec.append(cur);
/* note that we're okay on this round */
ok = true;
}
else if (cur.ofKind(ImplicitActionAnnouncement)
|| cur.ofKind(MultiObjectAnnouncement)
|| cur.ofKind(DefaultCommandReport)
|| cur.ofKind(ConvBoundaryReport))
{
/* we can keep these in summaries */
ok = true;
}
}
/*
* If this item isn't summarizable, or we've reached the last
* item, generate a summary of any we have so far. (We need
* to generate the summary on reaching the last item because
* there are no further items that could go in the summary.)
*/
if (!ok || i == cnt)
{
/* if we have two or more items, generate a summary */
if (vec.length() > 1)
{
local insIdx;
local txt;
/* remove each item in the vector */
foreach (local cur in vec)
{
local idx;
/* get the index of the current item */
idx = rpt.indexOf(cur);
/*
* we're summarizing this item, so remove the
* individual item - subsume it into the summary
*/
rpt.removeElementAt(idx);
--i;
--cnt;
/* insert the summary here */
insIdx = idx;
/*
* skip any implicit action announcements,
* default command announcements, and
* conversational boundary markers
*/
for (--idx ;
idx > 0
&& (rpt[idx].ofKind(ImplicitActionAnnouncement)
|| rpt[idx].ofKind(DefaultCommandReport)
|| rpt[idx].ofKind(ConvBoundaryReport)) ;
--idx) ;
/*
* if the preceding element is a multi-object
* announcement, remove it - let the summary
* mention the individual objects if it wants to
*/
if (idx > 0
&& rpt[idx].ofKind(MultiObjectAnnouncement))
{
/* remove this element and adjust the counters */
rpt.removeElementAt(idx);
--i;
--cnt;
--insIdx;
/*
* If this leaves us with a <.convbegin>
* preceded directly by a <.convend> for the
* same actor, the two cancel each other out.
* Simply remove both of them.
*/
if (idx <= rpt.length()
&& idx > 1
&& rpt[idx].ofKind(ConvBeginReport)
&& rpt[idx-1].ofKind(ConvEndReport)
&& rpt[idx].actorID == rpt[idx-1].actorID)
{
/*
* we have a canceling <.convend> +
* <.convbegin> pair - simply remove them
* both and adjust the counters
* accordingly
*/
rpt.removeRange(idx - 1, idx);
i -= 2;
cnt -= 2;
insIdx -= 2;
}
}
}
/* ask the caller for the summary */
txt = report(vec);
/* insert it */
rpt.insertAt(insIdx, new MainCommandReport(txt));
++cnt;
++i;
}
/* clear the vector */
if (vec.length() > 0)
vec.removeRange(1, vec.length());
}
/* if we've reached the end of the list, we're done */
if (i > cnt)
break;
}
}
/* activate - set up to capture output */
activate()
{
/* make myself active */
isActive = true;
}
/* deactivate - stop capturing output */
deactivate()
{
/* make myself inactive */
isActive = nil;
}
/*
* Count an iteration. An Action should call this once per iteration
* if it's a top-level (non-nested) command.
*/
newIter() { ++iter_; }
/*
* Flush the transcript in preparation for reading input. This shows
* all pending reports, clears the backlog of reports (so that we
* don't show them again in the future), and deactivates the
* transcript's capture feature so that subsequent output goes
* directly to the output stream.
*
* We return the former activation status - that is, we return true
* if the transcript was activated before the call, nil if not.
*/
flushForInput()
{
/* show our reports, and deactivate output capture */
local wasActive = showReports(true);
/* clear the reports, since we've now shown them all */
clearReports();
/* return the previous activation status */
return wasActive;
}
/*
* Show our reports. Returns true if the transcript was previously
* active, nil if not.
*/
showReports(deact)
{
local wasActive;
/*
* remember whether we were active or not originally, then
* deactivate (maybe just temporarily) so that we can write out
* our reports without recursively intercepting them
*/
wasActive = isActive;
deactivate();
/* first, apply all defined transformations to our transcript */
applyTransforms();
/*
* Temporarily cancel any sense context message blocking. We
* have already taken into account for each report whether or
* not the report was visible when it was generated, so we can
* display each report that made it past that check without any
* further conditions.
*/
callWithSenseContext(nil, nil, function()
{
/* show the reports */
foreach (local cur in reports_)
{
/* if we're allowed to show this report, show it */
if (canShowReport(cur))
cur.showMessage();
}
});
/*
* if we were active and we're not being asked to deactivate,
* re-activate now that we're finished showing our reports
*/
if (wasActive && !deact)
activate();
/* return the former activation status */
return wasActive;
}
/*
* Add a report.
*/
addReport(report)
{
/* check for a failure report */
if (report.isFailure)
{
/* set the failure flag for the entire command */
isFailure = true;
/*
* If this is an implied command, and the actor is in "NPC
* mode", suppress the report. When an implied action fails
* in NPC mode, we act as though we never attempted the
* action.
*/
if (gAction.isImplicit && gActor.impliedCommandMode() == ModeNPC)
{
/* add a failure marker, not the message report */
reports_.append(new FailCommandMarker());
/* that's all we need to add */
return;
}
}
/*
* Do not queue reports made while the sense context is blocking
* output because the player character cannot sense the locus of
* the action. Note that this check comes before we queue the
* report, but after we've noted any effect on the status of the
* overall action; even if we're not going to show the report,
* its status effects are still valid.
*/
if (senseContext.isBlocking)
return;
/*
* If the new report's iteration ID hasn't been set already, note
* the current iteration in the report. Some types of reports
* will have already set a specific iteration before we get here,
* so set the iteration ID only if the report hasn't done so
* already.
*/
if (report.iter_ == nil)
report.iter_ = iter_;
/* append the report */
reports_.append(report);
}
/* get the last report added */
getLastReport()
{
local cnt = reports_.length();
return (cnt == 0 ? nil : reports_[cnt]);
}
/* delete the last report added */
deleteLastReport()
{
local cnt = reports_.length();
if (cnt != 0)
reports_.removeElementAt(cnt);
}
/*
* Add a marker report. This adds a marker to the report stream,
* and returns the marker object. The marker doesn't show any
* message in the final display, but callers can use a pair of
* markers to identify a range of reports for later reordering or
* removal.
*/
addMarker()
{
/* create the new report */
local marker = new MarkerReport();
/* add it to the stream */
addReport(marker);
/* return the new report */
return marker;
}
/* delete the reports between two markers */
deleteRange(marker1, marker2)
{
local idx1, idx2;
/* find the indices of the two markers */
idx1 = reports_.indexOf(marker1);
idx2 = reports_.indexOf(marker2);
/* if we found both, delete the range */
if (idx1 != nil && idx2 != nil)
reports_.removeRange(idx1, idx2);
}
/*
* Pull out the reports between two markers, and reinsert them at
* the end of the transcript.
*/
moveRangeAppend(marker1, marker2)
{
local idx1, idx2;
/* find the indices of the two markers */
idx1 = reports_.indexOf(marker1);
idx2 = reports_.indexOf(marker2);
/* if we didn't find both, ignore the request */
if (idx1 == nil || idx2 == nil)
return;
/* append each item in the range to the end of the report list */
for (local i = idx1 ; i <= idx2 ; ++i)
reports_.append(reports_[i]);
/* delete the original copies */
reports_.removeRange(idx1, idx2);
}
/*
* Perform a callback on all of the reports in the transcript.
* We'll invoke the given callback function func(rpt) once for each
* report, with the report object as the parameter.
*/
forEachReport(func) { reports_.forEach(func); }
/*
* End the description section of the report. This adds a marker
* report that indicates that anything following (and part of the
* same action) is no longer part of the description; this can be
* important when we apply the default description suppression
* transformation, because it tells us not to consider the
* non-descriptive messages following this marker when, for example,
* suppressing default descriptive messages.
*/
endDescription()
{
/* add an end-of-description report */
addReport(new EndOfDescReport());
}
/*
* Announce that the action is implicit
*/
announceImplicit(action, msgProp)
{
/*
* If the actor performing the command is not in "player" mode,
* save an implicit action announcement; for NPC mode, we treat
* implicit command results like any other results, so we don't
* want a separate announcement.
*/
if (gActor.impliedCommandMode() == ModePlayer)
{
/* create the new report */
local report = new ImplicitActionAnnouncement(action, msgProp);
/* add it to the transcript */
addReport(report);
/* return it */
return report;
}
else
{
/* no need for a report */
return nil;
}
}
/*
* Announce a remapped action
*/
announceRemappedAction()
{
/* save a remapped-action announcement */
addReport(new RemappedActionAnnouncement());
}
/*
* Announce one of a set of objects to a multi-object action. We'll
* record this announcement for display with our report list.
*/
announceMultiActionObject(preCalcMsg, obj, whichObj)
{
/* save a multi-action object announcement */
addReport(new MultiObjectAnnouncement(
preCalcMsg, obj, whichObj, gAction));
}
/*
* Announce an object that was resolved with slight ambiguity.
*/
announceAmbigActionObject(obj, whichObj)
{
/* save an ambiguous object announcement */
addReport(new AmbigObjectAnnouncement(obj, whichObj, gAction));
}
/*
* Announce a default object.
*/
announceDefaultObject(obj, whichObj, action, allResolved)
{
/* save the default object announcement */
addReport(new DefaultObjectAnnouncement(
obj, whichObj, action, allResolved));
}
/*
* Add a command separator.
*/
addCommandSep()
{
/* add a command separator announcement */
addReport(new CommandSepAnnouncement());
}
/*
* clear our reports
*/
clearReports()
{
/* forget all of the reports in the main list */
if (reports_.length() != 0)
reports_.removeRange(1, reports_.length());
}
/*
* Can we show a given report? By default, we always return true,
* but subclasses might want to override this to suppress certain
* types of reports.
*/
canShowReport(report) { return true; }
/*
* Filter text. If we're active, we'll turn the text into a command
* report and add it to our report list, blocking the text from
* reaching the underlying stream; otherwise, we'll pass it through
* unchanged.
*/
filterText(ostr, txt)
{
/* if we're inactive, pass text through unchanged */
if (!isActive)
return txt;
/*
* If the current sense context doesn't allow any messages to be
* generated, block the generated text entirely. We want to
* block text or not according to the sense context in effect
* now; so we must note it now rather than wait until we
* actually display the report, since the context could be
* different by then.
*/
if (senseContext.isBlocking)
return nil;
/* add a main report to our list if the text is non-empty */
if (txt != '')
addReport(new MainCommandReport(txt));
/* capture the text - send nothing to the underlying stream */
return nil;
}
/* apply transformations */
applyTransforms()
{
/* apply each defined transformation */
foreach (local cur in transforms_)
cur.applyTransform(self, reports_);
}
/*
* check to see if the current action has a report matching the given
* criteria
*/
currentActionHasReport(func)
{
/* check to see if we can find a matching report */
return (findCurrentActionReport(func) != nil);
}
/* find a report in the current action that matches the given criteria */
findCurrentActionReport(func)
{
/*
* Find an action that's part of the current iteration and which
* matches the given function's criteria. Return the first match
* we find.
*/
return reports_.valWhich({x: x.iter_ == iter_ && (func)(x)});
}
/*
* iteration number - for an iterated top-level command, this helps
* us keep the results for a particular iteration grouped together
*/
iter_ = 1
/* our vector of reports */
reports_ = nil
/* our list of transformations */
transforms_ = [defaultReportTransform, implicitGroupTransform,
reportOrderTransform, complexMultiTransform]
;
/* ------------------------------------------------------------------------ */
/*
* Transcript Transform.
*/
class TranscriptTransform: object
/*
* Apply our transform to the transcript vector. By default, we do
* nothing; each subclass must override this to manipulate the vector
* to make the change it wants to make.
*/
applyTransform(trans, vec) { }
;
/* ------------------------------------------------------------------------ */
/*
* Transcript Transform: set before/main/after report order. We'll look
* for any before/after reports that are out of order with respect to
* their main reports, and move them into the appropriate positions.
*/
reportOrderTransform: TranscriptTransform
applyTransform(trans, vec)
{
/* scan for before/after reports */
for (local i = 1, local len = vec.length() ; i <= len ; ++i)
{
/* get this item */
local cur = vec[i];
/* if this is a before/after report, consider moving it */
if (cur.ofKind(FullCommandReport) && cur.seqNum != nil)
{
local idx;
/*
* This item cares about its sequencing, so it could be
* out of order with respect to other items from the same
* sequence. Find the first item with a higher sequence
* number from the same group, and make sure this item is
* before the first such item.
*/
for (idx = 1 ; idx < i ; ++idx)
{
local x;
/* get this item */
x = vec[idx];
/* if x should come after cur, we need to move cur */
if (x.ofKind(FullCommandReport)
&& x.seqNum > cur.seqNum
&& x.isPartOf(cur))
{
/* remove cur and reinsert it before x */
vec.removeElementAt(i);
vec.insertAt(idx, cur);
/* adjust our scan index for the removal */
--i;
/* no need to look any further */
break;
}
}
}
}
}
;
/* ------------------------------------------------------------------------ */
/*
* Transcript Transform: remove unnecessary default reports. We'll scan
* the transcript for default reports for actions which also have
* implicit announcements or non-default reports, and remove those
* default reports. We'll also remove default descriptive reports which
* also have non-default reports in the same action.
*/
defaultReportTransform: TranscriptTransform
applyTransform(trans, vec)
{
/* scan for default reports */
for (local i = 1, local len = vec.length() ; i <= len ; ++i)
{
local cur;
/* get this item */
cur = vec[i];
/*
* if this is a default report, check to see if we want to
* keep it
*/
if (cur.ofKind(DefaultCommandReport))
{
/*
* check for a main report or an implicit announcement
* associated with the same action; if we find anything,
* we don't need to keep the default report
*/
if (vec.indexWhich(
{x: (x != cur
&& cur.isPartOf(x)
&& (x.ofKind(FullCommandReport)
|| x.ofKind(ImplicitActionAnnouncement)))
}) != nil)
{
/* we don't need this default report */
vec.removeElementAt(i);
/* adjust our scan index for the removal */
--i;
--len;
}
}
/*
* if this is a default descriptive report, check to see if
* we want to keep it
*/
if (cur.ofKind(DefaultDescCommandReport))
{
local fullIdx;
/*
* check for a main report associated with the same
* action
*/
fullIdx = vec.indexWhich(
{x: (x != cur
&& cur.isPartOf(x)
&& x.ofKind(FullCommandReport))});
/*
* if we found another report, check to see if it comes
* before or after any 'end of description' for the same
* action
*/
if (fullIdx != nil)
{
local endIdx;
/* find the 'end of description' report, if any */
endIdx = vec.indexWhich(
{x: (x != cur
&& cur.isPartOf(x)
&& x.ofKind(EndOfDescReport))});
/*
* if we found a full report before the
* end-of-description report, then the full report is
* part of the description and thus should suppress
* the default report; otherwise, the description
* portion includes only the default report and the
* default report should thus remain
*/
if (endIdx == nil || fullIdx < endIdx)
{
/* don't keep the default descriptive report */
vec.removeElementAt(i);
/* adjust our indices for the removal */
--i;
--len;
}
}
}
}
}
;
/* ------------------------------------------------------------------------ */
/*
* Transcript Transform: group implicit announcements. We'll find any
* runs of consecutive implicit command announcements, and group each run
* into a single announcement listing all of the implied actions. For
* example, we'll turn this:
*
*. >go south
*. (first opening the door)
*. (first unlocking the door)
*
* this into:
*
*. >go south
*. (first opening the door and unlocking the door)
*
* In addition, if we find an implicit announcement in the middle of a
* set of regular command reports, and it's for an action nested within
* the action generating the regular reports, we'll start a new paragraph
* before the implicit announcement.
*/
implicitGroupTransform: TranscriptTransform
applyTransform(trans, vec)
{
/*
* Scan for implicit announcements whose actions failed, and mark
* the implicit actions as such. This allows us to phrase the
* implicit announcements as attempts rather than as actual
* actions, which sounds a little better because it doesn't clash
* with the failure report that immediately follows.
*/
for (local i = 1, local len = vec.length() ; i <= len ; ++i)
{
local sub;
/* get this item */
local cur = vec[i];
/*
* If this is an implicit action announcement, and its
* corresponding action (or any nested action) failed, mark
* the implicit announcement as a mere attempt. Likewise, if
* we're interrupting the action for interactive input, it's
* likewise just an incomplete attempt.
*/
if (cur.ofKind(ImplicitActionAnnouncement)
&& (sub = vec.valWhich(
{x: ((x.isFailure || x.isQuestion)
&& (x.isPartOf(cur)
|| x.isActionNestedIn(cur)))})) != nil)
{
/*
* it's either a failed attempt or an interruption for a
* question - note which one
*/
if (sub.isFailure)
cur.noteJustTrying();
else
cur.noteQuestion();
}
}
/*
* Scan for implicit announcement groups. Since we're only
* scanning for runs of two or more announcements, we can stop
* scanning one short of the end of the list - there's no need to
* check the last item because it can't possibly be followed by
* another item. Thus, scan while i < len.
*/
for (local i = 1, local len = vec.length() ; i < len ; ++i)
{
local origI = i;
/* get this item */
local cur = vec[i];
/*
* If it's an implied action announcement, and the next one
* qualifies for group inclusion, build a group. Note that
* because we only loop until we reach the second-to-last
* item, we know for sure there is indeed a next item to
* index here.
*/
if (cur.ofKind(ImplicitActionAnnouncement)
&& canGroupWith(cur, vec[i+1]))
{
local j;
local groupVec;
/* create a vector to hold the re-sorted group listing */
groupVec = new Vector(16);
/*
* Scan items for grouping. This time, we want to scan
* to the last (not second-to-last) item in the main
* list, since we could conceivably group everything
* remaining.
*/
for (j = i ; j <= len ; )
{
/* get this item */
cur = vec[j];
/* unstack any recursive grouping */
j = unstackRecursiveGroup(groupVec, vec, j);
/*
* if we've used now everything in the list, or the
* next item can't be grouped with the current item,
* we're done
*/
if (j > len || !canGroupWith(cur, vec[j]))
break;
}
/* process default object announcements */
processDefaultAnnouncements(groupVec);
/* build the composite message for the entire group */
vec[i].messageText_ = implicitAnnouncementGrouper
.compositeMessage(groupVec);
/*
* Clear the messages in the second through last grouped
* announcements. Leave the report objects themselves
* intact, so that our internal structural record of the
* transcript remains as it was, but make them silent in
* the displayed text, since these messages are now
* subsumed into the combined first message.
*/
for (++i ; i < j ; ++i)
vec[i].messageText_ = '';
/*
* continue the main loop from the next element after the
* last one we included in the group
*/
i = j - 1;
}
/*
* If this is an implied action or default object
* announcement that interrupts a set of regular command
* reports, and it's for an action nested within the action
* generating the reports, add a paragraph spacer before the
* implicit announcement.
*/
if ((cur.ofKind(ImplicitActionAnnouncement)
|| cur.ofKind(DefaultObjectAnnouncement))
&& cur.messageText_ != '')
{
local j;
/* scan back for the nearest announcement with text */
for (j = origI - 1 ; j >= 1 && vec[j].messageText_ == '' ;
--j) ;
/*
* if it's a regular command report, and our implied or
* default announcement is nested within it, add a
* paragraph spacer
*/
if (j >= 1
&& vec[j].ofKind(FullCommandReport)
&& cur.isActionNestedIn(vec[j]))
{
/*
* insert a paragraph spacer before the announcement
* - this will make the implied action and its
* results stand out as separate actions, rather than
* running everything together without spacing
*/
vec.insertAt(origI, new GroupSeparatorMessage(cur));
/* adjust our indices for the insertion */
++i;
++len;
}
}
}
}
/*
* "Unstack" a recursive group of nested announcements. Adds the
* recursive group to the output group vector in chronological order,
* and returns the index of the next item after the recursive group.
*
* A recursive group is a set of nested implicit commands, where one
* implicit command triggered another, which triggered another, and
* so on. The innermost of the nested set is the one that's actually
* executed first chronologically, since an implied command must be
* carried out before its enclosing command can proceed. For
* example:
*
*. >go south
*. (first opening the door)
*. (first unlocking the door)
*. (first taking the key out of the bag)
*
* Going south implies opening the door, but before we can open the
* door, we must unlock it, and before we can unlock it we must be
* holding the key. In report order, the innermost command is listed
* last, since it's nested within the enclosing commands.
* Chronologically, though, the innermost command is actually
* executed first. The purpose of this routine is to unstack these
* nested sets, rearranging them into chronological order.
*/
unstackRecursiveGroup(groupVec, vec, idx)
{
local cur;
/* remember the item we're tasked to work on */
cur = vec[idx];
/* skip the current item */
++idx;
/*
* Scan for items nested within vec[idx]. Process each child
* item first. An item is nested within us if can be grouped
* with us, and its action is a child of our action.
*/
for (local len = vec.length() ; idx <= len ; )
{
/* if the next item is nested within 'cur', process it */
if (canGroupWith(cur, vec[idx])
&& vec[idx].getAction().isNestedIn(cur.getAction()))
{
/*
* It's nested with us - process it recursively. Since
* our goal is to unstack these reports into
* chronological order, we must process our children
* first, so that they get added to the group vector
* first, since children chronologically predede their
* parents.
*/
idx = unstackRecursiveGroup(groupVec, vec, idx);
}
else
{
/* it's not nested within us, so we're done */
break;
}
}
/* add our item to the result vector */
groupVec.append(cur);
/*
* return the index of the next item; this is simply the current
* 'idx' value, since we've advanced it past each item we've
* processed
*/
return idx;
}
/*
* Process default object announcements in a grouped message vector.
*
* Default object announcements come in two flavors: with and without
* message text. Those without message text are present purely to
* retain a structural record of the default object in the internal
* transcript; we can simply remove these, since the actions that
* created them didn't even want default messages. For those that do
* include message text, remove them as well, but also use their
* actions to replace the corresponding parent actions, so that the
* parent actions reflect what actually happened with the final
* defaulted objects.
*/
processDefaultAnnouncements(vec)
{
/* scan the vector for default announcements */
for (local i = 1, local len = vec.length() ; i <= len ; ++i)
{
local cur = vec[i];
/* if this is a default announcement, process it */
if (cur.ofKind(DefaultObjectAnnouncement))
{
/*
* If it has a message, use its action to replace the
* parent action. The only way an implied command can
* have a defaulted object is for the implied command to
* have been stated with too few objects, so that an
* askForIobj (for example) occurred. In such cases, the
* default announcement will be a child action of the
* original underspecified action, so we can simply find
* the original action and replace it with the defaulted
* action.
*/
if (cur.messageText_ != '')
{
/*
* Scan for the parent announcement.
*
* Note that the implicit announcement containing the
* parent action will follow the default announcement
* in the result list, since the default announcement
* is a child of the parent.
*/
for (local j = i + 1 ; j <= len ; ++j)
{
/* if this is the parent action, replace it */
if (vec[j].getAction()
== cur.getAction().parentAction)
{
/* this is it - replace the action */
vec[j].setAction(cur.getAction());
/* no need to look any further */
break;
}
}
}
/* remove the default announcement from the list */
vec.removeElementAt(i);
/* adjust our list index and length for the deletion */
--i;
--len;
}
}
}
/*
* Can we group the second item with the first? Returns true if the
* second item is also an implicit action announcement, or it's a
* default object announcement whose parent action is the first
* item's action.
*/
canGroupWith(a, b)
{
/* if 'b' is also an implicit announcement, we can include it */
if (b.ofKind(ImplicitActionAnnouncement))
return true;
/*
* if 'b' is a default object announcement, and has the same
* parent action as 'a', then we can group it; otherwise we can't
*/
return (b.ofKind(DefaultObjectAnnouncement)
&& b.getAction().parentAction == a.getAction());
}
;
/* ------------------------------------------------------------------------ */
/*
* Transcript Transform: Complex Multi-object Separation. If we have an
* action that's being applied to one of a bunch of iterated objects, and
* the action has any implied command announcements associated with it,
* we'll set off the result for this command from its preceding and
* following commands by a paragraph separator.
*/
complexMultiTransform: TranscriptTransform
applyTransform(trans, vec)
{
/* scan the list for multi-object announcements */
foreach (local cur in vec)
{
/* if it's a multi-object announcement, check it */
if (cur.ofKind(MultiObjectAnnouncement))
{
local idx;
local cnt;
local sep;
/*
* We have a multi-object announcement. If we find only
* one other report within the group, and the report's
* text is short, let this report run together with its
* neighbors without any additional visual separation.
* Otherwise, set this group apart from its neighbors by
* adding a paragraph break before and after the group;
* this will make the results easier to read by visually
* separating each longish response as a separate
* paragraph.
*
* First, find the current item's index.
*/
idx = vec.indexWhich({x: x == cur});
/*
* now scan subsequent items in the same command
* iteration, and check to see if (1) we have more than
* one item, or (2) the item has a longish message
*/
for (cnt = 0, ++idx, sep = nil ;
idx <= vec.length() && cnt < 2 ; ++idx, ++cnt)
{
local sub = vec[idx];
/* if we've reached the end of the group, stop scanning */
if (sub.iter_ != cur.iter_)
break;
/*
* If it has long text, add visual separation. Note
* that "long" is just a heuristic, because we can't
* tell whether the text will wrap in any given
* interpreter - that depends on the width of the
* interpreter window and the font size, among other
* things, and we have no way of knowing any of this
* here.
*/
if (sub.ofKind(CommandReportMessage)
&& sub.messageText_ != nil
&& sub.messageText_.length() > 60)
{
/* it's long - add separation */
sep = true;
break;
}
}
/* if we need separation, add it now */
if (sep || cnt > 1)
{
/*
* This is indeed a complex iterated item. Set it
* off by paragraph breaks before and after the
* iteration.
*
* First, find the first item in this iteration. If
* it's not the first item in the whole transcript,
* insert a separator before it.
*/
idx = vec.indexWhich({x: x.iter_ == cur.iter_});
if (idx != 1)
vec.insertAt(idx, new GroupSeparatorMessage(cur));
/*
* Next, find the last item in this iteration. If
* it's no the last item in the entire transcript,
* add a separator after it.
*/
idx = vec.lastIndexWhich({x: x.iter_ == cur.iter_});
if (idx != vec.length())
vec.insertAt(idx + 1, new GroupSeparatorMessage(cur));
}
}
/*
* if it's a command result from an implied command, and we
* have another command result following from the enclosing
* command, add a separator between this result and the next
* result
*/
if (cur.ofKind(CommandReportMessage) && cur.isActionImplicit)
{
local idx;
/* get the index of this element */
idx = vec.indexOf(cur);
/*
* if there's another element following, check to see if
* it's a command report for an enclosing action (i.e.,
* an action that initiated this implied action)
*/
if (idx < vec.length())
{
local nxt;
/* get the next element */
nxt = vec[idx + 1];
/*
* if it's a command report for an action that
* encloses this action, or it's another implicit
* announcement, then put a separator before it
*/
if ((nxt.ofKind(CommandReportMessage)
&& nxt.getAction() != cur.getAction()
&& cur.isActionNestedIn(nxt))
|| nxt.ofKind(ImplicitActionAnnouncement))
{
/* add a separator */
vec.insertAt(idx + 1,
new InternalSeparatorMessage(cur));
}
}
}
}
}
;
/* ------------------------------------------------------------------------ */
/*
* Invoke a callback function using a transcript of the given class.
* Returns the return value of the callback function.
*/
withCommandTranscript(transcriptClass, func)
{
local transcript;
local oldTranscript;
/*
* if we already have an active transcript, just invoke the
* function, running everything through the existing active
* transcript
*/
if (gTranscript != nil && gTranscript.isActive)
{
/* invoke the callback and return the result */
return (func)();
}
/*
* Create a transcript of the given class. Make the transcript
* transient, since it's effectively part of the output stream state
* and thus shouldn't be saved or undone.
*/
transcript = transcriptClass.createTransientInstance();
/* make this transcript the current global transcript */
oldTranscript = gTranscript;
gTranscript = transcript;
/* install the transcript as a filter on the main output stream */
mainOutputStream.addOutputFilter(transcript);
/* make sure we undo our global changes before we leave */
try
{
/* invoke the callback and return the result */
return (func)();
}
finally
{
/* uninstall the transcript output filter */
mainOutputStream.removeOutputFilter(transcript);
/* restore the previous global transcript */
gTranscript = oldTranscript;
/* show the transcript results */
transcript.showReports(true);
}
}
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3