cfad47cfa3/t3compiler/tads3/lib/adv3/report.t

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
3
/* 
4
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
5
 *   
6
 *   TADS 3 Library: command reports
7
 *   
8
 *   This module defines the "command report" classes, which the command
9
 *   execution engine uses to keep track of the status of a command.  
10
 */
11
12
#include "adv3.h"
13
14
15
/* ------------------------------------------------------------------------ */
16
/*
17
 *   Command report objects.  The library uses these to control how the
18
 *   text from a command is displayed.  Game code can also use report
19
 *   objects to show and control command results, but this isn't usually
20
 *   necessary; game code can usually simply display messages directly.
21
 *   
22
 *   Reports are divided into two broad classes: "default" and "full"
23
 *   reports.
24
 *   
25
 *   A "default" report is one that simply confirms that an action was
26
 *   performed, and provides little additional information.  The library
27
 *   uses default reports for simple commands whose full implications
28
 *   should normally be obvious to a player typing such commands: take,
29
 *   drop, put in, and the like.  The library's default reports are
30
 *   usually quite terse: "Taken", "Dropped", "Done".
31
 *   
32
 *   A "full" report is one that gives the player more information than a
33
 *   simple confirmation.  These reports typically describe either the
34
 *   changes to the game state caused by a command or surprising side
35
 *   effects of a command.  For example, if the command is "push button,"
36
 *   and pushing the button opens the door next to the button, a full
37
 *   report would describe the door opening.
38
 *   
39
 *   Note that a full report is warranted any time a command describes
40
 *   anything beyond a simple confirmation.  In our door-opening button
41
 *   example, opening the door by pushing the button always warrants a
42
 *   full report, even if the player has already seen the effects of the
43
 *   button a hundred times before, and even if the button is labeled
44
 *   "push to open door."  It doesn't matter whether or not the
45
 *   consequences of the command ought to be obvious to the player; what
46
 *   matters is that the command warrants a report beyond a simple
47
 *   confirmation.  Any time a report is more than a simple confirmation,
48
 *   it is a full report, no matter how obvious to the player the effects
49
 *   of the action.
50
 *   
51
 *   Full reports are further divided into three subcategories by time
52
 *   ordering: "main," "before," and "after."  "Before" and "after"
53
 *   reports are ordered before and after (respectively) a main report.  
54
 */
55
class CommandReport: object
56
    construct()
57
    {
58
        /* 
59
         *   remember the action with which we're associated, unless a
60
         *   subclass already specifically set the action 
61
         */
62
        if (action_ == nil)
63
            action_ = gAction;
64
    }
65
66
    /* get/set my action */
67
    getAction() { return action_; }
68
    setAction(action) { action_ = action; }
69
70
    /* check to see if my action is implicit */
71
    isActionImplicit() { return action_ != nil && action_.isImplicit; }
72
73
    /* check to see if my action is nested in the other report's action */
74
    isActionNestedIn(other)
75
    {
76
        return (action_ != nil
77
                && other.getAction() != nil
78
                && action_.isNestedIn(other.getAction()));
79
    }
80
81
    /*
82
     *   Flag: if this property is true, this report indicates a failure.
83
     *   By default, a report does not indicate failure.  
84
     */
85
    isFailure = nil
86
87
    /*
88
     *   Flag: if this property is true, this report indicates an
89
     *   interruption for interactive input. 
90
     */
91
    isQuestion = nil
92
93
    /* iteration number current when we were added to the transcript */
94
    iter_ = nil
95
96
    /* the action I'm associated with */
97
    action_ = nil
98
99
    /*
100
     *   Am I part of the same action as the given report?  Returns true if
101
     *   this action is part of the same iteration and part of the same
102
     *   action as the other report.  
103
     */
104
    isPartOf(report)
105
    {
106
        /* 
107
         *   if I don't have an action, or the other report doesn't have an
108
         *   action, we're not related 
109
         */
110
        if (action_ == nil || report.action_ == nil)
111
            return nil;
112
113
        /* if our iterations don't match, we're not related */
114
        if (iter_ != report.iter_)
115
            return nil;
116
117
        /* check if I'm part of the other report's action */
118
        return action_.isPartOf(report.action_);
119
    }
120
;
121
122
/*
123
 *   Group separator.  This simply displays separation between groups of
124
 *   messages - that is, between one set of messages associated with a
125
 *   single action and a set of messages associated with a different
126
 *   action.  
127
 */
128
class GroupSeparatorMessage: CommandReport
129
    construct(report)
130
    {
131
        /* use the same action and iteration as the given report */
132
        action_ = report.getAction();
133
        iter_ = report.iter_;
134
    }
135
136
    /* show the normal command results separator */
137
    showMessage() { say(gLibMessages.complexResultsSeparator); }
138
;
139
140
/*
141
 *   Internal separator.  This displays separation within a group of
142
 *   messages for a command, to visually separate the results from an
143
 *   implied command from the results for the enclosing command. 
144
 */
145
class InternalSeparatorMessage: CommandReport
146
    construct(report)
147
    {
148
        /* use the same action and iteration as the given report */
149
        action_ = report.getAction();
150
        iter_ = report.iter_;
151
    }
152
153
    /* show the normal command results separator */
154
    showMessage() { say(gLibMessages.internalResultsSeparator); }
155
;
156
157
/*
158
 *   Report boundary marker.  This is a pseudo-report that doesn't display
159
 *   anything; its purpose is to allow a caller to identify a block of
160
 *   reports (the reports between two markers) for later removal or
161
 *   reordering.  
162
 */
163
class MarkerReport: CommandReport
164
    showMessage() { }
165
;
166
167
/*
168
 *   End-of-description marker.  This serves as a marker in the transcript
169
 *   stream to let us know where the descriptive reports for a given
170
 *   action end.  
171
 */
172
class EndOfDescReport: MarkerReport
173
;
174
175
/*
176
 *   Simple MessageResult-based command report 
177
 */
178
class CommandReportMessage: CommandReport, MessageResult
179
    construct([params])
180
    {
181
        /* invoke our base class constructors */
182
        inherited CommandReport();
183
        inherited MessageResult(params...);
184
    }
185
;
186
187
/* 
188
 *   default report 
189
 */
190
class DefaultCommandReport: CommandReportMessage
191
;
192
193
/*
194
 *   extra information report 
195
 */
196
class ExtraCommandReport: CommandReportMessage
197
;
198
199
/*
200
 *   default descriptive report 
201
 */
202
class DefaultDescCommandReport: CommandReportMessage
203
;
204
205
/*
206
 *   cosmetic spacing report 
207
 */
208
class CosmeticSpacingCommandReport: CommandReportMessage
209
;
210
211
/*
212
 *   base class for all "full" reports 
213
 */
214
class FullCommandReport: CommandReportMessage
215
    /* 
216
     *   a full report has a sequence number that tells us where the
217
     *   report goes relative to the others - the higher this number, the
218
     *   later the report goes 
219
     */
220
    seqNum = nil
221
;
222
223
/*
224
 *   "before" report - these come before the main report 
225
 */
226
class BeforeCommandReport: FullCommandReport
227
    seqNum = 1
228
;
229
230
/*
231
 *   main report 
232
 */
233
class MainCommandReport: FullCommandReport
234
    seqNum = 2
235
;
236
237
/*
238
 *   failure report 
239
 */
240
class FailCommandReport: FullCommandReport
241
    seqNum = 2
242
    isFailure = true
243
;
244
245
/*
246
 *   failure marker - this is a silent report that marks an action as
247
 *   having failed without actually generating any message text 
248
 */
249
class FailCommandMarker: MarkerReport
250
    isFailure = true
251
;
252
253
/*
254
 *   "after" report - these come after the main report 
255
 */
256
class AfterCommandReport: FullCommandReport
257
    seqNum = 3
258
;
259
260
/*
261
 *   An interruption for interactive input.  This is used to report a
262
 *   prompt for more information that's needed before the command can
263
 *   proceed, such as a prompt for a missing object, or a disambiguation
264
 *   prompt. 
265
 */
266
class QuestionCommandReport: MainCommandReport
267
    isQuestion = true;
