diff options
Diffstat (limited to 'engines/glk/agt/os_glk.cpp')
-rw-r--r-- | engines/glk/agt/os_glk.cpp | 6099 |
1 files changed, 6099 insertions, 0 deletions
diff --git a/engines/glk/agt/os_glk.cpp b/engines/glk/agt/os_glk.cpp new file mode 100644 index 0000000000..1d9abd6838 --- /dev/null +++ b/engines/glk/agt/os_glk.cpp @@ -0,0 +1,6099 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/agt/agility.h" +#include "glk/agt/interp.h" +#include "glk/agt/agt.h" + +namespace Glk { +namespace AGT { + +/* + * Glk interface for AGiliTy 1.1.1.1 + * ------------------------------- + * + * This module contains the the Glk porting layer for AGiliTy. It + * defines the Glk arguments list structure, the entry points for the + * Glk library framework to use, and all platform-abstracted I/O to + * link to Glk's I/O. + * + * The following items are omitted from this Glk port: + * + * o Calls to g_vm->glk_tick(). The Glk documentation states that the + * interpreter should call g_vm->glk_tick() every opcode or so. This is + * intrusive to code (it goes outside of this module), and since + * most Glk libraries do precisely nothing in g_vm->glk_tick(), there is + * little motivation to add it. + * + * o Glk tries to assert control over _all_ file I/O. It's just too + * disruptive to add it to existing code, so for now, the AGiliTy + * interpreter is still dependent on stdio and the like. + */ + +/* + * True and false definitions -- usually defined in glkstart.h, but we need + * them early, so we'll define them here too. We also need NULL, but that's + * normally from stdio.h or one of it's cousins. + */ +#ifndef FALSE +# define FALSE 0 +#endif +#ifndef TRUE +# define TRUE (!FALSE) +#endif + + +/*---------------------------------------------------------------------*/ +/* Module variables, miscellaneous externals not in header files */ +/*---------------------------------------------------------------------*/ + +/* Glk AGiliTy port version number. */ +static const glui32 GAGT_PORT_VERSION = 0x00010701; + +/* + * We use two Glk windows; one is two lines at the top of the display area + * for status, and the other is the remainder of the display area, used for, + * well, everything else. Where a particular Glk implementation won't do + * more than one window, the status window remains NULL. + */ +static winid_t gagt_main_window = NULL, + gagt_status_window = NULL; + +/* + * Transcript stream and input log. These are NULL if there is no current + * collection of these strings. + */ +static strid_t gagt_transcript_stream = NULL, + gagt_inputlog_stream = NULL; + +/* Input read log stream, for reading back an input log. */ +static strid_t gagt_readlog_stream = NULL; + +/* Options that may be turned off or set by command line flags. */ +enum FontMode { + FONT_AUTOMATIC, FONT_FIXED_WIDTH, FONT_PROPORTIONAL, FONT_DEBUG +}; +static FontMode gagt_font_mode = FONT_AUTOMATIC; + +enum DelayMode { + DELAY_FULL, DELAY_SHORT, DELAY_OFF +}; +static DelayMode gagt_delay_mode = DELAY_SHORT; +static int gagt_replacement_enabled = TRUE, + gagt_extended_status_enabled = TRUE, + gagt_abbreviations_enabled = TRUE, + gagt_commands_enabled = TRUE; + +/* Forward declaration of event wait functions. */ +static void gagt_event_wait(glui32 wait_type, event_t *event); +static void gagt_event_wait_2(glui32 wait_type_1, + glui32 wait_type_2, + event_t *event); + +/* + * Forward declaration of the g_vm->glk_exit() wrapper. Normal functions in this + * module should not to call g_vm->glk_exit() directly; they should always call it + * through the wrapper instead. + */ +static void gagt_exit(); + + +/*---------------------------------------------------------------------*/ +/* Glk port utility functions */ +/*---------------------------------------------------------------------*/ + +/* + * gagt_fatal() + * + * Fatal error handler. The function returns, expecting the caller to + * abort() or otherwise handle the error. + */ +static void gagt_fatal(const char *string) { + /* + * If the failure happens too early for us to have a window, print + * the message to stderr. + */ + if (!gagt_main_window) + error("INTERNAL ERROR: %s", string); + + /* Cancel all possible pending window input events. */ + g_vm->glk_cancel_line_event(gagt_main_window, NULL); + g_vm->glk_cancel_char_event(gagt_main_window); + + /* Print a message indicating the error. */ + g_vm->glk_set_window(gagt_main_window); + g_vm->glk_set_style(style_Normal); + g_vm->glk_put_string("\n\nINTERNAL ERROR: "); + g_vm->glk_put_string(string); + + g_vm->glk_put_string("\n\nPlease record the details of this error, try to" + " note down everything you did to cause it, and email" + " this information to simon_baldwin@yahoo.com.\n\n"); +} + + +/* + * gagt_malloc() + * gagt_realloc() + * + * Non-failing malloc and realloc; call gagt_fatal() and exit if memory + * allocation fails. + */ +static void *gagt_malloc(size_t size) { + void *pointer; + + pointer = malloc(size); + if (!pointer) { + gagt_fatal("GLK: Out of system memory"); + gagt_exit(); + } + + return pointer; +} + +static void *gagt_realloc(void *ptr, size_t size) { + void *pointer; + + pointer = realloc(ptr, size); + if (!pointer) { + gagt_fatal("GLK: Out of system memory"); + gagt_exit(); + } + + return pointer; +} + + +/* + * gagt_strncasecmp() + * gagt_strcasecmp() + * + * Strncasecmp and strcasecmp are not ANSI functions, so here are local + * definitions to do the same jobs. + */ +static int gagt_strncasecmp(const char *s1, const char *s2, size_t n) { + size_t index; + + for (index = 0; index < n; index++) { + int diff; + + diff = g_vm->glk_char_to_lower(s1[index]) - g_vm->glk_char_to_lower(s2[index]); + if (diff < 0 || diff > 0) + return diff < 0 ? -1 : 1; + } + + return 0; +} + +static int gagt_strcasecmp(const char *s1, const char *s2) { + size_t s1len, s2len; + int result; + + s1len = strlen(s1); + s2len = strlen(s2); + + result = gagt_strncasecmp(s1, s2, s1len < s2len ? s1len : s2len); + if (result < 0 || result > 0) + return result; + else + return s1len < s2len ? -1 : s1len > s2len ? 1 : 0; +} + + +/* + * gagt_debug() + * + * Handler for module debug output. If no debug, it ignores the call, + * otherwise it prints a debug message, prefixed by the function name. + */ +static void gagt_debug(const char *function, const char *format, ...) { + if (DEBUG_OUT) { + Common::WriteStream *ws = dynamic_cast<Common::WriteStream *>(debugfile); + assert(ws); + + ws->writeString(Common::String::format("%s (", function)); + if (format && strlen(format) > 0) { + va_list va; + + va_start(va, format); + Common::String data = Common::String::vformat(format, va); + ws->writeString(data); + va_end(va); + } + + ws->writeString(")\n"); + } +} + + +/*---------------------------------------------------------------------*/ +/* Functions not ported - functionally unchanged from os_none.c */ +/*---------------------------------------------------------------------*/ + +/* + * agt_tone() + * + * Produce a hz-Hertz sound for ms milliseconds. + */ +void agt_tone(int hz, int ms) { + gagt_debug("agt_tone", "hz=%d, ms=%d", hz, ms); +} + + +/* + * agt_rand() + * + * Return random number from a to b inclusive. The random number generator + * is seeded on the first call, to a reproducible sequence if stable_random, + * otherwise using time(). + */ +int agt_rand(int a, int b) { + int result; + + result = a + g_vm->getRandomNumber(0x7fffffff) % (b - a + 1); + gagt_debug("agt_rand", "a=%d, b=%d -> %d", a, b, result); + return result; +} + + +/*---------------------------------------------------------------------*/ +/* Workrounds for bugs in core AGiliTy. */ +/*---------------------------------------------------------------------*/ + +/* + * gagt_workround_menus() + * + * Somewhere in AGiliTy's menu handling stuff is a condition that sets up + * an eventual NULL dereference in rstrncpy(), called from num_name_func(). + * For some reason, perhaps memory overruns, perhaps something else, it + * happens after a few turns have been made through agt_menu(). Replacing + * agt_menu() won't avoid it. + * + * However, the menu stuff isn't too useful, or attractive, in a game, so one + * solution is to simply disable it. While not possible to do this directly, + * there is a sneaky way, using our carnal knowledge of core AGiliTy. In + * runverb.c, there is code to prevent menu mode from being turned on where + * verbmenu is NULL. Verbmenu is set up in agil.c on loading the game, but, + * crucially, is set up before agil.c calls start_interface(). So... here + * we can free it, set it to NULL, set menu_mode to 0 (it probably is already) + * and AGiliTy behaves as if the game prevents menu mode. + */ +static void gagt_workround_menus() { + free(verbmenu); + verbmenu = NULL; + + menu_mode = 0; +} + + +/* + * gagt_workround_fileexist() + * + * This function verifies that the game file can be opened, in effect second- + * guessing run_game(). + * + * AGiliTy's fileexist() has in it either a bug, or a misfeature. It always + * passes a nofix value of 1 into try_open_file(), which defeats the code to + * retry with both upper and lower cased filenames. So here we have to go + * round the houses, with readopen()/readclose(). + */ +static int gagt_workround_fileexist(fc_type fc, filetype ft) { + genfile file; + const char *errstr; + + errstr = NULL; + file = readopen(fc, ft, &errstr); + + if (file) { + readclose(file); + return TRUE; + } + return FALSE; +} + + +/*---------------------------------------------------------------------*/ +/* I/O interface start and stop functions. */ +/*---------------------------------------------------------------------*/ + +/* AGiliTy font_status values that indicate what font may be used. */ +enum { + GAGT_FIXED_REQUIRED = 1, GAGT_PROPORTIONAL_OKAY = 2 +}; + + +/* + * start_interface() + * close_interface() + * + * Startup and shutdown callout points. The start function for Glk looks + * at the value of font_status that the game sets, to see if it has a strong + * view of the font to use. If it does, then we'll reflect that in the + * module's font contol, perhaps overriding any command line options that the + * user has passed in. + */ +void start_interface(fc_type fc) { + switch (font_status) { + case GAGT_FIXED_REQUIRED: + gagt_font_mode = FONT_FIXED_WIDTH; + break; + + case GAGT_PROPORTIONAL_OKAY: + gagt_font_mode = FONT_PROPORTIONAL; + break; + + default: + break; + } + + gagt_workround_menus(); + + gagt_debug("start_interface", "fc=%p", fc); +} + +void close_interface() { + if (filevalid(scriptfile, fSCR)) + close_pfile(scriptfile, 0); + + gagt_debug("close_interface", ""); +} + + +/*---------------------------------------------------------------------*/ +/* Code page 437 to ISO 8859 Latin-1 translations */ +/*---------------------------------------------------------------------*/ + +/* + * AGiliTy uses IBM code page 437 characters, and Glk works in ISO 8859 + * Latin-1. There's some good news, in that a number of the characters, + * especially international ones, in these two sets are the same. The bad + * news is that, for codes above 127 (that is, beyond 7-bit ASCII), or for + * codes below 32, they live in different places. So, here is a table of + * conversions for codes not equivalent to 7-bit ASCII, and a pair of + * conversion routines. + * + * Note that some code page 437 characters don't have ISO 8859 Latin-1 + * equivalents. Predominantly, these are the box-drawing characters, which + * is a pity, because these are the ones that are used the most. Anyway, + * in these cases, the table substitutes an approximated base ASCII char- + * acter in its place. + * + * The first entry of table comments below is the character's UNICODE value, + * just in case it's useful at some future date. + */ +typedef const struct { + const unsigned char cp437; /* Code page 437 character. */ + const unsigned char iso8859_1; /* ISO 8859 Latin-1 character. */ +} gagt_char_t; +typedef gagt_char_t *gagt_charref_t; + +static gagt_char_t GAGT_CHAR_TABLE[] = { + /* + * Low characters -- those below 0x20. These are the really odd code + * page 437 characters, rarely used by AGT games. Low characters are + * omitted from the reverse lookup, and participate only in the forwards + * lookup from code page 437 to ISO 8859 Latin-1. + */ + {0x01, '@'}, /* 263a White smiling face */ + {0x02, '@'}, /* 263b Black smiling face */ + {0x03, '?'}, /* 2665 Black heart suit */ + {0x04, '?'}, /* 2666 Black diamond suit */ + {0x05, '?'}, /* 2663 Black club suit */ + {0x06, '?'}, /* 2660 Black spade suit */ + {0x07, 0xb7}, /* 2022 Bullet */ + {0x08, 0xb7}, /* 25d8 Inverse bullet */ + {0x09, 0xb7}, /* 25e6 White bullet */ + {0x0a, 0xb7}, /* 25d9 Inverse white circle */ + {0x0b, '?'}, /* 2642 Male sign */ + {0x0c, '?'}, /* 2640 Female sign */ + {0x0d, '?'}, /* 266a Eighth note */ + {0x0e, '?'}, /* 266b Beamed eighth notes */ + {0x0f, 0xa4}, /* 263c White sun with rays */ + {0x10, '>'}, /* 25b6 Black right-pointing triangle */ + {0x11, '<'}, /* 25c0 Black left-pointing triangle */ + {0x12, 0xa6}, /* 2195 Up down arrow */ + {0x13, '!'}, /* 203c Double exclamation mark */ + {0x14, 0xb6}, /* 00b6 Pilcrow sign */ + {0x15, 0xa7}, /* 00a7 Section sign */ + {0x16, '#'}, /* 25ac Black rectangle */ + {0x17, 0xa6}, /* 21a8 Up down arrow with base */ + {0x18, '^'}, /* 2191 Upwards arrow */ + {0x19, 'v'}, /* 2193 Downwards arrow */ + {0x1a, '>'}, /* 2192 Rightwards arrow */ + {0x1b, '<'}, /* 2190 Leftwards arrow */ + {0x1c, '?'}, /* 2310 Reversed not sign */ + {0x1d, '-'}, /* 2194 Left right arrow */ + {0x1e, '^'}, /* 25b2 Black up-pointing triangle */ + {0x1f, 'v'}, /* 25bc Black down-pointing triangle */ + + /* + * High characters -- those above 0x7f. These are more often used by AGT + * games, particularly for box drawing. + */ + {0x80, 0xc7}, /* 00c7 Latin capital letter c with cedilla */ + {0x81, 0xfc}, /* 00fc Latin small letter u with diaeresis */ + {0x82, 0xe9}, /* 00e9 Latin small letter e with acute */ + {0x83, 0xe2}, /* 00e2 Latin small letter a with circumflex */ + {0x84, 0xe4}, /* 00e4 Latin small letter a with diaeresis */ + {0x85, 0xe0}, /* 00e0 Latin small letter a with grave */ + {0x86, 0xe5}, /* 00e5 Latin small letter a with ring above */ + {0x87, 0xe7}, /* 00e7 Latin small letter c with cedilla */ + {0x88, 0xea}, /* 00ea Latin small letter e with circumflex */ + {0x89, 0xeb}, /* 00eb Latin small letter e with diaeresis */ + {0x8a, 0xe8}, /* 00e8 Latin small letter e with grave */ + {0x8b, 0xef}, /* 00ef Latin small letter i with diaeresis */ + {0x8c, 0xee}, /* 00ee Latin small letter i with circumflex */ + {0x8d, 0xec}, /* 00ec Latin small letter i with grave */ + {0x8e, 0xc4}, /* 00c4 Latin capital letter a with diaeresis */ + {0x8f, 0xc5}, /* 00c5 Latin capital letter a with ring above */ + {0x90, 0xc9}, /* 00c9 Latin capital letter e with acute */ + {0x91, 0xe6}, /* 00e6 Latin small ligature ae */ + {0x92, 0xc6}, /* 00c6 Latin capital ligature ae */ + {0x93, 0xf4}, /* 00f4 Latin small letter o with circumflex */ + {0x94, 0xf6}, /* 00f6 Latin small letter o with diaeresis */ + {0x95, 0xf2}, /* 00f2 Latin small letter o with grave */ + {0x96, 0xfb}, /* 00fb Latin small letter u with circumflex */ + {0x97, 0xf9}, /* 00f9 Latin small letter u with grave */ + {0x98, 0xff}, /* 00ff Latin small letter y with diaeresis */ + {0x99, 0xd6}, /* 00d6 Latin capital letter o with diaeresis */ + {0x9a, 0xdc}, /* 00dc Latin capital letter u with diaeresis */ + {0x9b, 0xa2}, /* 00a2 Cent sign */ + {0x9c, 0xa3}, /* 00a3 Pound sign */ + {0x9d, 0xa5}, /* 00a5 Yen sign */ + {0x9e, 'p'}, /* 20a7 Peseta sign */ + {0x9f, 'f'}, /* 0192 Latin small letter f with hook */ + {0xa0, 0xe1}, /* 00e1 Latin small letter a with acute */ + {0xa1, 0xed}, /* 00ed Latin small letter i with acute */ + {0xa2, 0xf3}, /* 00f3 Latin small letter o with acute */ + {0xa3, 0xfa}, /* 00fa Latin small letter u with acute */ + {0xa4, 0xf1}, /* 00f1 Latin small letter n with tilde */ + {0xa5, 0xd1}, /* 00d1 Latin capital letter n with tilde */ + {0xa6, 0xaa}, /* 00aa Feminine ordinal indicator */ + {0xa7, 0xba}, /* 00ba Masculine ordinal indicator */ + {0xa8, 0xbf}, /* 00bf Inverted question mark */ + {0xa9, '.'}, /* 2310 Reversed not sign */ + {0xaa, 0xac}, /* 00ac Not sign */ + {0xab, 0xbd}, /* 00bd Vulgar fraction one half */ + {0xac, 0xbc}, /* 00bc Vulgar fraction one quarter */ + {0xad, 0xa1}, /* 00a1 Inverted exclamation mark */ + {0xae, 0xab}, /* 00ab Left-pointing double angle quotation mark */ + {0xaf, 0xbb}, /* 00bb Right-pointing double angle quotation mark */ + {0xb0, '#'}, /* 2591 Light shade */ + {0xb1, '#'}, /* 2592 Medium shade */ + {0xb2, '#'}, /* 2593 Dark shade */ + {0xb3, '|'}, /* 2502 Box light vertical */ + {0xb4, '+'}, /* 2524 Box light vertical and left */ + {0xb5, '+'}, /* 2561 Box vertical single and left double */ + {0xb6, '|'}, /* 2562 Box vertical double and left single */ + {0xb7, '+'}, /* 2556 Box down double and left single */ + {0xb8, '+'}, /* 2555 Box down single and left double */ + {0xb9, '+'}, /* 2563 Box double vertical and left */ + {0xba, '|'}, /* 2551 Box double vertical */ + {0xbb, '\\'}, /* 2557 Box double down and left */ + {0xbc, '/'}, /* 255d Box double up and left */ + {0xbd, '+'}, /* 255c Box up double and left single */ + {0xbe, '+'}, /* 255b Box up single and left double */ + {0xbf, '\\'}, /* 2510 Box light down and left */ + {0xc0, '\\'}, /* 2514 Box light up and right */ + {0xc1, '+'}, /* 2534 Box light up and horizontal */ + {0xc2, '+'}, /* 252c Box light down and horizontal */ + {0xc3, '+'}, /* 251c Box light vertical and right */ + {0xc4, '-'}, /* 2500 Box light horizontal */ + {0xc5, '+'}, /* 253c Box light vertical and horizontal */ + {0xc6, '|'}, /* 255e Box vertical single and right double */ + {0xc7, '|'}, /* 255f Box vertical double and right single */ + {0xc8, '\\'}, /* 255a Box double up and right */ + {0xc9, '/'}, /* 2554 Box double down and right */ + {0xca, '+'}, /* 2569 Box double up and horizontal */ + {0xcb, '+'}, /* 2566 Box double down and horizontal */ + {0xcc, '+'}, /* 2560 Box double vertical and right */ + {0xcd, '='}, /* 2550 Box double horizontal */ + {0xce, '+'}, /* 256c Box double vertical and horizontal */ + {0xcf, '='}, /* 2567 Box up single and horizontal double */ + {0xd0, '+'}, /* 2568 Box up double and horizontal single */ + {0xd1, '='}, /* 2564 Box down single and horizontal double */ + {0xd2, '+'}, /* 2565 Box down double and horizontal single */ + {0xd3, '+'}, /* 2559 Box up double and right single */ + {0xd4, '+'}, /* 2558 Box up single and right double */ + {0xd5, '+'}, /* 2552 Box down single and right double */ + {0xd6, '+'}, /* 2553 Box down double and right single */ + {0xd7, '+'}, /* 256b Box vertical double and horizontal single */ + {0xd8, '+'}, /* 256a Box vertical single and horizontal double */ + {0xd9, '/'}, /* 2518 Box light up and left */ + {0xda, '/'}, /* 250c Box light down and right */ + {0xdb, '@'}, /* 2588 Full block */ + {0xdc, '@'}, /* 2584 Lower half block */ + {0xdd, '@'}, /* 258c Left half block */ + {0xde, '@'}, /* 2590 Right half block */ + {0xdf, '@'}, /* 2580 Upper half block */ + {0xe0, 'a'}, /* 03b1 Greek small letter alpha */ + {0xe1, 0xdf}, /* 00df Latin small letter sharp s */ + {0xe2, 'G'}, /* 0393 Greek capital letter gamma */ + {0xe3, 'p'}, /* 03c0 Greek small letter pi */ + {0xe4, 'S'}, /* 03a3 Greek capital letter sigma */ + {0xe5, 's'}, /* 03c3 Greek small letter sigma */ + {0xe6, 0xb5}, /* 00b5 Micro sign */ + {0xe7, 't'}, /* 03c4 Greek small letter tau */ + {0xe8, 'F'}, /* 03a6 Greek capital letter phi */ + {0xe9, 'T'}, /* 0398 Greek capital letter theta */ + {0xea, 'O'}, /* 03a9 Greek capital letter omega */ + {0xeb, 'd'}, /* 03b4 Greek small letter delta */ + {0xec, '.'}, /* 221e Infinity */ + {0xed, 'f'}, /* 03c6 Greek small letter phi */ + {0xee, 'e'}, /* 03b5 Greek small letter epsilon */ + {0xef, '^'}, /* 2229 Intersection */ + {0xf0, '='}, /* 2261 Identical to */ + {0xf1, 0xb1}, /* 00b1 Plus-minus sign */ + {0xf2, '>'}, /* 2265 Greater-than or equal to */ + {0xf3, '<'}, /* 2264 Less-than or equal to */ + {0xf4, 'f'}, /* 2320 Top half integral */ + {0xf5, 'j'}, /* 2321 Bottom half integral */ + {0xf6, 0xf7}, /* 00f7 Division sign */ + {0xf7, '='}, /* 2248 Almost equal to */ + {0xf8, 0xb0}, /* 00b0 Degree sign */ + {0xf9, 0xb7}, /* 2219 Bullet operator */ + {0xfa, 0xb7}, /* 00b7 Middle dot */ + {0xfb, '/'}, /* 221a Square root */ + {0xfc, 'n'}, /* 207f Superscript latin small letter n */ + {0xfd, 0xb2}, /* 00b2 Superscript two */ + {0xfe, '#'}, /* 25a0 Black square */ + {0xff, 0xa0}, /* 00a0 No-break space */ + {0, 0} /* 0000 [END OF TABLE] */ +}; + + +/* + * gagt_cp_to_iso() + * + * Convert a string from code page 437 into ISO 8859 Latin-1. The input and + * output buffers may be one and the same. + */ +static void gagt_cp_to_iso(const unsigned char *from_string, unsigned char *to_string) { + static int is_initialized = FALSE; + static unsigned char table[BYTE_MAX_VAL + 1]; + + int index; + unsigned char cp437, iso8859_1; + assert(from_string && to_string); + + if (!is_initialized) { + gagt_charref_t entry; + + /* + * Create a lookup entry for each code in the main table. Fill in gaps + * for 7-bit characters with their ASCII equivalent values. Any + * remaining codes not represented in the main table will map to zeroes + * in the lookup table, as static variables are initialized to zero. + */ + for (entry = GAGT_CHAR_TABLE; entry->cp437; entry++) { + cp437 = entry->cp437; + iso8859_1 = entry->iso8859_1; + +// assert(cp437 < 0x20 || (cp437 > INT8_MAX_VAL && cp437 <= BYTE_MAX_VAL)); + table[cp437] = iso8859_1; + } + for (index = 0; index <= INT8_MAX_VAL; index++) { + if (table[index] == 0) + table[index] = index; + } + + is_initialized = TRUE; + } + + for (index = 0; from_string[index] != '\0'; index++) { + cp437 = from_string[index]; + iso8859_1 = table[cp437]; + + to_string[index] = iso8859_1 ? iso8859_1 : cp437; + } + + to_string[index] = '\0'; +} + + +/* + * gagt_iso_to_cp() + * + * Convert a string from ISO 8859 Latin-1 to code page 437. The input and + * output buffers may be one and the same. + */ +static void gagt_iso_to_cp(const unsigned char *from_string, unsigned char *to_string) { + static int is_initialized = FALSE; + static unsigned char table[BYTE_MAX_VAL + 1]; + + int index; + unsigned char iso8859_1, cp437; + assert(from_string && to_string); + + if (!is_initialized) { + gagt_charref_t entry; + + /* + * Create a reverse lookup entry for each code in the main table, + * overriding all of the low table entries (that is, anything under + * 128) with their ASCII no matter what the table contained. + * + * Any codes not represented in the main table will map to zeroes in + * the reverse lookup table, since static variables are initialized to + * zero. The first 128 characters are equivalent to ASCII. Moreover, + * some ISO 8859 Latin-1 entries are faked as base ASCII; where an + * entry is already occupied, the main table entry is skipped, so the + * match, which is n:1 in the reverse direction, works in first-found + * mode. + */ + for (entry = GAGT_CHAR_TABLE; entry->iso8859_1; entry++) { + cp437 = entry->cp437; + iso8859_1 = entry->iso8859_1; + + if (table[iso8859_1] == 0) + table[iso8859_1] = cp437; + } + for (index = 0; index <= INT8_MAX_VAL; index++) + table[index] = index; + + is_initialized = TRUE; + } + + for (index = 0; from_string[index] != '\0'; index++) { + iso8859_1 = from_string[index]; + cp437 = table[iso8859_1]; + + to_string[index] = cp437 ? cp437 : iso8859_1; + } + + to_string[index] = '\0'; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port status line functions */ +/*---------------------------------------------------------------------*/ + +/* + * Buffered copy of the latest status line passed in by the interpreter. + * Buffering it means it's readily available to print for Glk libraries + * that don't support separate windows. We also need a copy of the last + * status buffer printed for non-windowing Glk libraries, for comparison. + */ +static char *gagt_status_buffer = NULL, + *gagt_status_buffer_printed = NULL; + +/* + * Indication that we are in mid-delay. The delay is silent, and can look + * kind of confusing, so to try to make it less so, we'll have the status + * window show something about it. + */ +static int gagt_inside_delay = FALSE; + + +/* + * agt_statline() + * + * This function is called from our call to print_statline(). Here we'll + * convert the string and buffer in an allocated area for later use. + */ +void agt_statline(const char *cp_string) { + assert(cp_string); + + free(gagt_status_buffer); + gagt_status_buffer = (char *)gagt_malloc(strlen(cp_string) + 1); + gagt_cp_to_iso((const unsigned char *)cp_string, (unsigned char *)gagt_status_buffer); + + gagt_debug("agt_statline", "string='%s'", cp_string); +} + + +/* + * gagt_status_update_extended() + * + * Helper for gagt_status_update() and gagt_status_in_delay(). This function + * displays the second line of any extended status display, giving a list of + * exits from the compass rose, and if in an AGT delay, a waiting indicator. + */ +static void gagt_status_update_extended() { + uint width, height; + assert(gagt_status_window); + + g_vm->glk_window_get_size(gagt_status_window, &width, &height); + if (height > 1) { + uint32 index; + int exit; + + /* Clear the second status line only. */ + g_vm->glk_window_move_cursor(gagt_status_window, 0, 1); + g_vm->glk_set_window(gagt_status_window); + g_vm->glk_set_style(style_User1); + for (index = 0; index < width; index++) + g_vm->glk_put_char(' '); + + /* + * Check bits in the compass rose, and print out exit names from + * the exitname[] array. + */ + g_vm->glk_window_move_cursor(gagt_status_window, 0, 1); + g_vm->glk_put_string(" Exits: "); + for (exit = 0; exit < (int)sizeof(exitname) / (int)sizeof(exitname[0]); exit++) { + if (compass_rose & (1 << exit)) { + g_vm->glk_put_string(exitname[exit]); + g_vm->glk_put_char(' '); + } + } + + /* If the delay flag is set, print a waiting indicator at the right. */ + if (gagt_inside_delay) { + g_vm->glk_window_move_cursor(gagt_status_window, + width - strlen("Waiting... "), 1); + g_vm->glk_put_string("Waiting... "); + } + + g_vm->glk_set_window(gagt_main_window); + } +} + + +/* + * gagt_status_update() + * + * + * This function calls print_statline() to prompt the interpreter into calling + * our agt_statline(), then if we have a status window, displays the status + * string, and calls gagt_status_update_extended() if necessary to handle the + * second status line. If we don't see a call to our agt_statline, we output + * a default status string. + */ +static void gagt_status_update() { + uint width, height; + uint32 index; + assert(gagt_status_window); + + g_vm->glk_window_get_size(gagt_status_window, &width, &height); + if (height > 0) { + g_vm->glk_window_clear(gagt_status_window); + g_vm->glk_window_move_cursor(gagt_status_window, 0, 0); + g_vm->glk_set_window(gagt_status_window); + + g_vm->glk_set_style(style_User1); + for (index = 0; index < width; index++) + g_vm->glk_put_char(' '); + g_vm->glk_window_move_cursor(gagt_status_window, 0, 0); + + /* Call print_statline() to refresh status line buffer contents. */ + print_statline(); + + /* See if we have a buffered status line available. */ + if (gagt_status_buffer) { + glui32 print_width; + + /* + * Print the basic buffered status string, truncating to the + * current status window width if necessary, then try adding a + * second line if extended status enabled. + */ + print_width = width < strlen(gagt_status_buffer) + ? width : strlen(gagt_status_buffer); + g_vm->glk_put_buffer(gagt_status_buffer, print_width); + + if (gagt_extended_status_enabled) + gagt_status_update_extended(); + } else { + /* + * We don't (yet) have a status line. Perhaps we're at the + * very start of a game. Print a standard message. + */ + g_vm->glk_put_string("Glk AGiliTy version 1.1.1.1"); + } + + g_vm->glk_set_window(gagt_main_window); + } +} + + +/* + * gagt_status_print() + * + * Print the current contents of the completed status line buffer out in the + * main window, if it has changed since the last call. This is for non- + * windowing Glk libraries. + * + * Like gagt_status_update(), this function calls print_statline() to prompt + * the interpreter into calling our agt_statline(), then if we have a new + * status line, it prints it. + */ +static void gagt_status_print() { + /* Call print_statline() to refresh status line buffer contents. */ + print_statline(); + + /* + * Do no more if there is no status line to print, or if the status + * line hasn't changed since last printed. + */ + if (!gagt_status_buffer + || (gagt_status_buffer_printed + && strcmp(gagt_status_buffer, gagt_status_buffer_printed) == 0)) + return; + + /* Set fixed width font to try to preserve status line formatting. */ + g_vm->glk_set_style(style_Preformatted); + + /* + * Bracket, and output the status line buffer. We don't need to put any + * spacing after the opening bracket or before the closing one, because + * AGiliTy puts leading/trailing spaces on its status lines. + */ + g_vm->glk_put_string("["); + g_vm->glk_put_string(gagt_status_buffer); + g_vm->glk_put_string("]\n"); + + /* Save the details of the printed status buffer. */ + free(gagt_status_buffer_printed); + gagt_status_buffer_printed = (char *)gagt_malloc(strlen(gagt_status_buffer) + 1); + strcpy(gagt_status_buffer_printed, gagt_status_buffer); +} + + +/* + * gagt_status_notify() + * + * Front end function for updating status. Either updates the status window + * or prints the status line to the main window. + * + * Functions interested in updating the status line should call either this + * function, or gagt_status_redraw(), and not print_statline(). + */ +static void gagt_status_notify() { + if (!BATCH_MODE) { + if (gagt_status_window) + gagt_status_update(); + else + gagt_status_print(); + } +} + + +/* + * gagt_status_redraw() + * + * Redraw the contents of any status window with the buffered status string. + * This function handles window sizing, and updates the interpreter with + * status_width, so may, and should, be called on resize and arrange events. + * + * Functions interested in updating the status line should call either this + * function, or gagt_status_notify(), and not print_statline(). + */ +static void gagt_status_redraw() { + if (!BATCH_MODE) { + if (gagt_status_window) { + uint width, height; + winid_t parent; + + /* + * Measure the status window, and update the interpreter's + * status_width variable. + */ + g_vm->glk_window_get_size(gagt_status_window, &width, &height); + status_width = width; + + /* + * Rearrange the status window, without changing its actual + * arrangement in any way. This is a hack to work round + * incorrect window repainting in Xglk; it forces a complete + * repaint of affected windows on Glk window resize and + * arrange events, and works in part because Xglk doesn't + * check for actual arrangement changes in any way before + * invalidating its windows. The hack should be harmless to + * Glk libraries other than Xglk, moreover, we're careful to + * activate it only on resize and arrange events. + */ + parent = g_vm->glk_window_get_parent(gagt_status_window); + g_vm->glk_window_set_arrangement(parent, + winmethod_Above | winmethod_Fixed, + height, NULL); + + gagt_status_update(); + } + } +} + + +/* + * gagt_status_in_delay() + * + * Tells status line functions whether the game is delaying, or not. This + * function updates the extended status line, if present, automatically. + */ +static void gagt_status_in_delay(int inside_delay) { + if (!BATCH_MODE) { + /* Save the new delay status flag. */ + gagt_inside_delay = inside_delay; + + /* + * Update just the second line of the status window display, if + * extended status is being displayed. + */ + if (gagt_status_window && gagt_extended_status_enabled) + gagt_status_update_extended(); + } +} + + +/* + * gagt_status_cleanup() + * + * Free memory resources allocated by status line functions. Called on game + * end. + */ +static void gagt_status_cleanup() { + free(gagt_status_buffer); + gagt_status_buffer = NULL; + + free(gagt_status_buffer_printed); + gagt_status_buffer_printed = NULL; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port color and text attribute handling */ +/*---------------------------------------------------------------------*/ + +/* + * AGT color and character attribute definitions. This is the range of + * values passed in to agt_textcolor(). + */ +enum { + AGT_BLACK = 0, + AGT_BLUE = 1, + AGT_GREEN = 2, + AGT_CYAN = 3, + AGT_RED = 4, + AGT_MAGENTA = 5, + AGT_BROWN = 6, + AGT_NORMAL = 7, + AGT_BLINKING = 8, + AGT_WHITE = 9, + AGT_FIXED_FONT = 10, + AGT_VARIABLE_FONT = 11, + AGT_EMPHASIS = -1, + AGT_DE_EMPHASIS = -2 +}; + +/* + * AGiliTy colors and text attributes seem a bit confused. Let's see if we + * can sort them out. Sadly, once we have, it's often not possible to + * render the full range in all Glk's anyway. Nevertheless... + */ +struct gagt_attrset_t { + int color; /* Text color. */ + int blink; /* Text blinking flag. */ + int fixed; /* Text fixed font flag. */ + int emphasis; /* Text emphasized flag. */ +}; + +/* + * Attributes as currently set by AGiliTy. The default values set up here + * correspond to AGT_NORMAL. + */ +static gagt_attrset_t gagt_current_attribute_set = { AGT_WHITE, FALSE, + FALSE, FALSE + }; + +/* + * An extra flag to indicate if we have coerced fixed font override. On + * some occasions, we need to ensure that we get fixed font no matter what + * the game says. + */ +static int gagt_coerced_fixed = FALSE; + +/* + * Bit masks for packing colors and attributes. Normally, I don't like + * bit-twiddling all that much, but for packing all of the above into a + * single byte, that's what we need. Stuff color into the low four bits, + * convenient since color is from 0 to 9, then use three bits for the other + * attributes. + */ +static const unsigned char GAGT_COLOR_MASK = 0x0f, + GAGT_BLINK_MASK = 1 << 4, + GAGT_FIXED_MASK = 1 << 5, + GAGT_EMPHASIS_MASK = 1 << 6; + +/* Forward declaration of message function. */ +static void gagt_standout_string(const char *message); + + +/* + * agt_textcolor() + * + * The AGiliTy porting guide defines the use of this function as: + * + * Set text color to color #c, where the colors are as follows: + * 0=Black, 1=Blue, 2=Green, 3=Cyan, + * 4=Red, 5=Magenta, 6=Brown, + * 7=Normal("White"-- which may actually be some other color) + * This should turn off blinking, bold, color, etc. and restore + * the text mode to its default appearance. + * 8=Turn on blinking. + * 9= *Just* White (not neccessarily "normal" and no need to turn off + * blinking) + * 10=Turn on fixed pitch font. + * 11=Turn off fixed pitch font + * Also used to set other text attributes: + * -1=emphasized text, used (e.g.) for room titles + * -2=end emphasized text + * + * Here we try to make sense of all this. Given an argument, we'll try to + * update our separated color and text attributes flags to reflect the + * expected text rendering. + */ +void agt_textcolor(int color) { + switch (color) { + case AGT_BLACK: + case AGT_BLUE: + case AGT_GREEN: + case AGT_CYAN: + case AGT_RED: + case AGT_MAGENTA: + case AGT_BROWN: + case AGT_WHITE: + gagt_current_attribute_set.color = color; + break; + + case AGT_NORMAL: + gagt_current_attribute_set.color = AGT_WHITE; + gagt_current_attribute_set.blink = FALSE; + gagt_current_attribute_set.fixed = FALSE; + gagt_current_attribute_set.emphasis = FALSE; + break; + + case AGT_BLINKING: + gagt_current_attribute_set.blink = TRUE; + break; + + case AGT_FIXED_FONT: + gagt_current_attribute_set.fixed = TRUE; + break; + + case AGT_VARIABLE_FONT: + gagt_current_attribute_set.fixed = FALSE; + break; + + case AGT_EMPHASIS: + gagt_current_attribute_set.emphasis = TRUE; + break; + + case AGT_DE_EMPHASIS: + gagt_current_attribute_set.emphasis = FALSE; + break; + + default: + gagt_fatal("GLK: Unknown color encountered"); + gagt_exit(); + } + + gagt_debug("agt_textcolor", "color=% d -> %d%s%s%s", + color, + gagt_current_attribute_set.color, + gagt_current_attribute_set.blink ? " blink" : "", + gagt_current_attribute_set.fixed ? " fixed" : "", + gagt_current_attribute_set.emphasis ? " bold" : ""); +} + + +/* + * gagt_coerce_fixed_font() + * + * This coerces, or relaxes, a fixed font setting. Used by box drawing, to + * ensure that we get a temporary fixed font setting for known differenti- + * ated parts of game output text. Pass in TRUE to coerce fixed font, and + * FALSE to relax it. + */ +static void gagt_coerce_fixed_font(int coerce) { + gagt_coerced_fixed = coerce; +} + + +/* + * gagt_pack_attributes() + * + * Pack a set of color and text rendering attributes into a single byte, + * and return it. This function is used so that a set of text attributes + * can be encoded into a byte array that parallels the output strings that + * we buffer from the interpreter. + */ +static unsigned char gagt_pack_attributes(const gagt_attrset_t *attribute_set, int coerced) { + unsigned char packed; + assert(attribute_set); + + /* Set the initial result to be color; these are the low bits. */ + assert((attribute_set->color & ~GAGT_COLOR_MASK) == 0); + packed = attribute_set->color; + + /* + * Now OR in the text attributes settings, taking either the value for + * fixed or the coerced fixed font. + */ + packed |= attribute_set->blink ? GAGT_BLINK_MASK : 0; + packed |= attribute_set->fixed || coerced ? GAGT_FIXED_MASK : 0; + packed |= attribute_set->emphasis ? GAGT_EMPHASIS_MASK : 0; + + return packed; +} + + +/* + * gagt_unpack_attributes() + * + * Unpack a set of packed current color and text rendering attributes from a + * single byte, and return the result of unpacking. This reconstitutes the + * text attributes that were current at the time of packing. + */ +static void gagt_unpack_attributes(unsigned char packed, gagt_attrset_t *attribute_set) { + assert(attribute_set); + + attribute_set->color = packed & GAGT_COLOR_MASK; + attribute_set->blink = (packed & GAGT_BLINK_MASK) != 0; + attribute_set->fixed = (packed & GAGT_FIXED_MASK) != 0; + attribute_set->emphasis = (packed & GAGT_EMPHASIS_MASK) != 0; +} + + +/* + * gagt_pack_current_attributes() + * + * Pack the current color and text rendering attributes into a single byte, + * and return it. + */ +static unsigned char gagt_pack_current_attributes() { + return gagt_pack_attributes(&gagt_current_attribute_set, gagt_coerced_fixed); +} + + +/* + * gagt_init_user_styles() + * + * Attempt to set up two defined styles, User1 and User2, to represent + * fixed font with AGT emphasis (rendered as Glk subheader), and fixed font + * with AGT blink (rendered as Glk emphasis), respectively. + * + * The Glk stylehints here may not actually be honored by the Glk library. + * We'll try to detect this later on. + */ +static void gagt_init_user_styles() { + /* + * Set User1 to be fixed width, bold, and not italic. Here we're sort of + * assuming that the style starts life equal to Normal. + */ + g_vm->glk_stylehint_set(wintype_TextBuffer, style_User1, + stylehint_Proportional, 0); + g_vm->glk_stylehint_set(wintype_TextBuffer, style_User1, stylehint_Weight, 1); + g_vm->glk_stylehint_set(wintype_TextBuffer, style_User1, stylehint_Oblique, 0); + + /* + * Set User2 to be fixed width, normal, and italic, with the same + * assumptions. + */ + g_vm->glk_stylehint_set(wintype_TextBuffer, style_User2, + stylehint_Proportional, 0); + g_vm->glk_stylehint_set(wintype_TextBuffer, style_User2, stylehint_Weight, 0); + g_vm->glk_stylehint_set(wintype_TextBuffer, style_User2, stylehint_Oblique, 1); +} + + +/* + * gagt_confirm_appearance() + * + * Attempt to find out if a Glk style's on screen appearance matches a given + * expectation. There's a chance (often 100% with current Xglk) that we + * can't tell, in which case we'll play safe, and say that it doesn't (our + * caller is hoping it does). + * + * That is, when we return FALSE, we mean either it's not as expected, or we + * don't know. + */ +static int gagt_confirm_appearance(glui32 style, glui32 stylehint, glui32 expected) { + uint result; + + if (g_vm->glk_style_measure(gagt_main_window, style, stylehint, &result)) { + /* + * Measurement succeeded, so return TRUE if the result matches the + * caller's expectation. + */ + if (result == expected) + return TRUE; + } + + /* No straight answer, or the style's stylehint failed to match. */ + return FALSE; +} + + +/* + * gagt_is_style_fixed() + * gagt_is_style_bold() + * gagt_is_style_oblique() + * + * Convenience functions for gagt_select_style(). A return of TRUE indicates + * that the style has this attribute; FALSE indicates either that it hasn't, + * or that it's not determinable. + */ +static int gagt_is_style_fixed(glui32 style) { + return gagt_confirm_appearance(style, stylehint_Proportional, 0); +} + +static int gagt_is_style_bold(glui32 style) { + return gagt_confirm_appearance(style, stylehint_Weight, 1); +} + +static int gagt_is_style_oblique(glui32 style) { + return gagt_confirm_appearance(style, stylehint_Oblique, 1); +} + + +/* + * gagt_select_style() + * + * Given a set of AGT text attributes, this function returns a Glk style that + * is suitable (or more accurately, the best we can come up with) for render- + * ing this set of attributes. + * + * For now, we ignore color totally, and just concentrate on the other attr- + * ibutes. This is because few, if any, games use color (no Photopia here), + * few Glk libraries, at least on Linux, allow fine grained control over text + * color, and even if you can get it, the scarcity of user-defined styles in + * Glk makes it too painful to contemplate. + */ +static glui32 gagt_select_style(gagt_attrset_t *attribute_set) { + glui32 style; + assert(attribute_set); + + /* + * Glk styles are mutually exclusive, so here we'll work here by making a + * precedence selection: AGT emphasis take precedence over AGT blinking, + * which itself takes precedence over normal text. Fortunately, few, if + * any, AGT games set both emphasis and blinking (not likely to be a + * pleasant combination). + * + * We'll try to map AGT emphasis to Glk Subheader, AGT blink to Glk + * Emphasized, and normal text to Glk Normal, with modifications to this + * for fixed width requests. + * + * First, then, see if emphasized text is requested in the attributes. + */ + if (attribute_set->emphasis) { + /* + * Consider whether something requested a fixed width font or + * disallowed a proportional one. + * + * Glk Preformatted is boring, flat, and lifeless. It often offers no + * fine grained control over emphasis, and so on. So here we try to + * find something better. However, not all Glk libraries implement + * stylehints, so we need to try to be careful to ensure that we get a + * fixed width font, no matter what else we may miss out on. + */ + if (attribute_set->fixed) { + /* + * To start off, we'll see if User1, the font we set up for fixed + * width bold, really is fixed width and bold. If it is, we'll + * use it. + * + * If it isn't, we'll check Subheader. Our Glk library probably + * isn't implementing stylehints, but if Subheader is fixed width, + * it may provide a better look than Preformatted -- certainly + * it's worth a go. + * + * If Subheader isn't fixed width, we'll take another look at User1. + * It could be that the check for bold wasn't definitive, but it + * is nevertheless bold. So check for fixed width -- if set, it's + * probably good enough to use this font, certainly no worse than + * Preformatted. + * + * If Subheader isn't guaranteed fixed width, nor is User1, we're + * cornered into Preformatted. + */ + if (gagt_is_style_fixed(style_User1) + && gagt_is_style_bold(style_User1)) + style = style_User1; + + else if (gagt_is_style_fixed(style_Subheader)) + style = style_Subheader; + + else if (gagt_is_style_fixed(style_User1)) + style = style_User1; + + else + style = style_Preformatted; + } else + /* This is the easy case, use Subheader. */ + style = style_Subheader; + } else if (attribute_set->blink) { + /* + * Again, consider whether something requested a fixed width + * font or disallowed a proportional one. + */ + if (attribute_set->fixed) { + /* + * As above, try to find something better than Preformatted, first + * trying User2, then Emphasized, then User2 again, and finally + * settling for Preformatted if neither of these two looks any + * better. + */ + if (gagt_is_style_fixed(style_User2) + && gagt_is_style_oblique(style_User2)) + style = style_User2; + + else if (gagt_is_style_fixed(style_Emphasized)) + style = style_Emphasized; + + else if (gagt_is_style_fixed(style_User2)) + style = style_User2; + + else + style = style_Preformatted; + } else + /* This is the easy case, use Emphasized. */ + style = style_Emphasized; + } else { + /* + * There's no emphasis or blinking in the attributes. In this case, + * use Preformatted for fixed width, and Normal for text that can be + * rendered proportionally. + */ + if (attribute_set->fixed) + style = style_Preformatted; + else + style = style_Normal; + } + + return style; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port output buffering functions */ +/*---------------------------------------------------------------------*/ + +/* + * Buffering game output happens at two levels. The first level is a single + * line buffer, used to catch text sent to us with agt_puts(). In parallel + * with the text strings, we keep and buffer the game text attributes, as + * handed to agt_textcolor(), that are in effect at the time the string is + * handed to us, packed for brevity. + * + * As each line is completed, by a call to agt_newline(), this single line + * buffer is transferred to a main text page buffer. The main page buffer + * has places in it where we can assign paragraph, font hints, and perhaps + * other marker information to a line. Initially unset, they're filled in + * at the point where we need to display the buffer. + */ + +/* + * Definition of font hints values. Font hints may be: + * o none, for lines not in a definite paragraph; + * o proportional, for lines that can probably be safely rendered in a + * proportional font (if the AGT game text attributes allow it) and + * where the newline may be replaced by a space; + * o proportional_newline, for lines that may be rendered using a + * proportional font, but where the newline looks like it matters; + * o proportional_newline_standout, for proportional_newline lines that + * are also standout (for spacing in display functions); + * o fixed_width, for tables and other text that looks like it is a + * candidate for fixed font output. + */ +typedef enum { + HINT_NONE, + HINT_PROPORTIONAL, + HINT_PROPORTIONAL_NEWLINE, + HINT_PROPORTIONAL_NEWLINE_STANDOUT, + HINT_FIXED_WIDTH +} gagt_font_hint_t; + +/* Magic number used to ensure a pointer points to a page buffer line. */ +static const unsigned int GAGT_LINE_MAGIC = 0x5bc14482; + +/* + * Definition of a single line buffer. This is a growable string and a + * parallel growable attributes array. The string is buffered without any + * null terminator -- not needed since we retain length. + */ +typedef struct { + unsigned char *data; /* Buffered character data. */ + unsigned char *attributes; /* Parallel character attributes, packed. */ + int allocation; /* Bytes allocated to each of the above. */ + int length; /* Amount of data actually buffered. */ +} gagt_string_t; +typedef gagt_string_t *gagt_stringref_t; + +/* + * Definition of a page buffer entry. This is a structure that holds the + * the result of a single line buffer above, plus additional areas that + * describe line text positioning, a blank line flag, a paragraph pointer + * (NULL if not in a paragraph), and a font hint. + */ +typedef struct gagt_line_s *gagt_lineref_t; +typedef struct gagt_paragraph_s *gagt_paragraphref_t; + +struct gagt_line_s { + unsigned int magic; /* Assertion check dog-tag. */ + + gagt_string_t buffer; /* Buffered line string data. */ + + int indent; /* Line indentation. */ + int outdent; /* Trailing line whitespace. */ + int real_length; /* Real line length. */ + int is_blank; /* Line blank flag. */ + int is_hyphenated; /* Line hyphenated flag. */ + + gagt_paragraphref_t paragraph; /* Paragraph containing the line. */ + gagt_font_hint_t font_hint; /* Line's font hint. */ + + gagt_lineref_t next; /* List next element. */ + gagt_lineref_t prior; /* List prior element. */ +}; + +/* + * Definition of the actual page buffer. This is a doubly-linked list of + * lines, with a tail pointer to facilitate adding entries at the end. + */ +static gagt_lineref_t gagt_page_head = NULL, + gagt_page_tail = NULL; + +/* + * Definition of the current output line; this one is appended to on + * agt_puts(), and transferred into the page buffer on agt_newline(). + */ +static gagt_string_t gagt_current_buffer = { NULL, NULL, 0, 0 }; + +/* + * gagt_string_append() + * gagt_string_transfer() + * gagt_string_free() + * + * String append, move, and allocation free for string_t buffers. + */ +static void gagt_string_append(gagt_stringref_t buffer, const char *string, + unsigned char packed_attributes) { + int length, bytes; + + /* + * Find the size we'll need from the line buffer to add this string, + * and grow buffer if necessary. + */ + length = strlen(string); + for (bytes = buffer->allocation; bytes < buffer->length + length;) + bytes = bytes == 0 ? 1 : bytes << 1; + + if (bytes > buffer->allocation) { + buffer->data = (uchar *)gagt_realloc(buffer->data, bytes); + buffer->attributes = (uchar *)gagt_realloc(buffer->attributes, bytes); + + buffer->allocation = bytes; + } + + /* Add string to the line buffer, and store packed text attributes. */ + memcpy(buffer->data + buffer->length, string, length); + memset(buffer->attributes + buffer->length, packed_attributes, length); + + buffer->length += length; +} + +static void gagt_string_transfer(gagt_stringref_t from, gagt_stringref_t to) { + *to = *from; + from->data = from->attributes = NULL; + from->allocation = from->length = 0; +} + +static void gagt_string_free(gagt_stringref_t buffer) { + free(buffer->data); + free(buffer->attributes); + buffer->data = buffer->attributes = NULL; + buffer->allocation = buffer->length = 0; +} + + +/* + * gagt_get_string_indent() + * gagt_get_string_outdent() + * gagt_get_string_real_length() + * gagt_is_string_blank() + * gagt_is_string_hyphenated() + * + * Metrics functions for string_t buffers. + */ +static int gagt_get_string_indent(const gagt_stringref_t buffer) { + int indent, index; + + indent = 0; + for (index = 0; + index < buffer->length && isspace(buffer->data[index]); + index++) + indent++; + + return indent; +} + +static int gagt_get_string_outdent(const gagt_stringref_t buffer) { + int outdent, index; + + outdent = 0; + for (index = buffer->length - 1; + index >= 0 && isspace(buffer->data[index]); index--) + outdent++; + + return outdent; +} + + +static int gagt_get_string_real_length(const gagt_stringref_t buffer) { + int indent, outdent; + + indent = gagt_get_string_indent(buffer); + outdent = gagt_get_string_outdent(buffer); + + return indent == buffer->length ? 0 : buffer->length - indent - outdent; +} + +static int gagt_is_string_blank(const gagt_stringref_t buffer) { + return gagt_get_string_indent(buffer) == buffer->length; +} + +static int gagt_is_string_hyphenated(const gagt_stringref_t buffer) { + int is_hyphenated; + + is_hyphenated = FALSE; + + if (!gagt_is_string_blank(buffer) + && gagt_get_string_real_length(buffer) > 1) { + int last; + + last = buffer->length - gagt_get_string_outdent(buffer) - 1; + + if (buffer->data[last] == '-') { + if (isalpha(buffer->data[last - 1])) + is_hyphenated = TRUE; + } + } + + return is_hyphenated; +} + + +/* + * gagt_output_delete() + * + * Delete all buffered page and line text. Free all malloc'ed buffer memory, + * and return the buffer variables to their initial values. + */ +static void gagt_output_delete() { + gagt_lineref_t line, next_line; + + for (line = gagt_page_head; line; line = next_line) { + assert(line->magic == GAGT_LINE_MAGIC); + next_line = line->next; + + gagt_string_free(&line->buffer); + + memset(line, 0, sizeof(*line)); + free(line); + } + + gagt_page_head = gagt_page_tail = NULL; + + gagt_string_free(&gagt_current_buffer); +} + + +/* + * agt_puts() + * + * Buffer the string passed in into our current single line buffer. The + * function converts to ISO 8859 Latin-1 encoding before buffering. + */ +void agt_puts(const char *cp_string) { + assert(cp_string); + + if (!BATCH_MODE) { + char *iso_string; + unsigned char packed; + int length; + + /* Update the apparent (virtual) window x position. */ + length = strlen(cp_string); + curr_x += length; + + /* + * Convert the buffer from IBM cp 437 to Glk's ISO 8859 Latin-1, and + * add string and packed text attributes to the current line buffer. + */ + iso_string = (char *)gagt_malloc(length + 1); + gagt_cp_to_iso((const uchar *)cp_string, (uchar *)iso_string); + packed = gagt_pack_current_attributes(); + gagt_string_append(&gagt_current_buffer, iso_string, packed); + + /* Add the string to any script file. */ + if (script_on) + textputs(scriptfile, iso_string); + + free(iso_string); + gagt_debug("agt_puts", "string='%s'", cp_string); + } +} + + +/* + * agt_newline() + * + * Accept a newline to the main window. Our job here is to append the + * current line buffer to the page buffer, and clear the line buffer to + * begin accepting new text. + */ +void agt_newline() { + if (!BATCH_MODE) { + gagt_lineref_t line; + + /* Update the apparent (virtual) window x position. */ + curr_x = 0; + + /* Create a new line entry for the page buffer. */ + line = (gagt_lineref_t)gagt_malloc(sizeof(*line)); + line->magic = GAGT_LINE_MAGIC; + + /* Move the line from the line buffer into the page buffer. */ + gagt_string_transfer(&gagt_current_buffer, &line->buffer); + + /* Fill in the line buffer metrics. */ + line->indent = gagt_get_string_indent(&line->buffer); + line->outdent = gagt_get_string_outdent(&line->buffer); + line->real_length = gagt_get_string_real_length(&line->buffer); + line->is_blank = gagt_is_string_blank(&line->buffer); + line->is_hyphenated = gagt_is_string_hyphenated(&line->buffer); + + /* For now, default the remaining page buffer fields for the line. */ + line->paragraph = NULL; + line->font_hint = HINT_NONE; + + /* Add to the list, creating a new list if necessary. */ + line->next = NULL; + line->prior = gagt_page_tail; + if (gagt_page_head) + gagt_page_tail->next = line; + else + gagt_page_head = line; + gagt_page_tail = line; + + /* Add a newline to any script file. */ + if (script_on) + textputs(scriptfile, "\n"); + + gagt_debug("agt_newline", ""); + } +} + + +/* + * gagt_get_first_page_line() + * gagt_get_next_page_line() + * gagt_get_prior_page_line() + * + * Iterator functions for the page buffer. These functions return the first + * line from the page buffer, the next line, or the previous line, given a + * line, respectively. They return NULL if no lines, or no more lines, are + * available. + */ +static gagt_lineref_t gagt_get_first_page_line() { + gagt_lineref_t line; + + line = gagt_page_head; + assert(!line || line->magic == GAGT_LINE_MAGIC); + return line; +} + +static gagt_lineref_t gagt_get_next_page_line(const gagt_lineref_t line) { + gagt_lineref_t next_line; + assert(line && line->magic == GAGT_LINE_MAGIC); + + next_line = line->next; + assert(!next_line || next_line->magic == GAGT_LINE_MAGIC); + return next_line; +} + +static gagt_lineref_t gagt_get_prior_page_line(const gagt_lineref_t line) { + gagt_lineref_t prior_line; + assert(line && line->magic == GAGT_LINE_MAGIC); + + prior_line = line->prior; + assert(!prior_line || prior_line->magic == GAGT_LINE_MAGIC); + return prior_line; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port paragraphing functions and data */ +/*---------------------------------------------------------------------*/ + +/* Magic number used to ensure a pointer points to a paragraph. */ +static const unsigned int GAGT_PARAGRAPH_MAGIC = 0xb9a2297b; + +/* Forward definition of special paragraph reference. */ +typedef const struct gagt_special_s *gagt_specialref_t; + +/* + * Definition of a paragraph entry. This is a structure that holds a + * pointer to the first line buffer in the paragraph. + */ +struct gagt_paragraph_s { + unsigned int magic; /* Assertion check dog-tag. */ + + gagt_lineref_t first_line; /* First line in the paragraph. */ + gagt_specialref_t special; /* Special paragraph entry. */ + + int line_count; /* Number of lines in the paragraph. */ + int id; /* Paragraph id, sequence, for debug only. */ + + gagt_paragraphref_t next; /* List next element. */ + gagt_paragraphref_t prior; /* List prior element. */ +}; + +/* + * A doubly-linked list of paragraphs, with a tail pointer to facilitate + * adding entries at the end. + */ +static gagt_paragraphref_t gagt_paragraphs_head = NULL, + gagt_paragraphs_tail = NULL; + +/* + * gagt_paragraphs_delete() + * + * Delete paragraphs held in the list. This function doesn't delete the + * page buffer lines, just the paragraphs describing the page. + */ +static void gagt_paragraphs_delete() { + gagt_paragraphref_t paragraph, next_paragraph; + + for (paragraph = gagt_paragraphs_head; paragraph; paragraph = next_paragraph) { + assert(paragraph->magic == GAGT_PARAGRAPH_MAGIC); + next_paragraph = paragraph->next; + + memset(paragraph, 0, sizeof(*paragraph)); + free(paragraph); + } + + gagt_paragraphs_head = gagt_paragraphs_tail = NULL; +} + + +/* + * gagt_find_paragraph_start() + * + * Find and return the next non-blank line in the page buffer, given a start + * point. Returns NULL if there are no more blank lines. + */ +static gagt_lineref_t gagt_find_paragraph_start(const gagt_lineref_t begin) { + gagt_lineref_t line, match; + + /* + * Advance line to the beginning of the next paragraph, stopping on the + * first non-blank line, or at the end of the page buffer. + */ + match = NULL; + for (line = begin; line; line = gagt_get_next_page_line(line)) { + if (!line->is_blank) { + match = line; + break; + } + } + + return match; +} + + +/* + * gagt_find_block_end() + * gagt_find_blank_line_block_end() + * + * Find and return the apparent end of a paragraph from the page buffer, + * given a start point, and an indentation reference. The end is either + * the point where indentation returns to the reference indentation, or + * the next blank line. + * + * Indentation reference can be -1, indicating that only the next blank + * line will end the paragraph. Indentation references less than 1 are + * also ignored. + */ +static gagt_lineref_t gagt_find_block_end(const gagt_lineref_t begin, int indent) { + gagt_lineref_t line, match; + + /* + * Initialize the match to be the start of the block, then advance line + * until we hit a blank line or the end of the page buffer. At this point, + * match contains the last line checked. + */ + match = begin; + for (line = begin; line; line = gagt_get_next_page_line(line)) { + /* + * Found if we reach a blank line, or when given an indentation to + * check for, we find it. + */ + if (line->is_blank || (indent > 0 && line->indent == indent)) + break; + + match = line; + } + + return match; +} + +static gagt_lineref_t gagt_find_blank_line_block_end(const gagt_lineref_t begin) { + return gagt_find_block_end(begin, -1); +} + + +/* + * gagt_find_paragraph_end() + * + * Find and return the apparent end of a paragraph from the page buffer, + * given a start point. The function attempts to recognize paragraphs by + * the "shape" of indentation. + */ +static gagt_lineref_t gagt_find_paragraph_end(const gagt_lineref_t first_line) { + gagt_lineref_t second_line; + + /* + * If the start line is the last line in the buffer, or if the next line + * is a blank line, return the start line as also being the end of the + * paragraph. + */ + second_line = gagt_get_next_page_line(first_line); + if (!second_line || second_line->is_blank) { + return first_line; + } + + /* + * Time to look at line indentations... + * + * If either line is grossly indented, forget about trying to infer + * anything from this, and just break the paragraph on the next blank line. + */ + if (first_line->indent > screen_width / 4 + || second_line->indent > screen_width / 4) { + return gagt_find_blank_line_block_end(second_line); + } + + /* + * If the first line is indented more than the second, end the paragraph + * on a blank line, or on a return in indentation to the level of the + * first line. Here we're looking for paragraphs with the shape + * + * aksjdj jfkasjd fjkasjd ajksdj fkaj djf akjsd fkjas dfs + * kasjdlkfjkj fj aksd jfjkasj dlkfja skjdk flaks dlf jalksdf + * ksjdf kjs kdf lasjd fkjalks jdfkjasjd flkjasl djfkasjfdkl + */ + else if (first_line->indent > second_line->indent) { + return gagt_find_block_end(second_line, first_line->indent); + } + + /* + * If the second line is more indented than the first, this may indicate + * a title line, followed by normal indented paragraphing. In this case, + * use the second line indentation as the reference, and begin searching + * at the next line. This finds + * + * ksjdkfjask ksadf + * kajskd fksjkfj jfkj jfkslaj fksjlfj jkjskjlfa j fjksal + * sjkkdjf sj fkjkajkdlfj lsjak dfjk djkfjskl dklf alks dfll + * fjksja jkj dksja kjdk kaj dskfj aksjdf aksjd kfjaks fjks + * + * and + * + * asdfj kjsdf kjs + * akjsdkj fkjs kdjfa lskjdl fjalsj dlfjksj kdj fjkd jlsjd + * jalksj jfk slj lkfjsa lkjd lfjlaks dlfkjals djkj alsjd + * kj jfksj fjksjl alkjs dlkjf lakjsd fkjas ldkj flkja fsd + */ + else if (second_line->indent > first_line->indent) { + gagt_lineref_t third_line; + + /* + * See if we have a third buffer line to look at. If we don't, or if + * we do but it's blank, the paragraph ends here. + */ + third_line = gagt_get_next_page_line(second_line); + if (!third_line || third_line->is_blank) { + return second_line; + } + + /* As above, give up on gross indentation. */ + if (second_line->indent > screen_width / 4 + || third_line->indent > screen_width / 4) { + return gagt_find_blank_line_block_end(third_line); + } + + /* + * If the second line indentation exceeds the third, this is probably + * a paragraph with a title line. In this case, end the paragraph on + * a return to the indentation of the second line. If not, just find + * the next blank line. + */ + else if (second_line->indent > third_line->indent) { + return gagt_find_block_end(third_line, second_line->indent); + } else { + return gagt_find_blank_line_block_end(third_line); + } + } + + /* + * Otherwise, the first and second line indentations are the same, so + * break only on the next empty line. This finds the simple + * + * ksd kjal jdljf lakjsd lkj lakjsdl jfla jsldj lfaksdj fksj + * lskjd fja kjsdlk fjlakjs ldkjfksj lkjdf kjalskjd fkjklal + * skjd fkaj djfkjs dkfjal sjdlkfj alksjdf lkajs ldkjf alljjf + */ + else { + assert(second_line->indent == first_line->indent); + return gagt_find_blank_line_block_end(second_line); + } +} + + +/* + * gagt_paragraph_page() + * + * This function breaks the page buffer into what appear to be paragraphs, + * based on observations of indentation and blank separator lines. + */ +static void gagt_paragraph_page() { + gagt_lineref_t start; + + assert(!gagt_paragraphs_head && !gagt_paragraphs_tail); + + /* Find the start of the first paragraph. */ + start = gagt_find_paragraph_start(gagt_get_first_page_line()); + while (start) { + gagt_paragraphref_t paragraph; + gagt_lineref_t end, line; + + /* Create a new paragraph entry. */ + paragraph = (gagt_paragraphref_t)gagt_malloc(sizeof(*paragraph)); + paragraph->magic = GAGT_PARAGRAPH_MAGIC; + paragraph->first_line = start; + paragraph->special = NULL; + paragraph->line_count = 1; + paragraph->id = gagt_paragraphs_tail ? gagt_paragraphs_tail->id + 1 : 0; + + /* Add to the list, creating a new list if necessary. */ + paragraph->next = NULL; + paragraph->prior = gagt_paragraphs_tail; + if (gagt_paragraphs_head) + gagt_paragraphs_tail->next = paragraph; + else + gagt_paragraphs_head = paragraph; + gagt_paragraphs_tail = paragraph; + + /* From the start, identify the paragraph end. */ + end = gagt_find_paragraph_end(start); + + /* + * Set paragraph in each line identified as part of this paragraph, + * and increment the paragraph's line count. + */ + for (line = start; + line != end; line = gagt_get_next_page_line(line)) { + line->paragraph = paragraph; + paragraph->line_count++; + } + end->paragraph = paragraph; + + /* + * If there's another line, look for the next paragraph there, + * otherwise we're done. + */ + line = gagt_get_next_page_line(end); + if (line) + start = gagt_find_paragraph_start(line); + else + start = NULL; + } +} + + +/* + * gagt_get_first_paragraph() + * gagt_get_next_paragraph() + * + * Iterator functions for the paragraphs list. + */ +static gagt_paragraphref_t gagt_get_first_paragraph() { + gagt_paragraphref_t paragraph; + + paragraph = gagt_paragraphs_head; + assert(!paragraph || paragraph->magic == GAGT_PARAGRAPH_MAGIC); + return paragraph; +} + +static gagt_paragraphref_t gagt_get_next_paragraph(const gagt_paragraphref_t paragraph) { + gagt_paragraphref_t next_paragraph; + assert(paragraph && paragraph->magic == GAGT_PARAGRAPH_MAGIC); + + next_paragraph = paragraph->next; + assert(!next_paragraph || next_paragraph->magic == GAGT_PARAGRAPH_MAGIC); + return next_paragraph; +} + + +/* + * gagt_get_first_paragraph_line() + * gagt_get_next_paragraph_line() + * gagt_get_prior_paragraph_line() + * + * Iterator functions for the page buffer. These functions implement a + * paragraph-based view of the page buffer. + * + * The functions find the first line of a given paragraph; given a line, + * the next line in the same paragraph, or NULL if line is the last para- + * graph line (or the last line in the page buffer); and given a line, + * the previous line in the same paragraph, or NULL if line is the first + * paragraph line (or the first line in the page buffer). + */ +static gagt_lineref_t gagt_get_first_paragraph_line(const gagt_paragraphref_t paragraph) { + assert(paragraph && paragraph->magic == GAGT_PARAGRAPH_MAGIC); + + /* Return the first line for the requested paragraph. */ + return paragraph->first_line; +} + +static gagt_lineref_t gagt_get_next_paragraph_line(const gagt_lineref_t line) { + gagt_lineref_t next_line; + + /* Get the next line; return it if the paragraph matches, else NULL. */ + next_line = gagt_get_next_page_line(line); + if (next_line && next_line->paragraph == line->paragraph) + return next_line; + else + return NULL; +} + +static gagt_lineref_t gagt_get_prior_paragraph_line(const gagt_lineref_t line) { + gagt_lineref_t prior_line; + + /* Get the previous line; return it if the paragraph matches, else NULL. */ + prior_line = gagt_get_prior_page_line(line); + if (prior_line && prior_line->paragraph == line->paragraph) + return prior_line; + else + return NULL; +} + + +/* + * gagt_get_paragraph_line_count() + * + * Return the count of lines contained in the paragraph. + */ +static int gagt_get_paragraph_line_count(const gagt_paragraphref_t paragraph) { + assert(paragraph && paragraph->magic == GAGT_PARAGRAPH_MAGIC); + + return paragraph->line_count; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port page buffer analysis functions */ +/*---------------------------------------------------------------------*/ + +/* + * Threshold for consecutive punctuation/spaces before we decide that a line + * is in fact part of a table, and a small selection of characters to apply + * a somewhat larger threshold to when looking for punctuation (typically, + * characters that appear together multiple times in non-table text). + */ +static const int GAGT_THRESHOLD = 4, + GAGT_COMMON_THRESHOLD = 8; +static const char *const GAGT_COMMON_PUNCTUATION = ".!?"; + + +/* + * gagt_line_is_standout() + * + * Return TRUE if a page buffer line appears to contain "standout" text. + * This is one of: + * - a line where all characters have some form of AGT text attribute + * set (blinking, fixed width font, or emphasis), + * - a line where each alphabetical character is uppercase. + * Typically, this describes room and other miscellaneous header lines. + */ +static int gagt_line_is_standout(const gagt_lineref_t line) { + int index, all_formatted, upper_count, lower_count; + + /* + * Look at the line, for cases where all characters in it have AGT font + * attributes, and counting the upper and lower case characters. Iterate + * over only the significant characters in the string. + */ + all_formatted = TRUE; + upper_count = lower_count = 0; + for (index = line->indent; + index < line->buffer.length - line->outdent; index++) { + gagt_attrset_t attribute_set; + unsigned char character; + + gagt_unpack_attributes(line->buffer.attributes[index], &attribute_set); + character = line->buffer.data[index]; + + /* + * If no AGT attribute is set for this character, then not all of the + * line is standout text. In this case, reset the all_formatted flag. + */ + if (!(attribute_set.blink + || attribute_set.fixed || attribute_set.emphasis)) + all_formatted = FALSE; + + /* Count upper and lower case characters. */ + if (islower(character)) + lower_count++; + else if (isupper(character)) + upper_count++; + } + + /* + * Consider standout if every character was formatted, or if the string + * is all uppercase. + */ + return all_formatted || (upper_count > 0 && lower_count == 0); +} + + +/* + * gagt_set_font_hint_proportional() + * gagt_set_font_hint_proportional_newline() + * gagt_set_font_hint_fixed_width() + * + * Helpers for assigning font hints. Font hints have strengths, and these + * functions ensure that gagt_assign_paragraph_font_hints() only increases + * strengths, and doesn't need to worry about checking before setting. In + * the case of newline, the function also adds standout to the font hint if + * appropriate. + */ +static void gagt_set_font_hint_proportional(gagt_lineref_t line) { + /* The only weaker hint than proportional is none. */ + if (line->font_hint == HINT_NONE) + line->font_hint = HINT_PROPORTIONAL; +} + +static void gagt_set_font_hint_proportional_newline(gagt_lineref_t line) { + /* + * Proportional and none are weaker than newline. Because of the way we + * set font hints, this function can't be called with a current line hint + * of proportional newline. + */ + if (line->font_hint == HINT_NONE || line->font_hint == HINT_PROPORTIONAL) { + if (gagt_line_is_standout(line)) + line->font_hint = HINT_PROPORTIONAL_NEWLINE_STANDOUT; + else + line->font_hint = HINT_PROPORTIONAL_NEWLINE; + } +} + +static void gagt_set_font_hint_fixed_width(gagt_lineref_t line) { + /* Fixed width font is the strongest hint. */ + if (line->font_hint == HINT_NONE + || line->font_hint == HINT_PROPORTIONAL + || line->font_hint == HINT_PROPORTIONAL_NEWLINE + || line->font_hint == HINT_PROPORTIONAL_NEWLINE_STANDOUT) + line->font_hint = HINT_FIXED_WIDTH; +} + + +/* + * gagt_assign_paragraph_font_hints() + * + * For a given paragraph in the page buffer, this function looks at the text + * style used, and assigns a font hint value to each line. Font hints + * indicate whether the line probably requires fixed width font, or may be + * okay in variable width, and for lines that look like they might be okay + * in variable width, whether the newline should probably be rendered at the + * end of the line, or if it might be omitted. + */ +static void gagt_assign_paragraph_font_hints(const gagt_paragraphref_t paragraph) { + static int is_initialized = FALSE; + static int threshold[BYTE_MAX_VAL + 1]; + + gagt_lineref_t line, first_line; + int is_table, in_list; + assert(paragraph); + + /* On first call, set up the table on punctuation run thresholds. */ + if (!is_initialized) { + int character; + + for (character = 0; character <= BYTE_MAX_VAL; character++) { + /* + * Set the threshold, either a normal value, or a larger one for + * punctuation characters that tend to have consecutive runs in + * non-table text. + */ + if (ispunct(character)) { + threshold[character] = strchr(GAGT_COMMON_PUNCTUATION, character) + ? GAGT_COMMON_THRESHOLD : GAGT_THRESHOLD; + } + } + + is_initialized = TRUE; + } + + /* + * Note the first paragraph line. This value is commonly used, and under + * certain circumstances, it's also modified later on. + */ + first_line = gagt_get_first_paragraph_line(paragraph); + assert(first_line); + + /* + * Phase 1 -- look for pages that consist of just one paragraph, + * itself consisting of only one line. + * + * There is no point in attempting alignment of text in a one paragraph, + * one line page. This would be, for example, an error message from the + * interpreter parser. In this case, set the line for proportional with + * newline, and return immediately. + */ + if (gagt_get_first_paragraph() == paragraph + && !gagt_get_next_paragraph(paragraph) + && !gagt_get_next_paragraph_line(first_line)) { + /* + * Set the first paragraph line for proportional with a newline, and + * return. + */ + gagt_set_font_hint_proportional_newline(first_line); + return; + } + + /* + * Phase 2 -- try to identify paragraphs that are tables, based on + * looking for runs of punctuation. + * + * Search for any string that has a run of apparent line drawing or other + * formatting characters in it. If we find one, we'll consider the + * paragraph to be a "table", that is, it has some quality that we might + * destroy if we used a proportional font. + */ + is_table = FALSE; + for (line = first_line; + line && !is_table; line = gagt_get_next_paragraph_line(line)) { + int index, counts[BYTE_MAX_VAL + 1], total_counts; + + /* + * Clear the initial counts. Using memset() here is an order of + * magnitude or two faster than a for-loop. Also there's a total count + * to detect when counts needs to be recleared, or is already clear. + */ + memset(counts, 0, sizeof(counts)); + total_counts = 0; + + /* + * Count consecutive punctuation in the line, excluding the indentation + * and outdent. + */ + for (index = line->indent; + index < line->buffer.length - line->outdent && !is_table; index++) { + int character; + character = line->buffer.data[index]; + + /* Test this character for punctuation. */ + if (ispunct(character)) { + /* + * Increment the count for this character, and note that + * counts are no longer empty, then compare against threshold. + */ + counts[character]++; + total_counts++; + + is_table = (counts[character] >= threshold[character]); + } else { + /* + * Re-clear all counts, again with memset() for speed, but only + * if they need clearing. As they often won't, this optimization + * saves quite a bit of work. + */ + if (total_counts > 0) { + memset(counts, 0, sizeof(counts)); + total_counts = 0; + } + } + } + } + + /* + * Phase 3 -- try again to identify paragraphs that are tables, based + * this time on looking for runs of whitespace. + * + * If no evidence found so far, look again, this time searching for any + * run of four or more spaces on the line (excluding any lead-in or + * trailing spaces). + */ + if (!is_table) { + for (line = first_line; + line && !is_table; line = gagt_get_next_paragraph_line(line)) { + int index, count; + + /* + * Count consecutive spaces in the line, excluding the indentation + * and outdent. + */ + count = 0; + for (index = line->indent; + index < line->buffer.length - line->outdent && !is_table; + index++) { + int character; + character = line->buffer.data[index]; + + if (isspace(character)) { + count++; + is_table = (count >= GAGT_THRESHOLD); + } else + count = 0; + } + } + } + + /* + * If the paragraph appears to be a table, and if it consists of more than + * just a single line, mark all lines as fixed font output and return. + */ + if (is_table && gagt_get_next_paragraph_line(first_line)) { + for (line = first_line; + line; line = gagt_get_next_paragraph_line(line)) { + gagt_set_font_hint_fixed_width(line); + } + + /* Nothing more to do. */ + return; + } + + /* + * Phase 4 -- consider separating the first line from the rest of + * the paragraph. + * + * Not a table, so the choice is between proportional rendering with a + * newline, and proportional rendering without... + * + * If the first paragraph line is standout or short, render it pro- + * portionally with a newline, and don't consider it as a further part of + * the paragraph. + */ + if (gagt_line_is_standout(first_line) + || first_line->real_length < screen_width / 2) { + /* Set the first paragraph line for a newline. */ + gagt_set_font_hint_proportional_newline(first_line); + + /* + * Disassociate this line from the rest of the paragraph by moving on + * the value of the first_line variable. If it turns out that there + * is no next paragraph line, then we have a one-line paragraph, and + * there's no more to do. + */ + first_line = gagt_get_next_paragraph_line(first_line); + if (!first_line) + return; + } + + /* + * Phase 5 -- try to identify lists by a simple initial look at line + * indentations. + * + * Look through the paragraph for apparent lists, and decide for each + * line whether it's appropriate to output a newline, and render + * proportionally, or just render proportionally. + * + * After this loop, each line will have some form of font hint assigned + * to it. + */ + in_list = FALSE; + for (line = first_line; + line; line = gagt_get_next_paragraph_line(line)) { + gagt_lineref_t next_line; + + next_line = gagt_get_next_paragraph_line(line); + + /* + * Special last-iteration processing. The newline is always output at + * the end of a paragraph, so if there isn't a next line, then this + * line is the last paragraph line. Set its font hint appropriately, + * and do no more for the line. + */ + if (!next_line) { + gagt_set_font_hint_proportional_newline(line); + continue; + } + + /* + * If the next line's indentation is deeper that that of the first + * line, this paragraph looks like it is trying to be some form of a + * list. In this case, make newline significant for the current line, + * and set the in_list flag so we can delay the return to proportional + * by one line. On return to first line indentation, make newline + * significant for the return line. + */ + if (next_line->indent > first_line->indent) { + gagt_set_font_hint_proportional_newline(line); + in_list = TRUE; + } else { + if (in_list) + gagt_set_font_hint_proportional_newline(line); + else + gagt_set_font_hint_proportional(line); + in_list = FALSE; + } + } + + /* + * Phase 6 -- look again for lines that look like they are supposed + * to stand out from their neighbors. + * + * Now rescan the paragraph, looking this time for lines that stand out + * from their neighbours. Make newline significant for each such line, + * and the line above, if there is one. + * + * Here we split the loop on lines so that we avoid looking at the prior + * line of the current first line -- because of "adjustments", it may not + * be the real paragraph first line. + * + * So, deal with the current first line... + */ + if (gagt_line_is_standout(first_line)) { + /* Make newline significant for this line. */ + gagt_set_font_hint_proportional_newline(first_line); + } + + /* ... then deal with the rest of the lines, looking for standouts. */ + for (line = gagt_get_next_paragraph_line(first_line); + line; line = gagt_get_next_paragraph_line(line)) { + if (gagt_line_is_standout(line)) { + gagt_lineref_t prior_line; + + /* Make newline significant for this line. */ + gagt_set_font_hint_proportional_newline(line); + + /* + * Make newline significant for the line above. There will always + * be one because we start the loop past the first line. + */ + prior_line = gagt_get_prior_paragraph_line(line); + gagt_set_font_hint_proportional_newline(prior_line); + } + } + + /* + * Phase 7 -- special case short lines at the paragraph start. + * + * Make a special case of lines that begin a paragraph, and are short and + * followed by a much longer line. This should catch games which output + * room titles above descriptions without using AGT fonts/bold/whatever. + * Without this trap, room titles and their descriptions are run together. + * This is more programmatic guesswork than heuristics. + */ + if (gagt_get_next_paragraph_line(first_line)) { + gagt_lineref_t next_line; + + next_line = gagt_get_next_paragraph_line(first_line); + + /* + * See if the first line is less than half width, and the second line + * is more than three quarters width. If it is, set newline as + * significant for the first paragraph line. + */ + if (first_line->real_length < screen_width / 2 + && next_line->real_length > screen_width * 3 / 4) { + gagt_set_font_hint_proportional_newline(first_line); + } + } + + /* + * Phase 8 -- special case paragraphs of only short lines. + * + * Make a special case out of paragraphs where all lines are short. This + * catches elements like indented addresses. + */ + if (gagt_get_next_paragraph_line(first_line)) { + int all_short; + + all_short = TRUE; + for (line = first_line; + line; line = gagt_get_next_paragraph_line(line)) { + /* Clear flag if this line isn't 'short'. */ + if (line->real_length >= screen_width / 2) { + all_short = FALSE; + break; + } + } + + /* + * If all lines were short, mark the complete paragraph as having + * significant newlines. + */ + if (all_short) { + for (line = first_line; + line; line = gagt_get_next_paragraph_line(line)) { + gagt_set_font_hint_proportional_newline(line); + } + } + } +} + + +/* + * gagt_assign_font_hints() + * + * + * Sets a font hint for each line of each page buffer paragraph that is not + * a special paragraph. + */ +static void gagt_assign_font_hints() { + gagt_paragraphref_t paragraph; + + for (paragraph = gagt_get_first_paragraph(); + paragraph; paragraph = gagt_get_next_paragraph(paragraph)) { + if (!paragraph->special) + gagt_assign_paragraph_font_hints(paragraph); + } +} + + +/*---------------------------------------------------------------------*/ +/* Glk port special paragraph functions and data */ +/*---------------------------------------------------------------------*/ + +/* + * It's helpful to handle some AGiliTy interpreter output specially, to im- + * prove the look of the text where Glk fonts and styles are available. We + * build a table of paragraphs the interpreter can come out with, and the + * replacement text we'll use when we see this paragraph. Note that matches + * are made after factoring out indentation, and replacement lines do not + * automatically print with a newline. All clear, then? Here's the table + * entry definition. + */ +enum { GAGT_SPECIAL_MATCH_MAX = 5 }; + +typedef const struct gagt_special_s { + const int line_count; + const char *const compare[GAGT_SPECIAL_MATCH_MAX + 1]; + const char *const replace; +} gagt_special_t; + +/* + * Table of special AGiliTy interpreter strings and paragraphs -- where one + * appears in game output, we'll print out its replacement instead. Be + * warned; these strings are VERY specific to AGiliTy 1.1.1.1, and are extre- + * mely likely to change with any future interpreter releases. They also + * omit initializers with abandon, expecting the compiler to default these + * to NULL/zero. Replacement strings embed style encoding as |x, where x is + * E(mphasized), S(ubheader), or N(ormal) for convenience. + */ +static gagt_special_t GAGT_SPECIALS[] = { + + /* Initial screen AGT game type line. */ + { + 1, + {"[Created with Malmberg and Welch's Adventure Game Toolkit]"}, + "|ECreated with Malmberg and Welch's Adventure Game Toolkit|N\n" + }, + + /* Normal version of initial interpreter information block. */ + { + 4, + { + "This game is being executed by", + "AGiliTy: The (Mostly) Universal AGT Interpreter version 1.1.1.1", + "Copyright (C) 1996-99,2001 by Robert Masenten", + "Glk version" + }, + "This game is being executed by:\n\n" + " |SAGiliTy, The (Mostly) Universal AGT Interpreter, Version 1.1.1.1|N\n" + " |ECopyright (C) 1996-1999,2001 by Robert Masenten|N\n" + " |EGlk version|N\n" + }, + + /* AGiliTy "information" screen header block. */ + { + 5, + { + "AGiliTy", + "The (Mostly) Universal AGT Interpreter, version 1.1.1.1", + "Copyright (C) 1996-1999,2001 by Robert Masenten", + "[Glk version]", + "-----------------------------------------------------------" + }, + "|SAGiliTy, The (Mostly) Universal AGT Interpreter, Version 1.1.1.1|N\n" + "|ECopyright (C) 1996-1999,2001 by Robert Masenten|N\n" + "|EGlk version|N\n" + }, + + /* "HIT ANY KEY" message, usually displayed after a game's introduction. */ + { + 1, + {"--- HIT ANY KEY ---"}, + "|E[Press any key...]|N" + }, + + /* Alternative, shrunken version of initial interpreter information block. */ + { + 2, + { + "Being run by AGiliTy version 1.1.1.1, Copyright (C) 1996-99,2001" + " Robert Masenten", + "Glk version" + }, + "This game is being executed by:\n\n" + " |SAGiliTy, The (Mostly) Universal AGT Interpreter, Version 1.1.1.1|N\n" + " |ECopyright (C) 1996-1999,2001 by Robert Masenten|N\n" + " |EGlk version|N\n" + }, + + /* Alternative, minimal version of initial interpreter information block. */ + { + 1, + { + "Being run by AGiliTy version 1.1.1.1, Copyright (C) 1996-99,2001" + " Robert Masenten" + }, + "This game is being executed by:\n\n" + " |SAGiliTy, The (Mostly) Universal AGT Interpreter, Version 1.1.1.1|N\n" + " |ECopyright (C) 1996-1999,2001 by Robert Masenten|N\n" + " |EGlk version|N\n" + }, + + /* Lengthy version of the "Created with..." message. */ + { + 2, + { + "This game was created with Malmberg and Welch's Adventure Game Toolkit;" + " it is", + "being executed by" + }, + "|ECreated with Malmberg and Welch's Adventure Game Toolkit|N\n" + }, + + /* Three-line version of initial interpreter information block. */ + { + 3, + { + "AGiliTy: The (Mostly) Universal AGT Interpreter version 1.1.1.1", + "Copyright (C) 1996-99,2001 by Robert Masenten", + "Glk version" + }, + "This game is being executed by:\n\n" + " |SAGiliTy, The (Mostly) Universal AGT Interpreter, Version 1.1.1.1|N\n" + " |ECopyright (C) 1996-1999,2001 by Robert Masenten|N\n" + " |EGlk version|N\n" + }, + + /* + * Assorted special verb output messages, with the extra icky quality that + * we have to spot messages that wrap because we forced screen_width to 80. + */ + { + 2, + { + "[Now in BRIEF mode (room descriptions will only be printed" + " when they are entered", + "the first time)]" + }, + "|E[Now in BRIEF mode: Room descriptions will only be printed" + " when rooms are entered for the first time.]|N\n" + }, + + { + 2, + { + "[Now in VERBOSE mode (room descriptions will be printed" + " every time you enter a", + "room)]" + }, + "|E[Now in VERBOSE mode: Room descriptions will be printed" + " every time you enter a room.]|N\n" + }, + + { + 1, + {"[LISTEXIT mode on: room exits will be listed.]"}, + "|E[LISTEXIT mode on: Room exits will be listed.]|N\n" + }, + + { + 1, + {"[LISTEXIT mode off: room exits will not be listed.]"}, + "|E[LISTEXIT mode off: Room exits will not be listed.]|N\n" + }, + + /* End of table sentinel entry. Do not delete. */ + {0, {NULL}, NULL} +}; + + +/* + * gagt_compare_special_line() + * gagt_compare_special_paragraph() + * + * Helpers for gagt_find_equivalent_special(). Compare line data case- + * insensitively, taking care to use lengths rather than relying on line + * buffer data being NUL terminated (which it's not); and iterate a complete + * special paragraph comparison. + */ +static int gagt_compare_special_line(const char *compare, const gagt_lineref_t line) { + /* + * Return true if the lengths match, and the real line data (excluding + * indent and outdent) also matches, ignoring case. + */ + return (int)strlen(compare) == line->real_length + && gagt_strncasecmp(compare, + (const char *)line->buffer.data + line->indent, + line->real_length) == 0; +} + +static int gagt_compare_special_paragraph(const gagt_specialref_t special, + const gagt_paragraphref_t paragraph) { + /* If the line counts match, compare line by line. */ + if (special->line_count == gagt_get_paragraph_line_count(paragraph)) { + gagt_lineref_t line; + int index, is_match; + + is_match = TRUE; + for (index = 0, line = gagt_get_first_paragraph_line(paragraph); + index < special->line_count && line; + index++, line = gagt_get_next_paragraph_line(line)) { + if (!gagt_compare_special_line(special->compare[index], line)) { + is_match = FALSE; + break; + } + } + + return is_match; + } + + /* Line count mismatch; return FALSE. */ + return FALSE; +} + + +/* + * gagt_find_equivalent_special() + * + * Given a paragraph, see if it matches any of the special ones set up in + * our array. Returns the special, or NULL if no match. + */ +static gagt_specialref_t gagt_find_equivalent_special(gagt_paragraphref_t paragraph) { + gagt_specialref_t special, match; + + /* Check each special paragraph entry for a match against this paragraph. */ + match = NULL; + for (special = GAGT_SPECIALS; special->replace; special++) { + if (gagt_compare_special_paragraph(special, paragraph)) { + match = special; + break; + } + } + + return match; +} + + +/* + * gagt_mark_specials() + * + * Search for and mark any lines that match special paragraphs. + */ +static void gagt_mark_specials() { + static int is_verified = FALSE; + + /* + * Verify special paragraphs table contents. This checks that each entry + * ends with a NULL comparison, has a replacement, and that the line count + * matches. + */ + if (!is_verified) { + gagt_specialref_t special; + + for (special = GAGT_SPECIALS; special->replace; special++) { + int line_count, index; + + line_count = 0; + for (index = 0; special->compare[index]; index++) + line_count++; + + assert(special->line_count == line_count); + assert(special->replace); + assert(!special->compare[GAGT_SPECIAL_MATCH_MAX]); + } + + is_verified = TRUE; + } + + /* + * Search all paragraphs for special matches, if enabled. When a special + * match is found, mark the paragraph with a pointer to the matching entry. + */ + if (gagt_replacement_enabled) { + gagt_paragraphref_t paragraph; + + for (paragraph = gagt_get_first_paragraph(); + paragraph; paragraph = gagt_get_next_paragraph(paragraph)) { + paragraph->special = gagt_find_equivalent_special(paragraph); + } + } +} + + +/* + * gagt_display_special() + * + * Display the replacement text for the specified special table entry. The + * current Glk style in force is passed in; we return the Glk style in force + * after we've done. + */ +static glui32 gagt_display_special(const gagt_specialref_t special, glui32 current_style) { + glui32 set_style; + int index, marker, length; + const char *string; + assert(special); + + /* Extract replacement string and length. */ + string = special->replace; + assert(string); + length = strlen(string); + + set_style = current_style; + + /* + * Iterate each character in replacement string, looking for style escapes, + * and flushing delayed output when one is found. + */ + marker = 0; + for (index = 0; index < length; index++) { + if (string[index] == '|') { + glui32 style; + + /* Flush delayed output accumulated so far, excluding escape. */ + g_vm->glk_put_buffer(string + marker, index - marker); + marker = index + 2; + + /* Determine any new text style. */ + style = set_style; + switch (string[++index]) { + case 'E': + style = style_Emphasized; + break; + + case 'S': + style = style_Subheader; + break; + + case 'N': + style = style_Normal; + break; + + default: + gagt_fatal("GLK: Invalid replacement style escape"); + gagt_exit(); + } + + /* If style changed, update Glk's style setting. */ + if (style != set_style) { + g_vm->glk_set_style(style); + set_style = style; + } + } + } + + /* Output any remaining delayed characters. */ + if (marker < length) + g_vm->glk_put_buffer(string + marker, length - marker); + + return set_style; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port output functions */ +/*---------------------------------------------------------------------*/ + +/* + * Flag for if the user entered "help" as their last input, or if hints have + * been silenced as a result of already using a Glk command. + */ +static int gagt_help_requested = FALSE, + gagt_help_hints_silenced = FALSE; + +/* + * gagt_display_register_help_request() + * gagt_display_silence_help_hints() + * gagt_display_provide_help_hint() + * + * Register a request for help, and print a note of how to get Glk command + * help from the interpreter unless silenced. + */ +static void gagt_display_register_help_request() { + gagt_help_requested = TRUE; +} + +static void gagt_display_silence_help_hints() { + gagt_help_hints_silenced = TRUE; +} + +static glui32 gagt_display_provide_help_hint(glui32 current_style) { + if (gagt_help_requested && !gagt_help_hints_silenced) { + g_vm->glk_set_style(style_Emphasized); + g_vm->glk_put_string("[Try 'glk help' for help on special interpreter" + " commands]\n"); + + gagt_help_requested = FALSE; + return style_Emphasized; + } + + return current_style; +} + + +/* + * gagt_display_text_element() + * + * Display an element of a buffer string using matching packed attributes. + * The currently set Glk style is supplied, and the function returns the + * new currently set Glk style. + * + * The function handles a flag to coerce fixed width font. + */ +static glui32 gagt_display_text_element(const char *string, const unsigned char *attributes, + int length, glui32 current_style, int fixed_width) { + int marker, index; + glui32 set_style; + assert(g_vm->glk_stream_get_current()); + + set_style = current_style; + + /* + * Iterate each character in the line range. We actually delay output + * until we see a change in style; that way, we can send a buffer of + * characters to Glk, rather than sending them just one at a time. + */ + marker = 0; + for (index = 0; index < length; index++) { + gagt_attrset_t attribute_set; + glui32 style; + assert(attributes && string); + + /* + * Unpack the AGT font attributes for this character, and add fixed + * width font coercion. + */ + gagt_unpack_attributes(attributes[index], &attribute_set); + attribute_set.fixed |= fixed_width; + + /* + * Decide on any applicable new Glk text styling. If it's different + * to the current style, output the delayed characters, and update + * Glk's style setting. + */ + style = gagt_select_style(&attribute_set); + if (style != set_style) { + g_vm->glk_put_buffer(string + marker, index - marker); + marker = index; + + g_vm->glk_set_style(style); + set_style = style; + } + } + + /* Output any remaining delayed characters. */ + if (marker < length) + g_vm->glk_put_buffer(string + marker, length - marker); + + return set_style; +} + + +/* + * gagt_display_line() + * + * Display a page buffer line, starting in the current Glk style, and + * returning the new current Glk style. + * + * The function takes additional flags to force fixed width font, skip over + * indentation and trailing line whitespace, and trim hyphens (if skipping + * trailing whitespace). + */ +static glui32 gagt_display_line(const gagt_lineref_t line, glui32 current_style, + int fixed_width, int skip_indent, int skip_outdent, + int trim_hyphen) { + int start, length; + glui32 set_style; + + /* + * Check the skip indent flag to find the first character to display, and + * the count of characters to display. + */ + start = 0; + length = line->buffer.length; + if (skip_indent) { + start += line->indent; + length -= line->indent; + } + + /* Adjust length for skipping outdent and trimming hyphens. */ + if (skip_outdent) { + length -= line->outdent; + if (trim_hyphen && line->is_hyphenated) + length--; + } + + /* Display this line segment. */ + set_style = gagt_display_text_element((const char *)line->buffer.data + start, + line->buffer.attributes + start, + length, current_style, fixed_width); + + return set_style; +} + + +/* + * gagt_display_hinted_line() + * + * Display a page buffer line, starting in the current Glk style, and + * returning the new current Glk style. The function uses the font hints + * from the line, and receives the font hint of the prior line. + */ +static glui32 gagt_display_hinted_line(const gagt_lineref_t line, glui32 current_style, + gagt_font_hint_t prior_hint) { + glui32 style; + + style = current_style; + switch (line->font_hint) { + case HINT_FIXED_WIDTH: + /* Force fixed width font on the line. */ + style = gagt_display_line(line, style, TRUE, FALSE, FALSE, FALSE); + + g_vm->glk_put_char('\n'); + break; + + case HINT_PROPORTIONAL: + /* + * Permit proportional font, and suppress outdent. Suppress indent + * too if this line follows a line that suppressed newline, or is the + * first line in the paragraph. For all cases, trim the hyphen from + * hyphenated lines. + */ + if (prior_hint == HINT_PROPORTIONAL || prior_hint == HINT_NONE) + style = gagt_display_line(line, style, FALSE, TRUE, TRUE, TRUE); + else + style = gagt_display_line(line, style, FALSE, FALSE, TRUE, TRUE); + + /* + * Where the line is not hyphenated, output a space in place of newline. + * This lets paragraph text to flow to the full display width. + */ + if (!line->is_hyphenated) + g_vm->glk_put_char(' '); + break; + + case HINT_PROPORTIONAL_NEWLINE: + case HINT_PROPORTIONAL_NEWLINE_STANDOUT: + /* + * As above, permit proportional font, suppress outdent, and suppress + * indent too under certain conditions; in this case, only when the + * prior line suppressed newline. + */ + if (prior_hint == HINT_PROPORTIONAL) + style = gagt_display_line(line, style, FALSE, TRUE, TRUE, FALSE); + else + style = gagt_display_line(line, style, FALSE, FALSE, TRUE, FALSE); + + g_vm->glk_put_char('\n'); + break; + + case HINT_NONE: + gagt_fatal("GLK: Page buffer line with no font hint"); + gagt_exit(); + + default: + gagt_fatal("GLK: Invalid font hint encountered"); + gagt_exit(); + } + + return style; +} + + +/* + * gagt_display_auto() + * + * Display buffered output text to the Glk main window using a bunch of + * occasionally rather dodgy heuristics to try to automatically set a suitable + * font for the way the text is structured, while replacing special paragraphs + * with altered text. + */ +static void gagt_display_auto() { + gagt_paragraphref_t paragraph; + glui32 style; + + style = style_Normal; + g_vm->glk_set_style(style); + + /* Handle each paragraph. */ + for (paragraph = gagt_get_first_paragraph(); + paragraph; paragraph = gagt_get_next_paragraph(paragraph)) { + /* If a special paragraph, output replacement text instead. */ + if (paragraph->special) { + style = gagt_display_special(paragraph->special, style); + g_vm->glk_put_char('\n'); + } else { + gagt_lineref_t line; + gagt_font_hint_t prior_hint; + + /* Get the first line of the paragraph. */ + line = gagt_get_first_paragraph_line(paragraph); + + /* + * Output a blank line where the first line of the first paragraph + * is standout; this sets it apart from the prompt. + */ + if (paragraph == gagt_get_first_paragraph() + && line == gagt_get_first_paragraph_line(paragraph)) { + if (line->font_hint == HINT_PROPORTIONAL_NEWLINE_STANDOUT) + g_vm->glk_put_char('\n'); + } + + /* Handle each line of the paragraph. */ + prior_hint = HINT_NONE; + for (; line; line = gagt_get_next_paragraph_line(line)) { + /* + * Print this line according to its font hint, noting any change + * of style and the line's font hint for use next iteration as + * the prior hint. + */ + style = gagt_display_hinted_line(line, style, prior_hint); + prior_hint = line->font_hint; + } + + /* Output the newline for the end of the paragraph. */ + g_vm->glk_put_char('\n'); + } + } + + /* If no paragraphs at all, but a current buffer, output a newline. */ + if (!gagt_get_first_paragraph() && gagt_current_buffer.length > 0) + g_vm->glk_put_char('\n'); + + /* Output any help hint and unterminated line from the line buffer. */ + style = gagt_display_provide_help_hint(style); + style = gagt_display_text_element((const char *)gagt_current_buffer.data, + gagt_current_buffer.attributes, + gagt_current_buffer.length, style, FALSE); +} + + +/* + * gagt_display_manual() + * + * Display buffered output text in the Glk main window, with either a fixed + * width or a proportional font. + */ +static void gagt_display_manual(int fixed_width) { + gagt_lineref_t line; + glui32 style; + + style = style_Normal; + g_vm->glk_set_style(style); + + for (line = gagt_get_first_page_line(); + line; line = gagt_get_next_page_line(line)) { + gagt_paragraphref_t paragraph; + + paragraph = line->paragraph; + + /* + * If this is a special paragraph, display the replacement text on + * its first line and ignore remaining special lines. Otherwise, + * display the page buffer line using either fixed or proportional + * font, as requested. + */ + if (paragraph && paragraph->special) { + if (gagt_get_first_paragraph_line(paragraph) == line) + style = gagt_display_special(paragraph->special, style); + } else { + style = gagt_display_line(line, style, fixed_width, + FALSE, FALSE, FALSE); + g_vm->glk_put_char('\n'); + } + } + + /* Output any help hint and unterminated line from the line buffer. */ + style = gagt_display_provide_help_hint(style); + style = gagt_display_text_element((const char *)gagt_current_buffer.data, + gagt_current_buffer.attributes, + gagt_current_buffer.length, + style, fixed_width); +} + + +/* + * gagt_display_debug() + * + * Display the analyzed page buffer in a form that shows all of its gory + * detail. + */ +static void gagt_display_debug() { + gagt_lineref_t line; + char buffer[256]; + + g_vm->glk_set_style(style_Preformatted); + for (line = gagt_get_first_page_line(); + line; line = gagt_get_next_page_line(line)) { + gagt_paragraphref_t paragraph; + + paragraph = line->paragraph; + sprintf(buffer, + "%2d:%2d->%2ld A=%-3d L=%-2d I=%-2d O=%-2d R=%-2d %c%c| ", + paragraph ? paragraph->id + 1 : 0, + paragraph ? paragraph->line_count : 0, + paragraph && paragraph->special + ? paragraph->special - GAGT_SPECIALS + 1 : 0, + line->buffer.allocation, line->buffer.length, + line->indent, line->outdent, + line->real_length, + line->is_hyphenated ? 'h' : '_', + line->is_blank ? 'b' : + line->font_hint == HINT_PROPORTIONAL ? 'P' : + line->font_hint == HINT_PROPORTIONAL_NEWLINE ? 'N' : + line->font_hint == HINT_PROPORTIONAL_NEWLINE_STANDOUT ? 'S' : + line->font_hint == HINT_FIXED_WIDTH ? 'F' : '_'); + g_vm->glk_put_string(buffer); + + g_vm->glk_put_buffer((const char *)line->buffer.data, line->buffer.length); + g_vm->glk_put_char('\n'); + } + + if (gagt_current_buffer.length > 0) { + sprintf(buffer, + "__,__->__ A=%-3d L=%-2d I=__ O=__ R=__ %s| ", + gagt_current_buffer.allocation, gagt_current_buffer.length, + gagt_help_requested ? "HR" : "__"); + g_vm->glk_put_string(buffer); + + g_vm->glk_put_buffer((const char *)gagt_current_buffer.data, gagt_current_buffer.length); + } + + gagt_help_requested = FALSE; +} + + +/* + * gagt_output_flush() + * + * Flush any buffered output text to the Glk main window, and clear the + * buffer ready for new output text. The function concerns itself with + * both the page buffer and any unterminated line in the line buffer. + */ +static void gagt_output_flush() { + /* + * Run the analysis of page buffer contents. This will fill in the + * paragraph and font hints fields, any any applicable special pointer, + * for every line held in the buffer. + */ + gagt_paragraph_page(); + gagt_mark_specials(); + gagt_assign_font_hints(); + + /* + * Select the appropriate display routine to use, and call it. The display + * routines present somewhat different output, and are responsible for + * displaying both the page buffer _and_ any buffered current line text. + */ + switch (gagt_font_mode) { + case FONT_AUTOMATIC: + gagt_display_auto(); + break; + + case FONT_FIXED_WIDTH: + gagt_display_manual(TRUE); + break; + + case FONT_PROPORTIONAL: + gagt_display_manual(FALSE); + break; + + case FONT_DEBUG: + gagt_display_debug(); + break; + + default: + gagt_fatal("GLK: Invalid font mode encountered"); + gagt_exit(); + } + + /* Empty the buffer, ready for new game strings. */ + gagt_paragraphs_delete(); + gagt_output_delete(); +} + + +/* + * agt_clrscr() + * + * Clear the main playing area window. Although there may be little point + * in flushing (rather than emptying) the buffers, nevertheless that is + * what we do. + */ +void agt_clrscr() { + if (!BATCH_MODE) { + /* Update the apparent (virtual) window x position. */ + curr_x = 0; + + /* Flush any pending buffered output, and clear the main window. */ + gagt_output_flush(); + g_vm->glk_window_clear(gagt_main_window); + + /* Add a series of newlines to any script file. */ + if (script_on) + textputs(scriptfile, "\n\n\n\n"); + + gagt_debug("agt_clrscr", ""); + } +} + + +/* + * gagt_styled_string() + * gagt_styled_char() + * gagt_standout_string() + * gagt_standout_char() + * gagt_normal_string() + * gagt_normal_char() + * gagt_header_string() + * + * Convenience functions to print strings in assorted styles. A standout + * string is one that hints that it's from the interpreter, not the game. + */ +static void gagt_styled_string(glui32 style, const char *message) { + assert(message); + + g_vm->glk_set_style(style); + g_vm->glk_put_string(message); + g_vm->glk_set_style(style_Normal); +} + +static void gagt_styled_char(glui32 style, char c) { + char buffer[2]; + + buffer[0] = c; + buffer[1] = '\0'; + gagt_styled_string(style, buffer); +} + +static void gagt_standout_string(const char *message) { + gagt_styled_string(style_Emphasized, message); +} + +static void gagt_standout_char(char c) { + gagt_styled_char(style_Emphasized, c); +} + +static void gagt_normal_string(const char *message) { + gagt_styled_string(style_Normal, message); +} + +static void gagt_normal_char(char c) { + gagt_styled_char(style_Normal, c); +} + +static void gagt_header_string(const char *message) { + gagt_styled_string(style_Header, message); +} + + +/*---------------------------------------------------------------------*/ +/* Glk port delay functions */ +/*---------------------------------------------------------------------*/ + +/* Number of milliseconds in a second (traditionally, 1000). */ +static const int GAGT_MS_PER_SEC = 1000; + +/* + * Number of milliseconds to timeout. Because of jitter in the way Glk + * generates timeouts, it's worthwhile implementing a delay using a number + * of shorter timeouts. This minimizes inaccuracies in the actual delay. + */ +static const glui32 GAGT_DELAY_TIMEOUT = 50; + +/* The character key that can be pressed to cancel, and suspend, delays. */ +static const char GAGT_DELAY_SUSPEND = ' '; + +/* + * Flag to temporarily turn off all delays. This is set when the user + * cancels a delay with a keypress, and remains set until the next time + * that AGiliTy requests user input. This way, games that call agt_delay() + * sequentially don't require multiple keypresses to jump out of delay + * sections. + */ +static int gagt_delays_suspended = FALSE; + + +/* + * agt_delay() + * + * Delay for the specified number of seconds. The delay can be canceled + * by a user keypress. + */ +void agt_delay(int seconds) { + glui32 milliseconds, delayed; + int delay_completed; + + /* Suppress delay if in fast replay or batch mode. */ + if (fast_replay || BATCH_MODE) + return; + + /* + * Do nothing if Glk doesn't have timers, if the delay state is set to + * ignore delays, if a zero or negative delay was specified, or if delays + * are currently temporarily suspended. + */ + if (!g_vm->glk_gestalt(gestalt_Timer, 0) + || gagt_delay_mode == DELAY_OFF + || seconds <= 0 || gagt_delays_suspended) + return; + + /* Flush any pending buffered output, and refresh status to show waiting. */ + gagt_output_flush(); + gagt_status_in_delay(TRUE); + + /* Calculate the number of milliseconds to delay. */ + milliseconds = (seconds * GAGT_MS_PER_SEC) + / (gagt_delay_mode == DELAY_SHORT ? 2 : 1); + + /* Request timer events, and let a keypress cancel the delay. */ + g_vm->glk_request_char_event(gagt_main_window); + g_vm->glk_request_timer_events(GAGT_DELAY_TIMEOUT); + + /* + * Implement the delay using a sequence of shorter Glk timeouts, with an + * option to cancel the delay with a keypress. + */ + delay_completed = TRUE; + for (delayed = 0; delayed < milliseconds; delayed += GAGT_DELAY_TIMEOUT) { + event_t event; + + /* Wait for the next timeout, or a character. */ + gagt_event_wait_2(evtype_CharInput, evtype_Timer, &event); + if (event.type == evtype_CharInput) { + /* + * If suspend requested, stop the delay, and set the delay + * suspension flag, and a note that the delay loop didn't complete. + * Otherwise, reissue the character input request. + */ + if (event.val1 == GAGT_DELAY_SUSPEND) { + gagt_delays_suspended = TRUE; + delay_completed = FALSE; + break; + } else + g_vm->glk_request_char_event(gagt_main_window); + } + } + + /* Cancel any pending character input, and timer events. */ + if (delay_completed) + g_vm->glk_cancel_char_event(gagt_main_window); + g_vm->glk_request_timer_events(0); + + /* Clear the waiting indicator. */ + gagt_status_in_delay(FALSE); + + gagt_debug("agt_delay", "seconds=%d [%lu mS] -> %s", seconds, milliseconds, + delay_completed ? "completed" : "canceled"); +} + + +/* + * gagt_delay_resume() + * + * Unsuspend delays. This function should be called by agt_input() and + * agt_getkey(), to re-enable delays when the interpreter next requests + * user input. + */ +static void gagt_delay_resume() { + gagt_delays_suspended = FALSE; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port box drawing functions */ +/*---------------------------------------------------------------------*/ + +/* Saved details of any current box dimensions and flags. */ +static unsigned long gagt_box_flags = 0; +static int gagt_box_busy = FALSE, + gagt_box_width = 0, + gagt_box_height = 0, + gagt_box_startx = 0; + + +/* + * gagt_box_rule() + * gagt_box_position() + * + * Draw a line at the top or bottom of a box, and position the cursor + * with a box indent. + */ +static void gagt_box_rule(int width) { + char *ruler; + + /* Write a +--...--+ ruler to delimit a box. */ + ruler = (char *)gagt_malloc(width + 2 + 1); + memset(ruler + 1, '-', width); + ruler[0] = ruler[width + 1] = '+'; + ruler[width + 2] = '\0'; + agt_puts(ruler); + free(ruler); +} + +static void gagt_box_position(int indent) { + char *spaces; + + /* Write a newline before the indent. */ + agt_newline(); + + /* Write the indent to the start of box text. */ + spaces = (char *)gagt_malloc(indent + 1); + memset(spaces, ' ', indent); + spaces[indent] = '\0'; + agt_puts(spaces); + free(spaces); +} + + +/* + * agt_makebox() + * agt_qnewline() + * agt_endbox() + * + * Start a box of given width, height, and with given flags. Write a new + * line in the box. And end the box. + */ +void agt_makebox(int width, int height, unsigned long flags) { + assert(!gagt_box_busy); + + gagt_box_busy = TRUE; + gagt_box_flags = flags; + gagt_box_width = width; + gagt_box_height = height; + + /* If no centering requested, set the indent to zero. */ + if (gagt_box_flags & TB_NOCENT) + gagt_box_startx = 0; + else { + int centering_width; + + /* + * Calculate the indent for centering, adding 4 characters for borders. + * Here, since screen_width is artificial, we'll center off status_width + * if it is less than screen width, otherwise we'll center by using + * screen_width. The reason for shrinking to screen_width is that if + * we don't, we could drive curr_x to beyond screen_width with our box + * indentations, and that confuses AGiliTy. + */ + if (status_width < screen_width) + centering_width = status_width; + else + centering_width = screen_width; + if (gagt_box_flags & TB_BORDER) + gagt_box_startx = (centering_width - gagt_box_width - 4) / 2; + else + gagt_box_startx = (centering_width - gagt_box_width) / 2; + + /* If the box turns out wider than the window, abandon centering. */ + if (gagt_box_startx < 0) + gagt_box_startx = 0; + } + + /* + * When in a box, we'll coerce fixed width font by setting it in the AGT + * font attributes. This ensures that the box displays as accurately as + * we're able to achieve. + */ + gagt_coerce_fixed_font(TRUE); + + /* Position the cursor for the box, and if bordered, write the rule. */ + gagt_box_position(gagt_box_startx); + if (gagt_box_flags & TB_BORDER) { + gagt_box_rule(gagt_box_width + 2); + gagt_box_position(gagt_box_startx); + agt_puts("| "); + } + + gagt_debug("agt_makebox", "width=%d, height=%d, flags=0x%lx", + width, height, flags); +} + +void agt_qnewline() { + assert(gagt_box_busy); + + /* Write box characters for the current and next line. */ + if (gagt_box_flags & TB_BORDER) { + agt_puts(" |"); + gagt_box_position(gagt_box_startx); + agt_puts("| "); + } else + gagt_box_position(gagt_box_startx); + + gagt_debug("agt_qnewline", ""); +} + +void agt_endbox() { + assert(gagt_box_busy); + + /* Finish off the current box. */ + if (gagt_box_flags & TB_BORDER) { + agt_puts(" |"); + gagt_box_position(gagt_box_startx); + gagt_box_rule(gagt_box_width + 2); + } + agt_newline(); + + /* An extra newline here improves the appearance. */ + agt_newline(); + + /* Back to allowing proportional font output again. */ + gagt_coerce_fixed_font(FALSE); + + gagt_box_busy = FALSE; + gagt_box_flags = gagt_box_width = gagt_box_startx = 0; + + gagt_debug("agt_endbox", ""); +} + + +/*---------------------------------------------------------------------*/ +/* Glk command escape functions */ +/*---------------------------------------------------------------------*/ + +/* + * gagt_command_script() + * + * Turn game output scripting (logging) on and off. + */ +static void gagt_command_script(const char *argument) { + assert(argument); + + if (gagt_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gagt_transcript_stream) { + gagt_normal_string("Glk transcript is already on.\n"); + return; + } + + fileref = g_vm->glk_fileref_create_by_prompt(fileusage_Transcript + | fileusage_TextMode, + filemode_WriteAppend, 0); + if (!fileref) { + gagt_standout_string("Glk transcript failed.\n"); + return; + } + + gagt_transcript_stream = g_vm->glk_stream_open_file(fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gagt_transcript_stream) { + gagt_standout_string("Glk transcript failed.\n"); + return; + } + + g_vm->glk_window_set_echo_stream(gagt_main_window, gagt_transcript_stream); + + gagt_normal_string("Glk transcript is now on.\n"); + } + + else if (gagt_strcasecmp(argument, "off") == 0) { + if (!gagt_transcript_stream) { + gagt_normal_string("Glk transcript is already off.\n"); + return; + } + + g_vm->glk_stream_close(gagt_transcript_stream, NULL); + gagt_transcript_stream = NULL; + + g_vm->glk_window_set_echo_stream(gagt_main_window, NULL); + + gagt_normal_string("Glk transcript is now off.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk transcript is "); + gagt_normal_string(gagt_transcript_stream ? "on" : "off"); + gagt_normal_string(".\n"); + } + + else { + gagt_normal_string("Glk transcript can be "); + gagt_standout_string("on"); + gagt_normal_string(", or "); + gagt_standout_string("off"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_inputlog() + * + * Turn game input logging on and off. + */ +static void gagt_command_inputlog(const char *argument) { + assert(argument); + + if (gagt_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gagt_inputlog_stream) { + gagt_normal_string("Glk input logging is already on.\n"); + return; + } + + fileref = g_vm->glk_fileref_create_by_prompt(fileusage_InputRecord + | fileusage_BinaryMode, + filemode_WriteAppend, 0); + if (!fileref) { + gagt_standout_string("Glk input logging failed.\n"); + return; + } + + gagt_inputlog_stream = g_vm->glk_stream_open_file(fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gagt_inputlog_stream) { + gagt_standout_string("Glk input logging failed.\n"); + return; + } + + gagt_normal_string("Glk input logging is now on.\n"); + } + + else if (gagt_strcasecmp(argument, "off") == 0) { + if (!gagt_inputlog_stream) { + gagt_normal_string("Glk input logging is already off.\n"); + return; + } + + g_vm->glk_stream_close(gagt_inputlog_stream, NULL); + gagt_inputlog_stream = NULL; + + gagt_normal_string("Glk input log is now off.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk input logging is "); + gagt_normal_string(gagt_inputlog_stream ? "on" : "off"); + gagt_normal_string(".\n"); + } + + else { + gagt_normal_string("Glk input logging can be "); + gagt_standout_string("on"); + gagt_normal_string(", or "); + gagt_standout_string("off"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_readlog() + * + * Set the game input log, to read input from a file. + */ +static void gagt_command_readlog(const char *argument) { + assert(argument); + + if (gagt_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gagt_readlog_stream) { + gagt_normal_string("Glk read log is already on.\n"); + return; + } + + fileref = g_vm->glk_fileref_create_by_prompt(fileusage_InputRecord + | fileusage_BinaryMode, + filemode_Read, 0); + if (!fileref) { + gagt_standout_string("Glk read log failed.\n"); + return; + } + + if (!g_vm->glk_fileref_does_file_exist(fileref)) { + g_vm->glk_fileref_destroy(fileref); + gagt_standout_string("Glk read log failed.\n"); + return; + } + + gagt_readlog_stream = g_vm->glk_stream_open_file(fileref, filemode_Read, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gagt_readlog_stream) { + gagt_standout_string("Glk read log failed.\n"); + return; + } + + gagt_normal_string("Glk read log is now on.\n"); + } + + else if (gagt_strcasecmp(argument, "off") == 0) { + if (!gagt_readlog_stream) { + gagt_normal_string("Glk read log is already off.\n"); + return; + } + + g_vm->glk_stream_close(gagt_readlog_stream, NULL); + gagt_readlog_stream = NULL; + + gagt_normal_string("Glk read log is now off.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk read log is "); + gagt_normal_string(gagt_readlog_stream ? "on" : "off"); + gagt_normal_string(".\n"); + } + + else { + gagt_normal_string("Glk read log can be "); + gagt_standout_string("on"); + gagt_normal_string(", or "); + gagt_standout_string("off"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_abbreviations() + * + * Turn abbreviation expansions on and off. + */ +static void gagt_command_abbreviations(const char *argument) { + assert(argument); + + if (gagt_strcasecmp(argument, "on") == 0) { + if (gagt_abbreviations_enabled) { + gagt_normal_string("Glk abbreviation expansions are already on.\n"); + return; + } + + gagt_abbreviations_enabled = TRUE; + gagt_normal_string("Glk abbreviation expansions are now on.\n"); + } + + else if (gagt_strcasecmp(argument, "off") == 0) { + if (!gagt_abbreviations_enabled) { + gagt_normal_string("Glk abbreviation expansions are already off.\n"); + return; + } + + gagt_abbreviations_enabled = FALSE; + gagt_normal_string("Glk abbreviation expansions are now off.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk abbreviation expansions are "); + gagt_normal_string(gagt_abbreviations_enabled ? "on" : "off"); + gagt_normal_string(".\n"); + } + + else { + gagt_normal_string("Glk abbreviation expansions can be "); + gagt_standout_string("on"); + gagt_normal_string(", or "); + gagt_standout_string("off"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_fonts() + * + * Set the value for gagt_font_mode depending on the argument from the + * user's command escape. + * + * Despite our best efforts, font control may still be wrong in some games. + * This command gives us a chance to correct that. + */ +static void gagt_command_fonts(const char *argument) { + assert(argument); + + if (gagt_strcasecmp(argument, "fixed") == 0) { + if (gagt_font_mode == FONT_FIXED_WIDTH) { + gagt_normal_string("Glk font control is already 'fixed'.\n"); + return; + } + + gagt_font_mode = FONT_FIXED_WIDTH; + gagt_normal_string("Glk font control is now 'fixed'.\n"); + } + + else if (gagt_strcasecmp(argument, "variable") == 0 + || gagt_strcasecmp(argument, "proportional") == 0) { + if (gagt_font_mode == FONT_PROPORTIONAL) { + gagt_normal_string("Glk font control is already 'proportional'.\n"); + return; + } + + gagt_font_mode = FONT_PROPORTIONAL; + gagt_normal_string("Glk font control is now 'proportional'.\n"); + } + + else if (gagt_strcasecmp(argument, "auto") == 0 + || gagt_strcasecmp(argument, "automatic") == 0) { + if (gagt_font_mode == FONT_AUTOMATIC) { + gagt_normal_string("Glk font control is already 'automatic'.\n"); + return; + } + + gagt_font_mode = FONT_AUTOMATIC; + gagt_normal_string("Glk font control is now 'automatic'.\n"); + } + + else if (gagt_strcasecmp(argument, "debug") == 0) { + if (gagt_font_mode == FONT_DEBUG) { + gagt_normal_string("Glk font control is already 'debug'.\n"); + return; + } + + gagt_font_mode = FONT_DEBUG; + gagt_normal_string("Glk font control is now 'debug'.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk font control is set to '"); + switch (gagt_font_mode) { + case FONT_AUTOMATIC: + gagt_normal_string("automatic"); + break; + + case FONT_FIXED_WIDTH: + gagt_normal_string("fixed"); + break; + + case FONT_PROPORTIONAL: + gagt_normal_string("proportional"); + break; + + case FONT_DEBUG: + gagt_normal_string("debug"); + break; + + default: + gagt_fatal("GLK: Invalid font mode encountered"); + gagt_exit(); + } + gagt_normal_string("'.\n"); + } + + else { + /* Avoid mentioning the debug setting. */ + gagt_normal_string("Glk font control can be "); + gagt_standout_string("fixed"); + gagt_normal_string(", "); + gagt_standout_string("proportional"); + gagt_normal_string(", or "); + gagt_standout_string("automatic"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_delays() + * + * Set a value for gagt_delay_mode depending on the argument from + * the user's command escape. + */ +static void gagt_command_delays(const char *argument) { + assert(argument); + + if (!g_vm->glk_gestalt(gestalt_Timer, 0)) { + gagt_normal_string("Glk delays are not available.\n"); + return; + } + + if (gagt_strcasecmp(argument, "full") == 0 + || gagt_strcasecmp(argument, "on") == 0) { + if (gagt_delay_mode == DELAY_FULL) { + gagt_normal_string("Glk delay mode is already 'full'.\n"); + return; + } + + gagt_delay_mode = DELAY_FULL; + gagt_normal_string("Glk delay mode is now 'full'.\n"); + } + + else if (gagt_strcasecmp(argument, "short") == 0 + || gagt_strcasecmp(argument, "half") == 0) { + if (gagt_delay_mode == DELAY_SHORT) { + gagt_normal_string("Glk delay mode is already 'short'.\n"); + return; + } + + gagt_delay_mode = DELAY_SHORT; + gagt_normal_string("Glk delay mode is now 'short'.\n"); + } + + else if (gagt_strcasecmp(argument, "none") == 0 + || gagt_strcasecmp(argument, "off") == 0) { + if (gagt_delay_mode == DELAY_OFF) { + gagt_normal_string("Glk delay mode is already 'none'.\n"); + return; + } + + gagt_delay_mode = DELAY_OFF; + gagt_normal_string("Glk delay mode is now 'none'.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk delay mode is set to '"); + switch (gagt_delay_mode) { + case DELAY_FULL: + gagt_normal_string("full"); + break; + + case DELAY_SHORT: + gagt_normal_string("short"); + break; + + case DELAY_OFF: + gagt_normal_string("none"); + break; + + default: + gagt_fatal("GLK: Invalid delay mode encountered"); + gagt_exit(); + } + gagt_normal_string("'.\n"); + } + + else { + gagt_normal_string("Glk delay mode can be "); + gagt_standout_string("full"); + gagt_normal_string(", "); + gagt_standout_string("short"); + gagt_normal_string(", or "); + gagt_standout_string("none"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_width() + * + * Print out the (approximate) display width, from status_width. It's + * approximate because the main window might include a scrollbar that + * the status window doesn't have, may use a different size font, and so + * on. But the main window won't tell us a width at all - it always + * returns zero. If we don't happen to have a status window available + * to us, there's not much we can say. + * + * Note that this function uses the interpreter variable status_width, + * so it's important to keep this updated with the current window size at + * all times. + */ +static void gagt_command_width(const char *argument) { + char buffer[16]; + assert(argument); + + if (!gagt_status_window) { + gagt_normal_string("Glk's current display width is unknown.\n"); + return; + } + + gagt_normal_string("Glk's current display width is approximately "); + sprintf(buffer, "%d", status_width); + gagt_normal_string(buffer); + gagt_normal_string(status_width == 1 ? " character" : " characters"); + gagt_normal_string(".\n"); +} + + +/* + * gagt_command_replacements() + * + * Turn Glk special paragraph replacement on and off. + */ +static void gagt_command_replacements(const char *argument) { + assert(argument); + + if (gagt_strcasecmp(argument, "on") == 0) { + if (gagt_replacement_enabled) { + gagt_normal_string("Glk replacements are already on.\n"); + return; + } + + gagt_replacement_enabled = TRUE; + gagt_normal_string("Glk replacements are now on.\n"); + } + + else if (gagt_strcasecmp(argument, "off") == 0) { + if (!gagt_replacement_enabled) { + gagt_normal_string("Glk replacements are already off.\n"); + return; + } + + gagt_replacement_enabled = FALSE; + gagt_normal_string("Glk replacements are now off.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk replacements are "); + gagt_normal_string(gagt_replacement_enabled ? "on" : "off"); + gagt_normal_string(".\n"); + } + + else { + gagt_normal_string("Glk replacements can be "); + gagt_standout_string("on"); + gagt_normal_string(", or "); + gagt_standout_string("off"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_statusline() + * + * Turn the extended status line on and off. + */ +static void gagt_command_statusline(const char *argument) { + assert(argument); + + if (!gagt_status_window) { + gagt_normal_string("Glk status window is not available.\n"); + return; + } + + if (gagt_strcasecmp(argument, "extended") == 0 + || gagt_strcasecmp(argument, "full") == 0) { + if (gagt_extended_status_enabled) { + gagt_normal_string("Glk status line mode is already 'extended'.\n"); + return; + } + + /* Expand the status window down to a second line. */ + g_vm->glk_window_set_arrangement(g_vm->glk_window_get_parent(gagt_status_window), + winmethod_Above | winmethod_Fixed, 2, NULL); + gagt_extended_status_enabled = TRUE; + + gagt_normal_string("Glk status line mode is now 'extended'.\n"); + } + + else if (gagt_strcasecmp(argument, "short") == 0 + || gagt_strcasecmp(argument, "normal") == 0) { + if (!gagt_extended_status_enabled) { + gagt_normal_string("Glk status line mode is already 'short'.\n"); + return; + } + + /* Shrink the status window down to one line. */ + g_vm->glk_window_set_arrangement(g_vm->glk_window_get_parent(gagt_status_window), + winmethod_Above | winmethod_Fixed, 1, NULL); + gagt_extended_status_enabled = FALSE; + + gagt_normal_string("Glk status line mode is now 'short'.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk status line mode is set to '"); + gagt_normal_string(gagt_extended_status_enabled ? "extended" : "short"); + gagt_normal_string("'.\n"); + } + + else { + gagt_normal_string("Glk status line can be "); + gagt_standout_string("extended"); + gagt_normal_string(", or "); + gagt_standout_string("short"); + gagt_normal_string(".\n"); + } +} + + +/* + * gagt_command_print_version_number() + * gagt_command_version() + * + * Print out the Glk library version number. + */ +static void gagt_command_print_version_number(glui32 version) { + char buffer[64]; + + sprintf(buffer, "%u.%u.%u", + version >> 16, (version >> 8) & 0xff, version & 0xff); + gagt_normal_string(buffer); +} + +static void gagt_command_version(const char *argument) { + glui32 version; + assert(argument); + + gagt_normal_string("This is version "); + gagt_command_print_version_number(GAGT_PORT_VERSION); + gagt_normal_string(" of the Glk AGiliTy port.\n"); + + version = g_vm->glk_gestalt(gestalt_Version, 0); + gagt_normal_string("The Glk library version is "); + gagt_command_print_version_number(version); + gagt_normal_string(".\n"); +} + + +/* + * gagt_command_commands() + * + * Turn command escapes off. Once off, there's no way to turn them back on. + * Commands must be on already to enter this function. + */ +static void gagt_command_commands(const char *argument) { + assert(argument); + + if (gagt_strcasecmp(argument, "on") == 0) { + gagt_normal_string("Glk commands are already on.\n"); + } + + else if (gagt_strcasecmp(argument, "off") == 0) { + gagt_commands_enabled = FALSE; + gagt_normal_string("Glk commands are now off.\n"); + } + + else if (strlen(argument) == 0) { + gagt_normal_string("Glk commands are "); + gagt_normal_string(gagt_commands_enabled ? "on" : "off"); + gagt_normal_string(".\n"); + } + + else { + gagt_normal_string("Glk commands can be "); + gagt_standout_string("on"); + gagt_normal_string(", or "); + gagt_standout_string("off"); + gagt_normal_string(".\n"); + } +} + +/* Glk subcommands and handler functions. */ +struct gagt_command_t { + const char *const command; /* Glk subcommand. */ + void (* const handler)(const char *argument); /* Subcommand handler. */ + const int takes_argument; /* Argument flag. */ +} ; +typedef const gagt_command_t *gagt_commandref_t; + +static void gagt_command_summary(const char *argument); +static void gagt_command_help(const char *argument); + +static gagt_command_t GAGT_COMMAND_TABLE[] = { + {"summary", gagt_command_summary, FALSE}, + {"script", gagt_command_script, TRUE}, + {"inputlog", gagt_command_inputlog, TRUE}, + {"readlog", gagt_command_readlog, TRUE}, + {"abbreviations", gagt_command_abbreviations, TRUE}, + {"fonts", gagt_command_fonts, TRUE}, + {"delays", gagt_command_delays, TRUE}, + {"width", gagt_command_width, FALSE}, + {"replacements", gagt_command_replacements, TRUE}, + {"statusline", gagt_command_statusline, TRUE}, + {"version", gagt_command_version, FALSE}, + {"commands", gagt_command_commands, TRUE}, + {"help", gagt_command_help, TRUE}, + {NULL, NULL, FALSE} +}; + + +/* + * gagt_command_summary() + * + * Report all current Glk settings. + */ +static void gagt_command_summary(const char *argument) { + gagt_commandref_t entry; + assert(argument); + + /* + * Call handlers that have status to report with an empty argument, + * prompting each to print its current setting. + */ + for (entry = GAGT_COMMAND_TABLE; entry->command; entry++) { + if (entry->handler == gagt_command_summary + || entry->handler == gagt_command_help) + continue; + + entry->handler(""); + } +} + + +/* + * gagt_command_help() + * + * Document the available Glk cmds. + */ +static void gagt_command_help(const char *cmd) { + gagt_commandref_t entry, matched; + assert(cmd); + + if (strlen(cmd) == 0) { + gagt_normal_string("Glk cmds are"); + for (entry = GAGT_COMMAND_TABLE; entry->command; entry++) { + gagt_commandref_t next; + + next = entry + 1; + gagt_normal_string(next->command ? " " : " and "); + gagt_standout_string(entry->command); + gagt_normal_string(next->command ? "," : ".\n\n"); + } + + gagt_normal_string("Glk cmds may be abbreviated, as long as" + " the abbreviation is unambiguous. Use "); + gagt_standout_string("glk help"); + gagt_normal_string(" followed by a Glk cmd name for help on that" + " cmd.\n"); + return; + } + + matched = NULL; + for (entry = GAGT_COMMAND_TABLE; entry->command; entry++) { + if (gagt_strncasecmp(cmd, entry->command, strlen(cmd)) == 0) { + if (matched) { + gagt_normal_string("The Glk cmd "); + gagt_standout_string(cmd); + gagt_normal_string(" is ambiguous. Try "); + gagt_standout_string("glk help"); + gagt_normal_string(" for more information.\n"); + return; + } + matched = entry; + } + } + if (!matched) { + gagt_normal_string("The Glk cmd "); + gagt_standout_string(cmd); + gagt_normal_string(" is not valid. Try "); + gagt_standout_string("glk help"); + gagt_normal_string(" for more information.\n"); + return; + } + + if (matched->handler == gagt_command_summary) { + gagt_normal_string("Prints a summary of all the current Glk AGiliTy" + " settings.\n"); + } + + else if (matched->handler == gagt_command_script) { + gagt_normal_string("Logs the game's output to a file.\n\nUse "); + gagt_standout_string("glk script on"); + gagt_normal_string(" to begin logging game output, and "); + gagt_standout_string("glk script off"); + gagt_normal_string(" to end it. Glk AGiliTy will ask you for a file" + " when you turn scripts on.\n"); + } + + else if (matched->handler == gagt_command_inputlog) { + gagt_normal_string("Records the cmds you type into a game.\n\nUse "); + gagt_standout_string("glk inputlog on"); + gagt_normal_string(", to begin recording your cmds, and "); + gagt_standout_string("glk inputlog off"); + gagt_normal_string(" to turn off input logs. You can play back" + " recorded cmds into a game with the "); + gagt_standout_string("glk readlog"); + gagt_normal_string(" cmd.\n"); + } + + else if (matched->handler == gagt_command_readlog) { + gagt_normal_string("Plays back cmds recorded with "); + gagt_standout_string("glk inputlog on"); + gagt_normal_string(".\n\nUse "); + gagt_standout_string("glk readlog on"); + gagt_normal_string(". cmd play back stops at the end of the" + " file. You can also play back cmds from a" + " text file created using any standard editor.\n"); + } + + else if (matched->handler == gagt_command_abbreviations) { + gagt_normal_string("Controls abbreviation expansion.\n\nGlk AGiliTy" + " automatically expands several standard single" + " letter abbreviations for you; for example, \"x\"" + " becomes \"examine\". Use "); + gagt_standout_string("glk abbreviations on"); + gagt_normal_string(" to turn this feature on, and "); + gagt_standout_string("glk abbreviations off"); + gagt_normal_string(" to turn it off. While the feature is on, you" + " can bypass abbreviation expansion for an" + " individual game cmd by prefixing it with a" + " single quote.\n"); + } + + else if (matched->handler == gagt_command_fonts) { + gagt_normal_string("Controls the way Glk AGiliTy uses fonts.\n\n" + "AGT games normally assume 80x25 monospaced font" + " displays. Glk can often use proportional fonts." + " To try to improve text display, Glk AGiliTy will" + " attempt to automatically detect when game text" + " can be displayed safely in a proportional font," + " and when fixed width fonts are required. For" + " some games, however, you may need to override" + " it. Use "); + gagt_standout_string("glk fonts automatic"); + gagt_normal_string(", "); + gagt_standout_string("glk fonts proportional"); + gagt_normal_string(", and "); + gagt_standout_string("glk fonts fixed"); + gagt_normal_string(" to switch between Glk AGiliTy font modes.\n"); + } + + else if (matched->handler == gagt_command_delays) { + gagt_normal_string("Shortens, or eliminates, AGT game delays.\n\nUse "); + gagt_standout_string("glk delays full"); + gagt_normal_string(", "); + gagt_standout_string("glk delays short"); + gagt_normal_string(", or "); + gagt_standout_string("glk delays none"); + gagt_normal_string(". In Glk AGiliTy, you can also end an AGT game's" + " delay early, by pressing Space while the game is" + " delaying.\n"); + } + + else if (matched->handler == gagt_command_width) { + gagt_normal_string("Prints the screen width available for fixed font" + " display.\n\nEven though Glk AGiliTy tries to handle" + " issues surrounding proportional font displays for" + " you automatically, some game elements may still" + " need to display in fixed width fonts. These" + " elements will be happiest if the available screen" + " width is at least 80 columns.\n"); + } + + else if (matched->handler == gagt_command_replacements) { + gagt_normal_string("Controls game text scanning and replacement.\n\n" + "Glk AGiliTy can monitor the game's output, and" + " replace a few selected standard messages with" + " equivalents, printed using a style that stands" + " out better in Glk displays. Use "); + gagt_standout_string("glk replacements on"); + gagt_normal_string(" to turn this feature on, and "); + gagt_standout_string("glk replacements off"); + gagt_normal_string(" to turn it off.\n"); + } + + else if (matched->handler == gagt_command_statusline) { + gagt_normal_string("Controls the Glk AGiliTy status line display.\n\n" + "Use "); + gagt_standout_string("glk statusline extended"); + gagt_normal_string(" to display a full, two line status display, and "); + gagt_standout_string("glk statusline short"); + gagt_normal_string(" for a single line status display.\n"); + } + + else if (matched->handler == gagt_command_version) { + gagt_normal_string("Prints the version numbers of the Glk library" + " and the Glk AGiliTy port.\n"); + } + + else if (matched->handler == gagt_command_commands) { + gagt_normal_string("Turn off Glk cmds.\n\nUse "); + gagt_standout_string("glk cmds off"); + gagt_normal_string(" to disable all Glk cmds, including this one." + " Once turned off, there is no way to turn Glk" + " cmds back on while inside the game.\n"); + } + + else if (matched->handler == gagt_command_help) + gagt_command_help(""); + + else + gagt_normal_string("There is no help available on that Glk cmd." + " Sorry.\n"); +} + + +/* + * gagt_command_escape() + * + * This function is handed each input line. If the line contains a specific + * Glk port command, handle it and return TRUE, otherwise return FALSE. + */ +static int gagt_command_escape(const char *string) { + int posn; + char *string_copy, *cmd, *argument; + assert(string); + + /* + * Return FALSE if the string doesn't begin with the Glk command escape + * introducer. + */ + posn = strspn(string, "\t "); + if (gagt_strncasecmp(string + posn, "glk", strlen("glk")) != 0) + return FALSE; + + /* Take a copy of the string, without any leading space or introducer. */ + string_copy = (char *)gagt_malloc(strlen(string + posn) + 1 - strlen("glk")); + strcpy(string_copy, string + posn + strlen("glk")); + + /* + * Find the subcommand; the first word in the string copy. Find its end, + * and ensure it terminates with a NUL. + */ + posn = strspn(string_copy, "\t "); + cmd = string_copy + posn; + posn += strcspn(string_copy + posn, "\t "); + if (string_copy[posn] != '\0') + string_copy[posn++] = '\0'; + + /* + * Now find any argument data for the command, ensuring it too terminates + * with a NUL. + */ + posn += strspn(string_copy + posn, "\t "); + argument = string_copy + posn; + posn += strcspn(string_copy + posn, "\t "); + string_copy[posn] = '\0'; + + /* + * Try to handle the command and argument as a Glk subcommand. If it + * doesn't run unambiguously, print command usage. Treat an empty command + * as "help". + */ + if (strlen(cmd) > 0) { + gagt_commandref_t entry, matched; + int matches; + + /* + * Search for the first unambiguous table cmd string matching + * the cmd passed in. + */ + matches = 0; + matched = NULL; + for (entry = GAGT_COMMAND_TABLE; entry->command; entry++) { + if (gagt_strncasecmp(cmd, entry->command, strlen(cmd)) == 0) { + matches++; + matched = entry; + } + } + + /* If the match was unambiguous, call the command handler. */ + if (matches == 1) { + gagt_normal_char('\n'); + matched->handler(argument); + + if (!matched->takes_argument && strlen(argument) > 0) { + gagt_normal_string("[The "); + gagt_standout_string(matched->command); + gagt_normal_string(" cmd ignores arguments.]\n"); + } + } + + /* No match, or the cmd was ambiguous. */ + else { + gagt_normal_string("\nThe Glk cmd "); + gagt_standout_string(cmd); + gagt_normal_string(" is "); + gagt_normal_string(matches == 0 ? "not valid" : "ambiguous"); + gagt_normal_string(". Try "); + gagt_standout_string("glk help"); + gagt_normal_string(" for more information.\n"); + } + } else { + gagt_normal_char('\n'); + gagt_command_help(""); + } + + /* The string contained a Glk cmd; return TRUE. */ + free(string_copy); + return TRUE; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port input functions */ +/*---------------------------------------------------------------------*/ + +/* Longest line we're going to buffer for input. */ +enum { GAGT_INPUTBUFFER_LENGTH = 256 }; + +/* Table of single-character command abbreviations. */ +typedef const struct { + const char abbreviation; /* Abbreviation character. */ + const char *const expansion; /* Expansion string. */ +} gagt_abbreviation_t; +typedef gagt_abbreviation_t *gagt_abbreviationref_t; + +static gagt_abbreviation_t GAGT_ABBREVIATIONS[] = { + {'c', "close"}, {'g', "again"}, {'i', "inventory"}, + {'k', "attack"}, {'l', "look"}, {'p', "open"}, + {'q', "quit"}, {'r', "drop"}, {'t', "take"}, + {'x', "examine"}, {'y', "yes"}, {'z', "wait"}, + {'\0', NULL} +}; + + +/* + * gagt_expand_abbreviations() + * + * Expand a few common one-character abbreviations commonly found in other + * game systems, but not always normal in AGT games. + */ +static void gagt_expand_abbreviations(char *buffer, int size) { + char *command_, abbreviation; + const char *expansion; + gagt_abbreviationref_t entry; + assert(buffer); + + /* Ignore anything that isn't a single letter command_. */ + command_ = buffer + strspn(buffer, "\t "); + if (!(strlen(command_) == 1 + || (strlen(command_) > 1 && isspace(command_[1])))) + return; + + /* Scan the abbreviations table for a match. */ + abbreviation = g_vm->glk_char_to_lower((unsigned char) command_[0]); + expansion = NULL; + for (entry = GAGT_ABBREVIATIONS; entry->expansion; entry++) { + if (entry->abbreviation == abbreviation) { + expansion = entry->expansion; + break; + } + } + + /* + * If a match found, check for a fit, then replace the character with the + * expansion string. + */ + if (expansion) { + if ((int)strlen(buffer) + (int)strlen(expansion) - 1 >= size) + return; + + memmove(command_ + strlen(expansion) - 1, command_, strlen(command_) + 1); + memcpy(command_, expansion, strlen(expansion)); + + gagt_standout_string("["); + gagt_standout_char(abbreviation); + gagt_standout_string(" -> "); + gagt_standout_string(expansion); + gagt_standout_string("]\n"); + } +} + + +/* + * agt_input() + * + * Read a line from the keyboard, allocating space for it using malloc. + * AGiliTy defines the following for the in_type argument: + * + * in_type: 0=command, 1=number, 2=question, 3=userstr, 4=filename, + * 5=RESTART,RESTORE,UNDO,QUIT + * Negative values are for internal use by the interface (i.e. this module) + * and so are free to be defined by the porter. + * + * Since it's unclear what use we can make of this information in Glk, + * for the moment the argument is ignored. It seems that no-one else + * uses it, either. + */ +char *agt_input(int in_type) { + event_t event; + int length; + char *buffer; + + /* + * Update the current status line display, and flush any pending buffered + * output. Release any suspension of delays. + */ + gagt_status_notify(); + gagt_output_flush(); + gagt_delay_resume(); + + /* Reset current x, as line input implies a newline. */ + curr_x = 0; + + /* Allocate a line input buffer, allowing 256 characters and a NUL. */ + length = GAGT_INPUTBUFFER_LENGTH + 1; + buffer = (char *)gagt_malloc(length); + + /* + * If we have an input log to read from, use that until it is exhausted. + * On end of file, close the stream and resume input from line requests. + */ + if (gagt_readlog_stream) { + glui32 chars; + + /* Get the next line from the log stream. */ + chars = g_vm->glk_get_line_stream(gagt_readlog_stream, buffer, length); + if (chars > 0) { + /* Echo the line just read in input style. */ + g_vm->glk_set_style(style_Input); + g_vm->glk_put_buffer(buffer, chars); + g_vm->glk_set_style(style_Normal); + + /* + * Convert the string from Glk's ISO 8859 Latin-1 to IBM cp 437, + * add to any script, and return it. + */ + gagt_iso_to_cp((const uchar *)buffer, (uchar *)buffer); + if (script_on) + textputs(scriptfile, buffer); + return buffer; + } + + /* + * We're at the end of the log stream. Close it, and then continue + * on to request a line from Glk. + */ + g_vm->glk_stream_close(gagt_readlog_stream, NULL); + gagt_readlog_stream = NULL; + } + + /* Set this up as a read buffer for the main window, and wait. */ + g_vm->glk_request_line_event(gagt_main_window, buffer, length - 1, 0); + gagt_event_wait(evtype_LineInput, &event); + + /* Terminate the input line with a NUL. */ + assert((int)event.val1 < length); + buffer[event.val1] = '\0'; + + /* + * If neither abbreviations nor local commands are enabled, use the data + * read above without further massaging. + */ + if (gagt_abbreviations_enabled || gagt_commands_enabled) { + char *cmd; + + /* + * If the first non-space input character is a quote, bypass all + * abbreviation expansion and local command recognition, and use the + * unadulterated input, less introductory quote. + */ + cmd = buffer + strspn(buffer, "\t "); + if (cmd[0] == '\'') { + /* Delete the quote with memmove(). */ + memmove(cmd, cmd + 1, strlen(cmd)); + } else { + /* Check for, and expand, any abbreviated commands. */ + if (gagt_abbreviations_enabled) + gagt_expand_abbreviations(buffer, length); + + /* + * Check for standalone "help", then for Glk port special commands; + * suppress the interpreter's use of this input for Glk commands. + */ + if (gagt_commands_enabled) { + int posn; + + posn = strspn(buffer, "\t "); + if (gagt_strncasecmp(buffer + posn, "help", strlen("help")) == 0) { + if (strspn(buffer + posn + strlen("help"), "\t ") + == strlen(buffer + posn + strlen("help"))) { + gagt_display_register_help_request(); + } + } + + if (gagt_command_escape(buffer)) { + gagt_display_silence_help_hints(); + buffer[0] = '\0'; + return buffer; + } + } + } + } + + /* + * If there is an input log active, log this input string to it. Note that + * by logging here we get any abbreviation expansions but we won't log glk + * special commands, nor any input read from a current open input log. + */ + if (gagt_inputlog_stream) { + g_vm->glk_put_string_stream(gagt_inputlog_stream, buffer); + g_vm->glk_put_char_stream(gagt_inputlog_stream, '\n'); + } + + /* + * Convert from Glk's ISO 8859 Latin-1 to IBM cp 437, and add to any script. + */ + gagt_iso_to_cp((const uchar *)buffer, (uchar *)buffer); + if (script_on) + textputs(scriptfile, buffer); + + gagt_debug("agt_input", "in_type=%d -> '%s'", in_type, buffer); + return buffer; +} + + +/* + * agt_getkey() + * + * Read a single character and return it. AGiliTy defines the echo_char + * argument as: + * + * If echo_char=1, echo character. If 0, then the character is not + * required to be echoed (and ideally shouldn't be). + * + * However, I've found that not all other ports really do this, and in + * practice it doesn't always look right. So for Glk, the character is + * always echoed to the main window. + */ +char agt_getkey(rbool echo_char) { + event_t event; + char buffer[3]; + assert(g_vm->glk_stream_get_current()); + + /* + * Update the current status line display, and flush any pending buffered + * output. Release any suspension of delays. + */ + gagt_status_notify(); + gagt_output_flush(); + gagt_delay_resume(); + + /* Reset current x, as echoed character input implies a newline. */ + curr_x = 0; + + /* + * If we have an input log to read from, use that as above until it is + * exhausted. We take just the first character of a given line. + */ + if (gagt_readlog_stream) { + glui32 chars; + char logbuffer[GAGT_INPUTBUFFER_LENGTH + 1]; + + /* Get the next line from the log stream. */ + chars = g_vm->glk_get_line_stream(gagt_readlog_stream, + logbuffer, sizeof(logbuffer)); + if (chars > 0) { + /* Take just the first character, adding a newline if necessary. */ + buffer[0] = logbuffer[0]; + buffer[1] = buffer[0] == '\n' ? '\0' : '\n'; + buffer[2] = '\0'; + + /* Echo the character just read in input style. */ + g_vm->glk_set_style(style_Input); + g_vm->glk_put_string(buffer); + g_vm->glk_set_style(style_Normal); + + /* + * Convert from Glk's ISO 8859 Latin-1 to IBM cp 437, add to any + * script, and return the character. + */ + gagt_iso_to_cp((const uchar *)buffer, (uchar *)buffer); + if (script_on) + textputs(scriptfile, buffer); + return buffer[0]; + } + + /* + * We're at the end of the log stream. Close it, and then continue + * on to request a character from Glk. + */ + g_vm->glk_stream_close(gagt_readlog_stream, NULL); + gagt_readlog_stream = NULL; + } + + /* + * Request a single character from main window, and wait. Ignore non- + * ASCII codes that Glk returns for special function keys; we just want + * one ASCII return value. (Glk does treat Return as a special key, + * though, and we want to pass that back as ASCII return.) + */ + do { + g_vm->glk_request_char_event(gagt_main_window); + gagt_event_wait(evtype_CharInput, &event); + } while (event.val1 > BYTE_MAX_VAL && event.val1 != keycode_Return); + + /* + * Save the character into a short string buffer, converting Return + * to newline, and adding a newline if not Return. + */ + buffer[0] = event.val1 == keycode_Return ? '\n' : event.val1; + buffer[1] = buffer[0] == '\n' ? '\0' : '\n'; + buffer[2] = '\0'; + + /* If there is an input log active, log this input string to it. */ + if (gagt_inputlog_stream) + g_vm->glk_put_string_stream(gagt_inputlog_stream, buffer); + + /* + * No matter what echo_char says, as it happens, the output doesn't look + * great if we don't write out the character, and also a newline (c.f. + * the "Yes/No" confirmation of the QUIT command)... + */ + g_vm->glk_set_style(style_Input); + g_vm->glk_put_string(buffer); + g_vm->glk_set_style(style_Normal); + + /* + * Convert from Glk's ISO 8859 Latin-1 to IBM cp 437, and add to any + * script. + */ + gagt_iso_to_cp((const uchar *)buffer, (uchar *)buffer); + if (script_on) + textputs(scriptfile, buffer); + + gagt_debug("agt_getkey", "echo_char=%d -> '%c'", + echo_char, buffer[0] == '\n' ? '$' : buffer[0]); + return buffer[0]; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port event functions */ +/*---------------------------------------------------------------------*/ + +/* + * We have some clever atexit() finalizer handling for exit() calls that + * come from the core interpreter. However, an exit() call could also come + * from Glk; Xkill for example. To tell the difference, we'll have the + * event wait functions set a flag to indicate when g_vm->glk_select() is active. + */ +static int gagt_in_glk_select = FALSE; + +/* + * gagt_event_wait_2() + * gagt_event_wait() + * + * Process Glk events until one of the expected type, or types, arrives. + * Return the event of that type. + */ +static void gagt_event_wait_2(glui32 wait_type_1, glui32 wait_type_2, event_t *event) { + assert(event); + + do { + gagt_in_glk_select = TRUE; + g_vm->glk_select(event); + gagt_in_glk_select = FALSE; + + switch (event->type) { + case evtype_Arrange: + case evtype_Redraw: + gagt_status_redraw(); + break; + default: + break; + } + } while (!(event->type == (EvType)wait_type_1 || event->type == (EvType)wait_type_2)); +} + +static void gagt_event_wait(glui32 wait_type, event_t *event) { + assert(event); + gagt_event_wait_2(wait_type, evtype_None, event); +} + + +/* + * gagt_event_in_glk_select() + * + * Return TRUE if we're currently awaiting an event in g_vm->glk_select(). Used + * by the finalizer to distinguish interpreter and glk exit() calls. + */ +static int gagt_event_in_glk_select() { + return gagt_in_glk_select; +} + + +/*---------------------------------------------------------------------*/ +/* Miscellaneous Glk port startup and options functions */ +/*---------------------------------------------------------------------*/ + +/* + * Default screen height and width, and also a default status width for + * use with Glk libraries that don't support separate windows. + */ +static const int GAGT_DEFAULT_SCREEN_WIDTH = 80, + GAGT_DEFAULT_SCREEN_HEIGHT = 25, + GAGT_DEFAULT_STATUS_WIDTH = 76; + + +/* + * agt_option() + * + * Platform-specific setup and options handling. AGiliTy defines the + * arguments and options as: + * + * If setflag is 0, then the option was prefixed with NO_. Return 1 if + * the option is recognized. + * + * The Glk port has no options file handling, so none of this is + * implemented here. + */ +rbool agt_option(int optnum, char *optstr[], rbool setflag) { + gagt_debug("agt_option", "optnum=%d, optstr=%s, setflag=%d", + optnum, optstr[0], setflag); + return 0; +} + + +/* + * agt_globalfile() + * + * Global options file handle handling. For now, this is a stub, since + * there is no .agilrc for this port. + */ +genfile agt_globalfile(int fid) { + gagt_debug("agt_globalfile", "fid=%d", fid); + return badfile(fCFG); +} + + +/* + * init_interface() + * + * General initialization for the module; sets some variables, and creates + * the Glk windows to work in. Called from the AGiliTy main(). + */ +void init_interface(int argc, char *argv[]) { + glui32 status_height; + + /* + * Begin with some default values for global variables that this module + * is somehow responsible for. + */ + script_on = center_on = par_fill_on = FALSE; + scriptfile = badfile(fSCR); + debugfile = nullptr; // stderr; + + /* + * Set up AGT-specific Glk styles. This needs to be done before any Glk + * window is opened. + */ + gagt_init_user_styles(); + + /* + * Create the main game window. The main game window creation must succeed. + * If it fails, we'll return, and the caller can detect this by looking + * for a NULL main window. + */ + gagt_main_window = g_vm->glk_window_open(0, 0, 0, wintype_TextBuffer, 0); + if (!gagt_main_window) + return; + + /* + * Set the main window to be the default window, for convenience. We do + * this again in glk_main() -- this call is here just in case this version + * of init_interface() is ever called by AGiliTy's main. + */ + g_vm->glk_set_window(gagt_main_window); + + /* + * Screen height is something we don't use. Linux Xglk returns dimensions + * of 0x0 for text buffer windows, so we can't measure the main window + * height anyway. But... the height does come into play in AGiliTy's + * agil.c, when the interpreter is deciding how to output game titles, and + * how much of its own subsequent verbiage to output. This gives us a + * problem, since this "verbiage" is stuff we look for and replace with + * our own special text. So... sigh, set 25, and try to cope in the + * special text we've set up with all the variations that ensue. + * + * Screen width does get used, but so, so many games, and for that matter + * the interpreter itself, assume 80 chars, so it's simplest just to set, + * and keep, this, and put up with the minor odd effects (making it match + * status_width, or making it something like MAX_INT to defeat the game's + * own wrapping, gives a lot of odder effects, trust me on this one...). + */ + screen_width = GAGT_DEFAULT_SCREEN_WIDTH; + screen_height = GAGT_DEFAULT_SCREEN_HEIGHT; + + /* + * Create a status window, with one or two lines as selected by user + * options or flags. We can live without a status window if we have to. + */ + status_height = gagt_extended_status_enabled ? 2 : 1; + g_vm->glk_stylehint_set(wintype_TextGrid, style_User1, stylehint_ReverseColor, 1); + gagt_status_window = g_vm->glk_window_open(gagt_main_window, + winmethod_Above | winmethod_Fixed, + status_height, wintype_TextGrid, 0); + if (gagt_status_window) { + /* + * Call gagt_status_redraw() to set the interpreter's status_width + * variable initial value. + */ + gagt_status_redraw(); + } else { + /* + * No status window, so set a suitable default status width. In this + * case, we're using a value four characters less than the set screen + * width. AGiliTy's status line code will fill to this width with + * justified text, and we add two characters of bracketing when + * displaying status lines for Glks that don't support separate windows, + * making a total of 78 characters, which should be fairly standard. + */ + status_width = GAGT_DEFAULT_STATUS_WIDTH; + } + + agt_clrscr(); + gagt_debug("init_interface", "argc=%d, argv=%p", argc, argv); +} + + +/*---------------------------------------------------------------------*/ +/* Replacement interface.c functions */ +/*---------------------------------------------------------------------*/ + +/* Get_user_file() type codes. */ +enum { + AGT_SCRIPT = 0, + AGT_SAVE = 1, + AGT_RESTORE = 2, + AGT_LOG_READ = 3, + AGT_LOG_WRITE = 4 +}; + +/* Longest acceptable filename. */ +enum { GAGT_MAX_PATH = 1024 }; + + +#ifdef GLK_ANSI_ONLY +/* + * gagt_confirm() + * + * Print a confirmation prompt, and read a single input character, taking + * only [YyNn] input. If the character is 'Y' or 'y', return TRUE. + * + * This function is only required for the ANSI version of get_user_file(). + */ +static int +gagt_confirm(const char *prompt) { + event_t event; + unsigned char response; + assert(prompt); + + /* + * Print the confirmation prompt, in a style that hints that it's from the + * interpreter, not the game. + */ + gagt_standout_string(prompt); + + /* Wait for a single 'Y' or 'N' character response. */ + response = ' '; + do { + g_vm->glk_request_char_event(gagt_main_window); + gagt_event_wait(evtype_CharInput, &event); + + if (event.val1 <= BYTE_MAX_VAL) + response = g_vm->glk_char_to_upper(event.val1); + } while (!(response == 'Y' || response == 'N')); + + /* Echo the confirmation response, and a blank line. */ + g_vm->glk_set_style(style_Input); + g_vm->glk_put_string(response == 'Y' ? "Yes" : "No"); + g_vm->glk_set_style(style_Normal); + g_vm->glk_put_string("\n"); + + return response == 'Y'; +} +#endif + + +/* + * gagt_get_user_file() + * + * Alternative versions of functions to get a file name from the user, and + * return a file stream structure. These functions are front-ended by the + * main get_user_file() function, which first converts the AGT file type + * into Glk usage and filemode, and also a mode for fopen()/fdopen(). + * + * The ANSI version of the function prompts for the file using the simple + * method of querying the user through input in the main window. It then + * constructs a file stream around the path entered, and returns it. + * + * The non-ANSI, Glk version is more sneaky. It prompts for a file using + * Glk's functions to get filenames by prompt, file selection dialog, or + * whatever. Then it attempts to uncover which file descriptor Glk opened + * its file on, dup's it, closes the Glk stream, and returns a file stream + * built on this file descriptor. This is all highly non-ANSI, requiring + * dup() and fdopen(), and making some assumptions about the way that dup, + * open, and friends work. It works on Linux, and on Mac (CodeWarrior). + * It may also work for you, but if it doesn't, or if your system lacks + * things like dup or fdopen, define g_vm->glk_ANSI_ONLY and use the safe version. + * + * If GARGLK is used, non-ansi version calls garglk_fileref_get_name() + * instead, and opens a file the highly portable way, but still with a + * Glkily nice prompt dialog. + */ +#ifdef GLK_ANSI_ONLY +static genfile +gagt_get_user_file(glui32 usage, glui32 fmode, const char *fdtype) { + char filepath[GAGT_MAX_PATH]; + event_t event; + int index, all_spaces; + genfile retfile; + assert(fdtype); + + /* Prompt in a similar way to Glk. */ + switch (usage) { + case fileusage_SavedGame: + gagt_normal_string("Enter saved game"); + break; + + case fileusage_Transcript: + gagt_normal_string("Enter transcript file"); + break; + + case fileusage_InputRecord: + gagt_normal_string("Enter command record file"); + break; + } + switch (fmode) { + case filemode_Read: + gagt_normal_string(" to load: "); + break; + + case filemode_Write: + gagt_normal_string(" to store: "); + break; + } + + /* Get the path to the file from the user. */ + g_vm->glk_request_line_event(gagt_main_window, filepath, sizeof(filepath) - 1, 0); + gagt_event_wait(evtype_LineInput, &event); + + /* Terminate the file path with a NUL. */ + assert(event.val1 < sizeof(filepath)); + filepath[event.val1] = '\0'; + + /* Reject file paths that only contain any whitespace characters. */ + all_spaces = TRUE; + for (index = 0; index < strlen(filepath); index++) { + if (!isspace(filepath[index])) { + all_spaces = FALSE; + break; + } + } + if (all_spaces) + return badfile(fSAV); + + /* Confirm overwrite of any existing file. */ + if (fmode == filemode_Write) { + genfile file; + + file = fopen(filepath, "r"); + if (file) { + fclose(file); + + if (!gagt_confirm("Overwrite existing file? [y/n] ")) + return badfile(fSAV); + } + } + + /* Open and return a FILE* stream, or badfile if this fails. */ + retfile = fopen(filepath, fdtype); + return retfile ? retfile : badfile(fSAV); +} +#endif + +#ifndef GLK_ANSI_ONLY +static genfile gagt_get_user_file(glui32 usage, glui32 fmode, const char *fdtype) { + frefid_t fileref; + genfile retfile; + assert(fdtype); + + /* Try to get a Glk file reference with these attributes. */ + fileref = g_vm->glk_fileref_create_by_prompt(usage, (FileMode)fmode, 0); + if (!fileref) + return badfile(fSAV); + + /* + * Reject the file reference if we're expecting to read from it, + * and the referenced file doesn't exist. + */ + if (fmode == filemode_Read && !g_vm->glk_fileref_does_file_exist(fileref)) { + g_vm->glk_fileref_destroy(fileref); + return badfile(fSAV); + } + + /* + * Now, it gets ugly. Glk assumes that the interpreter will do all of + * its reading and writing using the Glk streams read/write functions. + * It won't; at least, not without major surgery. So here we're going + * to do some dangerous stuff... + * + * Since a Glk stream is opaque, it's hard to tell what the underlying + * file descriptor is for it. We can get it if we want to play around + * in the internals of the strid_t structure, but it's unpleasant. + * The alternative is, arguably, no more pleasant, but it makes for + * (perhaps) more portable code. What we'll do is to dup a file, then + * immediately close it, and call g_vm->glk_stream_open_file(). The open() + * in g_vm->glk_stream_open_file() will return the same file descriptor number + * that we just close()d (in theory...). This makes the following two + * major assumptions: + * + * 1) g_vm->glk_stream_open_file() opens precisely one file with open() + * 2) open() always uses the lowest available file descriptor number, + * like dup() + * + * Believe it or not, this is better than the alternatives. There is + * no Glk function to return the filename from a frefid_t, and it + * moves about in different Glk libraries so we can't just take it + * from a given offset. And there is no Glk function to return the + * underlying file descriptor or FILE* from a Glk stream either. :-( + */ + +#ifdef GARGLK + retfile = fopen(g_vm->garglk_fileref_get_name(fileref), fdtype); +#else + strid_t stream; + int tryfd, glkfd, dupfd, retfd; + + /* So, start by dup()'ing the first file descriptor we can, ... */ + glkfd = -1; + for (tryfd = 0; tryfd < FD_SETSIZE; tryfd++) { + glkfd = fcntl(tryfd, F_DUPFD, 0); + if (glkfd != -1) + break; + } + if (tryfd >= FD_SETSIZE) { + g_vm->glk_fileref_destroy(fileref); + return badfile(fSAV); + } + + /* ...then closing it, ... */ + close(glkfd); + + /* ...now open the Glk stream, assuming it opens on file 'glkfd', ... */ + stream = g_vm->glk_stream_open_file(fileref, fmode, 0); + if (!stream) { + g_vm->glk_fileref_destroy(fileref); + return badfile(fSAV); + } + + /* ...dup() the Glk file onto another file descriptor, ... */ + dupfd = fcntl(glkfd, F_DUPFD, 0); + assert(dupfd != -1); + + /* ...close and destroy the Glk edifice for this file, ... */ + g_vm->glk_stream_close(stream, NULL); + g_vm->glk_fileref_destroy(fileref); + + /* ...for neatness, dup() back to the old Glk file descriptor, ... */ + retfd = fcntl(dupfd, F_DUPFD, 0); + assert(retfd != -1 && retfd == glkfd); + close(dupfd); + + /* ...and finally, open a FILE* stream onto the return descriptor. */ + retfile = fdopen(retfd, fdtype); + if (!retfile) + return badfile(fSAV); +#endif /* GARGLK */ + + /* + * The result of all of this should now be that retfile is a FILE* wrapper + * round a file descriptor open on a file indicated by the user through Glk. + * Return it. + */ + return retfile; +} +#endif + + +/* + * get_user_file() + * + * Get a file name from the user, and return the file stream structure. + * This is a front-end to ANSI and non-ANSI variants of the function. + */ +genfile get_user_file(int type) { + glui32 usage = 0, fmode = 0; + const char *fdtype; + genfile retfile; + + gagt_output_flush(); + + /* Map AGiliTy type to Glk usage and filemode. */ + switch (type) { + case AGT_SCRIPT: + usage = fileusage_Transcript; + fmode = filemode_Write; + break; + + case AGT_SAVE: + usage = fileusage_SavedGame; + fmode = filemode_Write; + break; + + case AGT_RESTORE: + usage = fileusage_SavedGame; + fmode = filemode_Read; + break; + + case AGT_LOG_READ: + usage = fileusage_InputRecord; + fmode = filemode_Read; + break; + + case AGT_LOG_WRITE: + usage = fileusage_InputRecord; + fmode = filemode_Write; + break; + + default: + gagt_fatal("GLK: Unknown file type encountered"); + gagt_exit(); + } + + /* From these, determine a mode type for the f[d]open() call. */ + if (fmode == filemode_Write) + fdtype = usage == fileusage_SavedGame ? "wb" : "w"; + else + fdtype = usage == fileusage_SavedGame ? "rb" : "r"; + + /* Get a file stream from these using the appropriate function. */ + retfile = gagt_get_user_file(usage, fmode, fdtype); + + gagt_debug("get_user_file", "type=%d -> %p", type, retfile); + return retfile; +} + + +/* + * set_default_filenames() + * + * Set defaults for last save, log, and script filenames. + */ +void set_default_filenames(fc_type fc) { + /* + * There is nothing to do in this function, since Glk has its own ideas on + * default names for files obtained with a prompt. + */ + gagt_debug("set_default_filenames", "fc=%p", fc); +} + + +/*---------------------------------------------------------------------*/ +/* Functions intercepted by link-time wrappers */ +/*---------------------------------------------------------------------*/ + +/* + * __wrap_toupper() + * __wrap_tolower() + * + * Wrapper functions around toupper(), tolower(), and fatal(). The Linux + * linker's --wrap option will convert calls to mumble() to __wrap_mumble() + * if we give it the right options. We'll use this feature to translate + * all toupper() and tolower() calls in the interpreter code into calls to + * Glk's versions of these functions. + * + * It's not critical that we do this. If a linker, say a non-Linux one, + * won't do --wrap, then just do without it. It's unlikely that there + * will be much noticeable difference. + */ +int __wrap_toupper(int ch) { + unsigned char uch; + + uch = g_vm->glk_char_to_upper((unsigned char) ch); + return (int) uch; +} + +int __wrap_tolower(int ch) { + unsigned char lch; + + lch = g_vm->glk_char_to_lower((unsigned char) ch); + return (int) lch; +} + + +/*---------------------------------------------------------------------*/ +/* Replacements for AGiliTy main() and options parsing */ +/*---------------------------------------------------------------------*/ + +/* External declaration of interface.c's set default options function. */ +extern void set_default_options(); + +/* + * The following values need to be passed between the startup_code and main + * functions. + */ +static int gagt_saved_argc = 0; /* Recorded argc. */ +static char **gagt_saved_argv = NULL, /* Recorded argv. */ + *gagt_gamefile = NULL; /* Name of game file. */ +static const char *gagt_game_message = NULL; /* Error message. */ + +/* + * Flag to set if we want to test for a clean exit. Without this it's a + * touch tricky sometimes to corner AGiliTy into calling exit() for us; it + * tends to require a broken game file. + */ +static int gagt_clean_exit_test = FALSE; + + +/* + * gagt_parse_option() + * + * Glk-ified version of AGiliTy's parse_options() function. In practice, + * because Glk has got to them first, most options that come in here are + * probably going to be single-character ones, since this is what we told + * Glk in the arguments structure above. The Glk font control and other + * special tweaky flags will probably be the only multiple-character ones. + */ +static int gagt_parse_option(const char *option) { + unsigned int index; + assert(option); + + assert(option[0] == '-'); + for (index = 1; option[index]; index++) { + switch (option[index]) { + case 'g': + switch (option[++index]) { + case 'f': + gagt_font_mode = FONT_FIXED_WIDTH; + break; + case 'p': + gagt_font_mode = FONT_PROPORTIONAL; + break; + case 'a': + gagt_font_mode = FONT_AUTOMATIC; + break; + case 'd': + gagt_delay_mode = DELAY_FULL; + break; + case 'h': + gagt_delay_mode = DELAY_SHORT; + break; + case 'n': + gagt_delay_mode = DELAY_OFF; + break; + case 'r': + gagt_replacement_enabled = FALSE; + break; + case 'x': + gagt_abbreviations_enabled = FALSE; + break; + case 's': + gagt_extended_status_enabled = TRUE; + break; + case 'l': + gagt_extended_status_enabled = FALSE; + break; + case 'c': + gagt_commands_enabled = FALSE; + break; + case 'D': + DEBUG_OUT = TRUE; + break; + case '#': + gagt_clean_exit_test = TRUE; + break; + default: + return FALSE; + } + break; + + case 'p': + debug_parse = TRUE; + break; + case 'a': + DEBUG_DISAMBIG = TRUE; + break; + case 'd': + DEBUG_AGT_CMD = TRUE; + break; + case 'x': + DEBUG_EXEC_VERB = TRUE; + break; + case 's': + DEBUG_SMSG = TRUE; + break; +#ifdef MEM_INFO + case 'M': + DEBUG_MEM = TRUE; + break; +#endif + case 'm': + descr_maxmem = 0; + break; + case 't': + BATCH_MODE = TRUE; + break; + case 'c': + make_test = TRUE; + break; + case '1': + irun_mode = TRUE; + break; +#ifdef OPEN_FILE_AS_TEXT + case 'b': + open_as_binary = TRUE; + break; +#endif + + case '?': + default: + return FALSE; + } + } + + return TRUE; +} + + +/* + * gagt_startup_code() + * gagt_main() + * + * Together, these functions take the place of the original AGiliTy main(). + * The first one is called from glkunix_startup_code(), to parse and + * generally handle options. The second is called from glk_main(), and + * does the real work of running the game. + */ +int gagt_startup_code(int argc, char *argv[]) { + int argv_index; + + /* + * Before doing anything else, stash argc and argv away for use by + * gagt_main() below. + */ + gagt_saved_argc = argc; + gagt_saved_argv = argv; + + /* Make the mandatory call for initialization. */ + set_default_options(); + + /* Handle command line arguments. */ + for (argv_index = 1; + argv_index < argc && argv[argv_index][0] == '-'; argv_index++) { + /* + * Handle an option string coming after "-". If the options parse + * fails, return FALSE. + */ + if (!gagt_parse_option(argv[argv_index])) + return FALSE; + } + + /* + * Get the name of the game file. Since we need this in our call from + * glk_main, we need to keep it in a module static variable. If the game + * file name is omitted, then here we'll set the pointer to NULL, and + * complain about it later in main. Passing the message string around + * like this is a nuisance... + */ + if (argv_index == argc - 1) { + gagt_gamefile = argv[argv_index]; + gagt_game_message = NULL; +#ifdef GARGLK + char *s; + s = strrchr(gagt_gamefile, '\\'); + if (s) g_vm->garglk_set_story_name(s + 1); + s = strrchr(gagt_gamefile, '/'); + if (s) g_vm->garglk_set_story_name(s + 1); +#endif /* GARGLK */ + } else { + gagt_gamefile = NULL; + if (argv_index < argc - 1) + gagt_game_message = "More than one game file was given" + " on the command line."; + else + gagt_game_message = "No game file was given on the command line."; + } + + /* All startup options were handled successfully. */ + return TRUE; +} + +static void gagt_main() { + fc_type fc; + assert(gagt_saved_argc != 0 && gagt_saved_argv); + + /* Ensure AGiliTy internal types have the right sizes. */ + if (sizeof(integer) < 2 || sizeof(int32) < 4 || sizeof(uint32) < 4) { + gagt_fatal("GLK: Types sized incorrectly, recompilation is needed"); + gagt_exit(); + } + + /* + * Initialize the interface. As it happens, init_interface() is in our + * module here (above), and ignores argc and argv, but since the main() in + * AGiliTy passes them, we'll do so here, just in case we ever want to go + * back to using AGiliTy's main() function. + * + * init_interface() can fail if there is a problem creating the main + * window. As it doesn't return status, we have to detect this by checking + * that gagt_main_window is not NULL. + */ + init_interface(gagt_saved_argc, gagt_saved_argv); + if (!gagt_main_window) { + gagt_fatal("GLK: Can't open main window"); + gagt_exit(); + } + g_vm->glk_window_clear(gagt_main_window); + g_vm->glk_set_window(gagt_main_window); + g_vm->glk_set_style(style_Normal); + + /* If there's a problem with the game file, complain now. */ + if (!gagt_gamefile) { + assert(gagt_game_message); + if (gagt_status_window) + g_vm->glk_window_close(gagt_status_window, NULL); + gagt_header_string("Glk AGiliTy Error\n\n"); + gagt_normal_string(gagt_game_message); + gagt_normal_char('\n'); + gagt_exit(); + } + + /* + * Create a game file context, and try to ensure it will open successfully + * in run_game(). + */ + fc = init_file_context(gagt_gamefile, fDA1); + if (!(gagt_workround_fileexist(fc, fAGX) + || gagt_workround_fileexist(fc, fDA1))) { + if (gagt_status_window) + g_vm->glk_window_close(gagt_status_window, NULL); + gagt_header_string("Glk AGiliTy Error\n\n"); + gagt_normal_string("Can't find or open game '"); + gagt_normal_string(gagt_gamefile); + gagt_normal_char('\''); + gagt_normal_char('\n'); + gagt_exit(); + } + + /* + * Run the game interpreter in AGiliTy. run_game() releases the file + * context, so we don't have to, don't want to, and shouldn't. + */ + run_game(fc); + + /* + * Handle any updated status, and flush all remaining buffered output; + * this also frees all malloc'ed memory in the buffers. + */ + gagt_status_notify(); + gagt_output_flush(); + + /* + * Free any temporary memory that may have been used by status line + * functions. + */ + gagt_status_cleanup(); + + /* Close any open transcript, input log, and/or read log. */ + if (gagt_transcript_stream) { + g_vm->glk_stream_close(gagt_transcript_stream, NULL); + gagt_transcript_stream = NULL; + } + if (gagt_inputlog_stream) { + g_vm->glk_stream_close(gagt_inputlog_stream, NULL); + gagt_inputlog_stream = NULL; + } + if (gagt_readlog_stream) { + g_vm->glk_stream_close(gagt_readlog_stream, NULL); + gagt_readlog_stream = NULL; + } +} + + +/*---------------------------------------------------------------------*/ +/* Linkage between Glk entry/exit calls and the AGiliTy interpreter */ +/*---------------------------------------------------------------------*/ + +/* + * Safety flags, to ensure we always get startup before main, and that + * we only get a call to main once. + */ +static int gagt_startup_called = FALSE, + gagt_main_called = FALSE; + +/* + * We try to catch calls to exit() from the interpreter, and redirect them + * to g_vm->glk_exit(). To help tell these calls from a call to exit() from + * g_vm->glk_exit() itself, we need to monitor when interpreter code is running, + * and when not. + */ +static int gagt_agility_running = FALSE; + + +/* + * gagt_finalizer() + * + * ANSI atexit() handler. This is the first part of trying to catch and re- + * direct the calls the core AGiliTy interpreter makes to exit() -- we really + * want it to call g_vm->glk_exit(), but it's hard to achieve. There are three + * basic approaches possible, and all have drawbacks: + * + * o #define exit to gagt_something, and provide the gagt_something() + * function. This type of macro definition is portable for the most + * part, but tramples the code badly, and messes up the build of the + * non-interpreter "support" binaries. + * o Use ld's --wrap to wrapper exit. This only works with Linux's linker + * and so isn't at all portable. + * o Register an exit handler with atexit(), and try to cope in it after + * exit() has been called. + * + * Here we try the last of these. The one sticky part of it is that in our + * exit handler we'll want to call g_vm->glk_exit(), which will in all likelihood + * call exit(). And multiple calls to exit() from a program are "undefined". + * + * In practice, C runtimes tend to do one of three things: they treat the + * exit() call from the exit handler as if it was a return; they recurse + * indefinitely through the hander; or they do something ugly (abort, for + * example). The first of these is fine, ideal in fact, and seems to be the + * Linux and SVR4 behavior. The second we can avoid with a flag. The last + * is the problem case, seen only with SVR3 (and even then, it occurs only + * on program exit, after everything's cleaned up, and for that matter only + * on abnormal exit). + * + * Note that here we're not expecting to get a call to this routine, and if + * we do, and interpreter code is still running, it's a sign that we need + * to take actions we'd hoped not to have to take. + */ +static void gagt_finalizer() { + /* + * If interpreter code is still active, and we're not in a g_vm->glk_select(), + * the core interpreter code called exit(). Handle cleanup. + */ + if (gagt_agility_running && !gagt_event_in_glk_select()) { + event_t event; + + /* + * If we have a main window, try to update status (which may go to the + * status window, or to the main window) and flush any pending buffered + * output. + */ + if (gagt_main_window) { + gagt_status_notify(); + gagt_output_flush(); + } + + /* + * Clear the flag to avoid recursion, and call g_vm->glk_exit() to clean up + * Glk and terminate. This is the call that probably re-calls exit(), + * and thus prods "undefined" bits of the C runtime, so we'll make it + * configurable and overrideable for problem cases. + */ + gagt_agility_running = FALSE; + + /* + * We've decided not to take the dangerous route. + * + * In that case, providing we have a main window, fake a Glk-like-ish + * hit-any-key-and-wait message using a simple string in the main + * window. Not great, but usable where we're forced into bypassing + * g_vm->glk_exit(). If we have no main window, there's no point in doing + * anything more. + */ + if (gagt_main_window) { + g_vm->glk_cancel_char_event(gagt_main_window); + g_vm->glk_cancel_line_event(gagt_main_window, NULL); + + g_vm->glk_set_style(style_Alert); + g_vm->glk_put_string("\n\nHit any key to exit.\n"); + g_vm->glk_request_char_event(gagt_main_window); + gagt_event_wait(evtype_CharInput, &event); + } + } +} + + +/* + * gagt_exit() + * + * g_vm->glk_exit() local wrapper. This is the second part of trying to catch + * and redirect calls to exit(). g_vm->glk_finalizer() above needs to know that + * we called g_vm->glk_exit() already from here, so it doesn't try to do it again. + */ +static void gagt_exit() { + assert(gagt_agility_running); + + /* + * Clear the running flag to neutralize gagt_finalizer(), throw out any + * buffered output data, and then call the real g_vm->glk_exit(). + */ + gagt_agility_running = FALSE; + gagt_output_delete(); + g_vm->glk_exit(); +} + + +/* + * __wrap_exit() + * + * Exit() wrapper where a linker does --wrap. This is the third part of + * trying to catch and redirect calls to exit(). + * + * This function is for use only with IFP, and avoids a nasty attempt at + * reusing a longjmp buffer. IFP will redirect calls to exit() into + * g_vm->glk_exit() as a matter of course. It also handles atexit(), and we've + * registered a function with atexit() that calls g_vm->glk_exit(), and + * IFP redirects g_vm->glk_exit() to be an effective return from glk_main(). At + * that point it calls finalizers. So without doing something special for + * IFP, we'll find ourselves calling g_vm->glk_exit() twice -- once as the IFP + * redirected exit(), and once from our finalizer. Two returns from the + * function glk_main() is a recipe for unpleasantness. + * + * As IFP is Linux-only, at present, --wrap will always be available to IFP + * plugin builds. So here, we'll wrap exit() before IFP can get to it, and + * handle it safely. For non-IFP/non-wrap links, this is just an unused + * function definition, and can be safely ignored... + */ +void __wrap_exit(int status) { + assert(gagt_agility_running); + + /* + * In an IFP plugin, only the core interpreter code could have called exit() + * here -- we don't, and IFP redirects g_vm->glk_exit(), the only other potential + * caller of exit(). (It also redirects exit() if we don't get to it here + * first.) + * + * So, if we have a main window, flush it. This is the same cleanup as + * done by the finalizer. + */ + if (gagt_main_window) { + gagt_status_notify(); + gagt_output_flush(); + } + + /* Clear the running flag, and transform exit() into a g_vm->glk_exit(). */ + gagt_agility_running = FALSE; + g_vm->glk_exit(); +} + + +/* + * glk_main() + * + * Main entry point for Glk. Here, all startup is done, and we call our + * function to run the game. + */ +void glk_main() { + assert(gagt_startup_called && !gagt_main_called); + gagt_main_called = TRUE; + + /* + * Register gagt_finalizer() with atexit() to cleanup on exit. Note that + * this module doesn't expect the atexit() handler to be called on all + * forms of exit -- see comments in gagt_finalizer() for more. + */ + if (atexit(gagt_finalizer) != 0) { + gagt_fatal("GLK: Failed to register finalizer"); + gagt_exit(); + } + + /* + * If we're testing for a clean exit, deliberately call exit() to see what + * happens. We're hoping for a clean process termination, but our exit + * code explores "undefined" ANSI. If we get something ugly, like a core + * dump, we'll want to set GLK[AGIL]_CLEAN_EXIT. + */ + if (gagt_clean_exit_test) { + gagt_agility_running = TRUE; + return; + } + + /* + * The final part of trapping exit(). Set the running flag, and call the + * interpreter main function. Clear the flag when the main function returns. + */ + gagt_agility_running = TRUE; + gagt_main(); + gagt_agility_running = FALSE; +} + + +/*---------------------------------------------------------------------*/ +/* Glk linkage relevant only to the UNIX platform */ +/*---------------------------------------------------------------------*/ + +/* + * Glk arguments for UNIX versions of the Glk interpreter. + */ +/* +glkunix_argumentlist_t glkunix_arguments[] = { + {(char *) "-gf", glkunix_arg_NoValue, + (char *) "-gf Force Glk to use only a fixed width font"}, + {(char *) "-gp", glkunix_arg_NoValue, + (char *) "-gp Allow Glk to use only a proportional font"}, + {(char *) "-ga", glkunix_arg_NoValue, + (char *) "-ga Try to use a suitable Glk font automatically"}, + {(char *) "-gd", glkunix_arg_NoValue, + (char *) "-gd Delay for the full period in Glk"}, + {(char *) "-gh", glkunix_arg_NoValue, + (char *) "-gh Delay for approximately half the period in Glk"}, + {(char *) "-gn", glkunix_arg_NoValue, + (char *) "-gn Turn off all game delays in Glk"}, + {(char *) "-gr", glkunix_arg_NoValue, + (char *) "-gr Turn off Glk text replacement"}, + {(char *) "-gx", glkunix_arg_NoValue, + (char *) "-gx Turn off Glk abbreviation expansions"}, + {(char *) "-gs", glkunix_arg_NoValue, + (char *) "-gs Display a short status window in Glk"}, + {(char *) "-gl", glkunix_arg_NoValue, + (char *) "-gl Display an extended status window in Glk"}, + {(char *) "-gc", glkunix_arg_NoValue, + (char *) "-gc Turn off Glk command escapes in games"}, + {(char *) "-gD", glkunix_arg_NoValue, + (char *) "-gD Turn on Glk port module debug tracing"}, + {(char *) "-g#", glkunix_arg_NoValue, + (char *) "-g# Test for clean exit (Glk module debugging only)"}, + {(char *) "-1", glkunix_arg_NoValue, + (char *) "-1 IRUN Mode: Print messages in first person"}, + {(char *) "-d", glkunix_arg_NoValue, + (char *) "-d Debug metacommand execution"}, + {(char *) "-t", glkunix_arg_NoValue, + (char *) "-t Test mode"}, + {(char *) "-c", glkunix_arg_NoValue, + (char *) "-c Create test file"}, + {(char *) "-m", glkunix_arg_NoValue, + (char *) "-m Force descriptions to be loaded from disk"}, +#ifdef OPEN_AS_TEXT + {(char *) "-b", glkunix_arg_NoValue, + (char *) "-b Open data files as binary files"}, +#endif + {(char *) "-p", glkunix_arg_NoValue, + (char *) "-p Debug parser"}, + {(char *) "-x", glkunix_arg_NoValue, + (char *) "-x Debug verb execution loop"}, + {(char *) "-a", glkunix_arg_NoValue, + (char *) "-a Debug disambiguation system"}, + {(char *) "-s", glkunix_arg_NoValue, + (char *) "-s Debug STANDARD message handler"}, +#ifdef MEM_INFO + {(char *) "-M", glkunix_arg_NoValue, + (char *) "-M Debug memory allocation"}, +#endif + {(char *) "", glkunix_arg_ValueCanFollow, + (char *) "filename game to run"}, + {NULL, glkunix_arg_End, NULL} +}; + +*/ +/* + * glkunix_startup_code() + * + * Startup entry point for UNIX versions of Glk AGiliTy. Glk will call + * glkunix_startup_code() to pass in arguments. On startup, we call our + * function to parse arguments and generally set stuff up. + */ + +int glk_startup_code(int argc, char *argv[]) { + assert(!gagt_startup_called); + gagt_startup_called = TRUE; + + return gagt_startup_code(argc, argv); +} + +} // End of namespace AGT +} // End of namespace Glk |