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

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
3
/* 
4
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
5
 *   
6
 *   TADS 3 Library: input
7
 *   
8
 *   This modules defines functions and objects related to reading input
9
 *   from the player.  
10
 */
11
12
#include "adv3.h"
13
14
15
/* ------------------------------------------------------------------------ */
16
/*
17
 *   Keyboard input parameter definition. 
18
 */
19
class InputDef: object
20
    /* 
21
     *   The prompt function.  This is a function pointer (which is
22
     *   frequently given as an anonymous function) or nil; if it's nil,
23
     *   we won't show any prompt at all, otherwise we'll call the
24
     *   function pointer to display a prompt as needed. 
25
     */
26
    promptFunc = nil
27
28
    /* 
29
     *   Allow real-time events.  If this is true, we'll allow real-time
30
     *   events to interrupt the input; if it's nil, we'll freeze the
31
     *   real-time clock while reading input.  
32
     */
33
    allowRealTime = nil
34
35
    /* 
36
     *   Begin the input style.  This should do anything required to set
37
     *   the font to the desired attributes for the input text.  By
38
     *   default, we'll simply display <.inputline> to set up the default
39
     *   input style.  
40
     */
41
    beginInputFont() { "<.inputline>"; }
42
43
    /* 
44
     *   End the input style.  By default, we'll close the <.inputline>
45
     *   that we opened in beginInputFont(). 
46
     */
47
    endInputFont() { "<./inputline>"; }
48
;
49
50
/*
51
 *   Basic keyboard input parameter definition.  This class defines
52
 *   keyboard input parameters with the real-time status and prompt
53
 *   function specified via the constructor.  
54
 */
55
class BasicInputDef: InputDef
56
    construct(allowRealTime, promptFunc)
57
    {
58
        self.allowRealTime = allowRealTime;
59
        self.promptFunc = promptFunc;
60
    }
61
;
62
63
64
/* ------------------------------------------------------------------------ */
65
/*
66
 *   Keyboard input manager. 
67
 */
68
inputManager: PostRestoreObject
69
    /*
70
     *   Read a line of input from the keyboard.
71
     *   
72
     *   If allowRealTime is true, we'll execute any real-time events that
73
     *   are already due to run, and then we'll allow the input to be
74
     *   interrupted by real-time events, if interrupted input is
75
     *   supported on the local platform.  Otherwise, we will not process
76
     *   any real-time events.
77
     *   
78
     *   promptFunc is a callback function to invoke to display the
79
     *   prompt.  This is provided as a callback so that we can re-display
80
     *   the prompt as necessary after real-time event interruptions.
81
     *   Note that if real-time interruption is not to be allowed, the
82
     *   caller can simply display the prompt before calling this routine
83
     *   rather than passing in a prompt callback, if desired.
84
     *   
85
     *   If we're in HTML mode, this will switch into the 'tads-input'
86
     *   font while reading the line, so this routine should be used
87
     *   wherever possible rather than calling inputLine() or
88
     *   inputLineTimeout() directly.  
89
     */
90
    getInputLine(allowRealTime, promptFunc)
91
    {
92
        /* read input using a basic InputDef for the given parameters */
93
        return getInputLineExt(new BasicInputDef(allowRealTime, promptFunc));
94
    }
95
96
    /*
97
     *   Read a line of input from the keyboard - extended interface,
98
     *   using the InputDef object to define the input parameters.
99
     *   'defObj' is an instance of class InputDef, defining how we're to
100
     *   handle the input.  
101
     */
102
    getInputLineExt(defObj)
