Created
March 8, 2017 01:18
-
-
Save curiousdannii/237b91a12f136ed617c2e778509575ef to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 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})({1:[function(require,module,exports){ | |
var clone = (function() { | |
'use strict'; | |
var nativeMap; | |
try { | |
nativeMap = Map; | |
} catch(_) { | |
// maybe a reference error because no `Map`. Give it a dummy value that no | |
// value will ever be an instanceof. | |
nativeMap = function() {}; | |
} | |
var nativeSet; | |
try { | |
nativeSet = Set; | |
} catch(_) { | |
nativeSet = function() {}; | |
} | |
var nativePromise; | |
try { | |
nativePromise = Promise; | |
} catch(_) { | |
nativePromise = function() {}; | |
} | |
/** | |
* Clones (copies) an Object using deep copying. | |
* | |
* This function supports circular references by default, but if you are certain | |
* there are no circular references in your object, you can save some CPU time | |
* by calling clone(obj, false). | |
* | |
* Caution: if `circular` is false and `parent` contains circular references, | |
* your program may enter an infinite loop and crash. | |
* | |
* @param `parent` - the object to be cloned | |
* @param `circular` - set to true if the object to be cloned may contain | |
* circular references. (optional - true by default) | |
* @param `depth` - set to a number if the object is only to be cloned to | |
* a particular depth. (optional - defaults to Infinity) | |
* @param `prototype` - sets the prototype to be used when cloning an object. | |
* (optional - defaults to parent prototype). | |
* @param `includeNonEnumerable` - set to true if the non-enumerable properties | |
* should be cloned as well. Non-enumerable properties on the prototype | |
* chain will be ignored. (optional - false by default) | |
*/ | |
function clone(parent, circular, depth, prototype, includeNonEnumerable) { | |
if (typeof circular === 'object') { | |
depth = circular.depth; | |
prototype = circular.prototype; | |
includeNonEnumerable = circular.includeNonEnumerable; | |
circular = circular.circular; | |
} | |
// maintain two arrays for circular references, where corresponding parents | |
// and children have the same index | |
var allParents = []; | |
var allChildren = []; | |
var useBuffer = typeof Buffer != 'undefined'; | |
if (typeof circular == 'undefined') | |
circular = true; | |
if (typeof depth == 'undefined') | |
depth = Infinity; | |
// recurse this function so we don't reset allParents and allChildren | |
function _clone(parent, depth) { | |
// cloning null always returns null | |
if (parent === null) | |
return null; | |
if (depth === 0) | |
return parent; | |
var child; | |
var proto; | |
if (typeof parent != 'object') { | |
return parent; | |
} | |
if (parent instanceof nativeMap) { | |
child = new nativeMap(); | |
} else if (parent instanceof nativeSet) { | |
child = new nativeSet(); | |
} else if (parent instanceof nativePromise) { | |
child = new nativePromise(function (resolve, reject) { | |
parent.then(function(value) { | |
resolve(_clone(value, depth - 1)); | |
}, function(err) { | |
reject(_clone(err, depth - 1)); | |
}); | |
}); | |
} else if (clone.__isArray(parent)) { | |
child = []; | |
} else if (clone.__isRegExp(parent)) { | |
child = new RegExp(parent.source, __getRegExpFlags(parent)); | |
if (parent.lastIndex) child.lastIndex = parent.lastIndex; | |
} else if (clone.__isDate(parent)) { | |
child = new Date(parent.getTime()); | |
} else if (useBuffer && Buffer.isBuffer(parent)) { | |
child = new Buffer(parent.length); | |
parent.copy(child); | |
return child; | |
} else if (parent instanceof Error) { | |
child = Object.create(parent); | |
} else { | |
if (typeof prototype == 'undefined') { | |
proto = Object.getPrototypeOf(parent); | |
child = Object.create(proto); | |
} | |
else { | |
child = Object.create(prototype); | |
proto = prototype; | |
} | |
} | |
if (circular) { | |
var index = allParents.indexOf(parent); | |
if (index != -1) { | |
return allChildren[index]; | |
} | |
allParents.push(parent); | |
allChildren.push(child); | |
} | |
if (parent instanceof nativeMap) { | |
var keyIterator = parent.keys(); | |
while(true) { | |
var next = keyIterator.next(); | |
if (next.done) { | |
break; | |
} | |
var keyChild = _clone(next.value, depth - 1); | |
var valueChild = _clone(parent.get(next.value), depth - 1); | |
child.set(keyChild, valueChild); | |
} | |
} | |
if (parent instanceof nativeSet) { | |
var iterator = parent.keys(); | |
while(true) { | |
var next = iterator.next(); | |
if (next.done) { | |
break; | |
} | |
var entryChild = _clone(next.value, depth - 1); | |
child.add(entryChild); | |
} | |
} | |
for (var i in parent) { | |
var attrs; | |
if (proto) { | |
attrs = Object.getOwnPropertyDescriptor(proto, i); | |
} | |
if (attrs && attrs.set == null) { | |
continue; | |
} | |
child[i] = _clone(parent[i], depth - 1); | |
} | |
if (Object.getOwnPropertySymbols) { | |
var symbols = Object.getOwnPropertySymbols(parent); | |
for (var i = 0; i < symbols.length; i++) { | |
// Don't need to worry about cloning a symbol because it is a primitive, | |
// like a number or string. | |
var symbol = symbols[i]; | |
var descriptor = Object.getOwnPropertyDescriptor(parent, symbol); | |
if (descriptor && !descriptor.enumerable && !includeNonEnumerable) { | |
continue; | |
} | |
child[symbol] = _clone(parent[symbol], depth - 1); | |
if (!descriptor.enumerable) { | |
Object.defineProperty(child, symbol, { | |
enumerable: false | |
}); | |
} | |
} | |
} | |
if (includeNonEnumerable) { | |
var allPropertyNames = Object.getOwnPropertyNames(parent); | |
for (var i = 0; i < allPropertyNames.length; i++) { | |
var propertyName = allPropertyNames[i]; | |
var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName); | |
if (descriptor && descriptor.enumerable) { | |
continue; | |
} | |
child[propertyName] = _clone(parent[propertyName], depth - 1); | |
Object.defineProperty(child, propertyName, { | |
enumerable: false | |
}); | |
} | |
} | |
return child; | |
} | |
return _clone(parent, depth); | |
} | |
/** | |
* Simple flat clone using prototype, accepts only objects, usefull for property | |
* override on FLAT configuration object (no nested props). | |
* | |
* USE WITH CAUTION! This may not behave as you wish if you do not know how this | |
* works. | |
*/ | |
clone.clonePrototype = function clonePrototype(parent) { | |
if (parent === null) | |
return null; | |
var c = function () {}; | |
c.prototype = parent; | |
return new c(); | |
}; | |
// private utility functions | |
function __objToStr(o) { | |
return Object.prototype.toString.call(o); | |
} | |
clone.__objToStr = __objToStr; | |
function __isDate(o) { | |
return typeof o === 'object' && __objToStr(o) === '[object Date]'; | |
} | |
clone.__isDate = __isDate; | |
function __isArray(o) { | |
return typeof o === 'object' && __objToStr(o) === '[object Array]'; | |
} | |
clone.__isArray = __isArray; | |
function __isRegExp(o) { | |
return typeof o === 'object' && __objToStr(o) === '[object RegExp]'; | |
} | |
clone.__isRegExp = __isRegExp; | |
function __getRegExpFlags(re) { | |
var flags = ''; | |
if (re.global) flags += 'g'; | |
if (re.ignoreCase) flags += 'i'; | |
if (re.multiline) flags += 'm'; | |
return flags; | |
} | |
clone.__getRegExpFlags = __getRegExpFlags; | |
return clone; | |
})(); | |
if (typeof module === 'object' && module.exports) { | |
module.exports = clone; | |
} | |
},{}],2:[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":4}],3:[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":4}],4:[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, typed array, or a length number | |
function MemoryView( buffer, byteOffset, byteLength ) | |
{ | |
if ( typeof buffer === 'number' ) | |
{ | |
buffer = new ArrayBuffer( buffer ); | |
} | |
// Typed arrays | |
if ( buffer.buffer ) | |
{ | |
buffer = buffer.buffer; | |
} | |
return extend( new DataView( buffer, byteOffset, byteLength ), { | |
getUint8Array: function( start, length ) | |
{ | |
return new Uint8Array( this.buffer.slice( start, start + length ) ); | |
}, | |
getUint16Array: function( start, length ) | |
{ | |
// 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 ) ).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, | |
}; | |
},{}],5:[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.buffer, 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' ) | |
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 ) | |
{ | |
if ( e instanceof Error ) | |
{ | |
e.message = 'ZVM start: ' + e.message; | |
} | |
Glk.fatal_error( e ); | |
throw e; | |
} | |
}, | |
resume: function( resumearg ) | |
{ | |
var Glk = this.Glk, | |
glk_event = this.glk_event, | |
event_type, | |
run; | |
try | |
{ | |
event_type = glk_event.get_field( 0 ); | |
console.log('resume',event_type) | |
// 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_width(); | |
} | |
// 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 ) | |
{ | |
if ( e instanceof Error ) | |
{ | |
e.message = 'ZVM: ' + e.message; | |
} | |
Glk.fatal_error( e ); | |
throw 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":3,"./common/utils.js":4,"./zvm/disassembler.js":6,"./zvm/io.js":7,"./zvm/runtime.js":9,"./zvm/text.js":10}],6:[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":2}],7:[function(require,module,exports){ | |
/* | |
Z-Machine IO | |
============ | |
Copyright (c) 2017 The ifvms.js team | |
MIT licenced | |
https://github.com/curiousdannii/ifvms.js | |
*/ | |
'use strict'; | |
/* | |
TODO: | |
- style and colour support | |
- pre-existing line input | |
- timed input | |
- mouse input | |
*/ | |
var utils = require( '../common/utils.js' ), | |
U2S = utils.U2S16, | |
//S2U = utils.S2U16, | |
// Glulx key codes accepted by the Z-Machine | |
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; | |
})(), | |
/* | |
Try to support as many of the Z-Machine's formatting combinations as possible. | |
There are not enough styles to support them all, so sometimes bold formatting misses out. | |
This spreadsheet shows how the Z-Machine formatting is mapped to Glk styles | |
http://docs.google.com/spreadsheets/d/1Nvwyb_twC3_fPYDrjQu86b3KRAmLFDllIUvPUpMz108 | |
The index bits are (lowest to highest): mono, italic, bold, reverse | |
We use the default GlkOte styles as much as possible, but for full support zvm.css must also be used | |
*/ | |
style_mappings = [ | |
// main window | |
[ 0, 2, 1, 7, 4, 7, 5, 7, 9, 10, 6, 3, 6, 3, 6, 3 ], | |
// status window | |
[ 0, 0, 1, 1, 4, 4, 5, 5, 9, 9, 6, 6, 3, 3, 7, 7 ], | |
]; | |
module.exports = { | |
init_io: function() | |
{ | |
var Glk = this.Glk; | |
this.io = this.io || { | |
reverse: 0, | |
bold: 0, | |
italic: 0, | |
// 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 | |
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 | |
if ( !this.mainwin ) | |
{ | |
this.mainwin = Glk.glk_window_open( 0, 0, 0, 3, 201 ); | |
this.set_window( 0 ) | |
if ( this.version3 ) | |
{ | |
this.statuswin = Glk.glk_window_open( this.mainwin, 0x12, 1, 4, 202 ); | |
if ( this.statuswin ) | |
{ | |
Glk.glk_set_style_stream( Glk.glk_window_get_stream( this.statuswin ), style_mappings[1][ 0x08 ] ); | |
} | |
} | |
} | |
// Set the correct window | |
/*Glk.glk_set_window( this.upperwin && this.io.currentwin ? this.upperwin : this.mainwin ); | |
if ( this.io.currentwin ) | |
{ | |
Glk.glk_window_move_cursor( this.upperwin, this.io.col, this.io.row ); | |
}*/ | |
}, | |
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 ( window === 1 || window === -2 ) | |
{ | |
if ( this.upperwin ) | |
{ | |
this.Glk.glk_window_clear( this.upperwin ); | |
this.set_cursor( 0, 0 ); | |
} | |
} | |
if ( window === -1 ) | |
{ | |
this.split_window( 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 | |
{ | |
Glk.glk_window_set_arrangement( Glk.glk_window_get_parent( this.upperwin ), 0x12, io.maxheight, null ); | |
} | |
} | |
io.seenheight = io.maxheight; | |
io.maxheight = io.height; | |
}, | |
format: function() | |
{ | |
this.Glk.glk_set_style( style_mappings[ this.io.currentwin ][ !!this.io.mono | this.io.italic | this.io.bold | 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; | |
} | |
}, | |
// 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 ) | |
{ | |
len++; | |
this.v3_status(); | |
} | |
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.glk_request_line_event_uni( this.mainwin, buffer, initiallen ); | |
this.fix_upper_window(); | |
}, | |
// Request character input | |
read_char: function( storer, one, time, routine ) | |
{ | |
// Input stream 1 | |
if ( this.io.streams[0] ) | |
{ | |
var code = this.Glk.glk_get_char_stream_uni( this.io.streams[0] ); | |
// Check for EOF | |
if ( code === -1 ) | |
{ | |
this.input_stream( 0 ); | |
} | |
else | |
{ | |
this.variable( storer, code ); | |
return this.stop = 0; | |
} | |
} | |
this.read_data = { | |
routine: routine, | |
storer: storer, | |
time: time, | |
}; | |
this.Glk.glk_request_char_event_uni( this.mainwin ); | |
this.fix_upper_window(); | |
}, | |
set_colour: function( /*foreground, background*/ ) | |
{ | |
/*if ( foreground === 1 ) | |
{ | |
this.fg = undefined; | |
} | |
if ( foreground > 1 && foreground < 13 ) | |
{ | |
this.fg = foreground; | |
} | |
if ( background === 1 ) | |
{ | |
this.bg = undefined; | |
} | |
if ( background > 1 && background < 13 ) | |
{ | |
this.bg = background; | |
}*/ | |
}, | |
// Note that row and col must be decremented in JIT code | |
set_cursor: function( row, col ) | |
{console.log('set_cursor',row,col) | |
var io = this.io; | |
if ( row >= io.height ) | |
{ | |
// Moving the cursor to a row forces the upper window | |
// to open enough for that line to exist | |
this.split_window( row + 1 ); | |
} | |
if ( this.upperwin && row >= 0 && col >= 0 && col < io.width ) | |
{ | |
this.Glk.glk_window_move_cursor( this.upperwin, col, row ); | |
io.row = row; | |
io.col = col; | |
} | |
}, | |
set_font: function( font ) | |
{ | |
// We only support fonts 1 and 4 | |
if ( font !== 1 && font !== 4 ) | |
{ | |
return 0; | |
} | |
var returnval = this.io.mono & 0x04 ? 4 : 1; | |
if ( font !== returnval ) | |
{ | |
this.io.mono ^= 0x04; | |
this.format(); | |
} | |
return returnval; | |
}, | |
// Set styles | |
set_style: function( stylebyte ) | |
{ | |
var io = this.io; | |
// Setting the style to Roman will clear the others | |
if ( stylebyte === 0 ) | |
{ | |
io.reverse = io.bold = io.italic = 0; | |
io.mono &= 0xFE; | |
} | |
if ( stylebyte & 0x01 ) | |
{ | |
io.reverse = 0x08; | |
} | |
if ( stylebyte & 0x02 ) | |
{ | |
io.bold = 0x04; | |
} | |
if ( stylebyte & 0x04 ) | |
{ | |
io.italic = 0x02; | |
} | |
if ( stylebyte & 0x08 ) | |
{ | |
io.mono |= 0x01; | |
} | |
this.format(); | |
}, | |
// Set true colours | |
set_true_colour: function( /*foreground, background*/ ) | |
{ | |
// Convert a 15 bit colour to RGB | |
/*function convert_true_colour( colour ) | |
{ | |
// Stretch the five bits per colour out to 8 bits | |
var newcolour = Math.round( ( colour & 0x1F ) * 8.226 ) << 16 | |
| Math.round( ( ( colour & 0x03E0 ) >> 5 ) * 8.226 ) << 8 | |
| Math.round( ( ( colour & 0x7C00 ) >> 10 ) * 8.226 ); | |
newcolour = newcolour.toString( 16 ); | |
// Ensure the colour is 6 bytes long | |
while ( newcolour.length < 6 ) | |
{ | |
newcolour = '0' + newcolour; | |
} | |
return '#' + newcolour; | |
} | |
if ( foreground === 0xFFFF ) | |
{ | |
this.fg = undefined; | |
} | |
else if ( foreground < 0x8000 ) | |
{ | |
this.fg = convert_true_colour( foreground ); | |
} | |
if ( background === 0xFFFF ) | |
{ | |
this.bg = undefined; | |
} | |
else if ( background < 0x8000 ) | |
{ | |
this.bg = convert_true_colour( background ); | |
}*/ | |
}, | |
set_window: function( window ) | |
{console.log('set_window',window) | |
this.io.currentwin = window; | |
// Focusing the upper window resets the cursor to the top left; | |
// it also opens the upper window if it's not open | |
if ( window ) | |
{ | |
this.set_cursor( 0, 0 ); | |
} | |
this.Glk.glk_set_window( this.upperwin && window ? this.upperwin : this.mainwin ); | |
this.format(); | |
}, | |
split_window: function( lines ) | |
{ | |
var Glk = this.Glk, | |
io = this.io, | |
row = io.row, col = io.col, | |
oldheight = io.height, | |
str; | |
io.height = lines; | |
// Erase existing lines if we are expanding into existing rows | |
if ( this.upperwin && lines > oldheight ) | |
{ | |
str = Glk.glk_window_get_stream( this.upperwin ); | |
while ( oldheight < lines ) | |
{ | |
Glk.glk_window_move_cursor( this.upperwin, 0, oldheight++ ); | |
Glk.glk_put_jstring_stream( str, Array( io.width + 1 ).join( ' ' ) ); | |
} | |
Glk.glk_window_move_cursor( this.upperwin, col, row ); | |
} | |
// Don't decrease the height of the window yet, only increase | |
if ( lines > io.maxheight ) | |
{ | |
io.maxheight = lines; | |
// Set the height of the window | |
// Create the window if it doesn't exist | |
if ( !this.upperwin ) | |
{ | |
this.upperwin = Glk.glk_window_open( this.mainwin, 0x12, io.maxheight, 4, 203 ); | |
} | |
else | |
{ | |
Glk.glk_window_set_arrangement( Glk.glk_window_get_parent( this.upperwin ), 0x12, io.maxheight, null ); | |
} | |
} | |
if ( lines ) | |
{ | |
// Reset the cursor if it is now outside the window | |
if ( io.row >= lines ) | |
{ | |
this.set_cursor( 0, 0 ); | |
} | |
// 8.6.1.1.2: In version three the upper window is always cleared | |
if ( this.version3 ) | |
{ | |
Glk.glk_window_clear( this.upperwin ); | |
} | |
} | |
}, | |
// Update the header after restarting or restoring | |
update_header: function() | |
{ | |
var ram = this.ram; | |
// Reset the Xorshift seed | |
this.xorshift_seed = 0; | |
// Update the width - in version 3 does not actually set the header variables | |
this.update_width(); | |
// For version 3 we only set Flags 1 | |
if ( this.version3 ) | |
{ | |
return ram.setUint8( 0x01, | |
( ram.getUint8( 0x01 ) & 0x8F ) // Keep all except bits 4-6 | |
| ( this.statuswin ? 0x20 : 0x10 ) // If status win is available then set 0x20 for the upper win also being available, otherwise 0x10 for the status win itself | |
| 0x40 // Variable pitch font is default - Or can we tell from options if the font is fixed pitch? | |
); | |
} | |
// Flags 1 | |
ram.setUint8( 0x01, | |
0x00 // Colour is not supported yet | |
| 0x1C // Bold, italic and mono are supported | |
| 0x00 // Timed input not supported yet | |
); | |
// Flags 2: Clear bits 3, 5, 7: no character graphics, mouse or sound effects | |
// This is really a word, but we only care about the lower byte | |
ram.setUint8( 0x11, ram.getUint8( 0x11 ) & 0x57 ); | |
// Screen settings | |
ram.setUint8( 0x20, 255 ); // Infinite height | |
if ( this.version > 4 ) | |
{ | |
ram.setUint16( 0x24, 255 ); | |
ram.setUint16( 0x26, 0x0101 ); // Font height/width in "units" | |
} | |
// Colours | |
//ram.setUint8( 0x2C, isNaN( this.options.bg ) ? 1 : this.options.bg ); | |
//ram.setUint8( 0x2D, isNaN( this.options.fg ) ? 1 : this.options.fg ); | |
//this.extension_table( 5, this.options.fg_true ); | |
//this.extension_table( 6, this.options.bg_true ); | |
// Z Machine Spec revision | |
ram.setUint16( 0x32, 0x0102 ); | |
// Clear flags three, we don't support any of that stuff | |
this.extension_table( 4, 0 ); | |
}, | |
update_width: function() | |
{ | |
var Glk = this.Glk, | |
tempwin = Glk.glk_window_open( this.mainwin, 0x12, 0, 4, 204 ), | |
box = new Glk.RefBox(), | |
width; | |
Glk.glk_window_get_size( tempwin || this.mainwin, box ); | |
if ( tempwin ) | |
{ | |
Glk.glk_window_close( tempwin ); | |
} | |
// Get the width but limit to a max of 255 | |
this.io.width = width = Math.min( box.get_value(), 255 ); | |
if ( this.version > 3 ) | |
{ | |
this.ram.setUint8( 0x21, width ); | |
} | |
if ( this.version > 4 ) | |
{ | |
this.ram.setUint16( 0x22, width ); | |
} | |
if ( this.io.col >= width ) | |
{ | |
this.io.col = width - 1; | |
} | |
}, | |
// Output the version 3 status line | |
v3_status: function() | |
{ | |
if ( !this.statuswin ) | |
{ | |
return; | |
} | |
var Glk = this.Glk, | |
str = Glk.glk_window_get_stream( this.statuswin ), | |
memory = this.m, | |
width = this.io.width, | |
hours_score = memory.getUint16( this.globals + 2 ), | |
mins_turns = memory.getUint16( this.globals + 4 ), | |
proptable = memory.getUint16( this.objects + 9 * memory.getUint16( this.globals ) + 7 ), | |
shortname = '' + this.decode( proptable + 1, memory.getUint8( proptable ) * 2 ), | |
rhs; | |
// Handle the turns/score or time | |
if ( memory.getUint8( 0x01 ) & 0x02 ) | |
{ | |
rhs = 'Time: ' + ( hours_score % 12 === 0 ? 12 : hours_score % 12 ) + ':' + ( mins_turns < 10 ? '0' : '' ) + mins_turns + ' ' + ( hours_score > 11 ? 'PM' : 'AM' ); | |
} | |
else | |
{ | |
rhs = 'Score: ' + hours_score + ' Turns: ' + mins_turns; | |
} | |
// Print a blank line in reverse | |
Glk.glk_window_move_cursor( this.statuswin, 0, 0 ); | |
Glk.glk_put_jstring_stream( str, Array( width + 1 ).join( ' ' ) ); | |
// Trim the shortname if necessary | |
Glk.glk_window_move_cursor( this.statuswin, 0, 0 ); | |
Glk.glk_put_jstring_stream( str, ' ' + shortname.slice( 0, width - rhs.length - 4 ) ); | |
// Print the right hand side | |
Glk.glk_window_move_cursor( this.statuswin, width - rhs.length - 1, 0 ); | |
Glk.glk_put_jstring_stream( str, rhs ); | |
}, | |
}; | |
},{"../common/utils.js":4}],8:[function(require,module,exports){ | |
/* | |
Z-Machine opcodes | |
================= | |
Copyright (c) 2017 The ifvms.js team | |
MIT licenced | |
https://github.com/curiousdannii/ifvms.js | |
*/ | |
'use strict'; | |
/* | |
TODO: | |
Abstract out the signed conversions such that they can be eliminated if possible | |
don't access memory directly | |
*/ | |
var AST = require( '../common/ast.js' ), | |
Variable = AST.Variable, | |
Opcode = AST.Opcode, | |
Stopper = AST.Stopper, | |
Pauser = AST.Pauser, | |
PauserStorer = AST.PauserStorer, | |
Brancher = AST.Brancher, | |
BrancherStorer = AST.BrancherStorer, | |
Storer = AST.Storer, | |
Caller = AST.Caller, | |
CallerStorer = AST.CallerStorer, | |
opcode_builder = AST.opcode_builder, | |
// Common functions, variables and opcodes | |
simple_func = function( a ) { return '' + a; }, | |
stack_var = new Variable( this.e, 0 ), | |
alwaysbranch = opcode_builder( Brancher, function() { return 1; } ), | |
not = opcode_builder( Storer, function( a ) { return 'e.S2U(~' + a + ')'; } ), | |
// Indirect storer opcodes - rather non-generic I'm afraid | |
// Not used for inc/dec | |
// @load (variable) -> (result) | |
// @pull (variable) | |
// @store (variable) value | |
Indirect = Storer.subClass({ | |
storer: 0, | |
post: function() | |
{ | |
var operands = this.operands, | |
op0 = operands[0], | |
op0isVar = op0 instanceof Variable; | |
// Replace the indirect operand with a Variable, and set .indirect if needed | |
operands[0] = new Variable( this.e, op0isVar ? op0 : op0.v ); | |
if ( op0isVar || op0.v === 0 ) | |
{ | |
operands[0].indirect = 1; | |
} | |
// Get the storer | |
this.storer = this.code === 142 ? operands.pop() : operands.shift(); | |
// @pull needs an added stack. If for some reason it was compiled with two operands this will break! | |
if ( operands.length === 0 ) | |
{ | |
operands.push( stack_var ); | |
} | |
}, | |
func: simple_func, | |
}), | |
Incdec = Opcode.subClass({ | |
func: function( variable ) | |
{ | |
var varnum = variable.v - 1, | |
operator = this.code % 2 ? 1 : -1; | |
// Fallback to the runtime function if our variable is a variable operand itself | |
// Or, if it's a global | |
if ( variable instanceof Variable || varnum > 14 ) | |
{ | |
return 'e.incdec(' + variable + ',' + operator + ')'; | |
} | |
return ( varnum < 0 ? 'e.s[e.sp-1]' : 'e.l[' + varnum + ']' ) + ( operator === 1 ? '++' : '--' ); | |
}, | |
}), | |
// Version 3 @save/restore branch instead of store | |
V3SaveRestore = Stopper.subClass({ | |
brancher: 1, | |
toString: function() | |
{ | |
return 'e.stop=1;e.' + ( this.code === 181 ? 'save' : 'restore' ) + '(' + ( this.pc + 1 ) + ')'; | |
}, | |
}), | |
V45Restore = opcode_builder( PauserStorer, function() { return 'e.restore(' + ( this.next - 1 ) + ')'; } ), | |
V45Save = opcode_builder( PauserStorer, function() { return 'e.save(' + ( this.next - 1 ) + ')'; } ); | |
/*eslint brace-style: "off" */ | |
/*eslint indent: "off" */ | |
module.exports = function( version ) | |
{ | |
return { | |
/* je */ 1: opcode_builder( Brancher, function() { return arguments.length === 2 ? this.args( '===' ) : 'e.jeq(' + this.args() + ')'; } ), | |
/* jl */ 2: opcode_builder( Brancher, function( a, b ) { return a.U2S() + '<' + b.U2S(); } ), | |
/* jg */ 3: opcode_builder( Brancher, function( a, b ) { return a.U2S() + '>' + b.U2S(); } ), | |
// Too many U2S/S2U for these... | |
/* dec_chk */ 4: opcode_builder( Brancher, function( variable, value ) { return 'e.U2S(e.incdec(' + variable + ',-1))<' + value.U2S(); } ), | |
/* inc_chk */ 5: opcode_builder( Brancher, function( variable, value ) { return 'e.U2S(e.incdec(' + variable + ',1))>' + value.U2S(); } ), | |
/* jin */ 6: opcode_builder( Brancher, function() { return 'e.jin(' + this.args() + ')'; } ), | |
/* test */ 7: opcode_builder( Brancher, function() { return 'e.test(' + this.args() + ')'; } ), | |
/* or */ 8: opcode_builder( Storer, function() { return this.args( '|' ); } ), | |
/* and */ 9: opcode_builder( Storer, function() { return this.args( '&' ); } ), | |
/* test_attr */ 10: opcode_builder( Brancher, function() { return 'e.test_attr(' + this.args() + ')'; } ), | |
/* set_attr */ 11: opcode_builder( Opcode, function() { return 'e.set_attr(' + this.args() + ')'; } ), | |
/* clear_attr */ 12: opcode_builder( Opcode, function() { return 'e.clear_attr(' + this.args() + ')'; } ), | |
/* store */ 13: Indirect, | |
/* insert_obj */ 14: opcode_builder( Opcode, function() { return 'e.insert_obj(' + this.args() + ')'; } ), | |
/* loadw */ 15: opcode_builder( Storer, function( array, index ) { return 'e.m.getUint16(e.S2U(' + array + '+2*' + index.U2S() + '))'; } ), | |
/* loadb */ 16: opcode_builder( Storer, function( array, index ) { return 'e.m.getUint8(e.S2U(' + array + '+' + index.U2S() + '))'; } ), | |
/* get_prop */ 17: opcode_builder( Storer, function() { return 'e.get_prop(' + this.args() + ')'; } ), | |
/* get_prop_addr */ 18: opcode_builder( Storer, function() { return 'e.find_prop(' + this.args() + ')'; } ), | |
/* get_next_prop */ 19: opcode_builder( Storer, function() { return 'e.find_prop(' + this.args( ',0,' ) + ')'; } ), | |
/* add */ 20: opcode_builder( Storer, function() { return 'e.S2U(' + this.args( '+' ) + ')'; } ), | |
/* sub */ 21: opcode_builder( Storer, function() { return 'e.S2U(' + this.args( '-' ) + ')'; } ), | |
/* mul */ 22: opcode_builder( Storer, function() { return 'e.S2U(' + this.args( '*' ) + ')'; } ), | |
/* div */ 23: opcode_builder( Storer, function( a, b ) { return 'e.S2U(parseInt(' + a.U2S() + '/' + b.U2S() + '))'; } ), | |
/* mod */ 24: opcode_builder( Storer, function( a, b ) { return 'e.S2U(' + a.U2S() + '%' + b.U2S() + ')'; } ), | |
/* call_2s */ 25: CallerStorer, | |
/* call_2n */ 26: Caller, | |
/* set_colour */ 27: opcode_builder( Opcode, function() { return 'e.set_colour(' + this.args() + ')'; } ), | |
/* throw */ 28: opcode_builder( Stopper, function( value, cookie ) { return 'while(e.frames.length+1>' + cookie + '){e.frameptr=e.frames.pop()}return ' + value; } ), | |
/* jz */ 128: opcode_builder( Brancher, function( a ) { return a + '===0'; } ), | |
/* get_sibling */ 129: opcode_builder( BrancherStorer, function( obj ) { return 'e.get_sibling(' + obj + ')'; } ), | |
/* get_child */ 130: opcode_builder( BrancherStorer, function( obj ) { return 'e.get_child(' + obj + ')'; } ), | |
/* get_parent */ 131: opcode_builder( Storer, function( obj ) { return 'e.get_parent(' + obj + ')'; } ), | |
/* get_prop_length */ 132: opcode_builder( Storer, function( a ) { return 'e.get_prop_len(' + a + ')'; } ), | |
/* inc */ 133: Incdec, | |
/* dec */ 134: Incdec, | |
/* print_addr */ 135: opcode_builder( Opcode, function( addr ) { return 'e.print(2,' + addr + ')'; } ), | |
/* call_1s */ 136: CallerStorer, | |
/* remove_obj */ 137: opcode_builder( Opcode, function( obj ) { return 'e.remove_obj(' + obj + ')'; } ), | |
/* print_obj */ 138: opcode_builder( Opcode, function( obj ) { return 'e.print(3,' + obj + ')'; } ), | |
/* ret */ 139: opcode_builder( Stopper, function( a ) { return 'return ' + a; } ), | |
/* jump */ 140: opcode_builder( Stopper, function( a ) { return 'e.pc=' + a.U2S() + '+' + ( this.next - 2 ); } ), | |
/* print_paddr */ 141: opcode_builder( Opcode, function( addr ) { return 'e.print(2,' + addr + '*' + this.e.addr_multipler + ')'; } ), | |
/* load */ 142: Indirect.subClass( { storer: 1 } ), | |
143: version < 5 ? | |
/* not (v3/4) */ not : | |
/* call_1n (v5/8) */ Caller, | |
/* rtrue */ 176: opcode_builder( Stopper, function() { return 'return 1'; } ), | |
/* rfalse */ 177: opcode_builder( Stopper, function() { return 'return 0'; } ), | |
// Reconsider a generalised class for @print/@print_ret? | |
/* print */ 178: opcode_builder( Opcode, function( text ) { return 'e.print(2,' + text + ')'; }, { printer: 1 } ), | |
/* print_ret */ 179: opcode_builder( Stopper, function( text ) { return 'e.print(2,' + text + ');e.print(1,13);return 1'; }, { printer: 1 } ), | |
/* nop */ 180: Opcode, | |
/* save (v3/4) */ 181: version < 4 ? | |
V3SaveRestore : | |
V45Save, | |
/* restore(v3/4) */ 182: version < 4 ? | |
V3SaveRestore : | |
V45Restore, | |
/* restart */ 183: opcode_builder( Stopper, function() { return 'e.erase_window(-1);e.restart()'; } ), | |
/* ret_popped */ 184: opcode_builder( Stopper, function( a ) { return 'return ' + a; }, { post: function() { this.operands.push( stack_var ); } } ), | |
185: version < 5 ? | |
/* pop (v3/4) */ opcode_builder( Opcode, function() { return 's[--e.sp]'; } ) : | |
/* catch (v5/8) */ opcode_builder( Storer, function() { return 'e.frames.length+1'; } ), | |
/* quit */ 186: opcode_builder( Pauser, function() { return 'e.quit=1;e.Glk.glk_exit()'; } ), | |
/* new_line */ 187: opcode_builder( Opcode, function() { return 'e.print(1,13)'; } ), | |
188: version < 4 ? | |
/* show_status (v3) */ opcode_builder( Stopper, function() { return 'e.pc=' + this.next + ';e.v3_status()'; } ) : | |
/* act as a nop in later versions */ Opcode, | |
/* verify */ 189: alwaysbranch, // Actually check?? | |
/* piracy */ 191: alwaysbranch, | |
/* call_vs */ 224: CallerStorer, | |
/* storew */ 225: opcode_builder( Opcode, function( array, index, value ) { return 'e.ram.setUint16(e.S2U(' + array + '+2*' + index.U2S() + '),' + value + ')'; } ), | |
/* storeb */ 226: opcode_builder( Opcode, function( array, index, value ) { return 'e.ram.setUint8(e.S2U(' + array + '+' + index.U2S() + '),' + value + ')'; } ), | |
/* put_prop */ 227: opcode_builder( Opcode, function() { return 'e.put_prop(' + this.args() + ')'; } ), | |
/* read */ 228: version < 5 ? | |
opcode_builder( Pauser, function() { return 'e.read(0,' + this.args() + ')'; } ) : | |
opcode_builder( PauserStorer, function() { return 'e.read(' + this.storer.v + ',' + this.args() + ')'; } ), | |
/* print_char */ 229: opcode_builder( Opcode, function( a ) { return 'e.print(4,' + a + ')'; } ), | |
/* print_num */ 230: opcode_builder( Opcode, function( a ) { return 'e.print(0,' + a.U2S() + ')'; } ), | |
/* random */ 231: opcode_builder( Storer, function( a ) { return 'e.random(' + a.U2S() + ')'; } ), | |
/* push */ 232: opcode_builder( Storer, simple_func, { post: function() { this.storer = stack_var; }, storer: 0 } ), | |
/* pull */ 233: Indirect, | |
/* split_window */ 234: opcode_builder( Opcode, function( lines ) { return 'e.split_window(' + lines + ')'; } ), | |
/* set_window */ 235: opcode_builder( Opcode, function( wind ) { return 'e.set_window(' + wind + ')'; } ), | |
/* call_vs2 */ 236: CallerStorer, | |
/* erase_window */ 237: opcode_builder( Opcode, function( win ) { return 'e.erase_window(' + win.U2S() + ')'; } ), | |
/* erase_line */ 238: opcode_builder( Opcode, function( a ) { return 'e.erase_line(' + a + ')'; } ), | |
/* set_cursor */ 239: opcode_builder( Opcode, function( row, col ) { return 'e.set_cursor(' + row + '-1,' + col + '-1)'; } ), | |
/* get_cursor */ 240: opcode_builder( Opcode, function( addr ) { return 'e.get_cursor(' + addr + ')'; } ), | |
/* set_text_style */ 241: opcode_builder( Opcode, function( stylebyte ) { return 'e.set_style(' + stylebyte + ')'; } ), | |
/* buffer_mode */ 242: Opcode, // We don't support non-buffered output | |
/* output_stream */ 243: opcode_builder( Stopper, function() { return 'e.pc=' + this.next + ';e.output_stream(' + this.args() + ')'; } ), | |
/* input_stream */ 244: opcode_builder( Pauser, function() { return 'e.input_stream(' + this.args() + ')'; } ), | |
/* sound_effect */ 245: Opcode, // We don't support sounds | |
/* read_char */ 246: opcode_builder( PauserStorer, function() { return 'e.read_char(' + this.storer.v + ',' + ( this.args() || '1' ) + ')'; } ), | |
/* scan_table */ 247: opcode_builder( BrancherStorer, function() { return 'e.scan_table(' + this.args() + ')'; } ), | |
/* not (v5/8) */ 248: not, | |
/* call_vn */ 249: Caller, | |
/* call_vn2 */ 250: Caller, | |
/* tokenise */ 251: opcode_builder( Opcode, function() { return 'e.tokenise(' + this.args() + ')'; } ), | |
/* encode_text */ 252: opcode_builder( Opcode, function() { return 'e.encode_text(' + this.args() + ')'; } ), | |
/* copy_table */ 253: opcode_builder( Opcode, function() { return 'e.copy_table(' + this.args() + ')'; } ), | |
/* print_table */ 254: opcode_builder( Opcode, function() { return 'e.print_table(' + this.args() + ')'; } ), | |
/* check_arg_count */ 255: opcode_builder( Brancher, function( arg ) { return 'e.stack.getUint8(e.frameptr+5)&(1<<(' + arg + '-1))'; } ), | |
/* save */ 1000: V45Save, | |
/* restore */ 1001: V45Restore, | |
/* log_shift */ 1002: opcode_builder( Storer, function( a, b ) { return 'e.S2U(e.log_shift(' + a + ',' + b.U2S() + '))'; } ), | |
/* art_shift */ 1003: opcode_builder( Storer, function( a, b ) { return 'e.S2U(e.art_shift(' + a.U2S() + ',' + b.U2S() + '))'; } ), | |
/* set_font */ 1004: opcode_builder( Storer, function( font ) { return 'e.set_font(' + font + ')'; } ), | |
/* save_undo */ 1009: opcode_builder( Storer, function() { return 'e.save_undo(' + this.next + ',' + this.storer.v + ')'; } ), | |
// As the standard says calling this without a save point is illegal, we don't need to actually store anything (but it must still be disassembled) | |
/* restore_undo */ 1010: opcode_builder( Opcode, function() { return 'if(e.restore_undo())return'; }, { storer: 1 } ), | |
/* print_unicode */ 1011: opcode_builder( Opcode, function( a ) { return 'e.print(1,' + a + ')'; } ), | |
// Assume we can print and read all unicode characters rather than actually testing | |
/* check_unicode */ 1012: opcode_builder( Storer, function() { return 3; } ), | |
/* set_true_colour */ 1013: opcode_builder( Opcode, function() { return 'e.set_true_colour(' + this.args() + ')'; } ), | |
/* sound_data */ 1014: Opcode.subClass( { brancher: 1 } ), // We don't support sounds (but disassemble the branch address) | |
/* gestalt */ 1030: opcode_builder( Storer, function() { return 'e.gestalt(' + this.args() + ')'; } ), | |
/* parchment */ //1031: opcode_builder( Storer, function() { return 'e.op_parchment(' + this.args() + ')'; } ), | |
}; | |
}; | |
},{"../common/ast.js":2}],9:[function(require,module,exports){ | |
/* | |
Z-Machine runtime functions | |
=========================== | |
Copyright (c) 2017 The ifvms.js team | |
MIT licenced | |
https://github.com/curiousdannii/ifvms.js | |
*/ | |
'use strict'; | |
/* | |
TODO: | |
Save/restore: table, name, prompt support | |
*/ | |
const cloneDeep = require( 'clone' ) | |
const file = require( '../common/file.js' ) | |
const utils = require( '../common/utils.js' ) | |
const extend = utils.extend | |
const U2S = utils.U2S16 | |
const S2U = utils.S2U16 | |
// 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() | |
this.restore_file( new Uint8Array( 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 ) | |
{ | |
snapshot = { | |
glk: this.Glk.save_allstate(), | |
io: cloneDeep( this.io ), | |
ram: this.save_file( this.pc, 1 ), | |
read_data: cloneDeep( this.read_data ), | |
xorshift_seed: this.xorshift_seed, | |
} | |
delete snapshot.io.streams[2].str | |
delete snapshot.io.streams[2].str | |
delete snapshot.read_data.buffer | |
} | |
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; | |
case 0x2000: | |
return 1; | |
// These aren't really applicable, but 2 is closer than 1 | |
case 0x2001: | |
case 0x2002: | |
return 2; | |
} | |
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() | |
{ | |
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(); | |
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() ), | |
frames = this.frames.slice(), | |
zeroes = 0, | |
i, j, | |
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 ); | |
frames.push( this.frameptr ); | |
// Swap the bytes of the locals and stacks | |
if ( littleEndian && !autosaving ) | |
{ | |
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 | |
if ( this.undo_len > this.options.undo_len ) | |
{ | |
this.undo.shift(); | |
} | |
this.undo_len += this.staticmem + this.s.byteOffset + this.sp * 2; | |
this.undo.push({ | |
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, | |
}); | |
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, | |
}; | |
},{"../common/file.js":3,"../common/utils.js":4,"./opcodes.js":8,"clone":1}],10:[function(require,module,exports){ | |
/* | |
Z-Machine text functions | |
======================== | |
Copyright (c) 2017 The ifvms.js team | |
MIT licenced | |
https://github.com/curiousdannii/ifvms.js | |
*/ | |
/* | |
TODO: | |
Consider quote suggestions from 1.1 spec | |
*/ | |
module.exports = { | |
init_text: function() | |
{ | |
var self = this, | |
memory = this.m, | |
alphabet_addr = ( this.version > 4 ) && memory.getUint16( 0x34 ), | |
unicode_addr = this.extension_table( 3 ), | |
unicode_len = unicode_addr && memory.getUint8( unicode_addr++ ); | |
this.abbr_addr = memory.getUint16( 0x18 ); | |
// Generate alphabets | |
function make_alphabet( data ) | |
{ | |
var alphabets = [[], [], []], | |
i = 0; | |
while ( i < 78 ) | |
{ | |
alphabets[( i / 26 ) | 0][i % 26] = data[ i++ ]; | |
} | |
// A2->7 is always a newline | |
alphabets[2][1] = 13; | |
self.alphabets = alphabets; | |
} | |
// Make the unicode tables | |
function make_unicode( data ) | |
{ | |
var table = { 13: '\r' }, // New line conversion | |
reverse = { 13: 13 }, | |
i = 0; | |
while ( i < data.length ) | |
{ | |
table[155 + i] = String.fromCharCode( data[i] ); | |
reverse[data[i]] = 155 + i++; | |
} | |
i = 32; | |
while ( i < 127 ) | |
{ | |
table[i] = String.fromCharCode( i ); | |
reverse[i] = i++; | |
} | |
self.unicode_table = table; | |
self.reverse_unicode_table = reverse; | |
} | |
// Check for custom alphabets | |
make_alphabet( alphabet_addr ? memory.getUint8Array( alphabet_addr, 78 ) | |
// Or use the standard alphabet | |
: this.text_to_zscii( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \r0123456789.,!?_#\'"/\\-:()', 1 ) ); | |
// Check for a custom unicode table | |
make_unicode( unicode_addr ? memory.getUint16Array( unicode_addr, unicode_len ) | |
// Or use the default | |
: this.text_to_zscii( unescape( '%E4%F6%FC%C4%D6%DC%DF%BB%AB%EB%EF%FF%CB%CF%E1%E9%ED%F3%FA%FD%C1%C9%CD%D3%DA%DD%E0%E8%EC%F2%F9%C0%C8%CC%D2%D9%E2%EA%EE%F4%FB%C2%CA%CE%D4%DB%E5%C5%F8%D8%E3%F1%F5%C3%D1%D5%E6%C6%E7%C7%FE%F0%DE%D0%A3%u0153%u0152%A1%BF' ), 1 ) ); | |
// Parse the standard dictionary | |
this.dictionaries = {}; | |
this.dict = memory.getUint16( 0x08 ); | |
this.parse_dict( this.dict ); | |
// Optimise our own functions | |
/*if ( DEBUG ) | |
{ | |
if ( !debugflags.nooptimise ) | |
optimise_obj( this, 'TEXT' ); | |
}*/ | |
}, | |
// Decode Z-chars into ZSCII and then Unicode | |
decode: function( addr, length ) | |
{ | |
var memory = this.m, | |
start_addr = addr, | |
temp, | |
buffer = [], | |
i = 0, | |
zchar, | |
alphabet = 0, | |
result = [], | |
resulttexts = [], | |
usesabbr, | |
tenbit, | |
unicodecount = 0; | |
// Check if this one's been cached already | |
if ( this.jit[addr] ) | |
{ | |
return this.jit[addr]; | |
} | |
// If we've been given a length, then use it as the finaladdr, | |
// Otherwise don't go past the end of the file | |
length = length ? length + addr : this.eof; | |
// Go through until we've reached the end of the text or a stop bit | |
while ( addr < length ) | |
{ | |
temp = memory.getUint16( addr ); | |
addr += 2; | |
buffer.push( temp >> 10 & 0x1F, temp >> 5 & 0x1F, temp & 0x1F ); | |
// Stop bit | |
if ( temp & 0x8000 ) | |
{ | |
break; | |
} | |
} | |
// Process the Z-chars | |
while ( i < buffer.length ) | |
{ | |
zchar = buffer[i++]; | |
// Special chars | |
// Space | |
if ( zchar === 0 ) | |
{ | |
result.push( 32 ); | |
} | |
// Abbreviations | |
else if ( zchar < 4 ) | |
{ | |
usesabbr = 1; | |
result.push( -1 ); | |
resulttexts.push( '\uE000+this.abbr(' + ( 32 * ( zchar - 1 ) + buffer[i++] ) + ')+\uE000' ); | |
} | |
// Shift characters | |
else if ( zchar < 6 ) | |
{ | |
alphabet = zchar; | |
} | |
// Check for a 10 bit ZSCII character | |
else if ( alphabet === 2 && zchar === 6 ) | |
{ | |
// Check we have enough Z-chars left. | |
if ( i + 1 < buffer.length ) | |
{ | |
tenbit = buffer[i++] << 5 | buffer[i++]; | |
// A regular character | |
if ( tenbit < 768 ) | |
{ | |
result.push( tenbit ); | |
} | |
// 1.1 spec Unicode strings - not the most efficient code, but then noone uses this | |
else | |
{ | |
tenbit -= 767; | |
unicodecount += tenbit; | |
temp = i; | |
i = ( i % 3 ) + 3; | |
while ( tenbit-- ) | |
{ | |
result.push( -1 ); | |
resulttexts.push( String.fromCharCode( buffer[i] << 10 | buffer[i + 1] << 5 | buffer[i + 2] ) ); | |
// Set those characters so they won't be decoded again | |
buffer[i++] = buffer[i++] = buffer[i++] = 0x20; | |
} | |
i = temp; | |
} | |
} | |
} | |
// Regular characters | |
else if ( zchar < 0x20 ) | |
{ | |
result.push( this.alphabets[alphabet][ zchar - 6 ] ); | |
} | |
// Reset the alphabet | |
alphabet = alphabet < 4 ? 0 : alphabet - 3; | |
// Add to the index if we've had raw unicode | |
if ( ( i % 3 ) === 0 ) | |
{ | |
i += unicodecount; | |
unicodecount = 0; | |
} | |
} | |
result = this.zscii_to_text( result, resulttexts ); | |
// Abbreviations must be extracted at run time, so return a function instead | |
if ( usesabbr ) | |
{ | |
result = { | |
toString: ( Function( 'return"' + result.replace( /\\/g, '\\\\' ).replace( /"/g, '\\"' ).replace( /\r/g, '\\r' ).replace( /\uE000/g, '"' ) + '"' ) ).bind( this ), | |
}; | |
} | |
// Cache and return | |
if ( start_addr >= this.staticmem ) | |
{ | |
this.jit[start_addr] = result; | |
} | |
return result; | |
}, | |
// Encode ZSCII into Z-chars | |
encode: function( zscii ) | |
{ | |
var alphabets = this.alphabets, | |
zchars = [], | |
word_len = this.version3 ? 6 : 9, | |
i = 0, | |
achar, | |
temp, | |
result = []; | |
// Encode the Z-chars | |
while ( zchars.length < word_len ) | |
{ | |
achar = zscii[i++]; | |
// Space | |
if ( achar === 32 ) | |
{ | |
zchars.push( 0 ); | |
} | |
// Alphabets | |
else if ( ( temp = alphabets[0].indexOf( achar ) ) >= 0 ) | |
{ | |
zchars.push( temp + 6 ); | |
} | |
else if ( ( temp = alphabets[1].indexOf( achar ) ) >= 0 ) | |
{ | |
zchars.push( 4, temp + 6 ); | |
} | |
else if ( ( temp = alphabets[2].indexOf( achar ) ) >= 0 ) | |
{ | |
zchars.push( 5, temp + 6 ); | |
} | |
// 10-bit ZSCII / Unicode table | |
else if ( ( temp = this.reverse_unicode_table[achar] ) ) | |
{ | |
zchars.push( 5, 6, temp >> 5, temp & 0x1F ); | |
} | |
// Pad character | |
else if ( achar === undefined ) | |
{ | |
zchars.push( 5 ); | |
} | |
} | |
zchars.length = word_len; | |
// Encode to bytes | |
i = 0; | |
while ( i < word_len ) | |
{ | |
result.push( zchars[i++] << 2 | zchars[i] >> 3, ( zchars[i++] & 0x07 ) << 5 | zchars[i++] ); | |
} | |
result[ result.length - 2 ] |= 0x80; | |
return result; | |
}, | |
// In these two functions zscii means an array of ZSCII codes and text means a regular Javascript unicode string | |
zscii_to_text: function( zscii, texts ) | |
{ | |
var i = 0, l = zscii.length, | |
charr, | |
j = 0, | |
result = ''; | |
while ( i < l ) | |
{ | |
charr = zscii[i++]; | |
// Text substitution from abbreviations or 1.1 unicode | |
if ( charr === -1 ) | |
{ | |
result += texts[j++]; | |
} | |
// Regular characters | |
if ( ( charr = this.unicode_table[charr] ) ) | |
{ | |
result += charr; | |
} | |
} | |
return result; | |
}, | |
// If the second argument is set then don't use the unicode table | |
text_to_zscii: function( text, notable ) | |
{ | |
var array = [], i = 0, l = text.length, charr; | |
while ( i < l ) | |
{ | |
charr = text.charCodeAt( i++ ); | |
// Check the unicode table | |
if ( !notable ) | |
{ | |
charr = this.reverse_unicode_table[charr] || 63; | |
} | |
array.push( charr ); | |
} | |
return array; | |
}, | |
// Parse and cache a dictionary | |
parse_dict: function( addr ) | |
{ | |
var memory = this.m, | |
addr_start = addr, | |
dict = {}, | |
entry_len, | |
endaddr, | |
// Get the word separators | |
seperators_len = memory.getUint8( addr++ ); | |
// Support: IE, Safari, Firefox<38, Chrome<45, Opera<32, Node<4 | |
// These browsers don't support Uint8Array.indexOf() so convert to a normal array | |
dict.separators = Array.prototype.slice.call( memory.getUint8Array( addr, seperators_len ) ); | |
addr += seperators_len; | |
// Go through the dictionary and cache its entries | |
entry_len = memory.getUint8( addr++ ); | |
endaddr = addr + 2 + entry_len * memory.getUint16( addr ); | |
addr += 2; | |
while ( addr < endaddr ) | |
{ | |
dict[ Array.prototype.toString.call( memory.getUint8Array( addr, this.version3 ? 4 : 6 ) ) ] = addr; | |
addr += entry_len; | |
} | |
this.dictionaries[addr_start] = dict; | |
return dict; | |
}, | |
// Print an abbreviation | |
abbr: function( abbrnum ) | |
{ | |
return this.decode( this.m.getUint16( this.abbr_addr + 2 * abbrnum ) * 2 ); | |
}, | |
// Tokenise a text | |
tokenise: function( bufaddr, parseaddr, dictionary, flag ) | |
{ | |
// Use the default dictionary if one wasn't provided | |
dictionary = dictionary || this.dict; | |
// Parse the dictionary if needed | |
dictionary = this.dictionaries[dictionary] || this.parse_dict( dictionary ); | |
var memory = this.m, | |
ram = this.ram, | |
bufferlength = 1e3, | |
i = 1, | |
letter, | |
separators = dictionary.separators, | |
word, | |
words = [], | |
max_words, | |
dictword, | |
wordcount = 0; | |
// In versions 5 and 8 we can get the actual buffer length | |
if ( this.version > 4 ) | |
{ | |
bufferlength = memory.getUint8( bufaddr + i++ ) + 2; | |
} | |
// Find the words, separated by the separators, but as well as the separators themselves | |
while ( i < bufferlength ) | |
{ | |
letter = memory.getUint8( bufaddr + i ); | |
if ( letter === 0 ) | |
{ | |
break; | |
} | |
else if ( letter === 32 || separators.indexOf( letter ) >= 0 ) | |
{ | |
if ( letter !== 32 ) | |
{ | |
words.push( [ [letter], i ] ); | |
} | |
word = null; | |
} | |
else | |
{ | |
if ( !word ) | |
{ | |
words.push( [ [], i ] ); | |
word = words[ words.length - 1 ][0]; | |
} | |
word.push( letter ); | |
} | |
i++; | |
} | |
// Go through the text until we either have reached the max number of words, or we're out of words | |
max_words = Math.min( words.length, memory.getUint8( parseaddr ) ); | |
while ( wordcount < max_words ) | |
{ | |
dictword = dictionary['' + this.encode( words[wordcount][0] )]; | |
// If the flag is set then don't overwrite words which weren't found | |
if ( !flag || dictword ) | |
{ | |
// Fill out the buffer | |
ram.setUint16( parseaddr + 2 + wordcount * 4, dictword || 0 ); | |
ram.setUint8( parseaddr + 4 + wordcount * 4, words[wordcount][0].length ); | |
ram.setUint8( parseaddr + 5 + wordcount * 4, words[wordcount][1] ); | |
} | |
wordcount++; | |
} | |
// Update the number of found words | |
ram.setUint8( parseaddr + 1, wordcount ); | |
}, | |
}; | |
},{}]},{},[5])(5) | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment