/* $Id: text.c,v 1.5 2000/10/10 14:46:22 jholder Exp $ * -------------------------------------------------------------------- * see doc/License.txt for License Information * -------------------------------------------------------------------- * * File name: $Id: text.c,v 1.5 2000/10/10 14:46:22 jholder Exp $ * * Description: * * Modification history: * $Log: text.c,v $ * Revision 1.5 2000/10/10 14:46:22 jholder * Fixed text wrap bug when printing array w/ \r chars in it * * Revision 1.4 2000/10/04 23:07:57 jholder * fixed redirect problem with isolatin1 range chars * * Revision 1.3 2000/07/05 15:20:34 jholder * Updated code to remove warnings. * * Revision 1.2 2000/05/25 22:28:56 jholder * changes routine names to reflect zmachine opcode names per spec 1.0 * * Revision 1.1.1.1 2000/05/10 14:21:34 jholder * * imported * * * -------------------------------------------------------------------- */ /* * text.c * * Text manipulation routines * */ #include "ztypes.h" static int saved_formatting = ON; static int story_buffer = 0; static int story_pos = 0; static int story_count = 0; static int line_pos = 0; static int char_count = 0; /* * decode_text * * Convert ZSCII encoded text to ASCII. Text is encoded by squeezing each character * into 5 bits. 3 x 5 bit encoded characters can fit in one word with a spare * bit left over. The spare bit is used to signal to end of a string. The 5 bit * encoded characters can either be actual character codes or prefix codes that * modifier the following code. * */ void decode_text( unsigned long *address ) { int i, synonym_flag, synonym = 0, zscii_flag, zscii = 0; int data, code, shift_state, shift_lock; unsigned long addr; /* Set state variables */ shift_state = 0; shift_lock = 0; zscii_flag = 0; synonym_flag = 0; do { /* * Read one 16 bit word. Each word contains three 5 bit codes. If the * high bit is set then this is the last word in the string. */ data = read_data_word( address ); for ( i = 10; i >= 0; i -= 5 ) { /* Get code, high bits first */ code = ( data >> i ) & 0x1f; /* Synonym codes */ if ( synonym_flag ) { synonym_flag = 0; synonym = ( synonym - 1 ) * 64; addr = ( unsigned long ) get_word( h_synonyms_offset + synonym + ( code * 2 ) ) * 2; decode_text( &addr ); shift_state = shift_lock; } /* ZSCII codes */ else if ( zscii_flag ) { /* * If this is the first part ZSCII ten-bit code then remember it. * Because the codes are only 5 bits you need two codes to make * one eight bit ASCII character. The first code contains the * top 5 bits (although only 3 bits are used at the moment). * The second code contains the bottom 5 bits. */ if ( zscii_flag++ == 1 ) { zscii = code << 5; } /* * If this is the second part of a ten-bit ZSCII code then assemble the * character from the two codes and output it. */ else { zscii_flag = 0; write_zchar( ( unsigned char ) ( zscii | code ) ); } } /* Character codes */ else if ( code > 5 ) { code -= 6; /* * If this is character 0 in the punctuation set then the next two * codes make a ten-bit ZSCII character. (Std. Sec. 3.4) */ if ( shift_state == 2 && code == 0 ) { zscii_flag = 1; } /* * If this is character 1 in the punctuation set then this * is a new line. */ else if ( shift_state == 2 && code == 1 && h_type > V1 ) { z_new_line( ); } /* * This is a normal character so select it from the character * table appropriate for the current shift state. */ else { write_zchar( lookup_table[shift_state][code] ); } shift_state = shift_lock; } /* Special codes 0 to 5 */ else { /* Space: 0 Output a space character. */ if ( code == 0 ) { write_zchar( ' ' ); } else { /* The use of the synonym and shift codes is the only * difference between the different versions. */ if ( h_type < V3 ) { /* Newline or synonym: 1 * Output a newline character or set synonym flag. */ if ( code == 1 ) { if ( h_type == V1 ) { z_new_line( ); } else { synonym_flag = 1; synonym = code; } } else { /* * Shift keys: 2, 3, 4 or 5 * * Shift keys 2 & 3 only shift the next character and can be used regardless of * the state of the shift lock. Shift keys 4 & 5 lock the shift until reset. * * The following code implements the the shift code state transitions: * +-------------+-------------+-------------+-------------+ * | Shift State | Lock State | * +-------------+-------------+-------------+-------------+-------------+ * | Code | 2 | 3 | 4 | 5 | * +-------------+-------------+-------------+-------------+-------------+ * | lowercase | uppercase | punctuation | uppercase | punctuation | * | uppercase | punctuation | lowercase | punctuation | lowercase | * | punctuation | lowercase | uppercase | lowercase | uppercase | * +-------------+-------------+-------------+-------------+-------------+ */ if ( code < 4 ) { shift_state = ( shift_lock + code + 2 ) % 3; } else { shift_lock = shift_state = ( shift_lock + code ) % 3; } } } else /* not V3 */ { /* * Synonym table: 1, 2 or 3 * * Selects which of three synonym tables the synonym * code following in the next code is to use. */ if ( code < 4 ) { synonym_flag = 1; synonym = code; } /* * Shift key: 4 or 5 * * Selects the shift state for the next character, * either uppercase (4) or punctuation (5). The shift * state automatically gets reset back to lowercase for * V3+ games after the next character is output. * */ else { shift_state = code - 3; shift_lock = 0; } } } } } } while ( ( data & 0x8000 ) == 0 ); } /* decode_text */ /* * encode_text * * Pack a string into up to 9 codes or 3 words. * */ void encode_text( int len, const char *s, ZINT16 * buffer ) { int i, j, prev_table, table, next_table, shift_state, code, codes_count; char codes[9]; /* Initialise codes count and prev_table number */ codes_count = 0; prev_table = 0; /* Scan do the string one character at a time */ while ( len-- ) { /* * Set the table and code to be the ASCII character inducer, then * look for the character in the three lookup tables. If the * character isn't found then it will be an ASCII character. */ table = 2; code = 0; for ( i = 0; i < 3; i++ ) { for ( j = 0; j < 26; j++ ) { if ( lookup_table[i][j] == *s ) { table = i; code = j; } } } /* * Type 1 and 2 games differ on how the shift keys are used. Switch * now depending on the game version. */ if ( h_type < V3 ) { /* * If the current table is the same as the previous table then * just store the character code, otherwise switch tables. */ if ( table != prev_table ) { /* Find the table for the next character */ next_table = 0; if ( len ) { next_table = 2; for ( i = 0; i < 3; i++ ) { for ( j = 0; j < 26; j++ ) { if ( lookup_table[i][j] == s[1] ) next_table = i; } } } /* * Calculate the shift key. This magic. See the description in * decode_text for more information on version 1 and 2 shift * key changes. */ shift_state = ( table + ( prev_table * 2 ) ) % 3; /* Only store the shift key if there is a change in table */ if ( shift_state ) { /* * If the next character as the uses the same table as * this character then change the shift from a single * shift to a shift lock. Also remember the current * table for the next iteration. */ if ( next_table == table ) { shift_state += 2; prev_table = table; } else prev_table = 0; /* Store the code in the codes buffer */ if ( codes_count < 9 ) codes[codes_count++] = ( char ) ( shift_state + 1 ); } } } else { /* * For V3 games each uppercase or punctuation table is preceded * by a separate shift key. If this is such a shift key then * put it in the codes buffer. */ if ( table && codes_count < 9 ) codes[codes_count++] = ( char ) ( table + 3 ); } /* Put the character code in the code buffer */ if ( codes_count < 9 ) codes[codes_count++] = ( char ) ( code + 6 ); /* * Cannot find character in table so treat it as a literal ASCII * code. The ASCII code inducer (code 0 in table 2) is followed by * the high 3 bits of the ASCII character followed by the low 5 * bits to make 8 bits in total. */ if ( table == 2 && code == 0 ) { if ( codes_count < 9 ) codes[codes_count++] = ( char ) ( ( *s >> 5 ) & 0x07 ); if ( codes_count < 9 ) codes[codes_count++] = ( char ) ( *s & 0x1f ); } /* Advance to next character */ s++; } /* Pad out codes with shift 5's */ while ( codes_count < 9 ) codes[codes_count++] = 5; /* Pack codes into buffer */ buffer[0] = (short)(( ( ZINT16 ) codes[0] << 10 ) | ( ( ZINT16 ) codes[1] << 5 ) | ( ZINT16 ) codes[2]); buffer[1] = (short)(( ( ZINT16 ) codes[3] << 10 ) | ( ( ZINT16 ) codes[4] << 5 ) | ( ZINT16 ) codes[5]); buffer[2] = (short)(( ( ZINT16 ) codes[6] << 10 ) | ( ( ZINT16 ) codes[7] << 5 ) | ( ZINT16 ) codes[8]); /* Terminate buffer at 6 or 9 codes depending on the version */ if ( h_type < V4 ) buffer[1] |= 0x8000; else buffer[2] |= 0x8000; } /* encode_text */ /* * write_zchar * * High level Z-code character output routine. Translates Z-code characters to * machine specific character(s) before output. If it cannot translate it then * use the default translation. If the character is still unknown then display * a '?'. * */ void write_zchar( int c ) { char xlat_buffer[MAX_TEXT_SIZE + 1]; int i; c = ( unsigned int ) ( c & 0xff ); /* If character is not special character then just write it */ if ( c >= ' ' && c <= '~' ) { write_char( c ); } else if ( c == 13 ) { write_char( '\r' ); } else { /* Put default character in translation buffer */ xlat_buffer[0] = '?'; xlat_buffer[1] = '\0'; /* If translation fails then supply a default */ if ( codes_to_text( c, xlat_buffer ) ) { if ( c > 23 && c < 28 ) { /* Arrow keys - these must the keyboard keys used for input */ static char xlat[4] = { '\\', '/', '+', '-' }; xlat_buffer[0] = xlat[c - 24]; xlat_buffer[1] = '\0'; } else if ( c == 0 ) { /* Null - print nothing */ xlat_buffer[0] = '\0'; } else if ( c < 32 ) { /* Some other control character: print an octal escape. */ xlat_buffer[0] = '\\'; xlat_buffer[1] = ( char ) ( '0' + ( ( c >> 6 ) & 7 ) ); xlat_buffer[2] = ( char ) ( '0' + ( ( c >> 3 ) & 7 ) ); xlat_buffer[3] = ( char ) ( '0' + ( c & 7 ) ); xlat_buffer[4] = '\0'; } else if ( c > 178 && c < 219 ) { /* IBM line drawing characters to ASCII characters */ if ( c == 179 ) xlat_buffer[0] = '|'; else if ( c == 186 ) xlat_buffer[0] = '#'; else if ( c == 196 ) xlat_buffer[0] = '-'; else if ( c == 205 ) xlat_buffer[0] = '='; else xlat_buffer[0] = '+'; xlat_buffer[1] = '\0'; } else if ( c > 154 && c < 164 ) { /* German character replacements */ static char xlat[] = "aeoeueAeOeUess>><<"; xlat_buffer[0] = xlat[( ( c - 155 ) * 2 ) + 0]; xlat_buffer[1] = xlat[( ( c - 155 ) * 2 ) + 1]; xlat_buffer[2] = '\0'; } } /* Substitute translated characters */ for ( i = 0; xlat_buffer[i] != '\0'; i++ ) { write_char( ( unsigned char ) xlat_buffer[i] ); } } } /* write_zchar */ /* * translate_to_zscii * * */ zbyte_t translate_to_zscii(int c) { int i; if( c>= 0xa0 ) { if( h_unicode_table !=0 ) { fprintf(stderr,"[[ Unicode support not enabled yet. ]]"); } else { for (i = 0x9b; i <= 0xdf; i++) { if (c == zscii2latin1[i - 0x9b]) { return (zbyte_t) i; } } return '?'; } } return (zbyte_t) c; } /* * write_char * * High level character output routine. The write_char routine is slightly * complicated by the fact that the output can be limited by a fixed character * count, as well as, filling up the buffer. * */ void write_char( int c ) { char *cp; int right_len; /* Only do if text formatting is turned on */ if ( redirect_depth ) { /* If redirect is on then write the character to the status line * for V1 to V3 games or into the writeable data area for V4+ games */ if ( h_type < V4 ) { status_line[status_pos++] = ( char ) c; } else { set_byte( story_pos++, translate_to_zscii(c) ); story_count++; } } else if ( formatting == ON && screen_window == TEXT_WINDOW ) { if ( fit_line( line, line_pos, screen_cols - right_margin ) == 0 || char_count < 1 ) { /* Null terminate the line */ line[line_pos] = '\0'; /* If the next character is a space then no wrap is neccessary */ if ( c == ' ' ) { z_new_line( ); c = '\0'; } else { /* Wrap the line. First find the last space */ cp = strrchr( line, ' ' ); /* If no spaces in the lines then cannot do wrap */ if ( cp == NULL ) { /* Output the buffer and a new line */ z_new_line( ); } if (cp != NULL) { /* Terminate the line at the last space */ *cp++ = '\0'; /* Calculate the text length after the last space */ right_len = &line[line_pos] - cp; /* Output the buffer and a new line */ z_new_line( ); /* If any text to wrap then move it to the start of the line */ if ( right_len > 0 ) { memmove( line, cp, right_len ); line_pos = right_len; } } } } /* Put the character into the buffer and count it. * Decrement line width if the character is visible */ if ( c ) { line[line_pos++] = ( char ) c; /* Wrap the line when there is a newline in the stream. */ cp = strrchr( line, 13 ); if ( cp!= NULL ) { /* Terminate the line at the last space */ *cp++ = '\0'; /* Calculate the text length after the last space */ right_len = &line[line_pos] - cp; /* Output the buffer and a new line */ z_new_line( ); /* If any text to wrap then move it to the start of the line */ if ( right_len > 0 ) { memmove( line, cp, right_len ); line_pos = right_len; } } if ( isprint( c ) ) { char_count--; } } } else { /* No formatting or output redirection, so just output the character */ script_char( c ); output_char( c ); } } /* write_char */ /* * z_set_text_style * * Set a video attribute. Write the video mode, from 0 to 8, incremented. * This is so the output routines don't confuse video attribute 0 as the * end of the string. * */ void z_set_text_style( zword_t mode ) { //if ( mode >= MIN_ATTRIBUTE && mode <= MAX_ATTRIBUTE ) if (mode <= MAX_ATTRIBUTE ) { set_attribute( mode ); } else { fatal( "@set_text_style called with invalid mode." ); } } /* z_set_text_style */ /* * write_string * * Output a string * */ void write_string( const char *s ) { while ( *s ) write_char( *s++ ); } /* write_string */ /* * flush_buffer * * Send output buffer to the screen. * */ void flush_buffer( int flag ) { /* Terminate the line */ line[line_pos] = '\0'; /* Send the line buffer to the printer */ script_string( line ); flush_script( ); /* Send the line buffer to the screen */ output_string( line ); /* Reset the character count only if a carriage return is expected */ if ( flag == TRUE ) { char_count = screen_cols - right_margin; } /* Reset the buffer pointer */ line_pos = 0; } /* flush_buffer */ /* * z_buffer_mode * * Set the format mode flag. Formatting disables writing into the output buffer. * If set to 1, text output in the lower window on stream one is buffered so that * it can be word-wrapped properly. If set to 0, it isn't. * */ void z_buffer_mode( zword_t flag ) { /* Flush any current output */ flush_buffer( FALSE ); /* Set formatting depending on the flag */ if ( flag ) formatting = ON; else formatting = OFF; } /* z_buffer_mode */ /* * z_output_stream * * Set various printing modes. These can be: disabling output, scripting and * redirecting output. Redirection is peculiar. I use it to format the status * line for V1 to V3 games, otherwise it wasn't used. V4 games format the * status line themselves in an internal buffer in the writeable data area. * To use the normal text decoding routines they have to redirect output to * the writeable data area. This is done by passing in a buffer pointer. * The first word of the buffer will receive the number of characters * written since the output was redirected. The remainder of the buffer * will contain the redirected text. * */ typedef struct redirect_stash_struct { zword_t count; zword_t buffer; zword_t pos; } redirect_stash_t; void z_output_stream( zword_t type, zword_t option ) { static int redirect_size = 0; static redirect_stash_t *stash = NULL; if ( ( ZINT16 ) type == 1 ) { /* Turn on text output */ outputting = ON; } else if ( ( ZINT16 ) type == 2 ) { /* Turn on scripting */ open_script( ); } else if ( ( ZINT16 ) type == 3 ) { /* Turn on output redirection */ if ( redirect_depth == 0 ) { /* Disable text formatting during redirection */ saved_formatting = formatting; formatting = OFF; /* Enable text redirection */ redirect_depth = 1; } else { if ( redirect_size == 0 ) { redirect_size = 4; stash = ( redirect_stash_t * ) malloc( redirect_size * sizeof ( redirect_stash_t ) ); } if ( redirect_depth > redirect_size ) { redirect_size *= 2; stash = ( redirect_stash_t * ) realloc( stash, redirect_size * sizeof ( redirect_stash_t ) ); } if ( h_type < V4 ) { stash[redirect_depth - 1].pos = status_pos; } else { stash[redirect_depth - 1].pos = story_pos; stash[redirect_depth - 1].buffer = story_buffer; stash[redirect_depth - 1].count = story_count; } redirect_depth++; } /* Set up the redirection pointers */ if ( h_type < V4 ) { status_pos = 0; } else { story_count = 0; story_buffer = option; story_pos = option + 2; } } else if ( ( ZINT16 ) type == 4 ) { /* Turn on input recording */ open_record( ); } else if ( ( ZINT16 ) type == -1 ) { /* Turn off text output */ outputting = OFF; } else if ( ( ZINT16 ) type == -2 ) { /* Turn off scripting */ close_script( ); } else if ( ( ZINT16 ) type == -3 ) { /* Turn off output redirection */ if ( redirect_depth ) { if ( redirect_depth == 1 ) { /* Restore the format mode and turn off redirection */ formatting = saved_formatting; redirect_depth = 0; /* Terminate the redirection buffer and store the count of * character in the buffer into the first word of the buffer */ if ( h_type > V3 ) { set_word( story_buffer, story_count ); } } else { if ( h_type > V3 ) { set_word( story_buffer, story_count ); } redirect_depth--; if ( h_type < V4 ) { status_pos = stash[redirect_depth - 1].pos; } else { story_pos = stash[redirect_depth - 1].pos; story_buffer = stash[redirect_depth - 1].buffer; story_count = stash[redirect_depth - 1].count; } } } } else if ( ( ZINT16 ) type == -4 ) { /* Turn off input recording */ close_record( ); } } /* z_output_stream */ /* * z_print_char * * Write a character. * */ void z_print_char( zword_t c ) { write_zchar( ( char ) c ); } /* z_print_char */ /* * z_print_num * * Write a signed number. * */ void z_print_num( zword_t num ) { int i, count; char buffer[10]; i = ( ZINT16 ) num; sprintf( buffer, "%d", i ); count = strlen( buffer ); for ( i = 0; i < count; i++ ) write_char( buffer[i] ); } /* z_print_num */ /* * z_print_paddr * * Print using a packed address. Packed addresses are used to save space and * reference addresses outside of the data region. * */ void z_print_paddr( zword_t packed_address ) { unsigned long address; /* Convert packed address to real address */ address = ( unsigned long ) packed_address * story_scaler; /* Decode and output text at address */ decode_text( &address ); } /* z_print_paddr */ /* * z_print_addr * * Print using a real address. Real addresses are just offsets into the * data region. * */ void z_print_addr( zword_t offset ) { unsigned long address; address = offset; /* Decode and output text at address */ decode_text( &address ); } /* z_print_addr */ /* * z_print_obj * * Print an object description. Object descriptions are stored as ASCII * strings at the front of the property list for the object. * */ void z_print_obj( zword_t obj ) { zword_t offset; unsigned long address; /* Check for NULL object */ if ( obj == 0 ) return; /* Calculate address of property list */ offset = get_object_address( obj ); offset += ( h_type < V4 ) ? O3_PROPERTY_OFFSET : O4_PROPERTY_OFFSET; /* Read the property list address and skip the count byte */ address = ( unsigned long ) get_word( offset ) + 1; /* Decode and output text at address */ decode_text( &address ); } /* z_print_obj */ /* * z_print * * Print the string embedded in the instruction stream at this point. All * strings that do not need to be referenced by address are embedded in the * instruction stream. All strings that can be refered to by address are placed * at the end of the code region and referenced by packed address. * */ void z_print( void ) { /* Decode and output text at PC */ decode_text( &pc ); } /* z_print */ /* * z_print_ret * * Print a string embedded in the instruction stream as with print_literal, * except flush the output buffer and write a new line. After this return from * the current subroutine with a status of true. * */ void z_print_ret( void ) { z_print( ); z_new_line( ); z_ret( TRUE ); } /* z_print_ret */ /* * z_new_line * * Simply flush the current contents of the output buffer followed by a new * line. * */ void z_new_line( void ) { /* Only flush buffer if story redirect is off */ if ( redirect_depth == 0 ) { flush_buffer( TRUE ); script_new_line( ); output_new_line( ); } else { write_char( '\r' ); } } /* z_new_line */ /* * print_time * * Print the time as HH:MM [am|pm]. This is a bit language dependent and can * quite easily be changed. If you change the size of the time string output * then adjust the status line position in display_status_line. * */ void print_time( int hours, int minutes ) { int pm_indicator; /* Remember if time is pm */ pm_indicator = ( hours < 12 ) ? OFF : ON; /* Convert 24 hour clock to 12 hour clock */ hours %= 12; if ( hours == 0 ) hours = 12; /* Write hour right justified */ if ( hours < 10 ) write_char( ' ' ); z_print_num( (zword_t)hours ); /* Write hours/minutes separator */ write_char( ':' ); /* Write minutes zero filled */ if ( minutes < 10 ) write_char( '0' ); z_print_num( (zword_t)minutes ); /* Write the am or pm string */ if ( pm_indicator == ON ) write_string( " pm" ); else write_string( " am" ); } /* print_time */ /* * z_encode * * Convert text to packed text. * */ void z_encode( zword_t word_addr, zword_t word_length, zword_t word_offset, zword_t dest_addr ) { ZINT16 word[3]; int i; /* Encode the word */ encode_text( word_length, ( const char * ) &datap[word_addr + word_offset], word ); /* Move the encoded word, byte swapped, into the destination buffer */ for ( i = 0; i < 3; i++, dest_addr += 2 ) set_word( dest_addr, word[i] ); } /* z_encode */