103
    {
104
        /* make sure the command transcript is flushed */
105
        if (gTranscript != nil)
106
            gTranscript.flushForInput();
107
        
108
        /* 
109
         *   If a previous input was in progress, cancel it - this must be
110
         *   a recursive entry from a real-time event that's interrupting
111
         *   the enclosing input attempt. Simply cancel out the enclosing
112
         *   read attempt entirely in this case; if and when we return to
113
         *   the enclosing reader, that reader will start over with a
114
         *   fresh read attempt at that point.  
115
         */
116
        cancelInputInProgress(true);
117
        
118
        /* 
119
         *   Keep going until we finish reading the command.  We might
120
         *   have to try several times, because our attempts might be
121
         *   interrupted by real-time events. 
122
         */
123
        for (;;)
124
        {
125
            local result;
126
            local timeout;
127
            local t0;
128
129
            /* note the starting time, in case we want to freeze the clock */
130
            t0 = realTimeManager.getElapsedTime();
131
132
            /* process real-time events, if possible */
133
            timeout = processRealTimeEvents(defObj.allowRealTime);
134
135
            /* show the prompt and any pre-input codes */
136
            inputLineBegin(defObj);
137
138
        getInput:
139
            /* 
140
             *   Read the input.  (Note that if our timeout is nil, this
141
             *   will simply act like the ordinary untimed inputLine.)  
142
             */
143
            result = inputLineTimeout(timeout);
144
145
            /*
146
             *   If we're not allowing real-time event processing, freeze
147
             *   the clock during the read - set the elapsed game
148
             *   real-time clock back to the value it had on entry, so
149
             *   that the input effectively consumes no real time.  
150
             */
151
            if (!defObj.allowRealTime)
152
                realTimeManager.setElapsedTime(t0);
153
154
            /* check the event code from the result list */
155
            switch(result[1])
156
            {
157
            case InEvtNoTimeout:
158
                /* 
159
                 *   the platform doesn't support timeouts - note it for
160
                 *   future reference so that we don't ask for input with
161
                 *   timeout again, then go back to try the input again
162
                 *   without a timeout 
163
                 */
164
                noInputTimeout = true;
165
                timeout = nil;
166
                goto getInput;
167
            
168
            case InEvtLine:
169
                /* we've finished the current line - end input mode */
170
                inputLineEnd();
171
172
                /* return the line of text we got */
173
                return result[2];
174
            
175
            case InEvtTimeout:
176
                /* 
177
                 *   We got a timeout without finishing the input line.
178
                 *   This means that we have reached the time when the
179
                 *   next real-time event is ready to execute.  Simply
180
                 *   continue looping; we'll process all real-time events
181
                 *   that are ready to go, then we'll resume reading the
182
                 *   command.  
183
                 *   
184
                 *   Before we proceed, though, notify the command
185
                 *   sequencer (via the command-interrupt pseudo-tag) that
186
                 *   we're at the start of output text after an
187
                 *   interrupted command line input 
188
                 */
189
                "<.commandint>";
190
                break;
191
            
192
            case InEvtEof:
193
                /* 
194
                 *   End of file - this indicates that the user has closed
195
                 *   down the application, or that the keyboard has become
196
                 *   unreadable due to a hardware or OS error.
197
                 *   
198
                 *   Write a blank line to the display in an attempt to
199
                 *   flush any partially-entered command line text, then
200
                 *   throw an error to signal the EOF condition.  
201
                 */
202
                "\b";
203
                throw new EndOfFileException();
204
205
            case InEvtEndQuietScript:
206
                /* 
207
                 *   End of "quiet" script - this indicates that we've
208
                 *   been reading input from a script file, but we've now
209
                 *   reached the end of that file and are about to return
210
                 *   to reading from the keyboard.
211
                 *   
212
                 *   "Quiet script" mode causes all output to be hidden
213
                 *   while the script is being processed.  This means that
214
                 *   we won't have displayed a prompt for the current
215
                 *   line, or updated the status line.  We'll
216
                 *   automatically display a new prompt when we loop back
217
                 *   for another line of input, but we have to mark the
218
                 *   current input line as actually ended now for that to
219
                 *   happen.  
220
                 */
221
                inputLineInProgress = nil;
222
                inProgressDefObj = nil;
223
224
                /* 
225
                 *   update the status line, since the quiet script mode
226
                 *   will have suppressed all status line updates while we
227
                 *   were reading the script, and thus the last update
228
                 *   before this prompt won't have been shown 
229
                 */
230
                statusLine.showStatusLine();
231
232
                /* back for more */
233
                break;
234
            }
235
        }
236
    }
