├── example ├── web │ ├── game.ulx │ ├── engineWrapper │ │ ├── ifpress.ulx │ │ ├── tsconfig.json │ │ ├── game.html │ │ └── engineWrapper.ts │ ├── tsconfig.json │ ├── angular2 │ │ ├── engineWrapper │ │ │ └── tsconfig.json │ │ ├── tsconfig.json │ │ ├── app │ │ │ ├── main.ts │ │ │ ├── app.html │ │ │ └── app.ts │ │ ├── package.json │ │ ├── README.md │ │ └── index.html │ └── webworker.html └── node │ ├── package.json │ ├── tsconfig.json │ ├── engineWrapper │ ├── ifpress.ulx │ ├── tsconfig.json │ ├── story.ni │ ├── _engineWrapper.ts │ └── README.md │ ├── package-lock.json │ └── _runGameImage.ts ├── test ├── web │ ├── package.json │ ├── tsconfig.json │ ├── test.html │ ├── tests.ts │ ├── package-lock.json │ └── nodeunit.css ├── node │ ├── package.json │ ├── tsconfig.json │ └── core-tests-on-node.ts └── core │ ├── QuetzalTest.ts │ ├── UlxImageTest.ts │ ├── MemoryAccessTest.ts │ └── EngineTest.ts ├── .gitignore ├── README.md ├── core ├── Quetzal.ts ├── GlkWrapper.ts ├── Opcodec.ts ├── Output.ts ├── EngineWrapper.ts ├── UlxImage.ts ├── MemoryAccess.ts ├── Veneer.ts └── Opcodes.ts ├── b64.ts ├── web ├── WebWorker.md └── WebWorker.ts └── mersenne-twister.ts /example/web/game.ulx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiloplanz/glulx-typescript/HEAD/example/web/game.ulx -------------------------------------------------------------------------------- /test/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "typescript": "^4.8.4" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "out": "tests.js" 4 | }, 5 | "files": [ 6 | "tests.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /test/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "nodeunit": "^0.11.3", 4 | "typescript": "^4.8.4" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/node": "^18.11.9", 4 | "typescript": "^4.8.4" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "out": "runGameImage.js" 4 | }, 5 | "files" : [ "_runGameImage.ts"] 6 | } -------------------------------------------------------------------------------- /example/web/engineWrapper/ifpress.ulx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiloplanz/glulx-typescript/HEAD/example/web/engineWrapper/ifpress.ulx -------------------------------------------------------------------------------- /example/node/engineWrapper/ifpress.ulx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiloplanz/glulx-typescript/HEAD/example/node/engineWrapper/ifpress.ulx -------------------------------------------------------------------------------- /test/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "out": "tests.js" 4 | }, 5 | "files": [ 6 | "core-tests-on-node.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "out": "webworker.js" 4 | }, 5 | "files": [ 6 | "../../web/WebWorker.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/node/engineWrapper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "out": "engineWrapper.js" 4 | }, 5 | "files" : [ "_engineWrapper.ts"] 6 | } -------------------------------------------------------------------------------- /example/web/engineWrapper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "out": "engineWrapper.js" 4 | }, 5 | "files": [ 6 | "engineWrapper.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/web/angular2/engineWrapper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "out": "engineWrapper.js", 4 | "declaration": true, 5 | "target": "es6", 6 | }, 7 | "files": [ 8 | "../../../../core/EngineWrapper.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /example/web/angular2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "system", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "engineWrapper" 15 | ] 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node-v0.12.4-darwin-x64 2 | node_modules 3 | typings 4 | test.js 5 | tests.js 6 | runGameImage.js 7 | webworker.js 8 | app.js 9 | webworker.d.ts 10 | engineWrapper.js 11 | *.js.map 12 | engineWrapper.d.ts 13 | main.js 14 | b64.js 15 | mersenne-twister.js 16 | core/*.js 17 | .idea/encodings.xml 18 | .idea/glulx-typescript.iml 19 | .idea/misc.xml 20 | .idea/modules.xml 21 | .idea/typescript-compiler.xml 22 | .idea/vcs.xml 23 | .idea/workspace.xml 24 | -------------------------------------------------------------------------------- /example/web/angular2/app/main.ts: -------------------------------------------------------------------------------- 1 | // Written in 2016 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | 7 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 8 | import { AppComponent } from './app' 9 | 10 | 11 | platformBrowserDynamic().bootstrapModule(AppComponent); 12 | -------------------------------------------------------------------------------- /test/web/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | glulx-typescript browser-hosted test suite 5 | 6 | 7 | 8 | 9 | 10 |

glulx-typescript browser-hosted test suite

11 | 18 | 19 | -------------------------------------------------------------------------------- /example/web/angular2/app/app.html: -------------------------------------------------------------------------------- 1 |
2 | Select a game image (ULX file) from your computer 3 | 4 | 7 |
8 |
11 | 12 |

{{getChannel('LOCN')}}, {{time}}

13 | 14 | Score: {{getChannel('SCOR')}} 15 | 16 |
17 | 18 | {{getChannel('MAIN')}} 19 | 20 |
{{prompt}}
21 | 22 |
23 | 24 |
26 | - {{getChannel('TURN')}} - 27 |
28 | -------------------------------------------------------------------------------- /test/node/core-tests-on-node.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | 12 | let buffer = new FyreVM.MemoryAccess(1000, 10240); 13 | 14 | declare var exports: any 15 | 16 | FyreVM.NodeUnit.addMemoryTests(exports, buffer); 17 | 18 | FyreVM.NodeUnit.addImageTests(exports, buffer); 19 | 20 | FyreVM.NodeUnit.addEngineTests(exports, buffer); 21 | 22 | FyreVM.NodeUnit.addOpcodeTests(exports, buffer); 23 | 24 | FyreVM.NodeUnit.addQuetzalTests(exports); 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/web/tests.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | 13 | function addTests(tests){ 14 | 15 | let buffer = new FyreVM.MemoryAccess(1000, 10240); 16 | 17 | FyreVM.NodeUnit.addMemoryTests(tests, buffer); 18 | 19 | FyreVM.NodeUnit.addImageTests(tests, buffer); 20 | 21 | FyreVM.NodeUnit.addEngineTests(tests, buffer); 22 | 23 | FyreVM.NodeUnit.addOpcodeTests(tests, buffer); 24 | 25 | FyreVM.NodeUnit.addQuetzalTests(tests); 26 | 27 | } -------------------------------------------------------------------------------- /test/web/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "typescript": "^4.8.4" 9 | } 10 | }, 11 | "node_modules/typescript": { 12 | "version": "4.8.4", 13 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 14 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 15 | "dev": true, 16 | "bin": { 17 | "tsc": "bin/tsc", 18 | "tsserver": "bin/tsserver" 19 | }, 20 | "engines": { 21 | "node": ">=4.2.0" 22 | } 23 | } 24 | }, 25 | "dependencies": { 26 | "typescript": { 27 | "version": "4.8.4", 28 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 29 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 30 | "dev": true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/web/angular2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glulx-typescript-angular2-demo", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "npx tsc -p engineWrapper --out engineWrapper/engineWrapper.js && npx tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ", 6 | "tsc": "npx tsc -p engineWrapper --out engineWrapper/engineWrapper.js && npx tsc", 7 | "tsc:w": "npx tsc -w", 8 | "lite": "lite-server" 9 | }, 10 | "license": " CC0-1.0", 11 | "dependencies": { 12 | "@angular/common": "14.2.9", 13 | "@angular/compiler": "14.2.9", 14 | "@angular/core": "14.2.9", 15 | "@angular/platform-browser": "14.2.9", 16 | "@angular/platform-browser-dynamic": "14.2.9", 17 | "es6-shim": "^0.35.6", 18 | "reflect-metadata": "0.1.2", 19 | "rxjs": "6.6.7", 20 | "rxjs-compat": "^6.6.7", 21 | "systemjs": "0.19.26", 22 | "zone.js": "0.11.4" 23 | }, 24 | "devDependencies": { 25 | "concurrently": "^2.0.0", 26 | "lite-server": "^2.2.0", 27 | "typescript": "^4.8.4", 28 | "@types/core-js": "^2.5.4" 29 | } 30 | } -------------------------------------------------------------------------------- /example/web/angular2/README.md: -------------------------------------------------------------------------------- 1 | ## Angular2 Demo Application 2 | 3 | This is a demonstration of how to present an interactive fiction game in a web browser. 4 | 5 | The UI is written using the Angular2 framework, the game engine is embedded into it using the Engine Wrapper interface. 6 | 7 | ### Setting up the build environment 8 | 9 | The example includes a `package.json`, so `npm` should be able 10 | to download everything you need. 11 | 12 | $ cd examples/web/angular2 13 | $ npm install 14 | 15 | ### Compile the game engine and the application 16 | 17 | Then you can compile the game engine and the Angular2 application. 18 | 19 | $ npm run tsc 20 | 21 | Under the hood there are two compilation steps, first for the 22 | Engine Wrapper, then for the Angular2 application, look at `package.json` for details. 23 | 24 | 25 | ### Go play! 26 | 27 | You need to load it through a web server (even though all files are local, no Internet connection required). The easiest way to do that is to run it in developer mode 28 | 29 | $ npm start 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/core/QuetzalTest.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | 8 | module FyreVM { 9 | 10 | export module NodeUnit { 11 | 12 | function bytes(x: string) { 13 | let a = new Uint8Array(x.length); 14 | for (let i = 0; i < x.length; i++) { 15 | a[i] = x.charCodeAt(i); 16 | } 17 | return a; 18 | } 19 | 20 | function chars(x: Uint8Array): string { 21 | let l = x.byteLength; 22 | let s = ''; 23 | for (let i = 0; i < l; i++) { 24 | s += String.fromCharCode(x[i]); 25 | } 26 | return s; 27 | } 28 | 29 | 30 | function testRoundtrip(test) { 31 | let q = new Quetzal(); 32 | q.setChunk('abcd', bytes('some text')); 33 | let x = q.serialize(); 34 | q = Quetzal.load(x); 35 | test.equal(chars(q.getChunk('abcd')), 'some text'); 36 | test.done(); 37 | } 38 | 39 | export function addQuetzalTests(tests) { 40 | tests.Quetzal = { 41 | testRoundtrip: testRoundtrip 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /example/web/engineWrapper/game.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 25 | 26 | 27 | 28 |
Loading ...
29 |

30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 | 38 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /test/web/nodeunit.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Styles taken from qunit.css 3 | */ 4 | 5 | h1#nodeunit-header, h1.nodeunit-header { 6 | padding: 15px; 7 | font-size: large; 8 | background-color: #06b; 9 | color: white; 10 | font-family: 'trebuchet ms', verdana, arial; 11 | margin: 0; 12 | } 13 | 14 | h1#nodeunit-header a { 15 | color: white; 16 | } 17 | 18 | h2#nodeunit-banner { 19 | height: 2em; 20 | border-bottom: 1px solid white; 21 | background-color: #eee; 22 | margin: 0; 23 | font-family: 'trebuchet ms', verdana, arial; 24 | } 25 | h2#nodeunit-banner.pass { 26 | background-color: green; 27 | } 28 | h2#nodeunit-banner.fail { 29 | background-color: red; 30 | } 31 | 32 | h2#nodeunit-userAgent, h2.nodeunit-userAgent { 33 | padding: 10px; 34 | background-color: #eee; 35 | color: black; 36 | margin: 0; 37 | font-size: small; 38 | font-weight: normal; 39 | font-family: 'trebuchet ms', verdana, arial; 40 | font-size: 10pt; 41 | } 42 | 43 | div#nodeunit-testrunner-toolbar { 44 | background: #eee; 45 | border-top: 1px solid black; 46 | padding: 10px; 47 | font-family: 'trebuchet ms', verdana, arial; 48 | margin: 0; 49 | font-size: 10pt; 50 | } 51 | 52 | ol#nodeunit-tests { 53 | font-family: 'trebuchet ms', verdana, arial; 54 | font-size: 10pt; 55 | } 56 | ol#nodeunit-tests li strong { 57 | cursor:pointer; 58 | } 59 | ol#nodeunit-tests .pass { 60 | color: green; 61 | } 62 | ol#nodeunit-tests .fail { 63 | color: red; 64 | } 65 | 66 | p#nodeunit-testresult { 67 | margin-left: 1em; 68 | font-size: 10pt; 69 | font-family: 'trebuchet ms', verdana, arial; 70 | } 71 | -------------------------------------------------------------------------------- /example/node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "@types/node": "^18.11.9", 9 | "typescript": "^4.8.4" 10 | } 11 | }, 12 | "node_modules/@types/node": { 13 | "version": "18.11.9", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", 15 | "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", 16 | "dev": true 17 | }, 18 | "node_modules/typescript": { 19 | "version": "4.8.4", 20 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 21 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 22 | "dev": true, 23 | "bin": { 24 | "tsc": "bin/tsc", 25 | "tsserver": "bin/tsserver" 26 | }, 27 | "engines": { 28 | "node": ">=4.2.0" 29 | } 30 | } 31 | }, 32 | "dependencies": { 33 | "@types/node": { 34 | "version": "18.11.9", 35 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", 36 | "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", 37 | "dev": true 38 | }, 39 | "typescript": { 40 | "version": "4.8.4", 41 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 42 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 43 | "dev": true 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/core/UlxImageTest.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | /// 8 | 9 | module FyreVM { 10 | 11 | export module NodeUnit { 12 | 13 | function testImage(test) { 14 | let m: MemoryAccess = this; 15 | 16 | try { 17 | m.writeASCII(0, 'nope'); 18 | let image = new UlxImage(m); 19 | } 20 | catch (e) { 21 | test.equal(e.message, '.ulx file has wrong magic number nope'); 22 | } 23 | 24 | 25 | UlxImage.writeHeader({ 26 | endMem: 10 * 1024, 27 | ramStart: 50, 28 | version: 0x00030100 29 | }, m); 30 | let image = new UlxImage(m); 31 | 32 | test.equals(image.getMajorVersion(), 3, "major version"); 33 | test.equals(image.getMinorVersion(), 1, "minor version"); 34 | 35 | test.done(); 36 | 37 | }; 38 | 39 | function testSaveToQuetzal(test) { 40 | let m: MemoryAccess = this; 41 | UlxImage.writeHeader({ 42 | endMem: 10 * 1024, 43 | ramStart: 50, 44 | version: 0x00030100 45 | }, m); 46 | m.writeASCII(50, 'Hello World'); 47 | let image = new UlxImage(m); 48 | 49 | let q = image.saveToQuetzal(); 50 | let umem = new MemoryAccess(0); 51 | umem.buffer = new Uint8Array(q.getChunk('UMem')); 52 | 53 | test.equal(umem.readASCII(4, 11), 'Hello World'); 54 | test.equal(umem.readInt32(0), 10 * 1024 - 50); 55 | test.equal(q.getChunk('IFhd').byteLength, 128, 'IFhd'); 56 | test.done(); 57 | } 58 | 59 | 60 | export function addImageTests(tests, m: MemoryAccess) { 61 | tests.UlxImage = { 62 | testImage: testImage.bind(m), 63 | testSaveToQuetzal: testSaveToQuetzal.bind(m) 64 | } 65 | } 66 | 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /example/node/engineWrapper/story.ni: -------------------------------------------------------------------------------- 1 | "Stop the IFPress!" by David Cornelson 2 | 3 | Include FyreVM Core by David Cornelson. 4 | Include FyreVM Banner Output by David Cornelson. 5 | Include FyreVM Prologue by David Cornelson. 6 | 7 | The story headline is "An IF Press Demonstration". 8 | The story genre is "Zorkian". 9 | The story creation year is 2016. 10 | 11 | A trigger is a kind of value. The triggers are fire and stop. 12 | 13 | A room has a trigger called the event. The event of The Offices is stop. 14 | 15 | When play begins while outputting channels (this is the prologue channel rule): 16 | select the prologue channel; 17 | say "During the Great Underground Industrial Revolution, immediately following the disappearance of magic, the IF Press Gazette was created to make up for the lost tradition of the New Zork Times. Even without magic, some things seemed to get done in magical ways..."; 18 | select the main channel. 19 | 20 | The Press Room is a room. "Amidst the cavernous warehouse are large printing presses, large rolls of paper, and barrels of ink. Men and women scurry about, putting tomorrow's edition of the IF Press Gazette together. The loading dock is to the north while the offices are to the south." 21 | 22 | The Dock is north of The Press Room. "Trucks flow in and out of the docking area, picking up stacks of bound copies of the IF Press Gazette." 23 | 24 | The Offices are south of The Press Room. "Reporters and other personnel sit ensconced in small metal desks, tapping away on typewriters recently converted to manual interaction (they once acted via magical spells)." 25 | 26 | Every turn: 27 | if the location of the player is The Dock and the event of The Offices is stop: 28 | now the event of The Offices is fire; 29 | say "What do you wish to name your World?"; 30 | now the command prompt is ">>". 31 | 32 | After reading a command when event of The Offices is fire: 33 | now the event of The Offices is stop; 34 | Let R be the player's command in title case; 35 | say "[r]"; 36 | now the command prompt is ">"; 37 | reject the player's command. 38 | -------------------------------------------------------------------------------- /example/node/_runGameImage.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | 7 | /** 8 | * Simple command-line test driver using Node.js and readline. 9 | * 10 | * $ cd example/node 11 | * $ tsc 12 | * $ node runGameImage.js someGameImage.ulx 13 | * 14 | */ 15 | 16 | /// 17 | 18 | let readline = require('readline'); 19 | 20 | let rl = readline.createInterface({ 21 | input: process.stdin, 22 | output: process.stdout 23 | }); 24 | 25 | 26 | let fs = require('fs'); 27 | 28 | let buffer = fs.readFileSync(process.argv[2]); 29 | let testGame = new FyreVM.MemoryAccess(0); 30 | testGame.buffer = new Uint8Array(buffer); 31 | testGame['maxSize'] = testGame.buffer.byteLength * 2; 32 | 33 | let engine = new FyreVM.Engine(new FyreVM.UlxImage(testGame)); 34 | 35 | // enable FyreVM extensions 36 | // engine.enableFyreVM = false; 37 | 38 | // enable Glk emulation 39 | engine.glkMode = 1; 40 | 41 | function glk_window_clear() { 42 | readline.cursorTo(process.stdout, 0, 0); 43 | readline.clearScreenDown(process.stdout); 44 | } 45 | 46 | let prompt_line = ""; 47 | let room = ""; 48 | 49 | engine.lineWanted = function (callback) { 50 | let millis = Date.now() - engine['startTime'] 51 | console.info(`[cycles: ${engine['cycle']} millis: ${millis}]`) 52 | rl.question(`\u001b[1;36m${room} ${prompt_line}\u001b[0m`, callback); 53 | } 54 | 55 | engine.keyWanted = engine.lineWanted; 56 | engine.saveRequested = function (quetzal: FyreVM.Quetzal, callback) { 57 | fs.writeFileSync(process.argv[2] + ".fyrevm_saved_game", Buffer.from(new Uint8Array(quetzal.serialize()))); 58 | callback(true); 59 | } 60 | engine.loadRequested = function (callback) { 61 | let x = fs.readFileSync(process.argv[2] + ".fyrevm_saved_game"); 62 | if (x) { 63 | let q = FyreVM.Quetzal.load(new Uint8Array(x)); 64 | callback(q); 65 | } else { 66 | console.error("could not find the save game file"); 67 | callback(null); 68 | } 69 | } 70 | engine.outputReady = function (x) { 71 | if (engine['glkHandlers']) { 72 | engine['glkHandlers'][0x2A] = glk_window_clear; 73 | } 74 | if (x.MAIN !== undefined) 75 | process.stdout.write(x.MAIN); 76 | prompt_line = x.PRPT || prompt_line; 77 | room = x.LOCN || room; 78 | } 79 | 80 | engine.run(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Glulx VM in TypeScript with Channel IO (work in progess) 2 | 3 | [Glulx](http://en.wikipedia.org/wiki/Glulx) is a specification for a 32-bit virtual machine that runs Inform 6 and [Inform 7 story files](http://inform7.com). 4 | 5 | This project is an attempt to implement Glulx in TypeScript. 6 | 7 | It is based heavily on the [FyreVM](https://github.com/ChicagoDave/FyreVM) (a C# implementation). In particular, it also makes use of the contextual Channel IO layer introduced in FyreVM. 8 | 9 | ### Playing a game image 10 | 11 | If you have a Glulx game image (a .ulx file), you can try if it works... this is all still very much under construction, games that target FyreVM work best, Inform6-compiled Glulx games seem to work okay, Inform7 not so much. 12 | 13 | #### in your terminal 14 | 15 | You can compile a simple Node.js and readline based command line tool. 16 | 17 | $ cd examples/node 18 | $ tsc 19 | $ node runGameImage.js yourGameImageFile.ulx 20 | 21 | Note that no command line arguments are required for `tsc`. All compiler configuration is contained in [tsconfig.json](tsconfig.json). If you are actively editing the files, you may want to add a `-w` ("watch") flag to the command, though, to have it recompile when the files are updated. 22 | 23 | #### in your browser (simple) 24 | 25 | There is a simple HTML page that can load and run a game image. You need to load it through a web server (even though all files are local, no Internet connection required). The easiest way to do that is to `npm install -g http-server` and use that to serve the page: 26 | 27 | $ cd examples/web 28 | $ tsc 29 | $ http-server 30 | $ open http://127.0.0.1:8080/webworker.html 31 | 32 | Select a game image (ULX file) from your local file system to press START. 33 | 34 | #### in your browser (using Angular2) 35 | 36 | We also have [some example code](example/web/angular2/README.md) that shows how to integrate the game engine in an Angular2 application. 37 | 38 | 39 | 40 | ### Running unit tests 41 | 42 | 43 | There are some unit tests for core engine functionality that you can run on Node.js or in a browser. 44 | 45 | #### using nodeunit 46 | 47 | You need Node.js and nodeunit installed (as well as a TypeScript 1.8 compiler). 48 | 49 | Then you can compile everything in this project and run the test suite: 50 | 51 | $ cd test/node 52 | $ tsc 53 | $ nodeunit tests.js 54 | 55 | 56 | #### in the browser 57 | 58 | 59 | You can also run the same unit tests in your browser instead of on Node.js: 60 | 61 | $ cd test/web 62 | $ tsc 63 | $ open test.html 64 | -------------------------------------------------------------------------------- /test/core/MemoryAccessTest.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | 8 | 9 | 10 | module FyreVM { 11 | 12 | export module NodeUnit { 13 | 14 | function testReadWriteInt16(test) { 15 | this.writeInt16(0, 0xffff); 16 | this.writeInt16(2, 0xaaaa); 17 | test.equals(this.readInt16(0), 0xffff, "read back"); 18 | test.equals(this.readInt16(1), 0xffaa, "read back shifted by one"); 19 | test.done(); 20 | }; 21 | 22 | function testReadWriteInt32(test) { 23 | this.writeInt32(0, 0xffffdddd); 24 | this.writeInt32(4, 0xaaaabbbb); 25 | test.equals(this.readInt32(0), 0xffffdddd, "read back"); 26 | test.equals(this.readInt32(1), 0xffddddaa, "read back shifted by one"); 27 | test.done(); 28 | }; 29 | 30 | 31 | function testAlloc(test) { 32 | let allocator = new HeapAllocator(0, this); 33 | allocator.maxHeapExtent = 1000; 34 | test.equals(allocator.blockCount(), 0, "initially no blocks"); 35 | test.equals(allocator.alloc(100), 0, "could alloc 100 bytes"); 36 | test.equals(allocator.blockCount(), 1, "allocated the first block"); 37 | test.equals(allocator.alloc(950), null, "could not alloc another 950 bytes"); 38 | test.equals(allocator.blockCount(), 1, "no new block after failed allocation"); 39 | test.equals(allocator.alloc(100), 100, "could alloc 100 bytes"); 40 | test.equals(allocator.blockCount(), 2, "allocated the second block"); 41 | 42 | test.done(); 43 | } 44 | 45 | function testFree(test) { 46 | let allocator = new HeapAllocator(0, this); 47 | allocator.maxHeapExtent = 1000; 48 | 49 | let a = allocator.alloc(500); 50 | let b = allocator.alloc(500); 51 | test.equals(allocator.blockCount(), 2); 52 | 53 | allocator.free(a); 54 | test.equals(allocator.blockCount(), 1); 55 | 56 | let c = allocator.alloc(200); 57 | let d = allocator.alloc(300); 58 | test.equals(allocator.blockCount(), 3); 59 | 60 | allocator.free(b); 61 | test.equals(allocator.blockCount(), 2); 62 | 63 | allocator.free(c); 64 | test.equals(allocator.blockCount(), 1); 65 | 66 | allocator.free(d); 67 | test.equals(allocator.blockCount(), 0); 68 | 69 | test.done(); 70 | } 71 | 72 | 73 | export function addMemoryTests(tests, m: MemoryAccess) { 74 | tests.MemoryAccess = { 75 | testReadWriteInt16: testReadWriteInt16.bind(m), 76 | testReadWriteInt32: testReadWriteInt32.bind(m), 77 | testAlloc: testAlloc.bind(m), 78 | testFree: testFree.bind(m) 79 | }; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /example/web/angular2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | Interactive Fiction Angular2 Demo 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 45 | 46 | 47 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Loading... 89 | 90 | -------------------------------------------------------------------------------- /core/Quetzal.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | /// 8 | 9 | 10 | module FyreVM { 11 | 12 | 13 | 14 | /// Implements the Quetzal saved-game file specification by holding a list of 15 | /// typed data chunks which can be read from or written to streams. 16 | /// http://www.ifarchive.org/if-archive/infocom/interpreters/specification/savefile_14.txt 17 | export class Quetzal { 18 | 19 | private chunks : { [ name : string ] : Uint8Array } = {}; 20 | 21 | setChunk(name: string, value: Uint8Array){ 22 | if (name.length != 4){ 23 | throw new Error(`invalid chunk id ${name}, must be four ASCII chars`); 24 | } 25 | this.chunks[name] = value; 26 | } 27 | 28 | getChunk(name: string): Uint8Array { 29 | return this.chunks[name]; 30 | } 31 | 32 | getIFhdChunk(): Uint8Array { 33 | return this.getChunk('IFhd') 34 | } 35 | 36 | 37 | serialize() : Uint8Array{ 38 | // determine the buffer size 39 | let size = 12; // three int32 headers 40 | let { chunks } = this; 41 | for (let name in chunks) { 42 | size += 4; // the key 43 | size += 4; // the value length 44 | size += chunks[name].byteLength; 45 | } 46 | let fileLength = size - 8; 47 | if (size % 2){ 48 | size ++; // padding 49 | } 50 | 51 | let m = new MemoryAccess(size); 52 | m.writeByte(size-1, 0); 53 | m.writeASCII(0, 'FORM'); // IFF tag 54 | m.writeInt32(4, fileLength); 55 | m.writeASCII(8, 'IFZS'); // FORM sub-ID for Quetzal 56 | let pos = 12; 57 | for (let name in chunks) { 58 | m.writeASCII(pos, name); 59 | let value = chunks[name]; 60 | let len = value.byteLength; 61 | m.writeInt32(pos+4, len); 62 | m.buffer.set(new Uint8Array(value), pos+8); 63 | pos += 8 + len; 64 | } 65 | 66 | return m.buffer; 67 | } 68 | 69 | static load(buffer: Uint8Array): Quetzal { 70 | let q = new Quetzal(); 71 | let m = new MemoryAccess(0); 72 | m.buffer = buffer; 73 | let type = m.readASCII(0, 4); 74 | if (type !== 'FORM' && type !== 'LIST' && type !== 'CAT_'){ 75 | throw new Error(`invalid IFF type ${type}`); 76 | } 77 | let length = m.readInt32(4); 78 | if (buffer.byteLength < 8 + length){ 79 | throw new Error("Quetzal file is too short for ${length} bytes"); 80 | } 81 | type = m.readASCII(8, 4); 82 | if (type !== 'IFZS'){ 83 | throw new Error(`invalid IFF sub-type ${type}. Not a Quetzal file`); 84 | } 85 | let pos = 12; 86 | let limit = 8 + length; 87 | while (pos < limit){ 88 | let name = m.readASCII(pos, 4); 89 | length = m.readInt32(pos+4); 90 | let value = m.buffer.subarray(pos+8, pos+8+length); 91 | q.setChunk(name, value); 92 | pos += 8 + length; 93 | } 94 | return q; 95 | } 96 | 97 | /** 98 | * convenience method to encode a Quetzal file as Base64 99 | */ 100 | base64Encode(){ 101 | return Base64.fromByteArray(new Uint8Array(this.serialize())) 102 | } 103 | 104 | /** 105 | * convenience method to decode a Quetzal file from Base64 106 | */ 107 | static base64Decode(base64: string) : Quetzal { 108 | return Quetzal.load(Base64.toByteArray(base64)) 109 | } 110 | 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /b64.ts: -------------------------------------------------------------------------------- 1 | // slightly adapted from https://github.com/beatgammit/base64-js 2 | 3 | module Base64 { 4 | 5 | var lookup = [] 6 | var revLookup = [] 7 | 8 | function init() { 9 | var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 10 | for (var i = 0, len = code.length; i < len; ++i) { 11 | lookup[i] = code[i] 12 | revLookup[code.charCodeAt(i)] = i 13 | } 14 | 15 | revLookup['-'.charCodeAt(0)] = 62 16 | revLookup['_'.charCodeAt(0)] = 63 17 | } 18 | 19 | init() 20 | 21 | export function toByteArray(b64): Uint8Array { 22 | var i, j, l, tmp, placeHolders, arr 23 | var len = b64.length 24 | 25 | if (len % 4 > 0) { 26 | throw new Error('Invalid string. Length must be a multiple of 4') 27 | } 28 | 29 | // the number of equal signs (place holders) 30 | // if there are two placeholders, than the two characters before it 31 | // represent one byte 32 | // if there is only one, then the three characters before it represent 2 bytes 33 | // this is just a cheap hack to not do indexOf twice 34 | placeHolders = b64[len - 2] === '=' ? 2 : b64[len - 1] === '=' ? 1 : 0 35 | 36 | // base64 is 4/3 + up to two characters of the original data 37 | arr = new Uint8Array(len * 3 / 4 - placeHolders) 38 | 39 | // if there are placeholders, only get up to the last complete 4 chars 40 | l = placeHolders > 0 ? len - 4 : len 41 | 42 | var L = 0 43 | 44 | for (i = 0, j = 0; i < l; i += 4, j += 3) { 45 | tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)] 46 | arr[L++] = (tmp >> 16) & 0xFF 47 | arr[L++] = (tmp >> 8) & 0xFF 48 | arr[L++] = tmp & 0xFF 49 | } 50 | 51 | if (placeHolders === 2) { 52 | tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4) 53 | arr[L++] = tmp & 0xFF 54 | } else if (placeHolders === 1) { 55 | tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2) 56 | arr[L++] = (tmp >> 8) & 0xFF 57 | arr[L++] = tmp & 0xFF 58 | } 59 | 60 | return arr 61 | } 62 | 63 | function tripletToBase64(num) { 64 | return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F] 65 | } 66 | 67 | function encodeChunk(uint8, start, end) { 68 | var tmp 69 | var output = [] 70 | for (var i = start; i < end; i += 3) { 71 | tmp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]) 72 | output.push(tripletToBase64(tmp)) 73 | } 74 | return output.join('') 75 | } 76 | 77 | export function fromByteArray(uint8) { 78 | var tmp 79 | var len = uint8.length 80 | var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes 81 | var output = '' 82 | var parts = [] 83 | var maxChunkLength = 16383 // must be multiple of 3 84 | 85 | // go through the array every three bytes, we'll deal with trailing stuff later 86 | for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { 87 | parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) 88 | } 89 | 90 | // pad the end with zeros, but make sure to not forget the extra bytes 91 | if (extraBytes === 1) { 92 | tmp = uint8[len - 1] 93 | output += lookup[tmp >> 2] 94 | output += lookup[(tmp << 4) & 0x3F] 95 | output += '==' 96 | } else if (extraBytes === 2) { 97 | tmp = (uint8[len - 2] << 8) + (uint8[len - 1]) 98 | output += lookup[tmp >> 10] 99 | output += lookup[(tmp >> 4) & 0x3F] 100 | output += lookup[(tmp << 2) & 0x3F] 101 | output += '=' 102 | } 103 | 104 | parts.push(output) 105 | 106 | return parts.join('') 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /web/WebWorker.md: -------------------------------------------------------------------------------- 1 | ## Using a Web Worker to run the game engine 2 | 3 | The game engine is not the fastest thing in the world. 4 | 5 | You do not want to run it on the main UI thread of your web browser. 6 | 7 | Our solution is to wrap it into Web Worker and run it on a background thread. Communication between the UI and the engine is then done via asynchronous message passing. 8 | 9 | As a side-effect of this arrangement, the engine is pretty easy to embed into a web application (the Web Worker is a piece of self-contained Javascript). 10 | 11 | ------ 12 | 13 | ### Usage 14 | 15 | 35 | 36 | 37 | Also see the [example code](../example/web/webworker.html). 38 | 39 | ------ 40 | 41 | ### Message Format 42 | 43 | The Web Worker that runs the game engine communicates with the outside world using messages that you send to it with `w.postMessage(payload)` and receive from it using the `w.onmessage` callback. 44 | 45 | #### messages from the engine 46 | 47 | When the engine wants to tell you (or needs to do) something, 48 | it will send you a message, which mostly conforms to the [EngineWrapperState interface](../core/EngineWrapper.ts). 49 | 50 | w.onmessage = function(ev : MessageEvent){ 51 | let d : EngineWrapperState = ev.data; 52 | let state: EngineState = d.state; 53 | 54 | } 55 | 56 | ###### EngineState 57 | 58 | It contains an `EngineState` that you need to inspect 59 | to figure out what kind of message this is, and what you need to do about it. 60 | 61 | export const enum EngineState { 62 | loaded = 1, 63 | running = 2, 64 | completed = 100, 65 | error = -100, 66 | 67 | waitingForLineInput = 51, 68 | waitingForKeyInput = 52, 69 | waitingForGameSavedConfirmation = 53, 70 | waitingForLoadSaveGame = 54 71 | } 72 | 73 | ###### engine output 74 | 75 | The game output is sent via Channels. You should display them to the user as appropriate. 76 | 77 | console.info(d.channelData.MAIN) 78 | 79 | 80 | ###### user input 81 | 82 | When the game needs input from the user (such as the next command), it sends a message with the state `waitingForLineInput` or `waitingForKeyInput`. When you have that input, you need to send it back via a `lineInput` or `keyInput` command. 83 | 84 | 85 | 86 | #### commands to the engine 87 | 88 | export interface WebWorkerCommand { 89 | // actions 90 | loadImage? : ArrayBuffer|string, 91 | start?: boolean, 92 | lineInput?: string, 93 | keyInput?: string, 94 | saveSuccessful?: boolean 95 | restore?: ArrayBuffer|string|boolean, 96 | // configuration 97 | enableSaveGame? : boolean, 98 | } 99 | 100 | 101 | ##### loadImage 102 | 103 | `w.postMessage({loadImage: file})`, where `file` can be an `ArrayBuffer` object or a URL (a string). 104 | 105 | ##### start 106 | 107 | `w.postMessage({start: true})` 108 | 109 | ##### lineInput 110 | 111 | `w.postMessage({lineInput: 'look around'})` 112 | 113 | Send this when the engine is asking for a line input from the user. 114 | 115 | ##### keyInput 116 | 117 | Send this when the engine is asking for a keypress. 118 | 119 | ##### enableSaveGame 120 | 121 | By default, saving games is disabled. You can send `{enableSaveGame: true}`, which will give you an opportunity to receive Quetzal files that you can store somewhere (such as in `localStorage`). 122 | 123 | ##### saveSuccessful 124 | 125 | Send `{saveSuccessful: true}` (or `false`} to signal that you have finished storing the save game file. 126 | 127 | ##### restore 128 | 129 | Send `{restore: quetzal}` when the engine asks for a save game to be restored (or `false` when you don't have one). 130 | 131 | `quetzal` here can be either an `ArrayBuffer` or a URL (string). 132 | 133 | 134 | -------------------------------------------------------------------------------- /example/web/engineWrapper/engineWrapper.ts: -------------------------------------------------------------------------------- 1 | // Written in 2016 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /** 7 | * A simple demo of embedding EngineWrapper directly 8 | * (without going through a web worker) in a web browser 9 | */ 10 | 11 | 12 | /// 13 | /// 14 | 15 | module Example { 16 | 17 | // the EngineWrapper, instance created when game image is loaded 18 | let w: FyreVM.EngineWrapper; 19 | 20 | let promptLine: string; 21 | 22 | export function loadGameImage(url: string) { 23 | setText("status", `loading game image ${url}`); 24 | var reader = new XMLHttpRequest(); 25 | reader.open('GET', url); 26 | reader.responseType = 'arraybuffer'; 27 | reader.onreadystatechange = function (ev) { 28 | if (reader.readyState === XMLHttpRequest.DONE) { 29 | w = FyreVM.EngineWrapper.loadFromArrayBuffer(reader.response, true) 30 | process(w.run()) 31 | } 32 | } 33 | reader.send() 34 | } 35 | 36 | 37 | function process(result: FyreVM.EngineWrapperState) { 38 | let c = result.channelData 39 | if (c) { 40 | if (c.MAIN) { 41 | setText('MAIN', c.MAIN); 42 | } 43 | if (c.LOCN) { 44 | setText('LOCN', c.LOCN); 45 | } 46 | if (c.SCOR || +c.SCOR === 0) { 47 | setText('SCOR', "Score: " + c.SCOR); 48 | } 49 | if (c.TURN) { 50 | setText('TURN', "Turn: " + c.TURN); 51 | } 52 | if (c.TIME) { 53 | setText('TIME', Math.floor(+c.TIME / 100) + ":" + (+c.TIME % 100)); 54 | } 55 | if (c.PRPT) { 56 | promptLine = c.PRPT; 57 | } 58 | } 59 | 60 | 61 | switch (result.state) { 62 | case FyreVM.EngineState.waitingForKeyInput: 63 | case FyreVM.EngineState.waitingForLineInput: 64 | setText('status', 'waiting for your input...'); 65 | getInput(); 66 | break; 67 | case FyreVM.EngineState.completed: 68 | setText('status', 'game over'); 69 | break; 70 | case FyreVM.EngineState.waitingForLoadSaveGame: { 71 | let key = `fyrevm_saved_game_${Base64.fromByteArray(w.getIFhd())}` 72 | let q = localStorage[key] 73 | if (q) { 74 | q = FyreVM.Quetzal.base64Decode(q) 75 | } 76 | setTimeout( 77 | () => process(w.receiveSavedGame(q)) 78 | , 0) 79 | break; 80 | } 81 | case FyreVM.EngineState.waitingForGameSavedConfirmation: { 82 | let key = `fyrevm_saved_game_${Base64.fromByteArray(result.gameBeingSaved.getIFhdChunk())}` 83 | let q = result.gameBeingSaved.base64Encode() 84 | localStorage[key] = q 85 | setTimeout( 86 | () => process(w.saveGameDone(true)) 87 | , 0) 88 | break; 89 | } 90 | default: 91 | setText('status', "ERROR: unexpected Engine state " + result.state) 92 | console.error(result); 93 | break; 94 | 95 | } 96 | } 97 | 98 | 99 | 100 | 101 | function setText(id, text) { 102 | document.getElementById(id).textContent = text 103 | } 104 | 105 | 106 | 107 | function getInput() { 108 | var div = document.getElementById('PRPT'); 109 | div.innerHTML = '
' + promptLine + ' '; 110 | var input: any = div.getElementsByTagName('INPUT')[0]; 111 | input.focus(); 112 | input.scrollIntoView(); 113 | } 114 | 115 | 116 | export function sendCommand() { 117 | var div = document.getElementById('PRPT'); 118 | var input: any = div.getElementsByTagName('INPUT')[0]; 119 | var command = input.value; 120 | div.innerHTML = ''; 121 | setText('status', 'processing ...'); 122 | setText('MAIN', '\n\n\n... ' + command); 123 | // kick off the engine wrapped in setTimeout 124 | // a) to return from the submit handler quickly 125 | // b) to limit recursion depth 126 | setTimeout(() => process(w.receiveLine(command)), 0) 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /example/web/angular2/app/app.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 and 2016 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | 8 | import { Component, NgZone } from '@angular/core'; 9 | 10 | @Component({ 11 | selector: 'my-app', 12 | templateUrl: 'app/app.html' 13 | }) 14 | // Component controller 15 | export class AppComponent { 16 | 17 | private engineWrapper: FyreVM.EngineWrapper = null; 18 | 19 | private channelData; 20 | 21 | private engineState: FyreVM.EngineState 22 | 23 | userInput = ""; 24 | 25 | constructor(_ngZone: NgZone) { 26 | // TODO: find out how one is supposed to do this in Angular2 27 | window['handleKey'] = (ev) => _ngZone.run(() => this.handleKey(ev)) 28 | } 29 | 30 | 31 | loadAndStart() { 32 | 33 | let file = document.getElementById('gameImage')['files'][0]; 34 | let reader = new FileReader(); 35 | reader.onload = (ev) => { 36 | this.engineWrapper = FyreVM.EngineWrapper.loadFromFileReaderEvent(ev, true) 37 | this.process(this.engineWrapper.run()); 38 | } 39 | reader.readAsArrayBuffer(file); 40 | } 41 | 42 | process(result: FyreVM.EngineWrapperState) { 43 | if (result.channelData) { 44 | this.channelData = result.channelData; 45 | } 46 | this.engineState = result.state 47 | switch (result.state) { 48 | case FyreVM.EngineState.waitingForGameSavedConfirmation: { 49 | let key = `fyrevm_saved_game_${Base64.fromByteArray(result.gameBeingSaved.getIFhdChunk())}` 50 | let q = result.gameBeingSaved.base64Encode() 51 | localStorage[key] = q 52 | this.process(this.engineWrapper.saveGameDone(true)) 53 | } 54 | break; 55 | case FyreVM.EngineState.waitingForLoadSaveGame: { 56 | let key = `fyrevm_saved_game_${Base64.fromByteArray(this.engineWrapper.getIFhd())}` 57 | let q = localStorage[key] 58 | if (q) { 59 | q = FyreVM.Quetzal.base64Decode(q) 60 | } 61 | this.process(this.engineWrapper.receiveSavedGame(q)) 62 | } 63 | break; 64 | } 65 | } 66 | 67 | getChannel(name: string) { 68 | let c = this.channelData 69 | if (!c) return null 70 | return this.channelData[name] 71 | } 72 | 73 | sendLine(line: string) { 74 | this.process(this.engineWrapper.receiveLine(line)) 75 | } 76 | 77 | sendKey(key: string) { 78 | this.process(this.engineWrapper.receiveKey(key)) 79 | } 80 | 81 | handleKey($event: KeyboardEvent) { 82 | $event.preventDefault(); 83 | $event.stopPropagation(); 84 | if ($event.type === 'keyup') { 85 | let state = this.engineState; 86 | if (state === FyreVM.EngineState.waitingForLineInput) { 87 | let key = getKey($event); 88 | if (key === null) 89 | return; 90 | if (key.length === 1) { 91 | this.userInput += key; 92 | return; 93 | } 94 | switch (key) { 95 | case 'Backspace': 96 | let l = this.userInput.length; 97 | if (l) { 98 | this.userInput = this.userInput.substr(0, l - 1); 99 | } 100 | return; 101 | case 'Enter': 102 | this.sendLine(this.userInput); 103 | this.userInput = ''; 104 | return; 105 | } 106 | } 107 | if (state === FyreVM.EngineState.waitingForKeyInput) { 108 | let key = getKey($event); 109 | if (key === null) 110 | return; 111 | if (key.length === 1) { 112 | this.sendKey(key); 113 | return; 114 | } 115 | if (key === 'Enter') { 116 | this.sendKey('\n'); 117 | return; 118 | } 119 | } 120 | } 121 | } 122 | 123 | 124 | get prompt() { 125 | let state = this.engineState; 126 | if (state === FyreVM.EngineState.waitingForLineInput) 127 | return (this.getChannel('PRPT') || '>') + this.userInput; 128 | if (state === FyreVM.EngineState.waitingForKeyInput) 129 | return '...'; 130 | return ''; 131 | } 132 | 133 | 134 | get time() { 135 | let x = parseInt(this.getChannel('TIME')); 136 | let hours = printf02d(Math.floor(x / 100)); 137 | let mins = printf02d(x % 100); 138 | return `${hours}:${mins}`; 139 | } 140 | 141 | } 142 | 143 | 144 | function getKey($event: KeyboardEvent): string { 145 | // this is the standard way, but not implemented on all browsers 146 | let key = $event.key; 147 | if (key) return key; 148 | // use the deprecated "keyCode" as a fallback 149 | let code = $event.keyCode; 150 | if (code >= 31 && code < 127) { 151 | key = String.fromCharCode(code); 152 | if ($event.shiftKey) return key.toUpperCase(); 153 | return key.toLowerCase(); 154 | } 155 | if (code === 8) return 'Backspace'; 156 | if (code === 13) return 'Enter'; 157 | return null; 158 | } 159 | 160 | 161 | function printf02d(x: number): string { 162 | if (x < 10) 163 | return `0${x}`; 164 | return "" + x; 165 | } 166 | 167 | // http://jsperf.com/tobase64-implementations/10 168 | // http://stackoverflow.com/a/9458996/14955 169 | function base64(data) { 170 | if (!data) return null; 171 | var bytes = new Uint8Array(data); 172 | var len = bytes.byteLength; 173 | var chArray = new Array(len); 174 | for (var i = 0; i < len; i++) { 175 | chArray[i] = String.fromCharCode(bytes[i]); 176 | } 177 | return btoa(chArray.join("")); 178 | } -------------------------------------------------------------------------------- /example/node/engineWrapper/_engineWrapper.ts: -------------------------------------------------------------------------------- 1 | // Written in 2016 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | 7 | /** 8 | * Node.js command line to invoke the "EngineWrapper" to run a single game step 9 | * 10 | * $ cd example/node/engineWrapper 11 | * $ tsc 12 | * $ node engineWrapper.js someGameImage.ulx sessionName "look around" 13 | * 14 | * What this does is: 15 | * 16 | * 1) load the game image 17 | * 2) look for the latest savepoint for this session in the current working directory 18 | * 3) load that savepoint if found 19 | * 4) execute the game command given 20 | * 5) write the channel output to STDOUT 21 | * 6) create a new savepoint for the session 22 | * 23 | * 24 | * The implementation is pretty simplistic. 25 | * 26 | * In particular, there are no "magic hooks" into the game engine. 27 | * All is done using the regular "save" and "restore" commands 28 | * (which are transmitted to the running game just as if a user 29 | * entered them). What this also means is that the (potentially heavy) 30 | * boot process of the game software is executed every time. 31 | * 32 | * 33 | */ 34 | 35 | /// 36 | 37 | let imageFile = process.argv[2]; 38 | 39 | if (imageFile === undefined) { 40 | console.error("The first argument must be a valid FyreVM enabled Glulx story file. (GBlorb's are not supported at this time)"); 41 | process.exit(1) 42 | } 43 | 44 | if (imageFile == "--help") { 45 | console.info("USAGE: chester story.ulx session \"command\" turn"); 46 | } 47 | 48 | let sessionName = process.argv[3]; 49 | 50 | if (sessionName === undefined) { 51 | console.error("The second argument must be the name of the sessions stored ({sessionName}.{turn})"); 52 | process.exit(1) 53 | } 54 | 55 | let command = process.argv[4]; 56 | 57 | let fs = require('fs'); 58 | 59 | let sessionData: FyreVM.Quetzal = null; 60 | let turnData = process.argv[5]; 61 | let turn: number = 0; 62 | 63 | if (!(turnData === undefined)) { 64 | turn = Number(turnData); 65 | } 66 | 67 | let sessionFile = sessionName; 68 | let saveFile = ""; 69 | 70 | // Look for previous session files... 71 | let guessTurn: number = 0; 72 | let checkFile = ""; 73 | do { 74 | guessTurn++; 75 | checkFile = sessionFile + "." + guessTurn; 76 | } while (fs.existsSync(checkFile)); 77 | guessTurn--; 78 | 79 | // if user specified a turn, load that file 80 | let loadFile = false; 81 | if (turnData === undefined) { 82 | if (guessTurn > 0) { 83 | saveFile = sessionFile + "." + (guessTurn + 1); 84 | sessionFile = sessionFile + "." + guessTurn; 85 | loadFile = true; 86 | } else { 87 | saveFile = sessionFile + ".1"; 88 | } 89 | } else { 90 | saveFile = sessionFile + "." + (turn + 1); 91 | sessionFile = sessionFile + "." + turn; 92 | loadFile = true; 93 | } 94 | 95 | // Start a branch off the request turn file... 96 | if (fs.existsSync(saveFile)) { 97 | saveFile = sessionFile + ".1"; 98 | } 99 | 100 | if (loadFile) { 101 | if (fs.existsSync(sessionFile)) { 102 | sessionData = FyreVM.Quetzal.load(new Uint8Array(fs.readFileSync(sessionFile))); 103 | } else { 104 | console.error("Cannot file specified session file: " + sessionFile); 105 | } 106 | } 107 | 108 | if (command === undefined) { 109 | if (sessionData) { 110 | console.error("Please specify a command or start a new session. This one is already in progress."); 111 | process.exit(1); 112 | } 113 | } 114 | 115 | let game = new FyreVM.MemoryAccess(0); 116 | game.buffer = new Uint8Array(fs.readFileSync(imageFile)); 117 | game['maxSize'] = game.buffer.byteLength * 2; 118 | 119 | // load the image 120 | let engine = new FyreVM.EngineWrapper(game, true); 121 | 122 | let result = engine.run(); 123 | 124 | if (result.state !== FyreVM.EngineState.waitingForLineInput) { 125 | console.error(`engine does not accept input (state ${result.state})`); 126 | if (result.channelData) { 127 | console.error(JSON.stringify(result.channelData)); 128 | } 129 | process.exit(result.state); 130 | } 131 | 132 | // did we have an existing session? If so, load it 133 | if (sessionData) { 134 | engine.restoreSaveGame(sessionData); 135 | } 136 | 137 | // is there a command? If so, run it 138 | if (command) { 139 | let result = engine.receiveLine(command); 140 | console.info(JSON.stringify(result.channelData)); 141 | // inject a "look" to create the undo buffer for our command 142 | engine.receiveLine("look") 143 | // and update the session with the undo state 144 | fs.writeFileSync(saveFile, Buffer.from(new Uint8Array(engine.getUndoState().serialize()))); 145 | 146 | } 147 | else { 148 | // if not, print the initial output 149 | if (result.channelData) { 150 | console.info(JSON.stringify(result.channelData)); 151 | } 152 | // inject a "look" to create the undo buffer for our command 153 | engine.receiveLine("look") 154 | // and update the session with the undo state 155 | fs.writeFileSync(saveFile, Buffer.from(new Uint8Array(engine.getUndoState().serialize()))); 156 | } 157 | -------------------------------------------------------------------------------- /example/node/engineWrapper/README.md: -------------------------------------------------------------------------------- 1 | ## Node engineWrapper CLI Utility 2 | 3 | This is a command line tool that allows the author to test a FyreVM based Glulx story one turn at a time. 4 | 5 | ### To compile engineWrapper... 6 | 7 | $ git clone https://github.com/thiloplanz/glulx-typescript.git 8 | $ cd glulx-typescript/example/node/engineWrapper 9 | $ npx tsc 10 | 11 | ### To start a story... 12 | 13 | $ node engineWrapper.js ifpress.ulx session 14 | {"MAIN":"\nStop the IFPress!\nAn IF Press Demonstration by David Cornelson\nRelease 1 / Serial number 160421 / Inform 7 build 6L38 (I6/v6.33 lib 6/12N) SD\n\nAmidst the cavernous warehouse are large printing presses, large rolls of paper, and barrels of ink. Men and women scurry about, putting tomorrow's edition of the IF Press Gazette together. The loading dock is to the north while the offices are to the south.\n\n","PLOG":"During the Great Underground Industrial Revolution, immediately following the disappearance of magic, the IF Press Gazette was created to make up for the lost tradition of the New Zork Times. Even without magic, some things seemed to get done in magical ways...\n","PRPT":">","LOCN":"Press Room","SCOR":"0","TIME":"540","TURN":"1"} 15 | 16 | ### To continue story with a command... 17 | 18 | $ node engineWrapper.js ifpress.ulx session "north" 19 | {"MAIN":"\nTrucks flow in and out of the docking area, picking up stacks of bound copies of the IF Press Gazette.\n\n\n","PRPT":">","LOCN":"Dock","SCOR":"0","TIME":"541","TURN":"2","INFO":"{ storyTitle: \"Stop the IFPress!\", storyHeadline: \"An IF Press Demonstration\", storyAuthor: \"David Cornelson\", storyCreationYear: \"2016\", releaseNumber: \"1\", serialNumber: \"160421\", inform7Build: \"6L38\", inform6Library: \"6.33\", inform7Library: \"6/12N\", strictMode: \"S\", debugMode: \"D\" }"} 20 | 21 | ### And two more... 22 | 23 | $ node engineWrapper.js ifpress.ulx session "south" 24 | {"MAIN":"\nAmidst the cavernous warehouse are large printing presses, large rolls of paper, and barrels of ink. Men and women scurry about, putting tomorrow's edition of the IF Press Gazette together. The loading dock is to the north while the offices are to the south.\n\n\n","PRPT":">","LOCN":"Press Room","SCOR":"0","TIME":"542","TURN":"3","INFO":"{ storyTitle: \"Stop the IFPress!\", storyHeadline: \"An IF Press Demonstration\", storyAuthor: \"David Cornelson\", storyCreationYear: \"2016\", releaseNumber: \"1\", serialNumber: \"160421\", inform7Build: \"6L38\", inform6Library: \"6.33\", inform7Library: \"6/12N\", strictMode: \"S\", debugMode: \"D\" }"} 25 | 26 | $ node engineWrapper.js ifpress.ulx session "south" 27 | {"MAIN":"\nReporters and other personnel sit ensconced in small metal desks, tapping away on typewriters recently converted to manual interaction (they once acted via magical spells).\n\n\n","PRPT":">","LOCN":"Offices","SCOR":"0","TIME":"543","TURN":"4","INFO":"{ storyTitle: \"Stop the IFPress!\", storyHeadline: \"An IF Press Demonstration\", storyAuthor: \"David Cornelson\", storyCreationYear: \"2016\", releaseNumber: \"1\", serialNumber: \"160421\", inform7Build: \"6L38\", inform6Library: \"6.33\", inform7Library: \"6/12N\", strictMode: \"S\", debugMode: \"D\" }"} 28 | 29 | ### And we can backtrack to a previous turn to "branch" our play... 30 | 31 | $ node engineWrapper.js ifpress.ulx session "south" 1 32 | {"MAIN":"\nReporters and other personnel sit ensconced in small metal desks, tapping away on typewriters recently converted to manual interaction (they once acted via magical spells).\n\n\n","PRPT":">","LOCN":"Offices","SCOR":"0","TIME":"541","TURN":"2","INFO":"{ storyTitle: \"Stop the IFPress!\", storyHeadline: \"An IF Press Demonstration\", storyAuthor: \"David Cornelson\", storyCreationYear: \"2016\", releaseNumber: \"1\", serialNumber: \"160421\", inform7Build: \"6L38\", inform6Library: \"6.33\", inform7Library: \"6/12N\", strictMode: \"S\", debugMode: \"D\" }"} 33 | 34 | ### After all of these commands, our directory will contain the following session files (Quetzal save files): 35 | 36 | 04/22/2016 09:28 AM 107,336 session.1 37 | 04/22/2016 09:31 AM 107,336 session.1.1 38 | 04/22/2016 09:29 AM 107,336 session.2 39 | 04/22/2016 09:30 AM 107,336 session.3 40 | 04/22/2016 09:30 AM 107,336 session.4 41 | 42 | ### To continue the branched story execution, we change the session file... 43 | 44 | $ node engineWrapper.js ifpress.ulx session.1 "north" 45 | {"MAIN":"\nAmidst the cavernous warehouse are large printing presses, large rolls of paper, and barrels of ink. Men and women scurry about, putting tomorrow's edition of the IF Press Gazette together. The loading dock is to the north while the offices are to the south.\n\n\n","PRPT":">","LOCN":"Press Room","SCOR":"0","TIME":"542","TURN":"3","INFO":"{ storyTitle: \"Stop the IFPress!\", storyHeadline: \"An IF Press Demonstration\", storyAuthor: \"David Cornelson\", storyCreationYear: \"2016\", releaseNumber: \"1\", serialNumber: \"160421\", inform7Build: \"6L38\", inform6Library: \"6.33\", inform7Library: \"6/12N\", strictMode: \"S\", debugMode: \"D\" }"} 46 | 47 | ### And now we have one more session file: 48 | 49 | 04/22/2016 09:28 AM 107,336 session.1 50 | 04/22/2016 09:31 AM 107,336 session.1.1 51 | 04/22/2016 09:34 AM 107,336 session.1.2 52 | 04/22/2016 09:29 AM 107,336 session.2 53 | 04/22/2016 09:30 AM 107,336 session.3 54 | 04/22/2016 09:30 AM 107,336 session.4 55 | 56 | ### There is also a compiled version of "node engineWrapper.js" in the tools directory called 'chester' 57 | 58 | $ chester ifpress.ulx session "look" 59 | 60 | ...will work the same as "node engineWrapper.js" and node and typescript are not necessary. -------------------------------------------------------------------------------- /example/web/webworker.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 24 | 25 | 26 | 27 |
please select the game image
28 |