268
;
269
270
/*
271
 *   A conversation begin/end report.  This is a special marker we insert
272
 *   into the transcript to flag the boundaries of an NPC's conversational
273
 *   message.  
274
 */
275
class ConvBoundaryReport: CommandReport
276
    construct(id) { actorID = id; }
277
278
    /* the actor's ID number, as assigned by the ConversationManager */
279
    actorID = nil
280
;
281
class ConvBeginReport: ConvBoundaryReport
282
    showMessage() { say('<.convbegin ' + actorID + '>'); }
283
;
284
class ConvEndReport: ConvBoundaryReport
285
    construct(id, node)
286
    {
287
        inherited(id);
288
        defConvNode = node;
289
    }
290
    
291
    showMessage()
292
    {
293
        if (actorID != nil)
294
            say('<.convend ' + actorID + ' '
295
                + (defConvNode == nil ? '' : defConvNode) + '>');
296
    }
297
298
    /* the default new ConvNode for the actor */
299
    defConvNode = nil
300
;
301
302
/* ------------------------------------------------------------------------ */
303
/*
304
 *   Announcements.  We use these to track announcements to be made as
305
 *   part of an action's results. 
306
 */
307
class CommandAnnouncement: CommandReport
308
    construct([params])
309
    {
310
        /* inherit default handling */
311
        inherited();
312
313
        /* remember our text */
314
        messageText_ = getMessageText(params...);
315
    }
316
317
    /* 
318
     *   Get our message text.  By default, we simply get the gLibMessages
319
     *   message given by the property. 
320
     */
321
    getMessageText([params])
322
    {
323
        /* get the library message */
324
        return gLibMessages.(messageProp_)(params...);
325
    }
326
327
    /* 
328
     *   Show our message.  Our default implementation shows the library
329
     *   message given by our messageProp_ property, using the parameters
330
     *   we stored in our constructor.  
331
     */
332
    showMessage()
333
    {
334
        /* call gLibMessages to show our message */
335
        say(messageText_);
336
    }
337
338
    /* our gLibMessages property */
339
    messageProp_ = nil
340
341
    /* our message text */
342
    messageText_ = ''
343
;
344
345
/*
346
 *   Multiple-object announcement.  When the player applies a single
347
 *   command to a series of objects (as in "take the book and the folder"
348
 *   or "take all"), we'll show one of these announcements for each object,
349
 *   just before we execute the command for that object.  This announcement
350
 *   usually just shows the object's name plus suitable punctuation (in
351
 *   English, a colon), and helps the player see which results go with
352
 *   which objects.  
353
 */
354
class MultiObjectAnnouncement: CommandAnnouncement
355
    /* show the announceMultiActionObject message */
356
    messageProp_ = &announceMultiActionObject
357
;
358
359
/*
360
 *   Default object announcement.  We display this announcement whenever
361
 *   the player leaves out a required object from a command, but the parser
362
 *   is able to infer which object they must have meant.  The parser infers
363
 *   that an object was intended when a verb requires an object that the
364
 *   player didn't specify, and there's only one logical choice for the
365
 *   missing object.  We announce our assumption to put it out in the open,
366
 *   to ensure that the player is immediately alerted if they had something
367
 *   else in mind.
368
 *   
369
 *   In English, this type of announcement conventionally consists of
370
 *   simply the name of the assumed object, in parenthesis and on a line by
371
 *   itself.  In cases where the object role involves a prepositional
372
 *   phrase in the verb structure, we generally show the preposition before
373
 *   the object name.  This format usually reads intuitively, by combining
374
 *   with the text just above of the player's own command:
375
 *
376
 *.  >open
377
 *.  (the door>
378
 *.  You try opening the door, but it seems to be locked.
379
 *.
380
 *.  >unlock the door
381
 *   (with the key)
382
 */
383
class DefaultObjectAnnouncement: CommandAnnouncement
384
    construct(obj, whichObj, action, allResolved)
385
    {
386
        /* remember our object */
387
        obj_ = obj;
388
389
        /* remember the message parameters */
390
        whichObj_ = whichObj;
391
        allResolved_ = allResolved;
392
393
        /* remember my action */
394
        action_ = action;
395
396
        /* inherit default handling */
397
        inherited();
398
    }
399
400
    /* get our message text */
401
    getMessageText()
402
    {
403
        /* get the announcement message from our object */
404
        return obj_.announceDefaultObject(whichObj_, action_, allResolved_);
405
    }
406
407
    /* our defaulted object */
408
    obj_ = nil
409
410
    /* our message parameters */
411
    whichObj_ = nil
412
    allResolved_ = nil
413
;
414
415
/*
416
 *   Ambiguous object announcement.  We display this when the parser
417
 *   manages to resolve a noun phrase to an object (or objects) from an
418
 *   ambiguous set of possibilities, without having to ask the player for
419
 *   help but also without absolute certainty that the objects selected are
420
 *   the ones the player meant.  This happens when more than enough objects
421
 *   are logical possibilities for selection, but some objects are more
422
 *   logical choices than others.  The parser picks the most logical of the
423
 *   available options, but since other logical choices are present, the
424
 *   parser can't be certain that it chose the ones the player actually
425
 *   meant.  Because of this uncertainty, we generate one of these
426
 *   announcements each time this happens.  This report lets the player
427
 *   know exactly which object we chose, which will immediately alert the
428
 *   player when our selection is different from what they had in mind.
429
 *   
430
 *   In form, this type of announcement usually looks just like a default
431
 *   object announcement.  
432
 */
433
class AmbigObjectAnnouncement: CommandAnnouncement
434
    /* show the announceAmbigObject announcement */
435
    messageProp_ = &announceAmbigActionObject
436
;
437
438
/*
439
 *   Remapped action announcement.  This is used when we need to mention a
440
 *   defaulted or disambiguated object, but the player's original input was
441
 *   remapped to a different action that rearranges the object roles.  In
442
 *   these cases, rather than just announcing the defaulted object name, we
443
 *   announce the entire remapped action; we show the full action
444
 *   description because rearrangement of the object roles usually makes
445
 *   the standard object-only announcement confusing to read, since it
446
 *   doesn't naturally fit in as a continuation of what the user typed.
447
 *   
448
 *   In English, this message is usually shown with the entire verb phrase,
449
 *   in present participle form ("opening the door"), enclosed in
450
 *   parentheses and on a line by itself.  
451
 */
452
class RemappedActionAnnouncement: CommandAnnouncement
453
    construct()
454
    {
455
        /* use the action as the message parameter */
456
        inherited(gAction);
457
    }
458
459
    messageProp_ = &announceRemappedAction
460
;
461
462
/*
463
 *   Each language module must define a class called
464
 *   ImplicitAnnouncementContext, and three instances of the class, for use
465
 *   by the generic library.  The language module can define other
466
 *   instances of the context class as needed.  We minimally need the
467
 *   following instances to be defined by the language module:
468
 *   
469
 *   standardImpCtx: this is the standard context, which indicates that we
470
 *   want the default format for the implicit action announcement.
471
 *   
472
 *   tryingImpCtx: this is the "trying" context, which indicates that we
473
 *   want the announcement to phrase the action to indicate that we're only
474
 *   trying the action, not actually performing it.  We use this when the
475
 *   implicit action has failed, in which case we want our announcement to
476
 *   say that we're merely attempting the action; the announcement
477
 *   shouldn't imply that the action has actually been performed.
478
 *   
479
 *   askingImpCtx: this is the "asking" context, which indicates that the
480
 *   action was interrupted with an interactive question, such as a prompt
481
 *   for a missing direct object.
482
 *   
483
 *   We leave it up to the language module to define the class and these
484
 *   two instances.  This lets the language module represent the context
485
 *   types any way it likes.  
486
 */
487
488
/*
489
 *   Implicit action announcement.  This is displayed when we perform a
490
 *   command implicitly, which we usually do to fulfill a precondition of
491
 *   an action.
492
 *   
493
 *   In English, we usually show an implied action as the verb participle
494
 *   phrase ("opening the door"), prefixed with "first", and enclosed in
495
 *   parentheses on a line by itself (hence, "(first opening the door)").  
496
 */
497
class ImplicitActionAnnouncement: CommandAnnouncement
498
    construct(action, msg)
