UNPKG

@bitblit/asyncglk

Version:
1,444 lines (1,441 loc) 69.4 kB
/* 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