237
238
    /*
239
     *   Pause for a MORE prompt.  If freezeRealTime is true, we'll stop
240
     *   the real-time clock; otherwise we'll let it keep running.  Even if
241
     *   we don't freeze the clock, we won't actually process any real-time
242
     *   events while waiting: the point of the MORE prompt is to allow the
243
     *   player to read and acknowledge the on-screen display before
244
     *   showing anything new, so we don't want to allow any output to
245
     *   result from real-time events that occur while waiting for user
246
     *   input.  If any real-time events come due while we're waiting,
247
     *   we'll process them when we're done.
248
     *   
249
     *   In order to ensure that the display makes sense to the user, we
250
     *   flush any captured input in the transcript before pausing.  We
251
     *   re-activate transcript capture after the pause if it was active
252
     *   before.  Note that in some cases, this could affect the overall
253
     *   output for the command, since some commands wait until the very
254
     *   end of the command to go back and process the entire transcript
255
     *   for the command.  Since we interrupt the transcript, flushing any
256
     *   output that occurred before the pause, a command that goes back
257
     *   over its entire output stream at the end of the turn won't be able
258
     *   to see or modify any of the output that occurred prior to the
259
     *   pause, since we will have flushed the output to that point.  
260
     */
261
    pauseForMore(freezeRealTime)
262
    {
263
        local t0;
264
        local wasTranscriptActive = nil;
265
266
        /* 
267
         *   flush any command transcript and turn off transcript capture,
268
         *   so that we show any pent-up reports before pausing for the
269
         *   MORE prompt 
270
         */
271
        if (gTranscript != nil)
272
            wasTranscriptActive = gTranscript.flushForInput();
273
        
274
        /* 
275
         *   cancel any pending input - we must be interrupting the
276
         *   pending input with a real-time event 
277
         */
278
        cancelInputInProgress(true);
279
280
        /* note the starting time, in case we want to freeze the clock */
281
        t0 = realTimeManager.getElapsedTime();
282
283
        /* run the MORE prompt */
284
        morePrompt();
285
286
        /* if the transcript was previously active, re-activate it */
287
        if (wasTranscriptActive)
288
            gTranscript.activate();
289
290
        /* 
291
         *   if the caller wanted us to freeze the clock, restore the
292
         *   elapsed game real time to what it was when we started, so
293
         *   that the time the player took to acknowledge the MORE prompt
294
         *   won't count against the elapsed game time; otherwise, process
295
         *   any real-time events that came due while we were waiting 
296
         */
297
        if (freezeRealTime)
298
        {
299
            /* time was frozen - restore the original elapsed time */
300
            realTimeManager.setElapsedTime(t0);
301
        }
302
        else
303
        {
304
            /* 
305
             *   time wasn't frozen - check for any events that have come
306
             *   due since we started waiting, and process them
307
             *   immediately 
308
             */
309
            processRealTimeEvents(true);
310
        }
311
    }
312
313
    /*
314
     *   Read a keystroke, processing real-time events while waiting, if
315
     *   desired.  'allowRealTime' and 'promptFunc' work the same way they
316
     *   do with getInputLine().  
317
     */
318
    getKey(allowRealTime, promptFunc)
319
    {
320
        local evt;
321
        
322
        /* get an event */
323
        evt = getEventOrKey(allowRealTime, promptFunc, true);
324
325
        /* 
326
         *   the only event that getEventOrKey will return is a keystroke,
327
         *   so return the keystroke from the event record 
328
         */
329
        return evt[2];
330
    }
331
332
    /*
333
     *   Read an event, processing real-time events while waiting, if
334
     *   desired.  'allowRealTime' and 'promptFunc' work the same way they
335
     *   do with getInputLine().  
336
     */
337
    getEvent(allowRealTime, promptFunc)
338
    {
339
        /* read and return an event */
340
        return getEventOrKey(allowRealTime, promptFunc, nil);
341
    }
342
343
    /*
344
     *   Read an event or keystroke.  'allowRealTime' and 'promptFunc' work
345
     *   the same way they do in getInputLine().  If 'keyOnly' is true,
346
     *   then we're only interested in keystroke events, and we'll ignore
347
     *   any other events entered.
348
     *   
349
     *   Note that this routine is not generally called directly; callers
350
     *   should usually call the convenience routines getKey() or
351
     *   getEvent(), as needed.  
352
     */
353
    getEventOrKey(allowRealTime, promptFunc, keyOnly)
