{ "version": 3, "sources": ["../src/platform/zmachine.ts"], "sourcesContent": ["\nimport { Platform, BasePlatform, BaseDebugPlatform, Preset, EmuState, inspectSymbol } from \"../common/baseplatform\";\nimport { EmuHalt, PLATFORMS } from \"../common/emu\";\nimport { TeleType, TeleTypeWithKeyboard } from \"../common/teletype\";\nimport { InputResponse } from \"../common/basic/runtime\";\nimport { loadScript } from \"../common/util\";\n\nconst ZMACHINE_PRESETS = [\n { id: 'hello.inf', name: 'Hello World' },\n { id: 'house01.inf', name: 'House Tutorial #1' },\n { id: 'house02.inf', name: 'House Tutorial #2' },\n { id: 'house03.inf', name: 'House Tutorial #3' },\n { id: 'house04.inf', name: 'House Tutorial #4' },\n { id: 'house05.inf', name: 'House Tutorial #5' },\n { id: 'house06.inf', name: 'House Tutorial #6' },\n { id: 'house07.inf', name: 'House Tutorial #7' },\n { id: 'alice.inf', name: 'Through the Looking-Glass' },\n { id: 'aloneice.inf', name: 'Alone on the Ice' },\n { id: 'adventureland.inf', name: 'Adventureland' },\n { id: 'toyshop.inf', name: 'Toyshop' },\n { id: 'ruins1.inf', name: 'Ruins #1' },\n { id: 'ruins2.inf', name: 'Ruins #2' },\n { id: 'ruins3.inf', name: 'Ruins #3' },\n { id: 'balances.inf', name: 'Balances' },\n { id: 'museum.inf', name: 'Museum of Inform' },\n { id: 'advent.inf', name: 'Colossal Cave Adventure' },\n { id: 'ztrek.inf', name: 'Super Z Trek' },\n];\n\ndeclare var ZVM;\n\n// https://github.com/erkyrath/quixe/wiki/Quixe-Without-GlkOte#quixes-api\n// https://eblong.com/zarf/glk/glkote/docs.html\n// https://inform-fiction.org/zmachine/standards/z1point0/sect15.html#read_char\n// https://manpages.debian.org/testing/inform6-compiler/inform6.1.en.html\n\ninterface IFZVM {\n start();\n run();\n version: number;\n pc: number;\n ram: DataView;\n stack: DataView;\n read_data: { buffer?, routine?, time?};\n handle_line_input(len: number);\n handle_char_input(charcode: number);\n handle_create_fileref(fref: number);\n}\n\nfunction debug(...args: any[]) {\n //console.log(arguments);\n}\n\nclass GlkImpl {\n vm: IFZVM;\n input: HTMLInputElement;\n mainwnd : TeleTypeWithKeyboard;\n curwnd: TeleType;\n windows: { [win: number]: TeleType };\n windowcount: number;\n exited = false;\n\n constructor(page: HTMLElement, input: HTMLInputElement, upper: HTMLElement) {\n this.mainwnd = new TeleTypeWithKeyboard(page, false, input);\n this.mainwnd.keepinput = false; // input moves w/ cursor\n this.mainwnd.splitInput = false;\n this.mainwnd.uppercaseOnly = true;\n this.mainwnd.hideinput();\n this.windows = {\n 1: this.mainwnd,\n 2: new TeleType(upper, true),\n 3: new TeleType(null, true), // fake window for resizing\n };\n this.input = input;\n this.mainwnd.resolveInput = (resp:InputResponse) => {\n var s = resp.line;\n if (this.vm.read_data.buffer) {\n for (var i = 0; i < s.length; i++) {\n this.vm.read_data.buffer[i] = s.charCodeAt(i) & 0xff;\n }\n this.vm.handle_line_input(s.length);\n } else {\n this.vm.handle_char_input(s.charCodeAt(0));\n }\n this.vm.run();\n }\n this.reset();\n }\n reset() {\n this.windowcount = 0;\n this.exited = false;\n this.windows[1].clear();\n this.windows[2].clear();\n this.curwnd = this.windows[1];\n }\n init(options) {\n this.vm = options.vm;\n this.vm.start();\n }\n fatal_error(s: string) {\n throw new EmuHalt(s);\n }\n update() {\n // TODO\n }\n focusinput() {\n this.mainwnd.focusinput();\n }\n flushline() {\n this.curwnd.flushline();\n }\n\n glk_exit() {\n this.exited = true;\n this.flushline();\n this.windows[1].addtext(\"** Game exited **\", 1);\n }\n glk_window_clear(win) {\n debug('glk_window_clear', arguments);\n this.windows[win].clear();\n }\n glk_request_line_event_uni(win, buf, initlen) {\n this.mainwnd.waitingfor = 'line';\n this.focusinput();\n this.startinputtimer();\n }\n glk_request_char_event_uni(win, buf, initlen) {\n this.mainwnd.waitingfor = 'char';\n this.focusinput();\n this.startinputtimer();\n }\n startinputtimer() {\n /* TODO?\n var rd = this.vm.read_data;\n if (rd.routine && rd.time) {\n this.vm['call'](rd.routine);\n //this.vm.run();\n setTimeout(this.startinputtimer.bind(this), rd.time*10);\n }\n */\n }\n\n glk_put_jstring(val: string, allbytes) {\n //debug('glk_put_jstring', arguments);\n this.curwnd.print(val);\n }\n glk_put_jstring_stream(stream: number, val: string) {\n //debug('glk_put_jstring_stream', arguments);\n this.windows[stream].print(val);\n }\n glk_put_char_stream_uni(stream: number, ch: number) {\n //debug('glk_put_char_stream_uni', arguments);\n this.windows[stream].print(String.fromCharCode(ch));\n }\n glk_set_style(val) {\n this.curwnd.curstyle = val;\n }\n /*\n glk_put_char(ch) {\n debug('glk_put_char', arguments);\n }\n glk_put_string(val) {\n debug('glk_put_string', arguments);\n }\n glk_put_string_stream(str, val) {\n debug('glk_put_string_stream', arguments);\n }\n glk_put_buffer(arr) {\n debug('glk_put_buffer', arguments);\n }\n glk_put_buffer_stream(str, arr) {\n debug('glk_put_buffer_stream', arguments);\n }\n glk_set_style_stream(str, val) {\n debug('glk_set_style_stream', arguments);\n }\n glk_get_char_stream(str) {\n debug('glk_get_char_stream', arguments);\n }\n glk_get_line_stream(str, buf) {\n debug('glk_get_line_stream', arguments);\n }\n glk_get_buffer_stream(str, buf) {\n debug('glk_get_buffer_stream', arguments);\n }\n */\n glk_char_to_lower(val) {\n if (val >= 0x41 && val <= 0x5A)\n return val + 0x20;\n if (val >= 0xC0 && val <= 0xDE && val != 0xD7)\n return val + 0x20;\n return val;\n }\n glk_char_to_upper(val) {\n if (val >= 0x61 && val <= 0x7A)\n return val - 0x20;\n if (val >= 0xE0 && val <= 0xFE && val != 0xF7)\n return val - 0x20;\n return val;\n }\n glk_stylehint_set(wintype, styl, hint, value) {\n //debug('glk_stylehint_set', arguments);\n }\n glk_stylehint_clear(wintype, styl, hint) {\n //debug('glk_stylehint_clear', arguments);\n }\n glk_style_distinguish(win, styl1, styl2) {\n return 0;\n }\n glk_style_measure(win, styl, hint, resultref) {\n if (resultref)\n resultref.set_value(0);\n return 0;\n }\n glk_select(eventref) {\n debug('glk_select', arguments);\n }\n glk_window_open(splitwin, method, size, wintype, rock) {\n debug('glk_window_open', arguments);\n if (splitwin) {\n if (method != 0x12 || wintype != 4) return 0;\n if (size) {\n $(this.windows[2].page).show();\n return 2; // split window\n } else {\n return 3; // fake window\n }\n } else {\n return 1; // main window\n }\n }\n glk_window_close(win) {\n debug('glk_window_close', arguments);\n if (win == 2) {\n this.windows[win].clear();\n $(this.windows[win].page).hide();\n }\n }\n glk_window_get_parent(win) {\n debug('glk_window_get_parent', arguments);\n if (win == 1) return 0;\n else return 1;\n }\n glk_window_move_cursor(win, col, row) {\n debug('glk_window_move_cursor', arguments);\n this.windows[win].move_cursor(col, row);\n }\n glk_window_set_arrangement(win, method, size, unknown) {\n debug('glk_window_set_arrangement', arguments);\n if (win == 1) this.windows[2].setrows(size);\n }\n glk_window_get_stream(win) {\n debug('glk_window_get_stream', arguments);\n return win;\n }\n glk_set_window(win) {\n debug('glk_set_window', arguments);\n this.curwnd = this.windows[win];\n if (this.curwnd == null) this.fatal_error(\"no window \" + win);\n }\n glk_window_get_size(win, widthref: RefBox, heightref: RefBox) {\n debug('glk_window_get_size', arguments);\n // TODO: made up sizes, only status line supported\n if (widthref) widthref.set_value(STATUS_NUM_COLS);\n if (heightref) heightref.set_value(win == 1 ? 25 : 1);\n }\n garglk_set_reversevideo(val) {\n debug('garglk_set_reversevideo', arguments);\n this.curwnd.reverse = !!val;\n }\n garglk_set_reversevideo_stream(win, val) {\n debug('garglk_set_reversevideo_stream', arguments);\n this.windows[win].reverse = !!val;\n }\n glk_fileref_create_by_prompt(usage, mode, rock) {\n debug('glk_fileref_create_by_prompt', arguments);\n // TODO: support files?\n this.vm.handle_create_fileref(0);\n this.vm.run();\n }\n glk_gestalt(sel, val) {\n return this.glk_gestalt_ext(sel, val, null);\n }\n glk_gestalt_ext(sel, val, arr) {\n //debug('glk_gestalt_ext', arguments);\n switch (sel) {\n\n case 0: // gestalt_Version\n /* This implements Glk spec version 0.7.4? */\n return 0x00000101; // 0.1.1\n\n case 1: // gestalt_CharInput\n /* This is not a terrific approximation. Return false for function\n keys, control keys, and the high-bit non-printables. For\n everything else in the Unicode range, return true. */\n if (val <= Const.keycode_Left && val >= Const.keycode_End)\n return 1;\n if (val >= 0x100000000 - Const.keycode_MAXVAL)\n return 0;\n if (val > 0x10FFFF)\n return 0;\n if ((val >= 0 && val < 32) || (val >= 127 && val < 160))\n return 0;\n return 1;\n\n case 2: // gestalt_LineInput\n /* Same as the above, except no special keys. */\n if (val > 0x10FFFF)\n return 0;\n if ((val >= 0 && val < 32) || (val >= 127 && val < 160))\n return 0;\n return 1;\n\n case 3: // gestalt_CharOutput\n /* Same thing again. We assume that all printable characters,\n as well as the placeholders for nonprintables, are one character\n wide. */\n if ((val > 0x10FFFF)\n || (val >= 0 && val < 32)\n || (val >= 127 && val < 160)) {\n if (arr)\n arr[0] = 1;\n return 0; // gestalt_CharOutput_CannotPrint\n }\n if (arr)\n arr[0] = 1;\n return 2; // gestalt_CharOutput_ExactPrint\n\n case 4: // gestalt_MouseInput\n if (val == Const.wintype_TextBuffer)\n return 0;\n if (val == Const.wintype_Graphics && has_canvas)\n return 0;\n return 0;\n\n case 5: // gestalt_Timer\n return 0;\n\n case 6: // gestalt_Graphics\n return 0;\n\n case 7: // gestalt_DrawImage\n if (val == Const.wintype_TextBuffer)\n return 0;\n if (val == Const.wintype_Graphics && has_canvas)\n return 0;\n return 0;\n\n case 8: // gestalt_Sound\n return 0;\n\n case 9: // gestalt_SoundVolume\n return 0;\n\n case 10: // gestalt_SoundNotify\n return 0;\n\n case 11: // gestalt_Hyperlinks\n return 0;\n\n case 12: // gestalt_HyperlinkInput\n if (val == 3 || val == 4) // TextBuffer or TextGrid\n return 0;\n else\n return 0;\n\n case 13: // gestalt_SoundMusic\n return 0;\n\n case 14: // gestalt_GraphicsTransparency\n return 0;\n\n case 15: // gestalt_Unicode\n return 1;\n\n case 16: // gestalt_UnicodeNorm\n return 1;\n\n case 17: // gestalt_LineInputEcho\n return 1;\n\n case 18: // gestalt_LineTerminators\n return 1;\n\n case 19: // gestalt_LineTerminatorKey\n /* Really this result should be inspected from glkote.js. Since it\n isn't, be sure to keep these values in sync with \n terminator_key_names. */\n if (val == Const.keycode_Escape)\n return 0;\n if (val >= Const.keycode_Func12 && val <= Const.keycode_Func1)\n return 0;\n return 0;\n\n case 20: // gestalt_DateTime\n return 0;\n\n case 21: // gestalt_Sound2\n return 0;\n\n case 22: // gestalt_ResourceStream\n return 0;\n\n case 23: // gestalt_GraphicsCharInput\n return 0;\n\n case 0x1100: // reverse video, color\n return 0;\n }\n\n return 0;\n }\n\n /* Dummy return value, which means that the Glk call is still in progress,\n or will never return at all. This is used by glk_exit(), glk_select(),\n and glk_fileref_create_by_prompt().\n */\n DidNotReturn = { dummy: 'Glk call has not yet returned' };\n RefBox = RefBox;\n RefStruct = RefStruct;\n}\n\n/* RefBox: Simple class used for \"call-by-reference\" Glk arguments. The object\n is just a box containing a single value, which can be written and read.\n*/\nclass RefBox {\n value;\n set_value(val) {\n this.value = val;\n }\n get_value() {\n return this.value;\n }\n}\n\n/* RefStruct: Used for struct-type Glk arguments. After creating the\n object, you should call push_field() the appropriate number of times,\n to set the initial field values. Then set_field() can be used to\n change them, and get_fields() retrieves the list of all fields.\n\n (The usage here is loose, since Javascript is forgiving about arrays.\n Really the caller could call set_field() instead of push_field() --\n or skip that step entirely, as long as the Glk function later calls\n set_field() for each field. Which it should.)\n*/\nclass RefStruct {\n constructor(numels) {\n }\n fields = [];\n push_field(val) {\n this.fields.push(val);\n }\n set_field(pos, val) {\n this.fields[pos] = val;\n }\n get_field(pos) {\n return this.fields[pos];\n }\n get_fields() {\n return this.fields;\n }\n}\n\n\nconst has_canvas = typeof window === 'object';\n\nconst Const = {\n gestalt_Version: 0,\n gestalt_CharInput: 1,\n gestalt_LineInput: 2,\n gestalt_CharOutput: 3,\n gestalt_CharOutput_CannotPrint: 0,\n gestalt_CharOutput_ApproxPrint: 1,\n gestalt_CharOutput_ExactPrint: 2,\n gestalt_MouseInput: 4,\n gestalt_Timer: 5,\n gestalt_Graphics: 6,\n gestalt_DrawImage: 7,\n gestalt_Sound: 8,\n gestalt_SoundVolume: 9,\n gestalt_SoundNotify: 10,\n gestalt_Hyperlinks: 11,\n gestalt_HyperlinkInput: 12,\n gestalt_SoundMusic: 13,\n gestalt_GraphicsTransparency: 14,\n gestalt_Unicode: 15,\n gestalt_UnicodeNorm: 16,\n gestalt_LineInputEcho: 17,\n gestalt_LineTerminators: 18,\n gestalt_LineTerminatorKey: 19,\n gestalt_DateTime: 20,\n gestalt_Sound2: 21,\n gestalt_ResourceStream: 22,\n gestalt_GraphicsCharInput: 23,\n\n keycode_Unknown: 0xffffffff,\n keycode_Left: 0xfffffffe,\n keycode_Right: 0xfffffffd,\n keycode_Up: 0xfffffffc,\n keycode_Down: 0xfffffffb,\n keycode_Return: 0xfffffffa,\n keycode_Delete: 0xfffffff9,\n keycode_Escape: 0xfffffff8,\n keycode_Tab: 0xfffffff7,\n keycode_PageUp: 0xfffffff6,\n keycode_PageDown: 0xfffffff5,\n keycode_Home: 0xfffffff4,\n keycode_End: 0xfffffff3,\n keycode_Func1: 0xffffffef,\n keycode_Func2: 0xffffffee,\n keycode_Func3: 0xffffffed,\n keycode_Func4: 0xffffffec,\n keycode_Func5: 0xffffffeb,\n keycode_Func6: 0xffffffea,\n keycode_Func7: 0xffffffe9,\n keycode_Func8: 0xffffffe8,\n keycode_Func9: 0xffffffe7,\n keycode_Func10: 0xffffffe6,\n keycode_Func11: 0xffffffe5,\n keycode_Func12: 0xffffffe4,\n /* The last keycode is always (0x100000000 - keycode_MAXVAL) */\n keycode_MAXVAL: 28,\n\n evtype_None: 0,\n evtype_Timer: 1,\n evtype_CharInput: 2,\n evtype_LineInput: 3,\n evtype_MouseInput: 4,\n evtype_Arrange: 5,\n evtype_Redraw: 6,\n evtype_SoundNotify: 7,\n evtype_Hyperlink: 8,\n evtype_VolumeNotify: 9,\n\n style_Normal: 0,\n style_Emphasized: 1,\n style_Preformatted: 2,\n style_Header: 3,\n style_Subheader: 4,\n style_Alert: 5,\n style_Note: 6,\n style_BlockQuote: 7,\n style_Input: 8,\n style_User1: 9,\n style_User2: 10,\n style_NUMSTYLES: 11,\n\n wintype_AllTypes: 0,\n wintype_Pair: 1,\n wintype_Blank: 2,\n wintype_TextBuffer: 3,\n wintype_TextGrid: 4,\n wintype_Graphics: 5,\n\n winmethod_Left: 0x00,\n winmethod_Right: 0x01,\n winmethod_Above: 0x02,\n winmethod_Below: 0x03,\n winmethod_DirMask: 0x0f,\n\n winmethod_Fixed: 0x10,\n winmethod_Proportional: 0x20,\n winmethod_DivisionMask: 0xf0,\n\n winmethod_Border: 0x000,\n winmethod_NoBorder: 0x100,\n winmethod_BorderMask: 0x100,\n\n fileusage_Data: 0x00,\n fileusage_SavedGame: 0x01,\n fileusage_Transcript: 0x02,\n fileusage_InputRecord: 0x03,\n fileusage_TypeMask: 0x0f,\n\n fileusage_TextMode: 0x100,\n fileusage_BinaryMode: 0x000,\n\n filemode_Write: 0x01,\n filemode_Read: 0x02,\n filemode_ReadWrite: 0x03,\n filemode_WriteAppend: 0x05,\n\n seekmode_Start: 0,\n seekmode_Current: 1,\n seekmode_End: 2,\n\n stylehint_Indentation: 0,\n stylehint_ParaIndentation: 1,\n stylehint_Justification: 2,\n stylehint_Size: 3,\n stylehint_Weight: 4,\n stylehint_Oblique: 5,\n stylehint_Proportional: 6,\n stylehint_TextColor: 7,\n stylehint_BackColor: 8,\n stylehint_ReverseColor: 9,\n stylehint_NUMHINTS: 10,\n\n stylehint_just_LeftFlush: 0,\n stylehint_just_LeftRight: 1,\n stylehint_just_Centered: 2,\n stylehint_just_RightFlush: 3,\n\n imagealign_InlineUp: 1,\n imagealign_InlineDown: 2,\n imagealign_InlineCenter: 3,\n imagealign_MarginLeft: 4,\n imagealign_MarginRight: 5\n\n};\n\n//\n\nconst STATUS_NUM_COLS = 80;\n\nclass ZmachinePlatform implements Platform {\n mainElement: HTMLElement;\n zfile: Uint8Array;\n zvm;\n glk;\n focused = false;\n\n constructor(mainElement: HTMLElement) {\n this.mainElement = mainElement;\n $(mainElement).css('overflowY', 'auto');\n }\n\n async start() {\n await loadScript('./lib/zvm/ifvms.min.js');\n // create divs\n var parent = this.mainElement;\n var gameport = $('