UNPKG

ifvms

Version:

The Interactive Fiction Virtual Machines Suite - in Javascript

2,209 lines (1,934 loc) 105 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ZVM = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){ /* Abstract syntax trees for IF VMs ================================ Copyright (c) 2017 The ifvms.js team BSD licenced http://github.com/curiousdannii/ifvms.js */ 'use strict'; /* All AST nodes must use these functions, even constants (An exception is made for branch addresses and text literals which remain as primitives) toString() functions are used to generate JIT code Aside from Variable is currently generic and could be used for Glulx too TODO: Use strict mode for new Function()? When we can run through a whole game, test whether using common_func is faster (if its slower then not worth the file size saving) Can we eliminate the Operand class? Subclass Operand/Variable from Number? Replace calls to args() with arguments.join()? */ var utils = require( '../common/utils.js' ), Class = utils.Class, U2S = utils.U2S16, //S2U = utils.S2U16; // Generic/constant operand // Value is a constant Operand = Class.subClass({ init: function( engine, value ) { this.e = engine; this.v = value; }, toString: function() { return this.v; }, // Convert an Operand into a signed operand U2S: function() { return U2S( this.v ); }, }), // Variable operand // Value is the variable number // TODO: unrolling is needed -> retain immediate returns if optimisations are disabled Variable = Operand.subClass({ // Get a value toString: function() { var variable = this.v; // Indirect if ( this.indirect ) { return 'e.indirect(' + variable + ')'; } // Stack if ( variable === 0 ) { // If we've been passed a value we're setting a variable return 's[--e.sp]'; } // Locals if ( --variable < 15 ) { return 'l[' + variable + ']'; } // Globals return 'e.m.getUint16(' + ( this.e.globals + ( variable - 15 ) * 2 ) + ')'; }, // Store a value store: function( value ) { var variable = this.v; // Indirect variable if ( this.indirect ) { return 'e.indirect(' + variable + ',' + value + ')'; } // BrancherStorers need the value if ( this.returnval ) { return 'e.variable(' + variable + ',' + value + ')'; } // Stack if ( variable === 0 ) { // If we've been passed a value we're setting a variable return 't=' + value + ';s[e.sp++]=t'; } // Locals if ( --variable < 15 ) { return 'l[' + variable + ']=' + value; } // Globals return 'e.ram.setUint16(' + ( this.e.globals + ( variable - 15 ) * 2 ) + ',' + value + ')'; }, // Convert an Operand into a signed operand U2S: function() { return 'e.U2S(' + this + ')'; }, }), // Generic opcode // .func() must be set, which returns what .write() will actually return; it is passed the operands as its arguments Opcode = Class.subClass({ init: function( engine, context, code, pc, next, operands ) { this.e = engine; this.context = context; this.code = code; this.pc = pc; this.labels = [ this.pc + '/' + this.code ]; this.next = next; this.operands = operands; // Post-init function (so that they don't all have to call _super) if ( this.post ) { this.post(); } }, // Write out the opcode, passing .operands to .func(), with a JS comment of the pc/opcode toString: function() { return this.label() + ( this.func ? this.func.apply( this, this.operands ) : '' ); }, // Return a string of the operands separated by commas args: function( joiner ) { return this.operands.join( joiner ); }, // Generate a comment of the pc and code, possibly for more than one opcode label: function() { return '/* ' + this.labels.join() + ' */ '; }, }), // Stopping opcodes Stopper = Opcode.subClass({ stopper: 1, }), // Pausing opcodes (ie, set the pc at the end of the context) Pauser = Stopper.subClass({ post: function() { this.origfunc = this.func; this.func = this.newfunc; }, newfunc: function() { return 'e.stop=1;e.pc=' + this.next + ';' + this.origfunc.apply( this, arguments ); }, }), PauserStorer = Pauser.subClass({ storer: 1, post: function() { this.storer = this.operands.pop(); this.origfunc = this.func; this.func = this.newfunc; }, }), // Join multiple branchers together with varying logic conditions BrancherLogic = Class.subClass({ init: function( ops, code ) { this.ops = ops || []; this.code = code || '||'; }, toString: function() { var i = 0, ops = [], op; while ( i < this.ops.length ) { op = this.ops[i++]; // Accept either Opcodes or further BrancherLogics ops.push( op.func ? ( op.iftrue ? '' : '!(' ) + op.func.apply( op, op.operands ) + ( op.iftrue ? '' : ')' ) : op ); } return ( this.invert ? '(!(' : '(' ) + ops.join( this.code ) + ( this.invert ? '))' : ')' ); }, }), // Branching opcodes Brancher = Opcode.subClass({ // Flag for the disassembler brancher: 1, keyword: 'if', // Process the branch result now post: function() { var result, prev, // Calculate the offset brancher = this.operands.pop(), offset = brancher[1]; this.iftrue = brancher[0]; // Process the offset if ( offset === 0 || offset === 1 ) { result = 'e.ret(' + offset + ')'; } else { offset += this.next - 2; // Add this target to this context's list this.context.targets.push( offset ); result = 'e.pc=' + offset; } this.result = result + ';return'; this.offset = offset; this.cond = new BrancherLogic( [this] ); // TODO: re-enable /*if ( this.e.options.debug ) { // Stop if we must if ( debugflags.noidioms ) { return; } }*/ // Compare with previous statement if ( this.context.ops.length ) { prev = this.context.ops.pop(); // As long as no other opcodes have an offset property we can skip the instanceof check if ( /* prev instanceof Brancher && */ prev.offset === offset ) { // Goes to same offset so reuse the Brancher arrays this.cond.ops.unshift( prev.cond ); this.labels = prev.labels; this.labels.push( this.pc + '/' + this.code ); } else { this.context.ops.push( prev ); } } }, // Write out the brancher toString: function() { var result = this.result; // Account for Contexts if ( result instanceof Context ) { // Update the context to be a child of this context if ( this.e.options.debug ) { result.context = this.context; } result = result + ( result.stopper ? '; return' : '' ); // Extra line breaks for multi-op results if ( this.result.ops.length > 1 ) { result = '\n' + result + '\n'; if ( this.e.options.debug ) { result += this.context.spacer; } } } // Print out a label for all included branches and the branch itself return this.label() + this.keyword + this.cond + ' {' + result + '}'; }, }), // Brancher + Storer BrancherStorer = Brancher.subClass({ storer: 1, // Set aside the storer operand post: function() { BrancherStorer.super.post.call( this ); this.storer = this.operands.pop(); this.storer.returnval = 1; // Replace the func this.origfunc = this.func; this.func = this.newfunc; }, newfunc: function() { return this.storer.store( this.origfunc.apply( this, arguments ) ); }, }), // Storing opcodes Storer = Opcode.subClass({ // Flag for the disassembler storer: 1, // Set aside the storer operand post: function() { this.storer = this.operands.pop(); }, // Write out the opcode, passing it to the storer (if there still is one) toString: function() { var data = Storer.super.toString.call( this ); // If we still have a storer operand, use it // Otherwise (if it's been removed due to optimisations) just return func() return this.storer ? this.storer.store( data ) : data; }, }), // Routine calling opcodes Caller = Stopper.subClass({ // Fake a result variable result: { v: -1 }, // Write out the opcode toString: function() { // TODO: Debug: include label if possible return this.label() + 'e.call(' + this.operands.shift() + ',' + this.result.v + ',' + this.next + ',[' + this.args() + '])'; }, }), // Routine calling opcodes, storing the result CallerStorer = Caller.subClass({ // Flag for the disassembler storer: 1, post: function() { // We can't let the storer be optimised away here this.result = this.operands.pop(); }, }), // A generic context (a routine, loop body etc) Context = Class.subClass({ init: function( engine, pc ) { this.e = engine; this.pc = pc; this.pre = []; this.ops = []; this.post = []; this.targets = []; // Branch targets if ( engine.options.debug ) { this.spacer = ''; } }, toString: function() { if ( this.e.options.debug ) { // Indent the spacer further if needed if ( this.context ) { this.spacer = this.context.spacer + ' '; } // DEBUG: Pretty print! return this.pre.join( '' ) + ( this.ops.length > 1 ? this.spacer : '' ) + this.ops.join( ';\n' + this.spacer ) + this.post.join( '' ); } else { // Return the code return this.pre.join( '' ) + this.ops.join( ';' ) + this.post.join( '' ); } }, }), // A routine body RoutineContext = Context.subClass({ toString: function() { // TODO: Debug: If we have routine names, find this one's name // Add in some extra vars and return this.pre.unshift( 'var l=e.l,s=e.s,t=0;\n' ); return RoutineContext.super.toString.call( this ); }, }); // Opcode builder // Easily build a new opcode from a class function opcode_builder( Class, func, flags ) { flags = flags || {}; if ( func ) { /*if ( func.pop ) { flags.str = func; flags.func = common_func; } else {*/ flags.func = func; //} } return Class.subClass( flags ); } module.exports = { Operand: Operand, Variable: Variable, Opcode: Opcode, Stopper: Stopper, Pauser: Pauser, PauserStorer: PauserStorer, BrancherLogic: BrancherLogic, Brancher: Brancher, BrancherStorer: BrancherStorer, Storer: Storer, Caller: Caller, CallerStorer: CallerStorer, Context: Context, RoutineContext: RoutineContext, opcode_builder: opcode_builder, }; },{"../common/utils.js":3}],2:[function(require,module,exports){ /* File classes ============ Copyright (c) 2016 The ifvms.js team BSD licenced http://github.com/curiousdannii/ifvms.js */ 'use strict'; var utils = require( './utils.js' ), MemoryView = utils.MemoryView, // A basic IFF file, to be extended later // Currently supports buffer data IFF = utils.Class.subClass({ init: function( data ) { this.type = ''; this.chunks = []; if ( data ) { var view = MemoryView( data ), i = 12, length, chunk_length; // Check that it is actually an IFF file if ( view.getFourCC( 0 ) !== 'FORM' ) { throw new Error( 'Not an IFF file' ); } // Parse the file this.type = view.getFourCC( 8 ); length = view.getUint32( 4 ) + 8; while ( i < length ) { chunk_length = view.getUint32( i + 4 ); if ( chunk_length < 0 || ( chunk_length + i ) > length ) { throw new Error( 'IFF chunk out of range' ); } this.chunks.push({ type: view.getFourCC( i ), offset: i, data: view.getUint8Array( i + 8, chunk_length ), }); i += 8 + chunk_length; if ( chunk_length % 2 ) { i++; } } } }, write: function() { // Start with the IFF type var buffer_len = 12, i = 0, index = 12, out, chunk; // First calculate the required buffer length while ( i < this.chunks.length ) { // Replace typed arrays or dataviews with their buffers if ( this.chunks[i].data.buffer ) { this.chunks[i].data = this.chunks[i].data.buffer; } this.chunks[i].length = this.chunks[i].data.byteLength || this.chunks[i].data.length; buffer_len += 8 + this.chunks[i++].length; if ( buffer_len % 2 ) { buffer_len++; } } out = MemoryView( buffer_len ); out.setFourCC( 0, 'FORM' ); out.setUint32( 4, buffer_len - 8 ); out.setFourCC( 8, this.type ); // Go through the chunks and write them out i = 0; while ( i < this.chunks.length ) { chunk = this.chunks[i++]; out.setFourCC( index, chunk.type ); out.setUint32( index + 4, chunk.length ); out.setUint8Array( index + 8, chunk.data ); index += 8 + chunk.length; if ( index % 2 ) { index++; } } return out.buffer; }, }), Blorb = IFF.subClass({ init: function( data ) { this.super.init.call( this, data ); if ( data ) { if ( this.type !== 'IFRS' ) { throw new Error( 'Not a Blorb file' ); } // Process the RIdx chunk to find the main exec chunk if ( this.chunks[0].type !== 'RIdx' ) { throw new Error( 'Malformed Blorb: chunk 1 is not RIdx' ); } var view = MemoryView( this.chunks[0].data ), i = 4; while ( i < this.chunks[0].data.length ) { if ( view.getFourCC( i ) === 'Exec' && view.getUint32( i + 4 ) === 0 ) { this.exec = this.chunks.filter( function( chunk ) { return chunk.offset === view.getUint32( i + 8 ); })[0]; return; } i += 12; } } }, }), Quetzal = IFF.subClass({ // Parse a Quetzal savefile, or make a blank one init: function( data ) { this.super.init.call( this, data ); if ( data ) { // Check this is a Quetzal savefile if ( this.type !== 'IFZS' ) { throw new Error( 'Not a Quetzal savefile' ); } // Go through the chunks and extract the useful ones var i = 0, type, chunk_data, view; while ( i < this.chunks.length ) { type = this.chunks[i].type; chunk_data = this.chunks[i++].data; // Memory and stack chunks if ( type === 'CMem' || type === 'UMem' ) { this.memory = chunk_data; this.compressed = ( type === 'CMem' ); } else if ( type === 'Stks' ) { this.stacks = chunk_data; } // Story file data else if ( type === 'IFhd' ) { view = MemoryView( chunk_data.buffer ); this.release = view.getUint16( 0 ); this.serial = view.getUint8Array( 2, 6 ); // The checksum isn't used, but if we throw it away we can't round-trip this.checksum = view.getUint16( 8 ); // The pc is only a Uint24, but there's no function for that, so grab an extra byte and then discard it this.pc = view.getUint32( 9 ) & 0xFFFFFF; } } } }, // Write out a savefile write: function() { // Reset the IFF type this.type = 'IFZS'; // Format the IFhd chunk correctly var ifhd = MemoryView( 13 ); ifhd.setUint16( 0, this.release ); ifhd.setUint8Array( 2, this.serial ); ifhd.setUint32( 9, this.pc ); ifhd.setUint16( 8, this.checksum ); // Add the chunks this.chunks = [ { type: 'IFhd', data: ifhd }, { type: ( this.compressed ? 'CMem' : 'UMem' ), data: this.memory }, { type: 'Stks', data: this.stacks }, ]; // Return the byte array return this.super.write.call( this ); }, }); // Inspect a file and identify its format and version number function identify( buffer ) { var view = MemoryView( buffer ), blorb, format, version; // Blorb if ( view.getFourCC( 0 ) === 'FORM' && view.getFourCC( 8 ) === 'IFRS' ) { blorb = new Blorb( buffer ); if ( blorb.exec ) { format = blorb.exec.type; buffer = blorb.exec.data; if ( format === 'GLUL' ) { view = MemoryView( buffer ); version = view.getUint32( 4 ); } if ( format === 'ZCOD' ) { version = buffer[0]; } } } // Glulx else if ( view.getFourCC( 0 ) === 'Glul' ) { format = 'GLUL'; version = view.getUint32( 4 ); } // Z-Code else { version = view.getUint8( 0 ); if ( version > 0 && version < 9 ) { format = 'ZCOD'; } } if ( format && version ) { return { format: format, version: version, data: buffer, }; } } module.exports = { IFF: IFF, Blorb: Blorb, Quetzal: Quetzal, identify: identify, }; },{"./utils.js":3}],3:[function(require,module,exports){ /* Common untility functions ========================= Copyright (c) 2016 The ifvms.js team BSD licenced http://github.com/curiousdannii/ifvms.js */ 'use strict'; // Utility to extend objects function extend() { var old = arguments[0], i = 1, add, name; while ( i < arguments.length ) { add = arguments[i++]; for ( name in add ) { old[name] = add[name]; } } return old; } // Simple classes // Inspired by John Resig's class implementation // http://ejohn.org/blog/simple-javascript-inheritance/ function Class() {} Class.subClass = function( props ) { function newClass() { if ( this.init ) { this.init.apply( this, arguments ); } } newClass.prototype = extend( Object.create( this.prototype ), props ); newClass.subClass = this.subClass; newClass.super = newClass.prototype.super = this.prototype; return newClass; }; // An enhanced DataView // Accepts an ArrayBuffer, another view (MemoryView / DataView / TypedArray), or a length number function MemoryView( buffer, byteOffset, byteLength ) { // Length number if ( typeof buffer === 'number' ) { buffer = new ArrayBuffer( buffer ); } // MemoryView / DataView / TypedArray else if ( buffer.buffer ) { // If unspecified, byteOffset defaults at the beginning of the given view. Note // that We will adjust 'byteOffset' after using the initial value to calculate // the default byteLength below. byteOffset |= 0; // A view may be a subset of a potentially larger array buffer. Before extracting // the underlying buffer, map the given 'byteLength' and byteOffset' to the underlying // array buffer from which we will construct the DataView. // A specified 'byteLength' does not need to be adjusted, but if no byteLength was // given, we need to ensure that the resulting MemoryView does not extend past the // end of the typed array (see above). if ( typeof byteLength === 'undefined' ) { byteLength = buffer.byteLength - byteOffset; } // Map the 'byteOffset', which is currently relative to the typed array, to the same // location in the underlying array buffer. byteOffset += buffer.byteOffset; // Finally, extract the underlying array buffer. buffer = buffer.buffer; } // Else already an ArrayBuffer. No adjustments to 'byteOffset'/'byteLength' necessary. return extend( new DataView( buffer, byteOffset, byteLength ), { getUint8Array: function( start, length ) { // Note that start/length are non-optional, so we only need to adjust the start to // the byteOffset of the view. (See MemoryView ctor comments.) start += this.byteOffset; return new Uint8Array( this.buffer.slice( start, start + length ) ); }, getUint16Array: function( start, length ) { // Note that start/length are non-optional, so we only need to adjust the start to // the byteOffset of the view. (See MemoryView ctor comments.) start += this.byteOffset; // We cannot simply return a Uint16Array as most systems are little-endian return Uint8toUint16Array( new Uint8Array( this.buffer, start, length * 2 ) ); }, setUint8Array: function( start, data ) { if ( data instanceof ArrayBuffer ) { data = new Uint8Array( data ); } ( new Uint8Array( this.buffer, this.byteOffset, this.byteLength ) ).set( data, start ); }, //setBuffer16 NOTE: if we implement this we cannot simply set a Uint16Array as most systems are little-endian // For use with IFF files getFourCC: function( index ) { return String.fromCharCode( this.getUint8( index ), this.getUint8( index + 1 ), this.getUint8( index + 2 ), this.getUint8( index + 3 ) ); }, setFourCC: function( index, text ) { this.setUint8( index, text.charCodeAt( 0 ) ); this.setUint8( index + 1, text.charCodeAt( 1 ) ); this.setUint8( index + 2, text.charCodeAt( 2 ) ); this.setUint8( index + 3, text.charCodeAt( 3 ) ); }, } ); } // Utilities for 16-bit signed arithmetic function U2S16( value ) { return value << 16 >> 16; } function S2U16 ( value ) { return value & 0xFFFF; } // Utility to convert from byte arrays to word arrays function Uint8toUint16Array( array ) { var i = 0, l = array.length, result = new Uint16Array( l / 2 ); while ( i < l ) { result[i / 2] = array[i++] << 8 | array[i++]; } return result; } module.exports = { extend: extend, Class: Class, MemoryView: MemoryView, U2S16: U2S16, S2U16: S2U16, Uint8toUint16Array: Uint8toUint16Array, }; },{}],4:[function(require,module,exports){ /* ZVM - the ifvms.js Z-Machine (versions 3-5, 8) ============================================== Copyright (c) 2017 The ifvms.js team MIT licenced https://github.com/curiousdannii/ifvms.js */ /* This file is the public API of ZVM, which is based on the API of Quixe: https://github.com/erkyrath/quixe/wiki/Quixe-Without-GlkOte#quixes-api ZVM willfully ignores the standard in these ways: Non-buffered output is not supported Saving tables is not supported (yet?) No interpreter number or version is set Any other non-standard behaviour should be considered a bug */ 'use strict'; var utils = require( './common/utils.js' ), file = require( './common/file.js' ), default_options = { stack_len: 100 * 1000, undo_len: 1000 * 1000, }, api = { init: function() { // Create this here so that it won't be cleared on restart this.jit = {}; // The Quixe API expects the start function to be named init this.init = this.start; }, prepare: function( storydata, options ) { // If we are not given a glk option then we cannot continue if ( !options.Glk ) { throw new Error( 'A reference to Glk is required' ); } this.Glk = options.Glk; this.data = storydata; this.options = utils.extend( {}, default_options, options ); }, start: function() { var Glk = this.Glk, data; try { // Identify the format and version number of the data file we were given data = file.identify( this.data ); delete this.data; if ( !data || data.format !== 'ZCOD' ) { throw new Error( 'This is not a Z-Code file' ); } if ( [ 3, 4, 5, 8 ].indexOf( data.version ) < 0 ) { throw new Error( 'Unsupported Z-Machine version: ' + data.version ); } // Load the storyfile we are given into our MemoryView (an enhanced DataView) this.m = utils.MemoryView( data.data ); // Make a seperate MemoryView for the ram, and store the original ram this.staticmem = this.m.getUint16( 0x0E ); this.ram = utils.MemoryView( this.m, 0, this.staticmem ); this.origram = this.m.getUint8Array( 0, this.staticmem ); // Cache the game signature let signature = '' let i = 0 while ( i < 0x1E ) { signature += ( this.origram[i] < 0x10 ? '0' : '' ) + this.origram[i++].toString( 16 ) } this.signature = signature // Handle loading and clearing autosaves let autorestored const Dialog = this.options.Dialog if ( Dialog ) { if ( this.options.clear_vm_autosave ) { Dialog.autosave_write( signature, null ) } else if ( this.options.do_vm_autosave ) { try { const snapshot = Dialog.autosave_read( signature ) if ( snapshot ) { this.do_autorestore( snapshot ) autorestored = 1 } } catch (ex) { this.log('Autorestore failed, deleting it: ' + ex) Dialog.autosave_write( signature, null ) } } } // Initiate the engine, run, and wait for our first Glk event if ( !autorestored ) { this.restart(); this.run(); } if ( !this.quit ) { this.glk_event = new Glk.RefStruct(); if ( !this.glk_blocking_call ) { Glk.glk_select( this.glk_event ); } else { this.glk_event.push_field( this.glk_blocking_call ); } } Glk.update() } catch ( e ) { Glk.fatal_error( e ); console.log( e ); } }, resume: function( resumearg ) { var Glk = this.Glk, glk_event = this.glk_event, event_type, run; try { event_type = glk_event.get_field( 0 ); // Process the event if ( event_type === 2 ) { this.handle_char_input( glk_event.get_field( 2 ) ); run = 1; } if ( event_type === 3 ) { this.handle_line_input( glk_event.get_field( 2 ), glk_event.get_field( 3 ) ); run = 1; } // Arrange events if ( event_type === 5 ) { this.update_screen_size() } // glk_fileref_create_by_prompt handler if ( event_type === 'fileref_create_by_prompt' ) { run = this.handle_create_fileref( resumearg ); } this.glk_blocking_call = null; if ( run ) { this.run(); } // Wait for another event if ( !this.quit ) { this.glk_event = new Glk.RefStruct(); if ( !this.glk_blocking_call ) { Glk.glk_select( this.glk_event ); } else { this.glk_event.push_field( this.glk_blocking_call ); } } Glk.update() } catch ( e ) { Glk.fatal_error( e ); console.log( e ); } }, get_signature: function() { return this.signature }, // Run run: function() { var pc, result; // Stop when ordered to this.stop = 0; while ( !this.stop ) { pc = this.pc; if ( !this.jit[pc] ) { this.compile(); } result = this.jit[pc]( this ); // Return from a VM func if the JIT function returned a result if ( !isNaN( result ) ) { this.ret( result ); } } }, // Compile a JIT routine compile: function() { var context = this.disassemble(); // Compile the routine with new Function() this.jit[context.pc] = new Function( 'e', '' + context ); if ( context.pc < this.staticmem ) { this.log( 'Caching a JIT function in dynamic memory: ' + context.pc ); } }, }, VM = utils.Class.subClass( utils.extend( api, require( './zvm/runtime.js' ), require( './zvm/text.js' ), require( './zvm/io.js' ), require( './zvm/disassembler.js' ) ) ); module.exports = VM; },{"./common/file.js":2,"./common/utils.js":3,"./zvm/disassembler.js":5,"./zvm/io.js":6,"./zvm/runtime.js":8,"./zvm/text.js":9}],5:[function(require,module,exports){ /* Z-Machine disassembler - disassembles zcode into an AST ======================================================= Copyright (c) 2011 The ifvms.js team BSD licenced http://github.com/curiousdannii/ifvms.js */ /* Note: Nothing is done to check whether an instruction actually has a valid number of operands. Extras will usually be ignored while missing operands may throw errors at either the code building stage or when the JIT code is called. TODO: If we diassessemble part of what we already have before, can we just copy/slice the context? */ var AST = require( '../common/ast.js' ); module.exports.disassemble = function() { var pc, offset, // Set in the loop below memory = this.m, opcodes = this.opcodes, temp, code, opcode_class, operands_type, // The types of the operands, or -1 for var instructions operands, // Create the context for this code fragment context = new AST.RoutineContext( this, this.pc ); // Utility function to unpack the variable form operand types byte function get_var_operand_types( operands_byte, operands_type ) { for ( var i = 0; i < 4; i++ ) { operands_type.push( (operands_byte & 0xC0) >> 6 ); operands_byte <<= 2; } } // Set the context's root context to be itself, and add it to the list of subcontexts //context.root = context; //context.contexts[0] = context; // Run through until we can no more while ( 1 ) { // This instruction offset = pc = this.pc; code = memory.getUint8( pc++ ); // Extended instructions if ( code === 190 ) { operands_type = -1; code = memory.getUint8( pc++ ) + 1000; } else if ( code & 0x80 ) { // Variable form instructions if ( code & 0x40 ) { operands_type = -1; // 2OP instruction with VAR parameters if ( !(code & 0x20) ) { code &= 0x1F; } } // Short form instructions else { operands_type = [ (code & 0x30) >> 4 ]; // Clear the operand type if 1OP, keep for 0OPs if ( operands_type[0] < 3 ) { code &= 0xCF; } } } // Long form instructions else { operands_type = [ code & 0x40 ? 2 : 1, code & 0x20 ? 2 : 1 ]; code &= 0x1F; } // Check for missing opcodes if ( !opcodes[code] ) { this.log( '' + context ); this.stop = 1; throw new Error( 'Unknown opcode #' + code + ' at pc=' + offset ); } // Variable for quicker access to the opcode flags opcode_class = opcodes[code].prototype; // Variable form operand types if ( operands_type === -1 ) { operands_type = []; get_var_operand_types( memory.getUint8(pc++), operands_type ); // VAR_LONG opcodes have two operand type bytes if ( code === 236 || code === 250 ) { get_var_operand_types( memory.getUint8(pc++), operands_type ); } } // Load the operands operands = []; temp = 0; while ( temp < operands_type.length ) { // Large constant if ( operands_type[temp] === 0 ) { operands.push( new AST.Operand( this, memory.getUint16(pc) ) ); pc += 2; } // Small constant if ( operands_type[temp] === 1 ) { operands.push( new AST.Operand( this, memory.getUint8(pc++) ) ); } // Variable operand if ( operands_type[temp++] === 2 ) { operands.push( new AST.Variable( this, memory.getUint8(pc++) ) ); } } // Check for a store variable if ( opcode_class.storer ) { operands.push( new AST.Variable( this, memory.getUint8(pc++) ) ); } // Check for a branch address // If we don't calculate the offset now we won't be able to tell the difference between 0x40 and 0x0040 if ( opcode_class.brancher ) { temp = memory.getUint8( pc++ ); operands.push( [ temp & 0x80, // iftrue temp & 0x40 ? // single byte address temp & 0x3F : // word address, but first get the second byte of it ( temp << 8 | memory.getUint8( pc++ ) ) << 18 >> 18, ] ); } // Check for a text literal if ( opcode_class.printer ) { // Just use the address as an operand, the text will be decoded at run time operands.push( pc ); // Continue until we reach the stop bit // (or the end of the file, which will stop memory access errors, even though it must be a malformed storyfile) while ( pc < this.eof ) { temp = memory.getUint8( pc ); pc += 2; // Stop bit if ( temp & 0x80 ) { break; } } } // Update the engine's pc this.pc = pc; // Create the instruction context.ops.push( new opcodes[code]( this, context, code, offset, pc, operands ) ); // Check for the end of a large if block temp = 0; /*if ( context.targets.indexOf( pc ) >= 0 ) { if ( DEBUG ) { // Skip if we must if ( !debugflags.noidioms ) { temp = idiom_if_block( context, pc ); } } else { temp = idiom_if_block( context, pc ); } }*/ // We can't go any further if we have a final stopper :( if ( opcode_class.stopper && !temp ) { break; } } return context; }; },{"../common/ast.js":1}],6:[function(require,module,exports){ /* Z-Machine IO ============ Copyright (c) 2020 The ifvms.js team MIT licenced https://github.com/curiousdannii/ifvms.js */ 'use strict'; /* TODO: - pre-existing line input - timed input - mouse input - write colours into header */ const utils = require('../common/utils.js') const U2S = utils.U2S16 //S2U = utils.S2U16 // Glulx key codes accepted by the Z-Machine const ZSCII_keyCodes = (function() { var codes = { 0xfffffff9: 8, // delete/backspace 0xfffffffa: 13, // enter 0xfffffff8: 27, // escape 0xfffffffc: 129, // up 0xfffffffb: 130, // down 0xfffffffe: 131, // left 0xfffffffd: 132, // right 0xfffffff3: 146, // End / key pad 1 0xfffffff5: 148, // PgDn / key pad 3 0xfffffff4: 152, // Home / key pad 7 0xfffffff6: 154, // PgUp / key pad 9 }, i = 0; while ( i < 12 ) { codes[ 0xffffffef - i ] = 133 + i++; // function keys } return codes; })() // Style mappings // The index bits are (lowest to highest): mono, italic, bold const style_mappings = [0, 2, 1, 10, 4, 9, 5, 6] // Convert a 15 bit colour to RGB function convert_true_colour(colour) { const from5to8 = [0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255] // Stretch the five bits per colour out to 8 bits return (from5to8[colour & 0x1F] << 16) | (from5to8[(colour & 0x03E0) >> 5] << 8) | (from5to8[(colour & 0x7C00) >> 10]) } // The standard 15 bit colour values const zcolours = [ 0xFFFE, // Current 0xFFFF, // Default 0x0000, // Black 0x001D, // Red 0x0340, // Green 0x03BD, // Yellow 0x59A0, // Blue 0x7C1F, // Magenta 0x77A0, // Cyan 0x7FFF, // White 0x5AD6, // Light grey 0x4631, // Medium grey 0x2D6B, // Dark grey ] module.exports = { init_io: function() { this.io = { reverse: 0, bold: 0, italic: 0, bg: -1, fg: -1, // A variable for whether we are outputing in a monospaced font. If non-zero then we are // Bit 0 is for @set_style, bit 1 for the header, and bit 2 for @set_font mono: this.m.getUint8( 0x11 ) & 0x02, // A variable for checking whether the transcript bit has been changed transcript: this.m.getUint8( 0x11 ) & 0x01, // Index 0 is input stream 1, the output streams follow streams: [ 0, 1, {}, [], {} ], currentwin: 0, // Use Zarf's algorithm for the upper window // http://eblong.com/zarf/glk/quote-box.html // Implemented in fix_upper_window() and split_window() height: 0, // What the VM thinks the height is glkheight: 0, // Actual height of the Glk window maxheight: 0, // Height including quote boxes etc seenheight: 0, // Last height the player saw width: 0, row: 0, col: 0, }; //this.process_colours(); // Construct the windows if they do not already exist this.open_windows() }, erase_line: function( value ) { if ( value === 1 ) { var io = this.io, row = io.row, col = io.col; this._print( Array( io.width - io.col + 1 ).join( ' ' ) ); this.set_cursor( row, col ); } }, erase_window: function(window) { if (window < 1) { this.Glk.glk_window_clear(this.mainwin) if (this.io.bg >= 0) { this.Glk.glk_stylehint_set(3, 0, 8, this.io.bg) } else if (this.io.bg === -1) { this.Glk.glk_stylehint_clear(3, 0, 8) } } if (window !== 0) { if (window === -1) { this.split_window(0) } if (this.upperwin) { this.Glk.glk_window_clear(this.upperwin) this.set_cursor(0, 0) } } }, fileref_create_by_prompt: function( data ) { if ( typeof data.run === 'undefined' ) { data.run = 1; } this.fileref_data = data; this.glk_blocking_call = 'fileref_create_by_prompt'; this.Glk.glk_fileref_create_by_prompt( data.usage, data.mode, data.rock || 0 ); }, // Fix the upper window height before an input event fix_upper_window: function() { var Glk = this.Glk, io = this.io; // If we have seen the entire window, shrink it to what it should be if (io.seenheight >= io.maxheight) { io.maxheight = io.height; } if ( this.upperwin ) { if ( io.maxheight === 0 ) { Glk.glk_window_close( this.upperwin ); this.upperwin = null; } else if (io.maxheight !== io.glkheight) { Glk.glk_window_set_arrangement( Glk.glk_window_get_parent( this.upperwin ), 0x12, io.maxheight, null ); } io.glkheight = io.maxheight } io.seenheight = io.maxheight; io.maxheight = io.height; }, format: function() { this.Glk.glk_set_style(style_mappings[!!this.io.mono | this.io.italic | this.io.bold]) if (this.Glk.glk_gestalt(0x1100, 0)) { this.Glk.garglk_set_reversevideo(this.io.reverse) } }, get_cursor: function( array ) { this.ram.setUint16( array, this.io.row + 1 ); this.ram.setUint16( array + 2, this.io.col + 1 ); }, // Handle char input handle_char_input: function( charcode ) { var stream4 = this.io.streams[4], code = ZSCII_keyCodes[ charcode ] || this.reverse_unicode_table[ charcode ] || 63; this.variable( this.read_data.storer, code ); // Echo to the commands log if ( stream4.mode === 1 ) { stream4.cache += code; } if ( stream4.mode === 2 ) { this.Glk.glk_put_char_stream_uni( stream4.str, code ); } }, // Handle the result of glk_fileref_create_by_prompt() handle_create_fileref: function( fref ) { var Glk = this.Glk, data = this.fileref_data, str; if ( fref ) { if ( data.unicode ) { str = Glk.glk_stream_open_file_uni( fref, data.mode, data.rock || 0 ); } else { str = Glk.glk_stream_open_file( fref, data.mode, data.rock || 0 ); } Glk.glk_fileref_destroy( fref ); } if ( data.func === 'restore' || data.func === 'save' ) { this.save_restore_handler( str ); } if ( data.func === 'input_stream' ) { this.io.streams[0] = str; } if ( data.func === 'output_stream' ) { this.output_stream_handler( str ); } // Signal to resume() to call run() if required return data.run; }, // Handle line input handle_line_input: function( len, terminator ) { var ram = this.ram, options = this.read_data, streams = this.io.streams, // Cut the response to len, convert to a lower case string, and then to a ZSCII array command = String.fromCharCode.apply( null, options.buffer.slice( 0, len ) ) + '\n', response = this.text_to_zscii( command.slice( 0, -1 ).toLowerCase() ); // 7.1.1.1: The response must be echoed, Glk will handle this // But we do have to echo to the transcripts if ( streams[2].mode === 1 ) { streams[2].cache += command; } if ( streams[2].mode === 2 ) { this.Glk.glk_put_jstring_stream( streams[2].str, command ); } if ( streams[4].mode === 1 ) { streams[4].cache += command; } if ( streams[4].mode === 2 ) { this.Glk.glk_put_jstring_stream( streams[4].str, command ); } // Store the response if ( this.version < 5 ) { // Append zero terminator response.push( 0 ); // Store the response in the buffer ram.setUint8Array( options.bufaddr + 1, response ); } else { // Store the response length ram.setUint8( options.bufaddr + 1, len ); // Store the response in the buffer ram.setUint8Array( options.bufaddr + 2, response ); // Store the terminator this.variable( options.storer, isNaN( terminator ) ? 13 : terminator ); } if ( options.parseaddr ) { // Tokenise the response this.tokenise( options.bufaddr, options.parseaddr ); } }, input_stream: function( stream ) { var io = this.io; if ( stream && !io.streams[0] ) { this.fileref_create_by_prompt({ func: 'input_stream', mode: 0x02, rock: 212, unicode: 1, usage: 0x103, }); } if ( !stream && io.streams[0] ) { this.Glk.glk_stream_close( io.streams[0] ); io.streams[0] = 0; } }, // Open windows open_windows: function() { const Glk = this.Glk if (!this.mainwin) { // We will borrow the general approach of Bocfel to implement the Z-Machine's formatting model in Glk // https://github.com/garglk/garglk/blob/master/terps/bocfel/screen.c // Reset some Glk stylehints just in case const styles_to_reset = [1, 2, 4, 5, 6, 9, 10] for (let i = 0; i < 7; i++) { // Reset the size, weight, and obliqueness Glk.glk_stylehint_set(0, styles_to_reset[i], 3, 0) Glk.glk_stylehint_set(0, styles_to_reset[i], 4, 0) Glk.glk_stylehint_set(0, styles_to_reset[i], 5, 0) // And force proportional font Glk.glk_stylehint_set(0, styles_to_reset[i], 6, 1) } // Now set the style hints we will use // Bold will use subheader Glk.glk_stylehint_set(0, 4, 4, 1) // Italic will use emphasised Glk.glk_stylehint_set(0, 1, 5, 1) // Bold+italic will use alert Glk.glk_stylehint_set(0, 5, 4, 1) Glk.glk_stylehint_set(0, 5, 5, 1) // Fixed will use preformated Glk.glk_stylehint_set(0, 2, 6, 0) // Bold+fixed will use user1 Glk.glk_stylehint_set(0, 9, 4, 1) Glk.glk_stylehint_set(0, 9, 6, 0) // Italic+fixed will use user2 Glk.glk_stylehint_set(0, 10, 5, 1) Glk.glk_stylehint_set(0, 10, 6, 0) // Bold+italic+fixed will use note Glk.glk_stylehint_set(0, 6, 4, 1) Glk.glk_stylehint_set(0, 6, 5, 1) Glk.glk_stylehint_set(0, 6, 6, 0) this.mainwin = Glk.glk_window_open(0, 0, 0, 3, 201) Glk.glk_set_window(this.mainwin) if (this.version3) { this.statuswin = Glk.glk_window_open(this.mainwin, 0x12, 1, 4, 202) if (this.statuswin && this.Glk.glk_gestalt(0x1100, 0)) { Glk.garglk_set_reversevideo_stream(Glk.glk_window_get_stream(this.statuswin), 1) } } } else { // Clean up after restarting Glk.glk_stylehint_clear(0, 0, 8) if (this.Glk.glk_gestalt(0x1100, 0)) { Glk.garglk_set_zcolors_stream(this.mainwin.str, this.io.fg, this.io.bg) } Glk.glk_window_clear(this.mainwin) if (this.upperwin) { Glk.glk_window_close(this.upperwin) this.upperwin = null } } }, // Manage output streams output_stream: function( stream, addr, called_from_print ) { var ram = this.ram, streams = this.io.streams, data, text; stream = U2S( stream ); // The screen if ( stream === 1 ) { streams[1] = 1; } if ( stream === -1 ) { streams[1] = 0; } // Transcript if ( stream === 2 && !streams[2].mode ) { this.fileref_create_by_prompt({ func: 'output_stream', mode: 0x05, rock: 210, run: !called_from_print, str: 2, unicode: 1, usage: 0x102, }); streams[2].cache = ''; streams[2].mode = 1; if ( !called_from_print ) { this.stop = 1; } } if ( stream === -2 ) { ram.setUint8( 0x11, ( ram.getUint8( 0x11 ) & 0xFE ) ); if ( streams[2].mode === 2 ) { this.Glk.glk_stream_close( streams[2].str ); } streams[2].mode = this.io.transcript = 0; } // Memory if ( stream === 3 ) { streams[3].unshift( [ addr, '' ] ); } if ( stream === -3 ) { data = streams[3].shift(); text = this.text_to_zscii( data[1] ); ram.setUint16( data[0], text.length ); ram.setUint8Array( data[0] + 2, text ); } // Command list if ( stream === 4 && !streams[4].mode ) { this.fileref_create_by_prompt({ func: 'output_stream', mode: 0x05, rock: 211, str: 4, unicode: 1, usage: 0x103, }); streams[4].cache = ''; streams[4].mode = 1; this.stop = 1; } if ( stream === -4 ) { if ( streams[4].mode === 2 ) { this.Glk.glk_stream_close( streams[4].str ); } streams[4].mode = 0; } }, output_stream_handler: function( str ) { var ram = this.ram, streams = this.io.streams, data = this.fileref_data; if ( data.str === 2 ) { ram.setUint8( 0x11, ( ram.getUint8( 0x11 ) & 0xFE ) | ( str ? 1 : 0 ) ); if ( str ) { streams[2].mode = 2; streams[2].str = str; this.io.transcript = 1; if ( streams[2].cache ) { this.Glk.glk_put_jstring_stream( streams[2].str, streams[2].cache ); } } else { streams[2].mode = this.io.transcript = 0; } } if ( data.str === 4 ) { if ( str ) { streams[4].mode = 2; streams[4].str = str; if ( streams[4].cache ) { this.Glk.glk_put_jstring_stream( streams[4].str, streams[4].cache ); } } else { streams[4].mode = 0; } } }, // Print text! _print: function( text ) { var Glk = this.Glk, io = this.io, i = 0; // Stream 3 gets the text first if ( io.streams[3].length ) { io.streams[3][0][1] += text; } else { // Convert CR into LF text = text.replace( /\r/g, '\n' ); // Check the transcript bit // Because it might need to prompt for a file name, we return here, and will print again in the handler if ( ( this.m.getUint8( 0x11 ) & 0x01 ) !== io.transcript ) { this.output_stream( io.transcript ? -2 : 2, 0, 1 ); } // Check if the monospace font bit has changed // Unfortunately, even now Inform changes this bit for the font statement, even though the 1.1 standard depreciated it :( if ( ( this.m.getUint8( 0x11 ) & 0x02 ) !== ( io.mono & 0x02 ) ) { io.mono ^= 0x02; this.format(); } // For the upper window we print each character individually so that we can track the cursor position if ( io.currentwin && this.upperwin ) { // Don't automatically increase the size of the window // If we confirm that games do need this then we can implement it later while ( i < text.length && io.row < io.height ) { Glk.glk_put_jstring( text[i++] ); io.col++; if ( io.col === io.width ) { io.col = 0; io.row++; } } } else if ( !io.currentwin ) { if ( io.streams[1] ) { Glk.glk_put_jstring( text ); } // Transcript if ( io.streams[2].mode === 1 ) { io.streams[2].cache += text; } if ( io.streams[2].mode === 2 ) { Glk.glk_put_jstring_stream( io.streams[2].str, text ); } } } }, // Print many things print: function( type, val ) { var proptable, result; // Number if ( type === 0 ) { result = val; } // Unicode if ( type === 1 ) { result = String.fromCharCode( val ); } // Text from address if ( type === 2 ) { result = this.jit[ val ] || this.decode( val ); } // Object if ( type === 3 ) { proptable = this.m.getUint16( this.objects + ( this.version3 ? 9 : 14 ) * val + ( this.version3 ? 7 : 12 ) ); result = this.decode( proptable + 1, this.m.getUint8( proptable ) * 2 ); } // ZSCII if ( type === 4 ) { if ( !this.unicode_table[ val ] ) { return; } result = this.unicode_table[ val ]; } this._print( '' + result ); }, print_table: function( zscii, width, height, skip ) { height = height || 1; skip = skip || 0; var i = 0; while ( i++ < height ) { this._print( this.zscii_to_text( this.m.getUint8Array( zscii, width ) ) + ( i < height ? '\r' : '' ) ); zscii += width + skip; } }, // Process CSS default colours /*process_colours: function() { // Convert RGB to a Z-Machine true colour // RGB is a css colour code. rgb(), #000000 and #000 formats are supported. function convert_RGB( code ) { var round = Math.round, data = /(\d+),\s*(\d+),\s*(\d+)|#(\w{1,2})(\w{1,2})(\w{1,2})/.exec( code ), result; // Nice rgb() code if ( data[1] ) { result = [ data[1], data[2], data[3] ]; } else { // Messy CSS colour code result = [ parseInt( data[4], 16 ), parseInt( data[5], 16 ), parseInt( data[6], 16 ) ]; // Stretch out compact #000 codes to their full size if ( code.length === 4 ) { result = [ result[0] << 4 | result[0], result[1] << 4 | result[1], result[2] << 4 | result[2] ]; } } // Convert to a 15bit colour return round( result[2] / 8.226 ) << 10 | round( result[1] / 8.226 ) << 5 | round( result[0] / 8.226 ); } // Standard colours var colours = [ 0xFFFE, // Current 0xFFFF, // Default 0x0000, // Black 0x001D, // Red 0x0340, // Green 0x03BD, // Yellow 0x59A0, // Blue 0x7C1F, // Magenta 0x77A0, // Cyan 0x7FFF, // White 0x5AD6, // Light grey 0x4631, // Medium grey 0x2D6B, // Dark grey ], // Start with CSS colours provided by the runner fg_css = this.options.fgcolour, bg_css = this.options.bgcolour, // Convert to true colour for storing in the header fg_true = fg_css ? convert_RGB( fg_css ) : 0xFFFF, bg_true = bg_css ? convert_RGB( bg_css ) : 0xFFFF, // Search the list of standard colours fg = colours.indexOf( fg_true ), bg = colours.indexOf( bg_true ); // ZVMUI must have colours for reversing text, even if we don't write them to the header // So use the given colours or assume black on white if ( fg < 2 ) { fg = fg_css || 2; } if ( bg < 2 ) { bg = bg_css || 9; } utils.extend( this.options, { fg: fg, bg: bg, fg_true: fg_true, bg_true: bg_true, }); },*/ // Request line input read: function( storer, text, parse, time, routine ) { var len = this.m.getUint8( text ), initiallen = 0, buffer, input_stream1_len; if ( this.version3 ) { this.v3_status(); } // The spec is badly phrased; the buffer capacity includes the zero terminator // See https://github.com/DFillmore/Z-Machine-Standard/issues/76 if (this.version < 5) { len-- } //else //{ //initiallen = this.m.getUint8( text + 1 ); //} buffer = Array( len ); buffer.fill( 0 ) this.read_data = { buffer: buffer, bufaddr: text, // text-buffer parseaddr: parse, // parse-buffer routine: routine, storer: storer, time: time, }; // Input stream 1 if ( this.io.streams[0] ) { input_stream1_len = this.Glk.glk_get_line_stream_uni( this.io.streams[0], buffer ); // Check for a newline character if ( buffer[input_stream1_len - 1] === 0x0A ) { input_stream1_len--; } if ( input_stream1_len ) { this._print( String.fromCharCode.apply( null, buffer.slice( 0, input_stream1_len ) ) + '\n' ); this.handle_line_input( input_stream1_len ); return this.stop = 0; } else { this.input_stream( 0 ); } } // TODO: pre-existing input this.Glk