354
    {
355
        /* make sure the command transcript is flushed */
356
        if (gTranscript != nil)
357
            gTranscript.flushForInput();
358
        
359
        /* 
360
         *   Cancel any in-progress input.  If there's an in-progress
361
         *   input, a real-time event must be interrupting the input,
362
         *   which is recursively invoking us to start a new input. 
363
         */
364
        cancelInputInProgress(true);
365
        
366
        /* keep going until we get a keystroke or other event */
367
        for (;;)
368
        {
369
            local result;
370
            local timeout;
371
            local t0;
372
373
            /* note the starting time, in case we want to freeze the clock */
374
            t0 = realTimeManager.getElapsedTime();
375
376
            /* process real-time events, if possible */
377
            timeout = processRealTimeEvents(allowRealTime);
378
379
            /* show the prompt and any pre-input codes */
380
            inputEventBegin(promptFunc);
381
382
        getInput:
383
            /* 
384
             *   Read the input.  (Note that if our timeout is nil, this
385
             *   will simply act like the ordinary untimed inputLine.)  
386
             */
387
            result = inputEvent(timeout);
388
389
            /*
390
             *   If we're not allowing real-time event processing, freeze
391
             *   the clock during the read - set the elapsed game
392
             *   real-time clock back to the value it had on entry, so
393
             *   that the input effectively consumes no real time.  
394
             */
395
            if (!allowRealTime)
396
                realTimeManager.setElapsedTime(t0);
397
398
            /* check the event code from the result list */
399
            switch(result[1])
400
            {
401
            case InEvtNoTimeout:
402
                /* 
403
                 *   the platform doesn't support timeouts - note it for
404
                 *   future reference so that we don't ask for input with
405
                 *   timeout again, then go back to try the input again
406
                 *   without a timeout 
407
                 */
408
                noInputTimeout = true;
409
                timeout = nil;
410
                goto getInput;
411
            
412
            case InEvtTimeout:
413
                /* 
414
                 *   We got a timeout without finishing the input line.
415
                 *   This means that we have reached the time when the
416
                 *   next real-time event is ready to execute.  Simply
417
                 *   continue looping; we'll process all real-time events
418
                 *   that are ready to go, then we'll resume reading the
419
                 *   command.  
420
                 */
421
                break;
422
            
423
            case InEvtEof:
424
                /* 
425
                 *   End of file - this indicates that the user has closed
426
                 *   down the application, or that the keyboard has become
427
                 *   unreadable due to a hardware or OS error.
428
                 *   
429
                 *   Write a blank line to the display in an attempt to
430
                 *   flush any partially-entered command line text, then
431
                 *   throw an error to signal the EOF condition.  
432
                 */
433
                "\b";
434
                throw new EndOfFileException();
435
436
            case InEvtKey:
437
                /* keystroke - finish the input and return the event */
438
                inputEventEnd();
439
                return result;
440
441
            case InEvtHref:
442
                /* 
443
                 *   Hyperlink activation - if we're allowed to return
444
                 *   events other than keystrokes, finish the input and
445
                 *   return the event; otherwise, ignore the event and keep
446
                 *   looping.  
447
                 */
448
                if (!keyOnly)
449
                {
450
                    inputEventEnd();
451
                    return result;
452
                }
453
                break;
454
455
            default:
456
                /* ignore other events */
457
                break;
458
            }
459
        }
460
    }
461
462
    /*
463
     *   Cancel input in progress.
464
     *   
465
     *   If 'reset' is true, we'll clear any input state saved from the
466
     *   interrupted in-progress editing session; otherwise, we'll retain
467
     *   the saved editing state for restoration on the next input.
468
     *   
469
     *   This MUST be called before calling tadsSay().  Games should
470
     *   generally never call tadsSay() directly (call the library
471
     *   function say() instead), so in most cases authors will not need
472
     *   to worry about calling this on output.
473
     *   
474
     *   This MUST ALSO be called before performing any keyboard input.
475
     *   Callers using inputManager methods for keyboard operations won't
476
     *   have to worry about this, because the inputManager methods call
477
     *   this routine when necessary.  
478
     */
479
    cancelInputInProgress(reset)
480
    {
481
        /* cancel the interpreter's internal input state */
482
        inputLineCancel(reset);
483
484
        /* if we were editing a command line, terminate the editing session */
485
        if (inputLineInProgress)
486
        {
487
            /* do our normal after-input work */
488
            inputLineEnd();
489
        }
490
491
        /* if we were waiting for event input, note that we are no longer */
492
        if (inputEventInProgress)
493
        {
494
            /* do our normal after-input work */
495
            inputEventEnd();
496
        }
497
    }