29 |
30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 | 38 | 39 |
40 | 41 | 42 | 189 | -------------------------------------------------------------------------------- /web/WebWorker.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /** 7 | * Adapts an EngineWrapper in a way that can be used from 8 | * a WebWorker (browser background thread), controlled 9 | * from the main thread by simple command and result objects. 10 | * 11 | */ 12 | 13 | 14 | /// 15 | 16 | module FyreVM { 17 | 18 | interface FileReaderSync { 19 | readAsArrayBuffer(blob: Blob): any; 20 | } 21 | declare var FileReaderSync: { 22 | new(): FileReaderSync; 23 | } 24 | 25 | 26 | export interface WebWorkerCommand { 27 | // actions 28 | loadImage? : ArrayBuffer|string, 29 | start?: boolean, 30 | lineInput?: string, 31 | keyInput?: string, 32 | saveSuccessful?: boolean 33 | restore?: ArrayBuffer|string|boolean, 34 | // configuration 35 | enableSaveGame? : boolean, 36 | } 37 | 38 | 39 | export class WebWorker{ 40 | 41 | private engine; 42 | 43 | private queue: MessageEvent[]; 44 | 45 | private error: string; 46 | 47 | onMessage(ev: MessageEvent){ 48 | // don't do anything when in error state 49 | if (this.error){ 50 | return; 51 | } 52 | // just queue it if we are busy 53 | if (this.queue){ 54 | this.queue.push(ev); 55 | return; 56 | } 57 | this.handleMessage(ev); 58 | } 59 | 60 | private handleMessage(ev: MessageEvent) { 61 | let data : WebWorkerCommand = ev.data; 62 | if (!data) return; 63 | 64 | // configuration 65 | if (data.enableSaveGame){ 66 | this.engine.canSaveGames = true; 67 | return; 68 | } 69 | if (data.enableSaveGame === false){ 70 | this.engine.canSaveGames = false; 71 | return; 72 | } 73 | 74 | 75 | // commands 76 | if (data.loadImage){ 77 | this.loadImage(data); 78 | return; 79 | } 80 | if (data.start){ 81 | this.run(); 82 | return; 83 | } 84 | if (data.lineInput || data.lineInput === ''){ 85 | this.onEngineUpdate(this.engine.receiveLine(data.lineInput)); 86 | return; 87 | } 88 | if (data.keyInput || data.keyInput === ''){ 89 | this.onEngineUpdate(this.engine.receiveKey(data.keyInput)); 90 | return; 91 | } 92 | if (data.saveSuccessful || data.saveSuccessful === false){ 93 | this.onEngineUpdate(this.engine.saveGameDone(data.saveSuccessful)); 94 | return; 95 | } 96 | if (data.restore){ 97 | // raw data 98 | if (data.restore instanceof ArrayBuffer){ 99 | // TODO: how to cast properly ? 100 | let ab : any = data.restore; 101 | this.onEngineUpdate(this.engine.receiveSavedGame(Quetzal.load(ab))); 102 | } 103 | // URL 104 | let request = new XMLHttpRequest(); 105 | let worker = this; 106 | let url : any = data.restore; 107 | request.open("GET", url); 108 | request.responseType = 'arraybuffer'; 109 | request.onload = function(){ 110 | if (request.status !== 200){ 111 | worker.onEngineError(`${request.status} ${request.statusText}`); 112 | return; 113 | } 114 | worker.onEngineUpdate(worker.engine.receiveSavedGame(Quetzal.load(request.response))); 115 | } 116 | request.send(); 117 | return; 118 | } 119 | if (data.restore === false){ 120 | this.onEngineUpdate(this.engine.receiveSavedGame(null)); 121 | return; 122 | } 123 | this.onEngineError(`unknown command ${JSON.stringify(data)}`); 124 | } 125 | 126 | onEngineUpdate(ev: EngineWrapperState){ 127 | let p: any = postMessage; 128 | 129 | // some states get extra payload in the message 130 | if (ev.state === EngineState.waitingForGameSavedConfirmation){ 131 | // we pass out the IFhd separately, 132 | // so that client code does not have to parse the Quetzal file 133 | // it can be used to differentiate between multiple games 134 | ev['quetzal'] = this.engine.gameBeingSaved.serialize(); 135 | ev['quetzal.IFhd'] = this.engine.gameBeingSaved.getChunk('IFhd'); 136 | } 137 | if (ev.state === EngineState.waitingForLoadSaveGame){ 138 | // tell the client what IFhd we want 139 | ev['quetzal.IFhd'] = this.engine.getIFhd(); 140 | } 141 | 142 | p(ev); 143 | if (this.queue){ 144 | let ev = this.queue.shift(); 145 | if (this.queue.length === 0){ 146 | delete this.queue; 147 | } 148 | if (ev){ 149 | this.handleMessage(ev); 150 | } 151 | } 152 | } 153 | 154 | onEngineError(message: string){ 155 | this.queue = null; 156 | this.error = message; 157 | this.onEngineUpdate({ 158 | state: EngineState.error, 159 | errorMessage: message 160 | }) 161 | 162 | } 163 | 164 | loadImage(data){ 165 | let {loadImage} = data; 166 | 167 | if (loadImage instanceof ArrayBuffer){ 168 | this.loadImageFromBuffer(loadImage); 169 | return; 170 | } 171 | 172 | 173 | let worker = this; 174 | let request = new XMLHttpRequest(); 175 | request.open("GET", loadImage); 176 | request.responseType = 'arraybuffer'; 177 | request.onload = function(){ 178 | if (request.status !== 200){ 179 | worker.onEngineError(`${request.status} ${request.statusText} ${loadImage}`); 180 | return; 181 | } 182 | worker.loadImageFromBuffer(request.response); 183 | } 184 | this.queue = this.queue || []; 185 | request.send(); 186 | } 187 | 188 | private loadImageFromBuffer(arrayBuffer: ArrayBuffer){ 189 | try{ 190 | this.engine = EngineWrapper.loadFromArrayBuffer(arrayBuffer, false) 191 | } 192 | catch (e){ 193 | this.onEngineError(e.toString()); 194 | } 195 | } 196 | 197 | run(){ 198 | this.onEngineUpdate(this.engine.run()); 199 | } 200 | 201 | } 202 | 203 | } 204 | 205 | 206 | let worker = new FyreVM.WebWorker(); 207 | onmessage = worker.onMessage.bind(worker); 208 | console.info("started web worker"); 209 | 210 | -------------------------------------------------------------------------------- /core/GlkWrapper.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | 7 | /** 8 | * A wrapper to emulate minimal Glk functionality. 9 | */ 10 | 11 | /// 12 | 13 | module FyreVM { 14 | 15 | const enum GlkConst { 16 | wintype_TextBuffer = 3, 17 | 18 | evtype_None = 0, 19 | evtype_CharInput = 2, 20 | evtype_LineInput = 3, 21 | 22 | gestalt_CharInput = 1, 23 | gestalt_CharOutput = 3, 24 | gestalt_CharOutput_ApproxPrint = 1, 25 | gestalt_CharOutput_CannotPrint = 0, 26 | gestalt_CharOutput_ExactPrint = 2, 27 | gestalt_LineInput = 2, 28 | gestalt_Version = 0 29 | 30 | } 31 | 32 | interface StreamCloseResult { 33 | ok: boolean; 34 | read: number; 35 | written: number; 36 | } 37 | 38 | interface GlkStream { 39 | getId(): number; 40 | put(s: string): void; 41 | close(): StreamCloseResult; 42 | } 43 | 44 | class GlkWindowStream implements GlkStream { 45 | id : number; 46 | engine: Engine; 47 | 48 | constructor(id:number, engine: Engine){ 49 | this.id = id; 50 | this.engine = engine; 51 | } 52 | 53 | getId(){ 54 | return this.id; 55 | } 56 | 57 | put(s: string){ 58 | this.engine['outputBuffer'].write(s); 59 | } 60 | 61 | close(){ 62 | return { ok: false, written: 0, read: 0}; 63 | } 64 | 65 | } 66 | 67 | export function GlkWrapperCall(code: number, argc: number){ 68 | 69 | if (!this.glkHandlers){ 70 | this.glkHandlers = initGlkHandlers(); 71 | this.glkStreams = []; 72 | } 73 | 74 | if (argc > 8){ 75 | throw new Error(`Too many stack arguments for glk call ${code}: ${argc}`); 76 | } 77 | let glkArgs = []; 78 | while(argc--){ 79 | glkArgs.push(this.pop()); 80 | } 81 | let handler = this.glkHandlers[code]; 82 | if (handler){ 83 | return handler.apply(this, glkArgs); 84 | }else{ 85 | console.error(`unimplemented glk call ${code}`); 86 | return 0; 87 | } 88 | } 89 | 90 | export function GlkWrapperWrite(s: string){ 91 | if (this.glkCurrentStream){ 92 | this.glkCurrentStream.put(s); 93 | } 94 | } 95 | 96 | function stub() { return 0}; 97 | 98 | function initGlkHandlers(){ 99 | let handlers = []; 100 | 101 | // glk_stream_iterate 102 | handlers[0x40] = stub; 103 | 104 | // glk_window_iterate 105 | handlers[0x20] = function(win_id){ 106 | if (this.glkWindowOpen && win_id === 0) 107 | return 1; 108 | return 0; 109 | } 110 | 111 | // glk_fileref_iterate 112 | handlers[0x64] = stub; 113 | 114 | // glk_window_open 115 | handlers[0x23] = function(){ 116 | if (this.glkWindowOpen) 117 | return 0; 118 | this.glkWindowOpen = true; 119 | this.glkStreams[1] = new GlkWindowStream(1, this); 120 | return 1; 121 | } 122 | 123 | // glk_set_window 124 | handlers[0x2F] = function(){ 125 | if (this.glkWindowOpen){ 126 | this.glkCurrentStream = this.glkStreams[1]; 127 | } 128 | return 0; 129 | } 130 | 131 | // glk_set_style 132 | handlers[0x86] = stub; 133 | 134 | //glk_stylehint_set 135 | handlers[0xB0] = stub; 136 | 137 | // glk_style_distinguish 138 | handlers[0xB2] = stub; 139 | 140 | // glk_style_measure 141 | handlers[0xB3] = stub; 142 | 143 | // glk_char_to_lower 144 | handlers[0xA0] = function(ch){ 145 | return String.fromCharCode(ch).toLowerCase().charCodeAt(0); 146 | } 147 | 148 | // glk_char_to_upper 149 | handlers[0xA1] = function(ch){ 150 | return String.fromCharCode(ch).toUpperCase().charCodeAt(0); 151 | } 152 | 153 | // glk_request_line_event 154 | handlers[0xD0] = function(winId, buffer, bufferSize){ 155 | this.glkWantLineInput = true; 156 | this.glkLineInputBufSize = bufferSize; 157 | this.glkLineInputBuffer = buffer; 158 | } 159 | 160 | // glk_request_char_event 161 | handlers[0xD2] = function(){ 162 | this.glkWantCharInput = true; 163 | } 164 | 165 | // glk_put_char 166 | handlers[0x80] = function(c){ 167 | GlkWrapperWrite.call(this, String.fromCharCode(c)); 168 | } 169 | 170 | // glk_select 171 | handlers[0xC0] = function(reference) : any{ 172 | this.deliverOutput(); 173 | 174 | 175 | if (this.glkWantLineInput){ 176 | this.glkWantLineInput = false; 177 | if (!this.lineWanted){ 178 | GlkWriteReference.call(this, reference, GlkConst.evtype_LineInput, 1, 1, 0); 179 | return 0; 180 | } 181 | let callback = function(line = ''){ 182 | let max = this.image.writeASCII(this.glkLineInputBuffer, line, this.glkLineInputBufSize); 183 | GlkWriteReference.call(this, reference, GlkConst.evtype_LineInput, 1, max, 0); 184 | this.resumeAfterWait([0]); 185 | } 186 | 187 | this.lineWanted(callback.bind(this)); 188 | return 'wait'; 189 | }else if (this.glkWantCharInput){ 190 | this.glkWantCharInput = false; 191 | if (!this.keyWanted){ 192 | GlkWriteReference.call(this, reference, GlkConst.evtype_CharInput, 1, 0, 0); 193 | return 0; 194 | } 195 | let callback = function(line){ 196 | GlkWriteReference.call(this, reference, GlkConst.evtype_CharInput, 1, line.charCodeAt(0), 0); 197 | this.resumeAfterWait([0]); 198 | } 199 | 200 | this.lineWanted(callback.bind(this)); 201 | return 'wait'; 202 | 203 | }else{ 204 | // no event 205 | GlkWriteReference.call(this, reference, GlkConst.evtype_None, 0, 0, 0); 206 | } 207 | return 0; 208 | } 209 | 210 | return handlers; 211 | } 212 | 213 | 214 | function GlkWriteReference(reference: number, ...values: number[]) 215 | { 216 | if (reference == 0xffffffff){ 217 | for (let i=0; i 8 | 9 | 10 | module FyreVM { 11 | 12 | // build a map of all opcodes by name 13 | let opcodes = (function(oc){ 14 | let map = {}; 15 | for (let c in oc){ 16 | let op = oc[c]; 17 | map[op.name] = op; 18 | } 19 | return map; 20 | })(Opcodes.initOpcodes()); 21 | 22 | 23 | // coerce Javascript number into uint32 range 24 | function uint32(x:number) : number{ 25 | return x >>> 0; 26 | } 27 | function uint16(x: number) :number{ 28 | if (x < 0){ 29 | x = 0xFFFF + x + 1; 30 | } 31 | return x % 0x10000; 32 | } 33 | function uint8(x: number) :number{ 34 | if (x < 0){ 35 | x = 255 + x + 1; 36 | } 37 | return x % 256; 38 | } 39 | 40 | function parseHex(x: string): number { 41 | let n= new Number(`0x${x}`).valueOf(); 42 | if (isNaN(n)){ 43 | throw new Error(`invalid hex number ${x}`); 44 | } 45 | return n; 46 | } 47 | function parsePtr(x: string, params: any[], i: number, sig: number[]){ 48 | if (x.indexOf("R:") === 1){ 49 | // *R:00 50 | if (x.length == 5){ 51 | sig.push(LoadOperandType.ram_8); 52 | params[i] = parseHex(x.substring(3)); 53 | return; 54 | } 55 | // *R:1234 56 | if (x.length == 7){ 57 | sig.push(LoadOperandType.ram_16); 58 | params[i] = parseHex(x.substring(3)); 59 | return; 60 | } 61 | // *R:12345678 62 | if (x.length == 11){ 63 | sig.push(LoadOperandType.ram_32); 64 | params[i] = parseHex(x.substring(3)); 65 | return; 66 | } 67 | } 68 | 69 | // *1234 70 | if (x.length == 5){ 71 | sig.push(LoadOperandType.ptr_16); 72 | params[i] = parseHex(x.substring(1)); 73 | return; 74 | } 75 | // *00112233 76 | if (x.length == 9){ 77 | sig.push(LoadOperandType.ptr_32); 78 | params[i] = parseHex(x.substring(1)); 79 | return; 80 | } 81 | throw new Error(`unsupported address specification ${x}`); 82 | } 83 | function parseLocal(x: string, params: any[], i: number, sig: number[]){ 84 | // Fr:00 85 | if (x.length == 5){ 86 | sig.push(LoadOperandType.local_8); 87 | params[i] = parseHex(x.substring(3)); 88 | return; 89 | } 90 | throw new Error(`unsupported local frame address specification ${x}`); 91 | } 92 | 93 | /** 94 | * encode an opcode and its parameters 95 | */ 96 | export function encodeOpcode(name: string, ... params: any[]) : number[]{ 97 | let opcode : Opcode = opcodes[name]; 98 | if (!opcode){ 99 | throw new Error(`unknown opcode ${name}`); 100 | } 101 | 102 | let {loadArgs, storeArgs, code} = opcode; 103 | if (params.length != loadArgs + storeArgs){ 104 | throw new Error(`opcode '${name}' requires ${loadArgs+storeArgs} arguments, but you gave me ${params.length}: ${JSON.stringify(params)}`); 105 | } 106 | 107 | // opcode 108 | let result; 109 | if (code >= 0x1000){ 110 | result = [ 0xC0, 0x00, code >> 8, code & 0xFF]; 111 | } 112 | else if (code >= 0x80){ 113 | code = code + 0x8000; 114 | result = [ code >> 8, code & 0xFF]; 115 | } 116 | else { 117 | result = [ code ]; 118 | } 119 | 120 | // loadArgs signature 121 | let sig = []; 122 | let i = 0; 123 | for(;i 0xFFFFFFFF || x < - 0x100000000){ 139 | throw new Error(`immediate load operand ${x} out of signed 32 bit integer range.`); 140 | } 141 | sig.push(LoadOperandType.int32); 142 | continue; 143 | } 144 | if (typeof(x) === 'string'){ 145 | if (x === 'pop'){ 146 | sig.push(LoadOperandType.stack); 147 | continue; 148 | } 149 | if (x.indexOf("*") === 0){ 150 | parsePtr(x, params, i, sig); 151 | continue; 152 | } 153 | if (x.indexOf("Fr:") === 0){ 154 | parseLocal(x, params, i, sig); 155 | continue; 156 | } 157 | } 158 | throw new Error(`unsupported load argument ${x} for ${name}(${JSON.stringify(params)})`); 159 | } 160 | // storeArg signature 161 | if (storeArgs){ 162 | for (; i> 8); 213 | result.push(x & 0xFF); 214 | continue; 215 | } 216 | if (s === LoadOperandType.int32){ 217 | x = uint32(x); 218 | result.push(x >> 24); 219 | result.push((x >> 16) & 0xFF); 220 | result.push((x >> 8) & 0xFF); 221 | result.push(x & 0xFF); 222 | continue; 223 | } 224 | if (s === LoadOperandType.ptr_8 || s === LoadOperandType.ram_8 || s === LoadOperandType.local_8){ 225 | result.push(x); 226 | continue; 227 | } 228 | if (s === LoadOperandType.ptr_16 || s === LoadOperandType.ram_16){ 229 | result.push(x >> 8); 230 | result.push(x & 0xFF); 231 | continue; 232 | } 233 | if (s === LoadOperandType.ptr_32 || s === LoadOperandType.ram_32){ 234 | result.push(x >> 24); 235 | result.push((x >> 16) & 0xFF); 236 | result.push((x >> 8) & 0xFF); 237 | result.push(x & 0xFF); 238 | continue; 239 | } 240 | throw new Error(`unsupported argument ${x} of type ${s} for ${name}(${JSON.stringify(params)})`) 241 | 242 | } 243 | 244 | return result; 245 | } 246 | 247 | } -------------------------------------------------------------------------------- /core/Output.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | 8 | 9 | module FyreVM { 10 | 11 | /// Identifies an output system for use with @setiosys. 12 | export const enum IOSystem { 13 | /// Output is discarded. 14 | Null, 15 | /// Output is filtered through a Glulx function. 16 | Filter, 17 | /// Output is sent through FyreVM's channel system. 18 | Channels, 19 | /// Output is sent through Glk. 20 | Glk 21 | } 22 | 23 | export function SendCharToOutput(x: number){ 24 | switch(this.outputSystem){ 25 | case IOSystem.Null: return; 26 | case IOSystem.Channels: 27 | // TODO? need to handle Unicode characters larger than 16 bits 28 | this.outputBuffer.write(String.fromCharCode(x)); 29 | return; 30 | case IOSystem.Glk: 31 | if (this.glkMode === GlkMode.Wrapper) 32 | GlkWrapperWrite.call(this, String.fromCharCode(x)); 33 | return; 34 | 35 | } 36 | throw new Error(`unsupported output system ${this.outputSystem}`); 37 | } 38 | 39 | export function SendStringToOutput(x: string){ 40 | switch(this.outputSystem){ 41 | case IOSystem.Null: return; 42 | case IOSystem.Channels: 43 | this.outputBuffer.write(x); 44 | return; 45 | case IOSystem.Glk: 46 | if (this.glkMode === GlkMode.Wrapper) 47 | GlkWrapperWrite.call(this, x); 48 | return; 49 | } 50 | throw new Error(`unsupported output system ${this.outputSystem}`); 51 | } 52 | 53 | 54 | export const enum GLULX_HUFF { 55 | // String decoding table: header field offsets 56 | TABLESIZE_OFFSET = 0, 57 | NODECOUNT_OFFSET = 4, 58 | ROOTNODE_OFFSET = 8, 59 | 60 | // String decoding table: node types 61 | NODE_BRANCH = 0, 62 | NODE_END = 1, 63 | NODE_CHAR = 2, 64 | NODE_CSTR = 3, 65 | NODE_UNICHAR = 4, 66 | NODE_UNISTR = 5, 67 | NODE_INDIRECT = 8, 68 | NODE_DBLINDIRECT = 9, 69 | NODE_INDIRECT_ARGS = 10, 70 | NODE_DBLINDIRECT_ARGS = 11 71 | } 72 | 73 | /** 74 | * Prints the next character of a compressed string, consuming one or more bits. 75 | * 76 | */ 77 | export function NextCompressedChar(){ 78 | let engine: Engine = this; 79 | let {image} = engine; 80 | let node = image.readInt32(this.decodingTable + GLULX_HUFF.ROOTNODE_OFFSET); 81 | 82 | while (true){ 83 | let nodeType = image.readByte(node++); 84 | switch(nodeType){ 85 | case GLULX_HUFF.NODE_BRANCH: 86 | if (nextCompressedStringBit(engine)){ 87 | node = image.readInt32(node+4); // go right 88 | }else{ 89 | node = image.readInt32(node); // go left 90 | } 91 | break; 92 | case GLULX_HUFF.NODE_END: 93 | this.resumeFromCallStub(0); 94 | return; 95 | case GLULX_HUFF.NODE_CHAR: 96 | case GLULX_HUFF.NODE_UNICHAR: 97 | let c = (nodeType === GLULX_HUFF.NODE_UNICHAR) ? image.readInt32(node) : image.readByte(node); 98 | if (this.outputSystem === IOSystem.Filter){ 99 | this.performCall(this.filterAddress, [ c ], GLULX_STUB.RESUME_HUFFSTR, this.printingDigit, this.PC); 100 | }else{ 101 | SendCharToOutput.call(this, c); 102 | } 103 | return; 104 | case GLULX_HUFF.NODE_CSTR: 105 | if (this.outputSystem === IOSystem.Filter){ 106 | this.pushCallStub(GLULX_STUB.RESUME_HUFFSTR, this.printingDigit, this.PC, this.FP); 107 | this.PC = node; 108 | this.execMode = ExecutionMode.CString; 109 | }else{ 110 | SendStringToOutput.call(this, this.image.readCString(node)); 111 | } 112 | return; 113 | // TODO: the other node types 114 | default: 115 | throw new Error(`Unrecognized compressed string node type ${nodeType}`); 116 | } 117 | } 118 | } 119 | 120 | 121 | function nextCompressedStringBit(engine): boolean{ 122 | let result = ((engine.image.readByte(engine.PC) & ( 1 << engine.printingDigit)) !== 0) 123 | engine.printingDigit++; 124 | if (engine.printingDigit === 8){ 125 | engine.printingDigit = 0; 126 | engine.PC++; 127 | } 128 | return result; 129 | } 130 | 131 | export function NextCStringChar(){ 132 | let ch = this.image.readByte(this.PC++); 133 | if (ch === 0){ 134 | this.resumeFromCallStub(0); 135 | return; 136 | } 137 | if (this.outputSystem === IOSystem.Filter){ 138 | this.performCall(this.filterAddress, [ch], GLULX_STUB.RESUME_CSTR, 0, this.PC); 139 | }else{ 140 | SendCharToOutput(ch); 141 | } 142 | } 143 | 144 | export function NextUniStringChar(){ 145 | let ch = this.image.readInt32(this.PC); 146 | this.PC += 4; 147 | if (ch === 0){ 148 | this.resumeFromCallStub(0); 149 | return; 150 | } 151 | if (this.outputSystem === IOSystem.Filter){ 152 | this.performCall(this.filterAddress, [ch], GLULX_STUB.RESUME_UNISTR, 0, this.PC); 153 | }else{ 154 | SendCharToOutput(ch); 155 | } 156 | } 157 | 158 | export function NextDigit(){ 159 | let s:string = this.PC.toString(); 160 | if (this.printingDigit < s.length){ 161 | let ch = s.charAt(this.printingDigit); 162 | if (this.outputSystem === IOSystem.Filter){ 163 | this.performCall(this.filterAddress, [ch.charCodeAt(0)], GLULX_STUB.RESUME_NUMBER, this.printingDigit+1, this.PC); 164 | }else{ 165 | SendStringToOutput(ch); 166 | this.printingDigit++; 167 | } 168 | }else{ 169 | this.resumeFromCallStub(0); 170 | } 171 | } 172 | 173 | export interface ChannelData { 174 | [channel: string] : string; 175 | MAIN?: string; 176 | PRPT?: string; // prompt 177 | LOCN?: string; // location 178 | SCOR?: string; // score 179 | TIME?: string; // time (hhmm) 180 | TURN?: string; // turn count 181 | PLOG?: string; // prologue, 182 | DEAD?: string; // Death text (shown when player dies) 183 | ENDG?: string; // End game text 184 | INFO?: string; // Story info text (comes out as JSON) 185 | SNOT?: string; // Notify if score changes 186 | } 187 | 188 | 189 | export class OutputBuffer { 190 | 191 | // No special "StringBuilder" 192 | // simple String concatenation is said to be fast on modern browsers 193 | // http://stackoverflow.com/a/27126355/14955 194 | 195 | private channel = 'MAIN'; 196 | 197 | private channelData: ChannelData = { 198 | MAIN: '' 199 | } 200 | 201 | getChannel(): string{ 202 | return this.channel; 203 | } 204 | 205 | /** If the output channel is changed to any channel other than 206 | * "MAIN", the channel's contents will be 207 | * cleared first. 208 | */ 209 | setChannel(c: string){ 210 | if (c === this.channel) return; 211 | this.channel = c; 212 | if (c !== 'MAIN'){ 213 | this.channelData[c] = ''; 214 | } 215 | } 216 | 217 | /** 218 | * Writes a string to the buffer for the currently 219 | * selected output channel. 220 | */ 221 | write(s: string){ 222 | this.channelData[this.channel] += s; 223 | } 224 | 225 | /** 226 | * Packages all the output that has been stored so far, returns it, 227 | * and empties the buffer. 228 | */ 229 | flush() : ChannelData{ 230 | let {channelData} = this; 231 | let r : ChannelData= {}; 232 | for (let c in channelData) { 233 | let s = channelData[c]; 234 | if (s){ 235 | r[c] = s; 236 | channelData[c] = ''; 237 | } 238 | } 239 | return r; 240 | } 241 | 242 | 243 | } 244 | 245 | } -------------------------------------------------------------------------------- /mersenne-twister.ts: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * TypeScript port by Thilo Planz 4 | * 5 | * https://gist.github.com/thiloplanz/6abf04f957197e9e3912 6 | */ 7 | 8 | /* 9 | I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace 10 | so it's better encapsulated. Now you can have multiple random number generators 11 | and they won't stomp all over eachother's state. 12 | 13 | If you want to use this as a substitute for Math.random(), use the random() 14 | method like so: 15 | 16 | var m = new MersenneTwister(); 17 | var randomNumber = m.random(); 18 | 19 | You can also call the other genrand_{foo}() methods on the instance. 20 | 21 | If you want to use a specific seed in order to get a repeatable random 22 | sequence, pass an integer into the constructor: 23 | 24 | var m = new MersenneTwister(123); 25 | 26 | and that will always produce the same random sequence. 27 | 28 | Sean McCullough (banksean@gmail.com) 29 | */ 30 | 31 | /* 32 | A C-program for MT19937, with initialization improved 2002/1/26. 33 | Coded by Takuji Nishimura and Makoto Matsumoto. 34 | 35 | Before using, initialize the state by using init_genrand(seed) 36 | or init_by_array(init_key, key_length). 37 | 38 | Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, 39 | All rights reserved. 40 | 41 | Redistribution and use in source and binary forms, with or without 42 | modification, are permitted provided that the following conditions 43 | are met: 44 | 45 | 1. Redistributions of source code must retain the above copyright 46 | notice, this list of conditions and the following disclaimer. 47 | 48 | 2. Redistributions in binary form must reproduce the above copyright 49 | notice, this list of conditions and the following disclaimer in the 50 | documentation and/or other materials provided with the distribution. 51 | 52 | 3. The names of its contributors may not be used to endorse or promote 53 | products derived from this software without specific prior written 54 | permission. 55 | 56 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 57 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 58 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 59 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 60 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 61 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 62 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 63 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 64 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 65 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 66 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 67 | 68 | 69 | Any feedback is very welcome. 70 | http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html 71 | email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) 72 | */ 73 | 74 | class MersenneTwister{ 75 | 76 | /* Period parameters */ 77 | private N = 624; 78 | private M = 397; 79 | private MATRIX_A = 0x9908b0df; /* constant vector a */ 80 | private UPPER_MASK = 0x80000000; /* most significant w-r bits */ 81 | private LOWER_MASK = 0x7fffffff; /* least significant r bits */ 82 | 83 | private mt = new Array(this.N); /* the array for the state vector */ 84 | private mti = this.N + 1; /* mti==N+1 means mt[N] is not initialized */ 85 | 86 | constructor(seed?:number) { 87 | if (seed == undefined) { 88 | seed = new Date().getTime(); 89 | } 90 | this.init_genrand(seed); 91 | } 92 | 93 | /* initializes mt[N] with a seed */ 94 | private init_genrand(s:number) { 95 | this.mt[0] = s >>> 0; 96 | for (this.mti=1; this.mti>> 30); 98 | this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253) 99 | + this.mti; 100 | /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ 101 | /* In the previous versions, MSBs of the seed affect */ 102 | /* only MSBs of the array mt[]. */ 103 | /* 2002/01/09 modified by Makoto Matsumoto */ 104 | this.mt[this.mti] >>>= 0; 105 | /* for >32 bit machines */ 106 | } 107 | } 108 | 109 | /* initialize by an array with array-length */ 110 | /* init_key is the array for initializing keys */ 111 | /* key_length is its length */ 112 | /* slight change for C++, 2004/2/26 */ 113 | init_by_array(init_key, key_length) { 114 | var i, j, k; 115 | this.init_genrand(19650218); 116 | i=1; j=0; 117 | k = (this.N>key_length ? this.N : key_length); 118 | for (; k; k--) { 119 | var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30) 120 | this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525))) 121 | + init_key[j] + j; /* non linear */ 122 | this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ 123 | i++; j++; 124 | if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; } 125 | if (j>=key_length) j=0; 126 | } 127 | for (k=this.N-1; k; k--) { 128 | var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30); 129 | this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941)) 130 | - i; /* non linear */ 131 | this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ 132 | i++; 133 | if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; } 134 | } 135 | 136 | this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */ 137 | } 138 | 139 | /* generates a random number on [0,0xffffffff]-interval */ 140 | genrand_int32() { 141 | var y; 142 | var mag01 = new Array(0x0, this.MATRIX_A); 143 | /* mag01[x] = x * MATRIX_A for x=0,1 */ 144 | 145 | if (this.mti >= this.N) { /* generate N words at one time */ 146 | var kk; 147 | 148 | if (this.mti == this.N+1) /* if init_genrand() has not been called, */ 149 | this.init_genrand(5489); /* a default initial seed is used */ 150 | 151 | for (kk=0;kk>> 1) ^ mag01[y & 0x1]; 154 | } 155 | for (;kk>> 1) ^ mag01[y & 0x1]; 158 | } 159 | y = (this.mt[this.N-1]&this.UPPER_MASK)|(this.mt[0]&this.LOWER_MASK); 160 | this.mt[this.N-1] = this.mt[this.M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; 161 | 162 | this.mti = 0; 163 | } 164 | 165 | y = this.mt[this.mti++]; 166 | 167 | /* Tempering */ 168 | y ^= (y >>> 11); 169 | y ^= (y << 7) & 0x9d2c5680; 170 | y ^= (y << 15) & 0xefc60000; 171 | y ^= (y >>> 18); 172 | 173 | return y >>> 0; 174 | } 175 | 176 | /* generates a random number on [0,0x7fffffff]-interval */ 177 | genrand_int31() { 178 | return (this.genrand_int32()>>>1); 179 | } 180 | 181 | /* generates a random number on [0,1]-real-interval */ 182 | genrand_real1() { 183 | return this.genrand_int32()*(1.0/4294967295.0); 184 | /* divided by 2^32-1 */ 185 | } 186 | 187 | /* generates a random number on [0,1)-real-interval */ 188 | random() { 189 | return this.genrand_int32()*(1.0/4294967296.0); 190 | /* divided by 2^32 */ 191 | } 192 | 193 | /* generates a random number on (0,1)-real-interval */ 194 | genrand_real3() { 195 | return (this.genrand_int32() + 0.5)*(1.0/4294967296.0); 196 | /* divided by 2^32 */ 197 | } 198 | 199 | /* generates a random number on [0,1) with 53-bit resolution*/ 200 | genrand_res53() { 201 | var a=this.genrand_int32()>>>5, b=this.genrand_int32()>>>6; 202 | return(a*67108864.0+b)*(1.0/9007199254740992.0); 203 | } 204 | 205 | /* These real versions are due to Isaku Wada, 2002/01/09 added */ 206 | } -------------------------------------------------------------------------------- /test/core/EngineTest.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | /// 8 | 9 | module FyreVM { 10 | 11 | export module NodeUnit { 12 | 13 | let RAM = 0x03A0; 14 | 15 | export function makeTestImage(m: MemoryAccess, ...code: any[]): UlxImage { 16 | let c = 256; 17 | UlxImage.writeHeader({ 18 | endMem: 10 * 1024, 19 | version: 0x00030100, 20 | startFunc: c, 21 | stackSize: 1024, 22 | ramStart: RAM, 23 | decodingTbl: 0x3B0 24 | }, m, 0); 25 | 26 | 27 | for (let i = 0; i < code.length; i++) { 28 | let x = code[i]; 29 | if (typeof (x) === 'number') 30 | m.writeByte(c++, code[i]); 31 | else { 32 | // flatten arrays 33 | for (let j = 0; j < x.length; j++) { 34 | m.writeByte(c++, x[j]) 35 | } 36 | } 37 | } 38 | 39 | return new UlxImage(m); 40 | } 41 | 42 | let opcodes = Opcodes.initOpcodes(); 43 | 44 | 45 | export function op(name: string): any { 46 | for (var c in opcodes) { 47 | if (opcodes[c].name === name) { 48 | let code = opcodes[c].code; 49 | if (code >= 0x1000) { 50 | return [0xC0, 0x00, code >> 8, code & 0xFF]; 51 | } 52 | if (code >= 0x80) { 53 | code = code + 0x8000; 54 | return [code >> 8, code & 0xFF] 55 | } 56 | return code; 57 | } 58 | } 59 | throw new Error(`unknown opcode ${name}`); 60 | } 61 | 62 | 63 | 64 | export function p_in(a: LoadOperandType, b: LoadOperandType | StoreOperandType = 0) { 65 | return a + (b << 4); 66 | } 67 | 68 | export function p_out(a: StoreOperandType, b: StoreOperandType = 0) { 69 | return a + (b << 4); 70 | } 71 | 72 | export function stepImage(gameImage: UlxImage, stepCount = 1, test?, initialStack?: number[]): Engine { 73 | let engine: any = new Engine(gameImage); 74 | engine.bootstrap(); 75 | if (initialStack) { 76 | for (let i = initialStack.length - 1; i >= 0; i--) { 77 | engine.push(initialStack[i]); 78 | } 79 | } 80 | while (stepCount--) { 81 | engine.step(); 82 | } 83 | return engine; 84 | } 85 | 86 | export function addEngineTests(tests, m: MemoryAccess) { 87 | tests.Engine = {} 88 | 89 | 90 | tests.Engine.testLoadOperandTypeByte = 91 | function (test) { 92 | 93 | let gameImage = makeTestImage(m, 94 | CallType.stack, 0x00, 0x00, // type C0, no args 95 | encodeOpcode('add', 1, 1, RAM) 96 | ); 97 | stepImage(gameImage); 98 | test.equals(gameImage.readInt32(0x03A0), 2, "1+1=2"); 99 | test.done(); 100 | } 101 | 102 | tests.Engine.testLoadOperandTypeInt16 = 103 | function (test) { 104 | 105 | let gameImage = makeTestImage(m, 106 | CallType.stack, 0x00, 0x00, // type C0, no args 107 | encodeOpcode('add', 0x010F, 0x02F0, RAM) 108 | ); 109 | 110 | stepImage(gameImage); 111 | test.equals(gameImage.readInt32(0x03A0), 0x03FF, "0x010F+0x02F0=0x03FF"); 112 | test.done(); 113 | } 114 | 115 | tests.Engine.testLoadOperandTypeInt32 = 116 | function (test) { 117 | 118 | let gameImage = makeTestImage(m, 119 | CallType.stack, 0x00, 0x00, // type C0, no args 120 | encodeOpcode('add', 0x010F02F0, 0, RAM) 121 | ); 122 | 123 | stepImage(gameImage); 124 | test.equals(gameImage.readInt32(0x03A0), 0x010F02F0, "0x010F02F0+0"); 125 | test.done(); 126 | } 127 | 128 | tests.Engine.testLoadOperandTypePtr_32 = 129 | function (test) { 130 | 131 | let gameImage = makeTestImage(m, 132 | CallType.stack, 0x00, 0x00, // type C0, no args 133 | encodeOpcode('add', '*03A0', '*000003A0', RAM) 134 | ); 135 | 136 | gameImage.writeInt32(0x03A0, 0x01020304); 137 | stepImage(gameImage); 138 | test.equals(gameImage.readInt32(0x03A0), 0x02040608, "ramStart := add ramStart, ramStart"); 139 | test.done(); 140 | } 141 | 142 | tests.Engine.testLoadOperandTypeStack = 143 | function (test) { 144 | 145 | let gameImage = makeTestImage(m, 146 | CallType.stack, 0x00, 0x00, // type C0, no args 147 | encodeOpcode('add', 'pop', 'pop', RAM) 148 | ); 149 | 150 | gameImage.writeInt32(0x03A0, 0x01020304); 151 | stepImage(gameImage, 1, test, [12, 19]); 152 | test.equals(gameImage.readInt32(0x03A0), 31, "ramStart := add 12, 19"); 153 | test.done(); 154 | } 155 | 156 | tests.Engine.testLoadOperandTypeLocal = 157 | function (test) { 158 | 159 | let gameImage = makeTestImage(m, 160 | CallType.stack, 0x04, 0x02, 0x00, 0x00, // type C0, two locals 161 | encodeOpcode('copy', 12, 'Fr:00'), 162 | encodeOpcode('copy', 19, 'Fr:04'), 163 | encodeOpcode('add', 'Fr:00', 'Fr:04', RAM) 164 | ); 165 | 166 | gameImage.writeInt32(0x03A0, 0x01020304); 167 | stepImage(gameImage, 3, test); 168 | test.equals(gameImage.readInt32(0x03A0), 31, "ramStart := add 12, 19"); 169 | test.done(); 170 | } 171 | 172 | 173 | tests.Engine.testLoadOperandTypeRAM = 174 | function (test) { 175 | 176 | let gameImage = makeTestImage(m, 177 | CallType.stack, 0x00, 0x00, // type C0, no args 178 | encodeOpcode('add', '*R:0010', '*R:10', RAM) 179 | ); 180 | 181 | gameImage.writeInt32(0x03B0, 0x01020304); 182 | stepImage(gameImage, 1, test); 183 | test.equals(gameImage.readInt32(0x03A0), 0x02040608, "ramStart := add 0x01020304, 0x01020304"); 184 | test.done(); 185 | } 186 | 187 | tests.Engine.testStoreOperandTypePtr_32 = 188 | function (test) { 189 | 190 | let gameImage = makeTestImage(m, 191 | CallType.stack, 0x00, 0x00, // type C0, no args 192 | encodeOpcode('add', 1, 1, '*000003A0') 193 | ); 194 | stepImage(gameImage); 195 | test.equals(gameImage.readInt32(0x03A0), 2, "1+1=2"); 196 | test.done(); 197 | } 198 | 199 | tests.Engine.testStoreOperandTypeRAM_32 = 200 | function (test) { 201 | 202 | let gameImage = makeTestImage(m, 203 | CallType.stack, 0x00, 0x00, // type C0, no args 204 | encodeOpcode('add', 1, 1, '*R:00000021') 205 | ); 206 | stepImage(gameImage); 207 | test.equals(gameImage.readInt32(0x03C1), 2, "1+1=2"); 208 | test.done(); 209 | } 210 | 211 | 212 | tests.Engine.run = 213 | function (test) { 214 | let gameImage = makeTestImage(m, 215 | CallType.stack, 0x00, 0x00, // type C0, no args 216 | encodeOpcode('add', 1, 1, RAM), 217 | encodeOpcode('return', 0) 218 | ); 219 | let engine = new Engine(gameImage); 220 | engine.run(); 221 | test.done(); 222 | } 223 | 224 | tests.Engine.saveToQuetzal = 225 | function (test) { 226 | let gameImage = makeTestImage(m, 227 | CallType.stack, 0x00, 0x00, // type C0, no args 228 | encodeOpcode('add', 100, 11, 'push'), 229 | encodeOpcode('return', 0) 230 | ); 231 | let engine = new Engine(gameImage); 232 | engine.run(); 233 | let q = engine.saveToQuetzal(0, 0); 234 | test.equal(q.getChunk('IFhd').byteLength, 128, 'game file identifier present'); 235 | test.equal(q.getChunk('MAll'), undefined, 'no heap'); 236 | let stack = new MemoryAccess(0); 237 | stack.buffer = new Uint8Array(q.getChunk('Stks')); 238 | test.equal(stack.readInt32(12), 111, 'result data found in saved stack') 239 | test.done(); 240 | } 241 | 242 | tests.Engine.quetzalRoundTrip = 243 | function (test) { 244 | let gameImage = makeTestImage(m, 245 | CallType.stack, 0x00, 0x00, // type C0, no args 246 | encodeOpcode('add', 100, 11, 'push'), 247 | encodeOpcode('return', 0) 248 | ); 249 | let engine = new Engine(gameImage); 250 | engine.run(); 251 | let q = engine.saveToQuetzal(0, 0); 252 | engine = new Engine(gameImage); 253 | engine.loadFromQuetzal(q); 254 | test.equal(engine['stack'].readInt32(12), 111, 'result data found in saved stack'); 255 | test.done(); 256 | } 257 | 258 | } 259 | 260 | 261 | } 262 | } -------------------------------------------------------------------------------- /core/EngineWrapper.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 and 2016 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /** 7 | * A wrapper around Engine that can be communicates 8 | * via simple JSON-serializable messages. 9 | * 10 | */ 11 | 12 | /// 13 | module FyreVM{ 14 | 15 | export const enum EngineState { 16 | loaded = 1, 17 | running = 2, 18 | completed = 100, 19 | error = -100, 20 | 21 | waitingForLineInput = 51, 22 | waitingForKeyInput = 52, 23 | waitingForGameSavedConfirmation = 53, 24 | waitingForLoadSaveGame = 54 25 | } 26 | 27 | export interface EngineWrapperState { 28 | state: EngineState, 29 | channelData?: ChannelData, 30 | errorMessage?: string, 31 | gameBeingSaved?: Quetzal 32 | } 33 | 34 | export class EngineWrapper{ 35 | 36 | private engine: Engine 37 | 38 | private canSaveGames : boolean 39 | 40 | constructor(gameImage: MemoryAccess, canSaveGames: boolean = false){ 41 | this.canSaveGames = canSaveGames 42 | let engine = this.engine = new Engine(new UlxImage(gameImage)) 43 | 44 | // set up the callbacks 45 | engine.outputReady = 46 | (channelData) => { 47 | this.channelData = channelData 48 | } 49 | 50 | engine.keyWanted = 51 | (cb) => this.waitState(cb, EngineState.waitingForKeyInput) 52 | engine.lineWanted = 53 | (cb) => this.waitState(cb, EngineState.waitingForLineInput) 54 | engine.saveRequested = 55 | (quetzal, cb) => { 56 | if (!this.canSaveGames) { return cb(false); } 57 | this.waitState(cb, EngineState.waitingForGameSavedConfirmation) 58 | this.gameBeingSaved = quetzal 59 | } 60 | engine.loadRequested = 61 | (cb) => { 62 | if (!this.canSaveGames) { return cb(null); } 63 | this.waitState(cb, EngineState.waitingForLoadSaveGame); 64 | } 65 | } 66 | 67 | /** 68 | * convenience method to construct from an ArrayBuffer 69 | */ 70 | static loadFromArrayBuffer(arrayBuffer: ArrayBuffer, canSaveGames: boolean = false) : EngineWrapper { 71 | let image = new FyreVM.MemoryAccess(0,0) 72 | image.buffer = new Uint8Array(arrayBuffer) 73 | image['maxSize'] = arrayBuffer.byteLength 74 | return new EngineWrapper(image, canSaveGames) 75 | } 76 | 77 | /** 78 | * convenience method to construct from a FileReaderEvent 79 | * (which is supposed to have been successful) 80 | */ 81 | static loadFromFileReaderEvent(ev, canSaveGames: boolean = false) : EngineWrapper { 82 | return EngineWrapper.loadFromArrayBuffer(ev.target['result'], canSaveGames) 83 | } 84 | 85 | /** 86 | * convenience method to construct from a Base64 encoded string containing the game image 87 | */ 88 | static loadFromBase64(base64: string, canSaveGames: boolean = false) : EngineWrapper { 89 | return EngineWrapper.loadFromArrayBuffer(Base64.toByteArray(base64).buffer, canSaveGames) 90 | } 91 | 92 | 93 | 94 | // when the engine returns from processing 95 | // (because it is waiting for more input) 96 | // it will have invoked one of several callbacks 97 | // we use these to calculate the EngineState 98 | // and store the callback used to resume processing 99 | 100 | private resumeCallback; 101 | 102 | private engineState: EngineState; 103 | 104 | private channelData: ChannelData; 105 | 106 | private gameBeingSaved: Quetzal; 107 | 108 | private waitState(resumeCallback, state: EngineState){ 109 | this.resumeCallback = resumeCallback 110 | this.engineState = state 111 | } 112 | 113 | 114 | run() : EngineWrapperState{ 115 | this.engineState=EngineState.running; 116 | this.engine.run(); 117 | return this.currentState(); 118 | } 119 | 120 | private currentState() : EngineWrapperState { 121 | // check if the game is over 122 | if (this.engineState === EngineState.running 123 | && ! this.engine['running'] ){ 124 | this.engineState = EngineState.completed; 125 | } 126 | 127 | switch(this.engineState){ 128 | case EngineState.completed: 129 | case EngineState.waitingForKeyInput: 130 | case EngineState.waitingForLineInput: 131 | return { 132 | state: this.engineState, 133 | channelData: this.channelData, 134 | } 135 | case EngineState.waitingForGameSavedConfirmation: 136 | return { 137 | state: this.engineState, 138 | gameBeingSaved: this.gameBeingSaved 139 | } 140 | case EngineState.waitingForLoadSaveGame: 141 | return { 142 | state: this.engineState 143 | } 144 | default: 145 | console.error(`Unexpected engine state: ${this.engineState}`) 146 | return { 147 | state: this.engineState 148 | } 149 | } 150 | } 151 | 152 | receiveLine(line: string) : EngineWrapperState{ 153 | if (this.engineState !== EngineState.waitingForLineInput) 154 | throw new Error("Illegal state, engine is not waiting for line input"); 155 | this.engineState = EngineState.running; 156 | this.resumeCallback(line); 157 | return this.currentState(); 158 | } 159 | 160 | receiveKey(line: string) : EngineWrapperState{ 161 | if (this.engineState !== EngineState.waitingForKeyInput) 162 | throw new Error("Illegal state, engine is not waiting for key input"); 163 | 164 | this.engineState = EngineState.running; 165 | this.resumeCallback(line); 166 | return this.currentState(); 167 | } 168 | 169 | receiveSavedGame(quetzal: Quetzal): EngineWrapperState{ 170 | if (this.engineState !== EngineState.waitingForLoadSaveGame) 171 | throw new Error("Illegal state, engine is not waiting for a saved game to be loaded"); 172 | 173 | this.engineState = EngineState.running; 174 | this.resumeCallback(quetzal); 175 | return this.currentState(); 176 | } 177 | 178 | saveGameDone(success: boolean) : EngineWrapperState{ 179 | if (this.engineState !== EngineState.waitingForGameSavedConfirmation) 180 | throw new Error("Illegal state, engine is not waiting for a game to be saved"); 181 | 182 | this.gameBeingSaved = null; 183 | this.engineState = EngineState.running; 184 | this.resumeCallback(success); 185 | return this.currentState(); 186 | } 187 | 188 | getIFhd(): Uint8Array{ 189 | return this.engine['image']['memory'].copy(0, 128).buffer; 190 | } 191 | 192 | getUndoState() : Quetzal { 193 | let undoBuffers = this.engine['undoBuffers']; 194 | if (undoBuffers && undoBuffers[undoBuffers.length-1]){ 195 | return undoBuffers[undoBuffers.length-1]; 196 | } 197 | return null; 198 | } 199 | 200 | 201 | /** 202 | * convenience method to run "restore" and then 203 | * feed it the given savegame 204 | */ 205 | restoreSaveGame(quetzal: Quetzal) : EngineWrapperState{ 206 | let state = this.receiveLine("restore") 207 | if (state.state !== EngineState.waitingForLoadSaveGame) 208 | throw new Error("Illegal state, engine did not respond to RESTORE command"); 209 | return this.receiveSavedGame(quetzal) 210 | } 211 | 212 | /** 213 | * convenience method to run "save" 214 | */ 215 | saveGame() : Quetzal { 216 | let state = this.receiveLine("save") 217 | if (state.state !== EngineState.waitingForGameSavedConfirmation) 218 | throw new Error("Illegal state, engine did not respond to SAVE command"); 219 | let game = state.gameBeingSaved 220 | this.saveGameDone(true) 221 | return game 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /core/UlxImage.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | 8 | /** 9 | * Represents the ROM and RAM of a Glulx game image. 10 | */ 11 | 12 | module FyreVM { 13 | 14 | // Header size and field offsets 15 | const enum GLULX_HDR { 16 | SIZE = 36, 17 | MAGIC_OFFSET = 0, 18 | VERSION_OFFSET = 4, 19 | RAMSTART_OFFSET = 8, 20 | EXTSTART_OFFSET = 12, 21 | ENDMEM_OFFSET = 16, 22 | STACKSIZE_OFFSET = 20, 23 | STARTFUNC_OFFSET = 24, 24 | DECODINGTBL_OFFSET = 28, 25 | CHECKSUM_OFFSET = 32 26 | }; 27 | 28 | export interface GlulxHeader { 29 | magic?: string; 30 | version?: number; 31 | ramStart?: number; 32 | extStart?: number; 33 | endMem? : number; 34 | stackSize?: number; 35 | startFunc?: number; 36 | decodingTbl?: number; 37 | checksum?: number; 38 | } 39 | 40 | 41 | 42 | export class UlxImage{ 43 | 44 | private memory: MemoryAccess; 45 | private ramstart: number; 46 | private original: MemoryAccess; 47 | 48 | constructor(original: MemoryAccess){ 49 | this.original = original; 50 | this.loadFromOriginal(); 51 | } 52 | 53 | private loadFromOriginal(){ 54 | let stream = this.original; 55 | // read the header, to find out how much memory we need 56 | let header = stream.copy(0, GLULX_HDR.SIZE); 57 | let magic = header.readASCII(0, 4); 58 | if (magic !== 'Glul'){ 59 | throw new Error(`.ulx file has wrong magic number ${magic}`); 60 | } 61 | 62 | let endmem = header.readInt32(GLULX_HDR.ENDMEM_OFFSET); 63 | if (endmem < GLULX_HDR.SIZE){ 64 | throw new Error(`invalid endMem ${endmem} in .ulx file. Too small to even fit the header.`); 65 | } 66 | // now read the whole thing 67 | this.memory = stream.copy(0, endmem); 68 | // TODO: verify checksum 69 | this.ramstart = header.readInt32(GLULX_HDR.RAMSTART_OFFSET); 70 | if (this.ramstart > endmem){ 71 | throw new Error(`invalid ramStart ${this.ramstart} beyond endMem ${endmem}.`); 72 | } 73 | } 74 | 75 | getMajorVersion(): number{ 76 | return this.memory.readInt16(GLULX_HDR.VERSION_OFFSET); 77 | } 78 | 79 | getMinorVersion(): number{ 80 | return this.memory.readInt16(GLULX_HDR.VERSION_OFFSET+2) >> 8; 81 | } 82 | 83 | getStackSize(): number { 84 | return this.memory.readInt32(GLULX_HDR.STACKSIZE_OFFSET); 85 | } 86 | 87 | getEndMem(): number { 88 | return this.memory.size(); 89 | } 90 | 91 | getRamAddress(relativeAddress: number): number{ 92 | return this.ramstart + relativeAddress; 93 | } 94 | 95 | /** 96 | * sets the address at which memory ends. 97 | * This can be changed by the game with setmemsize, 98 | * or managed automatically be the heap allocator. 99 | */ 100 | setEndMem(value: number){ 101 | // round up to the next multiple of 256 102 | if (value % 256 != 0){ 103 | value = (value + 255) & 0xFFFFFF00; 104 | } 105 | if (this.memory.size() != value){ 106 | this.memory = this.memory.copy(0, value); 107 | } 108 | } 109 | 110 | getStartFunc(): number { 111 | return this.memory.readInt32(GLULX_HDR.STARTFUNC_OFFSET); 112 | } 113 | 114 | getDecodingTable(): number { 115 | return this.memory.readInt32(GLULX_HDR.DECODINGTBL_OFFSET); 116 | } 117 | 118 | saveToQuetzal(): Quetzal { 119 | let quetzal = new Quetzal(); 120 | // 'IFhd' identifies the first 128 bytes of the game file 121 | quetzal.setChunk('IFhd', this.original.copy(0, 128).buffer); 122 | // 'CMem' or 'UMem' are the compressed/uncompressed contents of RAM 123 | // TODO: implement compression 124 | let ramSize = this.getEndMem() - this.ramstart; 125 | let umem = new MemoryAccess(ramSize+4); 126 | umem.writeInt32(0, ramSize); 127 | umem.buffer.set(new Uint8Array(this.memory.buffer).subarray(this.ramstart, this.ramstart+ramSize), 4); 128 | quetzal.setChunk("UMem", umem.buffer); 129 | return quetzal; 130 | } 131 | 132 | readByte(address: number) : number { 133 | return this.memory.readByte(address); 134 | } 135 | 136 | readInt16(address: number) : number { 137 | return this.memory.readInt16(address); 138 | } 139 | 140 | readInt32(address: number) : number { 141 | return this.memory.readInt32(address); 142 | } 143 | 144 | readCString(address: number): string { 145 | return this.memory.readCString(address); 146 | } 147 | 148 | writeInt32(address: number, value: number) { 149 | if (address < this.ramstart) 150 | throw new Error(`Writing into ROM! offset: ${address}`); 151 | this.memory.writeInt32(address, value); 152 | } 153 | 154 | writeBytes(address: number, ...bytes: number[]){ 155 | if (address < this.ramstart) 156 | throw new Error(`Writing into ROM! offset: ${address}`); 157 | for (let i=0; i>8, value & 0xFF); 169 | return; 170 | default: 171 | this.writeInt32(address, value); 172 | } 173 | } 174 | 175 | /** 176 | * @param limit: the maximum number of bytes to write 177 | * returns the number of bytes written 178 | */ 179 | writeASCII(address: number, text: string, limit: number): number{ 180 | let bytes = []; 181 | for (let i=0; i 255){ 184 | c = 63; // '?' 185 | } 186 | bytes.push(c); 187 | } 188 | this.writeBytes(address, ...bytes); 189 | return bytes.length; 190 | } 191 | 192 | static writeHeader(fields: GlulxHeader, m: MemoryAccess, offset=0){ 193 | m.writeASCII(offset, fields.magic || 'Glul'); 194 | m.writeInt32(offset + GLULX_HDR.VERSION_OFFSET, fields.version); 195 | m.writeInt32(offset + GLULX_HDR.RAMSTART_OFFSET, fields.ramStart); 196 | m.writeInt32(offset + GLULX_HDR.EXTSTART_OFFSET, fields.extStart); 197 | m.writeInt32(offset + GLULX_HDR.ENDMEM_OFFSET, fields.endMem); 198 | m.writeInt32(offset + GLULX_HDR.STACKSIZE_OFFSET, fields.stackSize); 199 | m.writeInt32(offset + GLULX_HDR.STARTFUNC_OFFSET, fields.startFunc); 200 | m.writeInt32(offset + GLULX_HDR.DECODINGTBL_OFFSET, fields.decodingTbl); 201 | m.writeInt32(offset + GLULX_HDR.CHECKSUM_OFFSET, fields.checksum); 202 | } 203 | 204 | 205 | /** Reloads the game file, discarding all changes that have been made 206 | * to RAM and restoring the memory map to its original size. 207 | * 208 | * Use the optional "protection" parameters to preserve a RAM region 209 | */ 210 | revert(protectionStart=0, protectionLength=0){ 211 | let prot = this.copyProtectedRam(protectionStart, protectionLength); 212 | this.loadFromOriginal(); 213 | if (prot){ 214 | let d = []; 215 | for(let i=0; i 0){ 225 | if (protectionStart + protectionLength > this.getEndMem()){ 226 | protectionLength = this.getEndMem() - protectionStart; 227 | } 228 | // can only protect RAM 229 | let start = protectionStart - this.ramstart; 230 | if (start < 0){ 231 | protectionLength += start; 232 | start = 0; 233 | } 234 | prot = this.memory.copy(start + this.ramstart, protectionLength); 235 | } 236 | return prot; 237 | } 238 | 239 | restoreFromQuetzal(quetzal: Quetzal, protectionStart=0, protectionLength=0){ 240 | // TODO: support compressed RAM 241 | let newRam = quetzal.getChunk('UMem'); 242 | if (newRam){ 243 | let prot = this.copyProtectedRam(protectionStart, protectionLength); 244 | 245 | let r = new MemoryAccess(0); 246 | r.buffer= new Uint8Array(newRam); 247 | let length = r.readInt32(0); 248 | this.setEndMem(length + this.ramstart); 249 | let i=4; 250 | let j=this.ramstart; 251 | while(i= size){ 107 | result.offset = entry.offset; 108 | if (entry.length > size){ 109 | // keep the rest in the free list 110 | entry.offset += size; 111 | entry.length -= size; 112 | }else{ 113 | freeList[i] = null; 114 | } 115 | break; 116 | } 117 | } 118 | if (result.offset === -1){ 119 | // enforce max heap size 120 | if (this.maxHeapExtent && this.heapExtent + size > this.maxHeapExtent){ 121 | return null; 122 | } 123 | // add a new block 124 | result.offset = this.heapAddress + this.heapExtent; 125 | if (result.offset + size > this.endMem){ 126 | // grow the heap 127 | let newHeapAllocation = Math.max( 128 | this.heapExtent * 5 / 4, this.heapExtent + size); 129 | if (this.maxHeapExtent){ 130 | newHeapAllocation = Math.min(newHeapAllocation, this.maxHeapExtent); 131 | } 132 | 133 | if (! this.setEndMem(newHeapAllocation)){ 134 | return null; 135 | } 136 | } 137 | 138 | this.heapExtent += size; 139 | } 140 | 141 | // TODO: keep the list sorted 142 | blocks.push(result); 143 | 144 | return result.offset; 145 | } 146 | 147 | private setEndMem(newHeapAllocation: number) : boolean{ 148 | let newEndMem = this.heapAddress + newHeapAllocation; 149 | if (this.memory.setEndMem(newEndMem)){ 150 | this.endMem = newEndMem; 151 | return true; 152 | } 153 | return false; 154 | } 155 | 156 | blockCount() : number { 157 | return this.blocks.length; 158 | } 159 | 160 | /** 161 | * deallocates a previously allocated block 162 | */ 163 | free(address: number){ 164 | let {blocks, freeList} = this; 165 | // find the block 166 | for(let i=0; i 0 && this.heapExtent <= (this.endMem - this.heapAddress) / 2){ 187 | if (this.setEndMem(this.heapExtent)){ 188 | var newEndMem = this.endMem; 189 | for(let i=0; i= newEndMem){ 192 | freeList[i] = null; 193 | } 194 | } 195 | } 196 | } 197 | 198 | return; 199 | } 200 | } 201 | } 202 | } 203 | 204 | /** 205 | * Wrapper around ECMAScript 6 standard Uint8Array. 206 | * Provides access to a memory buffer. 207 | */ 208 | export class MemoryAccess { 209 | 210 | public buffer: Uint8Array; 211 | 212 | private maxSize: number; 213 | 214 | constructor(size: number, maxSize=size){ 215 | this.buffer = new Uint8Array(size); 216 | this.maxSize = maxSize; 217 | } 218 | 219 | /** 220 | * Reads a single byte (unsigned) 221 | */ 222 | readByte(offset: number){ 223 | return this.buffer[offset]; 224 | } 225 | 226 | /** 227 | * Writes a single byte (unsigned). 228 | * Writes 0 when value is undefined or null. 229 | */ 230 | writeByte(offset: number, value:number){ 231 | if (value < 0 || value > 255) 232 | throw new Error(`${value} is out of range for a byte`); 233 | this.buffer[offset] = value; 234 | } 235 | 236 | /** 237 | * Reads an unsigned, big-endian, 16-bit number 238 | */ 239 | readInt16(offset: number){ 240 | return (this.buffer[offset] * 256) + this.buffer[offset+1]; 241 | } 242 | 243 | // TypeScript does not like us calling "set" with an array directly 244 | private set(offset: number, value: any){ 245 | this.buffer.set(value, offset); 246 | } 247 | 248 | /** 249 | * Writes an unsigned, big-endian, 16-bit number. 250 | * Writes 0 when value is undefined or null. 251 | */ 252 | writeInt16(offset: number, value: number){ 253 | if (value < 0 || value > 0xFFFF) 254 | throw new Error(`${value} is out of range for uint16`); 255 | this.set(offset, [value >> 8, value & 0xFF]); 256 | } 257 | 258 | /** 259 | * Reads an unsigned, big-endian, 32-bit number 260 | */ 261 | readInt32(offset: number){ 262 | return this.buffer[offset] * 0x1000000 263 | + this.buffer[offset+1] * 0x10000 264 | + this.buffer[offset+2] * 0x100 265 | + this.buffer[offset+3]; 266 | } 267 | 268 | /** 269 | * Writes an unsigned, big-endian, 32-bit number 270 | * Writes 0 when value is undefined or null. 271 | */ 272 | writeInt32(offset: number, value: number){ 273 | value = value >>> 0; 274 | this.set(offset, [ value >> 24, value >> 16 & 0xFF, value >> 8 & 0xFF, value & 0xFF]) 275 | } 276 | 277 | /** 278 | * Converts part of the buffer into a String, 279 | * assumes that the data is valid ASCII 280 | */ 281 | readASCII(offset: number, length: number): string{ 282 | let len = 0, {buffer} = this, d = []; 283 | while(len < length){ 284 | let x = buffer[offset+len]; 285 | len++; 286 | d.push(x); 287 | } 288 | return String.fromCharCode(...d); 289 | } 290 | 291 | /** 292 | * reads a 0-terminated C-string 293 | */ 294 | readCString(offset:number): string{ 295 | let len = 0, {buffer} = this, d = []; 296 | while(true){ 297 | let x = buffer[offset+len]; 298 | if (x === 0) 299 | break; 300 | len++; 301 | d.push(x); 302 | } 303 | return String.fromCharCode(...d); 304 | } 305 | 306 | /** 307 | * Writes an ASCII String 308 | */ 309 | writeASCII(offset: number, value: string){ 310 | let codes = []; 311 | for (let i=0; i this.maxSize) 322 | return false; 323 | return true; 324 | } 325 | 326 | /** 327 | * Copy a part of the memory into a new buffer. 328 | * 329 | * The length can be more than there is data 330 | * in the original buffer. In this case the 331 | * new buffer will contain unspecified data 332 | * at the end. 333 | */ 334 | copy(offset: number, length: number) : MemoryAccess { 335 | // TODO: range check 336 | if (length > this.maxSize) 337 | throw new Error(`Memory request for ${length} bytes exceeds limit of ${this.maxSize}`); 338 | let result = new MemoryAccess(length); 339 | result.buffer.set(this.buffer.subarray(offset, offset+length)); 340 | result.maxSize = this.maxSize; 341 | return result; 342 | } 343 | 344 | /** 345 | * returns the number of bytes available 346 | */ 347 | size(){ 348 | return this.buffer.length; 349 | } 350 | 351 | } 352 | 353 | 354 | } 355 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /core/Veneer.ts: -------------------------------------------------------------------------------- 1 | // Written from 2015 to 2016 by Thilo Planz and Andrew Plotkin 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /** 7 | * Provides hardcoded versions of some commonly used veneer routines (low-level 8 | * functions that are automatically compiled into every Inform game). 9 | * Inform games rely heavily on these routines, and substituting our "native" versions 10 | * for the Glulx versions in the story file can increase performance significantly. 11 | */ 12 | 13 | /// 14 | 15 | module FyreVM { 16 | 17 | /// Identifies a veneer routine that is intercepted, or a constant that 18 | /// the replacement routine needs to use. 19 | export const enum VeneerSlot 20 | { 21 | // routine addresses 22 | Z__Region = 1, 23 | CP__Tab = 2, 24 | OC__Cl = 3, 25 | RA__Pr = 4, 26 | RT__ChLDW = 5, 27 | Unsigned__Compare = 6, 28 | RL__Pr = 7, 29 | RV__Pr = 8, 30 | OP__Pr = 9, 31 | RT__ChSTW = 10, 32 | RT__ChLDB = 11, 33 | Meta__class = 12, 34 | 35 | // object numbers and compiler constants 36 | String = 1001, 37 | Routine = 1002, 38 | Class = 1003, 39 | Object = 1004, 40 | RT__Err = 1005, 41 | NUM_ATTR_BYTES = 1006, 42 | classes_table = 1007, 43 | INDIV_PROP_START = 1008, 44 | cpv__start = 1009, 45 | ofclass_err = 1010, 46 | readprop_err = 1011, 47 | } 48 | 49 | 50 | // RAM addresses of compiler-generated global variables 51 | let SELF_OFFSET = 16; 52 | let SENDER_OFFSET = 20; 53 | 54 | // offsets of compiler-generated property numbers from INDIV_PROP_START 55 | let CALL_PROP = 5; 56 | let PRINT_PROP = 6; 57 | let PRINT_TO_ARRAY_PROP = 7; 58 | 59 | 60 | 61 | interface Veneer { 62 | string_mc : number, 63 | routine_mc : number, 64 | class_mc : number, 65 | object_mc : number, 66 | num_attr_bytes : number, 67 | classes_table: number, 68 | indiv_prop_start : number, 69 | cpv_start : number 70 | } 71 | 72 | /** 73 | * Registers a routine address or constant value, using the acceleration 74 | * codes defined in the Glulx specification. 75 | */ 76 | 77 | export function setSlotGlulx(isParam: boolean, slot: number, value) : boolean { 78 | if (isParam && slot === 6){ 79 | let image: UlxImage = this.image; 80 | if (value != image.getRamAddress(SELF_OFFSET)){ 81 | throw new Error("Unexpected value for acceleration parameter 6"); 82 | } 83 | return true; 84 | } 85 | if (isParam){ 86 | switch(slot){ 87 | case 0: return setSlotFyre.call(this, VeneerSlot.classes_table, value); 88 | case 1: return setSlotFyre.call(this, VeneerSlot.INDIV_PROP_START, value); 89 | case 2: return setSlotFyre.call(this, VeneerSlot.Class, value); 90 | case 3: return setSlotFyre.call(this, VeneerSlot.Object, value); 91 | case 4: return setSlotFyre.call(this, VeneerSlot.Routine, value); 92 | case 5: return setSlotFyre.call(this, VeneerSlot.String, value); 93 | case 7: return setSlotFyre.call(this, VeneerSlot.NUM_ATTR_BYTES, value); 94 | case 8: return setSlotFyre.call(this, VeneerSlot.cpv__start, value); 95 | default: return false; 96 | } 97 | } 98 | switch(slot){ 99 | case 1: return setSlotFyre.call(this, VeneerSlot.Z__Region, value); 100 | case 2: return setSlotFyre.call(this, VeneerSlot.CP__Tab, value); 101 | case 3: return setSlotFyre.call(this, VeneerSlot.RA__Pr, value); 102 | case 4: return setSlotFyre.call(this, VeneerSlot.RL__Pr, value); 103 | case 5: return setSlotFyre.call(this, VeneerSlot.OC__Cl, value); 104 | case 6: return setSlotFyre.call(this, VeneerSlot.RV__Pr, value); 105 | case 7: return setSlotFyre.call(this, VeneerSlot.OP__Pr, value); 106 | default: return false; 107 | 108 | } 109 | } 110 | 111 | 112 | /** 113 | * Registers a routine address or constant value, using the traditional 114 | * FyreVM slot codes. 115 | */ 116 | 117 | export function setSlotFyre(slot:VeneerSlot, value) : boolean{ 118 | let v : Veneer = this.veneer; 119 | switch(slot){ 120 | case VeneerSlot.Z__Region: this.veneer[value] = Z__Region; return true; 121 | case VeneerSlot.CP__Tab: this.veneer[value] = CP__Tab; return true; 122 | case VeneerSlot.OC__Cl: this.veneer[value] = OC__Cl; return true; 123 | case VeneerSlot.RA__Pr: this.veneer[value] = RA__Pr; return true; 124 | case VeneerSlot.RT__ChLDW: this.veneer[value] = RT__ChLDW; return true; 125 | case VeneerSlot.Unsigned__Compare: this.veneer[value] = Unsigned__Compare; return true; 126 | case VeneerSlot.RL__Pr: this.veneer[value] = RL__Pr; return true; 127 | case VeneerSlot.RV__Pr: this.veneer[value] = RV__Pr; return true; 128 | case VeneerSlot.OP__Pr: this.veneer[value] = OP__Pr; return true; 129 | case VeneerSlot.RT__ChSTW: this.veneer[value] = RT__ChSTW; return true; 130 | case VeneerSlot.RT__ChLDB: this.veneer[value] = RT__ChLDB; return true; 131 | case VeneerSlot.Meta__class: this.veneer[value] = Meta__class; return true; 132 | 133 | case VeneerSlot.String: v.string_mc = value; return true; 134 | case VeneerSlot.Routine: v.routine_mc = value; return true; 135 | case VeneerSlot.Class: v.class_mc = value; return true; 136 | case VeneerSlot.Object: v.object_mc = value; return true; 137 | case VeneerSlot.NUM_ATTR_BYTES: v.num_attr_bytes = value; return true; 138 | case VeneerSlot.classes_table: v.classes_table = value; return true; 139 | case VeneerSlot.INDIV_PROP_START: v.indiv_prop_start = value; return true; 140 | case VeneerSlot.cpv__start: v.cpv_start = value; return true; 141 | 142 | // run-time error handlers are just ignored (we log an error message instead, like Quixe does, no NestedCall a la FyreVM) 143 | case VeneerSlot.RT__Err: 144 | case VeneerSlot.ofclass_err: 145 | case VeneerSlot.readprop_err: 146 | return true; 147 | 148 | default: 149 | console.warn(`ignoring veneer ${slot} ${value}`); 150 | return false; 151 | 152 | } 153 | } 154 | 155 | function Unsigned__Compare(a,b){ 156 | a = a >>> 0; 157 | b = b >>> 0; 158 | if (a > b) return 1; 159 | if (a < b) return -1; 160 | return 0; 161 | } 162 | 163 | 164 | // distinguishes between strings, routines, and objects 165 | function Z__Region(address:number) : number{ 166 | let image: UlxImage = this.image; 167 | if (address < 36 || address >= image.getEndMem()) 168 | return 0; 169 | 170 | let type = image.readByte(address); 171 | if (type >= 0xE0) return 3; 172 | if (type >= 0xC0) return 2; 173 | if (type >= 0x70 && type <= 0x7F && address >= image.getRamAddress(0)) return 1; 174 | return 0; 175 | } 176 | 177 | 178 | // finds an object's common property table 179 | function CP__Tab(obj,id) : number 180 | { 181 | if (Z__Region.call(this, obj) != 1) 182 | { 183 | // error "handling" inspired by Quixe 184 | // instead of doing a NestedCall to the supplied error handler 185 | // just log an error message 186 | console.error("[** Programming error: tried to find the \".\" of (something) **]"); 187 | return 0; 188 | } 189 | let image: UlxImage = this.image; 190 | 191 | let otab = image.readInt32(obj + 16); 192 | if (otab == 0) 193 | return 0; 194 | let max = image.readInt32(otab); 195 | otab += 4; 196 | // PerformBinarySearch 197 | return this.opcodes[0x151].handler.call(this, id, 2, otab, 10, max, 0, 0); 198 | } 199 | 200 | 201 | // finds the location of an object ("parent()" function) 202 | function Parent(obj){ 203 | return this.image.readInt32(obj + 1 + this.veneer.num_attr_bytes + 12); 204 | } 205 | 206 | // determines whether an object is a member of a given class ("ofclass" operator) 207 | function OC__Cl(obj, cla) 208 | { 209 | let v : Veneer = this.veneer; 210 | 211 | switch (Z__Region.call(this, obj)) 212 | { 213 | case 3: 214 | return (cla === v.string_mc ? 1 : 0); 215 | 216 | case 2: 217 | return (cla === v.routine_mc ? 1 : 0); 218 | 219 | case 1: 220 | if (cla === v.class_mc) 221 | { 222 | if (Parent.call(this, obj) === v.class_mc) 223 | return 1; 224 | if (obj === v.class_mc || obj === v.string_mc || 225 | obj === v.routine_mc || obj === v.object_mc) 226 | return 1; 227 | return 0; 228 | } 229 | 230 | if (cla == this.veneer.object_mc) 231 | { 232 | if (Parent.call(this, obj) == v.class_mc) 233 | return 0; 234 | if (obj == v.class_mc || obj == v.string_mc || 235 | obj == v.routine_mc || obj == v.object_mc) 236 | return 0; 237 | return 1; 238 | } 239 | 240 | if (cla == v.string_mc || cla == v.routine_mc) 241 | return 0; 242 | 243 | if (Parent.call(this, cla) != v.class_mc) 244 | { 245 | console.error("[** Programming error: tried to apply 'ofclass' with non-class **]") 246 | return 0; 247 | } 248 | 249 | let image: UlxImage = this.image; 250 | 251 | let inlist = RA__Pr.call(this, obj, 2); 252 | if (inlist == 0) 253 | return 0; 254 | 255 | let inlistlen = RL__Pr.call(this, obj, 2) / 4; 256 | for (let jx = 0; jx < inlistlen; jx++) 257 | if (image.readInt32(inlist + jx * 4) === cla) 258 | return 1; 259 | 260 | return 0; 261 | 262 | default: 263 | return 0; 264 | } 265 | } 266 | 267 | // finds the address of an object's property (".&" operator) 268 | function RA__Pr(obj, id) : number 269 | { 270 | let cla = 0; 271 | let image: UlxImage = this.image; 272 | 273 | if ((id & 0xFFFF0000) != 0) 274 | { 275 | cla = image.readInt32(this.veneer.classes_table + 4 * (id & 0xFFFF)); 276 | if (OC__Cl.call(this, obj, cla) == 0) 277 | return 0; 278 | 279 | id >>= 16; 280 | obj = cla; 281 | } 282 | 283 | let prop = CP__Tab.call(this, obj, id); 284 | if (prop == 0) 285 | return 0; 286 | 287 | if (Parent.call(this, obj) === this.veneer.class_mc && cla == 0) 288 | if (id < this.veneer.indiv_prop_start || id >= this.veneer.indiv_prop_start + 8) 289 | return 0; 290 | 291 | if (image.readInt32(image.getRamAddress(SELF_OFFSET)) != obj) 292 | { 293 | let ix = (image.readByte(prop + 9) & 1); 294 | if (ix != 0) 295 | return 0; 296 | } 297 | 298 | return image.readInt32(prop + 4); 299 | } 300 | 301 | 302 | // finds the length of an object's property (".#" operator) 303 | function RL__Pr(obj, id): number 304 | { 305 | let cla = 0; 306 | let image: UlxImage = this.image; 307 | 308 | if ((id & 0xFFFF0000) != 0) 309 | { 310 | cla = image.readInt32(this.veneer.classes_table + 4 * (id & 0xFFFF)); 311 | if (OC__Cl.call(this, obj, cla) == 0) 312 | return 0; 313 | 314 | id >>= 16; 315 | obj = cla; 316 | } 317 | 318 | let prop = CP__Tab.call(this, obj, id); 319 | if (prop == 0) 320 | return 0; 321 | 322 | if (Parent.call(this, obj) == this.veneer.class_mc && cla == 0) 323 | if (id < this.veneer.indiv_prop_start || id >= this.veneer.indiv_prop_start + 8) 324 | return 0; 325 | 326 | if (image.readInt32(image.getRamAddress(SELF_OFFSET)) != obj) 327 | { 328 | let ix = (image.readByte(prop + 9) & 1); 329 | if (ix != 0) 330 | return 0; 331 | } 332 | 333 | return 4 * image.readInt16(prop + 2); 334 | } 335 | 336 | 337 | // performs bounds checking when reading from a word array ("-->" operator) 338 | function RT__ChLDW(array, offset) 339 | { 340 | let address = array + 4 * offset; 341 | let image: UlxImage = this.image; 342 | 343 | if (address >= image.getEndMem()) 344 | { 345 | console.error("[** Programming error: tried to read from word array beyond EndMem **]"); 346 | return 0; 347 | } 348 | return image.readInt32(address); 349 | } 350 | 351 | 352 | // reads the value of an object's property ("." operator) 353 | function RV__Pr(obj, id) 354 | { 355 | 356 | let addr = RA__Pr.call(this, obj, id); 357 | let image: UlxImage = this.image; 358 | 359 | if (addr == 0) 360 | { 361 | let v : Veneer = this.veneer; 362 | 363 | if (id > 0 && id < v.indiv_prop_start) 364 | return image.readInt32(v.cpv_start + 4 * id); 365 | 366 | console.error("[** Programming error: tried to read (something) **]"); 367 | return 0; 368 | } 369 | 370 | return image.readInt32(addr); 371 | } 372 | 373 | 374 | // determines whether an object provides a given property ("provides" operator) 375 | function OP__Pr(obj, id) 376 | { 377 | let v : Veneer = this.veneer; 378 | switch (Z__Region.call(this, obj)) 379 | { 380 | case 3: 381 | if (id == v.indiv_prop_start + PRINT_PROP || 382 | id == v.indiv_prop_start + PRINT_TO_ARRAY_PROP) 383 | return 1; 384 | else 385 | return 0; 386 | 387 | case 2: 388 | if (id == v.indiv_prop_start + CALL_PROP) 389 | return 1; 390 | else 391 | return 0; 392 | 393 | case 1: 394 | if (id >= v.indiv_prop_start && id < v.indiv_prop_start + 8) 395 | if (Parent.call(this, obj) == v.class_mc) 396 | return 1; 397 | 398 | if (RA__Pr.call(this, obj, id) != 0) 399 | return 1; 400 | else 401 | return 0; 402 | 403 | default: 404 | return 0; 405 | } 406 | } 407 | 408 | // performs bounds checking when writing to a word array ("-->" operator) 409 | function RT__ChSTW(array, offset, val) 410 | { 411 | let image: UlxImage = this.image; 412 | let address = array + 4 * offset; 413 | if (address >= image.getEndMem() || address < image.getRamAddress(0)) 414 | { 415 | console.error("[** Programming error: tried to write to word array outside of RAM **]"); 416 | return 0; 417 | } 418 | else 419 | { 420 | image.writeInt32(address, val); 421 | return 0; 422 | } 423 | } 424 | 425 | // performs bounds checking when reading from a byte array ("->" operator) 426 | function RT__ChLDB(array, offset) 427 | { 428 | let address = array + offset; 429 | let image: UlxImage = this.image; 430 | 431 | if (address >= image.getEndMem()){ 432 | console.error("[** Programming error: tried to read from byte array beyond EndMem **]"); 433 | return 0; 434 | } 435 | 436 | return image.readByte(address); 437 | } 438 | 439 | 440 | // determines the metaclass of a routine, string, or object ("metaclass()" function) 441 | function Meta__class(obj) : number 442 | { 443 | switch (Z__Region.call(this, obj)) 444 | { 445 | case 2: 446 | return this.veneer.routine_mc; 447 | case 3: 448 | return this.veneer.string_mc; 449 | case 1: 450 | if (Parent.call(this,obj) === this.veneer.class_mc) 451 | return this.veneer.class_mc; 452 | if (obj == this.veneer.class_mc || obj == this.veneer.string_mc || 453 | obj == this.veneer.routine_mc || obj == this.veneer.object_mc) 454 | return this.veneer.class_mc; 455 | return this.veneer.object_mc; 456 | default: 457 | return 0; 458 | } 459 | } 460 | 461 | 462 | 463 | 464 | } -------------------------------------------------------------------------------- /core/Opcodes.ts: -------------------------------------------------------------------------------- 1 | // Written in 2015 by Thilo Planz and Andrew Plotkin 2 | // To the extent possible under law, I have dedicated all copyright and related and neighboring rights 3 | // to this software to the public domain worldwide. This software is distributed without any warranty. 4 | // http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | /// 7 | /// 8 | /// 9 | 10 | module FyreVM { 11 | 12 | /** 13 | * an OpcodeHandler takes any number of arguments (all numbers) 14 | * and returns nothing, or a number, or multiple numbers 15 | */ 16 | export interface OpcodeHandler{ 17 | (...any:number[]) : void | number | number[] 18 | } 19 | 20 | export class Opcode { 21 | code: number; 22 | name: string; 23 | loadArgs: number; 24 | storeArgs: number; 25 | handler:OpcodeHandler; 26 | rule:OpcodeRule; 27 | constructor(code: number, name: string, loadArgs: number, storeArgs: number, handler:OpcodeHandler, rule?:OpcodeRule){ 28 | this.code = code; 29 | this.name = name; 30 | this.loadArgs = loadArgs; 31 | this.storeArgs = storeArgs; 32 | this.handler = handler; 33 | this.rule = rule; 34 | } 35 | } 36 | 37 | export const enum Gestalt { 38 | GlulxVersion = 0, 39 | TerpVersion = 1, 40 | ResizeMem = 2, 41 | Undo = 3, 42 | IOSystem = 4, 43 | Unicode = 5, 44 | MemCopy = 6, 45 | MAlloc = 7, 46 | MAllocHeap = 8, 47 | Acceleration = 9, 48 | AccelFunc = 10, 49 | Float = 11, 50 | 51 | /** 52 | * ExtUndo (12): 53 | * Returns 1 if the interpreter supports the hasundo and discardundo opcodes. 54 | * (This must true for any terp supporting Glulx 3.1.3. On a terp which does not support undo functionality, 55 | * these opcodes will be callable but will fail.) 56 | */ 57 | ExtUndo = 12 58 | } 59 | 60 | /// 61 | /// Selects a function for the FyreVM system call opcode. 62 | /// 63 | export const enum FyreCall 64 | { 65 | /// 66 | /// Reads a line from the user: args[1] = buffer, args[2] = buffer size. 67 | /// 68 | ReadLine = 1, 69 | /// 70 | /// Reads a character from the user: result = the 16-bit Unicode value. 71 | /// 72 | ReadKey = 2, 73 | /// 74 | /// Converts a character to lowercase: args[1] = the character, 75 | /// result = the lowercased character. 76 | /// 77 | ToLower = 3, 78 | /// 79 | /// Converts a character to uppercase: args[1] = the character, 80 | /// result = the uppercased character. 81 | /// 82 | ToUpper = 4, 83 | /// 84 | /// Selects an output channel: args[1] = an OutputChannel value (see Output.cs). 85 | /// 86 | Channel = 5, 87 | /// 88 | /// Registers a veneer function address or constant value: args[1] = a 89 | /// VeneerSlot value (see Veneer.cs), args[2] = the function address or 90 | /// constant value, result = nonzero if the value was accepted. 91 | /// 92 | SetVeneer = 6, 93 | /// XML Filtering will turn things into XAML tags for Silverlight or WPF. 94 | XMLFilter = 7, 95 | /// styles: { Roman = 1, Bold = 2, Italic = 3, Fixed = 4, Variable = 5,} 96 | SetStyle = 8 97 | } 98 | 99 | 100 | // coerce Javascript number into uint32 range 101 | function uint32(x:number) : number{ 102 | return x >>> 0; 103 | } 104 | 105 | // coerce uint32 number into (signed!) int32 range 106 | 107 | function int32(x: number) :number{ 108 | return x | 0; 109 | } 110 | 111 | 112 | export module Opcodes{ 113 | export function initOpcodes(){ 114 | let opcodes: Opcode[] = []; 115 | 116 | function opcode(code: number, name: string, loadArgs: number, storeArgs: number, handler:OpcodeHandler, rule?:OpcodeRule){ 117 | opcodes[code] = new Opcode(code, name, loadArgs, storeArgs, handler, rule); 118 | } 119 | 120 | opcode(0x00, 'nop', 0, 0, 121 | function(){ }); 122 | 123 | opcode(0x10, 'add', 2, 1, 124 | function add(a,b){ return uint32(a+b)}); 125 | 126 | opcode(0x11, 'sub', 2, 1, 127 | function sub(a,b){ return uint32(a-b)}); 128 | 129 | opcode(0x12, 'mul', 2, 1, 130 | function mul(a,b){ return uint32((Math).imul(int32(a),int32(b)))}); 131 | 132 | opcode(0x13, 'div', 2, 1, 133 | function div(a,b){ return uint32(int32(a) / int32(b))}); 134 | 135 | opcode(0x14, 'mod', 2, 1, 136 | function mod(a,b){ return uint32(int32(a) % int32(b))}); 137 | 138 | // TODO: check the specs 139 | opcode(0x15, 'neg', 1, 1, 140 | function neg(x){ 141 | return uint32(0xFFFFFFFF - x + 1)}); 142 | 143 | // TODO: check if it works, JS has signed ints, we want uint 144 | opcode(0x18, 'bitand', 2, 1, 145 | function bitand(a,b){ return uint32(uint32(a) & uint32(b))}); 146 | 147 | // TODO: check if it works, JS has signed ints, we want uint 148 | opcode(0x19, 'bitor', 2, 1, 149 | function bitor(a,b){ return uint32(uint32(a) | uint32(b))}); 150 | 151 | // TODO: check if it works, JS has signed ints, we want uint 152 | opcode(0x1A, 'bitxor', 2, 1, 153 | function bitxor(a,b){ return uint32(uint32(a) ^ uint32(b))}); 154 | 155 | // TODO: check if it works, JS has signed ints, we want uint 156 | opcode(0x1B, 'bitnot', 1, 1, 157 | function bitnot(x){ x = ~uint32(x); if (x<0) return 1 + x + 0xFFFFFFFF; return x; }); 158 | 159 | opcode(0x1C, 'shiftl', 2, 1, 160 | function shiftl(a,b){ 161 | if (uint32(b) >= 32) return 0; 162 | return uint32(a << b)}); 163 | 164 | opcode(0x1D, 'sshiftr', 2, 1, 165 | function sshiftr(a,b){ 166 | if (uint32(b) >= 32) return (a & 0x80000000) ? 0xFFFFFFFF : 0; 167 | return uint32(int32(a) >> b)}); 168 | 169 | opcode(0x1E, 'ushiftr', 2, 1, 170 | function ushiftr(a,b){ 171 | if (uint32(b) >= 32) return 0; 172 | return uint32(uint32(a) >>> b)}); 173 | 174 | 175 | opcode(0x20, 'jump', 1, 0, 176 | function jump(jumpVector){ 177 | this.takeBranch(jumpVector); 178 | } 179 | ); 180 | 181 | opcode(0x022, 'jz', 2, 0, 182 | function jz(condition, jumpVector){ 183 | if (condition === 0) 184 | this.takeBranch(jumpVector); 185 | } 186 | ); 187 | 188 | opcode(0x023, 'jnz', 2, 0, 189 | function jnz(condition, jumpVector){ 190 | if (condition !== 0) 191 | this.takeBranch(jumpVector); 192 | } 193 | ); 194 | 195 | 196 | opcode(0x024, 'jeq', 3, 0, 197 | function jeq(a, b, jumpVector){ 198 | if (a === b || uint32(a) === uint32(b)) 199 | this.takeBranch(jumpVector); 200 | } 201 | ); 202 | 203 | opcode(0x025, 'jne', 3, 0, 204 | function jne(a, b, jumpVector){ 205 | if (uint32(a) !== uint32(b)) 206 | this.takeBranch(jumpVector); 207 | } 208 | ); 209 | 210 | opcode(0x026, 'jlt', 3, 0, 211 | function jlt(a, b, jumpVector){ 212 | if (int32(a) < int32(b)) 213 | this.takeBranch(jumpVector); 214 | } 215 | ); 216 | 217 | opcode(0x027, 'jge', 3, 0, 218 | function jge(a, b, jumpVector){ 219 | if (int32(a) >= int32(b)) 220 | this.takeBranch(jumpVector); 221 | } 222 | ); 223 | 224 | opcode(0x028, 'jgt', 3, 0, 225 | function jgt(a, b, jumpVector){ 226 | if (int32(a) > int32(b)) 227 | this.takeBranch(jumpVector); 228 | } 229 | ); 230 | 231 | opcode(0x029, 'jle', 3, 0, 232 | function jle(a, b, jumpVector){ 233 | if (int32(a) <= int32(b)) 234 | this.takeBranch(jumpVector); 235 | } 236 | ); 237 | 238 | // TODO: check if it works, JS has signed ints, we want uint 239 | opcode(0x02A, 'jltu', 3, 0, 240 | function jltu(a, b, jumpVector){ 241 | if (a < b) 242 | this.takeBranch(jumpVector); 243 | } 244 | ); 245 | 246 | // TODO: check if it works, JS has signed ints, we want uint 247 | opcode(0x02B, 'jgeu', 3, 0, 248 | function jgeu(a, b, jumpVector){ 249 | if (a >= b) 250 | this.takeBranch(jumpVector); 251 | } 252 | ); 253 | 254 | // TODO: check if it works, JS has signed ints, we want uint 255 | opcode(0x02C, 'jgtu', 3, 0, 256 | function jgtu(a, b, jumpVector){ 257 | if (a > b) 258 | this.takeBranch(jumpVector); 259 | } 260 | ); 261 | 262 | // TODO: check if it works, JS has signed ints, we want uint 263 | opcode(0x02D, 'jleu', 3, 0, 264 | function jleu(a, b, jumpVector){ 265 | if (a <= b) 266 | this.takeBranch(jumpVector); 267 | } 268 | ); 269 | 270 | 271 | opcode(0x0104, 'jumpabs', 1, 0, 272 | function jumpabs(address){ 273 | this.PC = address; 274 | } 275 | ); 276 | 277 | opcode(0x30, 'call', 2, 0, 278 | function call(address:number, argc:number, destType:number, destAddr:number){ 279 | let args = []; 280 | while(argc--){ 281 | args.push(this.pop()) 282 | } 283 | this.performCall(address, args, destType, destAddr, this.PC); 284 | }, 285 | OpcodeRule.DelayedStore 286 | ) 287 | 288 | opcode(0x160, 'callf', 1, 0, 289 | function callf(address:number, destType:number, destAddr:number){ 290 | this.performCall(address, null, destType, destAddr, this.PC); 291 | }, 292 | OpcodeRule.DelayedStore 293 | ) 294 | 295 | opcode(0x161, 'callfi', 2, 0, 296 | function callfi(address:number, arg: number, destType:number, destAddr:number){ 297 | this.performCall(address, [uint32(arg)], destType, destAddr, this.PC); 298 | }, 299 | OpcodeRule.DelayedStore 300 | ) 301 | 302 | opcode(0x162, 'callfii', 3, 0, 303 | function callfii(address:number, arg1: number, arg2: number, destType:number, destAddr:number){ 304 | this.performCall(address, [uint32(arg1), uint32(arg2)], destType, destAddr, this.PC); 305 | }, 306 | OpcodeRule.DelayedStore 307 | ) 308 | 309 | opcode(0x163, 'callfiii', 4, 0, 310 | function callfiii(address:number, arg1: number, arg2: number, arg3: number, destType:number, destAddr:number){ 311 | this.performCall(address, [uint32(arg1), uint32(arg2), uint32(arg3)], destType, destAddr, this.PC); 312 | }, 313 | OpcodeRule.DelayedStore 314 | ) 315 | 316 | opcode(0x31, 'return', 1, 0, 317 | function _return(retVal:number){ 318 | this.leaveFunction(uint32(retVal)); 319 | }) 320 | 321 | opcode(0x32, "catch", 0, 0, 322 | function _catch(destType:number, destAddr:number, address:number){ 323 | this.pushCallStub(destType, destAddr, this.PC, this.FP); 324 | // the catch token is the value of sp after pushing that stub 325 | this.performDelayedStore(destType, destAddr, this.SP); 326 | this.takeBranch(address) 327 | }, 328 | OpcodeRule.Catch 329 | ) 330 | 331 | opcode(0x33, "throw", 2, 0, 332 | function _throw(ex, catchToken){ 333 | if (catchToken > this.SP) 334 | throw new Error("invalid catch token ${catchToken}"); 335 | // pop the stack back down to the stub pushed by catch 336 | this.SP = catchToken; 337 | 338 | // restore from the stub 339 | let stub = this.popCallStub(); 340 | this.PC = stub.PC; 341 | this.FP = stub.framePtr; 342 | this.frameLen = this.stack.readInt32(this.FP); 343 | this.localsPos = this.stack.readInt32(this.FP + 4); 344 | 345 | // store the thrown value and resume after the catch opcode 346 | this.performDelayedStore(stub.destType, stub.destAddr, ex); 347 | 348 | } 349 | ) 350 | 351 | opcode(0x34, "tailcall", 2, 0, 352 | function tailcall(address: number, argc: number){ 353 | let argv = []; 354 | while(argc--){ 355 | argv.push(this.pop()); 356 | } 357 | this.performCall(address, argv, 0, 0, 0, true); 358 | }); 359 | 360 | opcode(0x180, 'accelfunc', 2, 0, 361 | function(slot, value){ 362 | setSlotGlulx.call(this, false, slot, value); 363 | }); 364 | 365 | opcode(0x181, 'accelparam', 2, 0, 366 | function(slot, value){ 367 | setSlotGlulx.call(this, true, slot, value); 368 | }); 369 | 370 | opcode(0x40, "copy", 1, 1, 371 | function copy(x:number){ 372 | return uint32(x); 373 | }); 374 | 375 | opcode(0x41, "copys", 1, 1, 376 | function copys(x:number){ 377 | return x & 0xFFFF; 378 | }, OpcodeRule.Indirect16Bit); 379 | 380 | opcode(0x42, "copyb", 1, 1, 381 | function copyb(x:number){ 382 | return x & 0xFF; 383 | }, OpcodeRule.Indirect8Bit); 384 | 385 | opcode(0x44, "sexs", 1, 1, 386 | function sexs(x:number){ 387 | return x & 0x8000 ? uint32(x | 0xFFFF0000) : x & 0x0000FFFF; 388 | }); 389 | 390 | opcode(0x45, "sexb", 1, 1, 391 | function sexb(x:number){ 392 | return x & 0x80 ? uint32(x | 0xFFFFFF00) : x & 0x000000FF; 393 | }); 394 | 395 | opcode(0x48, "aload", 2, 1, 396 | function aload(array: number, index: number){ 397 | return this.image.readInt32(uint32(array+4*index)); 398 | }); 399 | 400 | opcode(0x49, "aloads", 2, 1, 401 | function aloads(array: number, index: number){ 402 | return this.image.readInt16(uint32(array+2*index)); 403 | }); 404 | 405 | opcode(0x4A, "aloadb", 2, 1, 406 | function aloadb(array: number, index: number){ 407 | return this.image.readByte(uint32(array+index)); 408 | }); 409 | 410 | opcode(0x4B, "aloadbit", 2, 1, 411 | function aloadbit(array: number, index: number){ 412 | index = int32(index); 413 | let bitx = index & 7; 414 | let address = array; 415 | if (index >= 0){ 416 | address += (index>>3); 417 | }else{ 418 | address -= (1+((-1-index)>>3)); 419 | } 420 | let byte = this.image.readByte(uint32(address)); 421 | return byte & (1 << bitx) ? 1 : 0; 422 | }); 423 | 424 | opcode(0x4C, "astore", 3, 0, 425 | function astore(array: number, index: number, value: number){ 426 | this.image.writeInt32(array+4*int32(index), uint32(value)); 427 | } 428 | ); 429 | 430 | opcode(0x4D, "astores", 3, 0, 431 | function astores(array: number, index: number, value: number){ 432 | value = value & 0xFFFF; 433 | this.image.writeBytes(array+2*index, value >> 8, value & 0xFF ); 434 | } 435 | ); 436 | 437 | opcode(0x4E, "astoreb", 3, 0, 438 | function astoreb(array: number, index: number, value: number){ 439 | this.image.writeBytes(array+index, value & 0xFF ); 440 | } 441 | ); 442 | 443 | opcode(0x4F, "astorebit", 3, 0, 444 | function astorebit(array: number, index: number, value: number){ 445 | index = int32(index); 446 | let bitx = index & 7; 447 | let address = array; 448 | if (index >= 0){ 449 | address += (index>>3); 450 | }else{ 451 | address -= (1+((-1-index)>>3)); 452 | } 453 | let byte = this.image.readByte(address); 454 | if (value === 0){ 455 | byte &= ~(1 << bitx); 456 | }else{ 457 | byte |= (1 << bitx); 458 | } 459 | this.image.writeBytes(address, byte); 460 | } 461 | ); 462 | 463 | opcode(0x70, 'streamchar', 1, 0, Engine.prototype.streamCharCore); 464 | 465 | opcode(0x73, 'streamunichar', 1, 0, Engine.prototype.streamUniCharCore); 466 | 467 | opcode(0x71, 'streamnum', 1, 0, Engine.prototype.streamNumCore); 468 | 469 | opcode(0x72, 'streamstr', 1, 0, Engine.prototype.streamStrCore); 470 | 471 | opcode(0x130, 'glk', 2, 1, 472 | function glk(code:number, argc: number){ 473 | switch(this.glkMode){ 474 | case GlkMode.None: 475 | // not really supported, just clear the stack 476 | while(argc--){ 477 | this.pop(); 478 | } 479 | return 0; 480 | case GlkMode.Wrapper: 481 | return GlkWrapperCall.call(this, code, argc); 482 | default: 483 | throw new Error(`unsupported glkMode ${this.glkMode}`); 484 | } 485 | } 486 | ); 487 | 488 | opcode(0x140, 'getstringtbl', 0, 1, 489 | function getstringtbl(){ 490 | return this.decodingTable; 491 | } 492 | ); 493 | 494 | opcode(0x141, 'setstringtbl', 1, 0, 495 | function setstringtbl(addr){ 496 | this.decodingTable = addr; 497 | } 498 | ); 499 | 500 | 501 | 502 | opcode(0x148, 'getiosys', 0, 2, 503 | function getiosys(){ 504 | switch(this.outputSystem){ 505 | case IOSystem.Null: return [0,0]; 506 | case IOSystem.Filter: return [1, this.filterAddress]; 507 | case IOSystem.Channels: return [20, 0]; 508 | case IOSystem.Glk: return [2, 0]; 509 | } 510 | } 511 | ); 512 | 513 | opcode(0x149, 'setiosys', 2, 0, 514 | function setiosys(system, rock){ 515 | switch(system){ 516 | case 0: 517 | this.outputSystem = IOSystem.Null; 518 | return; 519 | case 1: 520 | this.outputSystem = IOSystem.Filter; 521 | this.filterAddress = rock; 522 | return; 523 | case 2: 524 | if (this.glkMode !== GlkMode.Wrapper) 525 | throw new Error("Glk wrapper support has not been enabled"); 526 | this.outputSystem = IOSystem.Glk; 527 | return; 528 | case 20: 529 | if (!this.enableFyreVM) 530 | throw new Error("FyreVM support has been disabled"); 531 | this.outputSystem = IOSystem.Channels; 532 | return; 533 | default: 534 | throw new Error(`Unrecognized output system ${system}`); 535 | } 536 | } 537 | ); 538 | 539 | opcode(0x102, 'getmemsize', 0, 1, 540 | function getmemsize(){ 541 | return this.image.getEndMem(); 542 | } 543 | ); 544 | 545 | opcode(0x103, 'setmemsize', 1, 1, 546 | function setmemsize(size){ 547 | if (this.heap) 548 | throw new Error("setmemsize is not allowed while the heap is active"); 549 | try{ 550 | this.image.setEndMem(size); 551 | return 0; 552 | } 553 | catch (e){ 554 | console.error(e); 555 | return 1; 556 | } 557 | 558 | } 559 | ); 560 | 561 | opcode(0x170, 'mzero', 2, 0, 562 | function mzero(count, address){ 563 | let zeros = []; 564 | count = uint32(count); 565 | while(count--){ 566 | zeros.push(0); 567 | } 568 | this.image.writeBytes(address, ...zeros); 569 | } 570 | ); 571 | 572 | 573 | opcode(0x171, 'mcopy', 3, 0, 574 | function mcopy(count, from, to){ 575 | let data = []; 576 | count = uint32(count); 577 | for (let i = from; i this.SP - (this.FP + this.frameLen)) 681 | throw new Error("Stack underflow"); 682 | let start = this.SP - bytes; 683 | while(count--){ 684 | this.push(this.stack.readInt32(start)) 685 | start+= 4; 686 | } 687 | }); 688 | 689 | opcode(0x100, "gestalt", 2, 1, 690 | function gestalt(selector, arg){ 691 | switch(selector){ 692 | case Gestalt.GlulxVersion: return Versions.glulx; 693 | case Gestalt.TerpVersion: return Versions.terp; 694 | case Gestalt.ResizeMem: 695 | case Gestalt.Unicode: 696 | case Gestalt.MemCopy: 697 | case Gestalt.MAlloc: 698 | case Gestalt.Undo: 699 | case Gestalt.ExtUndo: 700 | case Gestalt.Acceleration: 701 | return 1; 702 | case Gestalt.Float: 703 | return 0; 704 | case Gestalt.IOSystem: 705 | if (arg === 0) return 1; // Null-IO 706 | if (arg === 1) return 1; // Filter 707 | if (arg === 20 && this.enableFyreVM) return 1; // Channel IO 708 | if (arg == 2 && this.glkMode === GlkMode.Wrapper) return 1; // Glk 709 | return 0; 710 | case Gestalt.MAllocHeap: 711 | if (this.heap) return this.heap.heapAddress; 712 | return 0; 713 | case Gestalt.AccelFunc: 714 | return 0; 715 | default: 716 | return 0; 717 | } 718 | } 719 | ); 720 | 721 | 722 | opcode(0x120, 'quit', 0, 0, 723 | function quit(){ this.running = false; }); 724 | 725 | opcode(0x122, 'restart', 0, 0, Engine.prototype.restart); 726 | 727 | opcode(0x123, 'save', 1, 0, 728 | function save(X, destType:number, destAddr:number){ 729 | // TODO: find out what that one argument X does ... 730 | let engine: Engine = this; 731 | if (engine.saveRequested){ 732 | let q = engine.saveToQuetzal(destType, destAddr); 733 | let resume = this.resumeAfterWait.bind(this); 734 | 735 | let callback = function(success:boolean){ 736 | if (success){ 737 | engine['performDelayedStore'](destType, destAddr, 0); 738 | }else{ 739 | engine['performDelayedStore'](destType, destAddr, 1); 740 | } 741 | resume(); 742 | } 743 | engine.saveRequested(q, callback); 744 | let wait: any = 'wait'; 745 | return wait; 746 | } 747 | engine['performDelayedStore'](destType, destAddr, 1); 748 | }, 749 | OpcodeRule.DelayedStore 750 | ) 751 | 752 | opcode(0x124, "restore", 1, 0, 753 | function restore(X, destType:number, destAddr:number){ 754 | // TODO: find out what that one argument X does ... 755 | let engine: Engine = this; 756 | if (engine.loadRequested){ 757 | let resume = this.resumeAfterWait.bind(this); 758 | let callback = function(quetzal:Quetzal){ 759 | if (quetzal){ 760 | engine.loadFromQuetzal(quetzal); 761 | resume(); 762 | return; 763 | } 764 | engine['performDelayedStore'](destType, destAddr, 1); 765 | resume(); 766 | } 767 | engine.loadRequested(callback); 768 | let wait: any = 'wait'; 769 | return wait; 770 | } 771 | engine['performDelayedStore'](destType, destAddr, 1); 772 | }, 773 | OpcodeRule.DelayedStore 774 | ) 775 | 776 | opcode(0x125, 'saveundo', 0, 0, 777 | function saveundo(destType:number, destAddr:number){ 778 | let q = this.saveToQuetzal(destType, destAddr); 779 | 780 | if (this.undoBuffers){ 781 | // TODO make MAX_UNDO_LEVEL configurable 782 | if (this.undoBuffers.length >= 3){ 783 | this.undoBuffers.unshift(); 784 | } 785 | this.undoBuffers.push(q); 786 | }else{ 787 | this.undoBuffers = [ q ]; 788 | } 789 | this.performDelayedStore(destType, destAddr, 0); 790 | }, OpcodeRule.DelayedStore); 791 | 792 | opcode(0x126, 'restoreundo', 0, 0, 793 | function restoreundo(destType:number, destAddr:number){ 794 | if (this.undoBuffers && this.undoBuffers.length){ 795 | let q = this.undoBuffers.pop(); 796 | this.loadFromQuetzal(q); 797 | }else{ 798 | this.performDelayedStore(destType, destAddr, 1); 799 | } 800 | 801 | }, OpcodeRule.DelayedStore); 802 | 803 | 804 | opcode(0x127, 'protect', 2, 0, 805 | function protect(start, length){ 806 | if (start < this.image.getEndMem()){ 807 | this.protectionStart = start; 808 | this.protectionLength = length; 809 | } 810 | } 811 | ) 812 | 813 | opcode(0x128, 'hasundo', 0, 1, 814 | // Test whether a VM state is available in temporary storage. 815 | // return 0 if a state is available, 1 if not. 816 | // If this returns 0, then restoreundo is expected to succeed. 817 | function hasundo(){ 818 | if (this.undoBuffers && this.undoBuffers.length) return 0; 819 | return 1; 820 | } 821 | ) 822 | 823 | opcode(0x129, 'discardundo', 0, 0, 824 | // Discard a VM state (the most recently saved) from temporary storage. If none is available, this does nothing. 825 | function discardundo(){ 826 | if (this.undoBuffers){ 827 | this.undoBuffers.pop(); 828 | } 829 | } 830 | ) 831 | 832 | 833 | opcode(0x110, 'random', 1, 1, 834 | function random(max){ 835 | if (max === 1 || max === 0xFFFFFFFF) 836 | return 0; 837 | 838 | let random: MersenneTwister = this.random; 839 | if (!random){ 840 | random = this.random = new MersenneTwister(); 841 | } 842 | if (max === 0){ 843 | return random.genrand_int32(); 844 | } 845 | 846 | max = int32(max); 847 | if (max < 0){ 848 | return uint32( - (random.genrand_int31() % -max)); 849 | } 850 | return random.genrand_int31() % max; 851 | } 852 | ); 853 | 854 | opcode(0x111, 'setrandom',1, 0, 855 | function setrandom(seed){ 856 | if (!seed) seed = undefined; 857 | this.random = new MersenneTwister(seed); 858 | } 859 | ); 860 | 861 | opcode(0x1000, 'fyrecall', 3, 1, Engine.prototype.fyreCall); 862 | 863 | return opcodes; 864 | } 865 | } 866 | 867 | const enum SearchOptions { 868 | KeyIndirect = 1, 869 | ZeroKeyTerminates = 2, 870 | ReturnIndex = 4 871 | } 872 | 873 | function PerformBinarySearch(key, keySize, start, structSize, numStructs, keyOffset, options){ 874 | if (options & SearchOptions.ZeroKeyTerminates) 875 | throw new Error("ZeroKeyTerminated option may not be used with binary search"); 876 | if (keySize > 4 && !(options & SearchOptions.KeyIndirect) ) 877 | throw new Error("KeyIndirect option must be used when searching for a >4 byte key"); 878 | let returnIndex = options & SearchOptions.ReturnIndex; 879 | let low =0, high = numStructs; 880 | key = key >>> 0; 881 | while (low < high){ 882 | let index = Math.floor((low+high) / 2); 883 | let cmp = compareKeys.call(this, key, start + index*structSize + keyOffset, keySize, options); 884 | if (cmp === 0){ 885 | // found it 886 | if (returnIndex) return index; 887 | return start+index*structSize; 888 | } 889 | if (cmp < 0){ 890 | high = index; 891 | }else{ 892 | low = index + 1; 893 | } 894 | } 895 | // did not find 896 | return returnIndex ? 0xFFFFFFFF : 0; 897 | } 898 | 899 | 900 | function PerformLinearSearch(key, keySize, start, structSize, numStructs, keyOffset, options){ 901 | if (keySize > 4 && !(options & SearchOptions.KeyIndirect) ) 902 | throw new Error("KeyIndirect option must be used when searching for a >4 byte key"); 903 | let returnIndex = options & SearchOptions.ReturnIndex; 904 | key = key >>> 0; 905 | for (let i = 0; numStructs === -1 || i>> 0; 927 | while (node){ 928 | let cmp = compareKeys.call(this, key, node + keyOffset, keySize, options); 929 | if (cmp === 0){ 930 | // found it 931 | return node; 932 | } 933 | if (options & SearchOptions.ZeroKeyTerminates){ 934 | if (keyIsZero.call(this, node + keyOffset, keySize)){ 935 | return 0; 936 | } 937 | } 938 | // advance the next item 939 | node = this.image.readInt32(node+nextOffset); 940 | } 941 | } 942 | 943 | function keyIsZero(address, size){ 944 | while(size--){ 945 | if (this.image.readByte(address+size) !== 0) 946 | return false; 947 | } 948 | return true; 949 | } 950 | 951 | function compareKeys(query:number, candidateAddr: number, keySize: number, options: number){ 952 | let { image } = this; 953 | if (options & SearchOptions.KeyIndirect){ 954 | // KeyIndirect *is* set 955 | // compare the bytes stored at query vs. candidateAddr 956 | for (let i=0; i b2) 962 | return 1; 963 | } 964 | return 0; 965 | } 966 | 967 | // KeyIndirect is *not* set 968 | // mask query to the appropriate size and compare it against the value stored at candidateAddr 969 | let ckey; 970 | switch(keySize){ 971 | case 1: 972 | ckey = image.readByte(candidateAddr); 973 | query &= 0xFF; 974 | return query - ckey; 975 | case 2: 976 | ckey = image.readInt16(candidateAddr); 977 | query &= 0xFFFF; 978 | return query - ckey; 979 | case 3: 980 | ckey = image.readInt32(candidateAddr) & 0xFFFFFF; 981 | query &= 0xFFFFFF; 982 | return query - ckey; 983 | case 4: 984 | ckey = image.readInt32(candidateAddr); 985 | return query - ckey; 986 | } 987 | 988 | } 989 | } --------------------------------------------------------------------------------