5873 lines
179 KiB
C++
5873 lines
179 KiB
C++
/* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#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.2
|
|
* -------------------------------
|
|
*
|
|
* This module contains 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;
|
|
|
|
/* 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 (!g_vm->gagt_main_window)
|
|
error("INTERNAL ERROR: %s", string);
|
|
|
|
/* Cancel all possible pending window input events. */
|
|
g_vm->glk_cancel_line_event(g_vm->gagt_main_window, nullptr);
|
|
g_vm->glk_cancel_char_event(g_vm->gagt_main_window);
|
|
|
|
/* Print a message indicating the error. */
|
|
g_vm->glk_set_window(g_vm->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 = nullptr;
|
|
|
|
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 = nullptr;
|
|
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:
|
|
g_vm->gagt_font_mode = FONT_FIXED_WIDTH;
|
|
break;
|
|
|
|
case GAGT_PROPORTIONAL_OKAY:
|
|
g_vm->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 = nullptr,
|
|
*gagt_status_buffer_printed = nullptr;
|
|
|
|
/*
|
|
* 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(g_vm->gagt_status_window);
|
|
|
|
g_vm->glk_window_get_size(g_vm->gagt_status_window, &width, &height);
|
|
if (height > 1) {
|
|
uint32 index;
|
|
int exit;
|
|
|
|
/* Clear the second status line only. */
|
|
g_vm->glk_window_move_cursor(g_vm->gagt_status_window, 0, 1);
|
|
g_vm->glk_set_window(g_vm->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(g_vm->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(g_vm->gagt_status_window,
|
|
width - strlen("Waiting... "), 1);
|
|
g_vm->glk_put_string("Waiting... ");
|
|
}
|
|
|
|
g_vm->glk_set_window(g_vm->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(g_vm->gagt_status_window);
|
|
|
|
g_vm->glk_window_get_size(g_vm->gagt_status_window, &width, &height);
|
|
if (height > 0) {
|
|
g_vm->glk_window_clear(g_vm->gagt_status_window);
|
|
g_vm->glk_window_move_cursor(g_vm->gagt_status_window, 0, 0);
|
|
g_vm->glk_set_window(g_vm->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(g_vm->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 (g_vm->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.2");
|
|
}
|
|
|
|
g_vm->glk_set_window(g_vm->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);
|
|
size_t ln = strlen(gagt_status_buffer) + 1;
|
|
gagt_status_buffer_printed = (char *)gagt_malloc(ln);
|
|
Common::strcpy_s(gagt_status_buffer_printed, ln, 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 (g_vm->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 (g_vm->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(g_vm->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(g_vm->gagt_status_window);
|
|
g_vm->glk_window_set_arrangement(parent,
|
|
winmethod_Above | winmethod_Fixed,
|
|
height, nullptr);
|
|
|
|
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 (g_vm->gagt_status_window && g_vm->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 = nullptr;
|
|
|
|
free(gagt_status_buffer_printed);
|
|
gagt_status_buffer_printed = nullptr;
|
|
}
|
|
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
/* 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 necessarily "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(g_vm->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 = nullptr,
|
|
gagt_page_tail = nullptr;
|
|
|
|
/*
|
|
* 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 = { nullptr, nullptr, 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 = nullptr;
|
|
from->allocation = from->length = 0;
|
|
}
|
|
|
|
static void gagt_string_free(gagt_stringref_t buffer) {
|
|
free(buffer->data);
|
|
free(buffer->attributes);
|
|
buffer->data = buffer->attributes = nullptr;
|
|
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 = nullptr;
|
|
|
|
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 = nullptr;
|
|
line->font_hint = HINT_NONE;
|
|
|
|
/* Add to the list, creating a new list if necessary. */
|
|
line->next = nullptr;
|
|
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 = nullptr,
|
|
gagt_paragraphs_tail = nullptr;
|
|
|
|
/*
|
|
* 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 = nullptr;
|
|
}
|
|
|
|
|
|
/*
|
|
* 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 = nullptr;
|
|
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 = nullptr;
|
|
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 = nullptr;
|
|
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 = nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* 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 nullptr;
|
|
}
|
|
|
|
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 nullptr;
|
|
}
|
|
|
|
|
|
/*
|
|
* 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.2",
|
|
"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.2|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.2",
|
|
"Copyright (C) 1996-1999,2001 by Robert Masenten",
|
|
"[Glk version]",
|
|
"-----------------------------------------------------------"
|
|
},
|
|
"|SAGiliTy, The (Mostly) Universal AGT Interpreter, Version 1.1.2|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.2, 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.2|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.2, Copyright (C) 1996-99,2001"
|
|
" Robert Masenten"
|
|
},
|
|
"This game is being executed by:\n\n"
|
|
" |SAGiliTy, The (Mostly) Universal AGT Interpreter, Version 1.1.2|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.2",
|
|
"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.2|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, {nullptr}, nullptr}
|
|
};
|
|
|
|
|
|
/*
|
|
* 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 = nullptr;
|
|
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 (g_vm->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();
|
|
break;
|
|
|
|
default:
|
|
gagt_fatal("GLK: Invalid font hint encountered");
|
|
gagt_exit();
|
|
break;
|
|
}
|
|
|
|
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;
|
|
Common::sprintf_s(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
|
|
? long(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) {
|
|
Common::sprintf_s(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 (g_vm->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(g_vm->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)
|
|
|| g_vm->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)
|
|
/ (g_vm->gagt_delay_mode == DELAY_SHORT ? 2 : 1);
|
|
|
|
/* Request timer events, and let a keypress cancel the delay. */
|
|
g_vm->glk_request_char_event(g_vm->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(g_vm->gagt_main_window);
|
|
}
|
|
}
|
|
|
|
/* Cancel any pending character input, and timer events. */
|
|
if (delay_completed)
|
|
g_vm->glk_cancel_char_event(g_vm->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 (g_vm->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;
|
|
}
|
|
|
|
g_vm->gagt_transcript_stream = g_vm->glk_stream_open_file(fileref,
|
|
filemode_WriteAppend, 0);
|
|
g_vm->glk_fileref_destroy(fileref);
|
|
if (!g_vm->gagt_transcript_stream) {
|
|
gagt_standout_string("Glk transcript failed.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->glk_window_set_echo_stream(g_vm->gagt_main_window, g_vm->gagt_transcript_stream);
|
|
|
|
gagt_normal_string("Glk transcript is now on.\n");
|
|
}
|
|
|
|
else if (gagt_strcasecmp(argument, "off") == 0) {
|
|
if (!g_vm->gagt_transcript_stream) {
|
|
gagt_normal_string("Glk transcript is already off.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->glk_stream_close(g_vm->gagt_transcript_stream, nullptr);
|
|
g_vm->gagt_transcript_stream = nullptr;
|
|
|
|
g_vm->glk_window_set_echo_stream(g_vm->gagt_main_window, nullptr);
|
|
|
|
gagt_normal_string("Glk transcript is now off.\n");
|
|
}
|
|
|
|
else if (strlen(argument) == 0) {
|
|
gagt_normal_string("Glk transcript is ");
|
|
gagt_normal_string(g_vm->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 (g_vm->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;
|
|
}
|
|
|
|
g_vm->gagt_inputlog_stream = g_vm->glk_stream_open_file(fileref,
|
|
filemode_WriteAppend, 0);
|
|
g_vm->glk_fileref_destroy(fileref);
|
|
if (!g_vm->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 (!g_vm->gagt_inputlog_stream) {
|
|
gagt_normal_string("Glk input logging is already off.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->glk_stream_close(g_vm->gagt_inputlog_stream, nullptr);
|
|
g_vm->gagt_inputlog_stream = nullptr;
|
|
|
|
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(g_vm->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 (g_vm->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;
|
|
}
|
|
|
|
g_vm->gagt_readlog_stream = g_vm->glk_stream_open_file(fileref, filemode_Read, 0);
|
|
g_vm->glk_fileref_destroy(fileref);
|
|
if (!g_vm->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 (!g_vm->gagt_readlog_stream) {
|
|
gagt_normal_string("Glk read log is already off.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->glk_stream_close(g_vm->gagt_readlog_stream, nullptr);
|
|
g_vm->gagt_readlog_stream = nullptr;
|
|
|
|
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(g_vm->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 (g_vm->gagt_abbreviations_enabled) {
|
|
gagt_normal_string("Glk abbreviation expansions are already on.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->gagt_abbreviations_enabled = TRUE;
|
|
gagt_normal_string("Glk abbreviation expansions are now on.\n");
|
|
}
|
|
|
|
else if (gagt_strcasecmp(argument, "off") == 0) {
|
|
if (!g_vm->gagt_abbreviations_enabled) {
|
|
gagt_normal_string("Glk abbreviation expansions are already off.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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(g_vm->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 g_vm->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 (g_vm->gagt_font_mode == FONT_FIXED_WIDTH) {
|
|
gagt_normal_string("Glk font control is already 'fixed'.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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 (g_vm->gagt_font_mode == FONT_PROPORTIONAL) {
|
|
gagt_normal_string("Glk font control is already 'proportional'.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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 (g_vm->gagt_font_mode == FONT_AUTOMATIC) {
|
|
gagt_normal_string("Glk font control is already 'automatic'.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->gagt_font_mode = FONT_AUTOMATIC;
|
|
gagt_normal_string("Glk font control is now 'automatic'.\n");
|
|
}
|
|
|
|
else if (gagt_strcasecmp(argument, "debug") == 0) {
|
|
if (g_vm->gagt_font_mode == FONT_DEBUG) {
|
|
gagt_normal_string("Glk font control is already 'debug'.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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 (g_vm->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 g_vm->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 (g_vm->gagt_delay_mode == DELAY_FULL) {
|
|
gagt_normal_string("Glk delay mode is already 'full'.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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 (g_vm->gagt_delay_mode == DELAY_SHORT) {
|
|
gagt_normal_string("Glk delay mode is already 'short'.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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 (g_vm->gagt_delay_mode == DELAY_OFF) {
|
|
gagt_normal_string("Glk delay mode is already 'none'.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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 (g_vm->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 (!g_vm->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 ");
|
|
Common::sprintf_s(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 (g_vm->gagt_replacement_enabled) {
|
|
gagt_normal_string("Glk replacements are already on.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->gagt_replacement_enabled = TRUE;
|
|
gagt_normal_string("Glk replacements are now on.\n");
|
|
}
|
|
|
|
else if (gagt_strcasecmp(argument, "off") == 0) {
|
|
if (!g_vm->gagt_replacement_enabled) {
|
|
gagt_normal_string("Glk replacements are already off.\n");
|
|
return;
|
|
}
|
|
|
|
g_vm->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(g_vm->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 (!g_vm->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 (g_vm->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(g_vm->gagt_status_window),
|
|
winmethod_Above | winmethod_Fixed, 2, nullptr);
|
|
g_vm->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 (!g_vm->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(g_vm->gagt_status_window),
|
|
winmethod_Above | winmethod_Fixed, 1, nullptr);
|
|
g_vm->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(g_vm->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];
|
|
|
|
Common::sprintf_s(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) {
|
|
g_vm->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(g_vm->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},
|
|
{nullptr, nullptr, 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 = nullptr;
|
|
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. */
|
|
size_t ln = strlen(string + posn) + 1 - 3 /*strlen("glk")*/;
|
|
string_copy = (char *)gagt_malloc(ln);
|
|
Common::strcpy_s(string_copy, ln, string + posn + 3 /*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 = nullptr;
|
|
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', nullptr}
|
|
};
|
|
|
|
|
|
/*
|
|
* 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 = nullptr;
|
|
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 (g_vm->gagt_readlog_stream) {
|
|
glui32 chars;
|
|
|
|
/* Get the next line from the log stream. */
|
|
chars = g_vm->glk_get_line_stream(g_vm->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(g_vm->gagt_readlog_stream, nullptr);
|
|
g_vm->gagt_readlog_stream = nullptr;
|
|
}
|
|
|
|
/* Set this up as a read buffer for the main window, and wait. */
|
|
g_vm->glk_request_line_event(g_vm->gagt_main_window, buffer, length - 1, 0);
|
|
gagt_event_wait(evtype_LineInput, &event);
|
|
if (g_vm->shouldQuit()) {
|
|
g_vm->glk_cancel_line_event(g_vm->gagt_main_window, &event);
|
|
return nullptr;
|
|
}
|
|
|
|
/* 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 (g_vm->gagt_abbreviations_enabled || g_vm->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 (g_vm->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 (g_vm->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 (g_vm->gagt_inputlog_stream) {
|
|
g_vm->glk_put_string_stream(g_vm->gagt_inputlog_stream, buffer);
|
|
g_vm->glk_put_char_stream(g_vm->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 (g_vm->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(g_vm->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(g_vm->gagt_readlog_stream, nullptr);
|
|
g_vm->gagt_readlog_stream = nullptr;
|
|
}
|
|
|
|
/*
|
|
* 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(g_vm->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 (g_vm->gagt_inputlog_stream)
|
|
g_vm->glk_put_string_stream(g_vm->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;
|
|
case evtype_Quit:
|
|
return;
|
|
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() {
|
|
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.
|
|
*/
|
|
g_vm->gagt_main_window = g_vm->glk_window_open(nullptr, 0, 0, wintype_TextBuffer, 0);
|
|
if (!g_vm->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(g_vm->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 = g_vm->gagt_extended_status_enabled ? 2 : 1;
|
|
g_vm->glk_stylehint_set(wintype_TextGrid, style_User1, stylehint_ReverseColor, 1);
|
|
g_vm->gagt_status_window = g_vm->glk_window_open(g_vm->gagt_main_window,
|
|
winmethod_Above | winmethod_Fixed,
|
|
status_height, wintype_TextGrid, 0);
|
|
if (g_vm->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();
|
|
}
|
|
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
/* 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(g_vm->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(g_vm->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();
|
|
|
|
|
|
/*
|
|
* 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(). The second is called
|
|
* from glk_main(), and does the real work of running the game.
|
|
*/
|
|
bool gagt_startup_code() {
|
|
/* Make the mandatory call for initialization. */
|
|
set_default_options();
|
|
|
|
/* All startup options were handled successfully. */
|
|
return TRUE;
|
|
}
|
|
|
|
static void gagt_main() {
|
|
fc_type fc;
|
|
|
|
/*
|
|
* 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 g_vm->gagt_main_window is not NULL.
|
|
*/
|
|
init_interface();
|
|
if (!g_vm->gagt_main_window) {
|
|
gagt_fatal("GLK: Can't open main window");
|
|
gagt_exit();
|
|
}
|
|
g_vm->glk_window_clear(g_vm->gagt_main_window);
|
|
g_vm->glk_set_window(g_vm->gagt_main_window);
|
|
g_vm->glk_set_style(style_Normal);
|
|
|
|
/*
|
|
* Create a game file context, and try to ensure it will open successfully
|
|
* in run_game().
|
|
*/
|
|
fc = init_file_context(g_vm->gagt_gamefile, fDA1);
|
|
if (!(gagt_workround_fileexist(fc, fAGX)
|
|
|| gagt_workround_fileexist(fc, fDA1))) {
|
|
if (g_vm->gagt_status_window)
|
|
g_vm->glk_window_close(g_vm->gagt_status_window, nullptr);
|
|
gagt_header_string("Glk AGiliTy Error\n\n");
|
|
gagt_normal_string("Can't find or open game '");
|
|
gagt_normal_string(g_vm->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 (g_vm->gagt_transcript_stream) {
|
|
g_vm->glk_stream_close(g_vm->gagt_transcript_stream, nullptr);
|
|
g_vm->gagt_transcript_stream = nullptr;
|
|
}
|
|
if (g_vm->gagt_inputlog_stream) {
|
|
g_vm->glk_stream_close(g_vm->gagt_inputlog_stream, nullptr);
|
|
g_vm->gagt_inputlog_stream = nullptr;
|
|
}
|
|
if (g_vm->gagt_readlog_stream) {
|
|
g_vm->glk_stream_close(g_vm->gagt_readlog_stream, nullptr);
|
|
g_vm->gagt_readlog_stream = nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
/* 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.
|
|
*/
|
|
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 (g_vm->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 (g_vm->gagt_main_window) {
|
|
g_vm->glk_cancel_char_event(g_vm->gagt_main_window);
|
|
g_vm->glk_cancel_line_event(g_vm->gagt_main_window, nullptr);
|
|
|
|
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(g_vm->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 (g_vm->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;
|
|
|
|
/*
|
|
* 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 (g_vm->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() {
|
|
assert(!gagt_startup_called);
|
|
gagt_startup_called = TRUE;
|
|
|
|
return gagt_startup_code();
|
|
}
|
|
|
|
} // End of namespace AGT
|
|
} // End of namespace Glk
|