498
499
    /*
500
     *   Process any real-time events that are ready to run, and return the
501
     *   timeout until the next real-time event.
502
     *   
503
     *   If allowRealTime is nil, we won't process real-time events at all;
504
     *   we'll merely return nil for the timeout to indicate to the caller
505
     *   that any user input interaction about to be attempted should wait
506
     *   indefinitely.  
507
     */
508
    processRealTimeEvents(allowRealTime)
509
    {
510
        local timeout;
511
        
512
        /* presume we will not use a timeout */
513
        timeout = nil;
514
515
        /* process real-time events, if allowed */
516
        if (allowRealTime)
517
        {
518
            local tNext;
519
            
520
            /* 
521
             *   Process any real-time events that are currently ready to
522
             *   execute, and note the amount of time until the next
523
             *   real-time event is ready.  
524
             */
525
            tNext = realTimeManager.executeEvents();
526
527
            /* 
528
             *   If there's an event pending, note the interval between the
529
             *   current time and the event's scheduled time - this will
530
             *   give us the maximum amount of time we want to wait for the
531
             *   user to edit the command line before interrupting to
532
             *   execute the pending event.  Ignore this if the platform
533
             *   doesn't support timeouts to begin with.  
534
             */
535
            if (tNext != nil && !noInputTimeout)
536
                timeout = tNext - realTimeManager.getElapsedTime();
537
        }
538
539
        /* return the timeout until the next real-time event */
540
        return timeout;
541
    }
542
543
    /*
544
     *   Begin reading key/event input.  We'll cancel any report gatherer
545
     *   so that prompt text shows immediately, and show the prompt if
546
     *   desired.  
547
     */
548
    inputEventBegin(promptFunc)
549
    {
550
        /* if we're not continuing previous input, show the prompt */
551
        if (!inputEventInProgress)
552
        {
553
            inputBegin(promptFunc);
554
555
            /* note that we're in input mode */
556
            inputEventInProgress = true;
557
        }
558
    }
559
560
    /*
561
     *   End keystroke/event input.
562
     */
563
    inputEventEnd()
564
    {
565
        /* if input is in progress, terminate it */
566
        if (inputEventInProgress)
567
        {
568
            /* note that we're no longer reading an event */
569
            inputEventInProgress = nil;
570
        }
571
    }
572
573
    /*
574
     *   Begin command line editing.  If we're in HTML mode, we'll show
575
     *   the appropriate codes to establish the input font.  
576
     */
577
    inputLineBegin(defObj)
578
    {
579
        /* notify the command sequencer that we're reading a command */
580
        "<.commandbefore>";
581
        
582
        /* if we're not resuming a session, set up a new session */
583
        if (!inputLineInProgress)
584
        {
585
            /* begin input */
586
            inputBegin(defObj.promptFunc);
587
            
588
            /* switch to input font */
589
            defObj.beginInputFont();
590
591
            /* note that we're in input mode */
592
            inputLineInProgress = true;
593
594
            /* remember the parameter object for this input */
595
            inProgressDefObj = defObj;
596
        }
597
    }
598
599
    /*
600
     *   End command line editing.  If we're in HTML mode, we'll show the
601
     *   appropriate codes to close the input font.  
602
     */
603
    inputLineEnd()
604
    {
605
        /* if input is in progress, terminate it */
606
        if (inputLineInProgress)
607
        {
608
            /* note that we're no longer reading a line of input */
609
            inputLineInProgress = nil;
610
611
            /* end input font mode */
612
            inProgressDefObj.endInputFont();
613
614
            /* notify the command sequencer that we're done reading */
615
            "<.commandafter>";
616
617
            /* 
618
             *   tell the main text area's output stream that we just
619
             *   ended an input line 
620
             */
621
            mainOutputStream.inputLineEnd();
622
623
            /* forget the parameter object for the input */
624
            inProgressDefObj = nil;
625
        }
626
    }
627
628
    /*
629
     *   Begin generic input.  Cancels command report list capture, and
630
     *   shows the prompt if given.  
631
     */
632
    inputBegin(promptFunc)
