cheap-glkote
Version:
Abstract JavaScript implementation of GlkOte
1,556 lines (1,364 loc) • 215 kB
JavaScript
'use strict';
/* GlkAPI -- a Javascript Glk API for IF interfaces
* GlkOte Library: version 2.3.2.
* Glk API which this implements: version 0.7.4.
* Designed by Andrew Plotkin <erkyrath@eblong.com>
* <http://eblong.com/zarf/glk/glkote.html>
*
* This Javascript library is copyright 2010-20 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.
*/
/* All state is contained in GlkClass. */
var GlkClass = function() {
var GlkOte = null; /* imported API object */
var VM = null; /* imported API object (the VM interface) */
var GiDispa = null; /* imported API object (the dispatch layer) */
var Blorb = null; /* imported API object (the resource layer) */
/* Environment capabilities. (Checked at init time.) */
var has_canvas;
/* 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;
/* 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) {
/* Either GlkOte was passed in or we must create one. */
if (vm_options.GlkOte) {
GlkOte = vm_options.GlkOte;
}
else if (window.GlkOteClass) {
GlkOte = new window.GlkOteClass();
}
/* Either Blorb was passed in or we don't have one. */
if (vm_options.Blorb) {
Blorb = vm_options.Blorb;
}
/* Check for canvas support. We don't rely on jquery here. */
has_canvas = (document.createElement('canvas').getContext != undefined);
VM = vm_options.vm;
GiDispa = vm_options.GiDispa; /* may be null/undefined */
vm_options.accept = accept_ui_event;
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();
}
/* Initialize the lower levels. */
if (GiDispa)
GiDispa.init({ io:vm_options.io, vm:vm_options.vm });
GlkOte.init(vm_options);
}
function is_inited() {
return (VM != null && GlkOte != null);
}
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 = complete_metrics(obj.metrics);
/* We ignore the support array. This library is updated in sync
with GlkOte, so we know what it supports. */
VM.start();
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 = complete_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;
}
}
/* Given a partial metrics object, return one with all the required
values. Missing values will default to 0 or the standard inherited
terms. (E.g., if "inspacingx" is missing it will default to
"inspacing", then "spacing", then 0. See measure_window() in
glkote.js or data_metrics_parse() in RemGlk.)
All values in the given object will be copied over; defaulting only
applies to missing values from the required set.
*/
function complete_metrics(metrics) {
// Default values if absolutely nothing is specified.
var res = {
width: 80,
height: 50,
gridcharwidth: 1,
gridcharheight: 1,
buffercharwidth: 1,
buffercharheight: 1,
gridmarginx: 0,
gridmarginy: 0,
buffermarginx: 0,
buffermarginy: 0,
graphicsmarginx: 0,
graphicsmarginy: 0,
outspacingx: 0,
outspacingy: 0,
inspacingx: 0,
inspacingy: 0,
};
// Various ways of specifying defaults.
var val;
val = metrics.charwidth;
if (val !== undefined) {
res.gridcharwidth = val;
res.buffercharwidth = val;
}
val = metrics.charheight;
if (val !== undefined) {
res.gridcharheight = val;
res.buffercharheight = val;
}
val = metrics.margin;
if (val !== undefined) {
res.gridmarginx = val;
res.gridmarginy = val;
res.buffermarginx = val;
res.buffermarginy = val;
res.graphicsmarginx = val;
res.graphicsmarginy = val;
}
val = metrics.gridmargin;
if (val !== undefined) {
res.gridmarginx = val;
res.gridmarginy = val;
}
val = metrics.buffermargin;
if (val !== undefined) {
res.buffermarginx = val;
res.buffermarginy = val;
}
val = metrics.graphicsmargin;
if (val !== undefined) {
res.graphicsmarginx = val;
res.graphicsmarginy = val;
}
val = metrics.marginx;
if (val !== undefined) {
res.gridmarginx = val;
res.buffermarginx = val;
res.graphicsmarginx = val;
}
val = metrics.marginy;
if (val !== undefined) {
res.gridmarginy = val;
res.buffermarginy = val;
res.graphicsmarginy = val;
}
val = metrics.spacing;
if (val !== undefined) {
res.inspacingx = val;
res.inspacingy = val;
res.outspacingx = val;
res.outspacingy = val;
}
val = metrics.inspacing;
if (val !== undefined) {
res.inspacingx = val;
res.inspacingy = val;
}
val = metrics.outspacing;
if (val !== undefined) {
res.outspacingx = val;
res.outspacingy = val;
}
val = metrics.spacingx;
if (val !== undefined) {
res.inspacingx = val;
res.outspacingx = val;
}
val = metrics.spacingy;
if (val !== undefined) {
res.inspacingy = val;
res.outspacingy = val;
}
// Copy over all the supplied fields. These override the defaults above.
res = Object.assign(res, metrics);
return res;
}
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<input.length; ix++)
win.linebuf[ix] = input.charCodeAt(ix);
var termcode = 0;
if (termkey && KeystrokeNameMap[termkey])
termcode = KeystrokeNameMap[termkey];
gli_selectref.set_field(0, Const.evtype_LineInput);
gli_selectref.set_field(1, win);
gli_selectref.set_field(2, input.length);
gli_selectref.set_field(3, termcode);
if (GiDispa)
GiDispa.unretain_array(win.linebuf);
win.line_request = false;
win.line_request_uni = false;
win.request_echo_line_input = null;
win.input_generation = null;
win.linebuf = null;
if (GiDispa)
GiDispa.prepare_resume(gli_selectref);
gli_selectref = null;
VM.resume();
}
function update() {
var dataobj = { type: 'update', gen: event_generation };
var winarray = null;
var contentarray = null;
var inputarray = null;
var win, obj, robj, useobj, lineobj, ls, val, ix, cx;
var initial, lastpos, laststyle, lasthyperlink;
if (geometry_changed) {
geometry_changed = false;
winarray = [];
for (win=gli_windowlist; win; win=win.next) {
if (win.type == Const.wintype_Pair)
continue;
obj = { id: win.disprock, rock: win.rock };
winarray.push(obj);
switch (win.type) {
case Const.wintype_TextBuffer:
obj.type = 'buffer';
break;
case Const.wintype_TextGrid:
obj.type = 'grid';
obj.gridwidth = win.gridwidth;
obj.gridheight = win.gridheight;
break;
case Const.wintype_Graphics:
obj.type = 'graphics';
obj.graphwidth = win.graphwidth;
obj.graphheight = win.graphheight;
break;
}
obj.left = win.bbox.left;
obj.top = win.bbox.top;
obj.width = win.bbox.right - win.bbox.left;
obj.height = win.bbox.bottom - win.bbox.top;
}
}
for (win=gli_windowlist; win; win=win.next) {
useobj = false;
obj = { id: win.disprock };
if (contentarray == null)
contentarray = [];
switch (win.type) {
case Const.wintype_TextBuffer:
gli_window_buffer_deaccumulate(win);
if (win.content.length) {
obj.text = win.content.slice(0);
win.content.length = 0;
useobj = true;
}
if (win.clearcontent) {
obj.clear = true;
win.clearcontent = false;
useobj = true;
if (!obj.text) {
obj.text = [];
}
win.reserve.length = 0;
}
if (obj.text && obj.text.length) {
for (ix=0; ix<obj.text.length; ix++) {
win.reserve.push(obj.text[ix]);
}
}
if (win.reserve.length > 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<win.gridheight; ix++) {
lineobj = win.lines[ix];
if (!lineobj.dirty)
continue;
lineobj.dirty = false;
ls = [];
lastpos = 0;
for (cx=0; cx<win.gridwidth; ) {
laststyle = lineobj.styles[cx];
lasthyperlink = lineobj.hyperlinks[cx];
for (; cx<win.gridwidth
&& lineobj.styles[cx] == laststyle
&& lineobj.hyperlinks[cx] == lasthyperlink;
cx++) { }
if (lastpos < cx) {
if (!lasthyperlink) {
ls.push(StyleNameMap[laststyle]);
ls.push(lineobj.chars.slice(lastpos, cx).join(''));
}
else {
robj = { style:StyleNameMap[laststyle], text:lineobj.chars.slice(lastpos, cx).join(''), hyperlink:lasthyperlink };
ls.push(robj);
}
lastpos = cx;
}
}
obj.lines.push({ line:ix, content:ls });
}
useobj = obj.lines.length;
break;
case Const.wintype_Graphics:
if (win.content.length) {
obj.draw = win.content.slice(0);
win.content.length = 0;
useobj = true;
}
/* Copy new drawing commands over to the reserve. Keep track
of the last (whole-window) fill command. */
var clearedat = -1;
if (obj.draw && obj.draw.length) {
for (ix=0; ix<obj.draw.length; ix++) {
var drawel = obj.draw[ix];
if (drawel.special == 'fill'
&& drawel.x === undefined && drawel.y === undefined
&& drawel.width === undefined && drawel.height === undefined) {
clearedat = win.reserve.length;
}
win.reserve.push(drawel);
}
}
if (clearedat >= 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<win.reserve.length && ix<clearedat; ix++) {
var drawel = win.reserve[ix];
if (drawel.special == 'setcolor')
setcol = drawel;
}
win.reserve.splice(0, clearedat);
if (setcol)
win.reserve.unshift(setcol);
}
break;
}
if (useobj)
contentarray.push(obj);
}
inputarray = [];
for (win=gli_windowlist; win; win=win.next) {
obj = null;
if (win.char_request) {
obj = { id: win.disprock, type: 'char', gen: win.input_generation };
if (win.type == Const.wintype_TextGrid) {
if (gli_window_grid_canonicalize(win)) {
obj.xpos = win.gridwidth;
obj.ypos = win.gridheight-1;
}
else {
obj.xpos = win.cursorx;
obj.ypos = win.cursory;
}
}
}
if (win.line_request) {
initial = '';
if (current_partial_outputs) {
val = current_partial_outputs[win.disprock];
if (val)
initial = val;
}
/* Note that the initial and terminators fields will be ignored
if this is a continued (old) input request. So it doesn't
matter if they're wrong. */
obj = { id: win.disprock, type: 'line', gen: win.input_generation,
maxlen: win.linebuf.length, initial: initial };
if (win.line_input_terminators.length) {
obj.terminators = win.line_input_terminators;
}
if (win.type == Const.wintype_TextGrid) {
if (gli_window_grid_canonicalize(win)) {
obj.xpos = win.gridwidth;
obj.ypos = win.gridheight-1;
}
else {
obj.xpos = win.cursorx;
obj.ypos = win.cursory;
}
}
}
if (win.hyperlink_request) {
if (!obj)
obj = { id: win.disprock };
obj.hyperlink = true;
}
if (win.mouse_request) {
if (!obj)
obj = { id: win.disprock };
obj.mouse = true;
}
if (obj)
inputarray.push(obj);
}
dataobj.windows = winarray;
dataobj.content = contentarray;
dataobj.input = inputarray;
if (gli_timer_lastsent != gli_timer_interval) {
//qlog("### timer update: " + gli_timer_interval);
dataobj.timer = gli_timer_interval;
gli_timer_lastsent = gli_timer_interval;
}
if (ui_specialinput) {
//qlog("### special input: " + ui_specialinput.type);
dataobj.specialinput = ui_specialinput;
}
if (ui_disabled) {
//qlog("### disabling ui");
dataobj.disable = true;
}
/* Clean this up; it's only meaningful within one run/update cycle. */
current_partial_outputs = null;
/* If we're doing an autorestore, gli_autorestore_glkstate will
contain additional setup information for the first update()
call only. */
if (gli_autorestore_glkstate)
dataobj.autorestore = gli_autorestore_glkstate;
gli_autorestore_glkstate = null;
GlkOte.update(dataobj, gli_autorestore_glkstate);
if (option_before_select_hook) {
option_before_select_hook();
}
if (option_do_vm_autosave) {
if (has_exited) {
/* On quit or fatal error, delete the autosave. */
VM.do_autosave(-1);
}
else {
/* If this is a good time, autosave. */
var eventarg = GiDispa.check_autosave();
if (eventarg)
VM.do_autosave(eventarg);
}
}
}
/* Return the library interface object that we were passed or created.
Call this if you want to use, e.g., the same Dialog object that GlkOte
is using.
*/
function get_library(val) {
switch (val) {
case 'VM': return VM;
case 'GlkOte': return GlkOte;
case 'GiDispa': return GiDispa;
case 'Blorb': return Blorb;
case 'Dialog': return GlkOte.getlibrary('Dialog');
}
/* Unrecognized library name. */
return null;
}
/* Wrap up the current display state as a (JSONable) object. This is
called from Quixe.vm_autosave.
*/
function save_allstate() {
var Dialog = GlkOte.getlibrary('Dialog');
var res = {};
if (gli_rootwin)
res.rootwin = gli_rootwin.disprock;
if (gli_currentstr)
res.currentstr = gli_currentstr.disprock;
if (gli_timer_interval)
res.timer_interval = gli_timer_interval;
res.windows = [];
for (var win = gli_windowlist; win; win = win.next) {
var obj = {
type: win.type, rock: win.rock, disprock: win.disprock,
style: win.style, hyperlink: win.hyperlink
};
if (win.parent)
obj.parent = win.parent.disprock;
obj.str = win.str.disprock;
if (win.echostr)
obj.echostr = win.echostr.disprock;
obj.bbox = {
left: win.bbox.left, right: win.bbox.right,
top: win.bbox.top, bottom: win.bbox.bottom
};
if (win.linebuf !== null) {
var info = GiDispa.get_retained_array(win.linebuf);
obj.linebuf = {
addr: info.addr,
len: info.len,
arr: info.arr.slice(0),
arg: info.arg.serialize()
};
}
obj.char_request = win.char_request;
obj.line_request = win.line_request;
obj.char_request_uni = win.char_request_uni;
obj.line_request_uni = win.line_request_uni;
obj.hyperlink_request = win.hyperlink_request;
obj.mouse_request = win.mouse_request;
obj.echo_line_input = win.echo_line_input;
obj.request_echo_line_input = win.request_echo_line_input;
obj.line_input_terminators = win.line_input_terminators.slice(0);
//### should have a request_line_input_terminators as well
switch (win.type) {
case Const.wintype_TextBuffer:
obj.reserve = win.reserve.slice(0);
break;
case Const.wintype_TextGrid:
obj.gridwidth = win.gridwidth;
obj.gridheight = win.gridheight;
obj.lines = [];
for (var ix=0; ix<win.lines.length; ix++) {
var ln = win.lines[ix];
obj.lines.push({
chars: ln.chars.slice(0),
styles: ln.styles.slice(0),
hyperlinks: ln.hyperlinks.slice(0)
});
}
obj.cursorx = win.cursorx;
obj.cursory = win.cursory;
break;
case Const.wintype_Graphics:
obj.graphwidth = win.graphwidth;
obj.graphheight = win.graphheight;
obj.reserve = win.reserve.slice(0);
break;
case Const.wintype_Pair:
obj.pair_dir = win.pair_dir;
obj.pair_division = win.pair_division;
obj.pair_key = win.pair_key.disprock;
obj.pair_keydamage = false;
obj.pair_size = win.pair_size;
obj.pair_hasborder = win.pair_hasborder;
obj.pair_vertical = win.pair_vertical;
obj.pair_backward = win.pair_backward;
obj.child1 = win.child1.disprock;
obj.child2 = win.child2.disprock;
break;
}
res.windows.push(obj);
}
res.streams = [];
for (var str = gli_streamlist; str; str = str.next) {
var obj = {
type: str.type, rock: str.rock, disprock: str.disprock,
unicode: str.unicode, isbinary: str.isbinary,
readcount: str.readcount, writecount: str.writecount,
readable: str.readable, writable: str.writable,
streaming: str.streaming
};
switch (str.type) {
case strtype_Window:
if (str.win)
obj.win = str.win.disprock;
break;
case strtype_Memory:
if (str.buf !== null) {
var info = GiDispa.get_retained_array(str.buf);
obj.buf = {
addr: info.addr,
len: info.len,
arr: info.arr.slice(0),
arg: info.arg.serialize()
};
}
obj.buflen = str.buflen;
obj.bufpos = str.bufpos;
obj.bufeof = str.bufeof;
break;
case strtype_Resource:
obj.resfilenum = str.resfilenum;
// Don't need str.buf
obj.buflen = str.buflen;
obj.bufpos = str.bufpos;
obj.bufeof = str.bufeof;
break;
case strtype_File:
obj.origfmode = str.origfmode;
if (!Dialog.streaming) {
obj.ref = str.ref;
gli_stream_flush_file(str);
// Don't need str.buf
obj.buflen = str.buflen;
obj.bufpos = str.bufpos;
obj.bufeof = str.bufeof;
}
else {
str.fstream.fflush();
obj.ref = str.ref;
obj.filepos = str.fstream.ftell();
}
break;
}
res.streams.push(obj);
}
res.filerefs = [];
for (var fref = gli_filereflist; fref; fref = fref.next) {
var obj = {
type: fref.type, rock: fref.rock, disprock: fref.disprock,
filename: fref.filename, textmode: fref.textmode,
filetype: fref.filetype, filetypename: fref.filetypename
};
obj.ref = fref.ref;
res.filerefs.push(obj);
}
// Ignore gli_schannellist, as it's currently always empty.
/* Save GlkOte-level information. This includes the overall metrics. */
res.glkote = GlkOte.save_allstate();
return res;
}
/* Take display information (created by save_allstate) and set up our
state to match it. Called from vm_autorestore.
*/
function restore_allstate(res)
{
var Dialog = GlkOte.getlibrary('Dialog');
if (gli_windowlist || gli_streamlist || gli_filereflist)
throw('restore_allstate: glkapi module has already been launched');
/* We build and register all the bare objects first. (In reverse
order so that the linked lists come out right way around.) */
for (var ix=res.windows.length-1; 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<res.windows.length; ix++) {
var obj = res.windows[ix];
var win = GiDispa.class_obj_from_id('window', obj.disprock);
win.parent = GiDispa.class_obj_from_id('window', obj.parent);
win.str = GiDispa.class_obj_from_id('stream', obj.str);
win.echostr = GiDispa.class_obj_from_id('stream', obj.echostr);
win.bbox = {
left: obj.bbox.left, right: obj.bbox.right,
top: obj.bbox.top, bottom: obj.bbox.bottom
};
win.input_generation = null;
if (obj.char_request || obj.line_request)
win.input_generation = event_generation;
win.linebuf = null;
if (obj.linebuf !== undefined) {
// should clone that object
win.linebuf = obj.linebuf.arr;
GiDispa.retain_array(win.linebuf, obj.linebuf);
}
win.char_request = obj.char_request;
win.line_request = obj.line_request;
win.char_request_uni = obj.char_request_uni;
win.line_request_uni = obj.line_request_uni;
win.hyperlink_request = obj.hyperlink_request;
win.mouse_request = obj.mouse_request;
win.echo_line_input = obj.echo_line_input;
win.request_echo_line_input = obj.request_echo_line_input;
win.line_input_terminators = obj.line_input_terminators.slice(0);
//### should have a request_line_input_terminators as well
switch (win.type) {
case Const.wintype_TextBuffer:
win.accum = [];
win.accumstyle = win.style;
win.accumhyperlink = win.hyperlink;
win.content = obj.reserve.slice(0);
win.clearcontent = false;
win.reserve = [];
break;
case Const.wintype_TextGrid:
win.gridwidth = obj.gridwidth;
win.gridheight = obj.gridheight;
win.lines = [];
for (var jx=0; jx<obj.lines.length; jx++) {
var ln = obj.lines[jx];
win.lines.push({
dirty: true,
chars: ln.chars.slice(0),
styles: ln.styles.slice(0),
hyperlinks: ln.hyperlinks.slice(0)
});
}
win.cursorx = obj.cursorx;
win.cursory = obj.cursory;
break;
case Const.wintype_Graphics:
win.graphwidth = obj.graphwidth;
win.graphheight = obj.graphheight;
win.content = obj.reserve.slice(0);
win.reserve = [];
break;
case Const.wintype_Pair:
win.pair_dir = obj.pair_dir;
win.pair_division = obj.pair_division;
win.pair_key = GiDispa.class_obj_from_id('window', obj.pair_key);
win.pair_keydamage = false;
win.pair_size = obj.pair_size;
win.pair_hasborder = obj.pair_hasborder;
win.pair_vertical = obj.pair_vertical;
win.pair_backward = obj.pair_backward;
win.child1 = GiDispa.class_obj_from_id('window', obj.child1);
win.child2 = GiDispa.class_obj_from_id('window', obj.child2);
break;
}
}
for (var ix=0; ix<res.streams.length; ix++) {
var obj = res.streams[ix];
var str = GiDispa.class_obj_from_id('stream', obj.disprock);
/* Defaults first... */
str.win = null;
str.ref = null;
str.file = null;
str.buf = null;
str.bufpos = 0;
str.buflen = 0;
str.bufeof = 0;
str.timer_id = null;
str.flush_func = null;
str.fstream = null;
switch (str.type) {
case strtype_Window:
str.win = GiDispa.class_obj_from_id('window', obj.win);
break;
case strtype_Memory:
if (obj.buf !== undefined) {
// should clone that object
str.buf = obj.buf.arr;
GiDispa.retain_array(str.buf, obj.buf);
}
str.buflen = obj.buflen;
str.bufpos = obj.bufpos;
str.bufeof = obj.bufeof;
break;
case strtype_Resource:
str.resfilenum = obj.resfilenum;
var el = Blorb.get_data_chunk(str.resfilenum);
if (el) {
str.buf = el.data;
}
str.buflen = obj.buflen;
str.bufpos = obj.bufpos;
str.bufeof = obj.bufeof;
break;
case strtype_File:
str.origfmode = obj.origfmode;
if (!Dialog.streaming) {
str.ref = obj.ref;
str.buflen = obj.buflen;
str.bufpos = obj.bufpos;
str.bufeof = obj.bufeof;
var content = Dialog.file_read(str.ref);
if (content == null) {
/* The file was somehow deleted. Create an empty
file (even in read mode). */
content = [];
Dialog.file_write(str.ref, '', true);
}
str.buf = content;
/* If the file has been shortened, we might have to
trim bufeof to fit within it. The game might see
the file pos mysteriously move; sorry. */
str.bufeof = content.length;
if (str.bufpos > 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<res.filerefs.length; ix++) {
var obj = res.filerefs[ix];
var fref = GiDispa.class_obj_from_id('fileref', obj.disprock);
fref.ref = obj.ref; // should deep clone
}
gli_rootwin = GiDispa.class_obj_from_id('window', res.rootwin);
gli_currentstr = GiDispa.class_obj_from_id('stream', res.currentstr);
if (res.timer_interval)
glk_request_timer_events(res.timer_interval);
/* Stash this for the next (first) GlkOte.update call. */
gli_autorestore_glkstate = res.glkote;
}
/* This is the handler for a VM fatal error. (Not for an error in our own
library!) We display the error message, and then push a final display
update, which kills all input fields in all windows.
*/
function fatal_error(msg) {
has_exited = true;
ui_disabled = true;
if (!GlkOte) {
// We haven't been initialized yet, so we can only try to log the error and hope someone sees it.
console.log('Fatal error:', msg);
return;
}
GlkOte.error(msg);
var dataobj = { type: 'update', gen: event_generation, disable: true };
dataobj.input = [];
GlkOte.update(dataobj);
}
/* All the numeric constants used by the Glk interface. We push these into
an object, for tidiness. */
var Const = {
gestalt_Version : 0,
gestalt_CharInput : 1,
gestalt_LineInput : 2,
gestalt_CharOutput : 3,
gestalt_CharOutput_CannotPrint : 0,
gestalt_CharOutput_ApproxPrint : 1,
gestalt_CharOutput_ExactPrint : 2,
gestalt_MouseInput : 4,
gestalt_Timer : 5,
gestalt_Graphics : 6,
gestalt_DrawImage : 7,
gestalt_Sound : 8,
gestalt_SoundVolume : 9,
gestalt_SoundNotify : 10,
gestalt_Hyperlinks : 11,
gestalt_HyperlinkInput : 12,
gestalt_SoundMusic : 13,
gestalt_GraphicsTransparency : 14,
gestalt_Unicode : 15,
gestalt_UnicodeNorm : 16,
gestalt_LineInputEcho : 17,
gestalt_LineTerminators : 18,
gestalt_LineTerminatorKey : 19,
gestalt_DateTime : 20,
gestalt_Sound2 : 21,
gestalt_ResourceStream : 22,
gestalt_GraphicsCharInput : 23,
keycode_Unknown : 0xffffffff,
keycode_Left : 0xfffffffe,
keycode_Right : 0xfffffffd,
keycode_Up : 0xfffffffc,
keycode_Down : 0xfffffffb,
keycode_Return : 0xfffffffa,
keycode_Delete : 0xfffffff9,
keycode_Escape : 0xfffffff8,
keycode_Tab : 0xfffffff7,
keycode_PageUp : 0xfffffff6,
keycode_PageDown : 0xfffffff5,
keycode_Home : 0xfffffff4,
keycode_End : 0xfffffff3,
keycode_Func1 : 0xffffffef,
keycode_Func2 : 0xffffffee,
keycode_Func3 : 0xffffffed,
keycode_Func4 : 0xffffffec,
keycode_Func5 : 0xffffffeb,
keycode_Func6 : 0xffffffea,
keycode_Func7 : 0xffffffe9,
keycode_Func8 : 0xffffffe8,
keycode_Func9 : 0xffffffe7,
keycode_Func10 : 0xffffffe6,
keycode_Func11 : 0xffffffe5,
keycode_Func12 : 0xffffffe4,
/* The last keycode is always (0x100000000 - keycode_MAXVAL) */
keycode_MAXVAL : 28,
evtype_None : 0,
evtype_Timer : 1,
evtype_CharInput : 2,
evtype_LineInput : 3,
evtype_MouseInput : 4,
evtype_Arrange : 5,
evtype_Redraw : 6,
evtype_SoundNotify : 7,
evtype_Hyperlink : 8,
evtype_VolumeNotify : 9,
style_Normal : 0,
style_Emphasized : 1,
style_Preformatted : 2,
style_Header : 3,
style_Subheader : 4,
style_Alert : 5,
style_Note : 6,
style_BlockQuote : 7,
style_Input : 8,
style_User1 : 9,
style_User2 : 10,
style_NUMSTYLES : 11,
wintype_AllTypes : 0,
wintype_Pair : 1,
wintype_Blank : 2,
wintype_TextBuffer : 3,
wintype_TextGrid : 4,
wintype_Graphics : 5,
winmethod_Left : 0x00,
winmethod_Right : 0x01,
winmethod_Above : 0x02,
winmethod_Below : 0x03,
winmethod_DirMask : 0x0f,
winmethod_Fixed : 0x10,
winmethod_Proportional : 0x20,
winmethod_DivisionMask : 0xf0,
winmethod_Border : 0x000,
winmethod_NoBorder : 0x100,
winmethod_BorderMask : 0x100,
fileusage_Data : 0x00,
fileusage_SavedGame : 0x01,
fileusage_Transcript : 0x02,
fileusage_InputRecord : 0x03,
fileusage_TypeMask : 0x0f,
fileusage_TextMode : 0x100,
fileusage_BinaryMode : 0x000,
filemode_Write : 0x01,
filemode_Read : 0x02,
filemode_ReadWrite : 0x03,
filemode_WriteAppend : 0x05,
seekmode_Start : 0,
seekmode_Current : 1,
seekmode_End : 2,
stylehint_Indentation : 0,
stylehint_ParaIndentation : 1,
stylehint_Justification : 2,
stylehint_Size : 3,
stylehint_Weight : 4,
stylehint_Oblique : 5,
stylehint_Proportional : 6,
stylehint_TextColor : 7,
stylehint_BackColor : 8,
stylehint_ReverseColor : 9,
stylehint_NUMHINTS : 10,
stylehint_just_LeftFlush : 0,
stylehint_just_LeftRight : 1,
stylehint_just_Centered : 2,
stylehint_just_RightFlush : 3,
imagealign_InlineUp : 1,
imagealign_InlineDown : 2,
imagealign_InlineCenter : 3,
imagealign_MarginLeft : 4,
imagealign_MarginRight : 5
};
var KeystrokeNameMap = {
/* The key values are taken from GlkOte's "char" event. A couple of them
are Javascript keywords, so they're in quotes, but that doesn't affect
the final structure. */
left : Const.keycode_Left,
right : Const.keycode_Right,
up : Const.keycode_Up,
down : Const.keycode_Down,
'return' : Const.keycode_Return,
'delete' : Const.keycode_Delete,
escape : Const.keycode_Escape,
tab : Const.keycode_Tab,
pageup : Const.keycode_PageUp,
pagedown : Const.keycode_PageDown,
home : Const.keycode_Home,
end : Const.keycode_End,
func1 : Const.keycode_Func1,
func2 : Const.keycode_Func2,
func3 : Const.keycode_Func3,
func4 : Const.keycode_Func4,
func5 : Const.keycode_Func5,
func6 : Const.keycode_Func6,
func7 : Const.keycode_Func7,
func8 : Const.keycode_Func8,
func9 : Const.keycode_Func9,
func10 : Const.keycode_Func10,
func11 : Const.keycode_Func11,
func12 : Const.keycode_Func12
};
/* The inverse of KeystrokeNameMap. We'll fill this in if needed. (It
generally isn't.) */
var KeystrokeValueMap = null;
var StyleNameMap = {
0 : 'normal',
1 : 'emphasized',
2 : 'preformatted',
3 : 'header',
4 : 'subheader',
5 : 'alert',
6 : 'note',
7 : 'blockquote',
8 : 'input',
9 : 'user1',
10 : 'user2'
};
var FileTypeMap = {
0: 'data',
1: 'save',
2: 'transcript',
3: 'command'
};
/* These tables were generated by casemap.py. */
/* Derived from Unicode data files, Unicode version 4.0.1. */
/* list all the special cases in unicode_upper_table */
var unicode_upper_table = {
181: 924, 223: [ 83,83 ], 255: 376, 305: 73, 329: [ 700,78 ],
383: 83, 405: 502, 414: 544, 447: 503, 454: 452,
457: 455, 460: 458, 477: 398, 496: [ 74,780 ], 499: 497,
595: 385, 596: 390, 598: 393, 599: 394, 601: 399,
603: 400, 608: 403, 611: 404, 616: 407, 617: 406,
623: 412, 626: 413, 629: 415, 640: 422, 643: 425,
648: 430, 650: 433, 651: 434, 658: 439, 837: 921,
912: [ 921,776,769 ], 940: 902, 941: 904, 942: 905, 943: 906,
944: [ 933,776,769 ], 962: 931, 972: 908, 973: 910, 974: 911,
976: 914, 977: 920, 981: 934, 982: 928, 1008: 922,
1010: 1017, 1013: 917, 1415: [ 1333,1362 ], 7830: [ 72,817 ], 7831: [ 84,776 ],
7832: [ 87,778 ], 7833: [ 89,778 ], 7834: [ 65,702 ], 7835: 7776, 8016: [ 933,787 ],
8018: [ 933,787,768 ], 8020: [ 933,787,769 ], 8022: [ 933,787,834 ], 8048: 8122, 8049: 8123,
8050: 8136, 8051: 8137, 8052: 8138, 8053: 8139, 8054: 8154,
8055: 8155, 8056: 8184, 8057: 8185, 8058: 8170, 8059: 8171,
8060: 8186, 8061: 8187, 8064: [ 7944,921 ], 8065: [ 7945,921 ], 8066: [ 7946,921 ],
8067: [ 7947,921 ], 8068: [ 7948,921 ], 8069: [ 7949,921 ], 8070: [ 7950,921 ], 8071: [ 7951,921 ],
8072: [ 7944,921 ], 8073: [ 7945,921 ], 8074: [ 7946,921 ], 8075: [ 7947,921 ], 8076: [ 7948,921 ],
8077: [ 7949,921 ], 8078: [ 7950,921 ], 8079: [ 7951,921 ], 8080: [ 7976,921 ], 8081: [ 7977,921 ],
8082: [ 7978,921 ], 8083: [ 7979,921 ], 8084: [ 7980,921 ], 8085: [ 7981,921 ], 8086: [ 7982,921 ],
8087: [ 7983,921 ], 8088: [ 7976,921 ], 8089: [ 7977,921 ], 8090: [ 7978,921 ], 8091: [ 7979,921 ],
8092: [ 7980,921 ], 8093: [ 7981,921 ], 8094: [ 7982,921 ], 8095: [ 7983,921 ], 8096: [ 8040,921 ],
8097: [ 8041,921 ], 8098: [ 8042,921 ], 8099: [ 8043,921 ], 8100: [ 8044,921 ], 8101: [ 8045,921 ],
8102: [ 8046,921 ], 8103: [ 8047,921 ], 8104: [ 8040,921 ], 8105: [ 8041,921 ], 8106: [ 8042,921 ],
8107: [ 8043,921 ], 8108: [ 8044,921 ], 8109: [ 8045,921 ], 8110: [ 8046,921 ], 8111: [ 8047,921 ],
8114: [ 8122,921 ], 8115: [ 913,921 ], 8116: [ 902,921 ], 8118: [ 913,834 ], 8119: [ 913,834,921 ],
8124: [ 913,921 ], 8126: 921, 8130: [ 8138,921 ], 8131: [ 919,921 ], 8132: [ 905,921 ],
8134: [ 919,834 ], 8135: [ 919,834,921 ], 8140: [ 919,921 ], 8146: [ 921,776,768 ], 8147: [ 921,776,769 ],
8150: [ 921,834 ], 8151: [ 921,776,834 ], 8162: [ 933,776,768 ], 8163: [ 933,776,769 ], 8164: [ 929,787 ],
8165: 8172, 8166: [ 933,834 ], 8167: [ 933,776,834 ], 8178: [ 8186,921 ], 8179: [ 937,921 ],
8180: [ 911,921 ], 8182: [ 937,834 ], 8183: [ 937,834,921 ], 8188: [ 937,921 ], 64256: [ 70,70 ],
64257: [ 70,73 ], 64258: [ 70,76 ], 64259: [ 70,70,73 ], 64260: [ 70,70,76 ], 64261: [ 83,84 ],
64262: [ 83,84 ], 64275: [ 1348,1350 ], 64276: [ 1348,1333 ], 64277: [ 1348,1339 ], 64278: [ 1358,1350 ],
64279: [ 1348,1341 ]
};
/* add all the regular cases to unicode_upper_table */
(function() {
var ls, ix, val;
var map = unicode_upper_table;
ls = [
7936, 7937, 7938, 7939, 7940, 7941, 7942, 7943,
7952, 7953, 7954, 7955, 7956, 7957, 7968, 7969,
7970, 7971, 7972, 7973, 7974, 7975, 7984, 7985,
7986, 7987, 7988, 7989, 7990, 7991, 8000, 8001,
8002, 8003, 8004, 8005, 8017, 8019, 8021, 8023,
8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039,
8112, 8113, 8144, 8145, 8160, 8161,
];
for (ix=0; ix<54; ix++) {
val = ls[ix];
map[val] = val+8;
}
for (val=257; val<=303; val+=2) {
map[val] = val-1;
}
for (val=331; val<=375; val+=2) {
map[val] = val-1;
}
for (val=505; val<=543; val+=2) {
map[val] = val-1;
}
for (val=1121; val<=1153; val+=2) {
map[val] = val-1;
}
for (val=1163; val<=1215; val+=2) {
map[val] = val-1;
}
for (val=1233; val<=1269; val+=2) {
map[val] = val-1;
}
for (val=7681; val<=7829; val+=2) {
map[val] = val-1;
}
for (val=7841; val<=7929; val+=2) {
map[val] = val-1;
}
ls = [
307, 309, 311, 314, 316, 318, 320, 322,
324, 326, 328, 378, 380, 382, 387, 389,
392, 396, 402, 409, 417, 419, 421, 424,
429, 432, 436, 438, 441, 445, 453, 456,
459, 462, 464, 466, 468, 470, 472, 474,
476, 479, 481, 483, 485, 487, 489, 491,
493, 495, 498, 501, 547, 549, 55