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 = '