633
    {
634
        /* 
635
         *   Turn off command transcript capture, if it's active.  Once
636
         *   we're soliciting input interactively, we can no longer
637
         *   usefully capture the text output of commands, but this is fine
638
         *   because we must be doing something for which capture isn't
639
         *   important anyway.  Reporting capture is used for things like
640
         *   selecting the kind of result to show, which clearly isn't a
641
         *   factor for actions involving interactive input.  
642
         */
643
        if (gTranscript != nil)
644
            gTranscript.flushForInput();
645
646
        /* if we have a prompt, display it */
647
        if (promptFunc != nil)
648
            (promptFunc)();
649
    }
650
651
    /* receive post-restore notification */
652
    execute()
653
    {
654
        /* 
655
         *   Reset the inputLine state.  If we had any previously
656
         *   interrupted input from the current interpreter session,
657
         *   forget it by cancelling and resetting the input line.  If we
658
         *   had an interrupted line in the session being restored, forget
659
         *   about that, too.  
660
         */
661
        inputLineCancel(true);
662
        inputLineInProgress = nil;
663
        inputEventInProgress = nil;
664
665
        /* 
666
         *   Clear the inputLineTimeout disabling flag - we might be
667
         *   restoring the game on a different platform from the one where
668
         *   the game started, so we might be able to use timed command
669
         *   line input even if we didn't when we started the game.  By
670
         *   clearing this flag, we'll check again to see if we can
671
         *   perform timed input; if we can't, we'll just set the flag
672
         *   again, so there will be no harm done.  
673
         */
674
        noInputTimeout = nil;
675
    }
676
677
    /* 
678
     *   Flag: command line input is in progress.  If this is set, it means
679
     *   that we interrupted command-line editing by a timeout, so we
680
     *   should not show a prompt the next time we go back to the keyboard
681
     *   for input.  
682
     */
683
    inputLineInProgress = nil
684
685
    /* the InputDef object for the input in progress */
686
    inProgressDefObj = nil
687
688
    /* flag: keystroke/event input is in progress */
689
    inputEventInProgress = nil
690
691
    /*
692
     *   Flag: inputLine does not support timeouts on the current platform.
693
     *   We set this when we get an InEvtNoTimeout return code from
694
     *   inputLineTimeout, so that we'll know not to try calling again with
695
     *   a timeout.  This applies to the current interpreter only, so we
696
     *   must ignore any value restored from a previously saved game, since
697
     *   the game might have been saved on a different platform.
698
     *   
699
     *   Note that if this value is nil, it means only that we've never
700
     *   seen an InEvtNoTimeout return code from inputLineEvent - it does
701
     *   NOT mean that timeouts are supported locally.
702
     *   
703
     *   We assume that the input functions are uniform in their treatment
704
     *   of timeouts; that is, we assume that if inputLineTimeout supports
705
     *   timeout, then so does inputEvent, and that if one doesn't support
706
     *   timeout, the other won't either.  
707
     */
708
    noInputTimeout = nil
709
;
710
711
712
/* ------------------------------------------------------------------------ */
713
/*
714
 *   Read a command line from the player.  Displays the main command
715
 *   prompt and returns a line of input.
716
 *   
717
 *   We process any pending real-time events before reading the command.
718
 *   If the local platform supports real-time command-line interruptions,
719
 *   we'll continue processing real-time events as they occur in the
720
 *   course of command editing.  
721
 */
722
readMainCommand(which)
723
{
724
    local str;
725
    
726
    /* execute any pre-command-prompt daemons */
727
    eventManager.executePrompt();
728
            
729
    /* 
730
     *   Read a line of input, allowing real-time event processing, and
731
     *   return the line of text we read.  Use the appropriate main
732
     *   command prompt for the given prompt mode.  
733
     */
734
    str = inputManager.getInputLine(
735
        true, {: gLibMessages.mainCommandPrompt(which) });
736
737
    /* return the string we read */
738
    return str;
739
}
740
741
742
/* ------------------------------------------------------------------------ */
743
/*
744
 *   End-of-file exception - this is thrown when readMainCommand()
745
 *   encounters end of file reading the console input. 
746
 */
747
class EndOfFileException: Exception
748
;
749
750
751
/* ------------------------------------------------------------------------ */
752
/*
753
 *   'Quitting' exception.  This isn't an error - it merely indicates that
754
 *   the user has explicitly asked to quit the game. 
755
 */
