File Coverage

blib/lib/Games/Rezrov/StoryFile.pm
Criterion Covered Total %
statement 608 1201 50.6
branch 181 590 30.6
condition 37 162 22.8
subroutine 133 196 67.8
pod 0 164 0.0
total 959 2313 41.4


line stmt bran cond sub pod time code
1             package Games::Rezrov::StoryFile;
2             # manages game file data and implements many non-io-related opcodes.
3             # Opcode inclusion made more sense in Java, where the data was a
4             # more sensible instance variable; oh well.
5             #
6             # This package is now STATIC, and does not instantiate objects, because:
7             # - it's faster not to have to dereference instance data
8             # - it's faster not to have to pass $self with every opcode/method call
9             #
10             # Not pretty, but v5+ games need all the speed they can get.
11             # See TPJ #13 for why :)
12             #
13              
14 1     1   1023 use strict;
  1         2  
  1         33  
15 1     1   843 use FileHandle;
  1         30397  
  1         6  
16 1     1   404 use Carp qw(cluck croak confess);
  1         6  
  1         58  
17             #use integer;
18             # "use integer" is required for mod() to work correctly; see
19             # math tests in "etude.z5"
20              
21 1     1   681 use Games::Rezrov::ZHeader;
  1         3  
  1         49  
22 1     1   714 use Games::Rezrov::ZObject;
  1         3  
  1         32  
23 1     1   7 use Games::Rezrov::ZText;
  1         2  
  1         18  
24 1     1   1135 use Games::Rezrov::ZStatus;
  1         3  
  1         25  
25 1     1   850 use Games::Rezrov::ZDict;
  1         3  
  1         43  
26 1     1   908 use Games::Rezrov::ZReceiver;
  1         3  
  1         31  
27 1     1   6 use Games::Rezrov::ZConst;
  1         2  
  1         22  
28 1     1   552 use Games::Rezrov::ZIO_Tools;
  1         2  
  1         58  
29 1     1   6 use Games::Rezrov::ZObjectCache;
  1         2  
  1         19  
30 1     1   5 use Games::Rezrov::Inliner;
  1         1  
  1         18  
31 1     1   564 use Games::Rezrov::Quetzal;
  1         5  
  1         41  
32 1     1   11 use Games::Rezrov::ZObjectStatus;
  1         3  
  1         33  
33              
34             # constants are SLOWER when they're accessed in other packages...WTF??
35 1     1   7 use constant FRAME_MAX_LOCAL_VARIABLES => 15;
  1         2  
  1         72  
36 1     1   6 use constant FRAME_DUMMY => 0;
  1         2  
  1         51  
37 1     1   7 use constant FRAME_PROCEDURE => 1;
  1         3  
  1         54  
38 1     1   6 use constant FRAME_FUNCTION => 2;
  1         2  
  1         174  
39              
40 1     1   7 use constant GV_SCORE => 1;
  1         3  
  1         50  
41             # 8.2.3.1: global variable holding game score (v3)
42              
43             # frame indices:
44 1     1   6 use constant FRAME_RPC => 0;
  1         2  
  1         44  
45 1     1   5 use constant FRAME_ARGC => 1;
  1         2  
  1         44  
46 1     1   6 use constant FRAME_CALL_TYPE => 2;
  1         3  
  1         42  
47 1     1   6 use constant FRAME_LOCAL => 3;
  1         2  
  1         50  
48 1     1   7 use constant FRAME_ROUTINE => 18;
  1         4  
  1         45  
49             # spec 5.2; there are 15 local vars
50              
51 1     1   6 use constant CALL => 1;
  1         3  
  1         43  
52 1     1   6 use constant PRINT_PADDR => 2;
  1         2  
  1         504  
53              
54             my $global_variable_address;
55             my $global_variable_word_addr;
56             my $header;
57             my $prompt_buffer;
58              
59             my $call_stack;
60             my $lines_wrote;
61             my $transcript_filename;
62              
63             # FIX ME, RESET THESE AS NECESSARY:
64             my $fm = 0;
65             my $selected_streams;
66             my $last_score = 0;
67             my $zios;
68             my $flushing;
69             my $wrote_something;
70             my $version;
71             my $groks_f3;
72             my $columns;
73             my $rows;
74             my ($ztext, $zstatus);
75             my $last_savefile;
76             my $quetzal;
77             my $tailing;
78             my $player_object;
79             my $player_confirmed;
80              
81             my $current_room;
82             my $object_cache;
83             my $current_input_stream;
84             my $input_filehandle;
85             my $window_cursors;
86             my $zdict;
87             my $push_command;
88             my $guessing_title;
89             my $full_version_output;
90             my $game_title;
91             my $last_prompt;
92             my $undo_slots;
93             my $last_input;
94             my $game_filename;
95             my $font_3_disabled;
96              
97             my %alternate_dictionaries;
98              
99             my %candidate_po;
100              
101             $Games::Rezrov::StoryFile::PC = 1;
102             # current game PC.
103              
104             $Games::Rezrov::StoryFile::STORY_BYTES = undef;
105             # story file data.
106             # HACK: this is *static* for speed. having to deref $self->bytes()
107             # all the time seems like it's going to be really slow.
108             # a further compromise might be to ditch the "object" approach altogether
109             # and just export all these functions; story data can still be kept
110             # "privately" in this module.
111              
112             my $dynamic_area;
113             # bytes in the story that can be changed by the game.
114             # Used for "verify" opcode and game saves/restores.
115             # (Also traditionally usually used for restarts, but we Lazily just
116             # reload the whole image)
117              
118             # more for-speed hacks related to writing bytes to the transcript stream:
119             my $current_window = Games::Rezrov::ZConst::LOWER_WIN;
120              
121 1     1   8 use constant UNSIGNED_BYTE => 0xff;
  1         2  
  1         81  
122              
123 1     1   7 use constant LINEFEED => 10;
  1         3  
  1         13974  
