ifvms
Version:
The Interactive Fiction Virtual Machines Suite - in Javascript
2,209 lines (1,934 loc) • 105 kB
JavaScript
(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