UNPKG

ifvms

Version:

The Interactive Fiction Virtual Machines Suite - in Javascript

1,059 lines (932 loc) 22.8 kB
/* Z-Machine runtime functions =========================== Copyright (c) 2020 The ifvms.js team MIT licenced https://github.com/curiousdannii/ifvms.js */ 'use strict'; /* TODO: Save/restore: table, name, prompt support */ const file = require( '../common/file.js' ) const utils = require( '../common/utils.js' ) const extend = utils.extend const U2S = utils.U2S16 const S2U = utils.S2U16 // A clone function which ignores the properties we don't want to serialise function clone( obj ) { const recurse = obj => typeof obj === 'object' ? clone( obj ) : obj const newobj = {} if ( Array.isArray( obj ) ) { return obj.map( recurse ) } for ( let prop in obj ) { if ( prop !== 'buffer' && prop !== 'str' ) { newobj[prop] = recurse( obj[prop] ) } } return newobj } // Test whether we are running on a littleEndian system const littleEndian = (function() { var testUint8Array = new Uint8Array( 2 ), testUint16Array = new Uint16Array( testUint8Array.buffer ); testUint16Array[0] = 1; return testUint8Array[0] === 1; })() function fix_stack_endianness( view, start, end, auto ) { if ( littleEndian && !auto ) { while ( start < end ) { view.setUint16( start, view.getUint16( start, 1 ) ); start += 2; } } } module.exports = { art_shift: function( number, places ) { return places > 0 ? number << places : number >> -places; }, // Call a routine call: function( addr, storer, next, args ) { // 6.4.3: Calls to 0 instead just store 0 if ( addr === 0 ) { if ( storer >= 0 ) { this.variable( storer, 0 ); } return this.pc = next; } // Get the number of locals and advance the pc this.pc = addr * this.addr_multipler; var locals_count = this.m.getUint8( this.pc++ ), stack = this.stack, i = 0, // Write the current stack use frameptr = this.frameptr; stack.setUint16( frameptr + 6, this.sp ); this.frames.push( frameptr ); // Create a new frame frameptr = this.frameptr = this.s.byteOffset + this.sp * 2; // Return address stack.setUint32( frameptr, next << 8 ); // Flags stack.setUint8( frameptr + 3, ( storer >= 0 ? 0 : 0x10 ) | locals_count ); // Storer stack.setUint8( frameptr + 4, storer >= 0 ? storer : 0 ); // Supplied arguments stack.setUint8( frameptr + 5, ( 1 << args.length ) - 1 ); // Create the locals and stack this.make_stacks(); this.sp = 0; while ( i < locals_count ) { this.l[i] = i < args.length ? args[i] : ( this.version < 5 ? this.m.getUint16( this.pc + i * 2 ) : 0 ); i++; } if ( this.version < 5 ) { this.pc += locals_count * 2; } }, clear_attr: function( object, attribute ) { var addr = this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0; this.ram.setUint8( addr, this.m.getUint8( addr ) & ~( 0x80 >> attribute % 8 ) ); }, copy_table: function( first, second, size ) { size = U2S( size ); var ram = this.ram, i = 0, allowcorrupt = size < 0; size = Math.abs( size ); // Simple case, zeroes if ( second === 0 ) { while ( i < size ) { ram.setUint8( first + i++, 0 ); } return; } if ( allowcorrupt ) { while ( i < size ) { ram.setUint8( second + i, this.m.getUint8( first + i++ ) ); } } else { ram.setUint8Array( second, this.m.getUint8Array( first, size ) ); } }, do_autorestore: function( snapshot ) { const Glk = this.Glk // Restore Glk Glk.restore_allstate( snapshot.glk ) // Get references to our Glk objects this.io = snapshot.io const RockBox = new Glk.RefBox() let obj while ( obj = Glk.glk_window_iterate( obj, RockBox ) ) { if ( RockBox.value === 201 ) { this.mainwin = obj if ( obj.linebuf ) { snapshot.read_data.buffer = obj.linebuf } } if ( RockBox.value === 202 ) { this.statuswin = obj } if ( RockBox.value === 203 ) { this.upperwin = obj } } obj = null while ( obj = Glk.glk_stream_iterate( obj, RockBox ) ) { if ( RockBox.value === 210 ) { this.io.streams[2].str = obj } if ( RockBox.value === 211 ) { this.io.streams[4].str = obj } } // Restart and restore the RAM and stacks this.restart(1) this.restore_file(this.options.Dialog.streaming ? new Uint8Array(snapshot.ram) : Uint8Array.from(snapshot.ram), 1) // Set remaining data from the snapshot this.read_data = snapshot.read_data this.xorshift_seed = snapshot.xorshift_seed }, do_autosave: function( save ) { if ( !this.options.Dialog ) { throw new Error( 'A reference to Dialog is required' ) } let snapshot = null if ( ( save || 0 ) >= 0 ) { const ram = this.save_file(this.pc, 1) snapshot = { glk: this.Glk.save_allstate(), io: clone( this.io ), ram: this.options.Dialog.streaming ? ram : Array.from(new Uint8Array(ram)), read_data: clone( this.read_data ), xorshift_seed: this.xorshift_seed, } } this.options.Dialog.autosave_write( this.signature, snapshot ) }, encode_text: function( zscii, length, from, target ) { this.ram.setUint8Array( target, this.encode( this.m.getUint8Array( zscii + from, length ) ) ); }, // Access the extension table extension_table: function( word, value ) { var addr = this.extension; if ( !addr || word > this.extension_count ) { return 0; } addr += 2 * word; if ( value === undefined ) { return this.m.getUint16( addr ); } this.ram.setUint16( addr, value ); }, // Find the address of a property, or given the previous property, the number of the next find_prop: function( object, property, prev ) { var memory = this.m, version3 = this.version3, this_property_byte, this_property, last_property = 0, // Get this property table properties = memory.getUint16( this.objects + ( version3 ? 9 : 14 ) * object + ( version3 ? 7 : 12 ) ); // Skip over the object's short name properties += memory.getUint8( properties ) * 2 + 1; // Run through the properties while ( 1 ) { this_property_byte = memory.getUint8( properties ); this_property = this_property_byte & ( version3 ? 0x1F : 0x3F ); // Found the previous property, so return this one's number if ( last_property === prev ) { return this_property; } // Found the property! Return its address if ( this_property === property ) { // Must include the offset return properties + ( !version3 && this_property_byte & 0x80 ? 2 : 1 ); } // Gone past the property if ( this_property < property ) { return 0; } // Go to next property last_property = this_property; // Calculate the size of this property and skip to the next if ( version3 ) { properties += ( this_property_byte >> 5 ) + 2; } else { if ( this_property_byte & 0x80 ) { this_property = memory.getUint8( properties + 1 ) & 0x3F; properties += this_property ? this_property + 2 : 66; } else { properties += this_property_byte & 0x40 ? 3 : 2; } } } }, // 1.2 spec @gestalt gestalt: function( id /*, arg*/ ) { switch ( id ) { case 1: return 0x0102; } return 0; }, // Get the first child of an object get_child: function( obj ) { if ( this.version3 ) { return this.m.getUint8( this.objects + 9 * obj + 6 ); } else { return this.m.getUint16( this.objects + 14 * obj + 10 ); } }, get_sibling: function( obj ) { if ( this.version3 ) { return this.m.getUint8( this.objects + 9 * obj + 5 ); } else { return this.m.getUint16( this.objects + 14 * obj + 8 ); } }, get_parent: function( obj ) { if ( this.version3 ) { return this.m.getUint8( this.objects + 9 * obj + 4 ); } else { return this.m.getUint16( this.objects + 14 * obj + 6 ); } }, get_prop: function( object, property ) { var memory = this.m, // Try to find the property addr = this.find_prop( object, property ), len; // If we have the property if ( addr ) { len = memory.getUint8( addr - 1 ); // Assume we're being called for a valid short property return memory[ ( this.version3 ? len >> 5 : len & 0x40 ) ? 'getUint16' : 'getUint8' ]( addr ); } // Use the default properties table // Remember that properties are 1-indexed return memory.getUint16( this.properties + 2 * ( property - 1 ) ); }, // Get the length of a property // This opcode expects the address of the property data, not a property block get_prop_len: function( addr ) { // Spec 1.1 if ( addr === 0 ) { return 0; } var value = this.m.getUint8( addr - 1 ); // Version 3 if ( this.version3 ) { return ( value >> 5 ) + 1; } // Two size/number bytes if ( value & 0x80 ) { value &= 0x3F; return value === 0 ? 64 : value; } // One byte size/number return value & 0x40 ? 2 : 1; }, // Quick hack for @inc/@dec/@inc_chk/@dec_chk incdec: function( varnum, change ) { if ( varnum === 0 ) { this.s[this.sp - 1] += change; return this.s[this.sp - 1]; } if ( --varnum < 15 ) { this.l[varnum] += change; return this.l[varnum]; } else { var offset = this.globals + ( varnum - 15 ) * 2; this.ram.setUint16( offset, this.m.getUint16( offset ) + change ); return this.ram.getUint16( offset ); } }, // Indirect variables indirect: function( variable, value ) { if ( variable === 0 ) { if ( arguments.length > 1 ) { return this.s[this.sp - 1] = value; } else { return this.s[this.sp - 1]; } } return this.variable( variable, value ); }, insert_obj: function( obj, dest ) { // First remove the obj from wherever it was this.remove_obj( obj ); // Now add it to the destination this.set_family( obj, dest, dest, obj, obj, this.get_child( dest ) ); }, // @jeq jeq: function() { var i = 1; // Account for many arguments while ( i < arguments.length ) { if ( arguments[i++] === arguments[0] ) { return 1; } } }, jin: function( child, parent ) { return this.get_parent( child ) === parent; }, log: function( message ) { if ( this.options.GlkOte ) { this.options.GlkOte.log( message ); } }, log_shift: function( number, places ) { return places > 0 ? number << places : number >>> -places; }, make_stacks: function() { var locals_count = this.stack.getUint8( this.frameptr + 3 ) & 0x0F; this.l = new Uint16Array( this.stack.buffer, this.frameptr + 8, locals_count ); this.s = new Uint16Array( this.stack.buffer, this.frameptr + 8 + locals_count * 2 ); }, put_prop: function( object, property, value ) { // Try to find the property var addr = this.find_prop( object, property ), len; if ( addr ) { len = this.m.getUint8( addr - 1 ); // Assume we're being called for a valid short property this.ram[ ( this.version3 ? len >> 5 : len & 0x40 ) ? 'setUint16' : 'setUint8' ]( addr, value ); } }, random: function( range ) { var seed = this.xorshift_seed; // Switch to the Xorshift RNG (or switch off if range == 0) if ( range < 1 ) { this.xorshift_seed = range; return 0; } // Pure randomness if ( seed === 0 ) { return 1 + ( Math.random() * range ) | 0; } // Based on the discussions in this forum topic, we will not implement the sequential mode recommended in the standard // http://www.intfiction.org/forum/viewtopic.php?f=38&t=16023 // Instead implement a 32 bit Xorshift generator seed ^= ( seed << 13 ); seed ^= ( seed >> 17 ); this.xorshift_seed = ( seed ^= ( seed << 5 ) ); return 1 + ( ( seed & 0x7FFF ) % range ); }, remove_obj: function( obj ) { var parent = this.get_parent( obj ), older_sibling, younger_sibling, temp_younger; // No parent, do nothing if ( parent === 0 ) { return; } older_sibling = this.get_child( parent ); younger_sibling = this.get_sibling( obj ); // obj is first child if ( older_sibling === obj ) { this.set_family( obj, 0, parent, younger_sibling ); } // obj isn't first child, so fix the older sibling else { // Go through the tree until we find the older sibling while ( 1 ) { temp_younger = this.get_sibling( older_sibling ); if ( temp_younger === obj ) { break; } older_sibling = temp_younger; } this.set_family( obj, 0, 0, 0, older_sibling, younger_sibling ); } }, // (Re)start the VM restart: function(autorestoring) { var ram = this.ram, version = ram.getUint8( 0x00 ), version3 = version === 3, addr_multipler = version3 ? 2 : ( version === 8 ? 8 : 4 ), flags2 = ram.getUint8( 0x11 ), property_defaults = ram.getUint16( 0x0A ), extension = ( version > 4 ) ? ram.getUint16( 0x36 ) : 0, stack = utils.MemoryView( this.options.stack_len ); // Reset the RAM, but preserve flags 2 ram.setUint8Array( 0, this.origram ); ram.setUint8( 0x11, flags2 ); extend( this, { // Locals and stacks of various kinds stack: stack, frameptr: 0, frames: [], s: new Uint16Array( stack.buffer, 8 ), sp: 0, l: [], undo: [], undo_len: 0, glk_blocking_call: null, // Get some header variables version: version, version3: version3, pc: ram.getUint16( 0x06 ), properties: property_defaults, objects: property_defaults + ( version3 ? 53 : 112 ), // 62-9 or 126-14 - if we take this now then we won't need to always decrement the object number globals: ram.getUint16( 0x0C ), // staticmem: set in prepare() eof: ( ram.getUint16( 0x1A ) || 65536 ) * addr_multipler, extension: extension, extension_count: extension ? ram.getUint16( extension ) : 0, // Routine and string multiplier addr_multipler: addr_multipler, // Opcodes for this version of the Z-Machine opcodes: require( './opcodes.js' )( version ), }); this.init_text(); if (!autorestoring) { this.init_io() } // Update the header this.update_header(); }, // Request a restore restore: function( pc ) { this.pc = pc; this.fileref_create_by_prompt({ func: 'restore', mode: 0x02, usage: 0x01, }); }, restore_file: function( data, autorestoring ) { var ram = this.ram, quetzal = new file.Quetzal( data ), qmem = quetzal.memory, stack = this.stack, flags2 = ram.getUint8( 0x11 ), temp, i = 0, j = 0; // Check this is a savefile for this story if ( ram.getUint16( 0x02 ) !== quetzal.release || ram.getUint16( 0x1C ) !== quetzal.checksum ) { return 0; } while ( i < 6 ) { if ( ram.getUint8( 0x12 + i ) !== quetzal.serial[i++] ) { return 0; } } i = 0; // Memory chunk // Reset the RAM ram.setUint8Array( 0, this.origram ); if ( quetzal.compressed ) { while ( i < qmem.length ) { temp = qmem[i++]; // Same memory if ( temp === 0 ) { j += 1 + qmem[i++]; } else { ram.setUint8( j, temp ^ this.origram[j++] ); } } } else { ram.setUint8Array( 0, qmem ); } // Preserve flags 2 ram.setUint8( 0x11, flags2 ); // Stacks stack.setUint8Array( 0, quetzal.stacks ); this.frames = []; i = 0; while ( i < quetzal.stacks.byteLength ) { this.frameptr = i; this.frames.push( i ); // Swap the bytes of the locals and stacks fix_stack_endianness( stack, j = i + 8, j += ( stack.getUint8( i + 3 ) & 0x0F ) * 2, autorestoring ) fix_stack_endianness( stack, j, j += stack.getUint16( i + 6 ) * 2, autorestoring ) i = j; } this.frames.pop(); this.sp = stack.getUint16( this.frameptr + 6 ); this.make_stacks(); this.pc = quetzal.pc; this.update_header(); // Collapse the upper window (8.6.1.3) if ( this.version3 ) { this.split_window( 0 ); } return 2; }, restore_undo: function() { if ( this.undo.length === 0 ) { return 0; } var state = this.undo.pop(); this.frameptr = state.frameptr; this.pc = state.pc; this.undo_len -= ( state.ram.byteLength + state.stack.byteLength ); // Replace the ram, preserving flags 2 state.ram[0x11] = this.m.getUint8( 0x11 ); this.ram.setUint8Array( 0, state.ram ); // Fix up the stack this.frames = state.frames; this.sp = state.sp; this.stack.setUint8Array( 0, state.stack ); this.make_stacks(); this.variable( state.var, 2 ); return 1; }, // Return from a routine ret: function( result ) { var stack = this.stack, // Get the storer and return pc from this frame frameptr = this.frameptr, storer = stack.getUint8( frameptr + 3 ) & 0x10 ? -1 : stack.getUint8( frameptr + 4 ); this.pc = stack.getUint32( frameptr ) >> 8; // Recreate the locals and stacks from the previous frame frameptr = this.frameptr = this.frames.pop(); this.make_stacks(); this.sp = stack.getUint16( frameptr + 6 ); // Store the result if there is one if ( storer >= 0 ) { this.variable( storer, result || 0 ); } }, // pc is the address of the storer operand (or branch in v3) save: function( pc ) { this.pc = pc; this.fileref_create_by_prompt({ func: 'save', mode: 0x01, usage: 0x01, }); }, save_file: function( pc, autosaving ) { var memory = this.m, quetzal = new file.Quetzal(), stack = utils.MemoryView( this.stack.buffer.slice() ), zeroes = 0, i, j, frameptr = this.frameptr, abyte; // IFhd chunk quetzal.release = memory.getUint16( 0x02 ); quetzal.serial = memory.getUint8Array( 0x12, 6 ); quetzal.checksum = memory.getUint16( 0x1C ); quetzal.pc = pc; // Memory chunk if ( autosaving ) { quetzal.memory = this.m.getUint8Array( 0, this.staticmem ) } else { const compressed_mem = [] quetzal.compressed = 1; for ( i = 0; i < this.staticmem; i++ ) { abyte = memory.getUint8( i ) ^ this.origram[i]; if ( abyte === 0 ) { if ( ++zeroes === 256 ) { compressed_mem.push( 0, 255 ); zeroes = 0; } } else { if ( zeroes ) { compressed_mem.push( 0, zeroes - 1 ); zeroes = 0; } compressed_mem.push( abyte ); } } quetzal.memory = compressed_mem; } // Stacks // Set the current sp stack.setUint16( frameptr + 6, this.sp ); // Swap the bytes of the locals and stacks if ( littleEndian && !autosaving ) { const frames = this.frames.slice() frames.push( frameptr ) for ( i = 0; i < frames.length; i++ ) { frameptr = frames[i]; fix_stack_endianness( stack, j = frameptr + 8, j += ( stack.getUint8( frameptr + 3 ) & 0x0F ) * 2 ); fix_stack_endianness( stack, j, j += stack.getUint16( frameptr + 6 ) * 2 ); } } quetzal.stacks = stack.getUint8Array( 0, this.frameptr + 8 + ( stack.getUint8( frameptr + 3 ) & 0x0F ) * 2 + this.sp * 2 ); return quetzal.write(); }, save_restore_handler: function( str ) { var memory = this.m, Glk = this.Glk, result = 0, buffer = [], temp, iftrue, offset; if ( str ) { // Save if ( this.fileref_data.func === 'save' ) { Glk.glk_put_buffer_stream( str, new Uint8Array( this.save_file( this.pc ) ) ); result = 1; } // Restore else { buffer = new Uint8Array( 128 * 1024 ); Glk.glk_get_buffer_stream( str, buffer ); result = this.restore_file( buffer.buffer ); } Glk.glk_stream_close( str ); } // Store the result / branch in z3 if ( this.version3 ) { // Calculate the branch temp = memory.getUint8( this.pc++ ); iftrue = temp & 0x80; offset = temp & 0x40 ? // single byte address temp & 0x3F : // word address, but first get the second byte of it ( temp << 8 | memory.getUint8( this.pc++ ) ) << 18 >> 18; if ( !result === !iftrue ) { if ( offset === 0 || offset === 1 ) { this.ret( offset ); } else { this.pc += offset - 2; } } } else { this.variable( memory.getUint8( this.pc++ ), result ); } }, save_undo: function( pc, variable ) { // Drop an old undo state if we've reached the limit, but always save at least one state var state if ( this.undo_len > this.options.undo_len ) { state = this.undo.shift() this.undo_len -= ( state.ram.byteLength + state.stack.byteLength ) } state = { frameptr: this.frameptr, frames: this.frames.slice(), pc: pc, ram: this.m.getUint8Array( 0, this.staticmem ), sp: this.sp, stack: this.stack.getUint8Array( 0, this.s.byteOffset + this.sp * 2 ), var: variable, } this.undo_len += ( state.ram.byteLength + state.stack.byteLength ) this.undo.push( state ) return 1 }, scan_table: function( key, addr, length, form ) { form = form || 0x82; var memoryfunc = form & 0x80 ? 'getUint16' : 'getUint8'; form &= 0x7F; length = addr + length * form; while ( addr < length ) { if ( this.m[memoryfunc]( addr ) === key ) { return addr; } addr += form; } return 0; }, set_attr: function( object, attribute ) { var addr = this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0; this.ram.setUint8( addr, this.m.getUint8( addr ) | 0x80 >> attribute % 8 ); }, set_family: function( obj, newparent, parent, child, bigsis, lilsis ) { var ram = this.ram, objects = this.objects; if ( this.version3 ) { // Set the new parent of the obj ram.setUint8( objects + 9 * obj + 4, newparent ); // Update the parent's first child if needed if ( parent ) { ram.setUint8( objects + 9 * parent + 6, child ); } // Update the little sister of a big sister if ( bigsis ) { ram.setUint8( objects + 9 * bigsis + 5, lilsis ); } } else { // Set the new parent of the obj ram.setUint16( objects + 14 * obj + 6, newparent ); // Update the parent's first child if needed if ( parent ) { ram.setUint16( objects + 14 * parent + 10, child ); } // Update the little sister of a big sister if ( bigsis ) { ram.setUint16( objects + 14 * bigsis + 8, lilsis ); } } }, test: function( bitmap, flag ) { return ( bitmap & flag ) === flag; }, test_attr: function( object, attribute ) { return ( this.m.getUint8( this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0 ) << attribute % 8 ) & 0x80; }, // Read or write a variable variable: function( variable, value ) { var havevalue = value !== undefined, offset; if ( variable === 0 ) { if ( havevalue ) { this.s[this.sp++] = value; } else { return this.s[--this.sp]; } } else if ( --variable < 15 ) { if ( havevalue ) { this.l[variable] = value; } else { return this.l[variable]; } } else { offset = this.globals + ( variable - 15 ) * 2; if ( havevalue ) { this.ram.setUint16( offset, value ); } else { return this.m.getUint16( offset ); } } return value; }, // Utilities for signed arithmetic U2S: U2S, S2U: S2U, };