499
    {
500
        /* use the given message property */
501
        messageProp_ = msg;
502
503
        /* 
504
         *   Inherit default.  The first message parameter is the action;
505
         *   the second is our standard implicit action context object,
506
         *   indicating that we want the normal context.  
507
         */
508
        inherited(action, standardImpCtx);
509
    }
510
511
    /* 
512
     *   Make this announcement silent.  This eliminates any announcement
513
     *   for this action, but makes it otherwise behave like a normal
514
     *   implied action. 
515
     */
516
    makeSilent()
517
    {
518
        /* clear my message text */
519
        messageText_ = '';
520
521
        /* 
522
         *   use the silent announcement message if we have to regenerate
523
         *   our text for another context 
524
         */
525
        messageProp_ = &silentImplicitAction;
526
    }
527
528
    /*
529
     *   Note that the action we're attempting is merely an attempt that
530
     *   failed.  This will change our report to indicate that we're only
531
     *   trying the action, rather than suggesting that we actually carried
532
     *   it out.  
533
     */
534
    noteJustTrying()
535
    {
536
        /* note that we're just trying the action */
537
        justTrying = true;
538
539
        /* change our message to the "trying" form */
540
        messageText_ = getMessageText(getAction(), tryingImpCtx);
541
    }
542
543
    /* 
544
     *   Note that the action we're attempting is incomplete, as it was
545
     *   interupted for interactive input (such as asking for a missing
546
     *   object). 
547
     */
548
    noteQuestion()
549
    {
550
        /* note that the action was interrupted with a question */
551
        justAsking = true;
552
553
        /* change our message to the "asking" form */
554
        messageText_ = getMessageText(getAction(), askingImpCtx);
555
    }
556
557
    /* 
558
     *   Flag: we're just attempting the action; this is set when we
559
     *   determine that the implicit action has failed, in which case we
560
     *   want an announcement indicating that we're merely attempting the
561
     *   action, not actually performing it.  Presume that we're actually
562
     *   going to perform the action; the action can change this if
563
     *   necessary.  
564
     */
565
    justTrying = nil
566
567
    /* flag: the action was interrupted with an interactive question */
568
    justAsking = nil
569
;
570
571
class CommandSepAnnouncement: CommandAnnouncement
572
    construct()
573
    {
574
        /* we're not associated with an iteration or action */
575
        action_ = nil;
576
        iter_ = 0;
577
    }
578
579
    showMessage()
580
    {
581
        /* show a command separator */
582
        "<.commandsep>";
583
    }
584
;
585
586
/* ------------------------------------------------------------------------ */
587
/*
588
 *   Command Transcript.  This is a "semantic transcript" of the results of
589
 *   a command.  This provides a list of CommandReport objects describing
590
 *   the results of the command.  
591
 */
592
class CommandTranscript: OutputFilter
593
    construct()
594
    {
595
        /* set up a vector to hold the reports */
596
        reports_ = new Vector(5);
597
    }
598
599
    /* 
600
     *   flag: the command has failed (i.e., at least one failure report
601
     *   has been generated) 
602
     */
603
    isFailure = nil
604
605
    /*
606
     *   Note that the current action has failed.  This is equivalent to
607
     *   adding a reportFailure() message to the transcript.  
608
     */
609
    noteFailure()
610
    {
611
        /* add an empty reportFailure message */
612
        reportFailure('');
613
    }
614
615
    /*
616
     *   Did the given action fail?  This scans the transcript to determine
617
     *   if there are any failure messages associated with the given
618
     *   action.   
619
     */
620
    actionFailed(action)
621
    {
622
        /* 
623
         *   scan the transcript for failure messages that are associated
624
         *   with the given action 
625
         */
626
        return reports_.indexWhich(
627
            {x: x.isPartOf(action) && x.isFailure}) != nil;
628
    }
629
630
    /* 
631
     *   flag: I'm active; when this is nil, we'll pass text through our
632
     *   filter routine unchanged 
633
     */
634
    isActive = true
635
636
    /*
637
     *   Summarize the current action's reports.  This allows a caller to
638
     *   turn a series of iterated reports into a single report for the
639
     *   entire action.  For example, we could change something like this:
640
     *   
641
     *   gold coin: Bob accepts the gold coin.
642
     *.  gold coin: Bob accepts the gold coin.
643
     *.  gold coin: Bob accepts the gold coin.
644
     *   
645
     *   into this:
646
     *   
647
     *   Bob accepts the three gold coins.
648
     *   
649
     *   This function runs through the reports for the current action,
650
     *   submitting each one to the 'cond' callback to see if it's of
651
     *   interest to the summary.  For each consecutive run of two or more
652
     *   reports that can be summarized, we'll remove the reports that
653
     *   'cond' accepted, and we'll remove the multiple-object announcement
654
     *   reports associated with them, and we'll insert a new report with
655
     *   the message returned by the 'report' callback.
656
     *   
657
     *   'cond' is called as cond(x), where 'x' is a report object.  This
658
     *   callback returns true if the report can be summarized for the
659
     *   caller's purposes, nil if not.
660
     *   
661
     *   'report' is called as report(vec), where 'vec' is a Vector
662
     *   consisting of all of the consecutive report objects that we're now
663
     *   summarizing.  This function returns a string giving the message to
664
     *   use in place of the reports we're removing.  This should be a
665
     *   summary message, standing in for the set of individual reports
666
     *   we're removing.
667
     *   
668
     *   There's an important subtlety to note.  If the messages you're
669
     *   summarizing are conversational (that is, if they're generated by
670
     *   TopicEntry responses), you should take care to generate the full
671
     *   replacement text in the 'report' part, rather than doing so in
672
     *   separate code that you run after summarizeAction() returns.  This
673
     *   is important because it ensures that the Conversation Manager
674
     *   knows that your replacement message is part of the same
675
     *   conversation.  If you wait until after summarizeAction() returns
676
     *   to generate more response text, the conversation manager won't
677
     *   realize that the additional text is part of the same conversation.
678
     */
679
    summarizeAction(cond, report)