756
class QuittingException: Exception
757
;
758
759
/* ------------------------------------------------------------------------ */
760
/*
761
 *   Base class for command input string preparsers.
762
 *   
763
 *   Preparsers must be registered in order to run.  During
764
 *   preinitialization, we will automatically register any existing
765
 *   preparser objects; preparsers that are created dynamically during
766
 *   execution must be registered explicitly, which can be accomplished by
767
 *   inheriting the default constructor from this class.  
768
 */
769
class StringPreParser: PreinitObject
770
    /*
771
     *   My execution order number.  When multiple preparsers are
772
     *   registered, we'll run the preparsers in ascending order of this
773
     *   value (i.e., smallest runOrder goes first).  
774
     */
775
    runOrder = 100
776
777
    /*
778
     *   Do our parsing.  Each instance should override this method to
779
     *   define the parsing that it does.
780
     *   
781
     *   'str' is the string to parse, and 'which' is the rmcXxx enum
782
     *   giving the type of command we're working with.
783
     *   
784
     *   This method returns a string or nil.  If the method returns a
785
     *   string, the caller will forget the original string and work from
786
     *   here on out with the new version returned; this allows the method
787
     *   to rewrite the original input as desired.  If the method returns
788
     *   nil, it means that the string has been fully handled and that
789
     *   further parsing of the same string is not desired.  
790
     */
791
    doParsing(str, which)
792
    {
793
        /* return the original string unchanged */
794
        return str;
795
    }
796
797
    /* 
798
     *   construction - when we dynamically create a preparser, register
799
     *   it by default
800
     */
801
    construct()
802
    {
803
        /* register the preparser */
804
        StringPreParser.registerPreParser(self);
805
    }
806
807
    /* run pre-initialization */
808
    execute()
809
    {
810
        /* register the preparser if it's not already registered */
811
        StringPreParser.registerPreParser(self);
812
    }
813
814
    /* register a preparser */
815
    registerPreParser(pp)
816
    {
817
        /* if the preparser isn't already in our list, add it */
818
        if (regList.indexOf(pp) == nil)
819
        {
820
            /* append this new item to the list */
821
            regList.append(pp);
822
823
            /* the list is no longer sorted */
824
            regListSorted = nil;
825
        }
826
    }
827
828
    /*
829
     *   Class method - Run all preparsers.  Returns the result of
830
     *   successively calling each preparser on the given string.  
831
     */
832
    runAll(str, which)
833
    {
834
        /* 
835
         *   if the list of preparsers isn't sorted, sort it in ascending
836
         *   order of execution order number
837
         */
838
        if (!regListSorted)
839
        {
840
            /* sort the list */
841
            regList.sort(SortAsc, {x, y: x.runOrder - y.runOrder});
842
            
843
            /* the list is now sorted */
844
            regListSorted = true;
845
        }
846
847
        /* run each preparser */
848
        foreach (local cur in regList)
849
        {
850
            /* run this preparser */
851
            str = cur.doParsing(str, which);
852
853
            /* 
854
             *   if the result is nil, it means that the string has been
855
             *   fully handled, so we need not run any further preparsing 
856
             */
857
            if (str == nil)
858
                return nil;
859
        }
860
861
        /* return the result of the series of preparsing steps */
862
        return str;
863
    }
864
865
    /* class property containing the list of registered parsers */
866
    regList = static new Vector(10)
867
868
    /* class property - the registration list has been sorted */
869
    regListSorted = nil
870
;
871
872
/* ------------------------------------------------------------------------ */
873
/*
874
 *   The "comment" pre-parser.  If the command line starts with a special
875
 *   prefix string (by default, "*", but this can be changed via our
876
 *   commentPrefix property), this pre-parser intercepts the command,
877
 *   treating it as a comment from the player and otherwise ignoring the
878
 *   entire input line.  The main purpose is to give players a way to put
879
 *   comments into recorded transcripts, as notes to themselves when later
880
 *   reviewing the transcripts or as notes to the author when submitting
881
 *   play-testing feedback.  
882
 */
883
commentPreParser: StringPreParser
884
    doParsing(str, which)
