/* Nitfol - z-machine interpreter using Glk for output. Copyright (C) 1999 Evin Robertson This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. The author can be reached at nitfol@deja.com */ #include "nitfol.h" #define UPPER_WINDOW 1 #define LOWER_WINDOW 0 static zwinid lower_win, upper_win; static zwinid current_window; #define STREAM1 1 #define STREAM2 2 #define STREAM3 4 #define STREAM4 8 static strid_t stream2, stream4; static int output_stream; static zword stream3_table_starts[16]; static zword stream3_table_locations[16]; static int stream3_nesting_depth; static int font = 1; static BOOL abort_output = FALSE; /* quickly stop outputting */ BOOL is_transcripting(void) { return (output_stream & STREAM2) != 0; } void set_transcript(strid_t stream) { if(stream) { if(z_memory) { zword flags2 = LOWORD(HD_FLAGS2) | b00000001; LOWORDwrite(HD_FLAGS2, flags2); } stream2 = stream; output_stream |= STREAM2; if(lower_win) z_set_transcript(lower_win, stream2); } else { if(z_memory) { zword flags2 = LOWORD(HD_FLAGS2) & b11111110; LOWORDwrite(HD_FLAGS2, flags2); } output_stream &= ~STREAM2; if(lower_win) z_set_transcript(lower_win, 0); } } /* initialize the windowing environment */ void init_windows(BOOL dofixed, glui32 maxwidth, glui32 maxheight) { z_init_windows(dofixed, draw_upper_callback, upper_mouse_callback, maxwidth, maxheight, &upper_win, &lower_win); current_window = lower_win; output_stream = STREAM1 | (output_stream & STREAM2); stream3_nesting_depth = 0; font = 1; if(output_stream & STREAM2) { set_transcript(stream2); } else { set_transcript(0); } if(zversion == 6) { v6_main_window_is(lower_win); } } static int upper_roomname_length; static void counting_glk_put_char(int ch) { upper_roomname_length++; glk_put_char(ch); } glui32 draw_upper_callback(winid_t win, glui32 width, glui32 height) { glui32 curx = 0, cury = 0; glui32 numlines = 0; if(win == NULL || height == 0) { if(zversion <= 3) numlines++; return numlines; } if(zversion <= 3) { zword location = get_var(16); offset short_name_off = object_name(location); glk_window_move_cursor(win, 0, cury); if(location && short_name_off) { glk_put_char(' '); curx++; upper_roomname_length = 0; decodezscii(short_name_off, counting_glk_put_char); curx += upper_roomname_length; } glk_window_move_cursor(win, width - 8, cury); if((zversion <= 2) || ((LOBYTE(HD_FLAGS1) & 2) == 0)) { if(width > curx + 26) { glk_window_move_cursor(win, width - 24, cury); w_glk_put_string("Score: "); g_print_znumber(get_var(17)); glk_window_move_cursor(win, width - 12, cury); w_glk_put_string("Moves: "); g_print_znumber(get_var(18)); } else { g_print_znumber(get_var(17)); /* score */ glk_put_char('/'); g_print_znumber(get_var(18)); /* turns */ } } else { const char *ampmstr[8] = { " AM", " PM" }; int ampm = 0; zword hours = get_var(17); zword minutes = get_var(18); while(hours >= 12) { hours-=12; ampm ^= 1; } if(hours == 0) hours = 12; if(hours < 10) glk_put_char(' '); g_print_number(hours); glk_put_char(':'); if(minutes < 10) glk_put_char('0'); g_print_number(minutes); w_glk_put_string(ampmstr[ampm]); } numlines++; cury++; glk_window_move_cursor(win, 0, cury); } return numlines; } void output_string(const char *s) { while(*s) output_char(*s++); } void output_char(int c) { static int starlength = 0; if(output_stream & STREAM3) { /* Table output */ zword num_chars = LOWORD(stream3_table_starts[stream3_nesting_depth-1]) +1; if((c < 32 && c != 13) || (c >= 127 && c <= 159) || (c > 255)) c = '?'; /* Section 7.5.3 */ LOBYTEwrite(stream3_table_locations[stream3_nesting_depth-1], c); stream3_table_locations[stream3_nesting_depth-1] += 1; LOWORDwrite(stream3_table_starts[stream3_nesting_depth-1], num_chars); } else { if(output_stream & STREAM1) { /* Normal screen output */ if(c >= 155 && c <= 251) { /* "extra characters" */ zword game_unicode_table = header_extension_read(3); if(game_unicode_table && LOBYTE(game_unicode_table) >= (zbyte) (c - 155)) { zword address = game_unicode_table + 1 + (c - 155) * 2; c = LOWORD(address); } else { const unsigned default_unicode_translation[] = { 0xe4, 0xf6, 0xfc, 0xc4, 0xd6, 0xdc, 0xdf, 0xbb, 0xab, 0xeb, 0xef, 0xff, 0xcb, 0xcf, 0xe1, 0xe9, 0xed, 0xf3, 0xfa, 0xfd, 0xc1, 0xc9, 0xcd, 0xd3, 0xda, 0xdd, 0xe0, 0xe8, 0xec, 0xf2, 0xf9, 0xc0, 0xc8, 0xcc, 0xd2, 0xd9, 0xe2, 0xea, 0xee, 0xf4, 0xfb, 0xc2, 0xca, 0xce, 0xd4, 0xdb, 0xe5, 0xc5, 0xf8, 0xd8, 0xe3, 0xf1, 0xf5, 0xc3, 0xd1, 0xd5, 0xe6, 0xc6, 0xe7, 0xc7, 0xfe, 0xf0, 0xde, 0xd0, 0xa3, 0x153, 0x152, 0xa1, 0xbf }; c = default_unicode_translation[c - 155]; } } if(c == '*') { if(++starlength == 3) /* Three asterisks usually means win or death */ if(automap_unexplore()) abort_output = TRUE; } else { starlength = 0; } if(font == 3) { const char font3trans[] = " <>/" "\\ --" "||||" "--\\/" /* 32-47 */ "\\//\\/\\@ " " |" "|-- " /* 48-63 */ " /" "\\/\\ " " " " " /* 64-79 */ " " "####" " X+" "udb*" /* 80-95 */ "?abc" "defg" "hijk" "lmno" /* 96-111 */ "pqrs" "tuvw" "xyzU" "DB?"; /* 112-126 */ if(c >= 32 && c <= 126) c = font3trans[c - 32]; } if(allow_output) z_put_char(current_window, c); } } } void n_print_number(unsigned n) { int i; char buffer[12]; int length = n_to_decimal(buffer, n); for(i = length - 1; i >= 0; i--) output_char(buffer[i]); } void g_print_number(unsigned n) { int i; char buffer[12]; int length = n_to_decimal(buffer, n); for(i = length - 1; i >= 0; i--) glk_put_char(buffer[i]); } void g_print_snumber(int n) { if(n < 0) { glk_put_char('-'); n = -n; } g_print_number(n); } void g_print_znumber(zword n) { if(is_neg(n)) { glk_put_char('-'); g_print_number(neg(n)); } else { g_print_number(n); } } void n_print_znumber(zword n) { if(is_neg(n)) { output_char('-'); n_print_number(neg(n)); } else { n_print_number(n); } } void stream4number(unsigned c) { if(output_stream & STREAM4) { glk_stream_set_current(stream4); glk_put_char('['); g_print_number(c); glk_put_char(']'); glk_put_char(10); } } void op_buffer_mode(void) { /* FIXME: Glk can't really do this. * I could rely on the Plotkin Bug to do it, but that's ugly and would * break 20 years from now when somebody fixes it. I could also print * spaces between each letter, which isn't the intended effect * * For now, do nothing. Doubt this opcode is used often anyway... */ } void op_check_unicode(void) { unsigned result = 0; if(operand[0] <= 255 && (glk_gestalt(gestalt_CharOutput, (unsigned char) operand[0]) != gestalt_CharOutput_CannotPrint)) result |= 1; if(operand[0] <= 255 && (glk_gestalt(gestalt_LineInput, (unsigned char) operand[0]) != FALSE)) result |= 2; mop_store_result(result); } void op_erase_line(void) { if(!allow_output) return; if(operand[0] == 1 && current_window == upper_win) { z_erase_line(current_window); } } void op_erase_window(void) { if(!allow_output) return; #ifdef DEBUG_IO n_show_debug(E_OUTPUT, "erase_window", operand[0]); return; #endif switch(operand[0]) { case neg(1): operand[0] = 0; op_split_window(); current_window = lower_win; case neg(2): case UPPER_WINDOW: z_clear_window(upper_win); if(operand[0] == UPPER_WINDOW) break; /* Ok, this is evil, but it works. */ case LOWER_WINDOW: z_clear_window(lower_win); break; } } void op_get_cursor(void) { zword x, y; z_getxy(upper_win, &x, &y); LOWORDwrite(operand[0], x); LOWORDwrite(operand[0] + ZWORD_SIZE, y); } void op_new_line(void) { output_char(13); } void op_output_stream(void) { if(operand[0] == 0) return; if(is_neg(operand[0])) { switch(neg(operand[0])) { case 1: if(!allow_output) return; output_stream &= ~STREAM1; break; case 2: if(!allow_output) return; set_transcript(0); break; case 3: if(stream3_nesting_depth) stream3_nesting_depth--; else n_show_error(E_OUTPUT, "stream3 unnested too many times", 0); if(!stream3_nesting_depth) output_stream &= ~STREAM3; break; case 4: if(!allow_output) return; glk_stream_close(stream4, NULL); stream4 = 0; output_stream &= ~STREAM4; break; default: n_show_error(E_OUTPUT, "unknown stream deselected", neg(operand[0])); } } else { switch(operand[0]) { case 1: if(!allow_output) return; output_stream |= STREAM1; break; case 2: if(!allow_output) return; if(!stream2) { stream2 = n_file_prompt(fileusage_Transcript | fileusage_TextMode, filemode_WriteAppend); } if(stream2) set_transcript(stream2); break; case 3: if(stream3_nesting_depth >= 16) { n_show_error(E_OUTPUT, "nesting stream 3 too deeply", stream3_nesting_depth); return; } LOWORDwrite(operand[1], 0); stream3_table_starts[stream3_nesting_depth] = operand[1]; stream3_table_locations[stream3_nesting_depth] = operand[1] + 2; output_stream |= STREAM3; stream3_nesting_depth++; break; case 4: if(!allow_output) return; stream4 = n_file_prompt(fileusage_InputRecord | fileusage_TextMode, filemode_WriteAppend); if(stream4) output_stream |= STREAM4; break; default: n_show_error(E_OUTPUT, "unknown stream selected", operand[0]); } } } void op_print(void) { int length; abort_output = FALSE; length = decodezscii(PC, output_char); if(!abort_output) PC += length; } void op_print_ret(void) { int length; abort_output = FALSE; length = decodezscii(PC, output_char); if(abort_output) return; PC += length; output_char(13); if(abort_output) return; mop_func_return(1); } void op_print_addr(void) { decodezscii(operand[0], output_char); } void op_print_paddr(void) { getstring(operand[0]); } void op_print_char(void) { if(operand[0] > 1023) { n_show_error(E_INSTR, "attempt to print character > 1023", operand[0]); return; } output_char(operand[0]); } void op_print_num(void) { n_print_znumber(operand[0]); } void op_print_table(void) { unsigned x, y; zword text = operand[0]; zword width = operand[1]; zword height = operand[2]; zword skips = operand[3]; zword startx, starty; unsigned win_width, win_height; z_getxy(current_window, &startx, &starty); z_getsize(current_window, &win_width, &win_height); if(numoperands < 4) skips = 0; if(numoperands < 3) height = 1; if(current_window == upper_win) { if(startx + width - 1 > win_width) { int diff; n_show_warn(E_OUTPUT, "table too wide; trimming", width); diff = startx + width - 1 - win_width; width -= diff; skips += diff; } if(starty + height - 1 > win_height) { n_show_warn(E_OUTPUT, "table too tall; trimming", height); height = win_height - starty + 1; } } for(y = 0; y < height; y++) { if(current_window == upper_win && allow_output) z_setxy(upper_win, startx, y+starty); for(x = 0; x < width; x++) { output_char(LOBYTE(text)); text++; } text += skips; if(current_window != upper_win && y+1 < height) output_char(13); } } void op_set_colour(void) { if(!allow_output) return; z_set_color(current_window, operand[0], operand[1]); } void op_set_cursor(void) { unsigned width, height; zword x = operand[1]; zword y = operand[0]; if(!allow_output) return; #ifdef DEBUG_IO n_show_debug(E_OUTPUT, "set_cursor y=", operand[0]); n_show_debug(E_OUTPUT, "set_cursor x=", operand[1]); #endif if(current_window != upper_win) { return; } z_getsize(current_window, &width, &height); if(y == 0 || y > height) { /* section 8.7.2.3 */ n_show_error(E_OUTPUT, "illegal line for set_cursor", y); if(y == 0 || y > 512) return; z_set_height(upper_win, y); /* Resize to allow broken games to work */ } if(x == 0 || x > width) { n_show_error(E_OUTPUT, "illegal column for set_cursor", x); return; } z_setxy(current_window, x, y); } void op_set_text_style(void) { if(!allow_output) return; z_set_style(current_window, operand[0]); } void op_set_window(void) { if(!allow_output) return; #ifdef DEBUG_IO n_show_debug(E_OUTPUT, "set_window", operand[0]); return; #endif switch(operand[0]) { case UPPER_WINDOW: current_window = upper_win; z_setxy(upper_win, 1, 1); break; case LOWER_WINDOW: current_window = lower_win; break; default: n_show_error(E_OUTPUT, "invalid window selected", operand[0]); } } void op_split_window(void) { if(!allow_output) return; #ifdef DEBUG_IO n_show_debug(E_OUTPUT, "split_window", operand[0]); #endif if(zversion == 6) return; if(operand[0] > 512) { n_show_error(E_OUTPUT, "game is being ridiculous", operand[0]); return; } if(zversion == 3) z_set_height(upper_win, 0); /* clear the whole upper window first */ z_set_height(upper_win, operand[0]); } static BOOL timer_callback(zword routine) { zword dummylocals[16]; in_timer = TRUE; mop_call(routine, 0, dummylocals, -2); /* add a special stack frame */ decode(); /* start interpreting the routine */ in_timer = FALSE; exit_decoder = FALSE; return time_ret; } BOOL upper_mouse_callback(BOOL is_char_event, winid_t win, glui32 x, glui32 y) { int i; if(!(LOBYTE(HD_FLAGS2) & b00100000)) return FALSE; header_extension_write(1, x + 1); header_extension_write(2, y + 1); stream4number(254); stream4number(x + 1); stream4number(y + 1); if(is_char_event) return TRUE; for(i = z_terminators; LOBYTE(i) != 0; i++) if(LOBYTE(i) == 255 || LOBYTE(i) == 254) return TRUE; /* @read will not receive mouse input inputs if they're not * terminating characters, but I'm not sure how to reasonably do * that and it shouldn't matter to most things */ return FALSE; } typedef struct alias_entry alias_entry; struct alias_entry { alias_entry *next; char *from; char *to; BOOL in_use, is_recursive; }; static alias_entry *alias_list = NULL; void parse_new_alias(const char *aliascommand, BOOL is_recursive) { char *from, *to; char *stringcopy = n_strdup(aliascommand); char *pcommand = stringcopy; while(isspace(*pcommand)) pcommand++; from = pcommand; while(isgraph(*pcommand)) pcommand++; if(*pcommand) { *pcommand = 0; pcommand++; } while(isspace(*pcommand)) pcommand++; to = pcommand; while(*to == ' ') to++; if(*to == 0) /* Expand blank aliases to a single space */ add_alias(from, " ", is_recursive); else add_alias(from, to, is_recursive); free(stringcopy); } void add_alias(const char *from, const char *to, BOOL is_recursive) { alias_entry newalias; remove_alias(from); newalias.next = NULL; newalias.from = n_strdup(from); newalias.to = n_strdup(to); newalias.in_use = FALSE; newalias.is_recursive = is_recursive; LEadd(alias_list, newalias); } BOOL remove_alias(const char *from) { alias_entry *p, *t; while(*from == ' ') from++; LEsearchremove(alias_list, p, t, n_strcmp(p->from, from) == 0, (n_free(p->from), n_free(p->to))); return t != NULL; } static alias_entry *find_alias(const char *text, int length) { alias_entry *p; LEsearch(alias_list, p, n_strmatch(p->from, text, length)); return p; } int search_for_aliases(char *text, int length, int maxlen) { int word_start = 0; int i; if(!length) return length; for(i = 0; i <= length; i++) { if(i == length || isspace(text[i]) || ispunct(text[i])) { int word_length = i - word_start; if(word_length) { alias_entry *p = find_alias(text + word_start, word_length); if(p && !(p->in_use)) { int newlen = strlen(p->to); if(length - word_length + newlen > maxlen) newlen = maxlen - length + word_length; n_memmove(text + word_start + newlen, text + word_start + word_length, maxlen - word_start - MAX(newlen, word_length)); n_memcpy(text + word_start, p->to, newlen); if(p->is_recursive) { p->in_use = TRUE; newlen = search_for_aliases(text + word_start, newlen, maxlen - word_start); p->in_use = FALSE; } length += newlen - word_length; i = word_start + newlen; } } word_start = i+1; } } return length; } int n_read(zword dest, unsigned maxlen, zword parse, unsigned initlen, zword timer, zword routine, unsigned char *terminator) { unsigned length; unsigned i; char *buffer = (char *) n_malloc(maxlen + 1); #ifdef SMART_TOKENISER forget_corrections(); #endif if(false_undo) initlen = 0; false_undo = FALSE; if(maxlen < 3) n_show_warn(E_OUTPUT, "small text buffer", maxlen); if(dest > dynamic_size || dest < 64) { n_show_error(E_OUTPUT, "input buffer in invalid location", dest); return 0; } if(dest + maxlen > dynamic_size) { n_show_error(E_OUTPUT, "input buffer exceeds dynamic memory", dest + maxlen); maxlen = dynamic_size - dest; } if(parse >= dest && dest + maxlen > parse) { n_show_warn(E_OUTPUT, "input buffer overlaps parse", dest + maxlen - parse); maxlen = parse - dest; } for(i = 0; i < maxlen; i++) buffer[i] = LOBYTE(dest + i); length = z_read(current_window, buffer, maxlen, initlen, timer, timer_callback, routine, terminator); if(read_abort) { n_free(buffer); return 0; } length = search_for_aliases(buffer, length, maxlen); for(i = 0; i < length; i++) { buffer[i] = glk_char_to_lower(buffer[i]); LOBYTEwrite(dest + i, buffer[i]); } if(parse) z_tokenise(buffer, length, parse, z_dictionary, TRUE); n_free(buffer); return length; } void stream4line(const char *buffer, int length, char terminator) { if(output_stream & STREAM4) { w_glk_put_buffer_stream(stream4, buffer, length); if(terminator != 10) stream4number(terminator); glk_put_char_stream(stream4, 10); } } void op_sread(void) { unsigned maxlen, length; unsigned char term; zword text = operand[0]; maxlen = LOBYTE(text) - 1; if(numoperands < 3) operand[2] = 0; if(numoperands < 4) operand[3] = 0; length = n_read(text + 1, maxlen, operand[1], 0, operand[2], operand[3], &term); if(!read_abort) { LOBYTEwrite(text + 1 + length, 0); /* zero terminator */ if(allow_saveundo) { if(!has_done_save_undo && auto_save_undo) saveundo(FALSE); has_done_save_undo = FALSE; } } } void op_aread(void) { int maxlen, length, initlen; unsigned char term; zword text = operand[0]; maxlen = LOBYTE(text); initlen = LOBYTE(text + 1); if(numoperands < 3) operand[2] = 0; if(numoperands < 4) operand[3] = 0; length = n_read(text + 2, maxlen, operand[1], initlen, operand[2], operand[3], &term); if(!read_abort) { LOBYTEwrite(text + 1, length); mop_store_result(term); if(allow_saveundo) { if(!has_done_save_undo && auto_save_undo) saveundo(FALSE); has_done_save_undo = FALSE; } } } void op_read_char(void) { zword validch = 0; if(in_timer) { n_show_error(E_OUTPUT, "input attempted during time routine", 0); mop_store_result(0); return; } if(operand[0] != 1) { n_show_warn(E_OUTPUT, "read_char with non-one first operand", operand[0]); mop_store_result(0); return; } if(numoperands < 2) operand[1] = 0; if(numoperands < 3) operand[2] = 0; validch = z_read_char(current_window, operand[1], timer_callback, operand[2]); if(read_abort) return; mop_store_result(validch); /* if(!has_done_save_undo && auto_save_undo_char) { saveundo(FALSE); has_done_save_undo = FALSE; } */ stream4number(validch); } void op_show_status(void) { if(!in_timer) z_flush_fixed(upper_win); } /* Returns a character, or returns 0 if it found a number, which it stores in *num */ unsigned char transcript_getchar(unsigned *num) { glsi32 c; *num = 0; if(!input_stream1) return 0; c = glk_get_char_stream(input_stream1); if(c == GLK_EOF) { glk_stream_close(input_stream1, NULL); input_stream1 = 0; return 0; } if(c == '[') { while((c = glk_get_char_stream(input_stream1)) != GLK_EOF) { if(c == ']') break; if(c >= '0' && c <= '9') { *num = (*num * 10) + (c - '0'); } } c = glk_get_char_stream(input_stream1); if(c != 10) n_show_error(E_OUTPUT, "input script not understood", c); return 0; } return c; } /* Returns line terminator. Writes up to *len bytes to dest, writing in the actual number of characters read in *len. */ unsigned char transcript_getline(char *dest, glui32 *length) { unsigned char term = 10; unsigned char c; unsigned num; glui32 len; if(!input_stream1) { *length = 0; return 0; } for(len = 0; len < *length; len++) { c = transcript_getchar(&num); if(!c) { term = num; break; } if(c == 10) break; dest[len] = c; } *length = len; return term; } void op_input_stream(void) { /* *FIXME*: 10.2.4 says we shouldn't wait for [MORE]. Glk won't allow this */ if(input_stream1) glk_stream_close(input_stream1, NULL); input_stream1 = 0; switch(operand[0]) { case 0: break; case 1: input_stream1 = n_file_prompt(fileusage_InputRecord | fileusage_TextMode, filemode_Read); break; default: n_show_error(E_OUTPUT, "unknown input stream selected", operand[0]); } } void op_set_font(void) { int lastfont; if(!allow_output) { mop_store_result(0); return; } lastfont = font; #ifdef DEBUG_IO n_show_debug(E_OUTPUT, "set_font", operand[0]); return; #endif switch(operand[0]) { case 1: font = 1; break; case 4: font = 4; break; case 3: if(enablefont3) font = 3; default: mop_store_result(0); return; } set_fixed(font == 4); mop_store_result(lastfont); } void op_print_unicode(void) { output_char(operand[0]); }