680
    {
681
        local vec = new Vector(8);
682
        local rpt = reports_;
683
        local cnt = rpt.length();
684
        local i;
685
686
        /* find the first report for the current action */
687
        for (i = 1 ; i <= cnt && rpt[i].getAction() != gAction ; ++i) ;
688
689
        /* iterate over the transcript for the current action */
690
        for ( ; ; ++i)
691
        {
692
            local ok;
693
694
            /* presume we won't find a summarizable item */
695
            ok = nil;
696
            
697
            /* if we're still in range, check what we have */
698
            if (i <= cnt)
699
            {
700
                /* get the current item */
701
                local cur = rpt[i];
702
703
                /* if this one is of interest, note it in the vector */
704
                if (cond(cur))
705
                {
706
                    /* add it to our vector of summarizable reports */
707
                    vec.append(cur);
708
709
                    /* note that we're okay on this round */
710
                    ok = true;
711
                }
712
                else if (cur.ofKind(ImplicitActionAnnouncement)
713
                         || cur.ofKind(MultiObjectAnnouncement)
714
                         || cur.ofKind(DefaultCommandReport)
715
                         || cur.ofKind(ConvBoundaryReport))
716
                {
717
                    /* we can keep these in summaries */
718
                    ok = true;
719
                }
720
            }
721
722
            /* 
723
             *   If this item isn't summarizable, or we've reached the last
724
             *   item, generate a summary of any we have so far.  (We need
725
             *   to generate the summary on reaching the last item because
726
             *   there are no further items that could go in the summary.)
727
             */
728
            if (!ok || i == cnt)
729
            {
730
                /* if we have two or more items, generate a summary */
731
                if (vec.length() > 1)
732
                {
733
                    local insIdx;
734
                    local txt;
735
                
736
                    /* remove each item in the vector */
737
                    foreach (local cur in vec)
738
                    {
739
                        local idx;
740
                        
741
                        /* get the index of the current item */
742
                        idx = rpt.indexOf(cur);
743
744
                        /* 
745
                         *   we're summarizing this item, so remove the
746
                         *   individual item - subsume it into the summary 
747
                         */
748
                        rpt.removeElementAt(idx);
749
                        --i;
750
                        --cnt;
751
752
                        /* insert the summary here */
753
                        insIdx = idx;
754
755
                        /* 
756
                         *   skip any implicit action announcements,
757
                         *   default command announcements, and
758
                         *   conversational boundary markers 
759
                         */
760
                        for (--idx ;
761
                             idx > 0
762
                             && (rpt[idx].ofKind(ImplicitActionAnnouncement)
763
                                 || rpt[idx].ofKind(DefaultCommandReport)
764
                                 || rpt[idx].ofKind(ConvBoundaryReport)) ;
765
                             --idx) ;
766
767
                        /*
768
                         *   if the preceding element is a multi-object
769
                         *   announcement, remove it - let the summary
770
                         *   mention the individual objects if it wants to 
771
                         */
772
                        if (idx > 0
773
                            && rpt[idx].ofKind(MultiObjectAnnouncement))
774
                        {
775
                            /* remove this element and adjust the counters */
776
                            rpt.removeElementAt(idx);
777
                            --i;
778
                            --cnt;
779
                            --insIdx;
780
781
                            /*
782
                             *   If this leaves us with a <.convbegin>
783
                             *   preceded directly by a <.convend> for the
784
                             *   same actor, the two cancel each other out.
785
                             *   Simply remove both of them.  
786
                             */
787
                            if (idx <= rpt.length()
788
                                && idx > 1
789
                                && rpt[idx].ofKind(ConvBeginReport)
790
                                && rpt[idx-1].ofKind(ConvEndReport)
791
                                && rpt[idx].actorID == rpt[idx-1].actorID)
792
                            {
793
                                /* 
794
                                 *   we have a canceling <.convend> +
795
                                 *   <.convbegin> pair - simply remove them
796
                                 *   both and adjust the counters
797
                                 *   accordingly 
798
                                 */
799
                                rpt.removeRange(idx - 1, idx);
800
                                i -= 2;
801
                                cnt -= 2;
802
                                insIdx -= 2;
803
                            }
804
                        }
805
                    }
806
807
                    /* ask the caller for the summary */
808
                    txt = report(vec);
809
810
                    /* insert it */
811
                    rpt.insertAt(insIdx, new MainCommandReport(txt));
812
                    ++cnt;
813
                    ++i;
814
                }
815
816
                /* clear the vector */
817
                if (vec.length() > 0)
818
                    vec.removeRange(1, vec.length());
819
            }
820
821
            /* if we've reached the end of the list, we're done */
822
            if (i > cnt)
823
                break;
824
        }
825
    }
826
827
    /* activate - set up to capture output */
828
    activate()
829
    {
830
        /* make myself active */
831
        isActive = true;
832
    }
833
834
    /* deactivate - stop capturing output */
835
    deactivate()
836
    {
837
        /* make myself inactive */
838
        isActive = nil;
839
    }
840
841
    /* 
842
     *   Count an iteration.  An Action should call this once per iteration
843
     *   if it's a top-level (non-nested) command.
844
     */
845
    newIter() { ++iter_; }
846
847
    /* 
848
     *   Flush the transcript in preparation for reading input.  This shows
849
     *   all pending reports, clears the backlog of reports (so that we
850
     *   don't show them again in the future), and deactivates the
851
     *   transcript's capture feature so that subsequent output goes
852
     *   directly to the output stream.
853
     *   
854
     *   We return the former activation status - that is, we return true
855
     *   if the transcript was activated before the call, nil if not.  
856
     */
857
    flushForInput()
858
    {
859
        /* show our reports, and deactivate output capture */
860
        local wasActive = showReports(true);
861
862
        /* clear the reports, since we've now shown them all */
863
        clearReports();
864
865
        /* return the previous activation status */
866
        return wasActive;
867
    }
868
869
    /* 
870
     *   Show our reports.  Returns true if the transcript was previously
871
     *   active, nil if not. 
872
     */
873
    showReports(deact)
874
    {
875
        local wasActive;
876
        
877
        /* 
878
         *   remember whether we were active or not originally, then
879
         *   deactivate (maybe just temporarily) so that we can write out
880
         *   our reports without recursively intercepting them
881
         */
882
        wasActive = isActive;
883
        deactivate();
884
885
        /* first, apply all defined transformations to our transcript */
886
        applyTransforms();
887
        
888
        /*
889
         *   Temporarily cancel any sense context message blocking.  We
890
         *   have already taken into account for each report whether or
891
         *   not the report was visible when it was generated, so we can
892
         *   display each report that made it past that check without any
893
         *   further conditions.  
894
         */
895
        callWithSenseContext(nil, nil, new function()
896
        {
897
            /* show the reports */            
898
            foreach (local cur in reports_)
899
            {
900
                /* if we're allowed to show this report, show it */
901
                if (canShowReport(cur))
902
                    cur.showMessage();
903
            }
904
        });
905
            
906
        /* 
907
         *   if we were active and we're not being asked to deactivate,
908
         *   re-activate now that we're finished showing our reports 
909
         */
910
        if (wasActive && !deact)
911
            activate();
912
913
        /* return the former activation status */
914
        return wasActive;
915
    }
916
917
    /*
918
     *   Add a report. 
919
     */
920
    addReport(report)
921
    {
922
        /* check for a failure report */
923
        if (report.isFailure)
924
        {
925
            /* set the failure flag for the entire command */
926
            isFailure = true;
927
928
            /* 
929
             *   If this is an implied command, and the actor is in "NPC
930
             *   mode", suppress the report.  When an implied action fails
931
             *   in NPC mode, we act as though we never attempted the
932
             *   action. 
933
             */
934
            if (gAction.isImplicit && gActor.impliedCommandMode() == ModeNPC)
935
            {
936
                /* add a failure marker, not the message report */
937
                reports_.append(new FailCommandMarker());
938
939
                /* that's all we need to add */
940
                return;
941
            }
942
        }
943
944
        /* 
945
         *   Do not queue reports made while the sense context is blocking
946
         *   output because the player character cannot sense the locus of
947
         *   the action.  Note that this check comes before we queue the
948
         *   report, but after we've noted any effect on the status of the
949
         *   overall action; even if we're not going to show the report,
950
         *   its status effects are still valid.  
951
         */
952
        if (senseContext.isBlocking)
953
            return;
954
955
        /* 
956
         *   If the new report's iteration ID hasn't been set already, note
957
         *   the current iteration in the report.  Some types of reports
958
         *   will have already set a specific iteration before we get here,
959
         *   so set the iteration ID only if the report hasn't done so
960
         *   already.  
961
         */
962
        if (report.iter_ == nil)
963
            report.iter_ = iter_;
964
965
        /* append the report */
966
        reports_.append(report);
967
    }
968
969
    /* get the last report added */
970
    getLastReport()
971
    {
972
        local cnt = reports_.length();
973
        return (cnt == 0 ? nil : reports_[cnt]);
974
    }
975
976
    /* delete the last report added */
977
    deleteLastReport()
978
    {
979
        local cnt = reports_.length();
980
        if (cnt != 0)
981
            reports_.removeElementAt(cnt);
982
    }
983
984
    /*
985
     *   Add a marker report.  This adds a marker to the report stream,
986
     *   and returns the marker object.  The marker doesn't show any
987
     *   message in the final display, but callers can use a pair of
988
     *   markers to identify a range of reports for later reordering or
989
     *   removal. 
990
     */
991
    addMarker()
992
    {
993
        /* create the new report */
994
        local marker = new MarkerReport();
995
996
        /* add it to the stream */
997
        addReport(marker);
998
999
        /* return the new report */
1000
        return marker;
1001
    }
1002
1003
    /* delete the reports between two markers */
1004
    deleteRange(marker1, marker2)
1005
    {
1006
        local idx1, idx2;
1007
        
1008
        /* find the indices of the two markers */
1009
        idx1 = reports_.indexOf(marker1);
1010
        idx2 = reports_.indexOf(marker2);
1011
1012
        /* if we found both, delete the range */
1013
        if (idx1 != nil && idx2 != nil)
1014
            reports_.removeRange(idx1, idx2);
1015
    }
