@bitblit/asyncglk
Version:
A Typescript Glk library
1,444 lines (1,441 loc) • 69.4 kB
JavaScript
/*
GlkApi
======
Copyright (c) 2024 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/
import { cloneDeep } from 'lodash-es';
import { Blorb } from '../blorb/blorb.js';
import { DEFAULT_METRICS, PACKAGE_VERSION } from '../common/constants.js';
import { BEBuffer_to_Array, utf8decoder } from '../common/misc.js';
import * as Protocol from '../common/protocol.js';
import { CachingDialogWrapper } from '../dialog/common/cache.js';
import { copy_array } from './common.js';
import * as Const from './constants.js';
import { evtype_Arrange, evtype_CharInput, evtype_Hyperlink, evtype_LineInput, evtype_MouseInput, evtype_None, evtype_Redraw, evtype_Timer, filemode_Read, filemode_ReadWrite, filemode_Write, filemode_WriteAppend, fileusage_SavedGame, fileusage_TypeMask, gestalt_CharInput, gestalt_CharOutput, gestalt_CharOutput_ExactPrint, gestalt_DateTime, gestalt_DrawImage, gestalt_GarglkText, gestalt_Graphics, gestalt_GraphicsCharInput, gestalt_GraphicsTransparency, gestalt_HyperlinkInput, gestalt_Hyperlinks, gestalt_LineInput, gestalt_LineInputEcho, gestalt_LineTerminatorKey, gestalt_LineTerminators, gestalt_MouseInput, gestalt_ResourceStream, gestalt_Timer, gestalt_Unicode, gestalt_UnicodeNorm, gestalt_Version, keycode_Escape, keycode_Func1, keycode_Func12, keycode_Left, keycode_MAXVAL, keycode_Unknown, seekmode_End, stylehint_BackColor, stylehint_Indentation, stylehint_Justification, stylehint_NUMHINTS, stylehint_Oblique, stylehint_ParaIndentation, stylehint_Proportional, stylehint_ReverseColor, stylehint_Size, stylehint_TextColor, stylehint_Weight, style_NUMSTYLES, winmethod_Above, winmethod_Below, winmethod_Border, winmethod_BorderMask, winmethod_DirMask, winmethod_DivisionMask, winmethod_Fixed, winmethod_Left, winmethod_NoBorder, winmethod_Proportional, winmethod_Right, wintype_AllTypes, wintype_Blank, wintype_Graphics, wintype_Pair, wintype_TextBuffer, wintype_TextGrid, zcolor_Current, zcolor_Default } from './constants.js';
import { FileRef } from './filerefs.js';
import * as Interface from './interface.js';
import { DidNotReturn } from './interface.js';
import { CSS_STYLE_PROPERTIES, FILE_MODES, FILE_TYPES, IMAGE_ALIGNMENTS, KEY_NAMES_TO_CODES, MAX_LATIN1, QUESTION_MARK, STYLE_NAMES, TERMINATOR_KEYS, TERMINATOR_KEYS_TO_CODES } from './lib_constants.js';
import { ArrayBackedStream, FileStream, NullStream } from './streams.js';
import { BlankWindow, BufferWindow, GraphicsWindow, GridWindow, PairWindow, TextWindow } from './windows.js';
export class RefBox {
value = 0;
get_value() {
return this.value;
}
set_value(val) {
this.value = val;
}
}
export class RefStruct {
fields = [];
get_field(index) {
return this.fields[index];
}
get_fields() {
return this.fields;
}
push_field(val) {
this.fields.push(val);
}
set_field(index, val) {
this.fields[index] = val;
}
}
export class AsyncGlk {
Blorb;
Dialog = null;
GiDispa;
GlkOte = null;
VM = null;
before_select_hook;
gestalt_hook;
// For assigning disprocks when there is no GiDispa
disprock_counter = 1;
do_autosave = false;
exited = false;
first_fref = null;
gen = 0;
metrics = DEFAULT_METRICS;
partial_inputs;
selectref;
special;
special_data;
current_stream = null;
first_stream = null;
stylehints = {
buffer: {},
grid: {},
};
support = [];
timer = {
interval: 0,
last_interval: 0,
started: 0,
};
version = PACKAGE_VERSION;
first_window = null;
root_window = null;
windows_changed = false;
// API functions
init(options) {
this.before_select_hook = options.before_select_hook;
this.Blorb = options.Blorb;
if (options.Dialog && !options.Dialog.async) {
// This synchronous GlkApi doesn't support AsyncDialog
this.Dialog = new CachingDialogWrapper(options.Dialog);
}
else {
throw new Error('No reference to Dialog');
}
this.do_autosave = !!options.do_vm_autosave;
// exit_warning
// extevent_hook
this.GiDispa = options.GiDispa;
this.gestalt_hook = options.glk_gestalt_hook;
if (options.GlkOte) {
this.GlkOte = options.GlkOte;
}
else {
throw new Error('No reference to GlkOte');
}
if (options.vm) {
this.VM = options.vm;
}
else {
throw new Error('No reference to VM');
}
this.before_select_hook?.();
this.GiDispa?.init({
io: this,
vm: this.VM,
});
const glkote_options = options;
glkote_options.accept = this.accept.bind(this);
this.GlkOte.init(glkote_options);
}
call_may_not_return(id) {
return id === 0x01 || id === 0xC0 || id === 0x62;
}
fatal_error(msg) {
this.exited = true;
if (!this.GlkOte) {
console.error('Fatal error: ' + msg);
return;
}
this.GlkOte.error(msg);
this.GlkOte.update({
type: 'update',
disable: true,
gen: this.gen,
});
}
getlibrary(class_name) {
switch (class_name) {
case 'Blorb':
return this.Blorb || null;
case 'Dialog':
return this.Dialog;
case 'GiDispa':
return this.GiDispa || null;
case 'GlkOte':
return this.GlkOte;
case 'VM':
return this.VM;
default:
return null;
}
}
inited() {
return !!(this.GlkOte && this.VM);
}
restore_allstate(_state) {
throw new Error('Autosaves not yet supported');
}
save_allstate() {
throw new Error('Autosaves not yet supported');
}
update() {
const state = {
gen: this.gen,
type: 'update',
};
if (this.exited) {
state.disable = true;
}
// Get the window updates
const contents = [];
const inputs = [];
const sizes = [];
for (let win = this.first_window; win; win = win.next) {
const update = win.update();
if (update.content) {
contents.push(update.content);
}
if (update.input) {
const input_update = update.input;
if (input_update.hyperlink || input_update.mouse || input_update.type) {
inputs.push(input_update);
}
}
if (this.windows_changed && update.size) {
sizes.push(update.size);
}
}
if (contents.length) {
state.content = contents;
}
if (inputs.length) {
state.input = inputs;
}
if (sizes.length) {
state.windows = sizes;
}
this.windows_changed = false;
// TODO: Page BG colour
// Special input
if (this.special) {
state.specialinput = this.special;
delete this.special;
}
// Timer
const timer = this.timer;
if (timer.last_interval !== timer.interval) {
state.timer = timer.interval;
timer.last_interval = timer.interval;
}
// Autorestore state?
// Clone the state so that any objects copied into it won't be at risk of modification by GlkOte
this.GlkOte.update(cloneDeep(state));
this.before_select_hook?.();
// TODO
// if (this.do_autosave) {}
}
// References to other things
Const = Const;
DidNotReturn = DidNotReturn;
RefBox = RefBox;
RefStruct = RefStruct;
// Extra functions
byte_array_to_string(arr) {
return String.fromCodePoint(...arr);
}
glk_put_jstring(val, _all_bytes) {
this.glk_put_jstring_stream(this.current_stream, val);
}
glk_put_jstring_stream(str, val, _all_bytes) {
if (!str) {
throw new Error('Invalid Stream');
}
str.put_string(val);
}
uni_array_to_string(arr) {
return String.fromCodePoint(...arr);
}
// The Glk functions
glk_buffer_canon_decompose_uni(buf, initlen) {
return buffer_transformer(buf, initlen, str => str.normalize('NFD'));
}
glk_buffer_canon_normalize_uni(buf, initlen) {
return buffer_transformer(buf, initlen, str => str.normalize('NFC'));
}
glk_buffer_to_lower_case_uni(buf, initlen) {
return buffer_transformer(buf, initlen, str => str.toLowerCase());
}
glk_buffer_to_title_case_uni(buf, initlen, lowerrest) {
return buffer_transformer(buf, initlen, buf => buf.reduce((prev, ch, index) => {
const special_cases = {
ß: 'Ss', DŽ: 'Dž', Dž: 'Dž', dž: 'Dž', LJ: 'Lj', Lj: 'Lj', lj: 'Lj', NJ: 'Nj', Nj: 'Nj', nj: 'Nj',
DZ: 'Dz', Dz: 'Dz', dz: 'Dz', և: 'Եւ', ᾲ: 'Ὰͅ', ᾳ: 'ᾼ', ᾴ: 'Άͅ', ᾷ: 'ᾼ͂', ᾼ: 'ᾼ', ῂ: 'Ὴͅ',
ῃ: 'ῌ', ῄ: 'Ήͅ', ῇ: 'ῌ͂', ῌ: 'ῌ', ῲ: 'Ὼͅ', ῳ: 'ῼ', ῴ: 'Ώͅ', ῷ: 'ῼ͂', ῼ: 'ῼ', ff: 'Ff',
fi: 'Fi', fl: 'Fl', ffi: 'Ffi', ffl: 'Ffl', ſt: 'St', st: 'St', ﬓ: 'Մն', ﬔ: 'Մե',
ﬕ: 'Մի', ﬖ: 'Վն', ﬗ: 'Մխ',
};
const slightly_less_special_cases = ['ᾈᾉᾊᾋᾌᾍᾎᾏ', 'ᾘᾙᾚᾛᾜᾝᾞᾟ', 'ᾨᾩᾪᾫᾬᾭᾮᾯ'];
let thischar = String.fromCodePoint(ch);
if (index === 0) {
if (special_cases[thischar]) {
thischar = special_cases[thischar];
}
else if (ch >= 8064 && ch < 8112) {
thischar = slightly_less_special_cases[((ch - 8064) / 16) | 0][ch % 8];
}
else {
thischar = thischar.toUpperCase();
}
}
else if (lowerrest) {
thischar = thischar.toLowerCase();
}
return prev + thischar;
}, ''), true);
}
glk_buffer_to_upper_case_uni(buf, initlen) {
return buffer_transformer(buf, initlen, str => str.toUpperCase());
}
glk_cancel_char_event(win) {
if (!win) {
throw new Error('Invalid Window');
}
delete win.input.type;
}
glk_cancel_hyperlink_event(win) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type === 'buffer' || win.type === 'grid') {
delete win.input.hyperlink;
}
}
glk_cancel_line_event(win, ev) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.input.type !== 'line') {
if (ev) {
set_event(ev);
}
return;
}
this.handle_line_input(win, this.partial_inputs?.[win.disprock] ?? '', ev);
}
glk_cancel_mouse_event(win) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type === 'graphics' || win.type === 'grid') {
delete win.input.mouse;
}
}
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;
}
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;
}
glk_current_simple_time(factor) {
return Math.floor(Date.now() / (factor * 1000));
}
glk_current_time(struct) {
timestamp_to_time_struct(Date.now(), struct);
}
glk_date_to_simple_time_local(struct, factor) {
return Math.floor(date_struct_to_timestamp_local(struct) / (factor * 1000));
}
glk_date_to_simple_time_utc(struct, factor) {
return Math.floor(date_struct_to_timestamp_utc(struct) / (factor * 1000));
}
glk_date_to_time_local(datestruct, timestruct) {
timestamp_to_time_struct(date_struct_to_timestamp_local(datestruct), timestruct);
}
glk_date_to_time_utc(datestruct, timestruct) {
timestamp_to_time_struct(date_struct_to_timestamp_utc(datestruct), timestruct);
}
glk_exit() {
this.exited = true;
// What is this for?
/*if (option_exit_warning) {
GlkOte.warning(option_exit_warning);
}*/
return DidNotReturn;
}
glk_fileref_create_by_name(usage, filename, rock) {
const fixed_filename = this.Dialog.file_clean_fixed_name(filename, usage & fileusage_TypeMask);
return this.create_fileref(fixed_filename, rock, usage);
}
glk_fileref_create_by_prompt(usage, fmode, rock) {
const filemode = FILE_MODES[fmode] ?? 'read';
const filetypenum = usage & fileusage_TypeMask;
const filetype = FILE_TYPES[filetypenum] ?? 'data';
this.special = {
filemode,
filetype,
type: 'fileref_prompt',
};
if (filetypenum === fileusage_SavedGame) {
this.special.gameid = this.VM.get_signature();
}
this.special_data = {
rock,
usage,
};
return DidNotReturn;
}
glk_fileref_create_from_fileref(usage, oldfref, rock) {
if (!oldfref) {
throw new Error('Invalid Fileref');
}
return this.create_fileref(oldfref.filename, rock, usage);
}
glk_fileref_create_temp(usage, rock) {
const filetypename = FILE_TYPES[usage & fileusage_TypeMask];
const dialog_fref = this.Dialog.file_construct_temp_ref(filetypename);
return this.create_fileref(dialog_fref.filename, rock, usage, dialog_fref);
}
glk_fileref_delete_file(fref) {
if (!fref) {
throw new Error('Invalid Fileref');
}
fref.delete_file();
}
glk_fileref_destroy(fref) {
if (!fref) {
throw new Error('Invalid Fileref');
}
this.GiDispa?.class_unregister('fileref', fref);
const prev = fref.prev;
const next = fref.next;
fref.prev = null;
fref.next = null;
if (prev) {
prev.next = next;
}
else {
this.first_fref = next;
}
if (next) {
next.prev = prev;
}
}
glk_fileref_does_file_exist(fref) {
if (!fref) {
throw new Error('Invalid Fileref');
}
return fref.exists();
}
glk_fileref_get_rock(fref) {
if (!fref) {
throw new Error('Invalid Fileref');
}
return fref.rock;
}
glk_fileref_iterate(fref, rockbox) {
const next_fref = fref ? fref.next : this.first_fref;
if (rockbox) {
rockbox.set_value(next_fref ? next_fref.rock : 0);
}
return next_fref;
}
glk_gestalt(sel, val) {
return this.glk_gestalt_ext(sel, val, null);
}
glk_gestalt_ext(sel, val, arr) {
const hook_res = this.gestalt_hook?.(sel, val, arr);
if (hook_res) {
return hook_res;
}
switch (sel) {
case gestalt_Version:
return 0x00000705;
case gestalt_CharInput:
// Known special keys can be returned
if (val <= keycode_Left && val >= keycode_Func12) {
return 1;
}
// But no other high bit values, non-unicode, or control codes
if (val >= (0x100000000 - keycode_MAXVAL) || val > 0x10FFFF || (val >= 0 && val < 32) || val >= 127 && val < 160) {
return 0;
}
// Otherwise assume yes
return 1;
case gestalt_LineInput:
// Same as above, except no special keys
if (val > 0x10FFFF || (val >= 0 && val < 32) || val >= 127 && val < 160) {
return 0;
}
return 1;
case gestalt_CharOutput:
// We'll output anything, but it may not result in something readable
if (arr) {
arr[0] = 1;
}
return gestalt_CharOutput_ExactPrint;
case gestalt_LineTerminatorKey:
return +(val === keycode_Escape || (val >= keycode_Func12 && val <= keycode_Func1));
// These are dependent on what GlkOte tells us it supports
// TODO: check all of these
case gestalt_DrawImage:
return +((val === wintype_Graphics || val === wintype_TextBuffer) && this.support.includes('graphics'));
case gestalt_GarglkText:
return +this.support.includes('garglktext');
case gestalt_Graphics:
case gestalt_GraphicsCharInput:
case gestalt_GraphicsTransparency:
return +this.support.includes('graphics');
case gestalt_Hyperlinks:
return +this.support.includes('hyperlinks');
case gestalt_HyperlinkInput:
return +((val === wintype_TextBuffer || val === wintype_TextGrid) && this.support.includes('hyperlinks'));
case gestalt_MouseInput:
return +((val === wintype_Graphics || val === wintype_TextGrid) && this.support.includes('graphics'));
case gestalt_Timer:
return +this.support.includes('timer');
// These are always supported
case gestalt_DateTime:
case gestalt_LineInputEcho:
case gestalt_LineTerminators:
case gestalt_ResourceStream:
case gestalt_Unicode:
case gestalt_UnicodeNorm:
return 1;
// Anything else is unsupported
default:
return 0;
}
}
glk_get_buffer_stream(str, buf) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode !== filemode_Read && str.fmode !== filemode_ReadWrite) {
throw new Error('Cannot read from write-only stream');
}
if (Array.isArray(buf)) {
return wrap_for_array(buf, Uint8Array, arr => str.get_buffer(arr));
}
else {
return str.get_buffer(buf);
}
}
glk_get_buffer_stream_uni(str, buf) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode !== filemode_Read && str.fmode !== filemode_ReadWrite) {
throw new Error('Cannot read from write-only stream');
}
if (Array.isArray(buf)) {
return wrap_for_array(buf, Uint32Array, arr => str.get_buffer(arr));
}
else {
return str.get_buffer(buf);
}
}
glk_get_char_stream(str) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode !== filemode_Read && str.fmode !== filemode_ReadWrite) {
throw new Error('Cannot read from write-only stream');
}
return str.get_char(false);
}
glk_get_char_stream_uni(str) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode !== filemode_Read && str.fmode !== filemode_ReadWrite) {
throw new Error('Cannot read from write-only stream');
}
return str.get_char(true);
}
glk_get_line_stream(str, buf) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode !== filemode_Read && str.fmode !== filemode_ReadWrite) {
throw new Error('Cannot read from write-only stream');
}
if (Array.isArray(buf)) {
// Handle the NULL byte by adding and subtracting 1 to the read length
return wrap_for_array(buf, Uint8Array, arr => str.get_line(arr) + 1) - 1;
}
else {
return str.get_line(buf);
}
}
glk_get_line_stream_uni(str, buf) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode !== filemode_Read && str.fmode !== filemode_ReadWrite) {
throw new Error('Cannot read from write-only stream');
}
if (Array.isArray(buf)) {
// Handle the NULL byte by adding and subtracting 1 to the read length
return wrap_for_array(buf, Uint32Array, arr => str.get_line(arr) + 1) - 1;
}
else {
return str.get_line(buf);
}
}
glk_image_draw(win, imgid, val1, val2) {
const info = this.Blorb?.get_image_info(imgid);
if (!info) {
return 0;
}
this.draw_image(win, info, info.height || 0, val1, val2, info.width || 0);
return 1;
}
glk_image_draw_scaled(win, imgid, val1, val2, width, height) {
const info = this.Blorb?.get_image_info(imgid);
if (!info) {
return 0;
}
this.draw_image(win, info, height || 0, val1, val2, width || 0);
return 1;
}
glk_image_get_info(imgid, width, height) {
const info = this.Blorb?.get_image_info(imgid);
if (height) {
height.set_value(info?.height || 0);
}
if (width) {
width.set_value(info?.width || 0);
}
return info ? 1 : 0;
}
glk_put_buffer(val) {
this.glk_put_buffer_stream(this.current_stream, val);
}
glk_put_buffer_stream(str, val) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode === filemode_Read) {
throw new Error('Cannot write to read-only stream');
}
str.put_buffer(val, false);
}
glk_put_buffer_stream_uni(str, val) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode === filemode_Read) {
throw new Error('Cannot write to read-only stream');
}
str.put_buffer(val, true);
}
glk_put_buffer_uni(val) {
this.glk_put_buffer_stream_uni(this.current_stream, val);
}
glk_put_char(val) {
this.glk_put_char_stream(this.current_stream, val);
}
glk_put_char_stream(str, val) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode === filemode_Read) {
throw new Error('Cannot write to read-only stream');
}
str.put_char(val);
}
glk_put_char_stream_uni(str, val) {
this.glk_put_char_stream(str, val);
}
glk_put_char_uni(val) {
this.glk_put_char_stream(this.current_stream, val);
}
glk_put_string(val) {
this.glk_put_string_stream(this.current_stream, val);
}
glk_put_string_stream(str, val) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.fmode === filemode_Read) {
throw new Error('Cannot write to read-only stream');
}
str.put_string(val);
}
glk_put_string_stream_uni(str, val) {
this.glk_put_string_stream(str, val);
}
glk_put_string_uni(val) {
this.glk_put_string_stream(this.current_stream, val);
}
glk_request_char_event(win) {
this.request_char_event(win, false);
}
glk_request_char_event_uni(win) {
this.request_char_event(win, true);
}
glk_request_hyperlink_event(win) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type === 'buffer' || win.type === 'grid') {
win.input.hyperlink = true;
}
}
glk_request_line_event(win, buf, initlen) {
this.request_line_event(win, buf, false, initlen);
}
glk_request_line_event_uni(win, buf, initlen) {
this.request_line_event(win, buf, true, initlen);
}
glk_request_mouse_event(win) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type === 'graphics' || win.type === 'grid') {
win.input.mouse = true;
}
}
glk_request_timer_events(msecs) {
this.timer.interval = msecs;
this.timer.started = msecs ? Date.now() : 0;
}
glk_schannel_create(_rock) {
return null;
}
glk_schannel_create_ext(_rock, _volume) {
return null;
}
glk_schannel_destroy(_schannel) {
throw new Error('Invalid Schannel');
}
glk_schannel_get_rock(_schannel) {
throw new Error('Invalid Schannel');
}
glk_schannel_iterate(_schannel, rockbox) {
if (rockbox) {
rockbox.set_value(0);
}
return null;
}
glk_schannel_pause(_schannel) {
throw new Error('Invalid Schannel');
}
glk_schannel_play(_schannel, _sound) {
throw new Error('Invalid Schannel');
}
glk_schannel_play_ext(_schannel, _sound, _repeats, _notify) {
throw new Error('Invalid Schannel');
}
glk_schannel_play_multi(_schannels, _sounds, _notify) {
throw new Error('Invalid Schannel');
}
glk_schannel_set_volume(_schannel, _volume) {
throw new Error('Invalid Schannel');
}
glk_schannel_set_volume_ext(_schannel, _volume, __duration, notify) {
throw new Error('Invalid Schannel');
}
glk_schannel_stop(_schannel) {
throw new Error('Invalid Schannel');
}
glk_schannel_unpause(_schannel) {
throw new Error('Invalid Schannel');
}
glk_select(ev) {
this.selectref = ev;
return DidNotReturn;
}
glk_select_poll(ev) {
// As JS is single threaded, the only event we could possibly have had since the last glk_select_poll call is a timer event
set_event(ev);
const timer = this.timer;
if (timer.interval) {
const now = Date.now();
if (now - timer.started > timer.interval) {
// Pretend we got a timer event
timer.last_interval = 0;
timer.started = now;
ev.set_field(0, evtype_Timer);
}
}
}
glk_set_echo_line_event(win, val) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type === 'buffer') {
win.echo_line_input = !!val;
}
}
glk_set_hyperlink(val) {
this.glk_set_hyperlink_stream(this.current_stream, val);
}
glk_set_hyperlink_stream(str, val) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.type === 'window') {
str.set_hyperlink(val);
}
}
glk_set_style(style) {
this.glk_set_style_stream(this.current_stream, style);
}
glk_set_style_stream(str, style) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.type === 'window') {
if (style < 0 || style > style_NUMSTYLES) {
style = 0;
}
str.set_style(STYLE_NAMES[style]);
}
}
glk_set_terminators_line_event(win, keycodes) {
if (!win) {
throw new Error('Invalid Window');
}
const terminators = [];
if (keycodes) {
for (const code of keycodes) {
if (TERMINATOR_KEYS[code]) {
terminators.push(TERMINATOR_KEYS[code]);
}
}
}
if (terminators.length) {
win.input.terminators = terminators;
}
else {
delete win.input.terminators;
}
}
glk_set_window(win) {
this.current_stream = win ? win.str : null;
}
glk_sound_load_hint(_sound, _load) { }
glk_stream_close(str, result) {
if (!str) {
throw new Error('Invalid Stream');
}
str.close(result);
this.unregister_stream(str);
}
glk_stream_get_current() {
return this.current_stream;
}
glk_stream_get_position(str) {
if (!str) {
throw new Error('Invalid Stream');
}
return str.get_position();
}
glk_stream_get_rock(str) {
if (!str) {
throw new Error('Invalid Stream');
}
return str.rock;
}
glk_stream_iterate(str, rockbox) {
const next_stream = str ? str.next : this.first_stream;
if (rockbox) {
rockbox.set_value(next_stream ? next_stream.rock : 0);
}
return next_stream;
}
glk_stream_open_file(fref, mode, rock) {
return this.create_file_stream(fref, mode, rock, false);
}
glk_stream_open_file_uni(fref, mode, rock) {
return this.create_file_stream(fref, mode, rock, true);
}
glk_stream_open_memory(buf, mode, rock) {
return this.create_memory_stream(buf, mode, rock, Uint8Array);
}
glk_stream_open_memory_uni(buf, mode, rock) {
return this.create_memory_stream(buf, mode, rock, Uint32Array);
}
glk_stream_open_resource(filenum, rock) {
return this.create_resource_stream(filenum, rock, false);
}
glk_stream_open_resource_uni(filenum, rock) {
return this.create_resource_stream(filenum, rock, true);
}
glk_stream_set_current(str) {
this.current_stream = str;
}
glk_stream_set_position(str, pos, seekmode) {
if (!str) {
throw new Error('Invalid Stream');
}
str.set_position(seekmode, pos);
}
glk_style_distinguish(_win, _style1, _style2) {
return 0;
}
glk_style_measure(_win, _style, _hint, result) {
if (result) {
result.set_value(0);
}
return 0;
}
glk_stylehint_clear(wintype, style, hint) {
const selector = `.Style_${STYLE_NAMES[style]}${hint <= stylehint_Justification ? '_par' : ''}`;
function remove_style(styles) {
if (styles[selector]) {
delete styles[selector][CSS_STYLE_PROPERTIES[hint]];
if (!Object.keys(styles[selector]).length) {
delete styles[selector];
}
}
}
if (wintype === wintype_AllTypes || wintype === wintype_TextBuffer) {
remove_style(this.stylehints.buffer);
}
if (wintype === wintype_AllTypes || wintype === wintype_TextGrid) {
remove_style(this.stylehints.grid);
}
}
glk_stylehint_set(wintype, style, hint, value) {
if (style < 0 || style >= style_NUMSTYLES || hint < 0 || hint >= stylehint_NUMHINTS) {
return;
}
if (wintype === wintype_AllTypes) {
this.glk_stylehint_set(wintype_TextBuffer, style, hint, value);
this.glk_stylehint_set(wintype_TextGrid, style, hint, value);
return;
}
if (wintype === wintype_Blank || wintype === wintype_Graphics || wintype === wintype_Pair) {
return;
}
const stylehints = wintype === wintype_TextBuffer ? this.stylehints.buffer : this.stylehints.grid;
const selector = `.Style_${STYLE_NAMES[style]}${hint <= stylehint_Justification ? '_par' : ''}`;
const justifications = ['left', 'justify', 'center', 'right'];
const weights = ['lighter', 'normal', 'bold'];
let stylevalue;
if (hint === stylehint_Indentation || hint === stylehint_ParaIndentation) {
stylevalue = value + 'em';
}
if (hint === stylehint_Justification) {
stylevalue = justifications[value];
}
if (hint === stylehint_Size) {
stylevalue = (1 + value * 0.1) + 'em';
}
if (hint === stylehint_Weight) {
stylevalue = weights[value + 1];
}
if (hint === stylehint_Oblique) {
stylevalue = value ? 'italic' : 'normal';
}
if (hint === stylehint_Proportional) {
stylevalue = value ? 0 : 1;
}
if (hint === stylehint_TextColor || hint === stylehint_BackColor) {
stylevalue = colour_code_to_css(value);
}
if (hint === stylehint_ReverseColor) {
stylevalue = value;
}
if (stylevalue === undefined) {
return;
}
if (!stylehints[selector]) {
stylehints[selector] = {};
}
stylehints[selector][CSS_STYLE_PROPERTIES[hint]] = stylevalue;
}
glk_tick() { }
glk_simple_time_to_date_local(time, factor, struct) {
timestamp_to_date_struct_local(time * 1000 * factor, struct);
}
glk_simple_time_to_date_utc(time, factor, struct) {
timestamp_to_date_struct_utc(time * 1000 * factor, struct);
}
glk_time_to_date_local(timestruct, datestruct) {
timestamp_to_date_struct_local(time_struct_to_timestamp(timestruct), datestruct);
}
glk_time_to_date_utc(timestruct, datestruct) {
timestamp_to_date_struct_utc(time_struct_to_timestamp(timestruct), datestruct);
}
glk_window_clear(win) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.input.type === 'line') {
throw new Error('Window has pending line input');
}
win.clear();
}
glk_window_close(win, stats) {
if (!win) {
throw new Error('Invalid Window');
}
win.str.close(stats);
if (win === this.root_window) {
// Close the root window, which means all windows
this.root_window = null;
this.remove_window(win, true);
}
else {
const parent_win = win.parent;
const sibling_win = parent_win.child1 === win ? parent_win.child2 : parent_win.child1;
const grandparent_win = parent_win.parent;
if (grandparent_win) {
if (grandparent_win.child1 === parent_win) {
grandparent_win.child1 = sibling_win;
}
else {
grandparent_win.child2 = sibling_win;
}
sibling_win.parent = grandparent_win;
}
else {
this.root_window = sibling_win;
sibling_win.parent = null;
}
this.remove_window(win, true);
this.remove_window(parent_win, false);
this.rearrange_window(sibling_win, parent_win.box);
}
}
glk_window_erase_rect(win, left, top, width, height) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type !== 'graphics') {
throw new Error('Invalid Window: not a graphics window');
}
win.draw.push({
height,
special: 'fill',
width,
x: left,
y: top,
});
}
glk_window_fill_rect(win, colour, left, top, width, height) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type !== 'graphics') {
throw new Error('Invalid Window: not a graphics window');
}
win.draw.push({
color: colour_code_to_css(colour),
height,
special: 'fill',
width,
x: left,
y: top,
});
}
glk_window_flow_break(win) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type === 'buffer') {
win.set_flow_break();
}
}
glk_window_get_arrangement(win, method, size, keywin) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type !== 'pair') {
throw new Error('Invalid Window: not a pair window');
}
if (keywin) {
keywin?.set_value(win.key);
}
if (method) {
method?.set_value(win.dir | (win.fixed ? winmethod_Fixed : winmethod_Proportional) | (win.border ? winmethod_Border : winmethod_NoBorder));
}
if (size) {
size?.set_value(win.size);
}
}
glk_window_get_echo_stream(win) {
if (!win) {
throw new Error('Invalid Window');
}
return win.echostr;
}
glk_window_get_parent(win) {
if (!win) {
throw new Error('Invalid Window');
}
return win.parent;
}
glk_window_get_rock(win) {
if (!win) {
throw new Error('Invalid Window');
}
return win.rock;
}
glk_window_get_root() {
return this.root_window;
}
glk_window_get_sibling(win) {
if (!win) {
throw new Error('Invalid Window');
}
const parent = win.parent;
if (!parent) {
return null;
}
if (parent.child1 === win) {
return parent.child2;
}
else {
return parent.child1;
}
}
glk_window_get_size(win, widthbox, heightbox) {
if (!win) {
throw new Error('Invalid Window');
}
const metrics = this.metrics;
let height = 0;
let width = 0;
switch (win.type) {
case 'buffer':
height = normalise_window_dimension((win.box.bottom - win.box.top - metrics.buffermarginy) / metrics.buffercharheight);
width = normalise_window_dimension((win.box.right - win.box.left - metrics.buffermarginx) / metrics.buffercharwidth);
break;
case 'graphics':
case 'grid':
height = win.height;
width = win.width;
break;
}
if (heightbox) {
heightbox?.set_value(height);
}
if (widthbox) {
widthbox?.set_value(width);
}
}
glk_window_get_stream(win) {
if (!win) {
throw new Error('Invalid Window');
}
return win.str;
}
glk_window_get_type(win) {
if (!win) {
throw new Error('Invalid Window');
}
return win.typenum;
}
glk_window_iterate(win, rockbox) {
const next_window = win ? win.next : this.first_window;
if (rockbox) {
rockbox?.set_value(next_window ? next_window.rock : 0);
}
return next_window;
}
glk_window_move_cursor(win, xpos, ypos) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type !== 'grid') {
throw new Error('Invalid Window: not a grid window');
}
win.x = Math.max(0, xpos);
win.y = Math.max(0, ypos);
}
glk_window_open(splitwin, method, size, wintype, rock) {
// Check the parameters
if (!this.root_window) {
if (splitwin) {
throw new Error('Invalid splitwin: must be null for first window');
}
}
else {
if (!splitwin) {
throw new Error('Invalid splitwin');
}
if (splitwin.type === 'pair') {
throw new Error('Invalid splitwin: must not be a pair window');
}
const division = method & winmethod_DivisionMask;
const direction = method & winmethod_DirMask;
if (division !== winmethod_Fixed && division !== winmethod_Proportional) {
throw new Error('Invalid method: must be fixed or proportional');
}
if (division === winmethod_Fixed && splitwin.type === 'blank') {
throw new Error('Invalid method: blank windows cannot be only be split proportionally');
}
if (direction !== winmethod_Above && direction !== winmethod_Below && direction !== winmethod_Left && direction !== winmethod_Right) {
throw new Error('Invalid method: bad direction');
}
}
// Create the window
let win;
switch (wintype) {
case wintype_Blank:
win = new BlankWindow(rock);
break;
case wintype_Graphics:
win = new GraphicsWindow(rock);
break;
case wintype_TextBuffer:
win = new BufferWindow(rock, this.stylehints.buffer);
break;
case wintype_TextGrid:
win = new GridWindow(rock, this.stylehints.grid);
break;
default:
throw new Error('Invalid wintype');
}
this.register_window(win);
// Rearrange the windows for the new window
if (splitwin) {
const pairwin = new PairWindow(win, method, size);
this.register_window(pairwin);
// Set up the win relations
pairwin.child1 = splitwin;
pairwin.child2 = win;
const oldparent = splitwin.parent;
splitwin.parent = pairwin;
win.parent = pairwin;
pairwin.parent = oldparent;
if (oldparent) {
if (oldparent.child1 === splitwin) {
oldparent.child1 = pairwin;
}
else {
oldparent.child2 = pairwin;
}
}
else {
this.root_window = pairwin;
}
this.rearrange_window(pairwin, splitwin.box);
}
else {
this.root_window = win;
this.rearrange_window(win, {
bottom: this.metrics.height,
left: 0,
right: this.metrics.width,
top: 0,
});
}
return win;
}
glk_window_set_arrangement(win, method, size, keywin) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type !== 'pair') {
throw new Error('Invalid Window: not a pair window');
}
if (keywin) {
if (keywin.type === 'pair') {
throw new Error('Invalid keywin: cannot be a pair window');
}
let win_parent = keywin;
while ((win_parent = win_parent?.parent)) {
if (win_parent === win) {
break;
}
}
if (!win_parent) {
throw new Error('keywin must be a descendent');
}
}
const new_dir = method & winmethod_DirMask;
const new_vertical = new_dir === winmethod_Left || new_dir === winmethod_Right;
if (!keywin) {
keywin = win.key;
}
if (new_vertical && !win.vertical) {
throw new Error('Invalid method: split must stay horizontal');
}
if (!new_vertical && win.vertical) {
throw new Error('Invalid method: split must stay vertical');
}
const new_fixed = (method & winmethod_DivisionMask) === winmethod_Fixed;
if (keywin.type === 'blank' && new_fixed) {
throw new Error('Invalid method: blank windows cannot be only be split proportionally');
}
const new_backward = new_dir === winmethod_Left || new_dir === winmethod_Above;
if (new_backward !== win.backward) {
// Switch the children
const temp_win = win.child1;
win.child1 = win.child2;
win.child2 = temp_win;
}
// Update the window
win.backward = new_backward;
win.border = (method & winmethod_BorderMask) === winmethod_BorderMask;
win.dir = new_dir;
win.fixed = new_fixed;
win.key = keywin;
win.size = size;
win.vertical = new_vertical;
this.rearrange_window(win, win.box);
}
glk_window_set_background_color(win, colour) {
if (!win) {
throw new Error('Invalid Window');
}
if (win.type !== 'graphics') {
throw new Error('Invalid Window: not a graphics window');
}
win.draw.push({
color: colour_code_to_css(colour),
special: 'setcolor',
});
}
glk_window_set_echo_stream(win, stream) {
if (!win) {
throw new Error('Invalid Window');
}
win.echostr = stream;
}
garglk_set_reversevideo(val) {
this.garglk_set_reversevideo_stream(this.current_stream, val);
}
garglk_set_reversevideo_stream(str, val) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.type === 'window') {
str.set_css('reverse', val ? 1 : undefined);
}
}
garglk_set_zcolors(fg, bg) {
this.garglk_set_zcolors_stream(this.current_stream, fg, bg);
}
garglk_set_zcolors_stream(str, fg, bg) {
if (!str) {
throw new Error('Invalid Stream');
}
if (str.type === 'window') {
if (fg !== zcolor_Current) {
str.set_css('color', fg === zcolor_Default ? undefined : colour_code_to_css(fg));
}
if (bg !== zcolor_Current) {
str.set_css('background-color', bg === zcolor_Default ? undefined : colour_code_to_css(bg));
}
}
}
// Private internal functions
/** Process an input event */
accept(ev) {
if (this.exited) {
return this.GlkOte.log('GlkApi has exited');
}
if (ev.gen !== this.gen) {
return this.GlkOte.log(`Input event has wrong generation number: expected ${this.gen}, received ${ev.gen}`);
}
this.gen++;
if (!this.selectref && ev.type !== 'init' && ev.type !== 'specialresponse') {
return;
}
this.partial_inputs = ev.partial;
let type = evtype_None;
let win = null;
let val1 = 0;
let val2 = 0;
let fref;
if ('window' in ev) {
for (win = this.first_window; win; win = win.next) {
if (win.disprock === ev.window) {
break;
}
}
}
switch (ev.type) {
case 'init':
this.metrics = normalise_metrics(ev.metrics);
this.support = ev.support;
this.VM.start();
return;
case 'arrange':
this.metrics = normalise_metrics(ev.metrics);
if (this.root_window) {
this.rearrange_window(this.root_window, {
bottom: this.metrics.height,
left: 0,
right: this.metrics.width,
top: 0,
});
}
type = evtype_Arrange;
break;
case 'char':
if (win?.input.type !== 'char') {
return;
}
delete win.input.type;
type = evtype_CharInput;
if (ev.value.length === 1) {
val1 = ev.value.codePointAt(0);
if (!win.uni_input && val1 > MAX_LATIN1) {
val1 = QUESTION_MARK;
}
}
else {
val1 = KEY_NAMES_TO_CODES[ev.value] ?? keycode_Unknown;
}
break;
case 'hyperlink':
if (!win?.input.hyperlink) {
return;
}
delete win.input.hyperlink;
type = evtype_Hyperlink;
val1 = ev.value;
break;
case 'line':
if (win?.input.type !== 'line') {
return;
}
this.handle_line_input(win, ev.value, this.selectref, ev.terminator);
this.GiDispa?.prepare_resume(this.selectref);
delete this.selectref;
break;
case 'mouse':
if (!win?.input.mouse) {
return;
}
delete win.input.mouse;
type = evtype_MouseInput;
val1 = ev.x;
val2 = ev.y;
break;
case 'redraw':
type = evtype_Redraw;
break;
case 'specialresponse': {
if (ev.response !== 'fileref_prompt') {
throw new Error('Unknown type of specialresponse event');
}
const dialog_fref = ev.value;
if (typeof dialog_fref === 'string') {
throw new Error('AsyncGlk no longer supports bare-string filenames from Dialog');
}
if (dialog_fref) {
fref = this.create_fileref(dialog_fref.filename, this.special_data.rock, this.special_data.usage, dialog_fref);
}
break;
}
case 'timer':
type = evtype_Timer;
this.timer.started = Date.now();
break;
default:
throw new Error(`Event type ${ev.type} not supported by AsyncGlk`);
}
if (this.selectref) {
set_event(this.selectref, type, win, val1, val2);
this.GiDispa?.prepare_resume(this.selectref);
delete this.selectref;
}
this.VM.resume(fref);
}
create_fileref(filename, rock, usage, dialog_fref) {
if (!dialog_fref) {
const filetype = usage & fileusage_TypeMask;
const signature = filetype === fileusage_SavedGame ? this.VM.get_signature() : undefined;
dialog_fref = this.Dialog.file_construct_ref(filename, FILE_TYPES[filetype] ?? 'xxx', signature);
}
const fref = new FileRef(this.Dialog, filename, dialog_fref, rock, usage);
f