File Coverage

blib/lib/Games/Rezrov/ZDict.pm
Criterion Covered Total %
statement 324 1196 27.0
branch 43 630 6.8
condition 10 143 6.9
subroutine 61 97 62.8
pod 0 41 0.0
total 438 2107 20.7


line stmt bran cond sub pod time code
1             package Games::Rezrov::ZDict;
2             # dictionary routines
3              
4 1     1   9 use strict;
  1         2  
  1         48  
5 1     1   34 use 5.004;
  1         4  
  1         44  
6             #use SelfLoader;
7              
8 1     1   560 use Games::Rezrov::ZObjectCache;
  1         3  
  1         25  
9 1     1   6 use Games::Rezrov::ZObject;
  1         2  
  1         16  
10 1     1   4 use Games::Rezrov::ZText;
  1         2  
  1         15  
11 1     1   5 use Games::Rezrov::ZConst;
  1         2  
  1         93  
12 1     1   5 use Games::Rezrov::ZObjectStatus;
  1         2  
  1         20  
13 1     1   5 use Games::Rezrov::Inliner;
  1         2  
  1         32  
14              
15 1         8 use Games::Rezrov::MethodMaker ([],
16             qw(
17             ztext
18             dictionary_word_start
19             entry_length
20             entry_count
21             separators
22             encoded_word_length
23             version
24             decoded_by_word
25             decoded_by_address
26             object_cache
27             last_random
28            
29             dictionary_fully_decoded
30              
31             bp_cheat_data
32 1     1   6 ));
  1         28  
33              
34 1     1   5 use constant OMAP_START_INDENT => 1;
  1         1  
  1         1353  
35 1     1   5 use constant OMAP_INDENT_STEP => 3;
  1         2  
  1         38  
36              
37 1     1   5 use constant WWW_BROWSER_EXES => qw(firefox netscape mozilla phoenix firebird);
  1         2  
  1         45  
38             # add more executables here
39              
40 1     1   4 use constant ZORK_1 => ("Zork I", 88, "840726", 41257);
  1         1  
  1         37  
41 1     1   5 use constant ZORK_2 => ("Zork II", 48, "840904", 55449);
  1         1  
  1         35  
42 1     1   4 use constant ZORK_3 => ("Zork III", 17, "840727", 11898);
  1         1  
  1         35  
43 1     1   4 use constant ENCHANTER => ("Enchanter", 29, "860820", 9543);
  1         1  
  1         34  
44 1     1   3 use constant SORCERER => ("Sorcerer", 15, "851108", 10467);
  1         2  
  1         35  
45 1     1   4 use constant SPELLBREAKER => ("Spellbreaker", 87, "860904", 2524);
  1         1  
  1         42  
46 1     1   3 use constant INFIDEL => ("Infidel", 22, "830916", 16674);
  1         1  
  1         67  
47 1     1   5 use constant ZTUU => ("Zork: The Undiscovered Underground", 16, 970828, 4485);
  1         2  
  1         59  
48 1     1   5 use constant PLANETFALL => ("Planetfall", 37, "851003", 726);
  1         3  
  1         53  
49 1     1   4 use constant BUREAUCRACY => ("Bureaucracy", 116, 870602, 64613);
  1         2  
  1         55  
50 1     1   5 use constant SAMPLER1 => ("Sampler", 55, 850823, 28449);
  1         1  
  1         55  
51 1     1   5 use constant BEYOND_ZORK => ("Beyond Zork", 57, 871221, 50605);
  1         1  
  1         47  
52              
53 1         47 use constant SNIDE_MESSAGES => (
54             'A hollow voice says, "cretin."',
55             'An invisible boot kicks you in the shin. Ouch!',
56             'An invisible hand smacks you in the head. Ouch!',
57             # 'An invisible hand slaps you smartly across the face. Ouch!',
58 1     1   5 );
  1         1  
59              
60 1         40 use constant PILFER_LOCAL_MESSAGES => (
61             'The %s glows briefly with a faint blue glow.',
62             'Sparks fly from the %s!',
63             'The %s shimmers briefly.',
64 1     1   4 );
  1         1  
65              
66 1         66 use constant PILFER_SELF_MESSAGES => (
67             'You feel invisible hands grope around your person.',
68             'You feel invisible hands rifling through your possessions.',
69            
70 1     1   4 );
  1         2  
71              
72 1         42 use constant PILFER_REMOTE_MESSAGES => (
73             'The earth seems to shift slightly beneath your feet.',
74             'You hear a roll of thunder in the distance.',
75             'A butterfly flits by, glistening green and gold and black. There is a sound of thunder...',
76             # Ray Bradbury = The Man
77             'The smell of burning leaves surrounds you.',
78 1     1   5 );
  1         1  
79              
80 1         58 use constant TELEPORT_MESSAGES => (
81             'You blink, and find your surroundings have changed...',
82             'You are momentarily dizzy, and then...',
83             '*** Poof! ***',
84             # 'The taste of salted peanuts fills your mouth.',
85            
86 1     1   4 );
  1         1  
87              
88 1         42 use constant TELEPORT_HERE_MESSAGES => (
89             "Look around you!",
90             "Sigh...",
91             # "So that's why cabs have minimum fares...",
92             "You experience the strange sensation of materializing in your own shoes.",
93 1     1   5 );
  1         1  
94              
95 1         70 use constant TELEPORT_TO_ITEM_MESSAGES => (
96             "Oh yes, that's right over here...",
97             "Right this way...",
98 1     1   5 );
  1         1  
99              
100 1         44 use constant SHAMELESS_MESSAGES => (
101             "Michael Edmonson just wishes he were an Implementor.",
102             "Michael Edmonson is a sinister, lurking presence in the dark places of the earth. His favorite diet is onion rings from Cooke's Seafood, but his insatiable appetite is tempered by his fear of light. Michael Edmonson has never been seen by the light of day, and few have survived his fearsome jaws to tell the tale.",
103             "Michael Edmonson has too much time on his hands.",
104             "Michael Edmonson is at this moment most likely parked in front of his whiz-bang PC.",
105 1     1   6 );
  1         1  
106              
107 1         54 use constant FROTZ_SELF_MESSAGES => (
108             "Nah.",
109             "Bizarre!",
110             "I'd like to; unfortunately it won't work.",
111             "How about one of your fine possessions instead?",
112 1     1   4 );
  1         1  
113              
114 1         39 use constant BANISH_MESSAGES => (
115             # 'The %s disappears in a shower of sparks.',
116             'A sinister black fog descends; when it lifts, the %s is nowhere to be seen.',
117             'There is a bright flash; when you open your eyes, the %s is nowhere to be seen.',
118             'The %s disappears with a pop.'
119 1     1   4 );
  1         2  
120              
121 1         43 use constant BANISH_CONTAINER_MESSAGES => (
122             'The %s flickers with a faint blue glow.',
123             'The %s shimmers briefly...'
124 1     1   4 );
  1         1  
125              
126 1         47 use constant BANISH_SELF_MESSAGES => (
127             'You feel a tickle...',
128             'Your load feels lighter.',
129             '%s? What %s?',
130 1     1   5 );
  1         7  
131              
132 1         63 use constant TRAVIS_MESSAGES => (
133             "Looking at the %s, you suddenly feel an inflated sense of self-esteem.",
134             "The %s looks more dangerous already.",
135             "The %s glows wickedly.",
136 1     1   5 );
  1         2  
137              
138 1         54 use constant LUMMOX_MESSAGES => (
139             "Your load feels less heavy.",
140             "Your possessions seem suddenly ephemeral.",
141             # "Suddenly, you get some great ideas on how to reorganize your closet.",
142             "You are struck with some great ideas on how to reorganize your closet.",
143 1     1   6 );
  1         1  
144              
145 1         40 use constant HELP_INFOCOM_URLS => (
146             "http://www.csd.uwo.ca/Infocom/Invisiclues/",
147 1     1   5 );
  1         1  
148              
149 1         42 use constant HELP_GENERIC_URLS => (
150             "http://www.yahoo.com/Recreation/Games/Interactive_Fiction/",
151 1     1   5 );
  1         2  
152              
153 1         49 use constant VILIFY_MESSAGES => (
154             "I never liked the look of that %s.",
155             "That %s is really asking for trouble.",
156 1     1   5 );
  1         1  
157              
158 1         46 use constant VILIFY_SELF_MESSAGES => (
159             "I never liked you to begin with!",
160             "Okay...you're ugly and your mother dresses you funny.",
161             "You are filled with self-loathing.",
162             "You disgust me."
163 1     1   4 );
  1         2  
164              
165 1         49 use constant BASTE_MESSAGES => (
166             "The %s looks mouth-wateringly delicious.",
167             # "The %s looks particularly toothsome.",
168             "Mmm, %s."
169 1     1   4 );
  1         2  
170              
171 1         41 use constant VOLUMINUS_SELF_MESSAGES => (
172             "You're pretty full of yourself already.",
173             "You're pretty full of it already.",
174 1     1   4 );
  1         2  
175              
176 1         33 use constant VOLUMINUS_MESSAGES => (
177             "The interior of the %s seems to recede away from you.",
178 1     1   5 );
  1         1  
179              
180 1         41 use constant VOLUMINUS_CLOSED_MESSAGES => (
181             "The %s seems to bulge for a moment."
182 1     1   4 );
  1         2  
183              
184 1         43 use constant GO_BACK_TO_X => (
185             "New York",
186             "San Francisco",
187             "New Jersey",
188 1     1   3 );
  1         2  
189              
190 1         40 use constant WWW_HELP_MESSAGES => (
191             "I can barely see what's going on there, but I'll see what I can do...",
192             "Perhaps your plea will be heard."
193 1     1   4 );
  1         1  
194              
195 1         120 use constant ANGIOTENSIN_MESSAGES => (
196             "It looks suspiciously like a children's vitamin.",
197             "Use caution when driving, operating machinery, or performing other hazardous activities.",
198             "Side effects may include dizziness or rash.",
199 1     1   4 );
  1         2  
200              
201 1         67 use constant CANT_FIND_YOU_YET_MESSAGES => (
202             "Sorry, I haven't got my bearings just yet; try again in a few moves.",
203             "Move around a little first so I can lock on to your signal...",
204             "Take a few steps first so I can triangulate your signal...",
205 1     1   5 );
  1         2  
206              
207 1         44 use constant SPEECH_ENABLED_MESSAGES => (
208             "Speech output enabled.",
209             "Hello.",
210             "Hello there.",
211             # "Bitchin' Betty activated.",
212             "Altitude! Altitude!",
213             "Dough Re Mi Fa So La Ti Dough..."
214 1     1   4 );
  1         2  
215              
216 1         45 use constant GMACHO_MESSAGES => (
217             "While your spellbook remains closed, its pages seem to rustle for a moment.",
218             "For a moment you could swear your spellbook was glowing with a faint blue glow.",
219 1     1   4 );
  1         2  
220              
221 1     1   5 use constant PLENTY_O_ROOM => 32000;
  1         1  
  1         15328  
222              
223             %Games::Rezrov::ZDict::MAGIC_WORDS = map {$_ => 1} (
224             "pilfer",
225             "teleport",
226             "#teleport",
227             "bamf",
228             "lingo",
229             "embezzle",
230             "omap",
231             "lumen",
232             "frotz",
233             "futz",
234             "travis",
235             "bickle",
236             "tail",
237             "#sa",
238             "#sp",
239             "#dta",
240             "#dat", "spiel",
241             "#sprop",
242             "rooms",
243             "items",
244             "#sgv",
245             "#slv",
246             "#ggv",
247             "#serials",
248             "lummox",
249             "systolic",
250             "vilify",
251             "baste", "nosh",
252             "voluminus",
253             # "compartmentalize",
254             "angiotensin",
255              
256             "gmacho",
257              
258             "verdelivre",
259             );
260              
261             %Games::Rezrov::ZDict::ALIASES = (
262             "x" => "examine",
263             "g" => "again",
264             "z" => "wait",
265             "l" => "look",
266             );
267              
268             my $INLINE_CODE = '
269             sub new {
270             my ($type, $addr) = @_;
271             my $self = [];
272             bless $self, $type;
273             $self->version(Games::Rezrov::StoryFile::version());
274             $self->ztext(Games::Rezrov::StoryFile::ztext());
275             my $header = Games::Rezrov::StoryFile::header();
276             $self->encoded_word_length($header->encoded_word_length());
277             my $dp;
278             if ($addr) {
279             $dp = $addr;
280             } else {
281             $dp = $header->dictionary_address();
282             }
283            
284             $self->decoded_by_word({});
285             $self->decoded_by_address({});
286              
287             #
288             # get token separators
289             #
290             my $sep_count = GET_BYTE_AT($dp++);
291             my %separators;
292             for (my $i=0; $i < $sep_count; $i++) {
293             $separators{chr(GET_BYTE_AT($dp++))} = 1;
294             }
295             $self->separators(\%separators);
296            
297             $self->entry_length(GET_BYTE_AT($dp++));
298             # number of bytes for each encoded word
299             $self->entry_count(Games::Rezrov::StoryFile::get_word_at($dp));
300             # number of words in the dictionary
301             $dp += 2;
302              
303             $self->dictionary_word_start($dp);
304             # start address of encoded words
305            
306             # die sprintf "%s %s\n", $self->entry_length(), $self->entry_count();
307            
308             return $self;
309             }
310              
311             ';
312              
313             Games::Rezrov::Inliner::inline(\$INLINE_CODE);
314             #print $INLINE_CODE;
315             #die;
316 1 50   1 0 3 eval $INLINE_CODE;
  1         4  
  1         4  
  1         5  
  1         7  
  1         4  
  1         29  
  1         2  
  1         3  
  0         0  
  1         66  
  1         29  
  1         28  
  1         5  
  1         3  
  1         6  
  3         15  
  1         30  
  1         31  
  1         7  
  1         3  
  1         25  
  1         5  