1016
1017
    /* 
1018
     *   Pull out the reports between two markers, and reinsert them at
1019
     *   the end of the transcript.  
1020
     */
1021
    moveRangeAppend(marker1, marker2)
1022
    {
1023
        local idx1, idx2;
1024
        
1025
        /* find the indices of the two markers */
1026
        idx1 = reports_.indexOf(marker1);
1027
        idx2 = reports_.indexOf(marker2);
1028
1029
        /* if we didn't find both, ignore the request */
1030
        if (idx1 == nil || idx2 == nil)
1031
            return;
1032
1033
        /* append each item in the range to the end of the report list */
1034
        for (local i = idx1 ; i <= idx2 ; ++i)
1035
            reports_.append(reports_[i]);
1036
1037
        /* delete the original copies */
1038
        reports_.removeRange(idx1, idx2);
1039
    }
1040
1041
    /* 
1042
     *   Perform a callback on all of the reports in the transcript.
1043
     *   We'll invoke the given callback function func(rpt) once for each
1044
     *   report, with the report object as the parameter. 
1045
     */
1046
    forEachReport(func) { reports_.forEach(func); }
1047
1048
    /*
1049
     *   End the description section of the report.  This adds a marker
1050
     *   report that indicates that anything following (and part of the
1051
     *   same action) is no longer part of the description; this can be
1052
     *   important when we apply the default description suppression
1053
     *   transformation, because it tells us not to consider the
1054
     *   non-descriptive messages following this marker when, for example,
1055
     *   suppressing default descriptive messages.  
1056
     */
1057
    endDescription()
1058
    {
1059
        /* add an end-of-description report */
1060
        addReport(new EndOfDescReport());
1061
    }
1062
1063
    /*
1064
     *   Announce that the action is implicit
1065
     */
1066
    announceImplicit(action, msgProp)
1067
    {
1068
        /* 
1069
         *   If the actor performing the command is not in "player" mode,
1070
         *   save an implicit action announcement; for NPC mode, we treat
1071
         *   implicit command results like any other results, so we don't
1072
         *   want a separate announcement.  
1073
         */
1074
        if (gActor.impliedCommandMode() == ModePlayer)
1075
        {
1076
            /* create the new report */
1077
            local report = new ImplicitActionAnnouncement(action, msgProp);
1078
1079
            /* add it to the transcript */
1080
            addReport(report);
1081
1082
            /* return it */
1083
            return report;
1084
        }
1085
        else
1086
        {
1087
            /* no need for a report */
1088
            return nil;
1089
        }
1090
    }
1091
1092
    /*
1093
     *   Announce a remapped action 
1094
     */
1095
    announceRemappedAction()
1096
    {
1097
        /* save a remapped-action announcement */
1098
        addReport(new RemappedActionAnnouncement());
1099
    }
1100
1101
    /*
1102
     *   Announce one of a set of objects to a multi-object action.  We'll
1103
     *   record this announcement for display with our report list.  
1104
     */
1105
    announceMultiActionObject(obj, whichObj)
1106
    {
1107
        /* save a multi-action object announcement */
1108
        addReport(new MultiObjectAnnouncement(obj, whichObj, gAction));
1109
    }
1110
1111
    /*
1112
     *   Announce an object that was resolved with slight ambiguity. 
1113
     */
1114
    announceAmbigActionObject(obj, whichObj)
1115
    {
1116
        /* save an ambiguous object announcement */
1117
        addReport(new AmbigObjectAnnouncement(obj, whichObj, gAction));
1118
    }
1119
1120
    /*
1121
     *   Announce a default object. 
1122
     */
1123
    announceDefaultObject(obj, whichObj, action, allResolved)
1124
    {
1125
        /* save the default object announcement */
1126
        addReport(new DefaultObjectAnnouncement(
1127
            obj, whichObj, action, allResolved));
1128
    }
1129
1130
    /*
1131
     *   Add a command separator. 
1132
     */
1133
    addCommandSep()
1134
    {
1135
        /* add a command separator announcement */
1136
        addReport(new CommandSepAnnouncement());
1137
    }
1138
1139
    /*
1140
     *   clear our reports 
1141
     */
1142
    clearReports()
1143
    {
1144
        /* forget all of the reports in the main list */
1145
        if (reports_.length() != 0)
1146
            reports_.removeRange(1, reports_.length());
1147
    }
1148
1149
    /*
1150
     *   Can we show a given report?  By default, we always return true,
1151
     *   but subclasses might want to override this to suppress certain
1152
     *   types of reports.  
1153
     */
1154
    canShowReport(report) { return true; }
1155
1156
    /*
1157
     *   Filter text.  If we're active, we'll turn the text into a command
1158
     *   report and add it to our report list, blocking the text from
1159
     *   reaching the underlying stream; otherwise, we'll pass it through
1160
     *   unchanged.  
1161
     */
1162
    filterText(ostr, txt)
1163
    {
1164
        /* if we're inactive, pass text through unchanged */
1165
        if (!isActive)
1166
            return txt;
1167
        
1168
        /* 
1169
         *   If the current sense context doesn't allow any messages to be
1170
         *   generated, block the generated text entirely.  We want to
1171
         *   block text or not according to the sense context in effect
1172
         *   now; so we must note it now rather than wait until we
1173
         *   actually display the report, since the context could be
1174
         *   different by then.  
1175
         */
1176
        if (senseContext.isBlocking)
1177
            return nil;
1178
1179
        /* add a main report to our list if the text is non-empty */
1180
        if (txt != '')
1181
            addReport(new MainCommandReport(txt));
1182
1183
        /* capture the text - send nothing to the underlying stream */
1184
        return nil;
1185
    }
1186
1187
    /* apply transformations */
1188
    applyTransforms()
1189
    {
1190
        /* apply each defined transformation */
1191
        foreach (local cur in transforms_)
1192
            cur.applyTransform(self, reports_);
1193
    }
1194
1195
    /* 
1196
     *   check to see if the current action has a report matching the given
1197
     *   criteria 
1198
     */
1199
    currentActionHasReport(func)
1200
    {
1201
        /* check to see if we can find a matching report */
1202
        return (findCurrentActionReport(func) != nil);
1203
    }
1204
1205
    /* find a report in the current action that matches the given criteria */
1206
    findCurrentActionReport(func)
1207
    {
1208
        /* 
1209
         *   Find an action that's part of the current iteration and which
1210
         *   matches the given function's criteria.  Return the first match
1211
         *   we find.  
1212
         */
1213
        return reports_.valWhich({x: x.iter_ == iter_ && (func)(x)});
1214
    }
1215
1216
    /* 
1217
     *   iteration number - for an iterated top-level command, this helps
1218
     *   us keep the results for a particular iteration grouped together 
1219
     */
1220
    iter_ = 1
1221
1222
    /* our vector of reports */
1223
    reports_ = nil
1224
1225
    /* our list of transformations */
1226
    transforms_ = [defaultReportTransform, implicitGroupTransform,
1227
                   reportOrderTransform, complexMultiTransform]
1228
;
1229
1230
/* ------------------------------------------------------------------------ */
1231
/*
1232
 *   Transcript Transform. 
1233
 */
1234
class TranscriptTransform: object
1235
    /*
1236
     *   Apply our transform to the transcript vector.  By default, we do
1237
     *   nothing; each subclass must override this to manipulate the vector
1238
     *   to make the change it wants to make.  
1239
     */
1240
    applyTransform(trans, vec) { }
1241
;
1242
1243
/* ------------------------------------------------------------------------ */
1244
/*
1245
 *   Transcript Transform: set before/main/after report order.  We'll look
1246
 *   for any before/after reports that are out of order with respect to
1247
 *   their main reports, and move them into the appropriate positions. 
1248
 */
1249
reportOrderTransform: TranscriptTransform
1250
    applyTransform(trans, vec)