885
    {
886
        /* get the amount of leading whitespace, so we can ignore it */
887
        local sp = rexMatch(leadPat, str);
888
        
889
        /* 
890
         *   if the command line starts with the comment prefix, treat it
891
         *   as a comment 
892
         */
893
        if (str.substr(sp + 1, commentPrefix.length()) == commentPrefix)
894
        {
895
            /*
896
             *   It's a comment.
897
             *   
898
             *   If a transcript is being recorded, simply acknowledge the
899
             *   comment; if not, acknowledge it, but with a warning that
900
             *   the comment isn't being saved anywhere 
901
             */
902
            if (scriptStatus.scriptFile != nil)
903
                gLibMessages.noteWithScript;
904
            else if (warningCount++ == 0)
905
                gLibMessages.noteWithoutScriptWarning;
906
            else
907
                gLibMessages.noteWithoutScript;
908
909
            /* 
910
             *   Otherwise completely ignore the command line.  To do this,
911
             *   simply return nil: this tells the parser that the command
912
             *   has been fully handled by the preparser. 
913
             */
914
            return nil;
915
        }
916
        else
917
        {
918
            /* it's not a command - return the string unchanged */
919
            return str;
920
        }
921
    }
922
923
    /* 
924
     *   The comment prefix.  You can change this to any character, or to
925
     *   any sequence of characters (longer sequences, such as '//', will
926
     *   work fine).  If a command line starts with this exact string (or
927
     *   starts with whitespace followed by this string), we'll consider
928
     *   the line to be a comment.  
929
     */
930
    commentPrefix = '*'
931
    
932
    /* 
933
     *   The leading-whitespace pattern.  We skip any text that matches
934
     *   this pattern at the start of a command line before looking for the
935
     *   comment prefix.
936
     *   
937
     *   If you don't want to allow leading whitespace before the comment
938
     *   prefix, you can simply change this to '' - a pattern consisting of
939
     *   an empty string always matches zero characters, so it will prevent
940
     *   us from skipping any leading charactres in the player's input.  
941
     */
942
    leadPat = static new RexPattern('<space>*')
943
944
    /* warning count for entering comments without SCRIPT in effect */
945
    warningCount = 0
946
947
    /*
948
     *   Use a lower execution order than the default, so that we run
949
     *   before most other pre-parsers.  Most other pre-parsers are written
950
     *   to handle actual commands, so it's usually just a waste of time to
951
     *   have them look at comments at all - and can occasionally be
952
     *   problematic, since the free-form text of a comment could confuse a
953
     *   pre-parser that's expecting a more conventional command format.
954
     *   When the comment pre-parser detects a comment, it halts any
955
     *   further processing of the command - so by running ahead of other
956
     *   pre-parsers, we'll effectively bypass other pre-parsers when we
957
     *   detect a comment.  
958
     */
959
    runOrder = 50
960
;
961
962
963
/* ------------------------------------------------------------------------ */
964
/*
965
 *   Read a line of text and return the token list and the original text.
966
 *   We keep going until a non-empty line of text is read.
967
 *   
968
 *   'which' is one of the rmcXxx enum values specifying what kind of
969
 *   command line we're reading.
970
 *   
971
 *   The return value is a list of two elements.  The first element is the
972
 *   string entered, and the second element is the token list.  
973
 */
974
readMainCommandTokens(which)
975
{
976
    local str;
977
    local toks;
978
979
    /* keep going until we get a non-empty command line */
980
    for (;;)
981
    {
982
        /* read a command line */
983
        str = readMainCommand(which);
984
985
        /* run any preparsing desired on the string */
986
        str = StringPreParser.runAll(str, which);
987
988
        /* 
989
         *   if preparsing returned nil, it means that the preparser fully
990
         *   handled the string - simply return nil to tell the caller
991
         *   that its work is done 
992
         */
993
        if (str == nil)
994
            return nil;
995
996
        try
997
        {
998
            /* tokenize the command string */
999
            toks = cmdTokenizer.tokenize(str);
1000
        }
1001
        catch (TokErrorNoMatch tokExc)
1002
        {
1003
            /* 
1004
             *   Invalid tokens in the response - complain about it.  Flag
1005
             *   the error as being in the first character of the
1006
             *   remaining string, since that's the character for which we
1007
             *   could find no match. 
1008
             */
1009
            gLibMessages.invalidCommandToken(tokExc.curChar_.htmlify());
1010
1011
            /* go back for another input line */
1012
            continue;
1013
        }
1014
1015
        /* if we got a non-empty token list, return it */
1016
        if (toks.length() != 0)
1017
            return [str, toks];
1018
1019
        /* show the empty-command reply */
1020
        gLibMessages.emptyCommandResponse();
1021
    }
1022
}
1023
1024