X-Git-Url: https://git.stderr.nl/gitweb?a=blobdiff_plain;f=interpreters%2Fbocfel%2Fscreen.c;fp=interpreters%2Fbocfel%2Fscreen.c;h=35feb56f7b251eb5d7e3cfc62201c245e9b85fe5;hb=3c59ba5eef5cb4d39c06eb7f523b9c3b026bdc9b;hp=0000000000000000000000000000000000000000;hpb=ed91d840318ed6ebfe3a5a77fa17114ddbf56640;p=projects%2Fchimara%2Fchimara.git diff --git a/interpreters/bocfel/screen.c b/interpreters/bocfel/screen.c new file mode 100644 index 0000000..35feb56 --- /dev/null +++ b/interpreters/bocfel/screen.c @@ -0,0 +1,2475 @@ +/*- + * Copyright 2010-2012 Chris Spiegel. + * + * This file is part of Bocfel. + * + * Bocfel is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License, version + * 2 or 3, as published by the Free Software Foundation. + * + * Bocfel 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 Bocfel. If not, see . + */ + +#include +#include +#include +#include + +#ifdef ZTERP_GLK +#include +#include +#endif + +#include "screen.h" +#include "branch.h" +#include "dict.h" +#include "io.h" +#include "memory.h" +#include "objects.h" +#include "osdep.h" +#include "process.h" +#include "stack.h" +#include "unicode.h" +#include "util.h" +#include "zterp.h" + +static struct window +{ + unsigned style; + + enum font { FONT_NONE = -1, FONT_PREVIOUS, FONT_NORMAL, FONT_PICTURE, FONT_CHARACTER, FONT_FIXED } font; + enum font prev_font; + +#ifdef ZTERP_GLK + winid_t id; + long x, y; /* Only meaningful for window 1 */ + int pending_read; + union line + { + char latin1[256]; + glui32 unicode[256]; + } *line; + int has_echo; +#endif +} windows[8], *mainwin = &windows[0], *curwin = &windows[0]; +#ifdef ZTERP_GLK +static struct window *upperwin = &windows[1]; +static struct window statuswin; +static long upper_window_height = 0; +static long upper_window_width = 0; +static winid_t errorwin; +#endif + +/* In all versions but 6, styles are global and stored in mainwin. For + * V6, styles are tracked per window and thus stored in each individual + * window. For convenience, this macro expands to the “style window” + * for any version. + */ +#define style_window (zversion == 6 ? curwin : mainwin) + +/* If the window needs to be temporarily switched (@show_status and + * @print_form print to specific windows, and window_change() might + * temporarily need to switch to the upper window), the code that + * requires a specific window can be wrapped in these macros. + */ +#ifdef ZTERP_GLK +#define SWITCH_WINDOW_START(win) { struct window *saved_ = curwin; curwin = (win); glk_set_window((win)->id); +#define SWITCH_WINDOW_END() curwin = saved_; glk_set_window(curwin->id); } +#else +#define SWITCH_WINDOW_START(win) { struct window *saved_ = curwin; curwin = (win); +#define SWITCH_WINDOW_END() curwin = saved_; } +#endif + +/* Output stream bits. */ +#define STREAM_SCREEN (1U << 1) +#define STREAM_TRANS (1U << 2) +#define STREAM_MEMORY (1U << 3) +#define STREAM_SCRIPT (1U << 4) + +static unsigned int streams = STREAM_SCREEN; +static zterp_io *transio, *scriptio; + +static struct +{ + uint16_t table; + uint16_t i; +} stables[16]; +static int stablei = -1; + +static int istream = ISTREAM_KEYBOARD; +static zterp_io *istreamio; + +struct input +{ + enum { INPUT_CHAR, INPUT_LINE } type; + + /* ZSCII value of key read for @read_char. */ + uint8_t key; + + /* Unicode line of chars read for @read. */ + uint32_t *line; + uint8_t maxlen; + uint8_t len; + uint8_t preloaded; + + /* Character used to terminate input. If terminating keys are not + * supported by the Glk implementation being used (or if Glk is not + * used at all) this will be ZSCII_NEWLINE; or in the case of + * cancellation, 0. + */ + uint8_t term; +}; + +/* This macro makes it so that code elsewhere needn’t check have_unicode before printing. */ +#define GLK_PUT_CHAR(c) do { if(!have_unicode) glk_put_char(unicode_to_latin1[c]); else glk_put_char_uni(c); } while(0) + +void show_message(const char *fmt, ...) +{ + va_list ap; + char message[1024]; + + va_start(ap, fmt); + vsnprintf(message, sizeof message, fmt, ap); + va_end(ap); + +#ifdef ZTERP_GLK + static int error_lines = 0; + + if(errorwin != NULL) + { + glui32 w, h; + + /* Allow multiple messages to stack, but force at least 5 lines to + * always be visible in the main window. This is less than perfect + * because it assumes that each message will be less than the width + * of the screen, but it’s not a huge deal, really; even if the + * lines are too long, at least Gargoyle and glktermw are graceful + * enough. + */ + glk_window_get_size(mainwin->id, &w, &h); + + if(h > 5) glk_window_set_arrangement(glk_window_get_parent(errorwin), winmethod_Below | winmethod_Fixed, ++error_lines, errorwin); + glk_put_char_stream(glk_window_get_stream(errorwin), UNICODE_LINEFEED); + } + else + { + errorwin = glk_window_open(mainwin->id, winmethod_Below | winmethod_Fixed, error_lines = 2, wintype_TextBuffer, 0); + } + + /* If windows are not supported (e.g. in cheapglk), messages will not + * get displayed. If this is the case, print to the main window. + */ + if(errorwin != NULL) + { + glk_set_style_stream(glk_window_get_stream(errorwin), style_Alert); + glk_put_string_stream(glk_window_get_stream(errorwin), message); + } + else + { + SWITCH_WINDOW_START(mainwin); + glk_put_string("\12["); + glk_put_string(message); + glk_put_string("]\12"); + SWITCH_WINDOW_END(); + } +#else + /* In Glk messages go to a separate window, but they're interleaved in + * non-Glk. Put brackets around the message in an attempt to offset + * it from the game a bit. + */ + fprintf(stderr, "\n[%s]\n", message); +#endif +} + +/* See §7. + * This returns true if the stream was successfully selected. + * Deselecting a stream is always successful. + */ +int output_stream(int16_t number, uint16_t table) +{ + if(number > 0) + { + streams |= 1U << number; + } + else if(number < 0) + { + if(number != -3 || stablei == 0) streams &= ~(1U << -number); + } + + if(number == 2) + { + STORE_WORD(0x10, WORD(0x10) | FLAGS2_TRANSCRIPT); + if(transio == NULL) + { + transio = zterp_io_open(options.transcript_name, ZTERP_IO_TRANS | (options.overwrite_transcript ? ZTERP_IO_WRONLY : ZTERP_IO_APPEND)); + if(transio == NULL) + { + STORE_WORD(0x10, WORD(0x10) & ~FLAGS2_TRANSCRIPT); + streams &= ~STREAM_TRANS; + warning("unable to open the transcript"); + } + } + } + else if(number == -2) + { + STORE_WORD(0x10, WORD(0x10) & ~FLAGS2_TRANSCRIPT); + } + + if(number == 3) + { + stablei++; + ZASSERT(stablei < 16, "too many stream tables"); + + stables[stablei].table = table; + user_store_word(stables[stablei].table, 0); + stables[stablei].i = 2; + } + else if(number == -3 && stablei >= 0) + { + user_store_word(stables[stablei].table, stables[stablei].i - 2); + stablei--; + } + + if(number == 4) + { + if(scriptio == NULL) + { + scriptio = zterp_io_open(options.record_name, ZTERP_IO_WRONLY | ZTERP_IO_INPUT); + if(scriptio == NULL) + { + streams &= ~STREAM_SCRIPT; + warning("unable to open the script"); + } + } + } + /* XXX v6 has even more handling */ + + return number < 0 || (streams & (1U << number)); +} + +void zoutput_stream(void) +{ + output_stream(zargs[0], zargs[1]); +} + +/* See §10. + * This returns true if the stream was successfully selected. + */ +int input_stream(int which) +{ + istream = which; + + if(istream == ISTREAM_KEYBOARD) + { + if(istreamio != NULL) + { + zterp_io_close(istreamio); + istreamio = NULL; + } + } + else if(istream == ISTREAM_FILE) + { + if(istreamio == NULL) + { + istreamio = zterp_io_open(options.replay_name, ZTERP_IO_INPUT | ZTERP_IO_RDONLY); + if(istreamio == NULL) + { + warning("unable to open the command script"); + istream = ISTREAM_KEYBOARD; + } + } + } + else + { + ZASSERT(0, "invalid input stream: %d", istream); + } + + return istream == which; +} + +void zinput_stream(void) +{ + input_stream(zargs[0]); +} + +/* This does not even pretend to understand V6 windows. */ +static void set_current_window(struct window *window) +{ + curwin = window; + +#ifdef ZTERP_GLK + if(curwin == upperwin && upperwin->id != NULL) + { + upperwin->x = upperwin->y = 0; + glk_window_move_cursor(upperwin->id, 0, 0); + } + + glk_set_window(curwin->id); +#endif + + set_current_style(); +} + +/* Find and validate a window. If window is -3 and the story is V6, + * return the current window. + */ +static struct window *find_window(uint16_t window) +{ + int16_t w = window; + + ZASSERT(zversion == 6 ? w == -3 || (w >= 0 && w < 8) : w == 0 || w == 1, "invalid window selected: %d", w); + + if(w == -3) return curwin; + + return &windows[w]; +} + +#ifdef ZTERP_GLK +/* When resizing the upper window, the screen’s contents should not + * change (§8.6.1); however, the way windows are handled with Glk makes + * this slightly impossible. When an Inform game tries to display + * something with “box”, it expands the upper window, displays the quote + * box, and immediately shrinks the window down again. This is a + * problem under Glk because the window immediately disappears. Other + * games, such as Bureaucracy, expect the upper window to shrink as soon + * as it has been requested. Thus the following system is used: + * + * If a request is made to shrink the upper window, it is granted + * immediately if there has been user input since the last window resize + * request. If there has not been user input, the request is delayed + * until after the next user input is read. + */ +static long delayed_window_shrink = -1; +static int saw_input; + +static void update_delayed(void) +{ + glui32 height; + + if(delayed_window_shrink == -1 || upperwin->id == NULL) return; + + glk_window_set_arrangement(glk_window_get_parent(upperwin->id), winmethod_Above | winmethod_Fixed, delayed_window_shrink, upperwin->id); + upper_window_height = delayed_window_shrink; + + /* Glk might resize the window to a smaller height than was requested, + * so track the actual height, not the requested height. + */ + glk_window_get_size(upperwin->id, NULL, &height); + if(height != upper_window_height) + { + /* This message probably won’t be seen in a window since the upper + * window is likely covering everything, but try anyway. + */ + show_message("Unable to fulfill window size request: wanted %ld, got %lu", delayed_window_shrink, (unsigned long)height); + upper_window_height = height; + } + + delayed_window_shrink = -1; +} + +/* Both the upper and lower windows have their own issues to deal with + * when there is line input. This function ensures that the cursor + * position is properly tracked in the upper window, and if possible, + * aids in the suppression of newline printing on input cancellation in + * the lower window. + */ +static void cleanup_screen(struct input *input) +{ + if(input->type != INPUT_LINE) return; + + /* If the current window is the upper window, the position of the + * cursor needs to be tracked, so after a line has successfully been + * read, advance the cursor to the initial position of the next line, + * or if a terminating key was used or input was canceled, to the end + * of the input. + */ + if(curwin == upperwin) + { + if(input->term != ZSCII_NEWLINE) upperwin->x += input->len; + + if(input->term == ZSCII_NEWLINE || upperwin->x >= upper_window_width) + { + upperwin->x = 0; + if(upperwin->y < upper_window_height) upperwin->y++; + } + + glk_window_move_cursor(upperwin->id, upperwin->x, upperwin->y); + } + + /* If line input echoing is turned off, newlines will not be printed + * when input is canceled, but neither will the input line. Fix that. + */ + if(curwin->has_echo) + { + glk_set_style(style_Input); + for(int i = 0; i < input->len; i++) GLK_PUT_CHAR(input->line[i]); + if(input->term == ZSCII_NEWLINE) glk_put_char(UNICODE_LINEFEED); + set_current_style(); + } +} + +/* In an interrupt, if the story tries to read or write, the previous + * read event (which triggered the interrupt) needs to be canceled. + * This function does the cancellation. + */ +static void cancel_read_events(struct window *window) +{ + if(window->pending_read) + { + event_t ev; + + glk_cancel_char_event(window->id); + glk_cancel_line_event(window->id, &ev); + + /* If the pending read was a line input, zero terminate the string + * so when it’s re-requested the length of the already-loaded + * portion can be discovered. Also deal with cursor positioning in + * the upper window, and line echoing in the lower window. + */ + if(ev.type == evtype_LineInput && window->line != NULL) + { + uint32_t line[ev.val1]; + struct input input = { .type = INPUT_LINE, .line = line, .term = 0, .len = ev.val1 }; + + if(have_unicode) window->line->unicode[ev.val1] = 0; + else window->line->latin1 [ev.val1] = 0; + + for(int i = 0; i < input.len; i++) + { + if(have_unicode) line[i] = window->line->unicode[i]; + else line[i] = window->line->latin1 [i]; + } + + cleanup_screen(&input); + } + + window->pending_read = 0; + window->line = NULL; + } +} + +static void clear_window(struct window *window) +{ + if(window->id == NULL) return; + + /* glk_window_clear() cannot be used while there are pending read requests. */ + cancel_read_events(window); + + glk_window_clear(window->id); + + window->x = window->y = 0; +} +#endif + +/* If restoring from an interrupt (which is a bad idea to begin with), + * it’s entirely possible that there will be pending read events that + * need to be canceled, so allow that. + */ +void cancel_all_events(void) +{ +#ifdef ZTERP_GLK + for(int i = 0; i < 8; i++) cancel_read_events(&windows[i]); +#endif +} + +static void resize_upper_window(long nlines) +{ +#ifdef ZTERP_GLK + if(upperwin->id == NULL) return; + + /* To avoid code duplication, put all window resizing code in + * update_delayed() and, if necessary, call it from here. + */ + delayed_window_shrink = nlines; + if(upper_window_height <= nlines || saw_input) update_delayed(); + + saw_input = 0; + + /* §8.6.1.1.2 */ + if(zversion == 3) clear_window(upperwin); + + /* As in a few other areas, changing the upper window causes reverse + * video to be deactivated, so reapply the current style. + */ + set_current_style(); +#endif +} + +void close_upper_window(void) +{ + /* The upper window is never destroyed; rather, when it’s closed, it + * shrinks to zero height. + */ + resize_upper_window(0); + +#ifdef ZTERP_GLK + delayed_window_shrink = -1; + saw_input = 0; +#endif + + set_current_window(mainwin); +} + +void get_screen_size(unsigned int *width, unsigned int *height) +{ + *width = 80; + *height = 24; + +#ifdef ZTERP_GLK + glui32 w, h; + + /* The main window can be proportional, and if so, its width is not + * generally useful because games tend to care about width with a + * fixed font. If a status window is available, or if an upper window + * is available, use that to calculate the width, because these + * windows will have a fixed-width font. The height is the combined + * height of all windows. + */ + glk_window_get_size(mainwin->id, &w, &h); + *height = h; + if(statuswin.id != NULL) + { + glk_window_get_size(statuswin.id, &w, &h); + *height += h; + } + if(upperwin->id != NULL) + { + glk_window_get_size(upperwin->id, &w, &h); + *height += h; + } + *width = w; +#else + zterp_os_get_screen_size(width, height); +#endif + + /* XGlk does not report the size of textbuffer windows, so here’s a safety net. */ + if(*width == 0) *width = 80; + if(*height == 0) *height = 24; + + /* Terrible hack: Because V6 is not properly supported, the window to + * which Journey writes its story is completely covered up by window + * 1. For the same reason, only the bottom 6 lines of window 1 are + * actually useful, even though the game expands it to cover the whole + * screen. By pretending that the screen height is only 6, the main + * window, where text is actually sent, becomes visible. + */ + if(is_story("83-890706") && *height > 6) *height = 6; +} + +#ifdef GLK_MODULE_LINE_TERMINATORS +static uint32_t *term_keys, term_size, term_nkeys; + +void term_keys_reset(void) +{ + free(term_keys); + term_keys = NULL; + term_size = 0; + term_nkeys = 0; +} + +static void insert_key(uint32_t key) +{ + if(term_nkeys == term_size) + { + term_size += 32; + + term_keys = realloc(term_keys, term_size * sizeof *term_keys); + if(term_keys == NULL) die("unable to allocate memory for terminating keys"); + } + + term_keys[term_nkeys++] = key; +} + +void term_keys_add(uint8_t key) +{ + switch(key) + { + case 129: insert_key(keycode_Up); break; + case 130: insert_key(keycode_Down); break; + case 131: insert_key(keycode_Left); break; + case 132: insert_key(keycode_Right); break; + case 133: insert_key(keycode_Func1); break; + case 134: insert_key(keycode_Func2); break; + case 135: insert_key(keycode_Func3); break; + case 136: insert_key(keycode_Func4); break; + case 137: insert_key(keycode_Func5); break; + case 138: insert_key(keycode_Func6); break; + case 139: insert_key(keycode_Func7); break; + case 140: insert_key(keycode_Func8); break; + case 141: insert_key(keycode_Func9); break; + case 142: insert_key(keycode_Func10); break; + case 143: insert_key(keycode_Func11); break; + case 144: insert_key(keycode_Func12); break; + + /* Keypad 0–9 should be here, but Glk doesn’t support that. */ + case 145: case 146: case 147: case 148: case 149: + case 150: case 151: case 152: case 153: case 154: + break; + + /* Mouse clicks would go here if I supported them. */ + case 252: case 253: case 254: + break; + + case 255: + for(int i = 129; i <= 144; i++) term_keys_add(i); + break; + + default: + ZASSERT(0, "invalid terminating key: %u", (unsigned)key); + break; + } +} +#endif + +/* Print out a character. The character is in “c” and is either Unicode + * or ZSCII; if the former, “unicode” is true. + */ +static void put_char_base(uint16_t c, int unicode) +{ + if(c == 0) return; + + if(streams & STREAM_MEMORY) + { + ZASSERT(stablei != -1, "invalid stream table"); + + /* When writing to memory, ZSCII should always be used (§7.5.3). */ + if(unicode) c = unicode_to_zscii_q[c]; + + user_store_byte(stables[stablei].table + stables[stablei].i++, c); + } + else + { + /* For screen and transcription, always prefer Unicode. */ + if(!unicode) c = zscii_to_unicode[c]; + + if(c != 0) + { + uint8_t zscii = 0; + + /* §16 makes no mention of what a newline in font 3 should map to. + * Other interpreters that implement font 3 assume it stays a + * newline, and this makes the most sense, so don’t do any + * translation in that case. + */ + if(curwin->font == FONT_CHARACTER && !options.disable_graphics_font && c != UNICODE_LINEFEED) + { + zscii = unicode_to_zscii[c]; + + /* These four characters have a “built-in” reverse video (see §16). */ + if(zscii >= 123 && zscii <= 126) + { + style_window->style ^= STYLE_REVERSE; + set_current_style(); + } + + c = zscii_to_font3[zscii]; + } +#ifdef ZTERP_GLK + if((streams & STREAM_SCREEN) && curwin->id != NULL) + { + cancel_read_events(curwin); + + if(curwin == upperwin) + { + /* Interpreters seem to have differing ideas about what + * happens when the cursor reaches the end of a line in the + * upper window. Some wrap, some let it run off the edge (or, + * at least, stop the text at the edge). The standard, from + * what I can see, says nothing on this issue. Follow Windows + * Frotz and don’t wrap. + */ + + if(c == UNICODE_LINEFEED) + { + if(upperwin->y < upper_window_height) + { + /* Glk wraps, so printing a newline when the cursor has + * already reached the edge of the screen will produce two + * newlines. + */ + if(upperwin->x < upper_window_width) GLK_PUT_CHAR(c); + + /* Even if a newline isn’t explicitly printed here + * (because the cursor is at the edge), setting + * upperwin->x to 0 will cause the next character to be on + * the next line because the text will have wrapped. + */ + upperwin->x = 0; + upperwin->y++; + } + } + else if(upperwin->x < upper_window_width && upperwin->y < upper_window_height) + { + upperwin->x++; + GLK_PUT_CHAR(c); + } + } + else + { + GLK_PUT_CHAR(c); + } + } +#else + if((streams & STREAM_SCREEN) && curwin == mainwin) zterp_io_putc(zterp_io_stdout(), c); +#endif + + /* If the reverse video bit was flipped (for the character font), flip it back. */ + if(zscii >= 123 && zscii <= 126) + { + style_window->style ^= STYLE_REVERSE; + set_current_style(); + } + + if((streams & STREAM_TRANS) && curwin == mainwin) zterp_io_putc(transio, c); + } + } +} + +void put_char_u(uint16_t c) +{ + put_char_base(c, 1); +} + +void put_char(uint8_t c) +{ + put_char_base(c, 0); +} + +static void put_string(const char *s) +{ + for(; *s != 0; s++) + { + if(*s == '\n') put_char(ZSCII_NEWLINE); + else put_char(*s); + } +} + +/* Decode and print a zcode string at address “addr”. This can be + * called recursively thanks to abbreviations; the initial call should + * have “in_abbr” set to 0. + * Each time a character is decoded, it is passed to the function + * “outc”. + */ +static int print_zcode(uint32_t addr, int in_abbr, void (*outc)(uint8_t)) +{ + int abbrev = 0, shift = 0, special = 0; + int c, lastc = 0; /* Initialize lastc to shut gcc up */ + uint16_t w; + uint32_t counter = addr; + int current_alphabet = 0; + + do + { + ZASSERT(counter < memory_size - 1, "string runs beyond the end of memory"); + + w = WORD(counter); + + for(int i = 10; i >= 0; i -= 5) + { + c = (w >> i) & 0x1f; + + if(special) + { + if(special == 2) lastc = c; + else outc((lastc << 5) | c); + + special--; + } + + else if(abbrev) + { + uint32_t new_addr; + + new_addr = user_word(header.abbr + 64 * (abbrev - 1) + 2 * c); + + /* new_addr is a word address, so multiply by 2 */ + print_zcode(new_addr * 2, 1, outc); + + abbrev = 0; + } + + else switch(c) + { + case 0: + outc(ZSCII_SPACE); + shift = 0; + break; + case 1: + if(zversion == 1) + { + outc(ZSCII_NEWLINE); + shift = 0; + break; + } + /* fallthrough */ + case 2: case 3: + if(zversion >= 3 || (zversion == 2 && c == 1)) + { + ZASSERT(!in_abbr, "abbreviation being used recursively"); + abbrev = c; + shift = 0; + } + else + { + shift = c - 1; + } + break; + case 4: case 5: + if(zversion <= 2) + { + current_alphabet = (current_alphabet + (c - 3)) % 3; + shift = 0; + } + else + { + shift = c - 3; + } + break; + case 6: + if(zversion <= 2) shift = (current_alphabet + shift) % 3; + + if(shift == 2) + { + shift = 0; + special = 2; + break; + } + /* fallthrough */ + default: + if(zversion <= 2 && c != 6) shift = (current_alphabet + shift) % 3; + + outc(atable[(26 * shift) + (c - 6)]); + shift = 0; + break; + } + } + + counter += 2; + } while((w & 0x8000) == 0); + + return counter - addr; +} + +/* Prints the string at addr “addr”. + * + * Returns the number of bytes the string took up. “outc” is passed as + * the character-print function to print_zcode(); if it is NULL, + * put_char is used. + */ +int print_handler(uint32_t addr, void (*outc)(uint8_t)) +{ + return print_zcode(addr, 0, outc != NULL ? outc : put_char); +} + +void zprint(void) +{ + pc += print_handler(pc, NULL); +} + +void zprint_ret(void) +{ + zprint(); + put_char(ZSCII_NEWLINE); + zrtrue(); +} + +void znew_line(void) +{ + put_char(ZSCII_NEWLINE); +} + +void zerase_window(void) +{ +#ifdef ZTERP_GLK + switch((int16_t)zargs[0]) + { + case -2: + for(int i = 0; i < 8; i++) clear_window(&windows[i]); + break; + case -1: + close_upper_window(); + /* fallthrough */ + case 0: + /* 8.7.3.2.1 says V5+ should have the cursor set to 1, 1 of the + * erased window; V4 the lower window goes bottom left, the upper + * to 1, 1. Glk doesn’t give control over the cursor when + * clearing, and that doesn’t really seem to be an issue; so just + * call glk_window_clear(). + */ + clear_window(mainwin); + break; + case 1: + clear_window(upperwin); + break; + default: + show_message("@erase_window: unhandled window: %d", (int16_t)zargs[0]); + break; + } + + /* glk_window_clear() kills reverse video in Gargoyle. Reapply style. */ + set_current_style(); +#endif +} + +void zerase_line(void) +{ +#ifdef ZTERP_GLK + /* XXX V6 does pixel handling here. */ + if(zargs[0] != 1 || curwin != upperwin || upperwin->id == NULL) return; + + for(long i = upperwin->x; i < upper_window_width; i++) GLK_PUT_CHAR(UNICODE_SPACE); + + glk_window_move_cursor(upperwin->id, upperwin->x, upperwin->y); +#endif +} + +/* XXX This is more complex in V6 and needs to be updated when V6 windowing is implemented. */ +static void set_cursor(uint16_t y, uint16_t x) +{ +#ifdef ZTERP_GLK + /* All the windows in V6 can have their cursor positioned; if V6 ever + * comes about this should be fixed. + */ + if(curwin != upperwin) return; + + /* -1 and -2 are V6 only, but at least Zracer passes -1 (or it’s + * trying to position the cursor to line 65535; unlikely!) + */ + if((int16_t)y == -1 || (int16_t)y == -2) return; + + /* §8.7.2.3 says 1,1 is the top-left, but at least one program (Paint + * and Corners) uses @set_cursor 0 0 to go to the top-left; so + * special-case it. + */ + if(y == 0) y = 1; + if(x == 0) x = 1; + + /* This is actually illegal, but some games (e.g. Beyond Zork) expect it to work. */ + if(y > upper_window_height) resize_upper_window(y); + + if(upperwin->id != NULL) + { + upperwin->x = x - 1; + upperwin->y = y - 1; + + glk_window_move_cursor(upperwin->id, x - 1, y - 1); + } +#endif +} + +void zset_cursor(void) +{ + set_cursor(zargs[0], zargs[1]); +} + +void zget_cursor(void) +{ +#ifdef ZTERP_GLK + user_store_word(zargs[0] + 0, upperwin->y + 1); + user_store_word(zargs[0] + 2, upperwin->x + 1); +#else + user_store_word(zargs[0] + 0, 1); + user_store_word(zargs[0] + 2, 1); +#endif +} + +#ifndef ZTERP_GLK +static int16_t fg_color = 1, bg_color = 1; +#elif defined(GARGLK) +static glui32 zcolor_map[] = { + zcolor_Default, + + 0x000000, /* Black */ + 0xef0000, /* Red */ + 0x00d600, /* Green */ + 0xefef00, /* Yellow */ + 0x006bb5, /* Blue */ + 0xff00ff, /* Magenta */ + 0x00efef, /* Cyan */ + 0xffffff, /* White */ + 0xb5b5b5, /* Light grey */ + 0x8c8c8c, /* Medium grey */ + 0x5a5a5a, /* Dark grey */ +}; +static glui32 fg_color = zcolor_Default, bg_color = zcolor_Default; + +void update_color(int which, unsigned long color) +{ + if(which < 2 || which > 12) return; + + zcolor_map[which - 1] = color; +} +#endif + +/* A window argument may be supplied in V6, and this needs to be implemented. */ +void zset_colour(void) +{ + /* Glk (apart from Gargoyle) has no color support. */ +#if !defined(ZTERP_GLK) || defined(GARGLK) + int16_t fg = zargs[0], bg = zargs[1]; + + /* In V6, each window has its own color settings. Since multiple + * windows are not supported, simply ignore all color requests except + * those in the main window. + */ + if(zversion == 6 && curwin != mainwin) return; + + if(options.disable_color) return; + + /* XXX -1 is a valid color in V6. */ +#ifdef GARGLK + if(fg >= 1 && fg <= (zversion >= 5 ? 12 : 9)) fg_color = zcolor_map[fg - 1]; + if(bg >= 1 && bg <= (zversion >= 5 ? 12 : 9)) bg_color = zcolor_map[bg - 1]; + +#else + if(fg >= 1 && fg <= 9) fg_color = fg; + if(bg >= 1 && bg <= 9) bg_color = bg; +#endif + + set_current_style(); +#endif +} + +#ifdef GARGLK +/* Convert a 15-bit color to a 24-bit color. */ +static glui32 convert_color(unsigned long color) +{ + /* Map 5-bit color values to 8-bit. */ + const uint8_t table[] = { + 0x00, 0x08, 0x10, 0x19, 0x21, 0x29, 0x31, 0x3a, + 0x42, 0x4a, 0x52, 0x5a, 0x63, 0x6b, 0x73, 0x7b, + 0x84, 0x8c, 0x94, 0x9c, 0xa5, 0xad, 0xb5, 0xbd, + 0xc5, 0xce, 0xd6, 0xde, 0xe6, 0xef, 0xf7, 0xff + }; + + return table[(color & 0x001f) >> 0] << 16 | + table[(color & 0x03e0) >> 5] << 8 | + table[(color & 0x7c00) >> 10] << 0; +} +#endif + +void zset_true_colour(void) +{ +#ifdef GARGLK + long fg = (int16_t)zargs[0], bg = (int16_t)zargs[1]; + + if (fg >= 0) fg_color = convert_color(fg); + else if(fg == -1) fg_color = zcolor_Default; + + if (bg >= 0) bg_color = convert_color(bg); + else if(bg == -1) bg_color = zcolor_Default; + + set_current_style(); +#endif +} + +int header_fixed_font; + +#ifdef GARGLK +/* Idea from Nitfol. */ +static const int style_map[] = +{ + style_Normal, + style_Normal, + + style_Subheader, /* Bold */ + style_Subheader, /* Bold */ + style_Emphasized, /* Italic */ + style_Emphasized, /* Italic */ + style_Alert, /* Bold Italic */ + style_Alert, /* Bold Italic */ + style_Preformatted, /* Fixed */ + style_Preformatted, /* Fixed */ + style_User1, /* Bold Fixed */ + style_User1, /* Bold Fixed */ + style_User2, /* Italic Fixed */ + style_User2, /* Italic Fixed */ + style_Note, /* Bold Italic Fixed */ + style_Note, /* Bold Italic Fixed */ +}; +#endif + +/* Yes, there are three ways to indicate that a fixed-width font should be used. */ +#define use_fixed_font() (header_fixed_font || curwin->font == FONT_FIXED || (style & STYLE_FIXED)) + +void set_current_style(void) +{ + unsigned style = style_window->style; +#ifdef ZTERP_GLK + if(curwin->id == NULL) return; + +#ifdef GARGLK + if(use_fixed_font()) style |= STYLE_FIXED; + + if(options.disable_fixed) style &= ~STYLE_FIXED; + + ZASSERT(style < 16, "invalid style selected: %x", (unsigned)style); + + glk_set_style(style_map[style]); + + garglk_set_reversevideo(style & STYLE_REVERSE); + + garglk_set_zcolors(fg_color, bg_color); +#else + /* Glk can’t mix other styles with fixed-width, but the upper window + * is always fixed, so if it is selected, there is no need to + * explicitly request it here. In addition, the user can disable + * fixed-width fonts or tell Bocfel to assume that the output font is + * already fixed (e.g. in an xterm); in either case, there is no need + * to request a fixed font. + * This means that another style can also be applied if applicable. + */ + if(use_fixed_font() && + !options.disable_fixed && + !options.assume_fixed && + curwin != upperwin) + { + glk_set_style(style_Preformatted); + return; + } + + /* According to standard 1.1, if mixed styles aren't available, the + * priority is Fixed, Italic, Bold, Reverse. + */ + if (style & STYLE_ITALIC) glk_set_style(style_Emphasized); + else if(style & STYLE_BOLD) glk_set_style(style_Subheader); + else if(style & STYLE_REVERSE) glk_set_style(style_Alert); + else glk_set_style(style_Normal); +#endif +#else + zterp_os_set_style(style, fg_color, bg_color); +#endif +} + +#undef use_fixed_font + +/* V6 has per-window styles, but all others have a global style; in this + * case, track styles via the main window. + */ +void zset_text_style(void) +{ + /* A style of 0 means all others go off. */ + if(zargs[0] == 0) style_window->style = STYLE_NONE; + else style_window->style |= zargs[0]; + + set_current_style(); +} + +/* Interpreters seem to disagree on @set_font. Given the code + + @set_font 4 -> i; + @set_font 1 -> j; + @set_font 0 -> k; + @set_font 1 -> l; + + * the following values are returned: + * Frotz 2.43: 0, 1, 1, 1 + * Gargoyle r384: 1, 4, 4, 4 + * Fizmo 0.6.5: 1, 4, 1, 0 + * Nitfol 0.5: 1, 4, 0, 1 + * Filfre .987: 1, 4, 0, 1 + * Zoom 1.1.4: 1, 1, 0, 1 + * ZLR 0.07: 0, 1, 0, 1 + * Windows Frotz 1.15: 1, 4, 1, 1 + * XZip 1.8.2: 0, 4, 0, 0 + * + * The standard says that “ID 0 means ‘the previous font’.” (§8.1.2). + * The Frotz 2.43 source code says that “zargs[0] = number of font or 0 + * to keep current font”. + * + * How to implement @set_font turns on the meaning of “previous”. Does + * it mean the previous font _after_ the @set_font call, meaning Frotz + * is right? Or is it the previous font _before_ the @set_font call, + * meaning the identity of two fonts needs to be tracked? + * + * Currently I do the latter. That yields the following: + * 1, 4, 1, 4 + * Almost comically, no interpreters agree with each other. + */ +void zset_font(void) +{ + struct window *win = curwin; + + if(zversion == 6 && znargs == 2 && (int16_t)zargs[1] != -3) + { + ZASSERT(zargs[1] < 8, "invalid window selected: %d", (int16_t)zargs[1]); + win = &windows[zargs[1]]; + } + + /* If no previous font has been stored, consider that an error. */ + if(zargs[0] == FONT_PREVIOUS && win->prev_font != FONT_NONE) + { + zargs[0] = win->prev_font; + zset_font(); + } + else if(zargs[0] == FONT_NORMAL || + (zargs[0] == FONT_CHARACTER && !options.disable_graphics_font) || + (zargs[0] == FONT_FIXED && !options.disable_fixed)) + { + store(win->font); + win->prev_font = win->font; + win->font = zargs[0]; + } + else + { + store(0); + } + + set_current_style(); +} + +void zprint_table(void) +{ + uint16_t text = zargs[0], width = zargs[1], height = zargs[2], skip = zargs[3]; + uint16_t n = 0; + +#ifdef ZTERP_GLK + uint16_t start = 0; /* initialize to appease gcc */ + + if(curwin == upperwin) start = upperwin->x + 1; +#endif + + if(znargs < 3) height = 1; + if(znargs < 4) skip = 0; + + for(uint16_t i = 0; i < height; i++) + { + for(uint16_t j = 0; j < width; j++) + { + put_char(user_byte(text + n++)); + } + + if(i + 1 != height) + { + n += skip; +#ifdef ZTERP_GLK + if(curwin == upperwin) + { + set_cursor(upperwin->y + 2, start); + } + else +#endif + { + put_char(ZSCII_NEWLINE); + } + } + } +} + +void zprint_char(void) +{ + /* Check 32 (space) first: a cursory examination of story files + * indicates that this is the most common value passed to @print_char. + * This appears to be due to V4+ games blanking the upper window. + */ +#define valid_zscii_output(c) ((c) == 32 || (c) == 0 || (c) == 9 || (c) == 11 || (c) == 13 || ((c) > 32 && (c) <= 126) || ((c) >= 155 && (c) <= 251)) + ZASSERT(valid_zscii_output(zargs[0]), "@print_char called with invalid character: %u", (unsigned)zargs[0]); +#undef valid_zscii_output + + put_char(zargs[0]); +} + +void zprint_num(void) +{ + char buf[7]; + int i = 0; + long v = (int16_t)zargs[0]; + + if(v < 0) v = -v; + + do + { + buf[i++] = '0' + (v % 10); + } while(v /= 10); + + if((int16_t)zargs[0] < 0) buf[i++] = '-'; + + while(i--) put_char(buf[i]); +} + +void zprint_addr(void) +{ + print_handler(zargs[0], NULL); +} + +void zprint_paddr(void) +{ + print_handler(unpack(zargs[0], 1), NULL); +} + +/* XXX This is more complex in V6 and needs to be updated when V6 windowing is implemented. */ +void zsplit_window(void) +{ + if(zargs[0] == 0) close_upper_window(); + else resize_upper_window(zargs[0]); +} + +void zset_window(void) +{ + set_current_window(find_window(zargs[0])); +} + +#ifdef ZTERP_GLK +static void window_change(void) +{ + /* When a textgrid (the upper window) in Gargoyle is rearranged, it + * forgets about reverse video settings, so reapply any styles to the + * current window (it doesn’t hurt if the window is a textbuffer). If + * the current window is not the upper window that’s OK, because + * set_current_style() is called when a @set_window is requested. + */ + set_current_style(); + + /* If the new window is smaller, the cursor of the upper window might + * be out of bounds. Pull it back in if so. + */ + if(zversion >= 3 && upperwin->id != NULL && upper_window_height > 0) + { + long x = upperwin->x, y = upperwin->y; + glui32 w, h; + + glk_window_get_size(upperwin->id, &w, &h); + + upper_window_width = w; + upper_window_height = h; + + if(x > w) x = w; + if(y > h) y = h; + + SWITCH_WINDOW_START(upperwin); + set_cursor(y + 1, x + 1); + SWITCH_WINDOW_END(); + } + + /* §8.4 + * Only 0x20 and 0x21 are mentioned; what of 0x22 and 0x24? Zoom and + * Windows Frotz both update the V5 header entries, so do that here, + * too. + * + * Also, no version restrictions are given, but assume V4+ per §11.1. + */ + if(zversion >= 4) + { + unsigned width, height; + + get_screen_size(&width, &height); + + STORE_BYTE(0x20, height > 254 ? 254 : height); + STORE_BYTE(0x21, width > 255 ? 255 : width); + + if(zversion >= 5) + { + STORE_WORD(0x22, width > UINT16_MAX ? UINT16_MAX : width); + STORE_WORD(0x24, height > UINT16_MAX ? UINT16_MAX : height); + } + } + else + { + zshow_status(); + } +} +#endif + +#ifdef ZTERP_GLK +static int timer_running; + +static void start_timer(uint16_t n) +{ + if(!TIMER_AVAILABLE()) return; + + if(timer_running) die("nested timers unsupported"); + glk_request_timer_events(n * 100); + timer_running = 1; +} + +static void stop_timer(void) +{ + if(!TIMER_AVAILABLE()) return; + + glk_request_timer_events(0); + timer_running = 0; +} + +static void request_char(void) +{ + if(have_unicode) glk_request_char_event_uni(curwin->id); + else glk_request_char_event(curwin->id); + + curwin->pending_read = 1; +} + +static void request_line(union line *line, glui32 maxlen, glui32 initlen) +{ + if(have_unicode) glk_request_line_event_uni(curwin->id, line->unicode, maxlen, initlen); + else glk_request_line_event(curwin->id, line->latin1, maxlen, initlen); + + curwin->pending_read = 1; + curwin->line = line; +} +#endif + +#define special_zscii(c) ((c) >= 129 && (c) <= 154) + +/* This is called when input stream 1 (read from file) is selected. If + * it succefully reads a character/line from the file, it fills the + * struct at “input” with the appropriate information and returns true. + * If it fails to read (likely due to EOF) then it sets the input stream + * back to the keyboard and returns false. + */ +static int istream_read_from_file(struct input *input) +{ + if(input->type == INPUT_CHAR) + { + long c; + + c = zterp_io_getc(istreamio); + if(c == -1) + { + input_stream(ISTREAM_KEYBOARD); + return 0; + } + + /* Don’t translate special ZSCII characters (cursor keys, function keys, keypad). */ + if(special_zscii(c)) input->key = c; + else input->key = unicode_to_zscii_q[c]; + } + else + { + long n; + uint16_t line[1024]; + + n = zterp_io_readline(istreamio, line, sizeof line / sizeof *line); + if(n == -1) + { + input_stream(ISTREAM_KEYBOARD); + return 0; + } + + if(n > input->maxlen) n = input->maxlen; + + input->len = n; + +#ifdef ZTERP_GLK + if(curwin->id != NULL) + { + glk_set_style(style_Input); + for(long i = 0; i < n; i++) GLK_PUT_CHAR(line[i]); + GLK_PUT_CHAR(UNICODE_LINEFEED); + set_current_style(); + } +#else + for(long i = 0; i < n; i++) zterp_io_putc(zterp_io_stdout(), line[i]); + zterp_io_putc(zterp_io_stdout(), UNICODE_LINEFEED); +#endif + + for(long i = 0; i < n; i++) input->line[i] = line[i]; + } + +#ifdef ZTERP_GLK + event_t ev; + + /* It’s possible that output is buffered, meaning that until + * glk_select() is called, output will not be displayed. When reading + * from a command-script, flush on each command so that output is + * visible while the script is being replayed. + */ + glk_select_poll(&ev); + switch(ev.type) + { + case evtype_None: + break; + case evtype_Arrange: + window_change(); + break; + default: + /* No other events should arrive. Timers are only started in + * get_input() and are stopped before that function returns. + * Input events will not happen with glk_select_poll(), and no + * other event type is expected to be raised. + */ + break; + } + + saw_input = 1; +#endif + + return 1; +} + +#ifdef GLK_MODULE_LINE_TERMINATORS +/* Glk returns terminating characters as keycode_*, but we need them as + * ZSCII. This should only ever be called with values that are matched + * in the switch, because those are the only ones that Glk was told are + * terminating characters. In the event that another keycode comes + * through, though, treat it as Enter. + */ +static uint8_t zscii_from_glk(glui32 key) +{ + switch(key) + { + case 13: return ZSCII_NEWLINE; + case keycode_Up: return 129; + case keycode_Down: return 130; + case keycode_Left: return 131; + case keycode_Right: return 131; + case keycode_Func1: return 133; + case keycode_Func2: return 134; + case keycode_Func3: return 135; + case keycode_Func4: return 136; + case keycode_Func5: return 137; + case keycode_Func6: return 138; + case keycode_Func7: return 139; + case keycode_Func8: return 140; + case keycode_Func9: return 141; + case keycode_Func10: return 142; + case keycode_Func11: return 143; + case keycode_Func12: return 144; + } + + return ZSCII_NEWLINE; +} +#endif + +#ifdef ZTERP_GLK +/* This is like strlen() but in addition to C strings it can find the + * length of a Unicode string (which is assumed to be zero terminated) + * if Unicode is being used. + */ +static size_t line_len(const union line *line) +{ + size_t i; + + if(!have_unicode) return strlen(line->latin1); + + for(i = 0; line->unicode[i] != 0; i++) + { + } + + return i; +} +#endif + +/* Attempt to read input from the user. The input type can be either a + * single character or a full line. If “timer” is not zero, a timer is + * started that fires off every “timer” tenths of a second (if the value + * is 1, it will timeout 10 times a second, etc.). Each time the timer + * times out the routine at address “routine” is called. If the routine + * returns true, the input is canceled. + * + * The function returns 1 if input was stored, 0 if there was a + * cancellation as described above. + */ +static int get_input(int16_t timer, int16_t routine, struct input *input) +{ + /* If either of these is zero, no timeout should happen. */ + if(timer == 0) routine = 0; + if(routine == 0) timer = 0; + + /* Flush all streams when input is requested. */ +#ifndef ZTERP_GLK + zterp_io_flush(zterp_io_stdout()); +#endif + zterp_io_flush(scriptio); + zterp_io_flush(transio); + + /* Generally speaking, newline will be the reason the line input + * stopped, so set it by default. It will be overridden where + * necessary. + */ + input->term = ZSCII_NEWLINE; + + if(istream == ISTREAM_FILE && istream_read_from_file(input)) return 1; +#ifdef ZTERP_GLK + int status = 0; + union line line; + struct window *saved = NULL; + + /* In V6, input might be requested on an unsupported window. If so, + * switch to the main window temporarily. + */ + if(curwin->id == NULL) + { + saved = curwin; + curwin = mainwin; + glk_set_window(curwin->id); + } + + if(input->type == INPUT_CHAR) + { + request_char(); + } + else + { + for(int i = 0; i < input->preloaded; i++) + { + if(have_unicode) line.unicode[i] = input->line[i]; + else line.latin1 [i] = input->line[i]; + } + + request_line(&line, input->maxlen, input->preloaded); + } + + if(timer != 0) start_timer(timer); + + while(status == 0) + { + event_t ev; + + glk_select(&ev); + + switch(ev.type) + { + case evtype_Arrange: + window_change(); + break; + + case evtype_Timer: + { + ZASSERT(timer != 0, "got unexpected evtype_Timer"); + + struct window *saved2 = curwin; + int ret; + + stop_timer(); + + ret = direct_call(routine); + + /* It’s possible for an interrupt to switch windows; if it + * does, simply switch back. This is the easiest way to deal + * with an undefined bit of the Z-machine. + */ + if(curwin != saved2) set_current_window(saved2); + + if(ret) + { + status = 2; + } + else + { + /* If this got reset to 0, that means an interrupt had to + * cancel the read event in order to either read or write. + */ + if(!curwin->pending_read) + { + if(input->type == INPUT_CHAR) request_char(); + else request_line(&line, input->maxlen, line_len(&line)); + } + + start_timer(timer); + } + } + + break; + + case evtype_CharInput: + ZASSERT(input->type == INPUT_CHAR, "got unexpected evtype_CharInput"); + ZASSERT(ev.win == curwin->id, "got evtype_CharInput on unexpected window"); + + status = 1; + + switch(ev.val1) + { + case keycode_Delete: input->key = 8; break; + case keycode_Return: input->key = 13; break; + case keycode_Escape: input->key = 27; break; + case keycode_Up: input->key = 129; break; + case keycode_Down: input->key = 130; break; + case keycode_Left: input->key = 131; break; + case keycode_Right: input->key = 132; break; + case keycode_Func1: input->key = 133; break; + case keycode_Func2: input->key = 134; break; + case keycode_Func3: input->key = 135; break; + case keycode_Func4: input->key = 136; break; + case keycode_Func5: input->key = 137; break; + case keycode_Func6: input->key = 138; break; + case keycode_Func7: input->key = 139; break; + case keycode_Func8: input->key = 140; break; + case keycode_Func9: input->key = 141; break; + case keycode_Func10: input->key = 142; break; + case keycode_Func11: input->key = 143; break; + case keycode_Func12: input->key = 144; break; + + default: + input->key = ZSCII_QUESTIONMARK; + + if(ev.val1 <= UINT16_MAX) + { + uint8_t c = unicode_to_zscii[ev.val1]; + + if(c != 0) input->key = c; + } + + break; + } + + break; + + case evtype_LineInput: + ZASSERT(input->type == INPUT_LINE, "got unexpected evtype_LineInput"); + ZASSERT(ev.win == curwin->id, "got evtype_LineInput on unexpected window"); + input->len = ev.val1; +#ifdef GLK_MODULE_LINE_TERMINATORS + if(zversion >= 5) input->term = zscii_from_glk(ev.val2); +#endif + status = 1; + break; + } + } + + stop_timer(); + + if(input->type == INPUT_CHAR) + { + glk_cancel_char_event(curwin->id); + } + else + { + /* On cancellation, the buffer still needs to be filled, because + * it’s possible that line input echoing has been turned off and the + * contents will need to be written out. + */ + if(status == 2) + { + event_t ev; + + glk_cancel_line_event(curwin->id, &ev); + input->len = ev.val1; + input->term = 0; + } + + for(glui32 i = 0; i < input->len; i++) + { + if(have_unicode) input->line[i] = line.unicode[i] > UINT16_MAX ? UNICODE_QUESTIONMARK : line.unicode[i]; + else input->line[i] = (uint8_t)line.latin1[i]; + } + } + + curwin->pending_read = 0; + curwin->line = NULL; + + if(status == 1) saw_input = 1; + + if(errorwin != NULL) + { + glk_window_close(errorwin, NULL); + errorwin = NULL; + } + + if(saved != NULL) + { + curwin = saved; + glk_set_window(curwin->id); + } + + return status != 2; +#else + if(input->type == INPUT_CHAR) + { + long n; + uint16_t line[64]; + + n = zterp_io_readline(zterp_io_stdin(), line, sizeof line / sizeof *line); + + /* On error/eof, or if an invalid key was typed, pretend “Enter” was hit. */ + if(n <= 0) + { + input->key = ZSCII_NEWLINE; + } + else + { + input->key = unicode_to_zscii[line[0]]; + if(input->key == 0) input->key = ZSCII_NEWLINE; + } + } + else + { + input->len = input->preloaded; + + if(input->maxlen > input->preloaded) + { + long n; + uint16_t line[1024]; + + n = zterp_io_readline(zterp_io_stdin(), line, sizeof line / sizeof *line); + if(n != -1) + { + if(n > input->maxlen - input->preloaded) n = input->maxlen - input->preloaded; + for(long i = 0; i < n; i++) input->line[i + input->preloaded] = line[i]; + input->len += n; + } + } + } + + return 1; +#endif +} + +void zread_char(void) +{ + uint16_t timer = 0; + uint16_t routine = zargs[2]; + struct input input = { .type = INPUT_CHAR }; + +#ifdef ZTERP_GLK + cancel_read_events(curwin); +#endif + + if(zversion >= 4 && znargs > 1) timer = zargs[1]; + + if(!get_input(timer, routine, &input)) + { + store(0); + return; + } + +#ifdef ZTERP_GLK + update_delayed(); +#endif + + if(streams & STREAM_SCRIPT) + { + /* Values 127 to 159 are not valid Unicode, and these just happen to + * match up to the values needed for special ZSCII keys, so store + * them as-is. + */ + if(special_zscii(input.key)) zterp_io_putc(scriptio, input.key); + else zterp_io_putc(scriptio, zscii_to_unicode[input.key]); + } + + store(input.key); +} + +#ifdef ZTERP_GLK +static void status_putc(uint8_t c) +{ + glk_put_char(zscii_to_unicode[c]); +} +#endif + +void zshow_status(void) +{ +#ifdef ZTERP_GLK + glui32 width, height; + char rhs[64]; + int first = variable(0x11), second = variable(0x12); + + if(statuswin.id == NULL) return; + + glk_window_clear(statuswin.id); + + SWITCH_WINDOW_START(&statuswin); + + glk_window_get_size(statuswin.id, &width, &height); + +#ifdef GARGLK + garglk_set_reversevideo(1); +#else + glk_set_style(style_Alert); +#endif + for(glui32 i = 0; i < width; i++) glk_put_char(ZSCII_SPACE); + + glk_window_move_cursor(statuswin.id, 1, 0); + + /* Variable 0x10 is global variable 1. */ + print_object(variable(0x10), status_putc); + + if(STATUS_IS_TIME()) + { + snprintf(rhs, sizeof rhs, "Time: %d:%02d%s ", (first + 11) % 12 + 1, second, first < 12 ? "am" : "pm"); + if(strlen(rhs) > width) snprintf(rhs, sizeof rhs, "%02d:%02d", first, second); + } + else + { + snprintf(rhs, sizeof rhs, "Score: %d Moves: %d ", first, second); + if(strlen(rhs) > width) snprintf(rhs, sizeof rhs, "%d/%d", first, second); + } + + if(strlen(rhs) <= width) + { + glk_window_move_cursor(statuswin.id, width - strlen(rhs), 0); + glk_put_string(rhs); + } + + SWITCH_WINDOW_END(); +#endif +} + +/* This is strcmp() except that the first string is Unicode. */ +static int unicmp(const uint32_t *s1, const char *s2) +{ + while(*s1 != 0 && *s2 == *s1) + { + s1++; + s2++; + } + + return *s1 - *s2; +} + +uint32_t read_pc; + +/* Try to parse a meta command. Returns true if input should be + * restarted, false to indicate no more input is required. In most + * cases input will be required because the game has requested it, but + * /undo and /restore jump to different locations, so the current @read + * no longer exists. + */ +static int handle_meta_command(const uint32_t *string) +{ + if(unicmp(string, "undo") == 0) + { + uint16_t flags2 = WORD(0x10); + int success = pop_save(); + + if(success != 0) + { + /* §6.1.2. */ + STORE_WORD(0x10, flags2); + + if(zversion >= 5) store(success); + else put_string("[undone]\n\n>"); + + return 0; + } + else + { + put_string("[no save found, unable to undo]"); + } + } + else if(unicmp(string, "scripton") == 0) + { + if(output_stream(OSTREAM_SCRIPT, 0)) put_string("[transcripting on]"); + else put_string("[transcripting failed]"); + } + else if(unicmp(string, "scriptoff") == 0) + { + output_stream(-OSTREAM_SCRIPT, 0); + put_string("[transcripting off]"); + } + else if(unicmp(string, "recon") == 0) + { + if(output_stream(OSTREAM_RECORD, 0)) put_string("[command recording on]"); + else put_string("[command recording failed]"); + } + else if(unicmp(string, "recoff") == 0) + { + output_stream(-OSTREAM_RECORD, 0); + put_string("[command recording off]"); + } + else if(unicmp(string, "replay") == 0) + { + if(input_stream(ISTREAM_FILE)) put_string("[replaying commands]"); + else put_string("[replaying commands failed]"); + } + else if(unicmp(string, "save") == 0) + { + if(interrupt_level() != 0) + { + put_string("[cannot call /save while in an interrupt]"); + } + else + { + uint32_t tmp = pc; + + /* pc is currently set to the next instruction, but the restore + * needs to come back to *this* instruction; so temporarily set + * pc back before saving. + */ + pc = read_pc; + if(do_save(1)) put_string("[saved]"); + else put_string("[save failed]"); + pc = tmp; + } + } + else if(unicmp(string, "restore") == 0) + { + if(do_restore(1)) + { + put_string("[restored]\n\n>"); + return 0; + } + else + { + put_string("[restore failed]"); + } + } + else if(unicmp(string, "help") == 0) + { + put_string( + "/undo: undo a turn\n" + "/scripton: start a transcript\n" + "/scriptoff: stop a transcript\n" + "/recon: start a command record\n" + "/recoff: stop a command record\n" + "/replay: replay a command record\n" + "/save: save the game\n" + "/restore: restore a game saved by /save" + ); + } + else + { + put_string("[unknown command]"); + } + + return 1; +} + +void zread(void) +{ + uint16_t text = zargs[0], parse = zargs[1]; + uint8_t maxchars = zversion >= 5 ? user_byte(text) : user_byte(text) - 1; + uint8_t zscii_string[maxchars]; + uint32_t string[maxchars + 1]; + struct input input = { .type = INPUT_LINE, .line = string, .maxlen = maxchars }; + uint16_t timer = 0; + uint16_t routine = zargs[3]; + +#ifdef ZTERP_GLK + cancel_read_events(curwin); +#endif + + if(zversion <= 3) zshow_status(); + + if(zversion >= 4 && znargs > 2) timer = zargs[2]; + + if(zversion >= 5) + { + int i; + + input.preloaded = user_byte(text + 1); + ZASSERT(input.preloaded <= maxchars, "too many preloaded characters: %d when max is %d", input.preloaded, maxchars); + + for(i = 0; i < input.preloaded; i++) string[i] = zscii_to_unicode[user_byte(text + i + 2)]; + string[i] = 0; + + /* Under garglk, preloaded input works as it’s supposed to. + * Under Glk, it can fail one of two ways: + * 1. The preloaded text is printed out once, but is not editable. + * 2. The preloaded text is printed out twice, the second being editable. + * I have chosen option #2. For non-Glk, option #1 is done by necessity. + */ +#ifdef GARGLK + if(curwin->id != NULL) garglk_unput_string_uni(string); +#endif + } + + if(!get_input(timer, routine, &input)) + { +#ifdef ZTERP_GLK + cleanup_screen(&input); +#endif + if(zversion >= 5) store(0); + return; + } + +#ifdef ZTERP_GLK + cleanup_screen(&input); +#endif + +#ifdef ZTERP_GLK + update_delayed(); +#endif + + if(options.enable_escape && (streams & STREAM_TRANS)) + { + zterp_io_putc(transio, 033); + zterp_io_putc(transio, '['); + for(int i = 0; options.escape_string[i] != 0; i++) zterp_io_putc(transio, options.escape_string[i]); + } + + for(int i = 0; i < input.len; i++) + { + zscii_string[i] = unicode_to_zscii_q[unicode_tolower(string[i])]; + if(streams & STREAM_TRANS) zterp_io_putc(transio, string[i]); + if(streams & STREAM_SCRIPT) zterp_io_putc(scriptio, string[i]); + } + + if(options.enable_escape && (streams & STREAM_TRANS)) + { + zterp_io_putc(transio, 033); + zterp_io_putc(transio, '['); + zterp_io_putc(transio, '0'); + zterp_io_putc(transio, 'm'); + } + + if(streams & STREAM_TRANS) zterp_io_putc(transio, UNICODE_LINEFEED); + if(streams & STREAM_SCRIPT) zterp_io_putc(scriptio, UNICODE_LINEFEED); + + if(!options.disable_meta_commands) + { + string[input.len] = 0; + + if(string[0] == '/') + { + if(handle_meta_command(string + 1)) + { + /* The game still wants input, so try again. */ + put_string("\n\n>"); + zread(); + } + + return; + } + + /* V1–4 do not have @save_undo, so simulate one each time @read is + * called. + * + * pc is currently set to the next instruction, but the undo needs + * to come back to *this* instruction; so temporarily set pc back + * before pushing the save. + */ + if(zversion <= 4) + { + uint32_t tmp_pc = pc; + + pc = read_pc; + push_save(); + pc = tmp_pc; + } + } + + if(zversion >= 5) + { + user_store_byte(text + 1, input.len); /* number of characters read */ + + for(int i = 0; i < input.len; i++) + { + user_store_byte(text + i + 2, zscii_string[i]); + } + + if(parse != 0) tokenize(text, parse, 0, 0); + + store(input.term); + } + else + { + for(int i = 0; i < input.len; i++) + { + user_store_byte(text + i + 1, zscii_string[i]); + } + + user_store_byte(text + input.len + 1, 0); + + tokenize(text, parse, 0, 0); + } +} + +void zprint_unicode(void) +{ + if(valid_unicode(zargs[0])) put_char_u(zargs[0]); + else put_char_u(UNICODE_QUESTIONMARK); +} + +void zcheck_unicode(void) +{ + uint16_t res = 0; + + /* valid_unicode() will tell which Unicode characters can be printed; + * and if the Unicode character is in the Unicode input table, it can + * also be read. If Unicode is not available, then any character >255 + * is invalid for both reading and writing. + */ + if(have_unicode || zargs[0] < 256) + { + if(valid_unicode(zargs[0])) res |= 0x01; + if(unicode_to_zscii[zargs[0]] != 0) res |= 0x02; + } + + store(res); +} + +/* Should picture_data and get_wind_prop be moved to a V6 source file? */ +void zpicture_data(void) +{ + if(zargs[0] == 0) + { + user_store_word(zargs[1] + 0, 0); + user_store_word(zargs[1] + 2, 0); + } + + /* No pictures means no valid pictures, so never branch. */ + branch_if(0); +} + +void zget_wind_prop(void) +{ + uint16_t val; + struct window *win; + + win = find_window(zargs[0]); + + /* These are mostly bald-faced lies. */ + switch(zargs[1]) + { + case 0: /* y coordinate */ + val = 0; + break; + case 1: /* x coordinate */ + val = 0; + break; + case 2: /* y size */ + val = 100; + break; + case 3: /* x size */ + val = 100; + break; + case 4: /* y cursor */ + val = 0; + break; + case 5: /* x cursor */ + val = 0; + break; + case 6: /* left margin size */ + val = 0; + break; + case 7: /* right margin size */ + val = 0; + break; + case 8: /* newline interrupt routine */ + val = 0; + break; + case 9: /* interrupt countdown */ + val = 0; + break; + case 10: /* text style */ + val = win->style; + break; + case 11: /* colour data */ + val = (9 << 8) | 2; + break; + case 12: /* font number */ + val = win->font; + break; + case 13: /* font size */ + val = (10 << 8) | 10; + break; + case 14: /* attributes */ + val = 0; + break; + case 15: /* line count */ + val = 0; + break; + case 16: /* true foreground colour */ + val = 0; + break; + case 17: /* true background colour */ + val = 0; + break; + default: + die("unknown window property: %u", (unsigned)zargs[1]); + } + + store(val); +} + +/* This is not correct, because @output_stream does not work as it + * should with a width argument; however, this does print out the + * contents of a table that was sent to stream 3, so it’s at least + * somewhat useful. + * + * Output should be to the currently-selected window, but since V6 is + * only marginally supported, other windows are not active. Send to the + * main window for the time being. + */ +void zprint_form(void) +{ + SWITCH_WINDOW_START(mainwin); + + for(uint16_t i = 0; i < user_word(zargs[0]); i++) + { + put_char(user_byte(zargs[0] + 2 + i)); + } + + put_char(ZSCII_NEWLINE); + + SWITCH_WINDOW_END(); +} + +void zmake_menu(void) +{ + branch_if(0); +} + +void zbuffer_screen(void) +{ + store(0); +} + +#ifdef GARGLK +/* Glk does not guarantee great control over how various styles are + * going to look, but Gargoyle does. Abusing the Glk “style hints” + * functions allows for quite fine-grained control over style + * appearance. First, clear the (important) attributes for each style, + * and then recreate each in whatever mold is necessary. Re-use some + * that are expected to be correct (emphasized for italic, subheader for + * bold, and so on). + */ +static void set_default_styles(void) +{ + int styles[] = { style_Subheader, style_Emphasized, style_Alert, style_Preformatted, style_User1, style_User2, style_Note }; + + for(int i = 0; i < 7; i++) + { + glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Size, 0); + glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Weight, 0); + glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Oblique, 0); + + /* This sets wintype_TextGrid to be proportional, which of course is + * wrong; but text grids are required to be fixed, so Gargoyle + * simply ignores this hint for those windows. + */ + glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Proportional, 1); + } +} +#endif + +int create_mainwin(void) +{ +#ifdef ZTERP_GLK + +#ifdef GARGLK + set_default_styles(); + + /* Bold */ + glk_stylehint_set(wintype_AllTypes, style_Subheader, stylehint_Weight, 1); + + /* Italic */ + glk_stylehint_set(wintype_AllTypes, style_Emphasized, stylehint_Oblique, 1); + + /* Bold Italic */ + glk_stylehint_set(wintype_AllTypes, style_Alert, stylehint_Weight, 1); + glk_stylehint_set(wintype_AllTypes, style_Alert, stylehint_Oblique, 1); + + /* Fixed */ + glk_stylehint_set(wintype_AllTypes, style_Preformatted, stylehint_Proportional, 0); + + /* Bold Fixed */ + glk_stylehint_set(wintype_AllTypes, style_User1, stylehint_Weight, 1); + glk_stylehint_set(wintype_AllTypes, style_User1, stylehint_Proportional, 0); + + /* Italic Fixed */ + glk_stylehint_set(wintype_AllTypes, style_User2, stylehint_Oblique, 1); + glk_stylehint_set(wintype_AllTypes, style_User2, stylehint_Proportional, 0); + + /* Bold Italic Fixed */ + glk_stylehint_set(wintype_AllTypes, style_Note, stylehint_Weight, 1); + glk_stylehint_set(wintype_AllTypes, style_Note, stylehint_Oblique, 1); + glk_stylehint_set(wintype_AllTypes, style_Note, stylehint_Proportional, 0); +#endif + + mainwin->id = glk_window_open(0, 0, 0, wintype_TextBuffer, 1); + if(mainwin->id == NULL) return 0; + glk_set_window(mainwin->id); + +#ifdef GLK_MODULE_LINE_ECHO + mainwin->has_echo = glk_gestalt(gestalt_LineInputEcho, 0); + if(mainwin->has_echo) glk_set_echo_line_event(mainwin->id, 0); +#endif + + return 1; +#else + return 1; +#endif +} + +int create_statuswin(void) +{ +#ifdef ZTERP_GLK + if(statuswin.id == NULL) statuswin.id = glk_window_open(mainwin->id, winmethod_Above | winmethod_Fixed, 1, wintype_TextGrid, 0); + return statuswin.id != NULL; +#else + return 0; +#endif +} + +int create_upperwin(void) +{ +#ifdef ZTERP_GLK + /* On a restart, this function will get called again. It would be + * possible to try to resize the upper window to 0 if it already + * exists, but it’s easier to just destroy and recreate it. + */ + if(upperwin->id != NULL) glk_window_close(upperwin->id, NULL); + + /* The upper window appeared in V3. */ + if(zversion >= 3) + { + upperwin->id = glk_window_open(mainwin->id, winmethod_Above | winmethod_Fixed, 0, wintype_TextGrid, 0); + upperwin->x = upperwin->y = 0; + upper_window_height = 0; + + if(upperwin->id != NULL) + { + glui32 w, h; + + glk_window_get_size(upperwin->id, &w, &h); + upper_window_width = w; + + if(h != 0 || upper_window_width == 0) + { + glk_window_close(upperwin->id, NULL); + upperwin->id = NULL; + } + } + } + + return upperwin->id != NULL; +#else + return 0; +#endif +} + +void init_screen(void) +{ + for(int i = 0; i < 8; i++) + { + windows[i].style = STYLE_NONE; + windows[i].font = FONT_NORMAL; + windows[i].prev_font = FONT_NONE; + +#ifdef ZTERP_GLK + clear_window(&windows[i]); +#ifdef GLK_MODULE_LINE_TERMINATORS + if(windows[i].id != NULL && term_nkeys != 0 && glk_gestalt(gestalt_LineTerminators, 0)) glk_set_terminators_line_event(windows[i].id, term_keys, term_nkeys); +#endif +#endif + } + + close_upper_window(); + +#ifdef ZTERP_GLK + if(statuswin.id != NULL) glk_window_clear(statuswin.id); + + if(errorwin != NULL) + { + glk_window_close(errorwin, NULL); + errorwin = NULL; + } + + stop_timer(); + +#ifdef GARGLK + fg_color = zcolor_Default; + bg_color = zcolor_Default; +#endif + +#else + fg_color = 1; + bg_color = 1; +#endif + + if(scriptio != NULL) zterp_io_close(scriptio); + scriptio = NULL; + + input_stream(ISTREAM_KEYBOARD); + + streams = STREAM_SCREEN; + stablei = -1; + set_current_window(mainwin); +}