1251
    {
1252
        /* scan for before/after reports */
1253
        for (local i = 1, local len = vec.length() ; i <= len ; ++i)
1254
        {
1255
            /* get this item */
1256
            local cur = vec[i];
1257
1258
            /* if this is a before/after report, consider moving it */
1259
            if (cur.ofKind(FullCommandReport) && cur.seqNum != nil)
1260
            {
1261
                local idx;
1262
                
1263
                /* 
1264
                 *   This item cares about its sequencing, so it could be
1265
                 *   out of order with respect to other items from the same
1266
                 *   sequence.  Find the first item with a higher sequence
1267
                 *   number from the same group, and make sure this item is
1268
                 *   before the first such item.  
1269
                 */
1270
                for (idx = 1 ; idx < i ; ++idx)
1271
                {
1272
                    local x;
1273
                    
1274
                    /* get this item */
1275
                    x = vec[idx];
1276
1277
                    /* if x should come after cur, we need to move cur */
1278
                    if (x.ofKind(FullCommandReport)
1279
                        && x.seqNum > cur.seqNum
1280
                        && x.isPartOf(cur))
1281
                    {
1282
                        /* remove cur and reinsert it before x */
1283
                        vec.removeElementAt(i);
1284
                        vec.insertAt(idx, cur);
1285
1286
                        /* adjust our scan index for the removal */
1287
                        --i;
1288
                        
1289
                        /* no need to look any further */
1290
                        break;
1291
                    }
1292
                }
1293
            }
1294
        }
1295
    }
1296
;
1297
1298
/* ------------------------------------------------------------------------ */
1299
/*
1300
 *   Transcript Transform: remove unnecessary default reports.  We'll scan
1301
 *   the transcript for default reports for actions which also have
1302
 *   implicit announcements or non-default reports, and remove those
1303
 *   default reports.  We'll also remove default descriptive reports which
1304
 *   also have non-default reports in the same action.  
1305
 */
1306
defaultReportTransform: TranscriptTransform
1307
    applyTransform(trans, vec)
1308
    {
1309
        /* scan for default reports */
1310
        for (local i = 1, local len = vec.length() ; i <= len ; ++i)
1311
        {
1312
            local cur;
1313
            
1314
            /* get this item */
1315
            cur = vec[i];
1316
            
1317
            /* 
1318
             *   if this is a default report, check to see if we want to
1319
             *   keep it 
1320
             */
1321
            if (cur.ofKind(DefaultCommandReport))
1322
            {
1323
                /* 
1324
                 *   check for a main report or an implicit announcement
1325
                 *   associated with the same action; if we find anything,
1326
                 *   we don't need to keep the default report 
1327
                 */
1328
                if (vec.indexWhich(
1329
                    {x: (x != cur
1330
                         && cur.isPartOf(x)
1331
                         && (x.ofKind(FullCommandReport)
1332
                             || x.ofKind(ImplicitActionAnnouncement)))
1333
                    }) != nil)
1334
                {
1335
                    /* we don't need this default report */
1336
                    vec.removeElementAt(i);
1337
1338
                    /* adjust our scan index for the removal */
1339
                    --i;
1340
                    --len;
1341
                }
1342
            }
1343
1344
            /*
1345
             *   if this is a default descriptive report, check to see if
1346
             *   we want to keep it 
1347
             */
1348
            if (cur.ofKind(DefaultDescCommandReport))
1349
            {
1350
                local fullIdx;
1351
                
1352
                /* 
1353
                 *   check for a main report associated with the same
1354
                 *   action
1355
                 */
1356
                fullIdx = vec.indexWhich(
1357
                    {x: (x != cur
1358
                         && cur.isPartOf(x)
1359
                         && x.ofKind(FullCommandReport))});
1360
                
1361
                /* 
1362
                 *   if we found another report, check to see if it comes
1363
                 *   before or after any 'end of description' for the same
1364
                 *   action 
1365
                 */
1366
                if (fullIdx != nil)
1367
                {
1368
                    local endIdx;
1369
1370
                    /* find the 'end of description' report, if any */
1371
                    endIdx = vec.indexWhich(
1372
                        {x: (x != cur
1373
                             && cur.isPartOf(x)
1374
                             && x.ofKind(EndOfDescReport))});
1375
1376
                    /* 
1377
                     *   if we found a full report before the
1378
                     *   end-of-description report, then the full report is
1379
                     *   part of the description and thus should suppress
1380
                     *   the default report; otherwise, the description
1381
                     *   portion includes only the default report and the
1382
                     *   default report should thus remain 
1383
                     */
1384
                    if (endIdx == nil || fullIdx < endIdx)
1385
                    {
1386
                        /* don't keep the default descriptive report */
1387
                        vec.removeElementAt(i);
1388
1389
                        /* adjust our indices for the removal */
1390
                        --i;
1391
                        --len;
1392
                    }
1393
                }
1394
            }
1395
        }
1396
    }
1397
;
1398
1399
/* ------------------------------------------------------------------------ */
1400
/*
1401
 *   Transcript Transform: group implicit announcements.  We'll find any
1402
 *   runs of consecutive implicit command announcements, and group each run
1403
 *   into a single announcement listing all of the implied actions.  For
1404
 *   example, we'll turn this:
1405
 *   
1406
 *.  >go south
1407
 *.  (first opening the door)
1408
 *.  (first unlocking the door)
1409
 *   
1410
 *   this into:
1411
 *   
1412
 *.  >go south
1413
 *.  (first opening the door and unlocking the door)
1414
 *   
1415
 *   In addition, if we find an implicit announcement in the middle of a
1416
 *   set of regular command reports, and it's for an action nested within
1417
 *   the action generating the regular reports, we'll start a new paragraph
1418
 *   before the implicit announcement.  
1419
 */
1420
implicitGroupTransform: TranscriptTransform
1421
    applyTransform(trans, vec)