317             undef $INLINE_CODE;
318              
319              
320             1;
321              
322             #__DATA__
323              
324             sub save_buffer {
325             # copy the input buffer to story memory.
326             # This may be called internally during oops emulation.
327 4     4 0 8 my ($self, $buf, $text_address) = @_;
328 4         7 my $mem_offset;
329 4         130 my $z_version = $self->version();
330 4         10 my $len = length $buf;
331 4 50       14 if ($z_version >= 5) {
332 0         0 Games::Rezrov::StoryFile::set_byte_at($text_address + 1, $len);
333 0         0 $mem_offset = $text_address + 2;
334             } else {
335 4         10 $mem_offset = $text_address + 1;
336             }
337            
338 4         16 for (my $i=0; $i < $len; $i++, $mem_offset++) {
339             # copy the buffer to memory
340 29         80 Games::Rezrov::StoryFile::set_byte_at($mem_offset, ord substr($buf,$i,1));
341             }
342 4 50       19 Games::Rezrov::StoryFile::set_byte_at($mem_offset, 0) if ($z_version <= 4);
343             # terminate the line
344             }
345              
346             sub tokenize_line {
347 4     4 0 16 my ($self, $text_address, $token_address, %options) = @_;
348             # $text_len, $oops_word) = @_;
349 4         8 my $text_len = $options{"-len"};
350 4         12 my $oops_word = $options{"-oops"};
351 4   50     31 my $flag = $options{"-flag"} || 0;
352            
353             # my $b1 = new Benchmark();
354 4         23 my $max_tokens = Games::Rezrov::StoryFile::get_byte_at($token_address);
355 4         8 my $token_p = $token_address + 2;
356             # pointer to location where token data will be written
357 4         124 my $separators = $self->separators();
358              
359             #
360             # Step 1: parse out the tokens
361             #
362 4         9 my $text_p = $text_address + 1;
363             # skip past max bytes enterable
364 4 50       109 if ($self->version() >= 5) {
365 0 0       0 $text_len = Games::Rezrov::StoryFile::get_byte_at($text_p) unless defined $text_len;
366             # needed if called from tokenize opcode (VAR 0x1b)
367 0         0 $text_p++;
368             # move pointer past length of entered text.
369             }
370 4         22 my $raw_input = Games::Rezrov::StoryFile::get_string_at($text_p, $text_len);
371             # print STDERR "raw: $raw_input\n";
372              
373 4         6 my $text_end = $text_p + $text_len;
374             # we're passed the length because in <= v4 we would have to count
375             # the bytes in the buffer, looking for terminating zero.
376              
377 4         8 my @tokens;
378 4         7 my $start_offset = 0;
379             # token start position
380 4         7 my $token = "";
381              
382 4         7 my $c;
383 4         8 my $token_done = 0;
384 4         5 my $all_done = 0;
385 4         12 while (! $all_done) {
386 33 100       57 if ($text_p >= $text_end) {
387             # finished
388 4         8 $token_done = 1;
389 4         7 $all_done = 1;
390             } else {
391 29 100       53 $start_offset = $text_p unless $start_offset;
392 29         84 $c = chr(Games::Rezrov::StoryFile::get_byte_at($text_p++));
393 29 100       87 if ($c eq ' ') {
    50          
394             # a space character:
395 2 50       5 if ($token ne "") {
396             # token is completed
397 2         4 $token_done = 1;
398             } else {
399             # ignore whitespace: move start pointer past it
400 0         0 $start_offset++;
401             }
402             } elsif (exists $separators->{$c}) {
403             # hit a game-specific token separator
404             # print STDERR "separator: $c\n";
405 0         0 $token_done = 1;
406 0 0       0 if ($token ne "") {
407             # a token is already built; use it, and move
408             # text pointer back one so we'll make a new token
409             # out of this separator
410 0         0 $text_p--;
411             } else {
412             # the separator itself is a token
413 0         0 $token = $c;
414             }
415             } else {
416             # append to the token
417 27         37 $token .= $c;
418             }
419             }
420 33 100       89 if ($token_done) {
421             # push @tokens, [ $token, $start_offset - $text_address ] if $token;
422 6 50       28 push @tokens, [ $token, $start_offset - $text_address ] if $token ne "";
423 6         13 $token = "";
424 6         19 $token_done = $start_offset = 0;
425             }
426             }
427             # printf STDERR "tokens: %s\n", join "/", map {$_->[0]} @tokens;
428              
429 4 0 33     19 if (@tokens == 3 and
      33        
      0        
      0        
430             Games::Rezrov::ZOptions::SHAMELESS() and
431             $tokens[0]->[0] =~ /^(who|what)$/i and
432             $tokens[1]->[0] =~ /^is$/ and
433             $tokens[2]->[0] =~ /^(michae\w*|edmons\w*)/) {
434             # shameless self-promotion
435 0 0       0 unless ($self->get_dictionary_address($1)) {
436             # don't do anything if name is in dictionary (e.g. Suspect has a Michael)
437 0         0 $self->write_text($self->random_message(SHAMELESS_MESSAGES));
438 0         0 $self->newline();
439 0         0 $self->newline();
440 0         0 $self->suppress_output();
441 0         0 return;
442             }
443             }
444              
445             #
446             # Step 2: store dictionary addresses for words
447             #
448 4         115 my $encoded_length = $self->encoded_word_length();
449 4         9 my $wrote_tokens = 0;
450 4         7 my $untrunc_token;
451 4         16 for (my $ti = 0; $ti < @tokens; $ti++) {
452 6         11 my ($token, $offset) = @{$tokens[$ti]};
  6         14  
453 6 50       16 if ($wrote_tokens++ < $max_tokens) {
454 6         12 $untrunc_token = lc($token);
455 6 100       18 $token = substr($token,0,$encoded_length)
456             if length($token) > $encoded_length;
457 6         26 my $addr = $self->get_dictionary_address($token);
458 6 50       19 if ($addr == 0) {
459             # NOP if in dictionary
460 0 0 0     0 if (Games::Rezrov::ZOptions::EMULATE_NOTIFY() and $token eq "notify") {
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0          
461 0         0 $self->notify_toggle();
462             } elsif (lc($token) eq "#speak") {
463             # toggle speech output
464 0         0 my $zio = Games::Rezrov::StoryFile::screen_zio();
465             # horrible
466 0         0 my $msg;
467 0 0       0 if ($zio->speaking()) {
468 0         0 $msg = "Speech output disabled.";
469 0         0 $zio->speaking(0);
470             } else {
471 0 0       0 if ($zio->init_speech_synthesis()) {
472             # ok
473 0         0 $msg = $self->random_message(SPEECH_ENABLED_MESSAGES);
474             } else {
475 0         0 $msg = $zio->speech_synthesis_error();
476             }
477             }
478 0         0 $self->write_text($msg);
479 0         0 newline();
480 0         0 newline();
481 0         0 suppress_output();
482             } elsif (lc($untrunc_token) eq "#listen") {
483             # toggle speech recognition
484 0         0 my $zio = Games::Rezrov::StoryFile::screen_zio();
485             # horrible
486 0         0 my $msg;
487 0 0       0 if ($zio->listening()) {
488 0         0 $msg = "Speech recognition disabled.";
489 0         0 $zio->speaking(0);
490             } else {
491 0 0       0 if ($zio->init_speech_recognition()) {
492             # ok
493 0         0 $msg = "Speech recognition enabled.";
494             } else {
495 0         0 $msg = $zio->speech_recognition_error();
496             }
497             }
498 0         0 $self->write_text($msg);
499 0         0 newline();
500 0         0 newline();
501 0         0 suppress_output();
502              
503             } elsif (lc($token) eq "#typo") {
504 0         0 my $status = !Games::Rezrov::ZOptions::CORRECT_TYPOS();
505 0 0       0 $self->write_text(sprintf "Typo correction is now %s.", $status ? "on" : "off");
506 0         0 Games::Rezrov::ZOptions::CORRECT_TYPOS($status);
507 0         0 $self->newline();
508 0         0 $self->newline();
509 0         0 $self->suppress_output();
510             } elsif (Games::Rezrov::ZOptions::EMULATE_HELP() and $token eq "help") {
511 0         0 $self->help();
512             } elsif (Games::Rezrov::ZOptions::EMULATE_OOPS() and ($oops_word or
513             (($token eq "oops") or
514             (Games::Rezrov::ZOptions::ALIASES() and $token eq "o")))) {
515 0 0       0 if ($oops_word) {
516             # replace misspelled word
517 0         0 $addr = $self->get_dictionary_address($oops_word);
518             } else {
519             # entered "oops"
520 0         0 my $last_input = Games::Rezrov::StoryFile::last_input();
521 0         0 $self->save_buffer($last_input, $text_address);
522 0         0 $self->tokenize_line($text_address,
523             $token_address,
524             "-len" => length($last_input),
525             "-oops" => $tokens[$ti + 1]->[0]);
526 0         0 return;
527             }
528             } elsif (Games::Rezrov::ZOptions::MAGIC() and exists $Games::Rezrov::ZDict::MAGIC_WORDS{$untrunc_token}) {
529 0         0 (my $what = $raw_input) =~ s/.*?${untrunc_token}\s*//i;
530             # use the raw input rather than joining the remaining tokens.
531             # Necessary if the query string contains what the game considers
532             # tokenization characters. For example, "Mrs. Robner" in Deadline
533             # is broken into 3 tokens: "Mrs", ".", and "Robner". Joined
534             # this is "Mrs . Robner", which doesn't match anything in the object
535             # table.
536             # print STDERR "magic: -$what-\n";
537 0         0 $self->magic($untrunc_token, $what);
538             # $ti < @tokens - 1 ?
539             # join " ", map {$_->[0]} @tokens[$ti + 1 .. $#tokens]
540             # : "");
541             } elsif (Games::Rezrov::ZOptions::ALIASES() and
542             exists $Games::Rezrov::ZDict::ALIASES{$untrunc_token}) {
543 0         0 $addr = $self->get_dictionary_address($Games::Rezrov::ZDict::ALIASES{$untrunc_token});
544             } elsif (Games::Rezrov::ZOptions::EMULATE_COMMAND_SCRIPT() and
545             $untrunc_token eq "#reco" or
546             $untrunc_token eq "#unre" or
547             $untrunc_token eq "#comm") {
548 0 0       0 if ($untrunc_token eq "#comm") {
549             # play back commands
550 0         0 Games::Rezrov::StoryFile::input_stream(Games::Rezrov::ZConst::INPUT_FILE);
551             } else {
552 0 0       0 Games::Rezrov::StoryFile::output_stream($untrunc_token eq "#reco" ? Games::Rezrov::ZConst::STREAM_COMMANDS : - Games::Rezrov::ZConst::STREAM_COMMANDS);
553             }
554 0         0 $self->newline();
555 0         0 $self->suppress_output();
556             } elsif ($untrunc_token eq "#cheat") {
557 0         0 my $status = !(Games::Rezrov::ZOptions::MAGIC());
558 0         0 Games::Rezrov::ZOptions::MAGIC($status);
559 0 0       0 $self->write_text(sprintf "Cheating is now %sabled.", $status ? "en" : "dis");
560 0         0 $self->newline();
561 0         0 $self->newline();
562 0         0 $self->suppress_output();
563             }
564             }
565            
566 6 50 33     39 if ($flag and $addr == 0) {
567             # sect15.html#tokenise:
568             # when $flag is set, don't touch entries not in the dictionary.
569 0         0 1;
570             } else {
571 6         24 Games::Rezrov::StoryFile::set_word_at($token_p, $addr);
572 6         21 Games::Rezrov::StoryFile::set_byte_at($token_p + 2, length $untrunc_token);
573 6         20 Games::Rezrov::StoryFile::set_byte_at($token_p + 3, $offset);
574             }
575 6         24 $token_p += 4;
576             } else {
577 0         0 $self->write_text("Too many tokens; ignoring $token");
578 0         0 $self->newline();
579             }
580             }
581              
582 4         15 Games::Rezrov::StoryFile::set_byte_at($token_address + 1, $wrote_tokens);
583             # record number of tokens written
584              
585             # my $b2 = new Benchmark();
586             # my $td = timediff($b2, $b1);
587             # printf STDERR "took: %s\n", timestr($td, 'all');
588              
589             }
590              
591             sub get_dictionary_address {
592             # get the dictionary address for the given token.
593             #
594             # NOTES:
595             # This does NOT conform to the spec; officially, we should encode
596             # the word and look up the encoded value. This would be a bit
597             # faster, but I'm too Lazy and Impatient right now to do it that
598             # way. Contains ugly hacks for non-alphanumeric "words".
599             #
600             # alas, certain v5 opcodes require text encoding. Tomorrow :)
601             #
602 7     7 0 12 my $self = $_[0];
603 7         15 my $token = lc($_[1]);
604              
605 7         218 my $max = $self->encoded_word_length();
606 7 50       20 $token = substr($token,0,$max) if length($token) > $max;
607             # make sure token is truncated to max length
608              
609 7         177 my $by_name = $self->decoded_by_word();
610              
611 7 100       22 if (exists $by_name->{$token}) {
612             # we already know where this word is; return its address
613             # print STDERR "cache hit for $token\n";
614 6         22 return $by_name->{$token};
615             } else {
616             # find the word
617 1         32 my $dict_start = $self->dictionary_word_start();
618 1         25 my $ztext = $self->ztext();
619 1         26 my $num_words = $self->entry_count();
620 1         27 my $entry_length = $self->entry_length();
621 1         30 my $by_address = $self->decoded_by_address();
622 1         4 my $char = substr($token,0,1);
623 1         2 my $search_index;
624 1         3 my $linear_search = 0;
625 1 50       19 if ($char =~ /[a-z]/) {
    0          
626 1         7 $search_index = int(($num_words - 1) * (ord(lc($char)) - ord('a')) / 26);
627             # pick an approximate start position
628             } elsif (ord($char) < ord 'a') {
629 0         0 $search_index = 0;
630 0         0 $linear_search = 1;
631             } else {
632 0         0 printf STDERR "tokenize: fix me, char %d", ord($char);
633             }
634              
635 1         3 my ($address, $word, $delta_mult, $delta, $next);
636 1         2 my $behind = -1;
637 1         3 my $ahead = $num_words;
638 1         1 while (1) {
639 8         10 $address = $dict_start + ($search_index * $entry_length);
640 8 50       17 if (exists $by_address->{$address}) {
641             # already know word for this address
642             # print STDERR "address cache hit!\n";
643 0         0 $word = $by_address->{$address};
644             } else {
645             # decode word at this address and cache
646 8         9 $word = ${$ztext->decode_text($address)};
  8         222  
647 8         22 $by_name->{$word} = $address;
648 8         20 $by_address->{$address} = $word;
649             }
650             # print "Got $word at $search_index\n";
651 8 50       14 if ($word eq $token) {
652             # found the word we're looking for: done
653 0         0 return $address;
654             } else {
655             # missed: search further
656 8 50       15 if ($linear_search) {
657 0         0 $next = $search_index + 1;
658             } else {
659 8         9 $delta_mult = $token cmp $word;
660             # determine direction we need to search
661 8 100       18 if ($delta_mult == -1) {
662             # ahead; need to search back
663 3         6 $delta = int(($search_index - $behind) / 2);
664 3         4 $ahead = $search_index;
665             } else {
666             # behind; need to search ahead
667 5         9 $delta = int(($ahead - $search_index) / 2);
668 5         8 $behind = $search_index;
669             }
670 8 100       19 $delta = 1 if $delta == 0;
671 8         10 $next = $search_index + ($delta * $delta_mult);
672             }
673 8 50 33     51 if ($next < 0 or $next >= $num_words) {
    100 66        
674             # out of range
675 0         0 return 0;
676             } elsif ($next == $ahead or $next == $behind) {
677             # word does not exist between flanking words
678 1         9 return 0;
679             } else {
680 7         9 $search_index = $next;
681             }
682             }
683             }
684             }
685 0         0 die;
686             }
687              
688             sub magic {
689             #
690             # >read dusty book
691             # The first page of the book was the table of contents. Only two
692             # chapter names can be read: The Legend of the Unseen Terror and
693             # The Legend of the Great Implementers.
694             #
695             # >read legend of the implementers
696             # This legend, written in an ancient tongue, speaks of the
697             # creation of the world. A more absurd account can hardly be
698             # imagined. The universe, it seems, was created by "Implementers"
699             # who directed the running of great engines. These engines
700             # produced this world and others, strange and wondrous, as a test
701             # or puzzle for others of their kind. It goes on to state that
702             # these beings stand ready to aid those entrapped within their
703             # creation. The great magician-philosopher Helfax notes that a
704             # creation of this kind is morally and logically indefensible and
705             # discards the theory as "colossal claptrap and kludgery."
706             #
707            
708 0     0 0 0 my ($self, $token, $what) = @_;
709 0         0 my $object_cache = $self->get_object_cache();
710              
711 0         0 my $player_object = Games::Rezrov::StoryFile::player_object();
712 0         0 my $current_room = Games::Rezrov::StoryFile::current_room();
713              
714 0 0       0 if ($what) {
715 0 0 0     0 if ($player_object and $what =~ /^(me|self)$/i) {
    0 0        
716             # for the purposes of these commands, consider "me" and "self"
717             # equivalent to the player object (whatever that's called)
718 0         0 my $desc = $object_cache->print($player_object);
719 0         0 $what = $$desc;
720             } elsif ($current_room and $what =~ /^here$/) {
721             # likewise consider "here" to be the current room
722 0         0 my $desc = $object_cache->print($current_room);
723 0         0 $what = $$desc;
724             }
725             }
726              
727 0         0 my $just_one_newline = 0;
728              
729 0 0 0     0 if (0 and $token eq "fbg") {
    0 0        
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
730             # can we make arbitrary things glow with a faint blue glow?
731             # (nope)
732             my $zo = new Games::Rezrov::ZObject(160);
733             # 160=mailbox
734             my $zp = $zo->get_property(12);
735             $self->write_text($zp->property_exists() ? "yes" : "no");
736             } elsif (0 and $token eq "fbg2") {
737             # do all objects with "blue glow" property behave the same?
738             my $object_cache = $self->get_object_cache();
739             for (my $i = 1; $i <= $object_cache->last_object(); $i++) {
740             my $zo = new Games::Rezrov::ZObject($i);
741             my $zp = $zo->get_property(12);
742             if ($zp->property_exists()) {
743             $zp->set_value(3);
744             $self->write_text(${$zo->print()});
745             $self->newline();
746             }
747             }
748 0         0 } elsif ($token eq "rooms") {
749 0         0 $self->dump_objects(2);
750             } elsif ($token eq "items") {
751 0         0 $self->dump_objects(3);
752             } elsif ($token eq "#serials") {
753 0         0 my $header = Games::Rezrov::StoryFile::header();
754 0         0 $self->write_text(sprintf "Z-machine version %d, ",
755             Games::Rezrov::StoryFile::version());
756 0         0 $self->write_text(sprintf "release %s, ", $header->release_number());
757 0         0 $self->write_text(sprintf "serial number %s, ", $header->serial_code());
758 0         0 $self->write_text(sprintf "checksum %s.", $header->file_checksum());
759             } elsif ($token eq "systolic") {
760             # lower blood pressure (Bureaucracy only)
761 0         0 $self->systolic();
762             } elsif ($token eq "angiotensin") {
763             # take blood pressure regulating medication (Bureaucracy only)
764 0         0 $self->medicate();
765             } elsif ($token eq "lummox") {
766             # remove restrictions on weight and number of items that can be carried
767 0         0 $self->lummox();
768             } elsif ($token eq "omap") {
769             # dump object relationships
770 0         0 $self->dump_objects(1, $what);
771             } elsif ($token eq "lingo") {
772             # dump the dictionary
773 0         0 $self->dump_dictionary($what);
774             } elsif ($token eq "embezzle") {
775             # manipulate game score
776 0 0       0 if ($self->version() > 3) {
    0          
    0          
777 0         0 $self->write_text("Sorry, this trick only works in version 3 games.");
778             } elsif (Games::Rezrov::StoryFile::header()->is_time_game()) {
779 0         0 $self->write_text("Sorry, this trick doesn't work in \"time\" games.");
780             } elsif (length $what) {
781 0 0       0 if ($what =~ /^-?\d+$/) {
782 0         0 Games::Rezrov::StoryFile::set_global_var(1, $what);
783 0         0 $self->write_text("\"Clickety click...\"");
784             # BOFH
785             } else {
786 0         0 $self->write_text("Is that a score on your planet?");
787             }
788             } else {
789 0         0 $self->write_text("Tell me what to set your score to.");
790             }
791             } elsif ($token =~ "#sgv") {
792 0         0 my ($var, $value) = split /\s+/, $what;
793 0         0 $self->write_text("Setting global variable $var to $value.");
794 0         0 Games::Rezrov::StoryFile::set_global_var($var, $value);
795             } elsif ($token =~ "#slv") {
796 0         0 my ($var, $value) = split /\s+/, $what;
797 0         0 $self->write_text("Setting local variable $var to $value.");
798 0         0 Games::Rezrov::StoryFile::set_variable($var, $value);
799             } elsif ($token =~ "#ggv") {
800 0         0 $self->write_text(sprintf "Global variable %d is %d.", $what,
801             Games::Rezrov::StoryFile::get_global_var($what));
802             } elsif ($token =~ "#?teleport") {
803 0         0 $self->teleport($what);
804             } elsif ($token eq "baste" or $token eq "nosh") {
805 0         0 $self->baste($token, $what);
806             } elsif ($token eq "voluminus") {
807 0         0 $self->voluminus($token, $what);
808             } elsif ($token eq "gmacho") {
809 0         0 $self->gmacho($token, $what);
810             } elsif ($token eq "verdelivre") {
811 0         0 $self->bookworm($token, $what);
812             # } elsif ($token eq "compartmentalize") {
813             # $self->compartmentalize($token, $what);
814             } elsif ($token eq "vilify") {
815 0         0 $Games::Rezrov::IGNORE_PROPERTY_ERRORS = 1;
816 0         0 $self->vilify($what);
817             } elsif ($token eq "travis" or $token eq "bickle") {
818 0         0 $self->travis($what);
819             } elsif ($token =~ /^(frotz|futz|lumen)$/) {
820 0         0 $self->frotz($what);
821             } elsif ($token eq "tail") {
822 0         0 $self->tail($what);
823             } elsif ($token eq "#sa") {
824 0         0 $self->set_attr($what);
825             } elsif ($token eq "#sp") {
826 0         0 $self->set_property($what);
827             } elsif ($token eq "#dta") {
828 0         0 $self->decode_text_at($what);
829             } elsif ($token eq "#dat" or $token eq "spiel") {
830 0         0 $self->decode_all_text(split /\s+/, $what);
831             } elsif ($token eq "#sprop") {
832 0         0 $self->property_dump($what);
833             } else {
834             # pilfer or bamf
835 0 0       0 my @hits = $what ? $object_cache->find($what, "-room" => 0) : ();
836 0 0       0 if (@hits > 1) {
    0          
    0          
    0          
    0          
837 0         0 $self->write_text(sprintf 'Hmm, which do you mean: %s?',
838 0         0 nice_list(sort map {$_->[1]} @hits));
839             } elsif (@hits == 1) {
840 0         0 my ($id, $desc) = @{$hits[0]};
  0         0  
841 0         0 my $zo = $object_cache->get($id);
842 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($hits[0]->[0],
843             $object_cache);
844              
845 0 0       0 if ($token eq "bamf") {
    0          
846             #
847             # Make an object disappear
848             #
849 0 0       0 if ($zstat->is_player()) {
    0          
850 0         0 $self->write_text("You are beyond help already.");
851             } elsif ($zstat->in_current_room()) {
852 0 0       0 if ($zstat->in_inventory()) {
    0          
853 0         0 $self->write_text(ucfirst(sprintf $self->random_message(BANISH_SELF_MESSAGES), $desc, $desc));
854             } elsif ($zstat->is_toplevel_child()) {
855             # top-level, should be visible
856 0         0 $self->write_text(sprintf $self->random_message(BANISH_MESSAGES), $desc);
857             } else {
858             # in something else
859 0         0 $self->write_text(sprintf $self->random_message(BANISH_CONTAINER_MESSAGES), ${$zstat->toplevel_child()->print()});
  0         0  
860             }
861 0         0 $self->move_object($id, 0);
862             # set the object's parent to zero (nothing)
863             } else {
864 0         0 $self->write_text(sprintf "I don't see any %s here.", ${$zo->print()});
  0         0  
865             }
866             } elsif ($token eq "pilfer") {
867             #
868             # Try to move and item to inventory
869             # (move it to this room and submit "take" command)
870             #
871 0         0 my $proceed = 0;
872 0 0 0     0 if (!$player_object or !Games::Rezrov::StoryFile::current_room()) {
    0          
    0          
873 0         0 $self->write_text("Sorry, I haven't got my bearings just yet. Maybe you could walk around a little and try again.");
874             } elsif ($zstat->is_player()) {
875 0 0       0 if ($desc eq "cretin") {
876 0         0 $self->write_text("\"cretin\" suits you, I see.");
877             } else {
878 0         0 $self->write_text($self->random_message(SNIDE_MESSAGES));
879             }
880             } elsif ($zstat->in_current_room()) {
881 0 0       0 if ($zstat->in_inventory()) {
    0          
882 0         0 $self->write_text($self->random_message(PILFER_SELF_MESSAGES));
883 0         0 $proceed = 1;
884             # sometimes makes sense: pilfer canary from egg, even
885             # when carrying it
886             } elsif ($zstat->is_toplevel_child()) {
887             # at top level in room (should already be visible)
888 0         0 $self->write_text($self->random_message(SNIDE_MESSAGES));
889 0         0 $self->newline();
890 0         0 $self->write_text(sprintf "The %s seems unaffected.", $desc);
891             } else {
892             # inside something else in this room
893 0         0 $self->write_text(sprintf $self->random_message(PILFER_LOCAL_MESSAGES), ${$zstat->toplevel_child->print});
  0         0  
894 0         0 $proceed = 1;
895             }
896             } else {
897 0         0 $self->write_text($self->random_message(PILFER_REMOTE_MESSAGES));
898 0         0 $proceed = 1;
899             }
900 0 0       0 if ($proceed) {
901 0         0 $self->move_object($id, $current_room);
902 0         0 my $thing = (reverse(split /\s+/, $desc))[0];
903             # if description is multiple words, use the last one.
904             # example: zork 1, "jewel-encrusted egg" becomes "egg".
905             # (parser doesn't understand "jewel-encrusted" part)
906             # room for improvement: check to make sure this word
907             # is in dictionary
908 0         0 $self->steal_turn("take " . $thing);
909 0         0 $just_one_newline = 1;
910             }
911             } else {
912 0         0 die "unknown cheat $token";
913             }
914             } elsif ($what) {
915 0         0 $self->write_text(sprintf "I don't know what that is, though I have seen a %s that you might be interested in...", ${$object_cache->get_random()});
  0         0  
916             } elsif ($token eq "pilfer") {
917 0         0 $self->write_text("Please tell me what you want to pilfer.");
918             } elsif ($token eq "bamf") {
919 0         0 $self->write_text("Please tell me what you want to make disappear.");
920             } else {
921 0         0 $self->write_text("Can you be more specific?");
922             }
923             }
924              
925 0         0 $self->newline();
926 0 0       0 $self->newline() unless $just_one_newline;
927 0         0 $self->suppress_output();
928             # suppress parser output ("I don't know the word XXX.");
929             }
930              
931             sub get_object_cache {
932             # FIX ME
933 0 0   0 0 0 unless ($_[0]->object_cache()) {
934 0         0 my $cache = new Games::Rezrov::ZObjectCache();
935 0         0 $cache->load_names();
936 0         0 $_[0]->object_cache($cache);
937             }
938 0         0 return $_[0]->object_cache();
939             }
940              
941             sub random_message {
942 0     0 0 0 my ($self, @messages) = @_;
943 0         0 my $index;
944 0   0     0 my $last_hash = $self->last_random() || $self->last_random({});
945 0         0 my $last_stamp = $last_hash->{$messages[0]};
946 0         0 while (1) {
947 0         0 $index = int(rand(scalar @messages));
948 0 0 0     0 last if (@messages == 1 or
      0        
949             !defined($last_stamp) or
950             $index ne $last_stamp);
951             # don't use the same index twice in a row for a given set of messages
952             }
953 0         0 $last_hash->{$messages[0]} = $index;
954 0         0 return $messages[$index];
955             }
956              
957             sub nice_list {
958 0 0   0 0 0 if (@_ == 1) {
    0          
959 0         0 return $_[0];
960             } elsif (@_ == 2) {
961 0         0 return join " or ", @_;
962             } else {
963 0         0 return join(", ", @_[0 .. ($#_ - 1)]) . ", or " . $_[$#_];
964             }
965             }
966              
967             sub decode_dictionary {
968             # decode entire dictionary
969 4     4 0 9 my ($self) = @_;
970              
971 4 100       135 unless ($self->dictionary_fully_decoded()) {
972 1         50 my $dict_start = $self->dictionary_word_start();
973 1         30 my $ztext = $self->ztext();
974 1         31 my $num_words = $self->entry_count();
975 1         30 my $entry_length = $self->entry_length();
976 1         36 my $by_name = $self->decoded_by_word();
977 1         30 my $by_address = $self->decoded_by_address();
978 1         2 my $address;
979              
980 1         6 for (my $index = 0; $index < $num_words; $index++) {
981 536         865 $address = $dict_start + ($index * $entry_length);
982 536 100       1439 unless (exists $by_address->{$address}) {
983 528         47848 my $word = $ztext->decode_text($address);
984 528         3088 $by_name->{$$word} = $address;
985 528         20164 $by_address->{$address} = $$word;
986             }
987             }
988             }
989              
990 4         119 $self->dictionary_fully_decoded(1);
991            
992             }
993              
994             sub dump_dictionary {
995 0     0 0 0 my ($self, $what) = @_;
996 0         0 $self->decode_dictionary();
997 0         0 my $by_name = $self->decoded_by_word();
998 0         0 my $by_address = $self->decoded_by_address();
999              
1000 0         0 my $rows = Games::Rezrov::StoryFile::rows();
1001 0         0 my $columns = Games::Rezrov::StoryFile::columns();
1002 0         0 my $len = $self->encoded_word_length();
1003 0         0 my $fit = int($columns / ($len + 2));
1004 0         0 my $fmt = '%-' . $len . "s";
1005 0         0 my $wrote = 0;
1006              
1007 0         0 my @words;
1008 0 0       0 if ($what) {
1009 0         0 @words = grep {/^$what/} sort keys %{$by_name};
  0         0  
  0         0  
1010             } else {
1011 0         0 my %temp = %{$by_name};
  0         0  
1012 0 0       0 if (Games::Rezrov::ZOptions::SHAMELESS()) {
1013 0         0 my $token_len = Games::Rezrov::StoryFile::header()->encoded_word_length();
1014 0         0 my ($word, $copy);
1015 0         0 foreach $word ("michael", "edmonson") {
1016 0         0 $copy = $word;
1017 0 0       0 $copy = substr($copy,0,$token_len) if length $copy > $token_len;
1018 0         0 $temp{$copy} = 1;
1019             }
1020             }
1021 0         0 @words = sort keys %temp;
1022             }
1023              
1024 0         0 foreach (@words) {
1025 0         0 $self->write_text(sprintf $fmt, $_);
1026 0 0       0 if (++$wrote % $fit) {
1027 0         0 $self->write_text(" ");
1028             } else {
1029 0         0 $self->newline();
1030             }
1031             }
1032             }
1033              
1034             sub dump_objects {
1035 0     0 0 0 my ($self, $type, $what) = @_;
1036 0         0 my $object_cache = $self->get_object_cache();
1037 0         0 my $last = $object_cache->last_object();
1038            
1039 0     0   0 $SIG{"__WARN__"} = sub {};
  0         0  
1040             # intercept perl's silly "deep recursion" warnings
1041            
1042 0 0       0 if ($type == 1) {
1043             # show object relationships
1044 0 0       0 if ($what) {
1045 0         0 my @hits = $object_cache->find($what, "-all" => 1);
1046 0 0       0 if (@hits > 1) {
    0          
1047 0         0 $self->write_text(sprintf 'Hmm, which do you mean: %s?', nice_list(map {$_->[1]} @hits));
  0         0  
1048             } elsif (@hits == 1) {
1049 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($hits[0]->[0],
1050             $object_cache);
1051              
1052 0 0       0 if (my $pr = $zstat->parent_room()) {
1053 0         0 $self->dump_object($pr, OMAP_START_INDENT, 1);
1054             } else {
1055 0         0 $self->dump_object($object_cache->get($hits[0]->[0]), OMAP_START_INDENT, 1);
1056             }
1057             } else {
1058 0         0 $self->write_text(sprintf 'I have no idea what you mean by "%s."', $what);
1059             }
1060             } else {
1061 0         0 my ($zo, $pid);
1062 0         0 my (%objs, %parents, @tops, %seen);
1063 0         0 for (my $i = 1; $i <= $last; $i++) {
1064 0         0 $zo = $object_cache->get($i);
1065 0         0 $pid = $zo->get_parent_id();
1066 0         0 $objs{$i} = $zo;
1067 0         0 $parents{$i} = $pid;
1068             }
1069              
1070 0         0 for (my $i = 1; $i <= $last; $i++) {
1071 0         0 $pid = $parents{$i};
1072 0 0 0     0 if ($pid == 0 or !$objs{$pid}) {
1073 0         0 push @tops, $i;
1074             }
1075             }
1076              
1077 0         0 foreach (@tops) {
1078 0 0       0 next if exists $seen{$_};
1079 0         0 $self->dump_object($objs{$_}, OMAP_START_INDENT, 0, \%seen);
1080             }
1081             }
1082             } else {
1083             # list rooms/items
1084 0 0       0 foreach ($type == 2 ? $object_cache->get_rooms() : $object_cache->get_items()) {
1085 0         0 $self->write_text(" " . $_);
1086 0         0 $self->newline();
1087             }
1088             }
1089             # delete $SIG{"__WARN__"};
1090             # doesn't restore handler (!)
1091 0         0 $SIG{"__WARN__"} = "";
1092             # but this does
1093             }
1094              
1095             sub dump_object {
1096 0     0 0 0 my ($self, $object, $indent, $no_sibs, $seen_ref) = @_;
1097              
1098 0         0 my $object_cache = $self->get_object_cache();
1099 0         0 my $id = $object->object_id();
1100 0         0 my $last = $object_cache->last_object();
1101 0 0       0 die unless $id;
1102 0         0 my $desc = $object_cache->print($id);
1103 0 0       0 if (defined $desc) {
1104 0 0       0 if ($seen_ref) {
1105 0 0       0 return if exists $seen_ref->{$id};
1106 0         0 $seen_ref->{$id} = 1;
1107             }
1108 0         0 $self->newline();
1109 0         0 $self->write_text((" " x $indent) . $$desc . " ($id)");
1110 0         0 my $child = $object_cache->get($object->get_child_id());
1111 0 0 0     0 $self->dump_object($child, $indent + OMAP_INDENT_STEP, 0, $seen_ref) if $child and
      0        
1112             $child->object_id() and
1113             $child->object_id() <= $last;
1114 0 0       0 unless ($no_sibs) {
1115 0         0 my $sib = $object_cache->get($object->get_sibling_id());
1116             # printf STDERR "sib of %s: %s (%d)\n", ${$object->print}, ${$sib->print}, $sib->object_id if $sib;
1117 0 0 0     0 $self->dump_object($sib, $indent, 0, $seen_ref) if $sib and
      0        
1118             $sib->object_id() and
1119             $sib->object_id() <= $last;
1120             }
1121             } else {
1122 0         0 print STDERR "No desc for item $id!\n";
1123             }
1124             }
1125              
1126             sub teleport {
1127             #
1128             # cheat command: move the player to a new location
1129             #
1130 0     0 0 0 my ($self, $where) = @_;
1131 0         0 my $player_object = Games::Rezrov::StoryFile::player_object();
1132 0 0       0 if (!$where) {
    0          
1133 0         0 $self->write_text("Where to?");
1134             } elsif (!$player_object) {
1135 0         0 $self->write_text($self->random_message(CANT_FIND_YOU_YET_MESSAGES));
1136             } else {
1137 0         0 my $object_cache = $self->get_object_cache();
1138 0         0 my @hits = $object_cache->find($where, "-room" => 1);
1139 0         0 my @item_hits = $object_cache->find($where);
1140 0 0 0     0 if (@hits == 1) {
    0          
    0          
    0          
1141             # only one possible destination: proceed
1142 0         0 my $room_id = $hits[0]->[0];
1143 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($room_id,
1144             $object_cache);
1145 0 0       0 if ($zstat->is_current_room()) {
1146             # destination object is the current room: be rude
1147 0         0 $self->write_text($self->random_message(TELEPORT_HERE_MESSAGES));
1148             } else {
1149             # "teleport" to the new room
1150 0         0 $self->move_object($player_object, $room_id);
1151             # make the player object a child of the new room object
1152 0         0 $self->write_text($self->random_message(TELEPORT_MESSAGES));
1153             # print an appropriate message
1154 0         0 $self->steal_turn("look");
1155             # steal player's next turn to describe new location
1156             }
1157             } elsif (@item_hits == 1 and @hits == 0) {
1158             # user has specified an item instead of a room; try to teleport
1159             # to the room the item is in
1160 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($item_hits[0]->[0],
1161             $object_cache);
1162            
1163 0 0       0 if ($zstat->parent_room()) {
1164             # item was in a room
1165 0         0 my $proceed = 1;
1166 0 0       0 if ($zstat->is_current_room()) {
    0          
    0          
1167             # destination is the current room: be rude
1168 0         0 $self->write_text($self->random_message(TELEPORT_HERE_MESSAGES));
1169 0         0 $proceed = 0;
1170             } elsif ($zstat->is_player()) {
1171 0         0 $self->write_text("Sure, just tell me where.");
1172 0         0 $proceed = 0;
1173             } elsif ($zstat->is_toplevel_child()) {
1174             # top-level, should be visible in new location
1175 0         0 $self->write_text($self->random_message(TELEPORT_TO_ITEM_MESSAGES));
1176             } else {
1177             # item is probably inside something else visible in the room
1178 0         0 my $desc = $zstat->toplevel_child()->print();
1179 0         0 $self->write_text(sprintf "I think it's around here somewhere; try the %s.", $$desc);
1180             # print description of item's toplevel container
1181             }
1182 0 0       0 if ($proceed) {
1183             # move the player to the room and steal turn to look around
1184 0         0 $self->move_object($player_object,
1185             $zstat->parent_room()->object_id());
1186 0         0 $self->steal_turn("look");
1187             }
1188             } else {
1189             # can't determine parent (many objects are in limbo until
1190             # something happens)
1191 0         0 my $random = $object_cache->get_random("-room" => 1);
1192 0         0 $self->write_text(sprintf "I don't where that is; how about the %s?", $$random);
1193             }
1194             } elsif (@hits > 1) {
1195             # ambiguous destination
1196 0         0 $self->write_text(sprintf 'Hmm, where you mean: %s?',
1197 0         0 nice_list(sort map {$_->[1]} @hits));
1198             } elsif (@item_hits > 1) {
1199             # ambiguous item
1200 0         0 $self->write_text(sprintf 'Hmm, which do you mean: %s?',
1201 0         0 nice_list(sort map {$_->[1]} @item_hits));
1202             } else {
1203             # no clue at all
1204 0         0 my $random = $object_cache->get_random("-room" => 1);
1205 0         0 $self->write_text(sprintf "I don't where that is; how about the %s?", $$random);
1206             }
1207             }
1208             }
1209              
1210             sub frotz {
1211             # cheat command --
1212             # "frotz" emulation, from Enchanter spell to cause something to emit light.
1213             # Zork I/II/III define frotz in their dictionaries! Aliases: "futz", "lumen"
1214             #
1215             # Light is usually provided by a particular object attribute,
1216             # which varies by game...
1217 0     0 0 0 my ($self, $what) = @_;
1218              
1219 0         0 my @SUPPORTED_GAMES = (
1220             [ ZORK_1, 20 ],
1221             [ ZORK_2, 19 ],
1222             [ ZORK_3, 15 ],
1223             [ INFIDEL, 21, 10 ],
1224             # In Infidel, attribute 21 provides light,
1225             # attribute 10 seems to show "lit and burning" in
1226             # inventory
1227             [ ZTUU, 9 ],
1228             [ PLANETFALL, 5 ]
1229             );
1230              
1231 0         0 my @attributes = $self->support_check(@SUPPORTED_GAMES);
1232 0 0       0 return unless @attributes;
1233             # die join ",", @attributes;
1234            
1235 0 0       0 unless ($what) {
1236 0         0 $self->write_text("Light up what?");
1237             } else {
1238             # know how to do it
1239 0         0 my $object_cache = $self->get_object_cache();
1240 0         0 my @hits = $object_cache->find($what);
1241 0 0       0 if (@hits == 1) {
    0          
1242             # just right
1243 0         0 my $id = $hits[0]->[0];
1244 0         0 my $zo = $object_cache->get($id);
1245 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($id,
1246             $object_cache);
1247 0         0 my $proceed = 0;
1248 0 0       0 if ($zstat->is_player()) {
    0          
    0          
1249 0         0 $self->write_text($self->random_message(FROTZ_SELF_MESSAGES));
1250             } elsif ($zstat->in_inventory()) {
1251 0         0 $proceed = 1;
1252             } elsif ($zstat->in_current_room()) {
1253 0 0       0 if ($zstat->is_toplevel_child()) {
1254             # items that are a top-level child of the room are OK;
1255             # even if we can't pick them up, assume they are visible
1256 0         0 $proceed = 1;
1257             } else {
1258             # things inside other things might not be visible; be coy
1259 0         0 $self->write_text(sprintf "Why don't you pick it up first.");
1260             }
1261             } else {
1262 0         0 $self->write_text(sprintf "I don't see any %s here!", $what);
1263             }
1264              
1265 0 0       0 if ($proceed) {
1266             # with apologies to "Enchanter" :)
1267 0         0 my $desc = $zo->print();
1268 0         0 $self->write_text(sprintf "There is an almost blinding flash of light as the %s begins to glow! It slowly fades to a less painful level, but the %s is now quite usable as a light source.", $$desc, $$desc);
1269 0         0 foreach (@attributes) {
1270 0         0 $zo->set_attr($_);
1271             }
1272             }
1273             } elsif (@hits > 1) {
1274             # too many
1275 0         0 $self->write_text(sprintf 'Hmm, which do you mean: %s?',
1276 0         0 nice_list(sort map {$_->[1]} @hits));
1277             } else {
1278             # no matches
1279 0         0 $self->write_text("What's that?");
1280             }
1281             }
1282             }
1283              
1284             sub travis {
1285             #
1286             # cheat command -- "travis": turn an ordinary item into a weapon.
1287             #
1288             # "Weapons" just seem to be items with a certain object property set...
1289             #
1290             # You lookin' at me?
1291             #
1292 0     0 0 0 my ($self, $what) = @_;
1293 0         0 my @SUPPORTED_GAMES = (
1294             [ ZORK_1, 29 ],
1295             );
1296              
1297 0   0     0 my $property = $self->support_check(@SUPPORTED_GAMES) || return;
1298              
1299 0 0       0 unless ($what) {
1300 0         0 $self->write_text("What do you want to use as a weapon?");
1301             } else {
1302 0         0 my $object_cache = $self->get_object_cache();
1303 0         0 my @hits = $object_cache->find($what);
1304 0 0       0 if (@hits == 1) {
    0          
1305 0         0 my $zo = $object_cache->get($hits[0]->[0]);
1306 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($hits[0]->[0],
1307             $object_cache);
1308 0 0       0 if ($zstat->is_player()) {
    0          
    0          
1309 0         0 $self->write_text("You're scary enough already.");
1310             } elsif ($zstat->in_inventory()) {
1311 0 0       0 if ($zo->test_attr($property)) {
1312 0         0 $self->write_text(sprintf "The %s looks pretty menacing already.", ${$zo->print});
  0         0  
1313             } else {
1314 0         0 $zo->set_attr($property);
1315 0         0 $self->write_text(sprintf $self->random_message(TRAVIS_MESSAGES), ${$zo->print});
  0         0  
1316             }
1317             } elsif ($zstat->in_current_room()) {
1318 0         0 $self->write_text("Pick it up, then we'll talk.");
1319             } else {
1320 0         0 $self->write_text(sprintf "I don't see any %s here!", ${$zo->print});
  0         0  
1321             }
1322             } elsif (@hits > 1) {
1323 0         0 $self->write_text(sprintf 'Hmm, which do you mean: %s?',
1324 0         0 nice_list(sort map {$_->[1]} @hits));
1325             } else {
1326 0         0 $self->write_text("What's that?");
1327             }
1328             }
1329             }
1330              
1331             sub support_check {
1332             # check if this game matches one of a given a list of game versions
1333 0     0 0 0 my ($self, @list) = @_;
1334 0         0 foreach (@list) {
1335 0         0 my ($name, $rnum, $serial, $checksum, @stuff) = @{$_};
  0         0  
1336 0 0       0 if (Games::Rezrov::StoryFile::is_this_game($rnum, $serial, $checksum)) {
1337             # yay
1338 0 0       0 return @stuff == 1 ? $stuff[0] : @stuff;
1339             }
1340             }
1341             # failed, complain:
1342 0 0       0 $self->write_text(sprintf "Sorry, this trickery only currently works in the following game%s:", scalar @list == 1 ? "" : "s");
1343 0         0 foreach (@list) {
1344 0         0 $self->newline();
1345 0         0 $self->write_text(sprintf " - %s (release %d, serial number %s, checksum %s)", @{$_});
  0         0  
1346             }
1347              
1348 0 0       0 if (my $title = Games::Rezrov::StoryFile::game_title()) {
1349 0         0 my $header = Games::Rezrov::StoryFile::header();
1350 0         0 $self->newline();
1351 0         0 $self->newline();
1352 0         0 $self->write_text("You appear to be playing \"$title\", ");
1353 0         0 $self->write_text(sprintf "release %s, ", $header->release_number());
1354 0         0 $self->write_text(sprintf "serial number %s, ", $header->serial_code());
1355 0         0 $self->write_text(sprintf "with checksum %s.", $header->file_checksum());
1356             }
1357            
1358 0         0 return ();
1359             }
1360              
1361             sub tail {
1362             # cheat command --
1363             # follow an object as it moves around; usually a "person"
1364 0     0 0 0 my ($self, $what) = @_;
1365 0 0       0 unless ($what) {
1366 0         0 $self->write_text("Who or what do you want to tail?");
1367             } else {
1368 0         0 my $object_cache = $self->get_object_cache();
1369 0         0 my @hits = $object_cache->find($what);
1370 0 0       0 if (@hits == 1) {
    0          
1371             # just right
1372 0         0 my $id = $hits[0]->[0];
1373 0         0 my $zo = $object_cache->get($id);
1374 0         0 my $target_desc = $zo->print();
1375 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($id,
1376             $object_cache);
1377 0 0       0 if (my $parent = $zstat->parent_room()) {
1378 0         0 Games::Rezrov::StoryFile::tail($id);
1379 0         0 my $zs2 = new Games::Rezrov::ZObjectStatus($parent->object_id(),
1380             $object_cache);
1381 0 0       0 if ($zs2->in_current_room()) {
1382             # in same room already
1383 0         0 $self->write_text(sprintf "OK.");
1384             } else {
1385             # our subject is elsewhere: go there
1386 0         0 my $desc = ${$parent->print()};
  0         0  
1387 0 0       0 if ($$target_desc =~ /^mr?s\. /i) {
    0          
1388 0         0 $self->write_text(sprintf "All right; she's in the %s.", $desc);
1389             } elsif ($$target_desc =~ /^mr\. /i) {
1390 0         0 $self->write_text(sprintf "All right; he's in the %s.", $desc);
1391             } else {
1392 0         0 $self->write_text(sprintf "All right; heading to %s.", $desc);
1393             }
1394 0         0 $self->newline();
1395 0         0 $self->teleport($desc);
1396             }
1397             } else {
1398 0         0 $self->write_text(sprintf "I don't know where %s is...", ${$zo->print});
  0         0  
1399             }
1400             } elsif (@hits > 1) {
1401 0         0 $self->write_text(sprintf 'Hmm, which one: %s?',
1402 0         0 nice_list(sort map {$_->[1]} @hits));
1403             } else {
1404 0         0 $self->write_text("Who or what is that?");
1405             }
1406             }
1407              
1408             }
1409              
1410             sub help {
1411             # when user types "help" and the game doesn't understand
1412 0     0 0 0 my $self = shift;
1413              
1414 0         0 my @stuff = gethostbyname("www.netscape.com");
1415 0 0       0 if (@stuff) {
1416 0         0 my $url;
1417 0   0     0 my $fvo = Games::Rezrov::StoryFile::full_version_output() || "";
1418 0 0       0 if ($fvo =~ /infocom/i) {
1419             # we're playing an infocom game
1420 0         0 $url = $self->random_message(HELP_INFOCOM_URLS);
1421             } else {
1422             # title disabled or not infocom
1423 0         0 $url = $self->random_message(HELP_GENERIC_URLS);
1424             }
1425 0         0 $self->call_web_browser($url);
1426             } else {
1427 0         0 $self->write_text("Connect to the Internet, then maybe I'll help you.");
1428             }
1429 0         0 $self->newline();
1430 0         0 $self->newline();
1431 0         0 $self->suppress_output();
1432             }
1433              
1434              
1435             sub call_web_browser {
1436             # try to call a web browser for a particular URL.
1437             # uses Netscape's remote-control interface if available
1438 0     0 0 0 my ($self, $url) = @_;
1439            
1440 0 0       0 if ($^O eq "MSWin32") {
1441 0         0 $self->write_text($self->random_message(WWW_HELP_MESSAGES));
1442              
1443             # system "start $url";
1444             # "start" seems to be trouble: app seems to hang if we run it
1445             # more than once without first closing the invoked web browser.
1446              
1447 0         0 my $cmd;
1448            
1449             #
1450             # find user's default browser
1451             #
1452 0         0 require Win32::TieRegistry;
1453 0     0   0 $SIG{"__WARN__"} = sub {};
  0         0  
1454             # Win32::TieRegistry can spew warnings
1455              
1456 0         0 my $key = new Win32::TieRegistry(
1457             'Classes\\.htm',
1458             );
1459             # find class name for .htm file association
1460              
1461 0 0       0 if ($key) {
1462 0         0 my $class = ($key->GetValue(''))[0];
1463 0 0       0 if ($class) {
1464             # find invocation
1465             #
1466             # IE:
1467             # "C:\Program Files\Internet Explorer\iexplore.exe" -nohome
1468             #
1469             # Firefox:
1470             # C:\PROGRA~1\MOZILL~2\FIREFOX.EXE -url "%1"
1471             #
1472 0         0 my $ckey = 'Classes\\' . $class . '\\shell\\open\\command';
1473 0         0 $key = new Win32::TieRegistry($ckey);
1474 0 0       0 if ($key) {
1475 0         0 ($cmd) = $key->GetValue('');
1476 0 0       0 if ($cmd =~ /%1/) {
1477             # placeholder for url (Phoenix|(Fire(bird|fox)))
1478 0         0 $cmd =~ s/\%1/$url/;
1479             } else {
1480             # raw (IE), just append
1481 0         0 $cmd .= " " . $url;
1482             }
1483             }
1484             }
1485             }
1486            
1487 0         0 my $exec_error = 0;
1488 0 0       0 if ($cmd) {
1489 0         0 require Win32::Process;
1490 0         0 import Win32::Process;
1491              
1492 0         0 my ($exe_name, $cmd_line);
1493              
1494 0 0       0 if ($cmd =~ /^([\"\'])/) {
1495             # exe name is quoted (e.g. IE); need to unquote before executing
1496 0         0 my $regexp = '^' . $1 . '([^\\' . $1 . ']+)' . $1 . '\s*(.*)';
1497 0 0       0 $cmd =~ /$regexp/ || die;
1498 0         0 ($exe_name, $cmd_line) = ($1, $2);
1499             } else {
1500             # unquoted executable (e.g. firefox)
1501 0         0 $cmd =~ /^(\S+)\s*(.*)/;
1502 0         0 ($exe_name, $cmd_line) = ($1, $2);
1503             }
1504              
1505 0         0 my $pobj;
1506 0 0       0 unless (
1507             Win32::Process::Create($pobj,
1508             $exe_name,
1509             $cmd_line,
1510             0,
1511             NORMAL_PRIORITY_CLASS(),
1512             ".")
1513             ) {
1514 0         0 $self->newline();
1515 0         0 my $error = Win32::FormatMessage(Win32::GetLastError());
1516 0         0 $error =~ s/\s+$//;
1517 0         0 $self->write_text(sprintf 'You quake in your boots as a booming voice intones: "%s"', $error);
1518 0         0 $exec_error = 1;
1519             }
1520             }
1521              
1522 0 0 0     0 if (not($cmd) or $exec_error) {
1523             # whatever
1524 0         0 system "explorer $url";
1525             }
1526              
1527             } else {
1528             # any good platform-independent way of doing this??
1529             # total hack based on Linux environment
1530 0         0 my @paths = split /:/, $ENV{PATH};
1531 0         0 my ($browser, $basename);
1532 0         0 foreach my $path (@paths) {
1533 0         0 foreach my $exe (WWW_BROWSER_EXES) {
1534 0         0 my $fq = $path . '/' . $exe;
1535 0 0       0 if (-x $fq) {
1536 0         0 $browser = $fq;
1537 0         0 $basename = $exe;
1538 0         0 last;
1539             }
1540             }
1541 0 0       0 last if $browser;
1542             }
1543            
1544 0 0 0     0 if ($browser and $ENV{DISPLAY}) {
1545             # found www browser executable on path
1546 0         0 $self->write_text($self->random_message(WWW_HELP_MESSAGES));
1547 0         0 my $tried_remote;
1548             my $cmd;
1549 0 0 0     0 if ($basename eq "netscape" or $basename eq "phoenix" or $basename eq "firebird") {
      0        
1550 0         0 $tried_remote = 1;
1551 0         0 $cmd = sprintf "%s -remote 'openURL(%s)' >/dev/null 2>&1", $browser, $url;
1552 0         0 system $cmd;
1553             # try remote invocation if browser is known to support it
1554             }
1555 0 0       0 if ($tried_remote ? $? : 1) {
    0          
1556             # remote command failed or browser not running
1557 0         0 my $cmd = sprintf '%s %s >/dev/null 2>&1 &', $browser, $url;
1558             # horrible
1559 0         0 system $cmd;
1560             }
1561             } else {
1562             # not X, or can't find browser, give up
1563 0         0 $self->write_text(sprintf "Perhaps the answers you seek may be found at %s. Sadly I am too feeble to take you there directly.", $url);
1564             }
1565             }
1566             }
1567              
1568             sub set_attr {
1569             #
1570             # cheat command: turn an object attribute on or off
1571             #
1572 0     0 0 0 my ($self, $what) = @_;
1573             # $what =~ s/^\s+//;
1574             # $what =~ s/\s+$//;
1575 0         0 my @stuff = split /\s+/, $what;
1576 0 0       0 if (@stuff == 3) {
1577 0         0 my ($oid, $pid, $state) = @stuff;
1578 0 0       0 if ($state) {
1579 0         0 Games::Rezrov::StoryFile::set_attr($oid, $pid);
1580             } else {
1581 0         0 Games::Rezrov::StoryFile::clear_attr($oid, $pid);
1582             }
1583 0         0 $self->write_text("Duly tweaked.");
1584             } else {
1585 0         0 $self->write_text("Specify object ID, attribute ID, state (0=clear, 1=set)");
1586             }
1587             }
1588              
1589             sub set_property {
1590             #
1591             # cheat command: set an object's property to a specified value
1592             #
1593 0     0 0 0 my ($self, $what) = @_;
1594 0         0 my @stuff = split /\s+/, $what;
1595 0 0       0 if (@stuff == 3) {
1596 0         0 my ($oid, $property, $value) = @stuff;
1597 0         0 Games::Rezrov::StoryFile::put_property($oid, $property, $value);
1598 0         0 $self->write_text("Duly tweaked.");
1599             } else {
1600 0         0 $self->write_text("Specify object ID, property ID, value");
1601             }
1602             }
1603              
1604             sub decode_text_at {
1605             # attempt to decode text at a given address; hack, not a real command
1606 0     0 0 0 my ($self, $what) = @_;
1607 0 0       0 return unless $what;
1608 0         0 my $zt = Games::Rezrov::StoryFile::ztext();
1609 0         0 Games::Rezrov::StoryFile::write_zchunk($zt->decode_text($what));
1610             }
1611              
1612             sub decode_all_text {
1613             # hack, try to find and decode all text in the game.
1614 0     0 0 0 my ($self, $start, $sl, $min_words) = @_;
1615 0         0 my $zt = Games::Rezrov::StoryFile::ztext();
1616 0         0 my $header = Games::Rezrov::StoryFile::header();
1617 0         0 my $flen = $header->file_length();
1618 0 0       0 $start = $header->static_memory_address() unless $start;
1619 0 0       0 $min_words = 3 unless $min_words;
1620             # die $start;
1621             # $start = 78463;
1622              
1623 0   0     0 my $SHOW_LEVEL = $sl || 4;
1624             # 1. unconditionally show text decoded from each possible address
1625             # 2. skip text ending at locations we've previously decoded as not bad
1626             # 3. don't show what we think is bad text
1627             # 4. only show text we're highly confident of
1628              
1629 0         0 my @last_after;
1630              
1631             ADDRESS:
1632 0         0 for (my $i=$start; $i < $flen; $i++) {
1633 0         0 my ($blob, $after) = $zt->decode_text($i);
1634              
1635 0 0       0 unless ($SHOW_LEVEL <= 1) {
1636             # if this blob's decoded end address matches one of the
1637             # end addresses of "okay" chunks we've seen recently,
1638             # skip it.
1639 0         0 foreach (@last_after) {
1640 0 0       0 next ADDRESS if $_ == $after;
1641             }
1642             }
1643              
1644 0         0 my $definitely_ok = 0;
1645 0         0 my $bad = 0;
1646            
1647 0         0 my @words;
1648 0         0 if (1) {
1649 0 0       0 if ($$blob =~ /\s{2,}/) {
1650             # sequential whitespace
1651 0 0       0 $bad = "too much whitespace" unless $$blob =~ /(\*{3,}|\x0d|\d\.\s+[A-Z])/;
1652             # except:
1653             # - asterisks
1654             # - 80840: You have two choices: 1. Leave 2. Become dinner.
1655             }
1656              
1657 0 0       0 $bad = "leading junk I" if $$blob =~ /^\s*[a-z\d\'\-]+[A-Z]\w/;
1658             # leading junk before a sentence starts.
1659             # planetfall:
1660             # 29023: [ok: 4] mxnYou're already in it!
1661             # 42037: [ok: 5] 'vnhnYou're already in the booth!
1662             # 59517: [definitely ok: 17] -uhnThe door is locked. You probably have to turn the dial to some number to open it.
1663             #
1664             # z1:
1665             # 31560: [definitely ok: 12] qduvQlhmIt's a well known fact that only schizophrenics say "Hello" to a
1666              
1667 0 0       0 $bad = "leading junk II" if $$blob =~ /^\s*[A-Z\d]\w*[a-z]+[A-Z]/;
1668             # z1:
1669             # 28386: [definitely ok: 13] HmZORK I: The Great Underground Empire
1670             # 34419: [ok: 3] 5mHow singularly useless.
1671             #
1672             # pf:
1673             # 29021: [definitely ok: 4] AsmxnYou're already in it!
1674             # 41486: CHnThe elevator door closes just as the monsters reach it! You slump back against the wall, exhausted from the chase. The elevator begins to move downward.
1675              
1676 0 0       0 $bad = "leading junk III" if $$blob =~ /^[a-z]+ [A-Z]/;
1677             # pf:
1678             # 26811: [ok: 10] edavkkthm Floyd giggles. "You look funny without any clothes on."
1679              
1680             # but make sure:
1681             # 106966: [definitely ok: 43] "Memoo tuu awl lab pursunel: Duu tuu xe daanjuris naatshur uv xe biioo eksperiments, an eemurjensee sistum haz bin instawld. Xis sistum wud flud xe entiir Biioo Lab wic aa dedlee fungasiid. Propur preecawshunz shud bee taakin if xis sistum iz evur yuuzd."
1682              
1683              
1684              
1685 0 0       0 if ($$blob =~ /(?
1686             # ok: "Mmm...that tasted just like" [planetfall]
1687 0 0       0 if ($$blob =~ /([\w\d]\.){2,}/) {
1688             # numeric sections or acronyms:
1689             # "Pouring or spilling non-liquids is specifically forbidden by section 17.9.2 of the Galactic Adventure Game Compendium of Rules."
1690             # S.P.S. Flathead
1691 0         0 1;
1692             } else {
1693 0         0 $bad = "bad comma/period position: $1";
1694             }
1695             # ok:
1696             }
1697            
1698             # $bad = "bad period/sentence" if $$blob =~ /(?
1699             # sentences must start capitalized; alas this breaks Zork I's
1700             # matchbox text (..."Mr. Anderson of Muddle, Mass. says:"...)
1701 0         0 foreach ($$blob =~ /(\w+)\.\s+[a-z]/g) {
1702             # look for suspicious periods, eg:
1703             # 43719: [ok: 20] vqu candles voa. and, being for the moment sated, throws it back. Fortunately, the troll has poor control, and the
1704 0 0       0 next if /[A-Z][a-z]+/;
1705             # but allow in proper abbreviations:
1706             # Mr. Anderson of Muddle, Mass. says: "Before I took this course I was a lowly bit twiddler. Now with what I learned at GUE Tech I feel really important and can obfuscate and confuse with the best."
1707 0         0 $bad = "suspicious period";
1708             }
1709              
1710 0 0       0 $bad = "space before period" if $$blob =~ /\s\.(?!\.\.)/;
1711             # ellipsis ok
1712              
1713 0 0       0 $bad = "bad comma" if $$blob =~ /\s,/;
1714              
1715             # $bad = "bad quote: $1" if $$blob =~ /(\s\')/;
1716             # OK:
1717             # - \n'Til one brave advent'rous spirit
1718             # - 80588: The cyclops, tired of all of your games and trickery, grabs you firmly. As he licks his chops, he says "Mmm. Just like Mom used to make 'em." It's nice to be appreciated.
1719              
1720 0 0       0 $bad = "bad punctuation" if $$blob =~ /[\!\?]\w/;
1721            
1722             # $bad = "multi punctuation" if $$blob =~ /[\'\.\,\;\:\?]{2,}/;
1723              
1724             # problematic?:
1725             # $bad = 1 if $$blob =~ /[bcdfghjklmnpqrstvwxyz]{5,}/i;
1726             # if too many consonants in a row.
1727             # 4 not enough: "filthy"
1728              
1729             # odd capitalization (problematic):
1730 0 0       0 $bad = "weird capitalization I" if $$blob =~ /[a-z][A-Z]\s+/;
1731 0 0       0 $bad = "weird capitalization II" if $$blob =~ /\s[a-z]+[A-Z]/;
1732             # ok: InvisiClues
1733            
1734 0         0 $$blob =~ s/^\s+//;
1735 0         0 $$blob =~ s/\s+$//;
1736             # ignore leading/trailing whitespace
1737             # my @words = split /\s+/, $$blob;
1738 0         0 @words = split /\s+/, $$blob;
1739              
1740 0 0       0 unless (@words >= $min_words) {
1741 0 0       0 $bad = sprintf "only %d words", scalar @words
1742             unless $$blob =~ /.+[\!\?\.\:]$/;
1743             # forgive low word counts for exclamations, etc
1744             }
1745              
1746 0         0 foreach (@words) {
1747 0 0       0 next unless length $_;
1748             # leading/trailing whitespace, or spaces around "..."
1749             # planetfall:
1750             #
1751             # Wow!!! Under the table are three keys, a sack of food, a reactor elevator access pass, one hundred gold pieces ... Just kidding. Actually, there's nothing there.
1752              
1753 0 0       0 next if $_ eq "...";
1754              
1755 0 0       0 next if /^[A-Z][a-z]+\.$/;
1756             # title; Mrs./Dr. etc
1757              
1758 0 0       0 next if /^[A-Z]\.$/;
1759             # initial: S. Eric Meretzky
1760            
1761 0 0       0 next if /^\(c\)$/i;
1762             # copyright
1763              
1764 0         0 s/\W+$//;
1765 0         0 s/^\W+//;
1766             # strip puntuncation, etc from end of sentences
1767             # catch cases like this -- ("n"), planetfall 29855
1768             # This n. You'll have to eat it right from the survival kit.
1769             # 80588: [no vowel: "Mmm] The cyclops, tired of all of your games and trickery, grabs you firmly. As he licks his chops, he says "Mmm. Just like Mom used to make 'em." It's nice to be appreciated.
1770              
1771 0 0       0 next unless $_;
1772             # might be leading punctuation:
1773             # 26127: [no vowel: ] , but both of these are blocked by closed bulkheads.
1774 0 0 0     0 next if /-/ and /^[\w-]+$/;
1775             # 67812: [no vowel: B-19-7] Suddenly, the robot comes to life and its head starts swivelling about. It notices you and bounds over. "Hi! I'm B-19-7, but to everyperson I'm called Floyd. Are you a doctor-person or a planner-person?
1776            
1777 0 0       0 next if /^\#?[\d,]+$/;
1778             # a number
1779             # 44128: There are 69,105 leaves here.
1780             # FIX ME: floating point/money/etc
1781             # 47374: [no vowel: #3] You are standing on the top of the Flood Control Dam #3, which was quite a tourist attraction in times far distant. There are paths to the north, south, and west, and a scramble down.
1782              
1783              
1784 0 0       0 unless (/[aeiouy]/i) {
1785             # require words to contain at least one vowel...
1786             # "y" allowed; eg "by"
1787 0 0 0     0 $bad = "no vowel: $_" unless /[\.\#]/
      0        
      0        
1788             or /^h?m{2,}$/i
1789             or /^\d+(rd|st|nd|th)$/
1790             or /^\d+\/\d+/;
1791             # except:
1792             # - 21st, 22nd, 23rd, 24th...
1793             # 88472: [no vowel: 22nd] Grues are vicious, carnivorous beasts first introduced to Earth by a visiting alien spaceship during the late 22nd century. Grues spread throughout the galaxy alongside man. Although now extinct on all civilized planets, they still exist in some backwater corners of the galaxy. Their favorite diet is Ensigns Seventh Class, but their insatiable appetite is tempered by their fear of light.
1794             # - fractions (1/4)
1795             # - acronyms (eg. "S.P.S. Flathead")
1796             # - FDC#3
1797             # - Mmmm...
1798             # - Hmm
1799             # but not:
1800             # 37729: [no vowel: hm] hm You are also incredibly famished. Better get some breakfast!
1801             #
1802             # -
1803             }
1804 0 0       0 $bad = "embedded quotes: $_" if /\w+\"\w+/;
1805             # embedded quotes no good
1806              
1807 0 0       0 $bad = "too much mixed-case" if /([A-Z][a-z]+){3,}/;
1808            
1809 0 0       0 $bad = "unlikely word: $_" if /[A-z]\d[A-z]/;
1810            
1811 0 0       0 if (length $_ == 1) {
    0          
1812 0 0       0 $bad = "bogus 1-char word: $_" unless /^[aio]$/i;
1813             # few very 1-letter words legal
1814             # "O, they ruled the solar system"
1815             } elsif (length($_) > 24) {
1816             # ok: Br'gun-te'elkner-ipg'nun
1817             # [planetfall]
1818 0         0 $bad = "too long: $_";
1819             } else {
1820              
1821 0 0       0 if (/^[aeiou]+$/i) {
1822 0 0 0     0 $bad = "all vowels: $_" unless ($_ eq 'aa') or /^[MCLXVI]+$/;
1823             # bad if all vowels:
1824             # - don't count y; "you" is ok
1825             # - roman numerals OK: 69098: [all vowels: II] The solid-gold coffin used for the burial of Ramses II is here.
1826             #
1827             # however, planetfall at 106966:
1828             # "Memoo tuu awl lab pursunel: Duu tuu xe daanjuris naatshur uv xe biioo eksperiments, an eemurjensee sistum haz bin instawld. Xis sistum wud flud xe entiir Biioo Lab wic aa dedlee fungasiid. Propur preecawshunz shud bee taakin if xis sistum iz evur yuuzd."
1829             }
1830            
1831             }
1832            
1833 0 0       0 $bad = "all consonants: $_" if $$blob =~ /^[bcdfghjklmnpqrstvwxyz]+$/i;
1834             # bad if all consonants
1835             }
1836             # die "\"$_\" bad " . length($_) if $bad;
1837 0         0 1;
1838             }
1839            
1840 0 0       0 unless ($bad) {
1841 0         0 my @hits = ($$blob =~ /\.\s*\w/g);
1842 0 0       0 if (@hits) {
1843             # if the blob contains periods that are positioned in a way
1844             # that seems to make sense, consider the blob confirmed
1845 0         0 my $p_all_ok = 1;
1846 0         0 foreach (@hits) {
1847 0 0       0 unless (/\.\s+[A-Z]/) {
1848 0         0 $p_all_ok = 0;
1849             }
1850             }
1851 0 0       0 $definitely_ok = 1 if $p_all_ok;
1852             # printf STDERR " comma check: %s, $bad $c_all_ok\n", $$blob;
1853             }
1854            
1855 0 0       0 $definitely_ok = 1 if $$blob =~ /\".*\"/;
1856             # embedded quoted string
1857              
1858 0 0       0 $definitely_ok = 1 if $$blob =~ /^[A-Z].+\.$/;
1859              
1860             # $definitely_ok = 1 if $$blob =~ /^[A-Z][A-z\d\s\'\-\.\!\,\;\:\(\)\?\*]+?\w[\!\?\.\:\"]{1,3}$/;
1861 0 0       0 $definitely_ok = 1 if $$blob =~ /^[A-Z].*[\!\?\.\:\"]{1,3}$/;
1862             # looks like one or more complete sentences.
1863             # allow ending with "..."
1864             # 44018: [ok: 6] I don't know the word "
1865             }
1866              
1867             # $definitely_ok = 0;
1868              
1869 0 0       0 unless ($bad) {
1870 0         0 push @last_after, $after;
1871 0 0       0 shift @last_after if @last_after > 5;
1872             }
1873              
1874 0 0       0 if ($bad ? $SHOW_LEVEL < 3 : $SHOW_LEVEL == 4 ? $definitely_ok : 1) {
    0          
    0          
1875 0         0 $$blob =~ s/\x0d/\x0a/g;
1876 0         0 if (0) {
1877             # testing
1878             my $tag;
1879             if ($bad) {
1880             $tag = "[$bad] ";
1881             } elsif ($SHOW_LEVEL == 4) {
1882             $tag = "";
1883             } else {
1884             $tag = sprintf "[%sok: %d] ",
1885             ($definitely_ok ? "definitely " : ""),
1886             scalar @words;
1887             }
1888             printf STDERR "%d: %s%s\n", $i, $tag, $$blob;
1889             } else {
1890             # for user
1891 0         0 $self->write_text(sprintf "%d: %s", $i, $$blob);
1892 0         0 $self->newline();
1893             }
1894             }
1895            
1896 0 0       0 if ($definitely_ok) {
1897             # if we're *really* sure about the blob, continue our decoding
1898             # after it's done (so we don't see redundant partially-decoded
1899             # bits).
1900 0         0 $i = $after - 1;
1901             }
1902             }
1903             }
1904              
1905             sub notify_toggle {
1906             # "notify" emulation: user is toggling state.
1907 0     0 0 0 my ($self) = @_;
1908 0         0 my $now = Games::Rezrov::ZOptions::notifying();
1909 0 0       0 my $status = $now ? 0 : 1;
1910 0 0       0 $self->write_text(sprintf "Score notification is now %s.", $status ? "on" : "off");
1911 0         0 $self->newline();
1912 0         0 $self->newline();
1913 0         0 $self->suppress_output();
1914 0         0 Games::Rezrov::ZOptions::notifying($status);
1915             }
1916              
1917             sub move_object {
1918 0     0 0 0 Games::Rezrov::StoryFile::insert_obj($_[1], $_[2]);
1919             # hee hee
1920             }
1921              
1922             sub steal_turn {
1923 0     0 0 0 Games::Rezrov::StoryFile::push_command($_[1]);
1924             }
1925              
1926             sub newline {
1927 0     0 0 0 Games::Rezrov::StoryFile::newline();
1928             }
1929              
1930             sub write_text {
1931 0     0 0 0 Games::Rezrov::StoryFile::write_text($_[1]);
1932             }
1933              
1934             sub suppress_output {
1935 0     0 0 0 Games::Rezrov::StoryFile::suppress_hack();
1936             }
1937              
1938             sub property_dump {
1939 0     0 0 0 my ($self, $what) = @_;
1940 0         0 my $header = Games::Rezrov::StoryFile::header();
1941 0         0 my $max_objects = $header->max_objects();
1942 0         0 my $oc = $self->object_cache();
1943 0         0 for (my $i=1; $i <= $max_objects; $i++) {
1944 0         0 my $zo = $oc->get($i);
1945 0         0 my $zp = $zo->get_property(Games::Rezrov::ZProperty::FIRST_PROPERTY);
1946 0         0 printf STDERR "%s: %s\n",
1947 0 0       0 ${$zo->print},
1948             ($zp->property_exists() ? $zp->property_number() : "no properties");
1949             }
1950             }
1951              
1952             sub lummox {
1953             # cheat command: remove restrictions on weight and number of items
1954             # that can be carried. So far, it seems that there are two global
1955             # variables involved: one holds the total weight of items that may
1956             # be carried, the other the maximum number of items that may be carried.
1957             #
1958             # Usually a 2OP compare_* opcode precedes this operation:
1959             #
1960             # count:1358 pc:37207 type:2OP opcode:3(0x03;raw=99) (compare_jg) operands:112,100
1961             # count:1359 pc:37211 type:1OP opcode:0(0x00;raw=160) (compare_jz) operands:1
1962             # count:1360 pc:37214 type:0OP opcode:2(0x02;raw=178) (print_text) operands:
1963             # count:1361 pc:37227 type:2OP opcode:2(0x02;raw=98) (compare_jl) operands:100,100
1964             # count:1362 pc:37259 type:0OP opcode:2(0x02;raw=178) (print_text) operands:
1965             # count:1363 pc:37262 type:0OP opcode:11(0x0b;raw=187) (newline) operands:
1966             # brass lantern: Your load is too heavy.
1967             #
1968             # see the "-hack" command-line switch to help decode which variable is used
1969             # for the opcode; in this case (Zork I, PC 37207, global variable # 133).
1970              
1971 0     0 0 0 my ($self) = @_;
1972 0         0 my @SUPPORTED_GAMES = (
1973             [ ZORK_1, 133, 59 ],
1974             [ ZORK_2, 159, 83 ],
1975             [ ZORK_3, 184, 116 ],
1976             [ PLANETFALL, 218, 128 ],
1977             );
1978            
1979              
1980 0         0 my ($total_weight, $max_items) = $self->support_check(@SUPPORTED_GAMES);
1981 0 0       0 return unless $total_weight;
1982              
1983 0         0 my $LOTSA_WEIGHT = 32000;
1984 0         0 my $LOTSA_ITEMS = 250;
1985 0 0 0     0 if (Games::Rezrov::StoryFile::get_global_var($total_weight) == $LOTSA_WEIGHT and Games::Rezrov::StoryFile::get_global_var($max_items) == $LOTSA_ITEMS) {
1986 0         0 $self->write_text("You feel pretty pumped up already.");
1987             } else {
1988 0         0 Games::Rezrov::StoryFile::set_global_var($total_weight, $LOTSA_WEIGHT);
1989 0         0 Games::Rezrov::StoryFile::set_global_var($max_items, $LOTSA_ITEMS);
1990 0         0 $self->write_text($self->random_message(LUMMOX_MESSAGES));
1991             }
1992             }
1993              
1994             sub systolic {
1995             # cheat command: lower blood pressure (bureaucracy only)
1996 0     0 0 0 my $self = shift;
1997 0         0 my @SUPPORTED_GAMES = (
1998             [ BUREAUCRACY, 232, 32082 ]
1999             );
2000              
2001 0 0       0 if (my ($var, $value) = $self->support_check(@SUPPORTED_GAMES)) {
2002 0         0 Games::Rezrov::StoryFile::set_global_var($var, $value);
2003 0         0 $self->write_text("You feel a bit calmer.");
2004             }
2005             }
2006              
2007             sub medicate {
2008             # cheat command: manage blood pressure (bureaucracy only)
2009 0     0 0 0 my $self = shift;
2010 0         0 my @SUPPORTED_GAMES = (
2011             [ BUREAUCRACY, 232, 32082 ]
2012             );
2013              
2014 0 0       0 if (my ($var, $value) = $self->support_check(@SUPPORTED_GAMES)) {
2015 0         0 my $data = $self->bp_cheat_data();
2016 0         0 my $doses = 1;
2017 0 0       0 if ($data) {
2018 0         0 $doses = $data->[0] + 1;
2019             }
2020 0         0 $self->bp_cheat_data([$doses, $var, $value]);
2021              
2022 0 0       0 if ($doses > 2) {
2023 0         0 $self->write_text("While your blood pressure medication is tantalizingly candylike, you've had enough.");
2024             } else {
2025 0         0 my $msg = "You pop a generic angiotensin-II receptor antagonist. " . $self->random_message(ANGIOTENSIN_MESSAGES);
2026 0         0 $self->write_text($msg);
2027             }
2028             }
2029             }
2030              
2031             sub blood_pressure_cheat_hook {
2032             # cheat: automatically manage blood pressure in "Bureaucracy"
2033 4     4 0 9 my ($self) = @_;
2034 4         132 my $ref = $self->bp_cheat_data();
2035 4 50       21 if ($ref) {
2036             # active
2037 0         0 my ($doses, $var, $value) = @{$ref};
  0         0  
2038 0         0 Games::Rezrov::StoryFile::set_global_var($var, $value);
2039             }
2040             }
2041              
2042             sub vilify {
2043             # cheat command --
2044             # make an object attackable.
2045 0     0 0 0 my ($self, $what) = @_;
2046              
2047 0         0 my @SUPPORTED_GAMES = (
2048             [ ZORK_1, 30 ],
2049             );
2050              
2051 0         0 my @attributes = $self->support_check(@SUPPORTED_GAMES);
2052 0 0       0 return unless @attributes;
2053             # die join ",", @attributes;
2054            
2055 0 0       0 unless ($what) {
2056 0         0 $self->write_text("Vilify what?");
2057             } else {
2058             # know how to do it
2059 0         0 my $object_cache = $self->get_object_cache();
2060 0         0 my @hits = $object_cache->find($what);
2061 0 0       0 if (@hits == 1) {
    0          
2062             # just right
2063 0         0 my $id = $hits[0]->[0];
2064 0         0 my $zo = $object_cache->get($id);
2065 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($id,
2066             $object_cache);
2067 0         0 my $proceed = 0;
2068 0         0 my $msg;
2069 0 0       0 if ($zstat->is_player()) {
    0          
2070 0         0 $proceed = 1;
2071 0         0 $msg = $self->random_message(VILIFY_SELF_MESSAGES);
2072             } elsif ($zstat->in_current_room()) {
2073 0         0 $proceed = 1;
2074 0         0 $msg = $self->random_message(VILIFY_MESSAGES);
2075 0 0       0 if ($zstat->in_inventory()) {
2076 0         0 $msg =~ s/\.$/; I don't know why you're toting it around./;
2077             }
2078             } else {
2079 0         0 $self->write_text(sprintf "I don't see any %s here!", $what);
2080             }
2081              
2082 0 0       0 if ($proceed) {
2083             # with apologies to "Enchanter" :)
2084 0         0 my $desc = $zo->print();
2085 0         0 $self->write_text(sprintf $msg, $$desc);
2086 0         0 foreach (@attributes) {
2087 0         0 $zo->set_attr($_);
2088             }
2089             }
2090             } elsif (@hits > 1) {
2091             # too many
2092 0         0 $self->write_text(sprintf 'Hmm, which do you mean: %s?',
2093 0         0 nice_list(sort map {$_->[1]} @hits));
2094             } else {
2095             # no matches
2096 0         0 $self->write_text("What's that?");
2097             }
2098             }
2099             }
2100              
2101             sub baste {
2102             # cheat command --
2103             # make an object edible.
2104 0     0 0 0 my ($self, $word, $what) = @_;
2105              
2106 0         0 my @SUPPORTED_GAMES = (
2107             [ ZORK_1, 21 ],
2108             );
2109              
2110 0         0 my @attributes = $self->support_check(@SUPPORTED_GAMES);
2111 0 0       0 return unless @attributes;
2112             # die join ",", @attributes;
2113            
2114 0 0       0 unless ($what) {
2115 0         0 $self->write_text(sprintf "%s what?", ucfirst(lc($word)));
2116             } else {
2117             # know how to do it
2118 0         0 my $object_cache = $self->get_object_cache();
2119 0         0 my @hits = $object_cache->find($what);
2120 0 0       0 if (@hits == 1) {
    0          
2121             # just right
2122 0         0 my $id = $hits[0]->[0];
2123 0         0 my $zo = $object_cache->get($id);
2124 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($id,
2125             $object_cache);
2126 0         0 my $proceed = 0;
2127 0         0 my $msg;
2128 0 0       0 if ($zstat->is_player()) {
    0          
2129 0         0 $proceed = 1;
2130 0         0 $msg = sprintf 'Go back to %s!', $self->random_message(GO_BACK_TO_X);
2131             # ", hippie!"
2132             } elsif ($zstat->in_current_room()) {
2133 0         0 $proceed = 1;
2134 0         0 $msg = $self->random_message(BASTE_MESSAGES);
2135             } else {
2136 0         0 $self->write_text(sprintf "I don't see any %s here!", $what);
2137             }
2138              
2139 0 0       0 if ($proceed) {
2140             # with apologies to "Enchanter" :)
2141 0         0 my $desc = $zo->print();
2142 0         0 $self->write_text(sprintf $msg, $$desc);
2143 0         0 foreach (@attributes) {
2144 0         0 $zo->set_attr($_);
2145             }
2146             }
2147             } elsif (@hits > 1) {
2148             # too many
2149 0         0 $self->write_text(sprintf 'Hmm, which do you mean: %s?',
2150 0         0 nice_list(sort map {$_->[1]} @hits));
2151             } else {
2152             # no matches
2153 0         0 $self->write_text("What's that?");
2154             }
2155             }
2156             }
2157              
2158             sub correct_typos {
2159             # attempt to correct typos as Nitfol interpreter does:
2160             #
2161             # If the entered word is in the dictionary, behave as normal.
2162             #
2163             # If the length of the word is less than 3 letters long, give up. We
2164             # don't want to make assumptions about what so short words might be.
2165             #
2166             # If the word is the same as a dictionary word with one transposition,
2167             # assume it is that word. exmaine becomes examine.
2168             #
2169             # If it is a dictionary word with one deleted letter, assume it is
2170             # that word. botle becomes bottle.
2171             #
2172             # If it is a dictionary word with one inserted letter, assume it is
2173             # that word. tastey becomes tasty.
2174             #
2175             # If it is a dictionary word with one substitution, assume it is that
2176             # word. opin becomes open.
2177             #
2178             # *** FIX ME: ***
2179             # - what to do when corrected word is truncated?
2180             # i.e. "mailbax" should be corrected to "mailbox", but token is "mailbo"
2181             # - deletion with irrelevant last token letter:
2182             # "malbox" should be "mailbo"
2183             # => do an object lookup?
2184              
2185 4     4 0 12 my ($self, $line) = @_;
2186 4         7 my $raw_line = $line;
2187 4         9 chomp $line;
2188              
2189 4         17 $self->decode_dictionary();
2190              
2191 4         9 my %words = %{$self->decoded_by_word()};
  4         116  
2192 4         155 foreach (keys %Games::Rezrov::ZDict::MAGIC_WORDS) {
2193             # use a copy of the dictionary so we can add cheat verbs to the
2194             # list of known words
2195 136         207 $words{$_} = 1;
2196             }
2197              
2198 4         149 my $encoded_length = $self->encoded_word_length();
2199 4         583 my @all_words = keys %words;
2200              
2201 4         105 my $i;
2202             my @subs;
2203              
2204 4         24 my $zoc = Games::Rezrov::StoryFile::get_zobject_cache();
2205 4         147 $zoc->load_names();
2206             # ugh
2207              
2208             my $correct_word = sub {
2209             # attempt to typo-correct a given word; must return word
2210             # (original or changed).
2211 6     6   16 my ($word) = @_;
2212 6         15 my $new_word = $word;
2213 6         16 my $token = lc($word);
2214 6 100       26 $token = substr($token,0,$encoded_length)
2215             if length($token) > $encoded_length;
2216 6         12 my $tlen = length($token);
2217 6 50 66     70 unless (length($word) < 3 or exists $words{$token} or $word =~ /^#/) {
      33        
2218             # attempt correction unless:
2219             # - word is too short
2220             # - word is already in dictionary
2221             # - word begins with a cheat/debug prefix ("#")
2222 0         0 my (@sub_hits, @trans_hits, @del_hits, @ins_hits);
2223              
2224             #
2225             # single-character insertion
2226             #
2227 0         0 for ($i=0; $i < $tlen; $i++) {
2228 0         0 my $try = "";
2229 0         0 for (my $j=0; $j < $tlen; $j++) {
2230 0 0       0 $try .= substr($token, $j, 1) unless $j == $i;
2231             }
2232             # print "$token $try\n";
2233 0 0       0 push @ins_hits, $try if exists $words{$try};
2234             }
2235              
2236             #
2237             # single-character deletion
2238             #
2239 0         0 for ($i=1; $i < $tlen; $i++) {
2240 0         0 my $regexp = substr($token, 0, $i) . "." . substr($token, $i);
2241 0 0       0 $regexp = substr($regexp, 0, $encoded_length) if length($regexp) > $encoded_length;
2242             # i.e. in zork I, "malbox" search for "ma.lbox" must
2243             # search dictionary for "ma.lbo" (only 6 characters)
2244              
2245             # my @h = grep {/$regexp/} @all_words;
2246 0         0 my @h = grep {/^$regexp$/} @all_words;
  0         0  
2247             # printf "%s: %s\n", $regexp, join ",", @h;
2248              
2249 0 0       0 push @del_hits, @h if @h;
2250             }
2251              
2252             #
2253             # single-character transpositions
2254             #
2255 0         0 for ($i=0; $i < $tlen - 1; $i++) {
2256 0         0 my $try = $token;
2257 0         0 my $save = substr($try, $i, 1);
2258 0         0 substr($try,$i,1) = substr($token,$i + 1,1);
2259 0         0 substr($try,$i+1,1) = $save;
2260 0 0       0 push @trans_hits, $try if exists $words{$try};
2261             }
2262              
2263             #
2264             # single-character substitutions
2265             #
2266 0         0 for ($i=0; $i < $tlen; $i++) {
2267 0         0 my $regexp = $token;
2268 0         0 substr($regexp, $i, 1) = '.';
2269 0         0 my @hits = grep {/^$regexp$/} @all_words;
  0         0  
2270 0 0       0 push @sub_hits, @hits if @hits;
2271             }
2272              
2273 0         0 foreach (\@trans_hits, \@del_hits, \@ins_hits, \@sub_hits) {
2274 0 0       0 $new_word = $_->[0], last if @{$_};
  0         0  
2275             }
2276              
2277 0 0       0 if ($word ne $new_word) {
2278             #
2279             # correction found
2280             #
2281              
2282 0 0       0 if (length($new_word) == $encoded_length) {
2283             # word might be truncated! e.g. in Zork I:
2284             #
2285             # - user enters "leaflwt"
2286             # - actual word is "leaflet"
2287             # - dictionary entry is truncated to 6 characters, "leafle".
2288             #
2289             # ...this is ugly because the corrected word is printed
2290             # to the screen. Look for matches for the corrected word in
2291             # the object database, using that object's description if it
2292             # matches.
2293              
2294 0         0 my @hits = $zoc->find($new_word);
2295 0 0       0 if (@hits == 1) {
2296 0         0 my $desc = $zoc->print($hits[0]->[0]);
2297 0 0       0 if (index(lc($$desc), lc($new_word)) == 0) {
2298             # require a perfect match; too strict?
2299             # in Zork I, "mailbox" object lookup returns "small mailbox",
2300             # which works, but I'm not certain other typos would do as well.
2301             # printf STDERR "%s => %s => %s\n", $word, $new_word, $$desc;
2302 0         0 $new_word = $$desc;
2303             # huzzah
2304             }
2305             }
2306             }
2307              
2308 0         0 push @subs, [ $word, $new_word ];
2309             }
2310             }
2311              
2312             # print STDERR "word: $new_word\n";
2313 6         25 return $new_word;
2314 4         54 };
2315              
2316             # $line =~ s/(\w+)/&$correct_word($1)/eg;
2317             # NO: excludes cheat commands
2318             # $line =~ s/(\S+)/&$correct_word($1)/eg;
2319             # NO: includes punctuation
2320              
2321 4         32 $line =~ s/([\#\w]+)/&$correct_word($1)/eg;
  6         19  
2322              
2323             # print STDERR "corrected: $line\n";
2324             # HACK: doesn't follow the tokenization rules in tokenize_line().
2325             # Direct queries to my associate, Dr. Sosumi.
2326              
2327 4         12 my $msg = "";
2328 4 50       15 if (@subs) {
2329             # something was corrected
2330 0         0 $msg = '[Assuming you meant ';
2331 0         0 for ($i=0; $i < @subs; $i++) {
2332 0 0       0 if ($i > 0) {
2333 0         0 $msg .= ', ';
2334 0 0       0 $msg .= 'and ' if $i == $#subs;
2335             }
2336 0         0 $msg .= sprintf '"%s" instead of "%s"', $subs[$i]->[1], $subs[$i]->[0];
2337             }
2338 0         0 $msg .= '.]';
2339             }
2340              
2341 4         568 return ($line, $msg);
2342             }
2343              
2344             sub gmacho {
2345             # cheat command --
2346             # move any spell to your scrollbook (Enchanter series)
2347 0     0 0   my ($self, $token, $what, %options) = @_;
2348              
2349 0           my $quiet = $options{"-quiet"};
2350              
2351 0 0         unless ($what) {
2352 0           $self->write_text(sprintf "%s what?", ucfirst(lc($token)));
2353 0           return 0;
2354             }
2355              
2356 0           my @SUPPORTED_GAMES = (
2357             [ ENCHANTER, 4 ],
2358             [ SORCERER, 7 ],
2359             [ SPELLBREAKER, 0 ],
2360             # attribute determining whether object is a spell.
2361             # don't know how this works in Spellbreaker;
2362             # looks like it "should" be attr 18, but doesn't work!
2363             );
2364              
2365 0           my @attributes = $self->support_check(@SUPPORTED_GAMES);
2366 0 0         return 0 unless @attributes;
2367              
2368 0           my $spell_attr = $attributes[0];
2369              
2370 0           my $object_cache = $self->get_object_cache();
2371              
2372 0           my @hits = $object_cache->find("spell book");
2373 0 0         unless (@hits == 1) {
2374 0 0         $self->write_text("Hmm, I can't seem to find your spell book.") unless $quiet;
2375 0           return 0;
2376             }
2377 0           my $spellbook_id = $hits[0]->[0];
2378            
2379 0           my @try = $what;
2380 0 0         unless ($what =~ / spell$/i) {
2381 0           push @try, $what . " spell";
2382             }
2383              
2384 0           my $found;
2385 0           my $worked = 0;
2386 0           foreach my $try (@try) {
2387 0           @hits = $object_cache->find($try);
2388 0 0         if (@hits == 1) {
2389             # found desired spell
2390 0           $found = 1;
2391 0           my $spell_id = $hits[0]->[0];
2392            
2393 0           my $usable = 1;
2394 0           my $zo = $object_cache->get($spell_id);
2395              
2396 0 0         if ($spell_attr) {
2397             # we know how to test if the requested object is a spell
2398 0           my $zp = $zo->get_property($attributes[0]);
2399 0           $usable = $zp->property_exists();
2400             }
2401              
2402 0 0         if ($usable) {
2403 0           my $parent = $zo->get_parent();
2404 0 0 0       if ($parent and $parent->object_id() == $spellbook_id) {
2405             # spell is already in spell book
2406 0           my $thing = $what;
2407 0           $thing =~ s/\s+.*//;
2408 0 0         $self->write_text("Great idea, Berzio, if only the $thing spell weren't already in your spellbook.") unless $quiet;
2409             } else {
2410 0           $self->move_object($spell_id, $spellbook_id);
2411 0 0         $self->write_text($self->random_message(GMACHO_MESSAGES)) unless $quiet;
2412 0           $worked = 1;
2413             }
2414             } else {
2415 0 0         $self->write_text("That doesn't appear to be a spell.") unless $quiet;
2416             }
2417 0           last;
2418             }
2419             }
2420 0 0 0       $self->write_text("I can't find that spell, if that is a spell.") unless $found or $quiet;
2421 0           return $worked;
2422             }
2423              
2424             sub voluminus {
2425             # cheat command --
2426             # expand the capacity of a container object.
2427             #
2428             # BTW, it's not that I don't know how to spell "voluminous".
2429             # I'm just a grown man who's read all the Harry Potter books.
2430              
2431 0     0 0   my ($self, $token, $what) = @_;
2432 0           my @SUPPORTED_GAMES = (
2433             [ ZORK_1, 19, 11, 10 ],
2434             # 0 = game ID
2435             # 1 = attribute # for whether object is a container
2436             # 2 = attribute # for whether container is open
2437             # 3 = property # for container capacity
2438             );
2439              
2440 0           my @attributes = $self->support_check(@SUPPORTED_GAMES);
2441 0 0         return unless @attributes;
2442 0           my ($attr_container, $attr_container_open, $property_capacity) = @attributes;
2443            
2444 0 0         unless ($what) {
2445 0           $self->write_text("Voluminus what?");
2446             } else {
2447             # given an object
2448 0           my $object_cache = $self->get_object_cache();
2449 0           my @hits = $object_cache->find($what);
2450 0 0         if (@hits == 1) {
    0          
2451             # just right
2452 0           my $id = $hits[0]->[0];
2453 0           my $zo = $object_cache->get($id);
2454 0           my $zstat = new Games::Rezrov::ZObjectStatus($id,
2455             $object_cache);
2456 0           my $proceed = 0;
2457 0           my $msg;
2458 0 0         if ($zstat->is_player()) {
    0          
2459 0           $msg = $self->random_message(VOLUMINUS_SELF_MESSAGES);
2460             } elsif ($zstat->in_current_room()) {
2461 0 0         if ($zo->test_attr($attr_container)) {
2462             # is the specified object a container?
2463 0           $proceed = 1;
2464 0 0         if ($zo->test_attr($attr_container_open)) {
2465 0           $msg = $self->random_message(VOLUMINUS_MESSAGES);
2466             } else {
2467 0           $msg = $self->random_message(VOLUMINUS_CLOSED_MESSAGES);
2468             }
2469             } else {
2470             # not a container
2471 0           $msg = "It's difficult to see how the %s could hold more, given that it can't hold anything.";
2472             }
2473             } else {
2474 0           $msg = sprintf "I don't see any %s here!", $what;
2475             }
2476              
2477 0 0         if ($msg) {
2478 0           my $desc = $zo->print();
2479 0           $self->write_text(sprintf $msg, $$desc);
2480             }
2481              
2482 0 0         if ($proceed) {
2483 0           Games::Rezrov::StoryFile::put_property($id, $property_capacity, PLENTY_O_ROOM);
2484             }
2485             } elsif (@hits > 1) {
2486             # too many
2487 0           $self->write_text(sprintf 'Hmm, which do you mean: %s?',
2488 0           nice_list(sort map {$_->[1]} @hits));
2489             } else {
2490             # no matches
2491 0           $self->write_text("What's that?");
2492             }
2493             }
2494             }
2495              
2496             sub compartmentalize {
2497             # cheat command --
2498             # make an object into a container.
2499             # *** doesn't seem to work: non-containers seem to be missing required capacity property.
2500              
2501 0     0 0   my ($self, $token, $what) = @_;
2502 0           my $PLENTY_O_ROOM = 32000;
2503 0           my @SUPPORTED_GAMES = (
2504             [ ZORK_1, 19, 11, 10 ],
2505             # 0 = game ID
2506             # 1 = attribute # for whether object is a container
2507             # 2 = attribute # for whether container is open
2508             # 3 = property # for container capacity
2509             );
2510              
2511 0           my @attributes = $self->support_check(@SUPPORTED_GAMES);
2512 0 0         return unless @attributes;
2513 0           my ($attr_container, $attr_container_open, $property_capacity) = @attributes;
2514            
2515 0 0         unless ($what) {
2516 0           $self->write_text("Compartmentalize what?");
2517             } else {
2518             # given an object
2519 0           my $object_cache = $self->get_object_cache();
2520 0           my @hits = $object_cache->find($what);
2521 0 0         if (@hits == 1) {
    0          
2522             # just right
2523 0           my $id = $hits[0]->[0];
2524 0           my $zo = $object_cache->get($id);
2525 0           my $zstat = new Games::Rezrov::ZObjectStatus($id,
2526             $object_cache);
2527 0           my $proceed = 0;
2528 0           my $msg;
2529 0 0         if ($zstat->is_player()) {
    0          
2530 0           $msg = $self->random_message(VOLUMINUS_SELF_MESSAGES);
2531             } elsif ($zstat->in_current_room()) {
2532 0           $proceed = 1;
2533 0           $msg = $self->random_message("compartmentalize test");
2534             } else {
2535 0           $msg = sprintf "I don't see any %s here!", $what;
2536             }
2537              
2538 0 0         if ($msg) {
2539 0           my $desc = $zo->print();
2540 0           $self->write_text(sprintf $msg, $$desc);
2541             }
2542              
2543 0 0         if ($proceed) {
2544 0           Games::Rezrov::StoryFile::set_attr($id, $attr_container);
2545 0           Games::Rezrov::StoryFile::set_attr($id, $attr_container_open);
2546 0           Games::Rezrov::StoryFile::put_property($id, $property_capacity, PLENTY_O_ROOM);
2547             }
2548             } elsif (@hits > 1) {
2549             # too many
2550 0           $self->write_text(sprintf 'Hmm, which do you mean: %s?',
2551 0           nice_list(sort map {$_->[1]} @hits));
2552             } else {
2553             # no matches
2554 0           $self->write_text("What's that?");
2555             }
2556             }
2557             }
2558              
2559             sub bookworm {
2560             # cheat command --
2561             # move all game spells to your scrollbook (Enchanter series)
2562 0     0 0   my ($self, $token, $what, %options) = @_;
2563              
2564 0           my @SUPPORTED_GAMES = (
2565             [ ENCHANTER, 4 ],
2566             [ SORCERER, 7 ],
2567             [ SPELLBREAKER, 0],
2568             # - attribute determining whether object is a spell
2569             );
2570              
2571 0           my @attributes = $self->support_check(@SUPPORTED_GAMES);
2572 0 0         return unless @attributes;
2573              
2574 0           my $object_cache = $self->get_object_cache();
2575 0           my @hits = $object_cache->find(" spell");
2576            
2577 0 0         if (@hits) {
2578 0           my $imported = 0;
2579 0           foreach my $ref (@hits) {
2580 0 0         next unless $ref->[1] =~ / spell$/i;
2581             # printf "DEBUG: %s\n", $ref->[1];
2582 0           $imported += $self->gmacho("gmacho", $ref->[1], "-quiet" => 1);
2583             }
2584 0 0         if ($imported) {
2585 0           $self->write_text("Your spellbook spins in the air, its pages flapping wildly!");
2586             } else {
2587 0           $self->write_text("Your spellbook twitches feebly.");
2588             }
2589             } else {
2590 0           $self->write_text("Sorry, I couldn't find any spells.");
2591             }
2592              
2593              
2594             }
2595              
2596             1;