Mercurial > web-gnusto
view gnusto-engine.js @ 93:817aa2851339 default tip
Added a very simple index.html.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Fri, 23 May 2008 03:50:44 -0700 |
parents | 7ddacb62f0e5 |
children |
line wrap: on
line source
// gnusto-lib.js || -*- Mode: Java; tab-width: 2; -*- // The Gnusto JavaScript Z-machine library. // $Header: /cvs/gnusto/src/xpcom/engine/gnusto-engine.js,v 1.116 2005/04/26 01:50:32 naltrexone42 Exp $ // // Copyright (c) 2003 Thomas Thurman // thomas@thurman.org.uk // // This program is free software; you can redistribute it and/or modify // it under the terms of version 2 of the GNU General Public License // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have be able to view the GNU General Public License at // http://www.gnu.org/copyleft/gpl.html ; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. const CVS_VERSION = '$Date: 2005/04/26 01:50:32 $'; const ENGINE_DESCRIPTION = "Gnusto's interactive fiction engine"; //////////////////////////////////////////////////////////////// // // PART THE FIRST // // STUFF FROM GNUSTO-LIB WHICH STILL NEEDS MERGING IN // //////////////////////////////////////////////////////////////// var default_unicode_translation_table = { 155:0xe4, // a-diaeresis 156:0xf6, // o-diaeresis 157:0xfc, // u-diaeresis 158:0xc4, // A-diaeresis 159:0xd6, // O-diaeresis 160:0xdc, // U-diaeresis 161:0xdf, // German "sz" ligature 162:0xbb, // right quotation marks 163:0xab, // left quotation marks 164:0xeb, // e-diaeresis 165:0xef, // i-diaeresis 166:0xff, // y-diaeresis 167:0xcb, // E-diaeresis 168:0xcf, // I-diaeresis 169:0xe1, // a-acute 170:0xe9, // e-acute 171:0xed, // i-acute 172:0xf3, // o-acute 173:0xfa, // u-acute 174:0xfd, // y-acute 175:0xc1, // A-acute 176:0xc9, // E-acute 177:0xcd, // I-acute 178:0xd3, // O-acute 179:0xda, // U-acute 180:0xdd, // Y-acute 181:0xe0, // a-grave 182:0xe8, // e-grave 183:0xec, // i-grave 184:0xf2, // o-grave 185:0xf9, // u-grave 186:0xc0, // A-grave 187:0xc8, // E-grave 188:0xcc, // I-grave 189:0xd2, // O-grave 190:0xd9, // U-grave 191:0xe2, // a-circumflex 192:0xea, // e-circumflex 193:0xee, // i-circumflex 194:0xf4, // o-circumflex 195:0xfb, // u-circumflex 196:0xc2, // A-circumflex 197:0xca, // E-circumflex 198:0xce, // I-circumflex 199:0xd4, // O-circumflex 200:0xdb, // U-circumflex 201:0xe5, // a-ring 202:0xc5, // A-ring 203:0xf8, // o-slash 204:0xd8, // O-slash 205:0xe3, // a-tilde 206:0xf1, // n-tilde 207:0xf5, // o-tilde 208:0xc3, // A-tilde 209:0xd1, // N-tilde 210:0xd5, // O-tilde 211:0xe6, // ae-ligature 212:0xc6, // AE-ligature 213:0xe7, // c-cedilla 214:0xc7, // C-cedilla 215:0xfe, // thorn 216:0xf0, // eth 217:0xde, // Thorn 218:0xd0, // Eth 219:0xa3, // pound sterling sign 220:0x153, // oe-ligature 221:0x152, // OE-ligature 222:0xa1, // inverted pling 223:0xbf, // inverted query }; const PARENT_REC = 0; const SIBLING_REC = 1; const CHILD_REC = 2; const CALLED_FROM_INTERRUPT = 0; //////////////////////////////////////////////////////////////// // Effect codes, returned from run(). See the explanation below // for |handlers|. // Returned when we're expecting a line of keyboard input. // // Answer with the string the user has entered. var GNUSTO_EFFECT_INPUT = '"RS"'; // Returned when we're expecting a single keypress (or mouse click). // TODO: The lowest nibble may be 1 if the Z-machine has asked // for timed input. // // Answer with the ZSCII code for the key pressed (see the Z-spec). var GNUSTO_EFFECT_INPUT_CHAR = '"RC"'; // Returned when the Z-machine requests we save the game. // Answer as in the Z-spec: 0 if we can't save, 1 if we can, or // 2 if we've just restored. var GNUSTO_EFFECT_SAVE = '"DS"'; // Returned when the Z-machine requests we load a game. // Answer 0 if we can't load. (If we can, we won't be around to answer.) var GNUSTO_EFFECT_RESTORE = '"DR"'; // Returned when the Z-machine requests we quit. // Not to be answered, obviously. var GNUSTO_EFFECT_QUIT = '"QU"'; // Returned when the Z-machine requests that we restart a game. // Assumedly, we won't be around to answer it. var GNUSTO_EFFECT_RESTART = '"NU"'; // Returned if we've run for more than a certain number of iterations. // This means that the environment gets a chance to do some housekeeping // if we're stuck deep in computation, or to break an infinite loop // within the Z-code. // // Any value may be used as an answer; it will be ignored. var GNUSTO_EFFECT_WIMP_OUT = '"WO"'; // Returned if we hit a breakpoint. // Any value may be used as an answer; it will be ignored. var GNUSTO_EFFECT_BREAKPOINT = '"BP"'; // Returned if either of the two header bits which // affect printing have changed since last time // (or if either of them is set on first printing). // FIXME: This needs to be moved out of the private use area. var GNUSTO_EFFECT_FLAGS_CHANGED = '"XC"'; // Returned if the story wants to check whether it's been pirated. // Answer 1 if it is, or 0 if it isn't. // You probably just want to return 0. var GNUSTO_EFFECT_PIRACY = '"CP"'; // Returned if the story wants to set the text style. // effect_parameters() will return a list: // [0] = a bitcoded text style, as in the Z-spec, // or -1 not to set the style. // [1] = the foreground colour to use, as in the Z-spec // [2] = the background colour to use, as in the Z-spec // Any value may be used as an answer; it will be ignored. var GNUSTO_EFFECT_STYLE = '"SS"'; // Returned if the story wants to cause a sound effect. // effect_parameters() will return a list, whose // vales aren't fully specified at present. // (Just go "bleep" for now.) // // Any value may be used as an answer; it will be ignored. var GNUSTO_EFFECT_SOUND = '"FX"'; var GNUSTO_EFFECT_SPLITWINDOW = '"TW"'; var GNUSTO_EFFECT_SETWINDOW = '"SW"'; var GNUSTO_EFFECT_ERASEWINDOW = '"YW"'; var GNUSTO_EFFECT_ERASELINE = '"YL"'; // Returned if the story wants to set the position of // the cursor in the upper window. The upper window should // be currently active. // // effect_parameters() will return a list: // [0] = the new Y coordinate // [1] = the new X coordinate // Any value may be used as an answer; it will be ignored. var GNUSTO_EFFECT_SETCURSOR = '"SC"'; var GNUSTO_EFFECT_SETBUFFERMODE = '"SB"'; var GNUSTO_EFFECT_SETINPUTSTREAM = '"SI"'; var GNUSTO_EFFECT_GETCURSOR = '"GC"'; // Returned if the story wants to print a table, as with // @print_table. (This is complicated enough to get its // own effect code, rather than just using an internal buffer // as most printing does.) // // effect_parameters() will return a list of lines to print. // // Any value may be used as an answer; it will be ignored. var GNUSTO_EFFECT_PRINTTABLE = '"PT"'; //////////////////////////////////////////////////////////////// // // PART THE SECOND // // THE HANDLERS AND HANDLER ARRAYS // //////////////////////////////////////////////////////////////// // JavaScript seems to have a problem with pointers to methods. // We solve this in a Pythonesque manner. Each instruction handler // is a simple function which takes two parameters: 1) the engine // asking the question (i.e. the value which would be "this" if // the function was a method), and 2) the list of actual arguments // given in the z-code for that function. function handleZ_je(engine, a) { if (a.length<2) { engine.logger('je','noop'); return ''; // it's a no-op } else if (a.length==2) { engine.logger('je',a[0] + '==' + a[1]); return engine._brancher(a[0]+'=='+a[1]); } else { var condition = ''; for (var i=1; i<a.length; i++) { if (i!=1) condition = condition + '||'; condition = condition + 't=='+a[i]; } engine.logger('je','t=' + a[0] + ';' + condition); return 't='+a[0]+';'+engine._brancher(condition); } } function handleZ_jl(engine, a) { engine.logger('jl',a[0] + '<' + a[1]); return engine._brancher(a[0]+'<'+a[1]); } function handleZ_jg(engine, a) { engine.logger('jg',a[0] + '>'+a[1]); return engine._brancher(a[0]+'>'+a[1]); } function handleZ_dec_chk(engine, a) { engine.logger('dec_chk',value + '-1 < ' + a[1]); return 't='+a[0]+';t2=_varcode_get(t)-1;_varcode_set(t2,t);'+engine._brancher('t2<'+a[1]); } function handleZ_inc_chk(engine, a) { engine.logger('inc_chk',value + '+1 > ' + a[1]); return 't='+a[0]+';t2=_varcode_get(t)+1;_varcode_set(t2,t);'+engine._brancher('t2>'+a[1]); } function handleZ_jin(engine, a) { engine.logger('jin',a[0] + ',' + a[1]); return engine._brancher("_obj_in("+a[0]+','+a[1]+')'); } function handleZ_test(engine, a) { engine.logger('test','t='+a[1]+';br(' + a[0] + '&t)==t)'); return 't='+a[1]+';'+engine._brancher('('+a[0]+'&t)==t'); } function handleZ_or(engine, a) { engine.logger('or','('+a[0] + '|' + a[1]+')&0xFFFF'); return engine._storer('('+a[0]+'|'+a[1]+')&0xffff'); } function handleZ_and(engine, a) { engine.logger('and',a[0] + '&' + a[1] + '&0xFFFF'); return engine._storer(a[0]+'&'+a[1]+'&0xffff'); } function handleZ_test_attr(engine, a) { engine.logger('test_attr',a[0] + ',' + a[1]); return engine._brancher('_test_attr('+a[0]+','+a[1]+')'); } function handleZ_set_attr(engine, a) { engine.logger('set_attr',a[0] + ',' + a[1]); return '_set_attr('+a[0]+','+a[1]+')'; } function handleZ_clear_attr(engine, a) { engine.logger('clear_attr',a[0] + ',' + a[1]); return '_clear_attr('+a[0]+','+a[1]+')'; } function handleZ_store(engine, a) { engine.logger('store',a[0] + ',' + a[1]); return "_varcode_set("+a[1]+","+a[0]+")"; } function handleZ_insert_obj(engine, a) { engine.logger('insert_obj',a[0] + ',' + a[1]); return "_insert_obj("+a[0]+','+a[1]+")"; } function handleZ_loadw(engine, a) { engine.logger('loadw',"getWord((1*"+a[0]+"+2*"+a[1]+")&0xFFFF)"); return engine._storer("getWord((1*"+a[0]+"+2*"+a[1]+")&0xFFFF)"); } function handleZ_loadb(engine, a) { return engine._storer("m_memory[0xFFFF&(1*"+a[0]+"+1*"+a[1]+")]"); } function handleZ_get_prop(engine, a) { engine.logger('get_prop',a[0]+','+a[1]); return engine._storer("_get_prop("+a[0]+','+a[1]+')'); } function handleZ_get_prop_addr(engine, a) { engine.logger('get_prop_addr',a[0]+','+a[1]); return engine._storer("_get_prop_addr("+a[0]+','+a[1]+')'); } function handleZ_get_next_prop(engine, a) { engine.logger('get_next_prop',a[0]+','+a[1]); return engine._storer("_get_next_prop("+a[0]+','+a[1]+')'); } function handleZ_add(engine, a) { engine.logger('add',a[0]+'+'+a[1]); return engine._storer(a[0]+'*1+'+a[1]+'*1'); } function handleZ_sub(engine, a) { engine.logger('sub',a[0]+'-'+a[1]); return engine._storer(a[0]+'-'+a[1]); } function handleZ_mul(engine, a) { engine.logger('mul',a[0]+'*'+a[1]); return engine._storer(a[0]+'*'+a[1]); } function handleZ_div(engine, a) { engine.logger('div',a[0]+'/'+a[1]); return engine._storer('_trunc_divide('+a[0]+','+a[1]+')'); } function handleZ_mod(engine, a) { engine.logger('mod',a[0]+'%'+a[1]); return engine._storer(a[0]+'%'+a[1]); } function handleZ_set_colour(engine, a) { engine.logger('set_colour',a[0] + ',' + a[1]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_STYLE+",-1,"+a[0]+','+a[1]+"];return"; } function handleZ_throw(engine, a) { engine.logger('throw','throw_stack_frame('+a[0]+');return'); engine.m_compilation_running = 0; return "_throw_stack_frame("+a[0]+");return"; } function handleZ_jz(engine, a) { engine.logger('jz',a[0]+'==0'); return engine._brancher(a[0]+'==0'); } function handleZ_get_sibling(engine, a) { engine.logger('get_sibling',"t=get_sibling("+a[0]+");"); return "t=_get_sibling("+a[0]+");"+engine._storer("t")+";"+engine._brancher("t"); } function handleZ_get_child(engine, a) { engine.logger('get_child',"t=get_child("+a[0]+");"); return "t=_get_child("+a[0]+");"+ engine._storer("t")+";"+ engine._brancher("t"); } function handleZ_get_parent(engine, a) { engine.logger('get_parent',"get_parent("+a[0]+");"); return engine._storer("_get_parent("+a[0]+")"); } function handleZ_get_prop_len(engine, a) { engine.logger('get_prop_len',"get_prop_len("+a[0]+");"); return engine._storer("_get_prop_len("+a[0]+')'); } function handleZ_inc(engine, a) { engine.logger('inc',a + '+1'); return "t="+a[0]+';_varcode_set(_varcode_get(t)+1, t)'; } function handleZ_dec(engine, a) { engine.logger('dec',a[0] + '-1'); return "t="+a[0]+';_varcode_set(_varcode_get(t)-1, t)'; } function handleZ_print_addr(engine, a) { engine.logger('print_addr','zscii_from('+a[0]+')'); return engine._handler_zOut('_zscii_from('+a[0]+')',0); } function handleZ_remove_obj(engine, a) { engine.logger('remove_obj',"remove_obj("+a[0]+','+a[1]+")"); return "_remove_obj("+a[0]+','+a[1]+")"; } function handleZ_print_obj(engine, a) { engine.logger('print_obj','name_of_object('+a[0]+',0)'); return engine._handler_zOut("_name_of_object("+a[0]+")",0); } function handleZ_ret(engine, a) { engine.logger('ret',"_func_return("+a[0]+');return'); engine.m_compilation_running=0; return "_func_return("+a[0]+');return'; } function handleZ_jump(engine, a) { engine.m_compilation_running=0; if (a[0] & 0x8000) { a[0] = (~0xFFFF) | a[0]; } var addr=(a[0] + engine.m_pc) - 2; engine.logger('jump',"pc="+addr+";return"); return "m_pc="+addr+";return"; } function handleZ_print_paddr(engine, a) { engine.logger('print_paddr',"zscii_from((("+a[0]+")&0xFFFF)*4)"); return engine._handler_zOut("_zscii_from("+engine.m_pc_translate_for_string(a[0])+")",0); } function handleZ_load(engine, a) { engine.logger('load',"store " + c); return engine._storer('_varcode_get('+a[0]+')'); } function handleZ_rtrue(engine, a) { engine.logger('rtrue',"_func_return(1);return"); engine.m_compilation_running=0; return "_func_return(1);return"; } function handleZ_rfalse(engine, a) { engine.logger('rfalse',"_func_return(0);return"); engine.m_compilation_running=0; return "_func_return(0);return"; } function handleZ_print(engine, a) { engine.logger('printret',"see handler_print"); return engine._handler_print('', 0); } function handleZ_print_ret(engine, a) { engine.m_compilation_running = 0; engine.logger('printret',"see handler_print"); return engine._handler_print('\n', 1)+';_func_return(1);return'; } function handleZ_nop(engine, a) { engine.logger('noop',''); return ""; } function handleZ_restart(engine, a) { engine.logger('restart',''); engine.m_compilation_running=0; return "m_effects=["+GNUSTO_EFFECT_RESTART+"];return"; } function handleZ_ret_popped(engine, a) { engine.logger('pop',"_func_return(gamestack.pop());return"); engine.m_compilation_running=0; return "_func_return(m_gamestack.pop());return"; } function handleZ_catch(engine, a) { // The stack frame cookie is specified by Quetzal 1.3b s6.2 // to be the number of frames on the stack. engine.logger('catch',"store call_stack.length"); return engine._storer("call_stack.length"); } function handleZ_pop(engine, a) { return "m_gamestack.pop()"; } function handleZ_quit(engine, a) { engine.logger('quit',''); engine.m_compilation_running=0; return "m_effects=["+GNUSTO_EFFECT_QUIT+"];return"; } function handleZ_new_line(engine, a) { engine.logger('newline',''); return engine._handler_zOut("'\\n'",0); } function handleZ_show_status(engine, a){ //(illegal from V4 onward) engine._handler_zOut(''); //chalk forces repaint of status bar return ""; } function handleZ_verify(engine, a) { return engine._brancher('_verify()'); } function handleZ_illegal_extended(engine, a) { // 190 can't be generated; it's the start of an extended opcode engine.logger('illegalop','190'); gnusto_error(199); } function handleZ_piracy(engine, a) { engine.m_compilation_running = 0; var setter = 'm_rebound=function(){'+engine._brancher('(!m_answers[0])')+'};'; engine.logger('piracy',"pc="+pc+";"+setter+"m_effects=[GNUSTO_EFFECT_PIRACY];return;"); return "m_pc="+engine.m_pc+";"+setter+"m_effects=["+GNUSTO_EFFECT_PIRACY+"];return"; } //////////////////////////////////////////////////////////////// // // Call handlers: // // Gosub-generating functions, in increasing order of // arity (no args, one arg, many args), with the // no-store versions first. The "*_vs2" instructions // are conceptually identical to the corresponding // "*_vs" instructions, and share the same handlers. // // naltrexone-- I've removed the VERBOSE lines // which were rendered incorrect by this. If you need to turn // them on again, I'll put them back in in the new form. function handleZ_call_1n(engine, a) { return engine._generate_gosub(a[0], '', 0); } function handleZ_call_1s(engine, a) { return engine._generate_gosub(a[0], '', 1); } function handleZ_call_2n(engine, a) { return engine._generate_gosub(a[0], a[1], 0); } function handleZ_call_2s(engine, a) { return engine._generate_gosub(a[0], a[1], 1); } function handleZ_call_vn(engine, a) { return engine._generate_gosub(a[0], a.slice(1), 0); } function handleZ_call_vs(engine, a) { return engine._generate_gosub(a[0], a.slice(1), 1); } //////////////////////////////////////////////////////////////// function handleZ_store_w(engine, a) { engine.logger('storew',"setWord("+a[2]+",1*"+a[0]+"+2*"+a[1]+")"); return "setWord("+a[2]+",1*"+a[0]+"+2*"+a[1]+")"; } function handleZ_storeb(engine, a) { engine.logger('storeb',"setByte("+a[2]+",1*"+a[0]+"+1*"+a[1]+")"); return "setByte("+a[2]+",1*"+a[0]+"+1*"+a[1]+")"; } function handleZ_putprop(engine, a) { engine.logger('putprop',"put_prop("+a[0]+','+a[1]+','+a[2]+')'); return "_put_prop("+a[0]+','+a[1]+','+a[2]+')'; } // read, aread, sread, whatever it's called today. // That's something that we can't deal with within gnusto: // ask the environment to magic something up for us. function handleZ_read(engine, a) { // JS representing number of deciseconds to wait before a // timeout should occur, or 0 if there shouldn't be one. var timeout_deciseconds; // JS representing the address of the timeout routine, // or 0 if there isn't one. var address_of_timeout_routine; engine.m_compilation_running = 0; // Since a[0] (address of the text buffer) is referenced so often, // we introduce a variable |a0| into JITspace with the same value. // A JS string telling us what to do if there isn't a timeout. var rebound_for_no_timeout = "_aread(m_answers[0],m_rebound_args[1],"+ "m_rebound_args[2],m_answers[1])"; // A JS string telling how to get the number of characters to "recap". var recaps_getter; // A JS string telling us how to get the number of characters the // text buffer can hold. var char_count_getter; if (engine.m_version>=5) { // In z5-z8, @read is a store instruction. rebound_for_no_timeout = engine._storer(rebound_for_no_timeout); } // Otherwise we just leave the call to _aread() as it is. if (engine.m_version>=5) { // z5+ use two header bytes at the start of the table. recaps_getter = "m_memory[0xFFFF&a0+1]"; char_count_getter = "m_memory[0xFFFF&a0]"; } else { // z1-z4 only use one. (They don't have recaps.) recaps_getter = '0'; char_count_getter = "m_memory[0xFFFF&a0]+1"; } if (a[2] && a[3] && (engine.m_version>=4)) { // This is a timed routine. // a[3] is the routine to call after a[2] deciseconds. timeout_deciseconds = a[2]; address_of_timeout_routine = engine.m_pc_translate_for_routine(a[3]); } else { // No timeout. timeout_deciseconds = '0'; address_of_timeout_routine = '0'; // Optimisation: In this case we could optimise rebound_setter // so that it doesn't check whether to call the interrupt // service routine. We haven't done this here for simplicity, // but we did it in the simpler @read_char. } // JS for a function to handle the answer to this effect. // The answer will be one integer; if the integer is zero, // it's a request for a timeout; if it's nonzero, it's a // keycode to be stored as the present instruction dictates. var rebound_setter = "m_rebound=function(){"+ "var t=1*m_answers[0];" + "if(t<0){"+ "_func_interrupt(m_rebound_args[0],onISRReturn_for_read);"+ // -ve: timeout "}else{"+ rebound_for_no_timeout + ";" + "}"+ "};"; var rebound_args_setter = "m_rebound_args=["+ address_of_timeout_routine + "," + // Where to jump on timeout "a0,"+ // Address of text buffer a[1]+","+ // Address of parse buffer "];"; /****************************************************************/ return "var a0=eval("+ a[0] + ");" + "m_pc=" + engine.m_pc + ";" + rebound_args_setter + rebound_setter + "m_effects=["+ GNUSTO_EFFECT_INPUT + "," + timeout_deciseconds + "," + recaps_getter + "," + char_count_getter + "," + "_terminating_characters()];return"; } function handleZ_print_char(engine, a) { engine.logger('print_char','zscii_char_to_ascii('+a[0]+')'); return engine._handler_zOut('_zscii_char_to_ascii('+a[0]+')',0); } function handleZ_print_num(engine, a) { engine.logger('print_num','handler_zout('+a[0]+')'); return engine._handler_zOut(a[0],0); } function handleZ_random(engine, a) { engine.logger('random',"random_number("+a[0]+")"); return engine._storer("_random_number("+a[0]+")"); } function handleZ_push(engine, a) { engine.logger('push',a[0]); return 'm_gamestack.push('+a[0]+')'; } function handleZ_pull(engine, a) { engine.logger('pull',a[0] +'=gamestack.pop()'); return '_varcode_set(m_gamestack.pop(),'+a[0]+')'; } function handleZ_split_window(engine, a) { engine.m_compilation_running=0; engine.logger('split_window','lines=' + a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_SPLITWINDOW+","+a[0]+"];return"; } function handleZ_set_window(engine, a) { engine.m_compilation_running=0; engine.logger('set_window','win=' + a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_SETWINDOW+","+a[0]+"];return"; } function handleZ_erase_window(engine, a) { engine.m_compilation_running=0; engine.logger('erase_window','win=' + a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_ERASEWINDOW+","+a[0]+"];return"; } function handleZ_erase_line(engine, a) { engine.m_compilation_running=0; engine.logger('erase_line',a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_ERASELINE+","+a[0]+"];return"; } function handleZ_set_cursor(engine, a) { engine.m_compilation_running=0; engine.logger('set_cursor',' ['+a[0]+', ' + a[1] + '] '); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_SETCURSOR+","+a[0]+","+a[1]+"];return"; } function handleZ_get_cursor(engine, a) { engine.m_compilation_running=0; engine.logger('get_cursor',a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_GETCURSOR+","+a[0]+"];return"; } function handleZ_set_text_style(engine, a) { engine.m_compilation_running=0; engine.logger('set_text_style',a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_STYLE+","+a[0]+",0,0];return"; } function handleZ_buffer_mode(engine, a) { engine.m_compilation_running=0; engine.logger('buffer_mode',a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_SETBUFFERMODE+","+a[0]+"];return"; } function handleZ_output_stream(engine, a) { engine.logger('output_stream',a[0]+', ' + a[1]); return '_set_output_stream('+a[0]+','+a[1]+')'; } function handleZ_input_stream(engine, a) { engine.m_compilation_running=0; engine.logger('input_stream',a[0]); return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_SETINPUTSTREAM+","+a[0]+"];return"; } function handleZ_sound_effect(engine, a) { // We're rather glossing over whether and how we // deal with callbacks at present. engine.m_compilation_running=0; engine.logger('sound_effect','better logging later'); while (a.length < 5) { a.push(0); } return "m_pc="+engine.m_pc+';m_effects=['+GNUSTO_EFFECT_SOUND+','+a[0]+','+a[1]+','+a[2]+','+a[3]+','+a[4]+'];return'; } // Maybe factor out "read" and this? function handleZ_read_char(engine, a) { // JS representing number of deciseconds to wait before a // timeout should occur, or 0 if there shouldn't be one. var timeout_deciseconds; // JS to set m_rebound_args to show where to jump if there's // a timeout. If there's not going to be a timeout, this should // be blank. var rebound_args_setter; // JS for a function to handle the answer to this effect. // The answer will be one integer; if the integer is zero, // it's a request for a timeout; if it's nonzero, it's a // keycode to be stored as the present instruction dictates. var rebound_setter; // Stop the engine! We want to get off! engine.m_compilation_running = 0; // a[0] is always 1; probably not worth checking for this. if (a[1] && a[2] && (engine.m_version>=4)) { // This is a timed routine. // a[2] is the routine to call after a[1] deciseconds. timeout_deciseconds = a[1]; rebound_args_setter = "m_rebound_args=["+ engine.m_pc_translate_for_routine(a[2])+'];'; rebound_setter = "m_rebound=function(){"+ "var t=1*m_answers[0];" + "if(t<0){"+ "_func_interrupt(m_rebound_args[0],onISRReturn_for_read_char);"+ // -ve: timeout "}else{"+ engine._storer("t") + // otherwise, a result to store. "}"+ "};"; } else { // No timeout. timeout_deciseconds = '0'; // We only set m_rebound_args when there's a timeout. rebound_args_setter = ''; // A much simpler rebound function, since zero isn't // a magic answer. rebound_setter = "m_rebound=function(){"+ engine._storer("1*m_answers[0]") + "};"; } return "m_pc="+engine.m_pc+";"+ rebound_args_setter + rebound_setter + "m_effects=["+GNUSTO_EFFECT_INPUT_CHAR+ ","+timeout_deciseconds+"];return"; } function handleZ_scan_table(engine, a) { engine.logger('scan_table',"t=scan_table("+a[0]+','+a[1]+"&0xFFFF,"+a[2]+"&0xFFFF," + a[3]+");"); if (a.length == 4) { return "t=_scan_table("+a[0]+','+a[1]+"&0xFFFF,"+a[2]+"&0xFFFF," + a[3]+");" + engine._storer("t") + ";" + engine._brancher('t'); } else { // must use the default for Form, 0x82 return "t=_scan_table("+a[0]+','+a[1]+"&0xFFFF,"+a[2]+"&0xFFFF," + 0x82 +");" + engine._storer("t") + ";" + engine._brancher('t'); } } function handleZ_not(engine, a) { engine.logger('not','~'+a[1]+'&0xffff'); return engine._storer('~'+a[1]+'&0xffff'); } function handleZ_tokenise(engine, a) { engine.logger('tokenise',"tokenise("+a[0]+","+a[1]+","+a[2]+","+a[3]+")"); return "_tokenise(("+a[0]+")&0xFFFF,("+a[1]+")&0xFFFF,"+a[2]+","+a[3]+")"; } function handleZ_encode_text(engine, a) { engine.logger('tokenise',"_encode_text("+a[0]+","+a[1]+","+a[2]+","+a[3]+")"); return "_encode_text("+a[0]+","+a[1]+","+a[2]+","+a[3]+")"; } function handleZ_copy_table(engine, a) { engine.logger('_copy_table',"copy_table("+a[0]+','+a[1]+','+a[2]+")"); return "_copy_table("+a[0]+','+a[1]+','+a[2]+")"; } function handleZ_print_table(engine, a) { // Jam in defaults: if (a.length < 3) { a.push(1); } // default height if (a.length < 4) { a.push(0); } // default skip engine.logger('print_table',"print_table("+a[0]+','+a[1]+','+a[2]+',' + a[3]+')'); return "m_pc="+engine.m_pc+";m_effects=_print_table("+a[0]+","+a[1]+","+a[2]+","+a[3]+");return"; } function handleZ_check_arg_count(engine, a) { engine.logger('check_arg_count',a[0]+'<=param_count()'); return engine._brancher(a[0]+'<=_param_count()'); } function handleZ_saveV123(engine, a) { engine.m_compilation_running=0; var setter = 'm_rebound=function(){'+ engine._brancher('m_answers[0]')+'};'; return "m_state_to_save=_saveable_state(1);m_pc="+engine.m_pc+";"+setter+";m_effects=["+GNUSTO_EFFECT_SAVE+"];return"; } function handleZ_saveV45678(engine, a) { engine.m_compilation_running=0; var setter = "m_rebound=function() { " + engine._storer('m_answers[0]') + "};"; return "m_state_to_save=_saveable_state("+ (engine.m_version==4? '1': '3') + ");m_pc="+engine.m_pc+";" + setter+";m_effects=["+GNUSTO_EFFECT_SAVE+"];return"; } function handleZ_restoreV123(engine, a) { engine.m_compilation_running=0; engine._brancher(''); // Throw it away; it's never used return "m_pc="+engine.m_pc+";m_effects=["+GNUSTO_EFFECT_RESTORE+"];return"; } function handleZ_restoreV45678(engine, a) { engine.m_compilation_running=0; var setter = 'm_rebound=function() { ' + 'var t=m_answers[0]; if (t==0){' + engine._storer('t') + '}};'; return "m_pc="+engine.m_pc+";" + setter + "m_effects=["+GNUSTO_EFFECT_RESTORE+"];return"; } function handleZ_log_shift(engine, a) { engine.logger('log_shift',"log_shift("+a[0]+','+a[1]+')'); // log_shift logarithmic-bit-shift. Right shifts are zero-padded return engine._storer("_log_shift("+a[0]+','+a[1]+')'); } function handleZ_art_shift(engine, a) { engine.logger('log_shift',"art_shift("+a[0]+','+a[1]+')'); // arithmetic-bit-shift. Right shifts are sign-extended return engine._storer("_art_shift("+a[0]+','+a[1]+')'); } function handleZ_set_font(engine, a) { engine.logger('set_font','('+a[0]+'<2?1:0) <<We only provide font 1.>>'); // We only provide font 1. return engine._storer('('+a[0]+'<2?1:0)'); } function handleZ_save_undo(engine, a) { // 3 is the distance between the PC at the moment we call save_undo // and the varcode which gives the place to store the success code. // This is different for other similar instructions, such as @save. return engine._storer('_save_undo(3)'); } function handleZ_restore_undo(engine, a) { // If the restore was successful, return from this block immediately // so that execution can continue with the new PC value. If that // doesn't happen, it must have failed, so store zero. return 'if(_restore_undo(3))return;'+engine._storer('0'); } function handleZ_print_unicode(engine, a) { engine.logger('print_unicode',"String.fromCharCode(" +a[0]+")"); return engine._handler_zOut("String.fromCharCode(" +a[0]+")",0); } function handleZ_check_unicode(engine, a) { engine.logger('check_unicode','we always say yes'); // We have no way of telling from JS whether we can // read or write a character, so let's assume we can // read and write all of them. We can always provide // methods to do so somehow (e.g. with an onscreen keyboard). return engine._storer('3'); } //////////////////////////////////////////////////////////////// // // |handlers| // // An array mapping opcodes to functions. Each function is passed // a series of arguments (between zero and eight, as the Z-machine // allows) as an array, called |a| below. It returns a string of JS, // called |r| in these comments, which can be evaluated to do the job of that // opcode. Note, again, that this is a string (not a function object). // // Extended ("EXT") opcodes are stored 1000 higher than their number. // For example, 1 is "je", but 1001 is "restore". // // |r|'s code may set |engine.m_compilation_running| to 0 to stop compile() from producing // code for any more opcodes after this one. (compile() likes to group // code up into blocks, where it can.) // // |r|'s code may contain a return statement to prevent the execution of // any further generated code before we get to take our bearings again. // For example, |r| must cause a return if it knows that a jump occurred. // If a handler wishes to send an effect to the environment, it should // set |m_effects| in the engine to a non-empty list and return. const handlers_v578 = { 1: handleZ_je, 2: handleZ_jl, 3: handleZ_jg, 4: handleZ_dec_chk, 5: handleZ_inc_chk, 6: handleZ_jin, 7: handleZ_test, 8: handleZ_or, 9: handleZ_and, 10: handleZ_test_attr, 11: handleZ_set_attr, 12: handleZ_clear_attr, 13: handleZ_store, 14: handleZ_insert_obj, 15: handleZ_loadw, 16: handleZ_loadb, 17: handleZ_get_prop, 18: handleZ_get_prop_addr, 19: handleZ_get_next_prop, 20: handleZ_add, 21: handleZ_sub, 22: handleZ_mul, 23: handleZ_div, 24: handleZ_mod, 25: handleZ_call_2s, 26: handleZ_call_2n, 27: handleZ_set_colour, 28: handleZ_throw, 128: handleZ_jz, 129: handleZ_get_sibling, 130: handleZ_get_child, 131: handleZ_get_parent, 132: handleZ_get_prop_len, 133: handleZ_inc, 134: handleZ_dec, 135: handleZ_print_addr, 136: handleZ_call_1s, 137: handleZ_remove_obj, 138: handleZ_print_obj, 139: handleZ_ret, 140: handleZ_jump, 141: handleZ_print_paddr, 142: handleZ_load, 143: handleZ_call_1n, 176: handleZ_rtrue, 177: handleZ_rfalse, 178: handleZ_print, 179: handleZ_print_ret, 180: handleZ_nop, //181: save (illegal in V5) //182: restore (illegal in V5) 183: handleZ_restart, 184: handleZ_ret_popped, 185: handleZ_catch, 186: handleZ_quit, 187: handleZ_new_line, // 188: show_status -- illegal from V4 onward 189: handleZ_verify, 190: handleZ_illegal_extended, 191: handleZ_piracy, 224: handleZ_call_vs, 225: handleZ_store_w, 226: handleZ_storeb, 227: handleZ_putprop, 228: handleZ_read, 229: handleZ_print_char, 230: handleZ_print_num, 231: handleZ_random, 232: handleZ_push, 233: handleZ_pull, 234: handleZ_split_window, 235: handleZ_set_window, 236: handleZ_call_vs, // call_vs2 237: handleZ_erase_window, 238: handleZ_erase_line, 239: handleZ_set_cursor, 240: handleZ_get_cursor, 241: handleZ_set_text_style, 242: handleZ_buffer_mode, 243: handleZ_output_stream, 244: handleZ_input_stream, 245: handleZ_sound_effect, 246: handleZ_read_char, 247: handleZ_scan_table, 248: handleZ_not, 249: handleZ_call_vn, 250: handleZ_call_vn, // call_vn2, 251: handleZ_tokenise, 252: handleZ_encode_text, 253: handleZ_copy_table, 254: handleZ_print_table, 255: handleZ_check_arg_count, 1000: handleZ_saveV45678, 1001: handleZ_restoreV45678, 1002: handleZ_log_shift, 1003: handleZ_art_shift, 1004: handleZ_set_font, //1005: draw_picture (V6 opcode) //1006: picture_dat (V6 opcode) //1007: erase_picture (V6 opcode) //1008: set_margins (V6 opcode) 1009: handleZ_save_undo, 1010: handleZ_restore_undo, 1011: handleZ_print_unicode, 1012: handleZ_check_unicode, //1013-1015: illegal //1016: move_window (V6 opcode) //1017: window_size (V6 opcode) //1018: window_style (V6 opcode) //1019: get_wind_prop (V6 opcode) //1020: scroll_window (V6 opcode) //1021: pop_stack (V6 opcode) //1022: read_mouse (V6 opcode) //1023: mouse_window (V6 opcode) //1024: push_stack (V6 opcode) //1025: put_wind_prop (V6 opcode) //1026: print_form (V6 opcode) //1027: make_menu (V6 opcode) //1028: picture_table (V6 opcode) }; // Differences between each version and v5. // Set a whole version to undefined if it's not implemented. // If a version is identical to v5, use '' rather than {} to // make the engine work with the original array rather than a copy. // When an opcode is illegal in the given version but not in v5, // it's marked with a zero. If you're working with a version which // doesn't support extended opcodes (below v5), don't worry about // zeroing out codes above 999-- they can't be accessed anyway. const handlers_fixups = { 1: { 25: 0, // call_2s 26: 0, // call_2n 27: 0, // set_colour 28: 0, // throw 136: 0, // call_1s 143: handleZ_not, // replaces call_1n 181: handleZ_saveV123, 182: handleZ_restoreV123, 185: handleZ_pop, // replaces catch 188: handleZ_show_status, 190: 0, // extended opcodes 191: 0, // piracy // 224 is shown in the ZMSD as being "call" before v4 and // "call_vs" thence; this appears to be simply a name change. // 228, similarly, is "sread" and then "aread". 236: 0, // call_vs 237: 0, // erase_window 238: 0, // erase_line 239: 0, // set_cursor 240: 0, // get_cursor 241: 0, // set_text_style 242: 0, // buffer_mode 246: 0, // read_char 247: 0, // scan_table, 248: 0, // not 249: 0, // call_vn 250: 0, // call_vn2 251: 0, // tokenise 252: 0, // encode_text 253: 0, // copy_table 254: 0, // print_table 255: 0, // check_arg_count }, 2: { 25: 0, // call_2s 26: 0, // call_2n 27: 0, // set_colour 28: 0, // throw 136: 0, // call_1s 143: handleZ_not, // replaces call_1n 181: handleZ_saveV123, 182: handleZ_restoreV123, 185: handleZ_pop, // replaces catch 188: handleZ_show_status, 190: 0, // extended opcodes 191: 0, // piracy // 224 is shown in the ZMSD as being "call" before v4 and // "call_vs" thence; this appears to be simply a name change. // 228, similarly, is "sread" and then "aread". 236: 0, // call_vs 237: 0, // erase_window 238: 0, // erase_line 239: 0, // set_cursor 240: 0, // get_cursor 241: 0, // set_text_style 242: 0, // buffer_mode 246: 0, // read_char 247: 0, // scan_table, 248: 0, // not 249: 0, // call_vn 250: 0, // call_vn2 251: 0, // tokenise 252: 0, // encode_text 253: 0, // copy_table 254: 0, // print_table 255: 0, // check_arg_count }, 3: { 25: 0, // call_2s 26: 0, // call_2n 27: 0, // set_colour 28: 0, // throw 136: 0, // call_1s 143: handleZ_not, // replaces call_1n 181: handleZ_saveV123, 182: handleZ_restoreV123, 185: handleZ_pop, // replaces catch 188: handleZ_show_status, 190: 0, // extended opcodes 191: 0, // piracy // 224 is shown in the ZMSD as being "call" before v4 and // "call_vs" thence; this appears to be simply a name change. // 228, similarly, is "sread" and then "aread". 236: 0, // call_vs 237: 0, // erase_window 238: 0, // erase_line 239: 0, // set_cursor 240: 0, // get_cursor 241: 0, // set_text_style 242: 0, // buffer_mode 246: 0, // read_char 247: 0, // scan_table, 248: 0, // not 249: 0, // call_vn 250: 0, // call_vn2 251: 0, // tokenise 252: 0, // encode_text 253: 0, // copy_table 254: 0, // print_table 255: 0, // check_arg_count }, 4: { // z4 is fittingly somewhere between z3 and z5 26: 0, // call_2n 27: 0, // set_colour 28: 0, // throw 143: handleZ_not, // replaces call_1n 181: handleZ_saveV45678, // was illegal in v5 (EXT used instead) 182: handleZ_restoreV45678, // ditto 185: handleZ_pop, // replaces catch 190: 0, // extended opcodes 191: 0, // piracy 248: 0, // not 249: 0, // call_vn 250: 0, // call_vn2 251: 0, // tokenise 252: 0, // encode_text 253: 0, // copy_table 254: 0, // print_table 255: 0, // check_arg_count }, 5: '', // The base copy *is* v5 6: undefined, // very complicated, and not yet implemented-- see bug 3621 7: '', // Defined to be the same as 5 8: '', // Defined to be the same as 5 }; //////////////////////////////////////////////////////////////// // // pc_translate_* // // Each of these functions returns a string of JS code to set the PC // to the address in |packed_target|, based on the current architecture. // // TODO: Would be good if we could pick up when it was a constant. function pc_translate_v123(p) { return '(('+p+')&0xFFFF)*2'; } function pc_translate_v45(p) { return '(('+p+')&0xFFFF)*4'; } function pc_translate_v67R(p) { return '(('+p+')&0xFFFF)*4+'+this.m_routine_start; } function pc_translate_v67S(p) { return '(('+p+')&0xFFFF)*4+'+this.m_string_start; } function pc_translate_v8(p) { return '(('+p+')&0xFFFF)*8'; } //////////////////////////////////////////////////////////////// // // PART THE THIRD // // THE NEW AMAZING COMPONENT WHICH PLAYS GAMES AND WASHES DISHES // AND LAYS THE TABLE AND WALKS THE DOG AND CLEANS THE OVEN AND... // //////////////////////////////////////////////////////////////// function gnusto_error(number) { message ='Component: engine\n'; for (var i=1; i<arguments.length; i++) { if (arguments[i] && arguments[i].toString) { message += '\nDetail: '+arguments[i].toString(); } } var procs = arguments.callee; var procstring = ''; var loop_count = 0; var loop_max = 100; while (procs!=null && loop_count<loop_max) { var name = procs.toString(); if (name==null) { procstring = ' (anon)'+procstring; } else { var r = name.match(/function (\w*)/); if (r==null) { procstring = ' (weird)' + procstring; } else { procstring = ' ' + r[1] + procstring; } } try { procs = procs.caller; } catch (e) { // A permission denied error may have just been raised, // perhaps because the caller is a chrome function that we // can't have access to. procs = null; } loop_count++; } if (loop_count==loop_max) { procstring = '...' + procstring; } message += '\n\nJS call stack:' + procstring; console.log("Error " + number + ": " + message); console.log('-- Temporary burin error: '); console.log(number); console.log(' '); console.log(message); console.log('\n'); throw 'Error '+number+': '+message; } //////////////////////////////////////////////////////////////// // // onISRReturn_... // // When a rebound function causes an interrupt, it may nominate // another function to clear up when the interrupt's done. // These are those functions. // // The PC will be reset before calling these functions; they // only have to deal with rebounds, causing further effects and // so on. // // Because these functions will be called as a result of an @return // (or @rtrue or whatever) you can be sure they're at the end of a // block in JITspace. // function onISRReturn_for_read_char(interrupt_info, result) { if (result) { // If an ISR returns true, we return as from the original // effect, storing zero for the keypress. interrupt_info.engine.m_answers[0] = 0; interrupt_info.rebound(); } else { // If an ISR returns false, we cause the same effect again. interrupt_info.engine.m_effects = interrupt_info.effects; interrupt_info.engine.m_rebound = interrupt_info.rebound; interrupt_info.engine.m_rebound_args = interrupt_info.rebound_args; } } function onISRReturn_for_read(interrupt_info, result) { var engine = interrupt_info.engine; if (result) { // If an ISR returns true, we return as from the original // effect. The terminating keypress is given as zero. // The contents of the text buffer are set to zero. engine.m_answers[0] = 0; // From this, the rebound will save the text and // parse buffers correctly: engine.m_answers[1] = ''; interrupt_info.rebound(); } else { // This is the really tricky part: // XXX FIXME: // If the effect has printed anything... what? engine.m_effects = interrupt_info.effects; engine.m_rebound = interrupt_info.rebound; engine.m_rebound_args = interrupt_info.rebound_args; } } //////////////////////////////////////////////////////////////// // // The Engine // // The object itself... function GnustoEngine(logfunc) { if (logfunc) this.logger = function(a, b) { logfunc("gnusto-engine: " + a + ": " + b); }; else this.logger = function() { }; } GnustoEngine.prototype = { //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// // // // PUBLIC METHODS // // // //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// loadStory: function ge_loadStory(sourceFile) { this.m_memory = sourceFile; this._initial_setup(); }, loadSavedGame: function ge_loadSavedGame(memLen, mem, mem_is_compressed, stacksLen, stacks, pc) { // FIXME: Still to do here: // There's a bit which should survive restore. // There are several bytes which should be reset (e.g. terp ID). function decodeStackInt(offset, length) { var result = stacks[offset++]; for (var i=1; i<length; i++) { result = (result<<8)|stacks[offset++]; } return result; } if (mem_is_compressed) { // Welcome to the decompression chamber. var temp = []; var cursor_compressed = 0; var cursor_original = 0; while (cursor_compressed < mem.length) { if (cursor_original >= this.m_original_memory.length) { // FIXME: proper error message gnusto_error(999, "overshoot in decompression"); } var candidate = mem[cursor_compressed++]; if (candidate == 0) { // Sequence of identical bytes. var run_length = mem[cursor_compressed++]+1; temp = temp.concat(this.m_original_memory.slice(cursor_original, cursor_original+run_length)); cursor_original += run_length; } else { // One different byte, XORed with the original. temp.push(candidate ^ this.m_original_memory[cursor_original++]); } } mem = temp; } // Firstly, zap all the important variables... // FIXME: Eventually we should work into copies, // and only move these over when we're sure everything's good. // Otherwise we could want to go back to how things were before. this.m_call_stack = []; this.m_gamestack = []; this.m_locals_stack = []; this.m_locals = []; this.m_result_targets = []; var evals_count = 0; // Pick up the amount of eval stack used by the bootstrap. evals_count = decodeStackInt(7, 1); this.m_gamestack_callbreaks = []; // Highest value yet pushed to m_gamestack_callbreaks var callbreaks_top = evals_count; var cursor = 8; for (var m=0; m<evals_count; m++) { this.m_gamestack.push(decodeStackInt(cursor, 2)); cursor+=2; } while (cursor<stacksLen) { this.m_call_stack.push(decodeStackInt(cursor, 3)); cursor+=3; //////////////////////////////////////////////////////////////// var flags = stacks[cursor++]; var varcode = stacks[cursor++]; if (flags & 0x10) { // Flag set to show that we should throw away // the result of this call. We represent that // by a varcode of -1. varcode = -1; } var locals_count = flags & 0xF; this.m_locals_stack.unshift(locals_count); this.m_result_targets.push(varcode); var logArgs = stacks[cursor++]+1; var argCount = 0; while (logArgs>1) { logArgs >>= 1; argCount++; } this.m_param_counts.unshift(argCount); evals_count = decodeStackInt(cursor, 2); cursor += 2; callbreaks_top += evals_count; this.m_gamestack_callbreaks.push(callbreaks_top); var locals_temp = []; for (var k=0; k<locals_count; k++) { locals_temp.push(decodeStackInt(cursor, 2)); cursor+=2; } this.m_locals = locals_temp.concat(this.m_locals); for (var m=0; m<evals_count; m++) { this.m_gamestack.push(decodeStackInt(cursor, 2)); cursor+=2; } } // Base locals aren't saved, so restore them as zeroes. for (var n=0; n<16; n++) { this.m_locals.push(0); } // Restore the memory. this.m_memory = mem.concat(this.m_memory.slice(mem.length)); if (this.m_version <= 3) { // This is pretty ugly, but then the design isn't too beautiful either. // The Quetzal code loads up with the PC pointing at the end of the @save // which saved it. The end is the half-an-instruction which gives a branch // address. (Ick.) But _brancher will compile that half-an-instruction into // JS of the form // if (<condition>){m_pc=<whatever>;return;} // So what we do is call |_brancher()| to compile this, and then // immediately evaluate the result to make the jump. The condition is // '1'-- we always want it to make the jump. And we have to wrap the // whole thing in a temporary function so that the "return" doesn't // mess things up. (The alternative would be to special-case // |_brancher()|, but this case is so very, very rare and perverted // that that seems inelegant.) this.m_pc = pc; eval("var t=new Function('with(this){'+_brancher('1')+'}');t.call(this);", this); } else { // The PC we're given is actually pointing at the varcode // into which the success code must be stored. It should be 2. // (This is specified by section 5.8 of the Quetzal document, // version 1.4.) this._varcode_set(2, this.m_memory[pc]); this.m_pc = pc+1; } }, resetStory: function ge_resetStory() { this.m_memory = this.m_original_memory.slice(); // Make a copy. this._initial_setup(); }, get version() { gnusto_error(101, "'version' not implemented"); }, get signature() { gnusto_error(101, "'signature' not implemented"); }, get cvsVersion() { return CVS_VERSION.substring(7, 26); }, setGoldenTrail: function ge_setGoldenTrail(value) { if (value) { this.m_goldenTrail = 1; } else { this.m_goldenTrail = 0; } }, setCopperTrail: function ge_setCopperTrail(value) { if (value) { this.m_copperTrail = 1; } else { this.m_copperTrail = 0; } }, effect: function ge_effect(which) { return this.m_effects[which]; }, answer: function ge_answer(which, what) { this.m_answers[which] = what; }, // Main point of entry for gnusto. Be sure to call start_game() // before calling this the first time. run: function ge_run() { //this.logger('run', answer); var start_pc = 0; var turns = 0; var jscode; var turns_limit = this.m_single_step? 1: 10000; if (this.m_rebound) { this.m_rebound(); this.m_rebound = 0; this.m_rebound_args = []; } this.m_effects = []; while(this.m_effects.length == 0) { if (turns++ >= turns_limit) { // Wimp out for now. // Can't use GNUSTO_EFFECT_WIMP_OUT directly // because it has "" around it. this.m_effects = ['WO']; return 1; } start_pc = this.m_pc; if (this.m_jit[start_pc]) { jscode = this.m_jit[start_pc]; } else { jscode=eval('dummy='+this._compile(), this); // Store it away, if it's in static memory (there's // not much point caching JIT from dynamic memory!) if (start_pc >= this.m_stat_start) { this.m_jit[start_pc] = jscode; } } // Some useful debugging code: if (this.m_copperTrail) { this.logger('pc : ' + start_pc.toString(16)); this.logger('jit : ' + jscode); } jscode(); } }, walk: function ge_walk(answer) { gnusto_error(101, "'walk' not implemented"); }, setRandomSeed: function ge_setRandomSeed(seed) { // This can be done by the private function _random_number(), // provided we give it a negative argument. We can also // pass it zero, which means the same to it as it does to us: // that we should return to nonseeded output. (Well, // non-obviously seeded anyway.) if (seed>0) { this._random_number(-seed); } else { this._random_number(seed); } }, //////////////////////////////////////////////////////////////// // // saveGame // // Saves a game out to a file. // saveGame: function ge_saveGame() { // Returns an array of |bytecount| integers, each // representing a byte of |number| in network byte order. function int_to_bytes(number, bytecount) { var result = []; result.length = bytecount; for (var i=0; i<bytecount; i++) { result[(bytecount-i)-1] = number & 0xFF; number >>= 8; } return result; } var state = this.m_state_to_save; var tag_FORM = [0x46, 0x4f, 0x52, 0x4d]; var tag_CMem = [0x43, 0x4d, 0x65, 0x6d]; var tag_UMem = [0x55, 0x4d, 0x65, 0x6d]; var tag_Stks = [0x53, 0x74, 0x6b, 0x73]; var content = [0x49, 0x46, 0x5a, 0x53, // IFZS 0x49, 0x46, 0x68, 0x64, // IFhd 0x00, 0x00, 0x00, 0x0d, // fixed length of 13 bytes state.m_memory[0x02], // Release number state.m_memory[0x03], state.m_memory[0x12], // Serial state.m_memory[0x13], state.m_memory[0x14], state.m_memory[0x15], state.m_memory[0x16], state.m_memory[0x17], state.m_memory[0x1C], // Checksum state.m_memory[0x1D], (state.m_pc>>16) & 0xFF, // PC (state.m_pc>> 8) & 0xFF, (state.m_pc ) & 0xFF, 0]; // pad if (this.m_compress_save_files) { var compressed = []; var same_count = 0; for (var i=0; i<this.m_stat_start; i++) { if (state.m_memory[i] == this.m_original_memory[i]) { same_count++; if (same_count == 256) { compressed.push(0); compressed.push(255); same_count = 0; } } else { if (same_count!=0) { compressed.push(0); compressed.push(same_count-1); same_count = 0; } compressed.push(state.m_memory[i]^this.m_original_memory[i]); } } if (same_count != 0) { // write out remaining same count compressed.push(0); compressed.push(same_count-1); } content = content.concat(tag_CMem); content = content.concat(int_to_bytes(compressed.length, 4)); content = content.concat(compressed); if ((compressed.length % 2) != 0) { // Odd number of bytes in the memory. Add one more. content.push(0); } } else { // Not using compressed memory. content = content.concat(tag_UMem); content = content.concat(int_to_bytes(this.m_stat_start, 4)); content = content.concat(this.m_memory.slice(0, this.m_stat_start)); if ((this.m_stat_start % 2) != 0) { // Odd number of bytes in the memory. Add one more. content.push(0); } } //////////////////////////////////////////////////////////////// // Write out the stacks. var stacks = [ // Firstly, the dummy first record 0x00, 0x00, 0x00, // PC 0x00, // flags 0x00, // varcode 0x00]; // args // And top it off with the amount of eval stack used. stacks = stacks.concat(int_to_bytes(this.m_gamestack_callbreaks[0], 2)); var locals_cursor = this.m_locals.length - 16; var gamestack_cursor = 0; for (var m=0; m<this.m_gamestack_callbreaks[0]; m++) { stacks = stacks.concat(int_to_bytes(this.m_gamestack[gamestack_cursor++], 2)); } for (var j=0; j<this.m_call_stack.length; j++) { stacks = stacks.concat(int_to_bytes(this.m_call_stack[j], 3)); // m_locals_stack is back to front so that we can always // refer to the current frame as m_l_s[x]. var local_count = this.m_locals_stack[this.m_locals_stack.length - (j+1)]; var flags = local_count; var target = this.m_result_targets[j]; // FIXME: This is ugly too. Why is m_p_c back to front? var args_supplied = this.m_param_counts[this.m_param_counts.length - (j+1)]; var eval_taken = this.m_gamestack_callbreaks[j] - gamestack_cursor; if (target==-1) { // This is a call-and-throw-away rather than a // call-and-store. We represent that with a magic // varcode of -1, but Quetzal sets a flag instead. target = 0; flags |= 0x10; } stacks = stacks.concat([flags, target, // I'm assuming that once a bit is set here, // all bits to its right are set too. // So we raise 2 to the power of the number // and subtract one. (1<<args_supplied)-1, (eval_taken>>8) & 0xFF, (eval_taken ) & 0xFF]); locals_cursor -= local_count; for (var k=0; k<local_count; k++) { stacks = stacks.concat(int_to_bytes(this.m_locals[locals_cursor+k], 2)); } for (var m=0; m<eval_taken; m++) { stacks = stacks.concat(int_to_bytes(this.m_gamestack[gamestack_cursor++], 2)); } } content = content.concat(tag_Stks); content = content.concat(int_to_bytes(stacks.length, 4)); content = content.concat(stacks); var quetzal = tag_FORM; quetzal = quetzal.concat(int_to_bytes(content.length, 4)); quetzal = quetzal.concat(content); this.m_quetzal_image = quetzal; return this.m_quetzal_image.length; }, saveGameData: function ge_saveGameData(len, result) { var temp = this.m_quetzal_image; this.m_quetzal_image = 0; return temp; }, get architecture() { return 'none'; }, get piracy() { return -1; }, get tandy() { return -1; }, get status() { return 'this is the status, hurrah!'; }, getStatusLine: function ge_getStatusLine(width) { //fnugry var current_room_object_number = this.getUnsignedWord(this.m_vars_start); var object_properties_address = this.getUnsignedWord(this.m_property_list_addr_start+(this.m_object_size*current_room_object_number)); var outtext = this._zscii_from(object_properties_address+1); if (outtext.length > width) { outtext = outtext.substring(0,width-3); var outtext2 = '...'; var spacebuffer = ''; } else { if ((this.m_version > 3) && ((this.getByte(1)&0x02)==2)) { // if it is a time game var hours = this.getUnsignedWord(this.m_vars_start+2); var minutes = this.getUnsignedWord(this.m_vars_start+4); if (minutes < 10) { var outtext2 = hours + ':0' + minutes; } else { var outtext2 = hours + ':' + minutes; } } else { // if it is a score game var outtext2 = 'Score: ' + this.getUnsignedWord(this.m_vars_start+2) + ' Moves: ' + this.getUnsignedWord(this.m_vars_start+4); } if ((outtext.length + outtext2.length + 1) > width) { outtext2 = ' S:' + this.getUnsignedWord(this.m_vars_start+2) + ' M:' + this.getUnsignedWord(this.m_vars_start+4); if ((outtext.length + outtext2.length + 1) > width) { outtext2 = ' ' + this.getUnsignedWord(this.m_vars_start+2) + '/' + this.getUnsignedWord(this.m_vars_start+4); } if ((outtext.length + outtext2.length + 1) > width) { outtext2 = ''; } } var spacebuffer = ''; while ((outtext.length + outtext2.length + spacebuffer.length) < width) { spacebuffer += ' '; } } return outtext + spacebuffer + outtext2; }, //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// // // // PRIVATE METHODS // // // //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// // _initial_setup // // Initialises our variables. // _initial_setup: function ge_initial_setup() { this.m_jit = []; this.m_compilation_running = 0; this.m_gamestack = []; this.m_gamestack_callbreaks = []; this.m_call_stack = []; this.m_locals = []; this.m_locals_stack = []; this.m_param_counts = []; this.m_result_targets = []; this.m_goldenTrail = 0; this.m_copperTrail = 0; this.m_version = this.m_memory[0]; this.m_himem = this.getUnsignedWord(0x4); this.m_pc = this.getUnsignedWord(0x6); this.m_dict_start = this.getUnsignedWord(0x8); this.m_objs_start = this.getUnsignedWord(0xA); this.m_vars_start = this.getUnsignedWord(0xC); this.m_stat_start = this.getUnsignedWord(0xE); this.m_abbr_start = this.getUnsignedWord(0x18); if (this.m_version>=4) { this.m_alpha_start = this.getUnsignedWord(0x34); this.m_object_tree_start = this.m_objs_start + 112; this.m_property_list_addr_start = this.m_object_tree_start + 12; this.m_object_size = 14; } else { this.m_alpha_start = 0; this.m_object_tree_start = this.m_objs_start + 53; this.m_property_list_addr_start = this.m_object_tree_start + 7; this.m_object_size = 9; } this.m_hext_start = this.getUnsignedWord(0x36); this.m_original_memory = this.m_memory.slice(); // Make a copy. // Use the correct addressing mode for this Z-machine version... if (this.m_version<=3) { // Versions 1 and 2 (prehistoric) and 3 ("Standard") this.m_pc_translate_for_routine = pc_translate_v123; this.m_pc_translate_for_string = pc_translate_v123; } else if (this.m_version<=5) { // Versions 4 ("Plus") and 5 ("Advanced") this.m_pc_translate_for_routine = pc_translate_v45; this.m_pc_translate_for_string = pc_translate_v45; } else if (this.m_version<=7) { // Versions 6 (the graphical one) and 7 (rare postInfocom extension) this.m_routine_start = this.getUnsignedWord(0x28)*8; this.m_string_start = this.getUnsignedWord(0x2a)*8; this.m_pc_translate_for_routine = pc_translate_v67R; this.m_pc_translate_for_string = pc_translate_v67S; } else if (this.m_version==8) { // Version 8 (normal postInfocom extension) this.m_pc_translate_for_routine = pc_translate_v8; this.m_pc_translate_for_string = pc_translate_v8; } else { gnusto_error(170, 'impossible: unknown z-version got this far'); } // And pick up the relevant instruction set. if (!(this.m_version in handlers_fixups)) { gnusto_error(311, 'unknown z-machine version'); } var fixups = handlers_fixups[this.m_version]; switch (typeof(fixups)) { case 'undefined': gnusto_error(101, 'z-machine version not implemented'); break; case 'string': this.m_handlers = handlers_v578; break; case 'object': this.m_handlers = {}; for (var original in handlers_v578) { this.m_handlers[original] = handlers_v578[original]; } for (var changed in fixups) { if ((typeof fixups[changed])=='function') { this.m_handlers[changed] = fixups[changed]; } else { delete this.m_handlers[changed]; } } break; default: gnusto_error(170, 'impossible: weird stuff in fixups table'); } // Set up separators. this.m_separator_count = this.m_memory[this.m_dict_start]; for (var i=0; i<this.m_separator_count; i++) { this.m_separators[i]=this._zscii_char_to_ascii(this.m_memory[this.m_dict_start + i+1]); } // If there is a header extension... if (this.m_hext_start > 0) { // get start of custom unicode table, if any this.m_unicode_start = this.getUnsignedWord(this.m_hext_start+6); if (this.m_unicode_start > 0) { // if there is one, get the char count-- characters beyond that point are undefined. this.m_custom_unicode_charcount = this.m_memory[this.m_unicode_start]; this.m_unicode_start += 1; } } this.m_rebound = 0; this.m_rebound_args = []; this.m_output_to_console = 1; this.m_streamthrees = []; this.m_output_to_script = 0; this.m_console_buffer = ''; this.m_transcript_buffer = ''; this.m_zalphabet[0] = 'abcdefghijklmnopqrstuvwxyz'; this.m_zalphabet[1] = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // T = magic ten bit flag if (this.m_version==1) { this.m_zalphabet[2] = 'T0123456789.,!?_#\'"/\\<-:()'; } else { this.m_zalphabet[2] = 'T\n0123456789.,!?_#\'"/\\-:()'; } var newchar; var newcharcode; if (this.m_alpha_start > 0) { // If there's a custom alphabet... for (var alpharow=0; alpharow<3; alpharow++){ var alphaholder = ''; for (var alphacol=0; alphacol<26; alphacol++) { newcharcode = this.m_memory[this.m_alpha_start + (alpharow*26) + alphacol]; if ((newcharcode >=155) && (newcharcode <=251)) { // Yes, custom alphabets can refer to custom unicode tables. Whee... if (this.m_unicode_start == 0) { alphaholder += String.fromCharCode(default_unicode_translation_table[newcharcode]); } else { if ((newcharcode-154)<= this.m_custom_unicode_charcount) alphaholder += String.fromCharCode(this.getUnsignedWord(this.m_unicode_start + ((newcharcode-155)*2))); else alphaholder += ' '; } } else { newchar = String.fromCharCode(newcharcode); if (newchar == '^') newchar = '\n'; // This is hackish, but I don't know a better way. alphaholder += newchar; } } this.m_zalphabet[alpharow]= alphaholder; // Replace the current row with the newly constructed one. } } // We don't also reset the debugging variables, because // they need to persist across re-creations of this object. // FIXME: Is this still true? // Clear the Z-engine's local variables. for (var i=0; i<16; i++) this.m_locals[i]=0; this.m_printing_header_bits = 0; this.m_leftovers = ''; }, getByte: function ge_getbyte(address) { if (address<0) { address &= 0xFFFF; } return this.m_memory[address]; }, setByte: function ge_setByte(value, address) { if (address<0) { address &= 0xFFFF; } this.m_memory[address] = value; }, getWord: function ge_getWord(address) { if (address<0) { address &= 0xFFFF; } return this._unsigned2signed((this.m_memory[address]<<8)| this.m_memory[address+1]); }, _unsigned2signed: function ge_unsigned2signed(value) { return ((value & 0x8000)?~0xFFFF:0)|value; }, _signed2unsigned: function ge_signed2unsigned(value) { return value & 0xFFFF; }, getUnsignedWord: function ge_getUnsignedWord(address) { if (address<0) { address &= 0xFFFF; } return (this.m_memory[address]<<8)|this.m_memory[address+1]; }, setWord: function ge_setWord(value, address) { if (address<0) { address &= 0xFFFF; } this.setByte((value>>8) & 0xFF, address); this.setByte((value) & 0xFF, address+1); }, // Inelegant function to load parameters according to a VAR byte (or word). _handle_variable_parameters: function ge_handle_var_parameters(args, types, bytecount) { var argcursor = 0; if (bytecount==1) { types = (types<<8) | 0xFF; } while (1) { var current = types & 0xC000; if (current==0xC000) { return; } else if (current==0x0000) { args[argcursor++] = this.getWord(this.m_pc); this.m_pc+=2; } else if (current==0x4000) { args[argcursor++] = this.m_memory[this.m_pc++]; } else if (current==0x8000) { args[argcursor++] = this._code_for_varcode(this.m_memory[this.m_pc++]); } else { gnusto_error(171); // impossible } types = (types << 2) | 0x3; } }, // _compile() returns a string of JavaScript code representing the // instruction at the program counter (and possibly the next few // instructions, too). It will change the PC to point to the end of the // code it's compiled. _compile: function ge_compile() { this.m_compilation_running = 1; code = ''; var starting_pc = this.m_pc; do { // List of arguments to the opcode. var args = []; this_instr_pc = this.m_pc; // Add the touch (see bug 4687). This lets us track progress simply. code = code + '_touch('+this.m_pc+');'; // So here we go... // what's the opcode? var instr = this.m_memory[this.m_pc++]; if (instr==0) { // If we just get a zero, we've probably // been directed off into deep space somewhere. gnusto_error(201); // lost in space } else if (instr==190) { // Extended opcode. instr = 1000+this.m_memory[this.m_pc++]; this._handle_variable_parameters(args, this.m_memory[this.m_pc++], 1); } else if (instr & 0x80) { if (instr & 0x40) { // Variable params if (!(instr & 0x20)) // This is a 2-op, despite having // variable parameters; reassign it. instr &= 0x1F; if (instr==250 || instr==236) { // We get more of them! var types = this.getUnsignedWord(this.m_pc); this.m_pc += 2; this._handle_variable_parameters(args, types, 2); } else this._handle_variable_parameters(args, this.m_memory[this.m_pc++], 1); } else { // Short. All 1-OPs except for one 0-OP. switch(instr & 0x30) { case 0x00: args[0] = this.getWord(this.m_pc); this.m_pc+=2; instr = (instr & 0x0F) | 0x80; break; case 0x10: args[0] = this.m_memory[this.m_pc++]; instr = (instr & 0x0F) | 0x80; break; case 0x20: args[0] = this._code_for_varcode(this.m_memory[this.m_pc++]); instr = (instr & 0x0F) | 0x80; break; case 0x30: // 0-OP. We don't need to get parameters, but we // *do* need to translate the opcode. instr = (instr & 0x0F) | 0xB0; break; } } } else { // Long if (instr & 0x40) args[0] = this._code_for_varcode(this.m_memory[this.m_pc++]); else args[0] = this.m_memory[this.m_pc++]; if (instr & 0x20) args[1] = this._code_for_varcode(this.m_memory[this.m_pc++]); else args[1] = this.m_memory[this.m_pc++]; instr &= 0x1F; } if (this.m_handlers[instr]) { code = code + this.m_handlers[instr](this, args)+';'; //this.logger(code,''); } else if (instr>=1128 && instr<=1255 && "special_instruction_EXT"+(instr-1000) in this) { // ZMSD 14.2: We provide a hook for plug-in instructions. // FIXME: This will no longer work in a component. // Can we do anything else instead? // (Maybe a component named @gnusto.org/specialinstr?op=XXX.) code = code + this["special_instruction_EXT"+(instr-1000)](args)+ ';'; //this.logger(code,''); } else { gnusto_error(200, this.m_pc.toString(16)); // no handler } } while(this.m_compilation_running); // When we're not in debug mode, dissembly only stops at places where // the THIS.M_PC must be reset; but in debug mode it's perfectly possible // to have |code| not read or write to the PC at all. So we need to // set it automatically at the end of each fragment. if (this.m_single_step||this.m_debug_mode) { code = code + 'm_pc='+this.m_pc; //this.logger(code,''); } // Name the function after the starting position, to make life // easier for Venkman. return 'function J'+starting_pc.toString(16)+'(){'+code+'}'; }, _param_count: function ge_param_count() { return this.m_param_counts[0]; }, _set_output_stream: function ge_set_output_stream(target, address) { if (target==0) { // then it's a no-op. } else if (target==1) { this.m_output_to_console = 1; } else if (target==2) { this.m_memory[0x10] |= 0x1; } else if (target==3) { if (this.m_streamthrees.length>15) { gnusto_error(202); // too many nested stream-3s } this.m_streamthrees.unshift([address, address+2]); } else if (target==4) { this.m_output_to_script = 1; } else if (target==-1) { this.m_output_to_console = 0; } else if (target==-2) { this.m_memory[0x10] &= ~0x1; } else if (target==-3) { if (this.m_streamthrees.length<1) { gnusto_error(203); // not enough nested stream-3s } var latest = this.m_streamthrees.shift(); this.setWord((latest[1]-latest[0])-2, latest[0]); } else if (target==-4) { this.m_output_to_script = 0; } else { gnusto_error(204, target); // weird output stream number } }, _trunc_divide: function ge_trunc_divide(over, under) { var result; if (under==0) { gnusto_error(701); // division by zero return 0; } result = over / under; if (result > 0) { return Math.floor(result); } else { return Math.ceil(result); } }, _zscii_char_to_ascii: function ge_zscii_char_to_ascii(zscii_code) { if (zscii_code<0 || zscii_code>1023) { gnusto_error(702, zscii_code); // illegal zscii code } var result; if (zscii_code==13 || zscii_code==10) { result = 10; } else if ((zscii_code>=32 && zscii_code<=126) || zscii_code==0) { result = zscii_code; } else if (zscii_code>=155 && zscii_code<=251) { // Extra characters. if (this.m_unicode_start == 0) return String.fromCharCode(default_unicode_translation_table[zscii_code]); else { // if we're using a custom unicode translation table... if ((zscii_code-154)<= this.m_custom_unicode_charcount) return String.fromCharCode(this.getUnsignedWord(this.m_unicode_start + ((zscii_code-155)*2))); else gnusto_error(703, zscii_code); // unknown zscii code } // FIXME: It's not clear what to do if they request a character // that's off the end of the table. } else { //let's do nothing for the release-- we'll check the spec afterwards. // FIXME: what release was that, and what are we doing now? // Is there anything in Bugzilla to track this? return "*";//gnusto_error(703, zscii_code); // unknown zscii code } return String.fromCharCode(result); }, _random_number: function ge_random_number(arg) { if (arg==0) { // zero returns to true random mode-- seed from system clock this.m_random_use_seed = this.m_random_use_sequence = 0; return 0; } else if (arg<-999) { // Large negative numbers cause us to enter a predictable // but non-sequential state. (In other words, the numbers // always come in the same order, but can't be trivially // predicted by humans.) this.m_random_state = Math.abs(arg); this.m_random_use_seed = 1; this.m_random_use_sequence = 0; return 0; } else if (arg<0) { // Small negative numbers cause us to enter a predictable sequential // state: according to the spec, 1, 2, 3 ... arg, 1, 2, 3... // (but according to Frotz, 1, 2, 3... arg-1, 1, 2, 3...) // BTW, the spec says "lower than 1000", but this is clearly // an error, because *all* negative numbers are lower than // 1000. We follow Frotz's lead in treating this as -1000 // and using the absolute value of the argument as the // sequence wrapping point. this.m_random_sequence_max = Math.abs(arg)-1; this.m_random_state = 0; this.m_random_use_seed = 0; this.m_random_use_sequence = 1; return 0; } else { // Positive argument. So they actually want a random number, // between 1 and arg inclusive. // Are we using any sort of predictable seeding? if (this.m_random_use_seed) { // Yes, given a particular seed. this.m_random_state--; return 1+(Math.round(Math.abs(Math.tan(this.m_random_state))*8.71*arg)%arg); } else if (this.m_random_use_sequence) { // Yes, given a particular sequence. var previous = this.m_random_state; this.m_random_state = this.m_random_state+1; if (this.m_random_state > this.m_random_sequence_max) { this.m_random_state = 0; } return 1 + (previous % arg); } else { // No. Use JS's random numbers. // (Hope these are generally good enough.) return 1 + Math.round((arg -1) * Math.random()); } } gnusto_error(170, 'random'); // impossible }, //////////////////////////////////////////////////////////////// // // _func_gosub // // Jumps to a subroutine within the Z-code, saving the current // state so that calling _func_return() will return to it. // // |to_address| -- address within the Z-code to jump to // |actuals| -- list of actual parameters // |from_address| -- source address // |result_target| -- varcode for where to put the result // _func_gosub: function ge_gosub(to_address, actuals, from_address, result_target) { this.m_call_stack.push(from_address); this.m_pc = to_address; var count = this.m_memory[this.m_pc++]; // Before version 5, Z-code put initial values for formal parameters // into the code itself. If we're running a version earlier than z5, // we have to interpret these. if (this.m_version<5) { var templocals = []; for (var i3=0; i3<count; i3++) { if (i3<actuals.length) { templocals.push(actuals[i3]); } else { templocals.push(this.getWord(this.m_pc)); } this.m_pc += 2; } this.m_locals = templocals.concat(this.m_locals); } else { for (var i5=count; i5>0; i5--) { if (i5<=actuals.length) { this.m_locals.unshift(actuals[i5-1]); } else { this.m_locals.unshift(0); } } } this.m_locals_stack.unshift(count); this.m_param_counts.unshift(actuals.length); this.m_result_targets.push(result_target); this.m_gamestack_callbreaks.push(this.m_gamestack.length); if (to_address==0) { // Rare special case: a call to 0 returns only false. this._func_return(0); } }, //////////////////////////////////////////////////////////////// // // _func_interrupt // // Like _func_gosub, except that it's used to break into a // running routine. This may only be called from a rebound function. // // |to_address| -- address of Z-code interrupt service routine. // |on_return| -- function to call when the routine is finished. // // |on_return| will be called with two parameters: // |info| : an object containing these fields: // |rebound|: saved value of m_rebound // |pc|: saved value of m_pc // |result|: the value the ISR returned. // _func_interrupt: function ge_interrupt(to_address, on_return) { this.m_interrupt_information.push({ 'on_return': on_return, 'rebound': this.m_rebound, 'rebound_args': this.m_rebound_args, 'engine': this, 'pc': this.m_pc, 'effects': this.m_effects, }); this._func_gosub(to_address, [], CALLED_FROM_INTERRUPT, -1); }, //////////////////////////////////////////////////////////////// // // Tokenises a string. // // See aread() for caveats. // Maybe we should allow aread() to pass in the correct value stored // in text_buffer, since it knows it already. It means we don't have // to figure it out ourselves. // _tokenise: function ge_tokenise(text_buffer, parse_buffer, dictionary, overwrite) { var tokenised_word_count = 0; var cursor = parse_buffer + 2; var words_count_addr = parse_buffer + 1; if (isNaN(dictionary)) dictionary = 0; if (isNaN(overwrite)) overwrite = 0; function look_up(engine, word, dict_addr) { function compare(engine, typed, mem_addr) { var j=0; var mem_char, typed_char; while (1) { if (j==typed.length) { // then they're the same return 0; } mem_char = engine.m_memory[mem_addr+j]; typed_char = typed.charCodeAt(j); if (mem_char==typed_char) { j++; } else if (mem_char<typed_char) { // less than... return -1; } else { return 1; } } } var entry_length = engine.m_memory[dict_addr+engine.m_separator_count+1]; var entries_count = engine.getWord(dict_addr+engine.m_separator_count+2); var entries_start = engine.m_dict_start+engine.m_separator_count+4; // Whether the dictionary is sorted. var is_sorted = 1; if (entries_count < 0) { // This should actually only happen on user dictionaries, // but the distinction isn't a useful one, and so we don't // bother to check. is_sorted = 0; entries_count = -entries_count; } var oldword = word; word = engine._into_zscii(word); if (is_sorted) { var low=0, high=entries_count-1; var median; var median_address; var comparison; while(1) { median = low + Math.round((high-low)/2); median_address = entries_start+median*entry_length; comparison = compare(engine, word, median_address); if (comparison<0) { if (low==high) { return 0; } low = median+1; } else if (comparison>0) { if (low==high) { return 0; } high = median-1; } else { return median_address; } if (low>high) { return 0; } } } else { // Unsorted search. Much simpler, but slower for (var i=0; i<entries_count; i++) { var address = entries_start+i*entry_length; if (compare(engine, word, address)==0) { return address; } } } return 0; } function add_to_parse_table(engine, dictionary, curword, wordpos) { var lexical = look_up(engine, curword, dictionary); if (!(overwrite && lexical==0)) { engine.setWord(lexical, cursor); cursor+=2; engine.setByte(curword.length, cursor++); engine.setByte(wordpos, cursor++); } else { // In overwrite mode, if we don't know a word, we skip // the corresponding record. cursor +=4; } tokenised_word_count++; return 1; } //////////////////////////////////////////////////////////////// // // Prepare |source|, a string containing all the characters in // text_buffer. (FIXME: Why don't we just work out of text_buffer?) var max_chars = this.m_memory[text_buffer]; var source = ''; if (dictionary==0) { // Use the standard game dictionary. dictionary = this.m_dict_start; } if (this.m_version <= 4) { max_chars ++; // Value stored in pre-z5 is one too low. var copycursor = text_buffer + 1; while(1) { var ch = this.m_memory[copycursor++]; if (ch==0) break; source += String.fromCharCode(ch); } } else { for (var i=0;i<this.m_memory[text_buffer + 1];i++) { source += String.fromCharCode(this.m_memory[text_buffer + 2 + i]); } } var words = []; var curword = ''; var wordindex = 0; var wordpos_increment; if (this.m_version <= 4) { wordpos_increment = 1; } else { wordpos_increment = 2; } // FIXME: Do this with regexps, for goodness' sake. for (var cpos=0; cpos < source.length; cpos++) { if (source[cpos] == ' ') { if (curword != '') { words[wordindex] = curword; add_to_parse_table(this, dictionary, words[wordindex], (cpos - words[wordindex].length) + wordpos_increment); wordindex++; curword = ''; } } else { if (this._is_separator(source[cpos])) { if (curword != '') { words[wordindex] = curword; add_to_parse_table(this, dictionary, words[wordindex], (cpos - words[wordindex].length) + wordpos_increment); wordindex++; } words[wordindex] = source[cpos]; add_to_parse_table(this, dictionary, words[wordindex], cpos + wordpos_increment); wordindex++; curword = ''; } else { curword += source[cpos]; } } } if (curword != '') { words[wordindex] = curword; add_to_parse_table(this, dictionary, words[wordindex], (cpos - words[wordindex].length) + wordpos_increment); } this.setByte(tokenised_word_count, words_count_addr); }, // Very very very limited implementation: // * Doesn't handle word separators. (FIXME: Does it yet?) // FIXME: Consider having no parameters; they're always filled in from // the same fields anyway. _aread: function ge_aread(terminating_keypress, text_buffer, parse_buffer, entered) { text_buffer &= 0xFFFF; parse_buffer &= 0xFFFF; var max_chars; var result; if (!entered) { entered = ' '; } if (this.m_version <= 4) { // In z1-z4, the array is null-terminated. max_chars = this.m_memory[text_buffer]+1; result = entered.substring(0,max_chars).toLowerCase(); for (var i=0;i<result.length;i++) { this.setByte(result.charCodeAt(i), text_buffer + 1 + i); } this.setByte(0, text_buffer + 1 + result.length); } else { // In z5-z8, the array starts with a size byte. max_chars = this.m_memory[text_buffer]; result = entered.substring(0,max_chars); this.setByte(result.length, text_buffer + 1); for (var i=0;i<result.length;i++) { this.setByte(result.charCodeAt(i), text_buffer + 2 + i); } } if (parse_buffer!=0 || this.m_version<5) { this._tokenise(text_buffer, parse_buffer, 0, 0); } if (terminating_keypress == 13) { return 10; // goodness knows why, but it's in the spec } else { return terminating_keypress; } }, // Returns a list of current terminating characters. // ASCII 13 will always be in the list, since the Enter key // is always a terminating character. _terminating_characters: function ge_terminating_characters() { if (this.m_version < 5) { // Versions before z5 don't have terminating character tables. return '\r'; } else { var terms_address = this.getWord(0x2e); var result = '\r'; while(1) { var ch = this.m_memory[terms_address++]; if (ch==0) { // Zero is a terminator. break; } else if ((ch>=129 && ch<=154) || (ch>=252)) { // Only function-key codes make it into the string. result += String.fromCharCode(ch); } } return result; } }, //////////////////////////////////////////////////////////////// // // _func_return // // Returns from a Z-code routine. // // |value| -- the numeric result of the routine. // It can also be null, in which case the store // won't happen (useful for returning from @throw). // _func_return: function ge_func_return(value) { for (var i=this.m_locals_stack.shift(); i>0; i--) { this.m_locals.shift(); } this.m_param_counts.shift(); this.m_pc = this.m_call_stack.pop(); // Force the gamestack to be the length it was when this // routine started. (ZMSD 6.3.2.) this.m_gamestack.length = this.m_gamestack_callbreaks.pop(); var target = this.m_result_targets.pop(); if (target!=-1 && value!=null) { this._varcode_set(value, target); } if (this.m_pc == CALLED_FROM_INTERRUPT) { var interrupt_info = this.m_interrupt_information.pop(); this.m_pc = interrupt_info.pc; interrupt_info.on_return(interrupt_info, value); } }, _throw_stack_frame: function throw_stack_frame(cookie) { // The cookie is the value of call_stack.length when @catch was // called. It cannot be less than 1 or greater than the current // value of call_stack.length. if (cookie>this.m_call_stack.length || cookie<1) { gnusto_error(207, cookie); } while (this.m_call_stack.length > cookie-1) { this._func_return(null); } }, _get_prop_addr: function ge_get_prop_addr(object, property) { if (object==0) {return 0;} var result = this._property_search(object, property, -1); if (result[2]) { return result[0]; } else { return 0; } }, _get_prop_len: function ge_get_prop_len(address) { address &= 0xFFFF; if (this.m_version<4) { return 1+(this.m_memory[address-1] >> 5); } else { // The last byte before the data is either the size byte of a 2-byte // field, or the only byte of a 1-byte field. We can tell the // difference using the top bit. var value = this.m_memory[address-1]; if (value & 0x80) { // A two-byte field, so we take the bottom five bits. value = value & 0x3F; if (value==0) { return 64; } else { return value; } } else { // A one-byte field. Our choice rests on a single bit. if (value & 0x40) { return 2; } else { return 1; } } } gnusto_error(170, 'get_prop_len'); // impossible }, _get_next_prop: function ge_get_next_prop(object, property) { if (object==0) return 0; // Kill that V0EFH before it starts. var result = this._property_search(object, -1, property); if (result[2]) { // There's a real property number in there; // return it. return result[3]; } else { // There wasn't a valid property following the one // we wanted. Why not? if (result[4]) { // Because the one we wanted was the last one. // Tell them to go back to square one. return 0; } else { // Because the one we wanted didn't exist. // They shouldn't have asked for it: barf. gnusto_error(205, property); } } gnusto_error(173); // impossible }, _get_prop: function ge_get_prop(object, property) { if (object==0) return 0; // Kill that V0EFH before it starts. var temp = this._property_search(object, property, -1); if (temp[1]==2) { return this.getWord(temp[0]); } else if (temp[1]==1) { return this.m_memory[temp[0]]; // should this be treated as signed? } else { // get_prop used on a property of the wrong length // Christminster (and perhaps others) use this vaguely // illegal hack to just check to see if a property exists // for a particular object. Evidently, only Zinc throws an // error here, so we probably shouldn't either. Also, currently // calling gnusto_error here tanks the browser. Don't yet know // why. So commenting it out for now. // gnusto_error(706, object, property); return this.getWord(temp[0]); } gnusto_error(174); // impossible }, // This is the function which does all searching of property lists. // It takes three parameters: // |object| -- the number of the object you're interested in // // The next parameters allow us to specify the property in two ways. // If you use both, it will "or" them together. // |property| -- the number of the property you're interested in, // or -1 if you don't mind. // |previous_property| -- the number of the property BEFORE the one // you're interested in, or 0 if you want the first one, // or -1 if you don't mind. // // If you specify a valid property, and the property doesn't exist, this // function will return the default value instead (and tell you it's done so). // // The function returns an array with these elements: // [0] = the property address. // [1] = the property length. // [2] = 1 if this property really belongs to the object, or // 0 if it doesn't (and if it doesn't, and you've specified // a valid |property|, then [0] and [1] will be properly // set to defaults.) // [3] = the number of the property. // Equal to |property| if you specified it. // May be -1, if |property| is -1 and [2]==0. // [4] = a piece of state only useful to get_next_prop(): // if the object does not contain the property (i.e. if [2]==0) // then this will be 1 if the final property was equal to // |previous_property|, and 0 otherwise. At all other times it will // be 0. _property_search: function ge_property_search(object, property, previous_property) { // Find the address of the property table. var props_address = this.getUnsignedWord(this.m_property_list_addr_start + object*this.m_object_size); // Skip the property table's header. props_address = props_address + this.m_memory[props_address]*2 + 1; // Now loop over each property and consider it. var previous_prop = 0; while(1) { var len = 1; var prop = this.m_memory[props_address++]; if (this.m_version < 4) { len = (prop>>5)+1; prop = prop & 0x1F; } else { if (prop & 0x80) { // Long format. len = this.m_memory[props_address++] & 0x3F; if (len==0) len = 64; } else { // Short format. if (prop & 0x40) len = 2; } prop = prop & 0x3F; } if (prop==property || previous_prop==previous_property) { return [props_address, len, 1, prop, 0]; } else if (prop < property) { // So it's not there. Can we get it from the defaults? if (property>0) // Yes, because it's a real property. return [this.m_objs_start + (property-1)*2, 2, 0, property, 0]; else // No: they didn't specify a particular // property. return [-1, -1, 0, property, previous_prop==property]; } props_address += len; previous_prop = prop; } gnusto_error(175); // impossible }, //////////////////////////////////////////////////////////////// // Functions that modify the object tree _set_attr: function ge_set_attr(object, bit) { if (object==0) return; // Kill that V0EFH before it starts. var address = this.m_object_tree_start + object*this.m_object_size + (bit>>3); var value = this.m_memory[address]; this.setByte(value | (128>>(bit%8)), address); }, _clear_attr: function ge_clear_attr(object, bit) { if (object==0) return; // Kill that V0EFH before it starts. var address = this.m_object_tree_start + object*this.m_object_size + (bit>>3); var value = this.m_memory[address]; this.setByte(value & ~(128>>(bit%8)), address); }, _test_attr: function ge_test_attr(object, bit) { if (object==0) return 0; // Kill that V0EFH before it starts. if ((this.m_memory[this.m_object_tree_start + object*this.m_object_size +(bit>>3)] & (128>>(bit%8)))) { return 1; } else { return 0; } }, _put_prop: function put_prop(object, property, value) { if (object == 0) return; var address = this._property_search(object, property, -1); if (!address[2]) { gnusto_error(704); // undefined property } if (address[1]==1) { this.setByte(value & 0xff, address[0]); } else if (address[1]==2) { this.setWord(value&0xffff, address[0]); } else { gnusto_error(705); // weird length } }, _get_older_sibling: function ge_get_older_sibling(object) { if (object==0) { return 0;} // Start at the eldest child. var candidate = this._get_child(this._get_parent(object)); if (object==candidate) { // evidently nothing doing there. return 0; } while (candidate) { next_along = this._get_sibling(candidate); if (next_along==object) { return candidate; // Yay! Got it! } candidate = next_along; } // We ran out, so the answer's 0. return 0; }, _insert_obj: function ge_insert_obj(mover, new_parent) { // First, remove mover from wherever it is in the tree now. var old_parent = this._get_parent(mover); var older_sibling = this._get_older_sibling(mover); var younger_sibling = this._get_sibling(mover); if (old_parent && this._get_child(old_parent)==mover) { this._set_child(old_parent, younger_sibling); } if (older_sibling) { this._set_sibling(older_sibling, younger_sibling); } // Now, slip it into the new place. this._set_parent(mover, new_parent); if (new_parent) { this._set_sibling(mover, this._get_child(new_parent)); this._set_child(new_parent, mover); } }, // FIXME: Why the new_parent?! _remove_obj: function ge_remove_obj(mover, new_parent) { this._insert_obj(mover, 0); }, _get_family: function ge_get_family(from, relationship) { if (from==0) {return 0;} if (this.m_version < 4) { return this.m_memory[this.m_object_tree_start + 4+relationship + from*this.m_object_size]; } else { // v4 and above. return this.getUnsignedWord(this.m_object_tree_start + 6+relationship*2 + from*this.m_object_size); } gnusto_error(170, 'get_family'); // impossible }, _get_parent: function ge_get_parent(from) { return this._get_family(from, PARENT_REC); }, _get_child: function ge_get_child(from) { return this._get_family(from, CHILD_REC); }, _get_sibling: function ge_get_sibling(from) { return this._get_family(from, SIBLING_REC); }, _set_family: function ge_set_family(from, to, relationship) { if (this.m_version < 4) { this.setByte(to, this.m_object_tree_start + 4+relationship + from*this.m_object_size); } else { // v4 and above. this.setWord(to, this.m_object_tree_start + 6+relationship*2 + from*this.m_object_size); } }, _set_parent: function ge_set_parent(from, to) { this._set_family(from, to, PARENT_REC); }, _set_child: function ge_set_child(from, to) { this._set_family(from, to, CHILD_REC); }, _set_sibling: function ge_set_sibling(from, to) { this._set_family(from, to, SIBLING_REC); }, _obj_in: function ge_obj_in(child, parent) { return this._get_parent(child) == parent; }, //////////////////////////////////////////////////////////////// // Implements @copy_table, as in the Z-spec. _copy_table: function ge_copy_table(first, second, size) { if (second==0) { // Zero out the first |size| bytes of |first|. for (var i=0; i<size; i++) { this.setByte(0, i+first); } } else { // Copy |size| bytes of |first| into |second|. var copy_forwards = 0; if (size<0) { size = -size; copy_forwards = 1; } else { if (first > second) { copy_forwards = 1; } else { copy_forwards = 0; } } if (copy_forwards) { for (var i=0; i<size; i++) { this.setByte(this.m_memory[first+i], second+i); } } else { for (var i=size-1; i>=0; i--) { this.setByte(this.m_memory[first+i], second+i); } } } }, //////////////////////////////////////////////////////////////// // Implements @scan_table, as in the Z-spec. _scan_table: function ge_scan_table(target_word, target_table, table_length, table_form) { // TODO: Optimise this some. var jumpby = table_form & 0x7F; var usewords = ((table_form & 0x80) == 0x80); var lastlocation = target_table + (table_length*jumpby); if (usewords) { //if the table is in the form of word values while (target_table < lastlocation) { if (((this.m_memory[0xFFFF&target_table]&0xFF) == ((target_word>>8)&0xFF)) && ((this.m_memory[0xFFFF&target_table+1]&0xFF) == (target_word&0xFF))) { return target_table; } target_table += jumpby; } } else { //if the table is in the form of byte values while (target_table < lastlocation) { if ((this.m_memory[0xFFFF&target_table]&0xFF) == (target_word&0xFFFF)) { return target_table; } target_table += jumpby; } } return 0; }, //////////////////////////////////////////////////////////////// // Returns the lines that @print_table should draw, as in // the Z-spec. // // It's rather poorly defined there: // * How is the text in memory encoded? // [Straight ZSCII, not five-bit encoded.] // * What happens to the cursor? Moved? // [We're guessing not.] // * Is the "table" a table in the Z-machine sense, or just // a contiguous block of memory? // [Just a contiguous block.] // * What if the width takes us off the edge of the screen? // * What if the height causes a [MORE] event? // // It also goes largely un-noted that this is the only possible // way to draw on the lower window away from the current // cursor position. (If we take the view that v5 windows are // roughly the same thing as v6 windows, though, windows don't // "own" any part of the screen, so there's no such thing as // drawing on the lower window per se.) // // FIXME: Add note that we now start with G_E_PRINTTABLE _print_table: function ge_print_table(address, width, height, skip) { var lines = []; for (var y=0; y<height; y++) { var s=''; for (var x=0; x<width; x++) { if (address<0) { address &= 0xFFFF; } s=s+this._zscii_char_to_ascii(this.m_memory[address++]); } lines.push(s); address += skip; } var result = ['PT', lines.length]; result = result.concat(lines); return result; }, _zscii_from: function ge_zscii_from(address, max_length, tell_length) { if (address in this.m_jit) { this.logger('zscii_from ' + address,'already in THIS.M_JIT'); // Already seen this one. if (tell_length) return this.m_jit[address]; else return this.m_jit[address][0]; } var temp = ''; var running = 1; var start_address = address; var home_alph=0; var alph = home_alph; // Should be: // -2 if we're not expecting a ten-bit character // -1 if we are, but we haven't seen any of it // n if we've seen half of one, where n is what we've seen var tenbit = -2; // Should be: // 0 if we're not expecting an abbreviation // z if we are, where z is the prefix var abbreviation = 0; if (!max_length) max_length = 65535; var stopping_place = address + max_length; while (running) { var word = this.getUnsignedWord(address); address += 2; running = ((word & 0x8000)==0) && address<stopping_place; for (var j=2; j>=0; j--) { var code = ((word>>(j*5))&0x1f); if (abbreviation) { temp = temp + this._zscii_from(this.getUnsignedWord((32*(abbreviation-1)+code)*2+this.m_abbr_start)*2); abbreviation = 0; alph=home_alph; } else if (tenbit==-2) { if (code>5) { if (alph==2 && code==6) tenbit = -1; else { temp = temp + this.m_zalphabet[alph][code-6]; alph = home_alph; } } else { if (code==0) { temp = temp + ' '; alph=home_alph; } else if (code<4) { if (this.getByte(0) > 2) {abbreviation = code;} else { if (code==2){ alph += 1; if (alph > 2) {alph=0;} } else if (code==3) { alph -= 1; if (alph < 0) {alph=2;} } else { if (this.getByte(0)==2) { abbreviation=1;} else { temp = temp + '\n'; //in z-1 this is a newline alph = home_alph; } } } } else { if (this.getByte(0) > 2) {alph = code-3;} else { if (code==4){ home_alph += 1; if (home_alph > 2) {home_alph=0;} } else { home_alph -= 1; if (home_alph < 0) {home_alph=2;} } alph=home_alph; } } } } else if (tenbit==-1) { tenbit = code; } else { temp = temp + this._zscii_char_to_ascii((tenbit<<5) + code); tenbit = -2; alph=home_alph; } } } if (start_address >= this.m_stat_start) { this.m_jit[start_address] = [temp, address]; } this.logger('zscii_from ' + address,temp); if (tell_length) { return [temp, address]; } else { return temp; } }, //////////////////////////////////////////////////////////////// // // encode_text // // Implements the @encode_text opcode. // |zscii_text|+|from| is the address of the unencoded text. // |length| is its length. // (It may also be terminated by a zero byte.) // |coded_text| is where to put the six bytes of encoded text. _encode_text: function ge_encode_text(zscii_text, length, from, coded_text) { zscii_text = (zscii_text + from) & 0xFFFF; var source = ''; while (length>0) { var b = this.m_memory[zscii_text]; if (b==0) break; source = source + this._zscii_char_to_ascii(b); zscii_text++; length--; } var result = this._into_zscii(source); for (var i=0; i<result.length; i++) { var c = result[i].charCodeAt(0); this.setByte(c, coded_text++); } }, //////////////////////////////////////////////////////////////// // // Encodes the ZSCII string |str| to its compressed form, // and returns it. // _into_zscii: function ge_into_zscii(str) { var result = ''; var buffer = []; var dictionary_entry_length; if (this.m_version < 4) { dictionary_entry_length = 4; } else { dictionary_entry_length = 6; } function emit(value) { buffer.push(value); if (buffer.length==3) { var temp = (buffer[0]<<10 | buffer[1]<<5 | buffer[2]); if (result.length == dictionary_entry_length-2) { // This'll be the last word. We need to set the stop bit. temp |= 0x8000; } result = result + String.fromCharCode(temp >> 8) + String.fromCharCode(temp & 0xFF); buffer = []; } } // Need to handle other alphabets. At present we only // handle alphabetic characters (A0). // Also need to handle ten-bit characters. // FIXME: Are the above still true? var cursor = 0; while (cursor<str.length && result.length<6) { var ch = str.charCodeAt(cursor++); if (ch>=65 && ch<=90) { // A to Z // These are NOT mapped to A1. ZSD3.7 // explicitly forbids use of upper case // during encoding. emit(ch-59); } else if (ch>=97 && ch<=122) { // a to z emit(ch-91); } else { var z2 = this.m_zalphabet[2].indexOf(String.fromCharCode(ch)); if (z2!=-1) { if (this.getByte(0)>2) { emit(5); // shift to weird stuff } else { emit(3);} //use a shift as 5 is shift_lock in z1-2 emit(z2+6); } else { if (this.getByte(0)>2) { emit(5); } else { emit(3);} //use a shift as 5 is shift_lock in z1-2 emit(6); emit(ch >> 5); emit(ch & 0x1F); } } } while (result.length<dictionary_entry_length) { emit(5); } return result.substring(0, dictionary_entry_length); }, _name_of_object: function ge_name_of_object(object) { if (object==0) { return "<void>"; } else { var aa = this.m_property_list_addr_start + object*this.m_object_size; return this._zscii_from(this.getUnsignedWord(aa)+1); } }, //////////////////////////////////////////////////////////////// // // Function to print the contents of leftovers. _print_leftovers: function ge_print_leftovers() { this._zOut(this.m_leftovers); // May as well clear it out and save memory, // although we won't be called again until it's // set otherwise. this.m_leftovers = ''; }, //////////////////////////////////////////////////////////////// // // Prints the text |text| on whatever input streams are // currently enabled. // // If this returns false, the text has been printed. // If it returns true, the text hasn't been printed, but // you must return the GNUSTO_EFFECT_FLAGS_CHANGED effect // to your caller. (There's a function handler_zOut() // which does all this for you.) _zOut: function ge_zOut(text) { if (this.m_streamthrees.length) { // Stream threes disable any other stream while they're on. // (And they can't cause flag changes, I suppose.) var current = this.m_streamthrees[0]; var address = this.m_streamthrees[0][1]; for (var i=0; i<text.length; i++) { this.setByte(text.charCodeAt(i), address++); } this.m_streamthrees[0][1] = address; } else { var bits = this.m_memory[0x10] & 0x03; var changed = bits != this.m_printing_header_bits; effect_parameters = this.m_printing_header_bits; this.m_printing_header_bits = bits; // OK, so should we bail? if (changed) { this.m_leftovers = text; this.m_rebound = this._print_leftovers; return 1; } else { if (this.m_output_to_console) { this.m_console_buffer = this.m_console_buffer + text; } if (bits & 1) { this.m_transcript_buffer = this.m_transcript_buffer + text; } } } return 0; }, //////////////////////////////////////////////////////////////// consoleText: function ge_console_text() { var temp = this.m_console_buffer.replace('\x00','','g'); this.m_console_buffer = ''; return temp; }, _transcript_text: function ge_transcript_text() { var temp = this.m_transcript_buffer.replace('\x00','','g'); this.m_transcript_buffer = ''; return temp; }, //////////////////////////////////////////////////////////////// _is_separator: function ge_IsSeparator(value) { for (var sepindex=0; sepindex < this.m_separator_count; sepindex++) { if (value == this.m_separators[sepindex]) return 1; } return 0; }, //////////////////////////////////////////////////////////////// // // code_for_varcode // // should one day be replaced by varcode_[sg]et, probably. // _code_for_varcode: function ge_code_for_varcode(varcode) { if (varcode==0) { return 'm_gamestack.pop()'; } else if (varcode < 0x10) { return 'm_locals['+(varcode-1)+']'; } else { return 'getWord('+(this.m_vars_start+(varcode-16)*2)+')'; } gnusto_error(170, 'code_for_varcode'); // impossible }, //////////////////////////////////////////////////////////////// // // varcode_get // // Retrieves the value specified by |varcode|, and returns it. // |varcode| is interpreted as in ZSD 4.2.2: // 0 = pop from game stack // 1-15 = local variables // 16 up = global variables // // TODO: We need a varcode_getcode() which returns a JS string // which will perform the same job as this function, to save us // the extra call we use when encoding "varcode_get(constant)". _varcode_get: function ge_varcode_get(varcode) { if (varcode==0) { return this.m_gamestack.pop(); } else if (varcode < 0x10) { return this.m_locals[(varcode-1)]; } else { return this.getWord(this.m_vars_start+(varcode-16)*2); } gnusto_error(170, 'varcode_get'); // impossible }, //////////////////////////////////////////////////////////////// // // varcode_set // // Stores the value |value| in the place specified by |varcode|. // |varcode| is interpreted as in ZSD 4.2.2. // 0 = push to game stack // 1-15 = local variables // 16 up = global variables // // TODO: We need a varcode_setcode() which returns a JS string // which will perform the same job as this function, to save us // the extra call we use when encoding "varcode_set(n, constant)". _varcode_set: function ge_varcode_set(value, varcode) { if (varcode==0) { this.m_gamestack.push(value); } else if (varcode < 0x10) { this.m_locals[varcode-1] = value; } else { this.setWord(value, this.m_vars_start+(varcode-16)*2); } }, _brancher: function ge_brancher(condition) { var inverted = 1; var temp = this.m_memory[this.m_pc++]; var target_address = temp & 0x3F; if (temp & 0x80) { inverted = 0; } if (!(temp & 0x40)) { target_address = (target_address << 8) | this.m_memory[this.m_pc++]; // and it's signed... if (target_address & 0x2000) { // sign bit's set; propagate it target_address = (~0x1FFF) | (target_address & 0x1FFF); } } var if_statement = condition; if (inverted) { if_statement = 'if(!('+if_statement+'))'; } else { if_statement = 'if('+if_statement+')'; } // Branches to the addresses 0 and 1 are actually returns with // those values. if (target_address == 0) { return if_statement + '{_func_return(0);return;}'; } if (target_address == 1) { return if_statement + '{_func_return(1);return;}'; } target_address = (this.m_pc + target_address) - 2; // This is an optimisation that's currently unimplemented: // if there's no code there, we should mark that we want it later. // [ if (!this.m_jit[target_address]) this.m_jit[target_address]=0; ] return if_statement + '{m_pc='+(target_address)+';return;}'; }, _storer: function ge_storer(rvalue) { var lvalue_varcode = this.m_memory[this.m_pc++]; if (rvalue.substring && rvalue.substring(0,11)=='_func_gosub') { // Special case: the results of gosubs can't // be stored synchronously. this.m_compilation_running = 0; // just to be sure we stop here. if (rvalue.substring(rvalue.length-4)!=',-1)') { // You really shouldn't pass us gosubs with // the result target filled in. gnusto_error(100, rvalue); // can't modify gosub } return rvalue.substring(0,rvalue.length-3) + lvalue_varcode + ')'; // Otherwise it must be a synchronous write, so... } else if (lvalue_varcode==0) { return 'm_gamestack.push('+rvalue+')'; } else if (lvalue_varcode < 0x10) { return 'm_locals['+(lvalue_varcode-1)+']='+rvalue; } else { return 'setWord('+rvalue+','+(this.m_vars_start+(lvalue_varcode-16)*2)+')'; } gnusto_error(170, 'storer'); // impossible }, //////////////////////////////////////////////////////////////// // // _generate_gosub // // Returns a JITstring which enters a new Zroutine (by calling // _func_gosub). // // |target| is the packed address to jump to. (The packing // algorithm varies according to the version of the Z-machine // we're using. // // |args| is something whose string representation is a // comma-delimited list of actual parameters to the function. // (An array is fine for this, as is a single number, as is // an empty string.) // // If |get_varcode| is defined and nonzero, we read one byte // and use that varcode as the return target of the call. // Otherwise the call will throw away its result. // _generate_gosub: function call_vn(target, arguments, get_varcode) { this.m_compilation_running = 0; var varcode = -1; if (get_varcode) { varcode = this.m_memory[this.m_pc++]; } return '_func_gosub('+ this.m_pc_translate_for_routine(target)+','+ '['+arguments.toString()+'],'+ this.m_pc+','+ varcode+')'; }, //////////////////////////////////////////////////////////////// // Returns a JS string that calls zOut() correctly to print // the line of text in |text|. (See zOut() for details of // what constitutes "correctly".) // // If |is_return| is set, the result will cause a Z-machine // return with a result of 1 (the same result as @rtrue). // If it's clear, the result will set the PC to the // address immediately after the current instruction. // _handler_zOut: function ge_handler_zOut(text, is_return) { var setter; if (is_return) { setter = '_func_return(1)'; } else { setter = 'm_pc=0x'+this.m_pc.toString(16); } return 'if(_zOut('+text+')){' + setter + ';m_effects=['+ GNUSTO_EFFECT_FLAGS_CHANGED + '];return 1}'; }, //////////////////////////////////////////////////////////////// // Returns a JS string which will print the text encoded // immediately after the current instruction. // // |suffix| is a string to add to the encoded string. It may // be null, in which case no string will be added. // // |is_return| is passed through unchanged to handler_zOut() // (this function is written in terms of that function). // See the comments for that function for details. _handler_print: function ge_handler_print(suffix, is_return) { var zf = this._zscii_from(this.m_pc,65535,1); var message = zf[0]; if (suffix) message = message + suffix; message=message. replace('\\','\\\\','g'). replace('"','\\"','g'). replace('\n','\\n','g'); // not elegant this.m_pc=zf[1]; this.logger('print',message); return this._handler_zOut('"'+message+'"', is_return); }, _log_shift: function ge_log_shift(value, shiftbits) { // log_shift logarithmic-bit-shift. Right shifts are zero-padded if (shiftbits < 0) { return (value >>> (-1* shiftbits)) & 0x7FFF; } else { return (value << shiftbits) & 0x7FFF; } }, _art_shift: function ge_art_shift(value, shiftbits) { // arithmetic-bit-shift. Right shifts are sign-extended if (shiftbits < 0) { return (value >> (-1* shiftbits)) & 0x7FFF; } else { return (value << shiftbits) &0x7FFF; } }, _touch: function ge_touch(address) { // Check for a breakpoint. // Actually, don't for now: we have no plans as to what we'd do // if we found one. // if (this.m_pc in this.m_breakpoints) { // if (address in this.m_breakpoints) { // if (this.m_breakpoints[address]==2) { // // A breakpoint we've just reurned from. // this.m_breakpoints[addr]=1; // set it ready for next time // return 0; // it doesn't trigger again this time. // } else if (this.m_breakpoints[addr]==1) { // // a genuine breakpoint! // this.m_pc = address; // return 1; // } // // return 0; // } // } if (this.m_goldenTrail) { this.logger("pc : "+address.toString(16)); } this.m_pc = address; }, _save_undo: function ge_save_undo(varcode_offset) { this.m_undo = this._saveable_state(varcode_offset); return 1; }, //////////////////////////////////////////////////////////////// // // _restore_undo // // Restores the undo information saved in m_undo. // // Returns zero if the restore failed, nonzero if it succeeded. // (If the function returns nonzero, the caller should return // control immediately rather than continuing on with the current // block of instructions: the PC is already set up. Think of it as // a particularly funky kind of goto.) // _restore_undo: function ge_restore_undo() { if (typeof this.m_undo != 'object') { // No undo information is saved in m_undo. return 0; } this.m_call_stack = this.m_undo.m_call_stack; this.m_locals = this.m_undo.m_locals; this.m_locals_stack = this.m_undo.m_locals_stack; this.m_param_counts = this.m_undo.m_param_counts; this.m_result_targets = this.m_undo.m_result_targets; this.m_gamestack = this.m_undo.m_gamestack; var mem = this.m_undo.m_memory; this.m_memory = mem.concat(this.m_memory.slice(mem.length)); // The PC we're given is actually pointing at the varcode // into which the success code must be stored. It should be 2. // (This is specified by section 5.8 of the Quetzal document, // version 1.4.) this._varcode_set(2, this.m_memory[this.m_undo.m_pc]); this.m_pc = this.m_undo.m_pc+1; // OK, clear that out so we can't un-undo. this.undo = 0; return 1; }, _saveable_state: function ge_saveable_state(varcode_offset) { var result = { 'm_memory': this.m_memory.slice(0, this.m_stat_start), 'm_pc': this.m_pc + varcode_offset, // move onto target varcode, 'm_call_stack': this.m_call_stack, 'm_locals': this.m_locals, 'm_locals_stack': this.m_locals_stack, 'm_param_counts': this.m_param_counts, 'm_result_targets': this.m_result_targets, 'm_gamestack': this.m_gamestack, }; return result; }, // Returns nonzero iff the memory verifies correctly for @verify // in our copy of the original file of this game, m_original_memory. // That is, all bytes after the header must total to the checksum // given in the header. We use the value in the orignal file's // header for comparison, not the one in the current header. _verify: function ge_verify() { var total = 0; var checksum = (this.m_original_memory[0x1c]<<8 | this.m_original_memory[0x1d]); for (var i=0x40; i<this.m_original_memory.length; i++) { total += this.m_original_memory[i]; } return (total & 0xFFFF) == checksum; }, //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// // // // PRIVATE VARIABLES // // // //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// // This will hold the filename of the current game file (so we can // reset the memory from it as needed. // XXXFIXME: this implies things about where we get game data! m_local_game_file: 0, // These are all initialised in the function start_game(). // The actual memory of the Z-machine, one byte per element. m_memory: [], // Hash mapping Z-code instructions to functions which return a // JavaScript string to handle them. m_handlers: 0, // |this.m_jit| is a cache for the results of compile(): it maps // memory locations to JS function objects. Theoretically, // executing the function associated with a given address is // equivalent to executing the z-code at that address. // // Note: the results of dissembly in dynamic memory should never // be put into this array, since the code can change. // // Planned features: // 1) compile() should know about this array, and should stop // dissembly if its program counter reaches any key in it. // 2) putting a flag value (probably zero) into this array will // have the effect of 1), but won't stop us replacing it with // real code later. m_jit: [], // If this is nonzero, the engine will report as it passes each instruction. m_goldenTrail: 0, // When this is nonzero, we should print JIT information to the burin, // for debugging. m_copperTrail: 0, // In ordinary use, compile() attempts to make the functions // it creates as long as possible. Sometimes, though, we have to // stop dissembling (for example, when we reach a RETURN) or it // will seem a good idea (say, when we meet an unconditional jump). // In such cases, a subroutine anywhere along the line may set // |m_compilation_running| to 0, and compile() will stop after the current // iteration. m_compilation_running: 0, // |gamestack| is the Z-machine's stack. m_gamestack: 0, // Stack which stores the depths of |m_gamestack| at each function call // on the function stack. (Quetzal needs to know this.) m_gamestack_callbreaks: [], // |himem| is the high memory mark. This is rarely used in practice; // we might stop noting it. m_himem: 0, // |pc| is the Z-machine's program counter. // // During compilation: // it points at the place in memory which we're currently decoding. // This may be in the middle of an instruction. (See m_current_instr // for a way not to have this problem.) // During execution (within or outside JITspace): // it points to the next address to be executed. It gets set // using _touch(). m_pc: 0, // |this_instr_pc| is the address of the start of the current instruction. // during compilation. This is not identical to |m_pc|, because that // can point to addresses within the middles of instructions. m_this_instr_pc: 0, // |dict_start| is the address of the dictionary in the Z-machine's memory. m_dict_start: 0, // |objs_start| is the address of the object table in the Z-machine's memory. m_objs_start: 0, // |vars_start| is the address of the global variables in the Z-machine's // memory. m_vars_start: 0, // |stat_start| is the address of the bottom of static memory. // Anything below this can change during the games. Anything // above this does not change like the shifting shadows. m_stat_start: 0, // Address of the start of the abbreviations table in memory. (Can this // be changed? If not, we could decode them all first.) m_abbr_start: 0, // Address of the start of the header extension table in memory. m_hext_start: 0, // Address of custom alphabet table (if any). m_alpha_start: 0, // Holder for the z-alphabet m_zalphabet: [], // Address of start of strings. // Only used in versions 6 and 7; otherwise undefined. m_string_start: 0, // Address of start of routines. // Only used in versions 6 and 7; otherwise undefined. m_routine_start: 0, // Address of Unicode Translation Table (if any). m_unicode_start: 0, m_custom_unicode_charcount: 0, // Information about the defined list of word separators m_separator_count: 0, m_separators: [], // |version| is the current Z-machine version. m_version: 0, // |call_stack| stores all the return addresses for all the functions // which are currently executing. m_call_stack: 0, // |locals| is an array of the Z-machine's local variables. m_locals: [], // |locals_stack| is a stack of the values of |locals| for functions // further down the call stack than the current function. m_locals_stack: 0, // |param_counts| is an array which stores the number of parameters for // each of the variables on |call_stack|, and the current function (so // that the number of parameters to the current function is in // param_counts[0]). (Hmm, that's a bit inconsistent. We should change it.) m_param_counts: 0, // |result_targets| is a stack whose use parallels |call_stack|. Instead of // storing return addresses, though, |result_targets| stores varcodes to // store a function's result into as it returns. For example, if a // function contains: // // b000: locals[7] = foo(locals[1]) // b005: something else // // and we're just now returning from the call to foo() in b000, the only // legitimate value we can set the PC to is b005 (b000 would cause an // infinite loop, after all), but we can't go on to b005 because we haven't // finished executing b000 yet. So on the top of |result_targets| there'll be // a varcode which represents locals[7]. Varcodes are as defined in ZSD 4.2.2: // 0 = push to game stack // 1-15 = local variables // 16 up = global variables // // Also, the magic value -1 causes the function's result to be thrown away. m_result_targets: [], // The function object to run first next time run() gets called, // before any other execution gets under way. Its argument will be the // |answer| formal parameter of run(). It can also be 0, which // is a no-op. run() will clear it to 0 after running it, whatever // happens. m_rebound: 0, // Any extra arguments for m_rebound. m_rebound_args: [], // Whether we're writing output to the ordinary screen (stream 1). m_output_to_console: 0, // Stream 2 is whether we're writing output to a game transcript, // but the state for that is stored in a bit in "Flags 2" in the header. // A list of streams writing out to main memory (collectively, stream 3). // The stream at the start of the list is the current one. // Each stream is represented as a list with two elements: [|start|, |end|]. // |start| is the address at the start of the memory block where the length // of the block will be stored after the stream is closed. |end| is the // current end of the block. m_streamthrees: [], // Whether we're writing copies of input to a script file (stream 4). // fixme: This is really something we need to tell the environment about, // since we can't deal with it ourselves. m_output_to_script: 0, // FIXME: Clarify the distinction here // If this is 1, run() will "wimp out" after every opcode. m_single_step: 0, m_debug_mode: 0, m_parser_debugging: 0, // Hash of breakpoints. If compile() reaches one of these, it will stop // before executing that instruction with GNUSTO_EFFECT_BREAKPOINT, and the // PC set to the address of that instruction. // // The keys of the hash are opcode numbers. The values are not yet stably // defined; at present, all values should be 1, except that if a breakpoint's // value is 2, it won't trigger, but it will be reset to 1. m_breakpoints: {}, // Buffer of text written to console. m_console_buffer: '', // Buffer of text written to transcript. m_transcript_buffer: '', // |effects| holds the current effect in its zeroth element, and any // parameters needed in the following elements. m_effects: [], // |answers| is a list of answers to an effect, given by the environment. m_answers: [], m_random_state: 0, m_random_use_seed: 0, m_random_use_sequence: 0, m_random_sequence_max: 0, // Values of the bottom two bits in Flags 2 (address 0x10), // used by the zOut function. // See <http://mozdev.org/bugs/show_bug.cgi?id=3344>. m_printing_header_bits: 0, // Leftover text which should be printed next run(), since // we couldn't print it this time because the flags had // changed. m_leftovers: '', // These pointers point at the currently-selected functions: m_pc_translate_for_routine: pc_translate_v45, m_pc_translate_for_string: pc_translate_v45, // If this is an object, it should contain copies of other // properties of the engine object with the same names, // backed up for @save_undo. These should be the same as // their namesakes at the moment of saving, except that: // m_memory needs only to hold dynamic memory // m_pc points at the } address (<z5) { which receives the // } varcode (>=z5) { success result. // // If this is not an object, no undo information is saved. // (Future: It should eventually be an array, for multiple undo.) m_undo: 0, // Like m_undo, but only well-defined during a save effect. m_state_to_save: 0, // Saved Quetzal image. Only well-defined between a call to saveGame() // and a call to saveGameData(). m_quetzal_image: 0, // Original state of the story file. Used when saving to produce // a compressed image by comparison. // // FIXME: This should make the restart effect redundant. // Make it so. m_original_memory: [], // Whether to save compressed or uncompressed games. This trades // having small files for saving slightly faster, and isn't // really worth it. We may hardwire it on permanently. m_compress_save_files: 1, // Offset of the (notional) 0th entry in the object tree from the // start of the object block, in bytes. (This is the size of the // property defaults table less m_object_size.) m_object_tree_start: 0, // Address of the property list address within the (notional) 0th // entry in the object block. This is m_object_tree_start plus // the offset within the record for that field (which varies by // architecture). m_property_list_addr_start: 0, // Size of an object in the objects table, in bytes. m_object_size: 14, // A stack of information about the routines that were suspended // for the current interrupt service routines to do their jobs. // Usually ISRs don't interrupt other ISRs, so this stack will have // either one or no elements. m_interrupt_information: [], }; // EOF gnusto-engine.js //