1422
    {
1423
        /* 
1424
         *   Scan for implicit announcements whose actions failed, and mark
1425
         *   the implicit actions as such.  This allows us to phrase the
1426
         *   implicit announcements as attempts rather than as actual
1427
         *   actions, which sounds a little better because it doesn't clash
1428
         *   with the failure report that immediately follows.  
1429
         */
1430
        for (local i = 1, local len = vec.length() ; i <= len ; ++i)
1431
        {
1432
            local sub;
1433
            
1434
            /* get this item */
1435
            local cur = vec[i];
1436
1437
            /* 
1438
             *   If this is an implicit action announcement, and its
1439
             *   corresponding action (or any nested action) failed, mark
1440
             *   the implicit announcement as a mere attempt.  Likewise, if
1441
             *   we're interrupting the action for interactive input, it's
1442
             *   likewise just an incomplete attempt.  
1443
             */
1444
            if (cur.ofKind(ImplicitActionAnnouncement)
1445
                && (sub = vec.valWhich(
1446
                    {x: ((x.isFailure || x.isQuestion)
1447
                         && (x.isPartOf(cur)
1448
                             || x.isActionNestedIn(cur)))})) != nil)
1449
            {
1450
                /* 
1451
                 *   it's either a failed attempt or an interruption for a
1452
                 *   question - note which one 
1453
                 */
1454
                if (sub.isFailure)
1455
                    cur.noteJustTrying();
1456
                else
1457
                    cur.noteQuestion();
1458
            }
1459
        }
1460
1461
        /* 
1462
         *   Scan for implicit announcement groups.  Since we're only
1463
         *   scanning for runs of two or more announcements, we can stop
1464
         *   scanning one short of the end of the list - there's no need to
1465
         *   check the last item because it can't possibly be followed by
1466
         *   another item.  Thus, scan while i < len.  
1467
         */
1468
        for (local i = 1, local len = vec.length() ; i < len ; ++i)
1469
        {
1470
            local origI = i;
1471
            
1472
            /* get this item */
1473
            local cur = vec[i];
1474
            
1475
            /* 
1476
             *   If it's an implied action announcement, and the next one
1477
             *   qualifies for group inclusion, build a group.  Note that
1478
             *   because we only loop until we reach the second-to-last
1479
             *   item, we know for sure there is indeed a next item to
1480
             *   index here.  
1481
             */
1482
            if (cur.ofKind(ImplicitActionAnnouncement)
1483
                && canGroupWith(cur, vec[i+1]))
1484
            {
1485
                local j;
1486
                local groupVec;
1487
1488
                /* create a vector to hold the re-sorted group listing */
1489
                groupVec = new Vector(16);
1490
1491
                /* 
1492
                 *   Scan items for grouping.  This time, we want to scan
1493
                 *   to the last (not second-to-last) item in the main
1494
                 *   list, since we could conceivably group everything
1495
                 *   remaining. 
1496
                 */
1497
                for (j = i ; j <= len ; )
1498
                {
1499
                    /* get this item */
1500
                    cur = vec[j];
1501
                    
1502
                    /* unstack any recursive grouping */
1503
                    j = unstackRecursiveGroup(groupVec, vec, j);
1504
1505
                    /* 
1506
                     *   if we've used now everything in the list, or the
1507
                     *   next item can't be grouped with the current item,
1508
                     *   we're done 
1509
                     */
1510
                    if (j > len || !canGroupWith(cur, vec[j]))
1511
                        break;
1512
                }
1513
1514
                /* process default object announcements */
1515
                processDefaultAnnouncements(groupVec);
1516
1517
                /* build the composite message for the entire group */
1518
                vec[i].messageText_ = implicitAnnouncementGrouper
1519
                    .compositeMessage(groupVec);
1520
1521
                /* 
1522
                 *   Clear the messages in the second through last grouped
1523
                 *   announcements.  Leave the report objects themselves
1524
                 *   intact, so that our internal structural record of the
1525
                 *   transcript remains as it was, but make them silent in
1526
                 *   the displayed text, since these messages are now
1527
                 *   subsumed into the combined first message.  
1528
                 */
1529
                for (++i ; i < j ; ++i)
1530
                    vec[i].messageText_ = '';
1531
1532
                /* 
1533
                 *   continue the main loop from the next element after the
1534
                 *   last one we included in the group 
1535
                 */
1536
                i = j - 1;
1537
            }
1538
1539
            /*
1540
             *   If this is an implied action or default object
1541
             *   announcement that interrupts a set of regular command
1542
             *   reports, and it's for an action nested within the action
1543
             *   generating the reports, add a paragraph spacer before the
1544
             *   implicit announcement.  
1545
             */
1546
            if ((cur.ofKind(ImplicitActionAnnouncement)
1547
                 || cur.ofKind(DefaultObjectAnnouncement))
1548
                && cur.messageText_ != '')
1549
            {
1550
                local j;
1551
                
1552
                /* scan back for the nearest announcement with text */
1553
                for (j = origI - 1 ; j >= 1 && vec[j].messageText_ == '' ;
1554
                     --j) ;
1555
1556
                /* 
1557
                 *   if it's a regular command report, and our implied or
1558
                 *   default announcement is nested within it, add a
1559
                 *   paragraph spacer 
1560
                 */
1561
                if (j >= 1
1562
                    && vec[j].ofKind(FullCommandReport)
1563
                    && cur.isActionNestedIn(vec[j]))
1564
                {
1565
                    /* 
1566
                     *   insert a paragraph spacer before the announcement
1567
                     *   - this will make the implied action and its
1568
                     *   results stand out as separate actions, rather than
1569
                     *   running everything together without spacing 
1570
                     */
1571
                    vec.insertAt(origI, new GroupSeparatorMessage(cur));
1572
1573
                    /* adjust our indices for the insertion */
1574
                    ++i;
1575
                    ++len;
1576
                }
1577
            }
1578
        }
1579
    }
1580
1581
    /*
1582
     *   "Unstack" a recursive group of nested announcements.  Adds the
1583
     *   recursive group to the output group vector in chronological order,
1584
     *   and returns the index of the next item after the recursive group.
1585
     *   
1586
     *   A recursive group is a set of nested implicit commands, where one
1587
     *   implicit command triggered another, which triggered another, and
1588
     *   so on.  The innermost of the nested set is the one that's actually
1589
     *   executed first chronologically, since an implied command must be
1590
     *   carried out before its enclosing command can proceed.  For
1591
     *   example:
1592
     *   
1593
     *.  >go south
1594
     *.  (first opening the door)
1595
     *.  (first unlocking the door)
1596
     *.  (first taking the key out of the bag)
1597
     *   
1598
     *   Going south implies opening the door, but before we can open the
1599
     *   door, we must unlock it, and before we can unlock it we must be
1600
     *   holding the key.  In report order, the innermost command is listed
1601
     *   last, since it's nested within the enclosing commands.
1602
     *   Chronologically, though, the innermost command is actually
1603
     *   executed first.  The purpose of this routine is to unstack these
1604
     *   nested sets, rearranging them into chronological order.  
1605
     */
1606
    unstackRecursiveGroup(groupVec, vec, idx)
1607
    {
1608
        local cur;
1609
1610
        /* remember the item we're tasked to work on */
1611
        cur = vec[idx];
1612
1613
        /* skip the current item */
1614
        ++idx;
1615
        
1616
        /*
1617
         *   Scan for items nested within vec[idx].  Process each child
1618
         *   item first.  An item is nested within us if can be grouped
1619
         *   with us, and its action is a child of our action.  
1620
         */
1621
        for (local len = vec.length() ; idx <= len ; )
1622
        {
1623
            /* if the next item is nested within 'cur', process it */
1624
            if (canGroupWith(cur, vec[idx])
1625
                && vec[idx].getAction().isNestedIn(cur.getAction()))
1626
            {
1627
                /* 
1628
                 *   It's nested with us - process it recursively.  Since
1629
                 *   our goal is to unstack these reports into
1630
                 *   chronological order, we must process our children
1631
                 *   first, so that they get added to the group vector
1632
                 *   first, since children chronologically predede their
1633
                 *   parents. 
1634
                 */
1635
                idx = unstackRecursiveGroup(groupVec, vec, idx);
1636
            }
1637
            else
1638
            {
1639
                /* it's not nested within us, so we're done */
1640
                break;
1641
            }
1642
        }
1643
1644
        /* add our item to the result vector */
1645
        groupVec.append(cur);
1646
1647
        /* 
1648
         *   return the index of the next item; this is simply the current
1649
         *   'idx' value, since we've advanced it past each item we've
1650
         *   processed 
1651
         */
1652
        return idx;
1653
    }
1654
1655
    /*
1656
     *   Process default object announcements in a grouped message vector.
1657
     *   
1658
     *   Default object announcements come in two flavors: with and without
1659
     *   message text.  Those without message text are present purely to
1660
     *   retain a structural record of the default object in the internal
1661
     *   transcript; we can simply remove these, since the actions that
1662
     *   created them didn't even want default messages.  For those that do
1663
     *   include message text, remove them as well, but also use their
1664
     *   actions to replace the corresponding parent actions, so that the
1665
     *   parent actions reflect what actually happened with the final
1666
     *   defaulted objects.  
1667
     */
1668
    processDefaultAnnouncements(vec)
1669
    {
1670
        /* scan the vector for default announcements */
1671
        for (local i = 1, local len = vec.length() ; i <= len ; ++i)
1672
        {
1673
            local cur = vec[i];
1674
            
1675
            /* if this is a default announcement, process it */
1676
            if (cur.ofKind(DefaultObjectAnnouncement))
1677
            {
1678
                /* 
1679
                 *   If it has a message, use its action to replace the
1680
                 *   parent action.  The only way an implied command can
1681
                 *   have a defaulted object is for the implied command to
1682
                 *   have been stated with too few objects, so that an
1683
                 *   askForIobj (for example) occurred.  In such cases, the
1684
                 *   default announcement will be a child action of the
1685
                 *   original underspecified action, so we can simply find
1686
                 *   the original action and replace it with the defaulted
1687
                 *   action.
1688
                 */
1689
                if (cur.messageText_ != '')
1690
                {
1691
                    /* 
1692
                     *   Scan for the parent announcement.
1693
                     *   
1694
                     *   Note that the implicit announcement containing the
1695
                     *   parent action will follow the default announcement
1696
                     *   in the result list, since the default announcement
1697
                     *   is a child of the parent.  
1698
                     */
1699
                    for (local j = i + 1 ; j <= len ; ++j)
1700
                    {
1701
                        /* if this is the parent action, replace it */
1702
                        if (vec[j].getAction()
1703
                            == cur.getAction().parentAction)
1704
                        {
1705
                            /* this is it - replace the action */
1706
                            vec[j].setAction(cur.getAction());
1707
                            
1708
                            /* no need to look any further */
1709
                            break;
1710
                        }
1711
                    }
1712
                }
1713
1714
                /* remove the default announcement from the list */
1715
                vec.removeElementAt(i);
1716
                
1717
                /* adjust our list index and length for the deletion */
1718
                --i;
1719
                --len;
1720
            }
1721
        }
1722
    }
