From eab87d32a9e1ae57d27d66768e4cf35c9ab91774 Mon Sep 17 00:00:00 2001 From: He4eT Date: Tue, 23 Feb 2021 12:31:02 +0500 Subject: [PATCH] Dirty initial commit --- .gitignore | 3 + LICENSE | 21 + package-lock.json | 55 + package.json | 24 + src/cheapGlkOte.js | 106 + src/fakeDialog.js | 36 + src/glkOte/glkapi.js | 6396 +++++++++++++++++++++++++++++++++++++ src/glkOte/glkote-term.js | 145 + src/index.js | 33 + src/stdio.js | 133 + tests/player.stdio.js | 82 + tests/praxix.z5 | Bin 0 -> 31744 bytes tests/runtests.sh | 5 + 13 files changed, 7039 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/cheapGlkOte.js create mode 100644 src/fakeDialog.js create mode 100644 src/glkOte/glkapi.js create mode 100644 src/glkOte/glkote-term.js create mode 100644 src/index.js create mode 100644 src/stdio.js create mode 100644 tests/player.stdio.js create mode 100644 tests/praxix.z5 create mode 100755 tests/runtests.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e37ecce --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log* + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f458d65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2008-2018, Andrew Plotkin, Dannii Willis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e2a5bf6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,55 @@ +{ + "name": "cheap-glkote", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "emglken": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/emglken/-/emglken-0.3.3.tgz", + "integrity": "sha512-JLDmbI9chxKU3DGtPxJjAHjoWuK99qJ0ipuWU5Cy60ZpkhUb2gHGFIIJAKkLjjBMMVcpQcDjX0pdA38LfE6kOA==", + "dev": true, + "requires": { + "glkote-term": "^0.4.4", + "minimist": "^1.2.5", + "mute-stream": "0.0.8" + } + }, + "glkote-term": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/glkote-term/-/glkote-term-0.4.4.tgz", + "integrity": "sha512-5l2t4QC9Pr4DgMz/OBGojgaAZJ3p0yf+e8pIYuz63kT0gBaHqsAuASYWQVqSkj60v6nUxKYJRzE0GQucf9PDxg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..df88f37 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "cheap-glkote", + "version": "0.1.0", + "description": "Abstract JavaScript implementation of GlkOte", + "author": "He4eT ", + "license": "MIT", + "keywords": [ + "glk", + "glkote", + "interactive fiction", + "interactive-fiction" + ], + "main": "src/index.js", + "dependencies": {}, + "devDependencies": { + "ansi-escapes": "^4.0.0", + "emglken": "^0.3.3", + "minimist": "^1.2.5", + "mute-stream": "0.0.8" + }, + "scripts": { + "test": "./tests/runtests.sh" + } +} diff --git a/src/cheapGlkOte.js b/src/cheapGlkOte.js new file mode 100644 index 0000000..446f2fb --- /dev/null +++ b/src/cheapGlkOte.js @@ -0,0 +1,106 @@ +const GlkOte = require('./glkOte/glkote-term') + +class CheapGlkOte extends GlkOte { + constructor(handlers) { + super() + + this.window = null + this.current_input_type = null + + this.handlers = handlers + } + + sendFn(message) { + this.send_response( + this.current_input_type, + this.window, + message) + this.current_input_type = null + } + + init(iface) { + /* Only one window can be opened */ + const glk_window_open = iface.Glk.glk_window_open + iface.Glk.glk_window_open = (splitwin, ...args) => + splitwin + ? null + : glk_window_open(splitwin, ...args) + + this.handlers.onInit() + super.init(iface) + } + + update_inputs(data) { + if (!data.length) return null + + const {type} = data[0] + if (['char', 'line'].includes(type)) { + this.current_input_type = type + this.handlers.onUpdateInputs(type) + } + } + + accept_specialinput(data) { + if (data.type === 'fileref_prompt') { + const callback = ref => + this.send_response( + 'specialresponse', null, 'fileref_prompt', ref) + + this.interface.Dialog.open( + data.filemode !== 'read', + data.filetype, + data.gameid, + callback) + } else { + this.error( + 'Request for unknown special input type: ' + data.type) + } + } + + update_content(messages) { + const filtered = + messages.filter(content => + content.id === this.window.id)[0] + + this.handlers.onUpdateContent(filtered) + } + + exit() { + this.handlers.onExit() + super.exit() + } + + cancel_inputs(data) { + if (data.length === 0) { + this.current_input_type = null + this.handlers.onUpdateInputs(null) + } + } + + disable(disable) { + this.disabled = disable + this.handlers.onDisable(disable) + } + + update_windows(data) { + data.forEach(win => { + if (win.type === 'buffer') { + this.window = win + } + }) + } + + log(msg) { + console.log(`[log]: ${msg}`) + } + + warning(msg) { + console.warn(`[warning]: ${msg}`) + } + + error(message) { + console.error(`[error]: ${message}`) + } +} + +module.exports = CheapGlkOte diff --git a/src/fakeDialog.js b/src/fakeDialog.js new file mode 100644 index 0000000..b9b5d66 --- /dev/null +++ b/src/fakeDialog.js @@ -0,0 +1,36 @@ +class FakeDialog { + constructor(handlers) { + this.streaming = false + this.handlers = handlers + this.path = 'fake/path' + } + + file_ref_exists = ref => false + + file_construct_ref(filename, usage, gameid) { + return { + filename: [this.path, filename].join('/'), + usage: usage || '' + } + } + + file_read(dirent, israw) { + console.log('fake_file_read', dirent, israw) + return 'content' + } + + file_write(dirent, content, israw) { + if (content.length === 0) return (void null) + console.log('fake_file_write', dirent, israw, content.length) + } + + open(tosave, usage, gameid, callback) { + this.handlers.onFileNameRequest(tosave, usage, callback) + } + + log(message) { + console.log(message) + } +} + +module.exports = FakeDialog diff --git a/src/glkOte/glkapi.js b/src/glkOte/glkapi.js new file mode 100644 index 0000000..c92832b --- /dev/null +++ b/src/glkOte/glkapi.js @@ -0,0 +1,6396 @@ +/* GlkAPI -- a Javascript Glk API for IF interfaces + * GlkOte Library: version 2.2.3. + * Glk API which this implements: version 0.7.4. + * Designed by Andrew Plotkin + * + * + * This Javascript library is copyright 2010-16 by Andrew Plotkin. + * It is distributed under the MIT license; see the "LICENSE" file. + * + * This file is a Glk API compatibility layer for glkote.js. It offers a + * set of Javascript calls which closely match the original C Glk API; + * these work by means of glkote.js operations. + * + * This API was built for Quixe, which is a pure-Javascript Glulx + * interpreter. Therefore, the API is a little strange. Notably, it + * accepts text buffers in the form of arrays of integers, not + * Javascript strings. Only the Glk calls that explicitly use strings + * (glk_put_string, etc) accept Javascript native strings. + * + * If you are writing an application in pure Javascript, you can use + * this layer (along with glkote.js). If you are writing a web app which + * is the front face of a server-side Glk app, ignore this file -- use + * glkote.js directly. + */ + +/* Known problems: + + Some places in the library get confused about Unicode characters + beyond 0xFFFF. They are handled correctly by streams, but grid windows + will think they occupy two characters rather than one, which will + throw off the grid spacing. + + Also, the glk_put_jstring() function can't handle them at all. Quixe + printing operations that funnel through glk_put_jstring() -- meaning, + most native string printing -- will break up three-byte characters + into a UTF-16-encoded pair of two-byte characters. This will come + out okay in a buffer window, but it will again mess up grid windows, + and will also double the write-count in a stream. +*/ + +/* Put everything inside the Glk namespace. */ + +Glk = function() { + +/* The VM interface object. */ +var VM = null; + +/* References to external libraries */ +var Dialog; +var GiDispa; +var GiLoad; +var GlkOte; + +/* Environment capabilities */ +var support = {}; + +/* Options from the vm_options object. */ +var option_exit_warning; +var option_do_vm_autosave; +var option_before_select_hook; +var option_extevent_hook; +var option_glk_gestalt_hook; + +/* Library display state. */ +var has_exited = false; +var ui_disabled = false; +var ui_specialinput = null; +var ui_specialcallback = null; +var event_generation = 0; +var current_partial_inputs = null; +var current_partial_outputs = null; + +// Set external variable references +function set_references( vm_options ) +{ + if ( vm_options.Dialog ) + { + Dialog = vm_options.Dialog; + } + if ( !Dialog ) + { + if ( typeof window !== 'undefined' && window.Dialog ) + { + Dialog = window.Dialog; + } + else + { + throw new Error( 'No reference to Dialog' ); + } + } + + if ( vm_options.GiDispa ) + { + GiDispa = vm_options.GiDispa; + } + else if ( !GiDispa && typeof window !== 'undefined' && window.GiDispa ) + { + GiDispa = window.GiDispa; + } + + if ( vm_options.GiLoad ) + { + GiLoad = vm_options.GiLoad; + } + else if ( !GiLoad && typeof window !== 'undefined' && window.GiLoad ) + { + GiLoad = window.GiLoad; + } + + if ( vm_options.GlkOte ) + { + GlkOte = vm_options.GlkOte; + } + if ( !GlkOte ) + { + if ( typeof window !== 'undefined' && window.GlkOte ) + { + GlkOte = window.GlkOte; + } + else + { + throw new Error('No reference to GlkOte'); + } + } +} + +/* Initialize the library, initialize the VM, and set it running. (It will + run until the first glk_select() or glk_exit() call.) + + The vm_options argument must have a vm_options.vm field, which must be an + appropriate VM interface object. (For example, Quixe.) This must have + init() and resume() methods. + + The vm_options argument is also passed through to GlkOte as the game + interface object. It can be used to affect some GlkOte display options, + such as window spacing. + + (You do not need to provide a vm_options.accept() function. The Glk + library sets that up for you.) +*/ +function init(vm_options) { + /* Set references to external libraries */ + set_references( vm_options ); + + VM = vm_options.vm; + if (GiDispa) + GiDispa.set_vm(VM); + + vm_options.accept = accept_ui_event; + + GlkOte.init(vm_options); + + option_exit_warning = vm_options.exit_warning; + option_do_vm_autosave = vm_options.do_vm_autosave; + option_before_select_hook = vm_options.before_select_hook; + option_extevent_hook = vm_options.extevent_hook; + option_glk_gestalt_hook = vm_options.glk_gestalt_hook; + + if (option_before_select_hook) { + option_before_select_hook(); + } +} + +function accept_ui_event(obj) { + var box; + + //qlog("### accept_ui_event: " + obj.type + ", gen " + obj.gen); + if (ui_disabled) { + /* We've hit glk_exit() or a VM fatal error, or just blocked the UI for + some modal dialog. */ + qlog("### ui is disabled, ignoring event"); + return; + } + + if (obj.gen != event_generation) { + GlkOte.log('Input event had wrong generation number: got ' + obj.gen + ', currently at ' + event_generation); + return; + } + event_generation += 1; + + /* Note any partial inputs; we'll need them if the game cancels a line + input. This may be undef. */ + current_partial_inputs = obj.partial; + + switch (obj.type) { + case 'init': + content_metrics = obj.metrics; + /* Process the support array */ + if (obj.support) { + obj.support.forEach(function(item) {support[item] = 1;}); + } + VM.init(); + break; + + case 'external': + var res = null; + if (option_extevent_hook) { + res = option_extevent_hook(obj.value); + } + if (!res && obj.value == 'timer') { + /* Timer events no longer come in this way, but we'll still + accept them. */ + gli_timer_started = Date.now(); + res = { type: Const.evtype_Timer }; + } + if (res && res.type) { + handle_external_input(res); + } + break; + + case 'timer': + gli_timer_started = Date.now(); + var res = { type: Const.evtype_Timer }; + handle_external_input(res); + break; + + case 'hyperlink': + handle_hyperlink_input(obj.window, obj.value); + break; + + case 'mouse': + handle_mouse_input(obj.window, obj.x, obj.y); + break; + + case 'char': + handle_char_input(obj.window, obj.value); + break; + + case 'line': + handle_line_input(obj.window, obj.value, obj.terminator); + break; + + case 'arrange': + content_metrics = obj.metrics; + box = { + left: content_metrics.outspacingx, + top: content_metrics.outspacingy, + right: content_metrics.width-content_metrics.outspacingx, + bottom: content_metrics.height-content_metrics.outspacingy + }; + if (gli_rootwin) + gli_window_rearrange(gli_rootwin, box); + handle_arrange_input(); + break; + + case 'redraw': + handle_redraw_input(); + break; + + case 'specialresponse': + if (obj.response == 'fileref_prompt') { + gli_fileref_create_by_prompt_callback(obj); + } + break; + } +} + +function handle_arrange_input() { + if (!gli_selectref) + return; + + gli_selectref.set_field(0, Const.evtype_Arrange); + gli_selectref.set_field(1, null); + gli_selectref.set_field(2, 0); + gli_selectref.set_field(3, 0); + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_redraw_input() { + if (!gli_selectref) + return; + + gli_selectref.set_field(0, Const.evtype_Redraw); + gli_selectref.set_field(1, null); + gli_selectref.set_field(2, 0); + gli_selectref.set_field(3, 0); + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_external_input(res) { + if (!gli_selectref) + return; + + /* This also handles timer input. */ + var val1 = 0; + var val2 = 0; + if (res.val1) + val1 = res.val1; + if (res.val2) + val2 = res.val2; + + gli_selectref.set_field(0, res.type); + gli_selectref.set_field(1, null); + gli_selectref.set_field(2, val1); + gli_selectref.set_field(3, val2); + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_hyperlink_input(disprock, val) { + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.hyperlink_request) + return; + + gli_selectref.set_field(0, Const.evtype_Hyperlink); + gli_selectref.set_field(1, win); + gli_selectref.set_field(2, val); + gli_selectref.set_field(3, 0); + + win.hyperlink_request = false; + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_mouse_input(disprock, xpos, ypos) { + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.mouse_request) + return; + + gli_selectref.set_field(0, Const.evtype_MouseInput); + gli_selectref.set_field(1, win); + gli_selectref.set_field(2, xpos); + gli_selectref.set_field(3, ypos); + + win.mouse_request = false; + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_char_input(disprock, input) { + var charval; + + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.char_request) + return; + + if (input.length == 1) { + charval = input.charCodeAt(0); + if (!win.char_request_uni) + charval = charval & 0xFF; + } + else { + charval = KeystrokeNameMap[input]; + if (!charval) + charval = Const.keycode_Unknown; + } + + gli_selectref.set_field(0, Const.evtype_CharInput); + gli_selectref.set_field(1, win); + gli_selectref.set_field(2, charval); + gli_selectref.set_field(3, 0); + + win.char_request = false; + win.char_request_uni = false; + win.input_generation = null; + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_line_input(disprock, input, termkey) { + var ix; + + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.line_request) + return; + + if (input.length > win.linebuf.length) + input = input.slice(0, win.linebuf.length); + + if (win.request_echo_line_input) { + ix = win.style; + gli_set_style(win.str, Const.style_Input); + gli_window_put_string(win, input); + if (win.echostr) + glk_put_jstring_stream(win.echostr, input); + gli_set_style(win.str, ix); + gli_window_put_string(win, "\n"); + if (win.echostr) + glk_put_jstring_stream(win.echostr, "\n"); + } + + for (ix=0; ix 100) { + win.reserve.splice(0, win.reserve.length-100); + } + break; + case Const.wintype_TextGrid: + if (win.gridwidth == 0 || win.gridheight == 0) + break; + obj.lines = []; + for (ix=0; ix= 0) { + /* We're going to delete every command before the + fill, except that we save the last setcolor. */ + var setcol = null; + for (ix=0; ix=0; ix--) { + var obj = res.windows[ix]; + var win = { + type: obj.type, rock: obj.rock, disprock: obj.disprock, + style: obj.style, hyperlink: obj.hyperlink + }; + GiDispa.class_register('window', win, win.disprock); + + win.prev = null; + win.next = gli_windowlist; + gli_windowlist = win; + if (win.next) + win.next.prev = win; + } + + for (var ix=res.streams.length-1; ix>=0; ix--) { + var obj = res.streams[ix]; + var str = { + type: obj.type, rock: obj.rock, disprock: obj.disprock, + unicode: obj.unicode, isbinary: obj.isbinary, + readcount: obj.readcount, writecount: obj.writecount, + readable: obj.readable, writable: obj.writable, + streaming: obj.streaming + }; + GiDispa.class_register('stream', str, str.disprock); + + str.prev = null; + str.next = gli_streamlist; + gli_streamlist = str; + if (str.next) + str.next.prev = str; + } + + for (var ix=res.filerefs.length-1; ix>=0; ix--) { + var obj = res.filerefs[ix]; + var fref = { + type: obj.type, rock: obj.rock, disprock: obj.disprock, + filename: obj.filename, textmode: obj.textmode, + filetype: obj.filetype, filetypename: obj.filetypename + }; + GiDispa.class_register('fileref', fref, fref.disprock); + + fref.prev = null; + fref.next = gli_filereflist; + gli_filereflist = fref; + if (fref.next) + fref.next.prev = fref; + } + + /* ...Now we fill in the cross-references. */ + + for (var ix=0; ix str.bufeof) + str.bufpos = str.bufeof; + } + else { + str.ref = obj.ref; + str.fstream = Dialog.file_fopen(str.origfmode, str.ref); + if (!str.fstream) { + /* This is the panic case. We can't reopen the stream, + but the game expects an open stream! We'll just + have to open a temporary file; the user will never + get their data, but at least the game won't crash. + (Better policy would be to prompt the user for + a new file location...) */ + var tempref = Dialog.file_construct_temp_ref(str.ref.usage); + str.fstream = Dialog.file_fopen(str.origfmode, tempref); + if (!str.fstream) + throw('restore_allstate: could not reopen even a temp stream for: ' + str.ref.filename); + } + + if (str.origfmode != Const.filemode_WriteAppend) { + /* Jump to the last known filepos. */ + str.fstream.fseek(obj.filepos, Const.seekmode_Start); + } + + str.buffer4 = str.fstream.BufferClass.alloc(4); + } + break; + + } + } + + for (var ix=0; ix> 10), 0xDC00 + (val & 0x3FF)); + } +} + +/* Given an array, return an array of the same length with all the values + trimmed to the range 0-255. This may be the same array. */ +function TrimArrayToBytes(arr) { + var ix, newarr; + var len = arr.length; + for (ix=0; ix= 0x100) + break; + } + if (ix == len) { + return arr; + } + newarr = Array(len); + for (ix=0; ix= 0x100) + newarr[ix] = 63; // '?' + else + newarr[ix] = arr[ix]; + } + return newarr; +} + +/* Convert an array of 8-bit values to a JS string, trimming if + necessary. */ +function ByteArrayToString(arr) { + var ix, newarr; + var len = arr.length; + if (len == 0) + return ''; + for (ix=0; ix= 0x100) + break; + } + if (ix == len) { + return String.fromCharCode.apply(this, arr); + } + newarr = Array(len); + for (ix=0; ix= 0x10000) + break; + } + if (ix == len) { + return String.fromCharCode.apply(this, arr); + } + newarr = Array(len); + for (ix=0; ix> 10), 0xDC00 + (val & 0x3FF)); + } + } + return newarr.join(''); +} + +/* Convert an array of 32-bit Unicode values to an array of 8-bit byte + values, encoded UTF-8. If all the values are 0-127, this returns the + same array. Otherwise it returns a new (longer) array. */ +function UniArrayToUTF8(arr) { + var count = 0; + + for (var ix=0; ix> 6)); + res.push(0x80 | (val & 0x03F) ); + } + else if (val < 0x10000) { + res.push(0xE0 | ((val & 0xF000) >> 12)); + res.push(0x80 | ((val & 0x0FC0) >> 6)); + res.push(0x80 | (val & 0x003F) ); + } + else if (val < 0x200000) { + res.push(0xF0 | ((val & 0x1C0000) >> 18)); + res.push(0x80 | ((val & 0x03F000) >> 12)); + res.push(0x80 | ((val & 0x000FC0) >> 6)); + res.push(0x80 | (val & 0x00003F) ); + } + else { + res.push(63); // '?' + } + } + + return res; +} + +/* Convert an array of 32-bit Unicode values to an array of 8-bit byte + values, encoded as big-endian words. */ +function UniArrayToBE32(arr) { + var res = new Array(4*arr.length); + for (var ix=0; ix> 24) & 0xFF; + res[4*ix+1] = (val >> 16) & 0xFF; + res[4*ix+2] = (val >> 8) & 0xFF; + res[4*ix+3] = (val) & 0xFF; + } + return res; +} + +/* Log the message in the browser's error log, if it has one. (This shows + up in Safari, in Opera, and in Firefox if you have Firebug installed.) +*/ +function qlog(msg) { + if (typeof console !== 'undefined' && console.log) + console.log(msg); +} + +/* RefBox: Simple class used for "call-by-reference" Glk arguments. The object + is just a box containing a single value, which can be written and read. +*/ +function RefBox() { + this.value = undefined; + this.set_value = function(val) { + this.value = val; + } + this.get_value = function() { + return this.value; + } +} + +/* RefStruct: Used for struct-type Glk arguments. After creating the + object, you should call push_field() the appropriate number of times, + to set the initial field values. Then set_field() can be used to + change them, and get_fields() retrieves the list of all fields. + + (The usage here is loose, since Javascript is forgiving about arrays. + Really the caller could call set_field() instead of push_field() -- + or skip that step entirely, as long as the Glk function later calls + set_field() for each field. Which it should.) +*/ +function RefStruct(numels) { + this.fields = []; + this.push_field = function(val) { + this.fields.push(val); + } + this.set_field = function(pos, val) { + this.fields[pos] = val; + } + this.get_field = function(pos) { + return this.fields[pos]; + } + this.get_fields = function() { + return this.fields; + } +} + +/* Dummy return value, which means that the Glk call is still in progress, + or will never return at all. This is used by glk_exit(), glk_select(), + and glk_fileref_create_by_prompt(). +*/ +var DidNotReturn = { dummy: 'Glk call has not yet returned' }; + +/* This returns a hint for whether the Glk call (by selector number) + might block or never return. True for glk_exit(), glk_select(), + and glk_fileref_create_by_prompt(). +*/ +function call_may_not_return(id) { + if (id == 0x001 || id == 0x0C0 || id == 0x062) + return true; + else + return false; +} + +var strtype_File = 1; +var strtype_Window = 2; +var strtype_Memory = 3; +var strtype_Resource = 4; + +/* Extra update information -- autorestore only. */ +var gli_autorestore_glkstate = null; + +/* Beginning of linked list of windows. */ +var gli_windowlist = null; +var gli_rootwin = null; +/* Set when any window is created, destroyed, or resized. */ +var geometry_changed = true; +/* Received from GlkOte; describes the window size. */ +var content_metrics = null; + +/* Beginning of linked list of streams. */ +var gli_streamlist = null; +/* Beginning of linked list of filerefs. */ +var gli_filereflist = null; +/* Beginning of linked list of schannels. */ +var gli_schannellist = null; + +/* The current output stream. */ +var gli_currentstr = null; + +/* During a glk_select() block, this is the RefStruct which will contain + the result. */ +var gli_selectref = null; + +/* This is used to assigned disprock values to windows, when there is + no GiDispa layer to provide them. */ +var gli_api_display_rocks = 1; + +/* A positive number if the timer is set. */ +var gli_timer_interval = null; +var gli_timer_started = null; /* when the setTimeout began */ +var gli_timer_lastsent = null; /* last interval sent to GlkOte */ + +function gli_new_window(type, rock) { + var win = {}; + win.type = type; + win.rock = rock; + win.disprock = undefined; + + win.parent = null; + win.str = gli_stream_open_window(win); + win.echostr = null; + win.style = Const.style_Normal; + win.hyperlink = 0; + + win.input_generation = null; + win.linebuf = null; + win.char_request = false; + win.line_request = false; + win.char_request_uni = false; + win.line_request_uni = false; + win.hyperlink_request = false; + win.mouse_request = false; + win.echo_line_input = true; + win.line_input_terminators = []; + win.request_echo_line_input = null; /* only used during a request */ + + /* window-type-specific info is set up in glk_window_open */ + + win.prev = null; + win.next = gli_windowlist; + gli_windowlist = win; + if (win.next) + win.next.prev = win; + + if (GiDispa) + GiDispa.class_register('window', win); + else + win.disprock = gli_api_display_rocks++; + /* We need to assign a disprock even if there's no GiDispa layer, + because GlkOte differentiates windows by their disprock. */ + geometry_changed = true; + + return win; +} + +function gli_delete_window(win) { + var prev, next; + + if (GiDispa) + GiDispa.class_unregister('window', win); + geometry_changed = true; + + win.echostr = null; + if (win.str) { + gli_delete_stream(win.str); + win.str = null; + } + + prev = win.prev; + next = win.next; + win.prev = null; + win.next = null; + + if (prev) + prev.next = next; + else + gli_windowlist = next; + if (next) + next.prev = prev; + + win.parent = null; + win.rock = null; + win.disprock = null; +} + +function gli_windows_unechostream(str) { + var win; + + for (win=gli_windowlist; win; win=win.next) { + if (win.echostr === str) + win.echostr = null; + } +} + +/* Add a (Javascript) string to the given window's display. */ +function gli_window_put_string(win, val) { + var ix, ch; + + //### might be efficient to split the implementation up into + //### gli_window_buffer_put_string(), etc, since many functions + //### know the window type when they call this + switch (win.type) { + case Const.wintype_TextBuffer: + if (win.style != win.accumstyle + || win.hyperlink != win.accumhyperlink) + gli_window_buffer_deaccumulate(win); + win.accum.push(val); + break; + case Const.wintype_TextGrid: + for (ix=0; ix= win.gridwidth) { + win.cursorx = 0; + win.cursory++; + } + if (win.cursory < 0) + win.cursory = 0; + else if (win.cursory >= win.gridheight) + break; /* outside the window */ + + if (ch == "\n") { + /* a newline just moves the cursor. */ + win.cursory++; + win.cursorx = 0; + continue; + } + + lineobj = win.lines[win.cursory]; + lineobj.dirty = true; + lineobj.chars[win.cursorx] = ch; + lineobj.styles[win.cursorx] = win.style; + lineobj.hyperlinks[win.cursorx] = win.hyperlink; + + win.cursorx++; + /* We can leave the cursor outside the window, since it will be + canonicalized next time a character is printed. */ + } + break; + } +} + +/* Canonicalize the cursor position. That is, the cursor may have + been left outside the window area; wrap it if necessary. + + Returns true if the cursor winds up wrapped outside the window entirely; + false if the cursor winds up at a legal printing position. +*/ +function gli_window_grid_canonicalize(win) { + if (win.cursorx < 0) + win.cursorx = 0; + else if (win.cursorx >= win.gridwidth) { + win.cursorx = 0; + win.cursory++; + } + if (win.cursory < 0) + win.cursory = 0; + else if (win.cursory >= win.gridheight) + return true; /* outside the window */ + return false; +} + +/* Take the accumulation of strings (since the last style change) and + assemble them into a buffer window update. This must be called + after each style change; it must also be called right before + GlkOte.update(). (Actually we call it right before win.accum.push + if the style has changed -- there's no need to call for *every* style + change if no text is being pushed out in between.) +*/ +function gli_window_buffer_deaccumulate(win) { + var conta = win.content; + var stylename = StyleNameMap[win.accumstyle]; + var text, ls, ix, obj, arr; + + if (win.accum.length) { + text = win.accum.join(''); + ls = text.split('\n'); + for (ix=0; ix win.gridheight) { + win.lines.length = win.gridheight; + } + else if (oldheight < win.gridheight) { + for (ix=oldheight; ix win.gridwidth) { + lineobj.dirty = true; + lineobj.chars.length = win.gridwidth; + lineobj.styles.length = win.gridwidth; + lineobj.hyperlinks.length = win.gridwidth; + } + else if (oldwidth < win.gridwidth) { + lineobj.dirty = true; + for (cx=oldwidth; cx= max) { + split = min; + } + else { + split = Math.min(Math.max(split, min), max-splitwid); + } + + win.pair_splitpos = split; + win.pair_splitwidth = splitwid; + if (win.pair_vertical) { + box1 = { + left: win.bbox.left, + right: win.pair_splitpos, + top: win.bbox.top, + bottom: win.bbox.bottom + }; + box2 = { + left: box1.right + win.pair_splitwidth, + right: win.bbox.right, + top: win.bbox.top, + bottom: win.bbox.bottom + }; + } + else { + box1 = { + top: win.bbox.top, + bottom: win.pair_splitpos, + left: win.bbox.left, + right: win.bbox.right + }; + box2 = { + top: box1.bottom + win.pair_splitwidth, + bottom: win.bbox.bottom, + left: win.bbox.left, + right: win.bbox.right + }; + } + if (!win.pair_backward) { + ch1 = win.child1; + ch2 = win.child2; + } + else { + ch1 = win.child2; + ch2 = win.child1; + } + + gli_window_rearrange(ch1, box1); + gli_window_rearrange(ch2, box2); + break; + + } +} + +function gli_new_stream(type, readable, writable, rock) { + var str = {}; + str.type = type; + str.rock = rock; + str.disprock = undefined; + + str.unicode = false; + /* isbinary is only meaningful for Resource and streaming-File streams */ + str.isbinary = false; + str.streaming = false; + str.ref = null; + str.win = null; + str.file = null; + + /* for buffer mode */ + str.buf = null; + str.bufpos = 0; + str.buflen = 0; + str.bufeof = 0; + str.timer_id = null; + str.flush_func = null; + + /* for streaming mode */ + str.fstream = null; + + str.readcount = 0; + str.writecount = 0; + str.readable = readable; + str.writable = writable; + + str.prev = null; + str.next = gli_streamlist; + gli_streamlist = str; + if (str.next) + str.next.prev = str; + + if (GiDispa) + GiDispa.class_register('stream', str); + + return str; +} + +function gli_delete_stream(str) { + var prev, next; + + if (str === gli_currentstr) { + gli_currentstr = null; + } + + gli_windows_unechostream(str); + + if (str.type == strtype_Memory) { + if (GiDispa) + GiDispa.unretain_array(str.buf); + } + else if (str.type == strtype_File) { + if (str.fstream) { + str.fstream.fclose(); + str.fstream = null; + } + } + + if (GiDispa) + GiDispa.class_unregister('stream', str); + + prev = str.prev; + next = str.next; + str.prev = null; + str.next = null; + + if (prev) + prev.next = next; + else + gli_streamlist = next; + if (next) + next.prev = prev; + + str.fstream = null; + str.buf = null; + str.readable = false; + str.writable = false; + str.ref = null; + str.win = null; + str.file = null; + str.rock = null; + str.disprock = null; +} + +function gli_stream_open_window(win) { + var str; + str = gli_new_stream(strtype_Window, false, true, 0); + str.unicode = true; + str.win = win; + return str; +} + +/* This is called on every write to a file stream. If a file is being + written intermittently (a transcript file, for example) we'd like to + flush the output every few seconds, in case the user closes the + browser without closing the file ("script off"). + + We do this by setting a ten-second timer (if there isn't one set already). + The timer calls a flush method on the stream. + + (If autosave is on, we'll wind up flushing on most glk_select calls, + which isn't quite as nicely paced. But it's a minor problem.) +*/ +function gli_stream_dirty_file(str) { + if (str.streaming) + GlkOte.log('### gli_stream_dirty_file called for streaming file!'); + if (str.timer_id === null) { + if (str.flush_func === null) { + /* Bodge together a closure to act as a stream method. */ + str.flush_func = function() { gli_stream_flush_file(str); }; + } + str.timer_id = setTimeout(str.flush_func, 10000); + } +} + +/* Write out the contents of a file stream to the "disk file". Because + localStorage doesn't support appending, we have to dump the entire + buffer out. +*/ +function gli_stream_flush_file(str) { + if (str.streaming) + GlkOte.log('### gli_stream_flush_file called for streaming file!'); + if (!(str.timer_id === null)) { + clearTimeout(str.timer_id); + } + str.timer_id = null; + Dialog.file_write(str.ref, str.buf); +} + +function gli_new_fileref(filename, usage, rock, ref) { + var fref = {}; + fref.filename = filename; + fref.rock = rock; + fref.disprock = undefined; + + fref.textmode = ((usage & Const.fileusage_TextMode) != 0); + fref.filetype = (usage & Const.fileusage_TypeMask); + fref.filetypename = FileTypeMap[fref.filetype]; + if (!fref.filetypename) { + fref.filetypename = 'xxx'; + } + + if (!ref) { + var gameid = ''; + if (fref.filetype == Const.fileusage_SavedGame) + gameid = VM.get_signature(); + ref = Dialog.file_construct_ref(fref.filename, fref.filetypename, gameid); + } + fref.ref = ref; + + fref.prev = null; + fref.next = gli_filereflist; + gli_filereflist = fref; + if (fref.next) + fref.next.prev = fref; + + if (GiDispa) + GiDispa.class_register('fileref', fref); + + return fref; +} + +function gli_delete_fileref(fref) { + var prev, next; + + if (GiDispa) + GiDispa.class_unregister('fileref', fref); + + prev = fref.prev; + next = fref.next; + fref.prev = null; + fref.next = null; + + if (prev) + prev.next = next; + else + gli_filereflist = next; + if (next) + next.prev = prev; + + fref.filename = null; + fref.ref = null; + fref.rock = null; + fref.disprock = null; +} + +/* Write one character (given as a Unicode value) to a stream. + This is called by both the one-byte and four-byte character APIs. +*/ +function gli_put_char(str, ch) { + if (!str || !str.writable) + throw('gli_put_char: invalid stream'); + + if (!str.unicode) { + if (ch < 0 || ch >= 0x100) + ch = 63; // '?' + } + + str.writecount += 1; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (!str.unicode) { + str.buffer4[0] = ch; + str.fstream.fwrite(str.buffer4, 1); + } + else { + if (!str.isbinary) { + /* cheap UTF-8 stream */ + var len; + if (ch < 0x10000) { + len = str.buffer4.write(String.fromCharCode(ch)); + str.fstream.fwrite(str.buffer4, len); // utf8 + } + else { + /* String.fromCharCode chokes on astral characters; + do it the hard way */ + var arr8 = UniArrayToUTF8([ch]); + var buf = str.fstream.BufferClass.from(arr8); + str.fstream.fwrite(buf); + } + } + else { + /* cheap big-endian stream */ + str.buffer4.writeUInt32BE(ch, 0, true); + str.fstream.fwrite(str.buffer4, 4); + } + } + } + else { + /* non-streaming... */ + gli_stream_dirty_file(str); + if (!str.unicode || (ch < 0x80 && !str.isbinary)) { + if (str.bufpos < str.buflen) { + str.buf[str.bufpos] = ch; + str.bufpos += 1; + if (str.bufpos > str.bufeof) + str.bufeof = str.bufpos; + } + } + else { + var arr; + if (!str.isbinary) + arr = UniArrayToUTF8([ch]); + else + arr = UniArrayToBE32([ch]); + var len = arr.length; + if (len > str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + } + } + break; + case strtype_Memory: + if (str.bufpos < str.buflen) { + str.buf[str.bufpos] = ch; + str.bufpos += 1; + if (str.bufpos > str.bufeof) + str.bufeof = str.bufpos; + } + break; + case strtype_Window: + if (str.win.line_request) + throw('gli_put_char: window has pending line request'); + gli_window_put_string(str.win, CharToString(ch)); + if (str.win.echostr) + gli_put_char(str.win.echostr, ch); + break; + } +} + +/* Write characters (given as an array of Unicode values) to a stream. + This is called by both the one-byte and four-byte character APIs. + The "allbytes" argument is a hint that all the array values are + already in the range 0-255. +*/ +function gli_put_array(str, arr, allbytes) { + var ix, len, val; + + if (!str || !str.writable) + throw('gli_put_array: invalid stream'); + + if (!str.unicode && !allbytes) { + arr = TrimArrayToBytes(arr); + allbytes = true; + } + + str.writecount += arr.length; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (!str.unicode) { + var buf = str.fstream.BufferClass.from(arr); + str.fstream.fwrite(buf); + } + else { + if (!str.isbinary) { + /* cheap UTF-8 stream */ + var arr8 = UniArrayToUTF8(arr); + var buf = str.fstream.BufferClass.from(arr8); + str.fstream.fwrite(buf); + } + else { + /* cheap big-endian stream */ + var buf = str.fstream.BufferClass.alloc(4*arr.length); + for (ix=0; ix str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + } + break; + case strtype_Memory: + len = arr.length; + if (len > str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + break; + case strtype_Window: + if (str.win.line_request) + throw('gli_put_array: window has pending line request'); + if (allbytes) + val = String.fromCharCode.apply(this, arr); + else + val = UniArrayToString(arr); + gli_window_put_string(str.win, val); + if (str.win.echostr) + gli_put_array(str.win.echostr, arr, allbytes); + break; + } +} + +function gli_get_char(str, want_unicode) { + var ch; + + if (!str || !str.readable) + return -1; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (!str.unicode) { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + str.readcount++; + return str.buffer4[0]; + } + else { + if (!str.isbinary) { + /* slightly less cheap UTF8 stream */ + var val0, val1, val2, val3; + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val0 = str.buffer4[0]; + if (val0 < 0x80) { + ch = val0; + } + else { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val1 = str.buffer4[0]; + if ((val1 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xE0) == 0xC0) { + ch = (val0 & 0x1F) << 6; + ch |= (val1 & 0x3F); + } + else { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val2 = str.buffer4[0]; + if ((val2 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xF0) == 0xE0) { + ch = (((val0 & 0xF)<<12) & 0x0000F000); + ch |= (((val1 & 0x3F)<<6) & 0x00000FC0); + ch |= (((val2 & 0x3F)) & 0x0000003F); + } + else if ((val0 & 0xF0) == 0xF0) { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val3 = str.buffer4[0]; + if ((val3 & 0xC0) != 0x80) + return -1; + ch = (((val0 & 0x7)<<18) & 0x1C0000); + ch |= (((val1 & 0x3F)<<12) & 0x03F000); + ch |= (((val2 & 0x3F)<<6) & 0x000FC0); + ch |= (((val3 & 0x3F)) & 0x00003F); + } + else { + return -1; + } + } + } + } + else { + /* cheap big-endian stream */ + var len = str.fstream.fread(str.buffer4, 4); + if (len < 4) + return -1; + /*### or buf.readUInt32BE(0, true) */ + ch = (str.buffer4[0] << 24); + ch |= (str.buffer4[1] << 16); + ch |= (str.buffer4[2] << 8); + ch |= str.buffer4[3]; + } + str.readcount++; + ch >>>= 0; + if (!want_unicode && ch >= 0x100) + return 63; // return '?' + return ch; + } + } + /* non-streaming, fall through to resource... */ + case strtype_Resource: + if (str.unicode) { + if (str.isbinary) { + /* cheap big-endian stream */ + if (str.bufpos >= str.bufeof) + return -1; + ch = str.buf[str.bufpos]; + str.bufpos++; + if (str.bufpos >= str.bufeof) + return -1; + ch = (ch << 8) | (str.buf[str.bufpos] & 0xFF); + str.bufpos++; + if (str.bufpos >= str.bufeof) + return -1; + ch = (ch << 8) | (str.buf[str.bufpos] & 0xFF); + str.bufpos++; + if (str.bufpos >= str.bufeof) + return -1; + ch = (ch << 8) | (str.buf[str.bufpos] & 0xFF); + str.bufpos++; + } + else { + /* slightly less cheap UTF8 stream */ + var val0, val1, val2, val3; + if (str.bufpos >= str.bufeof) + return -1; + val0 = str.buf[str.bufpos]; + str.bufpos++; + if (val0 < 0x80) { + ch = val0; + } + else { + if (str.bufpos >= str.bufeof) + return -1; + val1 = str.buf[str.bufpos]; + str.bufpos++; + if ((val1 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xE0) == 0xC0) { + ch = (val0 & 0x1F) << 6; + ch |= (val1 & 0x3F); + } + else { + if (str.bufpos >= str.bufeof) + return -1; + val2 = str.buf[str.bufpos]; + str.bufpos++; + if ((val2 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xF0) == 0xE0) { + ch = (((val0 & 0xF)<<12) & 0x0000F000); + ch |= (((val1 & 0x3F)<<6) & 0x00000FC0); + ch |= (((val2 & 0x3F)) & 0x0000003F); + } + else if ((val0 & 0xF0) == 0xF0) { + if (str.bufpos >= str.bufeof) + return -1; + val3 = str.buf[str.bufpos]; + str.bufpos++; + if ((val3 & 0xC0) != 0x80) + return -1; + ch = (((val0 & 0x7)<<18) & 0x1C0000); + ch |= (((val1 & 0x3F)<<12) & 0x03F000); + ch |= (((val2 & 0x3F)<<6) & 0x000FC0); + ch |= (((val3 & 0x3F)) & 0x00003F); + } + else { + return -1; + } + } + } + } + str.readcount++; + ch >>>= 0; + if (!want_unicode && ch >= 0x100) + return 63; // return '?' + return ch; + } + /* non-unicode file/resource, fall through to memory... */ + case strtype_Memory: + if (str.bufpos < str.bufeof) { + ch = str.buf[str.bufpos]; + str.bufpos++; + str.readcount++; + if (!want_unicode && ch >= 0x100) + return 63; // return '?' + return ch; + } + else { + return -1; // end of stream + } + default: + return -1; + } +} + +function gli_get_line(str, buf, want_unicode) { + if (!str || !str.readable) + return 0; + + var len = buf.length; + var gotnewline; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (len == 0) + return 0; + len -= 1; /* for the terminal null */ + gotnewline = false; + for (lx=0; lx= str.bufeof) { + len = 0; + } + else { + if (str.bufpos + len > str.bufeof) { + len = str.bufeof - str.bufpos; + } + } + gotnewline = false; + if (!want_unicode) { + for (lx=0; lx= 0x100) + ch = 63; // ch = '?' + buf[lx] = ch; + gotnewline = (ch == 10); + } + } + else { + for (lx=0; lx= str.bufeof) { + len = 0; + } + else { + if (str.bufpos + len > str.bufeof) { + len = str.bufeof - str.bufpos; + } + } + if (!want_unicode) { + for (lx=0; lx= 0x100) + ch = 63; // ch = '?' + buf[lx] = ch; + } + } + else { + for (lx=0; lx str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + } + break; + case strtype_Memory: + len = val.length; + if (len > str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + if (str.unicode || allbytes) { + for (ix=0; ix= 0x100) + ch = 63; // '?' + str.buf[str.bufpos+ix] = ch; + } + } + str.bufpos += len; + if (str.bufpos > str.bufeof) + str.bufeof = str.bufpos; + break; + case strtype_Window: + if (str.win.line_request) + throw('glk_put_jstring: window has pending line request'); + gli_window_put_string(str.win, val); + if (str.win.echostr) + glk_put_jstring_stream(str.win.echostr, val, allbytes); + break; + } +} + +function gli_set_style(str, val) { + if (!str || !str.writable) + throw('gli_set_style: invalid stream'); + + if (val >= Const.style_NUMSTYLES) + val = 0; + + if (str.type == strtype_Window) { + str.win.style = val; + if (str.win.echostr) + gli_set_style(str.win.echostr, val); + } +} + +function gli_set_hyperlink(str, val) { + if (!str || !str.writable) + throw('gli_set_hyperlink: invalid stream'); + + if (str.type == strtype_Window) { + str.win.hyperlink = val; + if (str.win.echostr) + gli_set_hyperlink(str.win.echostr, val); + } +} + +/* The catalog of Glk API functions. */ + +function glk_exit() { + /* For safety, this is fast and idempotent. */ + has_exited = true; + ui_disabled = true; + gli_selectref = null; + if (option_exit_warning) + GlkOte.warning(option_exit_warning); + update('exit'); + return DidNotReturn; +} + +function glk_tick() { + /* Do nothing. */ +} + +function glk_gestalt(sel, val) { + return glk_gestalt_ext(sel, val, null); +} + +function glk_gestalt_ext(sel, val, arr) { + switch (sel) { + + case 0: // gestalt_Version + /* This implements Glk spec version 0.7.4. */ + return 0x00000704; + + case 1: // gestalt_CharInput + /* This is not a terrific approximation. Return false for function + keys, control keys, and the high-bit non-printables. For + everything else in the Unicode range, return true. */ + if (val <= Const.keycode_Left && val >= Const.keycode_End) + return 1; + if (val >= 0x100000000-Const.keycode_MAXVAL) + return 0; + if (val > 0x10FFFF) + return 0; + if ((val >= 0 && val < 32) || (val >= 127 && val < 160)) + return 0; + return 1; + + case 2: // gestalt_LineInput + /* Same as the above, except no special keys. */ + if (val > 0x10FFFF) + return 0; + if ((val >= 0 && val < 32) || (val >= 127 && val < 160)) + return 0; + return 1; + + case 3: // gestalt_CharOutput + /* Same thing again. We assume that all printable characters, + as well as the placeholders for nonprintables, are one character + wide. */ + if ((val > 0x10FFFF) + || (val >= 0 && val < 32) + || (val >= 127 && val < 160)) { + if (arr) + arr[0] = 1; + return 0; // gestalt_CharOutput_CannotPrint + } + if (arr) + arr[0] = 1; + return 2; // gestalt_CharOutput_ExactPrint + + case 4: // gestalt_MouseInput + if (val == Const.wintype_TextGrid) + return 1; + if (support.graphics && val == Const.wintype_Graphics) + return 1; + return 0; + + case 5: // gestalt_Timer + return support.timer || 0; + + case 6: // gestalt_Graphics + return support.graphics || 0; + + case 7: // gestalt_DrawImage + if (support.graphics && (val == Const.wintype_TextBuffer || val == Const.wintype_Graphics)) + return 1; + return 0; + + case 8: // gestalt_Sound + return 0; + + case 9: // gestalt_SoundVolume + return 0; + + case 10: // gestalt_SoundNotify + return 0; + + case 11: // gestalt_Hyperlinks + return support.hyperlinks || 0; + + case 12: // gestalt_HyperlinkInput + if (support.hyperlinks && (val == Const.wintype_TextBuffer || val == Const.wintype_TextGrid)) + return 1; + else + return 0; + + case 13: // gestalt_SoundMusic + return 0; + + case 14: // gestalt_GraphicsTransparency + return support.graphics || 0; + + case 15: // gestalt_Unicode + return 1; + + case 16: // gestalt_UnicodeNorm + return 1; + + case 17: // gestalt_LineInputEcho + return 1; + + case 18: // gestalt_LineTerminators + return 1; + + case 19: // gestalt_LineTerminatorKey + /* Really this result should be inspected from glkote.js. Since it + isn't, be sure to keep these values in sync with + terminator_key_names. */ + if (val == Const.keycode_Escape) + return 1; + if (val >= Const.keycode_Func12 && val <= Const.keycode_Func1) + return 1; + return 0; + + case 20: // gestalt_DateTime + return 1; + + case 21: // gestalt_Sound2 + return 0; + + case 22: // gestalt_ResourceStream + return 1; + + case 23: // gestalt_GraphicsCharInput + return 0; + + } + + if (option_glk_gestalt_hook) { + var res = option_glk_gestalt_hook(sel, val, arr); + if (res !== undefined) + return res; + } + + return 0; +} + +function glk_window_iterate(win, rockref) { + if (!win) + win = gli_windowlist; + else + win = win.next; + + if (win) { + if (rockref) + rockref.set_value(win.rock); + return win; + } + + if (rockref) + rockref.set_value(0); + return null; +} + +function glk_window_get_rock(win) { + if (!win) + throw('glk_window_get_rock: invalid window'); + return win.rock; +} + +function glk_window_get_root() { + return gli_rootwin; +} + +function glk_window_open(splitwin, method, size, wintype, rock) { + var oldparent, box, val; + var pairwin, newwin; + + if (!gli_rootwin) { + if (splitwin) + throw('glk_window_open: splitwin must be null for first window'); + + oldparent = null; + box = { + left: content_metrics.outspacingx, + top: content_metrics.outspacingy, + right: content_metrics.width-content_metrics.outspacingx, + bottom: content_metrics.height-content_metrics.outspacingy + }; + } + else { + if (!splitwin) + throw('glk_window_open: splitwin must not be null'); + + val = (method & Const.winmethod_DivisionMask); + if (val != Const.winmethod_Fixed && val != Const.winmethod_Proportional) + throw('glk_window_open: invalid method (not fixed or proportional)'); + + val = (method & Const.winmethod_DirMask); + if (val != Const.winmethod_Above && val != Const.winmethod_Below + && val != Const.winmethod_Left && val != Const.winmethod_Right) + throw('glk_window_open: invalid method (bad direction)'); + + box = splitwin.bbox; + + oldparent = splitwin.parent; + if (oldparent && oldparent.type != Const.wintype_Pair) + throw('glk_window_open: parent window is not Pair'); + } + + newwin = gli_new_window(wintype, rock); + + switch (newwin.type) { + case Const.wintype_TextBuffer: + /* accum is a list of strings of a given style; newly-printed text + is pushed onto the list. accumstyle is the style of that text. + Anything printed in a different style (or hyperlink value) + triggers a call to gli_window_buffer_deaccumulate, which cleans + out accum and adds the results to the content array. The content + is in GlkOte format. + */ + newwin.accum = []; + newwin.accumstyle = null; + newwin.accumhyperlink = 0; + newwin.content = []; + newwin.clearcontent = false; + newwin.reserve = []; /* autosave of recent content */ + break; + case Const.wintype_TextGrid: + /* lines is a list of line objects. A line looks like + { chars: [...], styles: [...], hyperlinks: [...], dirty: bool }. + */ + newwin.gridwidth = 0; + newwin.gridheight = 0; + newwin.lines = []; + newwin.cursorx = 0; + newwin.cursory = 0; + break; + case Const.wintype_Graphics: + if (!support.graphics) { + /* Graphics windows not supported; silently return null */ + gli_delete_window(newwin); + return null; + } + newwin.content = []; + newwin.reserve = []; /* autosave of recent content */ + break; + case Const.wintype_Blank: + break; + case Const.wintype_Pair: + throw('glk_window_open: cannot open pair window directly') + default: + /* Silently return null */ + gli_delete_window(newwin); + return null; + } + + if (!splitwin) { + gli_rootwin = newwin; + gli_window_rearrange(newwin, box); + } + else { + /* create pairwin, with newwin as the key */ + pairwin = gli_new_window(Const.wintype_Pair, 0); + pairwin.pair_dir = method & Const.winmethod_DirMask; + pairwin.pair_division = method & Const.winmethod_DivisionMask; + pairwin.pair_key = newwin; + pairwin.pair_keydamage = false; + pairwin.pair_size = size; + pairwin.pair_hasborder = ((method & Const.winmethod_BorderMask) == Const.winmethod_Border); + pairwin.pair_vertical = (pairwin.pair_dir == Const.winmethod_Left || pairwin.pair_dir == Const.winmethod_Right); + pairwin.pair_backward = (pairwin.pair_dir == Const.winmethod_Left || pairwin.pair_dir == Const.winmethod_Above); + + pairwin.child1 = splitwin; + pairwin.child2 = newwin; + splitwin.parent = pairwin; + newwin.parent = pairwin; + pairwin.parent = oldparent; + + if (oldparent) { + if (oldparent.child1 == splitwin) + oldparent.child1 = pairwin; + else + oldparent.child2 = pairwin; + } + else { + gli_rootwin = pairwin; + } + + gli_window_rearrange(pairwin, box); + } + + return newwin; +} + +function glk_window_close(win, statsref) { + if (!win) + throw('glk_window_close: invalid window'); + + if (win === gli_rootwin || !win.parent) { + /* close the root window, which means all windows. */ + + gli_rootwin = null; + + /* begin (simpler) closation */ + + gli_stream_fill_result(win.str, statsref); + gli_window_close(win, true); + } + else { + /* have to jigger parent */ + var pairwin, grandparwin, sibwin, box, wx, keydamage_flag; + + pairwin = win.parent; + if (win === pairwin.child1) + sibwin = pairwin.child2; + else if (win === pairwin.child2) + sibwin = pairwin.child1; + else + throw('glk_window_close: window tree is corrupted'); + + box = pairwin.bbox; + + grandparwin = pairwin.parent; + if (!grandparwin) { + gli_rootwin = sibwin; + sibwin.parent = null; + } + else { + if (grandparwin.child1 === pairwin) + grandparwin.child1 = sibwin; + else + grandparwin.child2 = sibwin; + sibwin.parent = grandparwin; + } + + /* Begin closation */ + + gli_stream_fill_result(win.str, statsref); + + /* Close the child window (and descendants), so that key-deletion can + crawl up the tree to the root window. */ + gli_window_close(win, true); + + /* This probably isn't necessary, but the child *is* gone, so just + in case. */ + if (win === pairwin.child1) { + pairwin.child1 = null; + } + else if (win === pairwin.child2) { + pairwin.child2 = null; + } + + /* Now we can delete the parent pair. */ + gli_window_close(pairwin, false); + + keydamage_flag = false; + for (wx=sibwin; wx; wx=wx.parent) { + if (wx.type == Const.wintype_Pair) { + if (wx.pair_keydamage) { + keydamage_flag = true; + wx.pair_keydamage = false; + } + } + } + + if (keydamage_flag) { + box = content_box; + gli_window_rearrange(gli_rootwin, box); + } + else { + gli_window_rearrange(sibwin, box); + } + } +} + +function glk_window_get_size(win, widthref, heightref) { + if (!win) + throw('glk_window_get_size: invalid window'); + + var wid = 0; + var hgt = 0; + var boxwidth, boxheight; + + switch (win.type) { + + case Const.wintype_TextGrid: + boxwidth = win.bbox.right - win.bbox.left; + boxheight = win.bbox.bottom - win.bbox.top; + wid = Math.max(0, Math.floor((boxwidth-content_metrics.gridmarginx) / content_metrics.gridcharwidth)); + hgt = Math.max(0, Math.floor((boxheight-content_metrics.gridmarginy) / content_metrics.gridcharheight)); + break; + + case Const.wintype_TextBuffer: + boxwidth = win.bbox.right - win.bbox.left; + boxheight = win.bbox.bottom - win.bbox.top; + wid = Math.max(0, Math.floor((boxwidth-content_metrics.buffermarginx) / content_metrics.buffercharwidth)); + hgt = Math.max(0, Math.floor((boxheight-content_metrics.buffermarginy) / content_metrics.buffercharheight)); + break; + + case Const.wintype_Graphics: + boxwidth = win.bbox.right - win.bbox.left; + boxheight = win.bbox.bottom - win.bbox.top; + wid = boxwidth - content_metrics.graphicsmarginx; + hgt = boxheight - content_metrics.graphicsmarginy; + break; + } + + if (widthref) + widthref.set_value(wid); + if (heightref) + heightref.set_value(hgt); +} + +function glk_window_set_arrangement(win, method, size, keywin) { + var wx, newdir, newvertical, newbackward; + + if (!win) + throw('glk_window_set_arrangement: invalid window'); + if (win.type != Const.wintype_Pair) + throw('glk_window_set_arrangement: not a pair window'); + + if (keywin) { + if (keywin.type == Const.wintype_Pair) + throw('glk_window_set_arrangement: keywin cannot be a pair window'); + for (wx=keywin; wx; wx=wx.parent) { + if (wx == win) + break; + } + if (!wx) + throw('glk_window_set_arrangement: keywin must be a descendant'); + } + + newdir = method & Const.winmethod_DirMask; + newvertical = (newdir == Const.winmethod_Left || newdir == Const.winmethod_Right); + newbackward = (newdir == Const.winmethod_Left || newdir == Const.winmethod_Above); + if (!keywin) + keywin = win.pair_key; + + if (newvertical && !win.pair_vertical) + throw('glk_window_set_arrangement: split must stay horizontal'); + if (!newvertical && win.pair_vertical) + throw('glk_window_set_arrangement: split must stay vertical'); + + if (keywin && keywin.type == Const.wintype_Blank + && (method & Const.winmethod_DivisionMask) == Const.winmethod_Fixed) + throw('glk_window_set_arrangement: a blank window cannot have a fixed size'); + + if ((newbackward && !win.pair_backward) || (!newbackward && win.pair_backward)) { + /* switch the children */ + wx = win.child1; + win.child1 = win.child2; + win.child2 = wx; + } + + /* set up everything else */ + win.pair_dir = newdir; + win.pair_division = (method & Const.winmethod_DivisionMask); + win.pair_key = keywin; + win.pair_size = size; + + win.pair_hasborder = ((method & Const.winmethod_BorderMask) == Const.winmethod_Border); + win.pair_vertical = (win.pair_dir == Const.winmethod_Left || win.pair_dir == Const.winmethod_Right); + win.pair_backward = (win.pair_dir == Const.winmethod_Left || win.pair_dir == Const.winmethod_Above); + + gli_window_rearrange(win, win.bbox); +} + +function glk_window_get_arrangement(win, methodref, sizeref, keywinref) { + if (!win) + throw('glk_window_get_arrangement: invalid window'); + if (win.type != Const.wintype_Pair) + throw('glk_window_get_arrangement: not a pair window'); + + if (sizeref) + sizeref.set_value(win.pair_size); + if (keywinref) + keywinref.set_value(win.pair_key); + if (methodref) + methodref.set_value(win.pair_dir | win.pair_division | (win.pair_hasborder ? Const.winmethod_Border : Const.winmethod_NoBorder)); +} + +function glk_window_get_type(win) { + if (!win) + throw('glk_window_get_type: invalid window'); + return win.type; +} + +function glk_window_get_parent(win) { + if (!win) + throw('glk_window_get_parent: invalid window'); + return win.parent; +} + +function glk_window_clear(win) { + var ix, cx, lineobj; + + if (!win) + throw('glk_window_clear: invalid window'); + + if (win.line_request) { + throw('glk_window_clear: window has pending line request'); + } + + switch (win.type) { + case Const.wintype_TextBuffer: + win.accum.length = 0; + win.accumstyle = null; + win.accumhyperlink = 0; + win.content.length = 0; + win.clearcontent = true; + break; + case Const.wintype_TextGrid: + win.cursorx = 0; + win.cursory = 0; + for (ix=0; ix str.bufeof) + pos = str.bufeof; + str.bufpos = pos; + } +} + +function glk_stream_get_position(str) { + if (!str) + throw('glk_stream_get_position: invalid stream'); + + switch (str.type) { + case strtype_File: + if (str.streaming) { + return str.fstream.ftell(); + } + /* fall through to memory... */ + case strtype_Resource: + /* fall through to memory... */ + case strtype_Memory: + return str.bufpos; + default: + return 0; + } +} + +function glk_stream_set_current(str) { + gli_currentstr = str; +} + +function glk_stream_get_current() { + return gli_currentstr; +} + +function glk_fileref_create_temp(usage, rock) { + var filetype = (usage & Const.fileusage_TypeMask); + var filetypename = FileTypeMap[filetype]; + var ref = Dialog.file_construct_temp_ref(filetypename); + fref = gli_new_fileref(ref.filename, usage, rock, ref); + return fref; +} + +function glk_fileref_create_by_name(usage, filename, rock) { + /* Filenames that do not come from the user must be cleaned up. */ + filename = Dialog.file_clean_fixed_name(filename, (usage & Const.fileusage_TypeMask)); + + fref = gli_new_fileref(filename, usage, rock, null); + return fref; +} + +function glk_fileref_create_by_prompt(usage, fmode, rock) { + var modename; + + var filetype = (usage & Const.fileusage_TypeMask); + var filetypename = FileTypeMap[filetype]; + if (!filetypename) { + filetypename = 'xxx'; + } + + switch (fmode) { + case Const.filemode_Write: + modename = 'write'; + break; + case Const.filemode_ReadWrite: + modename = 'readwrite'; + break; + case Const.filemode_WriteAppend: + modename = 'writeappend'; + break; + case Const.filemode_Read: + default: + modename = 'read'; + break; + } + + var special = { + type: 'fileref_prompt', + filetype: filetypename, + filemode: modename + }; + var callback = { + usage: usage, + rock: rock + }; + + if (filetype == Const.fileusage_SavedGame) + special.gameid = VM.get_signature(); + + ui_specialinput = special; + ui_specialcallback = callback; + gli_selectref = null; + return DidNotReturn; +} + +function gli_fileref_create_by_prompt_callback(obj) { + var ref = obj.value; + var usage = ui_specialcallback.usage; + var rock = ui_specialcallback.rock; + + var fref = null; + if (ref) { + fref = gli_new_fileref(ref.filename, usage, rock, ref); + } + + // If reading a file which doesn't exist, return null + if ( ui_specialinput.filemode === 'read' && !Dialog.file_ref_exists( fref.ref ) ) + { + glk_fileref_destroy( fref ); + fref = null; + } + + ui_specialinput = null; + ui_specialcallback = null; + + if (GiDispa) + GiDispa.prepare_resume(fref); + VM.resume(fref); +} + +function glk_fileref_destroy(fref) { + if (!fref) + throw('glk_fileref_destroy: invalid fileref'); + gli_delete_fileref(fref); +} + +function glk_fileref_iterate(fref, rockref) { + if (!fref) + fref = gli_filereflist; + else + fref = fref.next; + + if (fref) { + if (rockref) + rockref.set_value(fref.rock); + return fref; + } + + if (rockref) + rockref.set_value(0); + return null; +} + +function glk_fileref_get_rock(fref) { + if (!fref) + throw('glk_fileref_get_rock: invalid fileref'); + return fref.rock; +} + +function glk_fileref_delete_file(fref) { + if (!fref) + throw('glk_fileref_delete_file: invalid fileref'); + Dialog.file_remove_ref(fref.ref); +} + +function glk_fileref_does_file_exist(fref) { + if (!fref) + throw('glk_fileref_does_file_exist: invalid fileref'); + if (Dialog.file_ref_exists(fref.ref)) + return 1; + else + return 0; +} + +function glk_fileref_create_from_fileref(usage, oldfref, rock) { + if (!oldfref) + throw('glk_fileref_create_from_fileref: invalid fileref'); + + var fref = gli_new_fileref(oldfref.filename, usage, rock, null); + return fref; +} + +function glk_put_char(ch) { + gli_put_char(gli_currentstr, ch & 0xFF); +} + +function glk_put_char_stream(str, ch) { + gli_put_char(str, ch & 0xFF); +} + +function glk_put_string(val) { + glk_put_jstring_stream(gli_currentstr, val, true); +} + +function glk_put_string_stream(str, val) { + glk_put_jstring_stream(str, val, true); +} + +function glk_put_buffer(arr) { + arr = TrimArrayToBytes(arr); + gli_put_array(gli_currentstr, arr, true); +} + +function glk_put_buffer_stream(str, arr) { + arr = TrimArrayToBytes(arr); + gli_put_array(str, arr, true); +} + +function glk_set_style(val) { + gli_set_style(gli_currentstr, val); +} + +function glk_set_style_stream(str, val) { + gli_set_style(str, val); +} + +function glk_get_char_stream(str) { + if (!str) + throw('glk_get_char_stream: invalid stream'); + return gli_get_char(str, false); +} + +function glk_get_line_stream(str, buf) { + if (!str) + throw('glk_get_line_stream: invalid stream'); + return gli_get_line(str, buf, false); +} + +function glk_get_buffer_stream(str, buf) { + if (!str) + throw('glk_get_buffer_stream: invalid stream'); + return gli_get_buffer(str, buf, false); +} + +function glk_char_to_lower(val) { + if (val >= 0x41 && val <= 0x5A) + return val + 0x20; + if (val >= 0xC0 && val <= 0xDE && val != 0xD7) + return val + 0x20; + return val; +} + +function glk_char_to_upper(val) { + if (val >= 0x61 && val <= 0x7A) + return val - 0x20; + if (val >= 0xE0 && val <= 0xFE && val != 0xF7) + return val - 0x20; + return val; +} + +/* Style hints are not supported. We will use the new style system. */ +function glk_stylehint_set(wintype, styl, hint, value) { } +function glk_stylehint_clear(wintype, styl, hint) { } +function glk_style_distinguish(win, styl1, styl2) { + return 0; +} +function glk_style_measure(win, styl, hint, resultref) { + if (resultref) + resultref.set_value(0); + return 0; +} + +function glk_select(eventref) { + gli_selectref = eventref; + return DidNotReturn; +} + +function glk_select_poll(eventref) { + /* Because the Javascript interpreter is single-threaded, we cannot + have gotten a timer event since the last glk_select call. */ + + eventref.set_field(0, Const.evtype_None); + eventref.set_field(1, null); + eventref.set_field(2, 0); + eventref.set_field(3, 0); + + if (gli_timer_interval) { + var now = Date.now(); + if (now - gli_timer_started > gli_timer_interval) { + /* We're past the timer interval, even though we got no + event. Let's pretend we did, reset it, and return a + timer event. */ + gli_timer_started = Date.now(); + /* Resend timer request at next update. */ + gli_timer_lastsent = null; + + eventref.set_field(0, Const.evtype_Timer); + } + } +} + +function glk_request_line_event(win, buf, initlen) { + if (!win) + throw('glk_request_line_event: invalid window'); + if (win.char_request || win.line_request) + throw('glk_request_line_event: window already has keyboard request'); + + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + if (initlen) { + /* This will be copied into the next update. */ + var ls = buf.slice(0, initlen); + if (!current_partial_outputs) + current_partial_outputs = {}; + current_partial_outputs[win.disprock] = ByteArrayToString(ls); + } + win.line_request = true; + win.line_request_uni = false; + if (win.type == Const.wintype_TextBuffer) + win.request_echo_line_input = win.echo_line_input; + else + win.request_echo_line_input = true; + win.input_generation = event_generation; + win.linebuf = buf; + if (GiDispa) + GiDispa.retain_array(buf); + } + else { + throw('glk_request_line_event: window does not support keyboard input'); + } +} + +function glk_cancel_line_event(win, eventref) { + if (!win) + throw('glk_cancel_line_event: invalid window'); + + if (!win.line_request) { + if (eventref) { + eventref.set_field(0, Const.evtype_None); + eventref.set_field(1, null); + eventref.set_field(2, 0); + eventref.set_field(3, 0); + } + return; + } + + var input = ""; + var ix, val; + + if (current_partial_inputs) { + val = current_partial_inputs[win.disprock]; + if (val) + input = val; + } + + if (input.length > win.linebuf.length) + input = input.slice(0, win.linebuf.length); + + if (win.request_echo_line_input) { + ix = win.style; + gli_set_style(win.str, Const.style_Input); + gli_window_put_string(win, input); + if (win.echostr) + glk_put_jstring_stream(win.echostr, input); + gli_set_style(win.str, ix); + gli_window_put_string(win, "\n"); + if (win.echostr) + glk_put_jstring_stream(win.echostr, "\n"); + } + + for (ix=0; ix= pos) + break; + grpstart = ix; + while (ix < pos && unicode_combin_table[arr[ix]]) + ix++; + grpend = ix; + if (grpend - grpstart >= 2) { + /* Sort this group. */ + for (jx = grpend-1; jx > grpstart; jx--) { + for (kx = grpstart; kx < jx; kx++) { + if (unicode_combin_table[arr[kx]] > unicode_combin_table[arr[kx+1]]) { + tmp = arr[kx]; + arr[kx] = arr[kx+1]; + arr[kx+1] = tmp; + } + } + } + } + } + + return pos; +} + +function gli_buffer_canon_compose_uni(arr, numchars) { + /* The algorithm for canonically composing characters in a string: + for each base character, compare it to all the following + combining characters (up to the next base character). If they're + composable, compose them. Repeat until no more pairs are found. */ + + var ix, jx, curch, newch, curclass, newclass, map, pos; + + if (numchars == 0) + return 0; + + pos = 0; + curch = arr[0]; + curclass = unicode_combin_table[curch]; + if (curclass) + curclass = 999; // just in case the first character is a combiner + ix = 1; + jx = ix; + while (true) { + if (jx >= numchars) { + arr[pos] = curch; + pos = ix; + break; + } + newch = arr[jx]; + newclass = unicode_combin_table[newch]; + map = unicode_compo_table[curch]; + if (map !== undefined && map[newch] !== undefined + && (!curclass || (newclass && curclass < newclass))) { + curch = map[newch]; + arr[pos] = curch; + } + else { + if (!newclass) { + pos = ix; + curch = newch; + } + curclass = newclass; + arr[ix] = newch; + ix++; + } + jx++; + } + + return pos; +} + +function glk_buffer_canon_decompose_uni(arr, numchars) { + var arrlen = arr.length; + var len; + + len = gli_buffer_canon_decompose_uni(arr, numchars); + + /* in case we stretched the array */ + arr.length = arrlen; + + return len; +} + +function glk_buffer_canon_normalize_uni(arr, numchars) { + var arrlen = arr.length; + var len; + + len = gli_buffer_canon_decompose_uni(arr, numchars); + len = gli_buffer_canon_compose_uni(arr, len); + + /* in case we stretched the array */ + arr.length = arrlen; + + return len; +} + +function glk_put_char_uni(ch) { + gli_put_char(gli_currentstr, ch); +} + +function glk_put_string_uni(val) { + glk_put_jstring_stream(gli_currentstr, val, false); +} + +function glk_put_buffer_uni(arr) { + gli_put_array(gli_currentstr, arr, false); +} + +function glk_put_char_stream_uni(str, ch) { + gli_put_char(str, ch); +} + +function glk_put_string_stream_uni(str, val) { + glk_put_jstring_stream(str, val, false); +} + +function glk_put_buffer_stream_uni(str, arr) { + gli_put_array(str, arr, false); +} + +function glk_get_char_stream_uni(str) { + if (!str) + throw('glk_get_char_stream_uni: invalid stream'); + return gli_get_char(str, true); +} + +function glk_get_buffer_stream_uni(str, buf) { + if (!str) + throw('glk_get_buffer_stream_uni: invalid stream'); + return gli_get_buffer(str, buf, true); +} + +function glk_get_line_stream_uni(str, buf) { + if (!str) + throw('glk_get_line_stream_uni: invalid stream'); + return gli_get_line(str, buf, true); +} + +function glk_stream_open_file_uni(fref, fmode, rock) { + if (!fref) + throw('glk_stream_open_file_uni: invalid fileref'); + + var str; + var fstream; + + if (fmode != Const.filemode_Read + && fmode != Const.filemode_Write + && fmode != Const.filemode_ReadWrite + && fmode != Const.filemode_WriteAppend) + throw('glk_stream_open_file_uni: illegal filemode'); + + if (fmode == Const.filemode_Read && !Dialog.file_ref_exists(fref.ref)) + return null; + + if (!Dialog.streaming) { + var content = null; + if (fmode != Const.filemode_Write) { + content = Dialog.file_read(fref.ref); + } + if (content == null) { + content = []; + if (fmode != Const.filemode_Read) { + /* We just created this file. (Or perhaps we're in Write mode + and we're truncating.) Write immediately, to create it and + get the creation date right. */ + Dialog.file_write(fref.ref, '', true); + } + } + if (content.length == null) + throw('glk_stream_open_file_uni: data read had no length'); + } + else { + fstream = Dialog.file_fopen(fmode, fref.ref); + if (!fstream) + return null; + } + + str = gli_new_stream(strtype_File, + (fmode != Const.filemode_Write), + (fmode != Const.filemode_Read), + rock); + str.unicode = true; + str.isbinary = !fref.textmode; + str.ref = fref.ref; + str.origfmode = fmode; + + if (!Dialog.streaming) { + str.streaming = false; + str.buf = content; + str.buflen = 0xFFFFFFFF; /* enormous */ + if (fmode == Const.filemode_Write) + str.bufeof = 0; + else + str.bufeof = content.length; + if (fmode == Const.filemode_WriteAppend) + str.bufpos = str.bufeof; + else + str.bufpos = 0; + } + else { + str.streaming = true; + str.fstream = fstream; + /* We'll want a Buffer object around for short and writes. */ + str.buffer4 = fstream.BufferClass.alloc(4); + } + + return str; +} + +function glk_stream_open_memory_uni(buf, fmode, rock) { + var str; + + if (fmode != Const.filemode_Read + && fmode != Const.filemode_Write + && fmode != Const.filemode_ReadWrite) + throw('glk_stream_open_memory: illegal filemode'); + + str = gli_new_stream(strtype_Memory, + (fmode != Const.filemode_Write), + (fmode != Const.filemode_Read), + rock); + str.unicode = true; + + if (buf) { + str.buf = buf; + str.buflen = buf.length; + str.bufpos = 0; + if (fmode == Const.filemode_Write) + str.bufeof = 0; + else + str.bufeof = str.buflen; + if (GiDispa) + GiDispa.retain_array(buf); + } + + return str; +} + +function glk_request_char_event_uni(win) { + if (!win) + throw('glk_request_char_event: invalid window'); + if (win.char_request || win.line_request) + throw('glk_request_char_event: window already has keyboard request'); + + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + win.char_request = true; + win.char_request_uni = true; + win.input_generation = event_generation; + } + else { + /* ### wintype_Graphics could accept char input if we set up the focus to allow it. See gestalt_GraphicsCharInput. */ + throw('glk_request_char_event: window does not support keyboard input'); + } +} + +function glk_request_line_event_uni(win, buf, initlen) { + if (!win) + throw('glk_request_line_event: invalid window'); + if (win.char_request || win.line_request) + throw('glk_request_line_event: window already has keyboard request'); + + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + if (initlen) { + /* This will be copied into the next update. */ + var ls = buf.slice(0, initlen); + if (!current_partial_outputs) + current_partial_outputs = {}; + current_partial_outputs[win.disprock] = UniArrayToString(ls); + } + win.line_request = true; + win.line_request_uni = true; + if (win.type == Const.wintype_TextBuffer) + win.request_echo_line_input = win.echo_line_input; + else + win.request_echo_line_input = true; + win.input_generation = event_generation; + win.linebuf = buf; + if (GiDispa) + GiDispa.retain_array(buf); + } + else { + throw('glk_request_line_event: window does not support keyboard input'); + } +} + +function glk_current_time(timevalref) { + var now = new Date().getTime(); + var usec; + + timevalref.set_field(0, Math.floor(now / 4294967296000)); + timevalref.set_field(1, Math.floor(now / 1000) >>>0); + usec = Math.floor((now % 1000) * 1000); + if (usec < 0) + usec = 1000000 + usec; + timevalref.set_field(2, usec); +} + +function glk_current_simple_time(factor) { + var now = new Date().getTime(); + return Math.floor(now / (factor * 1000)); +} + +function glk_time_to_date_utc(timevalref, dateref) { + var now = timevalref.get_field(0) * 4294967296000 + timevalref.get_field(1) * 1000 + timevalref.get_field(2) / 1000; + var obj = new Date(now); + + dateref.set_field(0, obj.getUTCFullYear()) + dateref.set_field(1, 1+obj.getUTCMonth()) + dateref.set_field(2, obj.getUTCDate()) + dateref.set_field(3, obj.getUTCDay()) + dateref.set_field(4, obj.getUTCHours()) + dateref.set_field(5, obj.getUTCMinutes()) + dateref.set_field(6, obj.getUTCSeconds()) + dateref.set_field(7, 1000*obj.getUTCMilliseconds()) +} + +function glk_time_to_date_local(timevalref, dateref) { + var now = timevalref.get_field(0) * 4294967296000 + timevalref.get_field(1) * 1000 + timevalref.get_field(2) / 1000; + var obj = new Date(now); + + dateref.set_field(0, obj.getFullYear()) + dateref.set_field(1, 1+obj.getMonth()) + dateref.set_field(2, obj.getDate()) + dateref.set_field(3, obj.getDay()) + dateref.set_field(4, obj.getHours()) + dateref.set_field(5, obj.getMinutes()) + dateref.set_field(6, obj.getSeconds()) + dateref.set_field(7, 1000*obj.getMilliseconds()) +} + +function glk_simple_time_to_date_utc(time, factor, dateref) { + var now = time*(1000*factor); + var obj = new Date(now); + + dateref.set_field(0, obj.getUTCFullYear()) + dateref.set_field(1, 1+obj.getUTCMonth()) + dateref.set_field(2, obj.getUTCDate()) + dateref.set_field(3, obj.getUTCDay()) + dateref.set_field(4, obj.getUTCHours()) + dateref.set_field(5, obj.getUTCMinutes()) + dateref.set_field(6, obj.getUTCSeconds()) + dateref.set_field(7, 1000*obj.getUTCMilliseconds()) +} + +function glk_simple_time_to_date_local(time, factor, dateref) { + var now = time*(1000*factor); + var obj = new Date(now); + + dateref.set_field(0, obj.getFullYear()) + dateref.set_field(1, 1+obj.getMonth()) + dateref.set_field(2, obj.getDate()) + dateref.set_field(3, obj.getDay()) + dateref.set_field(4, obj.getHours()) + dateref.set_field(5, obj.getMinutes()) + dateref.set_field(6, obj.getSeconds()) + dateref.set_field(7, 1000*obj.getMilliseconds()) +} + +function glk_date_to_time_utc(dateref, timevalref) { + var obj = new Date(0); + + obj.setUTCFullYear(dateref.get_field(0)); + obj.setUTCMonth(dateref.get_field(1)-1); + obj.setUTCDate(dateref.get_field(2)); + obj.setUTCHours(dateref.get_field(4)); + obj.setUTCMinutes(dateref.get_field(5)); + obj.setUTCSeconds(dateref.get_field(6)); + obj.setUTCMilliseconds(dateref.get_field(7)/1000); + + var now = obj.getTime(); + var usec; + + timevalref.set_field(0, Math.floor(now / 4294967296000)); + timevalref.set_field(1, Math.floor(now / 1000) >>>0); + usec = Math.floor((now % 1000) * 1000); + if (usec < 0) + usec = 1000000 + usec; + timevalref.set_field(2, usec); +} + +function glk_date_to_time_local(dateref, timevalref) { + var obj = new Date( + dateref.get_field(0), dateref.get_field(1)-1, dateref.get_field(2), + dateref.get_field(4), dateref.get_field(5), dateref.get_field(6), + dateref.get_field(7)/1000); + + var now = obj.getTime(); + var usec; + + timevalref.set_field(0, Math.floor(now / 4294967296000)); + timevalref.set_field(1, Math.floor(now / 1000) >>>0); + usec = Math.floor((now % 1000) * 1000); + if (usec < 0) + usec = 1000000 + usec; + timevalref.set_field(2, usec); +} + +function glk_date_to_simple_time_utc(dateref, factor) { + var obj = new Date(0); + + obj.setUTCFullYear(dateref.get_field(0)); + obj.setUTCMonth(dateref.get_field(1)-1); + obj.setUTCDate(dateref.get_field(2)); + obj.setUTCHours(dateref.get_field(4)); + obj.setUTCMinutes(dateref.get_field(5)); + obj.setUTCSeconds(dateref.get_field(6)); + obj.setUTCMilliseconds(dateref.get_field(7)/1000); + + var now = obj.getTime(); + return Math.floor(now / (factor * 1000)); +} + +function glk_date_to_simple_time_local(dateref, factor) { + var obj = new Date( + dateref.get_field(0), dateref.get_field(1)-1, dateref.get_field(2), + dateref.get_field(4), dateref.get_field(5), dateref.get_field(6), + dateref.get_field(7)/1000); + + var now = obj.getTime(); + return Math.floor(now / (factor * 1000)); +} + +/* End of Glk namespace function. Return the object which will + become the Glk global. */ +var api = { + version: '2.2.3', /* GlkOte/GlkApi version */ + set_references: set_references, + init : init, + update : update, + save_allstate : save_allstate, + restore_allstate : restore_allstate, + fatal_error : fatal_error, + + byte_array_to_string : ByteArrayToString, + uni_array_to_string : UniArrayToString, + Const : Const, + RefBox : RefBox, + RefStruct : RefStruct, + DidNotReturn : DidNotReturn, + call_may_not_return : call_may_not_return, + + glk_put_jstring : glk_put_jstring, + glk_put_jstring_stream : glk_put_jstring_stream, + + glk_exit : glk_exit, + glk_tick : glk_tick, + glk_gestalt : glk_gestalt, + glk_gestalt_ext : glk_gestalt_ext, + glk_window_iterate : glk_window_iterate, + glk_window_get_rock : glk_window_get_rock, + glk_window_get_root : glk_window_get_root, + glk_window_open : glk_window_open, + glk_window_close : glk_window_close, + glk_window_get_size : glk_window_get_size, + glk_window_set_arrangement : glk_window_set_arrangement, + glk_window_get_arrangement : glk_window_get_arrangement, + glk_window_get_type : glk_window_get_type, + glk_window_get_parent : glk_window_get_parent, + glk_window_clear : glk_window_clear, + glk_window_move_cursor : glk_window_move_cursor, + glk_window_get_stream : glk_window_get_stream, + glk_window_set_echo_stream : glk_window_set_echo_stream, + glk_window_get_echo_stream : glk_window_get_echo_stream, + glk_set_window : glk_set_window, + glk_window_get_sibling : glk_window_get_sibling, + glk_stream_iterate : glk_stream_iterate, + glk_stream_get_rock : glk_stream_get_rock, + glk_stream_open_file : glk_stream_open_file, + glk_stream_open_memory : glk_stream_open_memory, + glk_stream_close : glk_stream_close, + glk_stream_set_position : glk_stream_set_position, + glk_stream_get_position : glk_stream_get_position, + glk_stream_set_current : glk_stream_set_current, + glk_stream_get_current : glk_stream_get_current, + glk_fileref_create_temp : glk_fileref_create_temp, + glk_fileref_create_by_name : glk_fileref_create_by_name, + glk_fileref_create_by_prompt : glk_fileref_create_by_prompt, + glk_fileref_destroy : glk_fileref_destroy, + glk_fileref_iterate : glk_fileref_iterate, + glk_fileref_get_rock : glk_fileref_get_rock, + glk_fileref_delete_file : glk_fileref_delete_file, + glk_fileref_does_file_exist : glk_fileref_does_file_exist, + glk_fileref_create_from_fileref : glk_fileref_create_from_fileref, + glk_put_char : glk_put_char, + glk_put_char_stream : glk_put_char_stream, + glk_put_string : glk_put_string, + glk_put_string_stream : glk_put_string_stream, + glk_put_buffer : glk_put_buffer, + glk_put_buffer_stream : glk_put_buffer_stream, + glk_set_style : glk_set_style, + glk_set_style_stream : glk_set_style_stream, + glk_get_char_stream : glk_get_char_stream, + glk_get_line_stream : glk_get_line_stream, + glk_get_buffer_stream : glk_get_buffer_stream, + glk_char_to_lower : glk_char_to_lower, + glk_char_to_upper : glk_char_to_upper, + glk_stylehint_set : glk_stylehint_set, + glk_stylehint_clear : glk_stylehint_clear, + glk_style_distinguish : glk_style_distinguish, + glk_style_measure : glk_style_measure, + glk_select : glk_select, + glk_select_poll : glk_select_poll, + glk_request_line_event : glk_request_line_event, + glk_cancel_line_event : glk_cancel_line_event, + glk_request_char_event : glk_request_char_event, + glk_cancel_char_event : glk_cancel_char_event, + glk_request_mouse_event : glk_request_mouse_event, + glk_cancel_mouse_event : glk_cancel_mouse_event, + glk_request_timer_events : glk_request_timer_events, + glk_image_get_info : glk_image_get_info, + glk_image_draw : glk_image_draw, + glk_image_draw_scaled : glk_image_draw_scaled, + glk_window_flow_break : glk_window_flow_break, + glk_window_erase_rect : glk_window_erase_rect, + glk_window_fill_rect : glk_window_fill_rect, + glk_window_set_background_color : glk_window_set_background_color, + glk_schannel_iterate : glk_schannel_iterate, + glk_schannel_get_rock : glk_schannel_get_rock, + glk_schannel_create : glk_schannel_create, + glk_schannel_destroy : glk_schannel_destroy, + glk_schannel_play : glk_schannel_play, + glk_schannel_play_ext : glk_schannel_play_ext, + glk_schannel_stop : glk_schannel_stop, + glk_schannel_set_volume : glk_schannel_set_volume, + glk_schannel_create_ext : glk_schannel_create_ext, + glk_schannel_play_multi : glk_schannel_play_multi, + glk_schannel_pause : glk_schannel_pause, + glk_schannel_unpause : glk_schannel_unpause, + glk_schannel_set_volume_ext : glk_schannel_set_volume_ext, + glk_sound_load_hint : glk_sound_load_hint, + glk_set_hyperlink : glk_set_hyperlink, + glk_set_hyperlink_stream : glk_set_hyperlink_stream, + glk_request_hyperlink_event : glk_request_hyperlink_event, + glk_cancel_hyperlink_event : glk_cancel_hyperlink_event, + glk_buffer_to_lower_case_uni : glk_buffer_to_lower_case_uni, + glk_buffer_to_upper_case_uni : glk_buffer_to_upper_case_uni, + glk_buffer_to_title_case_uni : glk_buffer_to_title_case_uni, + glk_buffer_canon_decompose_uni : glk_buffer_canon_decompose_uni, + glk_buffer_canon_normalize_uni : glk_buffer_canon_normalize_uni, + glk_put_char_uni : glk_put_char_uni, + glk_put_string_uni : glk_put_string_uni, + glk_put_buffer_uni : glk_put_buffer_uni, + glk_put_char_stream_uni : glk_put_char_stream_uni, + glk_put_string_stream_uni : glk_put_string_stream_uni, + glk_put_buffer_stream_uni : glk_put_buffer_stream_uni, + glk_get_char_stream_uni : glk_get_char_stream_uni, + glk_get_buffer_stream_uni : glk_get_buffer_stream_uni, + glk_get_line_stream_uni : glk_get_line_stream_uni, + glk_stream_open_file_uni : glk_stream_open_file_uni, + glk_stream_open_memory_uni : glk_stream_open_memory_uni, + glk_request_char_event_uni : glk_request_char_event_uni, + glk_request_line_event_uni : glk_request_line_event_uni, + glk_set_echo_line_event : glk_set_echo_line_event, + glk_set_terminators_line_event : glk_set_terminators_line_event, + glk_current_time : glk_current_time, + glk_current_simple_time : glk_current_simple_time, + glk_time_to_date_utc : glk_time_to_date_utc, + glk_time_to_date_local : glk_time_to_date_local, + glk_simple_time_to_date_utc : glk_simple_time_to_date_utc, + glk_simple_time_to_date_local : glk_simple_time_to_date_local, + glk_date_to_time_utc : glk_date_to_time_utc, + glk_date_to_time_local : glk_date_to_time_local, + glk_date_to_simple_time_utc : glk_date_to_simple_time_utc, + glk_date_to_simple_time_local : glk_date_to_simple_time_local, + glk_stream_open_resource : glk_stream_open_resource, + glk_stream_open_resource_uni : glk_stream_open_resource_uni +}; + +if (typeof module !== 'undefined' && module.exports) { + module.exports = api; +} + +return api; + +}(); + +/* End of Glk library. */ \ No newline at end of file diff --git a/src/glkOte/glkote-term.js b/src/glkOte/glkote-term.js new file mode 100644 index 0000000..147e3cf --- /dev/null +++ b/src/glkOte/glkote-term.js @@ -0,0 +1,145 @@ +class GlkOte { + constructor() { + this.current_metrics = null + this.disabled = false + this.generation = 0 + this.interface = null + this.version = require('../../package.json').version + } + + measure_window() { + return { + buffercharheight: 1, + buffercharwidth: 1, + buffermarginx: 0, + buffermarginy: 0, + graphicsmarginx: 0, + graphicsmarginy: 0, + gridcharheight: 1, + gridcharwidth: 1, + gridmarginx: 0, + gridmarginy: 0, + height: 0, + inspacingx: 0, + inspacingy: 0, + outspacingx: 0, + outspacingy: 0, + width: 0, + } + } + + getinterface() { + return this.interface + } + + init(iface) { + if (!iface) { + this.error('No game interface object has been provided.') + } + if (!iface.accept) { + this.error('The game interface object must have an accept() function.') + } + + this.interface = iface + this.current_metrics = this.measure_window() + + this.send_response('init', null, this.current_metrics) + } + + update(data) { + if (data.type === 'error') { + this.error(data.message) + } + if (data.type === 'pass') { + return + } + if (data.type !== 'update' && data.type !== 'exit') { + this.log(`Ignoring unknown message type: ${data.type}`) + return + } + if (data.gen === this.generation) { + this.log(`Ignoring repeated generation number: ${data.gen}`) + return + } + if (data.gen < this.generation) { + this.log( + `Ignoring out-of-order generation number: got ${data.gen}, currently at ${this.generation}` + ) + return + } + this.generation = data.gen + + if (this.disabled) { + this.disable(false) + } + + /* Handle the update */ + if (data.input != null) { + this.cancel_inputs(data.input) + } + if (data.windows != null) { + this.update_windows(data.windows) + } + if (data.content != null && data.content.length) { + this.update_content(data.content) + } + if (data.input != null) { + this.update_inputs(data.input) + } + + /* Disable everything if requested */ + this.disabled = false + if (data.disabled || data.specialinput) { + this.disable(true) + } + + if (data.specialinput != null) { + this.accept_specialinput(data.specialinput) + } + + /* Detach all handlers and exit */ + if (data.type === 'exit') { + this.exit() + } + } + + send_response(type, win, val, val2) { + const res = { + type: type, + gen: this.generation, + } + + if (win) { + res.window = win.id + } + + if (type === 'init' || type === 'arrange') { + res.metrics = val + } + + if (type === 'init') { + res.support = this.support() + } + + if (type === 'char') { + res.value = val + } + + if (type === 'line') { + res.value = val + } + + if (type === 'specialresponse') { + res.response = val + res.value = val2 + } + + this.interface.accept(res) + } + + support() { + return [] + } +} + +module.exports = GlkOte diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..300e9a3 --- /dev/null +++ b/src/index.js @@ -0,0 +1,33 @@ +const FakeDialog = require('./fakeDialog') +const CheapGlkOte = require('./cheapGlkOte') + +const noop = () => void null + +const noopHandlers = [ + 'onInit', + 'onUpdateContent', + 'onDisable', + 'onUpdateInputs', + 'onFileNameRequest', + 'onExit', + 'setSend' +].reduce((acc, x) => (acc[x] = noop, acc), {}) + +module.exports = handlers_ => { + const handlers = + Object.assign({}, noopHandlers, handlers_) + + const Dialog = new FakeDialog(handlers) + const GlkOte = new CheapGlkOte(handlers) + + const sendFn = GlkOte.sendFn.bind(GlkOte) + + return { + sendFn, + glkInterface: { + Dialog, + GlkOte, + Glk: {} + } + } +} diff --git a/src/stdio.js b/src/stdio.js new file mode 100644 index 0000000..d9b40b2 --- /dev/null +++ b/src/stdio.js @@ -0,0 +1,133 @@ +const readline = require('readline') +const MuteStream = require('mute-stream') +const ansiEscapes = require('ansi-escapes') + +const stdin = process.stdin +const stdout = new MuteStream() +stdout.pipe(process.stdout) +const rl = readline.createInterface({ + input: stdin, + output: stdout, + prompt: '' +}) + +let send = _ => _ + +const setSend = fn => { + send = fn +} + +const onInit = () => { + if (stdin.isTTY) { + stdin.setRawMode(true) + } + readline.emitKeypressEvents(stdin) + rl.resume() +} + +const onUpdateContent = messages => + messages.text.forEach(({append, content}) => { + if (!append) { + stdout.write('\n') + } + + if (content) { + content.forEach(x => { + if (x.text === '>') return null + + if (x.style === 'input') { + if (stdout.isTTY) { + stdout.write(ansiEscapes.eraseLines(2)) + stdout.write('> ') + } + } + + stdout.write( + typeof x === 'string' + ? x + : x.text) + }) + } + }) + +const onUpdateInputs = type => { + type + ? attach_handlers(type) + : detach_handlers() +} + +const onExit = () => { + detach_handlers() + rl.close() + stdout.write('\n') +} + +const onDisable = disable => + disable + ? detach_handlers() + : attach_handlers() + +const onFileNameRequest = (tosave, usage, callback) => { + stdout.write('\n') + rl.question( + 'Please enter a file name (without an extension): ', + filename => callback(filename + ? {filename, usage} + : null)) +} + +const handle_char_input = (str, key) => { + const key_replacements = { + '\x7F': 'delete', + '\t': 'tab', + } + + detach_handlers() + + // Make sure this char isn't being remembered for the next line input + rl._line_buffer = null + rl.line = '' + + // Process special keys + const res = + key_replacements[str] || + str || + key.name.replace(/f(\d+)/, 'func$1') + + send(res) +} + +const attach_handlers = type => { + if (type === 'char') { + stdout.mute() + stdin.on('keypress', handle_char_input) + } + if (type === 'line') { + rl.on('line', handle_line_input) + } +} + +const detach_handlers = () => { + stdin.removeListener('keypress', handle_char_input) + rl.removeListener('line', handle_line_input) + stdout.unmute() +} + +const handle_line_input = line => { + if (stdout.isTTY) { + stdout.write(ansiEscapes.eraseLines(1)) + } + detach_handlers() + + send(line) +} + +module.exports.handlers = { + onInit, + onUpdateContent, + onDisable, + onUpdateInputs, + onFileNameRequest, + onExit, + setSend +} diff --git a/tests/player.stdio.js b/tests/player.stdio.js new file mode 100644 index 0000000..3f6d375 --- /dev/null +++ b/tests/player.stdio.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +/* + +Emglken runner +============== + +Copyright (c) 2020 Dannii Willis +MIT licenced +https://github.com/curiousdannii/emglken + +*/ + +const fs = require('fs') +const minimist = require('minimist') + +const CheapGlkOte = require('../src/') +const { handlers } = require('../src/stdio') + +const formats = [ + { + id: 'bocfel', + extensions: /z([3458]|blorb)$/, + engine: 'bocfel.js', + }, + + { + id: 'glulxe', + extensions: /(gblorb|ulx)$/, + engine: 'glulxe.js', + }, + + { + id: 'git', + extensions: /(gblorb|ulx)$/, + engine: 'git.js', + }, + + { + id: 'hugo', + extensions: /hex$/, + engine: 'hugo.js', + }, + + { + id: 'tads', + extensions: /(gam|t3)$/, + engine: 'tads.js', + }, +] + +function run() { + const argv = minimist(process.argv.slice(2)) + + const storyfile = argv._[0] + + let format + for (const formatspec of formats) { + if ( + formatspec.id === argv.vm || + (!argv.vm && formatspec.extensions.test(storyfile)) + ) { + format = formatspec + break + } + } + + if (!format) { + console.error('Unknown storyfile format') + return + } + + const { glkInterface, sendFn } = CheapGlkOte(handlers) + handlers.setSend(sendFn) + + const engine = require('emglken/src/' + format.engine) + const vm = new engine() + vm.prepare(fs.readFileSync(storyfile), glkInterface) + vm.start() +} + +run() diff --git a/tests/praxix.z5 b/tests/praxix.z5 new file mode 100644 index 0000000000000000000000000000000000000000..87f83a7ab4e33422807d0eff15d7b80c8e06771d GIT binary patch literal 31744 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 L>%adiHSj+GOSf>$ literal 0 HcmV?d00001 diff --git a/tests/runtests.sh b/tests/runtests.sh new file mode 100755 index 0000000..64947a7 --- /dev/null +++ b/tests/runtests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +node player.stdio.js praxix.z5 < <(sleep 1 && echo 'all')