Phase 1 PLOTZ: Perpetrating Linguistics On The Z-machine. (aka Poly-Lingual Opcode Translation for the Z-machine) Perl program that takes a Z story file and creates a Perl (or other language!) program that'll run the game. Plotz is what your mother does when she finds out you're wasting your life playing computer games instead of becoming a doctor. And you couldn't call once in a while? Btw, Plotz is also a machine company in Cleveland. And it was used on raif in 1999 to describe that indescribable thing that IF games have that draws you in and keeps you playing even if there isn't an actual plot. Phase 2 TOPAZ: Translating Opcodes for Parrot Assembly Z-machine Translate Z-code into Parrot Assembly instructions (using maybe a mix of Z opcodes and Parrot opcodes). Then run parrot on the PASM. (This should be done with Plotz::PIR.) Phase 3 PIZMON: Parrot/Pizmon Is the Z-machine Operating Natively The point here is that Parrot itself will think it's the Z-machine, not that it's emulating the Z-machine. It'll be reading opcodes, pushing stack frames, and calling subs just like the Z-machine. (Although there is that pesky problem of the missing stack.) ###################################################################### Questions for Z-machine list: - Confirm non-popping of stack in indirect vars. What about *direct* reference of the stack? load 0 -> i; ! does stack get decremented? load [n] -> i; ! does stack get decremented if n == 0? load [sp] -> i; ! does stack get decremented? What if sp = 0? - Is it true that vars (& stack) are *always* stored as unsigned? I.e., signed only happens during computation (or inc_chk). Then spec1.0's "dec 0 = -1" is wrong. It's 65535. ###################################################################### IO Eventually, I should have Input.pm, Output.pm (which stores all the outputstreams stuff, too), and ZIO.pm or a ZIO directory. (Call it UI? Screen?) (Don't want to separate $zio's input and output since that's heavily UI-dependent. But ZIO_Generic will turn into PlotzPerl::UI, with its subclasses.) If we do a read while stream 3 is selected, we still echo the input text to stream 1 (and possibly 2 and 4) and not 3. So "echoed input text" is a different beast than usual output text. read_char is NOT echoed to stream 1 (or 2 or 3), but IS echoed to stream 4. Note that Games::Rezrov doesn't read_char from input_stream 1 or dup read_char's inputs to output_stream 4 OR 2. I should probably use the format "[ZSCII VALUE]\n" mentioned in the spec's remarks. I need to move cursor stuff partly out of ZIO. After all, the game is supposed to know where it's currently sitting. @buffer_mode needs to flush the buffer if it gets turned off. ### Win32 Console We get an @event array back when we do $in->Console (@event is empty if we do a mouse click & ENABLE_MOUSE_INPUT is off) # Event type (1 keyboard, 2 mouse), key down or up (1,0) # repeat count, virtual keycode, virtual scan code (?) # char (if ASCII, otherwise 0), control key state # num lock -> 1 1 1 144 69 0 256 # '0', num lock on -> 1 1 1 96 82 48 32 # '0', num lock off -> 1 1 1 38 72 0 288 # F1 -> 1 1 1 112 59 0 0 # Home -> 1 1 1 36 71 0 256 # End -> 1 1 1 35 79 0 256 # Left -> 1 1 1 37 75 0 256 # Up -> 1 1 1 38 72 0 256 # Right-> 1 1 1 39 77 0 256 # Down -> 1 1 1 40 80 0 256 # Alt -> 1 1 1 18 56 0 2 [ignore these: Alt-Tab = next window, e.g.] # Shift-> 1 1 1 16 42 0 16 [ignore these - get next char] # Ctrl -> 1 1 1 17 29 0 8 # a -> 1 1 1 65 30 97 0 # A -> 1 1 1 65 30 65 16 #### WINDOWS The upper window doesn't get buffered, and doesn't get passed to stream 2. Upper window input DOES get passed to stream 3 or 4. That means that we need to know what window we're writing to when we decide whether to buffer or not. So it seems I need to make the current window an attribute of Output class. Can we get rid of the ZIO current_window attribute? A few things use it but maybe we can pass that in from Output. Or maybe not. Seems like we'll need rows/columns attributes for Output (or at least the Bufferable streams) as well as $zio. Maybe we can make window be a parallel class to OutputStream, and they'll just call each other once in a while? A read CAN happen in the upper window. (read_char? I think so.) Version 6 allows each window to have a "echo to stream 2" attribute. I can do this even if I'm not supporting v6. I can either make Window the only class, and call new for Lower & Upper (and Status) windows with the particular attributes that fit for that window, OR I can make separate Lower/Upper/Status classes. Latter is slightly nicer for OO, but former makes more sense if we imagine that SOMEDAY someone might want to create v6 output. Well, I could have a separate Window::v6 class, since there will be other differences. ARGH! Problem: each (bufferable) window needs its own buffer! [maybe not: see below] Ignore the problem since we never buffer anything but lower window? IRL (v6), each buffer needs to store text separately for each window. Make buffer an object. For now, it can be blessed text, which dies if you try to call it with a window != LOWER. But eventually, should be buffering separately for each window. Note that even if we buffer into windows, when you flush the buffers, they still have to go into the streams! Call $window->buffer($text) (which does $buffer.=$text) in stream_line() Call $self->write($window->get_buffer()) in stream_line(). Call $window->flush_buffer() ($buffer = "") in newline(). Actually, stream 2 should have only one buffer, while for stream 1 you would have a buffer in each window. That is, anything that will produce a separate output needs its own buffer. For now, we'll just give stream 1 and stream 2 their own buffers, rather than putting the buffer into the window. Each window also kinda needs to store its cursor position, or at least the stream 1 and 2 windows do. Should test in Frotz whether stream 3 really sucks up all text even as you switch windows. Also confirm all stream/window combos do what I think. One solution: Each stream has a set of windows. PP::Window::current_window returns integer current window. OS::output says $stream->get_window(PP::Window::current_window)->output($text); Stream 1: Window::Lower, Window::Upper (possibly Window::Status) Window::Lower ISA NewlineBuffered & Screen. Window::Upper ISA Screen. Stream 2: @Windows = (Window::Transcript->new, Window::DevNull->new); Stream 3: $Windows[1] = $Windows[0] = new Window::StackOfBuffered; (so writing to window 0 OR 1 writes to the same buffer in the stack) Stream 4: $Windows[1] = $Windows[0] = new Window::File [Or, streams 3-4 don't have 2 windows] Is ZIO pointed to by both Upper & Lower windows, or by the stream object that points to them. If the latter, might be tough to cross-communicate when printing text, newlines, etc. 8.6.1.3 - version 3, "restore" sets upper window to size 0. 8.7.3.3 - erase_window(-1) should happen at the beginning of every (v4+) game ############### restart. Spec 6 remarks say: "Note that the "state of play" does not include numerous input/output settings (the current window, cursor position, splitness or otherwise, which streams are selected, etc.): neither does it include the state of the random-number generator. (Games with elaborate status lines must redraw them after a restore has taken place.)" That doesn't seem to match what Dzip and Rezrov do, though. I think they reset almost everything. (See notes on paper.) See Graham Nelson's email. DON'T restart streams. Should be fine to rewrite other things. ############### SAVE Note that a given call stack frame in quetzal is sort of a mix of information from two different subs: - PC of first command AFTER the call opcode in caller() - in a Z-machine sense, where PC should go when I hit a @ret opcode - Variable to store result of current sub (plus discard bit) in caller(). In Z-speak, what local var to put result in, since once I return to caller(), I can't back up the PC to the middle of the calling opcode to see where I was supposed to store the result from the current routine. - Which args if any were supplied to this routine - Locals in this routine: - Evaluation stack for this routine. Btw, how do I figure out which address z_call should call? If I restore from a Frotz save file, I have no idea which subroutines make up the frames of the stack. All we have are the next_PC's. Which is fine when you're subroutine-less; you just restore by jumping to restore_PC, then each time you hit a return, you jump to next_PC. But how can we climb the stack? One way to do it is to grep the symbol table for routines like 'rtn\d+', sort those routines, and call the subroutine with the highest address less than the next_PC for the *next* frame of the call stack. Of course, if I'm getting so complicated that I'm reading the symbol table, perhaps I should rethink this whole "using subroutines" strategy... Make sure that restore test tests a call sp (and why not call locv while we're at it). NOTE Quetzal 3.5 NOTE: Quetzal 6.2 defines some info about catch and throw NOTE: Quetzal 7 - store some extra stuff? Author can be $>. See 7.5 Read them in and use them somehow? Store their score, time, and location, e.g. 7.8 IntD interpreter dependent chunk: ###################################################################### Glk?! For platform independence -- or will Parrot get me that anyway? We'll need to distribute Glk libraries, right? From the spec: Note that it is permissible for a routine to be in dynamic memory. Marnix Klooster suggests this might be used for compiling code at run time! Try Zork special commands like $ve ###################################################################### To create an interpreter: - Runtime.pm and IO.pm stuff can be called directly. - Mostly copy %trans and eval the result! - Change call_* translations. Need to do a jump and create local vars & stack. ###################################################################### We could make the translated Perl code more like Z-code with lots of additional overhead. (It would still be more like a running Z-machine than an interpreter - um, sort of.) Basically, make an array of global vars that ties to an object which uses set/get_global_var for FETCH/STORE. Same for $SP (watch out for indirect variables)! Lvalueness should be handled automatically. We could also make all arithmetic happen like Z-machine arithmetic if we made the locv's (and Global/SP) objects of a class that has arithmetic (except logical or/and/not etc.!) overloaded to do signed_word. Lots more overhead here, cuz every arithmetic op calls subs, which sort of loses the whole point of translation. But it's a little cooler - and maybe more like what PIZMON is supposed to do. We could use overload::constant(integer) to automatically make integers into objects whose arithmetic gets overloaded. Otherwise, "add 1 65532" won't work correctly. But this might break any actual arithmetic we have w/in that class. Um, maybe. Also, the alternative is just to recognize while translating when we get a situation w/ 2 constants (could probably do that even with a simple regexp). When that happens, we could EITHER do the arithmetic at translation time, or if we want to be more authentic, convert the values to their signed-word equivalents: "add 1 -4". Certainly could use ++/-- for inc/dec (which doesn't even have the integer constant problem - you wouldn't write 7++). We could even create an array whose index 0 is tied to a Stack object, 1-15 tied to LocalVar objects, 16-255 tied to GlobalVar objects. Actually, tie the whole array to do the dispatch automatically? Oteherwise, $arr[100]=1 will remove the tied GlobalVar from $arr[100]. We could translate je to do $foo == [$bar, $blah] and overload == to do the grep if lhs is object and rhs has []. Make Perl objects that represent Z Objects. But note that behind the scenes, the objects don't store props, attributes, parents, etc. Things that seem like getter/setter methods are really just calling the same old code that manipulates PlotzMemory. ###################################################################### DEBUG/OPTIMIZE Optimize: translate each opcode to include "$count{$opcode}++" in outputted code. Then running code will show what got called how many times so we can plan optimization. Or just use perl -d:DProf. Use perl -d:DProf on plotz, too. 240 out of 1111 strings in Zork don't use abbrevs. Print those explicitly? Probably not worth it. Not worth optimizing print: it's only done a few times per turn! Worry about the other several hundred opcodes! Objects could be cached, but things will break if someone writes object stuff directly to memory using storeb/storew. set/get_global_var can be calls to mult_add_string. It'll be less readable (only do it with -O option?) but faster, and it happens a lot. ###################################################################### Spec version1.1: Opcode operands are always evaluated from first to last - this order is important when the stack pointer appears as an argument. Thus "@sub sp sp" subtracts the second-from-top stack item from the topmost stack item. In the seven opcodes that take indirect variable references (inc, dec, inc_chk, dec_chk, load, store, pull), an indirect reference to the stack pointer does not push or pull the top item of the stack - it is read or written in place. [Argh! -ADK] Read must return 13 when terminated by the user pressing "enter". See section 3. The dictionary and flag operands of tokenise are optional. Given the existence of Quetzal, a portable saved file format, it is quite possible that after loading, the game may be running on a different interpreter to that on which the game started. As a result, it is strongly advisable to recheck any interpreter capabilities (eg Standard version, unicode support, etc) after loading. extended save/restore have new optional prompt set_text_style, set_font, set_colour are changed. See spec. ###################################################################### PIR stuff Note that perfect sub arg passing isn't necessary to do a simple addition, for example. For local vars and/or temp vars, use $I0 etc. or .locals? I could even use explicitly I0-15 (we know there's max 15 local vars) and I16-31 for temp vars. Ask on list. Can .local be declared mid-sub? Otherwise I could declare a couple in every sub. Is it bad to mix true registers with $I registers and/or .locals? Save/Restore may be a major problem with doing PIZMON. Because we need to change the Parrot call stack. Asked about it on the mailing list & didn't get much of an answer. If we can't modify the Parrot call stack (by using an opcode in C) then maybe we can do a hybrid sub/manual call stack procedure. It would work like this: - Before every sub call --- push the current local variables onto a stack, --- save the label of the line that calls the next sub in the stack. - If restoring, then go back to main. Foreach sub in the call stack, --- set the local variables in this frame from the saved stack, --- goto the line that will call the next sub in the call stack. Note that this does result in a bit of buildup in the true Parrot call stack (because when you call "restore" you'll have some stuff in the call stack. Then you goto program start and start adding new things to the stack without removing teh old.) But I think it's just a few subs worth, and for most uses of the Z-machine, there shouldn't be TOO much danger of overloading the Parrot call stack. When the time comes to try this, offer it as an alternative to the "change the call stack" opcode. ###################################################################### PIZMON. Parrot IS the Z-Machine. Should load opcodes (for the correct version!). Then jump to first routine's address (which is in header). Then keep running as Parrot, only interpreting its opcodes as Z opcodes. Extended opcodes don't work? Maybe we could consider the second byte the first arg, and make the extended_opcode_handler sub just be basically a sub hash my $ext=shift; &{$subhash{$ext}}(@_); I think there could be major problems with sizes. Parrot expects everything to happen on 4-byte boundaries, which it doesn't. It expects opcodes to be four bytes, and for there to be N arguments of 4 bytes each. IRL, we're going to have: Opcode 1 or 2 bytes (Types of operands) 1 or 2 bytes: 4 or 8 2-bit fields Operands Between 0 and 8 of these: each 1 or 2 bytes (Store variable) 1 byte (Branch offset) 1 or 2 bytes (Text to print) An encoded string (of unlimited length) Plus don't forget the extra byte at the beginning of each routine saying how many local variables it has. (Course, we could ignore that and always allow 15.) ###################################################################### "In Planetfall there is a blacked-out room you can't see anything in. There's a lamp in the game, but it's located in a lab full of deadly radiation. You can enter the lab and take the lamp, but will die of radiation poisoning before you can make it back to the darkened room." If we manually turn off the radiation, or "pilfer" the lamp, what will we find in that room? vim: tw=78