diff --git a/lib/co/index.js b/lib/co/index.js new file mode 100644 index 0000000..aacf6ef --- /dev/null +++ b/lib/co/index.js @@ -0,0 +1,239 @@ + +/** + * slice() reference. + */ + +var slice = Array.prototype.slice; + +/** + * Expose `co`. + */ + +module.exports = co['default'] = co.co = co; + +/** + * Wrap the given generator `fn` into a + * function that returns a promise. + * This is a separate function so that + * every `co()` call doesn't create a new, + * unnecessary closure. + * + * @param {GeneratorFunction} fn + * @return {Function} + * @api public + */ + +co.wrap = function (fn) { + createPromise.__generatorFunction__ = fn; + return createPromise; + function createPromise() { + return co.call(this, fn.apply(this, arguments)); + } +}; + +/** + * Execute the generator function or a generator + * and return a promise. + * + * @param {Function} fn + * @return {Promise} + * @api public + */ + +function co(gen) { + var ctx = this; + var args = slice.call(arguments, 1); + + // we wrap everything in a promise to avoid promise chaining, + // which leads to memory leak errors. + // see https://github.com/tj/co/issues/180 + return new Promise(function(resolve, reject) { + if (typeof gen === 'function') gen = gen.apply(ctx, args); + if (!gen || typeof gen.next !== 'function') return resolve(gen); + + onFulfilled(); + + /** + * @param {Mixed} res + * @return {Promise} + * @api private + */ + + function onFulfilled(res) { + var ret; + try { + ret = gen.next(res); + } catch (e) { + return reject(e); + } + next(ret); + return null; + } + + /** + * @param {Error} err + * @return {Promise} + * @api private + */ + + function onRejected(err) { + var ret; + try { + ret = gen.throw(err); + } catch (e) { + return reject(e); + } + next(ret); + } + + /** + * Get the next value in the generator, + * return a promise. + * + * @param {Object} ret + * @return {Promise} + * @api private + */ + + function next(ret) { + if (ret.done) return resolve(ret.value); + var value = toPromise.call(ctx, ret.value); + if (value && isPromise(value)) return value.then(onFulfilled, onRejected); + return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + + 'but the following object was passed: "' + String(ret.value) + '"')); + } + }); +} + +/** + * Convert a `yield`ed value into a promise. + * + * @param {Mixed} obj + * @return {Promise} + * @api private + */ + +function toPromise(obj) { + if (!obj) return obj; + if (isPromise(obj)) return obj; + if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); + if ('function' == typeof obj) return thunkToPromise.call(this, obj); + if (Array.isArray(obj)) return arrayToPromise.call(this, obj); + if (isObject(obj)) return objectToPromise.call(this, obj); + return obj; +} + +/** + * Convert a thunk to a promise. + * + * @param {Function} + * @return {Promise} + * @api private + */ + +function thunkToPromise(fn) { + var ctx = this; + return new Promise(function (resolve, reject) { + fn.call(ctx, function (err, res) { + if (err) return reject(err); + if (arguments.length > 2) res = slice.call(arguments, 1); + resolve(res); + }); + }); +} + +/** + * Convert an array of "yieldables" to a promise. + * Uses `Promise.all()` internally. + * + * @param {Array} obj + * @return {Promise} + * @api private + */ + +function arrayToPromise(obj) { + return Promise.all(obj.map(toPromise, this)); +} + +/** + * Convert an object of "yieldables" to a promise. + * Uses `Promise.all()` internally. + * + * @param {Object} obj + * @return {Promise} + * @api private + */ + +function objectToPromise(obj){ + var results = new obj.constructor(); + var keys = Object.keys(obj); + var promises = []; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var promise = toPromise.call(this, obj[key]); + if (promise && isPromise(promise)) defer(promise, key); + else results[key] = obj[key]; + } + return Promise.all(promises).then(function () { + return results; + }); + + function defer(promise, key) { + // predefine the key in the result + results[key] = undefined; + promises.push(promise.then(function (res) { + results[key] = res; + })); + } +} + +/** + * Check if `obj` is a promise. + * + * @param {Object} obj + * @return {Boolean} + * @api private + */ + +function isPromise(obj) { + return 'function' == typeof obj.then; +} + +/** + * Check if `obj` is a generator. + * + * @param {Mixed} obj + * @return {Boolean} + * @api private + */ + +function isGenerator(obj) { + return 'function' == typeof obj.next && 'function' == typeof obj.throw; +} + +/** + * Check if `obj` is a generator function. + * + * @param {Mixed} obj + * @return {Boolean} + * @api private + */ + +function isGeneratorFunction(obj) { + var constructor = obj.constructor; + if (!constructor) return false; + if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true; + return isGenerator(constructor.prototype); +} + +/** + * Check for plain object. + * + * @param {Mixed} val + * @return {Boolean} + * @api private + */ + +function isObject(val) { + return Object == val.constructor; +} diff --git a/lib/zork/index.d.ts b/lib/zork/index.d.ts new file mode 100644 index 0000000..d87b0c0 --- /dev/null +++ b/lib/zork/index.d.ts @@ -0,0 +1,98 @@ +declare class JSZM { + constructor(arr: ArrayLike); + + /** + * A generator function you define, which will be called to update the highlighting mode. + * fixpitch (if the argument is true) or normal (if argument is false). + */ + highlight(fixpitch: boolean): void; + + /** Normally false. Set it to true to tell the game that it is a Tandy computer; this affects some games. */ + isTandy: boolean; + + /** + * A generator function that you must define that will be called to print text. You must implement + * wrapping, buffering and scripting yourself. The second argument is true if it should be + * copied to the transcript or false if it should not be. + */ + print(text: string, scripting: boolean): void; + + /** + * A generator function which you must define which should return a string containing the player's input. + * Called when a READ instruction is executed. The argument is the maximum number of characters that are + * allowed (if you return a longer string, it will be truncated) + */ + read(maxlen: number): void; + + /** + * A generator function you can optionally define. When the game starts or if restarted (with the RESTART + * instruction), it will be called after memory is initialized but before executing any more. + */ + restarted(): void; + + /** + * A generator function you define, which is called when restoring a saved game. Return a Uint8Array with + * the same contents passed to save() if successful, or you can return false or null or undefined if it failed. + */ + restore(): void; + + /** + * A generator function. Call it to run the program from the beginning, and call the next() method of the + * returned object to begin and to continue. This generator may call your own generator functions which + * may yield; it doesn't otherwise yield by itself. You must set up the other methods before calling run + * so that it can properly set up the contents of the Z-machine mode byte. + * + * This generator only finishes when a QUIT instruction is executed. + */ + run(): IterableIterator; + + /** + * A generator function you can define yourself, and is called when saving the game. The argument is a Uint8Array, + * and you should attempt to save its contents somewhere, and then return true if successful or false if it failed. + */ + save(buf: Uint8Array): void; + + /** The serial number of the story file, as six ASCII characters. */ + serial: string; + + /** + * Normally null. You can set it to a generator function which will be called when the SCREEN opcode is executed if + * you want to implement split screen. + */ + screen(window: number): void; + + /** + * Normally null. You can set it to a generator function which will be called when the SPLIT opcode is executed if + * you want to implement split screen. + */ + split(height: number): void; + + /** + * False for score/moves and true for hours/minutes. Use this to determine the meaning of arguments to updateStatusLine. + */ + statusType: boolean; + + /** + * Normally null, but can be a generator function if you are implementing the status line. It is called when a READ or + * USL instruction is executed. See statusType for the meaning of v18 and v17. Return value is unused. + */ + updateStatusLine(text: string, v18: number, v17: number): void; + + /** + * A normal function. Calling it will attempt to verify the story file, and returns true if successful or false on error. + * You can override it with your own verification function if you want to. + */ + verify(): void; + + /** The ZORKID of the story file. This is what is normally displayed as the release number. */ + zorkid: number; + + static version: { + major: number; + minor: number; + subminor: number; + timestamp: number; + }; +} + +export = JSZM; \ No newline at end of file diff --git a/lib/zork/index.js b/lib/zork/index.js new file mode 100644 index 0000000..d798ab8 --- /dev/null +++ b/lib/zork/index.js @@ -0,0 +1 @@ +module.exports = require('./jszm') \ No newline at end of file diff --git a/lib/zork/jszm.js b/lib/zork/jszm.js new file mode 100644 index 0000000..dbd44ab --- /dev/null +++ b/lib/zork/jszm.js @@ -0,0 +1,682 @@ +/* + JSZM - JavaScript implementation of Z-machine + This program is in public domain. + + Documentation: + + The exported function called JSZM is the constructor, which takes a + Uint8Array as input. You can also use JSZM.Version for the version + number which is object with properties: major, minor, subminor, + timestamp. Properties of JSZM instances are: + + .highlight(fixpitch) = A generator function you define, which will be + called to update the highlighting mode, which is fixpitch (if the + argument is true) or normal (if argument is false). (You don't have to + set it if you aren't implementing variable pitch by default.) + + .isTandy = A boolean, normally false. Set it to true to tell the game + that it is a Tandy computer; this affects some games. + + .print(text,scripting) = A generator function that you must define, and + will be called to print text. You must implement wrapping and buffering + and scripting yourself. The second argument is true if it should be + copied to the transcript or false if it should not be. + + .read(maxlen) = A generator function which you must define yourself, and + which should return a string containing the player's input. Called when + a READ instruction is executed; the argument is the maximum number of + characters that are allowed (if you return a longer string, it will be + truncated). + + .restarted() = A generator function you can optionally define. When the + game starts or if restarted (with the RESTART instruction), it will be + called after memory is initialized but before executing any more. + + .restore() = A generator function you can define yourself, which is + called when restoring a saved game. Return a Uint8Array with the same + contents passed to save() if successful, or you can return false or null + or undefined if it failed. + + .run() = A generator function. Call it to run the program from the + beginning, and call the next() method of the returned object to begin + and to continue. This generator may call your own generator functions + which may yield; it doesn't otherwise yield by itself. You must set up + the other methods before calling run so that it can properly set up the + contents of the Z-machine mode byte. This generator only finishes when a + QUIT instruction is executed. + + .save(buf) = A generator function you can define yourself, and is called + when saving the game. The argument is a Uint8Array, and you should + attempt to save its contents somewhere, and then return true if + successful or false if it failed. + + .serial = The serial number of the story file, as six ASCII characters. + + .screen(window) = Normally null. You can set it to a generator function + which will be called when the SCREEN opcode is executed if you want to + implement split screen. + + .split(height) = Normally null. You can set it to a generator function + which will be called when the SPLIT opcode is executed if you want to + implement split screen. + + .statusType = False for score/moves and true for hours/minutes. Use this + to determine the meaning of arguments to updateStatusLine. + + .updateStatusLine(text,v18,v17) = Normally null, but can be a generator + function if you are implementing the status line. It is called when a + READ or USL instruction is executed. See statusType for the meaning of + v18 and v17. Return value is unused. + + .verify() = A normal function. Calling it will attempt to verify the + story file, and returns true if successful or false on error. You can + override it with your own verification function if you want to. + + .zorkid = The ZORKID of the story file. This is what is normally + displayed as the release number. +*/ + +"use strict"; + +const JSZM_Version={major:2,minor:0,subminor:2,timestamp:1480624305074}; + +function JSZM(arr) { + var mem; + mem=this.memInit=new Uint8Array(arr); + if(mem[0]!=3) throw new Error("Unsupported Z-code version."); + this.byteSwapped=!!(mem[1]&1); + this.statusType=!!(mem[1]&2); + this.serial=String.fromCharCode(...mem.slice(18,24)); + this.zorkid=(mem[2]<<(this.byteSwapped?0:8))|(mem[3]<<(this.byteSwapped?8:0)); +} + +JSZM.prototype={ + byteSwapped: false, + constructor: JSZM, + deserialize: function(ar) { + var e,i,j,ds,cs,pc,vi,purbot; + var g8,g16s,g16,g24,g32; + g8=()=>ar[e++]; + g16s=()=>(e+=2,vi.getInt16(e-2)); + g16=()=>(e+=2,vi.getUint16(e-2)); + g24=()=>(e+=3,vi.getUint32(e-4)&0xFFFFFF); + g32=()=>(e+=4,vi.getUint32(e-4)); + try { + e=purbot=this.getu(14); + vi=new DataView(ar.buffer); + if(ar[2]!=this.mem[2] || ar[3]!=this.mem[3]) return null; // ZORKID does not match + pc=g32(); + cs=new Array(g16()); + ds=Array.from({length:g16()},g16s); + for(i=0;i { + if(ts==3) { + y=v<<5; + ts=4; + } else if(ts==4) { + y+=v; + if(y==13) o+="\n"; + else if(y) o+=String.fromCharCode(y); + ts=ps; + } else if(ts==5) { + o+=this.getText(this.getu(this.fwords+(y+v)*2)*2); + ts=ps; + } else if(v==0) { + o+=" "; + } else if(v<4) { + ts=5; + y=(v-1)*32; + } else if(v<6) { + if(!ts) ts=v-3; + else if(ts==v-3) ps=ts; + else ps=ts=0; + } else if(v==6 && ts==2) { + ts=3; + } else { + o+="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*\n0123456789.,!?_#'\"/\\-:()"[ts*26+v-6]; + ts=ps; + } + }; + for(;;) { + w=this.getu(addr); + addr+=2; + d((w>>10)&31); + d((w>>5)&31); + d(w&31); + if(w&32768) break; + } + this.endText=addr; + return o; + }, + getu: function(x) { return this.view.getUint16(x,this.byteSwapped); }, + handleInput: function(str,t1,t2) { + var i,br,w; + // Put text + str=str.toLowerCase().slice(0,this.mem[t1]-1); + for(i=0;i(i=0,x.split("").filter(y => (i+=/[a-z]/.test(y)?1:/[0-9.,!?_#'"\/\\:\-()]/.test(y)?2:4)<7).join("")); + br=JSON.parse("["+str.replace(this.regBreak,(m,o)=>",["+(m.length)+","+(this.vocabulary.get(w(m))||0)+","+(o+1)+"]").slice(1)+"]"); + i=this.mem[t2+1]=br.length; + while(i--) { + this.putu(t2+i*4+2,br[i][1]); + this.mem[t2+i*4+4]=br[i][0]; + this.mem[t2+i*4+5]=br[i][2]; + } + }, + highlight: ()=>[], + isTandy: false, + mem: null, + memInit: null, + parseVocab: function(s) { + this.vocabulary=new Map(); + + if (s === 0) { // If the story file does not contain a dictionary.. + this.regBreak=new RegExp("[^ \\n\\t]+","g"); // use the default word separators + return; // and early exit. + } + + var e; + var n; + n=this.mem[s++]; + e=this.selfInsertingBreaks=String.fromCharCode(...this.mem.slice(s,s+n)); + e=e.split("").map(x=>(x.toUpperCase()==x.toLowerCase()?"":"\\")+x).join("")+"]"; + this.regBreak=new RegExp("["+e+"|[^ \\n\\t"+e+"+","g"); + s+=n; + e=this.mem[s++]; + n=this.get(s); + s+=2; + while(n--) { + this.vocabulary.set(this.getText(s),s); + s+=e; + } + }, + print: ()=>[], + put: function(x,y) { return this.view.setInt16(x,y,this.byteSwapped); }, + putu: function(x,y) { return this.view.setUint16(x,y&65535,this.byteSwapped); }, + read: ()=>[], + regBreak: null, + restarted: ()=>[], + restore: ()=>[], + run: function*() { + var mem,pc,cs,ds,op0,op1,op2,op3,opc,inst,x,y,z; + var globals,objects,fwords,defprop; + var addr,fetch,flagset,init,move,opfetch,pcfetch,pcget,pcgetb,pcgetu,predicate,propfind,ret,store,xfetch,xstore; + + // Functions + addr=(x) => (x&65535)<<1; + fetch=(x) => { + if(x==0) return ds.pop(); + if(x<16) return cs[0].local[x-1]; + return this.get(globals+2*x); + }; + flagset=() => { + op3=1<<(15&~op1); + op2=objects+op0*9+(op1&16?2:0); + opc=this.get(op2); + }; + const initRng = () => { + this.seed = (Math.random() * 0xFFFFFFFF) >>> 0; + }; + init=() => { + mem=this.mem=new Uint8Array(this.memInit); + this.view=new DataView(mem.buffer); + mem[1]&=3; + if(this.isTandy) mem[1]|=8; + if(!this.updateStatusLine) mem[1]|=16; + if(this.screen && this.split) mem[1]|=32; + this.put(16,this.savedFlags); + if(!this.vocabulary) this.parseVocab(this.getu(8)); + defprop=this.getu(10)-2; + globals=this.getu(12)-32; + this.fwords=fwords=this.getu(24); + cs=[]; + ds=[]; + pc=this.getu(6); + objects=defprop+55; + initRng(); + }; + move=(x,y) => { + var w,z; + // Remove from old FIRST-NEXT chain + if(z=mem[objects+x*9+4]) { + if(mem[objects+z*9+6]==x) { // is x.loc.first=x? + mem[objects+z*9+6]=mem[objects+x*9+5]; // x.loc.first=x.next + } else { + z=mem[objects+z*9+6]; // z=x.loc.first + while(z!=x) { + w=z; + z=mem[objects+z*9+5]; // z=z.next + } + mem[objects+w*9+5]=mem[objects+x*9+5]; // w.next=x.next + } + } + // Insert at beginning of new FIRST-NEXT chain + if(mem[objects+x*9+4]=y) { // x.loc=y + mem[objects+x*9+5]=mem[objects+y*9+6]; // x.next=y.first + mem[objects+y*9+6]=x; // y.first=x + } else { + mem[objects+x*9+5]=0; // x.next=0 + } + }; + opfetch=(x,y) => { + if((x&=3)==3) return; + opc=y; + return [pcget,pcgetb,pcfetch][x](); + }; + pcfetch=(x) => fetch(mem[pc++]); + pcget=() => { + pc+=2; + return this.get(pc-2); + }; + pcgetb=() => mem[pc++]; + pcgetu=() => { + pc+=2; + return this.getu(pc-2); + }; + predicate=(p) => { + var x=pcgetb(); + if(x&128) p=!p; + if(x&64) x&=63; else x=((x&63)<<8)|pcgetb(); + if(p) return; + if(x==0 || x==1) return ret(x); + if(x&0x2000) x-=0x4000; + pc+=x-2; + }; + propfind=() => { + var z=this.getu(objects+op0*9+7); + z+=mem[z]*2+1; + while(mem[z]) { + if((mem[z]&31)==op1) { + op3=z+1; + return true; + } else { + z+=(mem[z]>>5)+2; + } + } + op3=0; + return false; + }; + ret=(x) => { + ds=cs[0].ds; + pc=cs[0].pc; + cs.shift(); + store(x); + }; + store=(y) => { + var x=pcgetb(); + if(x==0) ds.push(y); + else if(x<16) cs[0].local[x-1]=y; + else this.put(globals+2*x,y); + }; + xfetch=(x) => { + if(x==0) return ds[ds.length-1]; + if(x<16) return cs[0].local[x-1]; + return this.get(globals+2*x); + }; + xstore=(x,y) => { + if(x==0) ds[ds.length-1]=y; + else if(x<16) cs[0].local[x-1]=y; + else this.put(globals+2*x,y); + }; + + // Initializations + init(); + yield*this.restarted(); + yield*this.highlight(!!(this.savedFlags&2)); + + // Main loop + main: for(;;) { + inst=pcgetb(); + if(inst<128) { + // 2OP + if(inst&64) op0=pcfetch(); else op0=pcgetb(); + if(inst&32) op1=pcfetch(); else op1=pcgetb(); + inst&=31; + opc=2; + } else if(inst<176) { + // 1OP + x=(inst>>4)&3; + inst&=143; + if(x==0) op0=pcget(); + else if(x==1) op0=pcgetb(); + else if(x==2) op0=pcfetch(); + } else if(inst>=192) { + // EXT + x=pcgetb(); + op0=opfetch(x>>6,1); + op1=opfetch(x>>4,2); + op2=opfetch(x>>2,3); + op3=opfetch(x>>0,4); + if(inst<224) inst&=31; + } + switch(inst) { + case 1: // EQUAL? + predicate(op0==op1 || (opc>2 && op0==op2) || (opc==4 && op0==op3)); + break; + case 2: // LESS? + predicate(op0op1); + break; + case 4: // DLESS? + xstore(op0,x=xfetch(op0)-1); + predicate(xop1); + break; + case 6: // IN? + predicate(mem[objects+op0*9+4]==op1); + break; + case 7: // BTST + predicate((op0&op1)==op1); + break; + case 8: // BOR + store(op0|op1); + break; + case 9: // BAND + store(op0&op1); + break; + case 10: // FSET? + flagset(); + predicate(opc&op3); + break; + case 11: // FSET + flagset(); + this.put(op2,opc|op3); + break; + case 12: // FCLEAR + flagset(); + this.put(op2,opc&~op3); + break; + case 13: // SET + xstore(op0,op1); + break; + case 14: // MOVE + move(op0,op1); + break; + case 15: // GET + store(this.get((op0+op1*2)&65535)); + break; + case 16: // GETB + store(mem[(op0+op1)&65535]); + break; + case 17: // GETP + if(propfind()) store(mem[op3-1]&32?this.get(op3):mem[op3]); + else store(this.get(defprop+2*op1)); + break; + case 18: // GETPT + propfind(); + store(op3); + break; + case 19: // NEXTP + if(op1) { + // Return next property + propfind(); + store(mem[op3+(mem[op3-1]>>5)+1]&31); + } else { + // Return first property + x=this.getu(objects+op0*9+7); + store(mem[x+mem[x]*2+1]&31); + } + break; + case 20: // ADD + store(op0+op1); + break; + case 21: // SUB + store(op0-op1); + break; + case 22: // MUL + store(Math.imul(op0,op1)); + break; + case 23: // DIV + store(Math.trunc(op0/op1)); + break; + case 24: // MOD + store(op0%op1); + break; + case 128: // ZERO? + predicate(!op0); + break; + case 129: // NEXT? + store(x=mem[objects+op0*9+5]); + predicate(x); + break; + case 130: // FIRST? + store(x=mem[objects+op0*9+6]); + predicate(x); + break; + case 131: // LOC + store(mem[objects+op0*9+4]); + break; + case 132: // PTSIZE + store((mem[(op0-1)&65535]>>5)+1); + break; + case 133: // INC + x=xfetch(op0); + xstore(op0,x+1); + break; + case 134: // DEC + x=xfetch(op0); + xstore(op0,x-1); + break; + case 135: // PRINTB + yield*this.genPrint(this.getText(op0&65535)); + break; + case 137: // REMOVE + move(op0,0); + break; + case 138: // PRINTD + yield*this.genPrint(this.getText(this.getu(objects+op0*9+7)+1)); + break; + case 139: // RETURN + ret(op0); + break; + case 140: // JUMP + pc+=op0-2; + break; + case 141: // PRINT + yield*this.genPrint(this.getText(addr(op0))); + break; + case 142: // VALUE + store(xfetch(op0)); + break; + case 143: // BCOM + store(~op0); + break; + case 176: // RTRUE + ret(1); + break; + case 177: // RFALSE + ret(0); + break; + case 178: // PRINTI + yield*this.genPrint(this.getText(pc)); + pc=this.endText; + break; + case 179: // PRINTR + yield*this.genPrint(this.getText(pc)+"\n"); + ret(1); + break; + case 180: // NOOP + break; + case 181: // SAVE + this.savedFlags=this.get(16); + predicate(yield*this.save(this.serialize(ds,cs,pc))); + break; + case 182: // RESTORE + this.savedFlags=this.get(16); + if(z=yield*this.restore()) z=this.deserialize(z); + this.put(16,this.savedFlags); + if(z) ds=z[0],cs=z[1],pc=z[2]; + predicate(z); + break; + case 183: // RESTART + init(); + yield*this.restarted(); + break; + case 184: // RSTACK + ret(ds[ds.length-1]); + break; + case 185: // FSTACK + ds.pop(); + break; + case 186: // QUIT + return; + case 187: // CRLF + yield*this.genPrint("\n"); + break; + case 188: // USL + if(this.updateStatusLine) yield*this.updateStatusLine(this.getText(this.getu(objects+xfetch(16)*9+7)+1),xfetch(18),xfetch(17)); + break; + case 189: // VERIFY + predicate(this.verify()); + break; + case 224: // CALL + if(op0) { + x=mem[op0=addr(op0)]; + cs.unshift({ds:ds,pc:pc,local:new Int16Array(x)}); + ds=[]; + pc=op0+1; + for(x=0;x1 && mem[op0]>0) cs[0].local[0]=op1; + if(opc>2 && mem[op0]>1) cs[0].local[1]=op2; + if(opc>3 && mem[op0]>2) cs[0].local[2]=op3; + } else { + store(0); + } + break; + case 225: // PUT + this.put((op0+op1*2)&65535,op2); + break; + case 226: // PUTB + mem[(op0+op1)&65535]=op2; + break; + case 227: // PUTP + propfind(); + if(mem[op3-1]&32) this.put(op3,op2); + else mem[op3]=op2; + break; + case 228: // READ + yield*this.genPrint(""); + if(this.updateStatusLine) yield*this.updateStatusLine(this.getText(this.getu(objects+xfetch(16)*9+7)+1),xfetch(18),xfetch(17)); + this.handleInput(yield*this.read(mem[op0&65535]),op0&65535,op1&65535); + break; + case 229: // PRINTC + yield*this.genPrint(op0==13?"\n":op0?String.fromCharCode(op0):""); + break; + case 230: // PRINTN + yield*this.genPrint(String(op0)); + break; + case 231: // RANDOM + if (op0 <= 0) { // If 'op0' is non-positive, reseed the PRNG. + if (op0 === 0) { + initRng(); // If 0, seed using Math.random(). + } else { + this.seed = (op0 >>> 0); // If negative, seed with the specified value. + } + store(0); // Reseeding always returns 0. + break; + } + this.seed = (1664525 * this.seed + 1013904223) >>> 0; // Linear congruential generator + store(Math.floor((this.seed / 0xFFFFFFFF) * op0) + 1); // Return integer in range [1..op0] (inclusive). + break; + case 232: // PUSH + ds.push(op0); + break; + case 233: // POP + xstore(op0,ds.pop()); + break; + case 234: // SPLIT + if(this.split) yield*this.split(op0); + break; + case 235: // SCREEN + if(this.screen) yield*this.screen(op0); + break; + default: + throw new Error("JSZM: Invalid Z-machine opcode"); + } + } + + }, + save: ()=>[], + savedFlags: 0, + selfInsertingBreaks: null, + serial: null, + serialize: function(ds,cs,pc) { + var i,j,e,ar,vi; + e=this.getu(14); // PURBOT + i=e+cs.reduce((p,c)=>p+2*(c.ds.length+c.local.length)+6,0)+2*ds.length+8; + ar=new Uint8Array(i); + ar.set(new Uint8Array(this.mem.buffer,0,e)); + vi=new DataView(ar.buffer); + vi.setUint32(e,pc); + vi.setUint16(e+4,cs.length); + vi.setUint16(e+6,ds.length); + for(i=0;i) => void = () => { }; + terminalLine: string = ""; + terminal: Terminal; fitAddon: FitAddon; serializeAddon: SerializeAddon; @@ -35,25 +41,61 @@ export class DebugWidget implements FarpatchWidget { onInit(): void { console.log("Initialized Debug Widget"); } + + readLine(): Promise { + return new Promise((resolve, reject) => { }); + } + onFocus(element: HTMLElement): void { console.log("Displaying Debug Widget"); if (!this.initialized) { // Ensure the parent frame doesn't get any scrollbars, since we're taking up the whole view element.style.overflow = "hidden"; console.log("Initializing xterm.js"); - // var terminalContainer = document.createElement("div"); - // this.view.appendChild(terminalContainer); this.terminal.loadAddon(this.fitAddon); this.terminal.loadAddon(this.serializeAddon); this.terminal.onKey((e) => { - console.log("Key pressed: " + e.key); this.terminal.write(e.key); + if (e.key === '\h') { + if (this.terminalLine.length > 0) { + this.terminalLine = this.terminalLine.substring(0, this.terminalLine.length - 1); + } + } if (e.key === '\r') { this.terminal.write('\n'); + var zcb = this.zorkCallback; + var tl = this.terminalLine; + this.zorkCallback = () => { }; + this.terminalLine = ""; + zcb(tl); + } + else { + this.terminalLine += e.key; } }); this.terminal.open(this.view); - this.terminal.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n'); + + var zorkTerminal = this.terminal; + var debugWidget = this; + fetch("/zork1.z3").then((response) => { + response.arrayBuffer().then((buffer) => { + this.zork = co.co(function* () { + var zork = new JSZM(new Uint8Array(buffer)); + zork.print = function* (str: string) { + str = str.replace("\n", "\r\n"); + zorkTerminal.write(str); + }; + zork.read = function* (maxlen: number): Generator { + // console.log("Zork: read " + maxlen); + var val = yield new Promise((resolve, reject) => { + debugWidget.zorkCallback = resolve; + }); + return val; + }; + yield* zork.run(); + }); + }) + }); this.initialized = true; } element.appendChild(this.view); diff --git a/static/zork1.z3 b/static/zork1.z3 new file mode 100644 index 0000000..e447402 Binary files /dev/null and b/static/zork1.z3 differ diff --git a/tsconfig.json b/tsconfig.json index f6797ca..a831e70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "target": "ES5", + "target": "ES2015", "module": "CommonJS", "outDir": "./build", "strict": true