/*-
* Copyright 2010-2011 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, 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
#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)
/* Adapted from Gargoyle’s Frotz (Copyright 2010 Ben Cressey). */
static glui32 zcolor_map[] = {
zcolor_Default,
0x000000, /* 2 = black */
0xef0000, /* 3 = red */
0x00d600, /* 4 = green */
0xefef00, /* 5 = yellow */
0x006bb5, /* 6 = blue */
0xff00ff, /* 7 = magenta */
0x00efef, /* 8 = cyan */
0xffffff, /* 9 = white */
0xb5b5b5, /* 10 = light grey */
0x8c8c8c, /* 11 = medium grey */
0x5a5a5a /* 12 = dark grey */
};
static glui32 fg_color = zcolor_Default, bg_color = zcolor_Default;
void update_color(int which, unsigned long color)
{
if(which < 2 || which > 9) 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 == 6 ? 12 : 9)) fg_color = zcolor_map[fg - 1];
if(bg >= 1 && bg <= (zversion == 6 ? 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
}
/* Adapted from Gargoyle’s Frotz (Copyright 2010 Ben Cressey). */
#ifdef GARGLK
#define zB(i) (((((i) >> 10) & 0x1f) << 3) | ((((i) >> 10) & 0x1f) >> 2))
#define zG(i) (((((i) >> 5) & 0x1f) << 3) | ((((i) >> 5) & 0x1f) >> 2))
#define zR(i) (((((i) >> 0) & 0x1f) << 3) | ((((i) >> 0) & 0x1f) >> 2))
#define zRGB(i) (zR(i) << 16 | zG(i) << 8 | zB(i))
#endif
void zset_true_colour(void)
{
#ifdef GARGLK
long fg = (int16_t)zargs[0], bg = (int16_t)zargs[1];
if (fg >= 0) fg_color = zRGB(fg);
else if(fg == -1) fg_color = zcolor_Default;
if (bg >= 0) bg_color = zRGB(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. 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
{
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_u(zscii_to_unicode[user_byte(zargs[0] + 2 + i)]);
}
put_char_u(UNICODE_LINEFEED);
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);
}