UNPKG

ifvms

Version:

The Interactive Fiction Virtual Machines Suite - in Javascript

1,195 lines (1,069 loc) 27.7 kB
/* Z-Machine IO ============ Copyright (c) 2020 The ifvms.js team MIT licenced https://github.com/curiousdannii/ifvms.js */ 'use strict'; /* TODO: - pre-existing line input - timed input - mouse input - write colours into header */ const utils = require('../common/utils.js') const U2S = utils.U2S16 //S2U = utils.S2U16 // Glulx key codes accepted by the Z-Machine const ZSCII_keyCodes = (function() { var codes = { 0xfffffff9: 8, // delete/backspace 0xfffffffa: 13, // enter 0xfffffff8: 27, // escape 0xfffffffc: 129, // up 0xfffffffb: 130, // down 0xfffffffe: 131, // left 0xfffffffd: 132, // right 0xfffffff3: 146, // End / key pad 1 0xfffffff5: 148, // PgDn / key pad 3 0xfffffff4: 152, // Home / key pad 7 0xfffffff6: 154, // PgUp / key pad 9 }, i = 0; while ( i < 12 ) { codes[ 0xffffffef - i ] = 133 + i++; // function keys } return codes; })() // Style mappings // The index bits are (lowest to highest): mono, italic, bold const style_mappings = [0, 2, 1, 10, 4, 9, 5, 6] // Convert a 15 bit colour to RGB function convert_true_colour(colour) { const from5to8 = [0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255] // Stretch the five bits per colour out to 8 bits return (from5to8[colour & 0x1F] << 16) | (from5to8[(colour & 0x03E0) >> 5] << 8) | (from5to8[(colour & 0x7C00) >> 10]) } // The standard 15 bit colour values const zcolours = [ 0xFFFE, // Current 0xFFFF, // Default 0x0000, // Black 0x001D, // Red 0x0340, // Green 0x03BD, // Yellow 0x59A0, // Blue 0x7C1F, // Magenta 0x77A0, // Cyan 0x7FFF, // White 0x5AD6, // Light grey 0x4631, // Medium grey 0x2D6B, // Dark grey ] module.exports = { init_io: function() { this.io = { reverse: 0, bold: 0, italic: 0, bg: -1, fg: -1, // A variable for whether we are outputing in a monospaced font. If non-zero then we are // Bit 0 is for @set_style, bit 1 for the header, and bit 2 for @set_font mono: this.m.getUint8( 0x11 ) & 0x02, // A variable for checking whether the transcript bit has been changed transcript: this.m.getUint8( 0x11 ) & 0x01, // Index 0 is input stream 1, the output streams follow streams: [ 0, 1, {}, [], {} ], currentwin: 0, // Use Zarf's algorithm for the upper window // http://eblong.com/zarf/glk/quote-box.html // Implemented in fix_upper_window() and split_window() height: 0, // What the VM thinks the height is glkheight: 0, // Actual height of the Glk window maxheight: 0, // Height including quote boxes etc seenheight: 0, // Last height the player saw width: 0, row: 0, col: 0, }; //this.process_colours(); // Construct the windows if they do not already exist this.open_windows() }, erase_line: function( value ) { if ( value === 1 ) { var io = this.io, row = io.row, col = io.col; this._print( Array( io.width - io.col + 1 ).join( ' ' ) ); this.set_cursor( row, col ); } }, erase_window: function(window) { if (window < 1) { this.Glk.glk_window_clear(this.mainwin) if (this.io.bg >= 0) { this.Glk.glk_stylehint_set(3, 0, 8, this.io.bg) } else if (this.io.bg === -1) { this.Glk.glk_stylehint_clear(3, 0, 8) } } if (window !== 0) { if (window === -1) { this.split_window(0) } if (this.upperwin) { this.Glk.glk_window_clear(this.upperwin) this.set_cursor(0, 0) } } }, fileref_create_by_prompt: function( data ) { if ( typeof data.run === 'undefined' ) { data.run = 1; } this.fileref_data = data; this.glk_blocking_call = 'fileref_create_by_prompt'; this.Glk.glk_fileref_create_by_prompt( data.usage, data.mode, data.rock || 0 ); }, // Fix the upper window height before an input event fix_upper_window: function() { var Glk = this.Glk, io = this.io; // If we have seen the entire window, shrink it to what it should be if (io.seenheight >= io.maxheight) { io.maxheight = io.height; } if ( this.upperwin ) { if ( io.maxheight === 0 ) { Glk.glk_window_close( this.upperwin ); this.upperwin = null; } else if (io.maxheight !== io.glkheight) { Glk.glk_window_set_arrangement( Glk.glk_window_get_parent( this.upperwin ), 0x12, io.maxheight, null ); } io.glkheight = io.maxheight } io.seenheight = io.maxheight; io.maxheight = io.height; }, format: function() { this.Glk.glk_set_style(style_mappings[!!this.io.mono | this.io.italic | this.io.bold]) if (this.Glk.glk_gestalt(0x1100, 0)) { this.Glk.garglk_set_reversevideo(this.io.reverse) } }, get_cursor: function( array ) { this.ram.setUint16( array, this.io.row + 1 ); this.ram.setUint16( array + 2, this.io.col + 1 ); }, // Handle char input handle_char_input: function( charcode ) { var stream4 = this.io.streams[4], code = ZSCII_keyCodes[ charcode ] || this.reverse_unicode_table[ charcode ] || 63; this.variable( this.read_data.storer, code ); // Echo to the commands log if ( stream4.mode === 1 ) { stream4.cache += code; } if ( stream4.mode === 2 ) { this.Glk.glk_put_char_stream_uni( stream4.str, code ); } }, // Handle the result of glk_fileref_create_by_prompt() handle_create_fileref: function( fref ) { var Glk = this.Glk, data = this.fileref_data, str; if ( fref ) { if ( data.unicode ) { str = Glk.glk_stream_open_file_uni( fref, data.mode, data.rock || 0 ); } else { str = Glk.glk_stream_open_file( fref, data.mode, data.rock || 0 ); } Glk.glk_fileref_destroy( fref ); } if ( data.func === 'restore' || data.func === 'save' ) { this.save_restore_handler( str ); } if ( data.func === 'input_stream' ) { this.io.streams[0] = str; } if ( data.func === 'output_stream' ) { this.output_stream_handler( str ); } // Signal to resume() to call run() if required return data.run; }, // Handle line input handle_line_input: function( len, terminator ) { var ram = this.ram, options = this.read_data, streams = this.io.streams, // Cut the response to len, convert to a lower case string, and then to a ZSCII array command = String.fromCharCode.apply( null, options.buffer.slice( 0, len ) ) + '\n', response = this.text_to_zscii( command.slice( 0, -1 ).toLowerCase() ); // 7.1.1.1: The response must be echoed, Glk will handle this // But we do have to echo to the transcripts if ( streams[2].mode === 1 ) { streams[2].cache += command; } if ( streams[2].mode === 2 ) { this.Glk.glk_put_jstring_stream( streams[2].str, command ); } if ( streams[4].mode === 1 ) { streams[4].cache += command; } if ( streams[4].mode === 2 ) { this.Glk.glk_put_jstring_stream( streams[4].str, command ); } // Store the response if ( this.version < 5 ) { // Append zero terminator response.push( 0 ); // Store the response in the buffer ram.setUint8Array( options.bufaddr + 1, response ); } else { // Store the response length ram.setUint8( options.bufaddr + 1, len ); // Store the response in the buffer ram.setUint8Array( options.bufaddr + 2, response ); // Store the terminator this.variable( options.storer, isNaN( terminator ) ? 13 : terminator ); } if ( options.parseaddr ) { // Tokenise the response this.tokenise( options.bufaddr, options.parseaddr ); } }, input_stream: function( stream ) { var io = this.io; if ( stream && !io.streams[0] ) { this.fileref_create_by_prompt({ func: 'input_stream', mode: 0x02, rock: 212, unicode: 1, usage: 0x103, }); } if ( !stream && io.streams[0] ) { this.Glk.glk_stream_close( io.streams[0] ); io.streams[0] = 0; } }, // Open windows open_windows: function() { const Glk = this.Glk if (!this.mainwin) { // We will borrow the general approach of Bocfel to implement the Z-Machine's formatting model in Glk // https://github.com/garglk/garglk/blob/master/terps/bocfel/screen.c // Reset some Glk stylehints just in case const styles_to_reset = [1, 2, 4, 5, 6, 9, 10] for (let i = 0; i < 7; i++) { // Reset the size, weight, and obliqueness Glk.glk_stylehint_set(0, styles_to_reset[i], 3, 0) Glk.glk_stylehint_set(0, styles_to_reset[i], 4, 0) Glk.glk_stylehint_set(0, styles_to_reset[i], 5, 0) // And force proportional font Glk.glk_stylehint_set(0, styles_to_reset[i], 6, 1) } // Now set the style hints we will use // Bold will use subheader Glk.glk_stylehint_set(0, 4, 4, 1) // Italic will use emphasised Glk.glk_stylehint_set(0, 1, 5, 1) // Bold+italic will use alert Glk.glk_stylehint_set(0, 5, 4, 1) Glk.glk_stylehint_set(0, 5, 5, 1) // Fixed will use preformated Glk.glk_stylehint_set(0, 2, 6, 0) // Bold+fixed will use user1 Glk.glk_stylehint_set(0, 9, 4, 1) Glk.glk_stylehint_set(0, 9, 6, 0) // Italic+fixed will use user2 Glk.glk_stylehint_set(0, 10, 5, 1) Glk.glk_stylehint_set(0, 10, 6, 0) // Bold+italic+fixed will use note Glk.glk_stylehint_set(0, 6, 4, 1) Glk.glk_stylehint_set(0, 6, 5, 1) Glk.glk_stylehint_set(0, 6, 6, 0) this.mainwin = Glk.glk_window_open(0, 0, 0, 3, 201) Glk.glk_set_window(this.mainwin) if (this.version3) { this.statuswin = Glk.glk_window_open(this.mainwin, 0x12, 1, 4, 202) if (this.statuswin && this.Glk.glk_gestalt(0x1100, 0)) { Glk.garglk_set_reversevideo_stream(Glk.glk_window_get_stream(this.statuswin), 1) } } } else { // Clean up after restarting Glk.glk_stylehint_clear(0, 0, 8) if (this.Glk.glk_gestalt(0x1100, 0)) { Glk.garglk_set_zcolors_stream(this.mainwin.str, this.io.fg, this.io.bg) } Glk.glk_window_clear(this.mainwin) if (this.upperwin) { Glk.glk_window_close(this.upperwin) this.upperwin = null } } }, // Manage output streams output_stream: function( stream, addr, called_from_print ) { var ram = this.ram, streams = this.io.streams, data, text; stream = U2S( stream ); // The screen if ( stream === 1 ) { streams[1] = 1; } if ( stream === -1 ) { streams[1] = 0; } // Transcript if ( stream === 2 && !streams[2].mode ) { this.fileref_create_by_prompt({ func: 'output_stream', mode: 0x05, rock: 210, run: !called_from_print, str: 2, unicode: 1, usage: 0x102, }); streams[2].cache = ''; streams[2].mode = 1; if ( !called_from_print ) { this.stop = 1; } } if ( stream === -2 ) { ram.setUint8( 0x11, ( ram.getUint8( 0x11 ) & 0xFE ) ); if ( streams[2].mode === 2 ) { this.Glk.glk_stream_close( streams[2].str ); } streams[2].mode = this.io.transcript = 0; } // Memory if ( stream === 3 ) { streams[3].unshift( [ addr, '' ] ); } if ( stream === -3 ) { data = streams[3].shift(); text = this.text_to_zscii( data[1] ); ram.setUint16( data[0], text.length ); ram.setUint8Array( data[0] + 2, text ); } // Command list if ( stream === 4 && !streams[4].mode ) { this.fileref_create_by_prompt({ func: 'output_stream', mode: 0x05, rock: 211, str: 4, unicode: 1, usage: 0x103, }); streams[4].cache = ''; streams[4].mode = 1; this.stop = 1; } if ( stream === -4 ) { if ( streams[4].mode === 2 ) { this.Glk.glk_stream_close( streams[4].str ); } streams[4].mode = 0; } }, output_stream_handler: function( str ) { var ram = this.ram, streams = this.io.streams, data = this.fileref_data; if ( data.str === 2 ) { ram.setUint8( 0x11, ( ram.getUint8( 0x11 ) & 0xFE ) | ( str ? 1 : 0 ) ); if ( str ) { streams[2].mode = 2; streams[2].str = str; this.io.transcript = 1; if ( streams[2].cache ) { this.Glk.glk_put_jstring_stream( streams[2].str, streams[2].cache ); } } else { streams[2].mode = this.io.transcript = 0; } } if ( data.str === 4 ) { if ( str ) { streams[4].mode = 2; streams[4].str = str; if ( streams[4].cache ) { this.Glk.glk_put_jstring_stream( streams[4].str, streams[4].cache ); } } else { streams[4].mode = 0; } } }, // Print text! _print: function( text ) { var Glk = this.Glk, io = this.io, i = 0; // Stream 3 gets the text first if ( io.streams[3].length ) { io.streams[3][0][1] += text; } else { // Convert CR into LF text = text.replace( /\r/g, '\n' ); // Check the transcript bit // Because it might need to prompt for a file name, we return here, and will print again in the handler if ( ( this.m.getUint8( 0x11 ) & 0x01 ) !== io.transcript ) { this.output_stream( io.transcript ? -2 : 2, 0, 1 ); } // Check if the monospace font bit has changed // Unfortunately, even now Inform changes this bit for the font statement, even though the 1.1 standard depreciated it :( if ( ( this.m.getUint8( 0x11 ) & 0x02 ) !== ( io.mono & 0x02 ) ) { io.mono ^= 0x02; this.format(); } // For the upper window we print each character individually so that we can track the cursor position if ( io.currentwin && this.upperwin ) { // Don't automatically increase the size of the window // If we confirm that games do need this then we can implement it later while ( i < text.length && io.row < io.height ) { Glk.glk_put_jstring( text[i++] ); io.col++; if ( io.col === io.width ) { io.col = 0; io.row++; } } } else if ( !io.currentwin ) { if ( io.streams[1] ) { Glk.glk_put_jstring( text ); } // Transcript if ( io.streams[2].mode === 1 ) { io.streams[2].cache += text; } if ( io.streams[2].mode === 2 ) { Glk.glk_put_jstring_stream( io.streams[2].str, text ); } } } }, // Print many things print: function( type, val ) { var proptable, result; // Number if ( type === 0 ) { result = val; } // Unicode if ( type === 1 ) { result = String.fromCharCode( val ); } // Text from address if ( type === 2 ) { result = this.jit[ val ] || this.decode( val ); } // Object if ( type === 3 ) { proptable = this.m.getUint16( this.objects + ( this.version3 ? 9 : 14 ) * val + ( this.version3 ? 7 : 12 ) ); result = this.decode( proptable + 1, this.m.getUint8( proptable ) * 2 ); } // ZSCII if ( type === 4 ) { if ( !this.unicode_table[ val ] ) { return; } result = this.unicode_table[ val ]; } this._print( '' + result ); }, print_table: function( zscii, width, height, skip ) { height = height || 1; skip = skip || 0; var i = 0; while ( i++ < height ) { this._print( this.zscii_to_text( this.m.getUint8Array( zscii, width ) ) + ( i < height ? '\r' : '' ) ); zscii += width + skip; } }, // Process CSS default colours /*process_colours: function() { // Convert RGB to a Z-Machine true colour // RGB is a css colour code. rgb(), #000000 and #000 formats are supported. function convert_RGB( code ) { var round = Math.round, data = /(\d+),\s*(\d+),\s*(\d+)|#(\w{1,2})(\w{1,2})(\w{1,2})/.exec( code ), result; // Nice rgb() code if ( data[1] ) { result = [ data[1], data[2], data[3] ]; } else { // Messy CSS colour code result = [ parseInt( data[4], 16 ), parseInt( data[5], 16 ), parseInt( data[6], 16 ) ]; // Stretch out compact #000 codes to their full size if ( code.length === 4 ) { result = [ result[0] << 4 | result[0], result[1] << 4 | result[1], result[2] << 4 | result[2] ]; } } // Convert to a 15bit colour return round( result[2] / 8.226 ) << 10 | round( result[1] / 8.226 ) << 5 | round( result[0] / 8.226 ); } // Standard colours var colours = [ 0xFFFE, // Current 0xFFFF, // Default 0x0000, // Black 0x001D, // Red 0x0340, // Green 0x03BD, // Yellow 0x59A0, // Blue 0x7C1F, // Magenta 0x77A0, // Cyan 0x7FFF, // White 0x5AD6, // Light grey 0x4631, // Medium grey 0x2D6B, // Dark grey ], // Start with CSS colours provided by the runner fg_css = this.options.fgcolour, bg_css = this.options.bgcolour, // Convert to true colour for storing in the header fg_true = fg_css ? convert_RGB( fg_css ) : 0xFFFF, bg_true = bg_css ? convert_RGB( bg_css ) : 0xFFFF, // Search the list of standard colours fg = colours.indexOf( fg_true ), bg = colours.indexOf( bg_true ); // ZVMUI must have colours for reversing text, even if we don't write them to the header // So use the given colours or assume black on white if ( fg < 2 ) { fg = fg_css || 2; } if ( bg < 2 ) { bg = bg_css || 9; } utils.extend( this.options, { fg: fg, bg: bg, fg_true: fg_true, bg_true: bg_true, }); },*/ // Request line input read: function( storer, text, parse, time, routine ) { var len = this.m.getUint8( text ), initiallen = 0, buffer, input_stream1_len; if ( this.version3 ) { this.v3_status(); } // The spec is badly phrased; the buffer capacity includes the zero terminator // See https://github.com/DFillmore/Z-Machine-Standard/issues/76 if (this.version < 5) { len-- } //else //{ //initiallen = this.m.getUint8( text + 1 ); //} buffer = Array( len ); buffer.fill( 0 ) this.read_data = { buffer: buffer, bufaddr: text, // text-buffer parseaddr: parse, // parse-buffer routine: routine, storer: storer, time: time, }; // Input stream 1 if ( this.io.streams[0] ) { input_stream1_len = this.Glk.glk_get_line_stream_uni( this.io.streams[0], buffer ); // Check for a newline character if ( buffer[input_stream1_len - 1] === 0x0A ) { input_stream1_len--; } if ( input_stream1_len ) { this._print( String.fromCharCode.apply( null, buffer.slice( 0, input_stream1_len ) ) + '\n' ); this.handle_line_input( input_stream1_len ); return this.stop = 0; } else { this.input_stream( 0 ); } } // TODO: pre-existing input this.Glk.glk_request_line_event_uni( this.io.currentwin ? this.upperwin : this.mainwin, buffer, initiallen ); this.fix_upper_window(); }, // Request character input read_char: function( storer, one, time, routine ) { // Input stream 1 if ( this.io.streams[0] ) { var code = this.Glk.glk_get_char_stream_uni( this.io.streams[0] ); // Check for EOF if ( code === -1 ) { this.input_stream( 0 ); } else { this.variable( storer, code ); return this.stop = 0; } } this.read_data = { routine: routine, storer: storer, time: time, }; this.Glk.glk_request_char_event_uni( this.io.currentwin ? this.upperwin : this.mainwin ); this.fix_upper_window(); }, set_colour: function(foreground, background) { this.set_true_colour(zcolours[foreground], zcolours[background]) }, // Note that row and col must be decremented in JIT code set_cursor: function( row, col ) { var io = this.io; // 8.7.2.3: do nothing if the lower window is selected if ( !io.currentwin ) { return } if ( row >= io.height ) { // Moving the cursor to a row forces the upper window // to open enough for that line to exist this.split_window( row + 1 ); } if ( this.upperwin && row >= 0 && col >= 0 && col < io.width ) { this.Glk.glk_window_move_cursor( this.upperwin, col, row ); io.row = row; io.col = col; } }, set_font: function( font ) { var returnval = this.io.mono & 0x04 ? 4 : 1; if ( font === 0 ) { return returnval; } // We only support fonts 1 and 4 if ( font !== 1 && font !== 4 ) { return 0; } if ( font !== returnval ) { this.io.mono ^= 0x04; this.format(); } return returnval; }, // Set styles set_style: function( stylebyte ) { var io = this.io; // Setting the style to Roman will clear the others if ( stylebyte === 0 ) { io.reverse = io.bold = io.italic = 0; io.mono &= 0xFE; } if ( stylebyte & 0x01 ) { io.reverse = 1; } if ( stylebyte & 0x02 ) { io.bold = 0x04; } if ( stylebyte & 0x04 ) { io.italic = 0x02; } if ( stylebyte & 0x08 ) { io.mono |= 0x01; } this.format(); }, // Set true colours set_true_colour: function(foreground, background) { const Glk = this.Glk if (Glk.glk_gestalt(0x1100, 0)) { let fg, bg if (foreground === 0xFFFE) { fg = -2 } else { if (foreground === 0xFFFF) { fg = -1 } else { fg = convert_true_colour(foreground) } this.io.fg = fg } if (background === 0xFFFE) { bg = -2 } else { if (background === 0xFFFF) { bg = -1 } else { bg = convert_true_colour(background) } this.io.bg = bg } // Set the colours for each open window Glk.garglk_set_zcolors_stream(this.mainwin.str, fg, bg) if (this.upperwin) { Glk.garglk_set_zcolors_stream(this.upperwin.str, fg, bg) } } }, set_window: function( window ) { this.io.currentwin = window; // Focusing the upper window resets the cursor to the top left; // it also opens the upper window if it's not open if ( window ) { this.set_cursor( 0, 0 ); } this.Glk.glk_set_window( this.upperwin && window ? this.upperwin : this.mainwin ); this.format(); }, split_window: function( lines ) { var Glk = this.Glk, io = this.io, row = io.row, col = io.col, oldheight = io.height, str; io.height = lines; // Erase existing lines if we are expanding into existing rows if ( this.upperwin && lines > oldheight ) { str = Glk.glk_window_get_stream( this.upperwin ); while ( oldheight < lines ) { Glk.glk_window_move_cursor( this.upperwin, 0, oldheight++ ); Glk.glk_put_jstring_stream( str, Array( io.width + 1 ).join( ' ' ) ); } Glk.glk_window_move_cursor( this.upperwin, col, row ); } // Don't decrease the height of the window yet, only increase if ( lines > io.maxheight ) { io.maxheight = lines; // Set the height of the window // Create the window if it doesn't exist if ( !this.upperwin ) { if (this.io.bg >= 0) { Glk.glk_stylehint_set(4, 0, 8, this.io.bg) } this.upperwin = Glk.glk_window_open( this.mainwin, 0x12, io.maxheight, 4, 203 ); if (this.Glk.glk_gestalt(0x1100, 0)) { Glk.garglk_set_zcolors_stream(this.upperwin.str, this.io.fg, this.io.bg) } Glk.glk_stylehint_clear(4, 0, 8) } else { Glk.glk_window_set_arrangement( Glk.glk_window_get_parent( this.upperwin ), 0x12, io.maxheight, null ); } io.glkheight = io.maxheight } if ( lines ) { // Reset the cursor if it is now outside the window if ( io.row >= lines ) { this.set_cursor( 0, 0 ); } // 8.6.1.1.2: In version three the upper window is always cleared if ( this.version3 ) { Glk.glk_window_clear( this.upperwin ); } } }, // Update the header after restarting or restoring update_header: function() { var ram = this.ram; // Reset the Xorshift seed this.xorshift_seed = 0; // Update the screen size variables - in version 3 does not actually set the header variables this.update_screen_size() // For version 3 we only set Flags 1 if ( this.version3 ) { return ram.setUint8( 0x01, ( ram.getUint8( 0x01 ) & 0x8F ) // Keep all except bits 4-6 | ( this.statuswin ? 0x20 : 0x10 ) // If status win is available then set 0x20 for the upper win also being available, otherwise 0x10 for the status win itself | 0x40 // Variable pitch font is default - Or can we tell from options if the font is fixed pitch? ); } // Flags 1 ram.setUint8( 0x01, (this.Glk.glk_gestalt(0x1100, 0) ? 1 : 0) // Check if colour is supported | 0x1C // Bold, italic and mono are supported | 0x00 // Timed input not supported yet ); // Flags 2: Clear bits 3, 5, 7: no character graphics, mouse or sound effects // This is really a word, but we only care about the lower byte ram.setUint8( 0x11, ram.getUint8( 0x11 ) & 0x57 ); // Font height/width in "units" if ( this.version > 4 ) { ram.setUint16( 0x26, 0x0101 ) } // Colours //ram.setUint8( 0x2C, isNaN( this.options.bg ) ? 1 : this.options.bg ); //ram.setUint8( 0x2D, isNaN( this.options.fg ) ? 1 : this.options.fg ); //this.extension_table( 5, this.options.fg_true ); //this.extension_table( 6, this.options.bg_true ); // Z Machine Spec revision ram.setUint16( 0x32, 0x0102 ); // Clear flags three, we don't support any of that stuff this.extension_table( 4, 0 ); }, update_screen_size: function() { const Glk = this.Glk const height_box = new Glk.RefBox() const width_box = new Glk.RefBox() const tempwin = Glk.glk_window_open( this.mainwin, 0x12, 0, 4, 0 ) let height = 0 let width = 0 // The main window is proportional, so its width may not be accurate // If the upper or status window is present, use its width, or else try to make a temp window // The height is the total of all windows Glk.glk_window_get_size( this.mainwin, width_box, height_box ) height = height_box.get_value() if ( this.upperwin ) { Glk.glk_window_get_size( this.upperwin, width_box, height_box ) height += height_box.get_value() } if ( this.statuswin ) { Glk.glk_window_get_size( this.statuswin, width_box, height_box ) height += height_box.get_value() } if ( tempwin ) { Glk.glk_window_get_size( tempwin, width_box, 0 ) Glk.glk_window_close( tempwin ) } // Use whichever width was available width = width_box.get_value() // Cap the dimensions // Height is capped to 254 as 255 means infinite, which breaks some games height = Math.min( height, 254 ) width = this.io.width = Math.min( width, 255 ) // Update the header if ( this.version > 3 ) { this.ram.setUint8( 0x20, height ) this.ram.setUint8( 0x21, width ) } if ( this.version > 4 ) { this.ram.setUint16( 0x22, width ) this.ram.setUint16( 0x24, height ) } // Fix the cursor if it is outside the window if ( this.io.col >= width ) { this.io.col = width - 1 } }, // Output the version 3 status line v3_status: function() { if ( !this.statuswin ) { return; } var Glk = this.Glk, str = Glk.glk_window_get_stream( this.statuswin ), memory = this.m, width = this.io.width, hours_score = memory.getUint16( this.globals + 2 ), mins_turns = memory.getUint16( this.globals + 4 ), proptable = memory.getUint16( this.objects + 9 * memory.getUint16( this.globals ) + 7 ), shortname = '' + this.decode( proptable + 1, memory.getUint8( proptable ) * 2 ), rhs; // Handle the turns/score or time if ( memory.getUint8( 0x01 ) & 0x02 ) { rhs = 'Time: ' + ( hours_score % 12 === 0 ? 12 : hours_score % 12 ) + ':' + ( mins_turns < 10 ? '0' : '' ) + mins_turns + ' ' + ( hours_score > 11 ? 'PM' : 'AM' ); } else { rhs = 'Score: ' + hours_score + ' Turns: ' + mins_turns; } // Print a blank line in reverse Glk.glk_window_move_cursor( this.statuswin, 0, 0 ); Glk.glk_put_jstring_stream( str, Array( width + 1 ).join( ' ' ) ); // Trim the shortname if necessary Glk.glk_window_move_cursor( this.statuswin, 0, 0 ); Glk.glk_put_jstring_stream( str, ' ' + shortname.slice( 0, width - rhs.length - 4 ) ); // Print the right hand side Glk.glk_window_move_cursor( this.statuswin, width - rhs.length - 1, 0 ); Glk.glk_put_jstring_stream( str, rhs ); }, };