-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathparser.t
6419 lines (5621 loc) · 234 KB
/
parser.t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#charset "us-ascii"
#include "advlite.h"
/* ------------------------------------------------------------------------ */
/*
* Temporary scaffolding for the game world. This gives us information
* on scope, pronoun antecedents, and other information the parser needs
* from the game world.
*/
World: PreinitObject
/*
* Get the list of objects in scope
*/
scope()
{
local s = scope_;
if (s == nil)
scope_ = s = Q.scopeList(gPlayerChar);
return s;
}
/* cached scope list */
scope_ = nil
/*
* A list of all Mentionables in the game, useful for building scope lists
* for resolving Topics.
*/
universalScope = nil
buildUniversalScope()
{
local vec = new Vector(100);
forEachInstance(Mentionable, {o: vec.append(o) });
universalScope = vec.toList;
}
execute()
{
buildUniversalScope();
}
;
/* ------------------------------------------------------------------------ */
/*
* Parser is the class that implements the main parsing procedure, namely
* taking a line of text from the player, figuring out what it means, and
* executing it.
*
* The conventional IF parsing loop simply consists of reading a line of
* text from the player, calling Parser.parse() on the string, and
* repeating.
*
* In most cases you'll just need a single Parser instance. The Parser
* object keeps track of unfinished commands, such as when we need to ask
* for disambiguation help or for a missing object. If for some reason
* you want to keep multiple sets of this kind of state (reading input
* from more than one player, for example), you can create as many Parser
* instances as needed.
*/
class Parser: object
/*
* Auto-Look: Should we treat an empty command line (i.e., the user
* just presses Return) as a LOOK AROUND command?
*
* The traditional handling since the Infocom era has always been to
* treat an empty command line as a parsing error, and display an
* error message along the lines of "I beg your pardon?". Given that
* an empty command line has no conflicting meaning, though, we
* *could* assign it a meaning.
*
* But what meaning should that be? A blank line is the simplest
* possible command for a player to enter, so it would make sense to
* define it as some very commonly used command. It's also fairly
* easy to enter a blank line accidentally (which is partly why the
* traditional reply is an error message), so the command should be
* benign - it shouldn't be a problem to enter it unintentionally.
* It can't be anything with parallel verbs, like NORTH, since then
* there'd be no good reason to pick NORTH instead of, say, SOUTH.
* Finally, it has to be intransitive, since it obviously won't
* involve an object name. The obvious candidates that fit all of
* these criteria are LOOK and INVENTORY. LOOK is probably the more
* useful and the more frequently used of the two, so it's the one we
* choose by default.
*
* If this property is set to true, we'll perform a LOOK AROUND
* command when the player enters a blank command line. If nil,
* we'll show an error message.
*/
autoLook = true
/*
* Default Actions: Should we treat a command line that consists entirely
* of a single noun phrase to be a "Default Action" on the named object?
* The precise meaning of the default action varies by object. For most
* objects, it's EXAMINE. For locations, it's GO TO.
*
* We make the default value nil since setting it to true can result in
* some rather odd parser behaviour.
*/
defaultActions = true
/*
* Should we attempt automatic spelling correction? If this is true, whenever a command
* fails, we'll check for a word that we don't recognize; if we find one, we'll try applying
* spelling correction to see if we can come up with a working command.
*
* Our spelling correction algorithm is designed to be quite conservative. In particular, we
* generally limit candidates for "correct" words to the vocabulary for objects that are
* actually in scope, which avoids revealing the existence of objects that haven't been seen
* yet; and we only apply a correction when it yields a command that parses and resolves
* correctly. When we can't correct a command and get something resolvable, we don't even
* mention that we tried. This avoids the bizarre, random guesses at "corrections" that often
* show up in other applications, and more importantly avoids giving away information that the
* player shouldn't know yet.
*
* We set this to true by default, in an attempt to reduce the player's typing workload by
* automatically correcting simple typos when possible. If for some reason the spelling
* corrector is problematic in a particular game, you can disable it by setting this property
* to nil.
*
* As an experiment, change the default value to be nil when we're in a conversation and true
* otherwise, since over-zealous spelling corrections can be particularly troublesome in a
* conversational context.
*/
autoSpell = (gPlayerChar.currentInterlocutor == nil)
/*
* Maximum spelling correction time, in milliseconds. The spelling
* correction process is iterative, and each iteration involves a new
* parsing attempt. On a fast machine this doesn't tend to be
* noticeable, but it's conceivable that a pathological case could
* involve a large number of attempts that could be noticeably slow
* on an older machine. To avoid stalling the game while we
* overanalyze the spelling possibilities, we set an upper bound to
* the actual elapsed time for spelling correction. Each time we
* consider a new correction candidate, we'll check the elapsed time,
* and abort the process if it exceeds this limit. Note that this
* limit doesn't limit the parsing time itself - we'll never
* interrupt that mid-stream.
*/
spellTimeLimit = 250
/*
* When the parser doesn't recognize a word, should it say so? If
* this property is set to true, when parsing fails, we'll scan the
* command line for a word that's not in the dictionary and show a
* message such as "I don't know the word <foo>." If this property
* is nil, the parser will instead simply say that it doesn't
* recognize the syntax, or that the object in question isn't
* present, without saying specifically which word wasn't recognized,
* or indeed even admitting that there was such a thing.
*
* There are two schools of thought on this, both concerned with
* optimizing the user experience.
*
* The first school holds that the parser's job is to be as helpful
* as possible. First and foremost, that means we should understand
* the user's input as often as possible. But when we can't, it
* means that we should be do our best to explain what we didn't
* understand, to help the user formulate a working command next
* time. In the case of a word the parser doesn't recognize, we can
* be pretty sure that the unknown word is the reason we can't
* understand the input. The best way to help the user correct the
* problem is to let them know exactly which word we didn't know,
* rather than make them guess at what we didn't understand. This is
* the way the classic Infocom games worked, and it's the traditional
* TADS default as well.
*
* The second school holds that the user's overriding interest is
* maintaining suspension of disbelief, and that the parser should do
* its best not to interfere with that. A major aspect of this in IF
* the illusion that the game world is as boundless as the real
* world. Missing dictionary words tend to break this illusion: if
* the user types EXAMINE ZEBRA, and the parser replies that it
* doesn't know the word "zebra", we've suddenly exposed a limit of
* the game world. If we instead play coy and simply say that
* there's no zebra currently present, we allow the player to imagine
* that a zebra might yet turn up. This is the way Inform games
* typically work.
*
* Each approach has its advantages and disadvantages, adherents and
* detractors, and it seems that neither one is objectively "right".
* It comes down to taste. But there seems to be a clear preference
* among the majority of players in the modern era for the second
* approach. The key factor is probably that typical IF commands are
* so short that it's easy enough to spot a typo without help from
* the parser, so the clarity benefits of "unknown word" messages
* seem considerably outweighed by the harm they do to the illusion
* of boundlessness. So, our default is the second option, playing
* coy.
*/
showUnknownWords = nil
/*
* Parse and execute a command line. This is the main parsing
* routine. We take the text of a command line, parse it against the
* grammar defined in the language module, resolve the noun phrases
* to game-world objects, and execute the action. If the command
* line has more than one verb phrase, we repeat the process for each
* one.
*
* 'str' is the text of the command line, as entered by the player.
*/
parse(str)
{
/* Make sure our current SpecialVerb is set to nil before we start parsing a new command. */
specialVerbMgr.currentSV = nil;
/* tokenize the input */
local toks;
try
{
/* run the command tokenizer over the input string */
toks = cmdTokenizer.tokenize(str);
/* Dispose of any unwanted terminal punctuation */
while(toks.length > 0 && getTokType(toks[toks.length]) == tokPunct)
toks = toks.removeElementAt(toks.length);
}
catch (TokErrorNoMatch err)
{
/*
* The tokenizer found a character (usually a punctuation
* mark) that doesn't fit any of the token rules.
*/
DMsg(token error, 'I don\'t understand the punctuation {1}',
err.curChar_);
/* give up on the parse */
return;
}
/*
* Assume initially that the actor is the player character, but only
* if we don't have a question, since if the player is replying to a
* question the actor may already have been resolved.
*/
if(question == nil)
gActor = gPlayerChar;
/* no spelling corrections have been attempted yet */
local history = new transient SpellingHistory(self);
/* we're starting with the first command in the string */
local firstCmd = true;
/* parse the tokens */
try
{
/* if there are no tokens, simply perform the empty command */
if (toks.length() == 0)
{
/*
* this counts as a new command, so forget any previous
* question or typo information
*/
question = nil;
lastTokens = nil;
/* process an empty command */
emptyCommand();
/* we're done */
return;
}
/* check for an OOPS command */
local lst = oopsCommand.parseTokens(toks, cmdDict);
if (lst.length() != 0)
{
/* this only works if we have an error to correct */
local ui;
if (lastTokens == nil
|| (ui = spellingCorrector.findUnknownWord(lastTokens))
== nil)
{
/* OOPS isn't available - throw an error */
throw new CantOopsError();
}
/* apply the correction, and proceed to parse the result */
toks = OopsProduction.applyCorrection(lst[1], lastTokens, ui);
}
/* Update the vocabulary of any game objects with alternating/changing vocab. */
updateVocab();
/* Allow the specialVerb Manager to adjust our toks */
toks = specialVerbMgr.matchSV(toks);
/*
* Parse each predicate in the command line, until we run out
* of tokens. The beginning of a whole new command line is
* definitely the beginning of a sentence, so start parsing
* with firstCommandPhrase.
*/
for (local root = firstCommandPhrase ; toks.length() != 0 ; )
{
/* we don't have a parse list yet */
local cmdLst = nil;
/*
* we haven't found a resolution error in a non-command
* parsing yet
*/
local qErr = nil, defErr = nil;
/*
* If we have an outstanding question, and it takes
* priority over interpreting input as a new command, try
* parsing the input against the question. Only do this
* on the first command on the line - a question answer
* has to be the entire input, so if we've already parsed
* earlier commands on the same line, this definitely
* isn't an answer to a past question.
*/
if (firstCmd && question != nil && question.priority)
{
/* try parsing against the Question */
local l = question.parseAnswer(toks, cmdDict);
/* if it parsed and resolved, this is our command */
if (l != nil && l.cmd != nil)
cmdLst = l;
/* if it parsed but didn't resolved, note the error */
if (l != nil)
qErr = l.getResErr();
}
/*
* if the question didn't grab it, try parsing as a whole
* new command against the ordinary command grammar
*/
if (cmdLst == nil || cmdLst.cmd == nil)
{
cmdLst = new CommandList(
root, toks, cmdDict, { p: new Command(p) });
}
/*
* If we didn't find any resolvable commands, and this is
* the first command, check to see if it's an answer to
* an outstanding query. We only check this if the
* regular grammar parsing fails, because anything that
* looks like a valid new command overrides a past query.
* This is important because some of the short, common
* commands sometimes can look like noun phrases, so we
* explicitly give preference to interpreting these as
* brand new commands.
*/
if (cmdLst.cmd == nil
&& firstCmd
&& question != nil
&& !question.priority)
{
/* try parsing against the Question */
local l = question.parseAnswer(toks, cmdDict);
/* if it parsed and resolved, this is our command */
if (l != nil && l.cmd != nil)
cmdLst = l;
/* if it parsed but didn't resolved, note the error */
if (l != nil)
qErr = l.getResErr();
}
/*
* If we don't have a command yet, and this is the first
* command on the line, handle it as a conversational command
* if conversation is in progress; otherwise if default
* actions are enabled, check to see if the command looks like
* a single noun phrase. If so, handle it as the default
* action on the noun.
*/
if (cmdLst.cmd == nil
&& firstCmd)
{
local l;
/*
* If a conversation is in progress parse the command line
* as the single topic object phrase of a Say command,
* provided that the first word on the command line
* doesn't match a possible action.
*/
if(gPlayerChar.currentInterlocutor != nil
&& cmdLst.length == 0
&& Q.canTalkTo(gPlayerChar,
gPlayerChar.currentInterlocutor)
&& str.find(',') == nil
&& gPlayerChar.currentInterlocutor.allowImplicitSay())
{
l = new CommandList(
topicPhrase, toks, cmdDict,
{ p: new Command(SayAction, p) });
libGlobal.lastCommandForUndo = str;
savepoint();
}
/*
* If the player char is not in conversation with anyone,
* or the first word of the command matches a possible
* command verb, then try parsing the command line as a
* single direct object phrase for the DefaultAction verb,
* provided defaultActions are enabled (which they aren't
* by default).
*/
else if(defaultActions)
l = new CommandList(
defaultCommandPhrase, toks, cmdDict,
{ p: new Command(p) });
/* accept a curable reply */
if (l != nil && l.acceptCurable() != nil)
{
cmdLst = l;
/* note any resolution error */
defErr = l.getResErr();
}
}
/*
* If we've applied a spelling correction, and the
* command match didn't consume the entire input, make
* sure what's left of the input has a valid parsing as
* another command. This ensures that we don't get a
* false positive by excessively shortening a command,
* which we can sometimes do by substituting a word like
* "then" for another word.
*/
if (cmdLst.length() != nil
&& history.hasCorrections())
{
/* get the best available parsing */
local c = cmdLst.getBestCmd();
/* if it doesn't use all the tokens, check what's left */
if (c != nil && c.tokenLen < toks.length())
{
/* try parsing the next command */
local l = commandPhrase.parseTokens(
c.nextTokens, cmdDict);
/*
* if that didn't work, invalidate the command by
* substituting an empty command list
*/
if (l.length() == 0)
cmdLst = new CommandList();
}
}
/*
* If we didn't find a parsing at all, it's a generic "I
* don't understand" error. If we found a parsing, but
* not a resolution, reject it if it's a spelling
* correction. We only want completely clean spelling
* corrections, without any errors.
*/
if (cmdLst.length() == 0
|| (history.hasCorrections()
&& cmdLst.getResErr() != nil
&& !cmdLst.getResErr().allowOnRespell))
{
/*
* If we were able to parse the input using one of
* the non-command interpretations, use the
* resolution error from that parsing. Otherwise, we
* simply can't make any sense of this input, so use
* the generic "I don't understand" error.
*/
local err = (qErr != nil ? qErr :
defErr != nil ? defErr :
new NotUnderstoodError());
/* look for a spelling correction */
local newToks = history.checkSpelling(toks, err);
if (newToks != nil)
{
/* parse again with the new tokens */
toks = newToks;
continue;
}
/*
* There's no spelling correction available. If we've
* settled on an auto-examine or question error, skip
* that and go back to "I don't understand" after
* all. We don't want to assume Auto-Examine unless we
* actually have something to examine, since we can
* parse noun phrase grammar out of practically any
* input.
*/
if (err is in (defErr, qErr))
{
/* return to the not-understood error */
err = new NotUnderstoodError();
/* check spelling again with this error */
newToks = history.checkSpelling(toks, err);
if (newToks != nil)
{
/* parse again with the new tokens */
toks = newToks;
continue;
}
/*
* We didn't find any spelling corrections this time
* through. Since we're rolling back to the
* not-understood error, discard any spelling
* corrections we attempted with other
* interpretations.
*/
history.clear();
}
/* fail with the error */
throw err;
}
/* if we found a resolvable command, execute it */
if (cmdLst.cmd != nil)
{
/* get the winning Command */
local cmd = cmdLst.cmd;
/*
* We next have to ensure that the player hasn't entered
* multiple nouns in a slot that only allows a single noun
* in the grammar. If the player has entered two objects
* like "the bat and the ball" in such a case, the
* badMulti flag will be set on the command object, so we
* first test for that and abort the command with a
* suitable error message if badMulti is not nil (by
* throwing a BadMultiError
*
* Unfortunately the badMulti flag doesn't get set if the
* player enters a multiple object as a plural (e.g.
* "bats"), so we need to trap this case too. We do that
* by checking whether there's multiple objects in the
* direct, indirect and accessory object slots at the same
* time as the grammar tag matching the slot in question
* is 'normal', which it is only for a single noun match.
*/
if(cmd && cmd.verbProd != nil &&
(cmd.badMulti != nil
|| (cmd.verbProd.dobjMatch != nil &&
cmd.verbProd.dobjMatch.grammarTag == 'normal'
&& cmd.dobjs.length > 1)
||
(cmd.verbProd.iobjMatch != nil &&
cmd.verbProd.iobjMatch.grammarTag == 'normal'
&& cmd.iobjs.length > 1)
||
(cmd.verbProd.accMatch != nil &&
cmd.verbProd.accMatch.grammarTag == 'normal'
&& cmd.accs.length > 1)
))
cmd.cmdErr = new BadMultiError(cmd.np);
/* if this command has a pending error, throw it */
if (cmd.cmdErr != nil)
throw cmd.cmdErr;
/*
* Forget any past question and typo information.
* The new command is either an answer to this
* question, or it's simply ignoring the question; in
* either case, the question is no longer in play for
* future input.
*/
question = nil;
lastTokens = nil;
/* note any spelling changes */
history.noteSpelling(toks);
/* execute the command */
cmd.exec();
/* start over with a new spelling correction history */
history = new transient SpellingHistory(self);
/*
* Set the root grammar production for the next
* predicate. If the previous command ended the
* sentence, start a new sentence; otherwise, use the
* additional clause syntax.
*/
root = cmd.endOfSentence
? firstCommandPhrase : commandPhrase;
/* we're no longer on the first command in the string */
firstCmd = nil;
/* go back and parse the remainder of the command line */
toks = cmd.nextTokens;
continue;
}
/*
* We weren't able to resolve any of the parse trees. If
* one of the errors is "curable", meaning that the
* player can fix it by answering a question, pick the
* first of those, in predicate priority order.
* Otherwise, just pick the first command overall in
* predicate priority order. In either case, since we
* didn't find any working alternatives, it's time to
* actually show the error and fail the command.
*/
local c = cmdLst.acceptAny();
/*
* If the error isn't curable, check for spelling errors,
* time permitting. Don't bother doing this with a
* curable error, since that will have its own way of
* solving the problem that reflects a better
* understanding of the input than considering it a
* simple typo.
*/
if (!c.cmdErr.curable)
{
/*
* For spelling correction purposes, if this is an
* unmatched noun error, but the command has a misc
* word list and an empty noun phrase, treat this as
* a "not understood" error. The combination of noun
* phrase errors suggests that we took a word that
* was meant to be part of the verb, and incorrectly
* parsed it as part of a noun phrase, leaving the
* verb structure and other noun phrase incomplete.
* This is really a verb syntax error, not a noun
* phrase error.
*/
local spellErr = c.cmdErr;
if (c.cmdErr.ofKind(UnmatchedNounError)
&& c.miscWordLists.length() > 0
&& c.missingNouns > 0)
spellErr = new NotUnderstoodError();
/* try spelling correction */
local newToks = history.checkSpelling(toks, spellErr);
/* if that worked, try the corrected command */
if (newToks != nil)
{
/* parse again with the new tokens */
toks = newToks;
continue;
}
}
/* re-throw the error that caused the resolution to fail */
throw c.cmdErr;
}
}
catch (ParseError err)
{
/*
* roll back any spelling changes to the last one that
* improved matters
*/
local h = history.rollback(toks, err);
toks = h.oldToks;
err = h.parseError;
/*
* if this is a curable error, it poses a question, which the
* player can answer on the next input
*/
if (err.curable)
question = new ParseErrorQuestion(err);
/*
* If the current error isn't curable, and unknown word
* disclosure is enabled, and there's a word in the command
* that's not in the dictionary, replace the parsing error
* with an unknown word error.
*/
local ui;
if (!err.curable
&& showUnknownWords
&& (ui = spellingCorrector.findUnknownWord(toks)) != nil)
{
/* find the misspelled word in the original tokens */
err = new UnknownWordError(getTokOrig(toks[ui]));
}
/*
* If the new error isn't an error in an OOPS command, save
* the token list for an OOPS command next time out.
*/
if (!err.ofKind(OopsError))
lastTokens = toks;
/* log any spelling changes we kept */
history.noteSpelling(toks);
/* display the error we finally decided upon */
err.display();
}
catch (CommandSignal sig)
{
/*
* On any command signal we haven't caught so far, simply
* stop processing this command line.
*/
}
}
/*
* The token list from the last command, if an error occurred. This
* is the token list that we'll retry if the player enters an OOPS
* command.
*/
lastTokens = nil
/*
* The outstanding Question object. When we ask an interactive
* question (such as a disambiguation query, a missing noun phrase
* query, or a custom question from the game), this is set to the
* Question waiting to be answered. We parse the next command
* against the Question to see if it's a reply, and if so we execute
* the reply.
*/
question = nil
/*
* Execute an empty command line. The parse() routine calls this
* when given a blank command line (i.e., the user simply pressed the
* Return key). By default, we execute a Look Around command if
* autoLook is enabled, otherwise we show the "I beg your pardon"
* error.
*/
emptyCommand()
{
if (autoLook)
new Command(Look).exec();
else
{
/*
* The player entered an empty command line (i.e., pressed
* Return at the command prompt, without typing anything else
* first). Note that this error can only occur if Auto-Look
* is disabled, since otherwise an empty command implicitly
* means LOOK AROUND.
*/
DMsg(empty command line, 'I beg your pardon?');
}
}
/*
* The action to be tried if the parser can't find a verb in the command
* line and tries to parse the command line as the single object of a
* DefaultAction command instead.
*/
DefaultAction = ExamineOrGoTo
/* Return an rmcXXXX enum code depending on the state of Parser.question */
rmcType()
{
if(Parser.question != nil && Parser.question.err != nil)
{
/*
* If the Parser error is an EmptyNounError then we're asking for
* an object.
*/
if(Parser.question.err.ofKind(EmptyNounError))
return rmcAskObject;
/*
* If the Parser error is an AmbiguousError then we're requesting
* disambiguation.
*/
if(Parser.question.err.ofKind(AmbiguousError))
return rmcDisambig;
}
/*
* If there's no special situation, assume we're reading a standard
* command.
*/
return rmcCommand;
}
/*
* Update the vocabulary of items in the game for which it might vary; specifically those with
* an altVocab defined.
*/
updateVocab()
{
/* Retrieve the list of items that have alternating vocabulary. */
local lst = libGlobal.altVocabLst;
/*
* If the list is longer than a certain amount, it may become more efficient to iterate
* over only those items in the list that are already in scope.
*/
if(lst.length > 30)
{
/* Get a list of items in scope. */
local scope = Q.scopeList(gPlayerChar).toList();
/* Reduce our list of variable vocab items to include only those in scope. */
lst = lst.intersect(scope);
}
/* Call updateVocab() on every item in our list. */
lst.forEach({ x: x.updateVocab() });
}
;
/* ------------------------------------------------------------------------ */
/*
* Base class for command execution signals. These allow execution
* handlers to terminate execution for the current command line or
* portion of the command line.
*/
class CommandSignal: Exception
;
/*
* Terminate the entire command line.
*/
class ExitCommandLineSignal: CommandSignal
;
/* ------------------------------------------------------------------------ */
/*
* A CommandList is a set of potential parsings for a given input with a
* given grammar.
*/
class CommandList: object
/*
* new CommandList(prod, toks, dict, wrapper) - construct a new
* CommandList object by parsing an input token list. 'prod' is the
* GrammarProd to parse against; 'toks' is the token list; 'dict' is
* the main parser dictionary; 'wrapper' is a callback function that
* maps a parse tree to a Command object.
*
* new CommandList(Command) - construct a CommandList containing a
* single pre-resolved Command object.
*/
construct([args])
{
/* check which argument list we have */
if (args.matchProto([GrammarProd, Collection, Dictionary, TypeFuncPtr]))
{
/* retrieve the arguments */
local prod = args[1], toks = args[2], dict = args[3],
wrapper = args[4];
/* parse the token list, and map the list to Command objects */
cmdLst = prod.parseTokens(toks, dict).mapAll(wrapper);
/* sort in priority order */
cmdLst = Command.sortList(cmdLst);
/*
* Go through the list, looking for an item with noun phrases we
* can resolve. Take the first item that we can properly
* resolve.
*/
foreach (local c in cmdLst)
{
try
{
/* resolve this phrase */
c.resolveNouns();
/* success - take this as the result; look no further */
/*
* But only if we haven't yet got a command or the new
* command didn't have to create a new Topic object.
*/
if(cmd == nil || !c.madeTopic)
cmd = c;
/*
* But if this command had to create a new Topic object,
* let's go on to see if we can find a better match.
*/
if(!cmd.madeTopic)
break;
}
catch(InsufficientNounsError err)
{
c.cmdErr = err;
throw err;
}
catch(NoneInOwnerError err)
{
c.cmdErr = err;
throw err;
}
catch (ParseError err)
{
/* save the error with the command */
c.cmdErr = err;
/*
* That didn't resolve correctly. But don't actually
* show the error message yet; instead, continue through
* the list to see if we can find another alternative
* that we can resolve.
*
* If it's the first curable error we've seen, note it.
*/
if (err.curable && curable == nil)
curable = c;
}
}
}
else if (args.matchProto([Command]))
{
/* get the command - it's the single list entry */
cmd = args[1];
cmdLst = [cmd];
}
else if (args.matchProto([]))
{
/* empty command list */
cmd = nil;
cmdLst = [];
}
else
throw new ArgumentMismatchError();
}
/* number of parsings in the list */
length() { return cmdLst.length(); }
/*
* Accept a curable resolution as the actual resolution. If we don't
* have an error-free resolution, we'll set 'cmd' to the curable
* resolution. Returns true if we have any resolution, nil if not.
*/
acceptCurable()
{
/* if we don't have an error-free resolution, accept a curable one */
if (cmd == nil)
cmd = curable;
/* indicate whether we have a resolution now */
return cmd;
}
/*
* Accept ANY command, with or without a resolution error, curable or
* not. We'll take the error-free resolution if we have one,
* otherwise the resolution with a curable error, otherwise just the
* first parsing in priority order.
*/
acceptAny()
{
/* accept the best command, and return it */
return cmd = getBestCmd();
}
/*
* Get the most promising command from the available parsings. This
* returns the first successfully resolved command in priority order,
* if any; otherwise the first command with a curable error, if any;
* otherwise the first command in priority order.
*/
getBestCmd()
{
/* if we have a parsed and resolved command, return it */
if (cmd != nil)
return cmd;
/* if we have a curable command, return it */
if (curable != nil)