124             # ascii
125              
126             my $buffering;
127             my ($upper_lines, $lower_lines);
128             # HACKS, FIX ME
129              
130             my $current_frame;
131              
132             my $lines_read = 0;
133              
134             my $TYPO_NOTIFY;
135              
136             my $GLOBAL_TEMP_CONTROL;
137             my $GLOBAL_TEMP_OFFSET;
138              
139             my %Z_TRANSLATIONS = (
140             0x18 => "UP",
141             0x19 => "DOWN",
142             0x1a => "LEFT",
143             0x1b => "RIGHT",
144             179 => '|',
145             186 => '#',
146             196 => '-',
147             205 => '=',
148             );
149             # in beyond zork, when the ZIO can't handle font 3, game can
150             # send control characters
151              
152             my $INLINE_CODE = '
153             sub call {
154             my ($argv, $type) = @_;
155             # call a routine, either as a procedure (result thrown away)
156             # or a function (result stored). First argument of argv
157             # is address of function to call.
158             if ($argv->[0] == 0) {
159             # spec 6.4.3: calls to address 0 return 0
160             store_result(0) if ($type == FRAME_FUNCTION);
161             } else {
162             push_frame($type);
163             # make a new frame of specified type
164            
165             $Games::Rezrov::StoryFile::PC = convert_packed_address($argv->[0], CALL);
166             # set the current PC
167            
168             my $args = GET_BYTE();
169             # spec 5.2: routine begins with an arg count
170             die "impossible arg count of $args"
171             if ($args < 0 || $args > FRAME_MAX_LOCAL_VARIABLES);
172            
173             # ZInterpreter.zdb.save("call type " + type + " argc:" + argc + " args:" + args); # debug
174             # current.arg_count = args;
175             my $argc = scalar @{$argv};
176             frame_argc($argc - 1);
177             # do not count procedure being called in argument count
178            
179             my $arg;
180             my $local_count = 0;
181             my $i = 1;
182             while (--$args >= 0) {
183             # set local variables
184             $arg = $version >= 5 ? 0 : GET_WORD();
185             # spec 5.2.1: default variables follow if version < 5
186             # $_[0]->set_local_var(++$local_count, (--$argc > 0) ? $argv->[$i++] : $arg);
187             $current_frame->[FRAME_LOCAL + ++$local_count - 1] =
188             (--$argc > 0) ? $argv->[$i++] : $arg;
189             # set local variables. There used to be a set_local_var()
190             # method, but it was inlined for speed :(
191             }
192             }
193             }
194              
195             sub store_result_MV {
196             # called by opcodes producing a result (stores it).
197             my $where = GET_BYTE();
198             # see spec 4.2.2, 4.6.
199             # zip code handles this in store_operand, and in the case of
200             # variable zero, pushes a new variable onto the stack.
201             # The store_variable() call only SETS the topmost variable,
202             # and does not add a new one. Is that code ever reached? WTF!
203              
204             # printf STDERR "store_result: %s where:%d\n", $_[1], $where;
205             # print STDERR "$where\n";
206              
207             if ($where == 0) {
208             # routine stack: push value
209             # see zmach06e.txt section 7.1 (page 33):
210              
211             # A variable number is a byte that indicates a certain variable.
212             # The meaning of a variable number is:
213             # 0: the top of the routine stack;
214             # 1-15: the local variable with that number;
215             # 16-255: the global variable with that number minus 16.
216            
217             # Writing to the variable with number 0 means to push a value onto
218             # the routine stack; reading this variable means pulling a value off.
219             routine_push(UNSIGNED_WORD($_[0]));
220             # make sure the value is cast into unsigned form.
221             # see add() for a lengthy debate on the subject.
222             } else {
223             set_variable($where, $_[0]);
224             # set_variable does casting for us
225             }
226             }
227              
228             sub store_result_GV {
229             # called by opcodes producing a result (stores it).
230             $GLOBAL_TEMP_CONTROL = GET_BYTE();
231             # see spec 4.2.2, 4.6.
232             # zip code handles this in store_operand, and in the case of
233             # variable zero, pushes a new variable onto the stack.
234             # The store_variable() call only SETS the topmost variable,
235             # and does not add a new one. Is that code ever reached? WTF!
236              
237             # printf STDERR "store_result: %s where:%d\n", $_[1], $GLOBAL_TEMP_CONTROL;
238             # print STDERR "$GLOBAL_TEMP_CONTROL\n";
239              
240             if ($GLOBAL_TEMP_CONTROL == 0) {
241             # routine stack: push value
242             # see zmach06e.txt section 7.1 (page 33):
243              
244             # A variable number is a byte that indicates a certain variable.
245             # The meaning of a variable number is:
246             # 0: the top of the routine stack;
247             # 1-15: the local variable with that number;
248             # 16-255: the global variable with that number minus 16.
249            
250             # Writing to the variable with number 0 means to push a value onto
251             # the routine stack; reading this variable means pulling a value off.
252             routine_push(UNSIGNED_WORD($_[0]));
253             # make sure the value is cast into unsigned form.
254             # see add() for a lengthy debate on the subject.
255             } else {
256             set_variable($GLOBAL_TEMP_CONTROL, $_[0]);
257             # set_variable does casting for us
258             }
259             }
260              
261             sub conditional_jump_MV {
262             # see spec section 4.7, zmach06e.txt section 7.3
263             # argument: condition
264             # "my" vars version: prettier, but slower
265             my $control = GET_BYTE();
266            
267             my $offset = $control & 0x3f;
268             # basic address is six low bits of the first byte.
269             if (($control & 0x40) == 0) {
270             # if "bit 6" is not set, address consists of the six (low) bits
271             # of the first byte plus the next 8 bits.
272             $offset = ($offset << 8) + GET_BYTE();
273             if (($offset & 0x2000) > 0) {
274             # if the highest bit (formerly bit 6 of the first byte)
275             # is set...
276             $offset |= 0xc000;
277             # turn on top two bits
278             # FIX ME: EXPLAIN THIS
279             }
280             }
281            
282             if ($control & 0x80 ? $_[0] : !$_[0]) {
283             # normally, branch occurs when condition is false.
284             # however, if topmost bit is set, jump occurs when condition is true.
285             if ($offset > 1) {
286             # jump
287             jump($offset);
288             } else {
289             # instead of jump, this is a RTRUE (1) or RFALSE (0)
290             ret($offset);
291             }
292             }
293             }
294              
295             sub conditional_jump_GV {
296             # see spec section 4.7, zmach06e.txt section 7.3
297             # argument: condition
298             # global variables version: hideous, but faster? (no "my" variable create/destroy)
299             $GLOBAL_TEMP_CONTROL = GET_BYTE();
300            
301             $GLOBAL_TEMP_OFFSET = $GLOBAL_TEMP_CONTROL & 0x3f;
302             # basic address is six low bits of the first byte.
303             if (($GLOBAL_TEMP_CONTROL & 0x40) == 0) {
304             # if "bit 6" is not set, address consists of the six (low) bits
305             # of the first byte plus the next 8 bits.
306             $GLOBAL_TEMP_OFFSET = ($GLOBAL_TEMP_OFFSET << 8) + GET_BYTE();
307             if (($GLOBAL_TEMP_OFFSET & 0x2000) > 0) {
308             # if the highest bit (formerly bit 6 of the first byte)
309             # is set...
310             $GLOBAL_TEMP_OFFSET |= 0xc000;
311             # turn on top two bits
312             # FIX ME: EXPLAIN THIS
313             }
314             }
315            
316             if ($GLOBAL_TEMP_CONTROL & 0x80 ? $_[0] : !$_[0]) {
317             # normally, branch occurs when condition is false.
318             # however, if topmost bit is set, jump occurs when condition is true.
319             if ($GLOBAL_TEMP_OFFSET > 1) {
320             # jump
321             jump($GLOBAL_TEMP_OFFSET);
322             } else {
323             # instead of jump, this is a RTRUE (1) or RFALSE (0)
324             ret($GLOBAL_TEMP_OFFSET);
325             }
326             }
327             }
328              
329              
330             sub add {
331             # signed 16-bit addition
332             # args: x, y
333             # my ($self, $x, $y) = @_;
334             # die if $x & 0x8000 or $y & 0x8000;
335              
336             # my $result = unsigned_word(signed_word($x) + signed_word($y));
337             # this does not work correctly; example:
338             # die in zork 1 (teleport chasm, N [grue]), score has -10 added
339             # to it, result is 65526. Since value is always stored internally,
340             # do not worry about converting to unsigned. Brings up a larger issue:
341             # sometimes store_result writes data to the story, in which case
342             # we need an unsigned value! Solution -- do this casting only if
343             # we _need_ to, ie writing bytes to the story: see set_global_var()
344              
345             # Unfortunately, this breaks Trinity:
346             # count:538 pc:97444 type:2OP opcode:20 (add) operands:36910,100
347             # here we get into trouble because the sum uses the sign bit (0x8000)
348             # but it is an UNSIGNED value! So in this case we *must* make sure
349             # the result is unsigned. Solution #2: change store_result to
350             # make sure everything is unsigned. Cast to signed only when we are
351             # sure the data is signed (see set_variable, scores)
352            
353             # store_result(signed_word($x) + signed_word($y));
354             store_result(SIGNED_WORD($_[0]) + SIGNED_WORD($_[1]));
355             }
356              
357             sub subtract {
358             # signed 16-bit subtraction: args: $x, $y
359             store_result(SIGNED_WORD($_[0]) - SIGNED_WORD($_[1]));
360             }
361              
362             sub multiply {
363             # signed 16-bit multiplication; args: $x, $y
364             store_result(SIGNED_WORD($_[0]) * SIGNED_WORD($_[1]));
365             }
366              
367             sub divide {
368             # signed 16-bit division; args: $x, $y
369             store_result(SIGNED_WORD($_[0]) / SIGNED_WORD($_[1]));
370             }
371              
372             sub compare_jg {
373             # jump if a is greater than b; signed 16-bit comparison
374             conditional_jump(SIGNED_WORD($_[0]) > SIGNED_WORD($_[1]));
375             }
376              
377             sub compare_jl {
378             # jump if a is less than b; signed 16-bit comparison
379             conditional_jump(SIGNED_WORD($_[0]) < SIGNED_WORD($_[1]));
380             }
381              
382             sub output_stream {
383             #
384             # select/deselect output streams.
385             #
386             my $str = SIGNED_WORD($_[0]);
387             my $table_start = $_[1];
388              
389             return if $str == 0;
390             # selecting stream 0 does nothing
391              
392             # print STDERR "output_stream $str\n";
393             my $astr = abs($str);
394             my $selecting = $str > 0 ? 1 : 0;
395             if ($astr == Games::Rezrov::ZConst::STREAM_REDIRECT) {
396             #
397             # stream 3: redirect output to a table exclusively (no other streams)
398             #
399             my $stack = $zios->[Games::Rezrov::ZConst::STREAM_REDIRECT];
400             if ($selecting) {
401             #
402             # selecting
403             #
404             my $buf = new Games::Rezrov::ZReceiver();
405             $buf->misc($table_start);
406             push @{$stack}, $buf;
407             fatal_error("illegal number of stream3 opens!") if @{$stack} > 16;
408             # 7.1.2.1.1: max 16 legal redirects
409             } else {
410             #
411             # deselecting: copy table to memory
412             #
413             my $buf = pop @{$stack};
414             my $table_start = $buf->misc();
415             my $pointer = $table_start + 2;
416             my $buffer = $buf->buffer();
417             # printf STDERR "Writing redirected chunk %s to %d\n", $buffer, $pointer;
418             for (my $i=0; $i < length($buffer); $i++) {
419             set_byte_at($pointer++, ord substr($buffer,$i,1));
420             }
421             set_word_at($table_start, ($pointer - $table_start - 2));
422             # record number of bytes written
423              
424             # printf STDERR "table redir; %d / %d = %s\n", length($buffer), get_word_at($table_start), get_string_at(24663, length($buffer));
425              
426             if (@{$stack}) {
427             # this is stacked; keep redirection on (7.1.2.1.1)
428             $selected_streams->[$astr] = 1;
429             }
430             if ($version == 6) {
431             # 7.1.2.1
432             fatal_error("stream 3 close under v6, needs a-fixin");
433             }
434             }
435             } elsif ($astr == Games::Rezrov::ZConst::STREAM_TRANSCRIPT) {
436             if ($selecting) {
437             # print STDERR "opening transcript\n";
438             if (my $filename = $transcript_filename ||
439             filename_prompt("-check" => 1,
440             "-ext" => "txt",
441             )) {
442             $transcript_filename = $filename;
443             # 7.1.1.2: only ask once per session
444             my $fh = new FileHandle;
445             if ($fh->open(">$filename")) {
446             $zios->[Games::Rezrov::ZConst::STREAM_TRANSCRIPT] = $fh;
447             } else {
448             write_text(sprintf "Yikes, I can\'t open %s: %s...", $filename, lc($!));
449             $selecting = 0;
450             }
451             } else {
452             $selecting = 0;
453             }
454             unless ($selecting) {
455             newline();
456             newline();
457             }
458             } else {
459             # closing transcript
460             my $fh = $zios->[Games::Rezrov::ZConst::STREAM_TRANSCRIPT];
461             $fh->close() if $fh;
462             }
463             } elsif ($astr == Games::Rezrov::ZConst::STREAM_COMMANDS) {
464             if ($selecting) {
465             my $filename = filename_prompt("-ext" => "cmd",
466             "-check" => 1);
467             if ($filename) {
468             my $fh = new FileHandle();
469             if ($fh->open(">$filename")) {
470             $zios->[Games::Rezrov::ZConst::STREAM_COMMANDS] = $fh;
471             write_text("Recording to $filename.");
472             } else {
473             write_text("Can\'t write to $filename.");
474             $selecting = 0;
475             }
476             }
477             } else {
478             my $fh = $zios->[Games::Rezrov::ZConst::STREAM_COMMANDS];
479             if ($fh) {
480             $fh->close();
481             write_text("Recording stopped.");
482             } else {
483             write_text("Um, I\'m not recording now.");
484             }
485             }
486             newline();
487             } elsif ($astr == Games::Rezrov::ZConst::STREAM_STEAL) {
488             # printf STDERR "steal: %s\n", $selecting;
489             $zios->[Games::Rezrov::ZConst::STREAM_STEAL] = $selecting ? new Games::Rezrov::ZReceiver() : undef;
490             } elsif ($astr != Games::Rezrov::ZConst::STREAM_SCREEN) {
491             fatal_error("Unknown stream $str");
492             }
493              
494             $selected_streams->[$astr] = $selecting;
495             }
496              
497             sub erase_window {
498             my $window = SIGNED_WORD($_[0]);
499             my $zio = screen_zio();
500             if ($window == -1) {
501             # 8.7.3.3:
502             # $self->split_window(Games::Rezrov::ZConst::UPPER_WIN, 0);
503             # WRONG!
504             split_window(0);
505             # collapse upper window to size 0
506             clear_screen();
507             # erase the entire screen
508             reset_write_count();
509             set_window(Games::Rezrov::ZConst::LOWER_WIN);
510             set_cursor(($version == 4 ? $rows : 1), 1);
511             # move cursor to the appropriate line for this version;
512             # hack: at least it\'s abstracted :)
513             } elsif ($window < 0 or $window > 1) {
514             $zio->fatal_error("erase_window $window !");
515             } else {
516             #
517             # erase specified window
518             #
519             my $restore = $zio->get_position(1);
520             my ($start, $end);
521             if ($window == Games::Rezrov::ZConst::UPPER_WIN) {
522             $start = 0;
523             $end = $upper_lines;
524             } elsif ($window == Games::Rezrov::ZConst::LOWER_WIN) {
525             $start = $upper_lines;
526             $end = $rows;
527             reset_write_count();
528             } else {
529             die "clear window $window!";
530             }
531             for (my $i = $start; $i < $end; $i++) {
532             # $zio->erase_line($i);
533             $zio->absolute_move(0, $i);
534             $zio->clear_to_eol();
535             }
536             &$restore();
537             # restore cursor position
538             }
539             }
540              
541             sub jump {
542             # unconditional jump; modifies PC
543             # see zmach06e.txt, section 8.4.
544             # argument: new offset
545             $Games::Rezrov::StoryFile::PC += SIGNED_WORD($_[0] - 2);
546             }
547              
548             sub print_num {
549             # print the given signed number.
550             write_text(SIGNED_WORD($_[0]));
551             }
552              
553             sub inc_jg {
554             my ($variable, $value) = @_;
555             # increment a variable, and branch if it is now greater than value.
556             # **indirect?**
557             $value = SIGNED_WORD($value);
558              
559             my $before = SIGNED_WORD(get_variable($variable));
560             my $new_val = SIGNED_WORD($before + 1);
561             set_variable($variable, $new_val);
562             conditional_jump($new_val > $value);
563             }
564              
565             sub increment {
566             # increment a variable (16 bits, signed). arg: variable #
567             # **indirect?**
568             my $value = SIGNED_WORD(get_variable($_[0])) + 1;
569             set_variable($_[0], UNSIGNED_WORD($value));
570             }
571              
572             sub decrement {
573             # decrement a variable (16 bits, signed)
574             # **indirect?**
575             my $value = SIGNED_WORD(get_variable($_[0])) - 1;
576             set_variable($_[0], UNSIGNED_WORD($value));
577             }
578              
579             sub dec_jl {
580             my ($variable, $value) = @_;
581             # decrement a signed 16-bit variable, and branch if it is now less than value.
582             # **indirect?**
583             $value = SIGNED_WORD($value);
584              
585             my $before = SIGNED_WORD(get_variable($variable));
586             my $new_val = SIGNED_WORD($before - 1);
587             set_variable($variable, UNSIGNED_WORD($new_val));
588             conditional_jump($new_val < $value);
589             }
590              
591             sub mod {
592             # store remainder after signed 16-bit division
593             if (1) {
594             use integer;
595             # without "use integer", "%" operator flunks etude.z5 tests
596             # (on all systems? linux anyway).
597             # For example: perl normally says (13 % -5) == -2;
598             # it "should" be 3, or (13 - (-5 * -2))
599             #
600             # "use integer" computes math ops in integer, thus always
601             # rounding towards zero and getting around the problem.
602             #
603             # Unfortunately, "use integer" must be scoped here lest it play
604             # havoc in other places which require floating point division:
605             # e.g. pixel-based text wrapping.
606             store_result(SIGNED_WORD($_[0]) % SIGNED_WORD($_[1]));
607             } else {
608             # an alternative workaround?:
609             my $x = SIGNED_WORD($_[0]);
610             my $y = SIGNED_WORD($_[1]);
611             my $times = int($x / $y);
612             # how many times does $y fit into $x; always round towards zero!
613             store_result($x - ($y * $times));
614             }
615             }
616              
617             sub z_store {
618             # opcode to set variable
619             # **indirect**
620             set_variable($_[0], $_[1], 1);
621             # when called as an opcode, set indirect stack reference flag
622             }
623              
624             sub set_variable {
625             # args:
626             # $_[0] = $variable
627             # $_[1] = $value
628             # $_[2] = if nonzero, "indirect stack reference" mode (see draft spec 1.1)
629             # printf STDERR "set_variable %s = %s\n", $_[1], $_[2];
630             # see spec 4.2.2
631             if ($_[0] == 0) {
632             # "top of routine stack": do we push, or just set?
633             # draft spec 1.1 says under certain circumstances we should just
634             # manipulate the first stack variable, and not push/pop.
635             #
636             # - for the 7 opcodes mentioned, frotz 2.43 only seems to follow
637             # the 1.1 spec for 3 of them:
638             #
639             # z_store, z_load, z_pull: set top of stack
640             # z_inc, z_inc_chk: push onto stack
641             # z_dec, z_dec_chk: pop from stack
642              
643             if ($_[2]) {
644             # indirect stack reference mode
645             $current_frame->[$#$current_frame] = UNSIGNED_WORD($_[1]);
646             # just set top variable, don\'t push
647             } else {
648             routine_push($_[1]);
649             }
650             } elsif ($_[0] <= 15) {
651             # local variable
652             $current_frame->[FRAME_LOCAL + $_[0] - 1] = UNSIGNED_WORD($_[1]);
653             # printf "set local var %d to %d\n", $_[0], $current_frame->[FRAME_LOCAL + $_[0] - 1];
654             # numbered starting at 1, not 0
655             } else {
656             # global
657             $_[0] -= 16;
658             # indexed starting at 0
659              
660             set_global_var($_[0], UNSIGNED_WORD($_[1]));
661             }
662             }
663              
664             sub art_shift {
665             # sect15.html#art_shift; ARiThmetic shift
666             my $number = SIGNED_WORD($_[0]);
667             my $places = SIGNED_WORD($_[1]);
668             store_result($places > 0 ? $number << $places : $number >> abs($places));
669             # sign bit persists after right shift
670             }
671              
672             sub log_shift {
673             # sect15.html#log_shift; LOGical shift
674             my $number = UNSIGNED_WORD($_[0]);
675             my $places = SIGNED_WORD($_[1]);
676             store_result($places > 0 ? $number << $places : abs($number) >> abs($places));
677             # sign bit cleared during right shift
678             }
679              
680             sub get_property_length {
681             # given the literal address of a property data block,
682             # find and store size of the property data (number of bytes).
683             # example usage: "inventory" cmd.
684             # arg: address
685             my $address = $_[0];
686              
687             # die "get_property_length";
688             # given the literal address of a property data block,
689             # find and store size of the property data (number of bytes).
690             # example usage: "inventory" cmd
691             my $addr = SIGNED_WORD($address - 1);
692             # subtract one because we are given data start location, not
693             # the index location (yuck). Also account for possible rollover;
694             # one example: (1) start sorcerer. (2) "ne" (3) "frotz me".
695             # int rollover crash: 0 becomes -1 instead of 65535.
696             my $size_byte = get_byte_at($addr & 0xffff);
697             my $result;
698             if ($version <= 3) {
699             # 12.4.1
700             $result = ($size_byte >> 5) + 1;
701             } else {
702             if (($size_byte & 0x80) > 0) {
703             # spec 12.4.2.1: this is the second size byte, length
704             # is in bottom 6 bits
705             $result = $size_byte & 0x3f;
706             if ($result == 0) {
707             # 12.4.2.1.1
708             # print STDERR "wacky inform compiler size; check this\n";
709             $result = 64;
710             }
711             } else {
712             # 12.4.2.2
713             $result = (($size_byte & 0x40) > 0) ? 2 : 1;
714             }
715             }
716             store_result($result);
717             }
718              
719             sub z_not {
720             # sect15.html#not
721             store_result(~$_[0]);
722             }
723              
724             sub zo_verify {
725             # verify game image.
726             # in most Infocom games this seems to be either "$ve" or "$verif".
727             # sect15.html#verify
728             my $stat = $header->static_memory_address();
729             my $flen = $header->file_length();
730             my $sum = 0;
731             for (my $i = 0x40; $i < $flen; $i++) {
732             $sum += ($i < $stat) ? save_area_byte($i) : GET_BYTE_AT($i);
733             }
734             $sum = $sum % 0x10000;
735             conditional_jump($sum == $header->file_checksum());
736             }
737              
738             sub copy_table {
739             # sect15.html#copy_table
740             my ($first, $second, $size) = @_;
741              
742             $size = SIGNED_WORD($size);
743              
744             # printf STDERR "table copy from %d=>%d; %s\n", $first, $second, get_string_at($first, $size);
745              
746             my $len = abs($size);
747             my $i;
748             if ($second == 0) {
749             # zero out all bytes in first table
750             for ($i = 0; $i < $len; $i++) {
751             set_byte_at($first + $i, 0);
752             }
753             } elsif ($size < 0) {
754             # we *must* copy forwards, even if this corrupts first table
755             # untested();
756             for ($i = 0; $i < $len; $i++) {
757             set_byte_at($second + $i, get_byte_at($first + $i));
758             }
759             } else {
760             # copy first into second; since they might overlap, save off first
761             my @buf;
762             for ($i = 0; $i < $len; $i++) {
763             $buf[$i] = get_byte_at($first + $i);
764             }
765             for ($i = 0; $i < $len; $i++) {
766             set_byte_at($second + $i, $buf[$i]);
767             }
768             }
769              
770             # printf STDERR "after table copy: %s\n", get_string_at($second, $size);
771             }
772              
773             sub set_global_var {
774             # set a global variable
775             set_word_at($global_variable_address + ($_[0] * 2), $_[1]);
776             # printf STDERR "set gv %d to %d\n", @_;
777              
778             if ($_[0] == GV_SCORE and
779             Games::Rezrov::ZOptions::EMULATE_NOTIFY() and
780             !$header->is_time_game()) {
781             # 8.2.3.1: "2nd" global variable holds score
782             # ("2nd" variable is index #1)
783             my $score = SIGNED_WORD($_[1]);
784             my $diff = $score - $last_score;
785             if ($diff and Games::Rezrov::ZOptions::notifying()) {
786             write_text(sprintf "[Your score just went %s by %d points, for a total of %d.]",
787             ($diff > 0 ? "up" : "down"),
788             abs($diff), $score);
789             newline();
790             if ($last_score == 0) {
791             write_text("[NOTE: you can toggle score notification on or off at any time with the NOTIFY command.]");
792             newline();
793             }
794             }
795             $last_score = $score;
796             }
797             }
798              
799             sub random {
800             my $value = SIGNED_WORD($_[0]);
801             # return a random number between 1 and specified number.
802             # With arg 0, seed random number generator, return 0
803             # With arg < 0, seed with that value, return 0
804             my $result = 0;
805             if ($value == 0) {
806             # seed the random number generator
807             srand();
808             } elsif ($value < 0) {
809             # use specified value as a seed
810             srand($value);
811             } else {
812             $result = int(rand($value)) + 1;
813             }
814             store_result($result);
815             }
816              
817             sub get_variable_GV2 {
818             # $_[0]: variable
819             # $_[1]: indirect stack reference mode
820             # global variables version: hideous, but faster? (no "my" variable create/destroy)
821             #
822             # Testing reveals heaviest data access seems to be in the order
823             # local variables, global variables, routine variables.
824             # Re-order to reflect this.
825             #
826            
827             if ($_[0] > 0 and $_[0] <= 15) {
828             # a local variable
829             # print STDERR "get_variable: local\n";
830             return $current_frame->[FRAME_LOCAL + $_[0] - 1];
831             # numbered starting from 1, not 0
832             } elsif ($_[0] != 0) {
833             # a global variable
834             # print STDERR "get_variable: global\n";
835              
836             # disgusting, but possibly faster?
837             # use a global, avoiding declaration/destruction of $tmp:
838             $GLOBAL_TEMP_OFFSET = $global_variable_address + (($_[0] - 16) * 2);
839             return GET_WORD_AT($GLOBAL_TEMP_OFFSET);
840             } else {
841             # a routine stack variable
842             # section 4.2.2:
843             # pop from top of routine stack
844             # print STDERR "get_variable: routine\n";
845             if ($_[1]) {
846             # indirect stack reference
847             return $current_frame->[$#$current_frame];
848             } else {
849             return routine_pop();
850             }
851             }
852             }
853              
854             sub get_variable_GV3 {
855             # EXPERIMENTAL:
856             # halfway-to-inlinable, except for $_[1] :/
857             # - also assumes global variable table is on a word boundary,
858             # which sadly will not work
859              
860             # $_[0]: variable
861             # $_[1]: indirect stack reference mode
862             # global variables version: hideous, but faster? (no "my" variable create/destroy)
863             #
864             # Testing reveals heaviest data access seems to be in the order
865             # local variables, global variables, routine variables.
866             # Re-order to reflect this.
867             #
868            
869             return ($_[0] > 0 and $_[0] <= 15) ?
870             $current_frame->[FRAME_LOCAL + $_[0] - 1]
871             # a local variable
872            
873             : (
874              
875             $_[0] != 0 ?
876             # a global variable
877             # vec($Games::Rezrov::StoryFile::STORY_BYTES, ($global_variable_word_addr + $_[0] - 16), 16)
878             die("this will not work")
879             :
880             (
881             # a routine stack variable
882             # section 4.2.2:
883             # pop from top of routine stack
884             $_[1] ? $current_frame->[$#$current_frame] : routine_pop()
885             )
886              
887             );
888             }
889              
890             sub get_variable_GV4 {
891             # EXPERIMENTAL:
892             # halfway-to-inlinable, except for $_[1] :/
893              
894             # $_[0]: variable
895             # $_[1]: indirect stack reference mode
896             # global variables version: hideous, but faster? (no "my" variable create/destroy)
897             #
898             # Testing reveals heaviest data access seems to be in the order
899             # local variables, global variables, routine variables.
900             # Re-order to reflect this.
901             #
902            
903             return ($_[0] > 0 and $_[0] <= 15) ?
904             $current_frame->[FRAME_LOCAL + $_[0] - 1]
905             # a local variable
906            
907             : (
908              
909             $_[0] != 0 ?
910             # a global variable
911             # vec($Games::Rezrov::StoryFile::STORY_BYTES, ($global_variable_word_addr + $_[0] - 16), 16)
912             die("this will not work")
913             :
914             (
915             # a routine stack variable
916             # section 4.2.2:
917             # pop from top of routine stack
918             $_[1] ? $current_frame->[$#$current_frame] : routine_pop()
919             )
920              
921             );
922             }
923              
924              
925             ';
926              
927             Games::Rezrov::Inliner::inline(\$INLINE_CODE);
928 1 0 33 1 0 953 eval $INLINE_CODE;
  1 50 0 336 0 36  
  1 50 100 0 0 5  
  336 50 0 152 0 1400  
  0 100 0 56 0 0  
  0 100 0 15 0 0  
  0 100 33 1367 0 0  
  152 100 33 0 0 260  
  152 100 0 0 0 321  
  6 100   376 0 26  
  146 100   0 0 481  
  146 0   16 0 544  
  146 0   1 0 428  
  146 0   5 0 765  
  146 0   6326 0 172  
  146 0   0 0 292  
  146 0   0 0 560  
  146 0   143 0 245  
  146 50   10 0 206  
  146 0   949 0 170  
  146 0   0 0 515  
  634 50   0 0 1289  
  634 0   195 0 5774  
  56 0   1 0 271  
  15 0   3 0 79  
  1367 0   0 0 2290  
  1367 50   144 0 1625  
  1367 50   1301 0 3169  
  571 100   1697 0 939  
  571 100   0 0 1446  
  486 0   28 0 850  
  1367 0   0 0 15316  
  898 0   215 0 1622  
  830 0   0   1412  
  68 0       246  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       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       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  376 0       811  
  376 0       919  
  376 0       709  
  376 0       727  
  376 0       993  
  376 0       734  
  0 50       0  
  0 100       0  
  16 100       108  
  1 0       12  
  1 0       8  
  1         7  
  1         7  
  1         17  
  1         6  
  1         6  
  1         8  
  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         0  
  5         12  
  5         16  
  5         23  
  5         10  
  5         13  
  5         9  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  5         12  
  6326         27183  
  4586         125441  
  642         990  
  642         40399  
  1098         2163  
  0         0  
  1098         2924  
  0         0  
  0         0  
  143         249  
  143         370  
  143         334  
  143         345  
  143         273  
  143         396  
  10         32  
  10         50  
  949         27642  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  195         1158  
  1         10  
  1         3  
  1         5  
  1         2  
  1         4  
  1         32  
  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         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  
  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         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  1         5  
  3         23  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  144         542  
  144         25221  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  1301         3760  
  0         0  
  0         0  
  0         0  
  1157         18052  
  144         184  
  144         533  
  1697         3229  
  1697         3050  
  1140         25717  
  557         1232  
  0         0  
  0         0  
  0         0  
  0         0  
  28         149  
  0         0  
  215         575  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
929             undef $INLINE_CODE;
930              
931             if (1) {
932             *Games::Rezrov::StoryFile::conditional_jump = \&Games::Rezrov::StoryFile::conditional_jump_GV;
933             *Games::Rezrov::StoryFile::get_variable = \&Games::Rezrov::StoryFile::get_variable_GV2;
934             *Games::Rezrov::StoryFile::store_result = \&Games::Rezrov::StoryFile::store_result_GV;
935             } else {
936             *Games::Rezrov::StoryFile::conditional_jump = \&Games::Rezrov::StoryFile::conditional_jump_MV;
937             *Games::Rezrov::StoryFile::get_variable = \&Games::Rezrov::StoryFile::get_variable_MV;
938             *Games::Rezrov::StoryFile::store_result = \&Games::Rezrov::StoryFile::store_result_MV;
939             }
940              
941             1;
942              
943             sub new {
944 1     1 0 14 my ($type, $filename, $zio) = @_;
945 1         3 my $self = [];
946 1         4 bless $self, $type;
947 1         7 $zio->set_window(Games::Rezrov::ZConst::LOWER_WIN);
948 1         3 $game_filename = $filename;
949 1         3 $zios = [];
950 1         4 $zios->[Games::Rezrov::ZConst::STREAM_SCREEN] = $zio;
951 1         3 $zios->[Games::Rezrov::ZConst::STREAM_REDIRECT] = [];
952             # this stream redirects to memory and can be a stack
953 1         3 $selected_streams = [];
954              
955 1         3 $version = 0;
956             # don't even ask :P
957              
958 1         11 return $self;
959             }
960              
961             sub compare_jz {
962             # branch if the value is zero
963 372     372 0 9713 conditional_jump($_[0] == 0);
964             }
965              
966             sub setup {
967 1     1 0 9 my $zio = screen_zio();
968              
969 1 50 33     8 die "zio did not set up geometry" unless $rows and $columns;
970              
971             #
972             # Set up "loading" message:
973             #
974 1 50       6 if ($zio->can_split()) {
975 0         0 my $message = "The story is loading...";
976 0         0 clear_screen();
977 0 0       0 if ($zio->fixed_font_default()) {
978 0         0 my $start_x = int(($columns / 2) - length($message) / 2);
979 0         0 my $start_y = int($rows / 2);
980 0         0 $zio->write_string($message, $start_x, $start_y);
981             } else {
982 0         0 my $width = $zio->string_width($message);
983 0         0 my ($max_x, $max_y) = $zio->get_pixel_geometry();
984 0         0 my $pixel_center = ($max_x / 2) - ($width / 2);
985 0         0 my $column = ($pixel_center / $max_x) * $columns;
986 0         0 $zio->absolute_move(int($column), int($rows / 2));
987 0         0 $zio->write_string($message);
988             }
989 0         0 $zio->update();
990             }
991            
992 1         4 load();
993              
994 1         12 my @no_title_games = (
995             [ Games::Rezrov::ZDict::SAMPLER1 ],
996             [ Games::Rezrov::ZDict::BEYOND_ZORK ]
997             );
998              
999 1         5 foreach my $game (@no_title_games) {
1000 2         3 my @v = @{$game};
  2         5  
1001 2         3 shift @v;
1002 2 50       6 if (is_this_game(@v)) {
1003 0         0 Games::Rezrov::ZOptions::GUESS_TITLE(0);
1004             }
1005             }
1006              
1007 1         3 $current_input_stream = Games::Rezrov::ZConst::INPUT_KEYBOARD;
1008 1         2 $undo_slots = [];
1009 1         2 $window_cursors = [];
1010             # cursor positions for individual windows
1011 1         4 reset_write_count();
1012 1         36 $object_cache = new Games::Rezrov::ZObjectCache();
1013 1         21 $quetzal = new Games::Rezrov::Quetzal();
1014            
1015             # story _must_ be loaded beyond this point...
1016 1 50       3883 Games::Rezrov::ZOptions::EMULATE_NOTIFY(0) if ($version > 3);
1017             # our notification trick only works for v3 games
1018            
1019 1         11 $ztext = new Games::Rezrov::ZText();
1020 1         11 $zstatus = new Games::Rezrov::ZStatus();
1021 1         31 $zdict = new Games::Rezrov::ZDict();
1022 1 50 33     7 if (Games::Rezrov::ZOptions::EMULATE_UNDO() and
1023             $zdict->get_dictionary_address("undo")) {
1024             # disable undo emulation for games that supply the word "undo"
1025 0         0 Games::Rezrov::ZOptions::EMULATE_UNDO(0);
1026             }
1027            
1028 1         33 output_stream(Games::Rezrov::ZConst::STREAM_SCREEN());
1029            
1030 1         2 $current_window = Games::Rezrov::ZConst::LOWER_WIN;
1031             # HACKS, FIX ME
1032             # $zio->set_version($self);
1033 1         33 erase_window(-1);
1034             # collapses the upper window
1035              
1036 1 50 33     10 if ($version <= 3 and
      33        
1037             $zio->can_split() and
1038             !$zio->manual_status_line()) {
1039             # Centralized management of the status line.
1040             # Perform a split_window(), we'll use the "upper window"
1041             # for the status line.
1042             # This is BROKEN: Seastalker is a v3 game that uses the upper window!
1043 0         0 split_window(1);
1044             }
1045            
1046 1         4 set_window(Games::Rezrov::ZConst::LOWER_WIN);
1047              
1048 1         7 if (0) {
1049             # debugging
1050             set_cursor(1,1);
1051             my $message = "line 1, column 1";
1052             write_zchunk(\$message);
1053             screen_zio()->update();
1054             sleep 10;
1055             }
1056             }
1057              
1058             sub AUTOLOAD {
1059             # probably an unimplemented opcode.
1060             # Send output to the ZIO to print it, as STDERR might not be "visible"
1061             # for some ZIO implementations
1062 0     0   0 fatal_error(sprintf 'unknown sub "%s": unimplemented opcode?', $Games::Rezrov::StoryFile::AUTOLOAD);
1063             }
1064              
1065             sub load {
1066             # completely (re-) load game data. Resets all state info.
1067 2     2 0 8 my ($just_version) = @_;
1068 2         27 my $size = -s $game_filename;
1069 2 50       74 open(GAME, $game_filename) || die "can't open $game_filename: $!\n";
1070 2         6 binmode GAME;
1071 2 100       13 if ($just_version) {
1072             #
1073             # hack: just get the version of the game (first byte).
1074             #
1075             # We do this so we can initialize the I/O layer and put up
1076             # a "loading" message while we wait. We need the version
1077             # to figure out whether to create a status line in the ZIO;
1078             # important for Tk version (visually annoying to create status
1079             # line later on)
1080             #
1081 1         3 my $buf;
1082 1 50       31 if (read(GAME, $buf, 1) == 1) {
1083 1         8 return unpack "C", $buf;
1084             } else {
1085 0         0 die "huh?";
1086             }
1087             } else {
1088 1         152 my $read = read(GAME, $Games::Rezrov::StoryFile::STORY_BYTES, $size);
1089 1         12 close GAME;
1090 1 50       6 die "read error" unless $read == $size;
1091              
1092 1         192 my $zio = screen_zio();
1093 1         44 $header = new Games::Rezrov::ZHeader($zio);
1094 1         28 $global_variable_address = $header->global_variable_address();
1095              
1096 1         5 $global_variable_word_addr = int($global_variable_address / 2);
1097             # is this always aligned on a word boundary???
1098             # NO! Many games do not. This won't work, but it would
1099             # have been nice to get words via a single vec() of size 16 rather than
1100             # two vecs() of size 8 and a shift!
1101              
1102 1         29 my $static = $header->static_memory_address();
1103 1         19 $dynamic_area = substr($Games::Rezrov::StoryFile::STORY_BYTES, 0, $static);
1104             # vec($dynamic_area, 0x50, 8) = 12;
1105            
1106             # $self->header($header);
1107            
1108 1         29 $version = $header->version();
1109 1         11 $groks_f3 = $zio->groks_font_3();
1110              
1111             # $last_score = 0;
1112             # for "NOTIFY" emulation
1113 1         5 reset_cheats();
1114             }
1115             }
1116              
1117             sub get_byte_at {
1118             # return an 8-bit byte at specified storyfile offset.
1119             # die unless @_ == 2;
1120             # print STDERR "get_byte_at $_[1]\n" if $_[1] < 0x38;
1121             # print STDERR "gba\n";
1122 685     685 0 18638 return vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[0], 8);
1123             }
1124              
1125             sub save_area_byte {
1126             # return byte in "pristine" game image
1127 0     0 0 0 return vec($dynamic_area, $_[0], 8);
1128             }
1129              
1130             sub get_save_area {
1131             # return ref to "pristine" game image
1132             # Don't use this :)
1133 0     0 0 0 return \$dynamic_area;
1134             }
1135              
1136             sub get_story {
1137             # return ref to game data
1138             # Don't use this :)
1139 4     4 0 345 return \$Games::Rezrov::StoryFile::STORY_BYTES;
1140             }
1141              
1142             sub set_byte_at {
1143             # set an 8-bit byte at the specified storyfile offset to the
1144             # specified value.
1145             # print STDERR "sba\n";
1146 627     627 0 17265 vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[0], 8) = $_[1];
1147             # printf STDERR " set_byte_at %s = %s\n", $_[1], $_[2];
1148             }
1149              
1150             sub get_word_at {
1151             # return unsigned 16-bit word at specified offset
1152             # die unless @_ == 2;
1153            
1154             # print STDERR "gwa\n";
1155              
1156             # return ((vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[1], 8) << 8) + vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[1] + 1, 8));
1157             # return unpack "n", substr($Games::Rezrov::StoryFile::STORY_BYTES, $_[1], 2);
1158              
1159             # using vec() and doing our bit-twiddling manually seems faster
1160             # than using unpack(), either with a substr...
1161             #
1162             # $x = unpack "n", substr($Games::Rezrov::StoryFile::STORY_BYTES, $where, 2);
1163             #
1164             # or with using null bytes in the unpack...
1165             #
1166             # $x = unpack "x$where n", $Games::Rezrov::StoryFile::STORY_BYTES
1167             #
1168             # Oh well...
1169            
1170             # print STDERR "get_word_at $_[1]\n" if $_[1] < 0x38;
1171              
1172 419     419 0 12024 return ((vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[0], 8) << 8) +
1173             vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[0] + 1, 8));
1174             }
1175              
1176             sub set_word_at {
1177             # set 16-bit word at specified index to specified value
1178             # die unless @_ == 3;
1179             # croak if ($_[1] == 30823);
1180             # print STDERR "swa\n";
1181 352     352 0 1119 vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[0], 8) = ($_[1] >> 8) & UNSIGNED_BYTE;
1182 352         968 vec($Games::Rezrov::StoryFile::STORY_BYTES, $_[0] + 1, 8) = $_[1] & UNSIGNED_BYTE;
1183 352 50       10032 if ($_[0] == Games::Rezrov::ZHeader::FLAGS_2) {
1184             # activity in flags controlling printer transcripting.
1185             # Transcripting is set by the game and not by its own opcode.
1186             # see 7.3, 7.4
1187 0 0       0 my $str = $_[1] & Games::Rezrov::ZHeader::TRANSCRIPT_ON ? Games::Rezrov::ZConst::STREAM_TRANSCRIPT : - Games::Rezrov::ZConst::STREAM_TRANSCRIPT;
1188             # temp variable to prevent "modification of read-only value"
1189             # error when output_stream() tries to cast @_ to signed short
1190              
1191 0         0 output_stream($str);
1192             # use stream-style notification to tell the game about transcripting
1193             }
1194             }
1195              
1196             sub get_string_at {
1197             # return string of bytes at given offset
1198 5     5 0 51 return substr($Games::Rezrov::StoryFile::STORY_BYTES, $_[0], $_[1]);
1199             }
1200              
1201             sub reset_cheats {
1202 2 100   2 0 47 $zdict->bp_cheat_data(0) if $zdict;
1203             # reset "angiotensin" cheat (Bureaucracy)
1204              
1205 2         16 $last_score = get_global_var(GV_SCORE);
1206             # print STDERR "last_score: $last_score\n";
1207             # for NOTIFY emulation not to get confused after restore
1208             # FIX ME: block off to use only if cheat active and correct game version.
1209             }
1210              
1211             sub reset_game {
1212             # init/reset game state
1213 1     1 0 3 $Games::Rezrov::StoryFile::PC = 0;
1214 1         2 $call_stack = [];
1215 1         4 $lines_read = 0;
1216              
1217 1         5 reset_cheats();
1218              
1219 1 50       33 if ($header->version() == 6) {
1220             # 5.4: "main" routine
1221 0         0 call_proc($header->first_instruction_address());
1222             } else {
1223             # 5.5
1224 1         7 push_frame(FRAME_DUMMY);
1225             # create toplevel "dummy" frame: no parent, but can still
1226             # create local and stack variables. Also consistent with
1227             # Quetzal savefile model
1228 1         32 $Games::Rezrov::StoryFile::PC = $header->first_instruction_address();
1229             }
1230             # FIX ME: we could pack the address and then do a standard call()...
1231            
1232 1         5 set_buffering(1);
1233             # 7.2.1: buffering is always on for v1-3, on by default for v4+.
1234             # We call this here so each implementation of ZIO doesn't have
1235             # to set the default.
1236              
1237 1         5 reset_write_count();
1238 1         4 clear_screen();
1239 1         6 set_window(Games::Rezrov::ZConst::LOWER_WIN());
1240              
1241             # FIX ME: reset zios() array here!
1242             # centralize all this with setup() stuff...
1243             }
1244              
1245             sub reset_storyfile {
1246             # FIX ME: everything in the header should be wiped but the
1247             # "printer transcript bit," etc.
1248 0     0 0 0 load();
1249             # hack
1250             }
1251              
1252             sub push_frame {
1253             # push a call frame onto stack
1254 147     147 0 236 my ($type) = @_;
1255              
1256 147         288 $current_frame = [];
1257 147         416 frame_call_type($type);
1258 147         342 frame_return_pc($Games::Rezrov::StoryFile::PC);
1259 147         596 $#$current_frame = FRAME_ROUTINE - 1;
1260             # expand frame so routine variables will start to be added at correct index
1261 147         192 push @{$call_stack}, $current_frame;
  147         3986  
1262             }
1263              
1264             sub load_variable {
1265             # get the value of a variable and store it.
1266             # **indirect**
1267 0     0 0 0 store_result(get_variable($_[0], 1));
1268             }
1269              
1270             sub convert_packed_address {
1271             # unpack a packed address. See spec 1.2.3
1272 150 50 33 150 0 755 if ($version >= 1 and $version <= 3) {
    0 0        
    0 0        
    0          
1273 150         3914 return $_[0] * 2;
1274             } elsif ($version == 4 or $version == 5) {
1275 0         0 return $_[0] * 4;
1276             } elsif ($version == 6 or $version == 7) {
1277 0 0       0 my $offset = $_[1] == CALL ? $header->routines_offset() : $header->strings_offset();
1278 0         0 return ($_[0] * 4) + (8 * $offset);
1279             # 4P + 8R_O Versions 6 and 7, for routine calls
1280             # 4P + 8S_O Versions 6 and 7, for print_paddr
1281             # R_O and S_O are the routine and strings offsets (specified in the header as words at $28 and $2a, respectively).
1282             } elsif ($version == 8) {
1283 0         0 return $_[0] * 8;
1284             } else {
1285 0         0 die "don't know how to unpack addr for version $version";
1286             }
1287             }
1288              
1289             sub ret {
1290 142     142 0 249 my ($value) = @_;
1291             # return from a subroutine
1292 142         323 my $call_type = pop_frame();
1293              
1294 142 50       317 if ($call_type == FRAME_FUNCTION) {
    0          
1295 142         3787 store_result($value);
1296             } elsif ($call_type != FRAME_PROCEDURE) {
1297 0         0 die("unknown frame call type!");
1298             }
1299 142         3859 return $value;
1300             # might be needed for an interrupt call (not yet implemented)
1301             }
1302              
1303              
1304             sub get_variable_MV {
1305             # $_[0]: variable
1306             # $_[1]: indirect stack reference mode
1307             # "my" vars version: prettier, but slower
1308 0 0   0 0 0 if ($_[0] == 0) {
    0          
1309             # section 4.2.2:
1310             # pop from top of routine stack
1311             # print STDERR "rp\n";
1312 0 0       0 if ($_[1]) {
1313             # indirect stack reference
1314 0         0 return $current_frame->[$#$current_frame];
1315             } else {
1316 0         0 return routine_pop();
1317             }
1318             } elsif ($_[0] <= 15) {
1319             # a local variable
1320             # print STDERR "lv\n";
1321 0         0 return $current_frame->[FRAME_LOCAL + $_[0] - 1];
1322             # numbered starting from 1, not 0
1323             } else {
1324             # a global variable
1325             # print STDERR "gv\n";
1326             # return get_global_var($_[1] - 16);
1327             # most readable, but slowest
1328             # return get_word_at($_[0]->global_variable_address() + (($_[1] - 16) * 2));
1329             # faster, less readable
1330              
1331 0         0 my $tmp = $global_variable_address + (($_[0] - 16) * 2);
1332             # - 16 = convert to index starting at 0
1333             # print STDERR "get gv $_[0]\n";
1334 0         0 return ((vec($Games::Rezrov::StoryFile::STORY_BYTES, $tmp, 8) << 8) +
1335             vec($Games::Rezrov::StoryFile::STORY_BYTES, $tmp + 1, 8));
1336             # fastest, almost unreadable :(
1337              
1338             #
1339             # alternate approach:
1340             # disgusting, but possibly faster?
1341             # use a global, avoiding declaration/destruction of $tmp, above
1342             #
1343              
1344             # $GLOBAL_TMP = $global_variable_address + (($_[0] - 16) * 2);
1345             # return ((vec($Games::Rezrov::StoryFile::STORY_BYTES, $GLOBAL_TMP, 8) << 8) +
1346             # vec($Games::Rezrov::StoryFile::STORY_BYTES, $GLOBAL_TMP + 1, 8));
1347              
1348             }
1349             }
1350              
1351             sub get_variable_GV {
1352             # $_[0]: variable
1353             # $_[1]: indirect stack reference mode
1354             # global variables version: hideous, but faster? (no "my" variable create/destroy)
1355 0 0   0 0 0 if ($_[0] == 0) {
    0          
1356             # section 4.2.2:
1357             # pop from top of routine stack
1358             # print STDERR "get_variable: routine\n";
1359 0 0       0 if ($_[1]) {
1360             # indirect stack reference
1361 0         0 return $current_frame->[$#$current_frame];
1362             } else {
1363 0         0 return routine_pop();
1364             }
1365             } elsif ($_[0] <= 15) {
1366             # a local variable
1367             # print STDERR "get_variable: local\n";
1368 0         0 return $current_frame->[FRAME_LOCAL + $_[0] - 1];
1369             # numbered starting from 1, not 0
1370             } else {
1371             # a global variable
1372             # print STDERR "get_variable: global\n";
1373              
1374             #
1375             # disgusting, but possibly faster?
1376             # use a global, avoiding declaration/destruction of $tmp:
1377             #
1378              
1379 0         0 $GLOBAL_TEMP_OFFSET = $global_variable_address + (($_[0] - 16) * 2);
1380 0         0 return ((vec($Games::Rezrov::StoryFile::STORY_BYTES, $GLOBAL_TEMP_OFFSET, 8) << 8) +
1381             vec($Games::Rezrov::StoryFile::STORY_BYTES, $GLOBAL_TEMP_OFFSET + 1, 8));
1382              
1383             }
1384             }
1385              
1386             sub unsigned_word {
1387             # pack a signed value into an unsigned value.
1388             # Necessary to ensure the sign bit is placed at 0x8000.
1389 0     0 0 0 return unpack "S", pack "s", $_[0];
1390             }
1391              
1392             sub compare_je {
1393             # branch if first operand is equal to any of the others
1394 228     228 0 299 my $first = shift;
1395             # print STDERR "je\n";
1396 228         586 foreach (@_) {
1397 299 100       2103 conditional_jump(1), return if $_ == $first;
1398             }
1399 173         4909 conditional_jump(0);
1400             }
1401              
1402             sub store_word {
1403 202     202 0 318 my ($array_address, $word_index, $value) = @_;
1404             # set a word at a specified offset in a specified array offset.
1405 202         284 $array_address += (2 * $word_index);
1406 202         388 set_word_at($array_address, $value);
1407             }
1408              
1409             sub store_byte {
1410 546     546 0 1009 my ($array_address, $byte_index, $value) = @_;
1411 546         1312 set_byte_at($array_address + $byte_index, $value);
1412             }
1413              
1414             sub pop_frame {
1415 142     142 0 174 my $last_frame = pop @{$call_stack};
  142         275  
1416 142         299 my $call_type = $last_frame->[FRAME_CALL_TYPE];
1417             # print "pop: $call_type\n";
1418 142         199 $Games::Rezrov::StoryFile::PC = $last_frame->[FRAME_RPC];
1419 142         240 $current_frame = $call_stack->[$#$call_stack];
1420             # set frame to calling frame
1421 142         477 return $call_type;
1422             }
1423              
1424             sub get_word_index {
1425             # get a word from the specified index of the specified array
1426 215     215 0 323 my ($address, $index) = @_;
1427 215         573 store_result(get_word_at($address + (2 * $index)));
1428             }
1429              
1430             sub put_property {
1431 0     0 0 0 my ($object, $property, $value) = @_;
1432 0         0 my $zobj = get_zobject($object);
1433 0         0 my $zprop = $zobj->get_property($property);
1434 0         0 $zprop->set_value($value);
1435             }
1436              
1437             sub test_attr {
1438             # jump if some object has an attribute set
1439 63     63 0 105 my ($object, $attribute) = @_;
1440 63         153 my $zobj = get_zobject($object);
1441 63   66     1756 conditional_jump($zobj and $zobj->test_attr($attribute));
1442             # watch out for object 0
1443             }
1444              
1445             sub set_attr {
1446             # turn on given attribute of given object
1447 8     8 0 17 my ($object, $attribute) = @_;
1448 8 50       22 if (my $zobj = get_zobject($object)) {
1449             # unless object 0
1450 8         211 $zobj->set_attr($attribute);
1451             }
1452             }
1453              
1454             sub clear_attr {
1455             # clear given attribute of given object
1456 3     3 0 10 my ($object, $attribute) = @_;
1457 3 50       12 if (my $zobj = get_zobject($object)) {
1458             # unless object 0
1459 3         92 $zobj->clear_attr($attribute);
1460             }
1461             }
1462              
1463             sub print_text {
1464             # decode a string at the PC and move PC past it
1465 20     20 0 23 my $blob;
1466 20         587 ($blob, $Games::Rezrov::StoryFile::PC) = $ztext->decode_text($Games::Rezrov::StoryFile::PC);
1467 20         64 write_zchunk($blob);
1468             }
1469              
1470              
1471             sub write_zchunk {
1472 35     35 0 56 my $chunk = $_[0];
1473              
1474             # print STDERR "Chunk: $$chunk\n";
1475 35 50       88 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_REDIRECT]) {
1476             # 7.1.2.2: when active, no other streams get output
1477 0         0 my $stack = $zios->[Games::Rezrov::ZConst::STREAM_REDIRECT];
1478             # printf STDERR "redirected chunk: %s\n", $$chunk;
1479 0         0 $stack->[$#$stack]->buffer_zchunk($chunk);
1480             } else {
1481             #
1482             # other streams
1483             #
1484 35 50       90 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_SCREEN]) {
1485             #
1486             # screen
1487             #
1488 35 50 33     102 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_STEAL] and
1489             $current_window == Games::Rezrov::ZConst::LOWER_WIN) {
1490             # temporarily steal lower window output
1491 0         0 $zios->[Games::Rezrov::ZConst::STREAM_STEAL]->buffer_zchunk($chunk);
1492             } else {
1493 35         86 my $zio = $zios->[Games::Rezrov::ZConst::STREAM_SCREEN];
1494            
1495 35 50 33     171 if ($buffering and $current_window != Games::Rezrov::ZConst::UPPER_WIN) {
1496 35         199 $zio->buffer_zchunk($chunk);
1497             } else {
1498 0         0 foreach (unpack("c*", $$chunk)) {
1499 0 0       0 if ($_ == Games::Rezrov::ZConst::Z_NEWLINE) {
1500 0         0 $prompt_buffer = "";
1501 0         0 $zio->newline();
1502             } else {
1503 0         0 $zio->write_zchar($_);
1504             }
1505             }
1506             }
1507             }
1508             }
1509              
1510 35 50 33     1804 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_TRANSCRIPT] and
1511             $current_window == Games::Rezrov::ZConst::LOWER_WIN) {
1512             #
1513             # Game transcript
1514             #
1515 0 0       0 if (my $fh = $zios->[Games::Rezrov::ZConst::STREAM_TRANSCRIPT]) {
1516 0         0 my $c = $$chunk;
1517 0         0 my $nl = chr(Games::Rezrov::ZConst::Z_NEWLINE);
1518 0         0 $c =~ s/$nl/\n/g;
1519 0         0 print $fh $c;
1520             }
1521             }
1522             }
1523             }
1524            
1525             sub print_ret {
1526             # print string at PC, move past it, then return true
1527 1     1 0 4 print_text();
1528 1         6 newline();
1529 1         5 rtrue();
1530             }
1531              
1532             sub newline {
1533 17     17 0 53 write_zchar(Games::Rezrov::ZConst::Z_NEWLINE());
1534             }
1535              
1536             sub loadb {
1537             # get the byte at index "index" of array "array"
1538 628     628 0 980 my ($array, $index) = @_;
1539 628         1386 store_result(get_byte_at($array + $index));
1540             }
1541              
1542             sub bitwise_and {
1543             # story bitwise "and" of the arguments.
1544             # FIX ME: signed???
1545 13     13 0 351 store_result($_[0] & $_[1]);
1546             }
1547              
1548             sub bitwise_or {
1549             # story bitwise "or" of the arguments.
1550             # FIX ME: signed???
1551 0     0 0 0 store_result($_[0] | $_[1]);
1552             }
1553              
1554             sub rtrue {
1555             # return TRUE from this subroutine.
1556 24     24 0 92 ret(1);
1557             }
1558              
1559             sub rfalse {
1560             # return FALSE from this subroutine.
1561 4     4 0 16 ret(0);
1562             }
1563              
1564             sub write_text {
1565             # write a given string to ZIO.
1566 7     7 0 31 write_zchunk(\$_[0]);
1567 7 50       114 newline() if $_[1];
1568             # foreach (unpack "C*", $_[1]) {
1569             # $_[0]->write_zchar($_);
1570             # }
1571             }
1572              
1573             sub insert_obj {
1574 4     4 0 9 my ($object, $destination_obj) = @_;
1575             # move object to become the first child of the destination
1576             # object.
1577             #
1578             # object = O, destination_obj = D
1579             #
1580             # reorganize me: move to ZObject?
1581            
1582             # my $o = new Games::Rezrov::ZObject($object, $self);
1583 4 50       17 return unless $object;
1584             # if object being moved is ID 0, do nothing (bogus object)
1585              
1586 4         14 my $o = get_zobject($object);
1587             # my $d = new Games::Rezrov::ZObject($destination_obj, $self);
1588 4         15 my $d = get_zobject($destination_obj);
1589              
1590 4 100       18 if ($player_object) {
1591             # already know the object ID for the player
1592 3 50       15 $current_room = $destination_obj if $player_object == $object;
1593 3 50       11 if ($tailing) {
1594             # we're tailing an object...
1595 0 0       0 if ($tailing == $object) {
1596 0         0 newline();
1597 0         0 write_text(sprintf "Tailing %s: you are now in %s...", ${$o->print}, ${$d->print});
  0         0  
  0         0  
1598 0         0 newline();
1599 0         0 insert_obj($player_object, $destination_obj);
1600             # $self->suppress_hack();
1601             # $self->push_command("look");
1602             }
1603             }
1604             }
1605              
1606 4 100       13 unless ($player_confirmed) {
1607             # unless ($player_confirmed or $push_command) {
1608             # record object movements to determine which is the "player"
1609             # object, aka "cretin" :)
1610 1 50       34 if ($object_cache->is_room($destination_obj)) {
1611 1         6 $candidate_po{$lines_read}{$object} = $destination_obj;
1612             }
1613             }
1614              
1615 4 50       22 if (Games::Rezrov::ZOptions::SNOOP_OBJECTS()) {
1616 0         0 my $o1 = $o->print($ztext);
1617 0 0       0 my $o2 = $d ? $d->print($ztext) : "(null)";
1618 0         0 write_text(sprintf '[Move "%s" to "%s"]', $$o1, $$o2);
1619 0         0 newline();
1620             }
1621            
1622 4         130 $o->remove();
1623             # unlink o from its parent and siblings
1624            
1625 4         105 $o->set_parent_id($destination_obj);
1626             # set new o's parent to d
1627            
1628 4 50       14 if ($d) {
1629             # look out for destination of object 0
1630 4         102 my $old_child_id = $d->get_child_id();
1631            
1632 4         101 $d->set_child_id($object);
1633             # set d's child ID to o
1634            
1635 4 100       38 if ($old_child_id > 0) {
1636             # d had children; make them the new siblings of o,
1637             # which is now d's child.
1638 3         74 $o->set_sibling_id($old_child_id);
1639             }
1640             }
1641            
1642             }
1643              
1644             sub pull {
1645             # pop a value from a stack and store in specified variable.
1646             # **indirect**
1647 0 0   0 0 0 if ($version == 6) {
1648 0 0       0 if ($_[0]) {
1649 0         0 fatal_error("v6: pull from user stack");
1650             } else {
1651 0         0 store_result(routine_pop());
1652             # broken? (indirect var?)
1653             }
1654             } else {
1655 0         0 set_variable($_[0], routine_pop(), 1);
1656             # set indirect stack reference mode
1657             }
1658             }
1659              
1660             sub jin {
1661             # jump if parent of obj1 is obj2
1662             # or if obj2 is 0 (null) and obj1 has no parent.
1663 27     27 0 46 my ($obj1, $obj2) = @_;
1664             # my $x = new Games::Rezrov::ZObject($obj1, $self);
1665 27 50       55 if ($obj1 == 0) {
1666             # no such object; consider its parent zero as well
1667 0 0       0 conditional_jump($obj2 == 0 ? 1 : 0);
1668             } else {
1669 27         64 my $x = get_zobject($obj1);
1670 27         41 my $jump = 0;
1671 27 50       57 if ($obj2 == 0) {
1672 0 0       0 $jump = ($x->get_parent_id() == 0 ? 1 : 0);
1673 0         0 write_text("[ jin(): untested! ]");
1674 0         0 newline();
1675             } else {
1676 27 100       658 $jump = $x->get_parent_id() == $obj2 ? 1 : 0;
1677             }
1678 27         676 conditional_jump($jump);
1679             }
1680             }
1681              
1682             sub print_object {
1683             # print short name of object (Z-encoded string in object property header)
1684 4     4 0 14 my $zobj = get_zobject($_[0]);
1685 4         17 my $highlight = Games::Rezrov::ZOptions::HIGHLIGHT_OBJECTS();
1686             # set_text_style(Games::Rezrov::ZConst::STYLE_BOLD) if $highlight;
1687 4         6 my $old;
1688 4 50       12 $old = swap_text_style(Games::Rezrov::ZConst::STYLE_BOLD) if $highlight;
1689 4         113 write_zchunk($zobj->print($ztext));
1690             # set_text_style(Games::Rezrov::ZConst::STYLE_ROMAN) if $highlight;
1691 4 50       128 toggle_text_style($old) if $highlight;
1692              
1693             }
1694              
1695             sub get_parent {
1696             # get parent object of this object and store result.
1697             # arg: object
1698 32     32 0 212 my $zobj = get_zobject($_[0]);
1699 32 50       868 store_result($zobj ? $zobj->get_parent_id() : 0);
1700             # if object ID 0, will be undef
1701             }
1702              
1703             sub get_child {
1704             # get child object ID for this object ID and store result, then
1705             # jump if it exists.
1706             #
1707             # arg: object
1708 23     23 0 74 my $zobj = get_zobject($_[0]);
1709 23 50       636 my $id = $zobj ? $zobj->get_child_id() : 0;
1710             # if object ID 0, will be undef
1711 23         598 store_result($id);
1712 23         607 conditional_jump($id != 0);
1713             }
1714              
1715             sub get_sibling {
1716             # get sibling object ID for this object ID and store result, then
1717             # jump if it exists.
1718             #
1719             # arg: object
1720 21     21 0 81 my $zobj = get_zobject($_[0]);
1721 21 50       562 my $id = $zobj ? $zobj->get_sibling_id() : 0;
1722             # if object ID 0, will be undef
1723 21         807 store_result($id);
1724 21         522 conditional_jump($id != 0);
1725             }
1726              
1727             sub get_property {
1728             # retrieve the specified property of the specified object.
1729             # args: $object, $property
1730 25 50   25 0 64 if (my $zobj = get_zobject($_[0])) {
1731 25         714 store_result($zobj->get_property($_[1])->get_value());
1732             } else {
1733             # object 0
1734 0         0 store_result(0);
1735             }
1736             }
1737              
1738             sub ret_popped {
1739             # return with a variable popped from the routine stack.
1740 17     17 0 62 ret(routine_pop());
1741             }
1742              
1743             sub stack_pop {
1744             # pop and discard topmost variable from the stack
1745 0 0   0 0 0 if ($version >= 5) {
1746 0         0 fatal_error("catch() unimplemented");
1747             } else {
1748 0         0 routine_pop();
1749             }
1750             }
1751              
1752             sub read_line {
1753 4     4 0 10 my ($argv, $interpreter, $start_pc) = @_;
1754             # Read and tokenize a command.
1755             # multi-arg approach taken from zip; this call has many
1756             # possible arguments.
1757              
1758 4         8 my $text_address = $argv->[0];
1759 4   50     30 my $token_address = $argv->[1] || 0;
1760 4         8 my $time = 0;
1761 4         7 my $routine = 0;
1762 4         8 $lines_read++;
1763              
1764 4 100       15 if (%candidate_po) {
1765             # we have possible candidates for the player object.
1766             #
1767             # ZTUU: 1st move, 2 items moved to player, player moved to room
1768             # LGOP: "stool" and "it" (player) both moved to same room
1769             # SeaStalker: don't even get me started [FIX ME]...
1770             #
1771             # - checking for a toplevel child works
1772 1         3 my %seen;
1773             my %dest;
1774 1         8 my @turns = (sort {$a <=> $b} keys %candidate_po);
  0         0  
1775             # move through turns sequentially
1776              
1777 1         5 foreach my $turn (@turns) {
1778 1         2 while (my ($pid, $dest) = each %{$candidate_po{$turn}}) {
  2         14  
1779             # printf STDERR "Turn %d; %s => %s\n", $turn, $pid, $dest;
1780             # my $zs = new Games::Rezrov::ZObjectStatus($pid, $object_cache);
1781             # printf "yow -- %s: \"%s\"\n", $pid, $zs->is_toplevel_child();
1782             # next unless $zs->is_toplevel_child();
1783             # this disambiguates the player in ZTUU, but doesn't work for
1784             # seastalker [toplevel child detection broken because of
1785             # wacky case in location names]
1786 1         3 $seen{$pid}++;
1787 1         4 $dest{$pid} = $dest;
1788             # the most recent destination
1789             }
1790             }
1791              
1792 1 50       4 if ($version <= 3) {
1793 1         6 my $current_room = get_global_var(0);
1794             # 8.2.2.1
1795 1         5 my @candidates = grep {$dest{$_} == $current_room} keys %dest;
  1         6  
1796 1 50       5 if (@candidates == 1) {
1797             # in version 3 games, the current room is stored in global
1798             # variable 0. If only one object moved to that target, that's it.
1799 1         2 $player_object = $candidates[0];
1800 1         4 $player_confirmed = 1;
1801             # print STDERR "v3 confirmed: player is $player_object\n";
1802             }
1803             }
1804              
1805 1 50       5 unless ($player_confirmed) {
1806 0         0 my @ok = grep {$seen{$_} > 1} keys %seen;
  0         0  
1807 0 0       0 if (@ok == 1) {
    0          
1808             # exactly one object was observed moving multiple turns
1809 0         0 $player_object = $ok[0];
1810             # print STDERR "confirmed: player is $player_object \n";
1811 0         0 $player_confirmed = 1;
1812             } elsif (keys %seen == 1) {
1813             # in many games, the first object moved is the player.
1814             # Temporarily consider this the player until confirmation;
1815             # allows us to teleport even as the first move of the game.
1816             #
1817             # not true in: LGOP, ZTUU, etc...
1818 0         0 $player_object = (keys %seen)[0];
1819             # print STDERR "candidate po: $player_object \n";
1820             } else {
1821             # printf STDERR "failed: %d (%s) %d (%s)\n", scalar keys %seen, (join ",", keys %seen), scalar @ok, join(",", @ok);
1822             }
1823             }
1824              
1825 1 50       8 %candidate_po = () if $player_confirmed;
1826            
1827 1 50       5 delete $candidate_po{shift @turns} if @turns > 3;
1828             # remove tracking for oldest turns
1829              
1830 1 50       7 $current_room = $dest{$player_object} if $player_object;
1831             }
1832            
1833 4         21 my $max_text_length = get_byte_at($text_address);
1834 4 50       14 $max_text_length++ if ($version <= 4);
1835             # sect15.html#sread
1836            
1837 4 50       16 if (@{$argv} > 2) {
  4         16  
1838             # timeout / routine specified
1839 0         0 $time = $argv->[2];
1840 0         0 $routine = $argv->[3];
1841             }
1842              
1843 4         30 $zdict->blood_pressure_cheat_hook();
1844             # hack
1845            
1846 4         16 flush();
1847             # flush any buffered output before the prompt.
1848             # Also very important before hijacking/restoring ZIO when guessing
1849             # the title.
1850              
1851 4         18 reset_write_count();
1852              
1853 4         7 my $bef_pc = $Games::Rezrov::StoryFile::PC;
1854 4         10 my $s = "";
1855              
1856 4         22 my $guess_title = Games::Rezrov::ZOptions::GUESS_TITLE();
1857            
1858 4 50       18 if (is_stream_selected(Games::Rezrov::ZConst::STREAM_STEAL)) {
    50          
1859             # suppressing parser output up until the next prompt
1860 0         0 my $old = $zios->[Games::Rezrov::ZConst::STREAM_STEAL];
1861 0         0 output_stream(- Games::Rezrov::ZConst::STREAM_STEAL);
1862 0         0 my $suppressed = $old->buffer();
1863             # print STDERR "steal active: $suppressed\n";
1864 0 0       0 if ($push_command) {
1865 0         0 $s = $push_command;
1866             # print STDERR "pushing: $s\n";
1867 0         0 $push_command = "";
1868             } else {
1869 0 0       0 if ($guessing_title) {
1870 0         0 $full_version_output = $suppressed;
1871 0 0       0 if ($suppressed =~ /\s*(.*?)[\x0a\x0d]/) {
1872 0         0 $game_title = $1;
1873 0         0 screen_zio()->set_game_title("rezrov: " . $1);
1874             }
1875             }
1876 0         0 my $regexp = '.*' . chr(Games::Rezrov::ZConst::Z_NEWLINE);
1877             # delete everything before the prompt (everything up to last newline)
1878 0         0 $suppressed =~ s/$regexp//o;
1879 0         0 $last_prompt = $suppressed;
1880 0         0 $prompt_buffer = $suppressed;
1881             # because flush() never sees the output this came from
1882            
1883 0 0       0 if ($guessing_title) {
1884             # prompt was printed "last time", don't print again
1885 0         0 $guessing_title = 0;
1886             } else {
1887             # print the prompt
1888 0         0 screen_zio()->write_string($suppressed);
1889             }
1890             }
1891             } elsif ($guess_title) {
1892             #
1893             # The axe crashes against the rock, throwing sparks!
1894             #
1895 0 0 0     0 if (!$game_title and $player_object) {
1896             # delay submitting the "version" command until an object has been
1897             # moved; this necessary for games that read a line before the real
1898             # parser starts. Example: Leather Goddesses of Phobos.
1899             # Doesn't work: AMFV
1900 0 0       0 if ($zdict->get_dictionary_address("version")) {
1901 0         0 $guessing_title = 1;
1902 0         0 $s = "version";
1903             # submit a surreptitious "version" command to the interpreter
1904 0         0 suppress_hack();
1905             # temporarily hijack output
1906             } else {
1907             # game doesn't understand "version"; forget it.
1908             # example: Advent.z5
1909 0         0 $game_title = "not gonna happen";
1910 0         0 screen_zio()->set_game_title("rezrov");
1911             }
1912             }
1913             }
1914              
1915 4         8 my $undo_data;
1916 4 50       25 if (Games::Rezrov::ZOptions::EMULATE_UNDO()) {
1917             # save undo information
1918 4         9 my $tmp_pc = $Games::Rezrov::StoryFile::PC;
1919 4         8 $Games::Rezrov::StoryFile::PC = $start_pc;
1920             # fix me: move to quetzal itself
1921 4         41 $undo_data = $quetzal->save("", "-undo" => 1);
1922 4         44 $Games::Rezrov::StoryFile::PC = $tmp_pc;
1923             }
1924              
1925 4 50       19 unless (length $s) {
1926 4 50       15 if ($current_input_stream == Games::Rezrov::ZConst::INPUT_FILE) {
1927             #
1928             # we're fetching commands from a script file.
1929             #
1930 4         15 $s = <$input_filehandle>;
1931 4 50       12 if (defined($s)) {
1932             # got a command; display it
1933 4         13 chomp $s;
1934 4   50     24 write_text($s || "");
1935 4         15 newline();
1936             } else {
1937             # end of file
1938 0         0 input_stream(Games::Rezrov::ZConst::INPUT_KEYBOARD);
1939 0 0       0 die "quitting!\n" if Games::Rezrov::ZOptions::PLAYBACK_DIE();
1940 0         0 $s = "";
1941             }
1942             }
1943              
1944 4 50       21 unless (length $s) {
1945             #
1946             # Get commands from the user
1947             #
1948 0         0 my $initial_buf;
1949 0 0       0 if ($version <= 3) {
    0          
1950 0         0 display_status_line();
1951             } elsif ($version >= 5) {
1952             # sect15.html#read
1953             # there may be some text already displayed as if we had typed it
1954 0         0 my $initial = get_byte_at($text_address + 1);
1955 0 0       0 $initial_buf = get_string_at($text_address + 2, $initial) if $initial;
1956             }
1957              
1958 0         0 my $sz = screen_zio();
1959            
1960 0         0 $s = $sz->get_input($max_text_length, 0,
1961             "-time" => $time,
1962             "-routine" => $routine,
1963             "-zi" => $interpreter,
1964             "-preloaded" => $initial_buf,
1965             );
1966              
1967 0 0 0     0 if ($s and $sz->speaking and $s !~ /^\#speak/) {
      0        
1968 0         0 $sz->speak($s, "-gender" => 2);
1969             # say command unless we intend to turn off speech
1970             }
1971             }
1972             }
1973             # printf STDERR "cmd: $s\n";
1974              
1975 4 50       22 if (Games::Rezrov::ZOptions::CORRECT_TYPOS()) {
1976 4         22 my $msg;
1977 4         29 ($s, $msg) = $zdict->correct_typos($s);
1978 4 50       17 if ($msg) {
1979 0         0 write_text($msg);
1980 0         0 newline();
1981 0 0       0 unless ($TYPO_NOTIFY) {
1982 0         0 write_text("[NOTE: you can toggle typo correction on or off at any time with the #TYPO command.]");
1983 0         0 newline();
1984 0         0 $TYPO_NOTIFY=1;
1985             }
1986             }
1987             }
1988              
1989 4 50       23 if (Games::Rezrov::ZOptions::EMULATE_UNDO()) {
1990 4 50       15 if ($s eq "undo") {
1991             # want to undo; restore the old data
1992 0 0       0 if (@{$undo_slots}) {
  0         0  
1993 0         0 $quetzal->restore("", pop @{$undo_slots});
  0         0  
1994 0         0 write_text("Undone");
1995 0 0       0 if (@{$undo_slots}) {
  0         0  
1996 0 0       0 write_text(sprintf " (%d more turn%s may be undone)", scalar @{$undo_slots}, (scalar @{$undo_slots} == 1 ? "" : "s"));
  0         0  
  0         0  
1997             }
1998 0         0 write_text(".");
1999 0         0 newline();
2000 0         0 newline();
2001 0   0     0 write_text($last_prompt || ">");
2002             # hack!
2003              
2004 0 0       0 if ($player_object) {
2005             # after we "undo" we might be in a different room; find
2006             # the current one. Important if we try to pilfer something:
2007             # without this, it will go to the room before the undo!
2008 0         0 $object_cache->load_names();
2009 0         0 my $zstat = new Games::Rezrov::ZObjectStatus($player_object,
2010             $object_cache);
2011 0 0       0 if (my $parent = $zstat->parent_room()) {
2012 0         0 $current_room = $parent->object_id();
2013             }
2014             }
2015              
2016 0         0 return;
2017             } else {
2018 0         0 write_text("Can't undo now, sorry.");
2019 0         0 newline();
2020 0         0 newline();
2021 0         0 suppress_hack();
2022             }
2023             } else {
2024             # save this undo slot
2025 4         7 push @{$undo_slots}, $undo_data;
  4         12  
2026 4         9 while (@{$undo_slots} > Games::Rezrov::ZOptions::UNDO_SLOTS()) {
  4         19  
2027             # pop old ones
2028 0         0 shift @{$undo_slots};
  0         0  
2029             }
2030             }
2031             }
2032              
2033 4 50       13 die("PC corrupt after get_input; was:$bef_pc now:" . $Games::Rezrov::StoryFile::PC)
2034             if ($Games::Rezrov::StoryFile::PC != $bef_pc);
2035             # interrupt routine sanity check
2036              
2037              
2038 4         19 stream_dup(Games::Rezrov::ZConst::STREAM_TRANSCRIPT, $s);
2039 4         12 stream_dup(Games::Rezrov::ZConst::STREAM_COMMANDS, $s);
2040              
2041             # printf STDERR "input: %s\n", $s;
2042 4         11 $s = substr($s, 0, $max_text_length);
2043             # truncate input if necessary
2044              
2045 4         25 $zdict->save_buffer($s, $text_address);
2046            
2047 4 50 33     21 if ($version >= 5 && $token_address == 0) {
2048             # print STDERR "Skipping tokenization; test this!\n";
2049             } else {
2050 4         22 $zdict->tokenize_line($text_address,
2051             $token_address,
2052             "-len" => length($s),
2053             );
2054             }
2055              
2056             # $zdict->last_buffer($s);
2057             # last_input = s;
2058 4 50       18 store_result(10) if ($version >= 5);
2059             # sect15.html#sread; store terminating char ("newline")
2060              
2061 4         156 $last_input = $s;
2062             # save last user input; used in "oops" emulation
2063             }
2064              
2065             sub read_char {
2066 0     0 0 0 my ($argv, $zi) = @_;
2067             # read a single character
2068 0         0 reset_write_count();
2069 0         0 flush();
2070             # die("read_char: 1st arg must be 1") if ($argv->[0] != 1);
2071 0         0 my $time = 0;
2072 0         0 my $routine = 0;
2073 0 0       0 if (@{$argv} > 1) {
  0         0  
2074 0         0 $time = $argv->[1];
2075 0         0 $routine = $argv->[2];
2076             }
2077 0         0 my $result = screen_zio()->get_input(1, 1,
2078             "-time" => $time,
2079             "-routine" => $routine,
2080             "-zi" => $zi);
2081 0         0 my $code = ord(substr($result,0,1));
2082 0 0       0 $code = Games::Rezrov::ZConst::Z_NEWLINE if ($code == LINEFEED);
2083             # remap keyboard "linefeed" to what the Z-machine
2084             # will recognize as a "carriage return". This is required
2085             # for the startup form in "Bureaucracy", and probably other
2086             # places.
2087             #
2088             # - does keyboard ever return 13 (non-IBM-clones)?
2089             #
2090             # In spec terms:
2091             # - 10.7: only return characters defined in input stream
2092             # - 3.8: character "10" (linefeed) only defined for output.
2093 0         0 store_result($code);
2094             # store ascii value
2095             }
2096              
2097              
2098             sub display_status_line {
2099             # only called if needed; see spec 8.2
2100 0     0 0 0 my $zio = screen_zio();
2101 0 0       0 return unless $zio->can_split();
2102 0         0 $zstatus->update();
2103 0         0 my $right_chunk;
2104 0 0       0 if ($zstatus->time_game()) {
2105 0         0 my $hours = $zstatus->hours();
2106 0 0       0 if (Games::Rezrov::ZOptions::TIME_24()) {
2107 0         0 $right_chunk = sprintf("Time: %d:%02d%s", $hours,
2108             $zstatus->minutes());
2109             } else {
2110 0 0       0 $right_chunk = sprintf("Time: %d:%02d%s",
    0          
2111             ($hours > 12 ? $hours - 12 : $hours),
2112             $zstatus->minutes(),
2113             ($hours < 12 ? "am" : "pm"));
2114             }
2115             } else {
2116 0         0 $right_chunk = sprintf "Score:%d Moves:%d", $zstatus->score(), $zstatus->moves();
2117             }
2118            
2119 0 0       0 if ($zio->manual_status_line()) {
2120             # the ZIO wants to handle it
2121 0         0 $zio->status_hook($zstatus->location(), $right_chunk);
2122             } else {
2123             # "generic" status line handling; broken for screen-splitting v3 games
2124 0         0 my $restore = $zio->get_position(1);
2125 0         0 $zio->status_hook(0);
2126 0         0 $zio->write_string((" " x $columns), 0, 0);
2127             # erase
2128 0         0 $zio->write_string($zstatus->location(), 0, 0);
2129            
2130 0         0 $zio->write_string($right_chunk, $columns - length($right_chunk), 0);
2131 0         0 $zio->status_hook(1);
2132 0         0 &$restore();
2133             }
2134             }
2135              
2136             sub print_paddr {
2137             # print the string at the packed address given.
2138             # arg: address
2139 4     4 0 19 write_zchunk(scalar $ztext->decode_text(convert_packed_address($_[0], PRINT_PADDR)));
2140             }
2141              
2142             sub print_addr {
2143             # print the string at the address given; address is not packed
2144             # example: hollywood hijinx: "n", "knock"
2145 0     0 0 0 write_zchunk($ztext->decode_text($_[0]));
2146             }
2147              
2148             sub remove_object {
2149             # remove an object from its parent
2150 0     0 0 0 my ($object) = @_;
2151 0 0       0 if (my $zobj = get_zobject($object)) {
2152             # beware object 0
2153 0         0 $zobj->remove();
2154 0 0       0 if ($tailing) {
2155 0 0       0 if ($tailing == $zobj->object_id()) {
2156 0         0 write_text(sprintf "You can no longer tail %s.", ${$zobj->print});
  0         0  
2157 0         0 newline();
2158 0         0 $tailing = 0;
2159             }
2160             }
2161             }
2162             }
2163              
2164             sub get_property_addr {
2165 12     12 0 22 my ($object, $property) = @_;
2166             # store data address for given property of given object.
2167             # If property doesn't exist, store zero.
2168 12 50       26 if (my $zobj = get_zobject($object)) {
2169 12         301 my $zprop = $zobj->get_property($property);
2170 12 50       355 if ($zprop->property_exists()) {
2171 12         291 my $addr = $zprop->get_data_address();
2172             # printf STDERR "get_prop_addr for %s/%s=%s\n", $object, $property, $addr;
2173 12         292 store_result($addr);
2174             } else {
2175 0         0 store_result(0);
2176             }
2177             } else {
2178             # object 0
2179 0         0 store_result(0);
2180             }
2181             }
2182              
2183             sub test_flags {
2184             # jump if all flags in bitmap are set
2185 43     43 0 76 my ($bitmap, $flags) = @_;
2186 43         1073 conditional_jump(($bitmap & $flags) == $flags);
2187             }
2188              
2189             sub get_next_property {
2190 0     0 0 0 my ($object, $property) = @_;
2191             # return property number of the next property provided by
2192             # the given object's given property. With argument 0,
2193             # load property number of first property provided by that object.
2194             # example: zork 2 start, "get all"
2195              
2196 0         0 my $zobj = get_zobject($object);
2197              
2198 0         0 my $result = 0;
2199 0 0       0 if ($zobj) {
2200             # look out for object 0
2201 0 0       0 if ($property == 0) {
2202             # sect15.html#get_next_prop:
2203             # if called with zero, it gives the first property number present.
2204 0         0 my $zp = $zobj->get_property(Games::Rezrov::ZProperty::FIRST_PROPERTY);
2205 0         0 $result = $zp->property_number();
2206             } else {
2207 0         0 my $zp = $zobj->get_property($property);
2208 0 0       0 if ($zp->property_exists()) {
2209 0         0 $result = $zp->get_next()->property_number();
2210             } else {
2211 0         0 die("attempt to get next after bogus property");
2212             }
2213             }
2214             }
2215 0         0 store_result($result);
2216             }
2217              
2218              
2219             sub scan_table {
2220             # args: search, table, len [form]
2221             # Is "search" one of the entries in "table", which is "num_entries" entries
2222             # long? So return the address where it first occurs and branch. If not,
2223             # return 0 and don't. May be byte/word entries.
2224 0     0 0 0 my ($search, $table, $num_entries, $form) = @_;
2225 0         0 my ($entry_len, $check_len);
2226 0 0       0 if (defined $form) {
2227             # write_text("[custom form, check me!]");
2228             # newline();
2229            
2230 0         0 $entry_len = $form & 0x7f;
2231             # length of each entry in the table
2232 0 0       0 $check_len = ($form & 0x80) > 0 ? 2 : 1;
2233             # how many of the first bytes in each entry to check
2234             } else {
2235 0         0 $check_len = $entry_len = 2;
2236             }
2237 0         0 my ($addr, $value, $entry_count);
2238 0         0 my $found = 0;
2239 0         0 for ($addr = $table, $entry_count = 0;
2240             $entry_count < $num_entries;
2241             $entry_count++, $addr += $entry_len) {
2242 0 0       0 $value = ($check_len == 1) ?
2243             get_byte_at($addr) : get_word_at($addr);
2244             # yeah, yeah, it'd be more efficient to have a separate
2245             # loop, one for byte and one for word...
2246 0 0       0 $found = 1, last if ($value == $search);
2247             }
2248            
2249 0 0       0 store_result($found ? $addr : 0);
2250 0         0 conditional_jump($found);
2251             }
2252              
2253             sub set_window {
2254 3     3 0 9 my ($window) = @_;
2255             # print STDERR "set_window $window\n";
2256 3         10 flush();
2257 3         6 my $zio = screen_zio();
2258              
2259 3         17 $window_cursors->[$current_window] = $zio->get_position(1);
2260             # save callback to restore cursor position when we return to
2261             # this window
2262            
2263 3         11 $current_window = $window;
2264             # set current window
2265              
2266 3 50       11 if ($version >= 4) {
2267 0 0       0 if ($current_window == Games::Rezrov::ZConst::UPPER_WIN) {
2268             # 8.7.2: whenever upper window selected, cursor goes to top left
2269 0         0 set_cursor(1,1);
2270             } else {
2271             # restore old cursor position
2272 0         0 my $restore;
2273 0 0       0 if (defined $window_cursors->[$current_window]) {
2274             # restore former cursor position
2275 0         0 &{$window_cursors->[$current_window]};
  0         0  
2276             } else {
2277             # first switch to window
2278 0         0 $zio->absolute_move(0, $rows - 1);
2279             # 8.7.2.2: in v4 lower window cursor is always on last line.
2280             }
2281             }
2282             } else {
2283             # in v3, cursor always in lower left
2284 3         64 $zio->absolute_move(0, $rows - 1);
2285             }
2286 3         25 $zio->set_window($window);
2287             # for any local housekeeping
2288 3         12 $zio->set_text_style(font_mask());
2289             # since we always print in fixed font in the upper window,
2290             # make sure the zio gets a chance to turn this on/off as we enter/leave;
2291             # example: photopia.
2292             }
2293              
2294             sub set_cursor {
2295 1     1 0 4 my ($line, $column, $win) = @_;
2296 1         3 my $zio = screen_zio();
2297 1 50       5 $zio->fatal_error("set_cursor on window $win not supported") if $win;
2298              
2299 1         3 $line--;
2300 1         2 $column--;
2301             # given starting at 1, not 0
2302            
2303             # print STDERR "set_cursor\n";
2304 1 50       5 if ($current_window == Games::Rezrov::ZConst::UPPER_WIN) {
2305             # upper window: use offsets as specified
2306 0         0 $zio->absolute_move($column, $line);
2307             } else {
2308             # lower window: map coordinates given upper window size
2309 1         6 $zio->absolute_move($column, $line + $upper_lines);
2310             }
2311             }
2312              
2313              
2314             sub write_zchar {
2315             #
2316             # write a decoded z-char to selected output streams.
2317             #
2318 29 50   29 0 75 return if $_[0] == 0;
2319             # 3.8.2.1: "null" has no effect on any output stream
2320              
2321 29 50 33     222 if (($_[0] >= 179 and $_[0] <= 218) or
      66        
      33        
2322             ($_[0] >= 0x18 and $_[0] <= 0x1b)) {
2323             # sect16.html; convert IBM PC graphics codes
2324 0   0     0 my $trans = $Z_TRANSLATIONS{$_[0]} || "*";
2325             # print STDERR "trans for $_[0] => $trans\n";
2326 0 0       0 if (length $trans == 1) {
2327 0         0 $_[0] = ord($trans);
2328             } else {
2329 0         0 write_zchunk(\$trans);
2330 0         0 return;
2331             }
2332             }
2333            
2334 29 50       65 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_REDIRECT]) {
2335             #
2336             # 7.1.2.2: when active, no other streams get output
2337             #
2338 0         0 my $stack = $zios->[Games::Rezrov::ZConst::STREAM_REDIRECT];
2339             # printf STDERR "redirected: %s (%d)\n", chr($_[0]), $_[0];
2340 0         0 $stack->[$#$stack]->write_zchar($_[0]);
2341             } else {
2342             #
2343             # all the other streams
2344             #
2345 29 50       67 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_SCREEN]) {
2346             #
2347             # screen
2348             #
2349 29 50       65 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_SCREEN]) {
2350 29 50 33     82 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_STEAL] and
2351             $current_window == Games::Rezrov::ZConst::LOWER_WIN) {
2352             # temporarily steal lower window output
2353 0         0 $zios->[Games::Rezrov::ZConst::STREAM_STEAL]->buffer_zchar($_[0]);
2354             } else {
2355 29         47 my $zio = $zios->[Games::Rezrov::ZConst::STREAM_SCREEN];
2356            
2357 29 100 66     188 if ($buffering and $current_window != Games::Rezrov::ZConst::UPPER_WIN) {
2358             # 8.7.2.5: buffering never active in upper window (v. 3-5)
2359 28 100       58 if ($_[0] == Games::Rezrov::ZConst::Z_NEWLINE) {
2360 16         49 flush();
2361 16         28 $prompt_buffer = "";
2362 16         61 $zio->newline();
2363             } else {
2364 12         67 $zio->buffer_zchar($_[0]);
2365             }
2366             } else {
2367             # buffering off, or upper window
2368 1 50       12 if ($_[0] == Games::Rezrov::ZConst::Z_NEWLINE) {
2369 1         4 $prompt_buffer = "";
2370 1         6 $zio->newline();
2371             } else {
2372 0         0 $zio->write_zchar($_[0]);
2373             }
2374             }
2375             }
2376             }
2377             }
2378              
2379 29 50 33     769 if ($selected_streams->[Games::Rezrov::ZConst::STREAM_TRANSCRIPT] and
2380             $current_window == Games::Rezrov::ZConst::LOWER_WIN) {
2381             #
2382             # Game transcript
2383             #
2384 0         0 my $fh = $zios->[Games::Rezrov::ZConst::STREAM_TRANSCRIPT];
2385 0 0 0     0 print $fh (($_[0] || 0) == Games::Rezrov::ZConst::Z_NEWLINE) ? ($\ || "\n") : chr($_[0]);
      0        
2386             }
2387             }
2388             }
2389              
2390             sub screen_zio {
2391             # get the ZIO for the screen
2392 44     44 0 239 return $zios->[Games::Rezrov::ZConst::STREAM_SCREEN];
2393             }
2394              
2395             sub restore {
2396             # restore game
2397 0   0 0 0 0 my $filename = filename_prompt("-default" => $last_savefile || "",
2398             "-ext" => "sav",
2399             );
2400 0         0 my $success = 0;
2401 0 0       0 if ($filename) {
2402 0         0 $last_savefile = $filename;
2403 0         0 $success = $quetzal->restore($filename);
2404 0 0 0     0 if (!$success and $quetzal->error_message()) {
2405 0         0 write_text($quetzal->error_message());
2406 0         0 newline();
2407             }
2408             }
2409              
2410 0         0 reset_cheats();
2411              
2412             # $last_score = get_global_var(GV_SCORE);
2413             # for NOTIFY emulation not to get confused after restore
2414              
2415 0 0       0 if ($version <= 3) {
    0          
2416 0         0 conditional_jump($success);
2417             } elsif ($version == 4) {
2418             # sect15.html#save
2419 0 0       0 store_result($success ? 2 : 0);
2420             } else {
2421 0         0 store_result($success);
2422             }
2423             }
2424              
2425             sub filename_prompt {
2426             # my ($self, $prompt, $exist_check, $snide) = @_;
2427 0     0 0 0 my (%options) = @_;
2428            
2429 0   0     0 my $ext = $options{"-ext"} || die;
2430 0         0 my $default;
2431 0 0       0 unless ($default = $options{"-default"}) {
2432 0         0 ($default = $game_filename) =~ s/\..*//;
2433 0         0 $default .= ".$ext";
2434             }
2435              
2436 0         0 my $zio = screen_zio();
2437 0         0 my $prompt = sprintf "Filename [%s]: ", $default;
2438 0         0 $zio->write_string(sprintf "Filename [%s]: ", $default);
2439 0         0 $prompt_buffer = $prompt;
2440             # write_text(sprintf "Filename [%s]: ", $default);
2441 0   0     0 my $filename = $zio->get_input(50, 0) || $default;
2442 0 0       0 if ($filename) {
2443 0 0 0     0 if ($options{"-check"} and -f $filename) {
2444 0         0 $zio->write_string($filename . " exists, overwrite? [y/n]: ");
2445 0         0 $zio->update();
2446 0         0 my $proceed = $zio->get_input(1, 1);
2447 0 0       0 if ($proceed =~ /y/i) {
2448 0         0 write_text("Yes.");
2449 0         0 unlink($filename);
2450             } else {
2451 0         0 write_text("No.");
2452 0         0 $filename = "";
2453             }
2454 0         0 newline();
2455             }
2456             }
2457            
2458 0         0 return $filename;
2459             }
2460              
2461             sub save {
2462             # save game
2463 0     0 0 0 my $filename = filename_prompt("-ext" => "sav",
2464             "-check" => 1);
2465 0         0 my $success = 0;
2466 0 0       0 if ($filename) {
2467 0         0 $last_savefile = $filename;
2468             # $success = $q->save($filename, "-umem" => 1);
2469 0         0 $success = $quetzal->save($filename);
2470 0 0 0     0 if (!$success and $quetzal->error_message()) {
2471 0         0 write_text($quetzal->error_message());
2472 0         0 newline();
2473             }
2474             }
2475              
2476 0 0       0 if ($version <= 3) {
2477 0         0 conditional_jump($success);
2478             } else {
2479             # v4 +
2480 0         0 store_result($success);
2481             }
2482             }
2483              
2484             sub set_game_state {
2485             # called from Quetzal restore routines
2486 0     0 0 0 my ($stack, $pc) = @_;
2487 0         0 $call_stack = $stack;
2488 0         0 $current_frame = $stack->[$#$stack];
2489 0         0 $Games::Rezrov::StoryFile::PC = $pc;
2490             }
2491              
2492             sub snide_message {
2493 0     0 0 0 my @messages = ("Fine, be that way.",
2494             "Eh? Speak up!",
2495             "What?",
2496             );
2497 0         0 return $messages[int(rand(scalar @messages))];
2498             }
2499              
2500             sub save_undo {
2501             # v5+, save to RAM
2502             # BROKEN
2503 0     0 0 0 if (0) {
2504             my $undo_data = $quetzal->save("", "-undo" => 1);
2505             # print "saved $undo_data\n";
2506             $undo_slots = [ $undo_data ];
2507             store_result(1);
2508             } else {
2509             # not supported
2510 0         0 store_result(-1);
2511             }
2512             }
2513              
2514             sub restore_undo {
2515             # v5+, restore from RAM
2516             # BROKEN
2517 0     0 0 0 if (0) {
2518             # print "restoring " . $undo_slots->[0] . "\n";
2519             my $status = @{$undo_slots} ? $quetzal->restore("", pop @{$undo_slots}) : 0;
2520             store_result($status);
2521             } else {
2522 0         0 store_result(0);
2523             }
2524             }
2525              
2526             sub check_arg_count {
2527             # sect15.html#check_arg_count
2528             # branch if the given argument number has been provided by the routine
2529             # call to the current routine
2530 0     0 0 0 conditional_jump(frame_argc() >= $_[0]);
2531             }
2532              
2533             sub DESTROY {
2534             # must be defined so our AUTOLOAD won't catch destructor and complain
2535 1     1   0 1;
2536             }
2537              
2538             sub suppress_hack {
2539             # used when we're pulling a fast one with the parser,
2540             # intercepting user input. Suppress the game's output (usually
2541             # complaints about unknown vocabulary), restoring i/o and
2542             # printing the prompt (which is everything after the last
2543             # Games::Rezrov::ZConst::Z_NEWLINE) during the read_line() opcode.
2544             # cluck "suppress_hack\n";
2545 0     0 0 0 output_stream(Games::Rezrov::ZConst::STREAM_STEAL);
2546             }
2547              
2548             sub print_table {
2549             # print a "window" of text onscreen. Given text and width,
2550             # decode characters, moving down a line every "width" characters
2551             # to the same column (x position) where the table started.
2552             #
2553             # example: "sherlock", start game and enter "knock"
2554 0     0 0 0 my ($text, $width, $height, $skip) = @_;
2555 0 0       0 $height = 1 unless defined $height;
2556 0 0       0 $skip = 0 unless defined $skip;
2557 0         0 my $zio = screen_zio();
2558 0         0 my ($i, $j);
2559 0         0 my ($x, $y) = $zio->get_position();
2560              
2561             # printf STDERR "print_table: %s w:%d h:%d sk:%d\n", get_string_at($text, $width * $height), $width, $height, $skip;
2562              
2563 0         0 flush();
2564              
2565 0         0 my $char;
2566 0         0 for (my $i=0; $i < $height; $i++) {
2567 0         0 for(my $j=0; $j < $width; $j++) {
2568             # printf STDERR "pt: %d (%s)\n", get_byte_at($text), chr(get_byte_at($text));
2569 0         0 $char = get_byte_at($text++);
2570             # if ($char == Games::Rezrov::ZConst::Z_NEWLINE) {
2571             # die "hey now";
2572             # }
2573 0         0 write_zchar($char);
2574             }
2575 0         0 flush();
2576             # flush buffered text before moving to next line
2577 0 0       0 if ($skip) {
2578             # optionally skip specified number of chars between lines
2579 0         0 untested();
2580 0         0 $text += $skip;
2581             }
2582 0 0       0 if ($height > 1) {
2583 0         0 $zio->absolute_move($x, ++$y);
2584             # fix me: what if this goes out of bounds of the current window?
2585             }
2586             }
2587             }
2588              
2589             sub set_font {
2590 0     0 0 0 flush();
2591 0 0 0     0 if ($_[0] == 3 and $font_3_disabled) {
2592             # game wants font 3 but user has disabled it.
2593             # Don't even inform the ZIO.
2594 0         0 store_result(0);
2595             } else {
2596 0         0 store_result(screen_zio()->set_font($_[0]));
2597             }
2598             }
2599              
2600             sub set_color {
2601 0     0 0 0 my ($fg, $bg, $win) = @_;
2602 0 0       0 die sprintf("v6; fix me! %s", join ",", @_) if defined $win;
2603 0         0 my $zio = screen_zio();
2604 0         0 flush();
2605 0 0       0 if ($zio->can_use_color()) {
2606 0         0 foreach ([ $fg, 'fg' ],
2607             [ $bg, 'bg' ]) {
2608 0         0 my ($color_code, $method) = @{$_};
  0         0  
2609 0 0       0 if ($color_code == Games::Rezrov::ZConst::COLOR_CURRENT) {
    0          
    0          
2610             # nop?
2611 0         0 print STDERR "set color to current; huh?\n";
2612             } elsif ($color_code == Games::Rezrov::ZConst::COLOR_DEFAULT) {
2613 0         0 my $m2 = 'default_' . $method;
2614 0         0 $zio->$method($zio->$m2());
2615             } elsif (my $name = Games::Rezrov::ZConst::color_code_to_name($color_code)) {
2616 0         0 $zio->$method($name);
2617             # printf STDERR "set %s to %s\n", $method, $name;
2618             } else {
2619 0         0 die "set_color(): eek, " . $color_code;
2620             }
2621             }
2622 0         0 $zio->color_change_notify();
2623             }
2624             }
2625            
2626             sub fatal_error {
2627 0     0 0 0 my $zio = screen_zio();
2628 0         0 $zio->newline();
2629 0         0 $zio->fatal_error($_[0]);
2630             }
2631              
2632             sub split_window {
2633 1     1 0 2 my ($lines) = @_;
2634 1         4 my $zio = screen_zio();
2635              
2636 1         4 $upper_lines = $lines;
2637 1         3 $lower_lines = $rows - $lines;
2638             # print STDERR "split_window to $lines, ll=$lower_lines ul=$upper_lines\n";
2639              
2640 1         9 my ($x, $y) = $zio->get_position();
2641 1 50       5 if ($y < $upper_lines) {
2642             # 8.7.2.2
2643 0         0 $zio->absolute_move($x, $upper_lines);
2644             }
2645 1         3 screen_zio()->split_window($lines);
2646             # any local housekeeping
2647             }
2648              
2649             sub play_sound_effect {
2650             # hmm, should we pass this through?
2651 0     0 0 0 screen_zio()->play_sound_effect(@_);
2652             }
2653              
2654             sub input_stream {
2655 1     1 0 10 my ($stream, $filename) = @_;
2656             # $filename is an extension (only used internally)
2657 1         2 $current_input_stream = $stream;
2658 1 50       12 if ($stream == Games::Rezrov::ZConst::INPUT_FILE) {
    0          
2659 1   33     5 my $fn = $filename || filename_prompt("-ext" => "cmd");
2660             # filename provided if playing back from command line
2661 1         3 my $ok = 0;
2662 1 50       5 if ($fn) {
2663 1 50       8 if ($fn =~ /^\*main:/) {
    0          
2664             # hack for test.pl
2665 1         2 $ok = 1;
2666 1         3 $input_filehandle = $fn;
2667             } elsif (open(TRANS_IN, $fn)) {
2668 0         0 $ok = 1;
2669 0         0 $input_filehandle = \*TRANS_IN;
2670 0 0       0 write_text("Playing back commands from $fn...") unless defined $filename;
2671             # if name provided, don't print this message
2672             } else {
2673 0         0 write_text("Can't open \"$fn\" for playback: $!");
2674             }
2675 1         5 newline();
2676             }
2677 1 50       6 $current_input_stream = Games::Rezrov::ZConst::INPUT_KEYBOARD unless $ok;
2678             } elsif ($stream eq Games::Rezrov::ZConst::INPUT_KEYBOARD) {
2679 0         0 close TRANS_IN;
2680             } else {
2681 0         0 die;
2682             }
2683             }
2684              
2685             sub set_buffering {
2686             # whether text buffering is active
2687             # printf STDERR "set_buffering: $_[0]\n";
2688 1     1 0 3 $buffering = $_[0] == 1;
2689             }
2690              
2691             # font_mask() or font_mask(newmask)
2692             # specifying newmask replaces the current font mask
2693             # In either case, the returned mask is fudged a bit (for example, STYLE_FIXED is coerced if we're the upper window).
2694              
2695             sub font_mask {
2696 17 100   17 0 43 $fm = $_[0] if defined $_[0];
2697 17   100     65 my $fm2 = $fm || 0;
2698 17 50       44 $fm2 |= Games::Rezrov::ZConst::STYLE_FIXED
2699             if $current_window == Games::Rezrov::ZConst::UPPER_WIN;
2700             # 8.7.2.4:
2701             # An interpreter should use a fixed-pitch font when printing on the
2702             # upper window.
2703              
2704 17         22 if (0 and $header and $header->fixed_font_forced()) {
2705             # 8.1: game forcing use of fixed-width font
2706             # DISABLED: something seems to be wrong here...
2707             # photopia (all v5 games?) turn on this bit after 1 move?
2708             $fm2 |= Games::Rezrov::ZConst::STYLE_FIXED;
2709             }
2710              
2711 17         50 return $fm2;
2712             }
2713              
2714             sub set_text_style {
2715             # sets the specified style bits on the font, *unless* the value
2716             # STYLE_ROMAN is specified in which case all style bits are cleared.
2717 4     4 0 7 my $text_style = $_[0];
2718 4         37 flush();
2719 4         10 my $mask = font_mask();
2720 4 100       51 if ($text_style == Games::Rezrov::ZConst::STYLE_ROMAN) {
2721             # turn off all
2722 2         5 $mask = 0;
2723             } else {
2724 2         3 $mask |= $text_style;
2725             }
2726 4         10 $mask = font_mask($mask);
2727             # might be modified for upper window
2728            
2729 4         13 screen_zio()->set_text_style($mask);
2730 4         11 return $mask;
2731             }
2732              
2733             sub toggle_text_style {
2734             # toggle the specified style bits on the font. Rather pointless for
2735             # STYLE_ROMAN. Little more than XOR.
2736 2     2 0 5 my $text_style = $_[0];
2737 2         6 flush();
2738 2         71 set_text_style( font_mask( font_mask() ^ $text_style ) );
2739             }
2740              
2741             # 'swap' in the specified style bits.
2742             # Returns the bits that were actually changed.
2743             #
2744             # The idea is to be able to do this:
2745             # $tmp = swap_text_style( STYLE_FIXED );
2746             # print_fixed_width_text();
2747             # toggle_text_style($tmp);
2748             #
2749             # and not worry about whether or not the relevant style bit was already set
2750             # (if it was, the bit will be 0 in the return and so the toggle won't undo it)
2751             sub swap_text_style {
2752 2     2 0 9 my $old = font_mask();
2753 2         9 return $old ^ set_text_style(@_);
2754             }
2755              
2756             sub register_newline {
2757             # called by the ZIO whenever a newline is printed.
2758 0 0 0 0 0 0 return unless ($wrote_something and
      0        
      0        
2759             # don't count newlines that occur before any text;
2760             # example: start of "plundered hearts", after initial RETURN
2761             defined($current_window) and
2762             $lower_lines and
2763             $current_window == Games::Rezrov::ZConst::LOWER_WIN);
2764 0         0 my $wrote = $lines_wrote + 1;
2765              
2766             # printf STDERR "rn: %d/%d\n", $wrote, $lower_lines;
2767            
2768 0 0       0 if ($wrote >= ($lower_lines - 1)) {
2769             # need to pause; show prompt.
2770             # print STDERR "pausing...\n";
2771 0         0 my $zio = screen_zio();
2772 0         0 my $restore = $zio->get_position(1);
2773            
2774 0         0 set_cursor($lower_lines, 1);
2775 0         0 my $more_prompt = "[MORE]";
2776             # my $old = font_mask();
2777             # set_text_style(Games::Rezrov::ZConst::STYLE_REVERSE);
2778 0         0 my $old = swap_text_style(Games::Rezrov::ZConst::STYLE_REVERSE);
2779 0         0 $zio->write_string($more_prompt);
2780             # set_text_style(Games::Rezrov::ZConst::STYLE_ROMAN);
2781             # font_mask($old);
2782 0         0 toggle_text_style($old);
2783 0         0 $zio->update();
2784 0         0 $zio->get_input(1,1);
2785 0         0 set_cursor($lower_lines, 1);
2786 0         0 $zio->clear_to_eol();
2787              
2788             # $zio->erase_line($lower_lines);
2789             # $zio->erase_line($lower_lines - 1);
2790 0         0 $wrote = 0;
2791 0         0 &$restore();
2792             # restore old position
2793             }
2794 0         0 $lines_wrote = $wrote;
2795             }
2796              
2797             sub flush {
2798             # flush and format the characters buffered by the ZIO
2799 35 100   35 0 90 return if $flushing;
2800             # can happen w/combinations of attributes and pausing
2801             # cluck "flush";
2802 29         33 my $len;
2803 29         78 my $zio = screen_zio();
2804 29         128 my $buffer = $zio->get_buffer();
2805             # printf STDERR "flush: buffer= ->%s<-\n", $buffer;
2806 29         115 $zio->reset_buffer();
2807 29 100       84 return unless length $buffer;
2808             # print "fs\n";
2809 20         29 $flushing = 1;
2810 20         26 $wrote_something = 1;
2811              
2812 20         33 my $speech_buffer = $buffer;
2813             # save unmodified version for speech interface
2814              
2815 20 100 33     72 if (Games::Rezrov::ZOptions::BEAUTIFY_LOCATIONS() and
    50 66        
2816             $version < 4 and
2817             likely_location(\$buffer)) {
2818             # set_text_style(Games::Rezrov::ZConst::STYLE_BOLD);
2819 2         10 my $old = swap_text_style(Games::Rezrov::ZConst::STYLE_BOLD);
2820 2         14 $zio->write_string($buffer);
2821             # FIX ME: this might wrap; eg Tk, "Zork III: The Dungeon Master"
2822             # set_text_style(Games::Rezrov::ZConst::STYLE_ROMAN);
2823 2         10 toggle_text_style($old);
2824             } elsif (length $buffer) {
2825 18         26 $wrote_something = 1;
2826 18         27 my ($i, $have_left);
2827 18 50       106 if ($current_window != Games::Rezrov::ZConst::LOWER_WIN) {
    50          
2828             # buffering in upper window: nonstandard hack in effect.
2829             # assume we know what we're doing :)
2830             # print STDERR "hack! \"$buffer\"\n";
2831 0         0 $zio->write_string($buffer);
2832             } elsif (!$zio->fixed_font_default()) {
2833             #
2834             # Variable font; graphical wrapping
2835             #
2836 0         0 my ($x, $y) = $zio->get_pixel_position();
2837 0         0 my $total_width = ($zio->get_pixel_geometry())[0];
2838 0         0 my $pixels_left = $total_width - $x;
2839 0         0 my $plen;
2840 0         0 while ($len = length($buffer)) {
2841 0         0 $plen = $zio->string_width($buffer);
2842 0 0       0 if ($plen < $pixels_left) {
2843             # it'll fit; we're done
2844             # print STDERR "fits: $buffer\n";
2845 0         0 $zio->write_string($buffer);
2846 0         0 last;
2847             } else {
2848 0         0 my $wrapped = 0;
2849 0         0 my $i = int(length($buffer) * ($pixels_left / $plen));
2850             # print STDERR "pl=$pixels_left, plen=$plen i=$i\n";
2851 0         0 while (substr($buffer,$i,1) ne " ") {
2852             # move ahead to a word boundary
2853             # print STDERR "boundarizing\n";
2854 0 0       0 last if ++$i >= $len;
2855             }
2856              
2857 0         0 while (1) {
2858 0         0 $plen = $zio->string_width(substr($buffer,0,$i));
2859             # printf STDERR "%s = %s\n", substr($buffer,0,$i), $plen;
2860 0 0       0 if ($plen < $pixels_left) {
2861             # it'll fit
2862 0         0 $zio->write_string(substr($buffer,0,$i));
2863 0         0 $zio->newline();
2864 0         0 $buffer = substr($buffer, $i + 1);
2865 0         0 $wrapped = 1;
2866 0         0 last;
2867             } else {
2868             # retreat back a word
2869 0   0     0 while (--$i >= 0 and substr($buffer,$i,1) ne " ") { }
2870 0 0       0 last if ($i < 0);
2871             }
2872             }
2873              
2874 0 0       0 $zio->newline() unless ($wrapped);
2875             # if couldn't wrap at all on this line
2876 0         0 $pixels_left = $total_width;
2877             }
2878             }
2879             } else {
2880             #
2881             # Fixed font; do line/column wrapping
2882             #
2883 18         87 my ($x, $y) = $zio->get_position();
2884 18         27 $have_left = ($columns - $x);
2885             # Get start column position; we can't be sure we're starting at
2886             # column 0. This is an issue when flush() is called when changing
2887             # attributes. Example: "bureaucracy" intro paragraphs ("But
2888             # Happitec is going to be _much_ more fun...")
2889 18         48 while ($len = length($buffer)) {
2890 22 100       118 if ($len < $have_left) {
2891 18         71 $zio->write_string($buffer);
2892 18         36 last;
2893             } else {
2894             # printf STDERR "wrapping: %d, %d, %s x:$x y:$y col:$columns\n", length $buffer, $have_left, $buffer;
2895 4         7 my $wrapped = 0;
2896 4         13 for ($i = $have_left - 1; $i > 0; $i--) {
2897 20 100       57 if (substr($buffer, $i, 1) eq " ") {
2898 4         23 $zio->write_string(substr($buffer, 0, $i));
2899 4         24 $zio->newline();
2900 4         5 $wrapped = 1;
2901 4         15 $buffer = substr($buffer, $i + 1);
2902 4         10 last;
2903             }
2904             }
2905 4 50       13 $zio->newline() unless $wrapped;
2906             # if couldn't wrap at all
2907 4         13 $have_left = $columns;
2908             }
2909             }
2910             }
2911 18         41 $prompt_buffer = $buffer;
2912             # FIX ME
2913             }
2914              
2915 20 50       699 $zio->speak($speech_buffer) if $zio->speaking;
2916            
2917 20         55 $flushing = 0;
2918             }
2919            
2920             sub likely_location {
2921             #
2922             # STATIC: is the given string likely the name of a location?
2923             #
2924             # An earlier approach saved the buffer position before and after
2925             # StoryFile::object_print() opcode, and considered a string a
2926             # location if and only if the buffer was flushed with only an object
2927             # string in the buffer. Unfortunately this doesn't always work:
2928             #
2929             # Suspect: "Ballroom, Near Fireplace", where "Near Fireplace"
2930             # is an object, but Ballroom is not.
2931             #
2932             # It's not enough to check for all capitalized words:
2933             # Zork 1: "West of House"
2934             #
2935             # This approach "uglier" but works more often :)
2936 289     289 0 649 my $ref = shift;
2937 289         761 my $len = length $$ref;
2938 289 100 66     1454 if ($len and $len < 50) {
2939             # length?
2940 281         393 my $buffer = $$ref;
2941              
2942 281 100       5768 return 0 unless $buffer =~ /^[A-Z]/;
2943             # must start uppercased
2944              
2945 79 100       298 return 0 if $buffer =~ /\W$/;
2946             # can't end with a non-alphanum:
2947             # minizork.z3:
2948             # >i
2949             # You have: <---------
2950             # A leaflet
2951              
2952 75 50       210 return 0 if $buffer =~ /^\w - /;
2953             # sampler1_r55.z3:
2954             # T - The Tutorial
2955              
2956 75 50       231 unless ($buffer =~ /[a-z]/) {
2957             # if all uppercase...
2958 0 0       0 return 0 if $buffer =~ /[^\w ]/;
2959             # ...be extra strict about non-alphanumeric characters
2960             #
2961             # allowed: ENCHANTER
2962             # HOLLYWOOD HIJINX
2963             # but not:
2964             # ROBOT, GO NORTH (sampler, Planetfall)
2965             }
2966              
2967 75 50       281 if ($buffer =~ /\s[a-z]+$/) {
2968             # Can't end with a lowercase word;
2969             # Enchanter: "Flathead portrait"
2970 0         0 return 0;
2971             }
2972              
2973 75 50       201 return 0 if $buffer =~ /\s[a-z]\S{2,}\s+[a-z]\S{2,}/;
2974             # don't allow more than one "significant" lowercase-starting
2975             # word in a row.
2976             #
2977             # example: graffiti in Planetfall's brig:
2978             #
2979             # There once was a krip, name of Blather <--
2980             # Who told a young ensign named Smather <-- this is not caught here!
2981             # "I'll make you inherit
2982             # A trotting demerit <--
2983             # And ship you off to those stinking fawg-infested tar-pools of Krather".
2984             #
2985             # However, we must allow:
2986             #
2987             # Land of the Dead [Zork I]
2988             # Room in a Puzzle [Zork III]
2989              
2990 75 50       206 if ($buffer =~ /\s([a-z]\S*\s+){3,}/) {
2991             # in any case, don't allow 3 lowercase-starting words in a row.
2992             # back to the brig example:
2993             #
2994             # Who told a young ensign named Smather <-- we get this here
2995             # ^^^^^^^^^^^^^^^^^^^^^^^^^
2996 0         0 return 0;
2997             }
2998             # ( blech... )
2999              
3000 75 100       3049 return $buffer =~ /[^\w\s,:\'\-]/ ? 0 : 1;
3001             # - commas allowed: Cutthroats, "Your Room, on the bed"
3002             # - dashes allowed: Zork I, "North-South Passage"
3003             # - apostrophes allowed: Zork II, "Dragon's Lair"
3004             # - colons allowed (for game titles): "Zork III: ..."
3005             # - otherwise, everything except whitespace and alphanums verboten.
3006             } else {
3007 8         93 return 0;
3008             }
3009             }
3010              
3011             sub tokenize {
3012 0     0 0 0 my ($text, $parse, $dict, $flag) = @_;
3013              
3014 0         0 my $std_dictionary_addr = $header->dictionary_address();
3015 0         0 my $zd;
3016 0 0 0     0 if ($dict and $dict != $std_dictionary_addr) {
3017             # v5+; example: "beyond zork"
3018 0 0       0 unless ($zd = $alternate_dictionaries{$dict}) {
3019 0         0 $zd = $alternate_dictionaries{$dict} = new Games::Rezrov::ZDict($dict);
3020             }
3021             } else {
3022             # use default/standard dictionary
3023 0         0 $zd = $zdict;
3024             }
3025              
3026 0         0 $zd->tokenize_line($text,
3027             $parse,
3028             "-flag" => $flag,
3029             );
3030             # die join ",", @_;
3031             }
3032              
3033             sub get_zobject {
3034             # cache object requests; games seem to run about 5-10% faster,
3035             # the most gain seen in earlier games
3036 226     226 0 6480 return $object_cache->get($_[0]);
3037              
3038             # create every time; slow overhead
3039             # return new Games::Rezrov::ZObject($_[0]);
3040             }
3041              
3042             sub get_zobject_cache {
3043             # you don't see this
3044 4     4 0 15 return $object_cache;
3045             }
3046              
3047             sub rows {
3048 2 100   2 0 8 if (defined $_[0]) {
3049 1         3 $rows = $_[0];
3050 1 50       8 $header->set_rows($rows) if $header;
3051 1         5 reset_write_count();
3052 1 50       5 $lower_lines = $rows - $upper_lines if defined $upper_lines;
3053             }
3054 2         27 return $rows;
3055             }
3056              
3057             sub columns {
3058 2 100   2 0 6 if (defined $_[0]) {
3059             # ZIO notifies us of its columns
3060 1         3 $columns = $_[0];
3061 1 50       4 $header->set_columns($_[0]) if $header;
3062 1 50 33     10 display_status_line() if $version <= 3 and $zstatus;
3063             }
3064 2         28 return $columns;
3065             }
3066              
3067             sub reset_write_count {
3068 8     8 0 19 $lines_wrote = 0;
3069 8         48 $wrote_something = 0;
3070             }
3071              
3072             sub get_pc {
3073 4     4 0 23 return $Games::Rezrov::StoryFile::PC;
3074             }
3075              
3076             sub clear_screen {
3077 2     2 0 10 my $zio = screen_zio();
3078            
3079 2 50       20 if ($zio->can_use_color()) {
3080 0   0     0 my $fg = $zio->fg() || "";
3081 0   0     0 my $bg = $zio->bg() || "";
3082 0   0     0 my $dbg = $zio->default_bg() || "";
3083             # FIX ME!
3084            
3085             # printf STDERR "fg=%s/%s bg=%s/%s\n",$fg,$zio->default_fg, $bg, $zio->default_bg;
3086 0 0       0 if ($bg ne $dbg) {
3087             # the background color has changed; change the cursor color
3088             # to the current foreground color so we don't run the risk of it
3089             # "disappearing".
3090 0         0 $zio->cc($fg);
3091             }
3092 0         0 $zio->default_bg($bg);
3093 0         0 $zio->default_fg($fg);
3094 0         0 $zio->set_background_color();
3095             }
3096              
3097 2         13 $zio->clear_screen();
3098             }
3099              
3100             sub is_stream_selected {
3101 12     12 0 58 return $selected_streams->[$_[0]];
3102             }
3103              
3104             sub stream_dup {
3105 8     8 0 12 my ($stream, $string) = @_;
3106 8 50       16 if (is_stream_selected($stream)) {
3107 0         0 my $fh = $zios->[$stream];
3108 0         0 print $fh $string . $/;
3109             }
3110             }
3111              
3112             sub is_this_game {
3113             # do the given release number, serial number, and checksum
3114             # match those of this game?
3115 2     2 0 5 my ($release, $serial, $checksum) = @_;
3116             # printf "%s\n%s\n", join(",", @_), join ",", $header->release_number, $header->serial_code, $header->file_checksum;
3117 2   33     53 return ($header->release_number() eq $release and
3118             $header->serial_code() == $serial and
3119             $header->file_checksum() == $checksum);
3120             }
3121              
3122             sub get_global_var {
3123             # get the specified global variable
3124             # printf STDERR "get gv %d: %d\n", $_[0], get_word_at($global_variable_address + ($_[0] * 2));
3125 3     3 0 17 return get_word_at($global_variable_address + ($_[0] * 2));
3126             }
3127              
3128             sub routine_pop {
3129             # pop a variable from the routine stack.
3130 1115 50   1115 0 2295 if ($#$current_frame < FRAME_ROUTINE) {
3131 0         0 die "yikes: attempt to pop when no routine stack!\n";
3132             } else {
3133 1115         1206 return pop @{$current_frame};
  1115         31555  
3134             }
3135             }
3136              
3137             sub routine_push {
3138             # push a variable onto the routine stack
3139 1145     1145 0 1257 push @{$current_frame}, $_[0];
  1145         31049  
3140             }
3141              
3142             sub frame_argc {
3143 146 50   146 0 399 $current_frame->[FRAME_ARGC] = $_[0] if defined $_[0];
3144 146         3945 return $current_frame->[FRAME_ARGC];
3145             }
3146              
3147             sub frame_call_type {
3148 147 50   147 0 553 $current_frame->[FRAME_CALL_TYPE] = $_[0] if defined $_[0];
3149 147         237 return $current_frame->[FRAME_CALL_TYPE];
3150             }
3151              
3152             sub frame_return_pc {
3153 147 50   147 0 462 $current_frame->[FRAME_RPC] = $_[0] if defined $_[0];
3154 147         212 return $current_frame->[FRAME_RPC];
3155             }
3156              
3157             sub header {
3158 1258     1258 0 47580 return $header;
3159             }
3160              
3161             sub version {
3162 21     21 0 595 return $version;
3163             }
3164              
3165             sub game_title {
3166 1     1 0 33 return $game_title;
3167             }
3168              
3169             sub ztext {
3170 2     2 0 100 return $ztext;
3171             }
3172              
3173             sub prompt_buffer {
3174 0 0   0 0 0 $prompt_buffer = $_[0] if defined $_[0];
3175 0         0 return $prompt_buffer;
3176             }
3177              
3178             sub call_stack {
3179             # used by Quetzal
3180 4     4 0 41 return $call_stack;
3181             }
3182              
3183             sub player_object {
3184 70 50   70 0 178 $player_object = $_[0] if defined $_[0];
3185 70         272 return $player_object;
3186             }
3187              
3188             sub current_room {
3189 70 50   70 0 158 $current_room = $_[0] if defined $_[0];
3190 70         224 return $current_room;
3191             }
3192              
3193             sub push_command {
3194             # steal a turn from the player
3195 0     0 0 0 $push_command = shift;
3196             }
3197              
3198             sub last_input {
3199             # for "oops" emulation
3200 0     0 0 0 return $last_input;
3201             }
3202              
3203             sub full_version_output {
3204             # used by "help"
3205 0     0 0 0 return $full_version_output;
3206             }
3207              
3208             sub tail {
3209 0     0 0 0 $tailing = $_[0];
3210             }
3211              
3212             sub call_func {
3213             # call, "function" style (store result)
3214 152     152 0 4075 call(\@_, FRAME_FUNCTION);
3215             }
3216              
3217             sub call_proc {
3218             # call, "procedure" style (discard result)
3219 0     0 0   call(\@_, FRAME_PROCEDURE);
3220             }
3221              
3222             sub throw {
3223 0     0 0   unimplemented();
3224             }
3225              
3226             sub erase_line {
3227 0     0 0   untested();
3228 0 0         if ($_[0] == 1) {
3229             # if value not 1, do nothing
3230 0           screen_zio()->clear_to_eol();
3231             }
3232             }
3233              
3234             sub get_cursor {
3235             # put cursor coordinates at given offset
3236 0     0 0   untested();
3237 0           my ($x, $y) = screen_zio()->get_position();
3238 0           set_word_at($_[0], $y);
3239 0           set_word_at($_[1], $x);
3240             }
3241              
3242             sub untested {
3243 0     0 0   (my $subname = (caller(1))[3]) =~ s/.*://;
3244 0           printf STDERR "Untested opcode %s(); please email me if you see this!\n", $subname;
3245             }
3246              
3247             sub unimplemented {
3248 0     0 0   (my $subname = (caller(1))[3]) =~ s/.*://;
3249 0           fatal_error(sprintf 'opcode %s() unimplemented! Please email me.', $subname);
3250             }
3251              
3252              
3253             sub encode_text {
3254             # blech
3255 0     0 0   unimplemented();
3256             }
3257              
3258             sub nop {
3259 0     0 0   untested();
3260             }
3261              
3262             sub piracy {
3263             # sect15.html#piracy
3264 0     0 0   conditional_jump(1);
3265             }
3266              
3267             sub not_or_possibly_call {
3268             # :)
3269             # sect15.html#not
3270 0 0   0 0   if ($version < 5) {
3271 0           z_not(@_);
3272             } else {
3273 0           call_proc(@_);
3274             }
3275             }
3276              
3277             sub font_3_disabled {
3278 0 0   0 0   $font_3_disabled = $_[0] if defined($_[0]);
3279 0           return $font_3_disabled;
3280             }
3281              
3282             sub get_zdict {
3283 0     0 0   return $zdict;
3284             }
3285              
3286             1;