report.t | documentation |
#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
Generated on 5/16/2013 from TADS version 3.1.3