UNPKG

ifvms

Version:

The Interactive Fiction Virtual Machines Suite - in Javascript

410 lines (358 loc) 9.39 kB
/* Z-Machine text functions ======================== Copyright (c) 2017 The ifvms.js team MIT licenced https://github.com/curiousdannii/ifvms.js */ /* TODO: Consider quote suggestions from 1.1 spec */ module.exports = { init_text: function() { var self = this, memory = this.m, alphabet_addr = ( this.version > 4 ) && memory.getUint16( 0x34 ), unicode_addr = this.extension_table( 3 ), unicode_len = unicode_addr && memory.getUint8( unicode_addr++ ); this.abbr_addr = memory.getUint16( 0x18 ); // Generate alphabets function make_alphabet( data ) { var alphabets = [[], [], []], i = 0; while ( i < 78 ) { alphabets[( i / 26 ) | 0][i % 26] = data[ i++ ]; } // A2->7 is always a newline alphabets[2][1] = 13; self.alphabets = alphabets; } // Make the unicode tables function make_unicode( data ) { var table = { 13: '\r' }, // New line conversion reverse = { 13: 13 }, i = 0; while ( i < data.length ) { table[155 + i] = String.fromCharCode( data[i] ); reverse[data[i]] = 155 + i++; } i = 32; while ( i < 127 ) { table[i] = String.fromCharCode( i ); reverse[i] = i++; } self.unicode_table = table; self.reverse_unicode_table = reverse; } // Check for custom alphabets make_alphabet( alphabet_addr ? memory.getUint8Array( alphabet_addr, 78 ) // Or use the standard alphabet : this.text_to_zscii( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \r0123456789.,!?_#\'"/\\-:()', 1 ) ); // Check for a custom unicode table make_unicode( unicode_addr ? memory.getUint16Array( unicode_addr, unicode_len ) // Or use the default : this.text_to_zscii( unescape( '%E4%F6%FC%C4%D6%DC%DF%BB%AB%EB%EF%FF%CB%CF%E1%E9%ED%F3%FA%FD%C1%C9%CD%D3%DA%DD%E0%E8%EC%F2%F9%C0%C8%CC%D2%D9%E2%EA%EE%F4%FB%C2%CA%CE%D4%DB%E5%C5%F8%D8%E3%F1%F5%C3%D1%D5%E6%C6%E7%C7%FE%F0%DE%D0%A3%u0153%u0152%A1%BF' ), 1 ) ); // Parse the standard dictionary this.dictionaries = {}; this.dict = memory.getUint16( 0x08 ); this.parse_dict( this.dict ); // Optimise our own functions /*if ( DEBUG ) { if ( !debugflags.nooptimise ) optimise_obj( this, 'TEXT' ); }*/ }, // Decode Z-chars into ZSCII and then Unicode decode: function( addr, length ) { var memory = this.m, start_addr = addr, temp, buffer = [], i = 0, zchar, alphabet = 0, result = [], resulttexts = [], usesabbr, unicodecount = 0; // Check if this one's been cached already if ( this.jit[addr] ) { return this.jit[addr]; } // If we've been given a length, then use it as the finaladdr, // Otherwise don't go past the end of the file length = length ? length + addr : this.eof; // Go through until we've reached the end of the text or a stop bit while ( addr < length ) { temp = memory.getUint16( addr ); addr += 2; buffer.push( temp >> 10 & 0x1F, temp >> 5 & 0x1F, temp & 0x1F ); // Stop bit if ( temp & 0x8000 ) { break; } } // Process the Z-chars while ( i < buffer.length ) { zchar = buffer[i++]; // Special chars // Space if ( zchar === 0 ) { result.push( 32 ); } // Abbreviations else if ( zchar < 4 ) { usesabbr = 1; result.push( -1 ); resulttexts.push( '\uE000+this.abbr(' + ( 32 * ( zchar - 1 ) + buffer[i++] ) + ')+\uE000' ); } // Shift characters else if ( zchar < 6 ) { alphabet = zchar; } // Check for a 10 bit ZSCII character else if ( alphabet === 2 && zchar === 6 ) { // Check we have enough Z-chars left. if ( i + 1 < buffer.length ) { result.push( buffer[i++] << 5 | buffer[i++] ) } } // Regular characters else if ( zchar < 0x20 ) { result.push( this.alphabets[alphabet][ zchar - 6 ] ); } // Reset the alphabet alphabet = alphabet < 4 ? 0 : alphabet - 3; // Add to the index if we've had raw unicode if ( ( i % 3 ) === 0 ) { i += unicodecount; unicodecount = 0; } } result = this.zscii_to_text( result, resulttexts ); // Abbreviations must be extracted at run time, so return a function instead if ( usesabbr ) { result = { toString: ( Function( 'return"' + result.replace( /\\/g, '\\\\' ).replace( /"/g, '\\"' ).replace( /\r/g, '\\r' ).replace( /\uE000/g, '"' ) + '"' ) ).bind( this ), }; } // Cache and return if ( start_addr >= this.staticmem ) { this.jit[start_addr] = result; } return result; }, // Encode ZSCII into Z-chars encode: function( zscii ) { var alphabets = this.alphabets, zchars = [], word_len = this.version3 ? 6 : 9, i = 0, achar, temp, result = []; // Encode the Z-chars while ( zchars.length < word_len ) { achar = zscii[i++]; // Space if ( achar === 32 ) { zchars.push( 0 ); } // Alphabets else if ( ( temp = alphabets[0].indexOf( achar ) ) >= 0 ) { zchars.push( temp + 6 ); } else if ( ( temp = alphabets[1].indexOf( achar ) ) >= 0 ) { zchars.push( 4, temp + 6 ); } else if ( ( temp = alphabets[2].indexOf( achar ) ) >= 0 ) { zchars.push( 5, temp + 6 ); } // Pad character else if ( achar === undefined ) { zchars.push( 5 ); } // 10-bit ZSCII else { zchars.push( 5, 6, achar >> 5, achar & 0x1F ) } } zchars.length = word_len; // Encode to bytes i = 0; while ( i < word_len ) { result.push( zchars[i++] << 2 | zchars[i] >> 3, ( zchars[i++] & 0x07 ) << 5 | zchars[i++] ); } result[ result.length - 2 ] |= 0x80; return result; }, // In these two functions zscii means an array of ZSCII codes and text means a regular Javascript unicode string zscii_to_text: function( zscii, texts ) { var i = 0, l = zscii.length, charr, j = 0, result = ''; while ( i < l ) { charr = zscii[i++]; // Text substitution from abbreviations or 1.1 unicode if ( charr === -1 ) { result += texts[j++]; } // Regular characters if ( ( charr = this.unicode_table[charr] ) ) { result += charr; } } return result; }, // If the second argument is set then don't use the unicode table text_to_zscii: function( text, notable ) { var array = [], i = 0, l = text.length, charr; while ( i < l ) { charr = text.charCodeAt( i++ ); // Check the unicode table if ( !notable ) { charr = this.reverse_unicode_table[charr] || 63; } array.push( charr ); } return array; }, // Parse and cache a dictionary parse_dict: function( addr ) { var memory = this.m, addr_start = addr, dict = {}, entry_len, endaddr, // Get the word separators seperators_len = memory.getUint8( addr++ ); // Support: IE, Safari, Firefox<38, Chrome<45, Opera<32, Node<4 // These browsers don't support Uint8Array.indexOf() so convert to a normal array dict.separators = Array.prototype.slice.call( memory.getUint8Array( addr, seperators_len ) ); addr += seperators_len; // Go through the dictionary and cache its entries entry_len = memory.getUint8( addr++ ); endaddr = addr + 2 + entry_len * memory.getUint16( addr ); addr += 2; while ( addr < endaddr ) { dict[ Array.prototype.toString.call( memory.getUint8Array( addr, this.version3 ? 4 : 6 ) ) ] = addr; addr += entry_len; } this.dictionaries[addr_start] = dict; return dict; }, // Print an abbreviation abbr: function( abbrnum ) { return this.decode( this.m.getUint16( this.abbr_addr + 2 * abbrnum ) * 2 ); }, // Tokenise a text tokenise: function( bufaddr, parseaddr, dictionary, flag ) { // Use the default dictionary if one wasn't provided dictionary = dictionary || this.dict; // Parse the dictionary if needed dictionary = this.dictionaries[dictionary] || this.parse_dict( dictionary ); var memory = this.m, ram = this.ram, bufferlength = 1e3, i = 1, letter, separators = dictionary.separators, word, words = [], max_words, dictword, wordcount = 0; // In versions 5 and 8 we can get the actual buffer length if ( this.version > 4 ) { bufferlength = memory.getUint8( bufaddr + i++ ) + 2; } // Find the words, separated by the separators, but as well as the separators themselves while ( i < bufferlength ) { letter = memory.getUint8( bufaddr + i ); if ( letter === 0 ) { break; } else if ( letter === 32 || separators.indexOf( letter ) >= 0 ) { if ( letter !== 32 ) { words.push( [ [letter], i ] ); } word = null; } else { if ( !word ) { words.push( [ [], i ] ); word = words[ words.length - 1 ][0]; } word.push( letter ); } i++; } // Go through the text until we either have reached the max number of words, or we're out of words max_words = Math.min( words.length, memory.getUint8( parseaddr ) ); while ( wordcount < max_words ) { dictword = dictionary['' + this.encode( words[wordcount][0] )]; // If the flag is set then don't overwrite words which weren't found if ( !flag || dictword ) { // Fill out the buffer ram.setUint16( parseaddr + 2 + wordcount * 4, dictword || 0 ); ram.setUint8( parseaddr + 4 + wordcount * 4, words[wordcount][0].length ); ram.setUint8( parseaddr + 5 + wordcount * 4, words[wordcount][1] ); } wordcount++; } // Update the number of found words ram.setUint8( parseaddr + 1, wordcount ); }, };