1723
1724
    /* 
1725
     *   Can we group the second item with the first?  Returns true if the
1726
     *   second item is also an implicit action announcement, or it's a
1727
     *   default object announcement whose parent action is the first
1728
     *   item's action. 
1729
     */
1730
    canGroupWith(a, b)
1731
    {
1732
        /* if 'b' is also an implicit announcement, we can include it */
1733
        if (b.ofKind(ImplicitActionAnnouncement))
1734
            return true;
1735
1736
        /* 
1737
         *   if 'b' is a default object announcement, and has the same
1738
         *   parent action as 'a', then we can group it; otherwise we can't
1739
         */
1740
        return (b.ofKind(DefaultObjectAnnouncement)
1741
                && b.getAction().parentAction == a.getAction());
1742
    }
1743
;
1744
1745
/* ------------------------------------------------------------------------ */
1746
/*
1747
 *   Transcript Transform: Complex Multi-object Separation.  If we have an
1748
 *   action that's being applied to one of a bunch of iterated objects, and
1749
 *   the action has any implied command announcements associated with it,
1750
 *   we'll set off the result for this command from its preceding and
1751
 *   following commands by a paragraph separator.  
1752
 */
1753
complexMultiTransform: TranscriptTransform
1754
    applyTransform(trans, vec)
1755
    {
1756
        /* scan the list for multi-object announcements */
1757
        foreach (local cur in vec)
1758
        {
1759
            /* if it's a multi-object announcement, check it */
1760
            if (cur.ofKind(MultiObjectAnnouncement))
1761
            {
1762
                local idx;
1763
                local cnt;
1764
                local sep;
1765
                    
1766
                /* 
1767
                 *   We have a multi-object announcement.  If we find only
1768
                 *   one other report within the group, and the report's
1769
                 *   text is short, let this report run together with its
1770
                 *   neighbors without any additional visual separation.
1771
                 *   Otherwise, set this group apart from its neighbors by
1772
                 *   adding a paragraph break before and after the group;
1773
                 *   this will make the results easier to read by visually
1774
                 *   separating each longish response as a separate
1775
                 *   paragraph.
1776
                 *   
1777
                 *   First, find the current item's index.  
1778
                 */
1779
                idx = vec.indexWhich({x: x == cur});
1780
1781
                /* 
1782
                 *   now scan subsequent items in the same command
1783
                 *   iteration, and check to see if (1) we have more than
1784
                 *   one item, or (2) the item has a longish message 
1785
                 */
1786
                for (cnt = 0, ++idx, sep = nil ;
1787
                     idx <= vec.length() && cnt < 2 ; ++idx, ++cnt)
1788
                {
1789
                    local sub = vec[idx];
1790
                    
1791
                    /* if we've reached the end of the group, stop scanning */
1792
                    if (sub.iter_ != cur.iter_)
1793
                        break;
1794
                    
1795
                    /* 
1796
                     *   If it has long text, add visual separation.  Note
1797
                     *   that "long" is just a heuristic, because we can't
1798
                     *   tell whether the text will wrap in any given
1799
                     *   interpreter - that depends on the width of the
1800
                     *   interpreter window and the font size, among other
1801
                     *   things, and we have no way of knowing any of this
1802
                     *   here.  
1803
                     */
1804
                    if (sub.ofKind(CommandReportMessage)
1805
                        && sub.messageText_ != nil
1806
                        && sub.messageText_.length() > 60)
1807
                    {
1808
                        /* it's long - add separation */
1809
                        sep = true;
1810
                        break;
1811
                    }
1812
                }
1813
1814
                /* if we need separation, add it now */
1815
                if (sep || cnt > 1)   
1816
                {
1817
                    /* 
1818
                     *   This is indeed a complex iterated item.  Set it
1819
                     *   off by paragraph breaks before and after the
1820
                     *   iteration.
1821
                     *   
1822
                     *   First, find the first item in this iteration.  If
1823
                     *   it's not the first item in the whole transcript,
1824
                     *   insert a separator before it.  
1825
                     */
1826
                    idx = vec.indexWhich({x: x.iter_ == cur.iter_});
1827
                    if (idx != 1)
1828
                        vec.insertAt(idx, new GroupSeparatorMessage(cur));
1829
1830
                    /* 
1831
                     *   Next, find the last item in this iteration.  If
1832
                     *   it's no the last item in the entire transcript,
1833
                     *   add a separator after it. 
1834
                     */
1835
                    idx = vec.lastIndexWhich({x: x.iter_ == cur.iter_});
1836
                    if (idx != vec.length())
1837
                        vec.insertAt(idx + 1, new GroupSeparatorMessage(cur));
1838
                }
1839
            }
1840
1841
            /* 
1842
             *   if it's a command result from an implied command, and we
1843
             *   have another command result following from the enclosing
1844
             *   command, add a separator between this result and the next
1845
             *   result 
1846
             */
1847
            if (cur.ofKind(CommandReportMessage) && cur.isActionImplicit)
1848
            {
1849
                local idx;
1850
1851
                /* get the index of this element */
1852
                idx = vec.indexOf(cur);
1853
1854
                /* 
1855
                 *   if there's another element following, check to see if
1856
                 *   it's a command report for an enclosing action (i.e.,
1857
                 *   an action that initiated this implied action) 
1858
                 */
1859
                if (idx < vec.length())
1860
                {
1861
                    local nxt;
1862
1863
                    /* get the next element */
1864
                    nxt = vec[idx + 1];
1865
1866
                    /* 
1867
                     *   if it's a command report for an action that
1868
                     *   encloses this action, or it's another implicit
1869
                     *   announcement, then put a separator before it 
1870
                     */
1871
                    if ((nxt.ofKind(CommandReportMessage)
1872
                         && nxt.getAction() != cur.getAction()
1873
                         && cur.isActionNestedIn(nxt))
1874
                        || nxt.ofKind(ImplicitActionAnnouncement))
1875
                         
1876
                    {
1877
                        /* add a separator */
1878
                        vec.insertAt(idx + 1,
1879
                                     new InternalSeparatorMessage(cur));
1880
                    }
1881
                }
1882
            }
1883
        }
1884
    }
1885
;
1886
1887
/* ------------------------------------------------------------------------ */
1888
/*
1889
 *   Invoke a callback function using a transcript of the given class.
1890
 *   Returns the return value of the callback function.  
1891
 */
1892
withCommandTranscript(transcriptClass, func)
1893
{
1894
    local transcript;
1895
    local oldTranscript;
1896
1897
    /* 
1898
     *   if we already have an active transcript, just invoke the
1899
     *   function, running everything through the existing active
1900
     *   transcript 
1901
     */
1902
    if (gTranscript != nil && gTranscript.isActive)
1903
    {
1904
        /* invoke the callback and return the result */
1905
        return (func)();
1906
    }
1907
1908
    /* 
1909
     *   Create a transcript of the given class.  Make the transcript
1910
     *   transient, since it's effectively part of the output stream state
1911
     *   and thus shouldn't be saved or undone. 
1912
     */
1913
    transcript = transcriptClass.createTransientInstance();
1914
1915
    /* make this transcript the current global transcript */
1916
    oldTranscript = gTranscript;
1917
    gTranscript = transcript;
1918
1919
    /* install the transcript as a filter on the main output stream */
1920
    mainOutputStream.addOutputFilter(transcript);
1921
1922
    /* make sure we undo our global changes before we leave */
1923
    try
1924
    {
1925
        /* invoke the callback and return the result */
1926
        return (func)();
1927
    }
1928
    finally
1929
    {
1930
        /* uninstall the transcript output filter */
1931
        mainOutputStream.removeOutputFilter(transcript);
1932
1933
        /* restore the previous global transcript */
1934
        gTranscript = oldTranscript;
1935
1936
        /* show the transcript results */
1937
        transcript.showReports(true);
1938
    }
1939
}
1940