diff options
author | Paul Gilbert | 2019-09-02 20:57:19 -0700 |
---|---|---|
committer | Paul Gilbert | 2019-09-25 20:13:26 -0700 |
commit | 60c860c6a6c37b95ab8265d658cbcc144d51153b (patch) | |
tree | e21dac197fcd374bb8d348873c91230141e1fddf /engines/glk/adrift | |
parent | c893c5a60b8298aa99ae1a47dea51c6f04c419bc (diff) | |
download | scummvm-rg350-60c860c6a6c37b95ab8265d658cbcc144d51153b.tar.gz scummvm-rg350-60c860c6a6c37b95ab8265d658cbcc144d51153b.tar.bz2 scummvm-rg350-60c860c6a6c37b95ab8265d658cbcc144d51153b.zip |
GLK: ADRIFT: Skeleton sub-engine commit
Diffstat (limited to 'engines/glk/adrift')
36 files changed, 46180 insertions, 0 deletions
diff --git a/engines/glk/adrift/adrift.cpp b/engines/glk/adrift/adrift.cpp new file mode 100644 index 0000000000..4f02b400e6 --- /dev/null +++ b/engines/glk/adrift/adrift.cpp @@ -0,0 +1,56 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/adrift.h" + +namespace Glk { +namespace Adrift { + +Adrift *g_vm = nullptr; + +Adrift::Adrift(OSystem *syst, const GlkGameDescription &gameDesc) : GlkAPI(syst, gameDesc) { + g_vm = this; +} + +void Adrift::runGame() { + // TODO: run + + deinitialize(); +} + +bool Adrift::initialize() { + return true; +} + +void Adrift::deinitialize() { +} + +Common::Error Adrift::readSaveData(Common::SeekableReadStream *rs) { + return Common::kNoError; +} + +Common::Error Adrift::writeGameData(Common::WriteStream *ws) { + return Common::kNoError; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/adrift.h b/engines/glk/adrift/adrift.h new file mode 100644 index 0000000000..81d7d30def --- /dev/null +++ b/engines/glk/adrift/adrift.h @@ -0,0 +1,84 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef GLK_ADRIFT_ADRIFT +#define GLK_ADRIFT_ADRIFT + +#include "common/scummsys.h" +#include "common/serializer.h" +#include "common/stack.h" +#include "glk/glk_api.h" + +namespace Glk { +namespace Adrift { + +/** + * Adrift game interpreter + */ +class Adrift : public GlkAPI { +private: + /** + * Initialization + */ + bool initialize(); + + /** + * Deinitialization + */ + void deinitialize(); +public: + /** + * Constructor + */ + Adrift(OSystem *syst, const GlkGameDescription &gameDesc); + + /** + * Run the game + */ + void runGame(); + + /** + * Returns the running interpreter type + */ + virtual InterpreterType getInterpreterType() const override { + return INTERPRETER_ADRIFT; + } + + /** + * Load a savegame from the passed Quetzal file chunk stream + */ + virtual Common::Error readSaveData(Common::SeekableReadStream *rs) override; + + /** + * Save the game. The passed write stream represents access to the UMem chunk + * in the Quetzal save file that will be created + */ + virtual Common::Error writeGameData(Common::WriteStream *ws) override; + +}; + +extern Adrift *g_vm; + +} // End of namespace Alan2 +} // End of namespace Glk + +#endif diff --git a/engines/glk/adrift/os_glk.cpp b/engines/glk/adrift/os_glk.cpp new file mode 100644 index 0000000000..98f4f34169 --- /dev/null +++ b/engines/glk/adrift/os_glk.cpp @@ -0,0 +1,3565 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/adrift.h" +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/glk.h" +#include "glk/streams.h" +#include "glk/windows.h" +#include "common/textconsole.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o The Glk interface makes no effort to set text colors, background colors, + * and so forth, and minimal effort to set fonts and other style effects. + */ + +#undef _WIN32 /* Gargoyle */ + +/* + * True and false definitions -- usually defined in glkstart.h, but we need + * them early, so we'll define them here too. We also need nullptr, 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 other stuff */ +/*---------------------------------------------------------------------*/ + +/* Glk SCARE interface version number. */ +static const glui32 GSC_PORT_VERSION = 0x00010310; + +/* Two windows, one for the main text, and one for a status line. */ +static winid_t gsc_main_window = nullptr, + gsc_status_window = nullptr; + +/* + * Transcript stream and input log. These are nullptr if there is no current + * collection of these strings. + */ +static strid_t gsc_transcript_stream = nullptr, + gsc_inputlog_stream = nullptr; + +/* Input read log stream, for reading back an input log. */ +static strid_t gsc_readlog_stream = nullptr; + +/* Options that may be turned off or set by command line flags. */ +static int gsc_commands_enabled = TRUE, + gsc_abbreviations_enabled = TRUE, + gsc_unicode_enabled = TRUE; + +/* Adrift game to interpret. */ +static sc_game gsc_game = nullptr; + +/* Special out-of-band os_confirm() options used locally with os_glk. */ +static const sc_int GSC_CONF_SUBTLE_HINT = INT_MAX, + GSC_CONF_UNSUBTLE_HINT = INT_MAX - 1, + GSC_CONF_CONTINUE_HINTS = INT_MAX - 2; + +/* Forward declaration of event wait functions, and a short delay. */ +static void gsc_event_wait_2 (glui32 wait_type_1, + glui32 wait_type_2, event_t *event); +static void gsc_event_wait (glui32 wait_type, event_t *event); +static void gsc_short_delay(); + + +/*---------------------------------------------------------------------*/ +/* Glk port utility functions */ +/*---------------------------------------------------------------------*/ + +/* + * gsc_fatal() + * + * Fatal error handler. The function returns, expecting the caller to + * abort() or otherwise handle the error. + */ +static void +gsc_fatal (const char *string) +{ + /* + * If the failure happens too early for us to have a window, print + * the message to stderr. + */ + if (!gsc_main_window) { + error("\n\nINTERNAL ERROR: %s\n", string); + } + + /* Cancel all possible pending window input events. */ + g_vm->glk_cancel_line_event (gsc_main_window, nullptr); + g_vm->glk_cancel_char_event (gsc_main_window); + + /* Print a message indicating the error, and exit. */ + g_vm->glk_set_window (gsc_main_window); + g_vm->glk_set_style (style_Normal); + g_vm->glk_put_string ("\n\nINTERNAL ERROR: "); + g_vm->glk_put_string ((char *) 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"); +} + + +/* + * gsc_malloc() + * + * Non-failing malloc; call gsc_fatal and exit if memory allocation fails. + */ +static void * +gsc_malloc (size_t size) +{ + void *pointer; + + pointer = malloc (size > 0 ? size : 1); + if (!pointer) + { + gsc_fatal ("GLK: Out of system memory"); + g_vm->glk_exit (); + } + + return pointer; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port locale data */ +/*---------------------------------------------------------------------*/ + +/* Unicode values up to 256 are equivalent to iso 8859-1. */ +static const glui32 GSC_ISO_8859_EQUIVALENCE = 256; + +/* + * Lookup table pair for converting a given single character into unicode and + * iso 8859-1 (the lower byte of the unicode representation, assuming an upper + * byte of zero), and an ascii substitute should nothing else be available. + * Tables are 256 elements; although the first 128 characters of a codepage + * are usually standard ascii, making tables full-sized allows for support of + * codepages where they're not (dingbats, for example). + */ +enum { GSC_TABLE_SIZE = 256 }; +typedef struct { + const glui32 unicode[GSC_TABLE_SIZE]; + const sc_char *const ascii[GSC_TABLE_SIZE]; +} gsc_codepages_t; + +/* + * Locale contains a name and a pair of codepage structures, a main one and + * an alternate. The latter is intended for monospaced output. + */ +typedef struct { + const sc_char *const name; + const gsc_codepages_t main; + const gsc_codepages_t alternate; +} gsc_locale_t; + + +/* + * Locale for Latin1 -- cp1252 and cp850. + * + * The ascii representations of characters in this table are based on the + * general look of the characters, rather than pronounciation. Accented + * characters are generally rendered unaccented, and box drawing, shading, + * and other non-alphanumeric glyphs as either a similar shape, or as a + * character that might be recognizable as what it's trying to emulate. + */ +static const gsc_locale_t GSC_LATIN1_LOCALE = { + "Latin1", + /* cp1252 to unicode. */ +{ { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x000a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, + 0x002d, 0x002e, 0x002f, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, + 0x0036, 0x0037, 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, + 0x003f, 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, 0x0050, + 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, + 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0061, 0x0062, + 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, + 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, + 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, + 0x007e, 0x0000, 0x20ac, 0x0000, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, + 0x2021, 0x02c6, 0x2030, 0x0160, 0x2039, 0x0152, 0x0000, 0x017d, 0x0000, + 0x0000, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, 0x02dc, + 0x2122, 0x0161, 0x203a, 0x0153, 0x0000, 0x017e, 0x0178, 0x00a0, 0x00a1, + 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7, 0x00a8, 0x00a9, 0x00aa, + 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x00af, 0x00b0, 0x00b1, 0x00b2, 0x00b3, + 0x00b4, 0x00b5, 0x00b6, 0x00b7, 0x00b8, 0x00b9, 0x00ba, 0x00bb, 0x00bc, + 0x00bd, 0x00be, 0x00bf, 0x00c0, 0x00c1, 0x00c2, 0x00c3, 0x00c4, 0x00c5, + 0x00c6, 0x00c7, 0x00c8, 0x00c9, 0x00ca, 0x00cb, 0x00cc, 0x00cd, 0x00ce, + 0x00cf, 0x00d0, 0x00d1, 0x00d2, 0x00d3, 0x00d4, 0x00d5, 0x00d6, 0x00d7, + 0x00d8, 0x00d9, 0x00da, 0x00db, 0x00dc, 0x00dd, 0x00de, 0x00df, 0x00e0, + 0x00e1, 0x00e2, 0x00e3, 0x00e4, 0x00e5, 0x00e6, 0x00e7, 0x00e8, 0x00e9, + 0x00ea, 0x00eb, 0x00ec, 0x00ed, 0x00ee, 0x00ef, 0x00f0, 0x00f1, 0x00f2, + 0x00f3, 0x00f4, 0x00f5, 0x00f6, 0x00f7, 0x00f8, 0x00f9, 0x00fa, 0x00fb, + 0x00fc, 0x00fd, 0x00fe, 0x00ff }, + /* cp1252 to ascii. */ + { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, " ", + "\n", nullptr, nullptr, "\n", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", + "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", + "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", + "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", + "x", "y", "z", "{", "|", "}", "~", nullptr, "E", nullptr, ",", + "f", ",,", "...", "+", "#", "^", "%", "S", "<", "OE", nullptr, + "Z", nullptr, nullptr, "'", "'", "\"", "\"", "*", "-", "-", "~", + "[TM]","s", ">", "oe", nullptr, "z", "Y", " ", "!", "c", "GBP", + "*", "Y", "|", "S", "\"", "(C)", "a", "<<", "-", "-", "(R)", + "-", "o", "+/-", "2", "3", "'", "u", "P", "*", ",", "1", + "o", ">>", "1/4", "1/2", "3/4", "?", "A", "A", "A", "A", "A", + "A", "AE", "C", "E", "E", "E", "E", "I", "I", "I", "I", + "D", "N", "O", "O", "O", "O", "O", "x", "O", "U", "U", + "U", "U", "Y", "p", "ss", "a", "a", "a", "a", "a", "a", + "ae", "c", "e", "e", "e", "e", "i", "i", "i", "i", "d", + "n", "o", "o", "o", "o", "o", "/", "o", "u", "u", "u", + "u", "y", "P", "y" } }, + /* cp850 to unicode. */ +{ { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x000a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, + 0x002d, 0x002e, 0x002f, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, + 0x0036, 0x0037, 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, + 0x003f, 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, 0x0050, + 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, + 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0061, 0x0062, + 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, + 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, + 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, + 0x007e, 0x0000, 0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0, 0x00e5, + 0x00e7, 0x00ea, 0x00eb, 0x00e8, 0x00ef, 0x00ee, 0x00ec, 0x00c4, 0x00c5, + 0x00c9, 0x00e6, 0x00c6, 0x00f4, 0x00f6, 0x00f2, 0x00fb, 0x00f9, 0x00ff, + 0x00d6, 0x00dc, 0x00f8, 0x00a3, 0x00d8, 0x00d7, 0x0192, 0x00e1, 0x00ed, + 0x00f3, 0x00fa, 0x00f1, 0x00d1, 0x00aa, 0x00ba, 0x00bf, 0x00ae, 0x00ac, + 0x00bd, 0x00bc, 0x00a1, 0x00ab, 0x00bb, 0x2591, 0x2592, 0x2593, 0x2502, + 0x2524, 0x00c1, 0x00c2, 0x00c0, 0x00a9, 0x2563, 0x2551, 0x2557, 0x255d, + 0x00a2, 0x00a5, 0x2510, 0x2514, 0x2534, 0x252c, 0x251c, 0x2500, 0x253c, + 0x00e3, 0x00c3, 0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256c, + 0x00a4, 0x00f0, 0x00d0, 0x00ca, 0x00cb, 0x00c8, 0x0131, 0x00cd, 0x00ce, + 0x00cf, 0x2518, 0x250c, 0x2588, 0x2584, 0x00a6, 0x00cc, 0x2580, 0x00d3, + 0x00df, 0x00d4, 0x00d2, 0x00f5, 0x00d5, 0x00b5, 0x00fe, 0x00de, 0x00da, + 0x00db, 0x00d9, 0x00fd, 0x00dd, 0x00af, 0x00b4, 0x00ad, 0x00b1, 0x2017, + 0x00be, 0x00b6, 0x00a7, 0x00f7, 0x00b8, 0x00b0, 0x00a8, 0x00b7, 0x00b9, + 0x00b3, 0x00b2, 0x25a0, 0x00a0 }, + /* cp850 to ascii. */ + { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, " ", + "\n", nullptr, nullptr, "\n", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", + "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", + "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", + "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", + "x", "y", "z", "{", "|", "}", "~", nullptr, "C", "u", "e", + "a", "a", "a", "a", "c", "e", "e", "e", "i", "i", "i", + "A", "A", "E", "ae", "AE", "o", "o", "o", "u", "u", "y", + "O", "U", "o", "GBP", "O", "x", "f", "a", "i", "o", "u", + "n", "N", "a", "o", "?", "(R)", "-", "1/2", "1/4", "i", "<<", + ">>", "#", "#", "#", "|", "+", "A", "A", "A", "(C)", "+", + "|", "+", "+", "c", "Y", "+", "+", "+", "+", "+", "-", + "+", "a", "A", "+", "+", "+", "+", "+", "=", "+", "*", + "d", "D", "E", "E", "E", "i", "I", "I", "I", "+", "+", + ".", ".", "|", "I", ".", "O", "ss", "O", "O", "o", "O", + "u", "p", "P", "U", "U", "U", "y", "Y", "-", "'", "-", + "+/-", "=", "3/4", "P", "S", "/", ",", "deg", "\"", "*", "1", + "3", "2", ".", " " } } +}; + + +/* + * Locale for Cyrillic -- cp1251 and cp866. + * + * The ascii representations in this table, for alphabetic characters, follow + * linguistic rather than appearance rules, the essence of gost 16876-71. + * Capitalized cyrillic letters that translate to multiple ascii characters + * have the first ascii character only of the sequence translated. This gives + * the best appearance in normal sentences, but is not optimal in a run of + * all capitals (headings, for example). For non-alphanumeric characters, + * the general appearance and shape of the character being emulated is used. + */ +static const gsc_locale_t GSC_CYRILLIC_LOCALE = { + "Cyrillic", + /* cp1251 to unicode. */ +{ { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x000a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, + 0x002d, 0x002e, 0x002f, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, + 0x0036, 0x0037, 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, + 0x003f, 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, 0x0050, + 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, + 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0061, 0x0062, + 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, + 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, + 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, + 0x007e, 0x0000, 0x0402, 0x0403, 0x201a, 0x0453, 0x201e, 0x2026, 0x2020, + 0x2021, 0x20ac, 0x2030, 0x0409, 0x2039, 0x040a, 0x040c, 0x040b, 0x040f, + 0x0452, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, 0x0000, + 0x2122, 0x0459, 0x203a, 0x045a, 0x045c, 0x045b, 0x045f, 0x00a0, 0x040e, + 0x045e, 0x0408, 0x00a4, 0x0490, 0x00a6, 0x00a7, 0x0401, 0x00a9, 0x0404, + 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0407, 0x00b0, 0x00b1, 0x0406, 0x0456, + 0x0491, 0x00b5, 0x00b6, 0x00b7, 0x0451, 0x2116, 0x0454, 0x00bb, 0x0458, + 0x0405, 0x0455, 0x0457, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, + 0x0416, 0x0417, 0x0418, 0x0419, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, + 0x041f, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, + 0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f, 0x0430, + 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, + 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f, 0x0440, 0x0441, 0x0442, + 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044a, 0x044b, + 0x044c, 0x044d, 0x044e, 0x044f }, + /* cp1251 to gost 16876-71 ascii. */ + { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, " ", + "\n", nullptr, nullptr, "\n", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", + "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", + "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", + "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", + "x", "y", "z", "{", "|", "}", "~", nullptr, nullptr, nullptr, ",", + nullptr, ",,", "...", "+", "#", "E", "%", nullptr, "<", nullptr, nullptr, + nullptr, nullptr, nullptr, "'", "'", "\"", "\"", "*", "-", "-", nullptr, + "[TM]",nullptr, ">", nullptr, nullptr, nullptr, nullptr, " ", nullptr, nullptr, nullptr, + "*", "G", "|", "S", "Jo", "(C)", "Je", "<<", "-", "-", "(R)", + "Ji", "o", "+/-", "I", "i", "g", "u", "P", "*", "jo", nullptr, + "je", ">>", "j", "S", "s", "ji", "A", "B", "V", "G", "D", + "E", "Zh", "Z", "I", "Jj", "K", "L", "M", "N", "O", "P", + "R", "S", "T", "U", "F", "Kh", "C", "Ch", "Sh", "Shh", "\"", + "Y", "'", "Eh", "Ju", "Ja", "a", "b", "v", "g", "d", "e", + "zh", "z", "i", "jj", "k", "l", "m", "n", "o", "p", "r", + "s", "t", "u", "f", "kh", "c", "ch", "sh", "shh", "\"", "y", + "'", "eh", "ju", "ja" } }, + /* cp866 to unicode. */ +{ { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x000a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, + 0x002d, 0x002e, 0x002f, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, + 0x0036, 0x0037, 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, + 0x003f, 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, 0x0050, + 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, + 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0061, 0x0062, + 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, + 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, + 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, + 0x007e, 0x0000, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, + 0x0417, 0x0418, 0x0419, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, + 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f, 0x0430, 0x0431, + 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043a, + 0x043b, 0x043c, 0x043d, 0x043e, 0x043f, 0x2591, 0x2592, 0x2593, 0x2502, + 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255d, + 0x255c, 0x255b, 0x2510, 0x2514, 0x2534, 0x252c, 0x251c, 0x2500, 0x253c, + 0x255e, 0x255f, 0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256c, + 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256b, + 0x256a, 0x2518, 0x250c, 0x2588, 0x2584, 0x258c, 0x2590, 0x2580, 0x0440, + 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, + 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f, 0x0401, 0x0451, 0x0404, + 0x0454, 0x0407, 0x0457, 0x040e, 0x045e, 0x00b0, 0x2022, 0x00b7, 0x221a, + 0x2116, 0x00a4, 0x25a0, 0x00a0 }, + /* cp866 to gost 16876-71 ascii. */ + { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, " ", + "\n", nullptr, nullptr, "\n", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", + "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", + "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", + "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", + "x", "y", "z", "{", "|", "}", "~", nullptr, "A", "B", "V", + "G", "D", "E", "Zh", "Z", "I", "Jj", "K", "L", "M", "N", + "O", "P", "R", "S", "T", "U", "F", "Kh", "C", "Ch", "Sh", + "Shh", "\"", "Y", "'", "Eh", "Ju", "Ja", "a", "b", "v", "g", + "d", "e", "zh", "z", "i", "jj", "k", "l", "m", "n", "o", + "p", "#", "#", "#", "|", "+", "+", "+", "+", "+", "+", + "|", "+", "+", "+", "+", "+", "+", "+", "+", "+", "-", + "+", "+", "+", "+", "+", "+", "+", "+", "|", "+", "+", + "+", "+", "+", "+", "+", "+", "+", "+", "+", "+", "+", + "+", ".", ".", ".", ".", "r", "s", "t", "u", "f", "kh", + "c", "ch", "sh", "shh", "\"", "y", "'", "eh", "ju", "ja", "Jo", + "jo", "Je", "je", "Ji", "ji", nullptr, nullptr, "deg", "*", "*", nullptr, + nullptr, "*", ".", " " } } +}; + + +/*---------------------------------------------------------------------*/ +/* Glk port locale control and conversion functions */ +/*---------------------------------------------------------------------*/ + +#ifdef GLK_MODULE_UNICODE +static const sc_bool gsc_has_unicode = TRUE; +#else +static const sc_bool gsc_has_unicode = FALSE; + +/* Gestalt selector and stubs for non-unicode capable libraries. */ +static const glui32 gestalt_Unicode = 15; + +static void glk_put_char_uni (glui32 ch) +{ + glui32 unused; + unused = ch; + gsc_fatal ("GLK: Stub unicode function called"); +} + +static void glk_request_line_event_uni (winid_t win, + glui32 *buf, glui32 maxlen, glui32 initlen) +{ + winid_t unused1; + glui32 *unused2; + glui32 unused3, unused4; + unused1 = win; + unused2 = buf; + unused3 = maxlen; + unused4 = initlen; + gsc_fatal ("GLK: Stub unicode function called"); +} + +#endif + +/* + * Known valid character printing range. Some Glk libraries aren't accurate + * about what will and what won't print when queried with g_vm->glk_gestalt(), so + * we also make an explicit range check against guaranteed to print chars. + */ +static const glui32 GSC_MIN_PRINTABLE = ' ', + GSC_MAX_PRINTABLE = '~'; + + +/* List of pointers to supported and available locales, nullptr terminated. */ +static const gsc_locale_t *const GSC_AVAILABLE_LOCALES[] = { + &GSC_LATIN1_LOCALE, + &GSC_CYRILLIC_LOCALE, + nullptr +}; + +/* + * The locale for the game, set below explicitly or on game startup, and + * a fallback locale to use in case none has been set. + */ +static const gsc_locale_t *gsc_locale = nullptr; +static const gsc_locale_t *const gsc_fallback_locale = &GSC_LATIN1_LOCALE; + + +/* + * gsc_set_locale() + * + * Set a locale explicitly from the name passed in. + */ +static void +gsc_set_locale (const sc_char *name) +{ + const gsc_locale_t *matched = nullptr; + const gsc_locale_t *const *iterator; + assert (name); + + /* + * Search locales for a matching name, abbreviated if necessary. Stop on + * the first match found. + */ + for (iterator = GSC_AVAILABLE_LOCALES; *iterator; iterator++) + { + const gsc_locale_t *const locale = *iterator; + + if (sc_strncasecmp (name, locale->name, strlen (name)) == 0) + { + matched = locale; + break; + } + } + + /* If matched, set the global locale. */ + if (matched) + gsc_locale = matched; +} + + +/* + * gsc_put_char_uni() + * + * Wrapper around g_vm->glk_put_char_uni(). Handles, inelegantly, the problem of + * having to write transcripts as ascii. + */ +static void +gsc_put_char_uni (glui32 unicode, const char *ascii) +{ + /* If there is an transcript stream, temporarily disconnect it. */ + if (gsc_transcript_stream) + g_vm->glk_window_set_echo_stream (gsc_main_window, nullptr); + + g_vm->glk_put_char_uni (unicode); + + /* Print ascii to the transcript, then reattach it. */ + if (gsc_transcript_stream) + { + if (ascii) + g_vm->glk_put_string_stream (gsc_transcript_stream, (char *) ascii); + else + g_vm->glk_put_char_stream (gsc_transcript_stream, '?'); + + g_vm->glk_window_set_echo_stream (gsc_main_window, gsc_transcript_stream); + } +} + + +/* + * gsc_put_char_locale() + * + * Write a single character using the supplied locale. Select either the + * main or the alternate codepage depending on the flag passed in. + */ +static void +gsc_put_char_locale (sc_char ch, + const gsc_locale_t *locale, sc_bool is_alternate) +{ + const gsc_codepages_t *codepage; + unsigned char character; + glui32 unicode; + const char *ascii; + + /* + * Select either the main or the alternate codepage for this locale, and + * retrieve the unicode and ascii representations of the character. + */ + codepage = is_alternate ? &locale->alternate : &locale->main; + character = (unsigned char) ch; + unicode = codepage->unicode[character]; + ascii = codepage->ascii[character]; + + /* + * If a unicode representation exists, use for either iso 8859-1 or, if + * possible, direct unicode output. + */ + if (unicode > 0) + { + /* + * If unicode is in the range 1-255, this value is directly equivalent + * to the iso 8859-1 representation; otherwise the character has no + * direct iso 8859-1 glyph. + */ + if (unicode < GSC_ISO_8859_EQUIVALENCE) + { + /* + * If the iso 8859-1 character is one that this Glk library will + * print exactly, print and return. We add a check here for the + * guaranteed printable characters, since some Glk libraries don't + * return the correct values for gestalt_CharOutput for these. + */ + if (unicode == '\n' + || (unicode >= GSC_MIN_PRINTABLE && unicode <= GSC_MAX_PRINTABLE) + || g_vm->glk_gestalt (gestalt_CharOutput, + unicode) == gestalt_CharOutput_ExactPrint) + { + g_vm->glk_put_char ((unsigned char) unicode); + return; + } + } + + /* + * If no usable iso 8859-1 representation, see if unicode is enabled and + * if the Glk library can print the character exactly. If yes, output + * the character that way. + * + * TODO Using unicode output currently disrupts transcript output. Any + * echo stream connected for a transcript here will be a text rather than + * a unicode stream, so probably won't output the character correctly. + * For now, if there's a transcript, we try to write ascii output. + */ + if (gsc_unicode_enabled) + { + if (g_vm->glk_gestalt (gestalt_CharOutput, + unicode) == gestalt_CharOutput_ExactPrint) + { + gsc_put_char_uni (unicode, ascii); + return; + } + } + } + + /* + * No success with iso 8859-1 or unicode, so try for an ascii substitute. + * Substitute strings use only 7-bit ascii, and so all are safe to print + * directly with Glk. + */ + if (ascii) + { + g_vm->glk_put_string ((char *) ascii); + return; + } + + /* No apparent way to output this character, so print a '?'. */ + g_vm->glk_put_char ('?'); +} + + +/* + * gsc_put_char() + * gsc_put_char_alternate() + * gsc_put_buffer_using() + * gsc_put_buffer() + * gsc_put_string() + * gsc_put_string_alternate() + * + * Public functions for writing using the current or fallback locale. + */ +static void +gsc_put_char (const sc_char character) +{ + const gsc_locale_t *locale; + + locale = gsc_locale ? gsc_locale : gsc_fallback_locale; + gsc_put_char_locale (character, locale, FALSE); +} + +static void +gsc_put_char_alternate (const sc_char character) +{ + const gsc_locale_t *locale; + + locale = gsc_locale ? gsc_locale : gsc_fallback_locale; + gsc_put_char_locale (character, locale, TRUE); +} + +static void +gsc_put_buffer_using (const sc_char *buffer, + sc_int length, void (*putchar_function) (sc_char)) +{ + sc_int index_; + + for (index_ = 0; index_ < length; index_++) + putchar_function (buffer[index_]); +} + +static void +gsc_put_buffer (const sc_char *buffer, sc_int length) +{ + assert (buffer); + + gsc_put_buffer_using (buffer, length, gsc_put_char); +} + +static void +gsc_put_string (const sc_char *string) +{ + assert (string); + + gsc_put_buffer_using (string, strlen (string), gsc_put_char); +} + +static void +gsc_put_string_alternate (const sc_char *string) +{ + assert (string); + + gsc_put_buffer_using (string, strlen (string), gsc_put_char_alternate); +} + + +/* + * gsc_unicode_to_locale() + * gsc_unicode_buffer_to_locale() + * + * Convert a unicode character back to an sc_char through a locale. Used for + * reverse translations in line input. Returns '?' if there is no translation + * available. + */ +static sc_char +gsc_unicode_to_locale (glui32 unicode, const gsc_locale_t *locale) +{ + const gsc_codepages_t *codepage; + sc_int character; + + /* Always use the main codepage for input. */ + codepage = &locale->main; + + /* + * Search the unicode table sequentially for the input character. This is + * inefficient, but because game input is usually not copious, excusable. + */ + for (character = 0; character < GSC_TABLE_SIZE; character++) + { + if (codepage->unicode[character] == unicode) + break; + } + + /* Return the character translation, or '?' if none. */ + return character < GSC_TABLE_SIZE ? (sc_char) character : '?'; +} + +static void +gsc_unicode_buffer_to_locale (const glui32 *unicode, sc_int length, + sc_char *buffer, const gsc_locale_t *locale) +{ + sc_int index_; + + for (index_ = 0; index_ < length; index_++) + buffer[index_] = gsc_unicode_to_locale (unicode[index_], locale); +} + + +/* + * gsc_read_line_locale() + * + * Read in a line and translate out of the given locale. Returns the count + * of characters placed in the buffer. + */ +static sc_int +gsc_read_line_locale (sc_char *buffer, + sc_int length, const gsc_locale_t *locale) +{ + event_t event; + + /* + * If we have unicode, we have to use it to ensure that characters not in + * the Latin1 locale are properly translated. + */ + if (gsc_unicode_enabled) + { + glui32 *unicode; + + /* + * Allocate a unicode buffer long enough to hold all the characters, + * then read in a unicode line. + */ + unicode = (glui32 *)gsc_malloc (length * sizeof (*unicode)); + g_vm->glk_request_line_event_uni (gsc_main_window, unicode, length, 0); + gsc_event_wait (evtype_LineInput, &event); + + /* Convert the unicode buffer out, then free it. */ + gsc_unicode_buffer_to_locale (unicode, event.val1, buffer, locale); + free (unicode); + + /* Return the count of characters placed in the buffer. */ + return event.val1; + } + + /* No success with unicode, so fall back to standard line input. */ + g_vm->glk_request_line_event (gsc_main_window, buffer, length, 0); + gsc_event_wait (evtype_LineInput, &event); + + /* Return the count of characters placed in the buffer. */ + return event.val1; +} + + +/* + * gsc_read_line() + * + * Public function for reading using the current or fallback locale. + */ +static sc_int +gsc_read_line (sc_char *buffer, sc_int length) +{ + const gsc_locale_t *locale; + + locale = gsc_locale ? gsc_locale : gsc_fallback_locale; + return gsc_read_line_locale (buffer, length, locale); +} + + +/*---------------------------------------------------------------------*/ +/* Glk port status line functions */ +/*---------------------------------------------------------------------*/ + +/* + * Slop for right-justification of status lines, as an attempt to compensate + * for the fact that some characters in a games status line may use more than + * one position when printed, a particular problem in gost 16876-71 Cyrillic. + */ +static const sc_int GSC_STATUS_SLOP = 10; + +/* Size of saved status buffer used for non-windowing Glk status lines. */ +enum { GSC_STATUS_BUFFER_LENGTH = 74 }; + +/* Whitespace characters, used to detect empty status elements. */ +static const sc_char *const GSC_WHITESPACE = "\t\n\v\f\r "; + + +/* + * gsc_is_string_usable() + * + * Return TRUE if string is non-null, not zero-length or contains characters + * other than whitespace. + */ +static sc_bool +gsc_is_string_usable (const sc_char *string) +{ + /* If non-null, scan for any non-space character. */ + if (string) + { + sc_int index_; + + for (index_ = 0; string[index_] != '\0'; index_++) + { + if (!strchr (GSC_WHITESPACE, string[index_])) + return TRUE; + } + } + + /* nullptr, or no characters other than whitespace. */ + return FALSE; +} + + +/* + * gsc_status_update() + * + * Update the status line from the current game state. This is for windowing + * Glk libraries. + */ +static void +gsc_status_update() +{ + glui32 width, height; + uint index; + assert (gsc_status_window); + + g_vm->glk_window_get_size (gsc_status_window, &width, &height); + if (height > 0) + { + const sc_char *room; + + g_vm->glk_window_clear (gsc_status_window); + g_vm->glk_window_move_cursor (gsc_status_window, 0, 0); + g_vm->glk_set_window (gsc_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 (gsc_status_window, 0, 0); + + /* See if the game is indicating any current player room. */ + room = sc_get_game_room (gsc_game); + if (!gsc_is_string_usable (room)) + { + /* + * Player location is indeterminate, so print out a generic status, + * showing the game name and author. + */ + g_vm->glk_window_move_cursor (gsc_status_window, 1, 0); + gsc_put_string (sc_get_game_name (gsc_game)); + g_vm->glk_put_string (" | "); + gsc_put_string (sc_get_game_author (gsc_game)); + } + else + { + const sc_char *status; + char score[64]; + + /* Print the player location. */ + g_vm->glk_window_move_cursor (gsc_status_window, 1, 0); + gsc_put_string (room); + + /* Get the game's status line, or if none, format score. */ + status = sc_get_game_status_line (gsc_game); + if (!gsc_is_string_usable (status)) + { + sprintf (score, "Score: %ld", sc_get_game_score (gsc_game)); + status = score; + } + + /* Print the status line or score at window right, if it fits. */ + if (width > strlen (status) + GSC_STATUS_SLOP + 1) + { + glui32 position; + + position = width - strlen (status) - GSC_STATUS_SLOP; + g_vm->glk_window_move_cursor (gsc_status_window, position - 1, 0); + gsc_put_string (status); + } + } + + g_vm->glk_set_window (gsc_main_window); + } +} + + +/* + * gsc_status_safe_strcat() + * + * Helper for gsc_status_print(), concatenates strings only up to the + * available length. + */ +static void +gsc_status_safe_strcat (char *dest, size_t length, const char *src) +{ + size_t available, src_length; + + /* Append only as many characters as will fit. */ + src_length = strlen (src); + available = length - strlen (dest) - 1; + if (available > 0) + strncat (dest, src, src_length < available ? src_length : available); +} + + +/* + * gsc_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. + */ +static void +gsc_status_print() +{ + static char current_status[GSC_STATUS_BUFFER_LENGTH + 1]; + + const sc_char *room; + + /* Do nothing if the game isn't indicating any current player room. */ + room = sc_get_game_room (gsc_game); + if (gsc_is_string_usable (room)) + { + char buffer[GSC_STATUS_BUFFER_LENGTH + 1]; + const sc_char *status; + char score[64]; + + /* Make an attempt at a status line, starting with player location. */ + strcpy (buffer, ""); + gsc_status_safe_strcat (buffer, sizeof (buffer), room); + + /* Get the game's status line, or if none, format score. */ + status = sc_get_game_status_line (gsc_game); + if (!gsc_is_string_usable (status)) + { + sprintf (score, "Score: %ld", sc_get_game_score (gsc_game)); + status = score; + } + + /* Append the status line or score. */ + gsc_status_safe_strcat (buffer, sizeof (buffer), " | "); + gsc_status_safe_strcat (buffer, sizeof (buffer), status); + + /* If this matches the current saved status line, do nothing more. */ + if (strcmp (buffer, current_status) != 0) + { + /* Bracket, and output the status line buffer. */ + g_vm->glk_put_string ("[ "); + gsc_put_string (buffer); + g_vm->glk_put_string (" ]\n"); + + /* Save the details of the printed status buffer. */ + strcpy (current_status, buffer); + } + } +} + + +/* + * gsc_status_notify() + * + * Front end function for updating status. Either updates the status window + * or prints the status line to the main window. + */ +static void +gsc_status_notify() +{ + if (gsc_status_window) + gsc_status_update (); + else + gsc_status_print (); +} + + +/* + * gsc_status_redraw() + * + * Redraw the contents of any status window with the constructed status string. + * This function should be called on the appropriate Glk window resize and + * arrange events. + */ +static void +gsc_status_redraw() +{ + if (gsc_status_window) + { + winid_t parent; + + /* + * 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 (gsc_status_window); + g_vm->glk_window_set_arrangement (parent, + winmethod_Above | winmethod_Fixed, 1, nullptr); + gsc_status_update (); + } +} + + +/*---------------------------------------------------------------------*/ +/* 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 gsc_help_requested = FALSE, + gsc_help_hints_silenced = FALSE; + +/* Font descriptor type, encapsulating size and monospaced boolean. */ +typedef struct { + sc_bool is_monospaced; + sc_int size; +} gsc_font_size_t; + +/* Font stack and attributes for nesting tags. */ +enum { GSC_MAX_STYLE_NESTING = 32 }; +static gsc_font_size_t gsc_font_stack[GSC_MAX_STYLE_NESTING]; +static glui32 gsc_font_index = 0; +static glui32 gsc_attribute_bold = 0, + gsc_attribute_italic = 0, + gsc_attribute_underline = 0, + gsc_attribute_secondary_color = 0; + +/* Notional default font size, and limit font sizes. */ +static const sc_int GSC_DEFAULT_FONT_SIZE = 12, + GSC_MEDIUM_FONT_SIZE = 14, + GSC_LARGE_FONT_SIZE = 16; + +/* Milliseconds per second and timeouts count for delay tags. */ +static const glui32 GSC_MILLISECONDS_PER_SECOND = 1000; +static const glui32 GSC_TIMEOUTS_COUNT = 10; + +/* Number of hints to refuse before offering to end hint display. */ +static const sc_int GSC_HINT_REFUSAL_LIMIT = 5; + +/* The keypresses used to cancel any <wait x.x> early. */ +static const glui32 GSC_CANCEL_WAIT_1 = ' ', + GSC_CANCEL_WAIT_2 = keycode_Return; + + +/* + * gsc_output_register_help_request() + * gsc_output_silence_help_hints() + * gsc_output_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 +gsc_output_register_help_request() +{ + gsc_help_requested = TRUE; +} + +static void +gsc_output_silence_help_hints() +{ + gsc_help_hints_silenced = TRUE; +} + +static void +gsc_output_provide_help_hint() +{ + if (gsc_help_requested && !gsc_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"); + + gsc_help_requested = FALSE; + g_vm->glk_set_style (style_Normal); + } +} + + +/* + * gsc_set_glk_style() + * + * Set a Glk style based on the top of the font stack and attributes. + */ +static void gsc_set_glk_style() +{ + sc_bool is_monospaced; + sc_int font_size; + + /* Get the current font stack top, or default value. */ + if (gsc_font_index > 0) + { + is_monospaced = gsc_font_stack[gsc_font_index - 1].is_monospaced; + font_size = gsc_font_stack[gsc_font_index - 1].size; + } + else + { + is_monospaced = FALSE; + font_size = GSC_DEFAULT_FONT_SIZE; + } + + /* + * Map the font and current attributes into a Glk style. Because Glk styles + * aren't cumulative this has to be done by precedences. + */ + if (is_monospaced) + { + /* + * No matter the size or attributes, if monospaced use Preformatted + * style, as it's all we have. + */ + g_vm->glk_set_style (style_Preformatted); + } + else + { + /* + * For large and medium point sizes, use Header or Subheader styles + * respectively. + */ + if (font_size >= GSC_LARGE_FONT_SIZE) + g_vm->glk_set_style (style_Header); + else if (font_size >= GSC_MEDIUM_FONT_SIZE) + g_vm->glk_set_style (style_Subheader); + else + { + /* + * For bold, use Subheader; for italics, underline, or secondary + * color, use Emphasized. + */ + if (gsc_attribute_bold > 0) + g_vm->glk_set_style (style_Subheader); + else if (gsc_attribute_italic > 0 + || gsc_attribute_underline > 0 + || gsc_attribute_secondary_color > 0) + g_vm->glk_set_style (style_Emphasized); + else + { + /* + * There's nothing special about this text, so drop down to + * Normal style. + */ + g_vm->glk_set_style (style_Normal); + } + } + } +} + + +/* + * gsc_handle_font_tag() + * gsc_handle_endfont_tag() + * + * Push the settings of a font tag onto the font stack, and pop on end of + * font tag. Set the appropriate Glk style. + */ +static void +gsc_handle_font_tag (const sc_char *argument) +{ + /* Ignore the call on stack overrun. */ + if (gsc_font_index < GSC_MAX_STYLE_NESTING) + { + sc_char *lower, *face, *size; + sc_bool is_monospaced; + sc_int index_, font_size; + + /* Get the current top of stack, or default on empty stack. */ + if (gsc_font_index > 0) + { + is_monospaced = gsc_font_stack[gsc_font_index - 1].is_monospaced; + font_size = gsc_font_stack[gsc_font_index - 1].size; + } + else + { + is_monospaced = FALSE; + font_size = GSC_DEFAULT_FONT_SIZE; + } + + /* Copy and convert argument to all lowercase. */ + lower = (sc_char *)gsc_malloc (strlen (argument) + 1); + strcpy (lower, argument); + for (index_ = 0; lower[index_] != '\0'; index_++) + lower[index_] = g_vm->glk_char_to_lower (lower[index_]); + + /* Find any face= portion of the tag argument. */ + face = strstr (lower, "face="); + if (face) + { + /* + * There may be plenty of monospaced fonts, but we do only courier + * and terminal. + */ + is_monospaced = strncmp (face, "face=\"courier\"", 14) == 0 + || strncmp (face, "face=\"terminal\"", 15) == 0; + } + + /* Find the size= portion of the tag argument. */ + size = strstr (lower, "size="); + if (size) + { + sc_uint value; + + /* Deal with incremental and absolute sizes. */ + if (strncmp (size, "size=+", 6) == 0 + && sscanf (size, "size=+%lu", &value) == 1) + font_size += value; + else if (strncmp (size, "size=-", 6) == 0 + && sscanf (size, "size=-%lu", &value) == 1) + font_size -= value; + else if (sscanf (size, "size=%lu", &value) == 1) + font_size = value; + } + + /* Done with tag argument copy. */ + free (lower); + + /* + * Push the new font setting onto the font stack, and set Glk style. + */ + gsc_font_stack[gsc_font_index].is_monospaced = is_monospaced; + gsc_font_stack[gsc_font_index++].size = font_size; + gsc_set_glk_style(); + } +} + +static void +gsc_handle_endfont_tag() +{ + /* Unless underrun, pop the font stack and set Glk style. */ + if (gsc_font_index > 0) + { + gsc_font_index--; + gsc_set_glk_style(); + } +} + + +/* + * gsc_handle_attribute_tag() + * + * Increment the required attribute nesting counter, or decrement on end + * tag. Set the appropriate Glk style. + */ +static void +gsc_handle_attribute_tag (sc_int tag) +{ + /* + * Increment the required attribute nesting counter, and set Glk style. + */ + switch (tag) + { + case SC_TAG_BOLD: + gsc_attribute_bold++; + break; + case SC_TAG_ITALICS: + gsc_attribute_italic++; + break; + case SC_TAG_UNDERLINE: + gsc_attribute_underline++; + break; + case SC_TAG_COLOR: + gsc_attribute_secondary_color++; + break; + default: + break; + } + gsc_set_glk_style(); +} + +static void +gsc_handle_endattribute_tag (sc_int tag) +{ + /* + * Decrement the required attribute nesting counter, unless underrun, and + * set Glk style. + */ + switch (tag) + { + case SC_TAG_ENDBOLD: + if (gsc_attribute_bold > 0) + gsc_attribute_bold--; + break; + case SC_TAG_ENDITALICS: + if (gsc_attribute_italic > 0) + gsc_attribute_italic--; + break; + case SC_TAG_ENDUNDERLINE: + if (gsc_attribute_underline > 0) + gsc_attribute_underline--; + break; + case SC_TAG_ENDCOLOR: + if (gsc_attribute_secondary_color > 0) + gsc_attribute_secondary_color--; + break; + default: + break; + } + gsc_set_glk_style(); +} + + +/* + * gsc_handle_wait_tag() + * + * If Glk offers timers, delay for the requested period. Otherwise, this + * function does nothing. + */ +static void gsc_handle_wait_tag(const sc_char *argument) +{ + double delay = 0.0; + + /* Ignore the wait tag if the Glk doesn't have timers. */ + if (!g_vm->glk_gestalt (gestalt_Timer, 0)) + return; + + /* Determine the delay time, and convert to milliseconds. */ + if (sscanf (argument, "%lf", &delay) == 1 && delay > 0.0) + { + glui32 milliseconds, timeout; + + /* + * Work with timeouts at 1/10 of the wait period, to minimize Glk + * timer jitter. Allow the timeout to be canceled by keypress, as a + * user convenience. + */ + milliseconds = (glui32) (delay * GSC_MILLISECONDS_PER_SECOND); + timeout = milliseconds / GSC_TIMEOUTS_COUNT; + if (timeout > 0) + { + glui32 delayed; + sc_bool is_completed; + + /* Request timer events, and let a keypress cancel the wait. */ + g_vm->glk_request_char_event (gsc_main_window); + g_vm->glk_request_timer_events (timeout); + + /* Loop until delay completed or canceled by a keypress. */ + is_completed = TRUE; + for (delayed = 0; delayed < milliseconds; delayed += timeout) + { + event_t event; + + gsc_event_wait_2 (evtype_CharInput, evtype_Timer, &event); + if (event.type == evtype_CharInput) + { + /* Cancel the delay, or reissue the input request. */ + if (event.val1 == GSC_CANCEL_WAIT_1 + || event.val1 == GSC_CANCEL_WAIT_2) + { + is_completed = FALSE; + break; + } + else + g_vm->glk_request_char_event (gsc_main_window); + } + } + + /* Cancel any pending character input, and stop timers. */ + if (is_completed) + g_vm->glk_cancel_char_event (gsc_main_window); + g_vm->glk_request_timer_events (0); + } + } +} + + +/* + * gsc_reset_glk_style() + * + * Drop all stacked fonts and nested attributes, and return to normal Glk + * style. + */ +static void +gsc_reset_glk_style() +{ + /* Reset the font stack and attributes, and set a normal style. */ + gsc_font_index = 0; + gsc_attribute_bold = 0; + gsc_attribute_italic = 0; + gsc_attribute_underline = 0; + gsc_attribute_secondary_color = 0; + gsc_set_glk_style(); +} + + +/* + * os_print_tag() + * + * Interpret selected Adrift output control tags. Not all are implemented + * here; several are ignored. + */ +void +os_print_tag (sc_int tag, const sc_char *argument) +{ + event_t event; + assert (argument); + + switch (tag) + { + case SC_TAG_CLS: + /* Clear the main text display window. */ + g_vm->glk_window_clear (gsc_main_window); + break; + + case SC_TAG_FONT: + /* Handle with specific tag handler function. */ + gsc_handle_font_tag (argument); + break; + + case SC_TAG_ENDFONT: + /* Handle with specific endtag handler function. */ + gsc_handle_endfont_tag (); + break; + + case SC_TAG_BOLD: + case SC_TAG_ITALICS: + case SC_TAG_UNDERLINE: + case SC_TAG_COLOR: + /* Handle with common attribute tag handler function. */ + gsc_handle_attribute_tag (tag); + break; + + case SC_TAG_ENDBOLD: + case SC_TAG_ENDITALICS: + case SC_TAG_ENDUNDERLINE: + case SC_TAG_ENDCOLOR: + /* Handle with common attribute endtag handler function. */ + gsc_handle_endattribute_tag (tag); + break; + + case SC_TAG_CENTER: + case SC_TAG_RIGHT: + case SC_TAG_ENDCENTER: + case SC_TAG_ENDRIGHT: + /* + * We don't center or justify text, but so that things look right we do + * want a newline on starting or ending such a section. + */ + g_vm->glk_put_char ('\n'); + break; + + case SC_TAG_WAIT: + /* + * Update the status line now only if it has its own window, then + * handle with a specialized handler. + */ + if (gsc_status_window) + gsc_status_notify (); + gsc_handle_wait_tag (argument); + break; + + case SC_TAG_WAITKEY: + /* + * If reading an input log, ignore; it disrupts replay. Write a newline + * to separate off any unterminated game output instead. + */ + if (!gsc_readlog_stream) + { + /* Update the status line now only if it has its own window. */ + if (gsc_status_window) + gsc_status_notify (); + + /* Request a character event, and wait for it to be filled. */ + g_vm->glk_request_char_event (gsc_main_window); + gsc_event_wait (evtype_CharInput, &event); + } + else + g_vm->glk_put_char ('\n'); + break; + + default: + /* Ignore unimplemented and unknown tags. */ + break; + } +} + + +/* + * os_print_string() + * + * Print a text string to the main output window. + */ +void os_print_string(const sc_char *string) { + sc_bool is_monospaced; + assert (string); + assert (g_vm->glk_stream_get_current ()); + + /* + * Get the monospace font setting from the current top of stack, or + * default on empty stack. If set, we may need to use an alternative + * function to write this string. + */ + if (gsc_font_index > 0) + is_monospaced = gsc_font_stack[gsc_font_index - 1].is_monospaced; + else + is_monospaced = FALSE; + + /* + * The main window should always be the currently set window at this point, + * so we never be attempting monospaced output to the status window. + * Nevertheless, check anyway. + */ + if (is_monospaced + && g_vm->glk_stream_get_current () == g_vm->glk_window_get_stream (gsc_main_window)) + gsc_put_string_alternate (string); + else + gsc_put_string (string); +} + + +/* + * os_print_string_debug() + * + * Debugging output goes to the main Glk window -- no special effects or + * dedicated debugging window attempted. + */ +void +os_print_string_debug (const sc_char *string) +{ + assert (string); + assert (g_vm->glk_stream_get_current ()); + + gsc_put_string (string); +} + + +/* + * gsc_styled_string() + * gsc_styled_char() + * gsc_standout_string() + * gsc_standout_char() + * gsc_normal_string() + * gsc_normal_char() + * gsc_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 +gsc_styled_string (glui32 style, const char *message) +{ + assert (message); + + g_vm->glk_set_style (style); + g_vm->glk_put_string ((char *) message); + g_vm->glk_set_style (style_Normal); +} + +static void +gsc_styled_char (glui32 style, char c) +{ + char buffer[2]; + + buffer[0] = c; + buffer[1] = '\0'; + gsc_styled_string (style, buffer); +} + +static void +gsc_standout_string (const char *message) +{ + gsc_styled_string (style_Emphasized, message); +} + +static void +gsc_standout_char (char c) +{ + gsc_styled_char (style_Emphasized, c); +} + +static void +gsc_normal_string (const char *message) +{ + gsc_styled_string (style_Normal, message); +} + +static void +gsc_normal_char (char c) +{ + gsc_styled_char (style_Normal, c); +} + +static void +gsc_header_string (const char *message) +{ + gsc_styled_string (style_Header, message); +} + + +/* + * os_display_hints() + * + * This is a very basic hints display. In mitigation, very few games use + * hints at all, and those that do are usually sparse in what they hint at, so + * it's sort of good enough for the moment. + */ +void +os_display_hints (sc_game game) +{ + sc_game_hint hint; + sc_int refused; + + /* For each hint, print the question, and confirm hint display. */ + refused = 0; + for (hint = sc_get_first_game_hint (game); + hint; hint = sc_get_next_game_hint (game, hint)) + { + const sc_char *hint_question, *hint_text; + + /* If enough refusals, offer a way out of the loop. */ + if (refused >= GSC_HINT_REFUSAL_LIMIT) + { + if (!os_confirm (GSC_CONF_CONTINUE_HINTS)) + break; + refused = 0; + } + + /* Pop the question. */ + hint_question = sc_get_game_hint_question (game, hint); + gsc_normal_char ('\n'); + gsc_standout_string (hint_question); + gsc_normal_char ('\n'); + + /* Print the subtle hint, or on to the next hint. */ + hint_text = sc_get_game_subtle_hint (game, hint); + if (hint_text) + { + if (!os_confirm (GSC_CONF_SUBTLE_HINT)) + { + refused++; + continue; + } + gsc_normal_char ('\n'); + gsc_standout_string (hint_text); + gsc_normal_string ("\n\n"); + } + + /* Print the less than subtle hint, or on to the next hint. */ + hint_text = sc_get_game_unsubtle_hint (game, hint); + if (hint_text) + { + if (!os_confirm (GSC_CONF_UNSUBTLE_HINT)) + { + refused++; + continue; + } + gsc_normal_char ('\n'); + gsc_standout_string (hint_text); + gsc_normal_string ("\n\n"); + } + } +} + + +/*---------------------------------------------------------------------*/ +/* Glk resource handling functions */ +/*---------------------------------------------------------------------*/ + +/* + * os_play_sound() + * os_stop_sound() + * + * Stub functions. The unused variables defeat gcc warnings. + */ +void +os_play_sound (const sc_char *filepath, + sc_int offset, sc_int length, sc_bool is_looping) +{ + const sc_char *unused1; + sc_int unused2, unused3; + sc_bool unused4; + unused1 = filepath; + unused2 = offset; + unused3 = length; + unused4 = is_looping; +} + +void +os_stop_sound() +{ +} + + +/* + * os_show_graphic() + * + * For graphic-capable Glk libraries on Linux, attempt graphics using xv. The + * graphic capability test isn't really required, it's just a way of having + * graphics behave without surprises; someone using a non-graphical Glk + * probably won't expect graphics to pop up. + * + * For other cases, this is a stub function, with unused variables to defeat + * gcc warnings. + */ +#ifdef LINUX_GRAPHICS +static int gsclinux_graphics_enabled = TRUE; +static char *gsclinux_game_file = nullptr; +void +os_show_graphic (const sc_char *filepath, sc_int offset, sc_int length) +{ + const sc_char *unused1; + unused1 = filepath; + + if (length > 0 + && gsclinux_graphics_enabled && g_vm->glk_gestalt (gestalt_Graphics, 0)) + { + sc_char *buffer; + + /* + * Try to extract data with dd. Assuming that works, background xv to + * display the image, then background a job to delay ten seconds and + * then delete the temporary file containing the image. Systems lacking + * xv can usually use a small script, named xv, to invoke eog or an + * alternative image display binary. Not exactly finessed. + */ + assert (gsclinux_game_file); + buffer = gsc_malloc (strlen (gsclinux_game_file) + 128); + sprintf (buffer, "dd if=%s ibs=1c skip=%ld count=%ld obs=100k" + " of=/tmp/scare.jpg 2>/dev/null", + gsclinux_game_file, offset, length); + system (buffer); + free (buffer); + system ("xv /tmp/scare.jpg >/dev/null 2>&1 &"); + system ("( sleep 10; rm /tmp/scare.jpg ) >/dev/null 2>&1 &"); + } +} +#else +void +os_show_graphic (const sc_char *filepath, sc_int offset, sc_int length) +{ + const sc_char *unused1; + sc_int unused2, unused3; + unused1 = filepath; + unused2 = offset; + unused3 = length; +} +#endif + + +/*---------------------------------------------------------------------*/ +/* Glk command escape functions */ +/*---------------------------------------------------------------------*/ + +/* + * gsc_command_script() + * + * Turn game output scripting (logging) on and off. + */ +static void +gsc_command_script (const char *argument) +{ + assert (argument); + + if (sc_strcasecmp (argument, "on") == 0) + { + frefid_t fileref; + + if (gsc_transcript_stream) + { + gsc_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) + { + gsc_standout_string ("Glk transcript failed.\n"); + return; + } + + gsc_transcript_stream = g_vm->glk_stream_open_file (fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy (fileref); + if (!gsc_transcript_stream) + { + gsc_standout_string ("Glk transcript failed.\n"); + return; + } + + g_vm->glk_window_set_echo_stream (gsc_main_window, gsc_transcript_stream); + + gsc_normal_string ("Glk transcript is now on.\n"); + } + + else if (sc_strcasecmp (argument, "off") == 0) + { + if (!gsc_transcript_stream) + { + gsc_normal_string ("Glk transcript is already off.\n"); + return; + } + + g_vm->glk_stream_close (gsc_transcript_stream, nullptr); + gsc_transcript_stream = nullptr; + + g_vm->glk_window_set_echo_stream (gsc_main_window, nullptr); + + gsc_normal_string ("Glk transcript is now off.\n"); + } + + else if (strlen (argument) == 0) + { + gsc_normal_string ("Glk transcript is "); + gsc_normal_string (gsc_transcript_stream ? "on" : "off"); + gsc_normal_string (".\n"); + } + + else + { + gsc_normal_string ("Glk transcript can be "); + gsc_standout_string ("on"); + gsc_normal_string (", or "); + gsc_standout_string ("off"); + gsc_normal_string (".\n"); + } +} + + +/* + * gsc_command_inputlog() + * + * Turn game input logging on and off. + */ +static void +gsc_command_inputlog (const char *argument) +{ + assert (argument); + + if (sc_strcasecmp (argument, "on") == 0) + { + frefid_t fileref; + + if (gsc_inputlog_stream) + { + gsc_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) + { + gsc_standout_string ("Glk input logging failed.\n"); + return; + } + + gsc_inputlog_stream = g_vm->glk_stream_open_file (fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy (fileref); + if (!gsc_inputlog_stream) + { + gsc_standout_string ("Glk input logging failed.\n"); + return; + } + + gsc_normal_string ("Glk input logging is now on.\n"); + } + + else if (sc_strcasecmp (argument, "off") == 0) + { + if (!gsc_inputlog_stream) + { + gsc_normal_string ("Glk input logging is already off.\n"); + return; + } + + g_vm->glk_stream_close (gsc_inputlog_stream, nullptr); + gsc_inputlog_stream = nullptr; + + gsc_normal_string ("Glk input log is now off.\n"); + } + + else if (strlen (argument) == 0) + { + gsc_normal_string ("Glk input logging is "); + gsc_normal_string (gsc_inputlog_stream ? "on" : "off"); + gsc_normal_string (".\n"); + } + + else + { + gsc_normal_string ("Glk input logging can be "); + gsc_standout_string ("on"); + gsc_normal_string (", or "); + gsc_standout_string ("off"); + gsc_normal_string (".\n"); + } +} + + +/* + * gsc_command_readlog() + * + * Set the game input log, to read input from a file. + */ +static void +gsc_command_readlog (const char *argument) +{ + assert (argument); + + if (sc_strcasecmp (argument, "on") == 0) + { + frefid_t fileref; + + if (gsc_readlog_stream) + { + gsc_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) + { + gsc_standout_string ("Glk read log failed.\n"); + return; + } + + if (!g_vm->glk_fileref_does_file_exist (fileref)) + { + g_vm->glk_fileref_destroy (fileref); + gsc_standout_string ("Glk read log failed.\n"); + return; + } + + gsc_readlog_stream = g_vm->glk_stream_open_file (fileref, filemode_Read, 0); + g_vm->glk_fileref_destroy (fileref); + if (!gsc_readlog_stream) + { + gsc_standout_string ("Glk read log failed.\n"); + return; + } + + gsc_normal_string ("Glk read log is now on.\n"); + } + + else if (sc_strcasecmp (argument, "off") == 0) + { + if (!gsc_readlog_stream) + { + gsc_normal_string ("Glk read log is already off.\n"); + return; + } + + g_vm->glk_stream_close (gsc_readlog_stream, nullptr); + gsc_readlog_stream = nullptr; + + gsc_normal_string ("Glk read log is now off.\n"); + } + + else if (strlen (argument) == 0) + { + gsc_normal_string ("Glk read log is "); + gsc_normal_string (gsc_readlog_stream ? "on" : "off"); + gsc_normal_string (".\n"); + } + + else + { + gsc_normal_string ("Glk read log can be "); + gsc_standout_string ("on"); + gsc_normal_string (", or "); + gsc_standout_string ("off"); + gsc_normal_string (".\n"); + } +} + + +/* + * gsc_command_abbreviations() + * + * Turn abbreviation expansions on and off. + */ +static void +gsc_command_abbreviations (const char *argument) +{ + assert (argument); + + if (sc_strcasecmp (argument, "on") == 0) + { + if (gsc_abbreviations_enabled) + { + gsc_normal_string ("Glk abbreviation expansions are already on.\n"); + return; + } + + gsc_abbreviations_enabled = TRUE; + gsc_normal_string ("Glk abbreviation expansions are now on.\n"); + } + + else if (sc_strcasecmp (argument, "off") == 0) + { + if (!gsc_abbreviations_enabled) + { + gsc_normal_string ("Glk abbreviation expansions are already off.\n"); + return; + } + + gsc_abbreviations_enabled = FALSE; + gsc_normal_string ("Glk abbreviation expansions are now off.\n"); + } + + else if (strlen (argument) == 0) + { + gsc_normal_string ("Glk abbreviation expansions are "); + gsc_normal_string (gsc_abbreviations_enabled ? "on" : "off"); + gsc_normal_string (".\n"); + } + + else + { + gsc_normal_string ("Glk abbreviation expansions can be "); + gsc_standout_string ("on"); + gsc_normal_string (", or "); + gsc_standout_string ("off"); + gsc_normal_string (".\n"); + } +} + + +/* + * gsc_command_print_version_number() + * gsc_command_version() + * + * Print out the Glk library version number. + */ +static void +gsc_command_print_version_number (glui32 version) +{ + char buffer[64]; + + sprintf (buffer, "%lu.%lu.%lu", + (unsigned long) version >> 16, + (unsigned long) (version >> 8) & 0xff, + (unsigned long) version & 0xff); + gsc_normal_string (buffer); +} + +static void +gsc_command_version (const char *argument) +{ + glui32 version; + assert (argument); + + gsc_normal_string ("This is version "); + gsc_command_print_version_number (GSC_PORT_VERSION); + gsc_normal_string (" of the Glk SCARE port.\n"); + + version = g_vm->glk_gestalt (gestalt_Version, 0); + gsc_normal_string ("The Glk library version is "); + gsc_command_print_version_number (version); + gsc_normal_string (".\n"); +} + + +/* + * gsc_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 +gsc_command_commands (const char *argument) +{ + assert (argument); + + if (sc_strcasecmp (argument, "on") == 0) + { + gsc_normal_string ("Glk commands are already on.\n"); + } + + else if (sc_strcasecmp (argument, "off") == 0) + { + gsc_commands_enabled = FALSE; + gsc_normal_string ("Glk commands are now off.\n"); + } + + else if (strlen (argument) == 0) + { + gsc_normal_string ("Glk commands are "); + gsc_normal_string (gsc_commands_enabled ? "on" : "off"); + gsc_normal_string (".\n"); + } + + else + { + gsc_normal_string ("Glk commands can be "); + gsc_standout_string ("on"); + gsc_normal_string (", or "); + gsc_standout_string ("off"); + gsc_normal_string (".\n"); + } +} + + +/* + * gsc_command_license() + * + * Print licensing terms. + */ +static void +gsc_command_license (const char *argument) +{ + assert (argument); + + gsc_normal_string ("This program is free software; you can redistribute it" + " and/or modify it under the terms of version 2 of the" + " GNU General Public License as published by the Free" + " Software Foundation.\n\n"); + + gsc_normal_string ("This program is distributed in the hope that it will be" + " useful, but "); + gsc_standout_string ("WITHOUT ANY WARRANTY"); + gsc_normal_string ("; without even the implied warranty of "); + gsc_standout_string ("MERCHANTABILITY"); + gsc_normal_string (" or "); + gsc_standout_string ("FITNESS FOR A PARTICULAR PURPOSE"); + gsc_normal_string (". See the GNU General Public License for more" + " details.\n\n"); + + gsc_normal_string ("You should have received a copy of the GNU General" + " Public License along with this program; if not, write" + " to the Free Software Foundation, Inc., 51 Franklin" + " Street, Fifth Floor, Boston, MA 02110-1301 USA\n\n"); + + gsc_normal_string ("Please report any bugs, omissions, or misfeatures to "); + gsc_standout_string ("simon_baldwin@yahoo.com"); + gsc_normal_string (".\n"); +} + + +/* Glk subcommands and handler functions. */ +typedef const struct +{ + const char * const command; /* Glk subcommand. */ + void (* const handler) (const char *argument); /* Subcommand handler. */ + const int takes_argument; /* Argument flag. */ +} gsc_command_t; +typedef gsc_command_t *gsc_commandref_t; + +static void gsc_command_summary (const char *argument); +static void gsc_command_help (const char *argument); + +static gsc_command_t GSC_COMMAND_TABLE[] = { + {"summary", gsc_command_summary, FALSE}, + {"script", gsc_command_script, TRUE}, + {"inputlog", gsc_command_inputlog, TRUE}, + {"readlog", gsc_command_readlog, TRUE}, + {"abbreviations", gsc_command_abbreviations, TRUE}, + {"version", gsc_command_version, FALSE}, + {"commands", gsc_command_commands, TRUE}, + {"license", gsc_command_license, FALSE}, + {"help", gsc_command_help, TRUE}, + {nullptr, nullptr, FALSE} +}; + + +/* + * gsc_command_summary() + * + * Report all current Glk settings. + */ +static void +gsc_command_summary (const char *argument) +{ + gsc_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 = GSC_COMMAND_TABLE; entry->command; entry++) + { + if (entry->handler == gsc_command_summary + || entry->handler == gsc_command_license + || entry->handler == gsc_command_help) + continue; + + entry->handler (""); + } +} + + +/* + * gsc_command_help() + * + * Document the available Glk commands. + */ +static void +gsc_command_help (const char *command) +{ + gsc_commandref_t entry, matched; + assert (command); + + if (strlen (command) == 0) + { + gsc_normal_string ("Glk commands are"); + for (entry = GSC_COMMAND_TABLE; entry->command; entry++) + { + gsc_commandref_t next; + + next = entry + 1; + gsc_normal_string (next->command ? " " : " and "); + gsc_standout_string (entry->command); + gsc_normal_string (next->command ? "," : ".\n\n"); + } + + gsc_normal_string ("Glk commands may be abbreviated, as long as" + " the abbreviation is unambiguous. Use "); + gsc_standout_string ("glk help"); + gsc_normal_string (" followed by a Glk command name for help on that" + " command.\n"); + return; + } + + matched = nullptr; + for (entry = GSC_COMMAND_TABLE; entry->command; entry++) + { + if (sc_strncasecmp (command, entry->command, strlen (command)) == 0) + { + if (matched) + { + gsc_normal_string ("The Glk command "); + gsc_standout_string (command); + gsc_normal_string (" is ambiguous. Try "); + gsc_standout_string ("glk help"); + gsc_normal_string (" for more information.\n"); + return; + } + matched = entry; + } + } + if (!matched) + { + gsc_normal_string ("The Glk command "); + gsc_standout_string (command); + gsc_normal_string (" is not valid. Try "); + gsc_standout_string ("glk help"); + gsc_normal_string (" for more information.\n"); + return; + } + + if (matched->handler == gsc_command_summary) + { + gsc_normal_string ("Prints a summary of all the current Glk SCARE" + " settings.\n"); + } + + else if (matched->handler == gsc_command_script) + { + gsc_normal_string ("Logs the game's output to a file.\n\nUse "); + gsc_standout_string ("glk script on"); + gsc_normal_string (" to begin logging game output, and "); + gsc_standout_string ("glk script off"); + gsc_normal_string (" to end it. Glk SCARE will ask you for a file" + " when you turn scripts on.\n"); + } + + else if (matched->handler == gsc_command_inputlog) + { + gsc_normal_string ("Records the commands you type into a game.\n\nUse "); + gsc_standout_string ("glk inputlog on"); + gsc_normal_string (", to begin recording your commands, and "); + gsc_standout_string ("glk inputlog off"); + gsc_normal_string (" to turn off input logs. You can play back" + " recorded commands into a game with the "); + gsc_standout_string ("glk readlog"); + gsc_normal_string (" command.\n"); + } + + else if (matched->handler == gsc_command_readlog) + { + gsc_normal_string ("Plays back commands recorded with "); + gsc_standout_string ("glk inputlog on"); + gsc_normal_string (".\n\nUse "); + gsc_standout_string ("glk readlog on"); + gsc_normal_string (". Command play back stops at the end of the" + " file. You can also play back commands from a" + " text file created using any standard editor.\n"); + } + + else if (matched->handler == gsc_command_abbreviations) + { + gsc_normal_string ("Controls abbreviation expansion.\n\nGlk SCARE" + " automatically expands several standard single" + " letter abbreviations for you; for example, \"x\"" + " becomes \"examine\". Use "); + gsc_standout_string ("glk abbreviations on"); + gsc_normal_string (" to turn this feature on, and "); + gsc_standout_string ("glk abbreviations off"); + gsc_normal_string (" to turn it off. While the feature is on, you" + " can bypass abbreviation expansion for an" + " individual game command by prefixing it with a" + " single quote.\n"); + } + + else if (matched->handler == gsc_command_version) + { + gsc_normal_string ("Prints the version numbers of the Glk library" + " and the Glk SCARE port.\n"); + } + + else if (matched->handler == gsc_command_commands) + { + gsc_normal_string ("Turn off Glk commands.\n\nUse "); + gsc_standout_string ("glk commands off"); + gsc_normal_string (" to disable all Glk commands, including this one." + " Once turned off, there is no way to turn Glk" + " commands back on while inside the game.\n"); + } + + else if (matched->handler == gsc_command_license) + { + gsc_normal_string ("Prints Glk SCARE's software license.\n"); + } + + else if (matched->handler == gsc_command_help) + gsc_command_help (""); + + else + gsc_normal_string ("There is no help available on that Glk command." + " Sorry.\n"); +} + + +/* + * gsc_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 +gsc_command_escape (const char *string) +{ + int posn; + char *string_copy, *command, *argument; + assert (string); + + /* + * Return FALSE if the string doesn't begin with the Glk command escape + * introducer. + */ + posn = strspn (string, "\t "); + if (sc_strncasecmp (string + posn, "glk", strlen ("glk")) != 0) + return FALSE; + + /* Take a copy of the string, without any leading space or introducer. */ + string_copy = (char *)gsc_malloc (strlen (string + posn) + 1 - strlen ("glk")); + strcpy (string_copy, string + posn + strlen ("glk")); + + /* + * Find the subcommand; the first word in the string copy. Find its end, + * and ensure it terminates with a NUL. + */ + posn = strspn (string_copy, "\t "); + command = 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 (command) > 0) + { + gsc_commandref_t entry, matched; + int matches; + + /* + * Search for the first unambiguous table command string matching + * the command passed in. + */ + matches = 0; + matched = nullptr; + for (entry = GSC_COMMAND_TABLE; entry->command; entry++) + { + if (sc_strncasecmp (command, entry->command, strlen (command)) == 0) + { + matches++; + matched = entry; + } + } + + /* If the match was unambiguous, call the command handler. */ + if (matches == 1) + { + gsc_normal_char ('\n'); + matched->handler (argument); + + if (!matched->takes_argument && strlen (argument) > 0) + { + gsc_normal_string ("[The "); + gsc_standout_string (matched->command); + gsc_normal_string (" command ignores arguments.]\n"); + } + } + + /* No match, or the command was ambiguous. */ + else + { + gsc_normal_string ("\nThe Glk command "); + gsc_standout_string (command); + gsc_normal_string (" is "); + gsc_normal_string (matches == 0 ? "not valid" : "ambiguous"); + gsc_normal_string (". Try "); + gsc_standout_string ("glk help"); + gsc_normal_string (" for more information.\n"); + } + } + else + { + gsc_normal_char ('\n'); + gsc_command_help (""); + } + + /* The string contained a Glk command; return TRUE. */ + free (string_copy); + return TRUE; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port input functions */ +/*---------------------------------------------------------------------*/ + +/* Quote used to suppress abbreviation expansion and local commands. */ +static const char GSC_QUOTED_INPUT = '\''; + + +/* Table of single-character command abbreviations. */ +typedef const struct +{ + const char abbreviation; /* Abbreviation character. */ + const char *const expansion; /* Expansion string. */ +} gsc_abbreviation_t; +typedef gsc_abbreviation_t *gsc_abbreviationref_t; + +static gsc_abbreviation_t GSC_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} +}; + + +/* + * gsc_expand_abbreviations() + * + * Expand a few common one-character abbreviations commonly found in other + * game systems. + */ +static void +gsc_expand_abbreviations (char *buffer, int size) +{ + char *command, abbreviation; + const char *expansion; + gsc_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 && Common::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 = GSC_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 (strlen (buffer) + strlen (expansion) - 1 >= (unsigned int) size) + return; + + memmove (command + strlen (expansion) - 1, command, strlen (command) + 1); + memcpy (command, expansion, strlen (expansion)); + + gsc_standout_string ("["); + gsc_standout_char (abbreviation); + gsc_standout_string (" -> "); + gsc_standout_string (expansion); + gsc_standout_string ("]\n"); + } +} + + +/* + * os_read_line() + * + * Read and return a line of player input. + */ +sc_bool +os_read_line (sc_char *buffer, sc_int length) +{ + sc_int characters; + assert (buffer && length > 0); + + /* If a help request is pending, provide a user hint. */ + gsc_output_provide_help_hint (); + + /* + * Ensure normal style, update the status line, and issue an input prompt. + */ + gsc_reset_glk_style (); + gsc_status_notify (); + g_vm->glk_put_string (">"); + + /* + * 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 (gsc_readlog_stream) + { + glui32 chars; + + /* Get the next line from the log stream. */ + chars = g_vm->glk_get_line_stream (gsc_readlog_stream, buffer, length); + if (chars > 0) + { + /* Echo the line just read in input style. */ + g_vm->glk_set_style (style_Input); + gsc_put_buffer (buffer, chars); + g_vm->glk_set_style (style_Normal); + + /* Return this line as player input. */ + return TRUE; + } + + /* + * 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 (gsc_readlog_stream, nullptr); + gsc_readlog_stream = nullptr; + } + + /* + * No input log being read, or we just hit the end of file on one. Revert + * to normal line input; start by getting a new line from Glk. + */ + characters = gsc_read_line (buffer, length - 1); + assert (characters <= length); + buffer[characters] = '\0'; + + /* + * If neither abbreviations nor local commands are enabled, use the data + * read above without further massaging. + */ + if (gsc_abbreviations_enabled || gsc_commands_enabled) + { + char *command; + + /* + * 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. + */ + command = buffer + strspn (buffer, "\t "); + if (command[0] == GSC_QUOTED_INPUT) + { + /* Delete the quote with memmove(). */ + memmove (command, command + 1, strlen (command)); + } + else + { + /* Check for, and expand, and abbreviated commands. */ + if (gsc_abbreviations_enabled) + gsc_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 by + * returning FALSE. + */ + if (gsc_commands_enabled) + { + int posn; + + posn = strspn (buffer, "\t "); + if (sc_strncasecmp (buffer + posn, "help", strlen ("help"))== 0) + { + if (strspn (buffer + posn + strlen ("help"), "\t ") + == strlen (buffer + posn + strlen ("help"))) + { + gsc_output_register_help_request (); + } + } + + if (gsc_command_escape (buffer)) + { + gsc_output_silence_help_hints (); + return FALSE; + } + } + } + } + + /* + * 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 (gsc_inputlog_stream) + { + g_vm->glk_put_string_stream (gsc_inputlog_stream, buffer); + g_vm->glk_put_char_stream (gsc_inputlog_stream, '\n'); + } + + return TRUE; +} + + +/* + * os_read_line_debug() + * + * Read and return a debugger command line. There's no dedicated debugging + * window, so this is just a call to the normal readline, with an additional + * prompt. + */ +sc_bool +os_read_line_debug (sc_char *buffer, sc_int length) +{ + gsc_output_silence_help_hints (); + gsc_reset_glk_style (); + g_vm->glk_put_string ("[SCARE debug]"); + return os_read_line (buffer, length); +} + + +/* + * os_confirm() + * + * Confirm a game action with a yes/no prompt. + */ +sc_bool +os_confirm (sc_int type) +{ + sc_char response; + + /* + * Always allow game saves and hint display, and if we're reading from an + * input log, allow everything no matter what, on the assumption that the + * user knows what they are doing. + */ + if (gsc_readlog_stream + || type == SC_CONF_SAVE || type == SC_CONF_VIEW_HINTS) + return TRUE; + + /* Ensure back to normal style, and update status. */ + gsc_reset_glk_style (); + gsc_status_notify (); + + /* Prompt for the confirmation, based on the type. */ + if (type == GSC_CONF_SUBTLE_HINT) + g_vm->glk_put_string ("View the subtle hint for this topic"); + else if (type == GSC_CONF_UNSUBTLE_HINT) + g_vm->glk_put_string ("View the unsubtle hint for this topic"); + else if (type == GSC_CONF_CONTINUE_HINTS) + g_vm->glk_put_string ("Continue with hints"); + else + { + g_vm->glk_put_string ("Do you really want to "); + switch (type) + { + case SC_CONF_QUIT: + g_vm->glk_put_string ("quit"); + break; + case SC_CONF_RESTART: + g_vm->glk_put_string ("restart"); + break; + case SC_CONF_SAVE: + g_vm->glk_put_string ("save"); + break; + case SC_CONF_RESTORE: + g_vm->glk_put_string ("restore"); + break; + case SC_CONF_VIEW_HINTS: + g_vm->glk_put_string ("view hints"); + break; + default: + g_vm->glk_put_string ("do that"); + break; + } + } + g_vm->glk_put_string ("? "); + + /* Loop until 'yes' or 'no' returned. */ + do + { + event_t event; + + /* Wait for a standard key, ignoring Glk special keys. */ + do + { + g_vm->glk_request_char_event (gsc_main_window); + gsc_event_wait (evtype_CharInput, &event); + } + while (event.val1 > UCHAR_MAX); + response = g_vm->glk_char_to_upper (event.val1); + } + while (response != 'Y' && response != 'N'); + + /* Echo the confirmation response, and a new 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_char ('\n'); + + /* Use a short delay on restarts, if confirmed. */ + if (type == SC_CONF_RESTART && response == 'Y') + gsc_short_delay (); + + /* Return TRUE if 'Y' was entered. */ + return (response == 'Y'); +} + + +/*---------------------------------------------------------------------*/ +/* Glk port event functions */ +/*---------------------------------------------------------------------*/ + +/* Short delay before restarts; 1s, in 100ms segments. */ +static const glui32 GSC_DELAY_TIMEOUT = 100; +static const glui32 GSC_DELAY_TIMEOUTS_COUNT = 10; + +/* + * gsc_short_delay() + * + * Delay for a short period; used before restarting a completed game, to + * improve the display where 'r', or confirming restart, triggers an otherwise + * immediate, and abrupt, restart. + */ +static void +gsc_short_delay() +{ + /* Ignore the call if the Glk doesn't have timers. */ + if (g_vm->glk_gestalt (gestalt_Timer, 0)) + { + glui32 timeout; + + /* Timeout in small chunks to minimize Glk jitter. */ + g_vm->glk_request_timer_events (GSC_DELAY_TIMEOUT); + for (timeout = 0; timeout < GSC_DELAY_TIMEOUTS_COUNT; timeout++) + { + event_t event; + + gsc_event_wait (evtype_Timer, &event); + } + g_vm->glk_request_timer_events (0); + } +} + + +/* + * gsc_event_wait_2() + * gsc_event_wait() + * + * Process Glk events until one of the expected type, or types, arrives. + * Return the event of that type. + */ +static void +gsc_event_wait_2 (glui32 wait_type_1, glui32 wait_type_2, event_t * event) +{ + assert (event); + + do + { + g_vm->glk_select (event); + + switch (event->type) + { + case evtype_Arrange: + case evtype_Redraw: + /* Refresh any sensitive windows on size events. */ + gsc_status_redraw (); + break; + } + } + while (!(event->type == (EvType)wait_type_1 || event->type == (EvType)wait_type_2)); +} + +static void +gsc_event_wait (glui32 wait_type, event_t * event) +{ + assert (event); + + gsc_event_wait_2 (wait_type, evtype_None, event); +} + + +/*---------------------------------------------------------------------*/ +/* Glk port file functions */ +/*---------------------------------------------------------------------*/ + +/* + * os_open_file() + * + * Open a file for save or restore, and return a Glk stream for the opened + * file. + */ +void *os_open_file (sc_bool is_save) { + glui32 usage, fmode; + frefid_t fileref; + strid_t stream; + + usage = fileusage_SavedGame | fileusage_BinaryMode; + fmode = is_save ? filemode_Write : filemode_Read; + + fileref = g_vm->glk_fileref_create_by_prompt(usage, (FileMode)fmode, 0); + if (!fileref) + return nullptr; + + if (!is_save && !g_vm->glk_fileref_does_file_exist (fileref)) + { + g_vm->glk_fileref_destroy (fileref); + return nullptr; + } + + stream = g_vm->glk_stream_open_file (fileref, (FileMode)fmode, 0); + g_vm->glk_fileref_destroy (fileref); + + return stream; +} + + +/* + * os_write_file() + * os_read_file() + * + * Write/read the given buffered data to/from the open Glk stream. + */ +void +os_write_file (void *opaque, const sc_byte *buffer, sc_int length) +{ + strid_t stream = (strid_t) opaque; + assert (opaque && buffer); + + g_vm->glk_put_buffer_stream (stream, (char *) buffer, length); +} + +sc_int +os_read_file (void *opaque, sc_byte *buffer, sc_int length) +{ + strid_t stream = (strid_t) opaque; + assert (opaque && buffer); + + return g_vm->glk_get_buffer_stream (stream, (char *) buffer, length); +} + + +/* + * os_close_file() + * + * Close the opened Glk stream. + */ +void +os_close_file (void *opaque) +{ + strid_t stream = (strid_t) opaque; + assert (opaque); + + g_vm->glk_stream_close (stream, nullptr); +} + + +/*---------------------------------------------------------------------*/ +/* main() and options parsing */ +/*---------------------------------------------------------------------*/ + +/* Loading message flush delay timeout. */ +static const glui32 GSC_LOADING_TIMEOUT = 100; + +/* Enumerated game end options. */ +enum gsc_end_option { GAME_RESTART, GAME_UNDO, GAME_QUIT }; + +/* + * The following value needs to be passed between the startup_code and main + * functions. + */ +static char *gsc_game_message = nullptr; + + +/* + * gsc_callback() + * + * Callback function for reading in game and restore file data; fills a + * buffer with TAF or TAS file data from a Glk stream, and returns the byte + * count. + */ +static sc_int +gsc_callback (void *opaque, sc_byte *buffer, sc_int length) +{ + strid_t stream = (strid_t) opaque; + assert (stream); + + return g_vm->glk_get_buffer_stream (stream, (char *) buffer, length); +} + + +/* + * gsc_get_ending_option() + * + * Offer the option to restart, undo, or quit. Returns the selected game + * end option. Called on game completion. + */ +static enum gsc_end_option +gsc_get_ending_option() +{ + sc_char response; + + /* Ensure back to normal style, and update status. */ + gsc_reset_glk_style (); + gsc_status_notify (); + + /* Prompt for restart, undo, or quit. */ + g_vm->glk_put_string ("\nWould you like to RESTART, UNDO a turn, or QUIT? "); + + /* Loop until 'restart', 'undo' or 'quit'. */ + do + { + event_t event; + + do + { + g_vm->glk_request_char_event (gsc_main_window); + gsc_event_wait (evtype_CharInput, &event); + } + while (event.val1 > UCHAR_MAX); + response = g_vm->glk_char_to_upper (event.val1); + } + while (response != 'R' && response != 'U' && response != 'Q'); + + /* Echo the confirmation response, and a new line. */ + g_vm->glk_set_style (style_Input); + switch (response) + { + case 'R': + g_vm->glk_put_string ("Restart"); + break; + case 'U': + g_vm->glk_put_string ("Undo"); + break; + case 'Q': + g_vm->glk_put_string ("Quit"); + break; + default: + gsc_fatal ("GLK: Invalid response encountered"); + g_vm->glk_exit (); + } + g_vm->glk_set_style (style_Normal); + g_vm->glk_put_char ('\n'); + + /* Return the appropriate value for response. */ + switch (response) + { + case 'R': + return GAME_RESTART; + case 'U': + return GAME_UNDO; + case 'Q': + return GAME_QUIT; + default: + gsc_fatal ("GLK: Invalid response encountered"); + g_vm->glk_exit (); + } + + /* Unreachable; supplied to suppress compiler warning. */ + return GAME_QUIT; +} + + +/* + * gsc_startup_code() + * gsc_main + * + * Together, these functions take the place of the original main(). The + * first one is called from the platform-specific startup_code(), to parse + * and generally handle options. The second is called from g_vm->glk_main, and + * does the real work of running the game. + */ +static int +gsc_startup_code (strid_t game_stream, strid_t restore_stream, + sc_uint trace_flags, sc_bool enable_debugger, + sc_bool stable_random, const sc_char *locale) +{ + winid_t window; + assert (game_stream); + + /* Open a temporary Glk main window. */ + window = g_vm->glk_window_open (0, 0, 0, wintype_TextBuffer, 0); + if (window) + { + /* Clear and initialize the temporary window. */ + g_vm->glk_window_clear (window); + g_vm->glk_set_window (window); + g_vm->glk_set_style (style_Normal); + + /* + * Display a brief loading game message; here we have to use a timeout + * to ensure that the text is flushed to Glk. + */ + g_vm->glk_put_string ("Loading game...\n"); + if (g_vm->glk_gestalt (gestalt_Timer, 0)) + { + event_t event; + + g_vm->glk_request_timer_events (GSC_LOADING_TIMEOUT); + do + { + g_vm->glk_select (&event); + } + while (event.type != evtype_Timer); + g_vm->glk_request_timer_events (0); + } + } + + /* If the Glk libarary does not support unicode, disable it. */ + if (!gsc_has_unicode || !g_vm->glk_gestalt (gestalt_Unicode, 0)) + gsc_unicode_enabled = FALSE; + + /* + * If a locale was requested, set it in the core interpreter now. This + * locale will preempt any auto-detected one found from inspecting the + * game on creation. After game creation, the Glk locale is synchronized + * to the core interpreter's locale. + */ + if (locale) + sc_set_locale (locale); + + /* + * Set tracing flags, then try to create a SCARE game reference from the + * TAF file. Since we need this in our call from g_vm->glk_main, we have to keep + * it in a module static variable. If we can't open the TAF file, then + * we'll set the pointer to nullptr, and complain about it later in main. + * Passing the message string around like this is a nuisance... + */ + sc_set_trace_flags (trace_flags); + gsc_game = sc_game_from_callback (gsc_callback, game_stream); + if (!gsc_game) + { + gsc_game = nullptr; + gsc_game_message = "Unable to load an Adrift game from the" + " requested file."; + } + else + gsc_game_message = nullptr; + g_vm->glk_stream_close (game_stream, nullptr); + + /* + * If the game was created successfully and there is a restore stream, try + * to immediately restore the game from that stream. + */ + if (gsc_game && restore_stream) + { + if (!sc_load_game_from_callback (gsc_game, gsc_callback, restore_stream)) + { + sc_free_game (gsc_game); + gsc_game = nullptr; + gsc_game_message = "Unable to restore this Adrift game from the" + " requested file."; + } + else + gsc_game_message = nullptr; + } + if (restore_stream) + g_vm->glk_stream_close (restore_stream, nullptr); + + /* If successful, set game debugging and synchronize to the core's locale. */ + if (gsc_game) + { + sc_set_game_debugger_enabled (gsc_game, enable_debugger); + gsc_set_locale (sc_get_locale ()); + } + + /* Set portable and predictable random number generation if requested. */ + if (stable_random) + { + sc_set_portable_random (TRUE); + sc_reseed_random_sequence (1); + } + + /* Close the temporary window. */ + if (window) + g_vm->glk_window_close (window, nullptr); + + /* Set title of game */ +#ifdef GARGLK + g_vm->garglk_set_story_name(sc_get_game_name(gsc_game)); +#endif + + /* Game set up, perhaps successfully. */ + return TRUE; +} + +static void +gsc_main() +{ + sc_bool is_running; + + /* Ensure SCARE internal types have the right sizes. */ + if (!(sizeof (sc_byte) == 1 && sizeof (sc_char) == 1 + && sizeof (sc_uint) >= 4 && sizeof (sc_int) >= 4 + && sizeof (sc_uint) <= 8 && sizeof (sc_int) <= 8)) + { + gsc_fatal ("GLK: Types sized incorrectly, recompilation is needed"); + g_vm->glk_exit (); + } + + /* Create the Glk window, and set its stream as the current one. */ + gsc_main_window = g_vm->glk_window_open (0, 0, 0, wintype_TextBuffer, 0); + if (!gsc_main_window) + { + gsc_fatal ("GLK: Can't open main window"); + g_vm->glk_exit (); + } + g_vm->glk_window_clear (gsc_main_window); + g_vm->glk_set_window (gsc_main_window); + g_vm->glk_set_style (style_Normal); + + /* If there's a problem with the game file, complain now. */ + if (!gsc_game) + { + assert (gsc_game_message); + gsc_header_string ("Glk SCARE Error\n\n"); + gsc_normal_string (gsc_game_message); + gsc_normal_char ('\n'); + g_vm->glk_exit (); + } + + /* Try to create a one-line status window. We can live without it. */ + g_vm->glk_stylehint_set (wintype_TextGrid, style_User1, stylehint_ReverseColor, 1); + gsc_status_window = g_vm->glk_window_open (gsc_main_window, + winmethod_Above | winmethod_Fixed, + 1, wintype_TextGrid, 0); + + /* Repeat the game until no more restarts requested. */ + is_running = TRUE; + while (is_running) + { + /* Run the game until it ends, or the user quits. */ + gsc_status_notify (); + sc_interpret_game (gsc_game); + + /* + * If the game did not complete, the user quit explicitly, so leave the + * game repeat loop. + */ + if (!sc_has_game_completed (gsc_game)) + { + is_running = FALSE; + break; + } + + /* + * If reading from an input log, close it now. We need to request a + * user selection, probably modal, and after that we probably don't + * want the follow-on readlog data being used as game input. + */ + if (gsc_readlog_stream) + { + g_vm->glk_stream_close (gsc_readlog_stream, nullptr); + gsc_readlog_stream = nullptr; + } + + /* + * Get user selection of restart, undo a turn, or quit completed game. + * If undo is unavailable (this should not be possible), degrade to + * restart. + */ + switch (gsc_get_ending_option ()) + { + case GAME_RESTART: + gsc_short_delay (); + sc_restart_game (gsc_game); + break; + + case GAME_UNDO: + if (sc_is_game_undo_available (gsc_game)) + { + sc_undo_game_turn (gsc_game); + gsc_normal_string ("The previous turn has been undone.\n"); + } + else + { + gsc_normal_string ("Sorry, no undo is available.\n"); + gsc_short_delay (); + sc_restart_game (gsc_game); + } + break; + + case GAME_QUIT: + is_running = FALSE; + break; + } + } + + /* All done -- release game resources. */ + sc_free_game (gsc_game); + + /* Close any open transcript, input log, and/or read log. */ + if (gsc_transcript_stream) + { + g_vm->glk_stream_close (gsc_transcript_stream, nullptr); + gsc_transcript_stream = nullptr; + } + if (gsc_inputlog_stream) + { + g_vm->glk_stream_close (gsc_inputlog_stream, nullptr); + gsc_inputlog_stream = nullptr; + } + if (gsc_readlog_stream) + { + g_vm->glk_stream_close (gsc_readlog_stream, nullptr); + gsc_readlog_stream = nullptr; + } +} + + +/*---------------------------------------------------------------------*/ +/* Linkage between Glk entry/exit calls and the real interpreter */ +/*---------------------------------------------------------------------*/ + +/* + * Safety flags, to ensure we always get startup before main, and that + * we only get a call to main once. + */ +static int gsc_startup_called = FALSE, + gsc_main_called = FALSE; + +/* + * g_vm->glk_main() + * + * Main entry point for Glk. Here, all startup is done, and we call our + * function to run the game, or to report errors if gsc_game_message is set. + */ +void glk_main() { + assert (gsc_startup_called && !gsc_main_called); + gsc_main_called = TRUE; + + /* Call the generic interpreter main function. */ + gsc_main (); +} + + +/*---------------------------------------------------------------------*/ +/* Glk linkage relevant only to the UNIX platform */ +/*---------------------------------------------------------------------*/ +#ifdef UNUSED + +/* + * Glk arguments for UNIX versions of the Glk interpreter. + */ +glkunix_argumentlist_t glkunix_arguments[] = { + {(char *) "-nc", glkunix_arg_NoValue, + (char *) "-nc No local handling for Glk special commands"}, + {(char *) "-na", glkunix_arg_NoValue, + (char *) "-na Turn off abbreviation expansions"}, + {(char *) "-nu", glkunix_arg_NoValue, + (char *) "-nu Turn off any use of Unicode output"}, +#ifdef LINUX_GRAPHICS + {(char *) "-ng", glkunix_arg_NoValue, + (char *) "-ng Turn off attempts at game graphics"}, +#endif + {(char *) "-r", glkunix_arg_ValueFollows, + (char *) "-r FILE Restore from FILE on starting the game"}, + {(char *) "", glkunix_arg_ValueCanFollow, + (char *) "filename game to run"}, + {nullptr, glkunix_arg_End, nullptr} +}; + + +/* + * glkunix_startup_code() + * + * Startup entry point for UNIX versions of Glk interpreter. Glk will call + * glkunix_startup_code() to pass in arguments. On startup, parse arguments + * and open a Glk stream to the game, then call the generic gsc_startup_code() + * to build a game from the stream. On error, set the message in + * gsc_game_message; the core gsc_main() will report it when it's called. + */ +int +glkunix_startup_code (glkunix_startup_t * data) +{ + int argc = data->argc; + sc_char **argv = data->argv; + int argv_index; + sc_char *restore_from; + const sc_char *locale; + strid_t game_stream, restore_stream; + sc_uint trace_flags; + sc_bool enable_debugger, stable_random; + assert (!gsc_startup_called); + gsc_startup_called = TRUE; + +#ifdef GARGLK + garg_vm->glk_set_program_name("SCARE " SCARE_VERSION); + garg_vm->glk_set_program_info("SCARE " SCARE_VERSION + " by Simon Baldwin and Mark J. Tilford"); +#endif + + /* Handle command line arguments. */ + restore_from = nullptr; + for (argv_index = 1; + argv_index < argc && argv[argv_index][0] == '-'; argv_index++) + { + if (strcmp (argv[argv_index], "-nc") == 0) + { + gsc_commands_enabled = FALSE; + continue; + } + if (strcmp (argv[argv_index], "-na") == 0) + { + gsc_abbreviations_enabled = FALSE; + continue; + } + if (strcmp (argv[argv_index], "-nu") == 0) + { + gsc_unicode_enabled = FALSE; + continue; + } +#ifdef LINUX_GRAPHICS + if (strcmp (argv[argv_index], "-ng") == 0) + { + gsclinux_graphics_enabled = FALSE; + continue; + } +#endif + if (strcmp (argv[argv_index], "-r") == 0) + { + restore_from = argv[++argv_index]; + continue; + } + return FALSE; + } + + /* On invalid usage, set a complaint message and return. */ + if (argv_index != argc - 1) + { + gsc_game = nullptr; + if (argv_index < argc - 1) + gsc_game_message = "More than one game file" + " was given on the command line."; + else + gsc_game_message = "No game file was given on the command line."; + return TRUE; + } + + /* Open a stream to the TAF file, complain if this fails. */ + game_stream = glkunix_stream_open_pathname (argv[argv_index], FALSE, 0); + if (!game_stream) + { + gsc_game = nullptr; + gsc_game_message = "Unable to open the requested game file."; + return TRUE; + } + else + gsc_game_message = nullptr; + + /* + * If a restore requested, open a stream to the TAF (TAS) file, and + * again, complain if this fails. + */ + if (restore_from) + { + restore_stream = glkunix_stream_open_pathname (restore_from, FALSE, 0); + if (!restore_stream) + { + g_vm->glk_stream_close (game_stream, nullptr); + gsc_game = nullptr; + gsc_game_message = "Unable to open the requested restore file."; + return TRUE; + } + else + gsc_game_message = nullptr; + } + else + restore_stream = nullptr; + + /* Set SCARE trace flags and other general setup from the environment. */ + if (getenv ("SC_TRACE_FLAGS")) + trace_flags = strtoul (getenv ("SC_TRACE_FLAGS"), nullptr, 0); + else + trace_flags = 0; + enable_debugger = (getenv ("SC_DEBUGGER_ENABLED") != nullptr); + stable_random = (getenv ("SC_STABLE_RANDOM_ENABLED") != nullptr); + locale = getenv ("SC_LOCALE"); + +#ifdef LINUX_GRAPHICS + /* Note the path to the game file for graphics extraction. */ + gsclinux_game_file = argv[argv_index]; +#endif + + /* Use the generic startup code to complete startup. */ + return gsc_startup_code (game_stream, restore_stream, trace_flags, + enable_debugger, stable_random, locale); +} +#endif /* __unix */ + + +/*---------------------------------------------------------------------*/ +/* Glk linkage relevant only to the Windows platform */ +/*---------------------------------------------------------------------*/ +#ifdef _WIN32 + +#include <windows.h> + +#include "WinGlk.h" +#include "resource.h" + +/* Windows constants and external definitions. */ +static const unsigned int GSCWIN_glk_INIT_VERSION = 0x601; +extern int InitGlk (unsigned int iVersion); + +/* + * WinMain() + * + * Entry point for all Glk applications. + */ +int WINAPI +WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, + LPSTR lpCmdLine, int nCmdShow) +{ + /* Attempt to initialize both the Glk library and SCARE. */ + if (!(InitGlk (GSCWIN_glk_INIT_VERSION) && wing_vm->glk_startup_code (lpCmdLine))) + return 0; + + /* Run the application; no return from this routine. */ + g_vm->glk_main (); + g_vm->glk_exit (); + return 0; +} + + +/* + * wing_vm->glk_startup_code() + * + * Startup entry point for Windows versions of Glk interpreter. + */ +int +wing_vm->glk_startup_code (const char *cmdline) +{ + const char *filename, *locale; + frefid_t fileref; + strid_t game_stream; + sc_uint trace_flags; + sc_bool enable_debugger, stable_random; + assert (!gsc_startup_called); + gsc_startup_called = TRUE; + + /* Set up application and window. */ + wing_vm->glk_app_set_name ("Scare"); + wing_vm->glk_set_menu_name ("&Scare"); + wing_vm->glk_window_set_title ("Scare Adrift Interpreter"); + wing_vm->glk_set_about_text ("Windows Scare 1.3.10"); + wing_vm->glk_set_gui (IDI_SCARE); + + /* Open a stream to the game. */ + filename = wing_vm->glk_get_initial_filename (cmdline, + "Select an Adrift game to run", + "Adrift Files (.taf)|*.taf;All Files (*.*)|*.*||"); + if (!filename) + return 0; + + fileref = wing_vm->glk_fileref_create_by_name (fileusage_BinaryMode + | fileusage_Data, + (char *) filename, 0, 0); + if (!fileref) + return 0; + + game_stream = g_vm->glk_stream_open_file (fileref, filemode_Read, 0); + g_vm->glk_fileref_destroy (fileref); + if (!game_stream) + return 0; + + /* Set trace, debugger, and portable random flags. */ + if (getenv ("SC_TRACE_FLAGS")) + trace_flags = strtoul (getenv ("SC_TRACE_FLAGS"), nullptr, 0); + else + trace_flags = 0; + enable_debugger = (getenv ("SC_DEBUGGER_ENABLED") != nullptr); + stable_random = (getenv ("SC_STABLE_RANDOM_ENABLED") != nullptr); + locale = getenv ("SC_LOCALE"); + + /* Use the generic startup code to complete startup. */ + return gsc_startup_code (game_stream, nullptr, trace_flags, + enable_debugger, stable_random, locale); +} +#endif /* _WIN32 */ + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scare.h b/engines/glk/adrift/scare.h new file mode 100644 index 0000000000..43ce75af1d --- /dev/null +++ b/engines/glk/adrift/scare.h @@ -0,0 +1,188 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef ADRIFT_ADRIFT_H +#define ADRIFT_ADRIFT_H + +#include "common/scummsys.h" +#include "common/stream.h" +#undef longjmp +#undef setjmp +#include <setjmp.h> + +namespace Glk { +namespace Adrift { + +/* + * Base type definitions. SCARE integer types need to be at least 32 bits, + * so using long here is a good bet for almost all ANSI C implementations for + * 32 and 64 bit platforms; maybe also for any 16 bit ones. For 64 bit + * platforms configured for LP64, SCARE integer types will consume more space + * in data structures. Values won't wrap identically to 32 bit ones, but + * games shouldn't be relying on wrapping anyway. One final note -- in several + * places, SCARE allocates 32 bytes into which it will sprintf() a long; this + * is fine for both standard 32 bit and LP64 64 bit platforms, but is unsafe + * should SCARE ever be configured for 128 bit definitions of sc_[u]int. + */ +typedef char sc_char; +typedef unsigned char sc_byte; +typedef long sc_int; +typedef unsigned long sc_uint; +typedef int sc_bool; + +/* Enumerated confirmation types, passed to os_confirm(). */ +enum +{ SC_CONF_QUIT = 0, + SC_CONF_RESTART, SC_CONF_SAVE, SC_CONF_RESTORE, SC_CONF_VIEW_HINTS +}; + +/* HTML-like tag enumerated values, passed to os_print_tag(). */ +enum +{ SC_TAG_UNKNOWN = 0, SC_TAG_ITALICS, SC_TAG_ENDITALICS, SC_TAG_BOLD, + SC_TAG_ENDBOLD, SC_TAG_UNDERLINE, SC_TAG_ENDUNDERLINE, SC_TAG_COLOR, + SC_TAG_ENDCOLOR, SC_TAG_FONT, SC_TAG_ENDFONT, SC_TAG_BGCOLOR, SC_TAG_CENTER, + SC_TAG_ENDCENTER, SC_TAG_RIGHT, SC_TAG_ENDRIGHT, SC_TAG_WAIT, SC_TAG_WAITKEY, + SC_TAG_CLS, + + /* British spelling equivalents. */ + SC_TAG_COLOUR = SC_TAG_COLOR, + SC_TAG_ENDCOLOUR = SC_TAG_ENDCOLOR, + SC_TAG_BGCOLOUR = SC_TAG_BGCOLOR, + SC_TAG_CENTRE = SC_TAG_CENTER, + SC_TAG_ENDCENTRE = SC_TAG_ENDCENTER +}; + +/* OS interface function prototypes; interpreters must define these. */ +typedef void *sc_game; +extern void os_print_string (const sc_char *string); +extern void os_print_tag(sc_int tag, const sc_char *argument); +extern void os_play_sound (const sc_char *filepath, + sc_int offset, sc_int length, sc_bool is_looping); +extern void os_stop_sound(); +extern void os_show_graphic (const sc_char *filepath, + sc_int offset, sc_int length); +extern sc_bool os_read_line(sc_char *buffer, sc_int length); +extern sc_bool os_confirm(sc_int type); +extern void *os_open_file(sc_bool is_save); +extern void os_write_file (void *opaque, const sc_byte *buffer, sc_int length); +extern sc_int os_read_file (void *opaque, sc_byte *buffer, sc_int length); +extern void os_close_file (void *opaque); +extern void os_display_hints(sc_game game); + +extern void os_print_string_debug (const sc_char *string); +extern sc_bool os_read_line_debug(sc_char *buffer, sc_int length); + +/* Interpreter trace flag bits, passed to sc_set_trace_flags(). */ +enum +{ SC_TRACE_PARSE = 1, SC_TRACE_PROPERTIES = 2, SC_TRACE_VARIABLES = 4, + SC_TRACE_PARSER = 8, SC_TRACE_LIBRARY = 16, SC_TRACE_EVENTS = 32, + SC_TRACE_NPCS = 64, SC_TRACE_OBJECTS = 128, SC_TRACE_TASKS = 256, + SC_TRACE_PRINTFILTER = 512, + + SC_DUMP_TAF = 1024, SC_DUMP_PROPERTIES = 2048, SC_DUMP_VARIABLES = 4096, + SC_DUMP_PARSER_TREES = 8192, SC_DUMP_LOCALE_TABLES = 16384 +}; + +/* Module-wide trace control function prototype. */ +extern void sc_set_trace_flags(sc_uint trace_flags); + +/* Interpreter interface function prototypes. */ +extern sc_game sc_game_from_filename (const sc_char *filename); +extern sc_game sc_game_from_stream (Common::SeekableReadStream *stream); +extern sc_game sc_game_from_callback(sc_int (*callback) + (void *, sc_byte *, sc_int), + void *opaque); +extern void sc_interpret_game(sc_game game); +extern void sc_restart_game(sc_game game); +extern sc_bool sc_save_game(sc_game game); +extern sc_bool sc_load_game(sc_game game); +extern sc_bool sc_undo_game_turn(sc_game game); +extern void sc_quit_game(sc_game game); +extern sc_bool sc_save_game_to_filename(sc_game game, const sc_char *filename); +extern void sc_save_game_to_stream(sc_game game, Common::SeekableReadStream *stream); +extern void sc_save_game_to_callback(sc_game game, + void (*callback) + (void *, const sc_byte *, sc_int), + void *opaque); +extern sc_bool sc_load_game_from_filename(sc_game game, + const sc_char *filename); +extern sc_bool sc_load_game_from_stream(sc_game game, Common::SeekableReadStream *stream); +extern sc_bool sc_load_game_from_callback(sc_game game, + sc_int (*callback) + (void *, sc_byte *, sc_int), + void *opaque); +extern void sc_free_game(sc_game game); +extern sc_bool sc_is_game_running(sc_game game); +extern const sc_char *sc_get_game_name(sc_game game); +extern const sc_char *sc_get_game_author(sc_game game); +extern const sc_char *sc_get_game_compile_date(sc_game game); +extern sc_int sc_get_game_turns(sc_game game); +extern sc_int sc_get_game_score(sc_game game); +extern sc_int sc_get_game_max_score(sc_game game); +extern const sc_char *sc_get_game_room(sc_game game); +extern const sc_char *sc_get_game_status_line(sc_game game); +extern const sc_char *sc_get_game_preferred_font(sc_game game); +extern sc_bool sc_get_game_bold_room_names(sc_game game); +extern sc_bool sc_get_game_verbose(sc_game game); +extern sc_bool sc_get_game_notify_score_change(sc_game game); +extern sc_bool sc_has_game_completed(sc_game game); +extern sc_bool sc_is_game_undo_available(sc_game game); +extern void sc_set_game_bold_room_names(sc_game game, sc_bool flag); +extern void sc_set_game_verbose(sc_game game, sc_bool flag); +extern void sc_set_game_notify_score_change(sc_game game, sc_bool flag); + +extern sc_bool sc_does_game_use_sounds(sc_game); +extern sc_bool sc_does_game_use_graphics(sc_game); + +typedef void *sc_game_hint; +extern sc_game_hint sc_get_first_game_hint(sc_game game); +extern sc_game_hint sc_get_next_game_hint(sc_game game, sc_game_hint hint); +extern const sc_char *sc_get_game_hint_question(sc_game game, + sc_game_hint hint); +extern const sc_char *sc_get_game_subtle_hint(sc_game game, + sc_game_hint hint); +extern const sc_char *sc_get_game_unsubtle_hint(sc_game game, + sc_game_hint hint); + +extern void sc_set_game_debugger_enabled(sc_game game, sc_bool flag); +extern sc_bool sc_get_game_debugger_enabled(sc_game game); +extern sc_bool sc_run_game_debugger_command(sc_game game, + const sc_char *debug_command); +extern void sc_set_portable_random(sc_bool flag); +extern void sc_reseed_random_sequence(sc_uint new_seed); + +/* Locale control and query functions. */ +extern sc_bool sc_set_locale (const sc_char *name); +extern const sc_char *sc_get_locale(); + +/* A few possibly useful utilities. */ +extern sc_int sc_strncasecmp (const sc_char *s1, const sc_char *s2, sc_int n); +extern sc_int sc_strcasecmp (const sc_char *s1, const sc_char *s2); +extern const sc_char *sc_scare_version(); +extern sc_int sc_scare_emulation(); + +extern char *adrift_fgets(char *buf, int max, Common::SeekableReadStream *s); + +} // End of namespace Adrift +} // End of namespace Glk + +#endif diff --git a/engines/glk/adrift/scdebug.cpp b/engines/glk/adrift/scdebug.cpp new file mode 100644 index 0000000000..e2472cc77c --- /dev/null +++ b/engines/glk/adrift/scdebug.cpp @@ -0,0 +1,2717 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_uint DEBUG_MAGIC = 0xc4584d2e; +enum { DEBUG_BUFFER_SIZE = 256 }; + +/* Debugging command and command argument type. */ +typedef enum +{ DEBUG_NONE = 0, DEBUG_CONTINUE, DEBUG_STEP, DEBUG_BUFFER, DEBUG_RESOURCES, + DEBUG_HELP, DEBUG_GAME, + DEBUG_PLAYER, DEBUG_ROOMS, DEBUG_OBJECTS, DEBUG_NPCS, DEBUG_EVENTS, + DEBUG_TASKS, DEBUG_VARIABLES, + DEBUG_OLDPLAYER, DEBUG_OLDROOMS, DEBUG_OLDOBJECTS, DEBUG_OLDNPCS, + DEBUG_OLDEVENTS, DEBUG_OLDTASKS, DEBUG_OLDVARIABLES, + DEBUG_WATCHPLAYER, DEBUG_WATCHOBJECTS, DEBUG_WATCHNPCS, DEBUG_WATCHEVENTS, + DEBUG_WATCHTASKS, DEBUG_WATCHVARIABLES, + DEBUG_CLEARPLAYER, DEBUG_CLEAROBJECTS, DEBUG_CLEARNPCS, DEBUG_CLEAREVENTS, + DEBUG_CLEARTASKS, DEBUG_CLEARVARIABLES, + DEBUG_WATCHALL, DEBUG_CLEARALL, DEBUG_RANDOM, + DEBUG_QUIT +} +sc_command_t; + +typedef enum +{ COMMAND_QUERY = 0, COMMAND_RANGE, COMMAND_ONE, COMMAND_ALL } +sc_command_type_t; + +/* Table connecting debugging command strings to commands. */ +typedef struct +{ + const sc_char *const command_string; + const sc_command_t command; +} sc_strings_t; +static const sc_strings_t DEBUG_COMMANDS[] = { + {"continue", DEBUG_CONTINUE}, {"step", DEBUG_STEP}, {"buffer", DEBUG_BUFFER}, + {"resources", DEBUG_RESOURCES}, {"help", DEBUG_HELP}, {"game", DEBUG_GAME}, + {"player", DEBUG_PLAYER}, {"rooms", DEBUG_ROOMS}, {"objects", DEBUG_OBJECTS}, + {"npcs", DEBUG_NPCS}, {"events", DEBUG_EVENTS}, {"tasks", DEBUG_TASKS}, + {"variables", DEBUG_VARIABLES}, + {"oldplayer", DEBUG_OLDPLAYER}, {"oldrooms", DEBUG_OLDROOMS}, + {"oldobjects", DEBUG_OLDOBJECTS}, {"oldnpcs", DEBUG_OLDNPCS}, + {"oldevents", DEBUG_OLDEVENTS}, {"oldtasks", DEBUG_OLDTASKS}, + {"oldvariables", DEBUG_OLDVARIABLES}, + {"watchplayer", DEBUG_WATCHPLAYER}, {"clearplayer", DEBUG_CLEARPLAYER}, + {"watchobjects", DEBUG_WATCHOBJECTS}, {"watchnpcs", DEBUG_WATCHNPCS}, + {"watchevents", DEBUG_WATCHEVENTS}, {"watchtasks", DEBUG_WATCHTASKS}, + {"watchvariables", DEBUG_WATCHVARIABLES}, + {"clearobjects", DEBUG_CLEAROBJECTS}, {"clearnpcs", DEBUG_CLEARNPCS}, + {"clearevents", DEBUG_CLEAREVENTS}, {"cleartasks", DEBUG_CLEARTASKS}, + {"clearvariables", DEBUG_CLEARVARIABLES}, {"watchall", DEBUG_WATCHALL}, + {"clearall", DEBUG_CLEARALL}, {"random", DEBUG_RANDOM}, {"quit", DEBUG_QUIT}, + {NULL, DEBUG_NONE} +}; + +/* + * Debugging control information structure. The structure is created and + * added to the game on enabling debug, and removed and destroyed on + * disabling debugging. + */ +typedef struct sc_debugger_s +{ + sc_uint magic; + sc_bool *watch_objects; + sc_bool *watch_npcs; + sc_bool *watch_events; + sc_bool *watch_tasks; + sc_bool *watch_variables; + sc_bool watch_player; + sc_bool single_step; + sc_bool quit_pending; + sc_uint elapsed_seconds; +} sc_debugger_t; + + +/* + * debug_is_valid() + * + * Return TRUE if pointer is a valid debugger, FALSE otherwise. + */ +static sc_bool +debug_is_valid (sc_debuggerref_t debug) +{ + return debug && debug->magic == DEBUG_MAGIC; +} + + +/* + * debug_get_debugger() + * + * Return the debugger reference from a game, or NULL if none. + */ +static sc_debuggerref_t +debug_get_debugger (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + return game->debugger; +} + + +/* + * debug_variable_count() + * + * Common helper to return the count of variables defined in a game. + */ +static sc_int +debug_variable_count (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key; + sc_int variable_count; + + /* Find and return the variables count. */ + vt_key.string = "Variables"; + variable_count = prop_get_child_count (bundle, "I<-s", &vt_key); + + return variable_count; +} + + +/* + * debug_initialize() + * + * Create a new set of debug control information, and append it to the + * game passed in. + */ +static void +debug_initialize (sc_gameref_t game) +{ + sc_debuggerref_t debug; + + /* Create the easy bits of the new debugging set. */ + debug = (sc_debuggerref_t)sc_malloc (sizeof (*debug)); + debug->magic = DEBUG_MAGIC; + debug->watch_player = FALSE; + debug->single_step = FALSE; + debug->quit_pending = FALSE; + debug->elapsed_seconds = 0; + + /* Allocate watchpoints for everything we can watch. */ + debug->watch_objects = (sc_bool *)sc_malloc (gs_object_count (game) + * sizeof (*debug->watch_objects)); + debug->watch_npcs = (sc_bool *)sc_malloc (gs_npc_count (game) + * sizeof (*debug->watch_npcs)); + debug->watch_events = (sc_bool *)sc_malloc (gs_event_count (game) + * sizeof (*debug->watch_events)); + debug->watch_tasks = (sc_bool *)sc_malloc (gs_task_count (game) + * sizeof (*debug->watch_tasks)); + debug->watch_variables = (sc_bool *)sc_malloc (debug_variable_count (game) + * sizeof (*debug->watch_variables)); + + /* Clear all watchpoint arrays. */ + memset (debug->watch_objects, FALSE, + gs_object_count (game) * sizeof (*debug->watch_objects)); + memset (debug->watch_npcs, FALSE, + gs_npc_count (game) * sizeof (*debug->watch_npcs)); + memset (debug->watch_events, FALSE, + gs_event_count (game) * sizeof (*debug->watch_events)); + memset (debug->watch_tasks, FALSE, + gs_task_count (game) * sizeof (*debug->watch_tasks)); + memset (debug->watch_variables, FALSE, + debug_variable_count (game) * sizeof (*debug->watch_variables)); + + /* Append the new debugger set to the game. */ + assert (!game->debugger); + game->debugger = debug; +} + + +/* + * debug_finalize() + * + * Destroy a debug data set, free its heap memory, and remove its reference + * from the game. + */ +static void +debug_finalize (sc_gameref_t game) +{ + sc_debuggerref_t debug = debug_get_debugger (game); + assert (debug_is_valid (debug)); + + /* Free all allocated watchpoint arrays. */ + sc_free (debug->watch_objects); + sc_free (debug->watch_npcs); + sc_free (debug->watch_events); + sc_free (debug->watch_tasks); + sc_free (debug->watch_variables); + + /* Poison and free the debugger itself. */ + memset (debug, 0xaa, sizeof (*debug)); + sc_free (debug); + + /* Remove the debug reference from the game. */ + game->debugger = NULL; +} + + +/* + * debug_help() + * + * Print debugging help. + */ +static void +debug_help (sc_command_t topic) +{ + /* Is help general, or specific? */ + if (topic == DEBUG_NONE) + { + if_print_debug ( + "The following debugging commands examine game state:\n\n"); + if_print_debug ( + " game -- Print general game information," + " and class counts\n" + " player -- Show the player location and position\n" + " rooms [Range] -- Print information on game rooms\n" + " objects [Range] -- Print information on objects in the game\n" + " npcs [Range] -- Print information on game NPCs\n" + " events [Range] -- Print information on the game's events\n" + " tasks [Range] -- Print information on the game's tasks\n" + " variables [Range] -- Show variables defined by the game\n\n"); + if_print_debug ( + "Most commands take range inputs. This can be a single number, to" + " apply the command to just that item, a range such as '0 to 10' (or" + " '0 - 10', '0 .. 10', or simply '0 10') to apply to that range of" + " items, or '*' to apply the command to all items of the class. If" + " omitted, the command is applied only to the items of the class" + " 'relevant' to the current game state; see the help for specific" + " commands for more on what is 'relevant'.\n\n"); + if_print_debug ( + "The 'player', 'objects', 'npcs', 'events', 'tasks', and 'variables'" + " commands may be prefixed with 'old', in which case the values" + " printed will be those for the previous game turn, rather than the" + " current values.\n\n"); + if_print_debug ( + "These debugging commands manage watchpoints:\n\n"); + if_print_debug ( + "The 'player', 'objects', 'npcs', 'events', 'tasks', and 'variables'" + " commands may be prefixed with 'watch', to set watchpoints." + " Watchpoints automatically enter the debugger when the item changes" + " state during a game turn. For example 'watchobject 10' monitors" + " object 10 for changes, and 'watchnpc *' monitors all NPCs. A" + " 'watch' command with no range prints out all watchpoints set for" + " that class.\n\n"); + if_print_debug ( + "Prefix commands with 'clear' to clear watchpoints, for example" + " 'clearnpcs *'. Use 'watchall' to obtain a complete list of every" + " watchpoint set, and 'clearall' to clear all watchpoints in one go." + " A 'clear' command with no range behaves the same as a 'watch'" + " command with no range.\n\n"); + if_print_debug ( + "These debugging commands print details of game output and control the" + " debugger and interpreter:\n\n"); + if_print_debug ( + " buffer -- Show the current buffered game text\n" + " resources -- Show current and requested game resources\n" + " random [Seed] -- Control the random number generator\n" + " step -- Run one game turn, then re-enter the debugger\n" + " continue -- Leave the debugger and resume the game\n" + " quit -- Exit the interpreter main loop\n" + " help [Command] -- Print help specific to Command\n\n"); + if_print_debug ( + "Debugging commands may be abbreviated to their shortest unambiguous" + " form.\n\n"); + if_print_debug ( + "Use the 'debug' or '#debug' command in a game, typed at the usual" + " game prompt, to return to the debugger.\n"); + return; + } + + /* Command-specific help. */ + switch (topic) + { + case DEBUG_HELP: + if_print_debug ( + "Give the name of the command you want help on, for example 'help" + " continue'.\n"); + break; + + case DEBUG_CONTINUE: + if_print_debug ( + "Leave the debugger and resume the game. Use the 'debug' or '#debug'" + " command in a game, typed at the usual game prompt, to return to the" + " debugger.\n"); + break; + + case DEBUG_STEP: + if_print_debug ( + "Run one game turn, then re-enter the debugger. Useful for games that" + " intercept empty input lines, which otherwise catch the 'debug'" + " command before SCARE can get to it.\n"); + break; + + case DEBUG_QUIT: + if_print_debug ( + "Exit the interpreter main loop. Equivalent to a confirmed 'quit'" + " from within the game itself, this ends the interpreter session.\n"); + break; + + case DEBUG_BUFFER: + if_print_debug ( + "Print the current text that the game has buffered for output. The" + " debugger catches games before they have printed their turn output" + " -- this is the text that will be filtered and printed on exiting the" + " debugger.\n"); + break; + + case DEBUG_RESOURCES: + if_print_debug ( + "Print any resources currently active, and any requested by the game" + " on the current turn. The requested resources will become the active" + " ones on exiting the debugger.\n"); + break; + + case DEBUG_RANDOM: + if_print_debug ( + "If no seed is given, report the current random number generator" + " setting. Otherwise, seed the random number generator with the value" + " given. This is useful for persuading games with random sections to" + " behave predictably. A new seed value of zero is invalid.\n"); + break; + + case DEBUG_GAME: + if_print_debug ( + "Print general game information, including the number of rooms," + " objects, events, tasks, and variables that the game defines\n"); + break; + + case DEBUG_PLAYER: + if_print_debug ( + "Print out the current player room and position, and any parent object" + " of the player character.\n"); + break; + + case DEBUG_OLDPLAYER: + if_print_debug ( + "Print out the player room and position from the previous turn, and" + " any parent object of the player character.\n"); + break; + + case DEBUG_ROOMS: + if_print_debug ( + "Print out the name and contents of rooms in the range. If no range," + " print details of the room containing the player.\n"); + break; + + case DEBUG_OLDROOMS: + if_print_debug ( + "Print out the name and contents of rooms in the range for the" + " previous turn. If no range, print details of the room that" + " contained the player on the previous turn.\n"); + break; + + case DEBUG_OBJECTS: + if_print_debug ( + "Print out details of all objects in the range. If no range, print" + " details of objects in the room containing the player, and visible to" + " the player.\n"); + break; + + case DEBUG_OLDOBJECTS: + if_print_debug ( + "Print out details of all objects in the range for the previous turn." + " If no range, print details of objects in the room that contained" + " the player, and were visible to the player.\n"); + break; + + case DEBUG_NPCS: + if_print_debug ( + "Print out details of all NPCs in the range. If no range, print" + " details of only NPCs in the room containing the player.\n"); + break; + + case DEBUG_OLDNPCS: + if_print_debug ( + "Print out details of all NPCs in the range for the previous turn." + " If no range, print details of only NPCs in the room that contained" + " the player.\n"); + break; + + case DEBUG_EVENTS: + if_print_debug ( + "Print out details of all events in the range. If no range, print" + " details of only events currently running.\n"); + break; + + case DEBUG_OLDEVENTS: + if_print_debug ( + "Print out details of all events in the range for the previous turn." + " If no range, print details of only events running on the previous" + " turn.\n"); + break; + + case DEBUG_TASKS: + if_print_debug ( + "Print out details of all tasks in the range. If no range, print" + " details of only tasks that are runnable, for the current state of" + " the game.\n"); + break; + + case DEBUG_OLDTASKS: + if_print_debug ( + "Print out details of all tasks in the range for the previous turn." + " If no range, print details of only tasks that were runnable, for" + " the previous state of the game.\n"); + break; + + case DEBUG_VARIABLES: + if_print_debug ( + "Print out the names, types, and values of all game variables in the" + " range. If no range, print details of all variables (equivalent to" + " 'variables *').\n"); + break; + + case DEBUG_OLDVARIABLES: + if_print_debug ( + "Print out the names, types, and values at the previous turn of all" + " game variables in the range. If no range, print details of all" + " variables (equivalent to 'variables *').\n"); + break; + + case DEBUG_WATCHPLAYER: + if_print_debug ( + "If no range is given, list any watchpoint on player movement. If" + " range '0' is given, set a watchpoint on player movement. Other" + " usages of 'watchplayer' behave as if no range is given.\n"); + break; + + case DEBUG_WATCHOBJECTS: + if_print_debug ( + "Set watchpoints on all objects in the range. If no range, list out" + " object watchpoints currently set.\n"); + break; + + case DEBUG_WATCHNPCS: + if_print_debug ( + "Set watchpoints on all NPCs in the range. If no range, list out NPC" + " watchpoints currently set.\n"); + break; + + case DEBUG_WATCHEVENTS: + if_print_debug ( + "Set watchpoints on all events in the range. If no range, list out" + " event watchpoints currently set.\n"); + break; + + case DEBUG_WATCHTASKS: + if_print_debug ( + "Set watchpoints on all tasks in the range. If no range, list out" + " task watchpoints currently set.\n"); + break; + + case DEBUG_WATCHVARIABLES: + if_print_debug ( + "Set watchpoints on all game variables in the range. If no range," + " list variable watchpoints currently set.\n"); + break; + + case DEBUG_CLEARPLAYER: + if_print_debug ( + "Clear any watchpoint set on player movements.\n"); + break; + + case DEBUG_CLEAROBJECTS: + if_print_debug ( + "Clear watchpoints on all objects in the range. If no range, list" + " out object watchpoints currently set.\n"); + break; + + case DEBUG_CLEARNPCS: + if_print_debug ( + "Clear watchpoints on all NPCs in the range. If no range, list out" + " NPC watchpoints currently set.\n"); + break; + + case DEBUG_CLEAREVENTS: + if_print_debug ( + "Clear watchpoints on all events in the range. If no range, list out" + " event watchpoints currently set.\n"); + break; + + case DEBUG_CLEARTASKS: + if_print_debug ( + "Clear watchpoints on all tasks in the range. If no range, list out" + " task watchpoints currently set.\n"); + break; + + case DEBUG_CLEARVARIABLES: + if_print_debug ( + "Clear watchpoints on all game variables in the range. If no range," + " list variable watchpoints currently set.\n"); + break; + + case DEBUG_WATCHALL: + if_print_debug ( + "Print out a list of all all watchpoints set for all the classes of" + " item on which watchpoints can be used.\n"); + break; + + case DEBUG_CLEARALL: + if_print_debug ( + "Clear all watchpoints set, on all classes of item on which" + " watchpoints can be used.\n"); + break; + + default: + if_print_debug ( + "Sorry, there is no help available on that at the moment.\n"); + break; + } +} + + +/* + * debug_print_quoted() + * debug_print_player() + * debug_print_room() + * debug_print_object() + * debug_print_npc() + * debug_print_event() + * debug_print_task() + * debug_print_variable() + * + * Low level output helpers. + */ +static void +debug_print_quoted (const sc_char *string) +{ + if_print_debug_character ('"'); + if_print_debug (string); + if_print_debug_character ('"'); +} + +static void +debug_print_player (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + const sc_char *playername; + + vt_key[0].string = "Globals"; + vt_key[1].string = "PlayerName"; + playername = prop_get_string (bundle, "S<-ss", vt_key); + if_print_debug ("Player "); + debug_print_quoted (playername); +} + +static void +debug_print_room (sc_gameref_t game, sc_int room) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_char buffer[32]; + const sc_char *name; + + if_print_debug ("Room "); + if (room < 0 || room >= gs_room_count (game)) + { + sprintf (buffer, "%ld ", room); + if_print_debug (buffer); + if_print_debug ("[Out of range]"); + return; + } + + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + sprintf (buffer, "%ld ", room); + if_print_debug (buffer); + debug_print_quoted (name); +} + +static void +debug_print_object (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_bool bstatic; + sc_char buffer[32]; + const sc_char *prefix, *name; + + if (object < 0 || object >= gs_object_count (game)) + { + if_print_debug ("Object "); + sprintf (buffer, "%ld ", object); + if_print_debug (buffer); + if_print_debug ("[Out of range]"); + return; + } + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Static"; + bstatic = prop_get_boolean (bundle, "B<-sis", vt_key); + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + if (bstatic) + if_print_debug ("Static "); + else + if_print_debug ("Dynamic "); + sprintf (buffer, "%ld ", object); + if_print_debug (buffer); + debug_print_quoted (prefix); + if_print_debug_character (' '); + debug_print_quoted (name); +} + +static void +debug_print_npc (sc_gameref_t game, sc_int npc) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_char buffer[32]; + const sc_char *prefix, *name; + + if_print_debug ("NPC "); + if (npc < 0 || npc >= gs_npc_count (game)) + { + sprintf (buffer, "%ld ", npc); + if_print_debug (buffer); + if_print_debug ("[Out of range]"); + return; + } + + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + sprintf (buffer, "%ld ", npc); + if_print_debug (buffer); + debug_print_quoted (prefix); + if_print_debug_character (' '); + debug_print_quoted (name); +} + +static void +debug_print_event (sc_gameref_t game, sc_int event) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_char buffer[32]; + const sc_char *name; + + if_print_debug ("Event "); + if (event < 0 || event >= gs_event_count (game)) + { + sprintf (buffer, "%ld ", event); + if_print_debug (buffer); + if_print_debug ("[Out of range]"); + return; + } + + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + sprintf (buffer, "%ld ", event); + if_print_debug (buffer); + debug_print_quoted (name); +} + +static void +debug_print_task (sc_gameref_t game, sc_int task) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_char buffer[32]; + const sc_char *command; + + if_print_debug ("Task "); + if (task < 0 || task >= gs_task_count (game)) + { + sprintf (buffer, "%ld ", task); + if_print_debug (buffer); + if_print_debug ("[Out of range]"); + return; + } + + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Command"; + vt_key[3].integer = 0; + command = prop_get_string (bundle, "S<-sisi", vt_key); + sprintf (buffer, "%ld ", task); + if_print_debug (buffer); + debug_print_quoted (command); +} + +static void +debug_print_variable (sc_gameref_t game, sc_int variable) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[3], vt_rvalue; + sc_char buffer[32]; + sc_int var_type; + const sc_char *name; + + if (variable < 0 || variable >= debug_variable_count (game)) + { + if_print_debug ("Variable "); + sprintf (buffer, "%ld ", variable); + if_print_debug (buffer); + if_print_debug ("[Out of range]"); + return; + } + + vt_key[0].string = "Variables"; + vt_key[1].integer = variable; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + if (var_get (vars, name, &var_type, &vt_rvalue)) + { + switch (var_type) + { + case VAR_INTEGER: + if_print_debug ("Integer "); + break; + case VAR_STRING: + if_print_debug ("String "); + break; + default: + if_print_debug ("[Invalid type] "); + break; + } + } + else + if_print_debug ("[Invalid variable] "); + sprintf (buffer, "%ld ", variable); + if_print_debug (buffer); + debug_print_quoted (name); +} + + +/* + * debug_game() + * + * Display overall game details. + */ +static void +debug_game (sc_gameref_t game, sc_command_type_t type) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_debuggerref_t debug = debug_get_debugger (game); + sc_vartype_t vt_key[2]; + const sc_char *version, *gamename, *compiledate, *gameauthor; + sc_int perspective, waitturns; + sc_bool has_sound, has_graphics, has_battle; + sc_char buffer[32]; + assert (debug_is_valid (debug)); + + if (type != COMMAND_QUERY) + { + if_print_debug ("The Game command takes no arguments.\n"); + return; + } + + if_print_debug ("Game "); + vt_key[0].string = "Globals"; + vt_key[1].string = "GameName"; + gamename = prop_get_string (bundle, "S<-ss", vt_key); + debug_print_quoted (gamename); + if_print_debug_character ('\n'); + + if_print_debug (" Compiled "); + vt_key[0].string = "CompileDate"; + compiledate = prop_get_string (bundle, "S<-s", vt_key); + debug_print_quoted (compiledate); + + if_print_debug (", Author "); + vt_key[0].string = "Globals"; + vt_key[1].string = "GameAuthor"; + gameauthor = prop_get_string (bundle, "S<-ss", vt_key); + debug_print_quoted (gameauthor); + if_print_debug_character ('\n'); + + vt_key[0].string = "VersionString"; + version = prop_get_string (bundle, "S<-s", vt_key); + if_print_debug (" Version "); + if_print_debug (version); + + vt_key[0].string = "Globals"; + vt_key[1].string = "Perspective"; + perspective = prop_get_integer (bundle, "I<-ss", vt_key); + switch (perspective) + { + case 0: + if_print_debug (", First person"); + break; + case 1: + if_print_debug (", Second person"); + break; + case 2: + if_print_debug (", Third person"); + break; + default: + if_print_debug (", [Unknown perspective]"); + break; + } + + vt_key[0].string = "Globals"; + vt_key[1].string = "WaitTurns"; + waitturns = prop_get_integer (bundle, "I<-ss", vt_key); + if_print_debug (", Waitturns "); + sprintf (buffer, "%ld", waitturns); + if_print_debug (buffer); + + vt_key[0].string = "Globals"; + vt_key[1].string = "Sound"; + has_sound = prop_get_boolean (bundle, "B<-ss", vt_key); + vt_key[1].string = "Graphics"; + has_graphics = prop_get_boolean (bundle, "B<-ss", vt_key); + if (has_sound) + if_print_debug (", Sound"); + if (has_graphics) + if_print_debug (", Graphics"); + if_print_debug_character ('\n'); + + vt_key[0].string = "Globals"; + vt_key[1].string = "BattleSystem"; + has_battle = prop_get_boolean (bundle, "B<-ss", vt_key); + if (has_battle) + if_print_debug (" Battle system\n"); + + if_print_debug (" Room count "); + sprintf (buffer, "%ld", gs_room_count (game)); + if_print_debug (buffer); + + if_print_debug (", Object count "); + sprintf (buffer, "%ld", gs_object_count (game)); + if_print_debug (buffer); + + if_print_debug (", NPC count "); + sprintf (buffer, "%ld", gs_npc_count (game)); + if_print_debug (buffer); + if_print_debug_character ('\n'); + + if_print_debug (" Event count "); + sprintf (buffer, "%ld", gs_event_count (game)); + if_print_debug (buffer); + + if_print_debug (", Task count "); + sprintf (buffer, "%ld", gs_task_count (game)); + if_print_debug (buffer); + + if_print_debug (", Variable count "); + sprintf (buffer, "%ld", debug_variable_count (game)); + if_print_debug (buffer); + if_print_debug_character ('\n'); + + if (game->is_running) + if_print_debug (" Running"); + else + if_print_debug (" Not running"); + if (game->has_completed) + if_print_debug (", Completed"); + else + if_print_debug (", Not completed"); + if (game->verbose) + if_print_debug (", Verbose"); + else + if_print_debug (", Not verbose"); + if (game->bold_room_names) + if_print_debug (", Bold"); + else + if_print_debug (", Not bold"); + if (game->undo_available) + if_print_debug (", Undo"); + else + if_print_debug (", No undo"); + if_print_debug_character ('\n'); + + if_print_debug (" Score "); + sprintf (buffer, "%ld", game->score); + if_print_debug (buffer); + if_print_debug (", Turns "); + sprintf (buffer, "%ld", game->turns); + if_print_debug (buffer); + if_print_debug (", Seconds "); + sprintf (buffer, "%lu", debug->elapsed_seconds); + if_print_debug (buffer); + if_print_debug_character ('\n'); +} + + +/* + * debug_player() + * + * Print a few brief details about the player status. + */ +static void +debug_player (sc_gameref_t game, + sc_command_t command, sc_command_type_t type) +{ + if (type != COMMAND_QUERY) + { + if_print_debug ("The Player command takes no arguments.\n"); + return; + } + + if (command == DEBUG_OLDPLAYER) + { + if (!game->undo_available) + { + if_print_debug ("There is no previous game state to examine.\n"); + return; + } + + game = game->undo; + assert (gs_is_game_valid (game)); + } + + debug_print_player (game); + if_print_debug_character ('\n'); + + if (gs_playerroom (game) == -1) + if_print_debug (" Hidden!\n"); + else + { + if_print_debug (" In "); + debug_print_room (game, gs_playerroom (game)); + if_print_debug_character ('\n'); + } + + switch (gs_playerposition (game)) + { + case 0: + if_print_debug (" Standing\n"); + break; + case 1: + if_print_debug (" Sitting\n"); + break; + case 2: + if_print_debug (" Lying\n"); + break; + default: + if_print_debug (" [Invalid position]\n"); + break; + } + + if (gs_playerparent (game) != -1) + { + if_print_debug (" Parent is "); + debug_print_object (game, gs_playerparent (game)); + if_print_debug_character ('\n'); + } +} + + +/* + * debug_normalize_arguments() + * + * Normalize a set of arguments parsed from a debugger command line, for + * debug commands that take ranges. + */ +static sc_bool +debug_normalize_arguments (sc_command_type_t type, + sc_int *arg1, sc_int *arg2, sc_int limit) +{ + sc_int low = 0, high = 0; + + /* Set range low and high depending on the command type. */ + switch (type) + { + case COMMAND_QUERY: + case COMMAND_ALL: + low = 0; + high = limit - 1; + break; + case COMMAND_ONE: + low = *arg1; + high = *arg1; + break; + case COMMAND_RANGE: + low = *arg1; + high = *arg2; + break; + default: + sc_fatal ("debug_normalize_arguments: bad command type\n"); + } + + /* If range is valid, copy out and return TRUE. */ + if (low >= 0 && low < limit && high >= 0 && high < limit && high >= low) + { + *arg1 = low; + *arg2 = high; + return TRUE; + } + + /* Input range is invalid. */ + return FALSE; +} + + +/* + * debug_filter_room() + * debug_dump_room() + * + * Print details of rooms and their direct contents. + */ +static sc_bool +debug_filter_room (sc_gameref_t game, sc_int room) +{ + return room == gs_playerroom (game); +} + +static void +debug_dump_room (sc_gameref_t game, sc_int room) +{ + sc_int object, npc; + + debug_print_room (game, room); + if_print_debug_character ('\n'); + + if (gs_room_seen (game, room)) + if_print_debug (" Visited\n"); + else + if_print_debug (" Not visited\n"); + + if (gs_playerroom (game) == room) + { + if_print_debug (" "); + debug_print_player (game); + if_print_debug_character ('\n'); + } + + for (object = 0; object < gs_object_count (game); object++) + { + if (obj_indirectly_in_room (game, object, room)) + { + if_print_debug (" "); + debug_print_object (game, object); + if_print_debug_character ('\n'); + } + } + + for (npc = 0; npc < gs_npc_count (game); npc++) + { + if (npc_in_room (game, npc, room)) + { + if_print_debug (" "); + debug_print_npc (game, npc); + if_print_debug_character ('\n'); + } + } +} + + +/* + * debug_filter_object() + * debug_dump_object() + * + * Print the changeable details of game objects. + */ +static sc_bool +debug_filter_object (sc_gameref_t game, sc_int object) +{ + return obj_indirectly_in_room (game, object, gs_playerroom (game)); +} + +static void +debug_dump_object (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int openness; + sc_vartype_t vt_key[3]; + sc_bool bstatic, is_statussed; + sc_int position, parent; + + debug_print_object (game, object); + if_print_debug_character ('\n'); + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Static"; + bstatic = prop_get_boolean (bundle, "B<-sis", vt_key); + + if (gs_object_seen (game, object)) + if_print_debug (" Seen"); + else + if_print_debug (" Not seen"); + if (bstatic) + { + if (gs_object_static_unmoved (game, object)) + if_print_debug (", Not relocated"); + else + if_print_debug (", Relocated"); + } + else + { + vt_key[2].string = "OnlyWhenNotMoved"; + if (prop_get_integer (bundle, "I<-sis", vt_key) == 1) + { + if (gs_object_unmoved (game, object)) + if_print_debug (", Not moved"); + else + if_print_debug (", Moved"); + } + } + openness = gs_object_openness (game, object); + switch (openness) + { + case OBJ_OPEN: + if_print_debug (", Open"); + break; + case OBJ_CLOSED: + if_print_debug (", Closed"); + break; + case OBJ_LOCKED: + if_print_debug (", Locked"); + break; + } + if_print_debug_character ('\n'); + + position = gs_object_position (game, object); + parent = gs_object_parent (game, object); + switch (position) + { + case OBJ_HIDDEN: + if (bstatic) + if_print_debug (" Static default\n"); + else + if_print_debug (" Hidden\n"); + break; + case OBJ_HELD_PLAYER: + if_print_debug (" Held by "); + debug_print_player (game); + if_print_debug_character ('\n'); + break; + case OBJ_HELD_NPC: + if_print_debug (" Held by "); + debug_print_npc (game, parent); + if_print_debug_character ('\n'); + break; + case OBJ_WORN_PLAYER: + if_print_debug (" Worn by "); + debug_print_player (game); + if_print_debug_character ('\n'); + break; + case OBJ_WORN_NPC: + if_print_debug (" Worn by "); + debug_print_npc (game, parent); + if_print_debug_character ('\n'); + break; + case OBJ_PART_NPC: + if_print_debug (" Part of "); + if (parent == -1) + debug_print_player (game); + else + debug_print_npc (game, parent); + if_print_debug_character ('\n'); + break; + case OBJ_ON_OBJECT: + if_print_debug (" On "); + debug_print_object (game, parent); + if_print_debug_character ('\n'); + break; + case OBJ_IN_OBJECT: + if_print_debug (" Inside "); + debug_print_object (game, parent); + if_print_debug_character ('\n'); + break; + default: + if_print_debug (" In "); + debug_print_room (game, position - 1); + if_print_debug_character ('\n'); + break; + } + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "CurrentState"; + is_statussed = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (is_statussed) + { + sc_char buffer[32]; + const sc_char *states; + + if_print_debug (" State "); + sprintf (buffer, "%ld", gs_object_state (game, object)); + if_print_debug (buffer); + + vt_key[2].string = "States"; + states = prop_get_string (bundle, "S<-sis", vt_key); + if_print_debug (" of "); + debug_print_quoted (states); + if_print_debug_character ('\n'); + } +} + + +/* + * debug_filter_npc() + * debug_dump_npc() + * + * Print stuff about NPCs. + */ +static sc_bool +debug_filter_npc (sc_gameref_t game, sc_int npc) +{ + return npc_in_room (game, npc, gs_playerroom (game)); +} + +static void +debug_dump_npc (sc_gameref_t game, sc_int npc) +{ + debug_print_npc (game, npc); + if_print_debug_character ('\n'); + + if (gs_npc_seen (game, npc)) + if_print_debug (" Seen\n"); + else + if_print_debug (" Not seen\n"); + + if (gs_npc_location (game, npc) - 1 == -1) + if_print_debug (" Hidden\n"); + else + { + if_print_debug (" In "); + debug_print_room (game, gs_npc_location (game, npc) - 1); + if_print_debug_character ('\n'); + } + + switch (gs_npc_position (game, npc)) + { + case 0: + if_print_debug (" Standing\n"); + break; + case 1: + if_print_debug (" Sitting\n"); + break; + case 2: + if_print_debug (" Lying\n"); + break; + default: + if_print_debug (" [Invalid position]\n"); + break; + } + + if (gs_npc_parent (game, npc) != -1) + { + if_print_debug (" Parent is "); + debug_print_object (game, gs_npc_parent (game, npc)); + if_print_debug_character ('\n'); + } + + if (gs_npc_walkstep_count (game, npc) > 0) + { + sc_char buffer[32]; + sc_int walk; + + if_print_debug (" Walkstep count "); + sprintf (buffer, "%ld", gs_npc_walkstep_count (game, npc)); + if_print_debug (buffer); + if_print_debug (", Walks { "); + for (walk = 0; walk < gs_npc_walkstep_count (game, npc); walk++) + { + sprintf (buffer, "%ld", gs_npc_walkstep (game, npc, walk)); + if_print_debug (buffer); + if_print_debug_character (' '); + } + if_print_debug ("}.\n"); + } +} + + +/* + * debug_filter_event() + * debug_dump_event() + * + * Print stuff about events. + */ +static sc_bool +debug_filter_event (sc_gameref_t game, sc_int event) +{ + return gs_event_state (game, event) == ES_RUNNING; +} + +static void +debug_dump_event (sc_gameref_t game, sc_int event) +{ + sc_char buffer[32]; + + debug_print_event (game, event); + if_print_debug_character ('\n'); + + switch (gs_event_state (game, event)) + { + case ES_WAITING: + if_print_debug (" Waiting\n"); + break; + case ES_RUNNING: + if_print_debug (" Running\n"); + break; + case ES_AWAITING: + if_print_debug (" Awaiting\n"); + break; + case ES_FINISHED: + if_print_debug (" Finished\n"); + break; + case ES_PAUSED: + if_print_debug (" Paused\n"); + break; + default: + if_print_debug (" [Invalid state]\n"); + break; + } + + if_print_debug (" Time "); + sprintf (buffer, "%ld\n", gs_event_time (game, event)); + if_print_debug (buffer); +} + + +/* + * debug_filter_task() + * debug_dump_task() + * + * Print stuff about tasks. + */ +static sc_bool +debug_filter_task (sc_gameref_t game, sc_int task) +{ + return task_can_run_task (game, task); +} + +static void +debug_dump_task (sc_gameref_t game, sc_int task) +{ + debug_print_task (game, task); + if_print_debug_character ('\n'); + + if (task_can_run_task (game, task)) + if_print_debug (" Runnable"); + else + if_print_debug (" Not runnable"); + if (gs_task_done (game, task)) + if_print_debug (", Done"); + else + if_print_debug (", Not done"); + if (gs_task_scored (game, task)) + if_print_debug (", Scored\n"); + else + if_print_debug (", Not scored\n"); +} + + +/* + * debug_dump_variable() + * + * Print stuff about variables. + */ +static void +debug_dump_variable (sc_gameref_t game, sc_int variable) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[3], vt_rvalue; + const sc_char *name; + sc_int var_type; + + debug_print_variable (game, variable); + if_print_debug_character ('\n'); + + vt_key[0].string = "Variables"; + vt_key[1].integer = variable; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + if_print_debug (" Value = "); + if (var_get (vars, name, &var_type, &vt_rvalue)) + { + switch (var_type) + { + case VAR_INTEGER: + { + sc_char buffer[32]; + + sprintf (buffer, "%ld", vt_rvalue.integer); + if_print_debug (buffer); + break; + } + case VAR_STRING: + debug_print_quoted (vt_rvalue.string); + break; + default: + if_print_debug ("[Unknown]"); + break; + } + } + else + if_print_debug ("[Unknown]"); + if_print_debug_character ('\n'); +} + + +/* + * debug_dump_common() + * + * Common handler for iterating dumps of classes. + */ +static void +debug_dump_common (sc_gameref_t game, sc_command_t command, + sc_command_type_t type, sc_int arg1, sc_int arg2) +{ + sc_int low = arg1, high = arg2; + sc_int limit, index_; + const sc_char *class_; + sc_bool (*filter_function) (sc_gameref_t, sc_int); + void (*dumper_function) (sc_gameref_t, sc_int); + sc_bool printed = FALSE; + + /* Initialize variables to avoid gcc warnings. */ + limit = 0; + class_ = NULL; + filter_function = NULL; + dumper_function = NULL; + + /* Switch to undo game on relevant commands. */ + switch (command) + { + case DEBUG_OLDROOMS: + case DEBUG_OLDOBJECTS: + case DEBUG_OLDNPCS: + case DEBUG_OLDEVENTS: + case DEBUG_OLDTASKS: + case DEBUG_OLDVARIABLES: + if (!game->undo_available) + { + if_print_debug ("There is no previous game state to examine.\n"); + return; + } + + game = game->undo; + assert (gs_is_game_valid (game)); + break; + + default: + break; + } + + /* Demultiplex dump command. */ + switch (command) + { + case DEBUG_ROOMS: + case DEBUG_OLDROOMS: + class_ = "Room"; + filter_function = debug_filter_room; + dumper_function = debug_dump_room; + limit = gs_room_count (game); + break; + case DEBUG_OBJECTS: + case DEBUG_OLDOBJECTS: + class_ = "Object"; + filter_function = debug_filter_object; + dumper_function = debug_dump_object; + limit = gs_object_count (game); + break; + case DEBUG_NPCS: + case DEBUG_OLDNPCS: + class_ = "NPC"; + filter_function = debug_filter_npc; + dumper_function = debug_dump_npc; + limit = gs_npc_count (game); + break; + case DEBUG_EVENTS: + case DEBUG_OLDEVENTS: + class_ = "Event"; + filter_function = debug_filter_event; + dumper_function = debug_dump_event; + limit = gs_event_count (game); + break; + case DEBUG_TASKS: + case DEBUG_OLDTASKS: + class_ = "Task"; + filter_function = debug_filter_task; + dumper_function = debug_dump_task; + limit = gs_task_count (game); + break; + case DEBUG_VARIABLES: + case DEBUG_OLDVARIABLES: + class_ = "Variable"; + filter_function = NULL; + dumper_function = debug_dump_variable; + limit = debug_variable_count (game); + break; + default: + sc_fatal ("debug_dump_common: invalid command\n"); + } + + /* Normalize to this limit. */ + if (!debug_normalize_arguments (type, &low, &high, limit)) + { + if (limit == 0) + { + if_print_debug ("There is nothing of type "); + debug_print_quoted (class_); + if_print_debug (" to print.\n"); + } + else + { + if_print_debug ("Invalid item or range for "); + debug_print_quoted (class_); + if (limit == 1) + if_print_debug ("; only 0 is valid.\n"); + else + { + sc_char buffer[32]; + + if_print_debug ("; valid values are 0 to "); + sprintf (buffer, "%ld", limit - 1); + if_print_debug (buffer); + if_print_debug (".\n"); + } + } + return; + } + + /* Print each item of the class, filtering on query commands. */ + for (index_ = low; index_ <= high; index_++) + { + if (type == COMMAND_QUERY + && filter_function && !filter_function (game, index_)) + continue; + + if (printed) + if_print_debug_character ('\n'); + dumper_function (game, index_); + printed = TRUE; + } + if (!printed) + { + if_print_debug ("Nothing of type "); + debug_print_quoted (class_); + if_print_debug (" is relevant.\nTry \""); + if_print_debug (class_); + if_print_debug (" *\" to show all items of this type.\n"); + } +} + + +/* + * debug_buffer() + * + * Print the current raw printfilter contents. + */ +static void +debug_buffer (sc_gameref_t game, sc_command_type_t type) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_char *buffer; + + if (type != COMMAND_QUERY) + { + if_print_debug ("The Buffer command takes no arguments.\n"); + return; + } + + buffer = pf_get_buffer (filter); + if (buffer) + if_print_debug (buffer); + else + if_print_debug ("There is no game text buffered.\n"); +} + + +/* + * debug_print_resource() + * + * Helper for debug_resources(). + */ +static void +debug_print_resource (const sc_resource_t *resource) +{ + sc_char buffer[32]; + + debug_print_quoted (resource->name); + if_print_debug (", offset "); + sprintf (buffer, "%ld", resource->offset); + if_print_debug (buffer); + if_print_debug (", length "); + sprintf (buffer, "%ld", resource->length); + if_print_debug (buffer); +} + + +/* + * debug_resources() + * + * Print any active and requested resources. + */ +static void +debug_resources (sc_gameref_t game, sc_command_type_t type) +{ + sc_bool printed = FALSE; + + if (type != COMMAND_QUERY) + { + if_print_debug ("The Resources command takes no arguments.\n"); + return; + } + + if (game->stop_sound) + { + if_print_debug ("Sound stop"); + if (strlen (game->requested_sound.name) > 0) + if_print_debug (" before new sound"); + if_print_debug (" requested"); + if (game->sound_active) + if_print_debug (", sound active"); + if_print_debug (".\n"); + printed = TRUE; + } + if (!res_compare_resource (&game->requested_sound, + &game->playing_sound)) + { + if_print_debug ("Requested Sound "); + debug_print_resource (&game->requested_sound); + if_print_debug (".\n"); + printed = TRUE; + } + if (!res_compare_resource (&game->requested_graphic, + &game->displayed_graphic)) + { + if_print_debug ("Requested Graphic "); + debug_print_resource (&game->requested_graphic); + if_print_debug (".\n"); + printed = TRUE; + } + + if (strlen (game->playing_sound.name) > 0) + { + if_print_debug ("Playing Sound "); + debug_print_resource (&game->playing_sound); + if_print_debug (".\n"); + printed = TRUE; + } + if (strlen (game->displayed_graphic.name) > 0) + { + if_print_debug ("Displaying Graphic "); + debug_print_resource (&game->displayed_graphic); + if_print_debug (".\n"); + printed = TRUE; + } + + if (!printed) + if_print_debug ("There is no game resource activity.\n"); +} + + +/* + * debug_random() + * + * Report the PRNG in use, and seed the random number generator to the + * given value. + */ +static void +debug_random (sc_command_type_t type, sc_int new_seed) +{ + const sc_char *random_type; + sc_char buffer[32]; + + if (type != COMMAND_ONE && type != COMMAND_QUERY) + { + if_print_debug ("The Random command takes either one argument or" + " no arguments.\n"); + return; + } + + random_type = sc_is_congruential_random () ? "congruential" : "platform"; + + if (type == COMMAND_QUERY) + { + if_print_debug ("The "); + if_print_debug (random_type); + if_print_debug (" random number generator is selected.\n"); + return; + } + + if (new_seed == 0) + { + if_print_debug ("The seed value may not be zero.\n"); + return; + } + + sc_seed_random (new_seed); + + if_print_debug ("Set seed "); + sprintf (buffer, "%ld", new_seed); + if_print_debug (buffer); + if_print_debug (" for the "); + if_print_debug (random_type); + if_print_debug (" random number generator.\n"); +} + + +/* + * debug_watchpoint_common() + * + * Common handler for setting and clearing watchpoints. + */ +static void +debug_watchpoint_common (sc_gameref_t game, sc_command_t command, + sc_command_type_t type, sc_int arg1, sc_int arg2) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + sc_int low = arg1, high = arg2; + sc_int limit, index_; + const sc_char *class_; + sc_bool *watchpoints, action; + sc_char buffer[32]; + assert (debug_is_valid (debug)); + + /* Initialize variables to avoid gcc warnings. */ + limit = 0; + class_ = NULL; + watchpoints = NULL; + action = FALSE; + + /* Set action to TRUE or FALSE, for setting/clearing watchpoints. */ + switch (command) + { + case DEBUG_WATCHPLAYER: + case DEBUG_WATCHOBJECTS: + case DEBUG_WATCHNPCS: + case DEBUG_WATCHEVENTS: + case DEBUG_WATCHTASKS: + case DEBUG_WATCHVARIABLES: + action = TRUE; + break; + case DEBUG_CLEARPLAYER: + case DEBUG_CLEAROBJECTS: + case DEBUG_CLEARNPCS: + case DEBUG_CLEAREVENTS: + case DEBUG_CLEARTASKS: + case DEBUG_CLEARVARIABLES: + action = FALSE; + break; + default: + sc_fatal ("debug_watchpoint_common: invalid command\n"); + } + + /* Handle player watchpoint setting. */ + if (command == DEBUG_WATCHPLAYER || command == DEBUG_CLEARPLAYER) + { + if (command == DEBUG_CLEARPLAYER) + { + debug->watch_player = action; + if_print_debug ("Cleared Player watchpoint.\n"); + } + else if (type == COMMAND_ONE && arg1 == 0) + { + debug->watch_player = action; + if_print_debug ("Set Player watchpoint.\n"); + } + else + { + if (debug->watch_player) + if_print_debug ("Player watchpoint is set.\n"); + else + if_print_debug ("No Player watchpoint is set; to set one, use" + " \"Watchplayer 0\".\n"); + } + return; + } + + /* Demultiplex watchpoint command. */ + switch (command) + { + case DEBUG_WATCHOBJECTS: + case DEBUG_CLEAROBJECTS: + class_ = "Object"; + watchpoints = debug->watch_objects; + limit = gs_object_count (game); + break; + case DEBUG_WATCHNPCS: + case DEBUG_CLEARNPCS: + class_ = "NPC"; + watchpoints = debug->watch_npcs; + limit = gs_npc_count (game); + break; + case DEBUG_WATCHEVENTS: + case DEBUG_CLEAREVENTS: + class_ = "Event"; + watchpoints = debug->watch_events; + limit = gs_event_count (game); + break; + case DEBUG_WATCHTASKS: + case DEBUG_CLEARTASKS: + class_ = "Task"; + watchpoints = debug->watch_tasks; + limit = gs_task_count (game); + break; + case DEBUG_WATCHVARIABLES: + case DEBUG_CLEARVARIABLES: + class_ = "Variable"; + watchpoints = debug->watch_variables; + limit = debug_variable_count (game); + break; + default: + sc_fatal ("debug_watchpoint_common: invalid command\n"); + } + + /* Normalize to this limit. */ + if (!debug_normalize_arguments (type, &low, &high, limit)) + { + if (limit == 0) + { + if_print_debug ("There is nothing of type "); + debug_print_quoted (class_); + if_print_debug (" to watch.\n"); + } + else + { + if_print_debug ("Invalid item or range for "); + debug_print_quoted (class_); + if (limit == 1) + if_print_debug ("; only 0 is valid.\n"); + else + { + if_print_debug ("; valid values are 0 to "); + sprintf (buffer, "%ld", limit - 1); + if_print_debug (buffer); + if_print_debug (".\n"); + } + } + return; + } + + /* On query, search the array for set flags, and print out. */ + if (type == COMMAND_QUERY) + { + sc_bool printed = FALSE; + + /* Scan for set watchpoints, and list each found. */ + for (index_ = low; index_ <= high; index_++) + { + if (watchpoints[index_]) + { + if (!printed) + { + if_print_debug ("Watchpoints are set for "); + if_print_debug (class_); + if_print_debug (" { "); + } + sprintf (buffer, "%ld", index_); + if_print_debug (buffer); + if_print_debug_character (' '); + printed = TRUE; + } + } + if (printed) + if_print_debug ("}.\n"); + else + { + if_print_debug ("No "); + if_print_debug (class_); + if_print_debug (" watchpoints are set.\n"); + } + return; + } + + /* + * For non-queries, set watchpoint flags as defined in action for + * the range determined, and print confirmation. + */ + for (index_ = low; index_ <= high; index_++) + watchpoints[index_] = action; + + if (action) + if_print_debug ("Set "); + else + if_print_debug ("Cleared "); + sprintf (buffer, "%ld ", high - low + 1); + if_print_debug (buffer); + if_print_debug (class_); + if (high == low) + if_print_debug (" watchpoint.\n"); + else + if_print_debug (" watchpoints.\n"); +} + + +/* + * debug_watchall_common() + * + * Common handler to list out and clear all set watchpoints at a stroke. + */ +static void +debug_watchall_common (sc_gameref_t game, + sc_command_t command, sc_command_type_t type) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + assert (debug_is_valid (debug)); + + if (type != COMMAND_QUERY) + { + if (command == DEBUG_WATCHALL) + if_print_debug ("The Watchall command takes no arguments.\n"); + else + if_print_debug ("The Clearall command takes no arguments.\n"); + return; + } + + /* Query all set watchpoints using common watchpoint handler... */ + if (command == DEBUG_WATCHALL) + { + debug_watchpoint_common (game, + DEBUG_WATCHPLAYER, COMMAND_QUERY, 0, 0); + debug_watchpoint_common (game, + DEBUG_WATCHOBJECTS, COMMAND_QUERY, 0, 0); + debug_watchpoint_common (game, + DEBUG_WATCHNPCS, COMMAND_QUERY, 0, 0); + debug_watchpoint_common (game, + DEBUG_WATCHEVENTS, COMMAND_QUERY, 0, 0); + debug_watchpoint_common (game, + DEBUG_WATCHTASKS, COMMAND_QUERY, 0, 0); + debug_watchpoint_common (game, + DEBUG_WATCHVARIABLES, COMMAND_QUERY, 0, 0); + return; + } + + /* ...but reset all the fast way, with memset(). */ + assert (command == DEBUG_CLEARALL); + debug->watch_player = FALSE; + memset (debug->watch_objects, FALSE, + gs_object_count (game) * sizeof (*debug->watch_objects)); + memset (debug->watch_npcs, FALSE, + gs_npc_count (game) * sizeof (*debug->watch_npcs)); + memset (debug->watch_events, FALSE, + gs_event_count (game) * sizeof (*debug->watch_events)); + memset (debug->watch_tasks, FALSE, + gs_task_count (game) * sizeof (*debug->watch_tasks)); + memset (debug->watch_variables, FALSE, + debug_variable_count (game) * sizeof (*debug->watch_variables)); + if_print_debug ("Cleared all watchpoints.\n"); +} + + +/* + * debug_compare_object() + * + * Compare two objects, and return TRUE if the same. + */ +static sc_bool +debug_compare_object (sc_gameref_t from, sc_gameref_t with, sc_int object) +{ + const sc_objectstate_t *from_object = from->objects + object; + const sc_objectstate_t *with_object = with->objects + object; + + return from_object->unmoved == with_object->unmoved + && from_object->static_unmoved == with_object->static_unmoved + && from_object->position == with_object->position + && from_object->parent == with_object->parent + && from_object->openness == with_object->openness + && from_object->state == with_object->state + && from_object->seen == with_object->seen; +} + + +/* + * debug_compare_npc() + * + * Compare two NPCs, and return TRUE if the same. + */ +static sc_bool +debug_compare_npc (sc_gameref_t from, sc_gameref_t with, sc_int npc) +{ + const sc_npcstate_t *from_npc = from->npcs + npc; + const sc_npcstate_t *with_npc = with->npcs + npc; + + if (from_npc->walkstep_count != with_npc->walkstep_count) + sc_fatal ("debug_compare_npc: walkstep count error\n"); + + return from_npc->location == with_npc->location + && from_npc->position == with_npc->position + && from_npc->parent == with_npc->parent + && from_npc->seen == with_npc->seen + && memcmp (from_npc->walksteps, with_npc->walksteps, + from_npc->walkstep_count + * sizeof (*from_npc->walksteps)) == 0; +} + + +/* + * debug_compare_event() + * + * Compare two events, and return TRUE if the same. + */ +static sc_bool +debug_compare_event (sc_gameref_t from, sc_gameref_t with, sc_int event) +{ + const sc_eventstate_t *from_event = from->events + event; + const sc_eventstate_t *with_event = with->events + event; + + return from_event->state == with_event->state + && from_event->time == with_event->time; +} + + +/* + * debug_compare_task() + * + * Compare two tasks, and return TRUE if the same. + */ +static sc_bool +debug_compare_task (sc_gameref_t from, sc_gameref_t with, sc_int task) +{ + const sc_taskstate_t *from_task = from->tasks + task; + const sc_taskstate_t *with_task = with->tasks + task; + + return from_task->done == with_task->done + && from_task->scored == with_task->scored; +} + + +/* + * debug_compare_variable() + * + * Compare two variables, and return TRUE if the same. + */ +static sc_bool +debug_compare_variable (sc_gameref_t from, sc_gameref_t with, sc_int variable) +{ + const sc_prop_setref_t bundle = from->bundle; + const sc_var_setref_t from_var = from->vars; + const sc_var_setref_t with_var = with->vars; + sc_vartype_t vt_key[3], vt_rvalue, vt_rvalue2; + const sc_char *name; + sc_int var_type, var_type2; + sc_bool equal = FALSE; + + if (from->bundle != with->bundle) + sc_fatal ("debug_compare_variable: property sharing malfunction\n"); + + vt_key[0].string = "Variables"; + vt_key[1].integer = variable; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + if (!var_get (from_var, name, &var_type, &vt_rvalue) + || !var_get (with_var, name, &var_type2, &vt_rvalue2)) + sc_fatal ("debug_compare_variable: can't find variable %s\n", name); + else if (var_type != var_type2) + sc_fatal ("debug_compare_variable: variable type mismatch %s\n", name); + + switch (var_type) + { + case VAR_INTEGER: + equal = (vt_rvalue.integer == vt_rvalue2.integer); + break; + case VAR_STRING: + equal = !strcmp (vt_rvalue.string, vt_rvalue2.string); + break; + default: + sc_fatal ("debug_compare_variable:" + " invalid variable type, %ld\n", var_type); + } + + return equal; +} + + +/* + * debug_check_class() + * + * Central handler for checking watchpoints. Compares a number of items + * of a class using the comparison function given, where indicated by a + * watchpoints flags array. Prints entries that differ, and returns TRUE + * if any differed. + */ +static sc_bool +debug_check_class (sc_gameref_t from, sc_gameref_t with, + const sc_char *class_, sc_int class_count, + const sc_bool *watchpoints, + sc_bool (*const compare_function) + (sc_gameref_t, sc_gameref_t, sc_int)) +{ + sc_int index_; + sc_bool triggered = FALSE; + + /* + * Scan the watchpoints array for set watchpoints, comparing classes + * where the watchpoint flag is set. + */ + for (index_ = 0; index_ < class_count; index_++) + { + if (!watchpoints[index_]) + continue; + + if (!compare_function (from, with, index_)) + { + sc_char buffer[32]; + + if (!triggered) + { + if_print_debug ("--- "); + if_print_debug (class_); + if_print_debug (" watchpoint triggered { "); + } + sprintf (buffer, "%ld ", index_); + if_print_debug (buffer); + triggered = TRUE; + } + } + if (triggered) + if_print_debug ("}.\n"); + + /* Return TRUE if anything differed. */ + return triggered; +} + + +/* + * debug_check_watchpoints() + * + * Checks the game against the undo game for all set watchpoints. Returns + * TRUE if any triggered, FALSE if none (or if the undo game isn't available, + * in which case no check is possible). + */ +static sc_bool +debug_check_watchpoints (sc_gameref_t game) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + const sc_gameref_t undo = game->undo; + sc_bool triggered; + assert (debug_is_valid (debug) && gs_is_game_valid (undo)); + + /* If no undo is present, no check is possible. */ + if (!game->undo_available) + return FALSE; + + /* Check first for player watchpoint. */ + triggered = FALSE; + if (debug->watch_player) + { + if (gs_playerroom (game) != gs_playerroom (undo) + || gs_playerposition (game) != gs_playerposition (undo) + || gs_playerparent (game) != gs_playerparent (undo)) + { + if_print_debug ("--- Player watchpoint triggered.\n"); + triggered |= TRUE; + } + } + + /* Now check other classes of watchpoint. */ + triggered |= debug_check_class (game, undo, + "Object", gs_object_count (game), + debug->watch_objects, debug_compare_object); + triggered |= debug_check_class (game, undo, + "NPC", gs_npc_count (game), + debug->watch_npcs, debug_compare_npc); + triggered |= debug_check_class (game, undo, + "Event", gs_event_count (game), + debug->watch_events, debug_compare_event); + triggered |= debug_check_class (game, undo, + "Task", gs_task_count (game), + debug->watch_tasks, debug_compare_task); + triggered |= debug_check_class (game, undo, + "Variable", debug_variable_count (game), + debug->watch_variables, + debug_compare_variable); + + return triggered; +} + + +/* + * debug_parse_command() + * + * Given a debugging command string, try to parse it and return the + * appropriate command and its arguments. Returns DEBUG_NONE if the parse + * fails. + */ +static sc_command_t +debug_parse_command (const sc_char *command_string, + sc_command_type_t *type, + sc_int *arg1, sc_int *arg2, sc_command_t *help_topic) +{ + sc_command_t return_command; + sc_command_type_t return_type; + sc_int val1, val2, converted, matches; + sc_char *help, *string, junk, wildcard; + sc_bool is_help, is_parsed, is_wildcard; + const sc_strings_t *entry; + + /* Allocate temporary strings long enough to take a copy of the input. */ + string = (sc_char *)sc_malloc (strlen (command_string) + 1); + help = (sc_char *)sc_malloc (strlen (command_string) + 1); + + /* + * Parse the input line, in a very simplistic fashion. The argument count + * is one less than sscanf converts. + */ + is_parsed = is_wildcard = is_help = FALSE; + val1 = val2 = 0; + converted = sscanf (command_string, " %s %s %c", help, string, &junk); + if (converted == 2 && sc_strcasecmp (help, "help") == 0) + { + is_help = TRUE; + is_parsed = TRUE; + } + sc_free (help); + if (!is_parsed) + { + converted = sscanf (command_string, + " %s %ld to %ld %c", string, &val1, &val2, &junk); + if (converted != 3) + converted = sscanf (command_string, + " %s %ld - %ld %c", string, &val1, &val2, &junk); + if (converted != 3) + converted = sscanf (command_string, + " %s %ld .. %ld %c", string, &val1, &val2, &junk); + if (converted != 3) + converted = sscanf (command_string, + " %s %ld %ld %c", string, &val1, &val2, &junk); + is_parsed |= converted == 3; + } + if (!is_parsed) + { + converted = sscanf (command_string, + " %s %ld %c", string, &val1, &junk); + is_parsed |= converted == 2; + } + if (!is_parsed) + { + converted = sscanf (command_string, + " %s %c %c", string, &wildcard, &junk); + if (converted == 2 && wildcard == '*') + { + is_wildcard = TRUE; + is_parsed = TRUE; + } + else + is_parsed |= converted == 1; + } + if (!is_parsed) + { + if_print_debug ("Invalid debug command."); + if_print_debug (" Type 'help' for a list of valid commands.\n"); + sc_free (string); + return DEBUG_NONE; + } + + /* Decide on a command type based on the parse. */ + if (is_wildcard) + return_type = COMMAND_ALL; + else if (converted == 3) + return_type = COMMAND_RANGE; + else if (converted == 2) + return_type = COMMAND_ONE; + else + return_type = COMMAND_QUERY; + + /* + * Find the first unambiguous command matching the string. If none, + * return DEBUG_NONE. + */ + matches = 0; + return_command = DEBUG_NONE; + for (entry = DEBUG_COMMANDS; entry->command_string; entry++) + { + if (sc_strncasecmp (string, entry->command_string, strlen (string)) == 0) + { + matches++; + return_command = entry->command; + } + } + if (matches != 1) + { + if (matches > 1) + if_print_debug ("Ambiguous debug command."); + else + if_print_debug ("Unrecognized debug command."); + if_print_debug (" Type 'help' for a list of valid commands.\n"); + sc_free (string); + return DEBUG_NONE; + } + + /* Done with temporary command parse area. */ + sc_free (string); + + /* + * Return the command type, arguments, and the debugging command. For help + * <topic>, the command is help, with the command on which help requested + * in *help_topic. All clear, then? + */ + *type = return_type; + *arg1 = val1; + *arg2 = val2; + *help_topic = is_help ? return_command : DEBUG_NONE; + return is_help ? DEBUG_HELP : return_command; +} + + +/* + * debug_dispatch() + * + * Dispatch a debugging command to the appropriate handler. + */ +static void +debug_dispatch (sc_gameref_t game, + sc_command_t command, sc_command_type_t type, + sc_int arg1, sc_int arg2, sc_command_t help_topic) +{ + /* Demultiplex debugging command, and call handlers. */ + switch (command) + { + case DEBUG_HELP: + debug_help (help_topic); + break; + case DEBUG_BUFFER: + debug_buffer (game, type); + break; + case DEBUG_RESOURCES: + debug_resources (game, type); + break; + case DEBUG_RANDOM: + debug_random (type, arg1); + break; + case DEBUG_GAME: + debug_game (game, type); + break; + case DEBUG_PLAYER: + case DEBUG_OLDPLAYER: + debug_player (game, command, type); + break; + case DEBUG_ROOMS: + case DEBUG_OBJECTS: + case DEBUG_NPCS: + case DEBUG_EVENTS: + case DEBUG_TASKS: + case DEBUG_VARIABLES: + case DEBUG_OLDROOMS: + case DEBUG_OLDOBJECTS: + case DEBUG_OLDNPCS: + case DEBUG_OLDEVENTS: + case DEBUG_OLDTASKS: + case DEBUG_OLDVARIABLES: + debug_dump_common (game, command, type, arg1, arg2); + break; + case DEBUG_WATCHPLAYER: + case DEBUG_WATCHOBJECTS: + case DEBUG_WATCHNPCS: + case DEBUG_WATCHEVENTS: + case DEBUG_WATCHTASKS: + case DEBUG_WATCHVARIABLES: + case DEBUG_CLEARPLAYER: + case DEBUG_CLEAROBJECTS: + case DEBUG_CLEARNPCS: + case DEBUG_CLEAREVENTS: + case DEBUG_CLEARTASKS: + case DEBUG_CLEARVARIABLES: + debug_watchpoint_common (game, command, type, arg1, arg2); + break; + case DEBUG_WATCHALL: + case DEBUG_CLEARALL: + debug_watchall_common (game, command, type); + break; + case DEBUG_NONE: + break; + default: + sc_fatal ("debug_dispatch: invalid debug command\n"); + } +} + + +/* + * debug_dialog() + * + * Create a small debugging dialog with the user. + */ +static void +debug_dialog (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_debuggerref_t debug = debug_get_debugger (game); + const sc_var_setref_t vars = gs_get_vars (game); + assert (debug_is_valid (debug)); + + /* + * Note elapsed seconds, so time stands still while debugging, and clear + * any pending game quit left over from prior dialogs (just in case). + */ + debug->elapsed_seconds = var_get_elapsed_seconds (vars); + debug->quit_pending = FALSE; + + /* Handle debug commands until debugger quit or game quit. */ + while (TRUE) + { + sc_char buffer[DEBUG_BUFFER_SIZE]; + sc_command_t command, help_topic; + sc_command_type_t type; + sc_int arg1, arg2; + + /* Get a debugging command string from the user. */ + do + { + if_read_debug (buffer, sizeof (buffer)); + } + while (sc_strempty (buffer)); + + /* Parse the command read, and handle dialog exit commands. */ + command = debug_parse_command (buffer, + &type, &arg1, &arg2, &help_topic); + if (command == DEBUG_CONTINUE || command == DEBUG_STEP) + { + if (!game->is_running) + { + if_print_debug ("The game is no longer running.\n"); + continue; + } + + debug->single_step = (command == DEBUG_STEP); + break; + } + else if (command == DEBUG_QUIT) + { + /* + * If the game is not running, we can't halt it, and we don't need + * to confirm the quit (either the player "quit" or the game + * completed), so leave the dialog loop. + */ + if (!game->is_running) + break; + + /* + * The game is still running, so confirm quit by requiring a repeat, + * or if this is the confirmation, force the game to a halt. + */ + if (!debug->quit_pending) + { + if_print_debug ("Use 'quit' again to confirm, or another" + " debugger command to cancel.\n"); + debug->quit_pending = TRUE; + continue; + } + + /* Drop printfilter contents and quit the game. */ + pf_empty (filter); + run_quit (game); + + /* Just in case... */ + if_print_debug ("Unable to quit from the game. Sorry.\n"); + continue; + } + + /* Dispatch the remaining debugging commands, and clear quit flag. */ + debug_dispatch (game, command, type, arg1, arg2, help_topic); + debug->quit_pending = FALSE; + } + + /* Restart time, and clear any pending game quit. */ + var_set_elapsed_seconds (vars, debug->elapsed_seconds); + debug->quit_pending = FALSE; +} + + +/* + * debug_run_command() + * + * Handle a single debugging command line from the outside world. Returns + * TRUE if valid, FALSE if invalid (parse failed, not understood). + */ +sc_bool +debug_run_command (sc_gameref_t game, const sc_char *debug_command) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + sc_command_t command, help_topic; + sc_command_type_t type; + sc_int arg1, arg2; + + /* If debugging disallowed (not initialized), refuse the call. */ + if (debug) + { + /* + * Parse the command string passed in, and return FALSE if the parse + * fails, or if it returns DEBUG_CONTINUE, DEBUG_STEP, or DEBUG_QUIT, + * none of which make any sense in this context. + */ + command = debug_parse_command (debug_command, + &type, &arg1, &arg2, &help_topic); + if (command == DEBUG_NONE + || command == DEBUG_CONTINUE || command == DEBUG_STEP + || command == DEBUG_QUIT) + return FALSE; + + /* Dispatch the remaining debugging commands, return successfully. */ + debug_dispatch (game, command, type, arg1, arg2, help_topic); + return TRUE; + } + + return FALSE; +} + + +/* + * debug_cmd_debugger() + * + * Called by the run main loop on user "debug" command request. Prints + * a polite refusal if debugging is not enabled, otherwise runs a debugging + * dialog. Uses if_print_string() as this isn't debug output. + */ +sc_bool +debug_cmd_debugger (sc_gameref_t game) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + + /* If debugging disallowed (not initialized), ignore the call. */ + if (debug) + debug_dialog (game); + else + if_print_string ("SCARE's game debugger is not enabled. Sorry.\n"); + + /* + * Set as administrative command, so as not to consume a game turn, and + * return successfully. + */ + game->is_admin = TRUE; + return TRUE; +} + + +/* + * debug_game_started() + * debug_game_ended() + * + * The first is called on entry to the game main loop, and gives us a chance + * to look at things before any turns are run, and to set watchpoints to + * catch things in games that use catch-all command tasks on startup (The PK + * Girl, for example). + * + * The second is called on exit from the game, and may make a final sweep for + * watchpoints and offer the debug dialog one last time. + */ +void +debug_game_started (sc_gameref_t game) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + + /* If debugging disallowed (not initialized), ignore the call. */ + if (debug) + { + /* Starting a new game, or a restore or undo of an old one? */ + if (!gs_room_seen (game, gs_playerroom (game))) + { + /* + * It's a new game starting or restarting. Print a banner, and + * run the debugger dialog. + */ + if_print_debug ("\n--- SCARE " SCARE_VERSION SCARE_PATCH_LEVEL + " Game Debugger\n" + "--- Type 'help' for a list of commands.\n"); + debug_dialog (game); + } + else + { + /* + * It's a restore or undo through memos, so run the dialog only if + * single-stepping; no need to check watchpoints for this case as + * none can be set -- no undo. + */ + if (debug->single_step) + debug_dialog (game); + } + } +} + +void +debug_game_ended (sc_gameref_t game) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + + /* If debugging disallowed (not initialized), ignore the call. */ + if (debug) + { + /* + * Using our carnal knowledge of the run main loop, we know here that + * if the loop exited with do_restart or do_restore, we'll get a call to + * debug_game_start() when the loop restarts. So in this case, ignore + * the call (even if single stepping). + */ + if (game->do_restart || game->do_restore) + return; + + /* + * Check for any final watchpoints, and print a message describing why + * we're here. Suppress the check for watchpoints if the user exited + * the game, as it'll only be a repeat of any found last turn update. + */ + if (!game->is_running) + { + if (game->has_completed) + { + debug_check_watchpoints (game); + if_print_debug ("\n--- The game has completed.\n"); + } + else + if_print_debug ("\n--- The game has exited.\n"); + } + else + { + debug_check_watchpoints (game); + if_print_debug ("\n--- The game is still running!\n"); + } + + /* Run a final dialog. */ + debug_dialog (game); + } +} + + +/* + * debug_turn_update() + * + * Called after each turn by the main game loop. Checks for any set + * watchpoints, and triggers a debug dialog when any fire. + */ +void +debug_turn_update (sc_gameref_t game) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + + /* If debugging disallowed (not initialized), ignore the call. */ + if (debug) + { + /* + * Again using carnal knowledge of the run main loop, if we're in + * mid-wait, ignore the call. Also, ignore the call if the game is + * no longer running, as we'll see a debug_game_ended() call come + * along to handle that. + */ + if (game->waitcounter > 0 || !game->is_running) + return; + + /* + * Run debugger dialog if any watchpoints triggered, or if single + * stepping (even if none triggered). + */ + if (debug_check_watchpoints (game) || debug->single_step) + debug_dialog (game); + } +} + + +/* + * debug_set_enabled() + * debug_get_enabled() + * + * Enable/disable debugging, and return debugging status. Debugging is + * enabled when there is a debugger reference in the game, and disabled + * when it's NULL -- that's the flag. To avoid lugging about all the + * watchpoint memory with a game, debugger data is allocated on enabling, + * and free'd on disabling; as a result, any set watchpoints are lost on + * disabling. + */ +void +debug_set_enabled (sc_gameref_t game, sc_bool enable) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + + /* + * If enabling and not already enabled, or disabling and not already + * disabled, either initialize or finalize.. + */ + if ((enable && !debug) || (!enable && debug)) + { + /* Initialize or finalize debugging, as appropriate. */ + if (enable) + debug_initialize (game); + else + debug_finalize (game); + } +} + +sc_bool +debug_get_enabled (sc_gameref_t game) +{ + const sc_debuggerref_t debug = debug_get_debugger (game); + + return debug != NULL; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scevents.cpp b/engines/glk/adrift/scevents.cpp new file mode 100644 index 0000000000..18d95b19ec --- /dev/null +++ b/engines/glk/adrift/scevents.cpp @@ -0,0 +1,855 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Event pause and resume tasks need more testing. + */ + +/* Trace flag, set before running. */ +static sc_bool evt_trace = FALSE; + + +/* + * evt_any_task_in_state() + * + * Return TRUE if any task at all matches the given completion state. + */ +static sc_bool +evt_any_task_in_state (sc_gameref_t game, sc_bool state) +{ + sc_int task; + + /* Scan tasks for any whose completion matches input. */ + for (task = 0; task < gs_task_count (game); task++) + { + if (gs_task_done (game, task) == state) + return TRUE; + } + + /* No tasks matched. */ + return FALSE; +} + + +/* + * evt_can_see_event() + * + * Return TRUE if player is in the right room for event text. + */ +sc_bool +evt_can_see_event (sc_gameref_t game, sc_int event) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int type; + + /* Check room list for the event and return it. */ + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "Where"; + vt_key[3].string = "Type"; + type = prop_get_integer (bundle, "I<-siss", vt_key); + switch (type) + { + case ROOMLIST_NO_ROOMS: + return FALSE; + case ROOMLIST_ALL_ROOMS: + return TRUE; + + case ROOMLIST_ONE_ROOM: + vt_key[3].string = "Room"; + return prop_get_integer (bundle, "I<-siss", vt_key) + == gs_playerroom (game); + + case ROOMLIST_SOME_ROOMS: + vt_key[3].string = "Rooms"; + vt_key[4].integer = gs_playerroom (game); + return prop_get_boolean (bundle, "B<-sissi", vt_key); + + default: + sc_fatal ("evt_can_see_event: invalid type, %ld\n", type); + return FALSE; + } +} + + +/* + * evt_move_object() + * + * Move an object from within an event. + */ +static void +evt_move_object (sc_gameref_t game, sc_int object, sc_int destination) +{ + /* Ignore negative values of object. */ + if (object >= 0) + { + if (evt_trace) + { + sc_trace ("Event: moving object %ld to room %ld\n", + object, destination); + } + + /* Move object depending on destination. */ + switch (destination) + { + case -1: /* Hidden. */ + gs_object_make_hidden (game, object); + break; + + case 0: /* Held by player. */ + gs_object_player_get (game, object); + break; + + case 1: /* Same room as player. */ + gs_object_to_room (game, object, gs_playerroom (game)); + break; + + default: + if (destination < gs_room_count (game) + 2) + gs_object_to_room (game, object, destination - 2); + else + { + sc_int roomgroup, room; + + roomgroup = destination - gs_room_count (game) - 2; + room = lib_random_roomgroup_member (game, roomgroup); + gs_object_to_room (game, object, room); + } + break; + } + + /* + * If static, mark as no longer unmoved. + * + * TODO Is this the only place static objects can be moved? And just + * how static is a static object if it's moveable, anyway? + */ + if (obj_is_static (game, object)) + gs_set_object_static_unmoved (game, object, FALSE); + } +} + + +/* + * evt_fixup_v390_v380_immediate_restart() + * + * Versions 3.9 and 3.8 differ from version 4.0 on immediate restart; they + * "miss" the event start actions and move one step into the event without + * comment. It's arguable if this is a feature or a bug; nevertheless, we + * can do the same thing here, though it's ugly. + */ +static sc_bool +evt_fixup_v390_v380_immediate_restart (sc_gameref_t game, sc_int event) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int version; + + vt_key[0].string = "Version"; + version = prop_get_integer (bundle, "I<-s", vt_key); + if (version < TAF_VERSION_400) + { + sc_int time1, time2; + + if (evt_trace) + sc_trace ("Event: applying 3.9/3.8 restart fixup\n"); + + /* Set to running state. */ + gs_set_event_state (game, event, ES_RUNNING); + + /* Set up event time to be one less than a proper start. */ + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "Time1"; + time1 = prop_get_integer (bundle, "I<-sis", vt_key); + vt_key[2].string = "Time2"; + time2 = prop_get_integer (bundle, "I<-sis", vt_key); + gs_set_event_time (game, event, sc_randomint (time1, time2) - 1); + } + + /* Return TRUE if we applied the fixup. */ + return version < TAF_VERSION_400; +} + + +/* + * evt_start_event() + * + * Change an event from WAITING to RUNNING. + */ +static void +evt_start_event (sc_gameref_t game, sc_int event) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int time1, time2, obj1, obj1dest; + + if (evt_trace) + sc_trace ("Event: starting event %ld\n", event); + + /* If event is visible, print its start text. */ + if (evt_can_see_event (game, event)) + { + const sc_char *starttext; + + /* Get and print start text. */ + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "StartText"; + starttext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (starttext)) + { + pf_buffer_string (filter, starttext); + pf_buffer_character (filter, '\n'); + } + + /* Handle any associated resource. */ + vt_key[2].string = "Res"; + vt_key[3].integer = 0; + res_handle_resource (game, "sisi", vt_key); + } + + /* Move event object to destination. */ + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "Obj1"; + obj1 = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + vt_key[2].string = "Obj1Dest"; + obj1dest = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + evt_move_object (game, obj1, obj1dest); + + /* Set the event's state and time. */ + gs_set_event_state (game, event, ES_RUNNING); + + vt_key[2].string = "Time1"; + time1 = prop_get_integer (bundle, "I<-sis", vt_key); + vt_key[2].string = "Time2"; + time2 = prop_get_integer (bundle, "I<-sis", vt_key); + gs_set_event_time (game, event, sc_randomint (time1, time2)); + + if (evt_trace) + sc_trace ("Event: start event handling done, %ld\n", event); +} + + +/* + * evt_get_starter_type() + * + * Return the starter type for an event. + */ +static sc_int +evt_get_starter_type (sc_gameref_t game, sc_int event) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int startertype; + + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "StarterType"; + startertype = prop_get_integer (bundle, "I<-sis", vt_key); + + return startertype; +} + + +/* + * evt_finish_event() + * + * Move an event to FINISHED, or restart it. + */ +static void +evt_finish_event (sc_gameref_t game, sc_int event) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int obj2, obj2dest, obj3, obj3dest; + sc_int task, startertype, restarttype; + sc_bool taskdir; + + if (evt_trace) + sc_trace ("Event: finishing event %ld\n", event); + + /* Set up invariant parts of the key. */ + vt_key[0].string = "Events"; + vt_key[1].integer = event; + + /* If event is visible, print its finish text. */ + if (evt_can_see_event (game, event)) + { + const sc_char *finishtext; + + /* Get and print finish text. */ + vt_key[2].string = "FinishText"; + finishtext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (finishtext)) + { + pf_buffer_string (filter, finishtext); + pf_buffer_character (filter, '\n'); + } + + /* Handle any associated resource. */ + vt_key[2].string = "Res"; + vt_key[3].integer = 4; + res_handle_resource (game, "sisi", vt_key); + } + + /* Move event objects to destination. */ + vt_key[2].string = "Obj2"; + obj2 = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + vt_key[2].string = "Obj2Dest"; + obj2dest = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + evt_move_object (game, obj2, obj2dest); + + vt_key[2].string = "Obj3"; + obj3 = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + vt_key[2].string = "Obj3Dest"; + obj3dest = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + evt_move_object (game, obj3, obj3dest); + + /* See if there is an affected task. */ + vt_key[2].string = "TaskAffected"; + task = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + if (task >= 0) + { + vt_key[2].string = "TaskFinished"; + taskdir = !prop_get_boolean (bundle, "B<-sis", vt_key); + if (task_can_run_task_directional (game, task, taskdir)) + { + if (evt_trace) + { + sc_trace ("Event: event running task %ld, %s\n", + task, taskdir ? "forwards" : "backwards"); + } + + task_run_task (game, task, taskdir); + } + else + { + if (evt_trace) + sc_trace ("Event: event can't run task %ld\n", task); + } + } + + /* Handle possible restart. */ + vt_key[2].string = "RestartType"; + restarttype = prop_get_integer (bundle, "I<-sis", vt_key); + switch (restarttype) + { + case 0: /* Don't restart. */ + startertype = evt_get_starter_type (game, event); + switch (startertype) + { + case 1: /* Immediate. */ + case 2: /* Random delay. */ + case 3: /* After task. */ + gs_set_event_state (game, event, ES_FINISHED); + gs_set_event_time (game, event, 0); + break; + + default: + sc_fatal ("evt_finish_event:" + " unknown value for starter type, %ld\n", startertype); + } + break; + + case 1: /* Restart immediately. */ + if (evt_fixup_v390_v380_immediate_restart (game, event)) + break; + else + evt_start_event (game, event); + break; + + case 2: /* Restart after delay. */ + startertype = evt_get_starter_type (game, event); + switch (startertype) + { + case 1: /* Immediate. */ + if (evt_fixup_v390_v380_immediate_restart (game, event)) + break; + else + evt_start_event (game, event); + break; + + case 2: /* Random delay. */ + { + sc_int start, end; + + gs_set_event_state (game, event, ES_WAITING); + vt_key[2].string = "StartTime"; + start = prop_get_integer (bundle, "I<-sis", vt_key); + vt_key[2].string = "EndTime"; + end = prop_get_integer (bundle, "I<-sis", vt_key); + gs_set_event_time (game, event, sc_randomint (start, end)); + break; + } + + case 3: /* After task. */ + gs_set_event_state (game, event, ES_AWAITING); + gs_set_event_time (game, event, 0); + break; + + default: + sc_fatal ("evt_finish_event: unknown StarterType\n"); + } + break; + + default: + sc_fatal ("evt_finish_event: unknown RestartType\n"); + } + + if (evt_trace) + sc_trace ("Event: finish event handling done, %ld\n", event); +} + + +/* + * evt_has_starter_task() + * evt_starter_task_is_complete() + * evt_pauser_task_is_complete() + * evt_resumer_task_is_complete() + * + * Return the status of start, pause and resume states of an event. + */ +static sc_bool +evt_has_starter_task (sc_gameref_t game, sc_int event) +{ + sc_int startertype; + + startertype = evt_get_starter_type (game, event); + return startertype == 3; +} + +static sc_bool +evt_starter_task_is_complete (sc_gameref_t game, sc_int event) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int task; + sc_bool start; + + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "TaskNum"; + task = prop_get_integer (bundle, "I<-sis", vt_key); + + start = FALSE; + if (task == 0) + { + if (evt_any_task_in_state (game, TRUE)) + start = TRUE; + } + else if (task > 0) + { + if (gs_task_done (game, task - 1)) + start = TRUE; + } + + return start; +} + +static sc_bool +evt_pauser_task_is_complete (sc_gameref_t game, sc_int event) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int pausetask; + sc_bool completed, pause; + + vt_key[0].string = "Events"; + vt_key[1].integer = event; + + vt_key[2].string = "PauseTask"; + pausetask = prop_get_integer (bundle, "I<-sis", vt_key); + vt_key[2].string = "PauserCompleted"; + completed = !prop_get_boolean (bundle, "B<-sis", vt_key); + + pause = FALSE; + if (pausetask == 1) + { + if (evt_any_task_in_state (game, completed)) + pause = TRUE; + } + else if (pausetask > 1) + { + if (completed == gs_task_done (game, pausetask - 2)) + pause = TRUE; + } + + return pause; +} + +static sc_bool +evt_resumer_task_is_complete (sc_gameref_t game, sc_int event) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int resumetask; + sc_bool completed, resume; + + vt_key[0].string = "Events"; + vt_key[1].integer = event; + + vt_key[2].string = "ResumeTask"; + resumetask = prop_get_integer (bundle, "I<-sis", vt_key); + vt_key[2].string = "ResumerCompleted"; + completed = !prop_get_boolean (bundle, "B<-sis", vt_key); + + resume = FALSE; + if (resumetask == 1) + { + if (evt_any_task_in_state (game, completed)) + resume = TRUE; + } + else if (resumetask > 1) + { + if (completed == gs_task_done (game, resumetask - 2)) + resume = TRUE; + } + + return resume; +} + + +/* + * evt_handle_preftime_notifications() + * + * Print messages and handle resources for the event where we're in mid-event + * and getting close to some number of turns from its end. + */ +static void +evt_handle_preftime_notifications (sc_gameref_t game, sc_int event) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int preftime1, preftime2; + const sc_char *preftext; + + vt_key[0].string = "Events"; + vt_key[1].integer = event; + + vt_key[2].string = "PrefTime1"; + preftime1 = prop_get_integer (bundle, "I<-sis", vt_key); + if (preftime1 == gs_event_time (game, event)) + { + vt_key[2].string = "PrefText1"; + preftext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (preftext)) + { + pf_buffer_string (filter, preftext); + pf_buffer_character (filter, '\n'); + } + + vt_key[2].string = "Res"; + vt_key[3].integer = 2; + res_handle_resource (game, "sisi", vt_key); + } + + vt_key[2].string = "PrefTime2"; + preftime2 = prop_get_integer (bundle, "I<-sis", vt_key); + if (preftime2 == gs_event_time (game, event)) + { + vt_key[2].string = "PrefText2"; + preftext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (preftext)) + { + pf_buffer_string (filter, preftext); + pf_buffer_character (filter, '\n'); + } + + vt_key[2].string = "Res"; + vt_key[3].integer = 3; + res_handle_resource (game, "sisi", vt_key); + } +} + + +/* + * evt_tick_event() + * + * Attempt to advance an event by one turn. + */ +static void +evt_tick_event (sc_gameref_t game, sc_int event) +{ + if (evt_trace) + { + sc_trace ("Event: ticking event %ld: state %ld, time %ld\n", event, + gs_event_state (game, event), gs_event_time (game, event)); + } + + /* Handle call based on current event state. */ + switch (gs_event_state (game, event)) + { + case ES_WAITING: + { + if (evt_trace) + sc_trace ("Event: ticking waiting event %ld\n", event); + + /* + * Because we also tick an event that goes from waiting to running, + * events started here will tick through RUNNING too, and have their + * time decremented. To get around this, so that the timer for one- + * shot events doesn't look one lower than it should after this + * transition, we need to set the initial time for events that start + * as soon as the game starts to one greater than that set by + * evt_start_time(). Here's the hack to do that; if the event starts + * immediately, its time will already be zero, even before decrement, + * which is how we tell which events to apply this hack to. + * + * TODO This seems to work, but also seems very dodgy. + */ + if (gs_event_time (game, event) == 0) + { + evt_start_event (game, event); + + /* If the event time was set to zero, finish immediately. */ + if (gs_event_time (game, event) <= 0) + evt_finish_event (game, event); + else + gs_set_event_time (game, event, gs_event_time (game, event) + 1); + break; + } + + /* + * Decrement the event's time, and if it goes to zero, start running + * the event. + */ + gs_decrement_event_time (game, event); + + if (gs_event_time (game, event) <= 0) + { + evt_start_event (game, event); + + /* If the event time was set to zero, finish immediately. */ + if (gs_event_time (game, event) <= 0) + evt_finish_event (game, event); + } + } + break; + + case ES_RUNNING: + { + if (evt_trace) + sc_trace ("Event: ticking running event %ld\n", event); + + /* + * Re-check the starter task; if it's no longer completed, we need + * to set the event back to waiting on task. + */ + if (evt_has_starter_task (game, event)) + { + if (!evt_starter_task_is_complete (game, event)) + { + if (evt_trace) + sc_trace ("Event: starter task not complete\n"); + + gs_set_event_state (game, event, ES_AWAITING); + gs_set_event_time (game, event, 0); + break; + } + } + + /* If the pauser has completed, but resumer not, pause this event. */ + if (evt_pauser_task_is_complete (game, event) + && !evt_resumer_task_is_complete (game, event)) + { + if (evt_trace) + sc_trace ("Event: pause complete\n"); + + gs_set_event_state (game, event, ES_PAUSED); + break; + } + + /* + * Decrement the event's time, and print any notifications for a set + * number of turns from the event end. + */ + gs_decrement_event_time (game, event); + + if (evt_can_see_event (game, event)) + evt_handle_preftime_notifications (game, event); + + /* If the time goes to zero, finish running the event. */ + if (gs_event_time (game, event) <= 0) + evt_finish_event (game, event); + } + break; + + case ES_AWAITING: + { + if (evt_trace) + sc_trace ("Event: ticking awaiting event %ld\n", event); + + /* + * Check the starter task. If it's completed, start running the + * event. + */ + if (evt_starter_task_is_complete (game, event)) + { + evt_start_event (game, event); + + /* If the event time was set to zero, finish immediately. */ + if (gs_event_time (game, event) <= 0) + evt_finish_event (game, event); + else + { + /* + * If the pauser has completed, but resumer not, immediately + * also pause this event. + */ + if (evt_pauser_task_is_complete (game, event) + && !evt_resumer_task_is_complete (game, event)) + { + if (evt_trace) + sc_trace ("Event: pause complete, immediate pause\n"); + + gs_set_event_state (game, event, ES_PAUSED); + } + } + } + } + break; + + case ES_FINISHED: + { + if (evt_trace) + sc_trace ("Event: ticking finished event %ld\n", event); + + /* + * Check the starter task; if it's not completed, we need to set the + * event back to waiting on task. + * + * A completed event needs to go back to waiting on its task, but we + * don't want to set it there as soon as the event finishes. We need + * to wait for the starter task to first become undone, otherwise the + * event just cycles endlessly, and they don't in Adrift itself. Here + * is where we wait for starter tasks to become undone. + */ + if (evt_has_starter_task (game, event)) + { + if (!evt_starter_task_is_complete (game, event)) + { + if (evt_trace) + sc_trace ("Event: starter task not complete\n"); + + gs_set_event_state (game, event, ES_AWAITING); + gs_set_event_time (game, event, 0); + break; + } + } + } + break; + + case ES_PAUSED: + { + if (evt_trace) + sc_trace ("Event: ticking paused event %ld\n", event); + + /* If the resumer has completed, resume this event. */ + if (evt_resumer_task_is_complete (game, event)) + { + if (evt_trace) + sc_trace ("Event: resume complete\n"); + + gs_set_event_state (game, event, ES_RUNNING); + break; + } + } + break; + + default: + sc_fatal ("evt_tick: invalid event state\n"); + } + + if (evt_trace) + { + sc_trace ("Event: after ticking event %ld: state %ld, time %ld\n", event, + gs_event_state (game, event), gs_event_time (game, event)); + } +} + + +/* + * evt_tick_events() + * + * Attempt to advance each event by one turn. + */ +void +evt_tick_events (sc_gameref_t game) +{ + sc_int event; + + /* + * Tick all events. If an event transitions into a running state from a + * paused or waiting state, tick that event again. + */ + for (event = 0; event < gs_event_count (game); event++) + { + sc_int prior_state, state; + + /* Note current state, and tick event forwards. */ + prior_state = gs_event_state (game, event); + evt_tick_event (game, event); + + /* + * If the event went from paused or waiting to running, tick again. + * This looks dodgy, and probably is, but it does keep timers correct + * by only re-ticking events that have transitioned from non-running + * states to a running one, and not already-running events. This is + * in effect just adding a bit of turn processing to a tick that would + * otherwise change state alone; a bit of laziness, in other words. + */ + state = gs_event_state (game, event); + if (state == ES_RUNNING + && (prior_state == ES_PAUSED || prior_state == ES_WAITING)) + evt_tick_event (game, event); + } +} + + +/* + * evt_debug_trace() + * + * Set event tracing on/off. + */ +void +evt_debug_trace (sc_bool flag) +{ + evt_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scexpr.cpp b/engines/glk/adrift/scexpr.cpp new file mode 100644 index 0000000000..ed3386020f --- /dev/null +++ b/engines/glk/adrift/scexpr.cpp @@ -0,0 +1,1677 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o The tokenizer doesn't differentiate between "&", "&&", and "and", but "&" + * is context sensitive -- either a logical and for numerics, or concaten- + * ation for strings. As a result, "&&" and "and" also work as string + * concatenators when used in string expressions. It may not be to spec, + * but we'll call this a "feature". + */ + +/* Assorted definitions and constants. */ +enum { MAX_NESTING_DEPTH = 32 }; +static const sc_char NUL = '\0'; +static const sc_char PERCENT = '%'; +static const sc_char SINGLE_QUOTE = '\''; +static const sc_char DOUBLE_QUOTE = '"'; + + +/* + * Tokens. Single character tokens are represented by their ascii value + * (0-255), others by values above 255. -1 represents a null token. Because + * '&' and '+' are context sensitive, the pseudo-token TOK_CONCATENATE + * serves to indicate string concatenation -- it's never returned by the + * tokenizer. + */ +enum +{ TOK_NONE = -1, + TOK_ADD = '+', TOK_SUBTRACT = '-', TOK_MULTIPLY = '*', TOK_DIVIDE = '/', + TOK_AND = '&', TOK_OR = '|', + TOK_LPAREN = '(', TOK_RPAREN = ')', TOK_COMMA = ',', TOK_POWER = '^', + TOK_EQUAL = '=', TOK_GREATER = '>', TOK_LESS = '<', + + TOK_IDENT = 256, + TOK_INTEGER, TOK_STRING, TOK_VARIABLE, TOK_UMINUS, TOK_UPLUS, + TOK_MOD, TOK_NOT_EQUAL, TOK_GREATER_EQ, TOK_LESS_EQ, TOK_IF, + TOK_MIN, TOK_MAX, TOK_EITHER, TOK_RANDOM, TOK_INSTR, TOK_LEN, TOK_VAL, + TOK_ABS, TOK_UPPER, TOK_LOWER, TOK_PROPER, TOK_RIGHT, TOK_LEFT, TOK_MID, + TOK_STR, TOK_CONCATENATE, + TOK_EOS +}; + +/* + * Small tables tying multicharacter tokens strings to tokens. At present, + * the string lengths for names are not used. + */ +typedef struct +{ + const sc_char *const name; + const sc_int length; + const sc_int token; +} sc_expr_multichar_t; + +static const sc_expr_multichar_t FUNCTION_TOKENS[] = { + {"either", 6, TOK_EITHER}, + {"proper", 6, TOK_PROPER}, {"pcase", 5, TOK_PROPER}, {"instr", 5, TOK_INSTR}, + {"upper", 5, TOK_UPPER}, {"ucase", 5, TOK_UPPER}, + {"lower", 5, TOK_LOWER}, {"lcase", 5, TOK_LOWER}, + {"right", 5, TOK_RIGHT}, {"left", 4, TOK_LEFT}, + {"rand", 4, TOK_RANDOM}, {"max", 3, TOK_MAX}, {"min", 3, TOK_MIN}, + {"mod", 3, TOK_MOD}, {"abs", 3, TOK_ABS}, {"len", 3, TOK_LEN}, + {"val", 3, TOK_VAL}, {"and", 3, TOK_AND}, {"mid", 3, TOK_MID}, + {"str", 3, TOK_STR}, {"or", 2, TOK_OR}, {"if", 2, TOK_IF}, + {NULL, 0, TOK_NONE} +}; +static const sc_expr_multichar_t OPERATOR_TOKENS[] = { + {"&&", 2, TOK_AND}, {"||", 2, TOK_OR}, + {"==", 2, TOK_EQUAL}, {"!=", 2, TOK_NOT_EQUAL}, + {"<>", 2, TOK_NOT_EQUAL}, {">=", 2, TOK_GREATER_EQ}, {"<=", 2, TOK_LESS_EQ}, + {NULL, 0, TOK_NONE} +}; + + +/* + * expr_multichar_search() + * + * Multicharacter token table search, returns the matching token, or + * TOK_NONE if no match. + */ +static sc_int +expr_multichar_search (const sc_char *name, const sc_expr_multichar_t *table) +{ + const sc_expr_multichar_t *entry; + + /* Scan the table for a case-independent full string match. */ + for (entry = table; entry->name; entry++) + { + if (sc_strcasecmp (name, entry->name) == 0) + break; + } + + /* Return the token matched, or TOK_NONE. */ + return entry->name ? entry->token : TOK_NONE; +} + + +/* Tokenizer variables. */ +static const sc_char *expr_expression = NULL; +static sc_int expr_index = 0; +static sc_vartype_t expr_token_value; +static sc_char *expr_temporary = NULL; +static sc_int expr_current_token = TOK_NONE; + +/* + * expr_tokenize_start() + * expr_tokenize_end() + * + * Start and wrap up expression string tokenization. + */ +static void +expr_tokenize_start (const sc_char *expression) +{ + static sc_bool initialized = FALSE; + + /* On first call only, verify the string lengths in the tables. */ + if (!initialized) + { + const sc_expr_multichar_t *entry; + + /* Compare table lengths with string lengths. */ + for (entry = FUNCTION_TOKENS; entry->name; entry++) + { + if (entry->length != (sc_int) strlen (entry->name)) + { + sc_fatal ("expr_tokenize_start:" + " token string length is wrong for \"%s\"\n", + entry->name); + } + } + + for (entry = OPERATOR_TOKENS; entry->name; entry++) + { + if (entry->length != (sc_int) strlen (entry->name)) + { + sc_fatal ("expr_tokenize_start:" + " operator string length is wrong for \"%s\"\n", + entry->name); + } + } + + initialized = TRUE; + } + + /* Save expression, and restart index. */ + expr_expression = expression; + expr_index = 0; + + /* Allocate a temporary token value/literals string. */ + assert (!expr_temporary); + expr_temporary = (sc_char *)sc_malloc (strlen (expression) + 1); + + /* Reset last token to none. */ + expr_current_token = TOK_NONE; +} + +static void +expr_tokenize_end (void) +{ + /* Deallocate temporary strings, clear expression. */ + sc_free (expr_temporary); + expr_temporary = NULL; + expr_expression = NULL; + expr_index = 0; + expr_current_token = TOK_NONE; +} + + +/* + * expr_next_token_unadjusted() + * expr_next_token() + * + * Return the next token from the current expression. The initial token may + * be adjusted into a unary +/- depending on the value of the previous token. + */ +static sc_int +expr_next_token_unadjusted (sc_vartype_t *token_value) +{ + sc_int c; + assert (expr_expression); + + /* Skip any and all leading whitespace. */ + do + { + c = expr_expression[expr_index++]; + } + while (sc_isspace (c) && c != NUL); + + /* Return EOS if at expression end. */ + if (c == NUL) + { + expr_index--; + return TOK_EOS; + } + + /* + * Identify and return numerics. We deal only with unsigned numbers here; + * the unary +/- tokens take care of any integer sign issues. + */ + else if (sc_isdigit (c)) + { + sc_int value; + + sscanf (expr_expression + expr_index - 1, "%ld", &value); + + while (sc_isdigit (c) && c != NUL) + c = expr_expression[expr_index++]; + expr_index--; + + token_value->integer = value; + return TOK_INTEGER; + } + + /* Identify and return variable references. */ + else if (c == PERCENT) + { + sc_int index_; + + /* Copy variable name. */ + c = expr_expression[expr_index++]; + for (index_ = 0; c != PERCENT && c != NUL;) + { + expr_temporary[index_++] = c; + c = expr_expression[expr_index++]; + } + expr_temporary[index_++] = NUL; + + if (c == NUL) + { + sc_error ("expr_next_token_unadjusted:" + " warning: unterminated variable name\n"); + expr_index--; + } + + /* Return a variable name. */ + token_value->string = expr_temporary; + return TOK_VARIABLE; + } + + /* Identify and return string literals. */ + else if (c == DOUBLE_QUOTE || c == SINGLE_QUOTE) + { + sc_int index_; + sc_char quote; + + /* Copy maximal string literal. */ + quote = c; + c = expr_expression[expr_index++]; + for (index_ = 0; c != quote && c != NUL;) + { + expr_temporary[index_++] = c; + c = expr_expression[expr_index++]; + } + expr_temporary[index_++] = NUL; + + if (c == NUL) + { + sc_error ("expr_next_token_unadjusted:" + " warning: unterminated string literal\n"); + expr_index--; + } + + /* Return string literal. */ + token_value->string = expr_temporary; + return TOK_STRING; + } + + /* Identify ids and other multicharacter tokens. */ + else if (sc_isalpha (c)) + { + sc_int index_, token; + + /* + * Copy maximal alphabetical string. While an ident would normally + * be alpha followed by zero or more alnum, for Adrift purposes we + * use only alpha -- all idents should really be "functions", and + * in particular we want to see "mod7" as "mod" and 7 separately. + */ + for (index_ = 0; sc_isalpha (c) && c != NUL;) + { + expr_temporary[index_++] = c; + c = expr_expression[expr_index++]; + } + expr_index--; + expr_temporary[index_++] = NUL; + + /* + * Check for a function name, and if known, return that, otherwise + * return a bare id. + */ + token = expr_multichar_search (expr_temporary, FUNCTION_TOKENS); + if (token == TOK_NONE) + { + token_value->string = expr_temporary; + return TOK_IDENT; + } + else + return token; + } + + /* + * Last chance check for two-character (multichar) operators, and if none + * then return a single-character token. + */ + else + { + sc_char operator_[3]; + sc_int token; + + /* + * Build a two-character string. If we happen to be at the last + * expression character, we'll pick up the expression NUL into + * operator_[1], so no need to special case end of expression here. + */ + operator_[0] = c; + operator_[1] = expr_expression[expr_index]; + operator_[2] = NUL; + + /* Search for this two-character operator. */ + if (operator_[0] != NUL && operator_[1] != NUL) + { + token = expr_multichar_search (operator_, OPERATOR_TOKENS); + if (token != TOK_NONE) + { + /* Matched, so advance expression index and return this token. */ + expr_index++; + return token; + } + } + + /* + * No match, or at last expression character; return a single character + * token. + */ + return c; + } +} + +static sc_int +expr_next_token (void) +{ + sc_int token; + sc_vartype_t token_value; + + /* + * Get the basic next token. We may adjust it later for unary minus/plus + * depending on what it is, and the prior token. + */ + token_value.voidp = NULL; + token = expr_next_token_unadjusted (&token_value); + + /* Special handling for unary minus/plus signs. */ + if (token == TOK_SUBTRACT || token == TOK_ADD) + { + /* + * Unary minus/plus if prior token was an operator or a comparison, left + * parenthesis, or comma, or if there was no prior token. + */ + switch (expr_current_token) + { + case TOK_MOD: + case TOK_POWER: + case TOK_ADD: + case TOK_SUBTRACT: + case TOK_MULTIPLY: + case TOK_DIVIDE: + case TOK_AND: + case TOK_OR: + case TOK_EQUAL: + case TOK_GREATER: + case TOK_LESS: + case TOK_NOT_EQUAL: + case TOK_GREATER_EQ: + case TOK_LESS_EQ: + case TOK_LPAREN: + case TOK_COMMA: + case TOK_NONE: + token = (token == TOK_SUBTRACT) ? TOK_UMINUS : TOK_UPLUS; + break; + + default: + break; + } + } + + /* Set current token to the one just found, and return it. */ + expr_current_token = token; + expr_token_value = token_value; + return token; +} + + +/* + * expr_current_token_value() + * + * Return the token value of the current token. Undefined if the current + * token is not numeric, an id, or a variable. + */ +static void +expr_current_token_value (sc_vartype_t *value) +{ + /* Quick check that the value is a valid one. */ + switch (expr_current_token) + { + case TOK_INTEGER: + case TOK_STRING: + case TOK_VARIABLE: + case TOK_IDENT: + break; + + default: + sc_fatal ("expr_current_token_value:" + " taking undefined token value, %ld\n", expr_current_token); + } + + /* Return value. */ + *value = expr_token_value; +} + + +/* + * Evaluation values stack, uses a variable type so it can contain both + * integers and strings, and flags strings for possible garbage collection + * on parse errors. + */ +typedef struct +{ + sc_bool is_collectible; + sc_vartype_t value; +} sc_stack_t; +static sc_stack_t expr_eval_stack[MAX_NESTING_DEPTH]; +static sc_int expr_eval_stack_index = 0; + +/* Variables set to reference for %...% values. */ +static sc_var_setref_t expr_varset = NULL; + +/* + * expr_eval_start() + * + * Reset the evaluation stack to an empty state, and register the variables + * set to use when referencing %...% variables. + */ +static void +expr_eval_start (sc_var_setref_t vars) +{ + expr_eval_stack_index = 0; + expr_varset = vars; +} + + +/* + * expr_eval_garbage_collect() + * + * In case of parse error, empty out and free all collectible malloced + * strings left in the evaluation array. + */ +static void +expr_eval_garbage_collect (void) +{ + sc_int index_; + + /* + * Find and free all collectible strings still in the stack. We have to + * free through mutable string rather than const string. + */ + for (index_ = 0; index_ < expr_eval_stack_index; index_++) + { + if (expr_eval_stack[index_].is_collectible) + sc_free (expr_eval_stack[index_].value.mutable_string); + } + + /* Reset the stack index, for clarity and neatness. */ + expr_eval_stack_index = 0; +} + + +/* + * expr_eval_push_integer() + * expr_eval_push_string() + * expr_eval_push_alloced_string() + * + * Push a value onto the values stack. Strings are malloc'ed and copied, + * and the copy is placed onto the stack, unless _alloced_string() is used; + * for this case, the input string is assumed to be already malloc'ed, and + * the caller should not subsequently free the string. + */ +static void +expr_eval_push_integer (sc_int value) +{ + if (expr_eval_stack_index >= MAX_NESTING_DEPTH) + sc_fatal ("expr_eval_push_integer: stack overflow\n"); + + expr_eval_stack[expr_eval_stack_index].is_collectible = FALSE; + expr_eval_stack[expr_eval_stack_index++].value.integer = value; +} + +static void +expr_eval_push_string (const sc_char *value) +{ + sc_char *value_copy; + + if (expr_eval_stack_index >= MAX_NESTING_DEPTH) + sc_fatal ("expr_eval_push_string: stack overflow\n"); + + /* Push a copy of value. */ + value_copy = (sc_char *)sc_malloc (strlen (value) + 1); + strcpy (value_copy, value); + expr_eval_stack[expr_eval_stack_index].is_collectible = TRUE; + expr_eval_stack[expr_eval_stack_index++].value.mutable_string = value_copy; +} + +static void +expr_eval_push_alloced_string (sc_char *value) +{ + if (expr_eval_stack_index >= MAX_NESTING_DEPTH) + sc_fatal ("expr_eval_push_alloced_string: stack overflow\n"); + + expr_eval_stack[expr_eval_stack_index].is_collectible = TRUE; + expr_eval_stack[expr_eval_stack_index++].value.mutable_string = value; +} + + +/* + * expr_eval_pop_integer() + * expr_eval_pop_string() + * + * Pop values off the values stack. Returned strings are malloc'ed copies, + * and the caller is responsible for freeing them. + */ +static sc_int +expr_eval_pop_integer (void) +{ + if (expr_eval_stack_index == 0) + sc_fatal ("expr_eval_pop_integer: stack underflow\n"); + + assert (!expr_eval_stack[expr_eval_stack_index - 1].is_collectible); + return expr_eval_stack[--expr_eval_stack_index].value.integer; +} + +static sc_char * +expr_eval_pop_string (void) +{ + if (expr_eval_stack_index == 0) + sc_fatal ("expr_eval_pop_string: stack underflow\n"); + + /* Returns mutable string rather than const string. */ + assert (expr_eval_stack[expr_eval_stack_index - 1].is_collectible); + return expr_eval_stack[--expr_eval_stack_index].value.mutable_string; +} + + +/* + * expr_eval_result() + * + * Return the top of the values stack as the expression result. + */ +static void +expr_eval_result (sc_vartype_t *vt_rvalue) +{ + if (expr_eval_stack_index != 1) + sc_fatal ("expr_eval_result: values stack not completed\n"); + + /* Clear down stack and return the top value. */ + expr_eval_stack_index = 0; + *vt_rvalue = expr_eval_stack[0].value; +} + + +/* + * expr_eval_abs() + * + * Return the absolute value of the given sc_int. Replacement for labs(), + * avoids tying sc_int to long types too closely. + */ +static sc_int +expr_eval_abs (sc_int value) +{ + return value < 0 ? -value : value; +} + + +/* Parse error jump buffer. */ +static jmp_buf expr_parse_error; + +/* + * expr_eval_action() + * + * Evaluate the effect of a token into the values stack. + */ +static void +expr_eval_action (sc_int token) +{ + sc_vartype_t token_value; + + switch (token) + { + /* Handle tokens representing stack pushes. */ + case TOK_INTEGER: + expr_current_token_value (&token_value); + expr_eval_push_integer (token_value.integer); + break; + + case TOK_STRING: + expr_current_token_value (&token_value); + expr_eval_push_string (token_value.string); + break; + + case TOK_VARIABLE: + { + sc_vartype_t vt_rvalue; + sc_int type; + + expr_current_token_value (&token_value); + if (!var_get (expr_varset, token_value.string, &type, &vt_rvalue)) + { + sc_error ("expr_eval_action:" + " undefined variable, %s\n", token_value.string); + longjmp (expr_parse_error, 1); + } + switch (type) + { + case VAR_INTEGER: + expr_eval_push_integer (vt_rvalue.integer); + break; + + case VAR_STRING: + expr_eval_push_string (vt_rvalue.string); + break; + + default: + sc_fatal ("expr_eval_action: bad variable type\n"); + } + break; + } + + /* Handle tokens representing functions returning numeric. */ + case TOK_IF: + { + sc_int test, val1, val2; + + /* Pop the test and alternatives, and push back result. */ + val2 = expr_eval_pop_integer (); + val1 = expr_eval_pop_integer (); + test = expr_eval_pop_integer (); + expr_eval_push_integer (test ? val1 : val2); + break; + } + + case TOK_MAX: + case TOK_MIN: + { + sc_int argument_count, index_, result; + + /* Get argument count off the top of the stack. */ + argument_count = expr_eval_pop_integer (); + assert (argument_count > 0); + + /* Find the max or min of these stacked values. */ + result = expr_eval_pop_integer (); + for (index_ = 1; index_ < argument_count; index_++) + { + sc_int next; + + next = expr_eval_pop_integer (); + switch (token) + { + case TOK_MAX: + result = (next > result) ? next : result; + break; + + case TOK_MIN: + result = (next < result) ? next : result; + break; + + default: + sc_fatal ("expr_eval_action: bad token, %ld\n", token); + } + } + + /* Push back the result. */ + expr_eval_push_integer (result); + break; + } + + case TOK_EITHER: + { + sc_int argument_count, pick, index_; + sc_int result = 0; + + /* Get argument count off the top of the stack. */ + argument_count = expr_eval_pop_integer (); + assert (argument_count > 0); + + /* + * Pick one of the top N items at random, then unstack all N and + * push back the value of the one picked. + */ + pick = sc_rand () % argument_count; + for (index_ = 0; index_ < argument_count; index_++) + { + sc_int val; + + val = expr_eval_pop_integer (); + if (index_ == pick) + result = val; + } + + /* Push back the result. */ + expr_eval_push_integer (result); + break; + } + + case TOK_INSTR: + { + sc_char *val1, *val2, *search; + sc_int result; + + /* Extract the two values to work on. */ + val2 = expr_eval_pop_string (); + val1 = expr_eval_pop_string (); + + /* + * Search for the second in the first. The result is the character + * position, starting at 1, or 0 if not found. Then free the popped + * strings, and push back the result. + */ + search = (val1[0] != NUL) ? strstr (val1, val2) : NULL; + result = (!search) ? 0 : search - val1 + 1; + sc_free (val1); + sc_free (val2); + expr_eval_push_integer (result); + break; + } + + case TOK_LEN: + { + sc_char *val; + sc_int result; + + /* Pop the top string, and push back its length. */ + val = expr_eval_pop_string (); + result = strlen (val); + sc_free (val); + expr_eval_push_integer (result); + break; + } + + case TOK_VAL: + { + sc_char *val; + sc_int result = 0; + + /* + * Extract the string at stack top, and try to convert, returning + * zero if conversion fails. Free the popped string, and push back + * the result. + */ + val = expr_eval_pop_string (); + sscanf (val, "%ld", &result); + sc_free (val); + expr_eval_push_integer (result); + break; + } + + /* Handle tokens representing unary numeric operations. */ + case TOK_UMINUS: + expr_eval_push_integer (-(expr_eval_pop_integer ())); + break; + + case TOK_UPLUS: + break; + + case TOK_ABS: + expr_eval_push_integer (expr_eval_abs (expr_eval_pop_integer ())); + break; + + /* Handle tokens representing most binary numeric operations. */ + case TOK_ADD: + case TOK_SUBTRACT: + case TOK_MULTIPLY: + case TOK_AND: + case TOK_OR: + case TOK_EQUAL: + case TOK_GREATER: + case TOK_LESS: + case TOK_NOT_EQUAL: + case TOK_GREATER_EQ: + case TOK_LESS_EQ: + case TOK_RANDOM: + { + sc_int val1, val2, result = 0; + + /* Extract the two values to work on. */ + val2 = expr_eval_pop_integer (); + val1 = expr_eval_pop_integer (); + + /* Generate the result value. */ + switch (token) + { + case TOK_ADD: + result = val1 + val2; + break; + case TOK_SUBTRACT: + result = val1 - val2; + break; + case TOK_MULTIPLY: + result = val1 * val2; + break; + case TOK_AND: + result = val1 && val2; + break; + case TOK_OR: + result = val1 || val2; + break; + case TOK_EQUAL: + result = val1 == val2; + break; + case TOK_GREATER: + result = val1 > val2; + break; + case TOK_LESS: + result = val1 < val2; + break; + case TOK_NOT_EQUAL: + result = val1 != val2; + break; + case TOK_GREATER_EQ: + result = val1 >= val2; + break; + case TOK_LESS_EQ: + result = val1 <= val2; + break; + case TOK_RANDOM: + result = sc_randomint (val1, val2); + break; + default: + sc_fatal ("expr_eval_action: bad token, %ld\n", token); + } + + /* Put result back at top of stack. */ + expr_eval_push_integer (result); + break; + } + + /* Handle division and modulus separately; they're "eccentric". */ + case TOK_DIVIDE: + case TOK_MOD: + { + sc_int val1, val2, x, y, result = 0; + + /* Extract the two values to work on, complain about division by 0. */ + val2 = expr_eval_pop_integer (); + val1 = expr_eval_pop_integer (); + if (val2 == 0) + { + sc_error ("expr_eval_action: attempt to divide by zero\n"); + expr_eval_push_integer (result); + break; + } + + /* + * ANSI/ISO C only defines integer division for positive values. + * Negative values usually work consistently across platforms, but are + * not guaranteed. For maximum portability, then, here we'll work + * carefully with positive integers only. + */ + x = expr_eval_abs (val1); + y = expr_eval_abs (val2); + + /* Generate the result value. */ + switch (token) + { + case TOK_DIVIDE: + /* + * Adrift's division apparently works by dividing using floating + * point, then applying (asymmetrical) rounding, so we have to do + * the same here. + */ + result = ((val1 < 0) == (val2 < 0)) + ? ((x / y) + (((x % y) * 2 >= y) ? 1 : 0)) + : -((x / y) + (((x % y) * 2 > y) ? 1 : 0)); + break; + + case TOK_MOD: + /* + * Adrift also breaks numerical consistency by defining mod in a + * conventional (non-rounded), way, so that A=(AdivB)*B+AmodB + * does not hold. + */ + result = (val1 < 0) ? -(x % y) : (x % y); + break; + + default: + sc_fatal ("expr_eval_action: bad token, %ld\n", token); + } + + /* Put result back at top of stack. */ + expr_eval_push_integer (result); + break; + } + + /* Handle power individually, to avoid needing a maths library. */ + case TOK_POWER: + { + sc_int val1, val2, result; + + /* Extract the two values to work on. */ + val2 = expr_eval_pop_integer (); + val1 = expr_eval_pop_integer (); + + /* Handle negative and zero power values first, as special cases. */ + if (val2 == 0) + result = 1; + else if (val2 < 0) + { + if (val1 == 0) + { + sc_error ("expr_eval_action: attempt to divide by zero\n"); + result = 0; + } + else if (val1 == 1) + result = val1; + else if (val1 == -1) + result = (-val2 & 1) ? val1 : -val1; + else + result = 0; + } + else + { + /* Raise to positive powers using the Russian Peasant algorithm. */ + while ((val2 & 1) == 0) + { + val1 = val1 * val1; + val2 >>= 1; + } + + result = val1; + val2 >>= 1; + while (val2 > 0) + { + val1 = val1 * val1; + if (val2 & 1) + result = result * val1; + val2 >>= 1; + } + } + + /* Put result back at top of stack. */ + expr_eval_push_integer (result); + break; + } + + /* Handle tokens representing functions returning string. */ + case TOK_LEFT: + case TOK_RIGHT: + { + sc_char *text; + sc_int length; + + /* + * Extract the text and length. If length is longer than text, or + * negative, do nothing. + */ + length = expr_eval_pop_integer (); + text = expr_eval_pop_string (); + if (length < 0 || length >= (sc_int) strlen (text)) + { + expr_eval_push_alloced_string (text); + break; + } + + /* + * Take the left or right segment -- for left, the operation is a + * simple truncation; for right, it's a memmove. + */ + switch (token) + { + case TOK_LEFT: + text[length] = NUL; + break; + + case TOK_RIGHT: + memmove (text, text + strlen (text) - length, length + 1); + break; + + default: + sc_fatal ("expr_eval_action: bad token, %ld\n", token); + } + + /* Put result back at top of stack. */ + expr_eval_push_alloced_string (text); + break; + } + + case TOK_MID: + { + sc_char *text; + sc_int length, start, limit; + + /* + * Extract the text, start, and length, re-basing start from 1 to 0, + * and calculate the limit on characters available for the move. + */ + length = expr_eval_pop_integer (); + start = expr_eval_pop_integer () - 1; + text = expr_eval_pop_string (); + limit = strlen (text); + + /* + * Clamp ranges that roam outside the available text -- start less + * than 0 to 0, and greater than len(text) to len(text), and length + * less than 0 to 0, and off string end to string end. + */ + if (start < 0) + start = 0; + else if (start > limit) + start = limit; + if (length < 0) + length = 0; + else if (length > limit - start) + length = limit - start; + + /* Move substring, terminate, and put back at top of stack. */ + memmove (text, text + start, length + 1); + text[length] = NUL; + expr_eval_push_alloced_string (text); + break; + } + + case TOK_STR: + { + sc_int val; + sc_char buffer[32]; + + /* + * Extract the value, convert it, and push back the resulting string. + * The leading space on positive values matches the Runner. + */ + val = expr_eval_pop_integer (); + sprintf (buffer, "% ld", val); + expr_eval_push_string (buffer); + break; + } + + + /* Handle tokens representing unary string operations. */ + case TOK_UPPER: + case TOK_LOWER: + case TOK_PROPER: + { + sc_char *text; + sc_int index_; + + /* Extract the value to work on. */ + text = expr_eval_pop_string (); + + /* Convert the entire string in place -- it's malloc'ed. */ + for (index_ = 0; text[index_] != NUL; index_++) + { + switch (token) + { + case TOK_UPPER: + text[index_] = sc_toupper (text[index_]); + break; + + case TOK_LOWER: + text[index_] = sc_tolower (text[index_]); + break; + + case TOK_PROPER: + if (index_ == 0 || sc_isspace (text[index_ - 1])) + text[index_] = sc_toupper (text[index_]); + else + text[index_] = sc_tolower (text[index_]); + break; + + default: + sc_fatal ("expr_eval_action: bad token, %ld\n", token); + } + } + + /* Put result back at top of stack. */ + expr_eval_push_alloced_string (text); + break; + } + + /* Handle token representing binary string operation. */ + case TOK_CONCATENATE: + { + sc_char *text1, *text2; + + /* Extract the two texts to work on. */ + text2 = expr_eval_pop_string (); + text1 = expr_eval_pop_string (); + + /* + * Resize text1 to be long enough for both, and concatenate, then + * free text2, and push back the concatenation. + */ + text1 = (sc_char *)sc_realloc (text1, strlen (text1) + strlen (text2) + 1); + strcat (text1, text2); + sc_free (text2); + expr_eval_push_alloced_string (text1); + break; + } + + default: + sc_fatal ("expr_eval_action: bad token, %ld\n", token); + } +} + + +/* Predictive parser lookahead token. */ +static sc_int expr_parse_lookahead = TOK_NONE; + +/* Forward declaration of factor parsers and string expression parser. */ +static void expr_parse_numeric_factor (void); +static void expr_parse_string_factor (void); +static void expr_parse_string_expr (void); + +/* + * expr_parse_match() + * + * Match a token to the lookahead, then advance lookahead. + */ +static void +expr_parse_match (sc_int token) +{ + if (expr_parse_lookahead == token) + expr_parse_lookahead = expr_next_token (); + else + { + /* Syntax error. */ + sc_error ("expr_parse_match: syntax error," + " expected %ld, got %ld\n", expr_parse_lookahead, token); + longjmp (expr_parse_error, 1); + } +} + + +/* + * Numeric operator precedence table. Operators are in order of precedence, + * with the highest being a factor. Each precedence entry permits several + * listed tokens. The end of the table (highest precedence) is marked by + * a list with no operators (although in practice we need to put a TOK_NONE + * in here since some C compilers won't accept { } as an empty initializer). + */ +typedef struct +{ + const sc_int token_count; + const sc_int tokens[6]; +} sc_precedence_entry_t; +#if 0 +/* + * Conventional (BASIC, C) precedence table for the parser. Exponentiation + * has the highest precedence, then multiplicative operations, additive, + * comparisons, and boolean combiners. + */ +static const sc_precedence_entry_t PRECEDENCE_TABLE[] = { + {1, {TOK_OR}}, + {1, {TOK_AND}}, + {2, {TOK_EQUAL, TOK_NOT_EQUAL}}, + {4, {TOK_GREATER, TOK_LESS, TOK_GREATER_EQ, TOK_LESS_EQ}}, + {2, {TOK_ADD, TOK_SUBTRACT}}, + {3, {TOK_MULTIPLY, TOK_DIVIDE, TOK_MOD}}, + {1, {TOK_POWER}}, + {0, {TOK_NONE}} +}; +#else +/* + * Adrift-like precedence table for the parser. Exponentiation and modulus + * operations seem to be implemented at the same level as addition and + * subtraction, and boolean 'and' and 'or' have equal precedence. + */ +static const sc_precedence_entry_t PRECEDENCE_TABLE[] = { + {2, {TOK_OR, TOK_AND}}, + {6, {TOK_EQUAL, TOK_NOT_EQUAL, + TOK_GREATER, TOK_LESS, TOK_GREATER_EQ, TOK_LESS_EQ}}, + {4, {TOK_ADD, TOK_SUBTRACT, TOK_POWER, TOK_MOD}}, + {2, {TOK_MULTIPLY, TOK_DIVIDE}}, + {0, {TOK_NONE}} +}; +#endif + + +/* + * expr_parse_contains_token() + * + * Helper for expr_parse_numeric_element(). Search the token list for the + * entry passed in, and return TRUE if it contains the given token. + */ +static int +expr_parse_contains_token (const sc_precedence_entry_t *entry, sc_int token) +{ + sc_bool is_matched; + sc_int index_; + + /* Search the entry's token list for the token passed in. */ + is_matched = FALSE; + for (index_ = 0; index_ < entry->token_count; index_++) + { + if (entry->tokens[index_] == token) + { + is_matched = TRUE; + break; + } + } + + return is_matched; +} + + +/* + * expr_parse_numeric_element() + * + * Parse numeric expression elements. This function uses the precedence table + * to match tokens, then decide whether, and how, to recurse into itself, or + * whether to parse a highest-precedence factor. + */ +static void +expr_parse_numeric_element (sc_int precedence) +{ + const sc_precedence_entry_t *entry; + + /* See if the level passed in has listed tokens. */ + entry = PRECEDENCE_TABLE + precedence; + if (entry->token_count == 0) + { + /* Precedence levels that hit the table end are factors. */ + expr_parse_numeric_factor (); + return; + } + + /* + * Parse initial higher-precedence factor, then others that associate + * with the given level. + */ + expr_parse_numeric_element (precedence + 1); + while (expr_parse_contains_token (entry, expr_parse_lookahead)) + { + sc_int token; + + /* Note token and match, parse next level, then action this token. */ + token = expr_parse_lookahead; + expr_parse_match (token); + expr_parse_numeric_element (precedence + 1); + expr_eval_action (token); + } +} + + +/* + * expr_parse_numeric_expr() + * + * Parse a complete numeric (sub-)expression. + */ +static void +expr_parse_numeric_expr (void) +{ + /* Call the parser of the lowest precedence operators. */ + expr_parse_numeric_element (0); +} + + +/* + * expr_parse_numeric_factor() + * + * Parse a numeric expression factor. + */ +static void +expr_parse_numeric_factor (void) +{ + /* Handle factors based on lookahead token. */ + switch (expr_parse_lookahead) + { + /* Handle straightforward factors first. */ + case TOK_LPAREN: + expr_parse_match (TOK_LPAREN); + expr_parse_numeric_expr (); + expr_parse_match (TOK_RPAREN); + break; + + case TOK_UMINUS: + expr_parse_match (TOK_UMINUS); + expr_parse_numeric_factor (); + expr_eval_action (TOK_UMINUS); + break; + + case TOK_UPLUS: + expr_parse_match (TOK_UPLUS); + expr_parse_numeric_factor (); + break; + + case TOK_INTEGER: + expr_eval_action (TOK_INTEGER); + expr_parse_match (TOK_INTEGER); + break; + + case TOK_VARIABLE: + { + sc_vartype_t token_value, vt_rvalue; + sc_int type; + + expr_current_token_value (&token_value); + if (!var_get (expr_varset, token_value.string, &type, &vt_rvalue)) + { + sc_error ("expr_parse_numeric_factor:" + " undefined variable, %s\n", token_value.string); + longjmp (expr_parse_error, 1); + } + if (type != VAR_INTEGER) + { + sc_error ("expr_parse_numeric_factor:" + " string variable in numeric context, %s\n", + token_value.string); + longjmp (expr_parse_error, 1); + } + expr_eval_action (TOK_VARIABLE); + expr_parse_match (TOK_VARIABLE); + break; + } + + /* Handle functions as factors. */ + case TOK_ABS: + /* Parse as "abs (val)". */ + expr_parse_match (TOK_ABS); + expr_parse_match (TOK_LPAREN); + expr_parse_numeric_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_ABS); + break; + + case TOK_IF: + /* Parse as "if (boolean, val1, val2)". */ + expr_parse_match (TOK_IF); + expr_parse_match (TOK_LPAREN); + expr_parse_numeric_expr (); + expr_parse_match (TOK_COMMA); + expr_parse_numeric_expr (); + expr_parse_match (TOK_COMMA); + expr_parse_numeric_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_IF); + break; + + case TOK_RANDOM: + /* Parse as "random (low, high)". */ + expr_parse_match (TOK_RANDOM); + expr_parse_match (TOK_LPAREN); + expr_parse_numeric_expr (); + expr_parse_match (TOK_COMMA); + expr_parse_numeric_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_RANDOM); + break; + + case TOK_MAX: + case TOK_MIN: + case TOK_EITHER: + /* Parse as "<func> (val1[,val2[,val3...]]])". */ + { + sc_int token, argument_count; + + /* Match up the function name and opening parenthesis. */ + token = expr_parse_lookahead; + expr_parse_match (token); + expr_parse_match (TOK_LPAREN); + + /* Count variable number of arguments as they are stacked. */ + expr_parse_numeric_expr (); + argument_count = 1; + while (expr_parse_lookahead == TOK_COMMA) + { + expr_parse_match (TOK_COMMA); + expr_parse_numeric_expr (); + argument_count++; + } + expr_parse_match (TOK_RPAREN); + + /* Push additional value -- the count of arguments. */ + expr_eval_push_integer (argument_count); + expr_eval_action (token); + break; + } + + case TOK_INSTR: + /* Parse as "instr (val1, val2)". */ + expr_parse_match (TOK_INSTR); + expr_parse_match (TOK_LPAREN); + expr_parse_string_expr (); + expr_parse_match (TOK_COMMA); + expr_parse_string_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_INSTR); + break; + + case TOK_LEN: + /* Parse as "len (val)". */ + expr_parse_match (TOK_LEN); + expr_parse_match (TOK_LPAREN); + expr_parse_string_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_LEN); + break; + + case TOK_VAL: + /* Parse as "val (val)". */ + expr_parse_match (TOK_VAL); + expr_parse_match (TOK_LPAREN); + expr_parse_string_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_VAL); + break; + + case TOK_IDENT: + /* Unrecognized function-type token. */ + sc_error ("expr_parse_numeric_factor: syntax error, unknown ident\n"); + longjmp (expr_parse_error, 1); + + default: + /* Syntax error. */ + sc_error ("expr_parse_numeric_factor:" + " syntax error, unexpected token, %ld\n", expr_parse_lookahead); + longjmp (expr_parse_error, 1); + } +} + + +/* + * expr_parse_string_expr() + * + * Parse a complete string (sub-)expression. + */ +static void +expr_parse_string_expr (void) +{ + /* + * Parse a string factor, then all repeated concatenations. Because the '+' + * and '&' are context sensitive, we have to invent/translate them into the + * otherwise unused TOK_CONCATENATE for evaluation. + */ + expr_parse_string_factor (); + while (expr_parse_lookahead == TOK_AND || expr_parse_lookahead == TOK_ADD) + { + expr_parse_match (expr_parse_lookahead); + expr_parse_string_factor (); + expr_eval_action (TOK_CONCATENATE); + } +} + + +/* + * expr_parse_string_factor() + * + * Parse a string expression factor. + */ +static void +expr_parse_string_factor (void) +{ + /* Handle factors based on lookahead token. */ + switch (expr_parse_lookahead) + { + /* Handle straightforward factors first. */ + case TOK_LPAREN: + expr_parse_match (TOK_LPAREN); + expr_parse_string_expr (); + expr_parse_match (TOK_RPAREN); + break; + + case TOK_STRING: + expr_eval_action (TOK_STRING); + expr_parse_match (TOK_STRING); + break; + + case TOK_VARIABLE: + { + sc_vartype_t token_value, vt_rvalue; + sc_int type; + + expr_current_token_value (&token_value); + if (!var_get (expr_varset, token_value.string, &type, &vt_rvalue)) + { + sc_error ("expr_parse_string_factor:" + " undefined variable, %s\n", token_value.string); + longjmp (expr_parse_error, 1); + } + if (type != VAR_STRING) + { + sc_error ("expr_parse_string_factor:" + " numeric variable in string context, %s\n", + token_value.string); + longjmp (expr_parse_error, 1); + } + expr_eval_action (TOK_VARIABLE); + expr_parse_match (TOK_VARIABLE); + break; + } + + /* Handle functions as factors. */ + case TOK_UPPER: + case TOK_LOWER: + case TOK_PROPER: + /* Parse as "<func> (text)". */ + { + sc_int token; + + token = expr_parse_lookahead; + expr_parse_match (token); + expr_parse_match (TOK_LPAREN); + expr_parse_string_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (token); + break; + } + + case TOK_LEFT: + case TOK_RIGHT: + /* Parse as "<func> (text,length)". */ + { + sc_int token; + + token = expr_parse_lookahead; + expr_parse_match (token); + expr_parse_match (TOK_LPAREN); + expr_parse_string_expr (); + expr_parse_match (TOK_COMMA); + expr_parse_numeric_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (token); + break; + } + + case TOK_MID: + /* Parse as "mid (text,start,length)". */ + expr_parse_match (TOK_MID); + expr_parse_match (TOK_LPAREN); + expr_parse_string_expr (); + expr_parse_match (TOK_COMMA); + expr_parse_numeric_expr (); + expr_parse_match (TOK_COMMA); + expr_parse_numeric_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_MID); + break; + + case TOK_STR: + /* Parse as "str (val)". */ + expr_parse_match (TOK_STR); + expr_parse_match (TOK_LPAREN); + expr_parse_numeric_expr (); + expr_parse_match (TOK_RPAREN); + expr_eval_action (TOK_STR); + break; + + case TOK_IDENT: + /* Unrecognized function-type token. */ + sc_error ("expr_parse_string_factor: syntax error, unknown ident\n"); + longjmp (expr_parse_error, 1); + + default: + /* Syntax error. */ + sc_error ("expr_parse_string_factor:" + " syntax error, unexpected token, %ld\n", expr_parse_lookahead); + longjmp (expr_parse_error, 1); + } +} + + +/* + * expr_evaluate_expression() + * + * Parse a string expression into a runtime values stack. Return the + * value of the expression. + */ +static sc_bool +expr_evaluate_expression (const sc_char *expression, sc_var_setref_t vars, + sc_int assign_type, sc_vartype_t *vt_rvalue) +{ + assert (assign_type == VAR_INTEGER || assign_type == VAR_STRING); + + /* Reset values stack and start tokenizer. */ + expr_eval_start (vars); + expr_tokenize_start (expression); + + /* Try parsing an expression, and catch errors. */ + if (setjmp (expr_parse_error) == 0) + { + /* Parse an expression, and ensure it ends at string end. */ + expr_parse_lookahead = expr_next_token (); + if (assign_type == VAR_STRING) + expr_parse_string_expr (); + else + expr_parse_numeric_expr (); + expr_parse_match (TOK_EOS); + } + else + { + /* Parse error -- clean up tokenizer, collect garbage, and fail. */ + expr_tokenize_end (); + expr_eval_garbage_collect (); + return FALSE; + } + + /* Clean up tokenizer and return successfully with result. */ + expr_tokenize_end (); + expr_eval_result (vt_rvalue); + return TRUE; +} + + +/* + * expr_eval_numeric_expression() + * expr_eval_string_expression() + * + * Public interfaces to expression evaluation. Evaluate an expression, and + * assign the result to either a numeric or a string. For string expressions, + * the return value is malloc'ed, and the caller is responsible for freeing + * it. + */ +sc_bool +expr_eval_numeric_expression (const sc_char *expression, + sc_var_setref_t vars, sc_int *rvalue) +{ + sc_vartype_t vt_rvalue; + sc_bool status; + assert (expression && vars && rvalue); + + /* Evaluate numeric expression, and return value if valid. */ + status = expr_evaluate_expression (expression, vars, VAR_INTEGER, &vt_rvalue); + if (status) + *rvalue = vt_rvalue.integer; + return status; +} + +sc_bool +expr_eval_string_expression (const sc_char *expression, + sc_var_setref_t vars, sc_char **rvalue) +{ + sc_vartype_t vt_rvalue; + sc_bool status; + assert (expression && vars && rvalue); + + /* Evaluate string expression, and return value if valid. */ + status = expr_evaluate_expression (expression, vars, VAR_STRING, &vt_rvalue); + if (status) + *rvalue = vt_rvalue.mutable_string; + return status; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scgamest.cpp b/engines/glk/adrift/scgamest.cpp new file mode 100644 index 0000000000..2d9d78fbb3 --- /dev/null +++ b/engines/glk/adrift/scgamest.cpp @@ -0,0 +1,1195 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_uint GAME_MAGIC = 0x35aed26e; + +/* + * gs_move_player_to_room() + * gs_player_in_room() + * + * Move the player to a given room, and check presence in a given room. + */ +void +gs_move_player_to_room (sc_gameref_t game, sc_int room) +{ + assert(gs_is_game_valid (game)); + + if (room < 0) + { + sc_fatal ("gs_move_player_to_room: invalid room, %ld\n", room); + return; + } + else if (room < game->room_count) + game->playerroom = room; + else + game->playerroom = lib_random_roomgroup_member (game, + room - game->room_count); + + game->playerparent = -1; + game->playerposition = 0; +} + +sc_bool +gs_player_in_room (sc_gameref_t game, sc_int room) +{ + assert(gs_is_game_valid (game)); + return game->playerroom == room; +} + + +/* + * gs_in_range() + * + * Helper for event, room, object, and npc range assertions. + */ +static sc_bool +gs_in_range (sc_int value, sc_int limit) +{ + return value >= 0 && value < limit; +} + + +/* + * gs_*() + * + * Game accessors and mutators. + */ +sc_var_setref_t +gs_get_vars (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->vars; +} + +sc_prop_setref_t +gs_get_bundle (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->bundle; +} + +sc_filterref_t +gs_get_filter (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->filter; +} + +sc_memo_setref_t +gs_get_memento (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->memento; +} + + +/* + * Game accessors and mutators for the player. + */ +void +gs_set_playerroom (sc_gameref_t gs, sc_int room) +{ + assert(gs_is_game_valid (gs)); + gs->playerroom = room; +} + +void +gs_set_playerposition (sc_gameref_t gs, sc_int position) +{ + assert(gs_is_game_valid (gs)); + gs->playerposition = position; +} + +void +gs_set_playerparent (sc_gameref_t gs, sc_int parent) +{ + assert(gs_is_game_valid (gs)); + gs->playerparent = parent; +} + +sc_int +gs_playerroom (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->playerroom; +} + +sc_int +gs_playerposition (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->playerposition; +} + +sc_int +gs_playerparent (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->playerparent; +} + + +/* + * Game accessors and mutators for events. + */ +sc_int +gs_event_count (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->event_count; +} + +void +gs_set_event_state (sc_gameref_t gs, sc_int event, sc_int state) +{ + assert(gs_is_game_valid (gs) && gs_in_range (event, gs->event_count)); + gs->events[event].state = state; +} + +void +gs_set_event_time (sc_gameref_t gs, sc_int event, sc_int etime) +{ + assert(gs_is_game_valid (gs) && gs_in_range (event, gs->event_count)); + gs->events[event].time = etime; +} + +sc_int +gs_event_state (sc_gameref_t gs, sc_int event) +{ + assert(gs_is_game_valid (gs) && gs_in_range (event, gs->event_count)); + return gs->events[event].state; +} + +sc_int +gs_event_time (sc_gameref_t gs, sc_int event) +{ + assert(gs_is_game_valid (gs) && gs_in_range (event, gs->event_count)); + return gs->events[event].time; +} + +void +gs_decrement_event_time (sc_gameref_t gs, sc_int event) +{ + assert(gs_is_game_valid (gs) && gs_in_range (event, gs->event_count)); + gs->events[event].time--; +} + + +/* + * Game accessors and mutators for rooms. + */ +sc_int +gs_room_count (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->room_count; +} + +void +gs_set_room_seen (sc_gameref_t gs, sc_int room, sc_bool seen) +{ + assert(gs_is_game_valid (gs) && gs_in_range (room, gs->room_count)); + gs->rooms[room].visited = seen; +} + +sc_bool +gs_room_seen (sc_gameref_t gs, sc_int room) +{ + assert(gs_is_game_valid (gs) && gs_in_range (room, gs->room_count)); + return gs->rooms[room].visited; +} + + +/* + * Game accessors and mutators for tasks. + */ +sc_int +gs_task_count (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->task_count; +} + +void +gs_set_task_done (sc_gameref_t gs, sc_int task, sc_bool done) +{ + assert(gs_is_game_valid (gs) && gs_in_range (task, gs->task_count)); + gs->tasks[task].done = done; +} + +void +gs_set_task_scored (sc_gameref_t gs, sc_int task, sc_bool scored) +{ + assert(gs_is_game_valid (gs) && gs_in_range (task, gs->task_count)); + gs->tasks[task].scored = scored; +} + +sc_bool +gs_task_done (sc_gameref_t gs, sc_int task) +{ + assert(gs_is_game_valid (gs) && gs_in_range (task, gs->task_count)); + return gs->tasks[task].done; +} + +sc_bool +gs_task_scored (sc_gameref_t gs, sc_int task) +{ + assert(gs_is_game_valid (gs) && gs_in_range (task, gs->task_count)); + return gs->tasks[task].scored; +} + + +/* + * Game accessors and mutators for objects. + */ +sc_int +gs_object_count (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->object_count; +} + +void +gs_set_object_openness (sc_gameref_t gs, sc_int object, sc_int openness) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].openness = openness; +} + +void +gs_set_object_state (sc_gameref_t gs, sc_int object, sc_int state) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].state = state; +} + +void +gs_set_object_seen (sc_gameref_t gs, sc_int object, sc_bool seen) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].seen = seen; +} + +void +gs_set_object_unmoved (sc_gameref_t gs, sc_int object, sc_bool unmoved) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].unmoved = unmoved; +} + +void +gs_set_object_static_unmoved (sc_gameref_t gs, sc_int object, sc_bool unmoved) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].static_unmoved = unmoved; +} + +sc_int +gs_object_openness (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + return gs->objects[object].openness; +} + +sc_int +gs_object_state (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + return gs->objects[object].state; +} + +sc_bool +gs_object_seen (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + return gs->objects[object].seen; +} + +sc_bool +gs_object_unmoved (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + return gs->objects[object].unmoved; +} + +sc_bool +gs_object_static_unmoved (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + return gs->objects[object].static_unmoved; +} + +sc_int +gs_object_position (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + return gs->objects[object].position; +} + +sc_int +gs_object_parent (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + return gs->objects[object].parent; +} + +static void +gs_object_move_onto_unchecked (sc_gameref_t gs, sc_int object, sc_int onto) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = OBJ_ON_OBJECT; + gs->objects[object].parent = onto; +} + +static void +gs_object_move_into_unchecked (sc_gameref_t gs, sc_int object, sc_int into) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = OBJ_IN_OBJECT; + gs->objects[object].parent = into; +} + +static void +gs_object_make_hidden_unchecked (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = OBJ_HIDDEN; + gs->objects[object].parent = -1; +} + +static void +gs_object_player_get_unchecked (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = OBJ_HELD_PLAYER; + gs->objects[object].parent = -1; +} + +static void +gs_object_npc_get_unchecked (sc_gameref_t gs, sc_int object, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = OBJ_HELD_NPC; + gs->objects[object].parent = npc; +} + +static void +gs_object_player_wear_unchecked (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = OBJ_WORN_PLAYER; + gs->objects[object].parent = 0; +} + +static void +gs_object_npc_wear_unchecked (sc_gameref_t gs, sc_int object, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = OBJ_WORN_NPC; + gs->objects[object].parent = npc; +} + +static void +gs_object_to_room_unchecked (sc_gameref_t gs, sc_int object, sc_int room) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + gs->objects[object].position = room + 1; + gs->objects[object].parent = -1; +} + +void +gs_object_move_onto (sc_gameref_t gs, sc_int object, sc_int onto) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != OBJ_ON_OBJECT + || gs->objects[object].parent != onto) + { + gs_object_move_onto_unchecked (gs, object, onto); + gs->objects[object].unmoved = FALSE; + } +} + +void +gs_object_move_into (sc_gameref_t gs, sc_int object, sc_int into) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != OBJ_IN_OBJECT + || gs->objects[object].parent != into) + { + gs_object_move_into_unchecked (gs, object, into); + gs->objects[object].unmoved = FALSE; + } +} + +void +gs_object_make_hidden (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != OBJ_HIDDEN) + { + gs_object_make_hidden_unchecked (gs, object); + gs->objects[object].unmoved = FALSE; + } +} + +void +gs_object_player_get (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != OBJ_HELD_PLAYER) + { + gs_object_player_get_unchecked (gs, object); + gs->objects[object].unmoved = FALSE; + } +} + +void +gs_object_npc_get (sc_gameref_t gs, sc_int object, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != OBJ_HELD_NPC + || gs->objects[object].parent != npc) + { + gs_object_npc_get_unchecked (gs, object, npc); + gs->objects[object].unmoved = FALSE; + } +} + +void +gs_object_player_wear (sc_gameref_t gs, sc_int object) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != OBJ_WORN_PLAYER) + { + gs_object_player_wear_unchecked (gs, object); + gs->objects[object].unmoved = FALSE; + } +} + +void +gs_object_npc_wear (sc_gameref_t gs, sc_int object, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != OBJ_WORN_NPC + || gs->objects[object].parent != npc) + { + gs_object_npc_wear_unchecked (gs, object, npc); + gs->objects[object].unmoved = FALSE; + } +} + +void +gs_object_to_room (sc_gameref_t gs, sc_int object, sc_int room) +{ + assert(gs_is_game_valid (gs) && gs_in_range (object, gs->object_count)); + if (gs->objects[object].position != room + 1) + { + gs_object_to_room_unchecked (gs, object, room); + gs->objects[object].unmoved = FALSE; + } +} + + +/* + * Game accessors and mutators for NPCs. + */ +sc_int +gs_npc_count (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + return gs->npc_count; +} + +void +gs_set_npc_location (sc_gameref_t gs, sc_int npc, sc_int location) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + gs->npcs[npc].location = location; +} + +sc_int +gs_npc_location (sc_gameref_t gs, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + return gs->npcs[npc].location; +} + +void +gs_set_npc_position (sc_gameref_t gs, sc_int npc, sc_int position) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + gs->npcs[npc].position = position; +} + +sc_int +gs_npc_position (sc_gameref_t gs, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + return gs->npcs[npc].position; +} + +void +gs_set_npc_parent (sc_gameref_t gs, sc_int npc, sc_int parent) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + gs->npcs[npc].parent = parent; +} + +sc_int +gs_npc_parent (sc_gameref_t gs, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + return gs->npcs[npc].parent; +} + +void +gs_set_npc_seen (sc_gameref_t gs, sc_int npc, sc_bool seen) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + gs->npcs[npc].seen = seen; +} + +sc_bool +gs_npc_seen (sc_gameref_t gs, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + return gs->npcs[npc].seen; +} + +sc_int +gs_npc_walkstep_count (sc_gameref_t gs, sc_int npc) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count)); + return gs->npcs[npc].walkstep_count; +} + +void +gs_set_npc_walkstep (sc_gameref_t gs, + sc_int npc, sc_int walk, sc_int walkstep) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count) + && gs_in_range (walk, gs->npcs[npc].walkstep_count)); + gs->npcs[npc].walksteps[walk] = walkstep; +} + +sc_int +gs_npc_walkstep (sc_gameref_t gs, sc_int npc, sc_int walk) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count) + && gs_in_range (walk, gs->npcs[npc].walkstep_count)); + return gs->npcs[npc].walksteps[walk]; +} + +void +gs_decrement_npc_walkstep (sc_gameref_t gs, sc_int npc, sc_int walk) +{ + assert(gs_is_game_valid (gs) && gs_in_range (npc, gs->npc_count) + && gs_in_range (walk, gs->npcs[npc].walkstep_count)); + gs->npcs[npc].walksteps[walk]--; +} + + +/* + * Convenience functions for bulk clearance of references. + */ +void +gs_clear_npc_references (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + memset (gs->npc_references, + FALSE, gs->npc_count * sizeof (*gs->npc_references)); +} + +void +gs_clear_object_references (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + memset (gs->object_references, + FALSE, gs->object_count * sizeof (*gs->object_references)); +} + +void +gs_set_multiple_references (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + memset (gs->multiple_references, + TRUE, gs->object_count * sizeof (*gs->multiple_references)); +} + +void +gs_clear_multiple_references (sc_gameref_t gs) +{ + assert(gs_is_game_valid (gs)); + memset (gs->multiple_references, + FALSE, gs->object_count * sizeof (*gs->multiple_references)); +} + + +/* + * gs_create() + * + * Create and initialize a game state. + */ +sc_gameref_t +gs_create (sc_var_setref_t vars, + sc_prop_setref_t bundle, sc_filterref_t filter) +{ + sc_gameref_t game; + sc_vartype_t vt_key[4]; + sc_int index_, bytes; + assert(vars && bundle && filter); + + /* Create the initial state structure. */ + game = (sc_gameref_t)sc_malloc (sizeof (*game)); + game->magic = GAME_MAGIC; + + /* Store the variables, properties bundle, and filter references. */ + game->vars = vars; + game->bundle = bundle; + game->filter = filter; + + /* Set memento to NULL for now; it's added later. */ + game->memento = NULL; + + /* Initialize for no debugger. */ + game->debugger = NULL; + + /* Initialize the undo buffers to NULL for now. */ + game->temporary = NULL; + game->undo = NULL; + game->undo_available = FALSE; + + /* Create rooms state array. */ + vt_key[0].string = "Rooms"; + game->room_count = prop_get_child_count (bundle, "I<-s", vt_key); + game->rooms = (sc_roomstate_t *)sc_malloc (game->room_count * sizeof (*game->rooms)); + + /* Set up initial rooms states. */ + for (index_ = 0; index_ < game->room_count; index_++) + gs_set_room_seen (game, index_, FALSE); + + /* Create objects state array. */ + vt_key[0].string = "Objects"; + game->object_count = prop_get_child_count (bundle, "I<-s", vt_key); + game->objects = (sc_objectstate_t *)sc_malloc (game->object_count * sizeof (*game->objects)); + + /* Set up initial object states. */ + for (index_ = 0; index_ < game->object_count; index_++) + { + const sc_char *inroomdesc; + sc_bool is_static, unmoved; + + vt_key[1].integer = index_; + + vt_key[2].string = "Static"; + is_static = prop_get_boolean (bundle, "B<-sis", vt_key); + if (is_static) + { + sc_int type; + + vt_key[2].string = "Where"; + vt_key[3].string = "Type"; + type = prop_get_integer (bundle, "I<-siss", vt_key); + if (type == ROOMLIST_NPC_PART) + { + sc_int parent; + + game->objects[index_].position = OBJ_PART_NPC; + + vt_key[2].string = "Parent"; + parent = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + game->objects[index_].parent = parent; + } + else + gs_object_make_hidden_unchecked (game, index_); + } + else + { + sc_int initialparent, initialposition; + + vt_key[2].string = "Parent"; + initialparent = prop_get_integer (bundle, "I<-sis", vt_key); + vt_key[2].string = "InitialPosition"; + initialposition = prop_get_integer (bundle, "I<-sis", vt_key); + switch (initialposition) + { + case 0: /* Hidden. */ + gs_object_make_hidden_unchecked (game, index_); + break; + + case 1: /* Held. */ + if (initialparent == 0) /* By player. */ + gs_object_player_get_unchecked (game, index_); + else /* By NPC. */ + gs_object_npc_get_unchecked (game, index_, initialparent - 1); + break; + + case 2: /* In container. */ + gs_object_move_into_unchecked (game, index_, + obj_container_object (game, initialparent)); + break; + + case 3: /* On surface. */ + gs_object_move_onto_unchecked (game, index_, + obj_surface_object (game, initialparent)); + break; + + default: /* In room, or worn by player/NPC. */ + if (initialposition >= 4 + && initialposition < 4 + game->room_count) + { + gs_object_to_room_unchecked (game, + index_, initialposition - 4); + } + else if (initialposition == 4 + game->room_count) + { + if (initialparent == 0) + gs_object_player_wear_unchecked (game, index_); + else + gs_object_npc_wear_unchecked (game, + index_, initialparent - 1); + } + else + { + sc_error ("gs_create: object in out of bounds room, %ld\n", + initialposition - 4 - game->room_count); + gs_object_to_room_unchecked (game, index_, -2); + } + } + } + + vt_key[2].string = "CurrentState"; + gs_set_object_state (game, index_, + prop_get_integer (bundle, "I<-sis", vt_key)); + + vt_key[2].string = "Openable"; + gs_set_object_openness (game, index_, + prop_get_integer (bundle, "I<-sis", vt_key)); + + gs_set_object_seen (game, index_, FALSE); + + vt_key[2].string = "InRoomDesc"; + inroomdesc = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (inroomdesc)) + { + vt_key[2].string = "OnlyWhenNotMoved"; + if (prop_get_integer (bundle, "I<-sis", vt_key) == 1) + unmoved = TRUE; + else + unmoved = FALSE; + } + else + unmoved = FALSE; + gs_set_object_unmoved (game, index_, unmoved); + gs_set_object_static_unmoved (game, index_, TRUE); + } + + /* Create tasks state array. */ + vt_key[0].string = "Tasks"; + game->task_count = prop_get_child_count (bundle, "I<-s", vt_key); + game->tasks = (sc_taskstate_t *)sc_malloc (game->task_count * sizeof (*game->tasks)); + + /* Set up initial tasks states. */ + for (index_ = 0; index_ < game->task_count; index_++) + { + gs_set_task_done (game, index_, FALSE); + gs_set_task_scored (game, index_, FALSE); + } + + /* Create events state array. */ + vt_key[0].string = "Events"; + game->event_count = prop_get_child_count (bundle, "I<-s", vt_key); + game->events = (sc_eventstate_t *)sc_malloc (game->event_count * sizeof (*game->events)); + + /* Set up initial events states. */ + for (index_ = 0; index_ < game->event_count; index_++) + { + sc_int startertype; + + vt_key[1].integer = index_; + vt_key[2].string = "StarterType"; + startertype = prop_get_integer (bundle, "I<-sis", vt_key); + + switch (startertype) + { + case 1: + gs_set_event_state (game, index_, ES_WAITING); + gs_set_event_time (game, index_, 0); + break; + + case 2: + { + sc_int start, end; + + gs_set_event_state (game, index_, ES_WAITING); + vt_key[2].string = "StartTime"; + start = prop_get_integer (bundle, "I<-sis", vt_key); + vt_key[2].string = "EndTime"; + end = prop_get_integer (bundle, "I<-sis", vt_key); + gs_set_event_time (game, index_, sc_randomint (start, end)); + break; + } + + case 3: + gs_set_event_state (game, index_, ES_AWAITING); + gs_set_event_time (game, index_, 0); + break; + } + } + + /* Create NPCs state array. */ + vt_key[0].string = "NPCs"; + game->npc_count = prop_get_child_count (bundle, "I<-s", vt_key); + game->npcs = (sc_npcstate_t *)sc_malloc (game->npc_count * sizeof (*game->npcs)); + + /* Set up initial NPCs states. */ + for (index_ = 0; index_ < game->npc_count; index_++) + { + sc_int walk, walkstep_count; + + gs_set_npc_position (game, index_, 0); + gs_set_npc_parent (game, index_, -1); + gs_set_npc_seen (game, index_, FALSE); + + vt_key[1].integer = index_; + + vt_key[2].string = "StartRoom"; + gs_set_npc_location (game, index_, + prop_get_integer (bundle, "I<-sis", vt_key)); + + vt_key[2].string = "Walks"; + walkstep_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + game->npcs[index_].walkstep_count = walkstep_count; + game->npcs[index_].walksteps = (sc_int *)sc_malloc (walkstep_count + * sizeof (*game->npcs[0].walksteps)); + + for (walk = 0; walk < walkstep_count; walk++) + gs_set_npc_walkstep (game, index_, walk, 0); + } + + /* Set up the player portions of the game state. */ + vt_key[0].string = "Header"; + vt_key[1].string = "StartRoom"; + game->playerroom = prop_get_integer (bundle, "I<-ss", vt_key); + vt_key[0].string = "Globals"; + vt_key[1].string = "ParentObject"; + game->playerparent = prop_get_integer (bundle, "I<-ss", vt_key) - 1; + vt_key[1].string = "Position"; + game->playerposition = prop_get_integer (bundle, "I<-ss", vt_key); + + /* Initialize score notifications from game properties. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "NoScoreNotify"; + game->notify_score_change = !prop_get_boolean (bundle, "B<-ss", vt_key); + + /* Miscellaneous state defaults. */ + game->turns = 0; + game->score = 0; + game->bold_room_names = TRUE; + game->verbose = FALSE; + game->current_room_name = NULL; + game->status_line = NULL; + game->title = NULL; + game->author = NULL; + game->hint_text = NULL; + + /* Resource controls. */ + res_clear_resource (&game->requested_sound); + res_clear_resource (&game->requested_graphic); + res_clear_resource (&game->playing_sound); + res_clear_resource (&game->displayed_graphic); + game->stop_sound = FALSE; + game->sound_active = FALSE; + + /* Initialize wait turns from game properties. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "WaitTurns"; + game->waitturns = prop_get_integer (bundle, "I<-ss", vt_key); + + /* Non-game conveniences. */ + game->is_running = FALSE; + game->has_notified = FALSE; + game->is_admin = FALSE; + game->has_completed = FALSE; + game->waitcounter = 0; + game->do_again = FALSE; + game->redo_sequence = 0; + game->do_restart = FALSE; + game->do_restore = FALSE; + + bytes = game->object_count * sizeof (*game->object_references); + game->object_references = (sc_bool *)sc_malloc (bytes); + memset (game->object_references, FALSE, bytes); + bytes = game->object_count * sizeof (*game->multiple_references); + game->multiple_references = (sc_bool *)sc_malloc (bytes); + memset (game->multiple_references, FALSE, bytes); + + bytes = game->npc_count * sizeof (*game->npc_references); + game->npc_references = (sc_bool *)sc_malloc (bytes); + memset (game->npc_references, FALSE, bytes); + + game->it_object = -1; + game->him_npc = -1; + game->her_npc = -1; + game->it_npc = -1; + + /* Clear the quit jump buffer for tidiness. */ + memset (&game->quitter, 0, sizeof (game->quitter)); + + /* Return the constructed game state. */ + return game; +} + + +/* + * gs_is_game_valid() + * + * Return TRUE if pointer is a valid game, FALSE otherwise. + */ +sc_bool +gs_is_game_valid (sc_gameref_t game) +{ + return game && game->magic == GAME_MAGIC; +} + + +/* + * gs_string_copy() + * + * Helper for gs_copy(), copies one malloc'ed string to another, or NULL + * if from is NULL, taking care not to leak memory. + */ +static void +gs_string_copy (sc_char **to_string, const sc_char *from_string) +{ + /* Free any current contents of to_string. */ + sc_free (*to_string); + + /* Copy from_string if set, otherwise set to_string to NULL. */ + if (from_string) + { + *to_string = (sc_char *)sc_malloc (strlen (from_string) + 1); + strcpy (*to_string, from_string); + } + else + *to_string = NULL; +} + + +/* + * gs_copy() + * + * Deep-copy the dynamic parts of a game onto another existing + * game structure. + */ +void +gs_copy (sc_gameref_t to, sc_gameref_t from) +{ + const sc_prop_setref_t bundle = from->bundle; + sc_vartype_t vt_key[3]; + sc_int var_count, var, npc; + assert(gs_is_game_valid (to) && gs_is_game_valid (from)); + + /* + * Copy over references to the properties bundle and filter. The debugger + * is specifically excluded, as it's considered to be tied to the game. + */ + to->bundle = from->bundle; + to->filter = from->filter; + + /* Copy over references to the undo buffers. */ + to->temporary = from->temporary; + to->undo = from->undo; + to->undo_available = from->undo_available; + + /* Copy over all variables values. */ + vt_key[0].string = "Variables"; + var_count = prop_get_child_count (bundle, "I<-s", vt_key); + + for (var = 0; var < var_count; var++) + { + const sc_char *name; + sc_int var_type; + + vt_key[1].integer = var; + + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Type"; + var_type = prop_get_integer (bundle, "I<-sis", vt_key); + + switch (var_type) + { + case TAFVAR_NUMERIC: + var_put_integer (to->vars, name, var_get_integer (from->vars, name)); + break; + + case TAFVAR_STRING: + var_put_string (to->vars, name, var_get_string (from->vars, name)); + break; + + default: + sc_fatal ("gs_copy: unknown variable type, %ld\n", var_type); + } + } + + /* Copy over the variable timestamp. */ + var_set_elapsed_seconds (to->vars, var_get_elapsed_seconds (from->vars)); + + /* Copy over room states. */ + assert(to->room_count == from->room_count); + memcpy (to->rooms, from->rooms, from->room_count * sizeof (*from->rooms)); + + /* Copy over object states. */ + assert(to->object_count == from->object_count); + memcpy (to->objects, from->objects, + from->object_count * sizeof (*from->objects)); + + /* Copy over task states. */ + assert(to->task_count == from->task_count); + memcpy (to->tasks, from->tasks, from->task_count * sizeof (*from->tasks)); + + /* Copy over event states. */ + assert(to->event_count == from->event_count); + memcpy (to->events, from->events, from->event_count * sizeof (*from->events)); + + /* Copy over NPC states individually, to avoid walks problems. */ + for (npc = 0; npc < from->npc_count; npc++) + { + to->npcs[npc].location = from->npcs[npc].location; + to->npcs[npc].position = from->npcs[npc].position; + to->npcs[npc].parent = from->npcs[npc].parent; + to->npcs[npc].seen = from->npcs[npc].seen; + to->npcs[npc].walkstep_count = from->npcs[npc].walkstep_count; + + /* Copy over NPC walks information. */ + assert(to->npcs[npc].walkstep_count == from->npcs[npc].walkstep_count); + memcpy (to->npcs[npc].walksteps, from->npcs[npc].walksteps, + from->npcs[npc].walkstep_count + * sizeof (*from->npcs[npc].walksteps)); + } + + /* Copy over player information. */ + to->playerroom = from->playerroom; + to->playerposition = from->playerposition; + to->playerparent = from->playerparent; + + /* + * Copy over miscellaneous other details. Specifically exclude bold rooms, + * verbose, and score notification, so that they are invariant across copies, + * particularly undo/restore. + */ + to->turns = from->turns; + to->score = from->score; + + gs_string_copy (&to->current_room_name, from->current_room_name); + gs_string_copy (&to->status_line, from->status_line); + gs_string_copy (&to->title, from->title); + gs_string_copy (&to->author, from->author); + gs_string_copy (&to->hint_text, from->hint_text); + + /* + * Specifically exclude playing sound and displayed graphic from the copy + * so that they remain invariant across game copies. + */ + to->requested_sound = from->requested_sound; + to->requested_graphic = from->requested_graphic; + to->stop_sound = from->stop_sound; + + to->is_running = from->is_running; + to->has_notified = from->has_notified; + to->is_admin = from->is_admin; + to->has_completed = from->has_completed; + + to->waitturns = from->waitturns; + + to->waitcounter = from->waitcounter; + to->do_again = from->do_again; + to->redo_sequence = from->redo_sequence; + to->do_restart = from->do_restart; + to->do_restore = from->do_restore; + + memcpy (to->object_references, from->object_references, + from->object_count * sizeof (*from->object_references)); + memcpy (to->multiple_references, from->multiple_references, + from->object_count * sizeof (*from->multiple_references)); + memcpy (to->npc_references, from->npc_references, + from->npc_count * sizeof (*from->npc_references)); + + to->it_object = from->it_object; + to->him_npc = from->him_npc; + to->her_npc = from->her_npc; + to->it_npc = from->it_npc; + + /* Copy over the quit jump buffer. */ + memcpy (&to->quitter, &from->quitter, sizeof (from->quitter)); +} + + +/* + * gs_destroy() + * + * Free all the memory associated with a game state. + */ +void +gs_destroy (sc_gameref_t game) +{ + sc_int npc; + assert(gs_is_game_valid (game)); + + /* Free the malloc'ed state arrays. */ + sc_free (game->rooms); + sc_free (game->objects); + sc_free (game->tasks); + sc_free (game->events); + for (npc = 0; npc < game->npc_count; npc++) + sc_free (game->npcs[npc].walksteps); + sc_free (game->npcs); + + /* Free the malloc'ed object and NPC references. */ + sc_free (game->object_references); + sc_free (game->multiple_references); + sc_free (game->npc_references); + + /* Free malloc'ed game strings. */ + sc_free (game->current_room_name); + sc_free (game->status_line); + sc_free (game->title); + sc_free (game->author); + sc_free (game->hint_text); + + /* Poison and free the game state itself. */ + memset (game, 0xaa, sizeof (*game)); + sc_free (game); +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scgamest.h b/engines/glk/adrift/scgamest.h new file mode 100644 index 0000000000..4287551bfd --- /dev/null +++ b/engines/glk/adrift/scgamest.h @@ -0,0 +1,192 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" + +#ifndef ADRIFT_GAMESTATE_H +#define ADRIFT_GAMESTATE_H + +namespace Glk { +namespace Adrift { + +/* Room state structure, tracks rooms visited by the player. */ +struct sc_roomstate_s { + sc_bool visited; +}; +typedef sc_roomstate_s sc_roomstate_t; + +/* + * Object state structure, tracks object movement, position, parent, openness + * for openable objects, state for stateful objects, and whether seen or not + * by the player. The enumerations are values assigned to position when the + * object is other than just "in a room"; otherwise position contains the + * room number + 1. + */ +enum { + OBJ_HIDDEN = -1, OBJ_HELD_PLAYER = 0, OBJ_HELD_NPC = -200, OBJ_WORN_PLAYER = -100, + OBJ_WORN_NPC = -300, OBJ_PART_PLAYER = -30, OBJ_PART_NPC = -30, OBJ_ON_OBJECT = -20, + OBJ_IN_OBJECT = -10 +}; +struct sc_objectstate_s { + sc_int position; + sc_int parent; + sc_int openness; + sc_int state; + sc_bool seen; + sc_bool unmoved; + sc_bool static_unmoved; +}; +typedef sc_objectstate_s sc_objectstate_t; + +/* Task state structure, tracks task done, and if task scored. */ +struct sc_taskstate_s { + sc_bool done; + sc_bool scored; +}; + +typedef sc_taskstate_s sc_taskstate_t; + +/* Event state structure, holds event state, and timing information. */ +enum +{ ES_WAITING = 1, + ES_RUNNING = 2, ES_AWAITING = 3, ES_FINISHED = 4, ES_PAUSED = 5 +}; +struct sc_eventstate_s { + sc_int state; + sc_int time; +}; +typedef sc_eventstate_s sc_eventstate_t; + +/* + * NPC state structure, tracks the NPC location and position, any parent + * object, whether the NPC seen, and if the NPC walks, the count of walk + * steps and a steps array sized to this count. + */ +struct sc_npcstate_s { + sc_int location; + sc_int position; + sc_int parent; + sc_int walkstep_count; + sc_int *walksteps; + sc_bool seen; +}; +typedef sc_npcstate_s sc_npcstate_t; + +/* + * Resource tracking structure, holds the resource name, including any + * trailing "##" for looping sounds, its offset into the game file, and its + * length. Two resources are held -- active, and requested. The game main + * loop compares the two, and notifies the interface on a change. + */ +struct sc_resource_s { + const sc_char *name; + sc_int offset; + sc_int length; +}; +typedef sc_resource_s sc_resource_t; + +/* + * Overall game state structure. Arrays are malloc'ed for the appropriate + * number of each of the above state structures. + */ +struct sc_game_s { + sc_uint magic; + + /* References to assorted helper subsystems. */ + sc_var_setref_t vars; + sc_prop_setref_t bundle; + sc_filterref_t filter; + sc_memo_setref_t memento; + sc_debuggerref_t debugger; + + /* Undo information, also used by the debugger. */ + struct sc_game_s *temporary; + struct sc_game_s *undo; + sc_bool undo_available; + + /* Basic game state -- rooms, objects, and so on. */ + sc_int room_count; + sc_roomstate_t *rooms; + sc_int object_count; + sc_objectstate_t *objects; + sc_int task_count; + sc_taskstate_t *tasks; + sc_int event_count; + sc_eventstate_t *events; + sc_int npc_count; + sc_npcstate_t *npcs; + sc_int playerroom; + sc_int playerposition; + sc_int playerparent; + sc_int turns; + sc_int score; + sc_bool bold_room_names; + sc_bool verbose; + sc_bool notify_score_change; + sc_char *current_room_name; + sc_char *status_line; + sc_char *title; + sc_char *author; + sc_char *hint_text; + + /* Resource management data. */ + sc_resource_t requested_sound; + sc_resource_t requested_graphic; + sc_bool stop_sound; + sc_bool sound_active; + + sc_resource_t playing_sound; + sc_resource_t displayed_graphic; + + /* Game running and game completed flags. */ + sc_bool is_running; + sc_bool has_completed; + + /* Player's setting for waitturns; overrides the game's. */ + sc_int waitturns; + + /* Miscellaneous library and main loop conveniences. */ + sc_int waitcounter; + sc_bool has_notified; + sc_bool is_admin; + sc_bool do_again; + sc_int redo_sequence; + sc_bool do_restart; + sc_bool do_restore; + sc_bool *object_references; + sc_bool *multiple_references; + sc_bool *npc_references; + sc_int it_object; + sc_int him_npc; + sc_int her_npc; + sc_int it_npc; + + /* Longjump buffer for external requests to quit. */ + jmp_buf quitter; +}; +typedef sc_game_s sc_game_t; + +} // End of namespace Adrift +} // End of namespace Glk + +#endif diff --git a/engines/glk/adrift/scinterf.cpp b/engines/glk/adrift/scinterf.cpp new file mode 100644 index 0000000000..044c4d8cf8 --- /dev/null +++ b/engines/glk/adrift/scinterf.cpp @@ -0,0 +1,1192 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" +#include "common/file.h" +#include "common/system.h" +#include "common/savefile.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_char NEWLINE = '\n'; +static const sc_char CARRIAGE_RETURN = '\r'; +static const sc_char NUL = '\0'; + +/* Global tracing flags. */ +static sc_uint if_trace_flags = 0; + + +/* + * if_initialize() + * + * First-time runtime checks for the overall interpreter. This function + * tries to ensure correct compile options. + */ +static void +if_initialize (void) +{ + static sc_bool initialized = FALSE; + + /* Only do checks on the first call. */ + if (!initialized) + { + /* Make a few quick checks on types and type sizes. */ + if (sizeof (sc_byte) != 1 || sizeof (sc_char) != 1) + { + sc_error ("if_initialize: sizeof sc_byte or sc_char" + " is not 1, check compile options\n"); + } + else if (sizeof (sc_uint) < 4 || sizeof (sc_int) < 4) + { + sc_error ("if_initialize: sizeof sc_uint or sc_int" + " is not at least 4, check compile options\n"); + } + else if (sizeof (sc_uint) > 8 || sizeof (sc_int) > 8) + { + sc_error ("if_initialize: sizeof sc_uint or sc_int" + " is more than 8, check compile options\n"); + } + else if (!((sc_uint) -1 > 0)) + { + sc_error ("if_initialize: sc_uint appears not to be unsigned," + " check compile options\n"); + } + + initialized = TRUE; + } +} + + +/* + * if_bool() + * sc_set_trace_flags() + * if_get_trace_flag() + * + * Set and retrieve tracing flags. Setting new values propagates the new + * tracing setting to all modules that support it. + */ +static sc_bool +if_bool (sc_uint flag) +{ + return flag ? TRUE : FALSE; +} + +void +sc_set_trace_flags (sc_uint trace_flags) +{ + if_initialize (); + + /* Save the value for queries. */ + if_trace_flags = trace_flags; + + /* Propagate tracing to modules that support it. */ + parse_debug_trace (if_bool (trace_flags & SC_TRACE_PARSE)); + prop_debug_trace (if_bool (trace_flags & SC_TRACE_PROPERTIES)); + var_debug_trace (if_bool (trace_flags & SC_TRACE_VARIABLES)); + uip_debug_trace (if_bool (trace_flags & SC_TRACE_PARSER)); + lib_debug_trace (if_bool (trace_flags & SC_TRACE_LIBRARY)); + evt_debug_trace (if_bool (trace_flags & SC_TRACE_EVENTS)); + npc_debug_trace (if_bool (trace_flags & SC_TRACE_NPCS)); + obj_debug_trace (if_bool (trace_flags & SC_TRACE_OBJECTS)); + task_debug_trace (if_bool (trace_flags & SC_TRACE_TASKS)); + restr_debug_trace (if_bool (trace_flags & SC_TRACE_TASKS)); + pf_debug_trace (if_bool (trace_flags & SC_TRACE_PRINTFILTER)); +} + +sc_bool +if_get_trace_flag (sc_uint bitmask) +{ + return if_bool (if_trace_flags & bitmask); +} + + +/* + * if_print_string_common() + * if_print_string() + * if_print_debug() + * if_print_character_common() + * if_print_character() + * if_print_debug_character() + * if_print_tag() + * + * Call OS-specific print function for the given arguments. + */ +static void +if_print_string_common (const sc_char *string, + void (*print_string_function) (const sc_char *)) +{ + assert (string); + + if (string[0] != NUL) + print_string_function (string); +} + +void +if_print_string (const sc_char *string) +{ + if_print_string_common (string, os_print_string); +} + +void +if_print_debug (const sc_char *string) +{ + if_print_string_common (string, os_print_string_debug); +} + +static void +if_print_character_common (sc_char character, + void (*print_string_function) (const sc_char *)) +{ + if (character != NUL) + { + sc_char buffer[2]; + + buffer[0] = character; + buffer[1] = NUL; + print_string_function (buffer); + } +} + +void +if_print_character (sc_char character) +{ + if_print_character_common (character, os_print_string); +} + +void +if_print_debug_character (sc_char character) +{ + if_print_character_common (character, os_print_string_debug); +} + +void +if_print_tag (sc_int tag, const sc_char *arg) +{ + assert (arg); + + os_print_tag (tag, arg); +} + + +/* + * if_read_line_common() + * if_read_line() + * if_read_debug() + * + * Call OS-specific line read function. Clean up any read data a little + * before returning it to the caller. + */ +static void +if_read_line_common (sc_char *buffer, sc_int length, + sc_bool (*read_line_function) (sc_char *, sc_int)) +{ + sc_bool is_line_available; + sc_int last; + assert (buffer && length > 0); + + /* Loop until valid player input is available. */ + do + { + /* Space first with a blank line, and clear the buffer. */ + if_print_character ('\n'); + memset (buffer, NUL, length); + + is_line_available = read_line_function (buffer, length); + } + while (!is_line_available); + + /* Drop any trailing newline/return. */ + last = strlen (buffer) - 1; + while (last >= 0 + && (buffer[last] == CARRIAGE_RETURN || buffer[last] == NEWLINE)) + buffer[last--] = NUL; +} + +void +if_read_line (sc_char *buffer, sc_int length) +{ + if_read_line_common (buffer, length, os_read_line); +} + +void +if_read_debug (sc_char *buffer, sc_int length) +{ + if_read_line_common (buffer, length, os_read_line_debug); +} + + +/* + * if_confirm() + * + * Call OS-specific confirm function. + */ +sc_bool +if_confirm (sc_int type) +{ + return os_confirm (type); +} + + +/* + * if_open_saved_game() + * if_write_saved_game() + * if_read_saved_game() + * if_close_saved_game() + * + * Call OS-specific functions for saving and restoring games. + */ +void * +if_open_saved_game (sc_bool is_save) +{ + return os_open_file (is_save); +} + +void +if_write_saved_game (void *opaque, const sc_byte *buffer, sc_int length) +{ + assert (buffer); + + os_write_file (opaque, buffer, length); +} + +sc_int +if_read_saved_game (void *opaque, sc_byte *buffer, sc_int length) +{ + assert (buffer); + + return os_read_file (opaque, buffer, length); +} + +void +if_close_saved_game (void *opaque) +{ + os_close_file (opaque); +} + + +/* + * if_display_hints() + * + * Call OS-specific hint display function. + */ +void +if_display_hints (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + os_display_hints ((sc_game) game); +} + + +/* + * if_update_sound() + * if_update_graphic() + * + * Call OS-specific sound and graphic handler functions. + */ +void +if_update_sound (const sc_char *filename, + sc_int sound_offset, sc_int sound_length, + sc_bool is_looping) +{ + if (strlen (filename) > 0) + os_play_sound (filename, sound_offset, sound_length, is_looping); + else + os_stop_sound (); +} + +void +if_update_graphic (const sc_char *filename, + sc_int graphic_offset, sc_int graphic_length) +{ + os_show_graphic (filename, graphic_offset, graphic_length); +} + + +/* + * sc_scare_version() + * sc_scare_emulation() + * + * Return a version string and Adrift emulation level. + */ +const sc_char * +sc_scare_version (void) +{ + if_initialize (); + return "SCARE " SCARE_VERSION SCARE_PATCH_LEVEL; +} + +sc_int +sc_scare_emulation (void) +{ + if_initialize (); + return SCARE_EMULATION; +} + + +/* + * if_file_read_callback() + * if_file_write_callback() + * + * Standard FILE* reader and writer callback for constructing callback-style + * calls from filename and stream variants. + */ +static sc_int +if_file_read_callback (void *opaque, sc_byte *buffer, sc_int length) { + Common::SeekableReadStream *stream = (Common::SeekableReadStream *)opaque; + sc_int bytes; + + bytes = stream->read(buffer, length); + if (stream->err()) + sc_error ("if_file_read_callback: warning: read error\n"); + + return bytes; +} + +static void if_file_write_callback (void *opaque, const sc_byte *buffer, sc_int length) +{ + Common::WriteStream *stream = (Common::WriteStream *) opaque; + + stream->write(buffer, length); + if (stream->err()) + sc_error ("if_file_write_callback: warning: write error\n"); +} + + +/* + * sc_game_from_filename() + * sc_game_from_stream() + * sc_game_from_callback() + * + * Called by the OS-specific layer to create a run context. The _filename() + * and _stream() variants are adapters for run_create(). + */ +sc_game +sc_game_from_filename (const sc_char *filename) { + Common::File *stream; + sc_game game; + + if_initialize (); + if (!filename) + { + sc_error ("sc_game_from_filename: nullptr filename\n"); + return nullptr; + } + + stream = new Common::File(); + if (!stream->open(filename)) { + delete stream; + sc_error ("sc_game_from_filename: fopen error\n"); + return nullptr; + } + + game = run_create (if_file_read_callback, stream); + delete stream; + + return game; +} + +sc_game sc_game_from_stream (Common::SeekableReadStream *stream) { + if_initialize (); + if (!stream) + { + sc_error ("sc_game_from_stream: nullptr stream\n"); + return nullptr; + } + + return run_create (if_file_read_callback, stream); +} + +sc_game +sc_game_from_callback (sc_int (*callback) (void *, sc_byte *, sc_int), + void *opaque) +{ + if_initialize (); + if (!callback) + { + sc_error ("sc_game_from_callback: nullptr callback\n"); + return nullptr; + } + + return run_create (callback, opaque); +} + + +/* + * if_game_error() + * + * Common function to verify that the game passed in to functions below + * is a valid game. Returns TRUE on game error, FALSE if okay. + */ +static sc_bool +if_game_error (sc_gameref_t game, const sc_char *function_name) +{ + /* Check for invalid game -- null pointer or bad magic. */ + if (!gs_is_game_valid (game)) + { + if (game) + sc_error ("%s: invalid game\n", function_name); + else + sc_error ("%s: nullptr game\n", function_name); + return TRUE; + } + + /* No game error. */ + return FALSE; +} + + +/* + * sc_interpret_game() + * sc_restart_game() + * sc_save_game() + * sc_load_game() + * sc_undo_game_turn() + * sc_quit_game() + * + * Called by the OS-specific layer to run a game loaded into a run context, + * and to quit the interpreter on demand, if required. sc_quit_game() + * is implemented as a longjmp(), so never returns to the caller -- + * instead, the program behaves as if sc_interpret_game() had returned. + * sc_load_game() will longjmp() if the restore is successful (thus + * behaving like sc_restart_game()), but will return if the game could not + * be restored. sc_undo_game_turn() behaves like sc_load_game(). + */ +void +sc_interpret_game (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_interpret_game")) + return; + + run_interpret (game_); +} + +void +sc_restart_game (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_restart_game")) + return; + + run_restart (game_); +} + +sc_bool +sc_save_game (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_save_game")) + return FALSE; + + return run_save_prompted (game_); +} + +sc_bool +sc_load_game (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_load_game")) + return FALSE; + + return run_restore_prompted (game_); +} + +sc_bool +sc_undo_game_turn (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_undo_game_turn")) + return FALSE; + + return run_undo (game_); +} + +void +sc_quit_game (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_quit_game")) + return; + + run_quit (game_); +} + + +/* + * sc_save_game_to_filename() + * sc_save_game_to_stream() + * sc_save_game_to_callback() + * sc_load_game_from_filename() + * sc_load_game_from_stream() + * sc_load_game_from_callback() + * + * Low level game saving and loading functions. The normal sc_save_game() + * and sc_load_game() functions act exactly as the "save" and "restore" + * game commands, in that they prompt the user for a stream to write or read. + * These alternative forms allow the caller to directly specify the data + * streams. + */ +sc_bool +sc_save_game_to_filename (sc_game game, const sc_char *filename) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + Common::OutSaveFile *sf; + + if (if_game_error (game_, "sc_save_game_to_filename")) + return FALSE; + + if (!filename) + { + sc_error ("sc_save_game_to_filename: nullptr filename\n"); + return FALSE; + } + + sf = g_system->getSavefileManager()->openForSaving(filename); + if (!sf) { + sc_error ("sc_save_game_to_filename: fopen error\n"); + return FALSE; + } + + run_save(game_, if_file_write_callback, sf); + sf->finalize(); + delete sf; + + return TRUE; +} + +void +sc_save_game_to_stream (sc_game game, Common::SeekableReadStream *stream) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_save_game_to_stream")) + return; + + if (!stream) + { + sc_error ("sc_save_game_to_stream: nullptr stream\n"); + return; + } + + run_save (game_, if_file_write_callback, stream); +} + +void +sc_save_game_to_callback (sc_game game, + void (*callback) (void *, const sc_byte *, sc_int), + void *opaque) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_save_game_to_callback")) + return; + + if (!callback) + { + sc_error ("sc_save_game_to_callback: nullptr callback\n"); + return; + } + + run_save (game_, callback, opaque); +} + +sc_bool +sc_load_game_from_filename (sc_game game, const sc_char *filename) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + Common::InSaveFile *sf; + sc_bool status; + + if (if_game_error (game_, "sc_load_game_from_filename")) + return FALSE; + + if (!filename) + { + sc_error ("sc_load_game_from_filename: nullptr filename\n"); + return FALSE; + } + + sf = g_system->getSavefileManager()->openForLoading(filename); + if (!sf) + { + sc_error ("sc_load_game_from_filename: fopen error\n"); + return FALSE; + } + + status = run_restore (game_, if_file_read_callback, sf); + delete sf; + + return status; +} + +sc_bool +sc_load_game_from_stream (sc_game game, Common::SeekableReadStream *stream) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_load_game_from_stream")) + return FALSE; + + if (!stream) + { + sc_error ("sc_load_game_from_stream: nullptr stream\n"); + return FALSE; + } + + return run_restore (game_, if_file_read_callback, stream); +} + +sc_bool +sc_load_game_from_callback (sc_game game, + sc_int (*callback) (void *, sc_byte *, sc_int), + void *opaque) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_load_game_from_callback")) + return FALSE; + + if (!callback) + { + sc_error ("sc_load_game_from_callback: nullptr callback\n"); + return FALSE; + } + + return run_restore (game_, callback, opaque); +} + + +/* + * sc_free_game() + * + * Called by the OS-specific layer to free run context memory. + */ +void +sc_free_game (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_free_game")) + return; + + run_destroy (game_); +} + + +/* + * sc_is_game_running() + * sc_get_game_name() + * sc_get_game_author() + * sc_get_game_compile_date() + * sc_get_game_turns() + * sc_get_game_score() + * sc_get_game_max_score() + * sc_get_game_room () + * sc_get_game_status_line () + * sc_get_game_preferred_font () + * sc_get_game_bold_room_names() + * sc_get_game_verbose() + * sc_get_game_notify_score_change() + * sc_has_game_completed() + * sc_is_game_undo_available() + * + * Return a few attributes of a game. + */ +sc_bool +sc_is_game_running (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_is_game_running")) + return FALSE; + + return run_is_running (game_); +} + +const sc_char * +sc_get_game_name (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_char *retval; + + if (if_game_error (game_, "sc_get_game_name")) + return "[invalid game]"; + + run_get_attributes (game_, &retval, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr); + return retval; +} + +const sc_char * +sc_get_game_author (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_char *retval; + + if (if_game_error (game_, "sc_get_game_author")) + return "[invalid game]"; + + run_get_attributes (game_, nullptr, &retval, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr); + return retval; +} + +const sc_char * +sc_get_game_compile_date (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_char *retval; + + if (if_game_error (game_, "sc_get_game_compile_date")) + return "[invalid game]"; + + run_get_attributes (game_, nullptr, nullptr, &retval, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + return retval; +} + +sc_int +sc_get_game_turns (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_int retval; + + if (if_game_error (game_, "sc_get_game_turns")) + return 0; + + run_get_attributes (game_, nullptr, nullptr, nullptr, &retval, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + return retval; +} + +sc_int +sc_get_game_score (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_int retval; + + if (if_game_error (game_, "sc_get_game_score")) + return 0; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, &retval, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + return retval; +} + +sc_int +sc_get_game_max_score (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_int retval; + + if (if_game_error (game_, "sc_get_game_max_score")) + return 0; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, &retval, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + return retval; +} + +const sc_char * +sc_get_game_room (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_char *retval; + + if (if_game_error (game_, "sc_get_game_room")) + return "[invalid game]"; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &retval, + nullptr, nullptr, nullptr, nullptr, nullptr); + return retval; +} + +const sc_char * +sc_get_game_status_line (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_char *retval; + + if (if_game_error (game_, "sc_get_game_status_line")) + return "[invalid game]"; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + &retval, nullptr, nullptr, nullptr, nullptr); + return retval; +} + +const sc_char * +sc_get_game_preferred_font (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_char *retval; + + if (if_game_error (game_, "sc_get_game_preferred_font")) + return "[invalid game]"; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, &retval, nullptr, nullptr, nullptr); + return retval; +} + +sc_bool +sc_get_game_bold_room_names (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_bool retval; + + if (if_game_error (game_, "sc_get_game_bold_room_names")) + return FALSE; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, &retval, nullptr, nullptr); + return retval; +} + +sc_bool +sc_get_game_verbose (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_bool retval; + + if (if_game_error (game_, "sc_get_game_verbose")) + return FALSE; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, &retval, nullptr); + return retval; +} + +sc_bool +sc_get_game_notify_score_change (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_bool retval; + + if (if_game_error (game_, "sc_get_game_notify_score_change")) + return FALSE; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, &retval); + return retval; +} + +sc_bool +sc_has_game_completed (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_has_game_completed")) + return FALSE; + + return run_has_completed (game_); +} + +sc_bool +sc_is_game_undo_available (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_is_game_undo_available")) + return FALSE; + + return run_is_undo_available (game_); +} + + +/* + * sc_set_game_bold_room_names() + * sc_set_game_verbose() + * sc_set_game_notify_score_change() + * + * Set a few attributes of a game. + */ +void +sc_set_game_bold_room_names (sc_game game, sc_bool flag) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_bool bold, verbose, notify; + + if (if_game_error (game_, "sc_set_game_bold_room_names")) + return; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, &bold, &verbose, ¬ify); + run_set_attributes (game_, flag, verbose, notify); +} + +void +sc_set_game_verbose (sc_game game, sc_bool flag) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_bool bold, verbose, notify; + + if (if_game_error (game_, "sc_set_game_verbose")) + return; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, &bold, &verbose, ¬ify); + run_set_attributes (game_, bold, flag, notify); +} + +void +sc_set_game_notify_score_change (sc_game game, sc_bool flag) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + sc_bool bold, verbose, notify; + + if (if_game_error (game_, "sc_set_game_notify_score_change")) + return; + + run_get_attributes (game_, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, &bold, &verbose, ¬ify); + run_set_attributes (game_, bold, verbose, flag); +} + + +/* + * sc_does_game_use_sounds() + * sc_does_game_use_graphics() + * + * Indicate the game's use of resources. + */ +sc_bool +sc_does_game_use_sounds (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_does_game_use_sounds")) + return FALSE; + + return res_has_sound (game_); +} + +sc_bool +sc_does_game_use_graphics (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_does_game_use_graphics")) + return FALSE; + + return res_has_graphics (game_); +} + + +/* + * sc_get_first_game_hint() + * sc_get_next_game_hint() + * sc_get_game_hint_question() + * sc_get_game_subtle_hint() + * sc_get_game_sledgehammer_hint() + * + * Iterate currently available hints, and return strings for a hint. + */ +sc_game_hint +sc_get_first_game_hint (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_get_first_game_hint")) + return nullptr; + + return run_hint_iterate (game_, nullptr); +} + +sc_game_hint +sc_get_next_game_hint (sc_game game, sc_game_hint hint) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_hintref_t hint_ = (const sc_hintref_t)hint; + + if (if_game_error (game_, "sc_get_next_game_hint")) + return nullptr; + if (!hint_) + { + sc_error ("sc_get_next_game_hint: nullptr hint\n"); + return nullptr; + } + + return run_hint_iterate (game_, hint_); +} + +const sc_char * +sc_get_game_hint_question (sc_game game, sc_game_hint hint) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_hintref_t hint_ = (const sc_hintref_t)hint; + + if (if_game_error (game_, "sc_get_game_hint_question")) + return nullptr; + if (!hint_) + { + sc_error ("sc_get_game_hint_question: nullptr hint\n"); + return nullptr; + } + + return run_get_hint_question (game_, hint_); +} + +const sc_char * +sc_get_game_subtle_hint (sc_game game, sc_game_hint hint) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_hintref_t hint_ = (const sc_hintref_t)hint; + + if (if_game_error (game_, "sc_get_game_subtle_hint")) + return nullptr; + if (!hint_) + { + sc_error ("sc_get_game_subtle_hint: nullptr hint\n"); + return nullptr; + } + + return run_get_subtle_hint (game_, hint_); +} + +const sc_char * +sc_get_game_unsubtle_hint (sc_game game, sc_game_hint hint) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + const sc_hintref_t hint_ = (const sc_hintref_t)hint; + + if (if_game_error (game_, "sc_get_game_unsubtle_hint")) + return nullptr; + if (!hint_) + { + sc_error ("sc_get_game_unsubtle_hint: nullptr hint\n"); + return nullptr; + } + + return run_get_unsubtle_hint (game_, hint_); +} + + +/* + * sc_set_game_debugger_enabled() + * sc_is_game_debugger_enabled() + * sc_run_game_debugger_command() + * + * Enable, disable, and query game debugging, and run a single debug command. + */ +void +sc_set_game_debugger_enabled (sc_game game, sc_bool flag) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_set_game_debugger_enabled")) + return; + + debug_set_enabled (game_, flag); +} + +sc_bool +sc_get_game_debugger_enabled (sc_game game) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_get_game_debugger_enabled")) + return FALSE; + + return debug_get_enabled (game_); +} + +sc_bool +sc_run_game_debugger_command (sc_game game, const sc_char *debug_command) +{ + const sc_gameref_t game_ = (const sc_gameref_t)game; + + if (if_game_error (game_, "sc_run_game_debugger_command")) + return FALSE; + + return debug_run_command (game_, debug_command); +} + + +/* + * sc_set_locale() + * sc_get_locale() + * + * Set the interpreter locale, and get the currently set locale. + */ +sc_bool +sc_set_locale (const sc_char *name) +{ + if (!name) + { + sc_error ("sc_set_locale: nullptr name\n"); + return FALSE; + } + + return loc_set_locale (name); +} + +const sc_char * +sc_get_locale (void) +{ + return loc_get_locale (); +} + + +/* + * sc_set_portable_random() + * sc_reseed_random_sequence() + * + * Turn portable random number generation on and off, and supply a new seed + * for random number generators. + */ +void +sc_set_portable_random (sc_bool flag) +{ + if (flag) + sc_set_congruential_random (); + else + sc_set_platform_random (); +} + +void +sc_reseed_random_sequence (sc_uint new_seed) +{ + if (new_seed == 0) + { + sc_error ("sc_reseed_random_sequence: new_seed may not be 0\n"); + return; + } + + sc_seed_random (new_seed); +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sclibrar.cpp b/engines/glk/adrift/sclibrar.cpp new file mode 100644 index 0000000000..8f97a7c05a --- /dev/null +++ b/engines/glk/adrift/sclibrar.cpp @@ -0,0 +1,10983 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Ensure module messages precisely match the real Runner ones. This + * matters for ALRs. + * + * o Capacity checks on the player and on containers are implemented, but + * may not be right. + */ + +/* Assorted definitions and constants. */ +static const sc_char NUL = '\0'; +static const sc_char COMMA = ','; +enum +{ SECS_PER_MINUTE = 60, + MINS_PER_HOUR = 60, + SECS_PER_HOUR = 3600 +}; +enum { LIB_ALLOCATION_AVOIDANCE_SIZE = 128 }; + +/* Trace flag, set before running. */ +static sc_bool lib_trace = FALSE; + + +/* + * lib_warn_battle_system() + * + * Display a warning when the battle system is detected in a game. Print + * directly rather than using the printfilter to avoid possible clashes + * with ALRs. + */ +void +lib_warn_battle_system (void) +{ + if_print_tag (SC_TAG_FONT, "size=16"); + if_print_string ("SCARE WARNING"); + if_print_tag (SC_TAG_ENDFONT, ""); + + if_print_string ( + "\n\nThe game uses Adrift's Battle System, something not fully supported" + " by this release of SCARE.\n\n"); + + if_print_string ( + "SCARE will still run the game, but it will not create character" + " battles where they would normally occur. For some games, this may" + " be perfectly okay, as the Battle System is sometimes turned on" + " by accident in a game, but never actually used. For others, though," + " the omission of this feature may be more serious.\n\n"); + + if_print_string ("Please press a key to continue...\n\n"); + if_print_tag (SC_TAG_WAITKEY, ""); +} + + +/* + * lib_random_roomgroup_member() + * + * Return a random member of a roomgroup. + */ +sc_int +lib_random_roomgroup_member (sc_gameref_t game, sc_int roomgroup) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int count, room; + + /* Get the count of rooms in the group. */ + vt_key[0].string = "RoomGroups"; + vt_key[1].integer = roomgroup; + vt_key[2].string = "List2"; + count = prop_get_child_count (bundle, "I<-sis", vt_key); + if (count == 0) + { + sc_fatal ("lib_random_roomgroup_member:" + " no rooms in group %ld\n", roomgroup); + } + + /* Pick a room at random and return it. */ + vt_key[3].integer = sc_randomint (0, count - 1); + room = prop_get_integer (bundle, "I<-sisi", vt_key); + + if (lib_trace) + { + sc_trace ("Library: random room for group %ld is %ld\n", + roomgroup, room); + } + + return room; +} + + +/* + * lib_use_room_alt() + * + * Return TRUE if a particular alternate room description should be used. + */ +static sc_bool +lib_use_room_alt (sc_gameref_t game, sc_int room, sc_int alt) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int type; + sc_bool retval; + + /* Get alternate type. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Alts"; + vt_key[3].integer = alt; + vt_key[4].string = "Type"; + type = prop_get_integer (bundle, "I<-sisis", vt_key); + + /* Select based on type. */ + retval = FALSE; + switch (type) + { + case 0: /* Task. */ + { + sc_int var2, var3; + + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + if (var2 == 0) /* No task. */ + retval = TRUE; + else + { + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + + retval = gs_task_done (game, var2 - 1) == !(var3 != 0); + } + break; + } + + case 1: /* Stateful object. */ + { + sc_int var2, var3, object; + + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + if (var2 == 0) /* No object. */ + retval = TRUE; + else + { + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + + object = obj_stateful_index (game, var2 - 1); + retval = restr_pass_task_object_state (game, object + 1, var3 - 1); + } + break; + } + + case 2: /* Player condition. */ + { + sc_int var2, var3, object; + + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + + if (var3 == 0) + { + switch (var2) + { + case 0: case 2: case 5: + retval = TRUE; + break; + case 1: case 3: case 4: + retval = FALSE; + break; + default: + sc_fatal ("lib_use_room_alt:" + " invalid player condition, %ld\n", var2); + } + break; + } + + if (var2 == 2 || var2 == 3) + object = obj_wearable_object (game, var3 - 1); + else + object = obj_dynamic_object (game, var3 - 1); + + switch (var2) + { + case 0: /* Isn't holding (or wearing). */ + retval = gs_object_position (game, object) != OBJ_HELD_PLAYER + && gs_object_position (game, object) != OBJ_WORN_PLAYER; + break; + case 1: /* Is holding (or wearing). */ + retval = gs_object_position (game, object) == OBJ_HELD_PLAYER + || gs_object_position (game, object) == OBJ_WORN_PLAYER; + break; + case 2: /* Isn't wearing. */ + retval = gs_object_position (game, object) != OBJ_WORN_PLAYER; + break; + case 3: /* Is wearing. */ + retval = gs_object_position (game, object) == OBJ_WORN_PLAYER; + break; + case 4: /* Isn't in the same room as. */ + retval = !obj_indirectly_in_room (game, + object, gs_playerroom (game)); + break; + case 5: /* Is in the same room as. */ + retval = obj_indirectly_in_room (game, + object, gs_playerroom (game)); + break; + default: + sc_fatal ("lib_use_room_alt:" + " invalid player condition, %ld\n", var2); + } + break; + } + + default: + sc_fatal ("lib_use_room_alt: invalid type, %ld\n", type); + } + + return retval; +} + + +/* + * lib_find_starting_alt() + * + * Return the alt index for the alt at which we need to start running down + * the alts list when generating room names or descriptions. Returns -1 if + * no alt overrides the default room long description. + */ +static sc_int +lib_find_starting_alt (sc_gameref_t game, sc_int room) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int alt_count, alt, retval; + + /* Get count of room alternates. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Alts"; + alt_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + /* Search backwards for a method-0 or method-1 overriding description. */ + retval = -1; + for (alt = alt_count - 1; alt >= 0; alt--) + { + sc_int method; + + vt_key[3].integer = alt; + vt_key[4].string = "DisplayRoom"; + method = prop_get_integer (bundle, "I<-sisis", vt_key); + + if (!(method == 0 || method == 1)) + continue; + + if (lib_use_room_alt (game, room, alt)) + { + const sc_char *m1; + + vt_key[3].integer = alt; + vt_key[4].string = "M1"; + m1 = prop_get_string (bundle, "S<-sisis", vt_key); + if (!sc_strempty (m1)) + { + retval = alt; + break; + } + } + else + { + const sc_char *m2; + + vt_key[3].integer = alt; + vt_key[4].string = "M2"; + m2 = prop_get_string (bundle, "S<-sisis", vt_key); + if (!sc_strempty (m2)) + { + retval = alt; + break; + } + } + } + + /* Return the index of the base alt, or -1 if none found. */ + return retval; +} + + +/* + * lib_get_room_name() + * lib_print_room_name() + * + * Get/print out the name for a given room. + */ +const sc_char * +lib_get_room_name (sc_gameref_t game, sc_int room) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int alt_count, alt, start; + const sc_char *name; + + /* Get the basic room name, and the count of room alternates. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + vt_key[2].string = "Alts"; + alt_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + /* Get our starting point in the alts list. */ + start = lib_find_starting_alt (game, room); + + /* + * Run forwards through all alts lower than our starting point, or all alts + * if no starting point found. + */ + for (alt = (start != -1) ? start : 0; alt < alt_count; alt++) + { + /* Ignore all non-method-2 alts except for the starter. */ + if (alt != start) + { + sc_int method; + + vt_key[3].integer = alt; + vt_key[4].string = "DisplayRoom"; + method = prop_get_integer (bundle, "I<-sisis", vt_key); + + if (method != 2) + continue; + } + + /* If this alt offers a name change, note it and continue. */ + if (lib_use_room_alt (game, room, alt)) + { + const sc_char *changed; + + vt_key[3].integer = alt; + vt_key[4].string = "Changed"; + changed = prop_get_string (bundle, "S<-sisis", vt_key); + if (!sc_strempty (changed)) + name = changed; + } + } + + /* Return the final selected name. */ + return name; +} + +void +lib_print_room_name (sc_gameref_t game, sc_int room) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_char *name; + + /* Print the room name, possibly in bold. */ + name = lib_get_room_name (game, room); + if (game->bold_room_names) + { + pf_buffer_tag (filter, SC_TAG_BOLD); + pf_buffer_string (filter, name); + pf_buffer_tag (filter, SC_TAG_ENDBOLD); + } + else + pf_buffer_string (filter, name); + pf_buffer_character (filter, '\n'); +} + + +/* + * lib_print_object_np + * lib_print_object + * + * Convenience functions to print out an object's name, with a "normalized" + * prefix -- any "a"/"an"/"some" is replaced by "the" -- and with the full + * prefix. + */ +static void +lib_print_object_np (sc_gameref_t game, sc_int object) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *prefix, *normalized, *name; + + /* Get the object's prefix. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + + /* + * Normalize by skipping any leading "a"/"an"/"some", replacing it instead + * with "the", and skipping any odd "the" already present. If no prefix at + * all, add a "the " anyway. + * + * TODO This is empirical, based on observed Adrift Runner behavior, and + * what it's _really_ supposed to do is a mystery. This routine has been a + * real PITA. + */ + normalized = prefix; + if (sc_compare_word (prefix, "a", 1)) + { + normalized = prefix + 1; + pf_buffer_string (filter, "the"); + } + else if (sc_compare_word (prefix, "an", 2)) + { + normalized = prefix + 2; + pf_buffer_string (filter, "the"); + } + else if (sc_compare_word (prefix, "the", 3)) + { + normalized = prefix + 3; + pf_buffer_string (filter, "the"); + } + else if (sc_compare_word (prefix, "some", 4)) + { + normalized = prefix + 4; + pf_buffer_string (filter, "the"); + } + else if (sc_strempty (prefix)) + pf_buffer_string (filter, "the "); + + /* + * If the remaining normalized prefix isn't empty, print it, and a space. + * If it is, then consider adding a space to any "the" printed above, except + * for the one done for empty prefixes, that is. + */ + if (!sc_strempty (normalized)) + { + pf_buffer_string (filter, normalized); + pf_buffer_character (filter, ' '); + } + else if (normalized > prefix) + pf_buffer_character (filter, ' '); + + /* + * Print the object's name; here we also look for a leading article and + * strip if found -- some games may avoid prefix and do this instead. + */ + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + if (sc_compare_word (name, "a", 1)) + name += 1; + else if (sc_compare_word (name, "an", 2)) + name += 2; + else if (sc_compare_word (name, "the", 3)) + name += 3; + else if (sc_compare_word (name, "some", 4)) + name += 4; + pf_buffer_string (filter, name); +} + +static void +lib_print_object (sc_gameref_t game, sc_int object) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *prefix, *name; + + /* + * Get the object's prefix, and print if not empty, otherwise default to an + * "a " prefix, as that's what Adrift seems to do. + */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (prefix)) + { + pf_buffer_string (filter, prefix); + pf_buffer_character (filter, ' '); + } + else + pf_buffer_string (filter, "a "); + + /* Print object name. */ + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + pf_buffer_string (filter, name); +} + + +/* + * lib_print_npc_np + * lib_print_npc + * + * Convenience functions to print out an NPC's name, with and without + * any prefix. + */ +static void +lib_print_npc_np (sc_gameref_t game, sc_int npc) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *name; + + /* Get the NPC's short description, and print it. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + pf_buffer_string (filter, name); +} + +#if 0 +static void +lib_print_npc (sc_gameref_t game, sc_int npc) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *prefix; + + /* Get the NPC's prefix. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + + /* If the prefix isn't empty, print it, then print NPC name. */ + if (!sc_strempty (prefix)) + { + pf_buffer_string (filter, prefix); + pf_buffer_character (filter, ' '); + } + lib_print_npc_np (game, npc); +} +#endif + + +/* + * lib_select_response() + * lib_select_plurality() + * + * Convenience functions for multiple handlers. Returns the appropriate + * response string for a game, based on perspective or object plurality. + */ +static const sc_char * +lib_select_response (sc_gameref_t game, + const sc_char *second_person, + const sc_char *first_person, + const sc_char *third_person) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_int perspective; + const sc_char *response; + + /* Return the response appropriate for Perspective. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "Perspective"; + perspective = prop_get_integer (bundle, "I<-ss", vt_key); + switch (perspective) + { + case LIB_FIRST_PERSON: + response = first_person; + break; + case LIB_SECOND_PERSON: + response = second_person; + break; + case LIB_THIRD_PERSON: + response = third_person; + break; + default: + sc_error ("lib_select_response:" + " unknown perspective, %ld\n", perspective); + response = second_person; + break; + } + + return response; +} + +static const sc_char * +lib_select_plurality (sc_gameref_t game, sc_int object, + const sc_char *singular, const sc_char *plural) +{ + return obj_appears_plural (game, object) ? plural : singular; +} + + +/* + * lib_get_npc_inroom_text() + * + * Returns the inroom description to be use for an NPC; if the NPC has + * gone walkabout and offers a changed description, return that; otherwise + * return the standard inroom text. + */ +static const sc_char * +lib_get_npc_inroom_text (sc_gameref_t game, sc_int npc) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int walk_count, walk; + const sc_char *inroomtext; + + /* Get the count of NPC walks. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Walks"; + walk_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + /* Check for any active walk with a description, return if found. */ + for (walk = walk_count - 1; walk >= 0; walk--) + { + if (gs_npc_walkstep (game, npc, walk) > 0) + { + const sc_char *changeddesc; + + /* Get and check any walk active description. */ + vt_key[3].integer = walk; + vt_key[4].string = "ChangedDesc"; + changeddesc = prop_get_string (bundle, "S<-sisis", vt_key); + if (!sc_strempty (changeddesc)) + return changeddesc; + } + } + + /* Return the standard inroom text. */ + vt_key[2].string = "InRoomText"; + inroomtext = prop_get_string (bundle, "S<-sis", vt_key); + return inroomtext; +} + + +/* + * lib_print_room_contents() + * + * Print a list of the contents of a room. + */ +static void +lib_print_room_contents (sc_gameref_t game, sc_int room) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int object, npc, count, trail; + + /* List all objects that show their initial description. */ + count = 0; + for (object = 0; object < gs_object_count (game); object++) + { + if (obj_directly_in_room (game, object, room) + && obj_shows_initial_description (game, object)) + { + const sc_char *inroomdesc; + + /* Find and print in room description. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "InRoomDesc"; + inroomdesc = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (inroomdesc)) + { + if (count == 0) + pf_buffer_character (filter, '\n'); + else + pf_buffer_string (filter, " "); + pf_buffer_string (filter, inroomdesc); + count++; + } + } + } + if (count > 0) + pf_buffer_character (filter, '\n'); + + /* + * List dynamic objects directly located in the room, and not already listed + * above since they lack, or suppress, an in room description. + * + * If an object sets ListFlag, then if dynamic it's suppressed from the list + * where it would normally be included, but if static it's included where it + * would normally be excluded. + */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + if (obj_directly_in_room (game, object, room)) + { + const sc_char *inroomdesc; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "InRoomDesc"; + inroomdesc = prop_get_string (bundle, "S<-sis", vt_key); + + if (!obj_shows_initial_description (game, object) + || sc_strempty (inroomdesc)) + { + sc_bool listflag; + + vt_key[2].string = "ListFlag"; + listflag = prop_get_boolean (bundle, "B<-sis", vt_key); + + if (listflag == obj_is_static (game, object)) + { + if (count > 0) + { + if (count == 1) + pf_buffer_string (filter, + lib_select_plurality (game, trail, + "\nAlso here is ", + "\nAlso here are ")); + else + pf_buffer_string (filter, ", "); + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + } + } + if (count >= 1) + { + if (count == 1) + pf_buffer_string (filter, + lib_select_plurality (game, trail, + "\nAlso here is ", + "\nAlso here are ")); + else + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + pf_buffer_string (filter, ".\n"); + } + + /* List NPCs directly in the room that have an in room description. */ + count = 0; + for (npc = 0; npc < gs_npc_count (game); npc++) + { + if (npc_in_room (game, npc, room)) + { + const sc_char *description; + + /* Print any non='#' in-room description. */ + description = lib_get_npc_inroom_text (game, npc); + if (!sc_strempty (description) && sc_strcasecmp (description, "#")) + { + if (count == 0) + pf_buffer_character (filter, '\n'); + else + pf_buffer_string (filter, " "); + pf_buffer_string (filter, description); + count++; + } + } + } + if (count > 0) + pf_buffer_character (filter, '\n'); + + /* + * List NPCs in the room that don't have an in room description and that + * request a default "...is here" with "#". + * + * TODO Is this right? + */ + count = 0; + trail = -1; + for (npc = 0; npc < gs_npc_count (game); npc++) + { + if (npc_in_room (game, npc, room)) + { + const sc_char *description; + + /* Print name for descriptions marked '#'. */ + description = lib_get_npc_inroom_text (game, npc); + if (!sc_strempty (description) && !sc_strcasecmp (description, "#")) + { + if (count > 0) + { + if (count > 1) + pf_buffer_string (filter, ", "); + else + { + pf_buffer_character (filter, '\n'); + pf_new_sentence (filter); + } + lib_print_npc_np (game, trail); + } + trail = npc; + count++; + } + } + } + if (count >= 1) + { + if (count == 1) + { + pf_buffer_character (filter, '\n'); + pf_new_sentence (filter); + lib_print_npc_np (game, trail); + pf_buffer_string (filter, " is here"); + } + else + { + pf_buffer_string (filter, " and "); + lib_print_npc_np (game, trail); + pf_buffer_string (filter, " are here"); + } + pf_buffer_string (filter, ".\n"); + } +} + + +/* + * lib_print_room_description() + * + * Print out the long description for a given room. + */ +void +lib_print_room_description (sc_gameref_t game, sc_int room) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_bool showobjects, is_described, is_suppressed; + sc_int alt_count, alt, start, event; + + /* Get count of room alternates. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Alts"; + alt_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + /* Start with no description, and get our starting point in the alts list. */ + is_described = FALSE; + start = lib_find_starting_alt (game, room); + + /* Print the standard description unless a start alt indicates not. */ + if (start == -1) + is_suppressed = FALSE; + else + { + sc_int method; + + vt_key[3].integer = start; + vt_key[4].string = "DisplayRoom"; + method = prop_get_integer (bundle, "I<-sisis", vt_key); + + is_suppressed = (method == 0); + } + if (!is_suppressed) + { + const sc_char *description; + + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Long"; + description = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (description)) + { + pf_buffer_string (filter, description); + is_described = TRUE; + } + + vt_key[2].string = "Res"; + res_handle_resource (game, "sis", vt_key); + } + + /* Ensure that we're back to handling room alts. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Alts"; + + /* + * Run forwards through all alts lower than our starting point, or all alts + * if no starting point overrider found. + */ + showobjects = TRUE; + for (alt = (start != -1) ? start : 0; alt < alt_count; alt++) + { + /* Ignore all non-method-2 alts except for the starter. */ + if (alt != start) + { + sc_int method; + + vt_key[3].integer = alt; + vt_key[4].string = "DisplayRoom"; + method = prop_get_integer (bundle, "I<-sisis", vt_key); + + if (method != 2) + continue; + } + + if (lib_use_room_alt (game, room, alt)) + { + const sc_char *m1; + sc_int hideobjects; + + vt_key[3].integer = alt; + vt_key[4].string = "M1"; + m1 = prop_get_string (bundle, "S<-sisis", vt_key); + if (!sc_strempty (m1)) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, m1); + is_described = TRUE; + } + + vt_key[4].string = "Res1"; + res_handle_resource (game, "sisis", vt_key); + + vt_key[4].string = "HideObjects"; + hideobjects = prop_get_integer (bundle, "I<-sisis", vt_key); + if (hideobjects == 1) + showobjects = FALSE; + } + else + { + const sc_char *m2; + + vt_key[3].integer = alt; + vt_key[4].string = "M2"; + m2 = prop_get_string (bundle, "S<-sisis", vt_key); + if (!sc_strempty (m2)) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, m2); + is_described = TRUE; + } + + vt_key[4].string = "Res2"; + res_handle_resource (game, "sisis", vt_key); + } + } + + /* Print out any relevant event look text. */ + for (event = 0; event < gs_event_count (game); event++) + { + if (gs_event_state (game, event) == ES_RUNNING + && evt_can_see_event (game, event)) + { + const sc_char *looktext; + + vt_key[0].string = "Events"; + vt_key[1].integer = event; + vt_key[2].string = "LookText"; + looktext = prop_get_string (bundle, "S<-sis", vt_key); + if (is_described) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, looktext); + is_described = TRUE; + + vt_key[2].string = "Res"; + vt_key[3].integer = 1; + res_handle_resource (game, "sisi", vt_key); + } + } + if (is_described) + pf_buffer_character (filter, '\n'); + + /* Finally, print room contents. */ + if (showobjects) + lib_print_room_contents (game, room); +} + + +/* + * lib_can_go() + * + * Return TRUE if the player can move in the given direction. + */ +static sc_bool +lib_can_go (sc_gameref_t game, sc_int room, sc_int direction) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int restriction; + sc_bool is_restricted = FALSE; + + /* Set up invariant parts of key. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Exits"; + vt_key[3].integer = direction; + + /* Check for any movement restrictions. */ + vt_key[4].string = "Var1"; + restriction = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (restriction >= 0) + { + sc_int type; + + if (lib_trace) + sc_trace ("Library: hit move restriction\n"); + + /* Get restriction type. */ + vt_key[4].string = "Var3"; + type = prop_get_integer (bundle, "I<-sisis", vt_key); + switch (type) + { + case 0: /* Task type restriction */ + { + sc_int check; + + /* Get the expected completion state. */ + vt_key[4].string = "Var2"; + check = prop_get_integer (bundle, "I<-sisis", vt_key); + + if (lib_trace) + { + sc_trace ("Library: task %ld, check %ld\n", + restriction, check); + } + + /* Restrict if task isn't done/not done as expected. */ + if ((check != 0) == gs_task_done (game, restriction)) + is_restricted = TRUE; + break; + } + + case 1: /* Object state restriction */ + { + sc_int object, check, openable; + + /* Get the target object. */ + object = obj_stateful_object (game, restriction); + + /* Get the expected object state. */ + vt_key[4].string = "Var2"; + check = prop_get_integer (bundle, "I<-sisis", vt_key); + + if (lib_trace) + sc_trace ("Library: object %ld, check %ld\n", object, check); + + /* Check openable and lockable objects. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Openable"; + openable = prop_get_integer (bundle, "I<-sis", vt_key); + if (openable > 0) + { + sc_int lockable; + + /* See if lockable. */ + vt_key[2].string = "Key"; + lockable = prop_get_integer (bundle, "I<-sis", vt_key); + if (lockable >= 0) + { + /* Lockable. */ + if (check <= 2) + { + if (gs_object_openness (game, object) != check + 5) + is_restricted = TRUE; + } + else + { + if (gs_object_state (game, object) != check - 2) + is_restricted = TRUE; + } + } + else + { + /* Not lockable, though openable. */ + if (check <= 1) + { + if (gs_object_openness (game, object) != check + 5) + is_restricted = TRUE; + } + else + { + if (gs_object_state (game, object) != check - 1) + is_restricted = TRUE; + } + } + } + else + { + /* Not openable. */ + if (gs_object_state (game, object) != check + 1) + is_restricted = TRUE; + } + break; + } + } + } + + /* Return TRUE if not restricted. */ + return !is_restricted; +} + + +/* List of direction names, for printing and counting exits. */ +static const sc_char *const DIRNAMES_4[] = { + "north", "east", "south", "west", "up", "down", "in", "out", + NULL +}; +static const sc_char *const DIRNAMES_8[] = { + "north", "east", "south", "west", "up", "down", "in", "out", + "northeast", "southeast", "southwest", "northwest", + NULL +}; + + +/* + * lib_cmd_print_room_exits() + * + * Print a list of exits from the player room. + */ +sc_bool +lib_cmd_print_room_exits (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_bool eightpointcompass; + const sc_char *const *dirnames; + sc_int count, index_, trail; + + /* Decide on four or eight point compass names list. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "EightPointCompass"; + eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); + dirnames = eightpointcompass ? DIRNAMES_8 : DIRNAMES_4; + + /* Poll for an exit for each valid direction name. */ + count = 0; + trail = -1; + for (index_ = 0; dirnames[index_]; index_++) + { + sc_vartype_t vt_rvalue; + + vt_key[0].string = "Rooms"; + vt_key[1].integer = gs_playerroom (game); + vt_key[2].string = "Exits"; + vt_key[3].integer = index_; + if (prop_get (bundle, "I<-sisi", &vt_rvalue, vt_key) + && lib_can_go (game, gs_playerroom (game), index_)) + { + if (count > 0) + { + if (count == 1) + { + /* Vary text slightly for DispFirstRoom. */ + if (game->turns == 0) + pf_buffer_string (filter, "There are exits "); + else + pf_buffer_string (filter, + lib_select_response (game, + "You can move ", + "I can move ", + "%player% can move ")); + } + else + pf_buffer_string (filter, ", "); + pf_buffer_string (filter, dirnames[trail]); + } + trail = index_; + count++; + } + } + if (count >= 1) + { + if (count == 1) + { + /* Vary text slightly for DispFirstRoom. */ + if (game->turns == 0) + pf_buffer_string (filter, "There is an exit "); + else + pf_buffer_string (filter, + lib_select_response (game, + "You can only move ", + "I can only move ", + "%player% can only move ")); + } + else + pf_buffer_string (filter, " and "); + pf_buffer_string (filter, dirnames[trail]); + pf_buffer_string (filter, ".\n"); + } + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't go in any direction!\n", + "I can't go in any direction!\n", + "%player% can't go in any direction!\n")); + } + + return TRUE; +} + + +/* + * lib_describe_player_room() + * + * Print out details of the player room, in brief if verbose not set and the + * room has already been visited. + */ +static void +lib_describe_player_room (sc_gameref_t game, sc_bool force_verbose) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + + /* Print the room name. */ + lib_print_room_name (game, gs_playerroom (game)); + + /* Print other room details if applicable. */ + if (force_verbose + || game->verbose || !gs_room_seen (game, gs_playerroom (game))) + { + sc_bool showexits; + + /* Print room description, and objects and NPCs. */ + lib_print_room_description (game, gs_playerroom (game)); + + /* Print exits if the ShowExits global requests it. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "ShowExits"; + showexits = prop_get_boolean (bundle, "B<-ss", vt_key); + if (showexits) + { + pf_buffer_character (filter, '\n'); + lib_cmd_print_room_exits (game); + } + } +} + + +/* + * lib_cmd_look() + * + * Command handler for "look" command. + */ +sc_bool +lib_cmd_look (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_character (filter, '\n'); + lib_describe_player_room (game, TRUE); + return TRUE; +} + + +/* + * lib_cmd_quit() + * + * Called on "quit". Exits from the game main loop. + */ +sc_bool +lib_cmd_quit (sc_gameref_t game) +{ + if (if_confirm (SC_CONF_QUIT)) + game->is_running = FALSE; + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_restart() + * + * Called on "restart". Exits from the game main loop with restart + * request set. + */ +sc_bool +lib_cmd_restart (sc_gameref_t game) +{ + if (if_confirm (SC_CONF_RESTART)) + { + game->is_running = FALSE; + game->do_restart = TRUE; + } + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_undo() + * + * Called on "undo". Restores any undo game or memo to the main game. + */ +sc_bool +lib_cmd_undo (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_memo_setref_t memento = gs_get_memento (game); + + /* If an undo buffer is available, restore it. */ + if (game->undo_available) + { + gs_copy (game, game->undo); + game->undo_available = FALSE; + + lib_print_room_name (game, gs_playerroom (game)); + pf_buffer_string (filter, "[The previous turn has been undone.]\n"); + + /* Undo can't properly unravel layered sounds... */ + game->stop_sound = TRUE; + } + + /* + * If there is no undo buffer, try to restore one saved previously in a + * memo. If that works, treat as for restore from file, since that's + * effectively what it is. + */ + else if (memo_load_game (memento, game)) + { + lib_print_room_name (game, gs_playerroom (game)); + pf_buffer_string (filter, "[The previous turn has been undone.]\n"); + + game->is_running = FALSE; + game->do_restore = TRUE; + } + + /* If no undo buffer and memo restore failed, there's no undo available. */ + else if (game->turns == 0) + pf_buffer_string (filter, "You can't undo what hasn't been done.\n"); + else + pf_buffer_string (filter, "Sorry, no more undo is available.\n"); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_history_common() + * lib_cmd_history_number() + * lib_cmd_history() + * + * Prints a history of saved commands for the game. Print directly rather + * than using the printfilter to avoid possible clashes with ALRs. + */ +static sc_bool +lib_cmd_history_common (sc_gameref_t game, sc_int limit) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_memo_setref_t memento = gs_get_memento (game); + sc_int first, count, timestamp; + + /* + * The runner main loop will add an entry for the "history" command that + * got us here, but it hasn't done so yet. To keep the history list + * accurate for recalling commands, we add a surrogate "history" command + * to the history here, and remove it when we've done listing. This matches + * the c-shell, which always shows 'history' listed last. + */ + timestamp = var_get_elapsed_seconds (vars); + memo_save_command (memento, "[history]", timestamp, game->turns); + + /* Decide on the first history to display; all if limit is 0 or less. */ + if (limit > 0) + { + /* + * Get a count of the history length recorded. Because of the surrogate + * "history" above, this is always at least one. From this, choose a + * start point for the display; all if not enough history. + */ + count = memo_get_command_count (memento); + first = (count > limit) ? count - limit : 0; + } + else + first = 0; + + if_print_string ("These are your most recent game commands:\n\n"); + + /* Display history starting at the first entry determined above. */ + memo_first_command (memento); + for (count = 0; memo_more_commands (memento); count++) + { + const sc_char *command; + sc_int sequence, turns; + + /* Obtain the history entry, and write if included. */ + memo_next_command (memento, &command, &sequence, ×tamp, &turns); + if (count >= first) + { + sc_int hr, min, sec; + sc_char buffer[64]; + + /* Write the history entry sequence. */ + sprintf (buffer, "%4ld -- Time ", sequence); + if_print_string (buffer); + + /* Separate the timestamp out into components. */ + hr = timestamp / SECS_PER_HOUR; + min = (timestamp % SECS_PER_HOUR) / MINS_PER_HOUR; + sec = timestamp % SECS_PER_MINUTE; + + /* Print playing time as "[HHh ][M]Mm SSs". */ + if (hr > 0) + sprintf (buffer, "%ldh %02ldm %02lds", hr, min, sec); + else + sprintf (buffer, "%ldm %02lds", min, sec); + if_print_string (buffer); + + /* Follow up with the turns count, and the command string itself. */ + sprintf (buffer, ", turn %ld : ", turns); + if_print_string (buffer); + if_print_string (command); + if_print_character ('\n'); + } + } + + /* Remove the surrogate "history"; the main loop will add the real one. */ + memo_unsave_command (memento); + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_history_number (sc_gameref_t game) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int limit; + + /* Get requested length of history list, and complain if not valid. */ + limit = var_get_ref_number (vars); + if (limit < 1) + { + if_print_string ("That's not a valid history length.\n"); + + game->is_admin = TRUE; + return TRUE; + } + + return lib_cmd_history_common (game, limit); +} + +sc_bool +lib_cmd_history (sc_gameref_t game) +{ + return lib_cmd_history_common (game, 0); +} + + +/* + * lib_cmd_again() + * lib_cmd_redo_number() + * lib_cmd_redo_text_last_common() + * lib_cmd_redo_text() + * lib_cmd_redo_last() + * + * The first function is called on "again", and simply sets the game do_again + * flag. The others allow the user to select a command from the history list + * to re-run. + */ +sc_bool +lib_cmd_again (sc_gameref_t game) +{ + game->do_again = TRUE; + game->redo_sequence = 0; + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_redo_number (sc_gameref_t game) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_memo_setref_t memento = gs_get_memento (game); + sc_int sequence; + + /* + * Get the history sequence entry requested and validate it. The sequence + * may be positive (absolute) or negative (relative to history end), but + * not zero. + */ + sequence = var_get_ref_number (vars); + if (sequence != 0 && memo_find_command (memento, sequence)) + { + game->do_again = TRUE; + game->redo_sequence = sequence; + } + else + { + if_print_string ("No matching entry found in the command history.\n"); + + /* + * This is a failed redo, but returning FALSE will cause the game's + * unknown command message to come up. However, returning TRUE will + * cause the runner main loop to add this to its history, and at some + * point a "redo 7" could cause problems (say, when it's at sequence 7, + * where it'll cause an infinite loop). To work round this, here we'll + * return a redo_sequence _without_ do_again, and have the runner catch + * that as an indication not to save the command in its history. Sorry + * for the ugliness. + */ + game->do_again = FALSE; + game->redo_sequence = INT_MAX; + } + + game->is_admin = TRUE; + return TRUE; +} + +static sc_bool +lib_cmd_redo_text_last_common (sc_gameref_t game, const sc_char *target) +{ + const sc_memo_setref_t memento = gs_get_memento (game); + sc_bool is_do_last, is_contains; + sc_int length, matched_sequence; + + /* Make a special case of "!!", rerun the final command in the history. */ + is_do_last = (strcmp (target, "!") == 0); + + /* + * Differentiate starts-with and contains searches, setting is_contains and + * advancing by one if the target begins '?' (word search). Note target + * string length. + */ + is_contains = (target[0] == '?'); + target += is_contains ? 1 : 0; + length = strlen (target); + + /* If there's no text left to search for, reject this call now. */ + if (length == 0) + { + if_print_string ("No matching entry found in the command history.\n"); + + /* As with failed numeric redo above, special-case this return. */ + game->do_again = FALSE; + game->redo_sequence = INT_MAX; + + game->is_admin = TRUE; + return TRUE; + } + + /* + * Search saved commands for one that matches the target string in the + * required way. We want to return the most recently saved match, so ideally + * we'd search backwards, but the iterator is only forwards, so we do it the + * hard way. + */ + matched_sequence = 0; + memo_first_command (memento); + while (memo_more_commands (memento)) + { + const sc_char *command; + sc_int sequence, timestamp, turns; + sc_bool is_matched; + + /* Get the command; only command and sequence are relevant. */ + memo_next_command (memento, &command, &sequence, ×tamp, &turns); + + /* + * If this is the "!!" special case, match everything. Otherwise, + * either search the command for the target, or match if the command + * begins with the target. + */ + if (is_do_last) + is_matched = TRUE; + else if (is_contains) + { + sc_int index_; + + /* Search this command for an occurrence of target anywhere. */ + is_matched = FALSE; + for (index_ = strlen (command) - length; index_ >= 0; index_--) + { + if (sc_strncasecmp (command + index_, target, length) == 0) + { + is_matched = TRUE; + break; + } + } + } + else + is_matched = (sc_strncasecmp (command, target, length) == 0); + + /* If the command matched the target criteria, note it and continue. */ + if (is_matched) + matched_sequence = sequence; + } + + /* If we found a match, set the redo values accordingly. */ + if (matched_sequence > 0) + { + game->do_again = TRUE; + game->redo_sequence = matched_sequence; + } + else + { + if_print_string ("No matching entry found in the command history.\n"); + + /* As with failed numeric redo above, special-case this return. */ + game->do_again = FALSE; + game->redo_sequence = INT_MAX; + } + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_redo_text (sc_gameref_t game) +{ + const sc_var_setref_t vars = gs_get_vars (game); + + /* Call the common redo with the referenced text from %text%. */ + return lib_cmd_redo_text_last_common (game, var_get_ref_text (vars)); +} + +sc_bool +lib_cmd_redo_last (sc_gameref_t game) +{ + /* Call the common redo with, literally, "!", forming "!!" . */ + return lib_cmd_redo_text_last_common (game, "!"); +} + + +/* + * lib_cmd_hints() + * + * Called on "hints". Requests the interface to display any available hints. + */ +sc_bool +lib_cmd_hints (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int task; + sc_bool game_has_hints; + + /* + * Check for the presence of any game hints at all, no matter whether the + * task is runnable or not. + */ + game_has_hints = FALSE; + for (task = 0; task < gs_task_count (game); task++) + { + if (task_has_hints (game, task)) + { + game_has_hints = TRUE; + break; + } + } + + /* If the game has hints, display any relevant ones. */ + if (game_has_hints) + { + if (run_hint_iterate (game, NULL)) + { + if (if_confirm (SC_CONF_VIEW_HINTS)) + if_display_hints (game); + } + else + pf_buffer_string (filter, "There are currently no hints available.\n"); + } + else + { + pf_buffer_string (filter, + "There are no hints available for this adventure.\n"); + pf_buffer_string (filter, + "You're just going to have to work it out for" + " yourself...\n"); + } + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_print_string_bold() + * lib_print_string_italics() + * + * Convenience helpers for printing licensing and game information. + */ +static void +lib_print_string_bold (const sc_char *string) +{ + if_print_tag (SC_TAG_BOLD, ""); + if_print_string (string); + if_print_tag (SC_TAG_ENDBOLD, ""); +} + +static void +lib_print_string_italics (const sc_char *string) +{ + if_print_tag (SC_TAG_ITALICS, ""); + if_print_string (string); + if_print_tag (SC_TAG_ENDITALICS, ""); +} + + +/* + * lib_cmd_help() + * lib_cmd_license() + * + * A form of standard help output for games that don't define it themselves, + * and the GPL licensing. Print directly rather than using the printfilter + * to avoid possible clashes with ALRs. + */ +sc_bool +lib_cmd_help (sc_gameref_t game) +{ + if_print_string ( + "These are some of the typical commands used in this adventure:\n\n"); + + if_print_string ( + " [N]orth, [E]ast, [S]outh, [W]est, [U]p, [D]own, [In], [O]ut," + " [L]ook, [Exits]\n E[x]amine <object>, [Get <object>]," + " [Drop <object>], [...it], [...all]\n [Where is <object>]\n" + " [Give <object> to <character>], [Open...], [Close...]," + " [Ask <character> about <subject>]\n" + " [Wear <object>], [Remove <object>], [I]nventory\n" + " [Put <object> into <object>], [Put <object> onto <object>]\n"); + + if_print_string ("\nUse the "); + lib_print_string_italics ("Save"); + if_print_string (", "); + lib_print_string_italics ("Restore"); + if_print_string (", "); + lib_print_string_italics ("Undo"); + if_print_string (", and "); + lib_print_string_italics ("Quit"); + if_print_string ( + " commands to save and restore games, undo a move, and leave the " + " game. Use "); + lib_print_string_italics ("History"); + if_print_string (" and "); + lib_print_string_italics ("Redo"); + if_print_string ( + " to view and repeat recent game commands.\n"); + + if_print_string ("\nThe "); + lib_print_string_italics ("Hint"); + if_print_string (" command displays any game hints, "); + lib_print_string_italics ("Notify"); + if_print_string (" provides score change notification, and "); + lib_print_string_italics ("Verbose"); + if_print_string (" and "); + lib_print_string_italics ("Brief"); + if_print_string (" control room descriptions.\n"); + + if_print_string ("\nUse "); + lib_print_string_italics ("License"); + if_print_string ( + " to view SCARE's licensing terms and conditions, and "); + lib_print_string_italics ("Version"); + if_print_string ( + " to print both SCARE's and the game's version number.\n"); + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_license (sc_gameref_t game) +{ + lib_print_string_bold ("SCARE"); + if_print_string (" is "); + lib_print_string_italics ( + "Copyright (C) 2003-2008 Simon Baldwin and Mark J. Tilford"); + if_print_string (".\n\n"); + + if_print_string ( + "This program is free software; you can redistribute it and/or modify" + " it under the terms of version 2 of the GNU General Public License" + " as published by the Free Software Foundation.\n\n"); + + if_print_string ( + "This program is distributed in the hope that it will be useful, but "); + lib_print_string_bold ("WITHOUT ANY WARRANTY"); + if_print_string ("; without even the implied warranty of "); + lib_print_string_bold ("MERCHANTABILITY"); + if_print_string (" or "); + lib_print_string_bold ("FITNESS FOR A PARTICULAR PURPOSE"); + if_print_string ( + ". See the GNU General Public License for more details.\n\n"); + + if_print_string ( + "You should have received a copy of the GNU General Public License" + " along with this program; if not, write to the Free Software" + " Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301" + " USA\n\n"); + + if_print_string ("Please report any bugs, omissions, or misfeatures to "); + lib_print_string_italics ("simon_baldwin@yahoo.com"); + if_print_string (".\n"); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_information() + * + * Display a few small pieces of game information, done by a dialog GUI + * in real Adrift. Prints directly rather than using the printfilter to + * avoid possible clashes with ALRs. + */ +sc_bool +lib_cmd_information (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[2]; + const sc_char *gamename, *compile_date, *gameauthor; + sc_char *filtered; + + vt_key[0].string = "Globals"; + vt_key[1].string = "GameName"; + gamename = prop_get_string (bundle, "S<-ss", vt_key); + filtered = pf_filter_for_info (gamename, vars); + pf_strip_tags (filtered); + + if_print_string ("\""); + if_print_string (!sc_strempty (filtered) ? filtered : "Untitled"); + if_print_string ("\""); + sc_free (filtered); + + vt_key[0].string = "CompileDate"; + compile_date = prop_get_string (bundle, "S<-s", vt_key); + if (!sc_strempty (compile_date)) + { + if_print_string (", "); + if_print_string (compile_date); + } + + vt_key[0].string = "Globals"; + vt_key[1].string = "GameAuthor"; + gameauthor = prop_get_string (bundle, "S<-ss", vt_key); + filtered = pf_filter_for_info (gameauthor, vars); + pf_strip_tags (filtered); + + if_print_string (", "); + if_print_string (!sc_strempty (filtered) ? filtered : "Anonymous"); + if_print_string (".\n"); + sc_free (filtered); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_clear() + * + * Clear the main game window (almost). + */ +sc_bool +lib_cmd_clear (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_tag (filter, SC_TAG_CLS); + pf_buffer_string (filter, "Screen cleared.\n"); + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_statusline() + * + * Display the status line as would be shown by the Runner. Useful for + * interpreter builds that can't offer a true status line. Prints directly + * rather than using the printfilter to avoid possible clashes with ALRs. + */ +sc_bool +lib_cmd_statusline (sc_gameref_t game) +{ + const sc_char *name, *author, *room, *status; + sc_int score; + + /* + * Retrieve the game's name and author, the description of the current + * game room, and any formatted game status line. + */ + run_get_attributes (game, &name, &author, NULL, NULL, + &score, NULL, &room, &status, NULL, NULL, NULL, NULL); + + /* If nothing is yet determined, print the game name and author. */ + if (!room || sc_strempty (room)) + { + if_print_string (name); + if_print_string (" | "); + if_print_string (author); + } + else + { + /* Print the player location, and a separator. */ + if_print_string (room); + if_print_string (" | "); + + /* If the game offers a status line, print it, otherwise the score. */ + if (status && !sc_strempty (status)) + if_print_string (status); + else + { + sc_char buffer[32]; + + if_print_string ("Score: "); + sprintf (buffer, "%ld", score); + if_print_string (buffer); + } + } + if_print_character ('\n'); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_version() + * + * Display the "Runner version". Prints directly rather than using the + * printfilter to avoid possible clashes with ALRs. + */ +sc_bool +lib_cmd_version (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key; + sc_char buffer[64]; + sc_int major, minor, point; + const sc_char *version; + + if_print_string ("SCARE version "); + if_print_string (SCARE_VERSION SCARE_PATCH_LEVEL); + if_print_string (" [Adrift "); + major = SCARE_EMULATION / 1000; + minor = (SCARE_EMULATION % 1000) / 100; + point = SCARE_EMULATION % 100; + sprintf (buffer, "%ld.%02ld.%02ld", major, minor, point); + if_print_string (buffer); + if_print_string (" compatible], "); + + vt_key.string = "VersionString"; + version = prop_get_string (bundle, "S<-s", &vt_key); + if_print_string ("Generator version "); + if_print_string (version); + if_print_string (".\n"); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_wait() + * lib_cmd_wait_number() + * + * Set game waitcounter to a count of turns for which the main loop will run + * without taking input. Many Adrift Runners ignore any WaitTurns setting in + * the game, and use always use one; this might make a game misbehave, so to + * try to cover this case we supply 'wait N' as a player control to override + * the game's setting. The latter prints directly rather than using the + * printfilter to avoid possible clashes with ALRs. + */ +sc_bool +lib_cmd_wait (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_int waitturns; + + /* Note if wait turns is different from the game's setting. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "WaitTurns"; + waitturns = prop_get_integer (bundle, "I<-ss", vt_key); + if (waitturns != game->waitturns) + { + sc_char buffer[32]; + + pf_buffer_string (filter, "("); + sprintf (buffer, "%ld", game->waitturns); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, + game->waitturns == 1 ? " turn)\n" : " turns)\n"); + } + + /* Reset the wait counter to the current waitturns setting. */ + game->waitcounter = game->waitturns; + + pf_buffer_string (filter, "Time passes...\n"); + return TRUE; +} + +sc_bool +lib_cmd_wait_number (sc_gameref_t game) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int waitturns; + sc_char buffer[32]; + + /* Get and validate the waitturns setting. */ + waitturns = var_get_ref_number (vars); + if (waitturns < 1 || waitturns > 20) + { + if_print_string ("You can only wait between 1 and 20 turns.\n"); + game->is_admin = TRUE; + return TRUE; + } + + /* Update the game setting, and confirm for the player. */ + game->waitturns = waitturns; + + if_print_string ("The game will now wait "); + sprintf (buffer, "%ld", waitturns); + if_print_string (buffer); + if_print_string (waitturns == 1 ? " turn" : " turns"); + if_print_string (" for each 'wait' command you enter.\n"); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_verbose() + * lib_cmd_brief() + * + * Set/clear game verbose flag. Print directly rather than using the + * printfilter to avoid possible clashes with ALRs. + */ +sc_bool +lib_cmd_verbose (sc_gameref_t game) +{ + /* Set game verbose flag and return. */ + game->verbose = TRUE; + if_print_string ("The game is now in its "); + if_print_tag (SC_TAG_ITALICS, ""); + if_print_string ("verbose"); + if_print_tag (SC_TAG_ENDITALICS, ""); + if_print_string (" mode, which always gives long descriptions of locations" + " (even if you've been there before).\n"); + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_brief (sc_gameref_t game) +{ + /* Clear game verbose flag and return. */ + game->verbose = FALSE; + if_print_string ("The game is now in its "); + if_print_tag (SC_TAG_ITALICS, ""); + if_print_string ("brief"); + if_print_tag (SC_TAG_ENDITALICS, ""); + if_print_string (" mode, which gives long descriptions of places never" + " before visited and short descriptions otherwise.\n"); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_notify_on_off() + * lib_cmd_notify() + * + * Set/clear/query game score change notification flag. Print directly + * rather than using the printfilter to avoid possible clashes with ALRs. + */ +sc_bool +lib_cmd_notify_on_off (sc_gameref_t game) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_char *control; + + /* Get the text following the notify command, and check for "on"/"off". */ + control = var_get_ref_text (vars); + if (sc_strcasecmp (control, "on") == 0) + { + /* Set score change notification. */ + game->notify_score_change = TRUE; + if_print_string ("Game score change notification is now "); + if_print_tag (SC_TAG_ITALICS, ""); + if_print_string ("on"); + if_print_tag (SC_TAG_ENDITALICS, ""); + if_print_string (", and the game will tell you of any changes in the" + " score.\n"); + } + else if (sc_strcasecmp (control, "off") == 0) + { + /* Clear score change notification. */ + game->notify_score_change = FALSE; + if_print_string ("Game score change notification is now "); + if_print_tag (SC_TAG_ITALICS, ""); + if_print_string ("off"); + if_print_tag (SC_TAG_ENDITALICS, ""); + if_print_string (", and the game will be silent on changes in the" + " score.\n"); + } + else + { + if_print_string ("Use 'notify on' or 'notify off' to control game" + " score notification.\n"); + } + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_notify (sc_gameref_t game) +{ + /* Report the current state of notification. */ + if_print_string ("Game score change notification is "); + if_print_tag (SC_TAG_ITALICS, ""); + if_print_string (game->notify_score_change ? "on" : "off"); + if_print_tag (SC_TAG_ENDITALICS, ""); + + if (game->notify_score_change) + { + if_print_string (", and the game will tell you of any changes in the" + " score.\n"); + } + else + { + if_print_string (", and the game will be silent on changes in the" + " score.\n"); + } + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_time() + * lib_cmd_date() + * + * Print elapsed game time, and smart-alec "date" response. The Adrift + * Runner responds here with the system time and date, but we'll do something + * different. + */ +sc_bool +lib_cmd_time (sc_gameref_t game) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_uint timestamp; + sc_int hr, min, sec; + sc_char buffer[64]; + + /* Get elapsed game time and convert to hour, minutes, and seconds. */ + timestamp = var_get_elapsed_seconds (vars); + hr = timestamp / SECS_PER_HOUR; + min = (timestamp % SECS_PER_HOUR) / MINS_PER_HOUR; + sec = timestamp % SECS_PER_MINUTE; + if (hr > 0) + sprintf (buffer, "%ldh %02ldm %02lds", hr, min, sec); + else + sprintf (buffer, "%ldm %02lds", min, sec); + + /* Print the game's elapsed time. */ + if_print_string ("You have been running the game for "); + if_print_string (buffer); + if_print_string (".\n"); + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_date (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "Maybe we should just be good friends.\n"); + return TRUE; +} + + +/* + * Direction enumeration. Used by movement commands, to multiplex them all + * into a single function. The values are explicit to ensure they match + * enumerations in the game data. + */ +enum +{ DIR_NORTH = 0, DIR_EAST = 1, DIR_SOUTH = 2, DIR_WEST = 3, + DIR_UP = 4, DIR_DOWN = 5, DIR_IN = 6, DIR_OUT = 7, + DIR_NORTHEAST = 8, DIR_SOUTHEAST = 9, DIR_SOUTHWEST = 10, DIR_NORTHWEST = 11 +}; + + +/* + * lib_go() + * + * Central movement command, called by all movement handlers. + */ +static sc_bool +lib_go (sc_gameref_t game, sc_int direction) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5], vt_rvalue; + sc_bool eightpointcompass, is_trapped, is_exitable[12]; + sc_int destination, index_; + const sc_char *const *dirnames; + + /* Decide on four or eight point compass names list. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "EightPointCompass"; + eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); + dirnames = eightpointcompass ? DIRNAMES_8 : DIRNAMES_4; + + /* Start by seeing if there are any exits at all available. */ + is_trapped = TRUE; + for (index_ = 0; dirnames[index_]; index_++) + { + vt_key[0].string = "Rooms"; + vt_key[1].integer = gs_playerroom (game); + vt_key[2].string = "Exits"; + vt_key[3].integer = index_; + if (prop_get (bundle, "I<-sisi", &vt_rvalue, vt_key) + && lib_can_go (game, gs_playerroom (game), index_)) + { + is_exitable[index_] = TRUE; + is_trapped = FALSE; + } + else + is_exitable[index_] = FALSE; + } + if (is_trapped) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't go in any direction!\n", + "I can't go in any direction!\n", + "%player% can't go in any direction!\n")); + return TRUE; + } + + /* + * Check for the exit, and if it doesn't exist, refuse, and list the possible + * options. + */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = gs_playerroom (game); + vt_key[2].string = "Exits"; + vt_key[3].integer = direction; + vt_key[4].string = "Dest"; + if (prop_get (bundle, "I<-sisis", &vt_rvalue, vt_key)) + destination = vt_rvalue.integer - 1; + else + { + sc_int count, trail; + + pf_buffer_string (filter, + lib_select_response (game, + "You can't go in that direction, but you can move ", + "I can't go in that direction, but I can move ", + "%player% can't go in that direction, but can move ")); + + /* List available exits, found in exit test loop earlier. */ + count = 0; + trail = -1; + for (index_ = 0; dirnames[index_]; index_++) + { + if (is_exitable[index_]) + { + if (count > 0) + { + if (count > 1) + pf_buffer_string (filter, ", "); + pf_buffer_string (filter, dirnames[trail]); + } + trail = index_; + count++; + } + } + if (count >= 1) + { + if (count > 1) + pf_buffer_string (filter, " and "); + pf_buffer_string (filter, dirnames[trail]); + } + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Check for any movement restrictions. */ + if (!lib_can_go (game, gs_playerroom (game), direction)) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't go in that direction (at present).\n", + "I can't go in that direction (at present).\n", + "%player% can't go in that direction (at present).\n")); + return TRUE; + } + + if (lib_trace) + { + sc_trace ("Library: moving player from %ld to %ld\n", + gs_playerroom (game), destination); + } + + /* Indicate if getting off something or standing up first. */ + if (gs_playerparent (game) != -1) + { + pf_buffer_string (filter, "(Getting off "); + lib_print_object_np (game, gs_playerparent (game)); + pf_buffer_string (filter, " first)\n"); + } + else if (gs_playerposition (game) != 0) + pf_buffer_string (filter, "(Standing up first)\n"); + + /* Confirm and then make move. */ + pf_buffer_string (filter, + lib_select_response (game, + "You move ", + "I move ", + "%player% moves ")); + pf_buffer_string (filter, dirnames[direction]); + pf_buffer_string (filter, ".\n"); + + gs_move_player_to_room (game, destination); + + /* Describe the new room and return. */ + lib_describe_player_room (game, FALSE); + return TRUE; +} + + +/* + * lib_cmd_go_*() + * + * Direction-specific movement commands. + */ +sc_bool +lib_cmd_go_north (sc_gameref_t game) +{ + return lib_go (game, DIR_NORTH); +} + +sc_bool +lib_cmd_go_east (sc_gameref_t game) +{ + return lib_go (game, DIR_EAST); +} + +sc_bool +lib_cmd_go_south (sc_gameref_t game) +{ + return lib_go (game, DIR_SOUTH); +} + +sc_bool +lib_cmd_go_west (sc_gameref_t game) +{ + return lib_go (game, DIR_WEST); +} + +sc_bool +lib_cmd_go_up (sc_gameref_t game) +{ + return lib_go (game, DIR_UP); +} + +sc_bool +lib_cmd_go_down (sc_gameref_t game) +{ + return lib_go (game, DIR_DOWN); +} + +sc_bool +lib_cmd_go_in (sc_gameref_t game) +{ + return lib_go (game, DIR_IN); +} + +sc_bool +lib_cmd_go_out (sc_gameref_t game) +{ + return lib_go (game, DIR_OUT); +} + +sc_bool +lib_cmd_go_northeast (sc_gameref_t game) +{ + return lib_go (game, DIR_NORTHEAST); +} + +sc_bool +lib_cmd_go_southeast (sc_gameref_t game) +{ + return lib_go (game, DIR_SOUTHEAST); +} + +sc_bool +lib_cmd_go_northwest (sc_gameref_t game) +{ + return lib_go (game, DIR_NORTHWEST); +} + +sc_bool +lib_cmd_go_southwest (sc_gameref_t game) +{ + return lib_go (game, DIR_SOUTHWEST); +} + + +/* + * lib_compare_rooms() + * + * Helper for lib_cmd_go_room(). Compare the name of the passed in room + * with the string passed in, and return TRUE if they match. The routine + * requires that string is filtered, stripped, trimmed and normalized. + */ +static sc_bool +lib_compare_rooms (sc_gameref_t game, sc_int room, const sc_char *string) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_char *name, *compare_name; + sc_bool status; + + /* Get the name of the room, and filter it down to a plain string. */ + name = pf_filter (lib_get_room_name (game, room), vars, bundle); + pf_strip_tags (name); + sc_normalize_string (sc_trim_string (name)); + + /* Bypass any prefix on the room name. */ + if (sc_compare_word (name, "a", 1)) + compare_name = name + 1; + else if (sc_compare_word (name, "an", 2)) + compare_name = name + 2; + else if (sc_compare_word (name, "the", 3)) + compare_name = name + 3; + else + compare_name = name; + sc_trim_string (compare_name); + + /* Compare strings, then free the allocated name. */ + status = sc_strcasecmp (compare_name, string) == 0; + sc_free (name); + + return status; +} + + +/* + * lib_cmd_go_room() + * + * A weak replica of the Runner's claimed ability to go to a named room via + * rooms that have already been visited using a shortest-path search. This + * version scans adjacent rooms for accessibility, and then generates the + * required directional move for any unique match. + * + * Note that rooms can have the same name after they've been cleaned up for + * text comparisons, for example, two "Manor Grounds" at the start of Humbug, + * differentiated within the game with trailing "<some_tag>" components. + */ +sc_bool +lib_cmd_go_room (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5], vt_rvalue; + sc_bool eightpointcompass, is_trapped, is_ambiguous; + sc_int direction, destination, index_; + const sc_char *const *dirnames; + sc_char *name, *compare_name; + + /* Determine the requested room, and filter it down to a plain string. */ + name = pf_filter (var_get_ref_text (vars), vars, bundle); + pf_strip_tags (name); + sc_normalize_string (sc_trim_string (name)); + + /* Bypass any prefix on the request room name. */ + if (sc_compare_word (name, "a", 1)) + compare_name = name + 1; + else if (sc_compare_word (name, "an", 2)) + compare_name = name + 2; + else if (sc_compare_word (name, "the", 3)) + compare_name = name + 3; + else + compare_name = name; + sc_trim_string (compare_name); + + /* See if the named room is the current player room. */ + if (lib_compare_rooms (game, gs_playerroom (game), compare_name)) + { + pf_buffer_string (filter, "You are already there!\n"); + sc_free (name); + return TRUE; + } + + /* Decide on four or eight point compass names list. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "EightPointCompass"; + eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); + dirnames = eightpointcompass ? DIRNAMES_8 : DIRNAMES_4; + + /* Search adjacent and available rooms for a name match. */ + is_trapped = TRUE; + is_ambiguous = FALSE; + direction = -1; + destination = -1; + for (index_ = 0; dirnames[index_]; index_++) + { + vt_key[0].string = "Rooms"; + vt_key[1].integer = gs_playerroom (game); + vt_key[2].string = "Exits"; + vt_key[3].integer = index_; + if (prop_get (bundle, "I<-sisi", &vt_rvalue, vt_key) + && lib_can_go (game, gs_playerroom (game), index_)) + { + is_trapped = FALSE; + + /* + * Room is available. Compare its name with that requested provided + * that it's a location we've not already accepted (that is, some + * rooms are reachable by multiple directions, such as both "south" + * and "out"). + */ + vt_key[4].string = "Dest"; + if (prop_get (bundle, "I<-sisis", &vt_rvalue, vt_key)) + { + sc_int location; + + location = vt_rvalue.integer - 1; + if (location != destination + && lib_compare_rooms (game, location, compare_name)) + { + if (direction != -1) + is_ambiguous = TRUE; + direction = index_; + destination = location; + } + } + } + } + sc_free (name); + + /* If trapped or it's unclear where to go, handle these cases. */ + if (is_trapped) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't go in any direction!\n", + "I can't go in any direction!\n", + "%player% can't go in any direction!\n")); + return TRUE; + } + else if (is_ambiguous) + { + pf_buffer_string (filter, + "I'm not clear about where you want to go." + " Please try using just a direction.\n"); + pf_buffer_character (filter, '\n'); + lib_cmd_print_room_exits (game); + return TRUE; + } + + /* If no match, note it, otherwise handle as standard directional move. */ + if (direction == -1) + { + pf_buffer_string (filter, "I don't know how to get there from here.\n"); + pf_buffer_character (filter, '\n'); + lib_cmd_print_room_exits (game); + return TRUE; + } + + return lib_go (game, direction); +} + + +/* + * lib_cmd_examine_self() + * + * Show the long description of a player. + */ +sc_bool +lib_cmd_examine_self (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_int task, object, count, trail; + const sc_char *description, *position = NULL; + + /* Get selection task. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "Task"; + task = prop_get_integer (bundle, "I<-ss", vt_key) - 1; + + /* Select either the main or the alternate description. */ + if (task >= 0 && gs_task_done (game, task)) + vt_key[1].string = "AltDesc"; + else + vt_key[1].string = "PlayerDesc"; + + /* Print the description, or default response. */ + description = prop_get_string (bundle, "S<-ss", vt_key); + if (!sc_strempty (description)) + pf_buffer_string (filter, description); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are as well as can be expected," + " considering the circumstances.", + "I am as well as can be expected," + " considering the circumstances.", + "%player% is as well as can be expected," + " considering the circumstances.")); + } + + /* If not just standing on the floor, say more. */ + switch (gs_playerposition (game)) + { + case 0: + position = lib_select_response (game, + "You are standing", + "I am standing", + "%player% is standing"); + break; + case 1: + position = lib_select_response (game, + "You are sitting down", + "I am sitting down", + "%player% is sitting down"); + break; + case 2: + position = lib_select_response (game, + "You are lying down", + "I am lying down", + "%player% is lying down"); + break; + } + + if (position + && !(gs_playerposition (game) == 0 && gs_playerparent (game) == -1)) + { + pf_buffer_string (filter, " "); + pf_buffer_string (filter, position); + if (gs_playerparent (game) != -1) + { + pf_buffer_string (filter, " on "); + lib_print_object_np (game, gs_playerparent (game)); + } + pf_buffer_character (filter, '.'); + } + + /* Find and list each object worn by the player. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_WORN_PLAYER) + { + if (count > 0) + { + if (count == 1) + { + pf_buffer_string (filter, + lib_select_response (game, + " You are wearing ", + " I am wearing ", + " %player% is wearing ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + pf_buffer_string (filter, + lib_select_response (game, + " You are wearing ", + " I am wearing ", + " %player% is wearing ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_disambiguate_npc() + * + * Filter, then search the set of NPC matches. If only one matched, note + * and return it. If multiple matched, print a disambiguation message and + * the list, and return -1 with *is_ambiguous TRUE. If none matched, return + * -1 with *is_ambiguous FALSE if requested, otherwise print a message then + * return -1. + */ +static sc_int +lib_disambiguate_npc (sc_gameref_t game, + const sc_char *verb, sc_bool *is_ambiguous) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int count, index_, npc, listed; + + /* + * Filter out all referenced NPCs not actually visible or seen. Count the + * number of NPCs remaining as referenced by the last command, and note the + * last referenced NPC, for where count is 1. + */ + count = 0; + npc = -1; + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + if (game->npc_references[index_] + && gs_npc_seen (game, index_) + && npc_in_room (game, index_, gs_playerroom (game))) + { + count++; + npc = index_; + } + else + game->npc_references[index_] = FALSE; + } + + /* If the reference is unambiguous, set in variables and return it. */ + if (count == 1) + { + /* Set this NPC as the referenced character. */ + var_set_ref_character (vars, npc); + + /* Return, setting no ambiguity. */ + if (is_ambiguous) + *is_ambiguous = FALSE; + return npc; + } + + /* If nothing referenced, return no NPC. */ + if (count == 0) + { + if (is_ambiguous) + *is_ambiguous = FALSE; + else + { + pf_buffer_string (filter, + "Please be more clear, who do you want to "); + pf_buffer_string (filter, verb); + pf_buffer_string (filter, "?\n"); + } + return -1; + } + + /* The NPC reference is ambiguous, so list the choices. */ + pf_buffer_string (filter, "Please be more clear, who do you want to "); + pf_buffer_string (filter, verb); + pf_buffer_string (filter, "? "); + + pf_new_sentence (filter); + listed = 0; + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + if (game->npc_references[index_]) + { + lib_print_npc_np (game, index_); + listed++; + if (listed < count) + pf_buffer_string (filter, (listed < count - 1) ? ", " : " or "); + } + } + pf_buffer_string (filter, "?\n"); + + /* Return no NPC for an ambiguous reference. */ + if (is_ambiguous) + *is_ambiguous = TRUE; + return -1; +} + + +/* + * lib_disambiguate_object_common() + * lib_disambiguate_object() + * lib_disambiguate_object_extended() + * + * Filter, then search the set of object matches. If only one matched, note + * and return it. If multiple matched, print a disambiguation message and + * the list, and return -1 with *is_ambiguous TRUE. If none matched, return + * -1 with *is_ambiguous FALSE if requested, otherwise print a message then + * return -1. + * + * Extended disambiguation operates as normal disambiguation, except that if + * normal disambiguation returns more than one object, the resolver function, + * if supplied, is used to see if the multiple objects can be resolved into + * just one object. The resolver function can normally be the same as the + * function used to filter objects for multiple references. + */ +static sc_int +lib_disambiguate_object_common (sc_gameref_t game, const sc_char *verb, + sc_bool (*resolver) + (sc_gameref_t, sc_int, sc_int), + sc_int resolver_arg, + sc_bool *is_ambiguous) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int count, index_, object, listed; + + /* + * Filter out all referenced objects not actually visible or seen. Count + * the number of objects remaining as referenced by the last command, and + * note the last referenced object, for where count is 1. + */ + count = 0; + object = -1; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (game->object_references[index_] + && gs_object_seen (game, index_) + && obj_indirectly_in_room (game, index_, gs_playerroom (game))) + { + count++; + object = index_; + } + else + game->object_references[index_] = FALSE; + } + + /* + * If this reference is ambiguous and a resolver was supplied, try to + * resolve it unambiguously by calling the resolver filter on the remaining + * set references. + */ + if (resolver && count > 1) + { + sc_int retry_count; + + /* + * Search for objects accepted by the resolver filter, but don't filter + * references just yet. Again, note the last referenced. + */ + retry_count = 0; + object = -1; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (game->object_references[index_] + && resolver (game, index_, resolver_arg)) + { + retry_count++; + object = index_; + } + } + + /* See if we narrowed the field without eliminating every object. */ + if (retry_count > 0 && retry_count < count) + { + /* + * If we got down to a single object, the ambiguity is resolved. + * In this case, set count to 1 so that 'object' is returned. + */ + if (retry_count == 1) + count = retry_count; + else + { + /* + * We got down to fewer objects; reduce references so that the + * disambiguation message is clearer. Note that here we still + * leave with count greater than 1. + */ + count = 0; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (game->object_references[index_] + && resolver (game, index_, resolver_arg)) + count++; + else + game->object_references[index_] = FALSE; + } + } + } + } + + /* If the reference is unambiguous, set in variables and return it. */ + if (count == 1) + { + /* Set this object as referenced. */ + var_set_ref_object (vars, object); + + /* Return, setting no ambiguity. */ + if (is_ambiguous) + *is_ambiguous = FALSE; + return object; + } + + /* If nothing referenced, return no object. */ + if (count == 0) + { + if (is_ambiguous) + *is_ambiguous = FALSE; + else + { + pf_buffer_string (filter, + "Please be more clear, what do you want to "); + pf_buffer_string (filter, verb); + pf_buffer_string (filter, "?\n"); + } + return -1; + } + + /* The object reference is ambiguous, so list the choices. */ + pf_buffer_string (filter, "Please be more clear, what do you want to "); + pf_buffer_string (filter, verb); + pf_buffer_string (filter, "? "); + + pf_new_sentence (filter); + listed = 0; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (game->object_references[index_]) + { + lib_print_object_np (game, index_); + listed++; + if (listed < count) + pf_buffer_string (filter, (listed < count - 1) ? ", " : " or "); + } + } + pf_buffer_string (filter, "?\n"); + + /* Return no object for an ambiguous reference. */ + if (is_ambiguous) + *is_ambiguous = TRUE; + return -1; +} + +static sc_int +lib_disambiguate_object (sc_gameref_t game, + const sc_char *verb, sc_bool *is_ambiguous) +{ + return lib_disambiguate_object_common (game, verb, NULL, -1, is_ambiguous); +} + +static sc_int +lib_disambiguate_object_extended (sc_gameref_t game, const sc_char *verb, + sc_bool (*resolver) + (sc_gameref_t, sc_int, sc_int), + sc_int resolver_arg, + sc_bool *is_ambiguous) +{ + return lib_disambiguate_object_common (game, verb, + resolver, resolver_arg, is_ambiguous); +} + + +/* + * lib_list_npc_inventory() + * + * List objects carried and worn by an NPC. + */ +static sc_bool +lib_list_npc_inventory (sc_gameref_t game, sc_int npc, sc_bool is_described) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, count, trail; + sc_bool wearing; + + /* Find and list each object worn by the NPC. */ + count = 0; + trail = -1; + wearing = FALSE; + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_WORN_NPC + && gs_object_parent (game, object) == npc) + { + if (count > 0) + { + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " is wearing "); + } + else + pf_buffer_string (filter, ", "); + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " is wearing "); + } + else + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + wearing = TRUE; + } + + /* Find and list each object owned by the NPC. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_HELD_NPC + && gs_object_parent (game, object) == npc) + { + if (count > 0) + { + if (count == 1) + { + if (!wearing) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + } + else + pf_buffer_string (filter, ", and"); + pf_buffer_string (filter, " is carrying "); + } + else + pf_buffer_string (filter, ", "); + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + if (!wearing) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + } + else + pf_buffer_string (filter, ", and"); + pf_buffer_string (filter, " is carrying "); + } + else + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + pf_buffer_character (filter, '.'); + } + else + { + if (wearing) + pf_buffer_character (filter, '.'); + } + + /* Return TRUE if anything worn or carried. */ + return wearing || count > 0; +} + + +/* + * lib_cmd_examine_npc() + * + * Show the long description of the most recently referenced NPC, and a + * list of what they're wearing and carrying. + */ +sc_bool +lib_cmd_examine_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int npc, task, resource; + sc_bool is_ambiguous; + const sc_char *description; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "examine", &is_ambiguous); + if (npc == -1) + return is_ambiguous; + + /* Get selection task. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Task"; + task = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + + /* Select either the main or the alternate description. */ + if (task >= 0 && gs_task_done (game, task)) + { + vt_key[2].string = "AltText"; + resource = 1; + } + else + { + vt_key[2].string = "Descr"; + resource = 0; + } + + /* Print the description, or a default message if none. */ + description = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (description)) + pf_buffer_string (filter, description); + else + { + pf_buffer_string (filter, "There's nothing special about "); + lib_print_npc_np (game, npc); + pf_buffer_character (filter, '.'); + } + + /* Handle any associated resource. */ + vt_key[2].string = "Res"; + vt_key[3].integer = resource; + res_handle_resource (game, "sisi", vt_key); + + /* Print what the NPC is wearing and carrying. */ + lib_list_npc_inventory (game, npc, TRUE); + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_list_in_object_normal() + * + * List the objects in a given container object, normal format listing. + */ +static sc_bool +lib_list_in_object_normal (sc_gameref_t game, + sc_int container, sc_bool is_described) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, count, trail; + + /* List out the containers contained in this container. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + /* Contained? */ + if (gs_object_position (game, object) == OBJ_IN_OBJECT + && gs_object_parent (game, object) == container) + { + if (count > 0) + { + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, "Inside "); + lib_print_object_np (game, container); + pf_buffer_string (filter, + lib_select_plurality (game, trail, + " is ", " are ")); + } + else + pf_buffer_string (filter, ", "); + + /* Print out the current list object. */ + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, "Inside "); + lib_print_object_np (game, container); + pf_buffer_string (filter, + lib_select_plurality (game, trail, + " is ", " are ")); + } + else + pf_buffer_string (filter, " and "); + + /* Print out the final object. */ + lib_print_object (game, trail); + pf_buffer_character (filter, '.'); + } + + /* Return TRUE if anything listed. */ + return count > 0; +} + + +/* + * lib_list_in_object_alternate() + * + * List the objects in a given container object, alternate format listing. + */ +static sc_bool +lib_list_in_object_alternate (sc_gameref_t game, + sc_int container, sc_bool is_described) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, count, trail; + + /* List out the objects contained in this object. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + /* Contained? */ + if (gs_object_position (game, object) == OBJ_IN_OBJECT + && gs_object_parent (game, object) == container) + { + if (count > 0) + { + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + } + else + pf_buffer_string (filter, ", "); + + /* Print out the current list object. */ + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object (game, trail); + pf_buffer_string (filter, + lib_select_plurality (game, trail, + " is inside ", + " are inside ")); + } + else + { + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + pf_buffer_string (filter, " are inside "); + } + + /* Print out the container. */ + lib_print_object_np (game, container); + pf_buffer_character (filter, '.'); + } + + /* Return TRUE if anything listed. */ + return count > 0; +} + + +/* + * lib_list_in_object() + * + * List the objects in a given container object. + * + * TODO The Adrift Runner has two distinct styles it uses for listing objects + * within a container, but which it picks at any one point is, frankly, a + * mystery. The selection below seems to work with the few games checked for + * this, and in particular works with the ALR magic in "To Hell in a Hamper", + * but it's almost certainly wrong. Or, at minimum, incomplete. + */ +static sc_bool +lib_list_in_object (sc_gameref_t game, sc_int container, sc_bool is_described) +{ + sc_bool use_alternate_format = FALSE; + + /* + * Switch if the object is static and part of an NPC or the player, or if + * the count of contained objects in a dynamic container is exactly one. + */ + if (obj_is_static (game, container)) + { + sc_int object_position; + + object_position = gs_object_position (game, container); + + if (object_position == OBJ_PART_NPC || object_position == OBJ_PART_PLAYER) + use_alternate_format = TRUE; + } + else + { + sc_int object, count; + + count = 0; + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_IN_OBJECT + && gs_object_parent (game, object) == container) + count++; + if (count > 1) + break; + } + + if (count == 1) + use_alternate_format = TRUE; + } + + /* List contained objects using the selected handler. */ + return use_alternate_format + ? lib_list_in_object_alternate (game, container, is_described) + : lib_list_in_object_normal (game, container, is_described); +} + + +/* + * lib_list_on_object() + * + * List the objects on a given surface object. + */ +static sc_bool +lib_list_on_object (sc_gameref_t game, sc_int supporter, sc_bool is_described) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, count, trail; + + /* List out the objects standing on this object. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + /* Standing on? */ + if (gs_object_position (game, object) == OBJ_ON_OBJECT + && gs_object_parent (game, object) == supporter) + { + if (count > 0) + { + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + } + else + pf_buffer_string (filter, ", "); + + /* Print out the current list object. */ + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object (game, trail); + pf_buffer_string (filter, + lib_select_plurality (game, trail, + " is on ", + " are on ")); + } + else + { + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + pf_buffer_string (filter, " are on "); + } + + /* Print out the surface. */ + lib_print_object_np (game, supporter); + pf_buffer_character (filter, '.'); + } + + /* Return TRUE if anything listed. */ + return count > 0; +} + + +/* + * lib_list_object_state() + * + * Describe the state of a stateful object. + */ +static sc_bool +lib_list_object_state (sc_gameref_t game, sc_int object, sc_bool is_described) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_bool is_statussed; + sc_char *state; + + /* Get object statefulness. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "CurrentState"; + is_statussed = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + + /* Ensure this is a stateful object. */ + if (is_statussed) + { + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, " is ", " are ")); + + /* Add object state string. */ + state = obj_state_name (game, object); + if (state) + { + pf_buffer_string (filter, state); + sc_free (state); + pf_buffer_string (filter, "."); + } + else + { + sc_error ("lib_list_object_state: invalid object state\n"); + pf_buffer_string (filter, "[invalid state]."); + } + } + + /* Return TRUE if a state was printed. */ + return is_statussed; +} + + +/* + * lib_cmd_examine_object() + * + * Show the long description of the most recently referenced object. + */ +sc_bool +lib_cmd_examine_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int object, task, openness; + sc_bool is_described, is_statussed, is_mentioned, is_ambiguous, should_be; + const sc_char *description, *resource; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "examine", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Begin assuming no description printed. */ + is_described = FALSE; + + /* + * Get selection task and expected state; for the expected task state, FALSE + * indicates task completed, TRUE not completed. + */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Task"; + task = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + vt_key[2].string = "TaskNotDone"; + should_be = !prop_get_boolean (bundle, "B<-sis", vt_key); + + /* Select either the main or the alternate description. */ + if (task >= 0 && gs_task_done (game, task) == should_be) + { + vt_key[2].string = "AltDesc"; + resource = "Res2"; + } + else + { + vt_key[2].string = "Description"; + resource = "Res1"; + } + + /* Print the description, or a default response. */ + description = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (description)) + { + pf_buffer_string (filter, description); + is_described |= TRUE; + } + + /* Handle any associated resource. */ + vt_key[2].string = resource; + res_handle_resource (game, "sis", vt_key); + + /* If the object is openable, print its openness state. */ + openness = gs_object_openness (game, object); + switch (openness) + { + case OBJ_OPEN: + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is open.", " are open.")); + is_described |= TRUE; + break; + + case OBJ_CLOSED: + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is closed.", " are closed.")); + is_described |= TRUE; + break; + + case OBJ_LOCKED: + if (is_described) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is locked.", " are locked.")); + is_described |= TRUE; + break; + + default: + break; + } + + /* Add any extra details for stateful objects. */ + vt_key[1].integer = object; + vt_key[2].string = "CurrentState"; + is_statussed = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (is_statussed) + { + vt_key[2].string = "StateListed"; + is_mentioned = prop_get_boolean (bundle, "B<-sis", vt_key); + if (is_mentioned) + is_described |= lib_list_object_state (game, object, is_described); + } + + /* For open container objects, list out what's in them. */ + if (obj_is_container (game, object) && openness <= OBJ_OPEN) + is_described |= lib_list_in_object (game, object, is_described); + + /* For surface objects, list out what's on them. */ + if (obj_is_surface (game, object)) + is_described |= lib_list_on_object (game, object, is_described); + + /* If nothing yet said, print a default response. */ + if (!is_described) + { + pf_buffer_string (filter, + lib_select_response (game, + "You see nothing special about ", + "I see nothing special about ", + "%player% sees nothing special about ")); + lib_print_object_np (game, object); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_save_game_references() + * lib_restore_game_references() + * + * Helpers for trying game commands. Save and restore game references + * so that parsing game commands doesn't interfere with backend loops that + * are working through game references set by prior commands. Saving + * references uses the buffer passed in if possible, otherwise allocates + * its own buffer; testing the return value shows which happened. + */ +static sc_bool * +lib_save_object_references (sc_gameref_t game, sc_bool buffer[], sc_int length) +{ + sc_int required, available; + sc_bool *references; + + /* + * Calculate the required bytes for references, and then either allocate or + * use the buffer supplied. + */ + required = gs_object_count (game) * sizeof (*references); + available = length * sizeof (buffer[0]); + references = required > available ? (sc_bool *)sc_malloc (required) : buffer; + + /* Copy over references from the game, and return the saved copy. */ + memcpy (references, game->object_references, required); + return references; +} + +static void +lib_restore_object_references (sc_gameref_t game, const sc_bool references[]) +{ + sc_int bytes; + + /* Calculate the bytes in the references array, and copy back to the game. */ + bytes = gs_object_count (game) * sizeof (references[0]); + memcpy (game->object_references, references, bytes); +} + + +/* + * lib_try_game_command_common() + * lib_try_game_command_short() + * lib_try_game_command_with_object() + * lib_try_game_command_with_npc() + * + * Try a game command with a standard verb. Used by get and drop handlers + * to retry game commands using standard "get " and "drop " commands. This + * makes "take/pick up/put down" work with a game's overridden get/drop. + */ +static sc_bool +lib_try_game_command_common (sc_gameref_t game, + const sc_char *verb, sc_int object, + const sc_char *preposition, + sc_int associate, + sc_bool is_associate_object, + sc_bool is_associate_npc) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_char buffer[LIB_ALLOCATION_AVOIDANCE_SIZE]; + sc_bool references_buffer[LIB_ALLOCATION_AVOIDANCE_SIZE]; + const sc_char *prefix, *name; + sc_char *command; + sc_bool *references, status; + assert (!is_associate_object || !is_associate_npc); + + /* Save the game's references, for restore later on. */ + references = lib_save_object_references (game, references_buffer, + LIB_ALLOCATION_AVOIDANCE_SIZE); + + /* Get the addressed object's prefix and main name. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + /* Construct and try for game commands with a standard verb. */ + if (is_associate_object || is_associate_npc) + { + const sc_char *associate_prefix, *associate_name; + sc_int required; + + /* Get the associate's prefix and main name. */ + if (is_associate_object) + { + vt_key[0].string = "Objects"; + vt_key[1].integer = associate; + vt_key[2].string = "Prefix"; + associate_prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Short"; + associate_name = prop_get_string (bundle, "S<-sis", vt_key); + } + else + { + assert (is_associate_npc); + vt_key[0].string = "NPCs"; + vt_key[1].integer = associate; + vt_key[2].string = "Prefix"; + associate_prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Name"; + associate_name = prop_get_string (bundle, "S<-sis", vt_key); + } + + assert (preposition); + required = strlen (verb) + strlen (prefix) + strlen (name) + + strlen (preposition) + strlen (associate_prefix) + + strlen (associate_name) + 6; + command = required > (sc_int) sizeof (buffer) + ? (sc_char *)sc_malloc (required) : buffer; + + /* + * Try the command with and without prefixes on both the target object + * and the associate. + */ + sprintf (command, "%s %s %s %s %s %s", verb, + prefix, name, preposition, associate_prefix, associate_name); + status = run_game_task_commands (game, command); + if (!status) + { + sprintf (command, "%s %s %s %s %s", + verb, prefix, name, preposition, associate_name); + status = run_game_task_commands (game, command); + } + if (!status) + { + sprintf (command, "%s %s %s %s %s", + verb, name, preposition, associate_prefix, associate_name); + status = run_game_task_commands (game, command); + } + if (!status) + { + sprintf (command, "%s %s %s %s", + verb, name, preposition, associate_name); + status = run_game_task_commands (game, command); + } + } + else + { + sc_int required; + + required = strlen (verb) + strlen (prefix) + strlen (name) + 3; + command = required > (sc_int) sizeof (buffer) + ? (sc_char *)sc_malloc (required) : buffer; + + /* Try the command with and without prefixes on the addressed object. */ + sprintf (command, "%s %s %s", verb, prefix, name); + status = run_game_task_commands (game, command); + if (!status) + { + sprintf (command, "%s %s", verb, name); + status = run_game_task_commands (game, command); + } + } + + /* Restore the game object references back to their state on entry. */ + lib_restore_object_references (game, references); + + /* Free any allocations, and return the game command status. */ + if (command != buffer) + sc_free (command); + if (references != references_buffer) + sc_free (references); + return status; +} + +static sc_bool +lib_try_game_command_short (sc_gameref_t game, + const sc_char *verb, sc_int object) +{ + return lib_try_game_command_common (game, verb, object, + NULL, -1, FALSE, FALSE); +} + +static sc_bool +lib_try_game_command_with_object (sc_gameref_t game, + const sc_char *verb, sc_int object, + const sc_char *preposition, + sc_int other_object) +{ + return lib_try_game_command_common (game, verb, object, + preposition, other_object, TRUE, FALSE); +} + +static sc_bool +lib_try_game_command_with_npc (sc_gameref_t game, + const sc_char *verb, sc_int object, + const sc_char *preposition, sc_int npc) +{ + return lib_try_game_command_common (game, verb, object, + preposition, npc, FALSE, TRUE); +} + + +/* + * lib_parse_next_object() + * + * Helper for lib_parse_multiple_objects(). Extracts the next object, if any, + * from referenced text, and returns it. Disambiguates any ambiguous objects + * using the verb supplied, and sets are_more_objects if we found an object + * but there appear to be more following it. + */ +static sc_bool +lib_parse_next_object (sc_gameref_t game, const sc_char *verb, + sc_bool (*resolver) (sc_gameref_t, sc_int, sc_int), + sc_int resolver_arg, + sc_int *object, + sc_bool *are_more_objects, sc_bool *is_ambiguous) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_char *list; + sc_bool is_matched; + + /* Look for "object" or "object and ...", and set match and more flags. */ + list = var_get_ref_text (vars); + if (uip_match ("%object%", list, game)) + { + *are_more_objects = FALSE; + is_matched = TRUE; + } + else if (uip_match ("%object% and %text%", list, game)) + { + *are_more_objects = TRUE; + is_matched = TRUE; + } + else + is_matched = FALSE; + + /* If we extracted an object from referenced text, disambiguate. */ + if (is_matched) + *object = lib_disambiguate_object_extended (game, verb, + resolver, resolver_arg, + is_ambiguous); + else + *is_ambiguous = FALSE; + + /* Return TRUE if we matched anything. */ + return is_matched; +} + + +/* + * lib_parse_multiple_objects() + * + * Parser for commands that take multiple object targets from a %text% match. + * Parses object lists such as "object" and "object and object" and returns + * the multiple objects in the game's multiple_references. + */ +static sc_bool +lib_parse_multiple_objects (sc_gameref_t game, const sc_char *verb, + sc_bool (*resolver) (sc_gameref_t, sc_int, sc_int), + sc_int resolver_arg, + sc_int *count) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int count_, object; + sc_bool are_more_objects, is_ambiguous; + + /* Initialize variables to avoid gcc warnings. */ + object = -1; + are_more_objects = FALSE; + + /* Clear all current multiple object references, and the count. */ + gs_clear_multiple_references (game); + count_ = 0; + + /* + * Parse the first object from the list. If we get nothing here, return + * FALSE if it didn't look like a multiple object list, TRUE if ambiguous. + * Beyond here, we always return TRUE, since after this point _something_ + * looked believable... + */ + if (!lib_parse_next_object (game, verb, + resolver, resolver_arg, + &object, &are_more_objects, &is_ambiguous)) + return FALSE; + else if (object == -1) + { + if (is_ambiguous) + { + /* + * Return TRUE, with zero count, to cause caller to return. We get + * here if the first parsed object was ambiguous. In this case, + * the disambiguation has printed a message, so we want our caller + * to simply return TRUE to indicate that the command was handled, + * albeit not fully successfully. + */ + *count = count_; + return TRUE; + } + else + { + /* + * No object matched after disambiguation, so return FALSE to have + * our caller ignore the command. + */ + return FALSE; + } + } + + /* Mark this first object as referenced in the return array. */ + game->multiple_references[object] = TRUE; + count_++; + + /* Now parse each additional object from the list. */ + while (are_more_objects) + { + sc_int last_object; + + /* + * If no next object, leave the loop. If no disambiguation message + * then it was probably garble, so print a message for that case. We + * also catch repeated objects here. + */ + last_object = object; + if (!lib_parse_next_object (game, verb, + resolver, resolver_arg, + &object, &are_more_objects, &is_ambiguous) + || object == -1 + || game->multiple_references[object]) + { + if (!is_ambiguous) + { + pf_buffer_string (filter, + "I only understood you as far as wanting to "); + pf_buffer_string (filter, verb); + pf_buffer_character (filter, ' '); + lib_print_object_np (game, last_object); + pf_buffer_string (filter, ".\n"); + } + + /* Zero count to indicate an error somewhere in the list. */ + count_ = 0; + break; + } + + /* Mark the object as referenced in the return array. */ + game->multiple_references[object] = TRUE; + count_++; + } + + /* We found at least enough of an object list to say we matched. */ + *count = count_; + return TRUE; +} + + +/* + * lib_apply_multiple_filter() + * lib_apply_except_filter() + * + * Apply filters for multiple object frontends. Transfer multiple object + * references into standard object references, using the supplied filter. + * The first is inclusive, the second exclusive. + */ +static sc_int +lib_apply_multiple_filter (sc_gameref_t game, + sc_bool (*filter) (sc_gameref_t, sc_int, sc_int), + sc_int filter_arg, sc_int *references) +{ + sc_int count, object, references_; + + /* Clear all object references initially. */ + gs_clear_object_references (game); + + /* + * Find objects included by the filter, and transfer the reference of each + * from the multiple references into standard references. + */ + count = 0; + references_ = references ? *references : 0; + for (object = 0; object < gs_object_count (game); object++) + { + if (filter (game, object, filter_arg)) + { + /* Transfer the reference. */ + if (game->multiple_references[object]) + { + game->object_references[object] = TRUE; + count++; + game->multiple_references[object] = FALSE; + references_--; + } + } + } + + /* Copy back the updated reference count, return count. */ + if (references) + *references = references_; + return count; +} + +static sc_int +lib_apply_except_filter (sc_gameref_t game, + sc_bool (*filter) (sc_gameref_t, sc_int, sc_int), + sc_int filter_arg, sc_int *references) +{ + sc_int count, object, references_; + + /* Clear all object references initially. */ + gs_clear_object_references (game); + + /* + * Find objects included by the filter, and transfer the reference of each + * from the multiple references into standard references. + */ + count = 0; + references_ = references ? *references : 0; + for (object = 0; object < gs_object_count (game); object++) + { + if (filter (game, object, filter_arg)) + { + /* If excepted, remove from exceptions, else add to references. */ + if (game->multiple_references[object]) + { + game->multiple_references[object] = FALSE; + references_--; + } + else + { + game->object_references[object] = TRUE; + count++; + } + } + } + + /* Copy back the updated reference count, return count. */ + if (references) + *references = references_; + return count; +} + + +/* + * lib_cmd_count() + * + * Display player weight and size limits and amounts currently carried. + */ +sc_bool +lib_cmd_count (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int index_, size, weight; + sc_char buffer[32]; + + /* Sum sizes for objects currently held or worn by player. */ + size = 0; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (gs_object_position (game, index_) == OBJ_HELD_PLAYER + || gs_object_position (game, index_) == OBJ_WORN_PLAYER) + size += obj_get_size (game, index_); + } + + /* Sum weights for objects currently held or worn by player. */ + weight = 0; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (gs_object_position (game, index_) == OBJ_HELD_PLAYER + || gs_object_position (game, index_) == OBJ_WORN_PLAYER) + weight += obj_get_weight (game, index_); + } + + /* Print the player limits and amounts used. */ + pf_buffer_string (filter, "Size: You have "); + sprintf (buffer, "%ld", size); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, ". The most you can hold is "); + sprintf (buffer, "%ld", obj_get_player_size_limit (game)); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, ".\n"); + + pf_buffer_string (filter, "Weight: You have "); + sprintf (buffer, "%ld", weight); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, ". The most you can hold is "); + sprintf (buffer, "%ld", obj_get_player_weight_limit (game)); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, ".\n"); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_object_too_heavy() + * + * Return TRUE if the given object is too heavy for the player to carry. + */ +static sc_bool +lib_object_too_heavy (sc_gameref_t game, sc_int object, sc_bool *is_portable) +{ + sc_int player_limit, index_, weight, object_weight; + + /* Get the player limit and the given object weight. */ + player_limit = obj_get_player_weight_limit (game); + object_weight = obj_get_weight (game, object); + + /* Sum weights for objects currently held or worn by player. */ + weight = 0; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (gs_object_position (game, index_) == OBJ_HELD_PLAYER + || gs_object_position (game, index_) == OBJ_WORN_PLAYER) + weight += obj_get_weight (game, index_); + } + + /* If requested, return object portability. */ + if (is_portable) + *is_portable = !(object_weight > player_limit); + + /* Return TRUE if the new object exceeds limit. */ + return weight + object_weight > player_limit; +} + + +/* + * lib_object_too_large() + * + * Return TRUE if the given object is too large for the player to carry. + */ +static sc_bool +lib_object_too_large (sc_gameref_t game, sc_int object, sc_bool *is_portable) +{ + sc_int player_limit, index_, size, object_size; + + /* Get the player limit and the given object size. */ + player_limit = obj_get_player_size_limit (game); + object_size = obj_get_size (game, object); + + /* Sum sizes for objects currently held or worn by player. */ + size = 0; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (gs_object_position (game, index_) == OBJ_HELD_PLAYER + || gs_object_position (game, index_) == OBJ_WORN_PLAYER) + size += obj_get_size (game, index_); + } + + /* If requested, return object portability. */ + if (is_portable) + *is_portable = !(object_size > player_limit); + + /* Return TRUE if the new object exceeds limit. */ + return size + object_size > player_limit; +} + + +/* + * lib_cmd_take_npc() + * + * Reject attempts to take an npc. + */ +sc_bool +lib_cmd_take_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int npc; + sc_bool is_ambiguous; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "take", &is_ambiguous); + if (npc == -1) + return is_ambiguous; + + /* Reject this attempt. */ + pf_buffer_string (filter, "I don't think "); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " would appreciate being handled.\n"); + return TRUE; +} + + +/* + * lib_take_backend_common() + * + * Common backend handler for taking objects. Takes all objects currently + * referenced in the game, trying game commands first, and then moving other + * unhandled objects to the player inventory. + * + * Objects to action are flagged in object_references; objects requested but + * deemed not actionable are flagged in multiple_references. + */ +static void +lib_take_backend_common (sc_gameref_t game, sc_int associate, + sc_bool is_associate_object, sc_bool is_associate_npc) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object_count, object, count, trail, total, npc; + sc_int too_heavy, too_large; + sc_bool too_heavy_portable, too_large_portable, has_printed; + assert (!is_associate_object || !is_associate_npc); + + /* Initialize our notions of anything exceeding player capacity. */ + too_heavy_portable = too_large_portable = FALSE; + too_large = too_heavy = -1; + + /* + * Try game commands for all referenced objects first. If any succeed, + * remove that reference from the list. At the same time, filter out and + * flag any object that takes us over the player's capacity. We report + * only the first. + */ + has_printed = FALSE; + object_count = gs_object_count (game); + for (object = 0; object < object_count; object++) + { + sc_bool status; + + if (!game->object_references[object]) + continue; + + /* + * If the object is inside or on something already held by the player, + * capacity checks are meaningless. + */ + if (!((gs_object_position (game, object) == OBJ_IN_OBJECT + || gs_object_position (game, object) == OBJ_ON_OBJECT) + && obj_indirectly_held_by_player (game, + gs_object_parent (game, object)))) + { + sc_bool is_portable; + + /* + * See if the object takes us beyond capacity. If it does and it's + * the first of its kind, note it and continue. + */ + if (lib_object_too_heavy (game, object, &is_portable)) + { + if (too_heavy == -1) + { + too_heavy = object; + too_heavy_portable = is_portable; + } + game->object_references[object] = FALSE; + continue; + } + if (lib_object_too_large (game, object, &is_portable)) + { + if (too_large == -1) + { + too_large = object; + too_large_portable = is_portable; + } + game->object_references[object] = FALSE; + continue; + } + } + + /* Now try for a game command, using the associate if supplied. */ + if (is_associate_object) + status = lib_try_game_command_with_object (game, "get", + object, "from", associate); + else if (is_associate_npc) + status = lib_try_game_command_with_npc (game, "get", + object, "from", associate); + else + status = lib_try_game_command_short (game, "get", object); + if (status) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + } + } + + /* + * We attempt acquisition of get-able objects here only for cases where + * there is either no associate, or where the associate is an object. If + * the associate is an NPC, we're going to refuse all acquisitions later + * on, by forcing object references. + */ + total = 0; + if (!is_associate_npc) + { + sc_int parent, start, limit; + + /* + * Attempt to acquire each remaining get-able object in turn, looping + * on each possible parent object in turn, with an initial parent of + * -1 for objects not contained or supported. + * + * If we're dealing with only objects from a known container or + * supporter, eliminate all but one iteration of the parent search. + */ + start = is_associate_object ? associate : -1; + limit = is_associate_object ? associate : object_count - 1; + + for (parent = start; parent <= limit; parent++) + { + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + sc_bool is_portable; + + if (!game->object_references[object]) + continue; + + /* + * If parent is -1, ignore contained objects, otherwise ignore + * objects not contained, or if contained, not contained by the + * current parent. + */ + if (parent == -1) + { + if (gs_object_position (game, object) == OBJ_IN_OBJECT + || gs_object_position (game, object) == OBJ_ON_OBJECT) + continue; + } + else + { + if (!(gs_object_position (game, object) == OBJ_IN_OBJECT + || gs_object_position (game, object) == OBJ_ON_OBJECT)) + continue; + if (gs_object_parent (game, object) != parent) + continue; + } + + /* + * Here we have to repeat capacity checks. As objects are + * acquired more and more of the player's capacity gets used up. + * This means a check directly before each acquisition. + */ + if (parent == -1 + || !obj_indirectly_held_by_player (game, parent)) + { + if (lib_object_too_heavy (game, object, &is_portable)) + { + if (too_heavy == -1) + { + too_heavy = object; + too_heavy_portable = is_portable; + } + continue; + } + if (lib_object_too_large (game, object, &is_portable)) + { + if (too_large == -1) + { + too_large = object; + too_large_portable = is_portable; + } + continue; + } + } + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, total == 0 ? "\n" : " "); + if (parent == -1) + pf_buffer_string (filter, + lib_select_response (game, + "You pick up ", + "I pick up ", + "%player% picks up ")); + else + pf_buffer_string (filter, + lib_select_response (game, + "You take ", + "I take ", + "%player% takes ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + gs_object_player_get (game, object); + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, total == 0 ? "\n" : " "); + if (parent == -1) + pf_buffer_string (filter, + lib_select_response (game, + "You pick up ", + "I pick up ", + "%player% picks up ")); + else + pf_buffer_string (filter, + lib_select_response (game, + "You take ", + "I take ", + "%player% takes ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + if (parent != -1) + { + pf_buffer_string (filter, " from "); + lib_print_object_np (game, parent); + } + pf_buffer_character (filter, '.'); + } + total += count; + has_printed |= count > 0; + } + } + + /* + * If we ran out of capacity, either in weight or in size, print the + * details. Note that we currently only report the first object of any + * type to go over capacity. + */ + if (too_heavy != -1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, too_heavy); + pf_buffer_string (filter, + lib_select_plurality (game, too_heavy, " is", " are")); + pf_buffer_string (filter, + lib_select_response (game, + " too heavy for you to carry", + " too heavy for me to carry", + " too heavy for %player% to carry")); + if (too_heavy_portable) + pf_buffer_string (filter, " at the moment"); + pf_buffer_character (filter, '.'); + has_printed |= TRUE; + } + else if (too_large != -1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "Your hands are full", + "My hands are full", + "%player%'s hands are full")); + if (too_large_portable) + pf_buffer_string (filter, " at the moment"); + pf_buffer_character (filter, '.'); + has_printed |= TRUE; + } + + /* + * Note any remaining multiple references left out of the take operation. + * This is some workload... + * + * First, deal with the case where we have an associated object. + */ + if (is_associate_object) + { + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (gs_object_position (game, object) == OBJ_HELD_PLAYER + || gs_object_position (game, object) == OBJ_WORN_PLAYER) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, trail); + } + else + pf_buffer_string (filter, ", "); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, trail); + pf_buffer_string (filter, + lib_select_plurality (game, trail, + " is not ", + " are not ")); + } + else + { + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_string (filter, " are not "); + } + if (obj_is_container (game, associate)) + { + pf_buffer_string (filter, "in "); + if (obj_is_surface (game, associate)) + pf_buffer_string (filter, "or on "); + } + else + pf_buffer_string (filter, "on "); + lib_print_object_np (game, associate); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + } + + /* + * Now, deal with the case where we have an associated NPC. Once this + * case is handled, we can force the object references so that the code + * that follows on from here will report errors taking all objects. + * + * Note that this means that we can never successfully take an object + * from an NPC; that'll have to happen via a game's own commands. + */ + if (is_associate_npc) + { + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (gs_object_position (game, object) == OBJ_PART_NPC) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, associate); + pf_buffer_string (filter, " is not carrying "); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, associate); + pf_buffer_string (filter, " is not carrying "); + lib_print_object_np (game, trail); + } + else + { + pf_buffer_string (filter, " or "); + lib_print_object_np (game, trail); + } + pf_buffer_character (filter, '!'); + } + has_printed |= count > 0; + + /* + * Merge any remaining object references into multiple references, + * so that succeeding code complains about the inability to acquire + * these objects. + */ + for (object = 0; object < object_count; object++) + { + game->multiple_references[object] |= game->object_references[object]; + game->object_references[object] = FALSE; + } + } + + /* + * The remainder of this routine is common error reporting for both object + * and NPC associates (and also for no associates). + */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (gs_object_position (game, object) != OBJ_HELD_PLAYER) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You've already got ", + "I've already got ", + "%player% already has ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You've already got ", + "I've already got ", + "%player% already has ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '!'); + } + has_printed |= count > 0; + + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (gs_object_position (game, object) != OBJ_WORN_PLAYER) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You're already wearing ", + "I'm already wearing ", + "%player% is already wearing ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You're already wearing ", + "I'm already wearing ", + "%player% is already wearing ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '!'); + } + has_printed |= count > 0; + + for (npc = 0; npc < gs_npc_count (game); npc++) + { + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (gs_object_position (game, object) != OBJ_HELD_NPC + && gs_object_position (game, object) != OBJ_WORN_NPC) + continue; + if (gs_object_parent (game, object) != npc) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, gs_object_parent (game, trail)); + pf_buffer_string (filter, + lib_select_response (game, + " refuses to give you ", + " refuses to give me ", + " refuses to give %player% ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_npc_np (game, gs_object_parent (game, trail)); + pf_buffer_string (filter, + lib_select_response (game, + " refuses to give you ", + " refuses to give me ", + " refuses to give %player% ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '!'); + } + has_printed |= count > 0; + } + + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You can't take ", + "I can't take ", + "%player% can't take ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You can't take ", + "I can't take ", + "%player% can't take ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '!'); + } +} + + +/* + * lib_take_backend() + * lib_take_from_object_backend() + * lib_take_from_npc_backend() + * + * Facets of lib_take_backend_common(). Provide backend handling for either + * the plain "take" handlers, or the "take from <something>" handlers. + */ +static void +lib_take_backend (sc_gameref_t game) +{ + lib_take_backend_common (game, -1, FALSE, FALSE); +} + +static void +lib_take_from_object_backend (sc_gameref_t game, sc_int associate) +{ + lib_take_backend_common (game, associate, TRUE, FALSE); +} + +static void +lib_take_from_npc_backend (sc_gameref_t game, sc_int associate) +{ + lib_take_backend_common (game, associate, FALSE, TRUE); +} + + +/* + * lib_take_filter() + * lib_take_not_associated_filter() + * + * Helper functions for deciding if an object may be acquired in this context. + * Returns TRUE if an object may be acquired, FALSE otherwise. + */ +static sc_bool +lib_take_filter (sc_gameref_t game, sc_int object, sc_int unused) +{ + assert (unused == -1); + + /* + * To be take-able, an object must be visible in the room, not static, + * and not already held or worn by the player or an NPC. + */ + return obj_indirectly_in_room (game, object, gs_playerroom (game)) + && !obj_is_static (game, object) + && !(gs_object_position (game, object) == OBJ_HELD_PLAYER + || gs_object_position (game, object) == OBJ_WORN_PLAYER) + && !(gs_object_position (game, object) == OBJ_HELD_NPC + || gs_object_position (game, object) == OBJ_WORN_NPC); +} + +static sc_bool +lib_take_not_associated_filter (sc_gameref_t game, + sc_int object, sc_int unused) +{ + assert (unused == -1); + + /* In addition to other checks, the object may not be in or on an object. */ + return lib_take_filter (game, object, -1) + && !(gs_object_position (game, object) == OBJ_ON_OBJECT + || gs_object_position (game, object) == OBJ_IN_OBJECT); +} + + +/* + * lib_cmd_take_all() + * + * Attempt to take all objects currently visible to the player. + */ +sc_bool +lib_cmd_take_all (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_take_not_associated_filter, -1, + NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_take_backend (game); + else + pf_buffer_string (filter, "There is nothing to pick up here."); + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_take_except_multiple() + * + * Take all objects currently available to the player, excepting those listed + * in %text%. + */ +sc_bool +lib_cmd_take_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find leave target objects. */ + if (!lib_parse_multiple_objects (game, "leave", + lib_take_not_associated_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_take_not_associated_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_take_backend (game); + else + { + if (objects == 0) + pf_buffer_string (filter, "There is nothing else to pick up here."); + else + pf_buffer_string (filter, "There is nothing to pick up here."); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_take_multiple() + * + * Take all objects currently available to the player and listed in %text%. + */ +sc_bool +lib_cmd_take_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find take target objects. */ + if (!lib_parse_multiple_objects (game, "take", + lib_take_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_take_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_take_backend (game); + else + pf_buffer_string (filter, "There is nothing to pick up here."); + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_take_from_filter() + * + * Helper function for deciding if an object may be acquired in this context. + * Returns TRUE if an object may be acquired, FALSE otherwise. + */ +static sc_bool +lib_take_from_filter (sc_gameref_t game, sc_int object, sc_int associate) +{ + /* + * To be take-able, an object must be either inside or on the specified + * object. + */ + return (gs_object_position (game, object) == OBJ_IN_OBJECT + || gs_object_position (game, object) == OBJ_ON_OBJECT) + && !obj_is_static (game, object) + && gs_object_parent (game, object) == associate; +} + + +/* + * lib_take_from_empty() + * + * Common error handling for when nothing is taken from a container or + * supporter object. + */ +static void +lib_take_from_empty (sc_gameref_t game, sc_int associate, sc_bool is_except) +{ + const sc_filterref_t filter = gs_get_filter (game); + + if (obj_is_container (game, associate) && obj_is_surface (game, associate)) + { + if (gs_object_openness (game, associate) <= OBJ_OPEN) + { + if (is_except) + pf_buffer_string (filter, "There is nothing else in or on "); + else + pf_buffer_string (filter, "There is nothing in or on "); + lib_print_object_np (game, associate); + pf_buffer_character (filter, '.'); + } + else + { + if (is_except) + pf_buffer_string (filter, "There is nothing else on "); + else + pf_buffer_string (filter, "There is nothing on "); + lib_print_object_np (game, associate); + if (gs_object_openness (game, associate) == OBJ_LOCKED) + pf_buffer_string (filter, " and it is locked."); + else + pf_buffer_string (filter, " and it is closed."); + } + } + else + { + if (obj_is_container (game, associate)) + { + if (gs_object_openness (game, associate) <= OBJ_OPEN) + { + if (is_except) + pf_buffer_string (filter, "There is nothing else inside "); + else + pf_buffer_string (filter, "There is nothing inside "); + lib_print_object_np (game, associate); + pf_buffer_character (filter, '.'); + } + else + { + pf_new_sentence (filter); + lib_print_object_np (game, associate); + pf_buffer_string (filter, + lib_select_plurality (game, associate, + " is ", " are ")); + if (gs_object_openness (game, associate) == OBJ_LOCKED) + pf_buffer_string (filter, "locked."); + else + pf_buffer_string (filter, "closed."); + } + } + else + { + if (is_except) + pf_buffer_string (filter, "There is nothing else on "); + else + pf_buffer_string (filter, "There is nothing on "); + lib_print_object_np (game, associate); + pf_buffer_character (filter, '.'); + } + } +} + + +/* + * lib_take_from_is_valid() + * + * Validate the supporter requested in "take from" commands. + */ +static sc_bool +lib_take_from_is_valid (sc_gameref_t game, sc_int associate) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Disallow emptying non-container/non-surface objects. */ + if (!(obj_is_container (game, associate) + || obj_is_surface (game, associate))) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't take anything from ", + "I can't take anything from ", + "%player% can't take anything from ")); + lib_print_object_np (game, associate); + pf_buffer_string (filter, ".\n"); + return FALSE; + } + + /* If object is a container, and is closed, reject now. */ + if (obj_is_container (game, associate) + && gs_object_openness (game, associate) > OBJ_OPEN) + { + pf_new_sentence (filter); + lib_print_object_np (game, associate); + pf_buffer_string (filter, + lib_select_plurality (game, associate, + " is closed.\n", + " are closed.\n")); + return FALSE; + } + + /* Associate is a valid target for "take from". */ + return TRUE; +} + + +/* + * lib_cmd_take_all_from() + * + * Attempt to take all objects contained in or supported by a given object. + */ +sc_bool +lib_cmd_take_all_from (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int associate, objects; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + associate = lib_disambiguate_object (game, "take from", &is_ambiguous); + if (associate == -1) + return is_ambiguous; + + /* Validate the associate object to take from. */ + if (!lib_take_from_is_valid (game, associate)) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_take_from_filter, associate, + NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_take_from_object_backend (game, associate); + else + lib_take_from_empty (game, associate, FALSE); + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_take_from_except_multiple() + * + * Take all objects contained in or supported by a given object, excepting + * those listed in %text%. + */ +sc_bool +lib_cmd_take_from_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int associate, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + associate = lib_disambiguate_object (game, "take from", &is_ambiguous); + if (associate == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find leave target objects. */ + if (!lib_parse_multiple_objects (game, "leave", + lib_take_from_filter, associate, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Validate the associate object to take from. */ + if (!lib_take_from_is_valid (game, associate)) + return TRUE; + + /* As a special case, complain about requests to retain the associate. */ + if (game->multiple_references[associate]) + { + pf_buffer_string (filter, + "I only understood you as far as wanting to leave "); + lib_print_object_np (game, associate); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_take_from_filter, associate, + &references); + if (objects > 0 || references > 0) + lib_take_from_object_backend (game, associate); + else + lib_take_from_empty (game, associate, TRUE); + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_take_from_multiple() + * + * Take the objects currently inside an object and listed in %text%. This + * function isn't mandatory -- plain "take <object>" works fine with contain- + * ers and surfaces, but it's a standard in Adrift so here it is. + */ +sc_bool +lib_cmd_take_from_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int associate, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + associate = lib_disambiguate_object (game, "take from", &is_ambiguous); + if (associate == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find take target objects. */ + if (!lib_parse_multiple_objects (game, "take", + lib_take_from_filter, associate, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Validate the associate object to take from. */ + if (!lib_take_from_is_valid (game, associate)) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_take_from_filter, associate, + &references); + if (objects > 0 || references > 0) + lib_take_from_object_backend (game, associate); + else + lib_take_from_empty (game, associate, FALSE); + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_take_from_npc_filter() + * + * Helper function for deciding if an object may be acquired in this context. + * Returns TRUE if an object may be acquired, FALSE otherwise. + */ +static sc_bool +lib_take_from_npc_filter (sc_gameref_t game, sc_int object, sc_int associate) +{ + /* + * To be take-able, an object must be either held or worn by the specified + * NPC. + */ + return (gs_object_position (game, object) == OBJ_HELD_NPC + || gs_object_position (game, object) == OBJ_WORN_NPC) + && !obj_is_static (game, object) + && gs_object_parent (game, object) == associate; +} + + +/* + * lib_cmd_take_all_from_npc() + * + * Attempt to take all objects held or worn by a given NPC. + */ +sc_bool +lib_cmd_take_all_from_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int associate, objects; + sc_bool is_ambiguous; + + /* Get the referenced NPC, and if none, consider complete. */ + associate = lib_disambiguate_npc (game, "take from", &is_ambiguous); + if (associate == -1) + return is_ambiguous; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_take_from_npc_filter, associate, + NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_take_from_npc_backend (game, associate); + else + { + pf_new_sentence (filter); + lib_print_npc_np (game, associate); + pf_buffer_string (filter, " is not carrying anything!"); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_take_from_npc_except_multiple() + * + * Attempt to take all objects held or worn by a given NPC, excepting those + * listed in %text%. + */ +sc_bool +lib_cmd_take_from_npc_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int associate, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced NPC, and if none, consider complete. */ + associate = lib_disambiguate_npc (game, "take from", &is_ambiguous); + if (associate == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find leave target objects. */ + if (!lib_parse_multiple_objects (game, "leave", + lib_take_from_npc_filter, associate, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_take_from_npc_filter, associate, + &references); + if (objects > 0 || references > 0) + lib_take_from_npc_backend (game, associate); + else + { + pf_new_sentence (filter); + lib_print_npc_np (game, associate); + pf_buffer_string (filter, " is not carrying anything else!"); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_take_from_npc_multiple() + * + * Attempt to take the objects currently held or worn by an NPC and listed + * in %text%. + */ +sc_bool +lib_cmd_take_from_npc_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int associate, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced NPC, and if none, consider complete. */ + associate = lib_disambiguate_npc (game, "take from", &is_ambiguous); + if (associate == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find take target objects. */ + if (!lib_parse_multiple_objects (game, "take", + lib_take_from_npc_filter, associate, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_take_from_npc_filter, associate, + &references); + if (objects > 0 || references > 0) + lib_take_from_npc_backend (game, associate); + else + { + pf_new_sentence (filter); + lib_print_npc_np (game, associate); + pf_buffer_string (filter, " is not carrying anything!"); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_drop_backend() + * + * Common backend handler for dropping objects. Drops all objects currently + * referenced in the game, trying game commands first, and then moving other + * unhandled objects to the player room floor. + * + * Objects to action are flagged in object_references; objects requested but + * deemed not actionable are flagged in multiple_references. + */ +static void +lib_drop_backend (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object_count, object, count, trail; + sc_bool has_printed; + + /* + * Try game commands for all referenced objects first. If any succeed, + * remove that reference from the list. + */ + has_printed = FALSE; + object_count = gs_object_count (game); + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (lib_try_game_command_short (game, "drop", object)) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + } + } + + /* Drop every object that remains referenced. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You drop ", + "I drop ", + "%player% drops ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + gs_object_to_room (game, object, gs_playerroom (game)); + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You drop ", + "I drop ", + "%player% drops ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + /* Note any remaining multiple references left out of the drop operation. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, " or "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } +} + + +/* + * lib_drop_filter() + * + * Helper function for deciding if an object may be dropped in this context. + * Returns TRUE if an object may be dropped, FALSE otherwise. + */ +static sc_bool +lib_drop_filter (sc_gameref_t game, sc_int object, sc_int unused) +{ + assert (unused == -1); + + return !obj_is_static (game, object) + && gs_object_position (game, object) == OBJ_HELD_PLAYER; +} + + +/* + * lib_cmd_drop_all() + * + * Drop all objects currently held by the player. + */ +sc_bool +lib_cmd_drop_all (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_drop_filter, -1, + NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_drop_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You're not carrying anything.", + "I'm not carrying anything.", + "%player%'s not carrying anything.")); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_drop_except_multiple() + * + * Drop all objects currently held by the player, excepting those listed in + * %text%. + */ +sc_bool +lib_cmd_drop_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find retain target objects. */ + if (!lib_parse_multiple_objects (game, "retain", + lib_drop_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_drop_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_drop_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything", + "I am not holding anything", + "%player% is not holding anything")); + if (objects == 0) + pf_buffer_string (filter, " else"); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_drop_multiple() + * + * Drop all objects currently held by the player and listed in %text%. + */ +sc_bool +lib_cmd_drop_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find drop target objects. */ + if (!lib_parse_multiple_objects (game, "drop", + lib_drop_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_drop_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_drop_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything.", + "I am not holding anything.", + "%player% is not holding anything.")); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_give_object_npc() + * lib_cmd_give_object() + * + * Attempt to give an object to an NPC. + */ +sc_bool +lib_cmd_give_object_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, npc; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "give", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "give to", NULL); + if (npc == -1) + return TRUE; + + /* Reject if not holding the object offered. */ + if (gs_object_position (game, object) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You don't have ", + "I don't have ", + "%player% doesn't have ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; + } + + /* After all that, the npc is disinterested. */ + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " doesn't seem interested in "); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +sc_bool +lib_cmd_give_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "give", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Reject if not holding the object offered. */ + if (gs_object_position (game, object) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You don't have ", + "I don't have ", + "%player% doesn't have ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; + } + + /* After all that, we have to ask (and shouldn't this be "to whom?"). */ + pf_buffer_string (filter, "Give "); + lib_print_object_np (game, object); + pf_buffer_string (filter, " to who?\n"); + return TRUE; +} + + +/* + * lib_wear_backend() + * + * Common backend handler for wearing objects. Puts on all objects currently + * referenced in the game, moving objects to worn by player. + * + * Objects to action are flagged in object_references; objects requested but + * deemed not actionable are flagged in multiple_references. + */ +static void +lib_wear_backend (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object_count, object, count, trail; + sc_bool has_printed; + + /* + * Try game commands for all referenced objects first. If any succeed, + * remove that reference from the list. + */ + has_printed = FALSE; + object_count = gs_object_count (game); + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (lib_try_game_command_short (game, "wear", object)) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + } + } + + /* Wear every object referenced. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You put on ", + "I put on ", + "%player% puts on ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + gs_object_player_wear (game, object); + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You put on ", + "I put on ", + "%player% puts on ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + /* Note any remaining multiple references left out of the wear operation. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (gs_object_position (game, object) != OBJ_WORN_PLAYER) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are already wearing ", + "I am already wearing ", + "%player% is already wearing ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are already wearing ", + "I am already wearing ", + "%player% is already wearing ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (gs_object_position (game, object) == OBJ_HELD_PLAYER) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, " or "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You can't wear ", + "I can't wear ", + "%player% can't wear ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You can't wear ", + "I can't wear ", + "%player% can't wear ")); + } + else + pf_buffer_string (filter, " or "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } +} + + +/* + * lib_wear_filter() + * + * Helper function for deciding if an object may be worn in this context. + * Returns TRUE if an object may be worn, FALSE otherwise. + */ +static sc_bool +lib_wear_filter (sc_gameref_t game, sc_int object, sc_int unused) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + assert (unused == -1); + + /* + * The object is wearable if the player is holding it, and it's not static + * (static moved to player inventory by event), and if it's marked wearable + * in properties. + */ + if (gs_object_position (game, object) == OBJ_HELD_PLAYER + && !obj_is_static (game, object)) + { + sc_vartype_t vt_key[3]; + + /* Return wearability from the object properties. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Wearable"; + return prop_get_boolean (bundle, "B<-sis", vt_key); + } + + return FALSE; +} + + +/* + * lib_cmd_wear_all() + * + * Wear all wearable objects currently held by the player. + */ +sc_bool +lib_cmd_wear_all (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_wear_filter, -1, + NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_wear_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You're not carrying anything", + "I'm not carrying anything", + "%player%'s not carrying anything")); + pf_buffer_string (filter, " that can be worn."); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_wear_except_multiple() + * + * Wear all wearable objects currently held by the player, excepting those + * listed in %text%. + */ +sc_bool +lib_cmd_wear_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find retain target objects. */ + if (!lib_parse_multiple_objects (game, "retain", + lib_wear_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_wear_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_wear_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything", + "I am not holding anything", + "%player% is not holding anything")); + if (objects == 0) + pf_buffer_string (filter, " else"); + pf_buffer_string (filter, " that can be worn."); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_wear_multiple() + * + * Wear all objects currently held by the player, wearable, and listed + * in %text%. + */ +sc_bool +lib_cmd_wear_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find wear target objects. */ + if (!lib_parse_multiple_objects (game, "wear", + lib_wear_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_wear_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_wear_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything", + "I am not holding anything", + "%player% is not holding anything")); + pf_buffer_string (filter, " that can be worn."); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_remove_backend() + * + * Common backend handler for removing objects. Takes off on all objects + * currently referenced in the game, moving objects to held by player. + * + * Objects to action are flagged in object_references; objects requested but + * deemed not actionable are flagged in multiple_references. + */ +static void +lib_remove_backend (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object_count, object, count, trail; + sc_bool has_printed; + + /* + * Try game commands for all referenced objects first. If any succeed, + * remove that reference from the list. + */ + has_printed = FALSE; + object_count = gs_object_count (game); + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (lib_try_game_command_short (game, "remove", object)) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + } + } + + /* Remove every object referenced. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You remove ", + "I remove ", + "%player% removes ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + gs_object_player_get (game, object); + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You remove ", + "I remove ", + "%player% removes ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + /* Note any remaining multiple references left out of the remove operation. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not wearing ", + "I am not wearing ", + "%player% is not wearing ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not wearing ", + "I am not wearing ", + "%player% is not wearing ")); + } + else + pf_buffer_string (filter, " or "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '!'); + } +} + + +/* + * lib_remove_filter() + * + * Helper function for deciding if an object may be removed in this context. + * Returns TRUE if an object is currently being worn, FALSE otherwise. + */ +static sc_bool +lib_remove_filter (sc_gameref_t game, sc_int object, sc_int unused) +{ + assert (unused == -1); + + return !obj_is_static (game, object) + && gs_object_position (game, object) == OBJ_WORN_PLAYER; +} + + +/* + * lib_cmd_remove_all() + * + * Remove all objects currently held by the player. + */ +sc_bool +lib_cmd_remove_all (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_remove_filter, -1, + NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_remove_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You're not wearing anything", + "I'm not wearing anything", + "%player%'s not wearing anything")); + pf_buffer_string (filter, " that can be removed."); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_remove_except_multiple() + * + * Remove all objects currently worn by the player, excepting those listed + * in %text%. + */ +sc_bool +lib_cmd_remove_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find retain target objects. */ + if (!lib_parse_multiple_objects (game, "retain", + lib_remove_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_remove_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_remove_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not wearing anything", + "I am not wearing anything", + "%player% is not wearing anything")); + if (objects == 0) + pf_buffer_string (filter, " else"); + pf_buffer_string (filter, " that can be removed."); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_remove_multiple() + * + * Remove all objects currently worn by the player, and listed in %text%. + */ +sc_bool +lib_cmd_remove_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int objects, references; + + /* Parse the multiple objects list to find remove target objects. */ + if (!lib_parse_multiple_objects (game, "remove", + lib_remove_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_remove_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_remove_backend (game); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything", + "I am not holding anything", + "%player% is not holding anything")); + pf_buffer_string (filter, " that can be removed."); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_inventory() + * + * List objects carried and worn by the player. + */ +sc_bool +lib_cmd_inventory (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, count, trail; + sc_bool wearing; + + /* Find and list each object worn by the player. */ + count = 0; + trail = -1; + wearing = FALSE; + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_WORN_PLAYER) + { + if (count > 0) + { + if (count == 1) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are wearing ", + "I am wearing ", + "%player% is wearing ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are wearing ", + "I am wearing ", + "%player% is wearing ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + wearing = TRUE; + } + + /* Find and list each object owned by the player. */ + count = 0; + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_HELD_PLAYER) + { + if (count > 0) + { + if (count == 1) + { + if (wearing) + { + pf_buffer_string (filter, + lib_select_response (game, + ", and you are carrying ", + ", and I am carrying ", + ", and %player% is carrying ")); + } + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are carrying ", + "I am carrying ", + "%player% is carrying ")); + } + } + else + pf_buffer_string (filter, ", "); + lib_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + if (wearing) + { + pf_buffer_string (filter, + lib_select_response (game, + ", and you are carrying ", + ", and I am carrying ", + ", and %player% is carrying ")); + } + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are carrying ", + "I am carrying ", + "%player% is carrying ")); + } + } + else + pf_buffer_string (filter, " and "); + lib_print_object (game, trail); + pf_buffer_character (filter, '.'); + + /* Print contents of every container and surface carried. */ + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_HELD_PLAYER) + { + if (obj_is_container (game, object) + && gs_object_openness (game, object) <= OBJ_OPEN) + lib_list_in_object (game, object, TRUE); + + if (obj_is_surface (game, object)) + lib_list_on_object (game, object, TRUE); + } + } + pf_buffer_character (filter, '\n'); + } + else + { + if (wearing) + { + pf_buffer_string (filter, ", and "); + pf_buffer_string (filter, + lib_select_response (game, + "you are carrying nothing.\n", + "I am carrying nothing.\n", + "%player% is carrying nothing.\n")); + } + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are carrying nothing.\n", + "I am carrying nothing.\n", + "%player% is carrying nothing.\n")); + } + } + + /* Successful command. */ + return TRUE; +} + + +/* + * lib_cmd_open_object() + * + * Attempt to open the referenced object. + */ +sc_bool +lib_cmd_open_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, openness; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "open", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Get the current object openness. */ + openness = gs_object_openness (game, object); + + /* React to the request based on openness state. */ + switch (openness) + { + case OBJ_OPEN: + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is already open!\n", + " are already open!\n")); + return TRUE; + + case OBJ_CLOSED: + pf_buffer_string (filter, + lib_select_response (game, + "You open ", + "I open ", + "%player% opens ")); + lib_print_object_np (game, object); + pf_buffer_character (filter, '.'); + + /* Set open state, and list contents. */ + gs_set_object_openness (game, object, OBJ_OPEN); + lib_list_in_object (game, object, TRUE); + pf_buffer_character (filter, '\n'); + return TRUE; + + case OBJ_LOCKED: + pf_buffer_string (filter, + lib_select_response (game, + "You can't open ", + "I can't open ", + "%player% can't open ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " as it is locked!\n"); + return TRUE; + + default: + break; + } + + /* The object isn't openable. */ + pf_buffer_string (filter, + lib_select_response (game, + "You can't open ", + "I can't open ", + "%player% can't open ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; +} + + +/* + * lib_cmd_close_object() + * + * Attempt to close the referenced object. + */ +sc_bool +lib_cmd_close_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object, openness; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "close", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Get the current object openness. */ + openness = gs_object_openness (game, object); + + /* React to the request based on openness state. */ + switch (openness) + { + case OBJ_OPEN: + pf_buffer_string (filter, + lib_select_response (game, + "You close ", + "I close ", + "%player% closes ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + + /* Set closed state. */ + gs_set_object_openness (game, object, OBJ_CLOSED); + return TRUE; + + case OBJ_CLOSED: + case OBJ_LOCKED: + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is already closed!\n", + " are already closed!\n")); + return TRUE; + + default: + break; + } + + /* The object isn't closeable. */ + pf_buffer_string (filter, + lib_select_response (game, + "You can't close ", + "I can't close ", + "%player% can't close ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; +} + + +/* + * lib_attempt_key_acquisition() + * + * Automatically get an object being used as a key, if possible. + */ +static void +lib_attempt_key_acquisition (sc_gameref_t game, sc_int object) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Disallow getting static objects. */ + if (obj_is_static (game, object)) + return; + + /* If the object is not seen or available, reject the attempt. */ + if (!(gs_object_seen (game, object) + && obj_indirectly_in_room (game, object, gs_playerroom (game)))) + return; + + /* + * Check if we already have it, or are wearing it, or if a NPC has or is + * wearing it. + */ + if (gs_object_position (game, object) == OBJ_HELD_PLAYER + || gs_object_position (game, object) == OBJ_WORN_PLAYER + || gs_object_position (game, object) == OBJ_HELD_NPC + || gs_object_position (game, object) == OBJ_WORN_NPC) + return; + + /* + * If the object is contained in or on something we're already holding, + * capacity checks are meaningless. + */ + if (!obj_indirectly_held_by_player (game, object)) + { + if (lib_object_too_heavy (game, object, NULL) + || lib_object_too_large (game, object, NULL)) + return; + } + + /* Retry game commands for the object with a standard "get". */ + if (lib_try_game_command_short (game, "get", object)) + return; + + /* Note what we're doing. */ + if (gs_object_position (game, object) == OBJ_IN_OBJECT + || gs_object_position (game, object) == OBJ_ON_OBJECT) + { + pf_buffer_string (filter, "(Taking "); + lib_print_object_np (game, object); + + pf_buffer_string (filter, " from "); + lib_print_object_np (game, gs_object_parent (game, object)); + pf_buffer_string (filter, " first)\n"); + } + else + { + pf_buffer_string (filter, "(Picking up "); + lib_print_object_np (game, object); + pf_buffer_string (filter, " first)\n"); + } + + /* Take possession of the object. */ + gs_object_player_get (game, object); +} + + +/* + * lib_cmd_unlock_object_with() + * + * Attempt to unlock the referenced object. + */ +sc_bool +lib_cmd_unlock_object_with (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, key, openness; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "unlock", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* + * Now try to get the key from referenced text, and disambiguate as usual. + */ + if (!uip_match ("%object%", var_get_ref_text (vars), game)) + { + pf_buffer_string (filter, "What do you want to unlock that with?\n"); + return TRUE; + } + key = lib_disambiguate_object (game, "unlock that with", NULL); + if (key == -1) + return TRUE; + + /* React to the request based on openness state. */ + openness = gs_object_openness (game, object); + switch (openness) + { + case OBJ_OPEN: + case OBJ_CLOSED: + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is not locked!\n", + " are not locked!\n")); + return TRUE; + + case OBJ_LOCKED: + { + sc_vartype_t vt_key[3]; + sc_int key_index, the_key; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Key"; + key_index = prop_get_integer (bundle, "I<-sis", vt_key); + if (key_index == -1) + break; + + the_key = obj_dynamic_object (game, key_index); + if (the_key != key) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't unlock ", + "I can't unlock ", + "%player% can't unlock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with "); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + if (gs_object_position (game, key) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + gs_set_object_openness (game, object, OBJ_CLOSED); + pf_buffer_string (filter, + lib_select_response (game, + "You unlock ", + "I unlock ", + "%player% unlocks ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with "); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + default: + break; + } + + /* The object isn't lockable. */ + pf_buffer_string (filter, + lib_select_response (game, + "You can't unlock ", + "I can't unlock ", + "%player% can't unlock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + + +/* + * lib_cmd_unlock_object() + * + * Attempt to unlock the referenced object, automatically selecting key. + */ +sc_bool +lib_cmd_unlock_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, openness; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "unlock", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* React to the request based on openness state. */ + openness = gs_object_openness (game, object); + switch (openness) + { + case OBJ_OPEN: + case OBJ_CLOSED: + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is not locked!\n", + " are not locked!\n")); + return TRUE; + + case OBJ_LOCKED: + { + sc_vartype_t vt_key[3]; + sc_int key_index, key; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Key"; + key_index = prop_get_integer (bundle, "I<-sis", vt_key); + if (key_index == -1) + break; + + key = obj_dynamic_object (game, key_index); + lib_attempt_key_acquisition (game, key); + if (gs_object_position (game, key) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You don't have", + "I don't have", + "%player% doesn't have")); + pf_buffer_string (filter, " anything to unlock "); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with!\n"); + return TRUE; + } + + gs_set_object_openness (game, object, OBJ_CLOSED); + pf_buffer_string (filter, + lib_select_response (game, + "You unlock ", + "I unlock ", + "%player% unlocks ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with "); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + default: + break; + } + + /* The object isn't lockable. */ + pf_buffer_string (filter, + lib_select_response (game, + "You can't unlock ", + "I can't unlock ", + "%player% can't unlock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + + +/* + * lib_cmd_lock_object_with() + * + * Attempt to lock the referenced object. + */ +sc_bool +lib_cmd_lock_object_with (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, key, openness; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "lock", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* + * Now try to get the key from referenced text, and disambiguate as usual. + */ + if (!uip_match ("%object%", var_get_ref_text (vars), game)) + { + pf_buffer_string (filter, "What do you want to lock that with?\n"); + return TRUE; + } + key = lib_disambiguate_object (game, "lock that with", NULL); + if (key == -1) + return TRUE; + + /* React to the request based on openness state. */ + openness = gs_object_openness (game, object); + switch (openness) + { + case OBJ_OPEN: + pf_buffer_string (filter, + lib_select_response (game, + "You can't lock ", + "I can't lock ", + "%player% can't lock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " as it is open.\n"); + return TRUE; + + case OBJ_CLOSED: + { + sc_vartype_t vt_key[3]; + sc_int key_index, the_key; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Key"; + key_index = prop_get_integer (bundle, "I<-sis", vt_key); + if (key_index == -1) + break; + + the_key = obj_dynamic_object (game, key_index); + if (the_key != key) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't lock ", + "I can't lock ", + "%player% can't lock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with "); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + if (gs_object_position (game, key) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + gs_set_object_openness (game, object, OBJ_LOCKED); + pf_buffer_string (filter, lib_select_response (game, + "You lock ", + "I lock ", + "%player% locks ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with "); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + case OBJ_LOCKED: + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is already locked!\n", + " are already locked!\n")); + return TRUE; + + default: + break; + } + + /* The object isn't lockable. */ + pf_buffer_string (filter, + lib_select_response (game, + "You can't lock ", + "I can't lock ", + "%player% can't lock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + + +/* + * lib_cmd_lock_object() + * + * Attempt to lock the referenced object, automatically selecting key. + */ +sc_bool +lib_cmd_lock_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, openness; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "lock", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* React to the request based on openness state. */ + openness = gs_object_openness (game, object); + switch (openness) + { + case OBJ_OPEN: + pf_buffer_string (filter, + lib_select_response (game, + "You can't lock ", + "I can't lock ", + "%player% can't lock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " as it is open.\n"); + return TRUE; + + case OBJ_CLOSED: + { + sc_vartype_t vt_key[3]; + sc_int key_index, key; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Key"; + key_index = prop_get_integer (bundle, "I<-sis", vt_key); + if (key_index == -1) + break; + + key = obj_dynamic_object (game, key_index); + lib_attempt_key_acquisition (game, key); + if (gs_object_position (game, key) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You don't have", + "I don't have", + "%player% doesn't have")); + pf_buffer_string (filter, " anything to lock "); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with!\n"); + return TRUE; + } + + gs_set_object_openness (game, object, OBJ_LOCKED); + pf_buffer_string (filter, + lib_select_response (game, + "You lock ", + "I lock ", + "%player% locks ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, " with "); + lib_print_object_np (game, key); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + case OBJ_LOCKED: + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is already locked!\n", + " are already locked!\n")); + return TRUE; + + default: + break; + } + + /* The object isn't lockable. */ + pf_buffer_string (filter, + lib_select_response (game, + "You can't lock ", + "I can't lock ", + "%player% can't lock ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + + +/* + * lib_compare_subject() + * + * Compare a subject, comma or NUL terminated. Helper for ask. + */ +static sc_bool +lib_compare_subject (const sc_char *subject, sc_int posn, + const sc_char *string) +{ + sc_int word_posn, string_posn; + + /* Skip any leading subject spaces. */ + for (word_posn = posn; + subject[word_posn] != NUL && sc_isspace (subject[word_posn]);) + word_posn++; + for (string_posn = 0; + string[string_posn] != NUL && sc_isspace (string[string_posn]);) + string_posn++; + + /* Match characters from words with the string at position. */ + while (TRUE) + { + /* Any character mismatch means no match. */ + if (sc_tolower (subject[word_posn]) != sc_tolower (string[string_posn])) + return FALSE; + + /* Move to next character in each. */ + word_posn++; + string_posn++; + + /* + * If at space, advance over whitespace in subjects list. Stop when we + * hit the end of the element or list. + */ + while (sc_isspace (subject[word_posn]) + && subject[word_posn] != COMMA && subject[word_posn] != NUL) + subject++; + + /* Advance over whitespace in the current string too. */ + while (sc_isspace (string[string_posn]) && string[string_posn] != NUL) + string_posn++; + + /* + * If we found the end of the subject, and the end of the current string, + * we've matched. If not at the end of the current string, though, only + * a partial match. + */ + if (subject[word_posn] == NUL || subject[word_posn] == COMMA) + { + if (string[string_posn] == NUL) + break; + else + return FALSE; + } + } + + /* Matched in the loop; return TRUE. */ + return TRUE; +} + + +/* + * lib_npc_reply_to() + * + * Reply for an NPC on a given topic. Helper for ask. + */ +static sc_bool +lib_npc_reply_to (sc_gameref_t game, sc_int npc, sc_int topic) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int task; + const sc_char *response; + + /* Find any associated task to control response. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Topics"; + vt_key[3].integer = topic; + vt_key[4].string = "Task"; + task = prop_get_integer (bundle, "I<-sisis", vt_key); + + /* Get the response, and print if anything there. */ + if (task > 0 && gs_task_done (game, task - 1)) + vt_key[4].string = "AltReply"; + else + vt_key[4].string = "Reply"; + response = prop_get_string (bundle, "S<-sisis", vt_key); + if (!sc_strempty (response)) + { + pf_buffer_string (filter, response); + pf_buffer_character (filter, '\n'); + return TRUE; + } + + /* No response to this combination. */ + return FALSE; +} + + +/* + * lib_cmd_ask_npc_about() + * + * Converse with NPC. + */ +sc_bool +lib_cmd_ask_npc_about (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int npc, topic_count, topic, topic_match, default_topic; + sc_bool found, default_found, is_ambiguous; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "ask", &is_ambiguous); + if (npc == -1) + return is_ambiguous; + + if (lib_trace) + sc_trace ("Library: asking NPC %ld\n", npc); + + /* Get the topics the NPC converses about. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Topics"; + topic_count = prop_get_child_count (bundle, "I<-sis", vt_key); + topic_match = default_topic = -1; + found = default_found = FALSE; + for (topic = 0; topic < topic_count; topic++) + { + const sc_char *subjects; + sc_int posn; + + /* Get subject list for this topic. */ + vt_key[3].integer = topic; + vt_key[4].string = "Subject"; + subjects = prop_get_string (bundle, "S<-sisis", vt_key); + + /* If this is the special "*" topic, note and continue. */ + if (!sc_strcasecmp (subjects, "*")) + { + if (lib_trace) + sc_trace ("Library: \"*\" is %ld\n", topic); + + default_topic = topic; + default_found = TRUE; + continue; + } + + /* Split into subjects by comma delimiter. */ + for (posn = 0; subjects[posn] != NUL;) + { + if (lib_trace) + sc_trace ("Library: subject %s[%ld]\n", subjects, posn); + + /* See if this subject matches. */ + if (lib_compare_subject (subjects, posn, var_get_ref_text (vars))) + { + if (lib_trace) + sc_trace ("Library: matched\n"); + + topic_match = topic; + found = TRUE; + break; + } + + /* Move to next subject, or end of list. */ + while (subjects[posn] != COMMA && subjects[posn] != NUL) + posn++; + if (subjects[posn] == COMMA) + posn++; + } + } + + /* Handle any matched subject first, and "*" second. */ + if (found && lib_npc_reply_to (game, npc, topic_match)) + return TRUE; + else if (default_found && lib_npc_reply_to (game, npc, default_topic)) + return TRUE; + + /* NPC has no response. */ + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, + lib_select_response (game, + " does not respond to your question.\n", + " does not respond to my question.\n", + " does not respond to %player%'s question.\n")); + return TRUE; +} + + +/* + * lib_check_put_in_recursion() + * + * Checks for infinite recursion when placing an object in an object. Returns + * TRUE if no recursion detected. + */ +static sc_bool +lib_check_put_in_recursion (sc_gameref_t game, + sc_int object, sc_int container, sc_bool report) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int check; + + /* Avoid the obvious possibility of infinite recursion. */ + if (container == object) + { + if (report) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't put an object inside itself!", + "I can't put an object inside itself!", + "%player% can't put an object inside itself!")); + } + return FALSE; + } + + /* Avoid the subtle possibility of infinite recursion. */ + check = container; + while (gs_object_position (game, check) == OBJ_ON_OBJECT + || gs_object_position (game, check) == OBJ_IN_OBJECT) + { + check = gs_object_parent (game, check); + if (check == object) + { + if (report) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't put an object inside one", + "I can't put an object inside one", + "%player% can't put an object inside one")); + pf_buffer_string (filter, " it's on or in!"); + } + return FALSE; + } + } + + /* No infinite recursion detected. */ + return TRUE; +} + + +/* + * lib_put_in_backend() + * + * Common backend handler for placing objects in containers. Places all + * objects currently referenced in the game into a container, trying game + * commands first, and then moving other unhandled objects into the container. + * + * Objects to action are flagged in object_references; objects requested but + * deemed not actionable are flagged in multiple_references. + */ +static void +lib_put_in_backend (sc_gameref_t game, sc_int container) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object_count, object, count, trail, capacity, maxsize; + sc_bool has_printed; + + /* + * Try game commands for all referenced objects first. If any succeed, + * remove that reference from the list. At the same time, check for and + * weed out any moves that result in infinite recursion. + */ + has_printed = FALSE; + object_count = gs_object_count (game); + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + /* Reject and remove attempts to place objects in themselves. */ + if (!lib_check_put_in_recursion (game, object, container, !has_printed)) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + continue; + } + + if (lib_try_game_command_with_object (game, + "put", object, "in", container)) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + } + } + + /* Retrieve the container's limits. */ + maxsize = obj_get_container_maxsize (game, container); + capacity = obj_get_container_capacity (game, container); + + /* Put in every object that remains referenced. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + /* If too big, or exceeds container limits, ignore for now. */ + if (obj_get_size (game, object) > maxsize) + continue; + else + { + sc_int other, contains; + + contains = 0; + for (other = 0; other < gs_object_count (game); other++) + { + if (gs_object_position (game, other) == OBJ_IN_OBJECT + && gs_object_parent (game, other) == container) + contains++; + } + if (contains >= capacity) + continue; + } + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You put ", + "I put ", + "%player% puts ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + gs_object_move_into (game, object, container); + game->object_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You put ", + "I put ", + "%player% puts ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_string (filter, " inside "); + lib_print_object_np (game, container); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + /* + * Report objects not put in because of their size. These objects remain in + * standard references, as do objects rejected because of capacity limits. + * By removing too large objects in this loop, we're left later on with just + * the objects rejected by capacity limits. + */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (!(obj_get_size (game, object) > maxsize)) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, trail); + } + else + pf_buffer_string (filter, ", "); + } + trail = object; + count++; + + game->object_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, trail); + pf_buffer_string (filter, + lib_select_plurality (game, trail, + " is too big", + " are too big")); + } + else + { + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_string (filter, " are too big"); + } + pf_buffer_string (filter, " to fit inside "); + lib_print_object_np (game, container); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + /* + * Report objects not put in because the container is too full. This should + * be all remaining objects in standard references. + */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->object_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_new_sentence (filter); + lib_print_object_np (game, trail); + } + else + { + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + } + pf_buffer_string (filter, " can't fit inside "); + lib_print_object_np (game, container); + pf_buffer_string (filter, " at the moment."); + } + has_printed |= count > 0; + + /* Note any remaining multiple references left out of the operation. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, " or "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } +} + + +/* + * lib_put_in_filter() + * lib_put_in_not_container_filter() + * + * Helper functions for deciding if an object may be put in another this + * context. Returns TRUE if an object may be manipulated, FALSE otherwise. + */ +static sc_bool +lib_put_in_filter (sc_gameref_t game, sc_int object, sc_int unused) +{ + assert (unused == -1); + + return !obj_is_static (game, object) + && gs_object_position (game, object) == OBJ_HELD_PLAYER; +} + +static sc_bool +lib_put_in_not_container_filter (sc_gameref_t game, + sc_int object, sc_int container) +{ + return lib_put_in_filter (game, object, -1) && object != container; +} + + +/* + * lib_put_in_is_valid() + * + * Validate the container requested in "put in" commands. + */ +static sc_bool +lib_put_in_is_valid (sc_gameref_t game, sc_int container) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Verify that the container object is a container. */ + if (!obj_is_container (game, container)) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't put anything inside ", + "I can't put anything inside ", + "%player% can't put anything inside ")); + lib_print_object_np (game, container); + pf_buffer_string (filter, "!\n"); + return FALSE; + } + + /* If the container is closed, reject now. */ + if (gs_object_openness (game, container) > OBJ_OPEN) + { + pf_new_sentence (filter); + lib_print_object_np (game, container); + pf_buffer_string (filter, + lib_select_plurality (game, container, " is", " are")); + if (gs_object_openness (game, container) == OBJ_LOCKED) + pf_buffer_string (filter, " locked!\n"); + else + pf_buffer_string (filter, " closed!\n"); + return FALSE; + } + + /* Container is a valid target for "put in". */ + return TRUE; +} + + +/* + * lib_cmd_put_all_in() + * + * Put all objects currently held by the player into a container. + */ +sc_bool +lib_cmd_put_all_in (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int container, objects; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + container = lib_disambiguate_object (game, "put that into", &is_ambiguous); + if (container == -1) + return is_ambiguous; + + /* Validate the container object to take from. */ + if (!lib_put_in_is_valid (game, container)) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_put_in_not_container_filter, + container, NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_put_in_backend (game, container); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You're not carrying anything", + "I'm not carrying anything", + "%player%'s not carrying anything")); + if (obj_indirectly_held_by_player (game, container)) + pf_buffer_string (filter, " else"); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_put_in_except_multiple() + * + * Put all objects currently held by the player into an object, excepting + * those listed in %text%. + */ +sc_bool +lib_cmd_put_in_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int container, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + container = lib_disambiguate_object (game, "put that into", &is_ambiguous); + if (container == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find retain target objects. */ + if (!lib_parse_multiple_objects (game, "retain", + lib_put_in_not_container_filter, + container, &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Validate the container object to put into. */ + if (!lib_put_in_is_valid (game, container)) + return TRUE; + + /* As a special case, complain about requests to retain the container. */ + if (game->multiple_references[container]) + { + pf_buffer_string (filter, + "I only understood you as far as wanting to retain "); + lib_print_object_np (game, container); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_put_in_not_container_filter, + container, &references); + if (objects > 0 || references > 0) + lib_put_in_backend (game, container); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything", + "I am not holding anything", + "%player% is not holding anything")); + if (objects == 0) + pf_buffer_string (filter, " else"); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_put_in_multiple() + * + * Put all objects currently held by the player and listed in %text% into an + * object. + */ +sc_bool +lib_cmd_put_in_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int container, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + container = lib_disambiguate_object (game, "put that into", &is_ambiguous); + if (container == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find retain target objects. */ + if (!lib_parse_multiple_objects (game, "move", + lib_put_in_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Validate the container object to put into. */ + if (!lib_put_in_is_valid (game, container)) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_put_in_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_put_in_backend (game, container); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything.", + "I am not holding anything.", + "%player% is not holding anything.")); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_check_put_on_recursion() + * + * Checks for infinite recursion when placing an object on an object. Returns + * TRUE if no recursion detected. + */ +static sc_bool +lib_check_put_on_recursion (sc_gameref_t game, + sc_int object, sc_int supporter, sc_bool report) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int check; + + /* Avoid the obvious possibility of infinite recursion. */ + if (supporter == object) + { + if (report) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't put an object onto itself!", + "I can't put an object onto itself!", + "%player% can't put an object onto itself!")); + } + return FALSE; + } + + /* Avoid the subtle possibility of infinite recursion. */ + check = supporter; + while (gs_object_position (game, check) == OBJ_ON_OBJECT + || gs_object_position (game, check) == OBJ_IN_OBJECT) + { + check = gs_object_parent (game, check); + if (check == object) + { + if (report) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't put an object onto one", + "I can't put an object onto one", + "%player% can't put an object onto one")); + pf_buffer_string (filter, " it's on or in!"); + } + return FALSE; + } + } + + /* No infinite recursion detected. */ + return TRUE; +} + + +/* + * lib_put_on_backend() + * + * Common backend handler for placing objects on supporters. Places all + * objects currently referenced in the game onto a supporter, trying game + * commands first, and then moving other unhandled objects onto the supporter. + * + * Objects to action are flagged in object_references; objects requested but + * deemed not actionable are flagged in multiple_references. + */ +static void +lib_put_on_backend (sc_gameref_t game, sc_int supporter) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object_count, object, count, trail; + sc_bool has_printed; + + /* + * Try game commands for all referenced objects first. If any succeed, + * remove that reference from the list. At the same time, check for and + * weed out any moves that result in infinite recursion. + */ + has_printed = FALSE; + object_count = gs_object_count (game); + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + /* Reject and remove attempts to place objects on themselves. */ + if (!lib_check_put_on_recursion (game, object, supporter, !has_printed)) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + continue; + } + + if (lib_try_game_command_with_object (game, + "put", object, "on", supporter)) + { + game->object_references[object] = FALSE; + has_printed = TRUE; + } + } + + /* Put on every object that remains referenced. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->object_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You put ", + "I put ", + "%player% puts ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + gs_object_move_onto (game, object, supporter); + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You put ", + "I put ", + "%player% puts ")); + } + else + pf_buffer_string (filter, " and "); + lib_print_object_np (game, trail); + pf_buffer_string (filter, " onto "); + lib_print_object_np (game, supporter); + pf_buffer_character (filter, '.'); + } + has_printed |= count > 0; + + /* Note any remaining multiple references left out of the operation. */ + count = 0; + trail = -1; + for (object = 0; object < object_count; object++) + { + if (!game->multiple_references[object]) + continue; + + if (count > 0) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, ", "); + lib_print_object_np (game, trail); + } + trail = object; + count++; + + game->multiple_references[object] = FALSE; + } + + if (count >= 1) + { + if (count == 1) + { + if (has_printed) + pf_buffer_string (filter, " "); + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + } + else + pf_buffer_string (filter, " or "); + lib_print_object_np (game, trail); + pf_buffer_character (filter, '.'); + } +} + + +/* + * lib_put_on_filter() + * lib_put_on_not_supporter_filter() + * + * Helper functions for deciding if an object may be put on another this + * context. Returns TRUE if an object may be manipulated, FALSE otherwise. + */ +static sc_bool +lib_put_on_filter (sc_gameref_t game, sc_int object, sc_int unused) +{ + assert (unused == -1); + + return !obj_is_static (game, object) + && gs_object_position (game, object) == OBJ_HELD_PLAYER; +} + +static sc_bool +lib_put_on_not_supporter_filter (sc_gameref_t game, + sc_int object, sc_int supporter) +{ + return lib_put_on_filter (game, object, -1) && object != supporter; +} + + +/* + * lib_put_on_is_valid() + * + * Validate the supporter requested in "put on" commands. + */ +static sc_bool +lib_put_on_is_valid (sc_gameref_t game, sc_int supporter) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Verify that the supporter object is a supporter. */ + if (!obj_is_surface (game, supporter)) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't put anything on ", + "I can't put anything on ", + "%player% can't put anything on ")); + lib_print_object_np (game, supporter); + pf_buffer_string (filter, "!\n"); + return FALSE; + } + + /* Surface is a valid target for "put on". */ + return TRUE; +} + + +/* + * lib_cmd_put_all_on() + * + * Put all objects currently held by the player onto a supporter. + */ +sc_bool +lib_cmd_put_all_on (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int supporter, objects; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + supporter = lib_disambiguate_object (game, "put that onto", &is_ambiguous); + if (supporter == -1) + return is_ambiguous; + + /* Validate the supporter object to take from. */ + if (!lib_put_on_is_valid (game, supporter)) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + gs_set_multiple_references (game); + objects = lib_apply_multiple_filter (game, + lib_put_on_not_supporter_filter, + supporter, NULL); + gs_clear_multiple_references (game); + if (objects > 0) + lib_put_on_backend (game, supporter); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You're not carrying anything", + "I'm not carrying anything", + "%player%'s not carrying anything")); + if (obj_indirectly_held_by_player (game, supporter)) + pf_buffer_string (filter, " else"); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_put_on_except_multiple() + * + * Put all objects currently held by the player onto an object, excepting + * those listed in %text%. + */ +sc_bool +lib_cmd_put_on_except_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int supporter, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + supporter = lib_disambiguate_object (game, "put that onto", &is_ambiguous); + if (supporter == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find retain target objects. */ + if (!lib_parse_multiple_objects (game, "retain", + lib_put_on_not_supporter_filter, + supporter, &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Validate the supporter object to put into. */ + if (!lib_put_on_is_valid (game, supporter)) + return TRUE; + + /* As a special case, complain about requests to retain the supporter. */ + if (game->multiple_references[supporter]) + { + pf_buffer_string (filter, + "I only understood you as far as wanting to retain "); + lib_print_object_np (game, supporter); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_except_filter (game, + lib_put_on_not_supporter_filter, + supporter, &references); + if (objects > 0 || references > 0) + lib_put_on_backend (game, supporter); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything", + "I am not holding anything", + "%player% is not holding anything")); + if (objects == 0) + pf_buffer_string (filter, " else"); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_put_on_multiple() + * + * Put all objects currently held by the player and listed in %text% onto an + * object. + */ +sc_bool +lib_cmd_put_on_multiple (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int supporter, objects, references; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + supporter = lib_disambiguate_object (game, "put that onto", &is_ambiguous); + if (supporter == -1) + return is_ambiguous; + + /* Parse the multiple objects list to find retain target objects. */ + if (!lib_parse_multiple_objects (game, "move", + lib_put_on_filter, -1, + &references)) + return FALSE; + else if (references == 0) + return TRUE; + + /* Validate the supporter object to put into. */ + if (!lib_put_on_is_valid (game, supporter)) + return TRUE; + + /* Filter objects into references, then handle with the backend. */ + objects = lib_apply_multiple_filter (game, + lib_put_on_filter, -1, + &references); + if (objects > 0 || references > 0) + lib_put_on_backend (game, supporter); + else + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding anything.", + "I am not holding anything.", + "%player% is not holding anything.")); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + + +/* + * lib_cmd_read_object() + * lib_cmd_read_other() + * + * Attempt to read the referenced object, or something else. + */ +sc_bool +lib_cmd_read_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int object, task; + sc_bool is_readable, is_ambiguous; + const sc_char *readtext, *description; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "read", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Verify that the object is readable. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Readable"; + is_readable = prop_get_boolean (bundle, "B<-sis", vt_key); + if (!is_readable) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't read ", + "I can't read ", + "%player% can't read ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; + } + + /* Get and print the object's read text, if any. */ + vt_key[2].string = "ReadText"; + readtext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (readtext)) + { + pf_buffer_string (filter, readtext); + pf_buffer_character (filter, '\n'); + return TRUE; + } + + /* Degrade to a shortened object examine. */ + vt_key[2].string = "Task"; + task = prop_get_integer (bundle, "I<-sis", vt_key) - 1; + + /* Select either the main or the alternate description. */ + if (task >= 0 && gs_task_done (game, task)) + vt_key[2].string = "AltDesc"; + else + vt_key[2].string = "Description"; + + /* Print the description, or a "nothing special" default. */ + description = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (description)) + pf_buffer_string (filter, description); + else + { + pf_buffer_string (filter, "There is nothing special about "); + lib_print_object_np (game, object); + pf_buffer_character (filter, '.'); + } + + pf_buffer_character (filter, '\n'); + return TRUE; +} + +sc_bool +lib_cmd_read_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Reject the attempt. */ + pf_buffer_string (filter, + lib_select_response (game, + "You see no such thing.\n", + "I see no such thing.\n", + "%player% sees no such thing.\n")); + return TRUE; +} + + +/* + * lib_cmd_attack_npc() + * lib_cmd_attack_npc_with() + * + * Attempt to attack an NPC, with and without weaponry. + */ +sc_bool +lib_cmd_attack_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int npc; + sc_bool is_ambiguous; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "attack", &is_ambiguous); + if (npc == -1) + return is_ambiguous; + + /* Print a standard response. */ + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, + lib_select_response (game, + " avoids your feeble attempts.\n", + " avoids my feeble attempts.\n", + " avoids %player%'s feeble attempts.\n")); + return TRUE; +} + +sc_bool +lib_cmd_attack_npc_with (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, npc; + sc_vartype_t vt_key[3]; + sc_bool weapon, is_ambiguous; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "attack", &is_ambiguous); + if (npc == -1) + return is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "attack with", NULL); + if (object == -1) + return TRUE; + + /* Ensure the referenced object is held. */ + if (gs_object_position (game, object) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Check for static object moved to player by event. */ + if (obj_is_static (game, object)) + { + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, " is", " are")); + pf_buffer_string (filter, " not a weapon.\n"); + return TRUE; + } + + /* Print standard response depending on if the object is a weapon. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Weapon"; + weapon = prop_get_boolean (bundle, "B<-sis", vt_key); + if (weapon) + { + pf_buffer_string (filter, + lib_select_response (game, + "You swing at ", + "I swing at ", + "%player% swings at ")); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " with "); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_response (game, + " but you miss.\n", + " but I miss.\n", + " but misses.\n")); + } + else + { + /* + * TODO Adrift uses "affective" [sic] here. Should SCARE be right, or + * bug-compatible? + */ + pf_buffer_string (filter, "I don't think "); + lib_print_object_np (game, object); + pf_buffer_string (filter, " would be a very effective weapon.\n"); + } + return TRUE; +} + + +/* + * lib_cmd_kiss_npc() + * lib_cmd_kiss_object() + * lib_cmd_kiss_other() + * + * Reject romantic advances in all cases. + */ +sc_bool +lib_cmd_kiss_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int npc, gender; + sc_bool is_ambiguous; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "kiss", &is_ambiguous); + if (npc == -1) + return is_ambiguous; + + /* Reject this attempt. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Gender"; + gender = prop_get_integer (bundle, "I<-sis", vt_key); + + switch (gender) + { + case NPC_MALE: + pf_buffer_string (filter, "I'm not sure he would appreciate that!\n"); + break; + + case NPC_FEMALE: + pf_buffer_string (filter, "I'm not sure she would appreciate that!\n"); + break; + + case NPC_NEUTER: + pf_buffer_string (filter, "I'm not sure it would appreciate that!\n"); + break; + + default: + sc_error ("lib_cmd_kiss_npc: unknown gender, %ld\n", gender); + } + return TRUE; +} + +sc_bool +lib_cmd_kiss_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "kiss", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Reject this attempt. */ + pf_buffer_string (filter, "I'm not sure "); + lib_print_object_np (game, object); + pf_buffer_string (filter, " would appreciate that.\n"); + return TRUE; +} + +sc_bool +lib_cmd_kiss_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Reject this attempt. */ + pf_buffer_string (filter, "I'm not sure it would appreciate that.\n"); + return TRUE; +} + + +/* + * lib_cmd_buy_object() + * lib_cmd_buy_other() + * + * Standard responses to attempts to buy something. + */ +sc_bool +lib_cmd_buy_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "buy", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Reject this attempt. */ + pf_buffer_string (filter, "I don't think "); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, " is", " are")); + pf_buffer_string (filter, " for sale.\n"); + return TRUE; +} + +sc_bool +lib_cmd_buy_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Reject this attempt. */ + pf_buffer_string (filter, "I don't think that is for sale.\n"); + return TRUE; +} + + +/* + * lib_cmd_break_object() + * lib_cmd_break_other() + * + * Standard responses to attempts to break something. + */ +sc_bool +lib_cmd_break_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "break", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Reject this attempt. */ + pf_buffer_string (filter, + lib_select_response (game, + "You might need ", + "I might need ", + "%player% might need ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +sc_bool +lib_cmd_break_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Reject this attempt. */ + pf_buffer_string (filter, + lib_select_response (game, + "You might need that.\n", + "I might need that.\n", + "%player% might need that.\n")); + return TRUE; +} + + +/* + * lib_cmd_smell_object() + * lib_cmd_smell_other() + * + * Standard responses to attempts to smell something. + */ +sc_bool +lib_cmd_smell_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "smell", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Reject this attempt. */ + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, " smells normal.\n"); + return TRUE; +} + +sc_bool +lib_cmd_smell_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Reject this attempt. */ + pf_buffer_string (filter, "That smells normal.\n"); + return TRUE; +} + + +/* + * lib_cmd_sell_object() + * lib_cmd_sell_other() + * + * Standard responses to attempts to sell something. + */ +sc_bool +lib_cmd_sell_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "sell", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Reject this attempt. */ + pf_buffer_string (filter, "No-one is interested in buying "); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +sc_bool +lib_cmd_sell_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "No-one is interested in buying that.\n"); + return TRUE; +} + + +/* + * lib_cmd_eat_object() + * + * Consume edible objects. + */ +sc_bool +lib_cmd_eat_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int object; + sc_bool edible, is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "eat", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Check that we have the object to eat. */ + if (gs_object_position (game, object) != OBJ_HELD_PLAYER) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not holding ", + "I am not holding ", + "%player% is not holding ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Check for static object moved to player by event. */ + if (obj_is_static (game, object)) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't eat ", + "I can't eat ", + "%player% can't eat ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Is this object inedible? */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Edible"; + edible = prop_get_boolean (bundle, "B<-sis", vt_key); + if (!edible) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't eat ", + "I can't eat ", + "%player% can't eat ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + + /* Confirm, and hide the object. */ + pf_buffer_string (filter, + lib_select_response (game, + "You eat ", + "I eat ", "%player% eats ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, + ". Not bad, but it could do with a pinch of salt!\n"); + gs_object_make_hidden (game, object); + return TRUE; +} + + +/* Enumerated sit/stand/lie types. */ +enum +{ OBJ_STANDABLE_MASK = 1 << 0, + OBJ_LIEABLE_MASK = 1 << 1 +}; +enum +{ MOVE_SIT, MOVE_SIT_FLOOR, + MOVE_STAND, MOVE_STAND_FLOOR, MOVE_LIE, MOVE_LIE_FLOOR +}; + +/* + * lib_stand_sit_lie() + * + * Central handler for stand, sit, and lie commands. + */ +static sc_bool +lib_stand_sit_lie (sc_gameref_t game, sc_int movement) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, position; + const sc_char *already_doing_that, *success_message; + + /* Initialize variables to avoid gcc warnings. */ + object = -1; + already_doing_that = FALSE; + success_message = FALSE; + position = 0; + + /* Get a target object for movement, -1 if floor. */ + switch (movement) + { + case MOVE_STAND: + case MOVE_SIT: + case MOVE_LIE: + { + const sc_char *disambiguate, *cant_do_that; + sc_int sit_lie_flags, movement_mask; + sc_vartype_t vt_key[3]; + sc_bool is_ambiguous; + + /* Initialize variables to avoid gcc warnings. */ + disambiguate = NULL; + cant_do_that = NULL; + movement_mask = 0; + + /* Set disambiguation and not amenable messages. */ + switch (movement) + { + case MOVE_STAND: + disambiguate = "stand on"; + cant_do_that = lib_select_response (game, + "You can't stand on ", + "I can't stand on ", + "%player% can't stand on "); + movement_mask = OBJ_STANDABLE_MASK; + break; + case MOVE_SIT: + disambiguate = "sit on"; + cant_do_that = lib_select_response (game, + "You can't sit on ", + "I can't sit on ", + "%player% can't sit on "); + movement_mask = OBJ_STANDABLE_MASK; + break; + case MOVE_LIE: + disambiguate = "lie on"; + cant_do_that = lib_select_response (game, + "You can't lie on ", + "I can't lie on ", + "%player% can't lie on "); + movement_mask = OBJ_LIEABLE_MASK; + break; + default: + sc_fatal ("lib_sit_stand_lie: movement error, %ld\n", movement); + } + + /* Get the referenced object; if none, consider complete. */ + object = lib_disambiguate_object (game, disambiguate, &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Verify the referenced object is amenable. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "SitLie"; + sit_lie_flags = prop_get_integer (bundle, "I<-sis", vt_key); + if (!(sit_lie_flags & movement_mask)) + { + pf_buffer_string (filter, cant_do_that); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; + } + break; + } + + case MOVE_STAND_FLOOR: + case MOVE_SIT_FLOOR: + case MOVE_LIE_FLOOR: + object = -1; + break; + + default: + sc_fatal ("lib_sit_stand_lie: movement error, %ld\n", movement); + } + + /* Set up confirmation messages and position. */ + switch (movement) + { + case MOVE_STAND: + already_doing_that = lib_select_response (game, + "You are already standing on ", + "I am already standing on ", + "%player% is already standing on "); + success_message = lib_select_response (game, + "You stand on ", + "I stand on ", + "%player% stands on "); + position = 0; + break; + + case MOVE_STAND_FLOOR: + already_doing_that = lib_select_response (game, + "You are already standing!\n", + "I am already standing!\n", + "%player% is already standing!\n"); + success_message = lib_select_response (game, + "You stand up", + "I stand up", + "%player% stands up"); + position = 0; + break; + + case MOVE_SIT: + already_doing_that = lib_select_response (game, + "You are already sitting on ", + "I am already sitting on ", + "%player% is already sitting on "); + if (gs_playerposition (game) == 2) + success_message = lib_select_response (game, + "You sit up on ", + "I sit up on ", + "%player% sits up on "); + else + success_message = lib_select_response (game, + "You sit down on ", + "I sit down on ", + "%player% sits down on "); + position = 1; + break; + + case MOVE_SIT_FLOOR: + already_doing_that = lib_select_response (game, + "You are already sitting down.\n", + "I am already sitting down.\n", + "%player% is already sitting down.\n"); + if (gs_playerposition (game) == 2) + success_message = lib_select_response (game, + "You sit up on the ground.\n", + "I sit up on the ground.\n", + "%player% sits up on the ground.\n"); + else + success_message = lib_select_response (game, + "You sit down on the ground.\n", + "I sit down on the ground.\n", + "%player% sits down on the ground.\n"); + position = 1; + break; + + case MOVE_LIE: + already_doing_that = lib_select_response (game, + "You are already lying on ", + "I am already lying on ", + "%player% is already lying on "); + success_message = lib_select_response (game, + "You lie down on ", + "I lie down on ", + "%player% lies down on "); + position = 2; + break; + + case MOVE_LIE_FLOOR: + already_doing_that = lib_select_response (game, + "You are already lying down.\n", + "I am already lying down.\n", + "%player% is already lying down.\n"); + success_message = lib_select_response (game, + "You lie down on the ground.\n", + "I lie down on the ground.\n", + "%player% lies down on the ground.\n"); + position = 2; + break; + + default: + sc_fatal ("lib_sit_stand_lie: movement error, %ld\n", movement); + } + + /* See if already doing this. */ + if (gs_playerposition (game) == position && gs_playerparent (game) == object) + { + pf_buffer_string (filter, already_doing_that); + if (object != -1) + { + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + } + return TRUE; + } + + /* Confirm movement, with special case for getting off an object. */ + pf_buffer_string (filter, success_message); + if (movement == MOVE_STAND_FLOOR) + { + if (gs_playerparent (game) != -1) + { + pf_buffer_string (filter, " from "); + lib_print_object_np (game, gs_playerparent (game)); + } + pf_buffer_string (filter, ".\n"); + } + else if (object != -1) + { + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + } + + /* Adjust player position and parent. */ + gs_set_playerposition (game, position); + gs_set_playerparent (game, object); + return TRUE; +} + + +/* + * lib_cmd_stand_* + * lib_cmd_sit_* + * lib_cmd_lie_* + * + * Stand, sit, or lie on an object, or on the floor. + */ +sc_bool +lib_cmd_stand_on_object (sc_gameref_t game) +{ + return lib_stand_sit_lie (game, MOVE_STAND); +} + +sc_bool +lib_cmd_stand_on_floor (sc_gameref_t game) +{ + return lib_stand_sit_lie (game, MOVE_STAND_FLOOR); +} + +sc_bool +lib_cmd_sit_on_object (sc_gameref_t game) +{ + return lib_stand_sit_lie (game, MOVE_SIT); +} + +sc_bool +lib_cmd_sit_on_floor (sc_gameref_t game) +{ + return lib_stand_sit_lie (game, MOVE_SIT_FLOOR); +} + +sc_bool +lib_cmd_lie_on_object (sc_gameref_t game) +{ + return lib_stand_sit_lie (game, MOVE_LIE); +} + +sc_bool +lib_cmd_lie_on_floor (sc_gameref_t game) +{ + return lib_stand_sit_lie (game, MOVE_LIE_FLOOR); +} + + +/* + * lib_cmd_get_off_object() + * lib_cmd_get_off() + * + * Get off whatever supporter the player rests on. + */ +sc_bool +lib_cmd_get_off_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object; if none, consider complete. */ + object = lib_disambiguate_object (game, "get off", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Reject the attempt if the player is not on the given object. */ + if (gs_playerparent (game) != object) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not on ", + "I am not on ", + "%player% is not on ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; + } + + /* Confirm movement. */ + pf_buffer_string (filter, + lib_select_response (game, + "You get off ", "I get off ", + "%player% gets off ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + + /* Adjust player position and parent. */ + gs_set_playerposition (game, 0); + gs_set_playerparent (game, -1); + return TRUE; +} + +sc_bool +lib_cmd_get_off (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Reject the attempt if the player is not on anything. */ + if (gs_playerparent (game) == -1) + { + pf_buffer_string (filter, + lib_select_response (game, + "You are not on anything!\n", + "I am not on anything!\n", + "%player% is not on anything!\n")); + return TRUE; + } + + /* Confirm movement. */ + pf_buffer_string (filter, + lib_select_response (game, + "You get off ", "I get off ", + "%player% gets off ")); + lib_print_object_np (game, gs_playerparent (game)); + pf_buffer_string (filter, ".\n"); + + /* Adjust player position and parent. */ + gs_set_playerposition (game, 0); + gs_set_playerparent (game, -1); + return TRUE; +} + + +/* + * lib_cmd_save() + * lib_cmd_restore() + * + * Save/restore a game. + */ +sc_bool +lib_cmd_save (sc_gameref_t game) +{ + if (if_confirm (SC_CONF_SAVE)) + { + if (ser_save_game_prompted (game)) + if_print_string ("Ok.\n"); + else + if_print_string ("Save failed.\n"); + } + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_restore (sc_gameref_t game) +{ + if (if_confirm (SC_CONF_RESTORE)) + { + if (ser_load_game_prompted (game)) + { + if_print_string ("Ok.\n"); + game->is_running = FALSE; + game->do_restore = TRUE; + } + else + if_print_string ("Restore failed.\n"); + } + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_locate_object() + * lib_cmd_locate_npc() + * + * Display the location of a selected object, and selected NPC. + */ +sc_bool +lib_cmd_locate_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int index_, count, object, room, position, parent; + + game->is_admin = TRUE; + + /* + * Filter to remove unseen object references. Note that this is different + * from NPCs, who we acknowledge even when unseen. + */ + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (!gs_object_seen (game, index_)) + game->object_references[index_] = FALSE; + } + + /* Count the number of objects referenced by the last command. */ + count = 0; + object = -1; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (game->object_references[index_]) + { + count++; + object = index_; + } + } + + /* + * If no objects identified, be coy about revealing anything; if more than + * one, be vague. + */ + if (count == 0) + { + pf_buffer_string (filter, "I don't know where that is.\n"); + return TRUE; + } + else if (count > 1) + { + pf_buffer_string (filter, + "Please be more clear about what you want to" + " locate.\n"); + return TRUE; + } + + /* + * The reference is unambiguous, so we're responsible for noting it in + * variables. Disambiguation would normally do this for us, but we just + * bypassed it. + */ + var_set_ref_object (vars, object); + + /* See if we can print a message based on position and parent. */ + position = gs_object_position (game, object); + parent = gs_object_parent (game, object); + switch (position) + { + case OBJ_HIDDEN: + if (!obj_is_static (game, object)) + { + pf_buffer_string (filter, "I don't know where that is.\n"); + return TRUE; + } + break; + + case OBJ_HELD_PLAYER: + pf_new_sentence (filter); + pf_buffer_string (filter, + lib_select_response (game, + "You are carrying ", + "I am carrying ", + "%player% is carrying ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; + + case OBJ_WORN_PLAYER: + pf_new_sentence (filter); + pf_buffer_string (filter, + lib_select_response (game, + "You are wearing ", + "I am wearing ", + "%player% is wearing ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, "!\n"); + return TRUE; + + case OBJ_HELD_NPC: + case OBJ_WORN_NPC: + if (gs_npc_seen (game, parent)) + { + pf_new_sentence (filter); + lib_print_npc_np (game, parent); + pf_buffer_string (filter, + (position == OBJ_HELD_NPC) + ? " is holding " : " is wearing "); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + } + else + pf_buffer_string (filter, "I don't know where that is.\n"); + return TRUE; + + case OBJ_PART_NPC: + if (parent == -1) + { + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, " is", " are")); + pf_buffer_string (filter, + lib_select_response (game, + " a part of you!\n", + " a part of me!\n", + " a part of %player%!\n")); + } + else + { + if (gs_npc_seen (game, parent)) + { + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, + " is", " are")); + pf_buffer_string (filter, " a part of "); + lib_print_npc_np (game, parent); + pf_buffer_string (filter, ".\n"); + } + else + pf_buffer_string (filter, "I don't know where that is.\n"); + } + return TRUE; + + case OBJ_ON_OBJECT: + case OBJ_IN_OBJECT: + if (gs_object_seen (game, parent)) + { + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, " is", " are")); + pf_buffer_string (filter, + (position == OBJ_ON_OBJECT) ? " on " : " inside "); + lib_print_object_np (game, parent); + pf_buffer_string (filter, ".\n"); + } + else + pf_buffer_string (filter, "I don't know where that is.\n"); + return TRUE; + } + + /* + * Object is either static unmoved, or dynamic and on the floor of a room. + * Check each room for the object, stopping on first found. + */ + for (room = 0; room < gs_room_count (game); room++) + { + if (obj_indirectly_in_room (game, object, room)) + break; + } + if (room == gs_room_count (game)) + { + pf_buffer_string (filter, "I don't know where that is.\n"); + return TRUE; + } + + /* Check that this room's been visited by the player. */ + if (!gs_room_seen (game, room)) + { + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, + lib_select_plurality (game, object, " is", " are")); + pf_buffer_string (filter, + lib_select_response (game, + " somewhere that you haven't been yet.\n", + " somewhere that I haven't been yet.\n", + " somewhere that %player% hasn't been yet.\n")); + return TRUE; + } + + /* Print the details of the object's room. */ + pf_new_sentence (filter); + lib_print_object_np (game, object); + pf_buffer_string (filter, " -- "); + pf_buffer_string (filter, lib_get_room_name (game, room)); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +sc_bool +lib_cmd_locate_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int index_, count, npc, room; + + game->is_admin = TRUE; + + /* Count the number of NPCs referenced by the last command. */ + count = 0; + npc = -1; + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + if (game->npc_references[index_]) + { + count++; + npc = index_; + } + } + + /* + * If no NPCs identified, be coy about revealing anything; if more than one, + * be vague. The "... where that is..." is the correct message even for + * NPCs -- it's the same response as for lib_locate_other(). + */ + if (count == 0) + { + pf_buffer_string (filter, "I don't know where that is.\n"); + return TRUE; + } + else if (count > 1) + { + pf_buffer_string (filter, + "Please be more clear about who you want to locate.\n"); + return TRUE; + } + + /* + * The reference is unambiguous, so we're responsible for noting it in + * variables. Disambiguation would normally do this for us, but we just + * bypassed it. + */ + var_set_ref_character (vars, npc); + + /* See if this NPC has been seen yet. */ + if (!gs_npc_seen (game, npc)) + { + pf_buffer_string (filter, + lib_select_response (game, + "You haven't seen ", + "I haven't seen ", + "%player% hasn't seen ")); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " yet!\n"); + return TRUE; + } + + /* Check each room for the NPC, stopping on first found. */ + for (room = 0; room < gs_room_count (game); room++) + { + if (npc_in_room (game, npc, room)) + break; + } + if (room == gs_room_count (game)) + { + pf_buffer_string (filter, "I don't know where "); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " is.\n"); + return TRUE; + } + + /* Check that this room's been visited by the player. */ + if (!gs_room_seen (game, room)) + { + lib_print_npc_np (game, npc); + pf_buffer_string (filter, + lib_select_response (game, + " is somewhere that you haven't been yet.\n", + " is somewhere that I haven't been yet.\n", + " is somewhere that %player% hasn't been yet.\n")); + return TRUE; + } + + /* Print the location, and smart-alec response. */ + pf_new_sentence (filter); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " -- "); + pf_buffer_string (filter, lib_get_room_name (game, room)); +#if 0 + if (room == gs_playerroom (game)) + { + pf_buffer_string (filter, + lib_select_response (game, + " (Right next to you, silly!)", + " (Right next to me, silly!)", + " (Right next to %player%, silly!)")); + } +#endif + pf_buffer_string (filter, ".\n"); + return TRUE; +} + + +/* + * lib_cmd_turns() + * lib_cmd_score() + * + * Display turns taken and score so far. + */ +sc_bool +lib_cmd_turns (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_char buffer[32]; + + pf_buffer_string (filter, "You have taken "); + sprintf (buffer, "%ld", game->turns); + pf_buffer_string (filter, buffer); + if (game->turns == 1) + pf_buffer_string (filter, " turn so far.\n"); + else + pf_buffer_string (filter, " turns so far.\n"); + + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_score (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_int max_score, percent; + sc_char buffer[32]; + + /* Get max score, and calculate score as a percentage. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "MaxScore"; + max_score = prop_get_integer (bundle, "I<-ss", vt_key); + if (game->score > 0 && max_score > 0) + percent = (game->score * 100) / max_score; + else + percent = 0; + + /* Output carefully formatted response. */ + pf_buffer_string (filter, + lib_select_response (game, + "Your score is ", + "My score is ", + "%player%'s score is ")); + sprintf (buffer, "%ld", game->score); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, " out of a maximum of "); + sprintf (buffer, "%ld", max_score); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, ". ("); + sprintf (buffer, "%ld", percent); + pf_buffer_string (filter, buffer); + pf_buffer_string (filter, "%)\n"); + + game->is_admin = TRUE; + return TRUE; +} + + +/* + * lib_cmd_*() + * + * Standard response commands. These are uninteresting catch-all cases, + * but it's good to make then right as game ALRs may look for them. + */ +sc_bool +lib_cmd_profanity (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + "I really don't think there's any need for language like" + " that!\n"); + return TRUE; +} + +sc_bool +lib_cmd_examine_all (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "Please examine one object at a time.\n"); + return TRUE; +} + +sc_bool +lib_cmd_examine_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You see no such thing.\n", + "I see no such thing.\n", + "%player% sees no such thing.\n")); + return TRUE; +} + +sc_bool +lib_cmd_locate_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "I don't know where that is!\n"); + game->is_admin = TRUE; + return TRUE; +} + +sc_bool +lib_cmd_unix_like (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "This isn't Unix you know!\n"); + return TRUE; +} + +sc_bool +lib_cmd_dos_like (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "This isn't Dos you know!\n"); + return TRUE; +} + +sc_bool +lib_cmd_cry (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "There's no need for that!\n"); + return TRUE; +} + +sc_bool +lib_cmd_dance (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You do a little dance.\n", + "I do a little dance.\n", + "%player% does a little dance.\n")); + return TRUE; +} + +sc_bool +lib_cmd_eat_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "I don't understand what you are trying to eat.\n"); + return TRUE; +} + +sc_bool +lib_cmd_fight (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "There is nothing worth fighting here.\n"); + return TRUE; +} + +sc_bool +lib_cmd_feed (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "There is nothing worth feeding here.\n"); + return TRUE; +} + +sc_bool +lib_cmd_feel (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You feel nothing out of the ordinary.\n", + "I feel nothing out of the ordinary.\n", + "%player% feels nothing out of the ordinary.\n")); + return TRUE; +} + +sc_bool +lib_cmd_fly (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You can't fly.\n", + "I can't fly.\n", + "%player% can't fly.\n")); + return TRUE; +} + +sc_bool +lib_cmd_hint (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + "You're just going to have to work it out for" + " yourself...\n"); + return TRUE; +} + +sc_bool +lib_cmd_hum (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You hum a little tune.\n", + "I hum a little tune.\n", + "%player% hums a little tune.\n")); + return TRUE; +} + +sc_bool +lib_cmd_jump (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "Wheee-boinng.\n"); + return TRUE; +} + +sc_bool +lib_cmd_listen (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You hear nothing out of the ordinary.\n", + "I hear nothing out of the ordinary.\n", + "%player% hears nothing out of the ordinary.\n")); + return TRUE; +} + +sc_bool +lib_cmd_please (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "Your kindness gets you nowhere.\n", + "My kindness gets me nowhere.\n", + "%player%'s kindness gets nowhere.\n")); + return TRUE; +} + +sc_bool +lib_cmd_punch (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "Who do you think you are, Mike Tyson?\n"); + return TRUE; +} + +sc_bool +lib_cmd_run (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "Why would you want to run?\n", + "Why would I want to run?\n", + "Why would %player% want to run?\n")); + return TRUE; +} + +sc_bool +lib_cmd_shout (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "Aaarrrrgggghhhhhh!\n"); + return TRUE; +} + +sc_bool +lib_cmd_say (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_char *string = NULL; + + switch (sc_randomint (1, 5)) + { + case 1: + string = "Gosh, that was very impressive.\n"; + break; + case 2: + string = lib_select_response (game, + "Not surprisingly, no-one takes any notice" + " of you.\n", + "Not surprisingly, no-one takes any notice" + " of me.\n", + "Not surprisingly, no-one takes any notice" + " of %player%.\n"); + break; + case 3: + string = "Wow! That achieved a lot.\n"; + break; + case 4: + string = "Uh huh, yes, very interesting.\n"; + break; + default: + string = "That's the most interesting thing I've ever heard!\n"; + break; + } + + pf_buffer_string (filter, string); + return TRUE; +} + +sc_bool +lib_cmd_sing (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You sing a little song.\n", + "I sing a little song.\n", + "%player% sings a little song.\n")); + return TRUE; +} + +sc_bool +lib_cmd_sleep (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "Zzzzz. Bored are you?\n"); + return TRUE; +} + +sc_bool +lib_cmd_talk (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "No-one listens to your rabblings.\n", + "No-one listens to my rabblings.\n", + "No-one listens to %player%'s rabblings.\n")); + return TRUE; +} + +sc_bool +lib_cmd_thank (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "You're welcome.\n"); + return TRUE; +} + +sc_bool +lib_cmd_whistle (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + lib_select_response (game, + "You whistle a little tune.\n", + "I whistle a little tune.\n", + "%player% whistles a little tune.\n")); + return TRUE; +} + +sc_bool +lib_cmd_interrogation (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_char *string = NULL; + + switch (sc_randomint (1, 17)) + { + case 1: + string = "Why do you want to know?\n"; + break; + case 2: + string = "Interesting question.\n"; + break; + case 3: + string = "Let me think about that one...\n"; + break; + case 4: + string = "I haven't a clue!\n"; + break; + case 5: + string = "All these questions are hurting my head.\n"; + break; + case 6: + string = "I'm not going to tell you.\n"; + break; + case 7: + string = "Someday I'll know the answer to that one.\n"; + break; + case 8: + string = "I could tell you, but then I'd have to kill you.\n"; + break; + case 9: + string = "Ha, as if I'd tell you!\n"; + break; + case 10: + string = "Ask me again later.\n"; + break; + case 11: + string = "I don't know - could you ask anyone else?\n"; + break; + case 12: + string = "Err, yes?!?\n"; + break; + case 13: + string = "Let me just check my memory banks...\n"; + break; + case 14: + string = "Because that's just the way it is.\n"; + break; + case 15: + string = "Do I ask you all sorts of awkward questions?\n"; + break; + case 16: + string = "Questions, questions...\n"; + break; + default: + string = "Who cares.\n"; + break; + } + + pf_buffer_string (filter, string); + return TRUE; +} + +sc_bool +lib_cmd_xyzzy (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + "I'm sorry, but XYZZY doesn't do anything special in" + " this game!\n"); + return TRUE; +} + +sc_bool +lib_cmd_egotistic (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + +#if 0 + pf_buffer_string (filter, + "Campbell wrote this Adrift Runner. It's pretty" + " good huh!\n"); +#else + pf_buffer_string (filter, "No comment.\n"); +#endif + + return TRUE; +} + +sc_bool +lib_cmd_yes_or_no (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, + "That's interesting, but it doesn't mean much.\n"); + return TRUE; +} + + +/* + * lib_cmd_ask_npc() + * lib_cmd_ask_object() + * lib_cmd_ask_other() + * + * Malformed and rhetorical question responses. + */ +sc_bool +lib_cmd_ask_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int npc; + sc_bool is_ambiguous; + + /* Get the referenced npc, and if none, consider complete. */ + npc = lib_disambiguate_npc (game, "ask", &is_ambiguous); + if (npc == -1) + return is_ambiguous; + + /* Incomplete ask command, so offer help and return. */ + pf_buffer_string (filter, "Use the format \"ask "); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, " about [subject]\".\n"); + return TRUE; +} + +sc_bool +lib_cmd_ask_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* Get the referenced object, and if none, consider complete. */ + object = lib_disambiguate_object (game, "ask", &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* No reply. */ + pf_buffer_string (filter, + lib_select_response (game, + "You get no reply from ", + "I get no reply from ", + "%player% gets no reply from ")); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +sc_bool +lib_cmd_ask_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + /* Incomplete ask command, so offer help and return. */ + pf_buffer_string (filter, + "Use the format \"ask [character] about [subject]\".\n"); + return TRUE; +} + + +/* + * lib_cmd_kill_other() + * + * Uninteresting kill message when no weaponry is involved. + */ +sc_bool +lib_cmd_kill_other (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, "Now that isn't very nice.\n"); + return TRUE; +} + + +/* + * lib_nothing_happens_common() + * lib_nothing_happens_object() + * lib_nothing_happens_other() + * + * Central handler for a range of nothing-happens messages. More + * uninteresting responses. + */ +static sc_bool +lib_nothing_happens_common (sc_gameref_t game, + const sc_char *verb_general, + const sc_char *verb_third_person, + sc_bool is_object) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_int perspective, object; + const sc_char *person, *verb; + sc_bool is_ambiguous; + + /* Use person and verb tense according to perspective. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "Perspective"; + perspective = prop_get_integer (bundle, "I<-ss", vt_key); + switch (perspective) + { + case LIB_FIRST_PERSON: + person = "I "; + verb = verb_general; + break; + case LIB_SECOND_PERSON: + person = "You "; + verb = verb_general; + break; + case LIB_THIRD_PERSON: + person = "%player% "; + verb = verb_third_person; + break; + default: + sc_error ("lib_nothing_happens: unknown perspective, %ld\n", perspective); + person = "You "; + verb = verb_general; + break; + } + + /* If the command target was not an object, end it here. */ + if (!is_object) + { + pf_buffer_string (filter, person); + pf_buffer_string (filter, verb); + pf_buffer_string (filter, ", but nothing happens.\n"); + return TRUE; + } + + /* Get the referenced object. If none, return immediately. */ + object = lib_disambiguate_object (game, verb_general, &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Nothing happens. */ + pf_buffer_string (filter, person); + pf_buffer_string (filter, verb); + pf_buffer_character (filter, ' '); + lib_print_object_np (game, object); + pf_buffer_string (filter, ", but nothing happens.\n"); + return TRUE; +} + +static sc_bool +lib_nothing_happens_object (sc_gameref_t game, + const sc_char *verb_general, + const sc_char *verb_third_person) +{ + return lib_nothing_happens_common (game, + verb_general, verb_third_person, TRUE); +} + +static sc_bool +lib_nothing_happens_other (sc_gameref_t game, + const sc_char *verb_general, + const sc_char *verb_third_person) +{ + return lib_nothing_happens_common (game, + verb_general, verb_third_person, FALSE); +} + + +/* + * lib_cmd_*() + * + * Shake, rattle and roll, and assorted nothing-happens handlers. + */ +sc_bool +lib_cmd_hit_object (sc_gameref_t game) +{ + return lib_nothing_happens_object (game, "hit", "hits"); +} + +sc_bool +lib_cmd_kick_object (sc_gameref_t game) +{ + return lib_nothing_happens_object (game, "kick", "kicks"); +} + +sc_bool +lib_cmd_press_object (sc_gameref_t game) +{ + return lib_nothing_happens_object (game, "press", "presses"); +} + +sc_bool +lib_cmd_push_object (sc_gameref_t game) +{ + return lib_nothing_happens_object (game, "push", "pushes"); +} + +sc_bool +lib_cmd_pull_object (sc_gameref_t game) +{ + return lib_nothing_happens_object (game, "pull", "pulls"); +} + +sc_bool +lib_cmd_shake_object (sc_gameref_t game) +{ + return lib_nothing_happens_object (game, "shake", "shakes"); +} + +sc_bool +lib_cmd_hit_other (sc_gameref_t game) +{ + return lib_nothing_happens_other (game, "hit", "hits"); +} + +sc_bool +lib_cmd_kick_other (sc_gameref_t game) +{ + return lib_nothing_happens_other (game, "kick", "kicks"); +} + +sc_bool +lib_cmd_press_other (sc_gameref_t game) +{ + return lib_nothing_happens_other (game, "press", "presses"); +} + +sc_bool +lib_cmd_push_other (sc_gameref_t game) +{ + return lib_nothing_happens_other (game, "push", "pushes"); +} + +sc_bool +lib_cmd_pull_other (sc_gameref_t game) +{ + return lib_nothing_happens_other (game, "pull", "pulls"); +} + +sc_bool +lib_cmd_shake_other (sc_gameref_t game) +{ + return lib_nothing_happens_other (game, "shake", "shakes"); +} + + +/* + * lib_cant_do_common() + * lib_cant_do_object() + * lib_cant_do_other() + * + * Central handler for a range of can't-do messages. Yet more uninterest- + * ing responses. + */ +static sc_bool +lib_cant_do_common (sc_gameref_t game, + const sc_char *verb, sc_bool is_object) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* If the target is not an object, end it here. */ + if (!is_object) + { + pf_buffer_string (filter, + lib_select_response (game, + "You can't ", + "I can't ", "%player% can't ")); + pf_buffer_string (filter, verb); + pf_buffer_string (filter, " that.\n"); + return TRUE; + } + + /* Get the referenced object. If none, return immediately. */ + object = lib_disambiguate_object (game, verb, &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Whatever it is, don't do it. */ + pf_buffer_string (filter, + lib_select_response (game, + "You can't ", + "I can't ", "%player% can't ")); + pf_buffer_string (filter, verb); + pf_buffer_character (filter, ' '); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +static sc_bool +lib_cant_do_object (sc_gameref_t game, const sc_char *verb) +{ + return lib_cant_do_common (game, verb, TRUE); +} + +static sc_bool +lib_cant_do_other (sc_gameref_t game, const sc_char *verb) +{ + return lib_cant_do_common (game, verb, FALSE); +} + + +/* + * lib_cmd_*() + * + * Assorted can't-do messages. + */ +sc_bool +lib_cmd_block_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "block"); +} + +sc_bool +lib_cmd_climb_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "climb"); +} + +sc_bool +lib_cmd_clean_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "clean"); +} + +sc_bool +lib_cmd_cut_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "cut"); +} + +sc_bool +lib_cmd_drink_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "drink"); +} + +sc_bool +lib_cmd_light_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "light"); +} + +sc_bool +lib_cmd_lift_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "lift"); +} + +sc_bool +lib_cmd_move_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "move"); +} + +sc_bool +lib_cmd_rub_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "rub"); +} + +sc_bool +lib_cmd_stop_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "stop"); +} + +sc_bool +lib_cmd_suck_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "suck"); +} + +sc_bool +lib_cmd_touch_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "touch"); +} + +sc_bool +lib_cmd_turn_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "turn"); +} + +sc_bool +lib_cmd_unblock_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "unblock"); +} + +sc_bool +lib_cmd_wash_object (sc_gameref_t game) +{ + return lib_cant_do_object (game, "wash"); +} + +sc_bool +lib_cmd_block_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "block"); +} + +sc_bool +lib_cmd_climb_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "climb"); +} + +sc_bool +lib_cmd_clean_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "clean"); +} + +sc_bool +lib_cmd_close_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "close"); +} + +sc_bool +lib_cmd_lock_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "lock"); +} + +sc_bool +lib_cmd_unlock_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "unlock"); +} + +sc_bool +lib_cmd_stand_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "stand on"); +} + +sc_bool +lib_cmd_sit_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "sit on"); +} + +sc_bool +lib_cmd_lie_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "lie on"); +} + +sc_bool +lib_cmd_cut_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "cut"); +} + +sc_bool +lib_cmd_drink_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "drink"); +} + +sc_bool +lib_cmd_lift_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "lift"); +} + +sc_bool +lib_cmd_light_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "light"); +} + +sc_bool +lib_cmd_move_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "move"); +} + +sc_bool +lib_cmd_stop_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "stop"); +} + +sc_bool +lib_cmd_rub_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "rub"); +} + +sc_bool +lib_cmd_suck_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "suck"); +} + +sc_bool +lib_cmd_turn_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "turn"); +} + +sc_bool +lib_cmd_touch_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "touch"); +} + +sc_bool +lib_cmd_unblock_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "unblock"); +} + +sc_bool +lib_cmd_wash_other (sc_gameref_t game) +{ + return lib_cant_do_other (game, "wash"); +} + + +/* + * lib_dont_think_common() + * lib_dont_think_object() + * lib_dont_think_other() + * + * Central handler for a range of don't_think messages. Still more + * uninteresting responses. + */ +static sc_bool +lib_dont_think_common (sc_gameref_t game, + const sc_char *verb, sc_bool is_object) +{ + const sc_filterref_t filter = gs_get_filter (game); + sc_int object; + sc_bool is_ambiguous; + + /* If the target is not an object, end it here. */ + if (!is_object) + { + pf_buffer_string (filter, + lib_select_response (game, + "I don't think you can ", + "I don't think I can ", + "I don't think %player% can ")); + pf_buffer_string (filter, verb); + pf_buffer_string (filter, " that.\n"); + return TRUE; + } + + /* Get the referenced object. If none, return immediately. */ + object = lib_disambiguate_object (game, verb, &is_ambiguous); + if (object == -1) + return is_ambiguous; + + /* Whatever it is, don't do it. */ + pf_buffer_string (filter, "I don't think you can "); + pf_buffer_string (filter, verb); + pf_buffer_character (filter, ' '); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +static sc_bool +lib_dont_think_object (sc_gameref_t game, const sc_char *verb) +{ + return lib_dont_think_common (game, verb, TRUE); +} + +static sc_bool +lib_dont_think_other (sc_gameref_t game, const sc_char *verb) +{ + return lib_dont_think_common (game, verb, FALSE); +} + + +/* + * lib_cmd_*() + * + * Assorted don't-think messages. + */ +sc_bool +lib_cmd_fix_object (sc_gameref_t game) +{ + return lib_dont_think_object (game, "fix"); +} + +sc_bool +lib_cmd_mend_object (sc_gameref_t game) +{ + return lib_dont_think_object (game, "mend"); +} + +sc_bool +lib_cmd_repair_object (sc_gameref_t game) +{ + return lib_dont_think_object (game, "repair"); +} + +sc_bool +lib_cmd_fix_other (sc_gameref_t game) +{ + return lib_dont_think_other (game, "fix"); +} + +sc_bool +lib_cmd_mend_other (sc_gameref_t game) +{ + return lib_dont_think_other (game, "mend"); +} + +sc_bool +lib_cmd_repair_other (sc_gameref_t game) +{ + return lib_dont_think_other (game, "repair"); +} + + +/* + * lib_what() + * + * Central handler for doing something, but unsure to what. + */ +static sc_bool +lib_what (sc_gameref_t game, const sc_char *verb) +{ + const sc_filterref_t filter = gs_get_filter (game); + + pf_buffer_string (filter, verb); + pf_buffer_string (filter, " what?\n"); + return TRUE; +} + + +/* + * lib_cmd_*() + * + * Assorted "what?" messages. + */ +sc_bool +lib_cmd_block_what (sc_gameref_t game) +{ + return lib_what (game, "Block"); +} + +sc_bool +lib_cmd_break_what (sc_gameref_t game) +{ + return lib_what (game, "Break"); +} + +sc_bool +lib_cmd_destroy_what (sc_gameref_t game) +{ + return lib_what (game, "Destroy"); +} + +sc_bool +lib_cmd_smash_what (sc_gameref_t game) +{ + return lib_what (game, "Smash"); +} + +sc_bool +lib_cmd_buy_what (sc_gameref_t game) +{ + return lib_what (game, "Buy"); +} + +sc_bool +lib_cmd_clean_what (sc_gameref_t game) +{ + return lib_what (game, "Clean"); +} + +sc_bool +lib_cmd_climb_what (sc_gameref_t game) +{ + return lib_what (game, "Climb"); +} + +sc_bool +lib_cmd_cut_what (sc_gameref_t game) +{ + return lib_what (game, "Cut"); +} + +sc_bool +lib_cmd_drink_what (sc_gameref_t game) +{ + return lib_what (game, "Drink"); +} + +sc_bool +lib_cmd_fix_what (sc_gameref_t game) +{ + return lib_what (game, "Fix"); +} + +sc_bool +lib_cmd_hit_what (sc_gameref_t game) +{ + return lib_what (game, "Hit"); +} + +sc_bool +lib_cmd_kick_what (sc_gameref_t game) +{ + return lib_what (game, "Kick"); +} + +sc_bool +lib_cmd_light_what (sc_gameref_t game) +{ + return lib_what (game, "Light"); +} + +sc_bool +lib_cmd_lift_what (sc_gameref_t game) +{ + return lib_what (game, "Lift"); +} + +sc_bool +lib_cmd_mend_what (sc_gameref_t game) +{ + return lib_what (game, "Mend"); +} + +sc_bool +lib_cmd_move_what (sc_gameref_t game) +{ + return lib_what (game, "Move"); +} + +sc_bool +lib_cmd_press_what (sc_gameref_t game) +{ + return lib_what (game, "Press"); +} + +sc_bool +lib_cmd_pull_what (sc_gameref_t game) +{ + return lib_what (game, "Pull"); +} + +sc_bool +lib_cmd_push_what (sc_gameref_t game) +{ + return lib_what (game, "Push"); +} + +sc_bool +lib_cmd_repair_what (sc_gameref_t game) +{ + return lib_what (game, "Repair"); +} + +sc_bool +lib_cmd_sell_what (sc_gameref_t game) +{ + return lib_what (game, "Sell"); +} + +sc_bool +lib_cmd_shake_what (sc_gameref_t game) +{ + return lib_what (game, "Shake"); +} + +sc_bool +lib_cmd_rub_what (sc_gameref_t game) +{ + return lib_what (game, "Rub"); +} + +sc_bool +lib_cmd_stop_what (sc_gameref_t game) +{ + return lib_what (game, "Stop"); +} + +sc_bool +lib_cmd_suck_what (sc_gameref_t game) +{ + return lib_what (game, "Suck"); +} + +sc_bool +lib_cmd_touch_what (sc_gameref_t game) +{ + return lib_what (game, "Touch"); +} + +sc_bool +lib_cmd_turn_what (sc_gameref_t game) +{ + return lib_what (game, "Turn"); +} + +sc_bool +lib_cmd_unblock_what (sc_gameref_t game) +{ + return lib_what (game, "Unblock"); +} + +sc_bool +lib_cmd_wash_what (sc_gameref_t game) +{ + return lib_what (game, "Wash"); +} + +sc_bool +lib_cmd_drop_what (sc_gameref_t game) +{ + return lib_what (game, "Drop"); +} + +sc_bool +lib_cmd_get_what (sc_gameref_t game) +{ + return lib_what (game, "Take"); +} + +sc_bool +lib_cmd_give_what (sc_gameref_t game) +{ + return lib_what (game, "Give"); +} + +sc_bool +lib_cmd_open_what (sc_gameref_t game) +{ + return lib_what (game, "Open"); +} + +sc_bool +lib_cmd_remove_what (sc_gameref_t game) +{ + return lib_what (game, "Remove"); +} + +sc_bool +lib_cmd_wear_what (sc_gameref_t game) +{ + return lib_what (game, "Wear"); +} + +sc_bool +lib_cmd_lock_what (sc_gameref_t game) +{ + return lib_what (game, "Lock"); +} + +sc_bool +lib_cmd_unlock_what (sc_gameref_t game) +{ + return lib_what (game, "Unlock"); +} + + +/* + * lib_cmd_verb_object() + * lib_cmd_verb_character() + * + * Handlers for unrecognized verbs with known object/NPC. + */ +sc_bool +lib_cmd_verb_object (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int count, object, index_; + + /* Ensure the reference is unambiguous. */ + count = 0; + object = -1; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (game->object_references[index_] + && gs_object_seen (game, index_) + && obj_indirectly_in_room (game, index_, gs_playerroom (game))) + { + count++; + object = index_; + } + } + if (count != 1) + return FALSE; + + /* Save in variables. */ + var_set_ref_object (vars, object); + + /* Print don't understand message. */ + pf_buffer_string (filter, "I don't understand what you want me to do with "); + lib_print_object_np (game, object); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + +sc_bool +lib_cmd_verb_npc (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int count, npc, index_; + + /* Ensure the reference is unambiguous. */ + count = 0; + npc = -1; + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + if (game->npc_references[index_] + && gs_npc_seen (game, index_) + && npc_in_room (game, index_, gs_playerroom (game))) + { + count++; + npc = index_; + } + } + if (count != 1) + return FALSE; + + /* Save in variables. */ + var_set_ref_character (vars, npc); + + /* Print don't understand message; unlike objects, there's no "me" here. */ + pf_buffer_string (filter, "I don't understand what you want to do with "); + lib_print_npc_np (game, npc); + pf_buffer_string (filter, ".\n"); + return TRUE; +} + + +/* + * lib_debug_trace() + * + * Set library tracing on/off. + */ +void +lib_debug_trace (sc_bool flag) +{ + lib_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sclocale.cpp b/engines/glk/adrift/sclocale.cpp new file mode 100644 index 0000000000..25cf4ed065 --- /dev/null +++ b/engines/glk/adrift/sclocale.cpp @@ -0,0 +1,573 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Standard libc ctype.h functions vary their results according to the + * currently set locale. We want consistent Windows codepage 1252 or + * codepage 1251 (WinLatin1 or WinCyrillic) results. To get this, then, + * we have to define the needed functions internally to SCARE. + */ + +/* + * All the ctype boolean and character tables contain 256 elements, one for + * each possible sc_char value. This is used to size arrays and to verify + * that range setting functions do not overrun array boundaries. + */ +enum { TABLE_SIZE = 256 }; + + +/* + * loc_setrange_bool() + * loc_setranges_bool() + * + * Helpers for building ctype tables. Sets all elements from start to end + * inclusive to TRUE, and iterate this on a ranges array. + */ +static void +loc_setrange_bool (sc_int start, sc_int end, sc_bool table[]) +{ + sc_int index_; + + for (index_ = start; index_ <= end; index_++) + { + assert (index_ > -1 && index_ < TABLE_SIZE); + table[index_] = TRUE; + } +} + +static void +loc_setranges_bool (const sc_int ranges[], sc_bool table[]) +{ + sc_int index_; + + for (index_ = 0; ranges[index_] > -1; index_ += 2) + { + assert (ranges[index_] <= ranges[index_ + 1]); + loc_setrange_bool (ranges[index_], ranges[index_ + 1], table); + } +} + + +/* + * loc_setrange_char() + * loc_setranges_char() + * + * Helpers for building ctype conversion tables. Sets all elements from start + * to end inclusive to their index value plus the given offset, and iterate + * this on a ranges array. + */ +static void +loc_setrange_char (sc_int start, sc_int end, sc_int offset, sc_char table[]) +{ + sc_int index_; + + for (index_ = start; index_ <= end; index_++) + { + assert (index_ > -1 && index_ < TABLE_SIZE); + assert (index_ + offset > -1 && index_ + offset < TABLE_SIZE); + table[index_] = index_ + offset; + } +} + +static void +loc_setranges_char (const sc_int ranges[], sc_char table[]) +{ + sc_int index_; + + for (index_ = 0; ranges[index_] > -1; index_ += 3) + { + assert (ranges[index_] <= ranges[index_ + 1]); + loc_setrange_char (ranges[index_], + ranges[index_ + 1], ranges[index_ + 2], table); + } +} + + +/* + * A locale consists of a name, ranges for each table, and signatures for + * autodetection based on the game's compilation date. This is the static + * data portion of a locale. + */ +enum { RANGES_LENGTH = 32 }; +enum { SIGNATURE_COUNT = 24, SIGNATURE_LENGTH = 3 }; +typedef struct +{ + const sc_char *const name; + const sc_int isspace_ranges[RANGES_LENGTH]; + const sc_int isdigit_ranges[RANGES_LENGTH]; + const sc_int isalpha_ranges[RANGES_LENGTH]; + const sc_int toupper_ranges[RANGES_LENGTH]; + const sc_int tolower_ranges[RANGES_LENGTH]; + const sc_byte signature[SIGNATURE_COUNT][SIGNATURE_LENGTH]; +} sc_locale_t; + + +/* + * The locale table set is built from a locale using its ranges. There is one + * table for each function, and a pointer to the locale used to construct the + * table, for synchronization with changed locales. This is the dynamic data + * portion of a locale. + */ +typedef struct +{ + const sc_locale_t *locale; + sc_bool isspace[TABLE_SIZE]; + sc_bool isdigit[TABLE_SIZE]; + sc_bool isalpha[TABLE_SIZE]; + sc_char toupper[TABLE_SIZE]; + sc_char tolower[TABLE_SIZE]; +} sc_locale_table_t; + +/* + * Define a single static locale table set. This set re-initializes if it + * detects a locale change. + */ +static sc_locale_table_t loc_locale_tables = {NULL, {0}, {0}, {0}, {0}, {0}}; + + +/* + * loc_synchronize_tables() + * loc_check_tables_synchronized() + * + * Initialize tables for a locale. And compare the locale tables to a locale + * and if not for the same locale, (re-)initialize. + */ +static void +loc_synchronize_tables (const sc_locale_t *locale) +{ + /* Clear all tables and the locale pointer. */ + memset (&loc_locale_tables, 0, sizeof (loc_locale_tables)); + + /* Set ranges and attach the new locale. */ + loc_setranges_bool (locale->isspace_ranges, loc_locale_tables.isspace); + loc_setranges_bool (locale->isdigit_ranges, loc_locale_tables.isdigit); + loc_setranges_bool (locale->isalpha_ranges, loc_locale_tables.isalpha); + loc_setranges_char (locale->toupper_ranges, loc_locale_tables.toupper); + loc_setranges_char (locale->tolower_ranges, loc_locale_tables.tolower); + + loc_locale_tables.locale = locale; +} + +static void +loc_check_tables_synchronized (const sc_locale_t *locale) +{ + if (locale != loc_locale_tables.locale) + loc_synchronize_tables (locale); +} + + +/* + * Locale for Latin1. The signatures in this locale are null since it is the + * default locale; no matching required. Also, none may be practical, as this + * locale works for a large number of Western European languages (though in + * practice, it seems that only English and French Adrift Latin1 games exist). + */ +static const sc_locale_t LATIN1_LOCALE = { + "Latin1", + {9,13, 32,32, 160,160, -1}, + {48,57, -1}, + {65,90, 97,122, 192,214, 216,246, 248,255, 138,138, 140,140, + 142,142, 154,154, 156,156, 158,158, 159,159, -1}, + {0,TABLE_SIZE-1,0, 97,122,-32, 224,246,-32, 248,254,-32, 154,154,-16, + 156,156,-16, 158,158,-16, 255,255,-96, -1}, + {0,TABLE_SIZE-1,0, 65,90,32, 192,214,32, 216,222,32, 138,138,16, + 140,140,16, 142,142,16, 159,159,96, -1}, + {{0}} +}; + + +/* + * Locale for Cyrillic. The signatures in this locale are month names in + * both mixed case and lowercase Russian Cyrillic. + */ +static const sc_locale_t CYRILLIC_LOCALE = { + "Cyrillic", + {9,13, 32,32, 160,160, -1}, + {48,57, -1}, + {65,90, 97,122, 168,168, 184,184, 175,175, 191,191, 178,179, + 192,255, -1}, + {0,TABLE_SIZE-1,0, 97,122,-32, 184,184,-16, 191,191,-16, 179,179,-1, + 224,255,-32, -1}, + {0,TABLE_SIZE-1,0, 65,90,32, 168,168,16, 175,175,16, 178,178,1, + 192,223,32, -1}, + {{223, 237, 226}, {212, 229, 226}, {204, 224, 240}, {192, 239, 240}, + {204, 224, 233}, {200, 254, 237}, {200, 254, 235}, {192, 226, 227}, + {209, 229, 237}, {206, 234, 242}, {205, 238, 255}, {196, 229, 234}, + {255, 237, 226}, {244, 229, 226}, {236, 224, 240}, {224, 239, 240}, + {236, 224, 233}, {232, 254, 237}, {232, 254, 235}, {224, 226, 227}, + {241, 229, 237}, {238, 234, 242}, {237, 238, 255}, {228, 229, 234}} +}; + + +/* List of pointers to supported and available locales, NULL terminated. */ +static const sc_locale_t *const AVAILABLE_LOCALES[] = { + &LATIN1_LOCALE, + &CYRILLIC_LOCALE, + NULL +}; + +/* + * The locale for the game, set below explicitly or on game startup, and + * a flag to note if it's been set explicitly, to prevent autodetection from + * overwriting a manual setting. + */ +static const sc_locale_t *loc_locale = &LATIN1_LOCALE; +static sc_bool loc_is_autodetect_enabled = TRUE; + + +/* + * loc_locate_signature_in_date() + * + * Checks the format of the input date to ensure it matches the format + * "dd [Mm]mm yyyy". Returns the address of the month part of the string, or + * NULL if it doesn't match the expected format. + */ +static const sc_char * +loc_locate_signature_in_date (const sc_char *date) +{ + sc_int day, year, converted; + sc_char signature[SIGNATURE_LENGTH + 1]; + + /* Clear signature, and convert using a scanf format. */ + memset (signature, 0, sizeof (signature)); + converted = sscanf (date, "%2ld %3[^ 0-9] %4ld", &day, signature, &year); + + /* Valid if we converted three values, and month has three characters. */ + if (converted == 3 && strlen (signature) == SIGNATURE_LENGTH) + return strstr (date, signature); + else + return NULL; +} + + +/* + * loc_compare_locale_signatures() + * + * Search a locale's signatures for a match with the signature passed in. + * Returns TRUE if a match found, FALSE otherwise. Uses memcmp rather than + * any strcasecmp() variant because the signatures are in the locale's + * codepage, but the locale is not yet (by definition) set. + */ +static sc_bool +loc_compare_locale_signatures (const char *signature, const sc_locale_t *locale) +{ + sc_int index_; + sc_bool is_matched; + + /* Compare signatures, stopping on the first match found. */ + is_matched = FALSE; + for (index_ = 0; index_ < SIGNATURE_COUNT; index_++) + { + if (memcmp (locale->signature[index_], + signature, sizeof (locale->signature[0])) == 0) + { + is_matched = TRUE; + break; + } + } + + return is_matched; +} + + +/* + * loc_find_matching_locale() + * + * Look at the incoming date, expected to be in the format "dd [Mm]mm yyyy", + * where "[Mm]mm" is a standard month abbreviation of the locale in which the + * Generator was run. Match this with locale signatures, and return the + * first locale that matches, or NULL if none match. + */ +static const sc_locale_t * +loc_find_matching_locale (const sc_char *date, + const sc_locale_t *const *locales) +{ + const sc_char *signature; + const sc_locale_t *matched = NULL; + + /* Get the month part of date, and if valid, search locale signatures. */ + signature = loc_locate_signature_in_date (date); + if (signature) + { + const sc_locale_t *const *iterator; + + /* Search for this signature in the locale's signatures. */ + for (iterator = locales; *iterator; iterator++) + { + if (loc_compare_locale_signatures (signature, *iterator)) + { + matched = *iterator; + break; + } + } + } + + /* Return the matching locale, NULL if none matched. */ + return matched; +} + + +/* + * loc_detect_game_locale() + * + * Set an autodetected value for the locale based on looking at a game's + * compilation date. + */ +void +loc_detect_game_locale (sc_prop_setref_t bundle) +{ + assert (bundle); + + /* If an explicit locale has already been set, ignore the call. */ + if (loc_is_autodetect_enabled) + { + sc_vartype_t vt_key[1]; + const sc_char *compile_date; + const sc_locale_t *matched; + + /* Read the game's compilation date from the properties. */ + vt_key[0].string = "CompileDate"; + compile_date = prop_get_string (bundle, "S<-s", vt_key); + + /* Search for a matching locale based on the game compilation date. */ + matched = loc_find_matching_locale (compile_date, AVAILABLE_LOCALES); + + /* If a locale matched, set the global locale to it. */ + if (matched) + loc_locale = matched; + } +} + + +/* + * loc_ascii_tolower() + * loc_ascii_strncasecmp() + * + * The standard sc_strncasecmp() calls sc_tolower(), which is locale specific. + * This isn't a particular problem because it will set the Latin1 locale + * automatically before continuing. However, since locale names should always + * be in ascii anyway, it's slightly safer to just use an ascii-only version + * of this function. + */ +static sc_char +loc_ascii_tolower (sc_char ch) +{ + return (ch >= 'A' && ch <= 'Z') ? ch - 'A' + 'a' : ch; +} + +static sc_int +loc_ascii_strncasecmp (const sc_char *s1, const sc_char *s2, sc_int n) +{ + sc_int index_; + + for (index_ = 0; index_ < n; index_++) + { + sc_int diff; + + diff = loc_ascii_tolower (s1[index_]) - loc_ascii_tolower (s2[index_]); + if (diff < 0 || diff > 0) + return diff < 0 ? -1 : 1; + } + + return 0; +} + + +/* + * loc_set_locale() + * loc_get_locale() + * + * Set a locale explicitly from the name passed in, returning TRUE if a locale + * matched the name. Get the current locale, which may be the default locale + * if none yet set. + */ +sc_bool +loc_set_locale (const sc_char *name) +{ + const sc_locale_t *matched = NULL; + const sc_locale_t *const *iterator; + assert (name); + + /* + * Search locales for a matching name, abbreviated if necessary. Stop on + * the first match found. + */ + for (iterator = AVAILABLE_LOCALES; *iterator; iterator++) + { + const sc_locale_t *const locale = *iterator; + + if (loc_ascii_strncasecmp (name, locale->name, strlen (name)) == 0) + { + matched = locale; + break; + } + } + + /* If matched, set the global locale, and lock out future autodetection. */ + if (matched) + { + loc_locale = matched; + loc_is_autodetect_enabled = FALSE; + } + + return matched ? TRUE : FALSE; +} + +const sc_char * +loc_get_locale (void) +{ + return loc_locale->name; +} + + +/* + * loc_debug_dump_new_line() + * loc_debug_dump_bool_table() + * loc_debug_dump_char_table() + * loc_debug_dump() + * + * Print out locale tables. + */ +static int +loc_debug_dump_new_line (sc_int index_, sc_int count) +{ + return index_ < TABLE_SIZE - 1 && index_ % count == count - 1; +} + +static void +loc_debug_dump_bool_table (const sc_char *label, + sc_int count, const sc_bool table[]) +{ + sc_int index_; + + sc_trace ("loc_locale_tables.%s = {\n ", label); + for (index_ = 0; index_ < TABLE_SIZE; index_++) + { + sc_trace ("%s%s", table[index_] ? "T" : "F", + loc_debug_dump_new_line (index_, count) ? "\n " : ""); + } + sc_trace ("\n}\n"); +} + +static void +loc_debug_dump_char_table (const sc_char *label, + sc_int count, const sc_char table[]) +{ + sc_int index_; + + sc_trace ("loc_locale_tables.%s = {\n ", label); + for (index_ = 0; index_ < TABLE_SIZE; index_++) + { + sc_trace ("%02lx%s", (sc_int) (sc_byte) table[index_], + loc_debug_dump_new_line (index_, count) ? "\n " : " "); + } + sc_trace ("\n}\n"); +} + +void +loc_debug_dump (void) +{ + sc_trace ("Locale: debug dump follows...\n"); + + loc_check_tables_synchronized (loc_locale); + sc_trace ("loc_locale_tables" + ".locale->name = %s\n", loc_locale_tables.locale->name); + + loc_debug_dump_bool_table ("isspace", 64, loc_locale_tables.isspace); + loc_debug_dump_bool_table ("isdigit", 64, loc_locale_tables.isdigit); + loc_debug_dump_bool_table ("isalpha", 64, loc_locale_tables.isalpha); + loc_debug_dump_char_table ("toupper", 16, loc_locale_tables.toupper); + loc_debug_dump_char_table ("tolower", 16, loc_locale_tables.tolower); +} + + +/* + * loc_bool_template() + * loc_char_template() + * + * "Template" functions for locale variant ctype functions. Synchronize + * tables to the currently set locale, and return the value from the table. + */ +static sc_bool +loc_bool_template (sc_char character, const sc_bool table[]) +{ + loc_check_tables_synchronized (loc_locale); + return table[(sc_byte) character]; +} + +static sc_char +loc_char_template (sc_char character, const sc_char table[]) +{ + loc_check_tables_synchronized (loc_locale); + return table[(sc_byte) character]; +} + + +/* + * sc_isspace() + * sc_isalpha() + * sc_isdigit() + * sc_tolower() + * sc_toupper() + * + * Public entry points into locale variant ctype functions. + */ +sc_bool +sc_isspace (sc_char character) +{ + return loc_bool_template (character, loc_locale_tables.isspace); +} + +sc_bool +sc_isalpha (sc_char character) +{ + return loc_bool_template (character, loc_locale_tables.isalpha); +} + +sc_bool +sc_isdigit (sc_char character) +{ + return loc_bool_template (character, loc_locale_tables.isdigit); +} + +sc_char +sc_toupper (sc_char character) +{ + return loc_char_template (character, loc_locale_tables.toupper); +} + +sc_char +sc_tolower (sc_char character) +{ + return loc_char_template (character, loc_locale_tables.tolower); +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scmemos.cpp b/engines/glk/adrift/scmemos.cpp new file mode 100644 index 0000000000..4809b2078f --- /dev/null +++ b/engines/glk/adrift/scmemos.cpp @@ -0,0 +1,622 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_uint MEMENTO_MAGIC = 0x9fd33d1d; +enum { MEMO_ALLOCATION_BLOCK = 32 }; + +/* + * Game memo structure, saves a serialized game. Allocation is preserved so + * that structures can be reused without requiring reallocation. + */ +typedef struct sc_memo_s +{ + sc_byte *serialized_game; + sc_int allocation; + sc_int length; +} sc_memo_t; +typedef sc_memo_t *sc_memoref_t; + +/* + * Game command history structure, similar to a memo. Saves a player input + * command to create a history, reusing allocation where possible. + */ +typedef struct sc_history_s +{ + sc_char *command; + sc_int sequence; + sc_int timestamp; + sc_int turns; + sc_int allocation; + sc_int length; +} sc_history_t; +typedef sc_history_t *sc_historyref_t; + +/* + * Memo set structure. This reserves space for a predetermined number of + * serialized games, and an indicator cursor showing where additions are + * placed. The structure is a ring, with old elements being overwritten by + * newer arrivals. Also tacked onto this structure is a set of strings + * used to hold a command history that operates in a somewhat csh-like way, + * also a ring with limited capacity. + */ +enum { MEMO_UNDO_TABLE_SIZE = 16, MEMO_HISTORY_TABLE_SIZE = 64 }; +typedef struct sc_memo_set_s +{ + sc_uint magic; + sc_memo_t memo[MEMO_UNDO_TABLE_SIZE]; + sc_int memo_cursor; + + sc_history_t history[MEMO_HISTORY_TABLE_SIZE]; + sc_int history_count; + sc_int current_history; + sc_bool is_at_start; +} sc_memo_set_t; + + +/* + * memo_is_valid() + * + * Return TRUE if pointer is a valid memo set, FALSE otherwise. + */ +static sc_bool +memo_is_valid (sc_memo_setref_t memento) +{ + return memento && memento->magic == MEMENTO_MAGIC; +} + + +/* + * memo_round_up() + * + * Round up an allocation in bytes to the next allocation block. + */ +static sc_int +memo_round_up (sc_int allocation) +{ + sc_int extended; + + extended = allocation + MEMO_ALLOCATION_BLOCK - 1; + return (extended / MEMO_ALLOCATION_BLOCK) * MEMO_ALLOCATION_BLOCK; +} + + +/* + * memo_create() + * + * Create and return a new set of memos. + */ +sc_memo_setref_t +memo_create (void) +{ + sc_memo_setref_t memento; + + /* Create and initialize a clean set of memos. */ + memento = (sc_memo_setref_t)sc_malloc (sizeof (*memento)); + memento->magic = MEMENTO_MAGIC; + + memset (memento->memo, 0, sizeof (memento->memo)); + memento->memo_cursor = 0; + + memset (memento->history, 0, sizeof (memento->history)); + memento->history_count = 0; + memento->current_history = 0; + memento->is_at_start = FALSE; + + return memento; +} + + +/* + * memo_destroy() + * + * Destroy a memo set, and free its heap memory. + */ +void +memo_destroy (sc_memo_setref_t memento) +{ + sc_int index_; + assert (memo_is_valid (memento)); + + /* Free the content of any used memo and any used history. */ + for (index_ = 0; index_ < MEMO_UNDO_TABLE_SIZE; index_++) + { + sc_memoref_t memo; + + memo = memento->memo + index_; + sc_free (memo->serialized_game); + } + for (index_ = 0; index_ < MEMO_HISTORY_TABLE_SIZE; index_++) + { + sc_historyref_t history; + + history = memento->history + index_; + sc_free (history->command); + } + + /* Poison and free the memo set itself. */ + memset (memento, 0xaa, sizeof (*memento)); + sc_free (memento); +} + + +/* + * memo_save_game_callback() + * + * Callback function for game serialization. Appends the data passed in to + * that already stored in the memo. + */ +static void +memo_save_game_callback (void *opaque, const sc_byte *buffer, sc_int length) +{ + sc_memoref_t memo = (sc_memoref_t)opaque; + sc_int required; + assert (opaque && buffer && length > 0); + + /* + * If necessary, increase the allocation for this memo. Serialized games + * tend to grow slightly as the game progresses, so we add a bit of extra + * to the actual allocation. + */ + required = memo->length + length; + if (required > memo->allocation) + { + required = memo_round_up (required + 2 * MEMO_ALLOCATION_BLOCK); + memo->serialized_game = (sc_byte *)sc_realloc (memo->serialized_game, required); + memo->allocation = required; + } + + /* Add this block of data to the buffer. */ + memcpy (memo->serialized_game + memo->length, buffer, length); + memo->length += length; +} + + +/* + * memo_save_game() + * + * Store a game in the next memo slot. + */ +void +memo_save_game (sc_memo_setref_t memento, sc_gameref_t game) +{ + sc_memoref_t memo; + assert (memo_is_valid (memento)); + + /* + * If the current slot is in use, we can re-use its allocation. Saved + * games will tend to be of roughly equal sizes, so it's worth doing. + */ + memo = memento->memo + memento->memo_cursor; + memo->length = 0; + + /* Serialize the given game into this memo. */ + ser_save_game (game, memo_save_game_callback, memo); + + /* + * If serialization worked (failure would be a surprise), advance the + * current memo cursor. + */ + if (memo->length > 0) + { + memento->memo_cursor++; + memento->memo_cursor %= MEMO_UNDO_TABLE_SIZE; + } + else + sc_error ("memo_save_game: warning: game save failed\n"); +} + + +/* + * memo_load_game_callback() + * + * Callback function for game deserialization. Returns data from the memo + * until it's drained. + */ +static sc_int +memo_load_game_callback (void *opaque, sc_byte *buffer, sc_int length) +{ + sc_memoref_t memo = (sc_memoref_t)opaque; + sc_int bytes; + assert (opaque && buffer && length > 0); + + /* Send back either all the bytes, or as many as the buffer allows. */ + bytes = (memo->length < length) ? memo->length : length; + + /* Read and remove the first block of data (or all if less than length). */ + memcpy (buffer, memo->serialized_game, bytes); + memmove (memo->serialized_game, + memo->serialized_game + bytes, memo->length - bytes); + memo->length -= bytes; + + /* Return the count of bytes placed in the buffer. */ + return bytes; +} + + +/* + * memo_load_game() + * + * Restore a game from the last memo slot used, if possible. + */ +sc_bool +memo_load_game (sc_memo_setref_t memento, sc_gameref_t game) +{ + sc_int cursor; + sc_memoref_t memo; + assert (memo_is_valid (memento)); + + /* Look back one from the current memo cursor. */ + cursor = (memento->memo_cursor == 0) + ? MEMO_UNDO_TABLE_SIZE - 1 : memento->memo_cursor - 1; + memo = memento->memo + cursor; + + /* If this slot is not empty, restore the serialized game held in it. */ + if (memo->length > 0) + { + sc_bool status; + + /* + * Deserialize the given game from this memo; failure would be somewhat + * of a surprise here. + */ + status = ser_load_game (game, memo_load_game_callback, memo); + if (!status) + sc_error ("memo_load_game: warning: game load failed\n"); + + /* + * This should have drained the memo of all data, but to be sure that + * there's no chance of trying to restore from this slot again, we'll + * force it anyway. + */ + if (memo->length > 0) + { + sc_error ("memo_load_game: warning: data remains after loading\n"); + memo->length = 0; + } + + /* Regress current memo, and return TRUE if we restored a memo. */ + memento->memo_cursor = cursor; + return status; + } + + /* There are no more memos to restore. */ + return FALSE; +} + + +/* + * memo_is_load_available() + * + * Returns TRUE if a memo restore is likely to succeed if called, FALSE + * otherwise. + */ +sc_bool +memo_is_load_available (sc_memo_setref_t memento) +{ + sc_int cursor; + sc_memoref_t memo; + assert (memo_is_valid (memento)); + + /* + * Look back one from the current memo cursor. Return TRUE if this slot + * contains a serialized game. + */ + cursor = (memento->memo_cursor == 0) + ? MEMO_UNDO_TABLE_SIZE - 1 : memento->memo_cursor - 1; + memo = memento->memo + cursor; + return memo->length > 0; +} + + +/* + * memo_clear_games() + * + * Forget the memos of saved games. + */ +void +memo_clear_games (sc_memo_setref_t memento) +{ + sc_int index_; + assert (memo_is_valid (memento)); + + /* Deallocate every entry. */ + for (index_ = 0; index_ < MEMO_UNDO_TABLE_SIZE; index_++) + { + sc_memoref_t memo; + + memo = memento->memo + index_; + sc_free (memo->serialized_game); + } + + /* Reset all entries and the cursor. */ + memset (memento->memo, 0, sizeof (memento->memo)); + memento->memo_cursor = 0; +} + + +/* + * memo_save_command() + * + * Store a player command in the command history, evicting any least recently + * used item if necessary. + */ +void +memo_save_command (sc_memo_setref_t memento, + const sc_char *command, sc_int timestamp, sc_int turns) +{ + sc_historyref_t history; + sc_int length; + assert (memo_is_valid (memento)); + + /* As with memos, reuse the allocation of the next slot if it has one. */ + history = memento->history + + memento->history_count % MEMO_HISTORY_TABLE_SIZE; + + /* + * Resize the allocation for this slot if required. Strings tend to be + * short, so round up to a block to avoid too many reallocs. + */ + length = strlen (command) + 1; + if (history->allocation < length) + { + sc_int required; + + required = memo_round_up (length); + history->command = (sc_char *)sc_realloc (history->command, required); + history->allocation = required; + } + + /* Save the string into this slot, and normalize it for neatness. */ + strcpy (history->command, command); + sc_normalize_string (history->command); + history->sequence = memento->history_count + 1; + history->timestamp = timestamp; + history->turns = turns; + history->length = length; + + /* Increment the count of histories handled. */ + memento->history_count++; +} + + +/* + * memo_unsave_command() + * + * Remove the last saved command. This is special functionality for the + * history lister. To keep synchronized with the runner main loop, it needs + * to "invent" a history item at the end of the list before listing, then + * remove it again as the main runner loop will add the real thing. + */ +void +memo_unsave_command (sc_memo_setref_t memento) +{ + assert (memo_is_valid (memento)); + + /* Do nothing if for some reason there's no history to unsave. */ + if (memento->history_count > 0) + { + sc_historyref_t history; + + /* Decrement the count of histories handled, erase the prior entry. */ + memento->history_count--; + history = memento->history + + memento->history_count % MEMO_HISTORY_TABLE_SIZE; + history->sequence = 0; + history->timestamp = 0; + history->turns = 0; + history->length = 0; + } +} + + +/* + * memo_get_command_count() + * + * Return a count of available saved commands. + */ +sc_int +memo_get_command_count (sc_memo_setref_t memento) +{ + assert (memo_is_valid (memento)); + + /* Return the lesser of the history count and the history table size. */ + if (memento->history_count < MEMO_HISTORY_TABLE_SIZE) + return memento->history_count; + else + return MEMO_HISTORY_TABLE_SIZE; +} + + +/* + * memo_first_command() + * + * Iterator rewind function, reset current location to the first command. + */ +void +memo_first_command (sc_memo_setref_t memento) +{ + sc_int cursor; + sc_historyref_t history; + assert (memo_is_valid (memento)); + + /* + * If the buffer has cycled, we have the full complement of saved commands, + * so start iterating at the current cursor. Otherwise, start from index 0. + * Detect cycling by looking at the current slot; if it's filled, we've + * been here before. Set at_start flag to indicate the special case for + * circular buffers. + */ + cursor = memento->history_count % MEMO_HISTORY_TABLE_SIZE; + history = memento->history + cursor; + memento->current_history = (history->length > 0) ? cursor : 0; + memento->is_at_start = TRUE; +} + + +/* + * memo_next_command() + * + * Iterator function, return the next saved command and its sequence id + * starting at 1, and the timestamp and turns when the command was saved. + */ +void +memo_next_command (sc_memo_setref_t memento, + const sc_char **command, sc_int *sequence, + sc_int *timestamp, sc_int *turns) +{ + assert (memo_is_valid (memento)); + + /* If valid, return the current command and advance. */ + if (memo_more_commands (memento)) + { + sc_historyref_t history; + + /* Note the current history, and advance its index. */ + history = memento->history + memento->current_history; + memento->current_history++; + memento->current_history %= MEMO_HISTORY_TABLE_SIZE; + memento->is_at_start = FALSE; + + /* Return details from the history noted above. */ + *command = history->command; + *sequence = history->sequence; + *timestamp = history->timestamp; + *turns = history->turns; + } + else + { + /* Return NULL and zeroes if no more commands available. */ + *command = NULL; + *sequence = 0; + *timestamp = 0; + *turns = 0; + } +} + + +/* + * memo_more_commands() + * + * Iterator end function, returns TRUE if more commands are readable. + */ +sc_bool +memo_more_commands (sc_memo_setref_t memento) +{ + sc_int cursor; + sc_historyref_t history; + assert (memo_is_valid (memento)); + + /* Get the current effective write position, and the current history. */ + cursor = memento->history_count % MEMO_HISTORY_TABLE_SIZE; + history = memento->history + memento->current_history; + + /* + * More data if the current history is behind the write position and is + * occupied, or if it matches and is occupied and we're at the start of + * iteration (circular buffer special case). + */ + if (memento->current_history == cursor) + return (memento->is_at_start) ? history->length > 0 : FALSE; + else + return history->length > 0; +} + + +/* + * memo_find_command() + * + * Find and return the command string for the given sequence number (-ve + * indicates an offset from the last defined), or NULL if not found. + */ +const sc_char * +memo_find_command (sc_memo_setref_t memento, sc_int sequence) +{ + sc_int target, index_; + sc_historyref_t matched; + assert (memo_is_valid (memento)); + + /* Decide on a search target, depending on the sign of sequence. */ + target = (sequence < 0) ? memento->history_count + sequence + 1: sequence; + + /* + * A backwards search starting at the write position would probably be more + * efficient here, but this is a rarely called function so we'll do it the + * simpler way. + */ + matched = NULL; + for (index_ = 0; index_ < MEMO_HISTORY_TABLE_SIZE; index_++) + { + sc_historyref_t history; + + history = memento->history + index_; + if (history->sequence == target) + { + matched = history; + break; + } + } + + /* + * Return the command or NULL. If sequence passed in was zero, and the + * history was not full, this will still return NULL as it should, since + * this unused history's command found by the search above will be NULL. + */ + return matched ? matched->command : NULL; +} + + +/* + * memo_clear_commands() + * + * Forget all saved commands. + */ +void +memo_clear_commands (sc_memo_setref_t memento) +{ + sc_int index_; + assert (memo_is_valid (memento)); + + /* Deallocate every entry. */ + for (index_ = 0; index_ < MEMO_HISTORY_TABLE_SIZE; index_++) + { + sc_historyref_t history; + + history = memento->history + index_; + sc_free (history->command); + } + + /* Reset all entries, the count, and the iteration variables. */ + memset (memento->history, 0, sizeof (memento->history)); + memento->history_count = 0; + memento->current_history = 0; + memento->is_at_start = FALSE; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scnpcs.cpp b/engines/glk/adrift/scnpcs.cpp new file mode 100644 index 0000000000..ab11acc5ae --- /dev/null +++ b/engines/glk/adrift/scnpcs.cpp @@ -0,0 +1,640 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* Trace flag, set before running. */ +static sc_bool npc_trace = FALSE; + + +/* + * npc_in_room() + * + * Return TRUE if a given NPC is currently in a given room. + */ +sc_bool +npc_in_room (sc_gameref_t game, sc_int npc, sc_int room) +{ + if (npc_trace) + { + sc_trace ("NPC: checking NPC %ld in room %ld (NPC is in %ld)\n", + npc, room, gs_npc_location (game, npc)); + } + + return gs_npc_location (game, npc) - 1 == room; +} + + +/* + * npc_count_in_room() + * + * Return the count of characters in the room, including the player. + */ +sc_int +npc_count_in_room (sc_gameref_t game, sc_int room) +{ + sc_int count, npc; + + /* Start with the player. */ + count = gs_player_in_room (game, room) ? 1 : 0; + + /* Total up other NPCs inhabiting the room. */ + for (npc = 0; npc < gs_npc_count (game); npc++) + { + if (gs_npc_location (game, npc) - 1 == room) + count++; + } + return count; +} + + +/* + * npc_start_npc_walk() + * + * Start the given walk for the given NPC. + */ +void +npc_start_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[6]; + sc_int movetime; + + /* Retrieve movetime 0 for the NPC walk. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Walks"; + vt_key[3].integer = walk; + vt_key[4].string = "MoveTimes"; + vt_key[5].integer = 0; + movetime = prop_get_integer (bundle, "I<-sisisi", vt_key) + 1; + + /* Set up walkstep. */ + gs_set_npc_walkstep (game, npc, walk, movetime); +} + + +/* + * npc_turn_update() + * npc_setup_initial() + * + * Set initial values for NPC states, and update on turns. + */ +void +npc_turn_update (sc_gameref_t game) +{ + sc_int index_; + + /* Set current values for NPC seen states. */ + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + if (!gs_npc_seen (game, index_) + && npc_in_room (game, index_, gs_playerroom (game))) + gs_set_npc_seen (game, index_, TRUE); + } +} + +void +npc_setup_initial (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int index_; + + /* Start any walks that do not depend on a StartTask */ + vt_key[0].string = "NPCs"; + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + sc_int walk; + + /* Set up invariant parts of the properties key. */ + vt_key[1].integer = index_; + vt_key[2].string = "Walks"; + + /* Process each walk, starting at the last and working backwards. */ + for (walk = gs_npc_walkstep_count (game, index_) - 1; walk >= 0; walk--) + { + sc_int starttask; + + /* If StartTask is zero, start walk at game start. */ + vt_key[3].integer = walk; + vt_key[4].string = "StartTask"; + starttask = prop_get_integer (bundle, "I<-sisis", vt_key); + if (starttask == 0) + npc_start_npc_walk (game, index_, walk); + } + } + + /* Update seen flags for initial states. */ + npc_turn_update (game); +} + + +/* + * npc_room_in_roomgroup() + * + * Return TRUE if a given room is in a given group. + */ +static sc_bool +npc_room_in_roomgroup (sc_gameref_t game, sc_int room, sc_int group) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int member; + + /* Check roomgroup membership. */ + vt_key[0].string = "RoomGroups"; + vt_key[1].integer = group; + vt_key[2].string = "List"; + vt_key[3].integer = room; + member = prop_get_integer (bundle, "I<-sisi", vt_key); + return member != 0; +} + + +/* List of direction names, for printing entry/exit messages. */ +static const sc_char *const DIRNAMES_4[] = { + "the north", "the east", "the south", "the west", "above", "below", + "inside", "outside", + NULL +}; +static const sc_char *const DIRNAMES_8[] = { + "the north", "the east", "the south", "the west", "above", "below", + "inside", "outside", + "the north-east", "the south-east", "the south-west", "the north-west", + NULL +}; + +/* + * npc_random_adjacent_roomgroup_member() + * + * Return a random member of group adjacent to given room. + */ +static sc_int +npc_random_adjacent_roomgroup_member (sc_gameref_t game, + sc_int room, sc_int group) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_bool eightpointcompass; + sc_int roomlist[12], count, length, index_; + + /* If given room is "hidden", return nothing. */ + if (room == -1) + return -1; + + /* How many exits to consider? */ + vt_key[0].string = "Globals"; + vt_key[1].string = "EightPointCompass"; + eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); + if (eightpointcompass) + length = sizeof (DIRNAMES_8) / sizeof (DIRNAMES_8[0]) - 1; + else + length = sizeof (DIRNAMES_4) / sizeof (DIRNAMES_4[0]) - 1; + + /* Poll adjacent rooms. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Exits"; + count = 0; + for (index_ = 0; index_ < length; index_++) + { + sc_int adjacent; + + vt_key[3].integer = index_; + vt_key[4].string = "Dest"; + adjacent = prop_get_child_count (bundle, "I<-sisis", vt_key); + + if (adjacent > 0 && npc_room_in_roomgroup (game, adjacent - 1, group)) + { + roomlist[count] = adjacent - 1; + count++; + } + } + + /* Return a random adjacent room, or -1 if nothing is adjacent. */ + return (count > 0) ? roomlist[sc_randomint (0, count - 1)] : -1; +} + + +/* + * npc_announce() + * + * Helper for npc_tick_npc(). + */ +static void +npc_announce (sc_gameref_t game, sc_int npc, + sc_int room, sc_bool is_exit, sc_int npc_room) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5], vt_rvalue; + const sc_char *text, *name, *const *dirnames; + sc_int dir, dir_match; + sc_bool eightpointcompass, showenterexit, found; + + /* If no announcement required, return immediately. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "ShowEnterExit"; + showenterexit = prop_get_boolean (bundle, "B<-sis", vt_key); + if (!showenterexit) + return; + + /* Get exit or entry text, and NPC name. */ + vt_key[2].string = is_exit ? "ExitText" : "EnterText"; + text = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + /* Decide on four or eight point compass names list. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "EightPointCompass"; + eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); + dirnames = eightpointcompass ? DIRNAMES_8 : DIRNAMES_4; + + /* Set invariant key for room exit search. */ + vt_key[0].string = "Rooms"; + vt_key[1].integer = room; + vt_key[2].string = "Exits"; + + /* Find the room exit that matches the NPC room. */ + found = FALSE; + dir_match = 0; + for (dir = 0; dirnames[dir]; dir++) + { + vt_key[3].integer = dir; + if (prop_get (bundle, "I<-sisi", &vt_rvalue, vt_key)) + { + sc_int dest; + + /* Get room's direction destination, and compare. */ + vt_key[4].string = "Dest"; + dest = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (dest == npc_room) + { + dir_match = dir; + found = TRUE; + break; + } + } + } + + /* Print NPC exit/entry details. */ + pf_buffer_character (filter, '\n'); + pf_new_sentence (filter); + pf_buffer_string (filter, name); + pf_buffer_character (filter, ' '); + pf_buffer_string (filter, text); + if (found) + { + pf_buffer_string (filter, is_exit ? " to " : " from "); + pf_buffer_string (filter, dirnames[dir_match]); + } + pf_buffer_string (filter, ".\n"); + + /* Handle any associated resource. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Res"; + vt_key[3].integer = is_exit ? 3 : 2; + res_handle_resource (game, "sisi", vt_key); +} + + +/* + * npc_tick_npc_walk() + * + * Helper for npc_tick_npc(). + */ +static void +npc_tick_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[6]; + sc_int roomgroups, movetimes, walkstep, start, dest, destnum; + sc_int chartask, objecttask; + + if (npc_trace) + { + sc_trace ("NPC: ticking NPC %ld, walk %ld: step %ld\n", + npc, walk, gs_npc_walkstep (game, npc, walk)); + } + + /* Count roomgroups for later use. */ + vt_key[0].string = "RoomGroups"; + roomgroups = prop_get_child_count (bundle, "I<-s", vt_key); + + /* Get move times array length. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Walks"; + vt_key[3].integer = walk; + vt_key[4].string = "MoveTimes"; + movetimes = prop_get_child_count (bundle, "I<-sisis", vt_key); + + /* Find a step to match the movetime. */ + for (walkstep = 0; walkstep < movetimes - 1; walkstep++) + { + sc_int movetime; + + vt_key[5].integer = walkstep + 1; + movetime = prop_get_integer (bundle, "I<-sisisi", vt_key); + if (gs_npc_walkstep (game, npc, walk) > movetime) + break; + } + + /* Sort out a destination. */ + dest = start = gs_npc_location (game, npc) - 1; + + vt_key[4].string = "Rooms"; + vt_key[5].integer = walkstep; + destnum = prop_get_integer (bundle, "I<-sisisi", vt_key); + + if (destnum == 0) /* Hidden. */ + dest = -1; + else if (destnum == 1) /* Follow player. */ + dest = gs_playerroom (game); + else if (destnum < gs_room_count (game) + 2) + dest = destnum - 2; /* To room. */ + else if (destnum < gs_room_count (game) + 2 + roomgroups) + { + sc_int initial; + + /* For roomgroup walks, move only if walksteps has just refreshed. */ + vt_key[4].string = "MoveTimes"; + vt_key[5].integer = 0; + initial = prop_get_integer (bundle, "I<-sisisi", vt_key); + if (gs_npc_walkstep (game, npc, walk) == initial) + { + sc_int group; + + group = destnum - 2 - gs_room_count (game); + dest = npc_random_adjacent_roomgroup_member (game, start, group); + if (dest == -1) + dest = lib_random_roomgroup_member (game, group); + } + } + + /* See if the NPC actually moved. */ + if (start != dest) + { + if (npc_trace) + sc_trace ("NPC: walking NPC %ld moved to %ld\n", npc, dest); + + /* Move NPC to destination. */ + gs_set_npc_location (game, npc, dest + 1); + + /* Announce NPC movements, and handle meeting characters and objects. */ + if (gs_player_in_room (game, start)) + npc_announce (game, npc, start, TRUE, dest); + else if (gs_player_in_room (game, dest)) + npc_announce (game, npc, dest, FALSE, start); + } + + /* Handle meeting characters and objects. */ + vt_key[4].string = "CharTask"; + chartask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (chartask >= 0) + { + sc_int meetchar; + + /* Run meetchar task if appropriate. */ + vt_key[4].string = "MeetChar"; + meetchar = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if ((meetchar == -1 && gs_player_in_room (game, dest)) + || (meetchar >= 0 && dest == gs_npc_location (game, meetchar) - 1)) + { + if (task_can_run_task (game, chartask)) + task_run_task (game, chartask, TRUE); + } + } + + vt_key[4].string = "ObjectTask"; + objecttask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (objecttask >= 0) + { + sc_int meetobject; + + /* Run meetobject task if appropriate. */ + vt_key[4].string = "MeetObject"; + meetobject = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (meetobject >= 0 && obj_directly_in_room (game, meetobject, dest)) + { + if (task_can_run_task (game, objecttask)) + task_run_task (game, objecttask, TRUE); + } + } +} + + +/* + * npc_tick_npc() + * + * Move an NPC one step along current walk. + */ +static void +npc_tick_npc (sc_gameref_t game, sc_int npc) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[6]; + sc_int walk; + sc_bool has_moved = FALSE; + + if (npc_trace) + sc_trace ("NPC: ticking NPC %ld\n", npc); + + /* Set up invariant key parts. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Walks"; + + /* Find active walk, and if any found, make a step along it. */ + for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--) + { + sc_int starttask, stoppingtask; + + /* Ignore finished walks. */ + if (gs_npc_walkstep (game, npc, walk) <= 0) + continue; + + /* Get start task. */ + vt_key[3].integer = walk; + vt_key[4].string = "StartTask"; + starttask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + + /* + * Check that the starter is still complete, and if not, stop walk. + * Then keep on looking for an active walk. + */ + if (starttask >= 0 && !gs_task_done (game, starttask)) + { + if (npc_trace) + sc_trace ("NPC: stopping NPC %ld walk, start task undone\n", npc); + + gs_set_npc_walkstep (game, npc, walk, -1); + continue; + } + + /* Get stopping task. */ + vt_key[4].string = "StoppingTask"; + stoppingtask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + + /* + * If any stopping task has completed, ignore this walk but don't + * actually finish it; more like an event pauser, then. + * + * TODO Is this right? + */ + if (stoppingtask >= 0 && gs_task_done (game, stoppingtask)) + { + if (npc_trace) + sc_trace ("NPC: ignoring NPC %ld walk, stop task done\n", npc); + + continue; + } + + /* Decrement steps. */ + gs_decrement_npc_walkstep (game, npc, walk); + + /* If we just hit a walk end, loop if called for. */ + if (gs_npc_walkstep (game, npc, walk) == 0) + { + sc_bool is_loop; + + /* If walk is a loop, restart it. */ + vt_key[4].string = "Loop"; + is_loop = prop_get_boolean (bundle, "B<-sisis", vt_key); + if (is_loop) + { + vt_key[4].string = "MoveTimes"; + vt_key[5].integer = 0; + gs_set_npc_walkstep (game, npc, walk, + prop_get_integer (bundle, + "I<-sisisi", vt_key)); + } + else + gs_set_npc_walkstep (game, npc, walk, -1); + } + + /* + * If not yet made a move on this walk, make one, and once made, make + * no other + */ + if (!has_moved) + { + npc_tick_npc_walk (game, npc, walk); + has_moved = TRUE; + } + } +} + + +/* + * npc_tick_npcs() + * + * Move each NPC one step along current walk. + */ +void +npc_tick_npcs (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_gameref_t undo = game->undo; + sc_int npc; + + /* + * Compare the player location to last turn, to see if the player has moved + * this turn. If moved, look for meetings with NPCs. + * + * TODO Is this the right place to do this. After ticking each NPC, rather + * than before, seems more appropriate. But the messages come out in the + * right order by putting it here. + * + * Also, note that we take the shortcut of using the undo gamestate here, + * rather than properly recording the prior location of the player, and + * perhaps also NPCs, in the live gamestate. + */ + if (undo && !gs_player_in_room (undo, gs_playerroom (game))) + { + for (npc = 0; npc < gs_npc_count (game); npc++) + { + sc_int walk; + + /* Iterate each NPC's walks. */ + for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--) + { + sc_vartype_t vt_key[5]; + sc_int chartask; + + /* Ignore finished walks. */ + if (gs_npc_walkstep (game, npc, walk) <= 0) + continue; + + /* Retrieve any character meeting task for the NPC. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Walks"; + vt_key[3].integer = walk; + vt_key[4].string = "CharTask"; + chartask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (chartask >= 0) + { + sc_int meetchar; + + /* Run meetchar task if appropriate. */ + vt_key[4].string = "MeetChar"; + meetchar = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (meetchar == -1 && + gs_player_in_room (game, gs_npc_location (game, npc) - 1)) + { + if (task_can_run_task (game, chartask)) + task_run_task (game, chartask, TRUE); + } + } + } + } + } + + /* Iterate and tick each individual NPC. */ + for (npc = 0; npc < gs_npc_count (game); npc++) + npc_tick_npc (game, npc); +} + + +/* + * npc_debug_trace() + * + * Set NPC tracing on/off. + */ +void +npc_debug_trace (sc_bool flag) +{ + npc_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scobjcts.cpp b/engines/glk/adrift/scobjcts.cpp new file mode 100644 index 0000000000..9bef9952e3 --- /dev/null +++ b/engines/glk/adrift/scobjcts.cpp @@ -0,0 +1,1036 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_char NUL = '\0'; + +/* Trace flag, set before running. */ +static sc_bool obj_trace = FALSE; + + +/* + * obj_is_static() + * obj_is_surface() + * obj_is_container() + * + * Convenience functions to return TRUE for given object attributes. + */ +sc_bool +obj_is_static (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_bool bstatic; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Static"; + bstatic = prop_get_boolean (bundle, "B<-sis", vt_key); + return bstatic; +} + +sc_bool +obj_is_container (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_bool is_container; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Container"; + is_container = prop_get_boolean (bundle, "B<-sis", vt_key); + return is_container; +} + +sc_bool +obj_is_surface (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_bool is_surface; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Surface"; + is_surface = prop_get_boolean (bundle, "B<-sis", vt_key); + return is_surface; +} + + +/* + * obj_container_object() + * + * Return the index of the n'th container object found. + */ +sc_int +obj_container_object (sc_gameref_t game, sc_int n) +{ + sc_int object, count; + + /* Progress through objects until n containers found. */ + count = n; + for (object = 0; object < gs_object_count (game) && count >= 0; object++) + { + if (obj_is_container (game, object)) + count--; + } + return object - 1; +} + + +/* + * obj_container_index() + * + * Return index such that obj_container_object(index) == objnum. + */ +sc_int +obj_container_index (sc_gameref_t game, sc_int objnum) +{ + sc_int object, count; + + /* Progress through objects up to objnum. */ + count = 0; + for (object = 0; object < objnum; object++) + { + if (obj_is_container (game, object)) + count++; + } + return count; +} + + +/* + * obj_surface_object() + * + * Return the index of the n'th surface object found. + */ +sc_int +obj_surface_object (sc_gameref_t game, sc_int n) +{ + sc_int object, count; + + /* Progress through objects until n surfaces found. */ + count = n; + for (object = 0; object < gs_object_count (game) && count >= 0; object++) + { + if (obj_is_surface (game, object)) + count--; + } + return object - 1; +} + + +/* + * obj_surface_index() + * + * Return index such that obj_surface_object(index) == objnum. + */ +sc_int +obj_surface_index (sc_gameref_t game, sc_int objnum) +{ + sc_int object, count; + + /* Progress through objects up to objnum. */ + count = 0; + for (object = 0; object < objnum; object++) + { + if (obj_is_surface (game, object)) + count++; + } + return count; +} + + +/* + * obj_stateful_object() + * + * Return the index of the n'th openable or statussed object found. + */ +sc_int +obj_stateful_object (sc_gameref_t game, sc_int n) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, count; + + /* Progress through objects until n matches found. */ + count = n; + for (object = 0; object < gs_object_count (game) && count >= 0; object++) + { + sc_vartype_t vt_key[3]; + sc_bool is_openable, is_statussed; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Openable"; + is_openable = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + vt_key[2].string = "CurrentState"; + is_statussed = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (is_openable || is_statussed) + count--; + } + return object - 1; +} + + +/* + * obj_stateful_index() + * + * Return index such that obj_stateful_object(index) == objnum. + */ +sc_int +obj_stateful_index (sc_gameref_t game, sc_int objnum) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, count; + + /* Progress through objects up to objnum. */ + count = 0; + for (object = 0; object < objnum; object++) + { + sc_vartype_t vt_key[3]; + sc_bool is_openable, is_statussed; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Openable"; + is_openable = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + vt_key[2].string = "CurrentState"; + is_statussed = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (is_openable || is_statussed) + count++; + } + return count; +} + + +/* + * obj_state_name() + * + * Return the string name of the state of a given stateful object. The + * string is malloc'ed, and needs to be freed by the caller. Returns NULL + * if no valid state string found. + */ +sc_char * +obj_state_name (sc_gameref_t game, sc_int objnum) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *states; + sc_int length, state, count, first, last; + sc_char *string; + + /* Get the list of state strings for the object. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = objnum; + vt_key[2].string = "States"; + states = prop_get_string (bundle, "S<-sis", vt_key); + + /* Find the start of the element for the current state. */ + state = gs_object_state (game, objnum); + length = strlen (states); + for (first = 0, count = state; first < length && count > 1; first++) + { + if (states[first] == '|') + count--; + } + if (count != 1) + return NULL; + + /* Find the end of the state string. */ + for (last = first; last < length; last++) + { + if (states[last] == '|') + break; + } + + /* Allocate and take a copy of the state string. */ + string = (sc_char *)sc_malloc (last - first + 1); + memcpy (string, states + first, last - first); + string[last - first] = NUL; + + return string; +} + + +/* + * obj_dynamic_object() + * + * Return the index of the n'th non-static object found. + */ +sc_int +obj_dynamic_object (sc_gameref_t game, sc_int n) +{ + sc_int object, count; + + /* Progress through objects until n matches found. */ + count = n; + for (object = 0; object < gs_object_count (game) && count >= 0; object++) + { + if (!obj_is_static (game, object)) + count--; + } + return object - 1; +} + + +/* + * obj_wearable_object() + * + * Return the index of the n'th wearable object found. + */ +sc_int +obj_wearable_object (sc_gameref_t game, sc_int n) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, count; + + /* Progress through objects until n matches found. */ + count = n; + for (object = 0; object < gs_object_count (game) && count >= 0; object++) + { + if (!obj_is_static (game, object)) + { + sc_vartype_t vt_key[3]; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Wearable"; + if (prop_get_boolean (bundle, "B<-sis", vt_key)) + count--; + } + } + return object - 1; +} + + +/* + * Size is held in the ten's digit of SizeWeight, and weight in the units. + * Size and weight are multipliers -- the relative size and weight of objects + * rises by a factor of three for each incremental multiplier. These factors + * are also used for the maximum size of object that can fit in a container, + * and the number of these that fit. + */ +enum +{ OBJ_DIMENSION_DIVISOR = 10, + OBJ_DIMENSION_MULTIPLE = 3 +}; + +/* + * obj_get_size() + * obj_get_weight() + * + * Return the relative size and weight of an object. For containers, the + * weight includes the weight of each contained object. + * + * TODO It's possible to have static objects in the player inventory, moved + * by events -- how should these be handled, as they have no SizeWeight? + */ +sc_int +obj_get_size (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int size, count; + + /* TODO For now, give static objects no size. */ + if (obj_is_static (game, object)) + return 0; + + /* Size is the 'tens' component of SizeWeight. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "SizeWeight"; + count = prop_get_integer (bundle, "I<-sis", vt_key) / OBJ_DIMENSION_DIVISOR; + + /* + * Calculate base object size. Unlike weights below, we take this as simply + * being the maximum size; that is, when a container carries other objects + * its weight increases by the sum of objects carried, but its size remains + * constant. + */ + size = 1; + for (; count > 0; count--) + size *= OBJ_DIMENSION_MULTIPLE; + + if (obj_trace) + sc_trace ("Object: object %ld is size %ld\n", object, size); + + /* Return total size. */ + return size; +} + +sc_int +obj_get_weight (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int weight, count; + + /* TODO For now, give static objects no weight. */ + if (obj_is_static (game, object)) + return 0; + + /* Weight is the 'units' component of SizeWeight. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "SizeWeight"; + count = prop_get_integer (bundle, "I<-sis", vt_key) % OBJ_DIMENSION_DIVISOR; + + /* Calculate base object weight. */ + weight = 1; + for (; count > 0; count--) + weight *= OBJ_DIMENSION_MULTIPLE; + + /* If this is a container or a surface, add weights of parented objects. */ + if (obj_is_container (game, object) || obj_is_surface (game, object)) + { + sc_int other; + + /* Find and add contained or surface objects. */ + for (other = 0; other < gs_object_count (game); other++) + { + if ((gs_object_position (game, other) == OBJ_IN_OBJECT + || gs_object_position (game, other) == OBJ_ON_OBJECT) + && gs_object_parent (game, other) == object) + { + weight += obj_get_weight (game, other); + } + } + } + + if (obj_trace) + sc_trace ("Object: object %ld is weight %ld\n", object, weight); + + /* Return total weight. */ + return weight; +} + + +/* + * obj_convert_player_limit() + * obj_get_player_size_limit() + * obj_get_player_weight_limit() + * + * Return the limits set on the sizes and weights a player can handle. Not + * really object-related except that they deal with sizing multiples. + */ +static sc_int +obj_convert_player_limit (sc_int value) +{ + sc_int retval, index_; + + /* 'Tens' of value multiplied by 3 to the power 'units' of value. */ + retval = value / OBJ_DIMENSION_DIVISOR; + for (index_ = 0; index_ < value % OBJ_DIMENSION_DIVISOR; index_++) + retval *= OBJ_DIMENSION_MULTIPLE; + + return retval; +} + +sc_int +obj_get_player_size_limit (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_int max_size; + + vt_key[0].string = "Globals"; + vt_key[1].string = "MaxSize"; + max_size = prop_get_integer (bundle, "I<-ss", vt_key); + + return obj_convert_player_limit (max_size); +} + +sc_int +obj_get_player_weight_limit (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_int max_weight; + + vt_key[0].string = "Globals"; + vt_key[1].string = "MaxWt"; + max_weight = prop_get_integer (bundle, "I<-ss", vt_key); + + return obj_convert_player_limit (max_weight); +} + + +/* + * obj_get_container_maxsize() + * obj_get_container_capacity() + * + * Return the maximum size of an object that can be placed in a container, + * and the number that will fit. + */ +sc_int +obj_get_container_maxsize (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int maxsize, count; + + /* Maxsize is found from the 'units' component of Capacity. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Capacity"; + count = prop_get_integer (bundle, "I<-sis", vt_key) % OBJ_DIMENSION_DIVISOR; + + /* Calculate and return maximum size. */ + maxsize = 1; + for (; count > 0; count--) + maxsize *= OBJ_DIMENSION_MULTIPLE; + + if (obj_trace) + sc_trace ("Object: object %ld has max size %ld\n", object, maxsize); + + return maxsize; +} + +sc_int +obj_get_container_capacity (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int capacity; + + /* The count of objects is in the 'tens' component of Capacity. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Capacity"; + capacity = prop_get_integer (bundle, "I<-sis", vt_key) + / OBJ_DIMENSION_DIVISOR; + + if (obj_trace) + sc_trace ("Object: object %ld has capacity %ld\n", object, capacity); + + return capacity; +} + + +/* Sit/lie bit mask enumerations. */ +enum +{ OBJ_STANDABLE_MASK = 1 << 0, + OBJ_LIEABLE_MASK = 1 << 1 +}; + +/* + * obj_standable_object() + * + * Return the index of the n'th standable object found. + */ +sc_int +obj_standable_object (sc_gameref_t game, sc_int n) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, count; + + /* Progress through objects until n standable found. */ + count = n; + for (object = 0; object < gs_object_count (game) && count >= 0; object++) + { + sc_vartype_t vt_key[3]; + sc_int sit_lie_flags; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "SitLie"; + sit_lie_flags = prop_get_integer (bundle, "I<-sis", vt_key); + if (sit_lie_flags & OBJ_STANDABLE_MASK) + count--; + } + return object - 1; +} + + +/* + * obj_lieable_object() + * + * Return the index of the n'th lieable object found. + */ +sc_int +obj_lieable_object (sc_gameref_t game, sc_int n) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int object, count; + + /* Progress through objects until n lieable found. */ + count = n; + for (object = 0; object < gs_object_count (game) && count >= 0; object++) + { + sc_vartype_t vt_key[3]; + sc_int sit_lie_flags; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "SitLie"; + sit_lie_flags = prop_get_integer (bundle, "I<-sis", vt_key); + if (sit_lie_flags & OBJ_LIEABLE_MASK) + count--; + } + return object - 1; +} + + +/* + * obj_appears_plural() + * + * Return TRUE if the object appears to be plural. Adrift makes a guess at + * this to produce "... is on.." or "... are on...". It's not clear how it + * does it, but it looks something like: singular if prefix is "a" or "an" + * or ""; plural if prefix is "the" or "some" and short name ends with 's' + * that is not preceded by 'u'. + */ +sc_bool +obj_appears_plural (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *prefix, *name; + + /* Check prefix for "a", "an", or empty. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + + if (!(sc_strempty (prefix) + || sc_compare_word (prefix, "a", 1) + || sc_compare_word (prefix, "an", 2))) + { + sc_int length; + + /* Check name for ending in 's', but not 'us'. */ + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + length = strlen (name); + + if (!sc_strempty (name) + && sc_tolower (name[length - 1]) == 's' + && (length < 2 || sc_tolower (name[length - 2]) != 'u')) + return TRUE; + } + + /* Doesn't look plural. */ + return FALSE; +} + + +/* + * obj_directly_in_room_internal() + * obj_directly_in_room() + * + * Return TRUE if a given object is currently on the floor of a given room. + */ +static sc_bool +obj_directly_in_room_internal (sc_gameref_t game, sc_int object, sc_int room) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + + /* See if the object is static or dynamic. */ + if (obj_is_static (game, object)) + { + sc_vartype_t vt_key[5]; + sc_int type; + + /* Static object moved to player or room by event? */ + if (!gs_object_static_unmoved (game, object)) + { + if (gs_object_position (game, object) == OBJ_HELD_PLAYER) + return FALSE; + else + return gs_object_position (game, object) - 1 == room; + } + + /* Check and return the room list for the object. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Where"; + vt_key[3].string = "Type"; + type = prop_get_integer (bundle, "I<-siss", vt_key); + switch (type) + { + case ROOMLIST_ALL_ROOMS: + return TRUE; + case ROOMLIST_NO_ROOMS: + case ROOMLIST_NPC_PART: + return FALSE; + + case ROOMLIST_ONE_ROOM: + vt_key[3].string = "Room"; + return prop_get_integer (bundle, "I<-siss", vt_key) == room + 1; + + case ROOMLIST_SOME_ROOMS: + vt_key[3].string = "Rooms"; + vt_key[4].integer = room + 1; + return prop_get_boolean (bundle, "B<-sissi", vt_key); + + default: + sc_fatal ("obj_directly_in_room_internal:" + " invalid type, %ld\n", type); + return FALSE; + } + } + else + return gs_object_position (game, object) == room + 1; +} + +sc_bool +obj_directly_in_room (sc_gameref_t game, sc_int object, sc_int room) +{ + sc_bool result; + + /* Check, trace result, and return. */ + result = obj_directly_in_room_internal (game, object, room); + + if (obj_trace) + { + sc_trace ("Object: checking for object %ld directly in room %ld, %s\n", + object, room, result ? "true" : "false"); + } + + return result; +} + + +/* + * obj_indirectly_in_room_internal() + * obj_indirectly_in_room() + * + * Return TRUE if a given object is currently in a given room, either + * directly, on an object indirectly, in an open object indirectly, or + * carried by an NPC in the room. + */ +static sc_bool +obj_indirectly_in_room_internal (sc_gameref_t game, sc_int object, sc_int room) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + + /* See if the object is static or dynamic. */ + if (obj_is_static (game, object)) + { + sc_vartype_t vt_key[5]; + sc_int type; + + /* Static object moved to player or room by event? */ + if (!gs_object_static_unmoved (game, object)) + { + if (gs_object_position (game, object) == OBJ_HELD_PLAYER) + return gs_player_in_room (game, room); + else + return gs_object_position (game, object) - 1 == room; + } + + /* Check and return the room list for the object. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Where"; + vt_key[3].string = "Type"; + type = prop_get_integer (bundle, "I<-siss", vt_key); + switch (type) + { + case ROOMLIST_ALL_ROOMS: + return TRUE; + case ROOMLIST_NO_ROOMS: + return FALSE; + + case ROOMLIST_ONE_ROOM: + vt_key[3].string = "Room"; + return prop_get_integer (bundle, "I<-siss", vt_key) == room + 1; + + case ROOMLIST_SOME_ROOMS: + vt_key[3].string = "Rooms"; + vt_key[4].integer = room + 1; + return prop_get_boolean (bundle, "B<-sissi", vt_key); + + case ROOMLIST_NPC_PART: + { + sc_int npc; + + vt_key[2].string = "Parent"; + npc = prop_get_integer (bundle, "I<-sis", vt_key); + if (npc == 0) + return gs_player_in_room (game, room); + else + return npc_in_room (game, npc - 1, room); + } + + default: + sc_fatal ("obj_indirectly_in_room_internal:" + " invalid type, %ld\n", type); + return FALSE; + } + } + else + { + sc_int parent, position; + + /* Get dynamic object's parent and position. */ + parent = gs_object_parent (game, object); + position = gs_object_position (game, object); + + /* Decide depending on positioning. */ + switch (position) + { + case OBJ_HIDDEN: /* Hidden. */ + return FALSE; + + case OBJ_HELD_PLAYER: /* Held by player. */ + case OBJ_WORN_PLAYER: /* Worn by player. */ + return gs_player_in_room (game, room); + + case OBJ_HELD_NPC: /* Held by NPC. */ + case OBJ_WORN_NPC: /* Worn by NPC. */ + return npc_in_room (game, parent, room); + + case OBJ_IN_OBJECT: /* In another object. */ + { + sc_int openness; + + openness = gs_object_openness (game, parent); + switch (openness) + { + case OBJ_WONTCLOSE: + case OBJ_OPEN: + return obj_indirectly_in_room (game, parent, room); + default: + return FALSE; + } + } + + case OBJ_ON_OBJECT: /* On another object. */ + return obj_indirectly_in_room (game, parent, room); + + default: /* Within a room. */ + if (position > gs_room_count (game) + 1) + { + sc_error ("sc_object_indirectly_in_room:" + " position out of bounds, %ld\n", position); + } + return position - 1 == room; + } + } +} + +sc_bool +obj_indirectly_in_room (sc_gameref_t game, + sc_int object, sc_int room) +{ + sc_bool result; + + /* Check, trace result, and return. */ + result = obj_indirectly_in_room_internal (game, object, room); + + if (obj_trace) + { + sc_trace ("Object: checking for object %ld indirectly in room %ld, %s\n", + object, room, result ? "true" : "false"); + } + + return result; +} + + +/* + * obj_indirectly_held_by_player_internal() + * obj_indirectly_held_by_player() + * + * Return TRUE if a given object is currently held by the player, either + * directly, on an object indirectly, or in an open object indirectly. + */ +static sc_bool +obj_indirectly_held_by_player_internal (sc_gameref_t game, + sc_int object) +{ + /* See if the object is static or dynamic. */ + if (obj_is_static (game, object)) + { + /* Static object moved to player or room by event? */ + if (!gs_object_static_unmoved (game, object)) + { + if (gs_object_position (game, object) == OBJ_HELD_PLAYER) + return TRUE; + else + return FALSE; + } + + /* An unmoved static object is not held by the player. */ + return FALSE; + } + else + { + sc_int parent, position; + + /* Get dynamic object's parent and position. */ + parent = gs_object_parent (game, object); + position = gs_object_position (game, object); + + /* Decide depending on positioning. */ + switch (position) + { + case OBJ_HIDDEN: /* Hidden. */ + return FALSE; + + case OBJ_HELD_PLAYER: /* Held by player. */ + case OBJ_WORN_PLAYER: /* Worn by player. */ + return TRUE; + + case OBJ_HELD_NPC: /* Held by NPC. */ + case OBJ_WORN_NPC: /* Worn by NPC. */ + return FALSE; + + case OBJ_IN_OBJECT: /* In another object. */ + { + sc_int openness; + + openness = gs_object_openness (game, parent); + switch (openness) + { + case OBJ_WONTCLOSE: + case OBJ_OPEN: + return obj_indirectly_held_by_player (game, parent); + default: + return FALSE; + } + } + + case OBJ_ON_OBJECT: /* On another object. */ + return obj_indirectly_held_by_player (game, parent); + + default: /* Within a room. */ + return FALSE; + } + } +} + +sc_bool +obj_indirectly_held_by_player (sc_gameref_t game, sc_int object) +{ + sc_bool result; + + /* Check, trace result, and return. */ + result = obj_indirectly_held_by_player_internal (game, object); + + if (obj_trace) + { + sc_trace ("Object: checking for object %ld indirectly" + " held by player, %s\n", object, result ? "true" : "false"); + } + + return result; +} + + +/* + * sc_obj_shows_initial_description() + * + * Return TRUE if this object should be listed as room content. + */ +sc_bool +obj_shows_initial_description (sc_gameref_t game, sc_int object) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int onlywhennotmoved; + + /* Get only when moved property. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "OnlyWhenNotMoved"; + onlywhennotmoved = prop_get_integer (bundle, "I<-sis", vt_key); + + /* Combine this with game in mysterious ways. */ + switch (onlywhennotmoved) + { + case 0: + return TRUE; + + case 1: + return gs_object_unmoved (game, object); + + case 2: + { + sc_int initialposition; + + if (gs_object_unmoved (game, object)) + return TRUE; + + vt_key[2].string = "InitialPosition"; + initialposition = prop_get_integer (bundle, "I<-sis", vt_key) - 3; + return gs_object_position (game, object) == initialposition; + } + } + + /* What you talkin' 'bout, Willis? */ + return FALSE; +} + + +/* + * obj_turn_update() + * obj_setup_initial() + * + * Set initial values for object states, and update after a turn. + */ +void +obj_turn_update (sc_gameref_t game) +{ + sc_int index_; + + /* Update object seen flag to current state. */ + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (!gs_object_seen (game, index_) + && obj_indirectly_in_room (game, index_, gs_playerroom (game))) + gs_set_object_seen (game, index_, TRUE); + } +} + +void +obj_setup_initial (sc_gameref_t game) +{ + /* Set initial seen states for objects. */ + obj_turn_update (game); +} + + +/* + * obj_debug_trace() + * + * Set object tracing on/off. + */ +void +obj_debug_trace (sc_bool flag) +{ + obj_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scparser.cpp b/engines/glk/adrift/scparser.cpp new file mode 100644 index 0000000000..6f7642c3a7 --- /dev/null +++ b/engines/glk/adrift/scparser.cpp @@ -0,0 +1,2139 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Some of the "finer" points of pattern matching in relation to "*" + * wildcards, and %text%, are unknown. + * + * o The inclusion of part or all of prefixes in %character% and %object% + * matching may be right; then again, it may not be. + */ + +/* Assorted definitions and constants. */ +static const sc_char NUL = '\0'; +static const sc_char MINUS = '-'; +static const sc_char PLUS = '+'; +static const sc_char PERCENT = '%'; +static const sc_char *const WHITESPACE = "\t\n\v\f\r "; + +/* Pattern matching trace flag. */ +static sc_bool uip_trace = FALSE; + +/* Enumeration of tokens. TOK_NONE represents a non-occurring token. */ +typedef enum +{ + TOK_NONE = 0, + TOK_CHOICE, TOK_CHOICE_END, TOK_OPTIONAL, TOK_OPTIONAL_END, + TOK_ALTERNATES_SEPARATOR, + TOK_WILDCARD, TOK_WHITESPACE, TOK_WORD, TOK_VARIABLE, + TOK_CHARACTER_REFERENCE, TOK_OBJECT_REFERENCE, TOK_NUMBER_REFERENCE, + TOK_TEXT_REFERENCE, TOK_EOS +} sc_uip_tok_t; + +/* + * Small table tying token strings to tokens. Anything not whitespace and + * not caught by the table is a plain TOK_WORD. + */ +typedef struct +{ + const sc_char *const name; + const sc_int length; + const sc_uip_tok_t token; +} sc_uip_token_entry_t; + +static const sc_uip_token_entry_t UIP_TOKENS[] = { + {"[", 1, TOK_CHOICE}, {"]", 1, TOK_CHOICE_END}, + {"{", 1, TOK_OPTIONAL}, {"}", 1, TOK_OPTIONAL_END}, + {"/", 1, TOK_ALTERNATES_SEPARATOR}, + {"*", 1, TOK_WILDCARD}, + {"%character%", 11, TOK_CHARACTER_REFERENCE}, + {"%object%", 8, TOK_OBJECT_REFERENCE}, + {"%number%", 8, TOK_NUMBER_REFERENCE}, + {"%text%", 6, TOK_TEXT_REFERENCE}, + {NULL, 0, TOK_NONE} +}; + + +/* + * Tokenizer variables. The temporary is used for keeping word token values. + * For improved performance, we'll set it to indicate a static buffer if + * short enough, otherwise it's allocated. + */ +static const sc_char *uip_pattern = NULL; +static sc_int uip_index = 0; +static const sc_char *uip_token_value; +enum { UIP_ALLOCATION_AVOIDANCE_SIZE = 128 }; +static sc_char uip_static_temporary[UIP_ALLOCATION_AVOIDANCE_SIZE]; +static sc_char *uip_temporary = NULL; + +/* + * uip_tokenize_start() + * uip_tokenize_end() + * + * Start and wrap up pattern string tokenization. + */ +static void +uip_tokenize_start (const sc_char *pattern) +{ + static sc_bool initialized = FALSE; + sc_int required; + + /* On first call only, verify the string lengths in the table. */ + if (!initialized) + { + const sc_uip_token_entry_t *entry; + + /* Compare table lengths with string lengths. */ + for (entry = UIP_TOKENS; entry->name; entry++) + { + if (entry->length != (sc_int) strlen (entry->name)) + { + sc_fatal ("uip_tokenize_start:" + " table string length is wrong for \"%s\"\n", + entry->name); + } + } + + initialized = TRUE; + } + + /* Save pattern, and restart index. */ + uip_pattern = pattern; + uip_index = 0; + + /* Set up temporary; static if long enough, otherwise allocated. */ + required = strlen (pattern) + 1; + uip_temporary = (required > UIP_ALLOCATION_AVOIDANCE_SIZE) + ? (sc_char *)sc_malloc (required) : uip_static_temporary; +} + +static void +uip_tokenize_end (void) +{ + /* Deallocate temporary if required, and clear pattern and index. */ + if (uip_temporary != uip_static_temporary) + sc_free (uip_temporary); + uip_temporary = NULL; + uip_pattern = NULL; + uip_index = 0; +} + + +/* + * uip_next_token() + * + * Return the next token from the current pattern. + */ +static sc_uip_tok_t +uip_next_token (void) +{ + const sc_uip_token_entry_t *entry; + sc_char close; + assert (uip_pattern); + + /* Get next character, return EOS if at pattern end. */ + if (uip_pattern[uip_index] == NUL) + { + uip_token_value = NULL; + return TOK_EOS; + } + + /* If whitespace, skip it, then return a whitespace token. */ + if (sc_isspace (uip_pattern[uip_index])) + { + uip_index++; + while (sc_isspace (uip_pattern[uip_index]) + && uip_pattern[uip_index] != NUL) + uip_index++; + uip_token_value = NULL; + return TOK_WHITESPACE; + } + + /* Search the table for matching strings. */ + for (entry = UIP_TOKENS; entry->name; entry++) + { + if (strncmp (uip_pattern + uip_index, entry->name, entry->length) == 0) + break; + } + if (entry->name) + { + /* Advance over string, and return token. */ + uip_index += entry->length; + uip_token_value = NULL; + return entry->token; + } + + /* + * Search for a non-special variable reference. This is apparently an + * Adrift extension to the standard pattern match, allowing %user_var% to + * be used in patterns. If found, return a variable with the name as the + * token value. We can't interpolate the value into the string either + * here or earlier, so we have to save the variable's name, and retrieve + * it when we come to try the match. + */ + if (sscanf (uip_pattern + uip_index, "%%%[^%]%c", uip_temporary, &close) == 2 + && close == PERCENT) + { + uip_index += strlen (uip_temporary) + 2; + uip_token_value = uip_temporary; + return TOK_VARIABLE; + } + + /* + * Return a word. This is a contiguous run of non-pattern-special, non- + * whitespace, non-percent characters + */ + sscanf (uip_pattern + uip_index, "%[^][/{}*% \f\n\r\t\v]", uip_temporary); + uip_token_value = uip_temporary; + uip_index += strlen (uip_temporary); + return TOK_WORD; +} + + +/* + * uip_current_token_value() + * + * Return the token value of the current token. It is an error to call + * here if the current token is not a TOK_WORD or TOK_VARIABLE. + */ +static const sc_char * +uip_current_token_value (void) +{ + /* If the token value is NULL, the current token isn't a word. */ + if (!uip_token_value) + { + sc_fatal ("uip_current_token_value:" + " attempt to take undefined token value\n"); + } + + /* Return value. */ + return uip_token_value; +} + + +/* + * Parsed pattern tree node definition. The tree is a left child, right + * sibling representation, with token type, and word at nodes of type TOK_WORD. + * NODE_UNUSED must be zero to ensure that the statically allocated array that + * forms the node pool appears initially as containing only unused nodes. + */ +typedef enum +{ + NODE_UNUSED = 0, + NODE_CHOICE, NODE_OPTIONAL, NODE_WILDCARD, NODE_WHITESPACE, + NODE_CHARACTER_REFERENCE, NODE_OBJECT_REFERENCE, NODE_TEXT_REFERENCE, + NODE_NUMBER_REFERENCE, NODE_WORD, NODE_VARIABLE, NODE_LIST, NODE_EOS +} sc_pttype_t; +typedef struct sc_ptnode_s +{ + struct sc_ptnode_s *left_child; + struct sc_ptnode_s *right_sibling; + + sc_pttype_t type; + sc_char *word; + sc_bool is_allocated; +} sc_ptnode_t; +typedef sc_ptnode_t *sc_ptnoderef_t; + +/* Predictive parser lookahead token. */ +static sc_uip_tok_t uip_parse_lookahead = TOK_NONE; + +/* Parse error jump buffer. */ +static jmp_buf uip_parse_error; + +/* Parse tree for cleanup, and forward declaration of pattern list parser. */ +static sc_ptnoderef_t uip_parse_tree = NULL; +static void uip_parse_list (sc_ptnoderef_t list); + +/* + * Pool of statically allocated nodes, for faster allocations. Nodes are + * first allocated from here, then by straight malloc() if this pool is empty. + * An average game's peak node allocation seems to be around 40-50 nodes, so + * allowing 128 here should be plenty. + */ +enum { UIP_NODE_POOL_SIZE = 128 }; +static sc_ptnode_t uip_node_pool[UIP_NODE_POOL_SIZE]; +static sc_int uip_node_pool_cursor = 0; +static sc_int uip_node_pool_available = UIP_NODE_POOL_SIZE; + +/* + * Words held at nodes are usually short (15 chars covers 95% of English), so + * to avoid a lot of small allocations we use a pool of short strings, used + * first, then by straight malloc() should the pool empty. + */ +enum { UIP_WORD_POOL_SIZE = 64, UIP_SHORT_WORD_SIZE = 16 }; +typedef struct +{ + sc_bool is_in_use; + sc_char word[UIP_SHORT_WORD_SIZE]; +} sc_ptshortword_t; +typedef sc_ptshortword_t *sc_ptshortwordref_t; +static sc_ptshortword_t uip_word_pool[UIP_WORD_POOL_SIZE]; +static sc_int uip_word_pool_cursor = 0; +static sc_int uip_word_pool_available = UIP_WORD_POOL_SIZE; + +/* + * uip_parse_match() + * + * Match a token to the lookahead, then advance lookahead. + */ +static void +uip_parse_match (sc_uip_tok_t token) +{ + if (uip_parse_lookahead == token) + uip_parse_lookahead = uip_next_token (); + else + { + /* Syntax error. */ + sc_error ("uip_parse_match: syntax error, expected %ld, got %ld\n", + (sc_int) uip_parse_lookahead, (sc_int) token); + longjmp (uip_parse_error, 1); + } +} + + +/* + * uip_new_word() + * + * Return a string containing a copy of the word. Uses a ring allocator to + * allocate initially from static storage, for performance. If this is + * exhausted, backs off to standard allocation. + */ +static sc_char * +uip_new_word (const sc_char *word) +{ + sc_int required; + + /* + * Unless the pool is empty, search forwards from the next cursor position + * until an unused slot is found, or until the index wraps to the cursor. + */ + required = strlen (word) + 1; + if (uip_word_pool_available > 0 && required <= UIP_SHORT_WORD_SIZE) + { + sc_int index_; + sc_ptshortwordref_t shortword; + + index_ = (uip_word_pool_cursor + 1) % UIP_WORD_POOL_SIZE; + while (index_ != uip_word_pool_cursor) + { + if (!uip_word_pool[index_].is_in_use) + break; + index_ = (index_ + 1) % UIP_WORD_POOL_SIZE; + } + + if (uip_word_pool[index_].is_in_use) + sc_fatal ("uip_new_word: no free slot found in the words pool\n"); + + /* Use the slot and update the pool cursor and free count. */ + shortword = uip_word_pool + index_; + strcpy (shortword->word, word); + shortword->is_in_use = TRUE; + + uip_word_pool_cursor = index_; + uip_word_pool_available--; + + /* Return the address of the copied string. */ + return shortword->word; + } + else + { + sc_char *word_copy; + + /* Fall back to less efficient allocations. */ + word_copy = (sc_char *)sc_malloc (required); + strcpy (word_copy, word); + return word_copy; + } +} + + +/* + * uip_free_word() + * + * If the word was allocated, free its memory; if not, find its short word + * pool entry and return it to the pool. + */ +static void +uip_free_word (sc_char *word) +{ + const sc_char *first_in_pool, *last_in_pool; + + /* Obtain the range of valid addresses for words from the word pool. */ + first_in_pool = uip_word_pool[0].word; + last_in_pool = uip_word_pool[UIP_WORD_POOL_SIZE - 1].word; + + /* If from the pool, mark the entry as no longer in use, otherwise free. */ + if (word >= first_in_pool && word <= last_in_pool) + { + sc_int index_; + sc_ptshortwordref_t shortword; + + /* + * Calculate the index to the word pool entry from which this short + * word was allocated. + */ + index_ = (word - first_in_pool) / sizeof (uip_word_pool[0]); + shortword = uip_word_pool + index_; + assert (shortword->word == word); + + shortword->is_in_use = FALSE; + uip_word_pool_available++; + } + else + sc_free (word); +} + + +/* + * uip_new_node() + * + * Create a new node, populated with an initial type. Uses a ring allocator + * to allocate initially from static storage, for performance. If this is + * exhausted, backs off to standard allocation. + */ +static sc_ptnoderef_t +uip_new_node (sc_pttype_t type) +{ + sc_ptnoderef_t node; + + /* + * Unless the pool is empty, search forwards from the next cursor position + * until an unused slot is found, or until the index wraps to the cursor. + */ + if (uip_node_pool_available > 0) + { + sc_int index_; + + index_ = (uip_node_pool_cursor + 1) % UIP_NODE_POOL_SIZE; + while (index_ != uip_node_pool_cursor) + { + if (uip_node_pool[index_].type == NODE_UNUSED) + break; + index_ = (index_ + 1) % UIP_NODE_POOL_SIZE; + } + + if (uip_node_pool[index_].type != NODE_UNUSED) + sc_fatal ("uip_new_node: no free slot found in the nodes pool\n"); + + /* Use the slot and update the pool cursor and free count. */ + node = uip_node_pool + index_; + node->is_allocated = FALSE; + + uip_node_pool_cursor = index_; + uip_node_pool_available--; + } + else + { + /* Fall back to less efficient allocations. */ + node = (sc_ptnoderef_t)sc_malloc(sizeof (*node)); + node->is_allocated = TRUE; + } + + /* Fill in the remaining fields and return the new node. */ + node->left_child = NULL; + node->right_sibling = NULL; + node->type = type; + node->word = NULL; + + return node; +} + + +/* + * uip_destroy_node() + * + * Destroy a node, and any allocated word memory. If the node was allocated, + * free its memory; if not, return it to the pool. + */ +static void +uip_destroy_node (sc_ptnoderef_t node) +{ + /* Free any word contained at this node. */ + if (node->word) + uip_free_word (node->word); + + /* + * If the node was allocated, poison memory and free it. If it came from + * the node pool, set it to unused and update the availability count for + * the pool. + */ + if (node->is_allocated) + { + memset (node, 0xaa, sizeof (*node)); + sc_free (node); + } + else + { + node->type = NODE_UNUSED; + uip_node_pool_available++; + } +} + + +/* + * uip_parse_new_list() + * uip_parse_alternatives() + * + * Parse a set of .../.../... alternatives for choices and optionals. The + * first function is a helper, returning a newly constructed parsed list. + */ +static sc_ptnoderef_t +uip_parse_new_list (void) +{ + sc_ptnoderef_t list; + + /* Create a new list node, parse into it, and return it. */ + list = uip_new_node (NODE_LIST); + uip_parse_list (list); + return list; +} + +static void +uip_parse_alternatives (sc_ptnoderef_t node) +{ + sc_ptnoderef_t child; + + /* Parse initial alternative, then add other listed alternatives. */ + node->left_child = uip_parse_new_list (); + child = node->left_child; + while (uip_parse_lookahead == TOK_ALTERNATES_SEPARATOR) + { + uip_parse_match (TOK_ALTERNATES_SEPARATOR); + child->right_sibling = uip_parse_new_list (); + child = child->right_sibling; + } +} + + +/* + * uip_parse_element() + * + * Parse a single pattern element. + */ +static sc_ptnoderef_t +uip_parse_element (void) +{ + sc_ptnoderef_t node = NULL; + sc_uip_tok_t token; + + /* Handle pattern element based on lookahead token. */ + switch (uip_parse_lookahead) + { + case TOK_WHITESPACE: + uip_parse_match (TOK_WHITESPACE); + node = uip_new_node (NODE_WHITESPACE); + break; + + case TOK_CHOICE: + /* Parse a [...[/.../...]] choice. */ + uip_parse_match (TOK_CHOICE); + node = uip_new_node (NODE_CHOICE); + uip_parse_alternatives (node); + uip_parse_match (TOK_CHOICE_END); + break; + + case TOK_OPTIONAL: + /* Parse a {...[/.../...]} optional element. */ + uip_parse_match (TOK_OPTIONAL); + node = uip_new_node (NODE_OPTIONAL); + uip_parse_alternatives (node); + uip_parse_match (TOK_OPTIONAL_END); + break; + + case TOK_WILDCARD: + case TOK_CHARACTER_REFERENCE: + case TOK_OBJECT_REFERENCE: + case TOK_NUMBER_REFERENCE: + case TOK_TEXT_REFERENCE: + /* Parse %mumble% references and * wildcards. */ + token = uip_parse_lookahead; + uip_parse_match (token); + switch (token) + { + case TOK_WILDCARD: + node = uip_new_node (NODE_WILDCARD); + break; + case TOK_CHARACTER_REFERENCE: + node = uip_new_node (NODE_CHARACTER_REFERENCE); + break; + case TOK_OBJECT_REFERENCE: + node = uip_new_node (NODE_OBJECT_REFERENCE); + break; + case TOK_NUMBER_REFERENCE: + node = uip_new_node (NODE_NUMBER_REFERENCE); + break; + case TOK_TEXT_REFERENCE: + node = uip_new_node (NODE_TEXT_REFERENCE); + break; + default: + sc_fatal ("uip_parse_element: invalid token, %ld\n", (sc_int) token); + } + break; + + case TOK_WORD: + { + const sc_char *token_value; + sc_char *word; + + /* Take a copy of the token's word value. */ + token_value = uip_current_token_value (); + word = uip_new_word (token_value); + + /* Store details in a word node. */ + uip_parse_match (TOK_WORD); + node = uip_new_node (NODE_WORD); + node->word = word; + break; + } + + case TOK_VARIABLE: + { + const sc_char *token_value; + sc_char *name; + + /* Take a copy of the token's variable name value. */ + token_value = uip_current_token_value (); + name = uip_new_word (token_value); + + /* Store details in a variable node, overloading word. */ + uip_parse_match (TOK_VARIABLE); + node = uip_new_node (NODE_VARIABLE); + node->word = name; + break; + } + + default: + /* Syntax error. */ + sc_error ("uip_parse_element: syntax error," + " unexpected token, %ld\n", (sc_int) uip_parse_lookahead); + longjmp (uip_parse_error, 1); + } + + /* Return the newly created node. */ + assert (node); + return node; +} + + +/* + * uip_parse_list() + * + * Parse a list of pattern elements. + */ +static void +uip_parse_list (sc_ptnoderef_t list) +{ + sc_ptnoderef_t child, node; + + /* Add elements until a list terminator token is encountered. */ + child = list; + while (TRUE) + { + switch (uip_parse_lookahead) + { + case TOK_CHOICE_END: + case TOK_OPTIONAL_END: + case TOK_ALTERNATES_SEPARATOR: + /* Terminate list building and return. */ + return; + + case TOK_EOS: + /* Place EOS at the appropriate link and return. */ + node = uip_new_node (NODE_EOS); + if (child == list) + child->left_child = node; + else + child->right_sibling = node; + return; + + default: + /* Add the next node at the appropriate link. */ + node = uip_parse_element (); + if (child == list) + { + child->left_child = node; + child = child->left_child; + } + else + { + /* + * Make a special case of a choice or option next to another + * choice or option. In this case, add an (invented) whitespace + * node, to ensure a match with suitable input. + */ + if ((child->type == NODE_OPTIONAL || child->type == NODE_CHOICE) + && (node->type == NODE_OPTIONAL || node->type == NODE_CHOICE)) + { + sc_ptnoderef_t whitespace; + + /* Interpose invented whitespace. */ + whitespace = uip_new_node (NODE_WHITESPACE); + child->right_sibling = whitespace; + child = child->right_sibling; + } + + child->right_sibling = node; + child = child->right_sibling; + } + continue; + } + } +} + + +/* + * uip_destroy_tree() + * + * Free and destroy a parsed pattern tree. + */ +static void +uip_destroy_tree (sc_ptnoderef_t node) +{ + if (node) + { + /* Recursively destroy siblings, then left child. */ + uip_destroy_tree (node->right_sibling); + uip_destroy_tree (node->left_child); + + /* Destroy the node itself. */ + uip_destroy_node (node); + } +} + + +/* + * uip_debug_dump_node() + * uip_debug_dump() + * + * Print out a pattern match tree. + */ +static void +uip_debug_dump_node (sc_ptnoderef_t node, sc_int depth) +{ + /* End recursion on null node. */ + if (node) + { + sc_int index_; + + sc_trace (" "); + for (index_ = 0; index_ < depth; index_++) + sc_trace (" "); + + sc_trace ("%p", (void *) node); + switch (node->type) + { + case NODE_CHOICE: + sc_trace (", choice"); + break; + case NODE_OPTIONAL: + sc_trace (", optional"); + break; + case NODE_WILDCARD: + sc_trace (", wildcard"); + break; + case NODE_WHITESPACE: + sc_trace (", whitespace"); + break; + case NODE_CHARACTER_REFERENCE: + sc_trace (", character"); + break; + case NODE_OBJECT_REFERENCE: + sc_trace (", object"); + break; + case NODE_TEXT_REFERENCE: + sc_trace (", text"); + break; + case NODE_NUMBER_REFERENCE: + sc_trace (", number"); + break; + case NODE_WORD: + sc_trace (", word \"%s\"", node->word); + break; + case NODE_VARIABLE: + sc_trace (", variable \"%s\"", node->word); + break; + case NODE_LIST: + sc_trace (", list"); + break; + case NODE_EOS: + sc_trace (", <eos>"); + break; + default: + sc_trace (", unknown type %ld", (sc_int) node->type); + break; + } + if (node->left_child) + sc_trace (", left child %p", (void *) node->left_child); + if (node->right_sibling) + sc_trace (", right sibling %p", (void *) node->right_sibling); + sc_trace ("\n"); + + /* Recursively dump left child, then siblings. */ + uip_debug_dump_node (node->left_child, depth + 1); + uip_debug_dump_node (node->right_sibling, depth); + } +} + +static void +uip_debug_dump (void) +{ + sc_trace ("UIParser: debug dump follows...\n"); + if (uip_parse_tree) + { + sc_trace ("uip_parse_tree = {\n"); + uip_debug_dump_node (uip_parse_tree, 0); + sc_trace ("}\n"); + } + else + sc_trace ("uip_parse_tree = (nil)\n"); +} + + +/* String matching variables. */ +static const sc_char *uip_string = NULL; +static sc_int uip_posn = 0; +static sc_gameref_t uip_game = NULL; + +/* + * uip_match_start() + * uip_match_end() + * + * Set up a string for matching to a pattern tree, and wrap up matching. + */ +static void +uip_match_start (const sc_char *string, sc_gameref_t game) +{ + /* Save string, and restart index. */ + uip_string = string; + uip_posn = 0; + + /* Save the game we're working on. */ + uip_game = game; +} + +static void +uip_match_end (void) +{ + /* Clear match target string, and variable set. */ + uip_string = NULL; + uip_posn = 0; + uip_game = NULL; +} + + +/* + * uip_get_game() + * + * Safety wrapper to ensure module code sees a valid game when it requires + * one. + */ +static sc_gameref_t +uip_get_game (void) +{ + assert (gs_is_game_valid (uip_game)); + return uip_game; +} + + +/* Forward declaration of low level node matcher. */ +static sc_bool uip_match_node (sc_ptnoderef_t node); + +/* + * uip_match_eos() + * uip_match_word() + * uip_match_variable() + * uip_match_whitespace() + * uip_match_list() + * uip_match_alternatives() + * uip_match_choice() + * uip_match_optional() + * uip_match_wildcard() + * + * Text element and list/choice element match functions. Return TRUE, and + * advance position if necessary, on match, FALSE on no match, with position + * unchanged. + */ +static sc_bool +uip_match_eos (void) +{ + /* Check that we hit the string's end. */ + return uip_string[uip_posn] == NUL; +} + +static sc_bool +uip_match_word (sc_ptnoderef_t node) +{ + sc_int length; + const sc_char *word; + + /* Get the word to match. */ + assert (node->word); + word = node->word; + + /* Compare string text with this node's word, ignore case. */ + length = strlen (word); + if (sc_strncasecmp (uip_string + uip_posn, word, length) == 0) + { + /* Word match, advance position and return. */ + uip_posn += length; + return TRUE; + } + + /* No match. */ + return FALSE; +} + +static sc_bool +uip_match_variable (sc_ptnoderef_t node) +{ + const sc_gameref_t game = uip_get_game (); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int type; + sc_vartype_t vt_rvalue; + const sc_char *name; + + /* Get the variable name to match, from overloaded word. */ + assert (node->word); + name = node->word; + + /* Get the variable's value. */ + if (var_get (vars, name, &type, &vt_rvalue)) + { + sc_int length; + + /* Compare the value against the current string position. */ + switch (type) + { + case VAR_INTEGER: + { + sc_char value[32]; + + /* Compare numeric against the current string position. */ + sprintf (value, "%ld", vt_rvalue.integer); + length = strlen (value); + if (strncmp (uip_string + uip_posn, value, length) == 0) + { + /* Integer match, advance position and return. */ + uip_posn += length; + return TRUE; + } + break; + } + + case VAR_STRING: + /* Compare string value against the current string position. */ + length = strlen (vt_rvalue.string); + if (sc_strncasecmp (uip_string + uip_posn, + vt_rvalue.string, length) == 0) + { + /* String match, advance position and return. */ + uip_posn += length; + return TRUE; + } + break; + + default: + sc_fatal ("uip_match_variable: invalid variable type, %ld\n", type); + } + } + + /* No match, or no such variable. */ + return FALSE; +} + +static sc_bool +uip_match_whitespace (void) +{ + /* If next character is space, read whitespace and return. */ + if (sc_isspace (uip_string[uip_posn])) + { + /* Space match, advance position and return. */ + while (uip_string[uip_posn] != NUL && sc_isspace (uip_string[uip_posn])) + uip_posn++; + return TRUE; + } + + /* + * No match. However, if we're trying to match space, this is a word + * boundary. So... even though we're not sitting on a space, if the string + * prior character is whitespace, "double-match" the space. + * + * Also, match if we haven't yet matched any text. In effect, this means + * leading spaces on patterns will be ignored. + * + * TODO Is this what we want to happen? It seems harmless, even useful. + */ + if (uip_posn == 0 || sc_isspace (uip_string[uip_posn - 1])) + return TRUE; + + /* + * And that's not all. We also want to match whitespace if we're at the end + * of a string (another word boundary). This will permit patterns that end + * in optional elements to succeed since options and wildcards always match, + * even if to no text. + */ + if (uip_string[uip_posn] == NUL) + return TRUE; + + /* No match. Really. */ + return FALSE; +} + +static sc_bool +uip_match_list (sc_ptnoderef_t node) +{ + sc_ptnoderef_t child; + + /* + * If this list is empty, fail the match. This special-case handling is + * what catches constructed temporary lists for wildcard-like items that + * don't actually encompass anything. + */ + if (!node->left_child) + return FALSE; + + /* Match everything listed sequentially. */ + for (child = node->left_child; child; child = child->right_sibling) + { + if (!uip_match_node (child)) + { + /* No match. */ + return FALSE; + } + } + + /* Matched. */ + return TRUE; +} + +static sc_bool +uip_match_alternatives (sc_ptnoderef_t node) +{ + sc_ptnoderef_t child; + sc_int start_posn, extent; + sc_bool matched; + + /* Note the start position for rewind between tries. */ + start_posn = uip_posn; + + /* + * Try a match on each of the children, looking to see which one moves the + * position on the furthest. Match on this one. This is a "maximal munch". + */ + extent = uip_posn; + matched = FALSE; + for (child = node->left_child; child; child = child->right_sibling) + { + uip_posn = start_posn; + if (uip_match_node (child)) + { + /* Matched. */ + matched = TRUE; + if (uip_posn > extent) + extent = uip_posn; + } + } + + /* If matched, set position to extent; if not, back to start. */ + uip_posn = matched ? extent : start_posn; + + /* Return match status. */ + return matched; +} + +static sc_bool +uip_match_choice (sc_ptnoderef_t node) +{ + /* + * Return the result of matching alternatives. The choice will therefore + * fail if none of the alternatives match. + */ + return uip_match_alternatives (node); +} + +static sc_bool +uip_match_optional (sc_ptnoderef_t node) +{ + sc_int start_posn; + sc_ptnoderef_t list; + sc_bool matched; + + /* Note the start position for rewind on empty match. */ + start_posn = uip_posn; + + /* + * Look ahead to see if we can match to nothing, and still have the main + * pattern match. If we can, we'll go with this. It's a "minimal munch"-ish + * strategy, but seems to be what Adrift does in this situation. + */ + list = uip_new_node (NODE_LIST); + list->left_child = node->right_sibling; + + /* Match on the temporary list. */ + matched = uip_match_node (list); + + /* Free the temporary list node. */ + uip_destroy_node (list); + + /* + * If the temporary matched and consumed text, rewind position to match + * nothing. If it didn't, match alternatives to consume anything that may + * match our options. + */ + if (matched && uip_posn > start_posn) + uip_posn = start_posn; + else + uip_match_alternatives (node); + + /* Return TRUE no matter what. */ + return TRUE; +} + +static sc_bool +uip_match_wildcard (sc_ptnoderef_t node) +{ + sc_int start_posn, limit, index_; + sc_bool matched; + sc_ptnoderef_t list; + + /* + * At least one game uses patterns like "thing******...". Why? Who knows. + * But if we're in a list of wildcards, and not the first, ignore the call; + * only the final one needs handling. + */ + if (node->right_sibling && node->right_sibling->type == NODE_WILDCARD) + return TRUE; + + /* Note the start position for rewind on no match. */ + start_posn = uip_posn; + + /* + * To make life a little easier, we'll match on the tree to the right of + * this node by constructing a temporary list node, containing stuff to the + * right of the wildcard, and then matching on that. + */ + list = uip_new_node (NODE_LIST); + list->left_child = node->right_sibling; + + /* + * Repeatedly try to match the rest of the tree at successive character + * positions, and stop if we succeed. This is a "minimal munch", which may + * or may not be the right thing to be doing here. + * + * When scanning forward, take care to include the NUL, needed to match + * TOK_EOS. + */ + matched = FALSE; + limit = strlen (uip_string) + 1; + for (index_ = uip_posn + 1; index_ < limit; index_++) + { + uip_posn = index_; + if (uip_match_node (list)) + { + /* Wildcard match at this point. */ + uip_posn = index_; + matched = TRUE; + break; + } + } + + /* Free the temporary list node. */ + uip_destroy_node (list); + + /* If we didn't match in the loop, restore position. */ + if (!matched) + uip_posn = start_posn; + + /* Return TRUE whether we matched text or not. */ + return TRUE; +} + + +/* + * uip_match_number() + * uip_match_text() + * + * Attempt to match a number, or a word, from the string. + */ +static sc_bool +uip_match_number (void) +{ + const sc_gameref_t game = uip_get_game (); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int number; + + /* Attempt to read a number from input. */ + if (sscanf (uip_string + uip_posn, "%ld", &number) == 1) + { + /* Advance position over the number. */ + while (uip_string[uip_posn] == MINUS || uip_string[uip_posn] == PLUS) + uip_posn++; + while (sc_isdigit (uip_string[uip_posn])) + uip_posn++; + + /* Set number reference in variables and return. */ + var_set_ref_number (vars, number); + return TRUE; + } + + /* No match. */ + return FALSE; +} + +static sc_bool +uip_match_text (sc_ptnoderef_t node) +{ + const sc_gameref_t game = uip_get_game (); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int start_posn, limit, index_; + sc_bool matched; + sc_ptnoderef_t list; + + /* Note the start position for rewind on no match. */ + start_posn = uip_posn; + + /* + * As with wildcards, create a temporary list of the stuff to the right of + * the reference node, and match on that. + */ + list = uip_new_node (NODE_LIST); + list->left_child = node->right_sibling; + + /* + * Again, as with wildcards, repeatedly try to match the rest of the tree at + * successive character positions, stopping if we succeed. + */ + matched = FALSE; + limit = strlen (uip_string) + 1; + for (index_ = uip_posn + 1; index_ < limit; index_++) + { + uip_posn = index_; + if (uip_match_node (list)) + { + /* Text reference match at this point. */ + uip_posn = index_; + matched = TRUE; + break; + } + } + + /* Free the temporary list node. */ + uip_destroy_node (list); + + /* See if we found a match in the loop. */ + if (matched) + { + sc_char *string; + + /* Found a match; create a string and save the text. */ + string = (sc_char *)sc_malloc (uip_posn - start_posn + 1); + memcpy (string, uip_string + start_posn, uip_posn - start_posn); + string[uip_posn - start_posn] = NUL; + + /* + * Adrift seems to save referenced text as all-lowercase; we need to do + * the same. + */ + for (index_ = 0; string[index_] != NUL; index_++) + string[index_] = sc_tolower (string[index_]); + var_set_ref_text (vars, string); + sc_free (string); + + /* Return TRUE since we matched text. */ + return TRUE; + } + else + { + /* We didn't match in the loop; restore position. */ + uip_posn = start_posn; + + /* Return FALSE on no match. */ + return FALSE; + } +} + + +/* + * uip_skip_article() + * + * Skip over any "a"/"an"/"the"/"some" at the head of a string. Helper for + * %character% and %object% matchers. Returns the revised string position. + */ +static sc_int +uip_skip_article (const sc_char *string, sc_int start) +{ + sc_int posn; + + /* Skip over articles. */ + posn = start; + if (sc_compare_word (string + posn, "a", 1)) + posn += 1; + else if (sc_compare_word (string + posn, "an", 2)) + posn += 2; + else if (sc_compare_word (string + posn, "the", 3)) + posn += 3; + else if (sc_compare_word (string + posn, "some", 4)) + posn += 4; + + /* Skip any whitespace, and return. */ + while (sc_isspace (string[posn]) && string[posn] != NUL) + posn++; + return posn; +} + + +/* + * uip_compare_reference() + * + * Helper for %character% and %object% matchers. Matches multiple words + * if necessary, at the current position. Returns zero if the string + * didn't match, otherwise the length of the current position that matched + * the words passed in (the new value of uip_posn on match). + */ +static sc_int +uip_compare_reference (const sc_char *words) +{ + sc_int wpos, posn; + + /* Skip articles and lead in space on words and string. */ + wpos = uip_skip_article (words, 0); + posn = uip_skip_article (uip_string, uip_posn); + + /* Match characters from words with the string at position. */ + while (TRUE) + { + /* Any character mismatch means no words match. */ + if (sc_tolower (words[wpos]) != sc_tolower (uip_string[posn])) + return 0; + + /* Move to next character in each. */ + wpos++; + posn++; + + /* + * If at space, advance over whitespace in words list. Stop when we + * hit the end of the words list. + */ + while (sc_isspace (words[wpos]) && words[wpos] != NUL) + wpos++; + if (words[wpos] == NUL) + break; + + /* + * About to match another word, so advance over whitespace in the + * current string too. + */ + while (sc_isspace (uip_string[posn]) && uip_string[posn] != NUL) + posn++; + } + + /* + * We reached the end of words. If we're at the end of the match string, or + * at spaces, we've matched. + */ + if (sc_isspace (uip_string[posn]) || uip_string[posn] == NUL) + return posn; + + /* More text after the match, so it's not quite a match. */ + return 0; +} + + +/* + * uip_compare_prefixed_name() + * + * Helper for %character% and %object% matchers. Attempts a reference + * match against both the prefixed name, and if that fails, the plain name. + * Returns the extent of the match, or zero if no match. + */ +static sc_int +uip_compare_prefixed_name (const sc_char *prefix, const sc_char *name) +{ + sc_char buffer[UIP_SHORT_WORD_SIZE + UIP_SHORT_WORD_SIZE + 1]; + sc_char *string; + sc_int required, extent; + + /* Create a prefixed string, using the local buffer if possible. */ + required = strlen (prefix) + strlen (name) + 2; + string = required > (sc_int) sizeof (buffer) ? (sc_char *)sc_malloc (required) : buffer; + sprintf (string, "%s %s", prefix, name); + + /* Check against the prefixed name first, free string if required. */ + extent = uip_compare_reference (string); + if (string != buffer) + sc_free (string); + + /* If no match there, retry with just the plain name. */ + if (extent == 0) + extent = uip_compare_reference (name); + + /* Return the count of characters consumed in matching. */ + return extent; +} + + +/* + * uip_match_remainder() + * + * Helper for %character% and %object% matchers. Matches the remainder + * of a pattern, to resolve the difference between, say, "table leg" and + * "table". + */ +static sc_bool +uip_match_remainder (sc_ptnoderef_t node, sc_int extent) +{ + sc_ptnoderef_t list; + sc_int start_posn; + sc_bool matched; + + /* Note the start position, then advance to the given extent. */ + start_posn = uip_posn; + uip_posn = extent; + + /* + * Try to match everything after the node passed in, at this position in the + * string. + */ + list = uip_new_node (NODE_LIST); + list->left_child = node->right_sibling; + + /* Match on the temporary list. */ + matched = uip_match_node (list); + + /* Free the temporary list node, and restore position. */ + uip_destroy_node (list); + uip_posn = start_posn; + + /* Return TRUE if the pattern remainder matched. */ + return matched; +} + + +/* + * uip_match_character() + * + * Match a %character% reference. This function searches all NPC names and + * aliases for possible matches, and sets the game npc_references flag + * for any that match. The final one to match is also stored in variables. + */ +static sc_bool +uip_match_character (sc_ptnoderef_t node) +{ + const sc_gameref_t game = uip_get_game (); + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int npc_count, npc, max_extent; + + if (uip_trace) + sc_trace ("UIParser: attempting to match %%character%%\n"); + + /* Clear all current character references. */ + gs_clear_npc_references (game); + + /* Iterate characters, looking for a name or alias match. */ + max_extent = 0; + npc_count = gs_npc_count (game); + for (npc = 0; npc < npc_count; npc++) + { + sc_vartype_t vt_key[4]; + const sc_char *prefix, *name; + sc_int alias_count, alias, extent; + + /* Get the NPC's prefix and name. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + if (uip_trace) + sc_trace ("UIParser: trying %s\n", name); + + /* Compare this name, both prefixed and not. */ + extent = uip_compare_prefixed_name (prefix, name); + if (extent > 0 && uip_match_remainder (node, extent)) + { + if (uip_trace) + sc_trace ("UIParser: matched\n"); + + /* Increase the maximum match extent if required. */ + max_extent = (extent > max_extent) ? extent : max_extent; + + /* Save match in variables and game. */ + var_set_ref_character (vars, npc); + game->npc_references[npc] = TRUE; + } + + /* Now compare against all NPC aliases. */ + vt_key[2].string = "Alias"; + alias_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + for (alias = 0; alias < alias_count; alias++) + { + const sc_char *alias_name; + + /* + * Get the NPC alias. Version 3.9 games introduce empty aliases, + * so check here. + */ + vt_key[3].integer = alias; + alias_name = prop_get_string (bundle, "S<-sisi", vt_key); + if (sc_strempty (alias_name)) + continue; + + if (uip_trace) + sc_trace ("UIParser: trying alias %s\n", alias_name); + + /* Compare this alias name, both prefixed and not. */ + extent = uip_compare_prefixed_name (prefix, alias_name); + if (extent > 0 && uip_match_remainder (node, extent)) + { + if (uip_trace) + sc_trace ("UIParser: matched\n"); + + /* Increase the maximum match extent if required. */ + max_extent = (extent > max_extent) ? extent : max_extent; + + /* Save match in variables and game. */ + var_set_ref_character (vars, npc); + game->npc_references[npc] = TRUE; + } + } + } + + /* On match, advance position and return successfully. */ + if (max_extent > 0) + { + uip_posn = max_extent; + return TRUE; + } + + /* No match. */ + return FALSE; +} + + +/* + * uip_match_object() + * + * Match an %object% reference. This function searches all object names and + * aliases for possible matches, and sets the game object_references flag + * for any that match. The final one to match is also stored in variables. + */ +static sc_bool +uip_match_object (sc_ptnoderef_t node) +{ + const sc_gameref_t game = uip_get_game (); + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int object_count, object, max_extent; + + if (uip_trace) + sc_trace ("UIParser: attempting to match %%object%%\n"); + + /* Clear all current object references. */ + gs_clear_object_references (game); + + /* Iterate objects, looking for a name or alias match. */ + max_extent = 0; + object_count = gs_object_count (game); + for (object = 0; object < object_count; object++) + { + sc_vartype_t vt_key[4]; + const sc_char *prefix, *name; + sc_int alias_count, alias, extent; + + /* Get the object's prefix and name. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + if (uip_trace) + sc_trace ("UIParser: trying %s\n", name); + + /* Compare this name, both prefixed and not. */ + extent = uip_compare_prefixed_name (prefix, name); + if (extent > 0 && uip_match_remainder (node, extent)) + { + if (uip_trace) + sc_trace ("UIParser: matched\n"); + + /* Increase the maximum match extent if required. */ + max_extent = (extent > max_extent) ? extent : max_extent; + + /* Save match in variables and game. */ + var_set_ref_object (vars, object); + game->object_references[object] = TRUE; + } + + /* Now compare against all object aliases. */ + vt_key[2].string = "Alias"; + alias_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + for (alias = 0; alias < alias_count; alias++) + { + const sc_char *alias_name; + + /* + * Get the object alias. Version 3.9 games introduce empty aliases, + * so check here. + */ + vt_key[3].integer = alias; + alias_name = prop_get_string (bundle, "S<-sisi", vt_key); + if (sc_strempty (alias_name)) + continue; + + if (uip_trace) + sc_trace ("UIParser: trying alias %s\n", alias_name); + + /* Compare this alias name, both prefixed and not. */ + extent = uip_compare_prefixed_name (prefix, alias_name); + if (extent > 0 && uip_match_remainder (node, extent)) + { + if (uip_trace) + sc_trace ("UIParser: matched\n"); + + /* Increase the maximum match extent if required. */ + max_extent = (extent > max_extent) ? extent : max_extent; + + /* Save match in variables and game. */ + var_set_ref_object (vars, object); + game->object_references[object] = TRUE; + } + } + } + + /* On match, advance position and return successfully. */ + if (max_extent > 0) + { + uip_posn = max_extent; + return TRUE; + } + + /* No match. */ + return FALSE; +} + + +/* + * uip_match_node() + * + * Attempt to match the given node to the current match string/position. + * Return TRUE, with position advanced, on match, FALSE on fail with the + * position unchanged. + */ +static sc_bool +uip_match_node (sc_ptnoderef_t node) +{ + sc_bool match = FALSE; + + /* Match depending on node type. */ + switch (node->type) + { + case NODE_EOS: + match = uip_match_eos (); + break; + case NODE_WORD: + match = uip_match_word (node); + break; + case NODE_VARIABLE: + match = uip_match_variable (node); + break; + case NODE_WHITESPACE: + match = uip_match_whitespace (); + break; + case NODE_LIST: + match = uip_match_list (node); + break; + case NODE_CHOICE: + match = uip_match_choice (node); + break; + case NODE_OPTIONAL: + match = uip_match_optional (node); + break; + case NODE_WILDCARD: + match = uip_match_wildcard (node); + break; + case NODE_CHARACTER_REFERENCE: + match = uip_match_character (node); + break; + case NODE_OBJECT_REFERENCE: + match = uip_match_object (node); + break; + case NODE_NUMBER_REFERENCE: + match = uip_match_number (); + break; + case NODE_TEXT_REFERENCE: + match = uip_match_text (node); + break; + default: + sc_fatal ("uip_match_node: invalid type, %ld\n", (sc_int) node->type); + } + + return match; +} + + +/* + * uip_cleanse_string() + * uip_free_cleansed_string() + * + * Given a string, copy it to the given buffer, and trim leading and trailing + * spaces from it. If the string does not fit the buffer, malloc enough for + * a string copy and use that instead. The caller needs to free this + * allocation if it happens (detectable by comparing the return value to the + * buffer passed in), or call uip_free_cleansed_string. + */ +static sc_char * +uip_cleanse_string (const sc_char *original, sc_char *buffer, sc_int length) +{ + sc_int required; + sc_char *string; + + /* + * Use the supplied buffer if it is long enough, otherwise allocate, and + * copy the string. + */ + required = strlen (original) + 1; + string = (required < length) ? buffer : (sc_char *)sc_malloc (required); + strcpy (string, original); + + /* Trim, and return the string. */ + sc_trim_string (string); + return string; +} + +static sc_char * +uip_free_cleansed_string (sc_char *string, const sc_char *buffer) +{ + /* Free if the string was allocated by the function above. */ + if (string != buffer) + sc_free (string); + + /* Always returns NULL, for the syntactic convenience of the caller. */ + return NULL; +} + + +/* + * uip_debug_trace() + * + * Set pattern match tracing on/off. + */ +void +uip_debug_trace (sc_bool flag) +{ + uip_trace = flag; +} + + +/* + * uip_match() + * + * Match a string to a pattern, and return TRUE on match, FALSE otherwise. + * For performance, this function uses a local buffer to try to avoid the + * need to copy each of the pattern and match strings passed in. + */ +sc_bool +uip_match (const sc_char *pattern, const sc_char *string, sc_gameref_t game) +{ + static sc_char *cleansed; /* For setjmp safety. */ + sc_char buffer[UIP_ALLOCATION_AVOIDANCE_SIZE]; + sc_bool match; + assert (pattern && string && game); + + /* Start tokenizer. */ + cleansed = uip_cleanse_string (pattern, buffer, sizeof (buffer)); + if (uip_trace) + sc_trace ("UIParser: pattern \"%s\"\n", cleansed); + uip_tokenize_start (cleansed); + + /* Try parsing the pattern, and catch errors. */ + if (setjmp (uip_parse_error) == 0) + { + /* Parse the pattern into a match tree. */ + uip_parse_lookahead = uip_next_token (); + uip_parse_tree = uip_new_node (NODE_LIST); + uip_parse_list (uip_parse_tree); + uip_tokenize_end (); + cleansed = uip_free_cleansed_string (cleansed, buffer); + } + else + { + /* Parse error -- clean up and fail. */ + uip_tokenize_end (); + uip_destroy_tree (uip_parse_tree); + uip_parse_tree = NULL; + cleansed = uip_free_cleansed_string (cleansed, buffer); + return FALSE; + } + + /* Dump out the pattern tree if requested. */ + if (if_get_trace_flag (SC_DUMP_PARSER_TREES)) + uip_debug_dump (); + + /* Match the string to the pattern tree. */ + cleansed = uip_cleanse_string (string, buffer, sizeof (buffer)); + if (uip_trace) + sc_trace ("UIParser: string \"%s\"\n", cleansed); + uip_match_start (cleansed, game); + match = uip_match_node (uip_parse_tree); + + /* Clean up matching and free the parsed pattern tree. */ + uip_match_end (); + cleansed = uip_free_cleansed_string (cleansed, buffer); + uip_destroy_tree (uip_parse_tree); + uip_parse_tree = NULL; + + /* Return result of matching. */ + if (uip_trace) + sc_trace ("UIParser: %s\n", match ? "MATCHED!" : "No match"); + return match; +} + + +/* + * uip_replace_pronouns() + * + * Replaces pronouns by their respective object or NPC names, and returns the + * resulting string to the caller, or NULL if no pronouns were replaced. The + * return string is malloc'ed, so the caller needs to remember to free it. + */ +sc_char * +uip_replace_pronouns (sc_gameref_t game, const sc_char *string) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_int buffer_allocation; + sc_char *buffer; + const sc_char *current; + assert (string); + + if (uip_trace) + sc_trace ("UIParser: pronoun search \"%s\"\n", string); + + /* Begin with a NULL buffer for lazy allocation. */ + buffer_allocation = 0; + buffer = NULL; + + /* Search for pronouns until no more string remains. */ + current = string + strspn (string, WHITESPACE); + while (current[0] != NUL) + { + sc_vartype_t vt_key[3]; + sc_int object, npc, extent; + const sc_char *prefix, *name; + + /* Initially, no object or NPC, no names, and a zero extent. */ + object = npc = -1; + prefix = name = NULL; + extent = 0; + + /* + * Search for pronouns where we have an assigned object or NPC. We + * can't be sure of getting plurality right, and it's not always + * intuitive even in English -- is "a pair of scissors" an "it", or + * a "them"? Because of this, we just treat "it" and "them" equally + * for now. + */ + if (game->it_object != -1 && sc_compare_word (current, "it", 2)) + { + object = game->it_object; + extent = 2; + } + else if (game->it_object != -1 && sc_compare_word (current, "them", 4)) + { + object = game->it_object; + extent = 4; + } + else if (game->him_npc != -1 && sc_compare_word (current, "him", 3)) + { + npc = game->him_npc; + extent = 3; + } + else if (game->her_npc != -1 && sc_compare_word (current, "her", 3)) + { + npc = game->her_npc; + extent = 3; + } + else if (game->it_npc != -1 && sc_compare_word (current, "it", 2)) + { + npc = game->it_npc; + extent = 2; + } + + /* Assign prefix and name to the full object or NPC name, if any. */ + if (object > -1) + { + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + } + else if (npc > -1) + { + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + } + + /* + * If a pronoun was found, prefix and name indicate what to insert, and + * extent shows how much of the buffer to replace with them. + */ + if (prefix && name && extent > 0) + { + sc_char *position; + sc_int prefix_length, name_length, length, final; + + /* + * If not yet allocated, allocate a buffer now, and copy the input + * string into it. Then switch current to the equivalent location + * in the allocated buffer; basic copy-on-write. + */ + if (!buffer) + { + buffer_allocation = strlen (string) + 1; + buffer = (sc_char *)sc_malloc (buffer_allocation); + strcpy (buffer, string); + current = buffer + (current - string); + } + + /* + * If necessary, grow the output buffer for the replacement, + * remembering to adjust current for the new buffer allocated. + * At the same time, note the last character index for the move. + */ + prefix_length = strlen (prefix); + name_length = strlen (name); + length = prefix_length + name_length + 1; + if (length > extent) + { + sc_int offset; + + offset = current - buffer; + buffer_allocation += length - extent; + buffer = (sc_char *)sc_realloc (buffer, buffer_allocation); + current = buffer + offset; + final = length; + } + else + final = extent; + + /* Insert the replacement strings into the buffer. */ + position = buffer + (current - buffer); + memmove (position + length, + position + extent, + buffer_allocation - (current - buffer) - final); + memcpy (position, prefix, prefix_length); + position[prefix_length] = ' '; + memcpy (position + prefix_length + 1, name, name_length); + + /* Adjust current to skip over the replacement. */ + current += length; + + if (uip_trace) + sc_trace ("Parser: pronoun \"%s\"\n", buffer); + } + else + { + /* If no match, advance current over the unmatched word. */ + current += strcspn (current, WHITESPACE); + } + + /* Set current to the next word start. */ + current += strspn (current, WHITESPACE); + } + + /* Return the final string, or NULL if no pronoun replacements. */ + return buffer; +} + + +/* + * uip_assign_pronouns() + * + * Search a player command for object and NPC names, and assign any found to + * game pronouns. The string is searched from front to back, assigning + * pronouns for objects or NPC names as found. Later ones will overwrite + * earlier ones if there is more than one in the string. + */ +void +uip_assign_pronouns (sc_gameref_t game, const sc_char *string) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_char *current; + sc_int saved_ref_object, saved_ref_character; + assert (string); + + if (uip_trace) + sc_trace ("UIParser: pronoun assignment \"%s\"\n", string); + + /* Save var references so we can restore them later. */ + saved_ref_object = var_get_ref_object (vars); + saved_ref_character = var_get_ref_character (vars); + + /* Search for object and NPC names until no more string remains. */ + current = string + strspn (string, WHITESPACE); + while (current[0] != NUL) + { + if (uip_match ("%object% *", current, game)) + { + sc_int count, index_, object; + + /* + * "Disambiguate" by rejecting objects that the player hasn't seen + * or can't see. If the reference is unique, assign to the 'it' + * object pronoun. + */ + count = 0; + object = -1; + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + if (game->object_references[index_] + && gs_object_seen (game, index_) + && obj_indirectly_in_room (game, + index_, gs_playerroom (game))) + { + count++; + object = index_; + } + } + + if (count == 1) + { + game->it_object = object; + game->it_npc = -1; + + if (uip_trace) + sc_trace ("UIParser: object 'it/them' assigned %ld\n", object); + } + } + + if (uip_match ("%character% *", current, game)) + { + sc_int count, index_, npc; + + /* Do the same "disambiguation" as for objects above. */ + count = 0; + npc = -1; + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + if (game->npc_references[index_] + && gs_npc_seen (game, index_) + && npc_in_room (game, index_, gs_playerroom (game))) + { + count++; + npc = index_; + } + } + + if (count == 1) + { + sc_vartype_t vt_key[3]; + sc_int version, gender; + + /* + * Version 3.8 games lack NPC gender information, so for this + * case set "him"/"her" on each match, and never set "it"; this + * matches the version 3.8 runner. + */ + vt_key[0].string = "Version"; + version = prop_get_integer (bundle, "I<-s", vt_key); + if (version == TAF_VERSION_380) + { + game->him_npc = npc; + game->her_npc = npc; + game->it_npc = -1; + + if (uip_trace) + { + sc_trace ("UIParser: 3.8 pronouns" + " 'him' and 'her' assigned %ld\n", npc); + } + } + else + { + /* Find the NPC gender, so we know the pronoun to assign. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc; + vt_key[2].string = "Gender"; + gender = prop_get_integer (bundle, "I<-sis", vt_key); + + switch (gender) + { + case NPC_MALE: + game->him_npc = npc; + break; + case NPC_FEMALE: + game->her_npc = npc; + break; + case NPC_NEUTER: + game->it_npc = npc; + game->it_object = -1; + break; + default: + sc_error ("uip_assign_pronouns:" + " unknown gender, %ld\n", gender); + } + + if (uip_trace) + sc_trace ("UIParser: NPC 'him/her/it' assigned %ld\n", npc); + } + } + } + + /* + * Advance the string position by a complete word. This saves a lot + * of time -- there's no point looking for an object or NPC name in + * mid-word, and anyway it's not the right thing to do. + */ + current += strcspn (current, WHITESPACE); + current += strspn (current, WHITESPACE); + } + + /* Restore variables references. */ + var_set_ref_object (vars, saved_ref_object); + var_set_ref_character (vars, saved_ref_character); +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scprintf.cpp b/engines/glk/adrift/scprintf.cpp new file mode 100644 index 0000000000..91ee94e94f --- /dev/null +++ b/engines/glk/adrift/scprintf.cpp @@ -0,0 +1,1588 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Is the whole interpolation and ALR passes thing right? There's no + * documentation on it, and it's not intuitively implemented in Adrift. + * + * o Is dissecting HTML tags the right thing to do? + */ + +/* Assorted definitions and constants. */ +static const sc_uint PRINTFILTER_MAGIC = 0xb4736417; +enum +{ BUFFER_GROW_INCREMENT = 32, + ITERATION_LIMIT = 32 +}; +static const sc_char NUL = '\0'; +static const sc_char LESSTHAN = '<'; +static const sc_char GREATERTHAN = '>'; +static const sc_char PERCENT = '%'; +static const sc_char *const ENTITY_LESSTHAN = "<", + *const ENTITY_GREATERTHAN = ">", + *const ENTITY_PERCENT = "+percent+"; +enum +{ ENTITY_LENGTH = 4, + PERCENT_LENGTH = 9 +}; +static const sc_char *const ESCAPES = "<>%&+"; +static const sc_char *const WHITESPACE = "\t\n\v\f\r "; + +/* Trace flag, set before running. */ +static sc_bool pf_trace = FALSE; + + +/* + * Table tying HTML-like tag strings to enumerated tag types. Since it's + * scanned sequentially by strncmp(), it's ordered so that longer strings + * come before shorter ones. The <br> tag is missing because this is + * handled separately, as a simple put of '\n'. + */ +typedef struct +{ + const sc_char *const name; + const sc_int length; + const sc_int tag; +} sc_html_tags_t; + +static const sc_html_tags_t HTML_TAGS_TABLE[] = { + {"bgcolour", 8, SC_TAG_BGCOLOR}, {"bgcolor", 7, SC_TAG_BGCOLOR}, + {"waitkey", 7, SC_TAG_WAITKEY}, + {"center", 6, SC_TAG_CENTER}, {"/center", 7, SC_TAG_ENDCENTER}, + {"centre", 6, SC_TAG_CENTER}, {"/centre", 7, SC_TAG_ENDCENTER}, + {"right", 5, SC_TAG_RIGHT}, {"/right", 6, SC_TAG_ENDRIGHT}, + {"font", 4, SC_TAG_FONT}, {"/font", 5, SC_TAG_ENDFONT}, + {"wait", 4, SC_TAG_WAIT}, {"cls", 3, SC_TAG_CLS}, + {"i", 1, SC_TAG_ITALICS}, {"/i", 2, SC_TAG_ENDITALICS}, + {"b", 1, SC_TAG_BOLD}, {"/b", 2, SC_TAG_ENDBOLD}, + {"u", 1, SC_TAG_UNDERLINE}, {"/u", 2, SC_TAG_ENDUNDERLINE}, + {"c", 1, SC_TAG_COLOR}, {"/c", 2, SC_TAG_ENDCOLOR}, + {NULL, 0, SC_TAG_UNKNOWN} +}; + +/* + * Printfilter structure definition. It defines a buffer for output, + * associated size and length, a note of any conversion to apply to the next + * buffered character, and a flag to let the filter ignore incoming text. + */ +typedef struct sc_filter_s +{ + sc_uint magic; + sc_int buffer_length; + sc_int buffer_allocation; + sc_char *buffer; + sc_bool new_sentence; + sc_bool is_muted; + sc_bool needs_filtering; +} sc_filter_t; + + +/* + * pf_is_valid() + * + * Return TRUE if pointer is a valid printfilter, FALSE otherwise. + */ +static sc_bool +pf_is_valid (sc_filterref_t filter) +{ + return filter && filter->magic == PRINTFILTER_MAGIC; +} + + +/* + * pf_create() + * + * Create and return a new printfilter. + */ +sc_filterref_t +pf_create (void) +{ + static sc_bool initialized = FALSE; + + sc_filterref_t filter; + + /* On first call only, verify the string lengths in the table. */ + if (!initialized) + { + const sc_html_tags_t *entry; + + /* Compare table lengths with string lengths. */ + for (entry = HTML_TAGS_TABLE; entry->name; entry++) + { + if (entry->length != (sc_int) strlen (entry->name)) + { + sc_fatal ("pf_create:" + " table string length is wrong for \"%s\"\n", + entry->name); + } + } + + initialized = TRUE; + } + + /* Create a new printfilter. */ + filter = (sc_filterref_t)sc_malloc(sizeof (*filter)); + filter->magic = PRINTFILTER_MAGIC; + filter->buffer_length = 0; + filter->buffer_allocation = 0; + filter->buffer = NULL; + filter->new_sentence = FALSE; + filter->is_muted = FALSE; + filter->needs_filtering = FALSE; + + return filter; +} + + +/* + * pf_destroy() + * + * Destroy a printfilter and free its allocated memory. + */ +void +pf_destroy (sc_filterref_t filter) +{ + assert (pf_is_valid (filter)); + + /* Free buffer space, and poison and free the printfilter. */ + sc_free (filter->buffer); + memset (filter, 0xaa, sizeof (*filter)); + sc_free (filter); +} + + +/* + * pf_interpolate_vars() + * + * Replace %...% elements in a string by their variable values. If any + * variables were interpolated, returns an allocated string with replacements + * done, otherwise returns NULL. + * + * If a %...% element exists that is not a variable, then it's left in as is. + * Similarly, an unmatched (single) % in a string is also left as is. There + * appears to be no facility in the file format for escaping literal '%' + * characters, and since some games have strings with this character in them, + * this is probably all that can be done. + */ +static sc_char * +pf_interpolate_vars (const sc_char *string, sc_var_setref_t vars) +{ + sc_char *buffer, *name; + const sc_char *cursor; + const sc_char *marker; + sc_bool is_interpolated; + + /* + * Begin with NULL buffer and name strings for lazy allocation, and clear + * interpolation detection flag. + */ + buffer = NULL; + name = NULL; + is_interpolated = FALSE; + + /* Run through the string looking for variables. */ + marker = string; + for (cursor = (const sc_char *)strchr (marker, PERCENT); + cursor; cursor = (const sc_char *)strchr(marker, PERCENT)) + { + sc_int type; + sc_vartype_t vt_rvalue; + sc_char close; + + /* + * If not yet allocated, allocate a buffer for the return string and + * copy up to the percent character into it; otherwise append to buffer + * up to percent character. And if not yet done, allocate a name + * buffer guaranteed long enough. + */ + if (!buffer) + { + buffer = (sc_char *)sc_malloc (cursor - marker + 1); + memcpy (buffer, marker, cursor - marker); + buffer[cursor - marker] = NUL; + } + else + { + buffer = (sc_char *)sc_realloc (buffer, strlen (buffer) + cursor - marker + 1); + strncat (buffer, marker, cursor - marker); + } + if (!name) + name = (sc_char *)sc_malloc (strlen (string) + 1); + + /* + * Get the variable name, and from that, the value. If we encounter a + * mismatched '%' or unknown variable, skip it. + */ + if (sscanf (cursor, "%%%[^%]%c", name, &close) != 2 + || close != PERCENT + || !var_get (vars, name, &type, &vt_rvalue)) + { + buffer = (sc_char *)sc_realloc (buffer, strlen (buffer) + 2); + strncat (buffer, cursor, 1); + marker = cursor + 1; + continue; + } + + /* Get variable value and append to the string. */ + switch (type) + { + case VAR_INTEGER: + { + sc_char value[32]; + + sprintf (value, "%ld", vt_rvalue.integer); + buffer = (sc_char *)sc_realloc (buffer, strlen (buffer) + strlen (value) + 1); + strcat (buffer, value); + break; + } + + case VAR_STRING: + buffer = (sc_char *)sc_realloc (buffer, + strlen (buffer) + strlen (vt_rvalue.string) + 1); + strcat (buffer, vt_rvalue.string); + break; + + default: + sc_fatal ("pf_interpolate_vars: invalid variable type, %ld\n", type); + } + + /* Advance over the %...% variable name, and note success. */ + marker = cursor + strlen (name) + 2; + is_interpolated = TRUE; + } + + /* + * If we allocated a buffer and interpolated into it, append the remainder + * of the string. If we didn't interpolate successfully (the input contained + * a rogue '%' character), throw out the buffer as it will be the same as + * our input. + */ + if (buffer) + { + if (is_interpolated) + { + buffer = (sc_char *)sc_realloc (buffer, strlen (buffer) + strlen (marker) + 1); + strcat (buffer, marker); + } + else + { + sc_free (buffer); + buffer = NULL; + } + } + + /* Clean up, and return either the updated string or NULL. */ + sc_free (name); + return buffer; +} + + +/* + * pf_replace_alr() + * + * Helper for pf_replace_alrs(). Replace one ALR found in the string with + * its equivalent, updating the buffer at the address passed in, including + * reallocating if necessary. Return TRUE if the buffer was changed. + */ +static sc_bool +pf_replace_alr (const sc_char *string, + sc_char **buffer, sc_int alr, sc_prop_setref_t bundle) +{ + sc_vartype_t vt_key[3]; + const sc_char *marker, *cursor, *original, *replacement; + sc_char *buffer_ = *buffer; + + /* Retrieve the ALR original string, set replacement to NULL for now. */ + vt_key[0].string = "ALRs"; + vt_key[1].integer = alr; + vt_key[2].string = "Original"; + original = prop_get_string (bundle, "S<-sis", vt_key); + replacement = NULL; + + /* Ignore pathological empty originals. */ + if (original[0] == NUL) + return FALSE; + + /* Run through the marker string looking for things to replace. */ + marker = string; + for (cursor = strstr (marker, original); + cursor; cursor = strstr (marker, original)) + { + /* Optimize by retrieving the replacement string only on demand. */ + if (!replacement) + { + vt_key[2].string = "Replacement"; + replacement = prop_get_string (bundle, "S<-sis", vt_key); + } + + /* + * If not yet allocated, allocate a buffer for the return string and + * copy; else append to the existing buffer: basic copy-on-write. + */ + if (!buffer_) + { + buffer_ = (sc_char *)sc_malloc (cursor - marker + strlen (replacement) + 1); + memcpy (buffer_, marker, cursor - marker); + buffer_[cursor - marker] = NUL; + strcat (buffer_, replacement); + } + else + { + buffer_ = (sc_char *)sc_realloc (buffer_, strlen (buffer_) + + cursor - marker + strlen (replacement) + 1); + strncat (buffer_, marker, cursor - marker); + strcat (buffer_, replacement); + } + + /* Advance over the original. */ + marker = cursor + strlen (original); + } + + /* If any pending text, append it to the buffer. */ + if (replacement) + { + buffer_ = (sc_char *)sc_realloc (buffer_, strlen (buffer_) + strlen (marker) + 1); + strcat (buffer_, marker); + } + + /* Write back buffer, and if replacement set, the buffer was altered. */ + *buffer = buffer_; + return replacement != NULL; +} + + +/* + * pf_replace_alrs() + * + * Replace any ALRs found in the string with their equivalents. If any + * ALRs were replaced, returns an allocated string with replacements done, + * otherwise returns NULL. + */ +static sc_char * +pf_replace_alrs (const sc_char *string, sc_prop_setref_t bundle, + sc_bool alr_applied[], sc_int alr_count) +{ + sc_int index_; + sc_char *buffer1, *buffer2, **buffer; + const sc_char *marker; + + /* + * Begin with NULL buffers and alternate for lazy allocation. To avoid a + * lot of allocation and copying, we use two buffers to help with repeated + * ALR replacement. + */ + buffer1 = buffer2 = NULL; + buffer = &buffer1; + + /* Run through each ALR that exists. */ + marker = string; + for (index_ = 0; index_ < alr_count; index_++) + { + sc_vartype_t vt_key[3]; + sc_int alr; + + /* + * Ignore ALR indexes that have already been applied. This prevents + * endless loops in ALR replacement. + */ + if (alr_applied[index_]) + continue; + + /* + * Get the actual ALR number for the ALR. This comes from the index + * that we sorted earlier by length of original string. Try replacing + * that ALR in the current marker string. + */ + vt_key[0].string = "ALRs2"; + vt_key[1].integer = index_; + vt_key[2].string = "ALRIndex"; + alr = prop_get_integer (bundle, "I<-sis", vt_key); + + if (pf_replace_alr (marker, buffer, alr, bundle)) + { + /* + * The current buffer in use has been altered. This means that we + * have to switch the marker string to the buffer containing the + * replacement, and switch 'buffer' to the other one for the next + * ALR iteration. + */ + marker = *buffer; + buffer = (buffer == &buffer1) ? &buffer2 : &buffer1; + + /* Discard any content in the buffer switched to above. */ + if (*buffer) + (*buffer)[0] = NUL; + + /* Note this ALR as "used up", and unavailable for future passes. */ + alr_applied[index_] = TRUE; + } + } + + /* + * If marker points to one or other of the buffers, that buffer is the + * return string, and the other is garbage, and should now be freed (or + * was never used, in which case it is NULL). + */ + if (marker == buffer1) + { + sc_free (buffer2); + return buffer1; + } + else if (marker == buffer2) + { + sc_free (buffer1); + return buffer2; + } + else + return NULL; +} + + +/* + * pf_output_text() + * + * Edit the tag-stripped text element passed in, substituting < > + * +percent+ with < > %, then send to the OS-specific output functions. + */ +static void +pf_output_text (const sc_char *string) +{ + sc_int index_, b_index; + sc_char *buffer; + + /* Optimize away the allocation and copy if possible. */ + if (!(strstr (string, ENTITY_LESSTHAN) + || strstr (string, ENTITY_GREATERTHAN) + || strstr (string, ENTITY_PERCENT))) + { + if_print_string (string); + return; + } + + /* + * Copy characters from the string into the buffer, replacing any &..; + * elements by their single-character equivalents. We also replace any + * +percent+ elements by percent characters; apparently an undocumented + * Adrift Runner extension. + */ + buffer = (sc_char *)sc_malloc (strlen (string) + 1); + for (index_ = 0, b_index = 0; + string[index_] != NUL; index_++, b_index++) + { + if (sc_strncasecmp (string + index_, + ENTITY_LESSTHAN, ENTITY_LENGTH) == 0) + { + buffer[b_index] = LESSTHAN; + index_ += ENTITY_LENGTH - 1; + } + else if (sc_strncasecmp (string + index_, + ENTITY_GREATERTHAN, ENTITY_LENGTH) == 0) + { + buffer[b_index] = GREATERTHAN; + index_ += ENTITY_LENGTH - 1; + } + else if (sc_strncasecmp (string + index_, + ENTITY_PERCENT, PERCENT_LENGTH) == 0) + { + buffer[b_index] = PERCENT; + index_ += PERCENT_LENGTH - 1; + } + else + buffer[b_index] = string[index_]; + } + + /* Terminate, print, and free the buffer. */ + buffer[b_index] = NUL; + if_print_string (buffer); + sc_free (buffer); +} + + +/* + * pf_output_tag() + * + * Output an HTML-like tag element to the OS-specific tag handling function. + */ +static void +pf_output_tag (const sc_char *contents) +{ + const sc_html_tags_t *entry; + const sc_char *argument; + + /* For a simple <br> tag, just print out a newline. */ + if (sc_compare_word (contents, "br", 2)) + { + if_print_character ('\n'); + return; + } + + /* + * Search for the name in the HTML tags table. It should be a full match, + * that is, the character after the matched name must be space or NUL. + * The <bgcolour="xyz"> tag is the exception; here the terminator is '='. + */ + for (entry = HTML_TAGS_TABLE; entry->name; entry++) + { + if (sc_strncasecmp (contents, entry->name, entry->length) == 0) + { + sc_char next; + + next = contents[entry->length]; + if (next == NUL || sc_isspace (next) + || (entry->tag == SC_TAG_BGCOLOR && next == '=')) + break; + } + } + + /* If not matched, output an unknown tag with contents as its argument. */ + if (!entry->name) + { + if_print_tag (SC_TAG_UNKNOWN, contents); + return; + } + + /* + * Find the argument by skipping the tag name and any spaces. Again, for + * <bgcolour="xyz">, make a special case, passing the complete contents as + * argument (to match <font colour=...> for the client. + */ + argument = contents; + argument += (entry->tag != SC_TAG_BGCOLOR) ? entry->length : 0; + while (sc_isspace (argument[0])) + argument++; + if_print_tag (entry->tag, argument); +} + + +/* + * pf_output_untagged() + * + * Break apart HTML-like string into normal text elements, and HTML-like + * tags. + */ +static void +pf_output_untagged (const sc_char *string) +{ + sc_char *temporary, *untagged, *contents; + const sc_char *cursor; + const sc_char *marker; + + /* + * Optimize away the allocation and copy if possible. We need to check + * here both for tags and for entities; only if neither occurs is it safe + * to output the string directly. + */ + if (!strchr (string, LESSTHAN) + && !(strstr (string, ENTITY_LESSTHAN) + || strstr (string, ENTITY_GREATERTHAN) + || strstr (string, ENTITY_PERCENT))) + { + if_print_string (string); + return; + } + + /* + * Create a general temporary string, and alias it to both untagged text + * and the tag name, for sharing inside the loop. + */ + temporary = (sc_char *)sc_malloc (strlen (string) + 1); + untagged = contents = temporary; + + /* Run through the string looking for <...> tags. */ + marker = string; + for (cursor = (const sc_char *)strchr (marker, LESSTHAN); + cursor; cursor = (const sc_char *)strchr (marker, LESSTHAN)) + { + sc_char close; + + /* Handle characters up to the tag start; untagged text. */ + memcpy (untagged, marker, cursor - marker); + untagged[cursor - marker] = NUL; + pf_output_text (untagged); + + /* Catch and ignore completely empty tags. */ + if (cursor[1] == GREATERTHAN) + { + marker = cursor + 2; + continue; + } + + /* + * Get the text within the tag, reusing the temporary buffer. If this + * fails, allow the remainder of the line to be delivered as a tag; + * unknown, probably. + */ + if (sscanf (cursor, "<%[^>]%c", contents, &close) != 2 + || close != GREATERTHAN) + { + if (sscanf (cursor, "<%[^>]", contents) != 1) + { + sc_error ("pf_output_untagged: mismatched '%c'\n", LESSTHAN); + if_print_character (LESSTHAN); + marker = cursor + 1; + continue; + } + } + + /* Output tag, and advance marker over the <...> tag. */ + if (!sc_strempty (contents)) + pf_output_tag (contents); + marker = cursor + strlen (contents) + 1; + marker += (marker[0] == GREATERTHAN) ? 1 : 0; + } + + /* Output any remaining string text, and free the temporary buffer. */ + pf_output_text (marker); + sc_free (temporary); +} + + +/* + * pf_filter_internal() + * + * Filters an output string, interpolating variables and replacing ALR's. If + * any filtering was done, returns an allocated string that the caller needs + * to free; otherwise, return NULL. + * + * Bundle may be NULL, requesting that the function suppress ALR replacements, + * and do only variables; used for game info strings. + * + * The way Adrift does this is somewhat obscure, but the following seems to + * replicate it well enough for most practical purposes (it's unlikely that + * any game assumes or relies on anything not covered by this): + * + * repeat some number of times + * repeat some number of times + * interpolate variables + * repeat [some number of times?] + * for each ALR unused so far this pass + * search the current string for the ALR original + * if found + * replace this ALR in the current string + * mark this ALR as used + * until no more changes in the current string + * + */ +static sc_char * +pf_filter_internal (const sc_char *string, + sc_var_setref_t vars, sc_prop_setref_t bundle) +{ + sc_int alr_count, iteration; + sc_char *current; + sc_bool *alr_applied; + assert (string && vars); + + if (pf_trace) + sc_trace ("Printfilter: initial \"%s\"\n", string); + + /* If including ALRs, create a common set of ALR application flags. */ + if (bundle) + { + sc_vartype_t vt_key; + + /* Obtain a count of ALRs. */ + vt_key.string = "ALRs"; + alr_count = prop_get_child_count (bundle, "I<-s", &vt_key); + + /* + * Create a new set of ALR application flags. These are used to ensure + * that a given ALR is applied only once on a given pass. If the game + * has no ALRs, don't create a flag set. + */ + if (alr_count > 0) + { + alr_applied = (sc_bool *)sc_malloc (alr_count * sizeof (*alr_applied)); + memset (alr_applied, FALSE, alr_count * sizeof (*alr_applied)); + } + else + alr_applied = NULL; + } + else + { + /* Not including ALRs, so set alr count to 0, and flags to NULL. */ + alr_count = 0; + alr_applied = NULL; + } + + /* Loop for a sort-of arbitrary number of passes; probably enough. */ + current = NULL; + for (iteration = 0; iteration < ITERATION_LIMIT; iteration++) + { + sc_int inner_iteration; + const sc_char *initial; + sc_char *intermediate; + + /* Note the initial string, so we can check for no change. */ + initial = current; + + for (inner_iteration = 0; + inner_iteration < ITERATION_LIMIT; inner_iteration++) + { + /* + * Interpolate variables. If any changes were made, advance current + * to the interpolated version, and free the old current if required. + * Work on the current string, if any, otherwise the input string. + */ + intermediate = pf_interpolate_vars (current ? current : string, vars); + if (intermediate) + { + sc_free (current); + current = intermediate; + if (pf_trace) + { + sc_trace ("Printfilter: interpolated [%ld,%ld] \"%s\"\n", + iteration, inner_iteration, current); + } + } + else + break; + } + + /* If we have ALRs to process, search out and replace all findable. */ + if (alr_count > 0) + { + /* Replace ALRs until no more ALRs can be found. */ + inner_iteration = 0; + while (TRUE) + { + /* + * Replace ALRs, and advance current as for variables above. + * Leave the loop when ALR replacements stop. Again, work on + * the current string if any, otherwise the input string. + */ + intermediate = pf_replace_alrs (current ? current : string, + bundle, alr_applied, alr_count); + if (intermediate) + { + sc_free (current); + current = intermediate; + if (pf_trace) + { + sc_trace ("Printfilter: replaced [%ld,%ld] \"%s\"\n", + iteration, inner_iteration, current); + } + } + else + break; + inner_iteration++; + } + } + + /* If nothing changed this iteration, stop now. */ + if (current == initial) + break; + } + + /* Free any ALR application flags, and return current, NULL if no change. */ + sc_free (alr_applied); + return current; +} + + +/* + * pf_filter() + * + * A facet of pf_filter_internal(). Filter an output string, interpolating + * variables and replacing ALR's. Returns an allocated string that the caller + * needs to free. + */ +sc_char * +pf_filter (const sc_char *string, + sc_var_setref_t vars, sc_prop_setref_t bundle) +{ + sc_char *current; + + /* Filter this string, including ALRs replacements. */ + current = pf_filter_internal (string, vars, bundle); + + /* Our contract is to return an allocated string; copy if required. */ + if (!current) + { + current = (sc_char *)sc_malloc (strlen (string) + 1); + strcpy (current, string); + } + + return current; +} + + +/* + * pf_filter_for_info() + * + * A facet of pf_filter_internal(). Filters output, interpolating variables + * only (no ALR replacement), and returns the resulting string to the caller. + * Used on informational strings such as the game title and author. Returns + * an allocated string that the caller needs to free. + */ +sc_char * +pf_filter_for_info (const sc_char *string, sc_var_setref_t vars) +{ + sc_char *current; + + /* Filter this string, excluding ALRs replacements. */ + current = pf_filter_internal (string, vars, NULL); + + /* Our contract is to return an allocated string; copy if required. */ + if (!current) + { + current = (sc_char *)sc_malloc (strlen (string) + 1); + strcpy (current, string); + } + + return current; +} + + +/* + * pf_flush() + * + * Filter buffered data, interpolating variables and replacing ALR's, and + * send the resulting string to the output channel. + */ +void +pf_flush (sc_filterref_t filter, + sc_var_setref_t vars, sc_prop_setref_t bundle) +{ + assert (pf_is_valid (filter)); + assert (vars && bundle); + + /* See if there is any buffered data to flush. */ + if (filter->buffer_length > 0) + { + /* + * Filter the buffered string, then print it untagged. Remember to free + * the filtered version. If filtering made no difference, or if the + * buffer was already filtered by, say, checkpointing, just print the + * original buffer untagged instead. + */ + if (filter->needs_filtering) + { + sc_char *filtered; + + filtered = pf_filter_internal (filter->buffer, vars, bundle); + if (filtered) + { + pf_output_untagged (filtered); + sc_free (filtered); + } + else + pf_output_untagged (filter->buffer); + } + else + pf_output_untagged (filter->buffer); + + /* Remove buffered data by resetting length to zero. */ + filter->buffer_length = 0; + filter->needs_filtering = FALSE; + } + + /* Reset new sentence and mute flags. */ + filter->new_sentence = FALSE; + filter->is_muted = FALSE; +} + + +/* + * pf_append_string() + * + * Append a string to the filter buffer. + */ +static void +pf_append_string (sc_filterref_t filter, const sc_char *string) +{ + sc_int length, required; + + /* + * Calculate the required buffer size to append string. Remember to add + * one for the terminating NUL. + */ + length = strlen (string); + required = filter->buffer_length + length + 1; + + /* If this is more than the current buffer allocation, resize it. */ + if (required > filter->buffer_allocation) + { + sc_int new_allocation; + + /* Calculate the new malloc size, in increment chunks. */ + new_allocation = ((required + BUFFER_GROW_INCREMENT - 1) + / BUFFER_GROW_INCREMENT) * BUFFER_GROW_INCREMENT; + + /* Grow the buffer. */ + filter->buffer = (sc_char *)sc_realloc (filter->buffer, new_allocation); + filter->buffer_allocation = new_allocation; + } + + /* If empty, put a NUL into the buffer to permit strcat. */ + if (filter->buffer_length == 0) + filter->buffer[0] = NUL; + + /* Append the string to the buffer and extend length. */ + strcat (filter->buffer, string); + filter->buffer_length += length; +} + + +/* + * pf_checkpoint() + * + * Filter buffered data, interpolating variables and replacing ALR's, and + * store the result back in the buffer. This allows a string to be inter- + * polated in between main flushes; used to update buffered text with variable + * values before those values are updated by task actions. + */ +void +pf_checkpoint (sc_filterref_t filter, + sc_var_setref_t vars, sc_prop_setref_t bundle) +{ + assert (pf_is_valid (filter)); + assert (vars && bundle); + + /* See if there is any buffered data to filter. */ + if (filter->buffer_length > 0) + { + /* + * Filter the buffered string, and place the filtered result, if any, + * back into the filter buffer. We do this by setting the buffer length + * back to zero, then appending the filtered string; this keeps the + * grown buffer intact. + */ + if (filter->needs_filtering) + { + sc_char *filtered; + + filtered = pf_filter_internal (filter->buffer, vars, bundle); + if (filtered) + { + filter->buffer_length = 0; + pf_append_string (filter, filtered); + sc_free (filtered); + } + } + + /* Note the buffer as filtered, to avoid pointless filtering. */ + filter->needs_filtering = FALSE; + } +} + + +/* + * pf_get_buffer() + * pf_transfer_buffer() + * + * Return the raw, unfiltered, buffered text. Returns NULL if no buffered + * data available. Transferring the buffer transfers ownership of the buffer + * string to the caller, who is then responsible for freeing it. + * + * The second function is an optimization to avoid allocations and copying + * in client code. + */ +const sc_char * +pf_get_buffer (sc_filterref_t filter) +{ + assert (pf_is_valid (filter)); + + /* + * Return buffer if filter length is greater than zero. Note that this + * assumes that the buffer is a nul-terminated string. + */ + if (filter->buffer_length > 0) + { + assert (filter->buffer[filter->buffer_length] == NUL); + return filter->buffer; + } + else + return NULL; +} + +sc_char * +pf_transfer_buffer (sc_filterref_t filter) +{ + assert (pf_is_valid (filter)); + + /* + * If the filter length is greater than zero, pass out the buffer (a nul- + * terminated string) and zero our length, allocation, and set the buffer + * back to NULL; an empty in all except the free-ing. + */ + if (filter->buffer_length > 0) + { + sc_char *retval; + + /* Set the return value to be the buffered text. */ + assert (filter->buffer[filter->buffer_length] == NUL); + retval = filter->buffer; + + /* Clear all filter fields down to empty values. */ + filter->buffer_length = 0; + filter->buffer_allocation = 0; + filter->buffer = NULL; + filter->new_sentence = FALSE; + filter->is_muted = FALSE; + filter->needs_filtering = FALSE; + + /* Return the allocated buffered text. */ + return retval; + } + else + return NULL; +} + + +/* + * pf_empty() + * + * Empty any text currently buffered in the filter. + */ +void +pf_empty (sc_filterref_t filter) +{ + assert (pf_is_valid (filter)); + + /* Free any allocation, and return the filter to initialization state. */ + filter->buffer_length = 0; + filter->buffer_allocation = 0; + sc_free (filter->buffer); + filter->buffer = NULL; + filter->new_sentence = FALSE; + filter->is_muted = FALSE; + filter->needs_filtering = FALSE; +} + + +/* + * pf_buffer_string() + * pf_buffer_character() + * + * Add a string, and a single character, to the printfilter buffer. If muted, + * these functions do nothing. + */ +void +pf_buffer_string (sc_filterref_t filter, const sc_char *string) +{ + assert (pf_is_valid (filter)); + assert (string); + + /* Ignore the call if the printfilter is muted. */ + if (!filter->is_muted) + { + sc_int noted; + + /* Note append start, then append the string to the buffer. */ + noted = filter->buffer_length; + pf_append_string (filter, string); + + /* Adjust the first character of the appended string if flagged. */ + if (filter->new_sentence) + filter->buffer[noted] = sc_toupper (filter->buffer[noted]); + + /* Clear new sentence, and note as currently needing filtering. */ + filter->needs_filtering = TRUE; + filter->new_sentence = FALSE; + } +} + +void +pf_buffer_character (sc_filterref_t filter, sc_char character) +{ + sc_char buffer[2]; + assert (pf_is_valid (filter)); + + buffer[0] = character; + buffer[1] = NUL; + pf_buffer_string (filter, buffer); +} + + +/* + * pf_prepend_string() + * + * Add a string to the front of the printfilter buffer, rather than to the + * end. Generally less efficient than an append, these are for use by task + * running code, which needs to run task actions and then prepend the task's + * completion text. If muted, this function does nothing. + */ +void +pf_prepend_string (sc_filterref_t filter, const sc_char *string) +{ + assert (pf_is_valid (filter)); + assert (string); + + /* Ignore the call if the printfilter is muted. */ + if (!filter->is_muted) + { + if (filter->buffer_length > 0) + { + sc_char *copy; + + /* Take a copy of the current buffered string. */ + assert (filter->buffer[filter->buffer_length] == NUL); + copy = (sc_char *)sc_malloc (filter->buffer_length + 1); + strcpy (copy, filter->buffer); + + /* + * Now restart buffering with the input string passed in. Removing + * the current content by zeroing the length preserves the grown + * allocation of the main buffer. + */ + filter->buffer_length = 0; + pf_append_string (filter, string); + + /* Append the string saved above and then free it. */ + pf_append_string (filter, copy); + sc_free (copy); + + /* Adjust the first character of the prepended string if flagged. */ + if (filter->new_sentence) + filter->buffer[0] = sc_toupper (filter->buffer[0]); + + /* Clear new sentence, and note as currently needing filtering. */ + filter->needs_filtering = TRUE; + filter->new_sentence = FALSE; + } + else + /* No data, so the call is equivalent to a normal buffer. */ + pf_buffer_string (filter, string); + } +} + + +/* + * pf_new_sentence() + * + * Tells the printfilter to force the next non-space character to uppercase. + * Ignored if the printfilter is muted. + */ +void +pf_new_sentence (sc_filterref_t filter) +{ + assert (pf_is_valid (filter)); + + if (!filter->is_muted) + filter->new_sentence = TRUE; +} + + +/* + * pf_mute() + * pf_clear_mute() + * + * A muted printfilter ignores all new text additions. + */ +void +pf_mute (sc_filterref_t filter) +{ + assert (pf_is_valid (filter)); + + filter->is_muted = TRUE; +} + +void +pf_clear_mute (sc_filterref_t filter) +{ + assert (pf_is_valid (filter)); + + filter->is_muted = FALSE; +} + + +/* + * pf_buffer_tag() + * + * Insert an HTML-like tag into the buffered output data. The call is ignored + * if the printfilter is muted. + */ +void +pf_buffer_tag (sc_filterref_t filter, sc_int tag) +{ + const sc_html_tags_t *entry; + assert (pf_is_valid (filter)); + + /* Search the tags table for this tag. */ + for (entry = HTML_TAGS_TABLE; entry->name; entry++) + { + if (tag == entry->tag) + break; + } + + /* If found, output the equivalent string, enclosed in '<>' characters. */ + if (entry->name) + { + pf_buffer_character (filter, LESSTHAN); + pf_buffer_string (filter, entry->name); + pf_buffer_character (filter, GREATERTHAN); + } + else + sc_error ("pf_buffer_tag: invalid tag, %ld\n", tag); +} + + +/* + * pf_strip_tags_common() + * + * Strip HTML-like tags from a string. Used to process strings used in ways + * other than being passed to if_print_string(), for example room names and + * status lines. It ignores all tags except <br>, which it replaces with + * a newline if requested by allow_newlines. + */ +static void +pf_strip_tags_common (sc_char *string, sc_bool allow_newlines) +{ + sc_char *marker, *cursor; + + /* Run through the string looking for <...> tags. */ + marker = string; + for (cursor = strchr (marker, LESSTHAN); + cursor; cursor = strchr (marker, LESSTHAN)) + { + sc_char *tag_end; + + /* Locate tag end, and break if unterminated. */ + tag_end = strchr (cursor, GREATERTHAN); + if (!tag_end) + break; + + /* If the tag is <br>, replace with newline if requested. */ + if (allow_newlines) + { + if (tag_end - cursor == 3 + && sc_strncasecmp (cursor + 1, "br", 2) == 0) + *cursor++ = '\n'; + } + + /* Remove the tag from the string, then advance input. */ + memmove (cursor, tag_end + 1, strlen (tag_end)); + marker = cursor; + } +} + + +/* + * pf_strip_tags() + * pf_strip_tags_for_hints() + * + * Public interfaces to pf_strip_tags_common(). The hints version will + * allow <br> tags to map into newlines in hints strings. + */ +void +pf_strip_tags (sc_char *string) +{ + pf_strip_tags_common (string, FALSE); +} + +void +pf_strip_tags_for_hints (sc_char *string) +{ + pf_strip_tags_common (string, TRUE); +} + + +/* + * pf_escape() + * + * Escape <, >, and % characters in the input string. Used to filter player + * input prior to storing in referenced text. + * + * Adrift offers no escapes for & and + escapes, so for these we convert to + * the character itself followed by a space. The return string is malloc'ed, + * so the caller needs to remember to free it. + */ +sc_char * +pf_escape (const sc_char *string) +{ + const sc_char *marker, *cursor; + sc_char *buffer; + + /* Start with an empty return buffer. */ + buffer = (sc_char *)sc_malloc (strlen (string) + 1); + buffer[0] = NUL; + + /* Run through the string looking for <, >, %, or other escapes. */ + marker = string; + for (cursor = marker + strcspn (marker, ESCAPES); + cursor[0] != NUL; cursor = marker + strcspn (marker, ESCAPES)) + { + const sc_char *escape; + sc_char escape_buffer[3]; + + /* Extend buffer to hold the string so far. */ + if (cursor > marker) + { + buffer = (sc_char *)sc_realloc (buffer, strlen (buffer) + cursor - marker + 1); + buffer[strlen (buffer) + cursor - marker] = NUL; + memcpy (buffer + strlen (buffer), marker, cursor - marker); + } + + /* Determine the appropriate character escape. */ + if (cursor[0] == LESSTHAN) + escape = ENTITY_LESSTHAN; + else if (cursor[0] == GREATERTHAN) + escape = ENTITY_GREATERTHAN; + else if (cursor[0] == PERCENT) + escape = ENTITY_PERCENT; + else + { + /* + * No real escape available, so fake, badly, by appending a space + * for cases where we've encountered a character entity; leave + * others untouched. + */ + escape_buffer[0] = cursor[0]; + if (sc_strncasecmp (cursor, + ENTITY_LESSTHAN, ENTITY_LENGTH) == 0 + || sc_strncasecmp (cursor, + ENTITY_GREATERTHAN, ENTITY_LENGTH) == 0 + || sc_strncasecmp (cursor, + ENTITY_PERCENT, PERCENT_LENGTH) == 0) + { + escape_buffer[1] = ' '; + escape_buffer[2] = NUL; + } + else + escape_buffer[1] = NUL; + escape = escape_buffer; + } + + buffer = (sc_char *)sc_realloc (buffer, strlen (buffer) + strlen (escape) + 1); + strcat (buffer, escape); + + /* Pass over character escaped and continue. */ + cursor++; + marker = cursor; + } + + /* Add all remaining characters to the buffer. */ + if (cursor > marker) + { + buffer = (sc_char *)sc_realloc (buffer, strlen (buffer) + cursor - marker + 1); + buffer[strlen (buffer) + cursor - marker] = NUL; + memcpy (buffer + strlen (buffer), marker, cursor - marker); + } + + return buffer; +} + + +/* + * pf_compare_words() + * + * Matches multiple words from words in string. Returns the extent of + * the match if the string matched, 0 otherwise. + */ +static sc_int +pf_compare_words (const sc_char *string, const sc_char *words) +{ + sc_int word_posn, posn; + + /* None expected, but skip leading space. */ + for (word_posn = 0; sc_isspace (words[word_posn]) && words[word_posn] != NUL;) + word_posn++; + + /* Match characters from words with the string at position. */ + posn = 0; + while (TRUE) + { + /* Any character mismatch means no words match. */ + if (sc_tolower (words[word_posn]) != sc_tolower (string[posn])) + return 0; + + /* Move to next character in each. */ + word_posn++; + posn++; + + /* + * If at space, advance over whitespace in words list. Stop when we + * hit the end of the words list. + */ + while (sc_isspace (words[word_posn]) && words[word_posn] != NUL) + word_posn++; + if (words[word_posn] == NUL) + break; + + /* + * About to match another word, so advance over whitespace in the + * current string too. + */ + while (sc_isspace (string[posn]) && string[posn] != NUL) + posn++; + } + + /* + * We reached the end of words. If we're at the end of the match string, + * or at spaces, we've matched. + */ + if (sc_isspace (string[posn]) || string[posn] == NUL) + return posn; + + /* More text after the match, so it's not quite a match. */ + return 0; +} + + +/* + * pf_filter_input() + * + * Applies synonym changes to a player input string, and returns the resulting + * string to the caller, or NULL if no synonym changes were needed. The + * return string is malloc'ed, so the caller needs to remember to free it. + */ +sc_char * +pf_filter_input (const sc_char *string, sc_prop_setref_t bundle) +{ + sc_vartype_t vt_key[3]; + sc_int synonym_count, buffer_allocation; + sc_char *buffer; + const sc_char *current; + assert (string && bundle); + + if (pf_trace) + sc_trace ("Printfilter: input \"%s\"\n", string); + + /* Obtain a count of synonyms. */ + vt_key[0].string = "Synonyms"; + synonym_count = prop_get_child_count (bundle, "I<-s", vt_key); + + /* Begin with a NULL buffer for lazy allocation. */ + buffer_allocation = 0; + buffer = NULL; + + /* Loop over each word in the string. */ + current = string + strspn (string, WHITESPACE); + while (current[0] != NUL) + { + sc_int index_, extent; + + /* Search for a synonym match at this index into the buffer. */ + extent = 0; + for (index_ = 0; index_ < synonym_count; index_++) + { + const sc_char *original; + + /* Retrieve the synonym original string. */ + vt_key[0].string = "Synonyms"; + vt_key[1].integer = index_; + vt_key[2].string = "Original"; + original = prop_get_string (bundle, "S<-sis", vt_key); + + /* Compare the original at this point. */ + extent = pf_compare_words (current, original); + if (extent > 0) + break; + } + + /* + * If a synonym found was, index_ indicates it, and extent shows how + * much of the buffer to replace with it. + */ + if (index_ < synonym_count && extent > 0) + { + const sc_char *replacement; + sc_char *position; + sc_int length, final; + + /* + * If not yet allocated, allocate a buffer now, and copy the input + * string into it. Then switch current to the equivalent location + * in the allocated buffer. More basic copy-on-write. + */ + if (!buffer) + { + buffer_allocation = strlen (string) + 1; + buffer = (sc_char *)sc_malloc (buffer_allocation); + strcpy (buffer, string); + current = buffer + (current - string); + } + + /* Find the replacement text for this synonym. */ + vt_key[0].string = "Synonyms"; + vt_key[1].integer = index_; + vt_key[2].string = "Replacement"; + replacement = prop_get_string (bundle, "S<-sis", vt_key); + length = strlen (replacement); + + /* + * If necessary, grow the output buffer for the replacement, + * remembering to adjust current for the new buffer allocated. + * At the same time, note the last character index for the move. + */ + if (length > extent) + { + sc_int offset; + + offset = current - buffer; + buffer_allocation += length - extent; + buffer = (sc_char *)sc_realloc (buffer, buffer_allocation); + current = buffer + offset; + final = length; + } + else + final = extent; + + /* Insert the replacement string into the buffer. */ + position = buffer + (current - buffer); + memmove (position + length, + position + extent, + buffer_allocation - (current - buffer) - final); + memcpy (position, replacement, length); + + /* Adjust current to skip over the replacement. */ + current += length; + + if (pf_trace) + sc_trace ("Printfilter: synonym \"%s\"\n", buffer); + } + else + { + /* If no match, advance current over the unmatched word. */ + current += strcspn (current, WHITESPACE); + } + + /* Set current to the next word start. */ + current += strspn (current, WHITESPACE); + } + + /* Return the final string, or NULL if no synonym replacements. */ + return buffer; +} + + +/* + * pf_debug_trace() + * + * Set filter tracing on/off. + */ +void +pf_debug_trace (sc_bool flag) +{ + pf_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scprops.cpp b/engines/glk/adrift/scprops.cpp new file mode 100644 index 0000000000..bdf51dc4db --- /dev/null +++ b/engines/glk/adrift/scprops.cpp @@ -0,0 +1,1059 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_uint PROP_MAGIC = 0x7927b2e0; +enum +{ PROP_GROW_INCREMENT = 32, + MAX_INTEGER_KEY = 65535, + NODE_POOL_CAPACITY = 512 +}; +static const sc_char NUL = '\0'; + +/* Properties trace flag. */ +static sc_bool prop_trace = FALSE; + + +/* + * Property tree node definition, uses a child list representation for + * fast lookup on indexed nodes. Name is a variable type, as is property, + * which is also overloaded to contain the child count for internal nodes. + */ +struct sc_prop_node_s { + sc_vartype_t name; + sc_vartype_t property; + + struct sc_prop_node_s **child_list; +}; +typedef sc_prop_node_s sc_prop_node_t; +typedef sc_prop_node_t *sc_prop_noderef_t; + +/* + * Properties set structure. This is a set of properties, on which the + * properties functions operate (a properties "object"). Node string + * names are held in a dictionary to help save space. To avoid excessive + * malloc'ing of nodes, new nodes are preallocated in pools. + */ +struct sc_prop_set_s { + sc_uint magic; + sc_int dictionary_length; + sc_char **dictionary; + sc_int node_pools_length; + sc_prop_noderef_t *node_pools; + sc_int node_count; + sc_int orphans_length; + void **orphans; + sc_bool is_readonly; + sc_prop_noderef_t root_node; + sc_tafref_t taf; +}; +typedef sc_prop_set_s sc_prop_set_t; + + +/* + * prop_is_valid() + * + * Return TRUE if pointer is a valid properties set, FALSE otherwise. + */ +static sc_bool +prop_is_valid (sc_prop_setref_t bundle) +{ + return bundle && bundle->magic == PROP_MAGIC; +} + + +/* + * prop_round_up() + * + * Round up a count of elements to the next block of grow increments. + */ +static sc_int +prop_round_up (sc_int elements) +{ + sc_int extended; + + extended = elements + PROP_GROW_INCREMENT - 1; + return (extended / PROP_GROW_INCREMENT) * PROP_GROW_INCREMENT; +} + + +/* + * prop_ensure_capacity() + * + * Ensure that capacity exists in a growable array for a given number of + * elements, growing if necessary. + * + * Some libc's allocate generously on realloc(), and some not. Those that + * don't will thrash badly if we realloc() on each grow, so here we try to + * realloc() in blocks of elements, and thus need to realloc() much less + * frequently. + */ +static void * +prop_ensure_capacity (void *array, + sc_int old_size, sc_int new_size, sc_int element_size) +{ + sc_int current, required; + + /* + * See if there's any resize necessary, that is, does the new size round up + * to a larger number of elements than the old size. + */ + current = prop_round_up (old_size); + required = prop_round_up (new_size); + if (required > current) + { + sc_byte *new_array, *start_clearing; + + /* Grow array to the required size, and zero new elements. */ + new_array = (sc_byte *)sc_realloc (array, required * element_size); + start_clearing = new_array + current * element_size; + memset (start_clearing, 0, (required - current) * element_size); + + return new_array; + } + + /* No resize necessary. */ + return array; +} + + +/* + * prop_trim_capacity() + * + * Trim an array allocation back to the bare minimum required. This will + * "unblock" the allocations of prop_ensure_capacity(). Once trimmed, + * the array cannot ever be grown safely again. + */ +static void * +prop_trim_capacity (void *array, sc_int size, sc_int element_size) +{ + if (prop_round_up (size) > size) + return sc_realloc (array, size * element_size); + else + return array; +} + + +/* + * prop_compare() + * + * String comparison routine for sorting and searching dictionary strings. + * The function has return type "int" to match the libc implementations of + * bsearch() and qsort(). + */ +static int +prop_compare (const void *string1, const void *string2) +{ + return strcmp (*(sc_char *const *) string1, *(sc_char *const *) string2); +} + + +/* + * prop_dictionary_lookup() + * + * Find a string in the dictionary. If the string is not present, the + * function will add it. The function returns the string's address, if + * either added or already present. Any new dictionary entry will + * contain a malloced copy of the string passed in. + */ +static const sc_char * +prop_dictionary_lookup (sc_prop_setref_t bundle, const sc_char *string) +{ + sc_char *dict_string; + + /* + * Search the existing dictionary for the string. Although not GNU libc, + * some libc's loop or crash when given a list of zero length, so we need to + * trap that here. + */ + if (bundle->dictionary_length > 0) + { + const sc_char *const *dict_search; + + dict_search = (const sc_char *const *)bsearch (&string, bundle->dictionary, + bundle->dictionary_length, + sizeof (bundle->dictionary[0]), prop_compare); + if (dict_search) + return *dict_search; + } + + /* Not found, so copy the string for dictionary insertion. */ + dict_string = (sc_char *)sc_malloc (strlen (string) + 1); + strcpy (dict_string, string); + + /* Extend the dictionary if necessary. */ + bundle->dictionary = (sc_char **)prop_ensure_capacity (bundle->dictionary, + bundle->dictionary_length, + bundle->dictionary_length + 1, + sizeof (bundle->dictionary[0])); + + /* Add the new entry to the end of the dictionary array, and sort. */ + bundle->dictionary[bundle->dictionary_length++] = dict_string; + qsort (bundle->dictionary, + bundle->dictionary_length, + sizeof (bundle->dictionary[0]), prop_compare); + + /* Return the address of the new string. */ + return dict_string; +} + + +/* + * prop_new_node() + * + * Return the address of the next free properties node from the node pool. + * Using a pool gives a performance boost; the number of properties nodes + * for even a small game is large, and preallocating pools avoids excessive + * malloc's of small individual nodes. + */ +static sc_prop_noderef_t +prop_new_node (sc_prop_setref_t bundle) +{ + sc_int node_index; + sc_prop_noderef_t node; + + /* See if we need to create a new node pool. */ + node_index = bundle->node_count % NODE_POOL_CAPACITY; + if (node_index == 0) + { + sc_int required; + + /* Extend the node pools array if necessary. */ + bundle->node_pools = (sc_prop_noderef_t *)prop_ensure_capacity (bundle->node_pools, + bundle->node_pools_length, + bundle->node_pools_length + 1, + sizeof (bundle-> + node_pools[0])); + + /* Create a new node pool, and increment the length. */ + required = NODE_POOL_CAPACITY * sizeof (*bundle->node_pools[0]); + bundle->node_pools[bundle->node_pools_length] = (sc_prop_noderef_t)sc_malloc(required); + bundle->node_pools_length++; + } + + /* Find the next node address, and increment the node counter. */ + node = bundle->node_pools[bundle->node_pools_length - 1] + node_index; + bundle->node_count++; + + /* Return the new node. */ + return node; +} + + +/* + * prop_find_child() + * + * Find a child node of the given parent whose name matches that passed in. + */ +static sc_prop_noderef_t +prop_find_child (sc_prop_noderef_t parent, sc_int type, sc_vartype_t name) +{ + /* See if this node has any children. */ + if (parent->child_list) + { + sc_int index_; + sc_prop_noderef_t child; + + /* Do the lookup based on name type. */ + switch (type) + { + case PROP_KEY_INTEGER: + /* + * As with adding a child below, here we'll range-check an integer + * key just to make sure nobody has any unreal expectations of us. + */ + if (name.integer < 0) + sc_fatal ("prop_find_child: integer key cannot be negative\n"); + else if (name.integer > MAX_INTEGER_KEY) + sc_fatal ("prop_find_child: integer key is too large\n"); + + /* + * For integer lookups, return the child at the indexed offset + * directly, provided it exists. + */ + if (name.integer >= 0 && name.integer < parent->property.integer) + { + child = parent->child_list[name.integer]; + return child; + } + break; + + case PROP_KEY_STRING: + /* Scan children for a string name match. */ + for (index_ = 0; index_ < parent->property.integer; index_++) + { + child = parent->child_list[index_]; + if (strcmp (child->name.string, name.string) == 0) + break; + } + + /* Return child if we found a match. */ + if (index_ < parent->property.integer) + { + /* + * Before returning the child, try to improve future scans by + * moving the matched entry to index_ 0 -- this gives a key set + * sorted by recent usage, helpful as the same string key is + * used repeatedly in loops. + */ + if (index_ > 0) + { + memmove (parent->child_list + 1, + parent->child_list, index_ * sizeof (child)); + parent->child_list[0] = child; + } + return child; + } + break; + + default: + sc_fatal ("prop_find_child: invalid key type\n"); + } + } + + /* No matching child found. */ + return NULL; +} + + +/* + * prop_add_child() + * + * Add a new child node to the given parent. Return its reference. Set + * needs to be passed so that string names can be added to the dictionary. + */ +static sc_prop_noderef_t +prop_add_child (sc_prop_noderef_t parent, + sc_int type, sc_vartype_t name, sc_prop_setref_t bundle) +{ + sc_prop_noderef_t child; + + /* Not possible if growable allocations have been trimmed. */ + if (bundle->is_readonly) + sc_fatal ("prop_add_child: can't add to readonly properties\n"); + + /* Create the new node. */ + child = prop_new_node (bundle); + switch (type) + { + case PROP_KEY_INTEGER: + child->name.integer = name.integer; + break; + case PROP_KEY_STRING: + child->name.string = prop_dictionary_lookup (bundle, name.string); + break; + + default: + sc_fatal ("prop_add_child: invalid key type\n"); + } + + /* Initialize property and child list to visible nulls. */ + child->property.voidp = NULL; + child->child_list = NULL; + + /* Make a brief check for obvious overwrites. */ + if (!parent->child_list && parent->property.voidp) + sc_error ("prop_add_child: node overwritten, probable data loss\n"); + + /* Add the child to the parent, position dependent on key type. */ + switch (type) + { + case PROP_KEY_INTEGER: + /* + * Range check on integer keys, must be >= 0 for direct indexing to work, + * and we'll also apply a reasonableness constraint, to try to catch + * errors where string pointers are passed in as integers, which would + * otherwise lead to some extreme malloc() attempts. + */ + if (name.integer < 0) + sc_fatal ("prop_add_child: integer key cannot be negative\n"); + else if (name.integer > MAX_INTEGER_KEY) + sc_fatal ("prop_add_child: integer key is too large\n"); + + /* Resize the parent's child list if necessary. */ + parent->child_list = (sc_prop_noderef_t *)prop_ensure_capacity (parent->child_list, + parent->property.integer, + name.integer + 1, + sizeof (*parent->child_list)); + + /* Update the child count if the new node increases it. */ + if (parent->property.integer <= name.integer) + parent->property.integer = name.integer + 1; + + /* Store the child in its indexed list location. */ + parent->child_list[name.integer] = child; + break; + + case PROP_KEY_STRING: + /* Add a single entry to the child list, and resize. */ + parent->child_list = (sc_prop_noderef_t *)prop_ensure_capacity (parent->child_list, + parent->property.integer, + parent->property.integer + 1, + sizeof (*parent->child_list)); + + /* Store the child at the end of the list. */ + parent->child_list[parent->property.integer++] = child; + break; + + default: + sc_fatal ("prop_add_child: invalid key type\n"); + } + + return child; +} + + +/* + * prop_put() + * + * Add a property to a properties set. Duplicate entries will replace + * prior ones. + * + * Stores a value of variable type as a property. The value type is one of + * 'I', 'B', or 'S', for integer, boolean, and string values, held in the + * first character of format. The next two characters of format are "->", + * and are syntactic sugar. The remainder of format shows the key makeup, + * with 'i' indicating integer, and 's' string key elements. Example format: + * "I->sssis", stores an integer, with a key composed of three strings, an + * integer, and another string. + */ +void +prop_put (sc_prop_setref_t bundle, const sc_char *format, + sc_vartype_t vt_value, const sc_vartype_t vt_key[]) +{ + sc_prop_noderef_t node; + sc_int index_; + assert (prop_is_valid (bundle)); + + /* Format check. */ + if (!format || format[0] == NUL + || format[1] != '-' || format[2] != '>' || format[3] == NUL) + sc_fatal ("prop_put: format error\n"); + + /* Trace property put. */ + if (prop_trace) + { + sc_trace ("Property: put "); + switch (format[0]) + { + case PROP_STRING: + sc_trace ("\"%s\"", vt_value.string); + break; + case PROP_INTEGER: + sc_trace ("%ld", vt_value.integer); + break; + case PROP_BOOLEAN: + sc_trace ("%s", vt_value.boolean ? "true" : "false"); + break; + + default: + sc_trace ("%p [invalid type]", vt_value.voidp); + break; + } + sc_trace (", key \"%s\" : ", format); + for (index_ = 0; format[index_ + 3] != NUL; index_++) + { + sc_trace ("%s", index_ > 0 ? "," : ""); + switch (format[index_ + 3]) + { + case PROP_KEY_STRING: + sc_trace ("\"%s\"", vt_key[index_].string); + break; + case PROP_KEY_INTEGER: + sc_trace ("%ld", vt_key[index_].integer); + break; + + default: + sc_trace ("%p [invalid type]", vt_key[index_].voidp); + break; + } + } + sc_trace ("\n"); + } + + /* + * Iterate keys, finding matching child nodes at each level. If no matching + * child is found, insert one and continue. + */ + node = bundle->root_node; + for (index_ = 0; format[index_ + 3] != NUL; index_++) + { + sc_prop_noderef_t child; + sc_int type; + + /* + * Search this level for a name matching the key. If found, advance + * to that child node. Otherwise, add the node to the tree, including + * the set so that the dictionary can be extended. + */ + type = format[index_ + 3]; + child = prop_find_child (node, type, vt_key[index_]); + if (child) + node = child; + else + node = prop_add_child (node, type, vt_key[index_], bundle); + } + + /* + * Ensure that we're not about to overwrite an internal node child count. + */ + if (node->child_list) + sc_fatal ("prop_put: overwrite of internal node\n"); + + /* Set our properties in the final node. */ + switch (format[0]) + { + case PROP_INTEGER: + node->property.integer = vt_value.integer; + break; + case PROP_BOOLEAN: + node->property.boolean = vt_value.boolean; + break; + case PROP_STRING: + node->property.string = vt_value.string; + break; + + default: + sc_fatal ("prop_put: invalid property type\n"); + } +} + + +/* + * prop_get() + * + * Retrieve a property from a properties set. Format stuff as above, except + * with "->" replaced with "<-". Returns FALSE if no such property exists. + */ +sc_bool +prop_get (sc_prop_setref_t bundle, const sc_char *format, + sc_vartype_t *vt_rvalue, const sc_vartype_t vt_key[]) +{ + sc_prop_noderef_t node; + sc_int index_; + assert (prop_is_valid (bundle)); + + /* Format check. */ + if (!format || format[0] == NUL + || format[1] != '<' || format[2] != '-' || format[3] == NUL) + sc_fatal ("prop_get: format error\n"); + + /* Trace property get. */ + if (prop_trace) + { + sc_trace ("Property: get, key \"%s\" : ", format); + for (index_ = 0; format[index_ + 3] != NUL; index_++) + { + sc_trace ("%s", index_ > 0 ? "," : ""); + switch (format[index_ + 3]) + { + case PROP_KEY_STRING: + sc_trace ("\"%s\"", vt_key[index_].string); + break; + case PROP_KEY_INTEGER: + sc_trace ("%ld", vt_key[index_].integer); + break; + + default: + sc_trace ("%p [invalid type]", vt_key[index_].voidp); + break; + } + } + sc_trace ("\n"); + } + + /* + * Iterate keys, finding matching child nodes at each level. Stop if no + * matching child is found. + */ + node = bundle->root_node; + for (index_ = 0; format[index_ + 3] != NUL; index_++) + { + sc_int type; + + /* Move node down to the matching child, NULL if no match. */ + type = format[index_ + 3 ]; + node = prop_find_child (node, type, vt_key[index_]); + if (!node) + break; + } + + /* If key iteration halted because no child was found, return FALSE. */ + if (!node) + { + if (prop_trace) + sc_trace ("Property: ...get FAILED\n"); + + return FALSE; + } + + /* + * Enforce integer-only queries on internal nodes, since this is the only + * type of query that makes sense -- any other type is probably a mistake. + */ + if (node->child_list && format[0] != PROP_INTEGER) + sc_fatal ("prop_get: only integer gets on internal nodes\n"); + + /* Return the properties of the final node. */ + switch (format[0]) + { + case PROP_INTEGER: + vt_rvalue->integer = node->property.integer; + break; + case PROP_BOOLEAN: + vt_rvalue->boolean = node->property.boolean; + break; + case PROP_STRING: + vt_rvalue->string = node->property.string; + break; + + default: + sc_fatal ("prop_get: invalid property type\n"); + } + + /* Complete tracing property get. */ + if (prop_trace) + { + sc_trace ("Property: ...get returned : "); + switch (format[0]) + { + case PROP_STRING: + sc_trace ("\"%s\"", vt_rvalue->string); + break; + case PROP_INTEGER: + sc_trace ("%ld", vt_rvalue->integer); + break; + case PROP_BOOLEAN: + sc_trace ("%s", vt_rvalue->boolean ? "true" : "false"); + break; + + default: + sc_trace ("%p [invalid type]", vt_rvalue->voidp); + break; + } + sc_trace ("\n"); + } + return TRUE; +} + + +/* + * prop_trim_node() + * prop_solidify() + * + * Trim excess allocation from growable arrays, and fix the properties set + * so that no further property insertions are allowed. + */ +static void +prop_trim_node (sc_prop_noderef_t node) +{ + /* End recursion on null or childless node. */ + if (node && node->child_list) + { + sc_int index_; + + /* Recursively trim allocation on children. */ + for (index_ = 0; index_ < node->property.integer; index_++) + prop_trim_node (node->child_list[index_]); + + /* Trim allocation on this node. */ + node->child_list = (sc_prop_noderef_t *)prop_trim_capacity (node->child_list, + node->property.integer, + sizeof (*node->child_list)); + } +} + +void +prop_solidify (sc_prop_setref_t bundle) +{ + assert (prop_is_valid (bundle)); + + /* + * Trim back the dictionary, orphans, pools array, and every internal tree + * node. The one thing _not_ to trim is the final node pool -- there are + * references to nodes within it strewn all over the properties tree, and + * it's a large job to try to find and update them; instead, we just live + * with a little wasted heap memory. + */ + bundle->dictionary = (sc_char **)prop_trim_capacity (bundle->dictionary, + bundle->dictionary_length, + sizeof (bundle->dictionary[0])); + bundle->node_pools = (sc_prop_noderef_t *)prop_trim_capacity (bundle->node_pools, + bundle->node_pools_length, + sizeof (bundle->node_pools[0])); + bundle->orphans = (void **)prop_trim_capacity (bundle->orphans, + bundle->orphans_length, + sizeof (bundle->orphans[0])); + prop_trim_node (bundle->root_node); + + /* Set the bundle so that no more properties can be added. */ + bundle->is_readonly = TRUE; +} + + +/* + * prop_get_integer() + * prop_get_boolean() + * prop_get_string() + * + * Convenience functions to retrieve a property of a known type directly. + * It is an error for the property not to exist on retrieval. + */ +sc_int +prop_get_integer (sc_prop_setref_t bundle, + const sc_char *format, const sc_vartype_t vt_key[]) +{ + sc_vartype_t vt_rvalue; + assert (format[0] == PROP_INTEGER); + + if (!prop_get (bundle, format, &vt_rvalue, vt_key)) + sc_fatal ("prop_get_integer: can't retrieve property\n"); + + return vt_rvalue.integer; +} + +sc_bool +prop_get_boolean (sc_prop_setref_t bundle, + const sc_char *format, const sc_vartype_t vt_key[]) +{ + sc_vartype_t vt_rvalue; + assert (format[0] == PROP_BOOLEAN); + + if (!prop_get (bundle, format, &vt_rvalue, vt_key)) + sc_fatal ("prop_get_boolean: can't retrieve property\n"); + + return vt_rvalue.boolean; +} + +const sc_char * +prop_get_string (sc_prop_setref_t bundle, + const sc_char *format, const sc_vartype_t vt_key[]) +{ + sc_vartype_t vt_rvalue; + assert (format[0] == PROP_STRING); + + if (!prop_get (bundle, format, &vt_rvalue, vt_key)) + sc_fatal ("prop_get_string: can't retrieve property\n"); + + return vt_rvalue.string; +} + + +/* + * prop_get_child_count() + * + * Convenience function to retrieve a count of child properties available + * for a given property. Returns zero if the property does not exist. + */ +sc_int +prop_get_child_count (sc_prop_setref_t bundle, + const sc_char *format, const sc_vartype_t vt_key[]) +{ + sc_vartype_t vt_rvalue; + assert (format[0] == PROP_INTEGER); + + if (!prop_get (bundle, format, &vt_rvalue, vt_key)) + return 0; + + /* Return overloaded integer property value, the child count. */ + return vt_rvalue.integer; +} + + +/* + * prop_create_empty() + * + * Create a new, empty properties set, and return it. + */ +static sc_prop_setref_t +prop_create_empty() { + sc_prop_setref_t bundle; + + /* Create a new, empty set. */ + bundle = (sc_prop_setref_t)sc_malloc(sizeof (*bundle)); + bundle->magic = PROP_MAGIC; + + /* Begin with an empty strings dictionary. */ + bundle->dictionary_length = 0; + bundle->dictionary = NULL; + + /* Begin with no allocated node pools. */ + bundle->node_pools_length = 0; + bundle->node_pools = NULL; + bundle->node_count = 0; + + /* Begin with no adopted addresses. */ + bundle->orphans_length = 0; + bundle->orphans = NULL; + + /* Leave open for insertions. */ + bundle->is_readonly = FALSE; + + /* + * Start the set off with a root node. This will also kick off node pools, + * ensuring that every set has at least one node and one allocated pool. + */ + bundle->root_node = prop_new_node (bundle); + bundle->root_node->child_list = NULL; + bundle->root_node->name.string = "ROOT"; + bundle->root_node->property.voidp = NULL; + + /* No taf is yet connected with this set. */ + bundle->taf = NULL; + + return bundle; +} + + +/* + * prop_destroy_child_list() + * prop_destroy() + * + * Free set memory, and destroy a properties set structure. + */ +static void +prop_destroy_child_list (sc_prop_noderef_t node) +{ + /* End recursion on null or childless node. */ + if (node && node->child_list) + { + sc_int index_; + + /* Recursively destroy the children's child lists. */ + for (index_ = 0; index_ < node->property.integer; index_++) + prop_destroy_child_list (node->child_list[index_]); + + /* Free our own child list. */ + sc_free (node->child_list); + } +} + +void +prop_destroy (sc_prop_setref_t bundle) +{ + sc_int index_; + assert (prop_is_valid (bundle)); + + /* Destroy the dictionary, and free it. */ + for (index_ = 0; index_ < bundle->dictionary_length; index_++) + sc_free (bundle->dictionary[index_]); + bundle->dictionary_length = 0; + sc_free (bundle->dictionary); + bundle->dictionary = NULL; + + /* Free adopted addresses. */ + for (index_ = 0; index_ < bundle->orphans_length; index_++) + sc_free (bundle->orphans[index_]); + bundle->orphans_length = 0; + sc_free (bundle->orphans); + bundle->orphans = NULL; + + /* Walk the tree, destroying the child list for each node found. */ + prop_destroy_child_list (bundle->root_node); + bundle->root_node = NULL; + + /* Destroy each node pool. */ + for (index_ = 0; index_ < bundle->node_pools_length; index_++) + sc_free (bundle->node_pools[index_]); + bundle->node_pools_length = 0; + sc_free (bundle->node_pools); + bundle->node_pools = NULL; + + /* Destroy any taf associated with the bundle. */ + if (bundle->taf) + taf_destroy (bundle->taf); + + /* Poison and free the bundle. */ + memset (bundle, 0xaa, sizeof (*bundle)); + sc_free (bundle); +} + + +/* + * prop_create() + * + * Create a new properties set based on a taf, and return it. + */ +sc_prop_setref_t +prop_create (const sc_tafref_t taf) +{ + sc_prop_setref_t bundle; + + /* Create a new, empty set. */ + bundle = prop_create_empty (); + + /* Populate it with data parsed from the taf file. */ + if (!parse_game (taf, bundle)) + { + prop_destroy (bundle); + return NULL; + } + + /* Note the taf for destruction later, and return the new set. */ + bundle->taf = taf; + return bundle; +} + + +/* + * prop_adopt() + * + * Adopt a memory address for free'ing on destroy. + */ +void +prop_adopt (sc_prop_setref_t bundle, void *addr) +{ + assert (prop_is_valid (bundle)); + + /* Extend the orphans array if necessary. */ + bundle->orphans = (void **)prop_ensure_capacity (bundle->orphans, + bundle->orphans_length, + bundle->orphans_length + 1, + sizeof (bundle->orphans[0])); + + /* Add the new address to the end of the array. */ + bundle->orphans[bundle->orphans_length++] = addr; +} + + +/* + * prop_debug_is_dictionary_string() + * prop_debug_dump_node() + * prop_debug_dump() + * + * Print out a complete properties set. + */ +static sc_bool +prop_debug_is_dictionary_string (sc_prop_setref_t bundle, const void *pointer) +{ + const sc_char *const pointer_ = (const sc_char *const )pointer; + sc_int index_; + + /* Compare by pointer directly, not by string value comparisons. */ + for (index_ = 0; index_ < bundle->dictionary_length; index_++) + { + if (bundle->dictionary[index_] == pointer_) + return TRUE; + } + + return FALSE; +} + +static void +prop_debug_dump_node (sc_prop_setref_t bundle, + sc_int depth, sc_int child_index, sc_prop_noderef_t node) +{ + sc_int index_; + + /* Write node preamble, indented two spaces for each depth count. */ + for (index_ = 0; index_ < depth; index_++) + sc_trace (" "); + sc_trace ("%ld : %p", child_index, (void *) node); + + /* Write node, or just a newline if none. */ + if (node) + { + /* Print out the node's key, as hex and either string or decimal. */ + sc_trace (", name %p", node->name.voidp); + if (node != bundle->root_node) + { + if (prop_debug_is_dictionary_string (bundle, node->name.string)) + sc_trace (" \"%s\"", node->name.string); + else + sc_trace (" %ld", node->name.integer); + } + + if (node->child_list) + { + /* Recursively dump children. */ + sc_trace (", child count %ld\n", node->property.integer); + for (index_ = 0; index_ < node->property.integer; index_++) + { + prop_debug_dump_node (bundle, depth + 1, + index_, node->child_list[index_]); + } + } + else + { + /* Print out the node's property, again hex and string or decimal. */ + sc_trace (", property %p", node->property.voidp); + if (taf_debug_is_taf_string (bundle->taf, node->property.string)) + sc_trace (" \"%s\"\n", node->property.string); + else + sc_trace (" %ld\n", node->property.integer); + } + } + else + sc_trace ("\n"); +} + +void +prop_debug_dump (sc_prop_setref_t bundle) +{ + sc_int index_; + assert (prop_is_valid (bundle)); + + /* Dump complete structure. */ + sc_trace ("Property: debug dump follows...\n"); + sc_trace ("bundle->is_readonly = %s\n", + bundle->is_readonly ? "true" : "false"); + sc_trace ("bundle->dictionary_length = %ld\n", bundle->dictionary_length); + + sc_trace ("bundle->dictionary =\n"); + for (index_ = 0; index_ < bundle->dictionary_length; index_++) + { + sc_trace ("%3ld : %p \"%s\"\n", index_, + bundle->dictionary[index_], bundle->dictionary[index_]); + } + + sc_trace ("bundle->node_pools_length = %ld\n", bundle->node_pools_length); + + sc_trace ("bundle->node_pools =\n"); + for (index_ = 0; index_ < bundle->node_pools_length; index_++) + sc_trace ("%3ld : %p\n", index_, (void *) bundle->node_pools[index_]); + + sc_trace ("bundle->node_count = %ld\n", bundle->node_count); + sc_trace ("bundle->root_node = {\n"); + prop_debug_dump_node (bundle, 0, 0, bundle->root_node); + sc_trace ("}\nbundle->taf = %p\n", (void *) bundle->taf); +} + + +/* + * prop_debug_trace() + * + * Set property tracing on/off. + */ +void +prop_debug_trace (sc_bool flag) +{ + prop_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scprotos.h b/engines/glk/adrift/scprotos.h new file mode 100644 index 0000000000..549a0144fe --- /dev/null +++ b/engines/glk/adrift/scprotos.h @@ -0,0 +1,799 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" + +namespace Glk { +namespace Adrift { + +#ifndef ADRIFT_PROTOTYPES_H +#define ADRIFT_PROTOTYPES_H + +/* Runtime version and emulated version, for %version% variable and so on. */ +#ifndef SCARE_VERSION +# define SCARE_VERSION "1.3.10" +#endif +#ifndef SCARE_PATCH_LEVEL +# define SCARE_PATCH_LEVEL "" +#endif +#ifndef SCARE_EMULATION +# define SCARE_EMULATION 4046 +#endif + +/* True and false, unless already defined. */ +#ifndef FALSE +# define FALSE 0 +#endif +#ifndef TRUE +# define TRUE (!FALSE) +#endif + +/* Vartype typedef, supports relaxed typing. */ +typedef union +{ + sc_int integer; + sc_bool boolean; + const sc_char *string; + sc_char *mutable_string; + void *voidp; +} sc_vartype_t; + +/* Standard reader and writer callback function typedefs. */ +typedef sc_int (*sc_read_callbackref_t) (void *, sc_byte *, sc_int); +typedef void (*sc_write_callbackref_t) (void *, const sc_byte *, sc_int); + +/* + * Small utility and wrapper functions. For printf wrappers, try to apply + * gcc printf argument checking; this code is cautious about applying the + * checks. + */ +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95) +extern void sc_trace (const sc_char *format, ...) + __attribute__ ((__format__ (__printf__, 1, 2))); +extern void sc_error (const sc_char *format, ...) + __attribute__ ((__format__ (__printf__, 1, 2))); +extern void sc_fatal (const sc_char *format, ...) + __attribute__ ((__format__ (__printf__, 1, 2))); +#else +extern void sc_trace (const sc_char *format, ...); +extern void sc_error (const sc_char *format, ...); +extern void sc_fatal (const sc_char *format, ...); +#endif +extern void *sc_malloc (size_t size); +extern void *sc_realloc (void *pointer, size_t size); +extern void sc_free (void *pointer); +extern void sc_set_congruential_random (void); +extern void sc_set_platform_random (void); +extern sc_bool sc_is_congruential_random (void); +extern void sc_seed_random (sc_uint new_seed); +extern sc_int sc_rand (void); +extern sc_int sc_randomint (sc_int low, sc_int high); +extern sc_bool sc_strempty (const sc_char *string); +extern sc_char *sc_trim_string (sc_char *string); +extern sc_char *sc_normalize_string (sc_char *string); +extern sc_bool sc_compare_word (const sc_char *string, + const sc_char *word, sc_int length); +extern sc_uint sc_hash (const sc_char *string); + +/* TAF file reader/decompressor enumerations, opaque typedef and functions. */ +enum +{ TAF_VERSION_NONE = 0, + TAF_VERSION_400 = 400, + TAF_VERSION_390 = 390, + TAF_VERSION_380 = 380 +}; + +typedef struct sc_taf_s *sc_tafref_t; +extern void taf_destroy (sc_tafref_t taf); +extern sc_tafref_t taf_create (sc_read_callbackref_t callback, void *opaque); +extern sc_tafref_t taf_create_tas (sc_read_callbackref_t callback, + void *opaque); +extern void taf_first_line (sc_tafref_t taf); +extern const sc_char *taf_next_line (sc_tafref_t taf); +extern sc_bool taf_more_lines (sc_tafref_t taf); +extern sc_int taf_get_game_data_length (sc_tafref_t taf); +extern sc_int taf_get_version (sc_tafref_t taf); +extern sc_bool taf_debug_is_taf_string (sc_tafref_t taf, const void *addr); +extern void taf_debug_dump (sc_tafref_t taf); + +/* Properties store enumerations, opaque typedef, and functions. */ +enum +{ PROP_KEY_STRING = 's', + PROP_KEY_INTEGER = 'i' +}; +enum +{ PROP_INTEGER = 'I', + PROP_BOOLEAN = 'B', + PROP_STRING = 'S' +}; + +typedef struct sc_prop_set_s *sc_prop_setref_t; +extern sc_prop_setref_t prop_create (const sc_tafref_t taf); +extern void prop_destroy (sc_prop_setref_t bundle); +extern void prop_put (sc_prop_setref_t bundle, + const sc_char *format, sc_vartype_t vt_value, + const sc_vartype_t vt_key[]); +extern sc_bool prop_get (sc_prop_setref_t bundle, + const sc_char *format, sc_vartype_t *vt_value, + const sc_vartype_t vt_key[]); +extern void prop_solidify (sc_prop_setref_t bundle); +extern sc_int prop_get_integer (sc_prop_setref_t bundle, + const sc_char *format, + const sc_vartype_t vt_key[]); +extern sc_bool prop_get_boolean (sc_prop_setref_t bundle, + const sc_char *format, + const sc_vartype_t vt_key[]); +extern const sc_char *prop_get_string (sc_prop_setref_t bundle, + const sc_char *format, + const sc_vartype_t vt_key[]); +extern sc_int prop_get_child_count (sc_prop_setref_t bundle, + const sc_char *format, + const sc_vartype_t vt_key[]); +extern void prop_adopt (sc_prop_setref_t bundle, void *addr); +extern void prop_debug_trace (sc_bool flag); +extern void prop_debug_dump (sc_prop_setref_t bundle); + +/* Game parser enumeration and functions. */ +enum +{ ROOMLIST_NO_ROOMS = 0, + ROOMLIST_ONE_ROOM = 1, + ROOMLIST_SOME_ROOMS = 2, + ROOMLIST_ALL_ROOMS = 3, + ROOMLIST_NPC_PART = 4 +}; + +extern sc_bool parse_game (sc_tafref_t taf, sc_prop_setref_t bundle); +extern void parse_debug_trace (sc_bool flag); + +/* Game state structure for modules that use it. */ +typedef struct sc_game_s *sc_gameref_t; + +/* Hint type definition, a thinly disguised pointer to task entry. */ +typedef struct sc_taskstate_s *sc_hintref_t; + +/* Variables set enumerations, opaque typedef, and functions. */ +enum +{ TAFVAR_NUMERIC = 0, + TAFVAR_STRING = 1 +}; +enum +{ VAR_INTEGER = 'I', + VAR_STRING = 'S' +}; + +typedef struct sc_var_set_s *sc_var_setref_t; +extern void var_put (sc_var_setref_t vars, + const sc_char *name, sc_int type, sc_vartype_t vt_value); +extern sc_bool var_get (sc_var_setref_t vars, + const sc_char *name, sc_int *type, + sc_vartype_t *vt_rvalue); +extern void var_put_integer (sc_var_setref_t vars, + const sc_char *name, sc_int value); +extern sc_int var_get_integer (sc_var_setref_t vars, const sc_char *name); +extern void var_put_string (sc_var_setref_t vars, + const sc_char *name, const sc_char *string); +extern const sc_char *var_get_string (sc_var_setref_t vars, + const sc_char *name); +extern sc_var_setref_t var_create (sc_prop_setref_t bundle); +extern void var_destroy (sc_var_setref_t vars); +extern void var_register_game (sc_var_setref_t vars, sc_gameref_t game); +extern void var_set_ref_character (sc_var_setref_t vars, sc_int character); +extern void var_set_ref_object (sc_var_setref_t vars, sc_int object); +extern void var_set_ref_number (sc_var_setref_t vars, sc_int number); +extern void var_set_ref_text (sc_var_setref_t vars, const sc_char *text); +extern sc_int var_get_ref_character (sc_var_setref_t vars); +extern sc_int var_get_ref_object (sc_var_setref_t vars); +extern sc_int var_get_ref_number (sc_var_setref_t vars); +extern const sc_char *var_get_ref_text (sc_var_setref_t vars); +extern sc_uint var_get_elapsed_seconds (sc_var_setref_t vars); +extern void var_set_elapsed_seconds (sc_var_setref_t vars, sc_uint seconds); +extern void var_debug_trace (sc_bool flag); +extern void var_debug_dump (sc_var_setref_t vars); + +/* Expression evaluation functions. */ +extern sc_bool expr_eval_numeric_expression (const sc_char *expression, + sc_var_setref_t vars, + sc_int *rvalue); +extern sc_bool expr_eval_string_expression (const sc_char *expression, + sc_var_setref_t vars, + sc_char **rvalue); + +/* Print filtering opaque typedef and functions. */ +typedef struct sc_filter_s *sc_filterref_t; +extern sc_filterref_t pf_create (void); +extern void pf_destroy (sc_filterref_t filter); +extern void pf_buffer_string (sc_filterref_t filter, + const sc_char *string); +extern void pf_buffer_character (sc_filterref_t filter, + sc_char character); +extern void pf_prepend_string (sc_filterref_t filter, + const sc_char *string); +extern void pf_new_sentence (sc_filterref_t filter); +extern void pf_mute (sc_filterref_t filter); +extern void pf_clear_mute (sc_filterref_t filter); +extern void pf_buffer_tag (sc_filterref_t filter, sc_int tag); +extern void pf_strip_tags (sc_char *string); +extern void pf_strip_tags_for_hints (sc_char *string); +extern sc_char *pf_filter (const sc_char *string, + sc_var_setref_t vars, sc_prop_setref_t bundle); +extern sc_char *pf_filter_for_info (const sc_char *string, + sc_var_setref_t vars); +extern void pf_flush (sc_filterref_t filter, + sc_var_setref_t vars, sc_prop_setref_t bundle); +extern void pf_checkpoint (sc_filterref_t filter, + sc_var_setref_t vars, sc_prop_setref_t bundle); +extern const sc_char *pf_get_buffer (sc_filterref_t filter); +extern sc_char *pf_transfer_buffer (sc_filterref_t filter); +extern void pf_empty (sc_filterref_t filter); +extern sc_char *pf_escape (const sc_char *string); +extern sc_char *pf_filter_input (const sc_char *string, + sc_prop_setref_t bundle); +extern void pf_debug_trace (sc_bool flag); + +/* Game memo opaque typedef and functions. */ +typedef struct sc_memo_set_s *sc_memo_setref_t; +extern sc_memo_setref_t memo_create (void); +extern void memo_destroy (sc_memo_setref_t memento); +extern void memo_save_game (sc_memo_setref_t memento, sc_gameref_t game); +extern sc_bool memo_load_game (sc_memo_setref_t memento, sc_gameref_t game); +extern sc_bool memo_is_load_available (sc_memo_setref_t memento); +extern void memo_clear_games (sc_memo_setref_t memento); +extern void memo_save_command (sc_memo_setref_t memento, + const sc_char *command, sc_int timestamp, + sc_int turns); +extern void memo_unsave_command (sc_memo_setref_t memento); +extern sc_int memo_get_command_count (sc_memo_setref_t memento); +extern void memo_first_command (sc_memo_setref_t memento); +extern void memo_next_command (sc_memo_setref_t memento, + const sc_char **command, sc_int *sequence, + sc_int *timestamp, sc_int *turns); +extern sc_bool memo_more_commands (sc_memo_setref_t memento); +extern const sc_char *memo_find_command (sc_memo_setref_t memento, + sc_int sequence); +extern void memo_clear_commands (sc_memo_setref_t memento); + +/* Game state functions. */ +extern sc_gameref_t gs_create (sc_var_setref_t vars, sc_prop_setref_t bundle, + sc_filterref_t filter); +extern sc_bool gs_is_game_valid (sc_gameref_t game); +extern void gs_copy (sc_gameref_t to, sc_gameref_t from); +extern void gs_destroy (sc_gameref_t game); + +/* Game state accessors and mutators. */ +extern void gs_move_player_to_room (sc_gameref_t game, sc_int room); +extern sc_bool gs_player_in_room (sc_gameref_t game, sc_int room); +extern sc_var_setref_t gs_get_vars (sc_gameref_t gs); +extern sc_prop_setref_t gs_get_bundle (sc_gameref_t gs); +extern sc_filterref_t gs_get_filter (sc_gameref_t gs); +extern sc_memo_setref_t gs_get_memento (sc_gameref_t gs); +extern void gs_set_playerroom (sc_gameref_t gs, sc_int room); +extern void gs_set_playerposition (sc_gameref_t gs, sc_int position); +extern void gs_set_playerparent (sc_gameref_t gs, sc_int parent); +extern sc_int gs_playerroom (sc_gameref_t gs); +extern sc_int gs_playerposition (sc_gameref_t gs); +extern sc_int gs_playerparent (sc_gameref_t gs); +extern sc_int gs_event_count (sc_gameref_t gs); +extern void gs_set_event_state (sc_gameref_t gs, sc_int event, sc_int state); +extern void gs_set_event_time (sc_gameref_t gs, sc_int event, sc_int etime); +extern sc_int gs_event_state (sc_gameref_t gs, sc_int event); +extern sc_int gs_event_time (sc_gameref_t gs, sc_int event); +extern void gs_decrement_event_time (sc_gameref_t gs, sc_int event); +extern sc_int gs_room_count (sc_gameref_t gs); +extern void gs_set_room_seen (sc_gameref_t gs, sc_int room, sc_bool seen); +extern sc_bool gs_room_seen (sc_gameref_t gs, sc_int room); +extern sc_int gs_task_count (sc_gameref_t gs); +extern void gs_set_task_done (sc_gameref_t gs, sc_int task, sc_bool done); +extern void gs_set_task_scored (sc_gameref_t gs, sc_int task, sc_bool scored); +extern sc_bool gs_task_done (sc_gameref_t gs, sc_int task); +extern sc_bool gs_task_scored (sc_gameref_t gs, sc_int task); +extern sc_int gs_object_count (sc_gameref_t gs); +extern void gs_set_object_openness (sc_gameref_t gs, + sc_int object, sc_int openness); +extern void gs_set_object_state (sc_gameref_t gs, sc_int object, sc_int state); +extern void gs_set_object_seen (sc_gameref_t gs, sc_int object, sc_bool seen); +extern void gs_set_object_unmoved (sc_gameref_t gs, + sc_int object, sc_bool unmoved); +extern void gs_set_object_static_unmoved (sc_gameref_t gs, + sc_int object, sc_bool unmoved); +extern sc_int gs_object_openness (sc_gameref_t gs, sc_int object); +extern sc_int gs_object_state (sc_gameref_t gs, sc_int object); +extern sc_bool gs_object_seen (sc_gameref_t gs, sc_int object); +extern sc_bool gs_object_unmoved (sc_gameref_t gs, sc_int object); +extern sc_bool gs_object_static_unmoved (sc_gameref_t gs, sc_int object); +extern sc_int gs_object_position (sc_gameref_t gs, sc_int object); +extern sc_int gs_object_parent (sc_gameref_t gs, sc_int object); +extern void gs_object_move_onto (sc_gameref_t gs, sc_int object, sc_int onto); +extern void gs_object_move_into (sc_gameref_t gs, sc_int object, sc_int into); +extern void gs_object_make_hidden (sc_gameref_t gs, sc_int object); +extern void gs_object_player_get (sc_gameref_t gs, sc_int object); +extern void gs_object_npc_get (sc_gameref_t gs, sc_int object, sc_int npc); +extern void gs_object_player_wear (sc_gameref_t gs, sc_int object); +extern void gs_object_npc_wear (sc_gameref_t gs, sc_int object, sc_int npc); +extern void gs_object_to_room (sc_gameref_t gs, sc_int object, sc_int room); +extern sc_int gs_npc_count (sc_gameref_t gs); +extern void gs_set_npc_location (sc_gameref_t gs, sc_int npc, sc_int location); +extern sc_int gs_npc_location (sc_gameref_t gs, sc_int npc); +extern void gs_set_npc_position (sc_gameref_t gs, sc_int npc, sc_int position); +extern sc_int gs_npc_position (sc_gameref_t gs, sc_int npc); +extern void gs_set_npc_parent (sc_gameref_t gs, sc_int npc, sc_int parent); +extern sc_int gs_npc_parent (sc_gameref_t gs, sc_int npc); +extern void gs_set_npc_seen (sc_gameref_t gs, sc_int npc, sc_bool seen); +extern sc_bool gs_npc_seen (sc_gameref_t gs, sc_int npc); +extern sc_int gs_npc_walkstep_count (sc_gameref_t gs, sc_int npc); +extern void gs_set_npc_walkstep (sc_gameref_t gs, sc_int npc, + sc_int walk, sc_int walkstep); +extern sc_int gs_npc_walkstep (sc_gameref_t gs, sc_int npc, sc_int walk); +extern void gs_decrement_npc_walkstep (sc_gameref_t gs, + sc_int npc, sc_int walkstep); +extern void gs_clear_npc_references (sc_gameref_t gs); +extern void gs_clear_object_references (sc_gameref_t gs); +extern void gs_set_multiple_references (sc_gameref_t gs); +extern void gs_clear_multiple_references (sc_gameref_t gs); + +/* Pattern matching functions. */ +extern sc_bool uip_match (const sc_char *pattern, + const sc_char *string, sc_gameref_t game); +extern sc_char *uip_replace_pronouns (sc_gameref_t game, const sc_char *string); +extern void uip_assign_pronouns (sc_gameref_t game, const sc_char *string); +extern void uip_debug_trace (sc_bool flag); + +/* Library perspective enumeration and functions. */ +enum +{ LIB_FIRST_PERSON = 0, + LIB_SECOND_PERSON = 1, + LIB_THIRD_PERSON = 2 +}; + +extern void lib_warn_battle_system (void); +extern sc_int lib_random_roomgroup_member (sc_gameref_t game, sc_int roomgroup); +extern const sc_char *lib_get_room_name (sc_gameref_t game, sc_int room); +extern void lib_print_room_name (sc_gameref_t game, sc_int room); +extern void lib_print_room_description (sc_gameref_t game, sc_int room); +extern sc_bool lib_cmd_go_north (sc_gameref_t game); +extern sc_bool lib_cmd_go_east (sc_gameref_t game); +extern sc_bool lib_cmd_go_south (sc_gameref_t game); +extern sc_bool lib_cmd_go_west (sc_gameref_t game); +extern sc_bool lib_cmd_go_up (sc_gameref_t game); +extern sc_bool lib_cmd_go_down (sc_gameref_t game); +extern sc_bool lib_cmd_go_in (sc_gameref_t game); +extern sc_bool lib_cmd_go_out (sc_gameref_t game); +extern sc_bool lib_cmd_go_northeast (sc_gameref_t game); +extern sc_bool lib_cmd_go_southeast (sc_gameref_t game); +extern sc_bool lib_cmd_go_northwest (sc_gameref_t game); +extern sc_bool lib_cmd_go_southwest (sc_gameref_t game); +extern sc_bool lib_cmd_go_room (sc_gameref_t game); +extern sc_bool lib_cmd_verbose (sc_gameref_t game); +extern sc_bool lib_cmd_brief (sc_gameref_t game); +extern sc_bool lib_cmd_notify_on_off (sc_gameref_t game); +extern sc_bool lib_cmd_notify (sc_gameref_t game); +extern sc_bool lib_cmd_time (sc_gameref_t game); +extern sc_bool lib_cmd_date (sc_gameref_t game); +extern sc_bool lib_cmd_quit (sc_gameref_t game); +extern sc_bool lib_cmd_restart (sc_gameref_t game); +extern sc_bool lib_cmd_undo (sc_gameref_t game); +extern sc_bool lib_cmd_history (sc_gameref_t game); +extern sc_bool lib_cmd_history_number (sc_gameref_t game); +extern sc_bool lib_cmd_again (sc_gameref_t game); +extern sc_bool lib_cmd_redo_number (sc_gameref_t game); +extern sc_bool lib_cmd_redo_text (sc_gameref_t game); +extern sc_bool lib_cmd_redo_last (sc_gameref_t game); +extern sc_bool lib_cmd_hints (sc_gameref_t game); +extern sc_bool lib_cmd_help (sc_gameref_t game); +extern sc_bool lib_cmd_license (sc_gameref_t game); +extern sc_bool lib_cmd_information (sc_gameref_t game); +extern sc_bool lib_cmd_clear (sc_gameref_t game); +extern sc_bool lib_cmd_statusline (sc_gameref_t game); +extern sc_bool lib_cmd_version (sc_gameref_t game); +extern sc_bool lib_cmd_look (sc_gameref_t game); +extern sc_bool lib_cmd_print_room_exits (sc_gameref_t game); +extern sc_bool lib_cmd_wait (sc_gameref_t game); +extern sc_bool lib_cmd_wait_number (sc_gameref_t game); +extern sc_bool lib_cmd_examine_self (sc_gameref_t game); +extern sc_bool lib_cmd_examine_npc (sc_gameref_t game); +extern sc_bool lib_cmd_examine_object (sc_gameref_t game); +extern sc_bool lib_cmd_count (sc_gameref_t game); +extern sc_bool lib_cmd_take_all (sc_gameref_t game); +extern sc_bool lib_cmd_take_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_take_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_take_all_from (sc_gameref_t game); +extern sc_bool lib_cmd_take_from_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_take_from_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_take_all_from_npc (sc_gameref_t game); +extern sc_bool lib_cmd_take_from_npc_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_take_from_npc_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_take_npc (sc_gameref_t game); +extern sc_bool lib_cmd_drop_all (sc_gameref_t game); +extern sc_bool lib_cmd_drop_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_drop_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_wear_all (sc_gameref_t game); +extern sc_bool lib_cmd_wear_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_wear_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_remove_all (sc_gameref_t game); +extern sc_bool lib_cmd_remove_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_remove_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_kiss_npc (sc_gameref_t game); +extern sc_bool lib_cmd_kiss_object (sc_gameref_t game); +extern sc_bool lib_cmd_kiss_other (sc_gameref_t game); +extern sc_bool lib_cmd_kill_other (sc_gameref_t game); +extern sc_bool lib_cmd_eat_object (sc_gameref_t game); +extern sc_bool lib_cmd_give_object_npc (sc_gameref_t game); +extern sc_bool lib_cmd_inventory (sc_gameref_t game); +extern sc_bool lib_cmd_open_object (sc_gameref_t game); +extern sc_bool lib_cmd_close_object (sc_gameref_t game); +extern sc_bool lib_cmd_unlock_object_with (sc_gameref_t game); +extern sc_bool lib_cmd_lock_object_with (sc_gameref_t game); +extern sc_bool lib_cmd_unlock_object (sc_gameref_t game); +extern sc_bool lib_cmd_lock_object (sc_gameref_t game); +extern sc_bool lib_cmd_ask_npc_about (sc_gameref_t game); +extern sc_bool lib_cmd_put_all_in (sc_gameref_t game); +extern sc_bool lib_cmd_put_in_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_put_in_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_put_all_on (sc_gameref_t game); +extern sc_bool lib_cmd_put_on_except_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_put_on_multiple (sc_gameref_t game); +extern sc_bool lib_cmd_read_object (sc_gameref_t game); +extern sc_bool lib_cmd_read_other (sc_gameref_t game); +extern sc_bool lib_cmd_stand_on_object (sc_gameref_t game); +extern sc_bool lib_cmd_stand_on_floor (sc_gameref_t game); +extern sc_bool lib_cmd_attack_npc_with (sc_gameref_t game); +extern sc_bool lib_cmd_sit_on_object (sc_gameref_t game); +extern sc_bool lib_cmd_sit_on_floor (sc_gameref_t game); +extern sc_bool lib_cmd_lie_on_object (sc_gameref_t game); +extern sc_bool lib_cmd_lie_on_floor (sc_gameref_t game); +extern sc_bool lib_cmd_get_off_object (sc_gameref_t game); +extern sc_bool lib_cmd_get_off (sc_gameref_t game); +extern sc_bool lib_cmd_save (sc_gameref_t game); +extern sc_bool lib_cmd_restore (sc_gameref_t game); +extern sc_bool lib_cmd_locate_object (sc_gameref_t game); +extern sc_bool lib_cmd_locate_npc (sc_gameref_t game); +extern sc_bool lib_cmd_turns (sc_gameref_t game); +extern sc_bool lib_cmd_score (sc_gameref_t game); +extern sc_bool lib_cmd_get_what (sc_gameref_t game); +extern sc_bool lib_cmd_open_what (sc_gameref_t game); +extern sc_bool lib_cmd_close_other (sc_gameref_t game); +extern sc_bool lib_cmd_lock_other (sc_gameref_t game); +extern sc_bool lib_cmd_lock_what (sc_gameref_t game); +extern sc_bool lib_cmd_unlock_other (sc_gameref_t game); +extern sc_bool lib_cmd_unlock_what (sc_gameref_t game); +extern sc_bool lib_cmd_stand_other (sc_gameref_t game); +extern sc_bool lib_cmd_sit_other (sc_gameref_t game); +extern sc_bool lib_cmd_lie_other (sc_gameref_t game); +extern sc_bool lib_cmd_give_object (sc_gameref_t game); +extern sc_bool lib_cmd_give_what (sc_gameref_t game); +extern sc_bool lib_cmd_remove_what (sc_gameref_t game); +extern sc_bool lib_cmd_drop_what (sc_gameref_t game); +extern sc_bool lib_cmd_wear_what (sc_gameref_t game); +extern sc_bool lib_cmd_profanity (sc_gameref_t game); +extern sc_bool lib_cmd_examine_all (sc_gameref_t game); +extern sc_bool lib_cmd_examine_other (sc_gameref_t game); +extern sc_bool lib_cmd_locate_other (sc_gameref_t game); +extern sc_bool lib_cmd_unix_like (sc_gameref_t game); +extern sc_bool lib_cmd_dos_like (sc_gameref_t game); +extern sc_bool lib_cmd_ask_object (sc_gameref_t game); +extern sc_bool lib_cmd_ask_npc (sc_gameref_t game); +extern sc_bool lib_cmd_ask_other (sc_gameref_t game); +extern sc_bool lib_cmd_block_object (sc_gameref_t game); +extern sc_bool lib_cmd_block_other (sc_gameref_t game); +extern sc_bool lib_cmd_block_what (sc_gameref_t game); +extern sc_bool lib_cmd_break_object (sc_gameref_t game); +extern sc_bool lib_cmd_break_other (sc_gameref_t game); +extern sc_bool lib_cmd_break_what (sc_gameref_t game); +extern sc_bool lib_cmd_destroy_what (sc_gameref_t game); +extern sc_bool lib_cmd_smash_what (sc_gameref_t game); +extern sc_bool lib_cmd_buy_object (sc_gameref_t game); +extern sc_bool lib_cmd_buy_other (sc_gameref_t game); +extern sc_bool lib_cmd_buy_what (sc_gameref_t game); +extern sc_bool lib_cmd_clean_object (sc_gameref_t game); +extern sc_bool lib_cmd_clean_other (sc_gameref_t game); +extern sc_bool lib_cmd_clean_what (sc_gameref_t game); +extern sc_bool lib_cmd_climb_object (sc_gameref_t game); +extern sc_bool lib_cmd_climb_other (sc_gameref_t game); +extern sc_bool lib_cmd_climb_what (sc_gameref_t game); +extern sc_bool lib_cmd_cry (sc_gameref_t game); +extern sc_bool lib_cmd_cut_object (sc_gameref_t game); +extern sc_bool lib_cmd_cut_other (sc_gameref_t game); +extern sc_bool lib_cmd_cut_what (sc_gameref_t game); +extern sc_bool lib_cmd_drink_object (sc_gameref_t game); +extern sc_bool lib_cmd_drink_other (sc_gameref_t game); +extern sc_bool lib_cmd_drink_what (sc_gameref_t game); +extern sc_bool lib_cmd_dance (sc_gameref_t game); +extern sc_bool lib_cmd_eat_other (sc_gameref_t game); +extern sc_bool lib_cmd_feed (sc_gameref_t game); +extern sc_bool lib_cmd_fight (sc_gameref_t game); +extern sc_bool lib_cmd_feel (sc_gameref_t game); +extern sc_bool lib_cmd_fix_object (sc_gameref_t game); +extern sc_bool lib_cmd_fix_other (sc_gameref_t game); +extern sc_bool lib_cmd_fix_what (sc_gameref_t game); +extern sc_bool lib_cmd_fly (sc_gameref_t game); +extern sc_bool lib_cmd_hint (sc_gameref_t game); +extern sc_bool lib_cmd_attack_npc (sc_gameref_t game); +extern sc_bool lib_cmd_hit_object (sc_gameref_t game); +extern sc_bool lib_cmd_hit_other (sc_gameref_t game); +extern sc_bool lib_cmd_hit_what (sc_gameref_t game); +extern sc_bool lib_cmd_hum (sc_gameref_t game); +extern sc_bool lib_cmd_jump (sc_gameref_t game); +extern sc_bool lib_cmd_kick_object (sc_gameref_t game); +extern sc_bool lib_cmd_kick_other (sc_gameref_t game); +extern sc_bool lib_cmd_kick_what (sc_gameref_t game); +extern sc_bool lib_cmd_light_object (sc_gameref_t game); +extern sc_bool lib_cmd_light_other (sc_gameref_t game); +extern sc_bool lib_cmd_light_what (sc_gameref_t game); +extern sc_bool lib_cmd_lift_object (sc_gameref_t game); +extern sc_bool lib_cmd_lift_other (sc_gameref_t game); +extern sc_bool lib_cmd_lift_what (sc_gameref_t game); +extern sc_bool lib_cmd_listen (sc_gameref_t game); +extern sc_bool lib_cmd_mend_object (sc_gameref_t game); +extern sc_bool lib_cmd_mend_other (sc_gameref_t game); +extern sc_bool lib_cmd_mend_what (sc_gameref_t game); +extern sc_bool lib_cmd_move_object (sc_gameref_t game); +extern sc_bool lib_cmd_move_other (sc_gameref_t game); +extern sc_bool lib_cmd_move_what (sc_gameref_t game); +extern sc_bool lib_cmd_please (sc_gameref_t game); +extern sc_bool lib_cmd_press_object (sc_gameref_t game); +extern sc_bool lib_cmd_press_other (sc_gameref_t game); +extern sc_bool lib_cmd_press_what (sc_gameref_t game); +extern sc_bool lib_cmd_pull_object (sc_gameref_t game); +extern sc_bool lib_cmd_pull_other (sc_gameref_t game); +extern sc_bool lib_cmd_pull_what (sc_gameref_t game); +extern sc_bool lib_cmd_punch (sc_gameref_t game); +extern sc_bool lib_cmd_push_object (sc_gameref_t game); +extern sc_bool lib_cmd_push_other (sc_gameref_t game); +extern sc_bool lib_cmd_push_what (sc_gameref_t game); +extern sc_bool lib_cmd_repair_object (sc_gameref_t game); +extern sc_bool lib_cmd_repair_other (sc_gameref_t game); +extern sc_bool lib_cmd_repair_what (sc_gameref_t game); +extern sc_bool lib_cmd_rub_object (sc_gameref_t game); +extern sc_bool lib_cmd_rub_other (sc_gameref_t game); +extern sc_bool lib_cmd_rub_what (sc_gameref_t game); +extern sc_bool lib_cmd_run (sc_gameref_t game); +extern sc_bool lib_cmd_say (sc_gameref_t game); +extern sc_bool lib_cmd_sell_object (sc_gameref_t game); +extern sc_bool lib_cmd_sell_other (sc_gameref_t game); +extern sc_bool lib_cmd_sell_what (sc_gameref_t game); +extern sc_bool lib_cmd_shake_object (sc_gameref_t game); +extern sc_bool lib_cmd_shake_npc (sc_gameref_t game); +extern sc_bool lib_cmd_shake_other (sc_gameref_t game); +extern sc_bool lib_cmd_shake_what (sc_gameref_t game); +extern sc_bool lib_cmd_shout (sc_gameref_t game); +extern sc_bool lib_cmd_sing (sc_gameref_t game); +extern sc_bool lib_cmd_sleep (sc_gameref_t game); +extern sc_bool lib_cmd_smell_object (sc_gameref_t game); +extern sc_bool lib_cmd_smell_other (sc_gameref_t game); +extern sc_bool lib_cmd_stop_object (sc_gameref_t game); +extern sc_bool lib_cmd_stop_other (sc_gameref_t game); +extern sc_bool lib_cmd_stop_what (sc_gameref_t game); +extern sc_bool lib_cmd_suck_object (sc_gameref_t game); +extern sc_bool lib_cmd_suck_other (sc_gameref_t game); +extern sc_bool lib_cmd_suck_what (sc_gameref_t game); +extern sc_bool lib_cmd_talk (sc_gameref_t game); +extern sc_bool lib_cmd_thank (sc_gameref_t game); +extern sc_bool lib_cmd_touch_object (sc_gameref_t game); +extern sc_bool lib_cmd_touch_other (sc_gameref_t game); +extern sc_bool lib_cmd_touch_what (sc_gameref_t game); +extern sc_bool lib_cmd_turn_object (sc_gameref_t game); +extern sc_bool lib_cmd_turn_other (sc_gameref_t game); +extern sc_bool lib_cmd_turn_what (sc_gameref_t game); +extern sc_bool lib_cmd_unblock_object (sc_gameref_t game); +extern sc_bool lib_cmd_unblock_other (sc_gameref_t game); +extern sc_bool lib_cmd_unblock_what (sc_gameref_t game); +extern sc_bool lib_cmd_wash_object (sc_gameref_t game); +extern sc_bool lib_cmd_wash_other (sc_gameref_t game); +extern sc_bool lib_cmd_wash_what (sc_gameref_t game); +extern sc_bool lib_cmd_whistle (sc_gameref_t game); +extern sc_bool lib_cmd_interrogation (sc_gameref_t game); +extern sc_bool lib_cmd_xyzzy (sc_gameref_t game); +extern sc_bool lib_cmd_egotistic (sc_gameref_t game); +extern sc_bool lib_cmd_yes_or_no (sc_gameref_t game); +extern sc_bool lib_cmd_verb_object (sc_gameref_t game); +extern sc_bool lib_cmd_verb_npc (sc_gameref_t game); +extern void lib_debug_trace (sc_bool flag); + +/* Resource opaque typedef and control functions. */ +typedef struct sc_resource_s *sc_resourceref_t; +extern sc_bool res_has_sound (sc_gameref_t game); +extern sc_bool res_has_graphics (sc_gameref_t game); +extern void res_clear_resource (sc_resourceref_t resource); +extern sc_bool res_compare_resource (sc_resourceref_t from, + sc_resourceref_t with); +extern void res_handle_resource (sc_gameref_t game, + const sc_char *partial_format, + const sc_vartype_t vt_partial[]); +extern void res_sync_resources (sc_gameref_t game); +extern void res_cancel_resources (sc_gameref_t game); + +/* Game runner functions. */ +extern sc_bool run_game_task_commands (sc_gameref_t game, + const sc_char *string); +extern sc_gameref_t run_create (sc_read_callbackref_t callback, void *opaque); +extern void run_interpret (sc_gameref_t game); +extern void run_destroy (sc_gameref_t game); +extern void run_restart (sc_gameref_t game); +extern void run_save (sc_gameref_t game, + sc_write_callbackref_t callback, void *opaque); +extern sc_bool run_save_prompted (sc_gameref_t game); +extern sc_bool run_restore (sc_gameref_t game, + sc_read_callbackref_t callback, void *opaque); +extern sc_bool run_restore_prompted (sc_gameref_t game); +extern sc_bool run_undo (sc_gameref_t game); +extern void run_quit (sc_gameref_t game); +extern sc_bool run_is_running (sc_gameref_t game); +extern sc_bool run_has_completed (sc_gameref_t game); +extern sc_bool run_is_undo_available (sc_gameref_t game); +extern void run_debug_trace (sc_bool flag); +extern void run_get_attributes (sc_gameref_t game, + const sc_char **game_name, + const sc_char **game_author, + const sc_char **game_compile_date, + sc_int *turns, sc_int *score, + sc_int *max_score, + const sc_char **current_room_name, + const sc_char **status_line, + const sc_char **preferred_font, + sc_bool *bold_room_names, sc_bool *verbose, + sc_bool *notify_score_change); +extern void run_set_attributes (sc_gameref_t game, + sc_bool bold_room_names, sc_bool verbose, + sc_bool notify_score_change); +extern sc_hintref_t run_hint_iterate (sc_gameref_t game, sc_hintref_t hint); +extern const sc_char *run_get_hint_question (sc_gameref_t game, + sc_hintref_t hint); +extern const sc_char *run_get_subtle_hint (sc_gameref_t game, + sc_hintref_t hint); +extern const sc_char *run_get_unsubtle_hint (sc_gameref_t game, + sc_hintref_t hint); + +/* Event functions. */ +extern sc_bool evt_can_see_event (sc_gameref_t game, sc_int event); +extern void evt_tick_events (sc_gameref_t game); +extern void evt_debug_trace (sc_bool flag); + +/* Task functions. */ +extern sc_bool task_has_hints (sc_gameref_t game, sc_int task); +extern const sc_char *task_get_hint_question (sc_gameref_t game, sc_int task); +extern const sc_char *task_get_hint_subtle (sc_gameref_t game, sc_int task); +extern const sc_char *task_get_hint_unsubtle (sc_gameref_t game, sc_int task); +extern sc_bool task_can_run_task_directional (sc_gameref_t game, + sc_int task, sc_bool forwards); +extern sc_bool task_can_run_task (sc_gameref_t game, sc_int task); +extern sc_bool task_run_task (sc_gameref_t game, sc_int task, sc_bool forwards); +extern void task_debug_trace (sc_bool flag); + +/* Task restriction functions. */ +extern sc_bool restr_pass_task_object_state (sc_gameref_t game, + sc_int var1, sc_int var2); +extern sc_bool restr_eval_task_restrictions (sc_gameref_t game, + sc_int task, sc_bool *pass, + const sc_char **fail_message); +extern void restr_debug_trace (sc_bool flag); + +/* NPC gender enumeration and functions. */ +enum +{ NPC_MALE = 0, + NPC_FEMALE = 1, + NPC_NEUTER = 2 +}; + +extern sc_bool npc_in_room (sc_gameref_t game, sc_int npc, sc_int room); +extern sc_int npc_count_in_room (sc_gameref_t game, sc_int room); +extern void npc_setup_initial (sc_gameref_t game); +extern void npc_start_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk); +extern void npc_tick_npcs (sc_gameref_t game); +extern void npc_turn_update (sc_gameref_t game); +extern void npc_debug_trace (sc_bool flag); + +/* Object open/closed state enumeration and functions. */ +enum +{ OBJ_WONTCLOSE = 0, + OBJ_OPEN = 5, + OBJ_CLOSED = 6, + OBJ_LOCKED = 7 +}; + +extern sc_bool obj_is_static (sc_gameref_t game, sc_int object); +extern sc_bool obj_is_container (sc_gameref_t game, sc_int object); +extern sc_bool obj_is_surface (sc_gameref_t game, sc_int object); +extern sc_int obj_container_object (sc_gameref_t game, sc_int n); +extern sc_int obj_surface_object (sc_gameref_t game, sc_int n); +extern sc_bool obj_indirectly_in_room (sc_gameref_t game, + sc_int object, sc_int room); +extern sc_bool obj_indirectly_held_by_player (sc_gameref_t game, sc_int object); +extern sc_bool obj_directly_in_room (sc_gameref_t game, + sc_int object, sc_int room); +extern sc_int obj_stateful_object (sc_gameref_t game, sc_int n); +extern sc_int obj_dynamic_object (sc_gameref_t game, sc_int n); +extern sc_int obj_wearable_object (sc_gameref_t game, sc_int n); +extern sc_int obj_standable_object (sc_gameref_t game, sc_int n); +extern sc_int obj_get_size (sc_gameref_t game, sc_int object); +extern sc_int obj_get_weight (sc_gameref_t game, sc_int object); +extern sc_int obj_get_player_size_limit (sc_gameref_t game); +extern sc_int obj_get_player_weight_limit (sc_gameref_t game); +extern sc_int obj_get_container_maxsize (sc_gameref_t game, sc_int object); +extern sc_int obj_get_container_capacity (sc_gameref_t game, sc_int object); +extern sc_int obj_lieable_object (sc_gameref_t game, sc_int n); +extern sc_bool obj_appears_plural (sc_gameref_t game, sc_int object); +extern void obj_setup_initial (sc_gameref_t game); +extern sc_int obj_container_index (sc_gameref_t game, sc_int object); +extern sc_int obj_surface_index (sc_gameref_t game, sc_int object); +extern sc_int obj_stateful_index (sc_gameref_t game, sc_int object); +extern sc_char *obj_state_name (sc_gameref_t game, sc_int object); +extern sc_bool obj_shows_initial_description (sc_gameref_t game, sc_int object); +extern void obj_turn_update (sc_gameref_t game); +extern void obj_debug_trace (sc_bool flag); + +/* Game serialization functions. */ +extern void ser_save_game (sc_gameref_t game, + sc_write_callbackref_t callback, void *opaque); +extern sc_bool ser_save_game_prompted (sc_gameref_t game); +extern sc_bool ser_load_game (sc_gameref_t game, + sc_read_callbackref_t callback, void *opaque); +extern sc_bool ser_load_game_prompted (sc_gameref_t game); + +/* Locale support, and locale-sensitive functions. */ +extern void loc_detect_game_locale (sc_prop_setref_t bundle); +extern sc_bool loc_set_locale (const sc_char *name); +extern const sc_char *loc_get_locale (void); +extern sc_bool sc_isspace (sc_char character); +extern sc_bool sc_isdigit (sc_char character); +extern sc_bool sc_isalpha (sc_char character); +extern sc_char sc_toupper (sc_char character); +extern sc_char sc_tolower (sc_char character); +extern void loc_debug_dump (void); + +/* Debugger interface. */ +typedef struct sc_debugger_s *sc_debuggerref_t; +extern sc_bool debug_run_command (sc_gameref_t game, + const sc_char *debug_command); +extern sc_bool debug_cmd_debugger (sc_gameref_t game); +extern void debug_set_enabled (sc_gameref_t game, sc_bool enable); +extern sc_bool debug_get_enabled (sc_gameref_t game); +extern void debug_game_started (sc_gameref_t game); +extern void debug_game_ended (sc_gameref_t game); +extern void debug_turn_update (sc_gameref_t game); + +/* OS interface functions. */ +extern sc_bool if_get_trace_flag (sc_uint bitmask); +extern void if_print_string (const sc_char *string); +extern void if_print_debug (const sc_char *string); +extern void if_print_character (sc_char character); +extern void if_print_debug_character (sc_char character); +extern void if_print_tag (sc_int tag, const sc_char *arg); +extern void if_read_line (sc_char *buffer, sc_int length); +extern void if_read_debug (sc_char *buffer, sc_int length); +extern sc_bool if_confirm (sc_int type); +extern void *if_open_saved_game (sc_bool is_save); +extern void if_write_saved_game (void *opaque, + const sc_byte *buffer, sc_int length); +extern sc_int if_read_saved_game (void *opaque, + sc_byte *buffer, sc_int length); +extern void if_close_saved_game (void *opaque); +extern void if_display_hints (sc_gameref_t game); +extern void if_update_sound (const sc_char *filepath, + sc_int sound_offset, + sc_int sound_length, sc_bool is_looping); +extern void if_update_graphic (const sc_char *filepath, + sc_int graphic_offset, + sc_int graphic_length); + +#endif + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scresour.cpp b/engines/glk/adrift/scresour.cpp new file mode 100644 index 0000000000..bc78bf3dea --- /dev/null +++ b/engines/glk/adrift/scresour.cpp @@ -0,0 +1,349 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_char NUL = '\0'; + + +/* + * res_has_sound() + * res_has_graphics() + * + * Return TRUE if the game uses sound or graphics. + */ +sc_bool +res_has_sound (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_bool has_sound; + assert (gs_is_game_valid (game)); + + vt_key[0].string = "Globals"; + vt_key[1].string = "Sound"; + has_sound = prop_get_boolean (bundle, "B<-ss", vt_key); + return has_sound; +} + +sc_bool +res_has_graphics (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_bool has_graphics; + assert (gs_is_game_valid (game)); + + vt_key[0].string = "Globals"; + vt_key[1].string = "Graphics"; + has_graphics = prop_get_boolean (bundle, "B<-ss", vt_key); + return has_graphics; +} + + +/* + * res_set_resource() + * res_clear_resource() + * res_compare_resource() + * + * Convenience functions to set, clear, and compare resource fields. + */ +static void +res_set_resource (sc_resourceref_t resource, const sc_char *name, + sc_int offset, sc_int length) +{ + resource->name = name; + resource->offset = offset; + resource->length = length; +} + +void +res_clear_resource (sc_resourceref_t resource) +{ + res_set_resource (resource, "", 0, 0); +} + +sc_bool +res_compare_resource (sc_resourceref_t from, sc_resourceref_t with) +{ + return strcmp (from->name, with->name) == 0 + && from->offset == with->offset && from->length == with->length; +} + + +/* + * res_handle_resource() + * + * General helper for handling graphics and sound resources. Supplied with a + * partial key to the node containing resources, it identifies what resource + * is appropriate, and sets this as the requested resource in the game, for + * later use on sync'ing, using the handler appropriate for the game version. + * + * The partial format is something like "sis" (the bit to follow I<- or S<- + * in prop_get), and the partial key is guaranteed to contain at least + * strlen(partial_format) elements. + */ +void +res_handle_resource (sc_gameref_t game, + const sc_char *partial_format, + const sc_vartype_t vt_partial[]) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2], *vt_full; + sc_int partial_length, resource_start_offset; + sc_bool embedded; + sc_char *format; + assert (gs_is_game_valid (game)); + assert (partial_format && vt_partial); + + /* + * Check for resources. If this game doesn't use any, exit now to avoid the + * overhead of pointless lookups and allocations. + */ + if (!(res_has_sound (game) || res_has_graphics (game))) + return; + + /* + * Get the global offset for all resources. For version 3.9 games this + * should be zero. For version 4.0 games, it's the start of resource data + * in the TAF file where resources are embedded. + */ + vt_key[0].string = "ResourceOffset"; + resource_start_offset = prop_get_integer (bundle, "I<-s", vt_key); + + /* + * Get the flag that indicated embedded resources. For version 3.9 games + * this should be false. If not set, offset and length are forced to zero + * for interface functions. + */ + vt_key[0].string = "Globals"; + vt_key[1].string = "Embedded"; + embedded = prop_get_boolean (bundle, "B<-ss", vt_key); + + /* + * Allocate a format for use with properties calls, five characters longer + * than the partial passed in. Build a key one element larger than the + * partial supplied, and copy over all supplied elements. + */ + partial_length = strlen (partial_format); + format = (sc_char *)sc_malloc (partial_length + 5); + + vt_full = (sc_vartype_t *)sc_malloc ((partial_length + 1) * sizeof (vt_partial[0])); + memcpy (vt_full, vt_partial, partial_length * sizeof (vt_partial[0])); + + /* Search for sound resources, and offer if found. */ + if (res_has_sound (game)) + { + const sc_char *soundfile; + sc_int soundoffset, soundlen; + + /* Get soundfile property from the node supplied. */ + vt_full[partial_length].string = "SoundFile"; + strcpy (format, "S<-"); + strcat (format, partial_format); + strcat (format, "s"); + soundfile = prop_get_string (bundle, format, vt_full); + + /* If a sound is defined, handle it. */ + if (!sc_strempty (soundfile)) + { + if (embedded) + { + /* Retrieve offset and length. */ + vt_full[partial_length].string = "SoundOffset"; + strcpy (format, "I<-"); + strcat (format, partial_format); + strcat (format, "s"); + soundoffset = prop_get_integer (bundle, format, vt_full) + + resource_start_offset; + + vt_full[partial_length].string = "SoundLen"; + strcpy (format, "I<-"); + strcat (format, partial_format); + strcat (format, "s"); + soundlen = prop_get_integer (bundle, format, vt_full); + } + else + { + /* Coerce offset and length to zero. */ + soundoffset = 0; + soundlen = 0; + } + + /* + * If the sound is the special "##", latch stop, otherwise note + * details to play on sync. + */ + if (!strcmp (soundfile, "##")) + { + game->stop_sound = TRUE; + res_clear_resource (&game->requested_sound); + } + else + { + res_set_resource (&game->requested_sound, + soundfile, soundoffset, soundlen); + } + } + } + + /* Now do the same thing for graphics resources. */ + if (res_has_graphics (game)) + { + const sc_char *graphicfile; + sc_int graphicoffset, graphiclen; + + /* Get graphicfile property from the node supplied. */ + vt_full[partial_length].string = "GraphicFile"; + strcpy (format, "S<-"); + strcat (format, partial_format); + strcat (format, "s"); + graphicfile = prop_get_string (bundle, format, vt_full); + + /* If a graphic is defined, handle it. */ + if (!sc_strempty (graphicfile)) + { + if (embedded) + { + /* Retrieve offset and length. */ + vt_full[partial_length].string = "GraphicOffset"; + strcpy (format, "I<-"); + strcat (format, partial_format); + strcat (format, "s"); + graphicoffset = prop_get_integer (bundle, format, vt_full) + + resource_start_offset; + + vt_full[partial_length].string = "GraphicLen"; + strcpy (format, "I<-"); + strcat (format, partial_format); + strcat (format, "s"); + graphiclen = prop_get_integer (bundle, format, vt_full); + } + else + { + /* Coerce offset and length to zero. */ + graphicoffset = 0; + graphiclen = 0; + } + + /* Graphics resource retrieved, note to show on sync. */ + res_set_resource (&game->requested_graphic, + graphicfile, graphicoffset, graphiclen); + } + } + + /* Free allocated memory. */ + sc_free (format); + sc_free (vt_full); +} + + +/* + * res_sync_resources() + * + * Bring resources into line with the game; called on undo, restart, + * restore, and so on. + */ +void +res_sync_resources (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + /* Deal with any latched sound stop first. */ + if (game->stop_sound) + { + if (game->sound_active) + { + if_update_sound ("", 0, 0, FALSE); + game->sound_active = FALSE; + + res_clear_resource (&game->playing_sound); + } + game->stop_sound = FALSE; + } + + /* Look for a change of sound, and pass to interface on change. */ + if (!res_compare_resource (&game->playing_sound, + &game->requested_sound)) + { + const sc_char *name; + sc_char *clean_name; + sc_bool is_looping; + + /* If the sound name ends '##', this is a looping sound. */ + name = game->requested_sound.name; + is_looping = !strcmp (name + strlen (name) - 2, "##"); + + clean_name = (sc_char *)sc_malloc (strlen (name) + 1); + strcpy (clean_name, name); + if (is_looping) + clean_name[strlen (clean_name) - 2] = NUL; + + if_update_sound (clean_name, + game->requested_sound.offset, + game->requested_sound.length, is_looping); + game->playing_sound = game->requested_sound; + game->sound_active = TRUE; + + sc_free (clean_name); + } + + /* Look for a change of graphic, and pass to interface on change. */ + if (!res_compare_resource (&game->displayed_graphic, + &game->requested_graphic)) + { + if_update_graphic (game->requested_graphic.name, + game->requested_graphic.offset, + game->requested_graphic.length); + game->displayed_graphic = game->requested_graphic; + } +} + + +/* + * res_cancel_resources() + * + * Turn off sound and graphics, and reset the game's tracking of resources in + * use to match. Called on game restart or restore. + */ +void +res_cancel_resources (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + /* Request that everything stops and clears. */ + game->stop_sound = FALSE; + res_clear_resource (&game->requested_sound); + res_clear_resource (&game->requested_graphic); + + /* Synchronize to have the above take effect. */ + res_sync_resources (game); +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/screstrs.cpp b/engines/glk/adrift/screstrs.cpp new file mode 100644 index 0000000000..549ae5a50d --- /dev/null +++ b/engines/glk/adrift/screstrs.cpp @@ -0,0 +1,1163 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +enum { MAX_NESTING_DEPTH = 32 }; +static const sc_char NUL = '\0'; + +/* Trace flag, set before running. */ +static sc_bool restr_trace = FALSE; + + +/* + * restr_integer_variable() + * + * Return the index of the n'th integer found. + */ +static sc_int +restr_integer_variable (sc_gameref_t game, sc_int n) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int var_count, var, count; + + /* Get the count of variables. */ + vt_key[0].string = "Variables"; + var_count = prop_get_child_count (bundle, "I<-s", vt_key); + + /* Progress through variables until n integers found. */ + count = n; + for (var = 0; var < var_count && count >= 0; var++) + { + sc_int type; + + vt_key[1].integer = var; + vt_key[2].string = "Type"; + type = prop_get_integer (bundle, "I<-sis", vt_key); + if (type == TAFVAR_NUMERIC) + count--; + } + return var - 1; +} + + +/* + * restr_object_in_place() + * + * Is object in a certain place, state, or condition. + */ +static sc_bool +restr_object_in_place (sc_gameref_t game, + sc_int object, sc_int var2, sc_int var3) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int npc; + + if (restr_trace) + { + sc_trace ("Restr: checking" + " object in place, %ld, %ld, %ld\n", object, var2, var3); + } + + /* Var2 controls what we do. */ + switch (var2) + { + case 0: + case 6: /* In room */ + if (var3 == 0) + return gs_object_position (game, object) == OBJ_HIDDEN; + else + return gs_object_position (game, object) == var3; + + case 1: + case 7: /* Held by */ + if (var3 == 0) /* Player */ + return gs_object_position (game, object) == OBJ_HELD_PLAYER; + else if (var3 == 1) /* Ref character */ + npc = var_get_ref_character (vars); + else + npc = var3 - 2; + + return gs_object_position (game, object) == OBJ_HELD_NPC + && gs_object_parent (game, object) == npc; + + case 2: + case 8: /* Worn by */ + if (var3 == 0) /* Player */ + return gs_object_position (game, object) == OBJ_WORN_PLAYER; + else if (var3 == 1) /* Ref character */ + npc = var_get_ref_character (vars); + else + npc = var3 - 2; + + return gs_object_position (game, object) == OBJ_WORN_NPC + && gs_object_parent (game, object) == npc; + + case 3: + case 9: /* Visible to */ + if (var3 == 0) /* Player */ + return obj_indirectly_in_room (game, + object, gs_playerroom (game)); + else if (var3 == 1) /* Ref character */ + npc = var_get_ref_character (vars); + else + npc = var3 - 2; + + return obj_indirectly_in_room (game, object, + gs_npc_location (game, npc) - 1); + + case 4: + case 10: /* Inside */ + if (var3 == 0) /* Nothing? */ + return gs_object_position (game, object) != OBJ_IN_OBJECT; + + return gs_object_position (game, object) == OBJ_IN_OBJECT + && gs_object_parent (game, object) == obj_container_object (game, + var3 - 1); + + case 5: + case 11: /* On top of */ + if (var3 == 0) /* Nothing? */ + return gs_object_position (game, object) != OBJ_ON_OBJECT; + + return gs_object_position (game, object) == OBJ_ON_OBJECT + && gs_object_parent (game, object) == obj_surface_object (game, + var3 - 1); + + default: + sc_fatal ("restr_object_in_place: bad var2, %ld\n", var2); + return FALSE; + } +} + + +/* + * restr_pass_task_object_location() + * + * Evaluate restrictions relating to object location. + */ +static sc_bool +restr_pass_task_object_location (sc_gameref_t game, + sc_int var1, sc_int var2, sc_int var3) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_bool should_be; + sc_int object; + + if (restr_trace) + { + sc_trace ("Restr: running object" + " location restriction, %ld, %ld, %ld\n", var1, var2, var3); + } + + /* Initialize variables to avoid gcc warnings. */ + should_be = FALSE; + object = -1; + + /* See how things should look. */ + if (var2 >= 0 && var2 < 6) + should_be = TRUE; + else if (var2 >= 6 && var2 < 12) + should_be = FALSE; + else + sc_fatal ("restr_pass_task_object_location: bad var2, %ld\n", var2); + + /* Now find the addressed object. */ + if (var1 == 0) + { + object = -1; /* No object */ + should_be = !should_be; + } + else if (var1 == 1) + object = -1; /* Any object */ + else if (var1 == 2) + object = var_get_ref_object (vars); + else if (var1 >= 3) + object = obj_dynamic_object (game, var1 - 3); + else + sc_fatal ("restr_pass_task_object_location: bad var1, %ld\n", var1); + + /* + * Here it seems that we have to special case static objects that may have + * crept in through the referenced object. The object in place function + * isn't built to handle these. + * + * TODO What is the meaning of applying object restrictions to static + * objects? + */ + if (var1 == 2 && object != -1 && obj_is_static (game, object)) + { + if (restr_trace) + { + sc_trace ("Restr:" + " restriction object %ld is static, rejecting\n", object); + } + + return FALSE; + } + + /* Try to put it all together. */ + if (object == -1) + { + sc_int target; + + for (target = 0; target < gs_object_count (game); target++) + { + if (restr_object_in_place (game, target, var2, var3)) + return should_be; + } + return !should_be; + } + return should_be == restr_object_in_place (game, object, var2, var3); +} + + +/* + * restr_pass_task_object_state() + * + * Evaluate restrictions relating to object states. This function is called + * from the library by lib_pass_alt_room(), so cannot be static. + */ +sc_bool +restr_pass_task_object_state (sc_gameref_t game, sc_int var1, sc_int var2) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[3]; + sc_int object, openable, key; + + if (restr_trace) + { + sc_trace ("Restr:" + " running object state restriction, %ld, %ld\n", var1, var2); + } + + /* Find the object being addressed. */ + if (var1 == 0) + object = var_get_ref_object (vars); + else + object = obj_stateful_object (game, var1 - 1); + + /* We're interested only in openable objects. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Openable"; + openable = prop_get_integer (bundle, "I<-sis", vt_key); + if (openable > 0) + { + /* Is this object lockable? */ + vt_key[2].string = "Key"; + key = prop_get_integer (bundle, "I<-sis", vt_key); + if (key >= 0) + { + if (var2 <= 2) + return gs_object_openness (game, object) == var2 + 5; + else + return gs_object_state (game, object) == var2 - 2; + } + else + { + if (var2 <= 1) + return gs_object_openness (game, object) == var2 + 5; + else + return gs_object_state (game, object) == var2 - 1; + } + } + else + return gs_object_state (game, object) == var2 + 1; +} + + +/* + * restr_pass_task_task_state() + * + * Evaluate restrictions relating to task states. + */ +static sc_bool +restr_pass_task_task_state (sc_gameref_t game, sc_int var1, sc_int var2) +{ + sc_bool should_be; + + if (restr_trace) + sc_trace ("Restr: running task restriction, %ld, %ld\n", var1, var2); + + /* Initialize variables to avoid gcc warnings. */ + should_be = FALSE; + + /* See if the task should be done or not done. */ + if (var2 == 0) + should_be = TRUE; + else if (var2 == 1) + should_be = FALSE; + else + sc_fatal ("restr_pass_task_task_state: bad var2, %ld\n", var2); + + /* Check all tasks? */ + if (var1 == 0) + { + sc_int task; + + for (task = 0; task < gs_task_count (game); task++) + { + if (gs_task_done (game, task) == should_be) + return FALSE; + } + return TRUE; + } + + /* Check just the given task. */ + return gs_task_done (game, var1 - 1) == should_be; +} + + +/* + * restr_pass_task_char() + * + * Evaluate restrictions relating to player and NPCs. + */ +static sc_bool +restr_pass_task_char (sc_gameref_t game, sc_int var1, sc_int var2, sc_int var3) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int npc1, npc2; + + if (restr_trace) + { + sc_trace ("Restr:" + " running char restriction, %ld, %ld, %ld\n", var1, var2, var3); + } + + /* Handle var2 types 1 and 2. */ + if (var2 == 1) /* Not in same room as */ + return !restr_pass_task_char (game, var1, 0, var3); + else if (var2 == 2) /* Alone */ + return !restr_pass_task_char (game, var1, 3, var3); + + /* Decode NPC number, -1 if none. */ + npc1 = npc2 = -1; + if (var1 == 1) + npc1 = var_get_ref_character (vars); + else if (var1 > 1) + npc1 = var1 - 2; + + /* Player or NPC? */ + if (var1 == 0) + { + sc_vartype_t vt_key[2]; + sc_int gender; + + /* Player -- decode based on var2. */ + switch (var2) + { + case 0: /* In same room as */ + if (var3 == 1) + npc2 = var_get_ref_character (vars); + else if (var3 > 1) + npc2 = var3 - 2; + if (var3 == 0) /* Player */ + return TRUE; + else + return npc_in_room (game, npc2, gs_playerroom (game)); + + case 3: /* Not alone */ + return npc_count_in_room (game, gs_playerroom (game)) > 1; + + case 4: /* Standing on */ + return gs_playerposition (game) == 0 + && gs_playerparent (game) == obj_standable_object (game, + var3 - 1); + + case 5: /* Sitting on */ + return gs_playerposition (game) == 1 + && gs_playerparent (game) == obj_standable_object (game, + var3 - 1); + + case 6: /* Lying on */ + return gs_playerposition (game) == 2 + && gs_playerparent (game) == obj_lieable_object (game, + var3 - 1); + + case 7: /* Player gender */ + vt_key[0].string = "Globals"; + vt_key[1].string = "PlayerGender"; + gender = prop_get_integer (bundle, "I<-ss", vt_key); + return gender == var3; + + default: + sc_fatal ("restr_pass_task_char: invalid type, %ld\n", var2); + return FALSE; + } + } + else + { + sc_vartype_t vt_key[3]; + sc_int gender; + + /* NPC -- decode based on var2. */ + switch (var2) + { + case 0: /* In same room as */ + if (var3 == 0) + return npc_in_room (game, npc1, gs_playerroom (game)); + if (var3 == 1) + npc2 = var_get_ref_character (vars); + else if (var3 > 1) + npc2 = var3 - 2; + return npc_in_room (game, npc1, gs_npc_location (game, npc2) - 1); + + case 3: /* Not alone */ + return npc_count_in_room (game, gs_npc_location (game, npc1) - 1) > 1; + + case 4: /* Standing on */ + return gs_npc_position (game, npc1) == 0 + && gs_playerparent (game) == obj_standable_object (game, var3); + + case 5: /* Sitting on */ + return gs_npc_position (game, npc1) == 1 + && gs_playerparent (game) == obj_standable_object (game, var3); + + case 6: /* Lying on */ + return gs_npc_position (game, npc1) == 2 + && gs_playerparent (game) == obj_lieable_object (game, var3); + + case 7: /* NPC gender */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = npc1; + vt_key[2].string = "Gender"; + gender = prop_get_integer (bundle, "I<-sis", vt_key); + return gender == var3; + + default: + sc_fatal ("restr_pass_task_char: invalid type, %ld\n", var2); + return FALSE; + } + } +} + + +/* + * restr_pass_task_int_var() + * + * Helper for restr_pass_task_var(), handles integer variable restrictions. + */ +static sc_bool +restr_pass_task_int_var (sc_gameref_t game, + sc_int var2, sc_int var3, sc_int value) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[3]; + sc_int value2; + + if (restr_trace) + { + sc_trace ("Restr: running" + " integer var restriction, %ld, %ld, %ld\n", var2, var3, value); + } + + /* Compare against var3 if that's what var2 says. */ + switch (var2) + { + case 0: + return value < var3; + case 1: + return value <= var3; + case 2: + return value == var3; + case 3: + return value >= var3; + case 4: + return value > var3; + case 5: + return value != var3; + + default: + /* + * Compare against the integer var numbered in var3 - 1, or the + * referenced number if var3 is zero. Make sure that we're comparing + * integer variables. + */ + if (var3 == 0) + value2 = var_get_ref_number (vars); + else + { + const sc_char *name; + sc_int ivar, type; + + ivar = restr_integer_variable (game, var3 - 1); + vt_key[0].string = "Variables"; + vt_key[1].integer = ivar; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Type"; + type = prop_get_integer (bundle, "I<-sis", vt_key); + + if (type != TAFVAR_NUMERIC) + { + sc_fatal ("restr_pass_task_int_var:" + " non-integer in comparison, %s\n", name); + } + + /* Get the value in variable numbered in var3 - 1. */ + value2 = var_get_integer (vars, name); + } + + switch (var2) + { + case 10: + return value < value2; + case 11: + return value <= value2; + case 12: + return value == value2; + case 13: + return value >= value2; + case 14: + return value > value2; + case 15: + return value != value2; + + default: + sc_fatal ("restr_pass_task_int_var:" + " unknown int comparison, %ld\n", var2); + return FALSE; + } + } +} + + +/* + * restr_pass_task_string_var() + * + * Helper for restr_pass_task_var(), handles string variable restrictions. + */ +static sc_bool +restr_pass_task_string_var (sc_int var2, + const sc_char *var4, const sc_char *value) +{ + if (restr_trace) + { + sc_trace ("Restr: running string" + " var restriction, %ld, \"%s\", \"%s\"\n", var2, var4, value); + } + + /* Make comparison against var4 based on var2 value. */ + switch (var2) + { + case 0: + return strcmp (value, var4) == 0; /* == */ + case 1: + return strcmp (value, var4) != 0; /* != */ + + default: + sc_fatal ("restr_pass_task_string_var:" + " unknown string comparison, %ld\n", var2); + return FALSE; + } +} + + +/* + * restr_pass_task_var() + * + * Evaluate restrictions relating to variables. + */ +static sc_bool +restr_pass_task_var (sc_gameref_t game, + sc_int var1, sc_int var2, sc_int var3, + const sc_char *var4) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[3]; + sc_int type, value; + const sc_char *name, *string; + + if (restr_trace) + { + sc_trace ("Restr: running var restriction," + " %ld, %ld, %ld, \"%s\"\n", var1, var2, var3, var4); + } + + /* + * For var1=0, compare against referenced number. For var1=1, compare + * against referenced text. + */ + if (var1 == 0) + { + value = var_get_ref_number (vars); + return restr_pass_task_int_var (game, var2, var3, value); + } + else if (var1 == 1) + { + string = var_get_ref_text (vars); + return restr_pass_task_string_var (var2, var4, string); + } + + /* Get the name and type of the variable being addressed. */ + vt_key[0].string = "Variables"; + vt_key[1].integer = var1 - 2; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Type"; + type = prop_get_integer (bundle, "I<-sis", vt_key); + + /* Select first based on variable type. */ + switch (type) + { + case TAFVAR_NUMERIC: + value = var_get_integer (vars, name); + return restr_pass_task_int_var (game, var2, var3, value); + + case TAFVAR_STRING: + string = var_get_string (vars, name); + return restr_pass_task_string_var (var2, var4, string); + + default: + sc_fatal ("restr_pass_task_var: invalid variable type, %ld\n", type); + return FALSE; + } +} + + +/* + * restr_pass_task_restriction() + * + * Demultiplexer for task restrictions. + */ +static sc_bool +restr_pass_task_restriction (sc_gameref_t game, sc_int task, sc_int restriction) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int type, var1, var2, var3; + const sc_char *var4; + sc_bool result = FALSE; + + if (restr_trace) + { + sc_trace ("Restr:" + " evaluating task %ld restriction %ld\n", task, restriction); + } + + /* Get the task restriction type. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Restrictions"; + vt_key[3].integer = restriction; + vt_key[4].string = "Type"; + type = prop_get_integer (bundle, "I<-sisis", vt_key); + + /* Demultiplex depending on type. */ + switch (type) + { + case 0: /* Object location. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + result = restr_pass_task_object_location (game, var1, var2, var3); + break; + + case 1: /* Object state. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + result = restr_pass_task_object_state (game, var1, var2); + break; + + case 2: /* Task state. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + result = restr_pass_task_task_state (game, var1, var2); + break; + + case 3: /* Player and NPCs. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + result = restr_pass_task_char (game, var1, var2, var3); + break; + + case 4: /* Variable. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var4"; + var4 = prop_get_string (bundle, "S<-sisis", vt_key); + result = restr_pass_task_var (game, var1, var2, var3, var4); + break; + + default: + sc_fatal ("restr_pass_task_restriction:" + " unknown restriction type %ld\n", type); + } + + if (restr_trace) + { + sc_trace ("Restr: task %ld restriction" + " %ld is %s\n", task, restriction, result ? "PASS" : "FAIL"); + } + + return result; +} + + +/* Enumeration of restrictions combination string tokens. */ +enum +{ TOK_RESTRICTION = '#', + TOK_AND = 'A', + TOK_OR = 'O', + TOK_LPAREN = '(', + TOK_RPAREN = ')', + TOK_EOS = '\0' +}; + +/* #O#A(#O#)-style expression, for tokenizing. */ +static const sc_char *restr_expression = NULL; +static sc_int restr_index = 0; + +/* + * restr_tokenize_start() + * restr_tokenize_end() + * + * Start and wrap up restrictions combinations string tokenization. + */ +static void +restr_tokenize_start (const sc_char *expression) +{ + /* Save expression, and restart index. */ + restr_expression = expression; + restr_index = 0; +} + +static void +restr_tokenize_end (void) +{ + restr_expression = NULL; + restr_index = 0; +} + + +/* + * restr_next_token() + * + * Simple tokenizer for restrictions combination expressions. + */ +static sc_char +restr_next_token (void) +{ + assert (restr_expression); + + /* Find the next non-space, and return it. */ + while (TRUE) + { + /* Return NUL if at string end. */ + if (restr_expression[restr_index] == NUL) + return restr_expression[restr_index]; + + /* Spin on whitespace. */ + restr_index++; + if (sc_isspace (restr_expression[restr_index - 1])) + continue; + + /* Return the character just passed. */ + return restr_expression[restr_index - 1]; + } +} + + +/* Evaluation values stack. */ +static sc_bool restr_eval_values[MAX_NESTING_DEPTH]; +static sc_int restr_eval_stack = 0; + +/* + * The restriction number to evaluate. This advances with each call to + * evaluate and stack a restriction result. + */ +static sc_int restr_eval_restriction = 0; + +/* The current game used to evaluate restrictions, and the task in question. */ +static sc_gameref_t restr_eval_game = NULL; +static sc_int restr_eval_task = 0; + +/* The id of the lowest-indexed failing restriction. */ +static sc_int restr_lowest_fail = -1; + +/* + * restr_eval_start() + * + * Reset the evaluation stack to an empty state, and note the things we have + * to note for when we need to evaluate a restriction. + */ +static void +restr_eval_start (sc_gameref_t game, sc_int task) +{ + /* Clear stack. */ + restr_eval_stack = 0; + restr_eval_restriction = 0; + + /* Note evaluation details. */ + restr_eval_game = game; + restr_eval_task = task; + + /* Clear lowest indexed failing restriction. */ + restr_lowest_fail = -1; +} + + +/* + * restr_eval_push() + * + * Push a value onto the values stack. + */ +static void +restr_eval_push (sc_bool value) +{ + if (restr_eval_stack >= MAX_NESTING_DEPTH) + sc_fatal ("restr_eval_push: stack overflow\n"); + + restr_eval_values[restr_eval_stack++] = value; +} + + +/* + * expr_restr_action() + * + * Evaluate the effect of an and/or into the values stack. + */ +static void +restr_eval_action (sc_char token) +{ + /* Select action based on parsed token. */ + switch (token) + { + /* Handle evaluating and pushing a restriction result. */ + case TOK_RESTRICTION: + { + sc_bool result; + + /* Evaluate and push the next restriction. */ + result = restr_pass_task_restriction (restr_eval_game, + restr_eval_task, + restr_eval_restriction); + restr_eval_push (result); + + /* + * If the restriction failed, and there isn't yet a first failing one + * set, note this one as the first to fail. + */ + if (restr_lowest_fail == -1 && !result) + restr_lowest_fail = restr_eval_restriction; + + /* Increment restriction sequence identifier. */ + restr_eval_restriction++; + break; + } + + /* Handle cases of or-ing/and-ing restrictions. */ + case TOK_OR: + case TOK_AND: + { + sc_bool val1, val2, result = FALSE; + assert (restr_eval_stack >= 2); + + /* Get the top two stack values. */ + val1 = restr_eval_values[restr_eval_stack - 2]; + val2 = restr_eval_values[restr_eval_stack - 1]; + + /* Or, or and, into result. */ + switch (token) + { + case TOK_OR: + result = val1 || val2; + break; + case TOK_AND: + result = val1 && val2; + break; + + default: + sc_fatal ("restr_eval_action: bad token, '%c'\n", token); + } + + /* Put result back at top of stack. */ + restr_eval_stack--; + restr_eval_values[restr_eval_stack - 1] = result; + break; + } + + default: + sc_fatal ("restr_eval_action: bad token, '%c'\n", token); + } +} + + +/* + * restr_eval_result() + * + * Return the top of the values stack as the evaluation result. + */ +static sc_int +restr_eval_result (sc_int *lowest_fail) +{ + if (restr_eval_stack != 1) + sc_fatal ("restr_eval_result: values stack not completed\n"); + + *lowest_fail = restr_lowest_fail; + return restr_eval_values[0]; +} + + +/* Parse error jump buffer. */ +static jmp_buf restr_parse_error; + +/* Single lookahead token for parser. */ +static sc_char restr_lookahead = '\0'; + +/* + * restr_match() + * + * Match a token with an expectation. + */ +static void +restr_match (sc_char c) +{ + if (restr_lookahead == c) + restr_lookahead = restr_next_token (); + else + { + sc_error ("restr_match:" + " syntax error, expected %d, got %d\n", c, restr_lookahead); + longjmp (restr_parse_error, 1); + } +} + + +/* Forward declaration for recursion. */ +static void restr_bexpr (void); + +/* + * restr_andexpr() + * restr_orexpr() + * restr_bexpr() + * + * Expression parsers. Here we go again... + */ +static void +restr_andexpr (void) +{ + restr_bexpr (); + while (restr_lookahead == TOK_AND) + { + restr_match (TOK_AND); + restr_bexpr (); + restr_eval_action (TOK_AND); + } +} + +static void +restr_orexpr (void) +{ + restr_andexpr (); + while (restr_lookahead == TOK_OR) + { + restr_match (TOK_OR); + restr_andexpr (); + restr_eval_action (TOK_OR); + } +} + +static void +restr_bexpr (void) +{ + switch (restr_lookahead) + { + case TOK_RESTRICTION: + restr_match (TOK_RESTRICTION); + restr_eval_action (TOK_RESTRICTION); + break; + + case TOK_LPAREN: + restr_match (TOK_LPAREN); + restr_orexpr (); + restr_match (TOK_RPAREN); + break; + + default: + sc_error ("restr_bexpr: syntax error, unexpected %d\n", restr_lookahead); + longjmp (restr_parse_error, 1); + } +} + + +/* + * restr_get_fail_message() + * + * Get the FailMessage for the given task restriction; NULL if none. + */ +static const sc_char * +restr_get_fail_message (sc_gameref_t game, sc_int task, sc_int restriction) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + const sc_char *message; + + /* Get the restriction message. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Restrictions"; + vt_key[3].integer = restriction; + vt_key[4].string = "FailMessage"; + message = prop_get_string (bundle, "S<-sisis", vt_key); + + /* Return it, or NULL if empty. */ + return !sc_strempty (message) ? message : NULL; +} + + +/* + * restr_debug_trace() + * + * Set restrictions tracing on/off. + */ +void +restr_debug_trace (sc_bool flag) +{ + restr_trace = flag; +} + + +/* + * restr_eval_task_restrictions() + * + * Main handler for a given set of task restrictions. Returns TRUE in pass + * if the restrictions pass, FALSE if not. On FALSE pass returns, it also + * returns a fail message string from the restriction deemed to have caused + * the failure (that is, the first one with a FailMessage property), or NULL + * if no failing restriction has a FailMessage. The function's main return + * value is TRUE if restrictions parsed successfully, FALSE otherwise. + */ +sc_bool +restr_eval_task_restrictions (sc_gameref_t game, + sc_int task, sc_bool *pass, + const sc_char **fail_message) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int restr_count, lowest_fail; + const sc_char *pattern; + sc_bool result; + assert (pass && fail_message); + + /* Get the count of restrictions on the task. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Restrictions"; + restr_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + /* If none, stop now, acting as if all passed. */ + if (restr_count == 0) + { + if (restr_trace) + sc_trace ("Restr: task %ld has no restrictions\n", task); + + *pass = TRUE; + *fail_message = NULL; + return TRUE; + } + + /* Get the task's restriction combination pattern. */ + vt_key[2].string = "RestrMask"; + pattern = prop_get_string (bundle, "S<-sis", vt_key); + + if (restr_trace) + { + sc_trace ("Restr: task %ld" + " has %ld restrictions, %s\n", task, restr_count, pattern); + } + + /* Set up the evaluation stack and tokenizer. */ + restr_eval_start (game, task); + restr_tokenize_start (pattern); + + /* Try parsing the pattern, and catch errors. */ + if (setjmp (restr_parse_error) == 0) + { + /* Parse the pattern, and ensure it ends at string end. */ + restr_lookahead = restr_next_token (); + restr_orexpr (); + restr_match (TOK_EOS); + } + else + { + /* Parse error -- clean up tokenizer and return fail. */ + restr_tokenize_end (); + return FALSE; + } + + /* Clean up tokenizer and get the evaluation result. */ + restr_tokenize_end (); + result = restr_eval_result (&lowest_fail); + + if (restr_trace) + { + sc_trace ("Restr: task %ld" + " restrictions %s\n", task, result ? "PASS" : "FAIL"); + } + + /* + * Return the result, and if a restriction fails, then return the + * FailMessage of the lowest indexed failing restriction (or NULL if this + * restriction has no FailMessage). + * + * Then return TRUE since parsing and running the restrictions succeeded + * (even if the restrictions themselves didn't). + */ + *pass = result; + if (result) + *fail_message = NULL; + else + *fail_message = restr_get_fail_message (game, task, lowest_fail); + return TRUE; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scrunner.cpp b/engines/glk/adrift/scrunner.cpp new file mode 100644 index 0000000000..872a7648b7 --- /dev/null +++ b/engines/glk/adrift/scrunner.cpp @@ -0,0 +1,2252 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +enum { LINE_BUFFER_SIZE = 256 }; +static const sc_char NUL = '\0'; +static const sc_char SPECIAL_PATTERN = '#'; +static const sc_char WILDCARD_PATTERN = '*'; +static const sc_char *const WHITESPACE = "\t\n\v\f\r "; +static const sc_char *const SEPARATORS = ".,"; + + +/* + * run_is_task_function() + * + * Check for the presence of a command function in the first task command, + * and action it if found. This is a 4.0.42 compatibility hack -- at + * present, only getdynfromroom() exists. Returns TRUE if function found + * and handled. + */ +static sc_bool +run_is_task_function (const sc_char *pattern, sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[3]; + sc_int room, object; + sc_char *argument; + + /* Simple comparison against the one known task expression. */ + argument = (sc_char *)sc_malloc (strlen (pattern) + 1); + if (sscanf (pattern, " # %%object%% = getdynfromroom (%[^)])", argument) == 0) + { + sc_free (argument); + return FALSE; + } + + /* + * Compare the argument read in against known room names. + * + * TODO Is this simple room name comparison good enough? + */ + vt_key[0].string = "Rooms"; + for (room = 0; room < gs_room_count (game); room++) + { + const sc_char *name; + + vt_key[1].integer = room; + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + if (sc_strcasecmp (name, argument) == 0) + break; + } + sc_free (argument); + if (room == gs_room_count (game)) + return FALSE; + + /* + * Select a dynamic object from the room. + * + * TODO What are the selection criteria supposed to be? Here we use "on + * the floor". + */ + vt_key[0].string = "Objects"; + for (object = 0; object < gs_object_count (game); object++) + { + sc_bool bstatic; + + vt_key[1].integer = object; + vt_key[2].string = "Static"; + bstatic = prop_get_boolean (bundle, "B<-sis", vt_key); + if (!bstatic && obj_directly_in_room (game, object, room)) + break; + } + if (object == gs_object_count (game)) + return FALSE; + + /* Set this object reference, unambiguously, as if %object% match. */ + gs_clear_object_references (game); + game->object_references[object] = TRUE; + var_set_ref_object (vars, object); + + return TRUE; +} + + +/* Structure used to associate a pattern with a handler function. */ +typedef struct sc_commands_s +{ + const sc_char *const command; + sc_bool (*const handler) (sc_gameref_t game); +} sc_commands_t; +typedef sc_commands_t *sc_commandsref_t; + +/* Movement commands for the four point compass. */ +static sc_commands_t MOVE_COMMANDS_4[] = { + {"{go {to {the}}} [north/n]", lib_cmd_go_north}, + {"{go {to {the}}} [east/e]", lib_cmd_go_east}, + {"{go {to {the}}} [south/s]", lib_cmd_go_south}, + {"{go {to {the}}} [west/w]", lib_cmd_go_west}, + {"{go {to {the}}} [up/u]", lib_cmd_go_up}, + {"{go {to {the}}} [down/d]", lib_cmd_go_down}, + {"{go {to {the}}} [in]", lib_cmd_go_in}, + {"{go {to {the}}} [out/o]", lib_cmd_go_out}, + {NULL, NULL} +}; + +/* Movement commands for the eight point compass. */ +static sc_commands_t MOVE_COMMANDS_8[] = { + {"{go {to {the}}} [north/n]", lib_cmd_go_north}, + {"{go {to {the}}} [east/e]", lib_cmd_go_east}, + {"{go {to {the}}} [south/s]", lib_cmd_go_south}, + {"{go {to {the}}} [west/w]", lib_cmd_go_west}, + {"{go {to {the}}} [up/u]", lib_cmd_go_up}, + {"{go {to {the}}} [down/d]", lib_cmd_go_down}, + {"{go {to {the}}} [in]", lib_cmd_go_in}, + {"{go {to {the}}} [out/o]", lib_cmd_go_out}, + {"{go {to {the}}} [northeast/north-east/ne]", lib_cmd_go_northeast}, + {"{go {to {the}}} [southeast/south-east/se]", lib_cmd_go_southeast}, + {"{go {to {the}}} [northwest/north-west/nw]", lib_cmd_go_northwest}, + {"{go {to {the}}} [southwest/south-west/sw]", lib_cmd_go_southwest}, + {NULL, NULL} +}; + +/* "Priority" library commands, may take precedence over the game. */ +static sc_commands_t PRIORITY_COMMANDS[] = { + + /* Acquisition of and disposal of inventory. */ + {"[[get/take/remove/extract] [all/everything] from/empty] %object%", + lib_cmd_take_all_from}, + {"[[get/take/remove/extract] [all/everything] from/empty] %object%" + " [[except/but] {for}/apart from] %text%", + lib_cmd_take_from_except_multiple}, + {"[get/take/remove/extract] [all/everything]" + " [[except/but] {for}/apart from] %text% from %object%", + lib_cmd_take_from_except_multiple}, + {"[get/take/remove/extract] %text% from %object%", + lib_cmd_take_from_multiple}, + {"[get/take] [all/everything] from %character%", lib_cmd_take_all_from_npc}, + {"[get/take] [all/everything] from %character%" + " [[except/but] {for}/apart from] %text%", + lib_cmd_take_from_npc_except_multiple}, + {"[get/take] [all/everything]" + " [[except/but] {for}/apart from] %text% from %character%", + lib_cmd_take_from_npc_except_multiple}, + {"[get/take] %text% from %character%", lib_cmd_take_from_npc_multiple}, + {"[[get/take/pick up] [all/everything]/pick [all/everything] up]", + lib_cmd_take_all}, + {"[get/take/pick up] [all/everything] [[except/but] {for}/apart from] %text%", + lib_cmd_take_except_multiple}, + {"[get/take/pick up] %text%", lib_cmd_take_multiple}, + {"pick %text% up", lib_cmd_take_multiple}, + {"[[drop/put down] [all/everything]/put [all/everything] down]", + lib_cmd_drop_all}, + {"[drop/put down] [all/everything] [[except/but] {for}/apart from] %text%", + lib_cmd_drop_except_multiple}, + {"[drop/put down] %text%", lib_cmd_drop_multiple}, + {"put %text% down", lib_cmd_drop_multiple}, + {NULL, NULL} +}; + +/* Standard library commands, other than movement and priority above. */ +static sc_commands_t STANDARD_COMMANDS[] = { + + /* Inventory, and general investigation of surroundings. */ + {"[inventory/inv/i]", lib_cmd_inventory}, + {"[x/ex/exam/examine/l/look {at}] {{the} [room/location]}", lib_cmd_look}, + {"[x/ex/exam/examine/look {at/in}] %object%", lib_cmd_examine_object}, + {"[x/ex/exam/examine/look {at}] %character%", lib_cmd_examine_npc}, + {"[x/ex/exam/examine/look {at}] [me/self/myself]", lib_cmd_examine_self}, + {"[x/ex/exam/examine/look {at}] all", lib_cmd_examine_all}, + + /* Attempted acquisition of and disposal of NPCs. */ + {"[get/take/pick up] %character%", lib_cmd_take_npc}, + {"pick %character% up", lib_cmd_take_npc}, + + /* Manipulating selected objects. */ + {"put [all/everything] [in/into/inside {of}] %object%", lib_cmd_put_all_in}, + {"put [all/everything] [[except/but] {for}/apart from] %text%" + " [in/into/inside {of}] %object%", lib_cmd_put_in_except_multiple}, + {"put %text% [in/into/inside {of}] %object%", lib_cmd_put_in_multiple}, + {"put [all/everything] [on/onto/on top of] %object%", lib_cmd_put_all_on}, + {"put [all/everything] [[except/but] {for}/apart from] %text%" + " [on/onto/on top of] %object%", lib_cmd_put_on_except_multiple}, + {"put %text% [on/onto/on top of] %object%", lib_cmd_put_on_multiple}, + {"open %object%", lib_cmd_open_object}, + {"close %object%", lib_cmd_close_object}, + {"unlock %object% with %text%", lib_cmd_unlock_object_with}, + {"lock %object% with %text%", lib_cmd_lock_object_with}, + {"unlock %object%", lib_cmd_unlock_object}, + {"lock %object%", lib_cmd_lock_object}, + {"read %object%", lib_cmd_read_object}, + {"read *", lib_cmd_read_other}, + {"give %object% to %character%", lib_cmd_give_object_npc}, + {"sit {down/up} [on/in] %object%", lib_cmd_sit_on_object}, + {"stand {up/down} [on/in] %object%", lib_cmd_stand_on_object}, + {"[lie/lay] on %object%", lib_cmd_lie_on_object}, + {"get {down/up} off %object%", lib_cmd_get_off_object}, + {"get off", lib_cmd_get_off}, + {"sit {down/up} {[on/in] {the} [ground/floor]}", lib_cmd_sit_on_floor}, + {"stand {up/down} {[on/in] {the} [ground/floor]}", lib_cmd_stand_on_floor}, + {"[lie/lay] {down/up} {[on/in] {the} [ground/floor]}", lib_cmd_lie_on_floor}, + {"eat %object%", lib_cmd_eat_object}, + + /* Dressing up, and dressing down. */ + {"[[wear/put on/don] [all/everything]/put [all/everything] on]", + lib_cmd_wear_all}, + {"[wear/put on/don] [all/everything] [[except/but] {for}/apart from] %text%", + lib_cmd_wear_except_multiple}, + {"[wear/put on/don] %text%", lib_cmd_wear_multiple}, + {"put %text% on", lib_cmd_wear_multiple}, + {"[[remove/take off/doff] [all/everything]/take [all/everything] off/strip]", + lib_cmd_remove_all}, + {"[remove/take off/doff] [all/everything]" + " [[except/but] {for}/apart from] %text%", + lib_cmd_remove_except_multiple}, + {"[remove/take off/doff] %text%", lib_cmd_remove_multiple}, + {"take %text% off", lib_cmd_remove_multiple}, + + /* Selected NPC interactions and conversation. */ + {"ask %character% about %text%", lib_cmd_ask_npc_about}, + {"[attack/hit/kick/slap/shoot/stab] %character% with %object%", + lib_cmd_attack_npc_with}, + {"[attack/shoot] %character%", lib_cmd_attack_npc}, + + /* More movement, waiting, and miscellaneous administrative commands. */ + {"[goto/go {to}] %text%", lib_cmd_go_room}, + {"[goto/go {to}] *", lib_cmd_print_room_exits}, + {"[exit/exits/directions/where]", lib_cmd_print_room_exits}, + {"[wait/z] %number%", lib_cmd_wait_number}, + {"[wait/z]", lib_cmd_wait}, + {"save", lib_cmd_save}, + {"[restore/load]", lib_cmd_restore}, + {"restart", lib_cmd_restart}, + {"[again/g]", lib_cmd_again}, + {"[redo /!]%number%", lib_cmd_redo_number}, + {"[redo /!]%text%", lib_cmd_redo_text}, + {"[redo/!]", lib_cmd_redo_last}, + {"[quit/q]", lib_cmd_quit}, + {"turns", lib_cmd_turns}, + {"score", lib_cmd_score}, + {"undo", lib_cmd_undo}, + {"[hist/history] %number%", lib_cmd_history_number}, + {"[hist/history]", lib_cmd_history}, + {"[hint/hints]", lib_cmd_hints}, + {"verbose", lib_cmd_verbose}, + {"brief", lib_cmd_brief}, + {"[notify/notification] %text%", lib_cmd_notify_on_off}, + {"[notify/notification]", lib_cmd_notify}, + {"time", lib_cmd_time}, + {"date", lib_cmd_date}, + {"[help/commands]", lib_cmd_help}, + {"[gpl/license]", lib_cmd_license}, + {"[about/info/information/author]", lib_cmd_information}, + {"[clear/cls/clr]", lib_cmd_clear}, + {"status{line}", lib_cmd_statusline}, + {"version", lib_cmd_version}, + + {"[locate/where {is/are}/find] %object%", lib_cmd_locate_object}, + {"[locate/where {is}/find] %character%", lib_cmd_locate_npc}, + + {"[count/num]", lib_cmd_count}, + + /* Standard response commands; no real action, just output. */ + {"[get/take/pick up] *", lib_cmd_get_what}, + {"open *", lib_cmd_open_what}, + {"close *", lib_cmd_close_other}, + {"give %object% *", lib_cmd_give_object}, + {"give *", lib_cmd_give_what}, + {"lock %text%", lib_cmd_lock_other}, + {"lock", lib_cmd_lock_what}, + {"unlock %text%", lib_cmd_unlock_other}, + {"unlock", lib_cmd_unlock_what}, + {"sit {down/up} [on/in] *", lib_cmd_sit_other}, + {"stand {up/down} [on/in] *", lib_cmd_stand_other}, + {"[lie/lay] {down/up} [on/in] *", lib_cmd_lie_other}, + {"[remove/take off/doff] *", lib_cmd_remove_what}, + {"[drop/put down] *", lib_cmd_drop_what}, + {"[wear/put on/don] *", lib_cmd_wear_what}, + {"[shit/fuck/bastard/cunt/crap/hell/shag/bollocks/bollox/bugger] *", + lib_cmd_profanity}, + {"[x/examine/look {at}] *", lib_cmd_examine_other}, + {"[locate/where {is/are}/find] *", lib_cmd_locate_other}, + {"[cp/mv/ln/ls] *", lib_cmd_unix_like}, + {"dir *", lib_cmd_dos_like}, + {"ask %character% *", lib_cmd_ask_npc}, + {"ask %object% *", lib_cmd_ask_object}, + {"ask *", lib_cmd_ask_other}, + {"block %object% *", lib_cmd_block_object}, + {"block %text%", lib_cmd_block_other}, + {"block", lib_cmd_block_what}, + {"[break/destroy/smash] %object% *", lib_cmd_break_object}, + {"[break/destroy/smash] %text%", lib_cmd_break_other}, + {"break", lib_cmd_break_what}, + {"destroy", lib_cmd_destroy_what}, + {"smash", lib_cmd_smash_what}, + {"buy %object% *", lib_cmd_buy_object}, + {"buy %text%", lib_cmd_buy_other}, + {"buy", lib_cmd_buy_what}, + {"clean %object% *", lib_cmd_clean_object}, + {"clean %text%", lib_cmd_clean_other}, + {"clean", lib_cmd_clean_what}, + {"climb %object% *", lib_cmd_climb_object}, + {"climb %text%", lib_cmd_climb_other}, + {"climb", lib_cmd_climb_what}, + {"cry *", lib_cmd_cry}, + {"cut %object% *", lib_cmd_cut_object}, + {"cut %text%", lib_cmd_cut_other}, + {"cut", lib_cmd_cut_what}, + {"dance *", lib_cmd_dance}, + {"drink %object% *", lib_cmd_drink_object}, + {"drink %text%", lib_cmd_drink_other}, + {"drink", lib_cmd_drink_what}, + {"eat *", lib_cmd_eat_other}, + {"feed *", lib_cmd_feed}, + {"feel *", lib_cmd_feel}, + {"fight *", lib_cmd_fight}, + {"fix %object% *", lib_cmd_fix_object}, + {"fix %text%", lib_cmd_fix_other}, + {"fix", lib_cmd_fix_what}, + {"fly *", lib_cmd_fly}, + {"hint *", lib_cmd_hint}, + {"hit %character%", lib_cmd_attack_npc}, + {"hit %object% *", lib_cmd_hit_object}, + {"hit %text%", lib_cmd_hit_other}, + {"hit", lib_cmd_hit_what}, + {"hum *", lib_cmd_hum}, + {"jump *", lib_cmd_jump}, + {"kick %character%", lib_cmd_attack_npc}, + {"kick %object% *", lib_cmd_kick_object}, + {"kick %text%", lib_cmd_kick_other}, + {"kick", lib_cmd_kick_what}, + {"kiss %character% *", lib_cmd_kiss_npc}, + {"kiss %object% *", lib_cmd_kiss_object}, + {"kiss *", lib_cmd_kiss_other}, + {"kill *", lib_cmd_kill_other}, + {"lift %object% *", lib_cmd_lift_object}, + {"lift %text%", lib_cmd_lift_other}, + {"lift", lib_cmd_lift_what}, + {"light %object% *", lib_cmd_light_object}, + {"light %text%", lib_cmd_light_other}, + {"light", lib_cmd_light_what}, + {"listen *", lib_cmd_listen}, + {"mend %object% *", lib_cmd_mend_object}, + {"mend %text%", lib_cmd_mend_other}, + {"mend", lib_cmd_mend_what}, + {"move %object% *", lib_cmd_move_object}, + {"move %text%", lib_cmd_move_other}, + {"move", lib_cmd_move_what}, + {"please *", lib_cmd_please}, + {"press %object% *", lib_cmd_press_object}, + {"press %text%", lib_cmd_press_other}, + {"press", lib_cmd_press_what}, + {"pull %object% *", lib_cmd_pull_object}, + {"pull %text%", lib_cmd_pull_other}, + {"pull", lib_cmd_pull_what}, + {"punch *", lib_cmd_punch}, + {"push %object% *", lib_cmd_push_object}, + {"push %text%", lib_cmd_push_other}, + {"push", lib_cmd_push_what}, + {"repair %object% *", lib_cmd_repair_object}, + {"repair %text%", lib_cmd_repair_other}, + {"repair", lib_cmd_repair_what}, + {"rub %object% *", lib_cmd_rub_object}, + {"rub %text%", lib_cmd_rub_other}, + {"rub", lib_cmd_rub_what}, + {"run *", lib_cmd_run}, + {"say *", lib_cmd_say}, + {"sell %object% *", lib_cmd_sell_object}, + {"sell %text%", lib_cmd_sell_other}, + {"sell", lib_cmd_sell_what}, + {"shake %object% *", lib_cmd_shake_object}, + {"shake %text%", lib_cmd_shake_other}, + {"shake", lib_cmd_shake_what}, + {"shout *", lib_cmd_shout}, + {"sing *", lib_cmd_sing}, + {"sleep *", lib_cmd_sleep}, + {"smell %object% *", lib_cmd_smell_object}, + {"smell *", lib_cmd_smell_other}, + {"stop %object% *", lib_cmd_stop_object}, + {"stop %text%", lib_cmd_stop_other}, + {"stop", lib_cmd_stop_what}, + {"suck %object% *", lib_cmd_suck_object}, + {"suck %text%", lib_cmd_suck_other}, + {"suck", lib_cmd_suck_what}, + {"talk *", lib_cmd_talk}, + {"thank *", lib_cmd_thank}, + {"turn %object% *", lib_cmd_turn_object}, + {"turn %text%", lib_cmd_turn_other}, + {"turn", lib_cmd_turn_what}, + {"touch %object% *", lib_cmd_touch_object}, + {"touch %text%", lib_cmd_touch_other}, + {"touch", lib_cmd_touch_what}, + {"unblock %object% *", lib_cmd_unblock_object}, + {"unblock %text%", lib_cmd_unblock_other}, + {"unblock", lib_cmd_unblock_what}, + {"wash %object% *", lib_cmd_wash_object}, + {"wash %text%", lib_cmd_wash_other}, + {"wash", lib_cmd_wash_what}, + {"whistle *", lib_cmd_whistle}, + {"[why/when/what/can/how] *", lib_cmd_interrogation}, + {"xyzzy *", lib_cmd_xyzzy}, + {"campbell", lib_cmd_egotistic}, + {"[yes/no] *", lib_cmd_yes_or_no}, + {"* %object% *", lib_cmd_verb_object}, + {"* %character% *", lib_cmd_verb_npc}, + + /* SCARE debugger hook command, placed last just in case... */ + {"{#}debug{ger}", debug_cmd_debugger}, + + {NULL, NULL} +}; + + +/* + * run_priority_commands() + * run_standard_commands() + * + * Compare a user input string against commands recognized by the library, + * and action any command. Returns TRUE if the string matched a command + * that then ran successfully, FALSE otherwise. + * + * "Priority" commands are ones that Adrift seems to action no matter what + * the game tries to override. For example, a simple game with one "ball" + * object and a task "* ball *" should, if the task is restricted, override + * "take ball" such that the ball can never be acquired. Adrift lets the + * "take" succeed, though (and more curiously, may respond "I don't + * understand..." to "drop ball"). This could be an Adrift bug. Shrug. + * + * For now, I can't find any better way to try to handle it than to make + * object acquisition take precedence over game commands. + */ +static sc_bool +run_priority_commands (sc_gameref_t game, const sc_char *string) +{ + sc_commandsref_t command; + + for (command = PRIORITY_COMMANDS; command->command; command++) + { + if (uip_match (command->command, string, game)) + { + if (command->handler (game)) + return TRUE; + } + } + + /* Nothing matched match the string. Or if it did, its handler failed. */ + return FALSE; +} + +static sc_bool +run_standard_commands (sc_gameref_t game, const sc_char *string) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[2]; + sc_bool eightpointcompass; + sc_commandsref_t command; + + /* Select the appropriate movement commands. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "EightPointCompass"; + eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); + command = eightpointcompass ? MOVE_COMMANDS_8 : MOVE_COMMANDS_4; + + /* + * Search movement commands first, returning TRUE if any matching command + * handler succeeded. Then repeat for standard library commands. + */ + for (; command->command; command++) + { + if (uip_match (command->command, string, game)) + { + if (command->handler (game)) + return TRUE; + } + } + + for (command = STANDARD_COMMANDS; command->command; command++) + { + if (uip_match (command->command, string, game)) + { + if (command->handler (game)) + return TRUE; + } + } + + /* Nothing matched match the string. Or if it did, its handler failed. */ + return FALSE; +} + + +/* + * run_update_status() + * + * Update the game's current room and status line strings. + */ +static void +run_update_status (sc_gameref_t game) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[2]; + const sc_char *name, *status; + sc_char *filtered; + sc_bool statusbox; + + /* Get the current room name, and filter and untag it. */ + name = lib_get_room_name (game, gs_playerroom (game)); + filtered = pf_filter (name, vars, bundle); + pf_strip_tags (filtered); + + /* Free any existing room name, then save this room name. */ + sc_free (game->current_room_name); + game->current_room_name = filtered; + + /* See if the game does a status box. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "StatusBox"; + statusbox = prop_get_boolean (bundle, "B<-ss", vt_key); + if (statusbox) + { + /* Get the status line, and filter and untag it. */ + vt_key[1].string = "StatusBoxText"; + status = prop_get_string (bundle, "S<-ss", vt_key); + filtered = pf_filter (status, vars, bundle); + pf_strip_tags (filtered); + } + else + /* No status line, so use NULL. */ + filtered = NULL; + + /* Free any existing status line, then save this status text. */ + sc_free (game->status_line); + game->status_line = filtered; +} + + +/* + * run_notify_score_change() + * + * Print an indication of any score change, if appropriate. The change is + * detected by comparing against the undo game. Uses if_print_string() + * directly for printing, rather than the filter, so that it can place its + * output ahead of buffered printfilter text. + */ +static void +run_notify_score_change (sc_gameref_t game) +{ + const sc_gameref_t undo = game->undo; + sc_char buffer[32]; + assert (gs_is_game_valid (undo)); + + /* + * Do nothing if no undo available, or if notification is off, or if we've + * already done this once this turn. + */ + if (!game->undo_available + || !game->notify_score_change || game->has_notified) + return; + + /* Note any change in the score. */ + if (game->score > undo->score) + { + if_print_string ("(Your score has increased by "); + sprintf (buffer, "%ld", game->score - undo->score); + if_print_string (buffer); + if_print_string (")\n"); + } + else if (game->score < undo->score) + { + if_print_string ("(Your score has decreased by "); + sprintf (buffer, "%ld", undo->score - game->score); + if_print_string (buffer); + if_print_string (")\n"); + } + game->has_notified = TRUE; +} + + +/* + * run_match_task_common() + * run_match_task_commands() + * run_match_task_functions() + * + * Helpers for run_game_commands_common(). + * + * Search task command for a match to the string passed in, returning TRUE + * if a task command matches, FALSE otherwise. Ordinary or reverse commands + * are selected by 'forwards'. + */ +static sc_bool +run_match_task_common (sc_gameref_t game, + sc_int task, const sc_char *string, sc_bool forwards, + sc_bool is_library, sc_bool is_normal) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int command_count, command; + sc_bool is_matched; + + /* Get the count of task commands. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = forwards ? "Command" : "ReverseCommand"; + command_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + /* Iterate over commands, looking for patterns that match string. */ + is_matched = FALSE; + for (command = 0; command < command_count; command++) + { + const sc_char *pattern; + sc_int first; + + /* Retrieve the pattern for this command, find its first character. */ + vt_key[3].integer = command; + pattern = prop_get_string (bundle, "S<-sisi", vt_key); + first = strspn (pattern, WHITESPACE); + + /* Match using either the parser, or the special function matcher. */ + if (is_normal) + { + if (pattern[first] != SPECIAL_PATTERN) + { + /* + * Make a special case of library calls and commands that begin + * with a wildcard; these we ignore for this match attempt. + */ + if (is_library && pattern[first] == WILDCARD_PATTERN) + is_matched = FALSE; + else + is_matched = uip_match (pattern, string, game); + } + } + else + { + if (pattern[first] == SPECIAL_PATTERN) + is_matched = run_is_task_function (pattern, game); + } + + /* Stop searching if we find a match. */ + if (is_matched) + break; + } + + /* Return TRUE if we found a pattern match. */ + return is_matched; +} + +static sc_bool +run_match_task_commands (sc_gameref_t game, + sc_int task, const sc_char *string, + sc_bool forwards, sc_bool is_library) +{ + /* + * Match tasks using the normal pattern matcher, with or without any note + * about whether the call is from the library. + */ + return run_match_task_common (game, task, string, forwards, is_library, TRUE); +} + +static sc_bool +run_match_task_functions (sc_gameref_t game, + sc_int task, const sc_char *string, sc_bool forwards) +{ + /* Match tasks against "task command functions". */ + return run_match_task_common (game, task, string, forwards, FALSE, FALSE); +} + + +/* + * run_task_is_unrestricted() + * run_task_is_loudly_restricted() + * + * Helpers for run_game_commands_common(). + * + * Adapters for uncovering task restriction state. The first returns TRUE + * if the task is unrestricted, and can therefore run unimpeded. The second + * returns TRUE iff the task is restricted and has a fail message that + * indicates why it fails; such tasks, if run, produce their failure message + * and don't change state. + */ +static sc_bool +run_task_is_unrestricted (sc_gameref_t game, sc_int task) +{ + sc_bool restrictions_passed; + const sc_char *fail_message; + + /* + * Evaluate task restrictions, and if they fail to parse for some reason, + * return as if restrictions did not pass. + */ + if (!restr_eval_task_restrictions (game, task, + &restrictions_passed, &fail_message)) + { + sc_error ("run_task_is_unrestricted: restrictions error, %ld\n", task); + return FALSE; + } + + /* Return TRUE if the task is unrestricted. */ + return restrictions_passed; +} + +static sc_bool +run_task_is_loudly_restricted (sc_gameref_t game, sc_int task) +{ + sc_bool restrictions_passed; + const sc_char *fail_message; + + /* + * Evaluate task restrictions, and if they fail to parse for some reason, + * return as if restrictions did not pass. + */ + if (!restr_eval_task_restrictions (game, task, + &restrictions_passed, &fail_message)) + { + sc_error ("run_task_is_loudly_restricted:" + " restrictions error, %ld\n", task); + return TRUE; + } + + /* Return TRUE if the task is restricted and indicates why. */ + return !restrictions_passed && (fail_message != NULL); +} + + +/* + * run_game_commands_common() + * run_game_commands_in_parser_context() + * run_game_commands_in_library_context() + * + * The central handler for running, or at least trying to run, game-defined + * tasks that have commands that match the input string. Here's the algorithm + * as currently understood (and it may not be right, so be warned): + * + * for each task executable in the current room + * for direction in forwards, backwards + * for each command string defined by the task for this direction + * match against player input + * if any command string matched player input + * if task restrictions pass + * run the task actions in the current direction + * if the task actions produced output + * return + * is_matched := true + * break out of all loops + * + * if not is_matched and we're allowing restrictions to fail tasks + * for each task executable in the current room + * for direction in forwards, backwards + * for each command string defined by the task for this direction + * match against player input + * if any command string matched player input + * if task restrictions fail with an error message + * run the task, to persuade it to print this error message + * return + * + * Part of the fun and games is that run_game_task_commands() is called by the + * library to try to run "get " and "drop " game commands for standard get/drop + * handlers and get_all/drop_all handlers. No pressure, then. + */ +static sc_bool +run_game_commands_common (sc_gameref_t game, const sc_char *string, + sc_bool include_restrictions, sc_bool is_library) +{ + sc_bool is_matched = FALSE, is_handled = FALSE; + sc_bool *is_matching; + sc_int task_count, task, direction; + + /* + * Matching is expensive, so it helps to use a cache of results from the + * first loop in the second. If we're using the second, that is. + */ + task_count = gs_task_count (game); + if (include_restrictions) + { + is_matching = (sc_bool *)sc_malloc (task_count * sizeof (*is_matching)); + memset (is_matching, FALSE, task_count * sizeof (*is_matching)); + } + else + is_matching = NULL; + + /* + * Iterate over every task, ignoring those not runnable. For each runnable + * task, try matching task commands, and on matches, check restrictions and + * if they pass, try running the task. + */ + for (task = 0; task < task_count; task++) + { + if (!task_can_run_task (game, task)) + continue; + + /* + * Try matching forwards and reverse commands. If there's a match for + * unrestricted tasks, run the task, and if it runs (defined as printing + * some game output), we're done; otherwise, note the command match but + * keep searching for other possible matches. + */ + for (direction = 0; direction < 2; direction++) + { + const sc_bool is_forwards = !direction; + + if (task_can_run_task_directional (game, task, is_forwards) + && run_match_task_commands (game, task, string, + is_forwards, is_library)) + { + if (run_task_is_unrestricted (game, task)) + { + if (task_run_task (game, task, is_forwards)) + is_handled = TRUE; + is_matched = TRUE; + break; + } + + if (is_matching) + is_matching[task] = TRUE; + } + } + if (is_matched) + break; + } + + /* + * If no match, and we've been asked to consider failing restrictions, look + * through all of the runnable tasks again, this time searching for + * restricted ones with a fail message. Use the cache built above to weed + * out matches that are certain to fail. + */ + if (!is_handled && !is_matched && include_restrictions) + { + for (task = 0; task < task_count; task++) + { + if (!is_matching[task] || !task_can_run_task (game, task)) + continue; + + /* + * Check matches of forwards and reverse commands. If there's a + * match for restricted tasks (ones that have and will print a fail + * message if we try to run them), run the task to get the print of + * the fail message, and we're done. + */ + for (direction = 0; direction < 2; direction++) + { + const sc_bool is_forwards = !direction; + + if (task_can_run_task_directional (game, task, is_forwards) + && run_match_task_commands (game, task, string, + is_forwards, is_library)) + { + if (run_task_is_loudly_restricted (game, task)) + { + if (task_run_task (game, task, is_forwards)) + { + is_handled = TRUE; + break; + } + } + } + } + if (is_handled) + break; + } + } + + /* Return TRUE if any game task handled the command in some way. */ + sc_free (is_matching); + return is_handled; +} + +static sc_bool +run_game_commands_in_parser_context (sc_gameref_t game, const sc_char *string, + sc_bool include_restrictions) +{ + /* + * Try game commands, either with or without restrictions, and all full and + * complete parse matching (no special case for game commands that begin + * with a '*' wildcard). + */ + return run_game_commands_common (game, string, include_restrictions, FALSE); +} + +static sc_bool +run_game_commands_in_library_context (sc_gameref_t game, const sc_char *string) +{ + /* + * Try game commands, including restrictions, and noting that this is a + * library call so that the parse matcher can exclude game commands that + * begin with a '*' wildcard. + */ + return run_game_commands_common (game, string, TRUE, TRUE); +} + + +/* + * run_game_functions() + * + * Iterate over every task, ignoring those not runnable, searching just for + * "task command functions". These seem to happen in addition to any regular + * command matches, so we try them as a separate action. + */ +static void +run_game_functions (sc_gameref_t game, const sc_char *string) +{ + sc_int task_count, task, direction; + + /* Iterate over every task, ignoring those not runnable. */ + task_count = gs_task_count (game); + for (task = 0; task < task_count; task++) + { + if (!task_can_run_task (game, task)) + continue; + + /* + * Try matching forwards and reverse commands. I don't know if it's + * valid to put a function in a reverse command, but nevertheless... + */ + for (direction = 0; direction < 2; direction++) + { + const sc_bool is_forwards = !direction; + + if (task_can_run_task_directional (game, task, is_forwards) + && run_match_task_functions (game, task, string, is_forwards)) + { + if (run_task_is_unrestricted (game, task)) + task_run_task (game, task, is_forwards); + } + } + } +} + + +/* + * run_all_commands() + * run_game_task_commands() + * + * Alternative facets of run_commands_common(). The first is used by the + * main user input handling loop; the latter by the library when looking for + * game commands that override standard actions. + */ +static sc_bool +run_all_commands (sc_gameref_t game, const sc_char *string) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_bool status; + + /* + * Adrift command matching is just weird, perhaps broken. In theory, a + * game can override system commands with a properly constructed task and + * set of command matchers. However, the Runner isn't terribly consistent + * in when this will work and when not, and some games rely on that in- + * consistency. In particular, a game with a "* object" task that has + * failing restrictions will not be able to override the system's "take + * object", whereas a game's "take object", under the same circumstances, + * will. Yet if the restrictions pass, a game's "* object" overrides the + * system's "take object" with no apparent difficulty. + * + * For example, "The Woods Are Dark" has a "* ball *" task with the + * restriction "must be holding ball". Without special casing it, there's + * no way to get the ball in the first place. + * + * Trying to find the right way to do things here, then, has been tricky. + * Here's the current process: First, run game commands, ignoring any + * cases where restrictions fail to let the task run. Next, try "priority" + * system commands; ones that move objects to inventory. These system + * commands will call back into trying game commands for objects taken or + * dropped, and in those tries, allow overrides only if the game task is + * explicit about what it's doing (that is, doesn't start with "*"), and + * handle restrictions in those tries. After that, retry all game commands + * again with restrictions enabled. And finally, try all other standard + * library commands. + * + * TODO This is the fourth or fifth attempt at getting this to match the + * Runner, which is surprisingly inconsistent in this area. What on earth + * is the real behavior supposed to be? + */ + status = run_game_commands_in_parser_context (game, string, FALSE); + if (!status) + status = run_priority_commands (game, string); + if (!status) + status = run_game_commands_in_parser_context (game, string, TRUE); + if (!status) + status = run_standard_commands (game, string); + + /* + * For version 4.0 games, it seems that if any command succeeded, we need + * need to scan for and run any matching "task command functions", in + * addition to anything done above. + */ + if (status && !game->is_admin) + { + sc_vartype_t vt_key; + sc_int version; + + /* Check "task command functions" for version 4.0 only. */ + vt_key.string = "Version"; + version = prop_get_integer (bundle, "I<-s", &vt_key); + if (version == TAF_VERSION_400) + run_game_functions (game, string); + } + + return status; +} + +sc_bool +run_game_task_commands (sc_gameref_t game, const sc_char *string) +{ + return run_game_commands_in_library_context (game, string); +} + + +/* + * run_player_input() + * + * Take a line of player input and buffer it. Split the line into elements + * separated by periods. For the first element, try to match it to either a + * task or a standard command, and return TRUE if it matched, FALSE otherwise. + * + * On subsequent calls, successively work with the next line element until + * none remain. In this case, prompt for more player input and continue as + * above. + * + * For the case of "again" or "g", rerun the last successful command element. + * + * One extra special special case; if called with a game that is not running, + * this is a signal to reset all noted line input to initial conditions, and + * just return. Sorry about the ugliness. + */ +static sc_bool +run_player_input (sc_gameref_t game) +{ + static sc_char line_buffer[LINE_BUFFER_SIZE]; + static sc_char prior_element[LINE_BUFFER_SIZE]; + static sc_char line_element[LINE_BUFFER_SIZE]; + + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_memo_setref_t memento = gs_get_memento (game); + sc_bool is_rerunning, was_undo_available, status; + sc_char *filtered, *replaced; + const sc_char *command; + + /* Special case; reset statics if the game isn't running. */ + if (!game->is_running) + { + memset (line_buffer, NUL, sizeof (line_buffer)); + memset (prior_element, NUL, sizeof (prior_element)); + memset (line_element, NUL, sizeof (line_element)); + return TRUE; + } + + /* + * Save the settings of the game's do_again and undo_available flags for + * later checks. + */ + is_rerunning = game->do_again; + was_undo_available = game->undo_available; + + /* See if the player asked to rerun a command element. */ + if (game->do_again) + { + game->do_again = FALSE; + + /* Check there is a last element to repeat. */ + if (prior_element[0] == NUL) + { + pf_buffer_string (filter, "You can hardly repeat that.\n"); + return FALSE; + } + + /* Make the last element the current input element. */ + strcpy (line_element, prior_element); + } + else + { + sc_int length, extent; + + /* + * If there's none buffered, read a new line of player input. Other- + * wise, separate output so far with a newline. + */ + if (line_buffer[0] == NUL) + if_read_line (line_buffer, sizeof (line_buffer)); + else + if_print_character ('\n'); + + /* + * Find the length of the next input line element. Unless the line + * buffer is empty, we always take the first character, even if it's a + * separator. This catches odd input like "." and turns it into a + * parser complaint, rather than treating it as two empty commands with + * a separator between them; this makes it close to what Inform does + * with similar inputs. + */ + length = (line_buffer[0] == NUL) ? 0 : 1; + while (line_buffer[length] != NUL + && strchr (SEPARATORS, line_buffer[length]) == NULL) + length++; + + /* + * Make this the current input element, and remove it, the separator, + * and any trailing whitespace, from the front of the line buffer. + * Removing whitespace prevents "i. ." looking like "i" and ""; it + * instead looks like "i" and ".", and results in a parser complaint. + */ + memcpy (line_element, line_buffer, length); + line_element[length] = NUL; + + extent = length; + extent += (line_buffer[length] == NUL + || strchr (SEPARATORS, line_buffer[length]) == NULL) ? 0 : 1; + extent += strspn (line_buffer + extent, WHITESPACE); + memmove (line_buffer, + line_buffer + extent, strlen (line_buffer) - extent + 1); + } + + /* Copy the current game to the temporary undo buffer. */ + gs_copy (game->temporary, game); + + /* Filter the input element for synonyms, then for pronouns. */ + filtered = pf_filter_input (line_element, bundle); + replaced = uip_replace_pronouns (game, filtered ? filtered : line_element); + + /* + * If filtering didn't replace synonyms, or no pronouns were replaced, use + * the original line element. + */ + command = replaced ? sc_normalize_string (replaced) + : (filtered ? sc_normalize_string (filtered) : line_element); + if (command != line_element) + { + if_print_tag (SC_TAG_ITALICS, ""); + if_print_character ('['); + if_print_string (command); + if_print_character (']'); + if_print_tag (SC_TAG_ENDITALICS, ""); + if_print_character ('\n'); + } + + /* Try the command line element against command matchers. */ + status = run_all_commands (game, command); + if (!status) + { + /* Only complain on non-empty command input line elements. */ + if (!sc_strempty (command)) + { + sc_vartype_t vt_key[2]; + sc_char *escaped; + const sc_char *message; + + /* Command line element not understood. */ + escaped = pf_escape (sc_normalize_string (line_element)); + var_set_ref_text (vars, escaped); + sc_free (escaped); + vt_key[0].string = "Globals"; + vt_key[1].string = "DontUnderstand"; + message = prop_get_string (bundle, "S<-ss", vt_key); + pf_buffer_string (filter, message); + pf_buffer_character (filter, '\n'); + + /* + * On a line element that's not understood, throw out any remaining + * input line elements. + */ + line_buffer[0] = NUL; + sc_free (filtered); + sc_free (replaced); + return status; + } + } + else + { + /* + * Unless administrative, back up any valid undo, copy the temporary + * game into the undo buffer, flag the undo buffer as available, and + * assign any pronouns used in the command ready for the next iteration. + */ + if (!game->is_admin) + { + if (game->undo_available) + memo_save_game (memento, game->undo); + + gs_copy (game->undo, game->temporary); + game->undo_available = TRUE; + + uip_assign_pronouns (game, command); + } + } + sc_free (filtered); + sc_free (replaced); + + /* + * If do_again is set, we'll come round with the prior command in line + * element in a moment, so save nothing for that case. Otherwise save the + * command in the history. + */ + if (!sc_strempty (line_element) && !game->do_again) + { + /* + * If this is a failed redo, redo_sequence will be set but do_again will + * be clear. Suppress the save for this special case; otherwise, failed + * redo commands get into the history, where they can cause problems + * later on. + */ + if (game->redo_sequence == 0) + { + sc_int timestamp; + + timestamp = var_get_elapsed_seconds (vars); + memo_save_command (memento, line_element, timestamp, game->turns); + } + else + game->redo_sequence = 0; + } + + /* + * Special case restart and restore commands; throw out any remaining input + * and return straight away. Do the same if this was an undo, detected by + * noting that undo is no longer available, where it was on entry. + */ + if (game->do_restart || game->do_restore + || (was_undo_available && !game->undo_available)) + { + line_buffer[0] = NUL; + return status; + } + + /* If not empty, consider as saving for "again" calls and in the history. */ + if (!sc_strempty (line_element)) + { + /* + * Unless "again", note this line element as prior input. "Again" shows + * up as do_again set in the game, where it wasn't when we entered here. + */ + if (!game->do_again && !is_rerunning) + strcpy (prior_element, line_element); + + /* + * If this was a request to run a command from the history, copy that + * command into the prior_element for the next iteration. The library + * should have verified the value in redo_sequence, so fetching the + * command string should not fail. + */ + if (game->do_again && game->redo_sequence != 0) + { + const sc_char *redo_command; + + redo_command = memo_find_command (memento, game->redo_sequence); + if (redo_command) + strcpy (prior_element, redo_command); + else + { + sc_error ("run_player_input: invalid redo sequence request\n"); + game->do_again = FALSE; + } + game->redo_sequence = 0; + } + } + + return status; +} + + +/* + * run_main_loop() + * + * Main interpreter loop. + */ +static void +run_main_loop (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + + /* + * This may not be the very first time this game has been used, for example + * saving a game right at the start, or undo-ing back to the start through + * memos. Caught by looking to see if the player room is marked as seen. + */ + if (!gs_room_seen (game, gs_playerroom (game))) + { + sc_vartype_t vt_key[2]; + const sc_char *gamename, *startuptext; + sc_bool disp_first_room, battle_system; + + /* If battle system and no debugger display a warning. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "BattleSystem"; + battle_system = prop_get_boolean (bundle, "B<-ss", vt_key); + if (battle_system && !debug_get_enabled (game)) + { + if_print_tag (SC_TAG_CLS, ""); + lib_warn_battle_system (); + } + + /* Initial clear screen. */ + pf_buffer_tag (filter, SC_TAG_CLS); + + /* Print the game name. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "GameName"; + gamename = prop_get_string (bundle, "S<-ss", vt_key); + pf_buffer_string (filter, gamename); + pf_buffer_character (filter, '\n'); + + /* Print the game header. */ + vt_key[0].string = "Header"; + vt_key[1].string = "StartupText"; + startuptext = prop_get_string (bundle, "S<-ss", vt_key); + pf_buffer_string (filter, startuptext); + pf_buffer_character (filter, '\n'); + + /* If flagged, describe the initial room. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "DispFirstRoom"; + disp_first_room = prop_get_boolean (bundle, "B<-ss", vt_key); + if (disp_first_room) + lib_cmd_look (game); + + /* Handle any introductory resources. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "IntroRes"; + res_handle_resource (game, "ss", vt_key); + + /* Set initial values for NPC and object states. */ + npc_setup_initial (game); + obj_setup_initial (game); + + /* Nudge events and NPCs. */ + evt_tick_events (game); + npc_tick_npcs (game); + + /* + * Notify the debugger that the game has started. This is a chance to + * set watchpoints to catch game startup actions. Done before setting + * the initial room visited as this is how the debugger differentiates + * restarts from restore or undo back to game start. + */ + debug_game_started (game); + + /* Note the initial room as visited. */ + gs_set_room_seen (game, gs_playerroom (game), TRUE); + } + else + { + /* Notify the debugger that the game has restarted. */ + debug_game_started (game); + } + + /* + * Game loop, exits either when a command parser handler sets the game + * running flag to FALSE, or by call to run_quit(). + */ + while (game->is_running) + { + sc_bool status; + + /* + * Synchronize any resources in use; do this before flushing so that any + * appropriate graphics/sound appear before waits or waitkey tag delays + * invoked by flushing the printfilter. Also, print any score change + * notifications. + */ + res_sync_resources (game); + run_notify_score_change (game); + + /* + * Flush printfilter of any accumulated output, and clear any prior + * notion of administrative commands from input. + */ + pf_flush (filter, vars, bundle); + game->is_admin = FALSE; + + /* If waitcounter is zero, accept and try a command. */ + if (game->waitcounter == 0) + { + /* Not waiting, so handle a player input line. */ + run_update_status (game); + status = run_player_input (game); + + /* + * If waitcounter is now set, decrement it, as this turn counts as + * one of them. + */ + if (game->waitcounter > 0) + game->waitcounter--; + } + else + { + /* + * Currently "waiting"; decrement wait turns, then run a turn having + * taken no input. + */ + game->waitcounter--; + status = TRUE; + } + + /* + * Do usual turn stuff unless either something stopped the game, or the + * last command didn't match, or the last command did match but was + * administrative. + */ + if (status && !game->is_admin) + { + /* Increment turn counter, and clear notifications done flag. */ + game->turns++; + game->has_notified = FALSE; + + if (game->is_running) + { + /* Nudge events and NPCs. */ + evt_tick_events (game); + npc_tick_npcs (game); + + /* Update NPC and object states. */ + npc_turn_update (game); + obj_turn_update (game); + + /* Note the current room as visited. */ + gs_set_room_seen (game, gs_playerroom (game), TRUE); + + /* Give the debugger a chance to catch watchpoints. */ + debug_turn_update (game); + } + } + } + + /* + * Final status update, for games that vary it on completion, then notify + * the debugger that the game has ended, to let it make a last watchpoint + * scan and offer the dialog if appropriate. + */ + run_update_status (game); + debug_game_ended (game); + + /* + * Final resource sync, score change notification and printfilter flush + * on game-instigated loop exit. + */ + res_sync_resources (game); + run_notify_score_change (game); + pf_flush (filter, vars, bundle); + + /* + * Reset static variables inside run_player_input() with a call to it with + * is_running false; this is a special case. + */ + assert (!game->is_running); + run_player_input (game); +} + + +/* + * run_create() + * + * Create a game context from a callback. + */ +sc_gameref_t +run_create (sc_read_callbackref_t callback, void *opaque) +{ + sc_tafref_t taf; + sc_prop_setref_t bundle; + sc_var_setref_t vars, temporary_vars, undo_vars; + sc_filterref_t filter; + sc_gameref_t game, temporary_game, undo_game; + assert (callback); + + /* Create a new TAF using the callback; return NULL if this fails. */ + taf = taf_create (callback, opaque); + if (!taf) + return NULL; + else if (if_get_trace_flag (SC_DUMP_TAF)) + taf_debug_dump (taf); + + /* Create a properties bundle, and parse the TAF data into it. */ + bundle = prop_create (taf); + if (!bundle) + { + sc_error ("run_create: error parsing game data\n"); + taf_destroy (taf); + return NULL; + } + else if (if_get_trace_flag (SC_DUMP_PROPERTIES)) + prop_debug_dump (bundle); + + /* Try to set an interpreter locale from the properties bundle. */ + loc_detect_game_locale (bundle); + if (if_get_trace_flag (SC_DUMP_LOCALE_TABLES)) + loc_debug_dump (); + + /* Create a set of variables from the bundle. */ + vars = var_create (bundle); + if (if_get_trace_flag (SC_DUMP_VARIABLES)) + var_debug_dump (vars); + + /* Create a printfilter for the game. */ + filter = pf_create (); + + /* + * Create an initial game state, and register it with variables. Also, + * create undo buffers, and initialize them in the same way. + */ + game = gs_create (vars, bundle, filter); + var_register_game (vars, game); + + temporary_vars = var_create (bundle); + temporary_game = gs_create (temporary_vars, bundle, filter); + var_register_game (temporary_vars, temporary_game); + + undo_vars = var_create (bundle); + undo_game = gs_create (undo_vars, bundle, filter); + var_register_game (undo_vars, undo_game); + + /* Add the undo buffers and memos to the game, and return it. */ + game->temporary = temporary_game; + game->undo = undo_game; + game->memento = memo_create (); + return game; +} + + +/* + * run_restart_handler() + * + * Return a game context to initial states to restart a game. + */ +static void +run_restart_handler (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_gameref_t new_game; + sc_var_setref_t new_vars; + + /* + * Create a fresh set of variables from the current game properties, + * then a new game using these variables and existing properties and + * printfilter. + */ + new_vars = var_create (bundle); + new_game = gs_create (new_vars, bundle, filter); + var_register_game (new_vars, new_game); + + /* + * Overwrite the dynamic parts of the current game with the new one. + */ + new_game->temporary = game->temporary; + new_game->undo = game->undo; + gs_copy (game, new_game); + + /* Destroy invalid game status strings. */ + sc_free (game->current_room_name); + game->current_room_name = NULL; + sc_free (game->status_line); + game->status_line = NULL; + + /* + * Now it's safely copied, destroy the temporary new game, and its + * associated variable set. + */ + gs_destroy (new_game); + var_destroy (new_vars); + + /* Reset resources handling. */ + res_cancel_resources (game); +} + + +/* + * run_restore_handler() + * + * Adjust a game context for continuation after restoring a game. + */ +static void +run_restore_handler (sc_gameref_t game) +{ + /* Invalidate the undo buffer. */ + game->undo_available = FALSE; + + /* + * Resources handling? Arguably we should re-offer resources active when + * the game was saved, but I can't see how this can be achieved with Adrift + * the way it is. Canceling is too broad, so I'll go here with just + * stopping sounds (in case looping). + * + * TODO Rationalize what happens here. + */ + game->stop_sound = TRUE; +} + + +/* + * run_quit_handler() + * + * Tidy up printfilter and input statics on game quit. + */ +static void +run_quit_handler (sc_gameref_t game) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + + /* Flush printfilter and notifications of any dangling output. */ + run_notify_score_change (game); + pf_flush (filter, vars, bundle); + + /* Cancel any active resources. */ + res_cancel_resources (game); + + /* + * Make the special call to reset all of the static variables inside + * run_player_input(). + */ + assert (!game->is_running); + run_player_input (game); +} + + +/* + * run_interpret() + * + * Intepret the game in a game context. + */ +void +run_interpret (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + /* Verify the game is not already running, and is runnable. */ + if (game->is_running) + { + sc_error ("run_interpret: game is already running\n"); + return; + } + if (game->has_completed) + { + sc_error ("run_interpret: game has already completed\n"); + return; + } + + /* Refuse to run a game with no rooms. */ + if (gs_room_count (game) == 0) + { + sc_error ("run_interpret: game contains no rooms\n"); + return; + } + + /* Run the main interpreter loop until no more restarts. */ + game->is_running = TRUE; + do + { + /* Run the game until some form of halt is requested. */ + if (setjmp (game->quitter) == 0) + run_main_loop (game); + + /* + * If the halt was a restart or restore, cancel the request, handle + * restart or restore game adjustments, and set the game running + * again. + */ + if (game->do_restart) + { + game->do_restart = FALSE; + run_restart_handler (game); + game->is_running = TRUE; + } + + if (game->do_restore) + { + game->do_restore = FALSE; + run_restore_handler (game); + game->is_running = TRUE; + } + } + while (game->is_running); + + /* Tidy up the printfilter and input statics. */ + run_quit_handler (game); +} + + +/* + * run_destroy() + * + * Destroy a game context, and free all resources. + */ +void +run_destroy (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + /* Can't destroy the context of a running game. */ + if (game->is_running) + { + sc_error ("run_destroy: game is running, stop it first\n"); + return; + } + + /* + * Cancel any game state debugger -- this frees its resources. Only the + * primary game may have acquired a debugger. + */ + debug_set_enabled (game, FALSE); + assert (!debug_get_enabled (game->temporary)); + assert (!debug_get_enabled (game->undo)); + + /* + * Destroy the game state, variables, properties bundle, memos, undo + * buffers and their variables, and filter. The bundle and printfilter + * are shared by the main game, the undo game, and the temporary game, so + * destroy these only once! The main game has a memento, but it is not + * visible to these other two games, neither of which have one. + */ + assert (gs_get_bundle (game->temporary) == gs_get_bundle (game)); + assert (gs_get_filter (game->temporary) == gs_get_filter (game)); + assert (gs_get_vars (game->temporary) != gs_get_vars (game)); + assert (!gs_get_memento (game->temporary)); + var_destroy (gs_get_vars (game->temporary)); + gs_destroy (game->temporary); + + assert (gs_get_bundle (game->undo) == gs_get_bundle (game)); + assert (gs_get_filter (game->undo) == gs_get_filter (game)); + assert (gs_get_vars (game->undo) != gs_get_vars (game)); + assert (!gs_get_memento (game->undo)); + var_destroy (gs_get_vars (game->undo)); + gs_destroy (game->undo); + + prop_destroy (gs_get_bundle (game)); + pf_destroy (gs_get_filter (game)); + var_destroy (gs_get_vars (game)); + memo_destroy (gs_get_memento (game)); + + gs_destroy (game); +} + + +/* + * run_quit() + * + * Quits a running game. This function calls a longjump to act as if + * run_main_loop() returned, and so never returns to its caller. + */ +void +run_quit (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + /* Disallow quitting a non-running game. */ + if (!game->is_running) + { + sc_error ("run_quit: game is not running\n"); + return; + } + + /* Exit the main loop with a longjump. */ + game->is_running = FALSE; + longjmp (game->quitter, 1); + sc_fatal ("run_quit: unable to quit cleanly\n"); +} + + +/* + * run_restart() + * + * Restarts either a running or a stopped game. For running games, this + * function calls a longjump to act as if run_main_loop() returned, and so + * never returns to its caller. For stopped games, it returns. + */ +void +run_restart (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + /* + * If the game is running, stop it, request a restart, and exit the main + * loop with a longjump. + */ + if (game->is_running) + { + game->is_running = FALSE; + game->do_restart = TRUE; + longjmp (game->quitter, 1); + sc_fatal ("run_restart: unable to restart cleanly\n"); + } + + /* Restart locally, and ensure that the game remains stopped. */ + run_restart_handler (game); + game->is_running = FALSE; +} + + +/* + * run_save() + * run_save_prompted() + * + * Saves either a running or a stopped game. + */ +void +run_save (sc_gameref_t game, sc_write_callbackref_t callback, void *opaque) +{ + assert (gs_is_game_valid (game)); + assert (callback); + + ser_save_game (game, callback, opaque); +} + +sc_bool +run_save_prompted (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + return ser_save_game_prompted (game); +} + + +/* + * run_restore_common() + * run_restore() + * run_restore_prompted() + * + * Restores either a running or a stopped game. For running games, on + * successful restore, these functions call a longjump to act as if + * run_main_loop() returned, and so never return to their caller. On failed + * restore, and for stopped games, they will return, with TRUE if successful, + * FALSE if restore failed. + */ +static sc_bool +run_restore_common (sc_gameref_t game, + sc_read_callbackref_t callback, void *opaque) +{ + sc_bool is_running, status; + + /* + * Save the game running flag, and call the restore appropriate for the + * caller. The indication of a call from run_restore_prompted() is a + * callback of NULL; callback cannot be NULL for run_restore() calls. + */ + is_running = game->is_running; + status = callback ? ser_load_game (game, callback, opaque) + : ser_load_game_prompted (game); + if (status) + { + /* Loading a game clears is_running -- restore it here. */ + game->is_running = is_running; + + /* + * If the game is (was) running, set flags so that the interpreter + * loop cycles, and exit the main loop with a longjump. + */ + if (game->is_running) + { + game->is_running = FALSE; + game->do_restore = TRUE; + longjmp (game->quitter, 1); + sc_fatal ("run_restore_common: unable to restart cleanly\n"); + } + } + + /* Return TRUE on successful restore of a stopped game, FALSE on error. */ + return status; +} + +sc_bool +run_restore (sc_gameref_t game, sc_read_callbackref_t callback, void *opaque) +{ + assert (gs_is_game_valid (game)); + assert (callback); + + return run_restore_common (game, callback, opaque); +} + +sc_bool +run_restore_prompted (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + return run_restore_common (game, NULL, NULL); +} + + +/* + * run_undo() + * + * Undo a turn in either a running or a stopped game. Returns TRUE on + * successful undo, FALSE if no undo buffer is available. + */ +sc_bool +run_undo (sc_gameref_t game) +{ + const sc_memo_setref_t memento = gs_get_memento (game); + sc_bool is_running; + assert (gs_is_game_valid (game)); + + /* Save the game's running state, so we can restore it later. */ + is_running = game->is_running; + + /* If there's an undo buffer available, restore it. */ + if (game->undo_available) + { + /* Restore the undo buffer, and then restore running flag. */ + gs_copy (game, game->undo); + game->undo_available = FALSE; + game->is_running = is_running; + + /* Location may have changed; update status. */ + run_update_status (game); + + /* Bring resources into line with the revised game. */ + res_sync_resources (game); + return TRUE; + } + + /* + * If there is no undo buffer, try to restore one saved previously in a + * memo. Handle as if restoring from a file. + */ + if (memo_load_game (memento, game)) + { + /* Loading a game clears is_running -- restore it here. */ + game->is_running = is_running; + + /* + * If the game is (was) running, set flags so that the interpreter + * loop cycles, and exit the main loop with a longjump. + */ + if (game->is_running) + { + game->is_running = FALSE; + game->do_restore = TRUE; + longjmp (game->quitter, 1); + sc_fatal ("run_undo: unable to restart cleanly\n"); + } + + /* Game undo on non-running game accomplished with memos. */ + return TRUE; + } + + /* No undo buffer and no memos available. */ + return FALSE; +} + + +/* + * run_is_running() + * + * Query the game running state. + */ +sc_bool +run_is_running (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + return game->is_running; +} + + +/* + * run_has_completed() + * + * Query the game completion state. Completed games cannot be resumed, + * since they've run the exit task and thus have nowhere to go. + */ +sc_bool +run_has_completed (sc_gameref_t game) +{ + assert (gs_is_game_valid (game)); + + return game->has_completed; +} + + +/* + * run_is_undo_available() + * + * Query the game turn undo buffer and memo availability. + */ +sc_bool +run_is_undo_available (sc_gameref_t game) +{ + const sc_memo_setref_t memento = gs_get_memento (game); + assert (gs_is_game_valid (game)); + + return game->undo_available || memo_is_load_available (memento); +} + + +/* + * run_get_attributes() + * run_set_attributes() + * + * Get and set selected game attributes. + */ +void +run_get_attributes (sc_gameref_t game, + const sc_char **game_name, const sc_char **game_author, + const sc_char **game_compile_date, + sc_int *turns, sc_int *score, sc_int *max_score, + const sc_char **current_room_name, + const sc_char **status_line, const sc_char **preferred_font, + sc_bool *bold_room_names, sc_bool *verbose, + sc_bool *notify_score_change) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[2]; + assert (gs_is_game_valid (game)); + + /* Return the game name, author, and compile date if requested. */ + if (game_name) + { + if (!game->title) + { + const sc_char *gamename; + sc_char *filtered; + + vt_key[0].string = "Globals"; + vt_key[1].string = "GameName"; + gamename = prop_get_string (bundle, "S<-ss", vt_key); + + filtered = pf_filter_for_info (gamename, vars); + pf_strip_tags (filtered); + game->title = filtered; + } + *game_name = game->title; + } + if (game_author) + { + if (!game->author) + { + const sc_char *gameauthor; + sc_char *filtered; + + vt_key[0].string = "Globals"; + vt_key[1].string = "GameAuthor"; + gameauthor = prop_get_string (bundle, "S<-ss", vt_key); + + filtered = pf_filter_for_info (gameauthor, vars); + pf_strip_tags (filtered); + game->author = filtered; + } + *game_author = game->author; + } + if (game_compile_date) + { + vt_key[0].string = "CompileDate"; + *game_compile_date = prop_get_string (bundle, "S<-s", vt_key); + } + + /* Return the current room name and status line if requested. */ + if (current_room_name) + *current_room_name = game->current_room_name; + if (status_line) + *status_line = game->status_line; + + /* Return any game preferred font, or NULL if none. */ + if (preferred_font) + { + vt_key[0].string = "CustomFont"; + if (prop_get_boolean (bundle, "B<-s", vt_key)) + { + vt_key[0].string = "FontNameSize"; + *preferred_font = prop_get_string (bundle, "S<-s", vt_key); + } + else + *preferred_font = NULL; + } + + /* Return any other selected game attributes. */ + if (turns) + *turns = game->turns; + if (score) + *score = game->score; + if (max_score) + { + vt_key[0].string = "Globals"; + vt_key[1].string = "MaxScore"; + *max_score = prop_get_integer (bundle, "I<-ss", vt_key); + } + if (bold_room_names) + *bold_room_names = game->bold_room_names; + if (verbose) + *verbose = game->verbose; + if (notify_score_change) + *notify_score_change = game->notify_score_change; +} + +void +run_set_attributes (sc_gameref_t game, + sc_bool bold_room_names, sc_bool verbose, + sc_bool notify_score_change) +{ + assert (gs_is_game_valid (game)); + + /* Set game options. */ + game->bold_room_names = bold_room_names; + game->verbose = verbose; + game->notify_score_change = notify_score_change; +} + + +/* + * run_hint_iterate() + * + * Return the next hint appropriate to the game state, or the first if + * hint is NULL. Returns NULL if none, or no more hints. This function + * works with pointers to a task state rather than task indexes so that + * the token passed in and out is a pointer, and readily made opaque to + * the client as a void*. + */ +sc_hintref_t +run_hint_iterate (sc_gameref_t game, sc_hintref_t hint) +{ + sc_int task; + assert (gs_is_game_valid (game)); + + /* + * Hint is a pointer to a task state; convert to a task index, adding one + * to move on to the next task, or start at the first task if null. + */ + if (!hint) + task = 0; + else + { + /* Convert into pointer, and range check. */ + task = hint - game->tasks; + if (task < 0 || task >= gs_task_count (game)) + { + sc_error ("run_hint_iterate: invalid iteration hint\n"); + return NULL; + } + + /* Advance beyond current task. */ + task++; + } + + /* Scan for the next runnable task that offers a hint. */ + for (; task < gs_task_count (game); task++) + { + if (task_can_run_task (game, task) && task_has_hints (game, task)) + break; + } + + /* Return a pointer to the state of the task identified, or NULL. */ + return task < gs_task_count (game) ? game->tasks + task : NULL; +} + + +/* + * run_get_hint_common() + * run_get_hint_question() + * run_get_subtle_hint() + * run_get_unsubtle_hint() + * + * Return the strings for a hint. Front-ends to task functions. Each + * converts the hint "address" to a task index through pointer arithmetic, + * then filters it and returns a temporary, valid only until the next hint + * call. + * + * Hint strings are NULL if empty (not defined by the game). + */ +static const sc_char * +run_get_hint_common (sc_gameref_t game, sc_hintref_t hint, + const sc_char *(*handler) (sc_gameref_t, sc_int)) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_int task; + const sc_char *string; + assert (gs_is_game_valid (game)); + + /* Verify the caller passed in a valid hint. */ + task = hint - game->tasks; + if (task < 0 || task >= gs_task_count (game)) + { + sc_error ("run_get_hint_common: invalid iteration hint\n"); + return NULL; + } + else if (!task_has_hints (game, task)) + { + sc_error ("run_get_hint_common: task has no hint\n"); + return NULL; + } + + /* Get the required game text by calling the given handler function. */ + string = handler (game, task); + if (!sc_strempty (string)) + { + sc_char *filtered; + + /* Filter and strip tags, note in game. */ + filtered = pf_filter (string, vars, bundle); + pf_strip_tags_for_hints (filtered); + sc_free (game->hint_text); + game->hint_text = filtered; + } + else + { + /* Hint text is empty; drop any text noted in game. */ + sc_free (game->hint_text); + game->hint_text = NULL; + } + + return game->hint_text; +} + +const sc_char * +run_get_hint_question (sc_gameref_t game, sc_hintref_t hint) +{ + return run_get_hint_common (game, hint, task_get_hint_question); +} + +const sc_char * +run_get_subtle_hint (sc_gameref_t game, sc_hintref_t hint) +{ + return run_get_hint_common (game, hint, task_get_hint_subtle); +} + +const sc_char * +run_get_unsubtle_hint (sc_gameref_t game, sc_hintref_t hint) +{ + return run_get_hint_common (game, hint, task_get_hint_unsubtle); +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scserial.cpp b/engines/glk/adrift/scserial.cpp new file mode 100644 index 0000000000..3ade8ac124 --- /dev/null +++ b/engines/glk/adrift/scserial.cpp @@ -0,0 +1,826 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" +#include "common/textconsole.h" + +namespace Glk { +namespace Adrift { + +/* Assorted definitions and constants. */ +static const sc_char NEWLINE = '\n'; +static const sc_char CARRIAGE_RETURN = '\r'; +static const sc_char NUL = '\0'; + +enum { BUFFER_SIZE = 4096 }; + +/* Output buffer. */ +static sc_byte *ser_buffer = NULL; +static sc_int ser_buffer_length = 0; + +/* Callback and opaque pointer for use by output functions. */ +static sc_write_callbackref_t ser_callback = NULL; +static void *ser_opaque = NULL; + + +/* + * ser_flush() + * ser_buffer_character() + * + * Flush pending buffer contents; add a character to the buffer. + */ +static void ser_flush (sc_bool is_final) +{ + error("TODO"); +#ifdef TODO + static sc_bool initialized = FALSE; + static sc_byte *out_buffer = NULL; + static sc_int out_buffer_size = 0; + static z_stream stream; + + sc_int status; + + /* If this is an initial call, initialize deflation. */ + if (!initialized) + { + /* Allocate an initial output buffer. */ + out_buffer_size = BUFFER_SIZE; + out_buffer = sc_malloc (out_buffer_size); + + /* Initialize Zlib deflation functions. */ + stream.next_out = out_buffer; + stream.avail_out = out_buffer_size; + stream.next_in = ser_buffer; + stream.avail_in = 0; + + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + + status = deflateInit (&stream, Z_DEFAULT_COMPRESSION); + if (status != Z_OK) + { + sc_error ("ser_flush: deflateInit: error %ld\n", status); + ser_buffer_length = 0; + + sc_free (out_buffer); + out_buffer = NULL; + out_buffer_size = 0; + return; + } + + initialized = TRUE; + } + + /* Deflate data from the current output buffer. */ + stream.next_in = ser_buffer; + stream.avail_in = ser_buffer_length; + + /* Loop while deflate output is pending and buffer not emptied. */ + while (TRUE) + { + sc_int in_bytes, out_bytes; + + /* Compress stream data, with finish if this is the final flush. */ + if (is_final) + status = deflate (&stream, Z_FINISH); + else + status = deflate (&stream, Z_NO_FLUSH); + if (status != Z_STREAM_END && status != Z_OK) + { + sc_error ("ser_flush: deflate: error %ld\n", status); + ser_buffer_length = 0; + + sc_free (out_buffer); + out_buffer = NULL; + out_buffer_size = 0; + initialized = FALSE; + return; + } + + /* Calculate bytes used, and output. */ + in_bytes = ser_buffer_length - stream.avail_in; + out_bytes = out_buffer_size - stream.avail_out; + + /* See if compressed data is available. */ + if (out_bytes > 0) + { + /* Write it to save file output through the callback. */ + ser_callback (ser_opaque, out_buffer, out_bytes); + + /* Reset deflation stream for available space. */ + stream.next_out = out_buffer; + stream.avail_out = out_buffer_size; + } + + /* Remove consumed data from the input buffer. */ + if (in_bytes > 0) + { + /* Move any unused data, and reduce length. */ + memmove (ser_buffer, + ser_buffer + in_bytes, ser_buffer_length - in_bytes); + ser_buffer_length -= in_bytes; + + /* Reset deflation stream for consumed data. */ + stream.next_in = ser_buffer; + stream.avail_in = ser_buffer_length; + } + + /* If final flush, wait until deflate indicates finished. */ + if (is_final && status == Z_OK) + continue; + + /* If data was consumed or produced, break. */ + if (out_bytes > 0 || in_bytes > 0) + break; + } + + /* If this was a final call, clean up. */ + if (is_final) + { + /* Compression completed. */ + status = deflateEnd (&stream); + if (status != Z_OK) + sc_error ("ser_flush: warning: deflateEnd: error %ld\n", status); + + if (ser_buffer_length != 0) + { + sc_error ("ser_flush: warning: deflate missed data\n"); + ser_buffer_length = 0; + } + + /* Free the allocated compression buffer. */ + sc_free (ser_buffer); + ser_buffer = NULL; + + /* + * Free output buffer, and reset flag for reinitialization on the next + * call. + */ + sc_free (out_buffer); + out_buffer = NULL; + out_buffer_size = 0; + initialized = FALSE; + } +#endif +} + +static void +ser_buffer_character (sc_char character) +{ + /* Allocate the buffer if not yet done. */ + if (!ser_buffer) + { + assert (ser_buffer_length == 0); + ser_buffer = (sc_byte *)sc_malloc (BUFFER_SIZE); + } + + /* Add to the buffer, with intermediate flush if filled. */ + ser_buffer[ser_buffer_length++] = character; + if (ser_buffer_length == BUFFER_SIZE) + ser_flush (FALSE); +} + + +/* + * ser_buffer_buffer() + * ser_buffer_string() + * ser_buffer_int() + * ser_buffer_int_special() + * ser_buffer_uint() + * ser_buffer_boolean() + * + * Buffer a buffer, a string, an unsigned and signed integer, and a boolean. + */ +static void +ser_buffer_buffer (const sc_char *buffer, sc_int length) +{ + sc_int index_; + + /* Add each character to the buffer. */ + for (index_ = 0; index_ < length; index_++) + ser_buffer_character (buffer[index_]); +} + +static void +ser_buffer_string (const sc_char *string) +{ + /* Buffer string, followed by DOS style end-of-line. */ + ser_buffer_buffer (string, strlen (string)); + ser_buffer_character (CARRIAGE_RETURN); + ser_buffer_character (NEWLINE); +} + +static void +ser_buffer_int (sc_int value) +{ + sc_char buffer[32]; + + /* Convert to a string and buffer that. */ + sprintf (buffer, "%ld", value); + ser_buffer_string (buffer); +} + +static void +ser_buffer_int_special (sc_int value) +{ + sc_char buffer[32]; + + /* Weirdo formatting for compatibility. */ + sprintf (buffer, "% ld ", value); + ser_buffer_string (buffer); +} + +static void +ser_buffer_uint (sc_uint value) +{ + sc_char buffer[32]; + + /* Convert to a string and buffer that. */ + sprintf (buffer, "%lu", value); + ser_buffer_string (buffer); +} + +static void +ser_buffer_boolean (sc_bool boolean) +{ + /* Write a 1 for TRUE, 0 for FALSE. */ + ser_buffer_string (boolean ? "1" : "0"); +} + + +/* + * ser_save_game() + * + * Serialize a game and save its state using the given callback and opaque. + */ +void +ser_save_game (sc_gameref_t game, + sc_write_callbackref_t callback, void *opaque) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int index_, var_count; + assert (callback); + + /* Store the callback and opaque references, for writer functions. */ + ser_callback = callback; + ser_opaque = opaque; + + /* Write the game name. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "GameName"; + ser_buffer_string (prop_get_string (bundle, "S<-ss", vt_key)); + + /* Write the counts of rooms, objects, etc. */ + ser_buffer_int (gs_room_count (game)); + ser_buffer_int (gs_object_count (game)); + ser_buffer_int (gs_task_count (game)); + ser_buffer_int (gs_event_count (game)); + ser_buffer_int (gs_npc_count (game)); + + /* Write the score and player information. */ + ser_buffer_int (game->score); + ser_buffer_int (gs_playerroom (game) + 1); + ser_buffer_int (gs_playerparent (game)); + ser_buffer_int (gs_playerposition (game)); + + /* Write player gender. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "PlayerGender"; + ser_buffer_int (prop_get_integer (bundle, "I<-ss", vt_key)); + + /* + * Write encumbrance details. The player limits are constant for a given + * game, and can be extracted from properties. The current sizes and + * weights can also be recalculated from held objects, so we don't maintain + * them in the game. We can write constants here, then, and ignore + * the values on restoring. Note however that if the Adrift Runner is + * relying on these values, this may give it problems with one of our saved + * games. + */ + ser_buffer_int (90); + ser_buffer_int (0); + ser_buffer_int (90); + ser_buffer_int (0); + + /* Save rooms information. */ + for (index_ = 0; index_ < gs_room_count (game); index_++) + ser_buffer_boolean (gs_room_seen (game, index_)); + + /* Save objects information. */ + for (index_ = 0; index_ < gs_object_count (game); index_++) + { + ser_buffer_int (gs_object_position (game, index_)); + ser_buffer_boolean (gs_object_seen (game, index_)); + ser_buffer_int (gs_object_parent (game, index_)); + if (gs_object_openness (game, index_) != 0) + ser_buffer_int (gs_object_openness (game, index_)); + + if (gs_object_state (game, index_) != 0) + ser_buffer_int (gs_object_state (game, index_)); + + ser_buffer_boolean (gs_object_unmoved (game, index_)); + } + + /* Save tasks information. */ + for (index_ = 0; index_ < gs_task_count (game); index_++) + { + ser_buffer_boolean (gs_task_done (game, index_)); + ser_buffer_boolean (gs_task_scored (game, index_)); + } + + /* Save events information. */ + for (index_ = 0; index_ < gs_event_count (game); index_++) + { + sc_int startertype, task; + + /* Get starter task, if any. */ + vt_key[0].string = "Events"; + vt_key[1].integer = index_; + vt_key[2].string = "StarterType"; + startertype = prop_get_integer (bundle, "I<-sis", vt_key); + if (startertype == 3) + { + vt_key[2].string = "TaskNum"; + task = prop_get_integer (bundle, "I<-sis", vt_key); + } + else + task = 0; + + /* Save event details. */ + ser_buffer_int (gs_event_time (game, index_)); + ser_buffer_int (task); + ser_buffer_int (gs_event_state (game, index_) - 1); + if (task > 0) + ser_buffer_boolean (gs_task_done (game, task - 1)); + else + ser_buffer_boolean (FALSE); + } + + /* Save NPCs information. */ + for (index_ = 0; index_ < gs_npc_count (game); index_++) + { + sc_int walk; + + ser_buffer_int (gs_npc_location (game, index_)); + ser_buffer_boolean (gs_npc_seen (game, index_)); + for (walk = 0; walk < gs_npc_walkstep_count (game, index_); walk++) + ser_buffer_int_special (gs_npc_walkstep (game, index_, walk)); + } + + /* Save each variable. */ + vt_key[0].string = "Variables"; + var_count = prop_get_child_count (bundle, "I<-s", vt_key); + + for (index_ = 0; index_ < var_count; index_++) + { + const sc_char *name; + sc_int var_type; + + vt_key[1].integer = index_; + + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Type"; + var_type = prop_get_integer (bundle, "I<-sis", vt_key); + + switch (var_type) + { + case TAFVAR_NUMERIC: + ser_buffer_int (var_get_integer (vars, name)); + break; + + case TAFVAR_STRING: + ser_buffer_string (var_get_string (vars, name)); + break; + + default: + sc_fatal ("ser_save_game: unknown variable type, %ld\n", var_type); + } + } + + /* Save timing information. */ + ser_buffer_uint (var_get_elapsed_seconds (vars)); + + /* Save turns count. */ + ser_buffer_uint ((sc_uint) game->turns); + + /* + * Flush the last buffer contents, and drop the callback and opaque + * references. + */ + ser_flush (TRUE); + ser_callback = NULL; + ser_opaque = NULL; +} + + +/* + * ser_save_game_prompted() + * + * Serialize a game and save its state, requesting a save stream from + * the user. + */ +sc_bool +ser_save_game_prompted (sc_gameref_t game) +{ + void *opaque; + + /* + * Open an output stream, and if successful, save a game using the opaque + * value returned. + */ + opaque = if_open_saved_game (TRUE); + if (opaque) + { + ser_save_game (game, if_write_saved_game, opaque); + if_close_saved_game (opaque); + return TRUE; + } + + return FALSE; +} + + +/* TAS input file line counter. */ +static sc_tafref_t ser_tas = NULL; +static sc_int ser_tasline = 0; + +/* Restore error jump buffer. */ +static jmp_buf ser_tas_error; + +/* + * ser_get_string() + * ser_get_int() + * ser_get_uint() + * ser_get_boolean() + * + * Wrapper round obtaining the next TAS file line, with variants to convert + * the line content into an appropriate type. + */ +static const sc_char * +ser_get_string (void) +{ + const sc_char *string; + + /* Get the next line, and complain if absent. */ + string = taf_next_line (ser_tas); + if (!string) + { + sc_error ("ser_get_string: out of TAS data at line %ld\n", ser_tasline); + longjmp (ser_tas_error, 1); + } + + ser_tasline++; + return string; +} + +static sc_int +ser_get_int (void) +{ + const sc_char *string; + sc_int value; + + /* Get line, and scan for a single integer; return it. */ + string = ser_get_string (); + if (sscanf (string, "%ld", &value) != 1) + { + sc_error ("ser_get_int:" + " invalid integer at line %ld\n", ser_tasline - 1); + longjmp (ser_tas_error, 1); + } + + return value; +} + +static sc_uint +ser_get_uint (void) +{ + const sc_char *string; + sc_uint value; + + /* Get line, and scan for a single integer; return it. */ + string = ser_get_string (); + if (sscanf (string, "%lu", &value) != 1) + { + sc_error ("ser_get_uint:" + " invalid integer at line %ld\n", ser_tasline - 1); + longjmp (ser_tas_error, 1); + } + + return value; +} + +static sc_bool +ser_get_boolean (void) +{ + const sc_char *string; + sc_uint value; + + /* + * Get line, and scan for a single integer; check it's a valid-looking flag, + * and return it. + */ + string = ser_get_string (); + if (sscanf (string, "%lu", &value) != 1) + { + sc_error ("ser_get_boolean:" + " invalid boolean at line %ld\n", ser_tasline - 1); + longjmp (ser_tas_error, 1); + } + if (value != 0 && value != 1) + { + sc_error ("ser_get_boolean:" + " warning: suspect boolean at line %ld\n", ser_tasline - 1); + } + + return value != 0; +} + + +/* + * ser_load_game() + * + * Load a serialized game into the given game by repeated calls to the + * callback() function. + */ +sc_bool +ser_load_game (sc_gameref_t game, + sc_read_callbackref_t callback, void *opaque) +{ + static sc_var_setref_t new_vars; /* For setjmp safety */ + static sc_gameref_t new_game; /* For setjmp safety */ + + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int index_, var_count; + const sc_char *gamename; + + /* Create a TAF (TAS) reference from callbacks, for reader functions. */ + ser_tas = taf_create_tas (callback, opaque); + if (!ser_tas) + return FALSE; + + /* Reset line counter for error messages. */ + ser_tasline = 1; + + new_game = NULL; + new_vars = NULL; + + /* Set up error handling jump buffer, and handle errors. */ + if (setjmp (ser_tas_error) != 0) + { + /* Destroy any temporary game and variables. */ + if (new_game) + gs_destroy (new_game); + if (new_vars) + var_destroy (new_vars); + + /* Destroy the TAF (TAS) file and return fail status. */ + taf_destroy (ser_tas); + ser_tas = NULL; + return FALSE; + } + + /* + * Read the game name, and compare with the one in the game. Fail if + * they don't match exactly. A tighter check than this would perhaps be + * preferable, say, something based on the TAF file header, but this isn't + * in the save file format. + */ + vt_key[0].string = "Globals"; + vt_key[1].string = "GameName"; + gamename = prop_get_string (bundle, "S<-ss", vt_key); + if (strcmp (ser_get_string (), gamename) != 0) + longjmp (ser_tas_error, 1); + + /* Read and verify the counts in the saved game. */ + if (ser_get_int () != gs_room_count (game) + || ser_get_int () != gs_object_count (game) + || ser_get_int () != gs_task_count (game) + || ser_get_int () != gs_event_count (game) + || ser_get_int () != gs_npc_count (game)) + longjmp (ser_tas_error, 1); + + /* Create a variables set and game to restore into. */ + new_vars = var_create (bundle); + new_game = gs_create (new_vars, bundle, filter); + var_register_game (new_vars, new_game); + + /* All set to load TAF (TAS) data into the new game. */ + + /* Restore the score and player information. */ + new_game->score = ser_get_int (); + gs_set_playerroom (new_game, ser_get_int () - 1); + gs_set_playerparent (new_game, ser_get_int ()); + gs_set_playerposition (new_game, ser_get_int ()); + + /* Skip player gender. */ + (void) ser_get_int (); + + /* Skip encumbrance details, not currently maintained by the game. */ + (void) ser_get_int (); + (void) ser_get_int (); + (void) ser_get_int (); + (void) ser_get_int (); + + /* Restore rooms information. */ + for (index_ = 0; index_ < gs_room_count (new_game); index_++) + gs_set_room_seen (new_game, index_, ser_get_boolean ()); + + /* Restore objects information. */ + for (index_ = 0; index_ < gs_object_count (new_game); index_++) + { + sc_int openable, currentstate; + + /* Bypass mutators for position and parent. Fix later? */ + new_game->objects[index_].position = ser_get_int (); + gs_set_object_seen (new_game, index_, ser_get_boolean ()); + new_game->objects[index_].parent = ser_get_int (); + + vt_key[0].string = "Objects"; + vt_key[1].integer = index_; + vt_key[2].string = "Openable"; + openable = prop_get_integer (bundle, "I<-sis", vt_key); + gs_set_object_openness (new_game, index_, + openable != 0 ? ser_get_int () : 0); + + vt_key[2].string = "CurrentState"; + currentstate = prop_get_integer (bundle, "I<-sis", vt_key); + gs_set_object_state (new_game, index_, + currentstate != 0 ? ser_get_int () : 0); + + gs_set_object_unmoved (new_game, index_, ser_get_boolean ()); + } + + /* Restore tasks information. */ + for (index_ = 0; index_ < gs_task_count (new_game); index_++) + { + gs_set_task_done (new_game, index_, ser_get_boolean ()); + gs_set_task_scored (new_game, index_, ser_get_boolean ()); + } + + /* Restore events information. */ + for (index_ = 0; index_ < gs_event_count (new_game); index_++) + { + sc_int startertype, task; + + /* Restore first event details. */ + gs_set_event_time (new_game, index_, ser_get_int ()); + task = ser_get_int (); + gs_set_event_state (new_game, index_, ser_get_int () + 1); + + /* Verify and restore the starter task, if any. */ + if (task > 0) + { + vt_key[0].string = "Events"; + vt_key[1].integer = index_; + vt_key[2].string = "StarterType"; + startertype = prop_get_integer (bundle, "I<-sis", vt_key); + if (startertype != 3) + longjmp (ser_tas_error, 1); + + /* Restore task state. */ + gs_set_task_done (new_game, task - 1, ser_get_boolean ()); + } + else + (void) ser_get_boolean (); + } + + /* Restore NPCs information. */ + for (index_ = 0; index_ < gs_npc_count (new_game); index_++) + { + sc_int walk; + + gs_set_npc_location (new_game, index_, ser_get_int ()); + gs_set_npc_seen (new_game, index_, ser_get_boolean ()); + for (walk = 0; walk < gs_npc_walkstep_count (new_game, index_); walk++) + gs_set_npc_walkstep (new_game, index_, walk, ser_get_int ()); + } + + /* Restore each variable. */ + vt_key[0].string = "Variables"; + var_count = prop_get_child_count (bundle, "I<-s", vt_key); + + for (index_ = 0; index_ < var_count; index_++) + { + const sc_char *name; + sc_int var_type; + + vt_key[1].integer = index_; + + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Type"; + var_type = prop_get_integer (bundle, "I<-sis", vt_key); + + switch (var_type) + { + case TAFVAR_NUMERIC: + var_put_integer (new_vars, name, ser_get_int ()); + break; + + case TAFVAR_STRING: + var_put_string (new_vars, name, ser_get_string ()); + break; + + default: + sc_fatal ("ser_load_game: unknown variable type, %ld\n", var_type); + } + } + + /* Restore timing information. */ + var_set_elapsed_seconds (new_vars, ser_get_uint ()); + + /* Restore turns count. */ + new_game->turns = (sc_int) ser_get_uint (); + + /* + * Resources tweak -- set requested to match those in the current game + * so that they remain unchanged by the gs_copy() of new_game onto + * game. This way, both the requested and the active resources in the + * game are unchanged by restore. + */ + new_game->requested_sound = game->requested_sound; + new_game->requested_graphic = game->requested_graphic; + + /* + * Quitter tweak -- set the quit jump buffer in the new game to be the + * same as the current one, so that it remains unchanged by gs_copy(). The + * one in the new game is still the unset one from gs_create(). + */ + memcpy (&new_game->quitter, &game->quitter, sizeof (game->quitter)); + + /* + * If we got this far, we successfully restored the game from the file. + * As our final act, copy the new game onto the old one. + */ + new_game->temporary = game->temporary; + new_game->undo = game->undo; + gs_copy (game, new_game); + + /* Done with the temporary game and variables. */ + gs_destroy (new_game); + var_destroy (new_vars); + + /* Done with TAF (TAS) file; destroy it and return successfully. */ + taf_destroy (ser_tas); + ser_tas = NULL; + return TRUE; +} + + +/* + * ser_load_game_prompted() + * + * Load a serialized game into the given game, requesting a restore + * stream from the user. + */ +sc_bool +ser_load_game_prompted (sc_gameref_t game) +{ + void *opaque; + + /* + * Open an input stream, and if successful, try to load a game using + * the opaque value returned and the saved game callback. + */ + opaque = if_open_saved_game (FALSE); + if (opaque) + { + sc_bool status; + + status = ser_load_game (game, if_read_saved_game, opaque); + if_close_saved_game (opaque); + return status; + } + + return FALSE; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sctaffil.cpp b/engines/glk/adrift/sctaffil.cpp new file mode 100644 index 0000000000..6f0ebae171 --- /dev/null +++ b/engines/glk/adrift/sctaffil.cpp @@ -0,0 +1,860 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "common/textconsole.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Put integer and boolean read functions in here? + */ + +/* Assorted definitions and constants. */ +static const sc_uint TAF_MAGIC = 0x5bdcfa41; +enum +{ VERSION_HEADER_SIZE = 14, + V400_HEADER_EXTRA = 8 +}; +enum +{ OUT_BUFFER_SIZE = 31744, + IN_BUFFER_SIZE = 16384, + GROW_INCREMENT = 8 +}; +static const sc_char NEWLINE = '\n'; +static const sc_char CARRIAGE_RETURN = '\r'; +static const sc_char NUL = '\0'; + +/* Version 4.0, version 3.9, and version 3.8 TAF file signatures. */ +static const sc_byte + V400_SIGNATURE[VERSION_HEADER_SIZE] = {0x3c, 0x42, 0x3f, 0xc9, 0x6a, 0x87, + 0xc2, 0xcf, 0x93, 0x45, 0x3e, 0x61, + 0x39, 0xfa}; +static const sc_byte + V390_SIGNATURE[VERSION_HEADER_SIZE] = {0x3c, 0x42, 0x3f, 0xc9, 0x6a, 0x87, + 0xc2, 0xcf, 0x94, 0x45, 0x37, 0x61, + 0x39, 0xfa}; +static const sc_byte + V380_SIGNATURE[VERSION_HEADER_SIZE] = {0x3c, 0x42, 0x3f, 0xc9, 0x6a, 0x87, + 0xc2, 0xcf, 0x94, 0x45, 0x36, 0x61, + 0x39, 0xfa}; + +/* + * Game TAF data structure. The game structure contains the original TAF + * file header, a growable array of "slab" descriptors, each of which holds + * metadata for a "slab" (around a decompression buffer full of TAF strings), + * the length of the descriptor array and elements allocated, and a current + * location for iteration. + * + * Saved game files (.TAS) are just like TAF files except that they lack the + * header. So for files of this type, the header is all zeroes. + */ +struct sc_slabdesc_t { + sc_byte *data; + sc_int size; +}; +typedef sc_slabdesc_t *sc_slabdescref_t; +typedef struct sc_taf_s +{ + sc_uint magic; + sc_byte header[VERSION_HEADER_SIZE + V400_HEADER_EXTRA]; + sc_int version; + sc_int total_in_bytes; + sc_slabdescref_t slabs; + sc_int slab_count; + sc_int slabs_allocated; + sc_bool is_unterminated; + sc_int current_slab; + sc_int current_offset; +} sc_taf_t; + + +/* Microsoft Visual Basic PRNG magic numbers, initial and current state. */ +static const sc_int PRNG_CST1 = 0x43fd43fd, + PRNG_CST2 = 0x00c39ec3, + PRNG_CST3 = 0x00ffffff, + PRNG_INITIAL_STATE = 0x00a09e86; +static sc_int taf_random_state = 0x00a09e86; + +/* + * taf_random() + * taf_random_reset() + * + * Version 3.9 and version 3.8 games are obfuscated by xor'ing each character + * with the PRNG in Visual Basic. So here we have to emulate that, to unob- + * fuscate data from such game files. The PRNG generates 0..prng_cst3, which + * we multiply by 255 and then divide by prng_cst3 + 1 to get output in the + * range 0..254. Thanks to Rik Snel for uncovering this obfuscation. + */ +static sc_byte +taf_random (void) +{ + /* Generate and return the next pseudo-random number. */ + taf_random_state = (taf_random_state * PRNG_CST1 + PRNG_CST2) & PRNG_CST3; + return (UCHAR_MAX * (sc_uint) taf_random_state) / (sc_uint) (PRNG_CST3 + 1); +} + +static void +taf_random_reset (void) +{ + /* Reset PRNG to initial conditions. */ + taf_random_state = PRNG_INITIAL_STATE; +} + + +/* + * taf_is_valid() + * + * Return TRUE if pointer is a valid TAF structure, FALSE otherwise. + */ +static sc_bool +taf_is_valid (sc_tafref_t taf) +{ + return taf && taf->magic == TAF_MAGIC; +} + + +/* + * taf_create_empty() + * + * Allocate and return a new, empty TAF structure. + */ +static sc_tafref_t +taf_create_empty (void) +{ + sc_tafref_t taf; + + /* Create an empty TAF structure. */ + taf = (sc_tafref_t)sc_malloc(sizeof (*taf)); + taf->magic = TAF_MAGIC; + memset (taf->header, 0, sizeof (taf->header)); + taf->version = TAF_VERSION_NONE; + taf->total_in_bytes = 0; + taf->slabs = NULL; + taf->slab_count = 0; + taf->slabs_allocated = 0; + taf->is_unterminated = FALSE; + taf->current_slab = 0; + taf->current_offset = 0; + + /* Return the new TAF structure. */ + return taf; +} + + +/* + * taf_destroy() + * + * Free TAF memory, and destroy a TAF structure. + */ +void +taf_destroy (sc_tafref_t taf) +{ + sc_int index_; + assert (taf_is_valid (taf)); + + /* First free each slab in the slabs array,... */ + for (index_ = 0; index_ < taf->slab_count; index_++) + sc_free (taf->slabs[index_].data); + + /* + * ...then free slabs growable array, and poison and free the TAF structure + * itself. + */ + sc_free (taf->slabs); + memset (taf, 0xaa, sizeof (*taf)); + sc_free (taf); +} + + +/* + * taf_finalize_last_slab() + * + * Insert nul's into slab data so that it turns into a series of nul-terminated + * strings. Nul's are used to replace carriage return and newline pairs. + */ +static void +taf_finalize_last_slab (sc_tafref_t taf) +{ + sc_slabdescref_t slab; + sc_int index_; + + /* Locate the final slab in the slab descriptors array. */ + assert (taf->slab_count > 0); + slab = taf->slabs + taf->slab_count - 1; + + /* + * Replace carriage return and newline pairs with nuls, and individual + * carriage returns with a single newline. + */ + for (index_ = 0; index_ < slab->size; index_++) + { + if (slab->data[index_] == CARRIAGE_RETURN) + { + if (index_ < slab->size - 1 && slab->data[index_ + 1] == NEWLINE) + { + slab->data[index_] = NUL; + slab->data[index_ + 1] = NUL; + index_++; + } + else + slab->data[index_] = NEWLINE; + } + + /* Also protect against unlikely incoming nul characters. */ + else if (slab->data[index_] == NUL) + slab->data[index_] = NEWLINE; + } +} + + +/* + * taf_find_buffer_extent() + * + * Search backwards from the buffer end for a terminating carriage return and + * line feed. If none, found, return length and set is_unterminated to TRUE. + * Otherwise, return the count of usable bytes found in the buffer. + */ +static sc_int +taf_find_buffer_extent (const sc_byte *buffer, + sc_int length, sc_bool *is_unterminated) +{ + sc_int bytes; + + /* Search backwards from the buffer end for the final line feed. */ + for (bytes = length; bytes > 1; bytes--) + { + if (buffer[bytes - 2] == CARRIAGE_RETURN && buffer[bytes - 1] == NEWLINE) + break; + } + if (bytes < 2) + { + /* No carriage return and newline termination found. */ + *is_unterminated = TRUE; + return length; + } + + *is_unterminated = FALSE; + return bytes; +} + + +/* + * taf_append_buffer() + * + * Append a buffer of TAF lines to an existing TAF structure. Returns the + * number of characters consumed from the buffer. + */ +static sc_int +taf_append_buffer (sc_tafref_t taf, const sc_byte *buffer, sc_int length) +{ + sc_int bytes; + sc_bool is_unterminated; + + /* Locate the extent of appendable data in the buffer. */ + bytes = taf_find_buffer_extent (buffer, length, &is_unterminated); + + /* See if the last buffer handled contained at least one data line. */ + if (!taf->is_unterminated) + { + sc_slabdescref_t slab; + + /* Extend the slabs array if we've reached the current allocation. */ + if (taf->slab_count == taf->slabs_allocated) + { + taf->slabs_allocated += GROW_INCREMENT; + taf->slabs = (sc_slabdescref_t)sc_realloc (taf->slabs, + taf->slabs_allocated * sizeof (*taf->slabs)); + } + + /* Advance to the next unused slab in the slab descriptors array. */ + slab = taf->slabs + taf->slab_count; + taf->slab_count++; + + /* Copy the input buffer into the new slab. */ + slab->data = (sc_byte *)sc_malloc (bytes); + memcpy (slab->data, buffer, bytes); + slab->size = bytes; + } + else + { + sc_slabdescref_t slab; + + /* Locate the final slab in the slab descriptors array. */ + assert (taf->slab_count > 0); + slab = taf->slabs + taf->slab_count - 1; + + /* + * The last buffer we saw had no line endings in it. In this case, + * append the input buffer to the end of the last slab's data, rather + * than creating a new slab. This may cause allocation to overflow + * the system limits on single allocated areas on some platforms. + */ + slab->data = (sc_byte *)sc_realloc(slab->data, slab->size + bytes); + memcpy (slab->data + slab->size, buffer, bytes); + slab->size += bytes; + + /* + * Use a special case for the final carriage return and newline pairing + * that are split over two buffers; force correct termination of this + * slab. + */ + if (slab->size > 1 + && slab->data[slab->size - 2] == CARRIAGE_RETURN + && slab->data[slab->size - 1] == NEWLINE) + is_unterminated = FALSE; + } + + /* + * Note if this buffer requires that the next be coalesced with it. If it + * doesn't, finalize the last slab by breaking it into separate lines. + */ + taf->is_unterminated = is_unterminated; + if (!is_unterminated) + taf_finalize_last_slab (taf); + + /* Return count of buffer bytes consumed. */ + return bytes; +} + + +/* + * taf_unobfuscate() + * + * Unobfuscate a version 3.9 and version 3.8 TAF file from data read by + * repeated calls to the callback() function. Callback() should return the + * count of bytes placed in the buffer, or 0 if no more (end of file). + * Assumes that the file has been read past the header. + */ +static sc_bool +taf_unobfuscate (sc_tafref_t taf, sc_read_callbackref_t callback, + void *opaque, sc_bool is_gamefile) +{ + sc_byte *buffer; + sc_int bytes, used_bytes, total_bytes, index_; + + /* Reset the PRNG, and synchronize with the header already read. */ + taf_random_reset (); + for (index_ = 0; index_ < VERSION_HEADER_SIZE; index_++) + taf_random (); + + /* + * Malloc buffer, done to help systems with limited stacks, and initialize + * count of bytes read and used in the buffer to zero. + */ + buffer = (sc_byte *)sc_malloc (IN_BUFFER_SIZE); + used_bytes = 0; + total_bytes = 0; + + /* Unobfuscate in buffer sized chunks. */ + do + { + /* Try to obtain more data. */ + bytes = callback (opaque, + buffer + used_bytes, IN_BUFFER_SIZE - used_bytes); + + /* Unobfuscate data read in. */ + for (index_ = 0; index_ < bytes; index_++) + buffer[used_bytes + index_] ^= taf_random (); + + /* + * Add data read in and unobfuscated to buffer used data, and if + * unobfuscated data is available, add it to the TAF. + */ + used_bytes += bytes; + if (used_bytes > 0) + { + sc_int consumed; + + /* Add lines from this buffer to the TAF. */ + consumed = taf_append_buffer (taf, buffer, used_bytes); + + /* Move unused buffer data to buffer start. */ + memmove (buffer, buffer + consumed, IN_BUFFER_SIZE - consumed); + + /* Note counts of bytes consumed and remaining in the buffer. */ + used_bytes -= consumed; + total_bytes += consumed; + } + } + while (bytes > 0); + + /* + * Unobfuscation completed, note the total bytes read. This value is + * actually not used for version 3.9 and version 3.8 games, but we maintain + * it just in case. + */ + taf->total_in_bytes = total_bytes; + if (is_gamefile) + taf->total_in_bytes += VERSION_HEADER_SIZE; + + /* Check that we found the end of the input file as expected. */ + if (used_bytes > 0) + { + sc_error ("taf_unobfuscate:" + " warning: %ld unhandled bytes in the buffer\n", used_bytes); + } + + if (taf->is_unterminated) + sc_fatal ("taf_unobfuscate: unterminated final data slab\n"); + + /* Return successfully. */ + sc_free (buffer); + return TRUE; +} + + +/* + * taf_decompress() + * + * Decompress a version 4.0 TAF file from data read by repeated calls to the + * callback() function. Callback() should return the count of bytes placed + * in the buffer, 0 if no more (end of file). Assumes that the file has been + * read past the header. + */ +static sc_bool taf_decompress(sc_tafref_t taf, sc_read_callbackref_t callback, + void *opaque, sc_bool is_gamefile) +{ + error("TODO: decompress"); +#ifdef TODO + sc_byte *in_buffer, *out_buffer; +// z_stream stream; + sc_int status; + sc_bool is_first_block; + + /* + * Malloc buffers, done this way rather than as stack variables for systems + * such as PalmOS that may have limited stacks. + */ + in_buffer = sc_malloc (IN_BUFFER_SIZE); + out_buffer = sc_malloc (OUT_BUFFER_SIZE); + + /* Initialize Zlib inflation functions. */ + stream.next_out = out_buffer; + stream.avail_out = OUT_BUFFER_SIZE; + stream.next_in = in_buffer; + stream.avail_in = 0; + + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + + status = inflateInit (&stream); + if (status != Z_OK) + { + sc_error ("taf_decompress: inflateInit: error %ld\n", status); + sc_free (in_buffer); + sc_free (out_buffer); + return FALSE; + } + + /* + * Attempts to restore non-savefiles can arrive here, because there's no + * up-front header check, like the one for TAF files, applied to them. The + * first we see of the problem is when the first inflate() fails, so it's + * handy to use a flag here to block the error report for such cases. + */ + is_first_block = TRUE; + + /* Inflate the input buffers. */ + while (TRUE) + { + sc_int in_bytes, out_bytes; + + /* If the input buffer is empty, try to obtain more data. */ + if (stream.avail_in == 0) + { + in_bytes = callback (opaque, in_buffer, IN_BUFFER_SIZE); + stream.next_in = in_buffer; + stream.avail_in = in_bytes; + } + + /* Decompress as much stream data as we can. */ + status = inflate (&stream, Z_SYNC_FLUSH); + if (status != Z_STREAM_END && status != Z_OK) + { + if (is_gamefile || !is_first_block) + sc_error ("taf_decompress: inflate: error %ld\n", status); + sc_free (in_buffer); + sc_free (out_buffer); + return FALSE; + } + out_bytes = OUT_BUFFER_SIZE - stream.avail_out; + + /* See if decompressed data is available. */ + if (out_bytes > 0) + { + sc_int consumed; + + /* Add lines from this buffer to the TAF. */ + consumed = taf_append_buffer (taf, out_buffer, out_bytes); + + /* Move unused buffer data to buffer start. */ + memmove (out_buffer, + out_buffer + consumed, OUT_BUFFER_SIZE - consumed); + + /* Reset inflation stream for available space. */ + stream.next_out = out_buffer + out_bytes - consumed; + stream.avail_out += consumed; + } + + /* Enable full error reporting for non-gamefiles. */ + is_first_block = FALSE; + + /* If at inflation stream end and output is empty, leave loop. */ + if (status == Z_STREAM_END && stream.avail_out == OUT_BUFFER_SIZE) + break; + } + + /* + * Decompression completed, note the total bytes read for use when locating + * resources later on in the file. For what it's worth, this value is only + * used in version 4.0 games. + */ + taf->total_in_bytes = stream.total_in; + if (is_gamefile) + taf->total_in_bytes += VERSION_HEADER_SIZE + V400_HEADER_EXTRA; + + /* End inflation. */ + status = inflateEnd (&stream); + if (status != Z_OK) + sc_error ("taf_decompress: warning: inflateEnd: error %ld\n", status); + + if (taf->is_unterminated) + sc_fatal ("taf_decompress: unterminated final data slab\n"); + + /* Return successfully. */ + sc_free (in_buffer); + sc_free (out_buffer); + return TRUE; +#endif +} + + +/* + * taf_create_from_callback() + * + * Create a TAF structure from data read in by repeated calls to the + * callback() function. Callback() should return the count of bytes placed + * in the buffer, or 0 if no more (end of file). + */ +static sc_tafref_t +taf_create_from_callback (sc_read_callbackref_t callback, + void *opaque, sc_bool is_gamefile) +{ + sc_tafref_t taf; + sc_bool status = FALSE; + assert (callback); + + /* Create an empty TAF structure. */ + taf = taf_create_empty (); + + /* + * Determine the TAF file version in use. For saved games, we always use + * version 4.0 format. For others, it's determined from the header. + */ + if (is_gamefile) + { + sc_int in_bytes; + + /* + * Read in the ADRIFT header for game files. Start by reading in the + * shorter header common to all. + */ + in_bytes = callback (opaque, taf->header, VERSION_HEADER_SIZE); + if (in_bytes != VERSION_HEADER_SIZE) + { + sc_error ("taf_create: not enough data for standard TAF header\n"); + taf_destroy (taf); + return NULL; + } + + /* + * Compare the header with the known TAF signatures, and set TAF version + * appropriately. + */ + if (memcmp (taf->header, V400_SIGNATURE, VERSION_HEADER_SIZE) == 0) + { + /* Read in the version 4.0 header extension. */ + in_bytes = callback (opaque, + taf->header + VERSION_HEADER_SIZE, + V400_HEADER_EXTRA); + if (in_bytes != V400_HEADER_EXTRA) + { + sc_error ("taf_create:" + " not enough data for extended TAF header\n"); + taf_destroy (taf); + return NULL; + } + + taf->version = TAF_VERSION_400; + } + else if (memcmp (taf->header, V390_SIGNATURE, VERSION_HEADER_SIZE) == 0) + taf->version = TAF_VERSION_390; + else if (memcmp (taf->header, V380_SIGNATURE, VERSION_HEADER_SIZE) == 0) + taf->version = TAF_VERSION_380; + else + { + taf_destroy (taf); + return NULL; + } + } + else + { + /* Saved games are always considered to be version 4.0. */ + taf->version = TAF_VERSION_400; + } + + /* + * Call the appropriate game file reader function. For version 4.0 games, + * data is compressed with Zlib. For version 3.9 and version 3.8 games, + * it's obfuscated with the Visual Basic PRNG. + */ + switch (taf->version) + { + case TAF_VERSION_400: + status = taf_decompress (taf, callback, opaque, is_gamefile); + break; + + case TAF_VERSION_390: + case TAF_VERSION_380: + status = taf_unobfuscate (taf, callback, opaque, is_gamefile); + break; + + default: + sc_fatal ("taf_create: invalid version\n"); + } + if (!status) + { + taf_destroy (taf); + return NULL; + } + + /* Return successfully. */ + return taf; +} + + +/* + * taf_create() + * taf_create_tas() + * + * Public entry points for taf_create_from_callback(). Return a taf object + * constructed from either *.TAF (game) or *.TAS (saved game state) file data. + */ +sc_tafref_t +taf_create (sc_read_callbackref_t callback, void *opaque) +{ + return taf_create_from_callback (callback, opaque, TRUE); +} + +sc_tafref_t +taf_create_tas (sc_read_callbackref_t callback, void *opaque) +{ + return taf_create_from_callback (callback, opaque, FALSE); +} + + +/* + * taf_first_line() + * + * Iterator rewind function, reset current slab location to TAF data start. + */ +void +taf_first_line (sc_tafref_t taf) +{ + assert (taf_is_valid (taf)); + + /* Set current locations to TAF start. */ + taf->current_slab = 0; + taf->current_offset = 0; +} + + +/* + * taf_next_line() + * + * Iterator function, return the next line of data from a TAF, or NULL + * if no more lines. + */ +const sc_char * +taf_next_line (sc_tafref_t taf) +{ + assert (taf_is_valid (taf)); + + /* If there is a next line, return it and advance current. */ + if (taf->current_slab < taf->slab_count) + { + sc_char *line; + + /* Get the effective address of the current line. */ + line = (sc_char *) taf->slabs[taf->current_slab].data; + line += taf->current_offset; + + /* + * Advance to the next line. The + 2 skips the NULs used to replace the + * carriage return and line feed. + */ + taf->current_offset += strlen (line) + 2; + if (taf->current_offset >= taf->slabs[taf->current_slab].size) + { + taf->current_slab++; + taf->current_offset = 0; + } + + return line; + } + + /* No more lines, so return NULL. */ + return NULL; +} + + +/* + * taf_more_lines() + * + * Iterator end function, returns TRUE if more TAF lines are readable. + */ +sc_bool +taf_more_lines (sc_tafref_t taf) +{ + assert (taf_is_valid (taf)); + + /* Return TRUE if not at TAF data end. */ + return taf->current_slab < taf->slab_count; +} + + +/* + * taf_get_game_data_length() + * + * Returns the number of bytes read to decompress the game. Resources are + * appended to the TAF file after the game, so this value allows them to + * be located. + */ +sc_int +taf_get_game_data_length (sc_tafref_t taf) +{ + assert (taf_is_valid (taf)); + + /* + * Return the count of bytes inflated; this includes the TAF header length + * for TAF, rather than TAS, files. For TAS files, the count of file bytes + * read is irrelevant, and is never used. + */ + return taf->total_in_bytes; +} + + +/* + * taf_get_version() + * + * Return the version number of the TAF file, 400, 390, or 380. + */ +sc_int +taf_get_version (sc_tafref_t taf) +{ + assert (taf_is_valid (taf)); + + assert (taf->version != TAF_VERSION_NONE); + return taf->version; +} + + +/* + * taf_debug_is_taf_string() + * taf_debug_dump() + * + * Print out a complete TAF structure. The first function is a helper for + * properties debugging, indicating if a given address is a string in a TAF + * slab, and therefore safe to print. + */ +sc_bool +taf_debug_is_taf_string (sc_tafref_t taf, const void *addr) { + const sc_byte *const addr_ = (const sc_byte *const)addr; + sc_int index_; + + /* + * Compare pointer, by address directly, against all memory contained in + * the TAF slabs. Return TRUE if in range. + */ + for (index_ = 0; index_ < taf->slab_count; index_++) + { + if (addr_ >= taf->slabs[index_].data + && addr_ < taf->slabs[index_].data + taf->slabs[index_].size) + return TRUE; + } + + return FALSE; +} + +void +taf_debug_dump (sc_tafref_t taf) +{ + sc_int index_, current_slab, current_offset; + assert (taf_is_valid (taf)); + + /* Dump complete structure. */ + sc_trace ("TAFfile: debug dump follows...\n"); + sc_trace ("taf->header ="); + for (index_ = 0; index_ < (sc_int) sizeof (taf->header); index_++) + sc_trace (" %02x", taf->header[index_]); + sc_trace ("\n"); + + sc_trace ("taf->version = %s\n", + taf->version == TAF_VERSION_400 ? "4.00" : + taf->version == TAF_VERSION_390 ? "3.90" : + taf->version == TAF_VERSION_380 ? "3.80" : "[Unknown]"); + + sc_trace ("taf->slabs = \n"); + for (index_ = 0; index_ < taf->slab_count; index_++) + { + sc_trace ("%3ld : %p, %ld bytes\n", index_, + taf->slabs[index_].data, taf->slabs[index_].size); + } + + sc_trace ("taf->slab_count = %ld\n", taf->slab_count); + sc_trace ("taf->slabs_allocated = %ld\n", taf->slabs_allocated); + sc_trace ("taf->current_slab = %ld\n", taf->current_slab); + sc_trace ("taf->current_offset = %ld\n", taf->current_offset); + + /* Save current location. */ + current_slab = taf->current_slab; + current_offset = taf->current_offset; + + /* Print out taf lines using taf iterators. */ + sc_trace ("\ntaf iterators:\n"); + taf_first_line (taf); + for (index_ = 0; taf_more_lines (taf); index_++) + sc_trace ("%5ld %s\n", index_, taf_next_line (taf)); + + /* Restore current location. */ + taf->current_slab = current_slab; + taf->current_offset = current_offset; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sctafpar.cpp b/engines/glk/adrift/sctafpar.cpp new file mode 100644 index 0000000000..269308a953 --- /dev/null +++ b/engines/glk/adrift/sctafpar.cpp @@ -0,0 +1,3552 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Adds new "types" to jAsea's property descriptor: 'M' for multiline + * strings, 'Z', 'F'/'T', and 'E' for defaulted integers, booleans, and + * strings, 'i', 'b', and 's' for ignored integers, booleans, and strings, + * and '{...}' and '|...|' for "special" descriptions and version fixups + * that can't be described as things stand. + * + * o Adds new 'G' expression test, to check Global boolean. + * + * o The stack "adjustment" stuff is a bit of a bother. + */ + +/* Assorted definitions and constants. */ +static const sc_char NUL = '\0'; +enum +{ PARSE_TEMP_LENGTH = 256, + PARSE_MAX_DEPTH = 32 +}; + +/* Multiline separator sequences for the various versions supported. */ +enum { SEPARATOR_SIZE = 3 }; +static const sc_byte V400_SEPARATOR[SEPARATOR_SIZE] = {0xbd, 0xd0, 0x00}; +static const sc_byte V390_SEPARATOR[SEPARATOR_SIZE] = {0x2a, 0x2a, 0x00}; +static const sc_byte V380_SEPARATOR[SEPARATOR_SIZE] = {0x2a, 0x2a, 0x00}; + + +/* + * Tables of properties descriptors. These strings define the structure of + * a TAF file. Field keys are: + * + * $,#,B,M - string, integer, boolean, and multiline properties + * E,F,T,Z - string, integer, and boolean properties not in the TAF; + * set to "", FALSE, TRUE and zero on parsing (version < 4) + * i,s,b - string, integer, and boolean in the TAF, but not stored + * [num] - arrays of property, fixed size to num + * V - variable sized array of property, size in input file + * W - like V, but size - 1 in input file (version < 4) + * <class> - class of property, separate parse target (recurse) + * ?[!]expr: - conditional property based on expr + * G[!]expr: - conditional property based on expr using globals + * |...| - fixup specials for versions < 4 + * {special} - because some things just defy description + */ +typedef struct +{ + const sc_char *const class_name; + const sc_char *const descriptor; +} sc_parse_schema_t; + +/* Version 4.0 TAF file properties descriptor table. */ +static const sc_parse_schema_t V400_PARSE_SCHEMA[] = { + {"_GAME_", + "<HEADER>Header <GLOBAL>Globals V<ROOM>Rooms V<OBJECT>Objects V<TASK>Tasks" + " V<EVENT>Events V<NPC>NPCs V<ROOM_GROUP>RoomGroups V<SYNONYM>Synonyms" + " V<VARIABLE>Variables V<ALR>ALRs BCustomFont ?BCustomFont:$FontNameSize" + " $CompileDate"}, + {"HEADER", + "MStartupText #StartRoom MWinText"}, + {"GLOBAL", + "$GameName $GameAuthor $DontUnderstand #Perspective BShowExits #WaitTurns" + " BDispFirstRoom BBattleSystem #MaxScore $PlayerName BPromptName $PlayerDesc" + " #Task ?!#Task=0:$AltDesc #Position #ParentObject #PlayerGender" + " #MaxSize #MaxWt ?GBattleSystem:<BATTLE>Battle BEightPointCompass bNoDebug" + " BNoScoreNotify BNoMap bNoAutoComplete bNoControlPanel bNoMouse BSound" + " BGraphics <RESOURCE>IntroRes <RESOURCE>WinRes BStatusBox $StatusBoxText" + " iUnk1 iUnk2 BEmbedded"}, + {"BATTLE", + "iStaminaLo iStaminaHi iStrengthLo iStrengthHi iAccuracyLo iAccuracyHi" + " iDefenseLo iDefenseHi iAgilityLo iAgilityHi iRecovery"}, + {"ROOM", + "$Short $Long ?GEightPointCompass:[12]<ROOM_EXIT>Exits" + " ?!GEightPointCompass:[8]<ROOM_EXIT>Exits <RESOURCE>Res V<ROOM_ALT>Alts" + " ?!GNoMap:bHideOnMap"}, + {"ROOM_EXIT", + "{V400_ROOM_EXIT:#Dest_#Var1_#Var2_#Var3}"}, + {"ROOM_ALT", + "$M1 #Type <RESOURCE>Res1 $M2 #Var2 <RESOURCE>Res2 #HideObjects $Changed" + " #Var3 #DisplayRoom"}, + {"RESOURCE", + "?GSound:$SoundFile,#SoundLen,ZSoundOffset" + " ?GGraphics:$GraphicFile,#GraphicLen,ZGraphicOffset {V400_RESOURCE}"}, + {"OBJECT", + "$Prefix $Short V$Alias BStatic $Description #InitialPosition #Task" + " BTaskNotDone $AltDesc ?BStatic:<ROOM_LIST1>Where BContainer BSurface" + " #Capacity ?!BStatic:BWearable,#SizeWeight,#Parent" + " ?BStatic:{OBJECT:#Parent} #Openable ?#Openable=5:#Key ?#Openable=6:#Key" + " ?#Openable=7:#Key #SitLie ?!BStatic:BEdible BReadable ?BReadable:$ReadText" + " ?!BStatic:BWeapon #CurrentState ?!#CurrentState=0:$States,BStateListed" + " BListFlag <RESOURCE>Res1 <RESOURCE>Res2 ?GBattleSystem:<OBJ_BATTLE>Battle" + " $InRoomDesc #OnlyWhenNotMoved"}, + {"OBJ_BATTLE", + "iProtectionValue iHitValue iMethod iAccuracy"}, + {"ROOM_LIST1", + "#Type {ROOM_LIST1}"}, + {"TASK", + "V$Command $CompleteText $ReverseMessage $RepeatText $AdditionalMessage" + " #ShowRoomDesc BRepeatable BReversible V$ReverseCommand <ROOM_LIST0>Where" + " $Question ?$Question:$Hint1,$Hint2 V<TASK_RESTR>Restrictions" + " V<TASK_ACTION>Actions $RestrMask <RESOURCE>Res"}, + {"TASK_RESTR", + "#Type ?#Type=0:#Var1,#Var2,#Var3 ?#Type=1:#Var1,#Var2 ?#Type=2:#Var1,#Var2" + " ?#Type=3:#Var1,#Var2,#Var3 ?#Type=4:#Var1,#Var2,#Var3,$Var4 $FailMessage"}, + {"TASK_ACTION", + "#Type ?#Type=0:#Var1,#Var2,#Var3 ?#Type=1:#Var1,#Var2,#Var3" + " ?#Type=2:#Var1,#Var2 ?#Type=3:#Var1,#Var2,#Var3,$Expr,#Var5" + " ?#Type=4:#Var1 ?#Type=5:#Var1,#Var2 ?#Type=6:#Var1,#Var2,#Var3" + " ?#Type=7:iVar1,iVar2,iVar3"}, + {"ROOM_LIST0", + "#Type {ROOM_LIST0}"}, + {"EVENT", + "$Short #StarterType ?#StarterType=2:#StartTime,#EndTime" + " ?#StarterType=3:#TaskNum #RestartType BTaskFinished #Time1 #Time2" + " $StartText $LookText $FinishText <ROOM_LIST0>Where #PauseTask" + " BPauserCompleted #PrefTime1 $PrefText1 #ResumeTask BResumerCompleted" + " #PrefTime2 $PrefText2 #Obj2 #Obj2Dest #Obj3 #Obj3Dest #Obj1 #Obj1Dest" + " #TaskAffected [5]<RESOURCE>Res"}, + {"NPC", + "$Name $Prefix V$Alias $Descr #StartRoom $AltText #Task V<TOPIC>Topics" + " V<WALK>Walks BShowEnterExit ?BShowEnterExit:$EnterText,$ExitText" + " $InRoomText #Gender [4]<RESOURCE>Res ?GBattleSystem:<NPC_BATTLE>Battle"}, + {"NPC_BATTLE", + "iAttitude iStaminaLo iStaminaHi iStrengthLo iStrengthHi iAccuracyLo" + " iAccuracyHi iDefenseLo iDefenseHi iAgilityLo iAgilityHi iSpeed" + " iKilledTask iRecovery iStaminaTask"}, + {"TOPIC", + "$Subject $Reply #Task $AltReply"}, + {"WALK", + "#NumStops BLoop #StartTask #CharTask #MeetObject #ObjectTask #StoppingTask" + " #MeetChar $ChangedDesc {WALK:#Rooms_#Times}"}, + {"ROOM_GROUP", + "$Name {ROOM_GROUP:[]BList}"}, + {"SYNONYM", + "$Replacement $Original"}, + {"VARIABLE", + "$Name #Type $Value"}, + {"ALR", + "$Original $Replacement"}, + {NULL, NULL} +}; + +/* Version 3.9 TAF file properties descriptor table. */ +static const sc_parse_schema_t V390_PARSE_SCHEMA[] = { + {"_GAME_", + "<HEADER>Header <GLOBAL>Globals V<ROOM>Rooms V<OBJECT>Objects V<TASK>Tasks" + " V<EVENT>Events V<NPC>NPCs V<ROOM_GROUP>RoomGroups V<SYNONYM>Synonyms" + " V<VARIABLE>Variables V<ALR>ALRs BCustomFont ?BCustomFont:$FontNameSize" + " $CompileDate sPassword"}, + {"HEADER", + "MStartupText #StartRoom MWinText"}, + {"GLOBAL", + "$GameName $GameAuthor $DontUnderstand #Perspective BShowExits #WaitTurns" + " BDispFirstRoom BBattleSystem #MaxScore $PlayerName BPromptName $PlayerDesc" + " #Task ?!#Task=0:$AltDesc #Position #ParentObject #PlayerGender" + " #MaxSize #MaxWt ?GBattleSystem:<BATTLE>Battle BEightPointCompass bNoDebug" + " BNoScoreNotify BNoMap bNoAutoComplete bNoControlPanel bNoMouse" + " BSound BGraphics <RESOURCE>IntroRes <RESOURCE>WinRes FStatusBox" + " EStatusBoxText iUnk1 iUnk2 FEmbedded"}, + {"BATTLE", + "iStamina iStrength iDefense"}, + {"ROOM", + "$Short $Long $LastDesc ?GEightPointCompass:[12]<ROOM_EXIT>Exits" + " ?!GEightPointCompass:[8]<ROOM_EXIT>Exits $AddDesc1 #Task1 $AddDesc2 #Task2" + " #Obj $AltDesc #TypeHideObjects <RESOURCE>Res <RESOURCE>LastRes" + " <RESOURCE>Task1Res <RESOURCE>Task2Res <RESOURCE>AltRes" + " ?!GNoMap:bHideOnMap |V390_ROOM:_Alts_|"}, + {"ROOM_EXIT", + "{V390_V380_ROOM_EXIT:#Dest_#Var1_#Var2_ZVar3}"}, + {"RESOURCE", + "?GSound:$SoundFile,ZSoundLen,ZSoundOffset" + " ?GGraphics:$GraphicFile,ZGraphicLen,ZGraphicOffset"}, + {"OBJECT", + "$Prefix $Short" + " [1]$Alias BStatic $Description #InitialPosition #Task BTaskNotDone" + " $AltDesc ?BStatic:<ROOM_LIST1>Where BContainer BSurface #Capacity" + " ?!BStatic:BWearable,#SizeWeight,#Parent ?BStatic:{OBJECT:#Parent}" + " #Openable |V390_OBJECT:_Openable_,Key| #SitLie ?!BStatic:BEdible BReadable" + " ?BReadable:$ReadText ?!BStatic:BWeapon ZCurrentState FListFlag" + " <RESOURCE>Res1 <RESOURCE>Res2 ?GBattleSystem:<OBJ_BATTLE>Battle" + " EInRoomDesc ZOnlyWhenNotMoved"}, + {"OBJ_BATTLE", + "iProtectionValue iHitValue iMethod"}, + {"ROOM_LIST1", + "#Type {ROOM_LIST1}"}, + {"TASK", + "W$Command $CompleteText $ReverseMessage $RepeatText $AdditionalMessage" + " #ShowRoomDesc BRepeatable BReversible W$ReverseCommand <ROOM_LIST0>Where" + " $Question ?$Question:$Hint1,$Hint2 V<TASK_RESTR>Restrictions" + " V<TASK_ACTION>Actions |V390_TASK:$RestrMask| <RESOURCE>Res"}, + {"TASK_RESTR", + "#Type ?#Type=0:#Var1,#Var2,#Var3 ?#Type=1:#Var1,#Var2 ?#Type=2:#Var1,#Var2" + " ?#Type=3:#Var1,#Var2,#Var3 ?#Type=4:#Var1,#Var2,#Var3,EVar4" + ",|V390_TASK_RESTR:Var1>0?#Var1++| $FailMessage"}, + {"TASK_ACTION", + "#Type |V390_TASK_ACTION:Type>4?#Type++| ?#Type=0:#Var1,#Var2,#Var3" + " ?#Type=1:#Var1,#Var2,#Var3 ?#Type=2:#Var1,#Var2" + " ?#Type=3:#Var1,#Var2,#Var3,|V390_TASK_ACTION:$Expr_#Var5|" + " ?#Type=4:#Var1 ?#Type=6:#Var1,ZVar2,ZVar3 ?#Type=7:iVar1,iVar2,iVar3"}, + {"ROOM_LIST0", + "#Type {ROOM_LIST0}"}, + {"EVENT", + "$Short #StarterType ?#StarterType=2:#StartTime,#EndTime" + " ?#StarterType=3:#TaskNum #RestartType BTaskFinished #Time1 #Time2" + " $StartText $LookText $FinishText <ROOM_LIST0>Where #PauseTask" + " BPauserCompleted #PrefTime1 $PrefText1 #ResumeTask BResumerCompleted" + " #PrefTime2 $PrefText2 #Obj2 #Obj2Dest #Obj3 #Obj3Dest #Obj1 #Obj1Dest" + " #TaskAffected [5]<RESOURCE>Res"}, + {"NPC", + "$Name $Prefix [1]$Alias $Descr #StartRoom $AltText #Task V<TOPIC>Topics" + " V<WALK>Walks BShowEnterExit ?BShowEnterExit:$EnterText,$ExitText" + " $InRoomText #Gender [4]<RESOURCE>Res ?GBattleSystem:<NPC_BATTLE>Battle"}, + {"NPC_BATTLE", + "iAttitude iStamina iStrength iDefense iSpeed iKilledTask"}, + {"TOPIC", + "$Subject $Reply #Task $AltReply"}, + {"WALK", + "#NumStops BLoop #StartTask #CharTask #MeetObject #ObjectTask #StoppingTask" + " ZMeetChar $ChangedDesc {WALK:#Rooms_#Times}"}, + {"ROOM_GROUP", + "$Name {ROOM_GROUP:[]BList}"}, + {"SYNONYM", + "$Replacement $Original"}, + {"VARIABLE", + "$Name ZType $Value"}, + {"ALR", + "$Original $Replacement"}, + {NULL, NULL} +}; + +/* Version 3.8 TAF file properties descriptor table. */ +static const sc_parse_schema_t V380_PARSE_SCHEMA[] = { + {"_GAME_", + "<HEADER>Header <GLOBAL>Globals V<ROOM>Rooms V<OBJECT>Objects V<TASK>Tasks" + " V<EVENT>Events V<NPC>NPCs V<ROOM_GROUP>RoomGroups V<SYNONYM>Synonyms" + " FCustomFont $CompileDate sPassword |V380_GLOBAL:_MaxScore_|" + " |V380_OBJECT:_InitialPositions_|"}, + {"HEADER", + "MStartupText #StartRoom MWinText"}, + {"GLOBAL", + "$GameName $GameAuthor #MaxCarried |V380_MaxSize_MaxWt_| $DontUnderstand" + " #Perspective BShowExits #WaitTurns FDispFirstRoom FBattleSystem" + " EPlayerName FPromptName EPlayerDesc ZTask ZPosition ZParentObject" + " ZPlayerGender FEightPointCompass TNoScoreNotify FSound FGraphics" + " FStatusBox EStatusBoxText FEmbedded"}, + {"ROOM", + "$Short $Long $LastDesc [8]<ROOM_EXIT>Exits $AddDesc1 #Task1 $AddDesc2" + " #Task2 #Obj $AltDesc #TypeHideObjects |V380_ROOM:_Alts_|"}, + {"ROOM_EXIT", + "{V390_V380_ROOM_EXIT:#Dest_#Var1_#Var2_ZVar3}"}, + {"OBJECT", + "$Prefix $Short [1]$Alias BStatic $Description #InitialPosition #Task" + " BTaskNotDone $AltDesc ?BStatic:<ROOM_LIST1>Where #SurfaceContainer" + " FSurface ?#SurfaceContainer=2:TSurface FContainer" + " ?#SurfaceContainer=1:TContainer #Capacity |V380_OBJECT:#Capacity*10+2|" + " ?!BStatic:BWearable,#SizeWeight,#Parent ?BStatic:{OBJECT:#Parent}" + " #Openable |V380_OBJECT:_Openable_,Key| #SitLie ?!BStatic:BEdible BReadable" + " ?BReadable:$ReadText ?!BStatic:BWeapon ZCurrentState FListFlag" + " EInRoomDesc ZOnlyWhenNotMoved"}, + {"ROOM_LIST1", + "#Type {ROOM_LIST1}"}, + {"TASK", + "W$Command $CompleteText $ReverseMessage $RepeatText $AdditionalMessage" + " #ShowRoomDesc BRepeatable #Score BSingleScore [6]<TASK_MOVE>Movements" + " BReversible W$ReverseCommand #WearObj1 #WearObj2 #HoldObj1 #HoldObj2" + " #HoldObj3 #Obj1 #Task BTaskNotDone $TaskMsg $HoldMsg $WearMsg $CompanyMsg" + " BNotInSameRoom #NPC $Obj1Msg #Obj1Room <ROOM_LIST0>Where BKillsPlayer" + " BHoldingSameRoom $Question ?$Question:$Hint1,$Hint2 #Obj2" + " ?!#Obj2=0:#Obj2Var1,#Obj2Var2,$Obj2Msg BWinGame |V380_TASK:_Actions_|" + " |V380_TASK:_Restrictions_|"}, + {"TASK_MOVE", + "#Var1 #Var2 #Var3"}, + {"ROOM_LIST0", + "#Type {ROOM_LIST0}"}, + {"EVENT", + "$Short #StarterType ?#StarterType=2:#StartTime,#EndTime" + " ?#StarterType=3:#TaskNum #RestartType BTaskFinished #Time1 #Time2" + " $StartText $LookText $FinishText <ROOM_LIST0>Where #PauseTask" + " BPauserCompleted #PrefTime1 $PrefText1 #ResumeTask BResumerCompleted" + " #PrefTime2 $PrefText2 #Obj2 #Obj2Dest #Obj3 #Obj3Dest #Obj1 #Obj1Dest" + " #TaskAffected"}, + {"NPC", + "$Name $Prefix [1]$Alias $Descr #StartRoom $AltText #Task V<TOPIC>Topics" + " V<WALK>Walks BShowEnterExit ?BShowEnterExit:$EnterText,$ExitText" + " $InRoomText ZGender"}, + {"TOPIC", + "$Subject $Reply #Task $AltReply"}, + {"WALK", + "#NumStops BLoop #StartTask #CharTask #MeetObject" + " ?!#MeetObject=0:|V380_WALK:_MeetObject_| #ObjectTask ZMeetChar" + " {WALK:#Rooms_#Times} ZStoppingTask EChangedDesc"}, + {"ROOM_GROUP", + "$Name {ROOM_GROUP:[]BList}"}, + {"SYNONYM", + "$Replacement $Original"}, + {NULL, NULL} +}; + + +/* + * parse_select_schema() + * + * Select one of the parse schemata based on a TAF file. + */ +static const sc_parse_schema_t * +parse_select_schema (sc_tafref_t taf) +{ + /* Switch based on the TAF file version. */ + switch (taf_get_version (taf)) + { + case TAF_VERSION_400: + return V400_PARSE_SCHEMA; + case TAF_VERSION_390: + return V390_PARSE_SCHEMA; + case TAF_VERSION_380: + return V380_PARSE_SCHEMA; + default: + sc_fatal ("parse_select_schema: invalid TAF file version\n"); + return NULL; + } +} + + +/* The uncompressed TAF file from which we get all our data. */ +static sc_tafref_t parse_taf = NULL; +static sc_int parse_tafline = 0; + +/* The parse schema selected for this TAF file. */ +static sc_parse_schema_t const *parse_schema = NULL; + +/* Properties bundle and trace flag, set before parsing. */ +static sc_prop_setref_t parse_bundle = NULL; +static sc_bool parse_trace = FALSE; + +/* + * Stack of property keys. The stack is filled by parsing, and written + * to the property store on parse terminals. + */ +static sc_vartype_t parse_vt_key[PARSE_MAX_DEPTH]; +static sc_char parse_format[PARSE_MAX_DEPTH]; +static sc_int parse_depth = 0; + + +/* + * parse_push_key() + * parse_pop_key() + * + * Push a key of the given type onto the property key stack, and pop a key + * off on unwind. + */ +static void +parse_push_key (sc_vartype_t vt_key, sc_char type) +{ + if (parse_depth == PARSE_MAX_DEPTH) + sc_fatal ("parse_push_key: stack overrun\n"); + + /* Push the key, and its associated type. */ + parse_vt_key[parse_depth] = vt_key; + parse_format[parse_depth] = type; + parse_depth++; +} + +static void +parse_pop_key (void) +{ + /* Check the stack has something to pop, then pop it. */ + if (parse_depth == 0) + sc_fatal ("parse_pop_key: stack underrun\n"); + parse_depth--; +} + + +/* + * parse_retrieve_stack() + * + * This is ugly. The parse produces indexes before the things that they + * index. An expedient fix is to switch i-s keys before storing a property + * value + */ +static void +parse_retrieve_stack (sc_char format[], sc_vartype_t vt_key[], sc_int *depth) +{ + sc_int index_; + + /* Switch index-string key pairs. */ + for (index_ = 0; index_ < parse_depth; index_++) + { + if (index_ < parse_depth - 1 + && parse_format[index_] == PROP_KEY_INTEGER + && parse_format[index_ + 1] == PROP_KEY_STRING) + { + /* Swap format and key elements. */ + format[index_] = parse_format[index_ + 1]; + format[index_ + 1] = parse_format[index_]; + vt_key[index_] = parse_vt_key[index_ + 1]; + vt_key[index_ + 1] = parse_vt_key[index_]; + + index_++; + } + else + { + /* Simple copy of format and key elements. */ + format[index_] = parse_format[index_]; + vt_key[index_] = parse_vt_key[index_]; + } + } + + /* Return the parse depth. */ + *depth = parse_depth; +} + + +/* + * parse_stack_backtrace() + * + * Dump the parse stack. Used for diagnostics on finding what we think may + * be a bad game. + */ +static void +parse_stack_backtrace (void) +{ + sc_vartype_t vt_key[PARSE_MAX_DEPTH]; + sc_char format[PARSE_MAX_DEPTH]; + sc_int depth, index_; + + parse_retrieve_stack (format, vt_key, &depth); + + sc_error ("parse_stack_backtrace: version %s schema parsed to depth %ld\n", + (parse_schema == V400_PARSE_SCHEMA) ? "4.00" : + (parse_schema == V390_PARSE_SCHEMA) ? "3.90" : + (parse_schema == V380_PARSE_SCHEMA) ? "3.80" : "[Invalid]", + depth); + + sc_error ("parse_stack_backtrace: parse stack backtrace follows...\n"); + for (index_ = 0; index_ < depth; index_++) + { + sc_char type; + + type = format[index_]; + if (type == PROP_KEY_INTEGER) + sc_error ("%2ld - [%c] %ld\n", index_, type, vt_key[index_].integer); + else if (type == PROP_KEY_STRING) + sc_error ("%2ld - [%c] \"%s\"\n", index_, type, vt_key[index_].string); + else + sc_error ("%2ld - [%c] %p\n", index_, type, vt_key[index_].voidp); + } +} + + +/* + * parse_put_property() + * parse_get_property() + * + * Write or read a property based on the keys amassed so far. + */ +static void +parse_put_property (sc_vartype_t vt_value, sc_char type) +{ + sc_vartype_t vt_key[PARSE_MAX_DEPTH]; + sc_char format[PARSE_MAX_DEPTH + 4]; + sc_int depth; + + /* Retrieve the adjusted stack. */ + parse_retrieve_stack (format + 3, vt_key, &depth); + + /* Complete the format for the property put. */ + format[0] = type; + format[1] = '-'; + format[2] = '>'; + format[depth + 3] = NUL; + + /* Store the property under the stacked keys. */ + assert (parse_bundle); + prop_put (parse_bundle, format, vt_value, vt_key); +} + +static sc_bool +parse_get_property (sc_vartype_t *vt_rvalue, sc_char type) +{ + sc_vartype_t vt_key[PARSE_MAX_DEPTH]; + sc_char format[PARSE_MAX_DEPTH + 4]; + sc_int depth; + sc_bool status; + + /* Retrieve the adjusted stack. */ + parse_retrieve_stack (format + 3, vt_key, &depth); + + /* Complete the format for the property put. */ + format[0] = type; + format[1] = '<'; + format[2] = '-'; + format[depth + 3] = NUL; + + /* Retrieve the property using the stacked keys. */ + assert (parse_bundle); + status = prop_get (parse_bundle, format, vt_rvalue, vt_key); + + return status; +} + + +/* + * parse_get_child_count() + * + * Convenience form of parse_get_property(), retrieve an integer property + * indicating the child count of the effectively stacked node, or zero if + * no such node exists. + */ +static sc_int +parse_get_child_count (void) +{ + sc_vartype_t vt_rvalue; + + if (!parse_get_property (&vt_rvalue, PROP_INTEGER)) + vt_rvalue.integer = 0; + + return vt_rvalue.integer; +} + + +/* + * parse_get_integer_property() + * parse_get_boolean_property() + * parse_get_string_property() + * + * Convenience forms of parse_get_property(), retrieve directly, and report + * a fatal error if the property does not exist. + */ +static sc_int +parse_get_integer_property (void) +{ + sc_vartype_t vt_rvalue; + + if (!parse_get_property (&vt_rvalue, PROP_INTEGER)) + sc_fatal ("parse_get_integer_property: missing property\n"); + + return vt_rvalue.integer; +} + +static sc_bool +parse_get_boolean_property (void) +{ + sc_vartype_t vt_rvalue; + + if (!parse_get_property (&vt_rvalue, PROP_BOOLEAN)) + sc_fatal ("parse_get_boolean_property: missing property\n"); + + return vt_rvalue.boolean; +} + +static const sc_char * +parse_get_string_property (void) +{ + sc_vartype_t vt_rvalue; + + if (!parse_get_property (&vt_rvalue, PROP_STRING)) + sc_fatal ("parse_get_string_property: missing property\n"); + + return vt_rvalue.string; +} + + +/* Parse error jump buffer. */ +static jmp_buf parse_taf_error; + +/* Pushback line, and pushback requested flag. */ +static const sc_char *parse_pushback_line = NULL; +static sc_bool parse_use_pushback = FALSE; + +/* + * parse_get_taf_string() + * parse_get_taf_integer() + * parse_get_taf_boolean() + * parse_taf_pushback() + * + * Wrapper round obtaining the next TAF file line, with variants to convert + * the line content into an integer or boolean, and a function for effective + * TAF line pushback. + */ +static const sc_char * +parse_get_taf_string (void) +{ + const sc_char *line; + + /* If pushback requested, use that instead of reading. */ + if (parse_use_pushback) + { + /* Use the pushback line, and clear the request. */ + assert (parse_pushback_line); + line = parse_pushback_line; + parse_use_pushback = FALSE; + } + else + { + /* Get the next line, and complain if absent. */ + line = taf_next_line (parse_taf); + if (!line) + { + sc_error ("parse_get_taf_string:" + " out of TAF data at line %ld\n", parse_tafline); + parse_stack_backtrace (); + longjmp (parse_taf_error, 1); + } + + /* Note this line for possible pushback. */ + parse_pushback_line = line; + } + + /* Print out the line we're parsing if tracing. */ + if (parse_trace) + sc_trace ("Parse: read in line %ld : %s\n", parse_tafline, line); + + parse_tafline++; + return line; +} + +static sc_int +parse_get_taf_integer (void) +{ + const sc_char *line; + sc_int integer; + + /* Get line, and scan for a single integer; return it. */ + line = parse_get_taf_string (); + if (sscanf (line, "%ld", &integer) != 1) + { + sc_error ("parse_get_taf_integer:" + " invalid integer at line %ld\n", parse_tafline - 1); + parse_stack_backtrace (); + longjmp (parse_taf_error, 1); + } + + return integer; +} + +static sc_bool +parse_get_taf_boolean (void) +{ + const sc_char *line; + sc_uint boolean; + + /* + * Get line, and scan for a single integer; check it's a valid-looking flag, + * and return it. + */ + line = parse_get_taf_string (); + if (sscanf (line, "%lu", &boolean) != 1) + { + sc_error ("parse_get_taf_boolean:" + " invalid boolean at line %ld\n", parse_tafline - 1); + parse_stack_backtrace (); + longjmp (parse_taf_error, 1); + } + if (boolean != 0 && boolean != 1) + { + sc_error ("parse_get_taf_boolean:" + " warning: suspect boolean at line %ld\n", parse_tafline - 1); + } + + return boolean != 0; +} + +static void +parse_taf_pushback (void) +{ + if (parse_use_pushback || !parse_pushback_line) + sc_fatal ("parse_taf_pushback: too much pushback requested\n"); + + /* Set pushback request, and decrement line counter. */ + parse_use_pushback = TRUE; + parse_tafline--; + + /* Note pushback for tracing purposes. */ + if (parse_trace) + sc_trace ("Parse: push back at line %ld\n", parse_tafline); +} + + +/* Enumerations of parse types found in the parse schema. */ +enum +{ PARSE_INTEGER = '#', + PARSE_DEFAULT_ZERO = 'Z', + PARSE_BOOLEAN = 'B', + PARSE_DEFAULT_FALSE = 'F', + PARSE_DEFAULT_TRUE = 'T', + PARSE_STRING = '$', + PARSE_DEFAULT_EMPTY = 'E', + PARSE_MULTILINE = 'M', + PARSE_VECTOR = 'V', + PARSE_VECTOR_ALTERNATE = 'W', + PARSE_ARRAY = '[', + PARSE_EXPRESSION = '?', + PARSE_EXPRESSION_NOT = '!', + PARSE_GLOBAL_EXPRESSION = 'G', + PARSE_CLASS = '<', + PARSE_FIXUP = '|', + PARSE_SPECIAL = '{', + PARSE_IGNORE_INTEGER = 'i', + PARSE_IGNORE_BOOLEAN = 'b', + PARSE_IGNORE_STRING = 's' +}; + +/* Forward declarations of parse functions for recursion. */ +static void parse_element (const sc_char *element); +static void parse_class (const sc_char *class_); +static void parse_descriptor (const sc_char *descriptor); + +/* + * parse_array() + * + * Parse a descriptor [] array. + */ +static void +parse_array (const sc_char *array) +{ + sc_int count, index_; + sc_char element[PARSE_TEMP_LENGTH]; + + if (parse_trace) + sc_trace ("Parse: entering array %s\n", array); + + /* Find the count of elements in the array, and the element itself. */ + if (sscanf (array, "[%ld]%[^ ]", &count, element) != 2) + sc_fatal ("parse_array: bad array, %s\n", array); + + /* Parse the element for array count iterations, each a key. */ + for (index_ = 0; index_ < count; index_++) + { + sc_vartype_t vt_key; + + vt_key.integer = index_; + parse_push_key (vt_key, PROP_KEY_INTEGER); + + parse_element (element); + + parse_pop_key (); + } + + if (parse_trace) + sc_trace ("Parse: leaving array %s\n", array); +} + + +/* + * parse_vector_common() + * parse_vector() + * parse_vector_alternate() + * + * Parse a variable-length vector of properties. + */ +static void +parse_vector_common (const sc_char *vector, sc_int count) +{ + sc_int index_; + + /* Parse the vector property count times, pushing a key on each. */ + for (index_ = 0; index_ < count; index_++) + { + sc_vartype_t vt_key; + + vt_key.integer = index_; + parse_push_key (vt_key, PROP_KEY_INTEGER); + + parse_element (vector + 1); + + parse_pop_key (); + } +} + +static void +parse_vector (const sc_char *vector) +{ + sc_int count; + + if (parse_trace) + sc_trace ("Parse: entering vector %s\n", vector); + + /* Find the count of elements in the vector, and parse. */ + count = parse_get_taf_integer (); + parse_vector_common (vector, count); + + if (parse_trace) + sc_trace ("Parse: leaving vector %s\n", vector); +} + +static void +parse_vector_alternate (const sc_char *vector) +{ + sc_int count; + + if (parse_trace) + sc_trace ("Parse: entering alternate vector %s\n", vector); + + /* Element count, this is a vector described by size - 1. */ + count = parse_get_taf_integer () + 1; + parse_vector_common (vector, count); + + if (parse_trace) + sc_trace ("Parse: leaving alternate vector %s\n", vector); +} + + +/* + * parse_test_expression() + * parse_expression() + * + * Parse a conditional field definition, with runtime test. + */ +static sc_bool +parse_test_expression (const sc_char *test_expression) +{ + sc_vartype_t vt_key; + sc_char plhs[PARSE_TEMP_LENGTH]; + sc_int rhs; + sc_bool retval = FALSE; + + /* Identify the type of expression to evaluate. */ + switch (test_expression[0]) + { + case PARSE_BOOLEAN: + /* Read boolean property and return its value. */ + vt_key.string = test_expression + 1; + parse_push_key (vt_key, PROP_KEY_STRING); + retval = parse_get_boolean_property (); + parse_pop_key (); + break; + + case PARSE_INTEGER: + /* Get the left and right sides of = comparison. */ + if (sscanf (test_expression, "#%[^=]=%ld", plhs, &rhs) != 2) + { + sc_fatal ("parse_test_expression: bad = compare, %s\n", + test_expression + 1); + } + + /* Read integer property and return comparison. */ + vt_key.string = plhs; + parse_push_key (vt_key, PROP_KEY_STRING); + retval = (parse_get_integer_property () == rhs); + parse_pop_key (); + break; + + case PARSE_STRING: + /* Read property and return TRUE if not an empty string. */ + vt_key.string = test_expression + 1; + parse_push_key (vt_key, PROP_KEY_STRING); + retval = !sc_strempty (parse_get_string_property ()); + parse_pop_key (); + break; + + case PARSE_GLOBAL_EXPRESSION: + { + sc_vartype_t vt_gkey[2]; + + /* Read the given Global boolean property and return it. */ + vt_gkey[0].string = "Globals"; + vt_gkey[1].string = test_expression + 1; + retval = prop_get_boolean (parse_bundle, "B<-ss", vt_gkey); + break; + } + + default: + sc_fatal ("parse_test_expression:" + " bad expression, %s\n", test_expression + 1); + } + + if (parse_trace) + sc_trace ("Parse: expression is %s\n", retval ? "true" : "false"); + + return retval; +} + +static void +parse_expression (const sc_char *expression) +{ + sc_char test_expression[PARSE_TEMP_LENGTH]; + sc_bool is_present; + + if (parse_trace) + sc_trace ("Parse: entering expression %s\n", expression); + + /* Isolate the test part of the expression. */ + if (sscanf (expression, "?%[^:]", test_expression) != 1) + sc_fatal ("parse_expression: bad expression, %s\n", expression); + + /* Handle the remainder of the expression only if test passes. */ + is_present = (test_expression[0] == PARSE_EXPRESSION_NOT) + ? !parse_test_expression (test_expression + 1) + : parse_test_expression (test_expression); + if (is_present) + { + sc_int next; + + /* + * Following the ':' may be a single element, or a comma-separated list. + */ + for (next = strlen (test_expression) + 2; expression[next] != NUL; ) + { + sc_char element[PARSE_TEMP_LENGTH]; + + /* Get the next individual element to parse. */ + if (sscanf (expression + next, "%[^,]", element) != 1) + sc_fatal ("parse_expression: bad list, %s\n", expression + next); + + /* Parse this isolated element. */ + parse_element (element); + + /* Advance to the start of the next element. */ + next += strlen (element); + next += strspn (expression + next, ","); + } + } + + if (parse_trace) + sc_trace ("Parse: leaving expression %s\n", expression); +} + + +/* + * parse_read_multiline() + * + * Helper for parse_terminal(), reads in a multiline string. The return + * string is malloc'ed, and the caller needs to handle that. + */ +static sc_char * +parse_read_multiline (void) +{ + const sc_byte *separator = NULL; + const sc_char *line; + sc_char *multiline; + + /* Select the appropriate multiline separator. */ + switch (taf_get_version (parse_taf)) + { + case TAF_VERSION_400: + separator = V400_SEPARATOR; + break; + case TAF_VERSION_390: + separator = V390_SEPARATOR; + break; + case TAF_VERSION_380: + separator = V380_SEPARATOR; + break; + default: + sc_fatal ("parse_read_multiline: invalid TAF file version\n"); + break; + } + + /* Take a simple copy of the first line. */ + line = parse_get_taf_string (); + multiline = (sc_char *)sc_malloc (strlen (line) + 1); + strcpy (multiline, line); + + /* Now concatenate until separator found. */ + line = parse_get_taf_string (); + while (memcmp (line, separator, SEPARATOR_SIZE) != 0) + { + multiline = (sc_char *)sc_realloc (multiline, + strlen (multiline) + strlen (line) + 2); + strcat (multiline, "\n"); + strcat (multiline, line); + line = parse_get_taf_string (); + } + + return multiline; +} + + +/* + * parse_terminal() + * + * Common handler for string, integer, boolean, and multiline parse terminals. + */ +static void +parse_terminal (const sc_char *terminal) +{ + sc_vartype_t vt_key, vt_value; + + if (parse_trace) + sc_trace ("Parse: entering terminal %s\n", terminal); + + /* Push the key string. */ + vt_key.string = terminal + 1; + parse_push_key (vt_key, PROP_KEY_STRING); + + /* Retrieve, or invent, then store the value. */ + switch (terminal[0]) + { + case PARSE_INTEGER: + vt_value.integer = parse_get_taf_integer (); + parse_put_property (vt_value, PROP_INTEGER); + break; + case PARSE_DEFAULT_ZERO: + vt_value.integer = 0; + parse_put_property (vt_value, PROP_INTEGER); + break; + + case PARSE_BOOLEAN: + vt_value.boolean = parse_get_taf_boolean (); + parse_put_property (vt_value, PROP_BOOLEAN); + break; + case PARSE_DEFAULT_FALSE: + case PARSE_DEFAULT_TRUE: + vt_value.boolean = (terminal[0] == PARSE_DEFAULT_TRUE); + parse_put_property (vt_value, PROP_BOOLEAN); + break; + + case PARSE_STRING: + vt_value.string = parse_get_taf_string (); + parse_put_property (vt_value, PROP_STRING); + break; + case PARSE_DEFAULT_EMPTY: + vt_value.string = ""; + parse_put_property (vt_value, PROP_STRING); + break; + + case PARSE_MULTILINE: + /* Assign to and adopt mutable string rather than const string. */ + vt_value.mutable_string = parse_read_multiline (); + parse_put_property (vt_value, PROP_STRING); + + assert (parse_bundle); + prop_adopt (parse_bundle, vt_value.mutable_string); + break; + + case PARSE_IGNORE_INTEGER: + (void) parse_get_taf_integer (); + break; + case PARSE_IGNORE_BOOLEAN: + (void) parse_get_taf_boolean (); + break; + case PARSE_IGNORE_STRING: + (void) parse_get_taf_string (); + break; + + default: + sc_fatal ("parse_terminal: bad type, %c\n", terminal[0]); + } + + /* Pop terminal key. */ + parse_pop_key (); + + if (parse_trace) + sc_trace ("Parse: leaving terminal %s\n", terminal); +} + + +/* + * Resources table. This table enables resource offsets to be calculated + * for the various sound and graphic resources encountered on parsing + * version 4.0 games. It's unused if the version is not 4.0. + */ +typedef struct +{ + sc_char *name; + sc_uint hash; + sc_int length; + sc_int offset; +} sc_parse_resource_t; + +enum { RESOURCE_GROW_INCREMENT = 32 }; +static sc_int parse_resources_length = 0; +static sc_int parse_resources_size = 0; +static sc_parse_resource_t *parse_resources = NULL; + + +/* + * parse_clear_v400_resources_table() + * + * Free and clear down the version 4.0 resources table. + */ +static void +parse_clear_v400_resources_table (void) +{ + /* Free allocated memory and return to initial values. */ + if (parse_resources) + { + sc_int index_; + + for (index_ = 0; index_ < parse_resources_length; index_++) + sc_free (parse_resources[index_].name); + + sc_free (parse_resources); + parse_resources = NULL; + } + parse_resources_length = 0; + parse_resources_size = 0; +} + + +/* + * parse_get_v400_resource_offset() + * + * Notes version 4.0 resource names encountered in the parse, and their + * lengths, and builds up a list of resources with their data offsets. The + * function assumes that resources are appended to the TAF file in the + * order in which they are encountered when reading through the TAF file. + * + * A warning -- this function may return a new length. Resources that + * have been seen once already have non-useful (though apparently non-zero) + * lengths; this function needs to handle that. The caller needs to compare + * length with real_length to see if that happened. + */ +static sc_int +parse_get_v400_resource_offset (const sc_char *name, + sc_int length, sc_int *real_length) +{ + sc_char *clean_name; + sc_uint hash; + sc_int index_, offset; + + /* + * Take a copy of the name, and remove any trailing "##" looping sound + * indicator flag. Who thinks this junk up? + */ + clean_name = (sc_char *)sc_malloc (strlen (name) + 1); + strcpy (clean_name, name); + if (strcmp (clean_name + strlen (clean_name) - 2, "##") == 0) + clean_name[strlen (clean_name) - 2] = NUL; + + /* + * Scan the current resources list for a matching name, and if the resource + * is already known, return its offset. The hash check is an attempt to + * improve the search times, relative to using only string comparisons -- + * the table's not fully hashed. If found, we need to also pass back the + * corrected length. + */ + offset = -1; + hash = sc_hash (clean_name); + for (index_ = 0; index_ < parse_resources_length; index_++) + { + if (parse_resources[index_].hash == hash + && strcmp (parse_resources[index_].name, clean_name) == 0) + { + offset = parse_resources[index_].offset; + break; + } + } + if (offset != -1) + { + *real_length = parse_resources[index_].length; + sc_free (clean_name); + return offset; + } + + /* Resize the resources table if required. */ + if (parse_resources_length == parse_resources_size) + { + parse_resources_size += RESOURCE_GROW_INCREMENT; + parse_resources = (sc_parse_resource_t *)sc_realloc (parse_resources, + parse_resources_size * + sizeof (parse_resources[0])); + } + + /* + * Calculate the offset. For the first resource, it's zero; for others, + * it's one after the prior entry's offset and length. + */ + if (parse_resources_length == 0) + offset = 0; + else + { + offset = parse_resources[parse_resources_length - 1].offset + + parse_resources[parse_resources_length - 1].length + 1; + } + + /* Add details to the table. */ + parse_resources[parse_resources_length].name = clean_name; + parse_resources[parse_resources_length].hash = hash; + parse_resources[parse_resources_length].offset = offset; + parse_resources[parse_resources_length].length = length; + parse_resources_length++; + + *real_length = length; + return offset; +} + + +/* + * parse_handle_v400_resources() + * + * Extra special handling for version 4.0 resources; extracts details of + * the resource just parsed, and adds an offset property for each defined. + * + * A warning -- Adrift seems to use -ve numbers as lengths for resources + * already parsed, where TAF files include the resource. It's unclear + * what the -ve values mean, so here we ignore them and work off the + * resource file name given. This means we have to look for length not + * equal to zero, not just lengths greater than zero. + * + * TODO Work out what this means. The -ve lengths look like a form of + * 'resource number'; -(length+2) is tantalizingly close to the index into + * our parse_resources table, but not always... + */ +static void +parse_handle_v400_resources (sc_bool has_sound, sc_bool has_graphics) +{ + sc_vartype_t vt_key, vt_value; + const sc_char *file; + sc_int length, offset; + + /* + * Retrieve the file and length for the sound just parsed. If there's a + * file of non-zero length, rewrite its offset. + */ + if (has_sound) + { + /* Retrieve the file and length information. */ + vt_key.string = "SoundFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + file = parse_get_string_property (); + parse_pop_key (); + + vt_key.string = "SoundLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + length = parse_get_integer_property (); + parse_pop_key (); + + /* + * If defined and has a length, rewrite the offset, and also the length + * in case changed. + */ + if (!sc_strempty (file) && length != 0) + { + sc_int real_length; + + offset = parse_get_v400_resource_offset (file, length, &real_length); + vt_key.string = "SoundOffset"; + parse_push_key (vt_key, PROP_KEY_STRING); + + vt_value.integer = offset; + parse_put_property (vt_value, PROP_INTEGER); + + parse_pop_key (); + + /* Rewrite length if changed. */ + if (real_length != length) + { + vt_key.string = "SoundLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + + vt_value.integer = real_length; + parse_put_property (vt_value, PROP_INTEGER); + + parse_pop_key (); + } + } + } + + /* Now do the same thing for graphics. */ + if (has_graphics) + { + /* Retrieve the file and length information. */ + vt_key.string = "GraphicFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + file = parse_get_string_property (); + parse_pop_key (); + + vt_key.string = "GraphicLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + length = parse_get_integer_property (); + parse_pop_key (); + + /* + * If defined and has a length, rewrite the offset, and also the length + * in case changed. + */ + if (!sc_strempty (file) && length != 0) + { + sc_int real_length; + + offset = parse_get_v400_resource_offset (file, length, &real_length); + vt_key.string = "GraphicOffset"; + parse_push_key (vt_key, PROP_KEY_STRING); + + vt_value.integer = offset; + parse_put_property (vt_value, PROP_INTEGER); + + parse_pop_key (); + + /* Rewrite length if changed. */ + if (real_length != length) + { + vt_key.string = "GraphicLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + + vt_value.integer = real_length; + parse_put_property (vt_value, PROP_INTEGER); + + parse_pop_key (); + } + } + } +} + + +/* + * parse_special() + * + * Handler for special items that can't be described accurately, and + * therefore need careful treatment. + */ +static void +parse_special (const sc_char *special) +{ + if (parse_trace) + sc_trace ("Parse: entering special %s\n", special); + + /* Special handling for version 4.0 resources. */ + if (strcmp (special, "{V400_RESOURCE}") == 0) + { + sc_vartype_t vt_key[2]; + sc_bool has_sound, has_graphics; + + /* Get sound and graphics global flags. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "Sound"; + has_sound = prop_get_boolean (parse_bundle, "B<-ss", vt_key); + + vt_key[1].string = "Graphics"; + has_graphics = prop_get_boolean (parse_bundle, "B<-ss", vt_key); + + /* Apply special handling to the resources. */ + parse_handle_v400_resources (has_sound, has_graphics); + } + + /* Parse a version 4.0 optional set of room exit information. */ + else if (strcmp (special, "{V400_ROOM_EXIT:#Dest_#Var1_#Var2_#Var3}") == 0) + { + sc_int flag; + + /* Get next flag, and if true, pushback and parse. */ + flag = parse_get_taf_integer (); + if (flag != 0) + { + parse_taf_pushback (); + parse_descriptor ("#Dest #Var1 #Var2 #Var3"); + } + } + + /* Parse version 3.9 and version 3.8 optional room exit information. */ + else if (strcmp (special, + "{V390_V380_ROOM_EXIT:#Dest_#Var1_#Var2_ZVar3}") == 0) + { + sc_int flag; + + /* Get next flag, and if true, pushback and parse. */ + flag = parse_get_taf_integer (); + if (flag != 0) + { + parse_taf_pushback (); + parse_descriptor ("#Dest #Var1 #Var2 ZVar3"); + } + } + + /* Parse room lists, with optional extra room. */ + else if (strcmp (special, "{ROOM_LIST0}") == 0 + || strcmp (special, "{ROOM_LIST1}") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int room_count, num_rooms, type, index_; + + /* Retrieve the room list type. */ + vt_key.string = "Type"; + parse_push_key (vt_key, PROP_KEY_STRING); + type = parse_get_integer_property (); + parse_pop_key (); + + /* Write remaining room list depending on the type. */ + switch (type) + { + case ROOMLIST_NO_ROOMS: + case ROOMLIST_ALL_ROOMS: + case ROOMLIST_NPC_PART: + break; + + case ROOMLIST_ONE_ROOM: + /* Store this room as the single list entry. */ + parse_element ("#Room"); + break; + + case ROOMLIST_SOME_ROOMS: + /* Get count of rooms defined, add one if necessary. */ + vt_key.string = "Rooms"; + room_count = prop_get_child_count (parse_bundle, "I<-s", &vt_key); + + if (strcmp (special, "{ROOM_LIST1}") == 0) + num_rooms = room_count + 1; + else + num_rooms = room_count; + + /* Store an array of rooms flags for each room. */ + vt_key.string = "Rooms"; + parse_push_key (vt_key, PROP_KEY_STRING); + for (index_ = 0; index_ < num_rooms; index_++) + { + sc_bool this_room; + + /* Get flag for this room. */ + this_room = parse_get_taf_boolean (); + + /* Store flag directly. */ + vt_key.integer = index_; + parse_push_key (vt_key, PROP_KEY_INTEGER); + vt_value.boolean = this_room; + parse_put_property (vt_value, PROP_BOOLEAN); + parse_pop_key (); + } + parse_pop_key (); + break; + + default: + sc_fatal ("parse_special: bad type, %ld\n", type); + } + } + + /* Parse Parent number iff this object is an NPC part. */ + else if (strcmp (special, "{OBJECT:#Parent}") == 0) + { + sc_vartype_t vt_key; + sc_int type; + + /* Check object's Where room list Type for NPC part. */ + vt_key.string = "Where"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_key.string = "Type"; + parse_push_key (vt_key, PROP_KEY_STRING); + type = parse_get_integer_property (); + parse_pop_key (); + parse_pop_key (); + + /* Get Parent if the object is part of an NPC. */ + if (type == ROOMLIST_NPC_PART) + parse_element ("#Parent"); + } + + /* Parse a list of rooms and times for a walk. */ + else if (strcmp (special, "{WALK:#Rooms_#Times}") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int num_stops, index_; + + /* Obtain the count of stops in this walk. */ + vt_key.string = "NumStops"; + parse_push_key (vt_key, PROP_KEY_STRING); + num_stops = parse_get_integer_property (); + parse_pop_key (); + + /* Look for a room and time for each stop. */ + for (index_ = 0; index_ < num_stops; index_++) + { + sc_int room, time; + + /* Parse and store Rooms[index_]. */ + vt_key.string = "Rooms"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_key.integer = index_; + parse_push_key (vt_key, PROP_KEY_INTEGER); + + room = parse_get_taf_integer (); + + vt_value.integer = room; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + parse_pop_key (); + + /* Parse and store Times[index_]. */ + vt_key.string = "Times"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_key.integer = index_; + parse_push_key (vt_key, PROP_KEY_INTEGER); + + time = parse_get_taf_integer (); + + vt_value.integer = time; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + parse_pop_key (); + } + } + + /* Parse a room group variable size boolean list. */ + else if (strcmp (special, "{ROOM_GROUP:[]BList}") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int num_rooms, index_, l2index_; + sc_bool in_group; + + /* Get the count of rooms defined. */ + vt_key.string = "Rooms"; + num_rooms = prop_get_integer (parse_bundle, "I<-s", &vt_key); + + /* Read a boolean for each room. */ + l2index_ = 0; + for (index_ = 0; index_ < num_rooms; index_++) + { + in_group = parse_get_taf_boolean (); + + /* Store raw flag as List[index_]. */ + vt_key.string = "List"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_key.integer = index_; + parse_push_key (vt_key, PROP_KEY_INTEGER); + vt_value.boolean = in_group; + parse_put_property (vt_value, PROP_BOOLEAN); + + parse_pop_key (); + parse_pop_key (); + + /* Store in-group index'es as List2[0..n]. */ + if (in_group) + { + vt_key.string = "List2"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_key.integer = l2index_; + parse_push_key (vt_key, PROP_KEY_INTEGER); + vt_value.integer = index_; + parse_put_property (vt_value, PROP_INTEGER); + + parse_pop_key (); + parse_pop_key (); + + l2index_++; + } + } + } + + /* Error if no special handler available. */ + else + { + sc_fatal ("parse_special: no handler for \"%s\"\n", special); + } + + if (parse_trace) + sc_trace ("Parse: leaving special %s\n", special); +} + + +/* + * parse_fixup_v390_v380_room_alt() + * + * Helper for parse_fixup_v390_v380_room_alts(). Handles creation of + * version 4.0 room alts for version 3.9 and version 3.8 games. + */ +static void +parse_fixup_v390_v380_room_alt (const sc_char *m1, sc_int type, + const sc_char *resource1, + const sc_char *m2, sc_int var2, + const sc_char *resource2, + sc_int hide_objects, + const sc_char *changed, + sc_int var3, sc_int display_room) +{ + sc_vartype_t vt_key, vt_value, vt_gkey[2]; + sc_bool has_sound, has_graphics; + sc_int alt_count; + const sc_char *soundfile1, *graphicfile1; + const sc_char *soundfile2, *graphicfile2; + + /* + * Initialize resource files to empty, for cases where no resource is copied + * over from the main room (NULL resource1/2). + */ + soundfile1 = ""; + graphicfile1 = ""; + soundfile2 = ""; + graphicfile2 = ""; + + /* Get sound and graphics flags, always FALSE for version 3.8. */ + vt_gkey[0].string = "Globals"; + vt_gkey[1].string = "Sound"; + has_sound = prop_get_boolean (parse_bundle, "B<-ss", vt_gkey); + + vt_gkey[1].string = "Graphics"; + has_graphics = prop_get_boolean (parse_bundle, "B<-ss", vt_gkey); + + /* Get a count of alts so far defined for the room. */ + vt_key.string = "Alts"; + parse_push_key (vt_key, PROP_KEY_STRING); + alt_count = parse_get_child_count (); + parse_pop_key (); + + /* + * Lookup any resource details now, and save them. Because this is not + * version 4.0, we can ignore lengths, and set them to zero when needed. + */ + if (has_sound || has_graphics) + { + if (resource1) + { + vt_key.string = resource1; + parse_push_key (vt_key, PROP_KEY_STRING); + if (has_sound) + { + vt_key.string = "SoundFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + soundfile1 = parse_get_string_property (); + parse_pop_key (); + } + if (has_graphics) + { + vt_key.string = "GraphicFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + graphicfile1 = parse_get_string_property (); + parse_pop_key (); + } + parse_pop_key (); + } + + if (resource2) + { + vt_key.string = resource2; + parse_push_key (vt_key, PROP_KEY_STRING); + if (has_sound) + { + vt_key.string = "SoundFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + soundfile2 = parse_get_string_property (); + parse_pop_key (); + } + if (has_graphics) + { + vt_key.string = "GraphicFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + graphicfile2 = parse_get_string_property (); + parse_pop_key (); + } + parse_pop_key (); + } + } + + /* + * Create a room alt to match data passed in. Start with the Alts string + * and the index to the alt being written. To correctly emulate the parse, + * we also have to reverse the "Alts" and the index, as parse_put_property() + * will swap them. Madness. + */ + vt_key.integer = alt_count; + parse_push_key (vt_key, PROP_KEY_INTEGER); + vt_key.string = "Alts"; + parse_push_key (vt_key, PROP_KEY_STRING); + + /* Write M1 and Type. */ + vt_key.string = "M1"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = m1; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + vt_key.string = "Type"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = type; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + /* If resources, add these as retrieved above. */ + if (has_sound || has_graphics) + { + vt_key.string = "Res1"; + parse_push_key (vt_key, PROP_KEY_STRING); + if (has_sound) + { + vt_key.string = "SoundFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = soundfile1; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + vt_key.string = "SoundLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = 0; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + if (has_graphics) + { + vt_key.string = "GraphicFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = graphicfile1; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + vt_key.string = "GraphicLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = 0; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + parse_pop_key (); + } + + /* Write M2 and Var2. */ + vt_key.string = "M2"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = m2; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + vt_key.string = "Var2"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var2; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + /* If resources, again add these as retrieved above. */ + if (has_sound || has_graphics) + { + vt_key.string = "Res2"; + parse_push_key (vt_key, PROP_KEY_STRING); + if (has_sound) + { + vt_key.string = "SoundFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = soundfile2; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + vt_key.string = "SoundLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = 0; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + if (has_graphics) + { + vt_key.string = "GraphicFile"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = graphicfile2; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + vt_key.string = "GraphicLen"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = 0; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + parse_pop_key (); + } + + /* Finish off with the last four alt properties. */ + vt_key.string = "HideObjects"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = hide_objects; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + vt_key.string = "Changed"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = changed; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + vt_key.string = "Var3"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var3; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + vt_key.string = "DisplayRoom"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = display_room; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + parse_pop_key (); + parse_pop_key (); +} + + +/* Multiplier for combination AltDesc Type and HideObject values. */ +enum { V390_V380_ALT_TYPEHIDE_MULT = 10 }; + +/* + * parse_fixup_v390_v380_room_alts() + * + * Common helper function for parse_fixup_v390() and parse_fixup_v380(), + * converts version 3.9 and version 3.8 fixed room description alts into + * an equivalent array of version 4.0 style room alts. + */ +static void +parse_fixup_v390_v380_room_alts (void) +{ + sc_vartype_t vt_key; + const sc_char *m1, *m2, *changed; + sc_int type, var2, hide_objects, var3, display_room; + + /* Room alt invariants. */ + m2 = ""; /* No else text */ + changed = ""; /* No changed room name */ + + /* + * Create a room alt to override all others, controlled by an object + * condition and with optional object hiding. + */ + type = 2; /* Object condition */ + display_room = 0; /* Override all others */ + + vt_key.string = "Obj"; + parse_push_key (vt_key, PROP_KEY_STRING); + var3 = parse_get_integer_property (); + parse_pop_key (); + + if (var3 > 0) + { + sc_int typehideobjects; + + vt_key.string = "AltDesc"; + parse_push_key (vt_key, PROP_KEY_STRING); + m1 = parse_get_string_property (); + parse_pop_key (); + + vt_key.string = "TypeHideObjects"; + parse_push_key (vt_key, PROP_KEY_STRING); + typehideobjects = parse_get_integer_property (); + parse_pop_key (); + + var2 = typehideobjects / V390_V380_ALT_TYPEHIDE_MULT; + hide_objects = typehideobjects % V390_V380_ALT_TYPEHIDE_MULT; + + parse_fixup_v390_v380_room_alt (m1, type, "AltRes", + m2, var2, NULL, + hide_objects, changed, var3, + display_room); + } + + /* + * If a second task alternate description is defined, create a room alt to + * add after the main description, one that stops printing once done. + */ + type = 0; /* Task condition */ + display_room = 1; /* Print after main and stop */ + + vt_key.string = "Task2"; + parse_push_key (vt_key, PROP_KEY_STRING); + var2 = parse_get_integer_property (); + parse_pop_key (); + + if (var2 > 0) + { + vt_key.string = "AddDesc2"; + parse_push_key (vt_key, PROP_KEY_STRING); + m1 = parse_get_string_property (); + parse_pop_key (); + + var3 = 0; + hide_objects = 0; + + parse_fixup_v390_v380_room_alt (m1, type, "Task2Res", + m2, var2, NULL, + hide_objects, changed, var3, + display_room); + } + + /* Do the same for any first task additional description. */ + type = 0; /* Task condition */ + display_room = 1; /* Print after main and stop */ + + vt_key.string = "Task1"; + parse_push_key (vt_key, PROP_KEY_STRING); + var2 = parse_get_integer_property (); + parse_pop_key (); + + if (var2 > 0) + { + vt_key.string = "AddDesc1"; + parse_push_key (vt_key, PROP_KEY_STRING); + m1 = parse_get_string_property (); + parse_pop_key (); + + var3 = 0; + hide_objects = 0; + + parse_fixup_v390_v380_room_alt (m1, type, "Task1Res", + m2, var2, NULL, + hide_objects, changed, var3, + display_room); + } + + /* + * If still printing at this point, we need a catch-all room alt that will + * print. So create one with an always true condition. + */ + type = 0; /* Task condition */ + display_room = 2; /* Lowest priority output */ + + vt_key.string = "LastDesc"; + parse_push_key (vt_key, PROP_KEY_STRING); + m1 = parse_get_string_property (); + parse_pop_key (); + + if (!sc_strempty (m1)) + { + var2 = 0; /* No task - always TRUE */ + var3 = 0; + hide_objects = 0; + + parse_fixup_v390_v380_room_alt (m1, type, "LastRes", + m2, var2, NULL, + hide_objects, changed, var3, + display_room); + } +} + + +/* + * parse_fixup_v390() + * + * Handler for fixup special items to help with conversions from TAF version + * 3.9 format into version 4.0. + */ +static void +parse_fixup_v390 (const sc_char *fixup) +{ + if (parse_trace) + sc_trace ("Parse: entering version 3.9 fixup %s\n", fixup); + + /* Fixup a version 3.9 task action by incrementing Type > 4. */ + if (strcmp (fixup, "|V390_TASK_ACTION:Type>4?#Type++|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int type; + + /* Retrieve Type, and if > 4, increment. */ + vt_key.string = "Type"; + parse_push_key (vt_key, PROP_KEY_STRING); + type = parse_get_integer_property (); + + if (type > 4) + { + vt_value.integer = type + 1; + parse_put_property (vt_value, PROP_INTEGER); + } + + parse_pop_key (); + } + + /* Handle either Expr or Var5 for version 3.9 task actions. */ + else if (strcmp (fixup, "|V390_TASK_ACTION:$Expr_#Var5|") == 0) + { + sc_vartype_t vt_key; + sc_int var2; + + /* Either Expr or Var5, depending on Var2. */ + vt_key.string = "Var2"; + parse_push_key (vt_key, PROP_KEY_STRING); + var2 = parse_get_integer_property (); + parse_pop_key (); + + if (var2 == 5) + parse_descriptor ("$Expr ZVar5"); + else + parse_descriptor ("EExpr #Var5"); + } + + /* + * Exchange openable values 5 and 6, and write -1 key for openable objects. + */ + else if (strcmp (fixup, "|V390_OBJECT:_Openable_,Key|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int openable; + + /* Retrieve Openable, and if 5 or 6, exchange. */ + vt_key.string = "Openable"; + parse_push_key (vt_key, PROP_KEY_STRING); + openable = parse_get_integer_property (); + + if (openable == 5 || openable == 6) + { + vt_value.integer = (openable == 5) ? 6 : 5; + parse_put_property (vt_value, PROP_INTEGER); + } + + parse_pop_key (); + + /* For openable objects, store a Key of -1. */ + if (openable == 5 || openable == 6) + { + vt_key.string = "Key"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = -1; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + } + + /* Create a RestrMask that 'and's all the restrictions together. */ + else if (strcmp (fixup, "|V390_TASK:$RestrMask|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int restriction_count; + + /* Get a count of restrictions. */ + vt_key.string = "Restrictions"; + parse_push_key (vt_key, PROP_KEY_STRING); + restriction_count = parse_get_child_count (); + parse_pop_key (); + + /* Allocate and fill a new mask for these restrictions. */ + if (restriction_count > 0) + { + sc_char *restrmask; + sc_int index_; + + restrmask = (sc_char *)sc_malloc (2 * restriction_count); + strcpy (restrmask, "#"); + for (index_ = 1; index_ < restriction_count; index_++) + strcat (restrmask, "A#"); + + vt_key.string = "RestrMask"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = restrmask; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + + prop_adopt (parse_bundle, restrmask); + } + } + + /* + * Increment var1 for variable restrictions to compensate for there being no + * referenced text comparison (no string variables). + */ + else if (strcmp (fixup, "|V390_TASK_RESTR:Var1>0?#Var1++|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int var1; + + /* Retrieve Var1, and if greater than zero, increment. */ + vt_key.string = "Var1"; + parse_push_key (vt_key, PROP_KEY_STRING); + var1 = parse_get_integer_property (); + + if (var1 > 0) + { + vt_value.integer = var1 + 1; + parse_put_property (vt_value, PROP_INTEGER); + } + + parse_pop_key (); + } + + /* Convert version 3.9 fixed alts into a version 4.0 array. */ + else if (strcmp (fixup, "|V390_ROOM:_Alts_|") == 0) + { + parse_fixup_v390_v380_room_alts (); + } + + /* Error if no fixup special handler available. */ + else + { + sc_fatal ("parse_fixup_v390: no handler for \"%s\"\n", fixup); + } + + if (parse_trace) + sc_trace ("Parse: leaving version 3.9 fixup %s\n", fixup); +} + + +/* + * Object surface and container masks for version 3.8 object fixup, container + * capacity conversion factor and default object sizing, and the count of + * task movements in a version 3.8 task. + */ +enum { V380_OBJ_IS_SURFACE = 2, V380_OBJ_IS_CONTAINER = 1 }; +enum { V380_OBJ_CAPACITY_MULT = 10, V380_OBJ_DEFAULT_SIZE = 2 }; +enum { V380_TASK_MOVEMENTS = 6 }; + +/* + * parse_fixup_v380_action() + * + * Helper for parse_fixup_v380(), adds a task action. + */ +static void +parse_fixup_v380_action (sc_int type, sc_int var_count, + sc_int var1, sc_int var2, sc_int var3) +{ + sc_vartype_t vt_key, vt_value; + sc_int action_count; + + /* Get a count of actions so far defined for the task. */ + vt_key.string = "Actions"; + parse_push_key (vt_key, PROP_KEY_STRING); + action_count = parse_get_child_count (); + parse_pop_key (); + + /* Write actions key, reversed to emulate parse actions. */ + vt_key.integer = action_count; + parse_push_key (vt_key, PROP_KEY_INTEGER); + vt_key.string = "Actions"; + parse_push_key (vt_key, PROP_KEY_STRING); + + /* Write new action according to the given arguments. */ + vt_key.string = "Type"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = type; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + vt_key.string = "Var1"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var1; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + if (var_count > 1) + { + vt_key.string = "Var2"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var2; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + + if (var_count > 2) + { + vt_key.string = "Var3"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var3; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + + parse_pop_key (); + parse_pop_key (); +} + + +/* + * parse_fixup_v380_movement() + * + * Helper for parse_fixup_v380(), converts a task movement into an action. + */ +static void +parse_fixup_v380_movement (sc_int mvar1, sc_int mvar2, sc_int mvar3) +{ + sc_int var1; + + /* If nothing was selected to move, ignore the call. */ + if (mvar1 == 0) + return; + + /* + * Accept only player moves into rooms. Other combinations, such as move + * player to worn by player, are unlikely. And move player to same room as + * player isn't useful. + */ + if (mvar1 == 1) + { + if (mvar3 == 0 && mvar2 >= 2) + parse_fixup_v380_action (1, 3, 0, 0, mvar2 - 2); + return; + } + + /* + * Convert movement var1 into action var1. Var1 is the dynamic object + 3, + * or 2 for referenced object, or 0 for all held. + */ + switch (mvar1) + { + case 2: + var1 = 2; + break; /* Referenced obj */ + case 3: + var1 = 0; + break; /* All held */ + default: + var1 = mvar1 - 1; + break; /* Dynamic obj */ + } + + /* Dissect the rest of the movement. */ + switch (mvar3) + { + case 0: /* To room */ + /* + * Convert movement var2 into action var2 and var3. Var2 is 0 for move + * to room, 6 for move to player room. Var3 is 0 for hidden, otherwise + * the room number plus one. + */ + if (mvar2 == 0) /* Hidden */ + parse_fixup_v380_action (0, 3, var1, 0, 0); + else if (mvar2 == 1) /* Player room */ + parse_fixup_v380_action (0, 3, var1, 6, 0); + else /* Specified room */ + parse_fixup_v380_action (0, 3, var1, 0, mvar2 - 1); + break; + + case 1: /* To inside */ + case 2: /* To onto */ + /* + * Convert movement var2 and var3 into action var3 and var2, a simple + * conversion, but check that var2 is not 'not selected' first. + */ + if (mvar2 > 0) + parse_fixup_v380_action (0, 3, var1, mvar3 + 1, mvar2 - 1); + break; + + case 3: /* To held by */ + case 4: /* To worn by */ + /* + * Convert movement var2 and var3 into action var3 and var2, in this + * case a simple conversion, since version 4.0 task actions are close + * here. + */ + parse_fixup_v380_action (0, 3, var1, mvar3 + 1, mvar2); + break; + + default: + sc_fatal ("parse_fixup_v380_movement: invalid mvar3, %ld\n", mvar3); + } +} + + +/* + * parse_fixup_v380_restr() + * + * Helper for parse_fixup_v380(), adds a task restriction. + */ +static void +parse_fixup_v380_restr (sc_int type, sc_int var_count, + sc_int var1, sc_int var2, sc_int var3, + const sc_char *failmessage) +{ + sc_vartype_t vt_key, vt_value; + sc_int restriction_count; + + /* Get a count of restrictions so far defined for the task. */ + vt_key.string = "Restrictions"; + parse_push_key (vt_key, PROP_KEY_STRING); + restriction_count = parse_get_child_count (); + parse_pop_key (); + + /* Write restrictions key, reversed to emulate parse actions. */ + vt_key.integer = restriction_count; + parse_push_key (vt_key, PROP_KEY_INTEGER); + vt_key.string = "Restrictions"; + parse_push_key (vt_key, PROP_KEY_STRING); + + /* Write new restriction according to the given arguments. */ + vt_key.string = "Type"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = type; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + vt_key.string = "Var1"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var1; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + if (var_count > 1) + { + vt_key.string = "Var2"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var2; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + + if (var_count > 2) + { + vt_key.string = "Var3"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = var3; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + + vt_key.string = "FailMessage"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = failmessage; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + + parse_pop_key (); + parse_pop_key (); +} + + +/* + * parse_fixup_v380_obj_restr() + * parse_fixup_v380_task_restr() + * parse_fixup_v380_wear_restr() + * parse_fixup_v380_npc_restr() + * parse_fixup_v380_objroom_restr() + * parse_fixup_v380_objstate_restr() + * + * Helper handlers for parse_fixup_v380(); create task restrictions. + */ +static void +parse_fixup_v380_obj_restr (sc_bool holding, + sc_int holdobj, const sc_char *failmessage) +{ + /* Ignore if no object selected. */ + if (holdobj > 0) + { + sc_int var1, var2; + + /* + * Create version 4.0 task restriction to check for either the + * referenced object or a dynamic object being either held or in the + * same room (visible to player). + */ + var1 = (holdobj == 1) ? 2 : holdobj + 1; + var2 = holding ? 1 : 3; + parse_fixup_v380_restr (0, 3, var1, var2, 0, failmessage); + } +} + +static void +parse_fixup_v380_task_restr (sc_bool tasknotdone, sc_int task, + const sc_char *failmessage) +{ + /* Ignore if no task selected. */ + if (task > 0) + { + sc_int var2; + + /* Create version 4.0 restriction to check task state. */ + var2 = tasknotdone ? 1 : 0; + parse_fixup_v380_restr (2, 2, task, var2, 0, failmessage); + } +} + +static void +parse_fixup_v380_wear_restr (sc_int wearobj, const sc_char *failmessage) +{ + /* Ignore if no object selected. */ + if (wearobj > 0) + { + sc_vartype_t vt_key[3]; + sc_int object_count, object, dynamic, obj_index; + + /* + * Create version 4.0 restrictions for something or nothing worn by + * player. + */ + if (wearobj == 1) + { + parse_fixup_v380_restr (0, 3, 1, 2, 0, failmessage); + return; + } + else if (wearobj == 2) + { + parse_fixup_v380_restr (0, 3, 0, 2, 0, failmessage); + return; + } + + /* Get the count of objects defined. */ + vt_key[0].string = "Objects"; + object_count = prop_get_child_count (parse_bundle, "I<-s", vt_key); + + /* Convert wearobj from worn index to object index. */ + wearobj -= 2; + for (object = 0; object < object_count && wearobj > 0; object++) + { + sc_bool bstatic, wearable; + + vt_key[1].integer = object; + vt_key[2].string = "Static"; + bstatic = prop_get_boolean (parse_bundle, "B<-sis", vt_key); + if (!bstatic) + { + vt_key[2].string = "Wearable"; + wearable = prop_get_boolean (parse_bundle, "B<-sis", vt_key); + if (wearable) + wearobj--; + } + } + obj_index = object - 1; + + /* Now convert wearobj from object index to dynamic index. */ + dynamic = 0; + for (object = 0; object <= obj_index; object++) + { + sc_bool bstatic; + + vt_key[1].integer = object; + vt_key[2].string = "Static"; + bstatic = prop_get_boolean (parse_bundle, "B<-sis", vt_key); + if (!bstatic) + dynamic++; + } + dynamic--; + + /* Create version 4.0 restriction for object worn by player. */ + parse_fixup_v380_restr (0, 3, dynamic + 3, 2, 0, failmessage); + } +} + +static void +parse_fixup_v380_npc_restr (sc_bool notinsameroom, sc_int npc, + const sc_char *failmessage) +{ + /* Ignore if no NPC selected. */ + if (npc > 0) + { + sc_int var2; + + if (npc == 1) + { + /* Create restriction to look for alone, or not. */ + var2 = notinsameroom ? 3 : 2; + parse_fixup_v380_restr (3, 3, 0, var2, 0, failmessage); + return; + } + + /* Create restriction to look for company. */ + var2 = notinsameroom ? 1 : 0; + parse_fixup_v380_restr (3, 3, 0, var2, npc, failmessage); + } +} + +static void +parse_fixup_v380_objroom_restr (sc_int obj, sc_int objroom, + const sc_char *failmessage) +{ + /* Ignore if no object selected. */ + if (obj > 0) + { + /* Create version 4.0 restriction to check object in room. */ + parse_fixup_v380_restr (0, 3, obj + 1, 0, objroom, failmessage); + } +} + +static void +parse_fixup_v380_objstate_restr (sc_int obj, sc_int ivar1, sc_int ivar2, + const sc_char *failmessage) +{ + sc_vartype_t vt_key[3]; + sc_int object, dynamic, var2, var3; + + /* Initialize variables to avoid gcc warnings. */ + var2 = -1; + var3 = -1; + + /* Ignore restrictions with no "type". */ + if (ivar1 == 0) + return; + + /* Look for opened/closed restrictions, convert and return. */ + if (ivar1 == 3 || ivar1 == 4) + { + sc_int stateful; + + /* Convert obj from object to openable (stateful) index. */ + stateful = 0; + for (object = 0; object <= obj - 1; object++) + { + sc_int openable; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Openable"; + openable = prop_get_integer (parse_bundle, "I<-sis", vt_key); + if (openable > 0) + stateful++; + } + stateful--; + + /* + * Create a version 4.0 restriction that checks that an object's state + * is open (var2 = 0) or closed (var2 = 1). + */ + var2 = (ivar1 == 3) ? 0 : 1; + parse_fixup_v380_restr (1, 2, stateful + 1, var2, 0, failmessage); + return; + } + + /* Convert obj from object to dynamic index. */ + dynamic = 0; + for (object = 0; object <= obj - 1; object++) + { + sc_bool bstatic; + + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Static"; + bstatic = prop_get_boolean (parse_bundle, "B<-sis", vt_key); + if (!bstatic) + dynamic++; + } + dynamic--; + + /* Create version 4.0 object location restrictions for the rest. */ + switch (ivar1) + { + case 1: + var2 = 4; + var3 = ivar2; + break; /* Inside */ + case 2: + var2 = 5; + var3 = ivar2; + break; /* On */ + case 5: + var2 = 1; + var3 = ivar2 + 1; + break; /* Held by */ + case 6: + var2 = 2; + var3 = ivar2 + 1; + break; /* Worn by */ + default: + sc_fatal ("parse_fixup_v380_objstate_restr: invalid ivar1, %ld\n", ivar1); + } + parse_fixup_v380_restr (0, 3, dynamic + 3, var2, var3, failmessage); +} + + +/* + * parse_fixup_v380() + * + * Handler for fixup special items to help with conversions from TAF version + * 3.8 format into version 4.0. + */ +static void +parse_fixup_v380 (const sc_char *fixup) +{ + if (parse_trace) + sc_trace ("Parse: entering version 3.8 fixup %s\n", fixup); + + /* Convert container capacity attributes to version 4.0 values. */ + if (strcmp (fixup, "|V380_OBJECT:#Capacity*10+2|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int surfacecontainer; + + /* Get the object surface and container attributes. */ + vt_key.string = "SurfaceContainer"; + parse_push_key (vt_key, PROP_KEY_STRING); + surfacecontainer = parse_get_integer_property (); + parse_pop_key (); + + /* Convert capacity from version 3.8 format to version 4.0. */ + if (surfacecontainer == V380_OBJ_IS_CONTAINER) + { + sc_int capacity; + + vt_key.string = "Capacity"; + parse_push_key (vt_key, PROP_KEY_STRING); + capacity = parse_get_integer_property (); + + capacity = capacity * V380_OBJ_CAPACITY_MULT + V380_OBJ_DEFAULT_SIZE; + + vt_value.integer = capacity; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + } + + /* + * Exchange openable values 5 and 6, watch for a possible 1 from a 3.8 game + * (interpret as 0), and write -1 key for openable objects. + */ + else if (strcmp (fixup, "|V380_OBJECT:_Openable_,Key|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int openable; + + /* Retrieve Openable, and if 5 or 6, exchange. */ + vt_key.string = "Openable"; + parse_push_key (vt_key, PROP_KEY_STRING); + openable = parse_get_integer_property (); + + if (openable == 5 || openable == 6) + { + vt_value.integer = (openable == 5) ? 6 : 5; + parse_put_property (vt_value, PROP_INTEGER); + } + + /* If the odd value of 1, rewrite as zero. */ + else if (openable == 1) + { + vt_value.integer = 0; + parse_put_property (vt_value, PROP_INTEGER); + } + + parse_pop_key (); + + /* For openable objects, store a Key of -1. */ + if (openable == 5 || openable == 6) + { + vt_key.string = "Key"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.integer = -1; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + } + + /* Create version 4.0 task actions from a version 3.8 task. */ + else if (strcmp (fixup, "|V380_TASK:_Actions_|") == 0) + { + sc_vartype_t vt_key; + sc_int score; + sc_bool killsplayer, wingame; + sc_int movement; + + /* Retrieve the score change for the task. */ + vt_key.string = "Score"; + parse_push_key (vt_key, PROP_KEY_STRING); + score = parse_get_integer_property (); + parse_pop_key (); + + /* Create any appropriate score change action. */ + if (score != 0) + parse_fixup_v380_action (4, 1, score, 0, 0); + + /* Get player death and game winning flags. */ + vt_key.string = "KillsPlayer"; + parse_push_key (vt_key, PROP_KEY_STRING); + killsplayer = parse_get_boolean_property (); + parse_pop_key (); + vt_key.string = "WinGame"; + parse_push_key (vt_key, PROP_KEY_STRING); + wingame = parse_get_boolean_property (); + parse_pop_key (); + + /* Create any appropriate game ending actions. */ + if (killsplayer) + parse_fixup_v380_action (6, 1, 2, 0, 0); + if (wingame) + parse_fixup_v380_action (6, 1, 0, 0, 0); + + /* Handle each defined movement for the task. */ + for (movement = 0; movement < V380_TASK_MOVEMENTS; movement++) + { + sc_int mvar1, mvar2, mvar3; + + vt_key.integer = movement; + parse_push_key (vt_key, PROP_KEY_INTEGER); + vt_key.string = "Movements"; + parse_push_key (vt_key, PROP_KEY_STRING); + + /* Retrieve the movement parameters. */ + vt_key.string = "Var1"; + parse_push_key (vt_key, PROP_KEY_STRING); + mvar1 = parse_get_integer_property (); + parse_pop_key (); + vt_key.string = "Var2"; + parse_push_key (vt_key, PROP_KEY_STRING); + mvar2 = parse_get_integer_property (); + parse_pop_key (); + vt_key.string = "Var3"; + parse_push_key (vt_key, PROP_KEY_STRING); + mvar3 = parse_get_integer_property (); + parse_pop_key (); + + parse_pop_key (); + parse_pop_key (); + + /* Create the corresponding task action. */ + parse_fixup_v380_movement (mvar1, mvar2, mvar3); + } + } + + /* Create version 4.0 task restrictions from a version 3.8 task. */ + else if (strcmp (fixup, "|V380_TASK:_Restrictions_|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_bool holding, tasknotdone, notinsameroom; + sc_int holdobj1, holdobj2, holdobj3, task; + sc_int wearobj1, wearobj2, npc, obj1, obj1room, obj2; + const sc_char *holdmsg, *taskmsg, *wearmsg, *companymsg; + const sc_char *obj1msg; + sc_int restriction_count; + + /* Create restrictions for objects not held or absent. */ + vt_key.string = "HoldingSameRoom"; + parse_push_key (vt_key, PROP_KEY_STRING); + holding = parse_get_boolean_property (); + parse_pop_key (); + + vt_key.string = "HoldObj1"; + parse_push_key (vt_key, PROP_KEY_STRING); + holdobj1 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "HoldObj2"; + parse_push_key (vt_key, PROP_KEY_STRING); + holdobj2 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "HoldObj3"; + parse_push_key (vt_key, PROP_KEY_STRING); + holdobj3 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "HoldMsg"; + parse_push_key (vt_key, PROP_KEY_STRING); + holdmsg = parse_get_string_property (); + parse_pop_key (); + + parse_fixup_v380_obj_restr (holding, holdobj1, holdmsg); + parse_fixup_v380_obj_restr (holding, holdobj2, holdmsg); + parse_fixup_v380_obj_restr (holding, holdobj3, holdmsg); + + /* Create any task state restriction. */ + vt_key.string = "TaskNotDone"; + parse_push_key (vt_key, PROP_KEY_STRING); + tasknotdone = parse_get_boolean_property (); + parse_pop_key (); + + vt_key.string = "Task"; + parse_push_key (vt_key, PROP_KEY_STRING); + task = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "TaskMsg"; + parse_push_key (vt_key, PROP_KEY_STRING); + taskmsg = parse_get_string_property (); + parse_pop_key (); + + parse_fixup_v380_task_restr (tasknotdone, task, taskmsg); + + /* Create any object not worn restrictions. */ + vt_key.string = "WearObj1"; + parse_push_key (vt_key, PROP_KEY_STRING); + wearobj1 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "WearObj2"; + parse_push_key (vt_key, PROP_KEY_STRING); + wearobj2 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "WearMsg"; + parse_push_key (vt_key, PROP_KEY_STRING); + wearmsg = parse_get_string_property (); + parse_pop_key (); + + parse_fixup_v380_wear_restr (wearobj1, wearmsg); + parse_fixup_v380_wear_restr (wearobj2, wearmsg); + + /* Check for presence/absence of NPCs restriction. */ + vt_key.string = "NotInSameRoom"; + parse_push_key (vt_key, PROP_KEY_STRING); + notinsameroom = parse_get_boolean_property (); + parse_pop_key (); + + vt_key.string = "NPC"; + parse_push_key (vt_key, PROP_KEY_STRING); + npc = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "CompanyMsg"; + parse_push_key (vt_key, PROP_KEY_STRING); + companymsg = parse_get_string_property (); + parse_pop_key (); + + parse_fixup_v380_npc_restr (notinsameroom, npc, companymsg); + + /* Create any object location restriction. */ + vt_key.string = "Obj1"; + parse_push_key (vt_key, PROP_KEY_STRING); + obj1 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "Obj1Room"; + parse_push_key (vt_key, PROP_KEY_STRING); + obj1room = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "Obj1Msg"; + parse_push_key (vt_key, PROP_KEY_STRING); + obj1msg = parse_get_string_property (); + parse_pop_key (); + + parse_fixup_v380_objroom_restr (obj1, obj1room, obj1msg); + + /* And finally, any object state restriction. */ + vt_key.string = "Obj2"; + parse_push_key (vt_key, PROP_KEY_STRING); + obj2 = parse_get_integer_property (); + parse_pop_key (); + + if (obj2 > 0) + { + sc_int var1, var2; + const sc_char *obj2msg; + + vt_key.string = "Obj2Var1"; + parse_push_key (vt_key, PROP_KEY_STRING); + var1 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "Obj2Var2"; + parse_push_key (vt_key, PROP_KEY_STRING); + var2 = parse_get_integer_property (); + parse_pop_key (); + + vt_key.string = "Obj2Msg"; + parse_push_key (vt_key, PROP_KEY_STRING); + obj2msg = parse_get_string_property (); + parse_pop_key (); + + parse_fixup_v380_objstate_restr (obj2, var1, var2, obj2msg); + } + + /* Get a count of restrictions created. */ + vt_key.string = "Restrictions"; + parse_push_key (vt_key, PROP_KEY_STRING); + restriction_count = parse_get_child_count (); + parse_pop_key (); + + /* Allocate and fill a new mask for these restrictions. */ + if (restriction_count > 0) + { + sc_char *restrmask; + sc_int index_; + + restrmask = (sc_char *)sc_malloc (2 * restriction_count); + strcpy (restrmask, "#"); + for (index_ = 1; index_ < restriction_count; index_++) + strcat (restrmask, "A#"); + + vt_key.string = "RestrMask"; + parse_push_key (vt_key, PROP_KEY_STRING); + vt_value.string = restrmask; + parse_put_property (vt_value, PROP_STRING); + parse_pop_key (); + + prop_adopt (parse_bundle, restrmask); + } + } + + /* + * Adjust dynamic object initial positions and parents (where contained + * or on surfaces) into version 4.0 range. + */ + else if (strcmp (fixup, "|V380_OBJECT:_InitialPositions_|") == 0) + { + sc_vartype_t vt_key[3]; + sc_int object_count, object, *object_type; + + /* Get a count of objects. */ + vt_key[0].string = "Objects"; + object_count = prop_get_child_count (parse_bundle, "I<-s", vt_key); + + /* Build an array of object container/surface types. */ + object_type = (sc_int *)sc_malloc (object_count * sizeof (*object_type)); + for (object = 0; object < object_count; object++) + { + vt_key[1].integer = object; + vt_key[2].string = "SurfaceContainer"; + object_type[object] = prop_get_integer (parse_bundle, + "I<-sis", vt_key); + } + + /* Adjust each object's initial position if necessary. */ + for (object = 0; object < object_count; object++) + { + sc_vartype_t vt_value; + sc_bool is_static; + sc_int initialposition; + + /* Ignore static objects; we only want dynamic ones. */ + vt_key[1].integer = object; + vt_key[2].string = "Static"; + is_static = prop_get_boolean (parse_bundle, "B<-sis", vt_key); + if (is_static) + continue; + + /* If initial position is above on/in, increment. */ + vt_key[1].integer = object; + vt_key[2].string = "InitialPosition"; + initialposition = prop_get_integer (parse_bundle, "I<-sis", vt_key); + if (initialposition > 2) + { + vt_value.integer = initialposition + 1; + prop_put (parse_bundle, "I->sis", vt_value, vt_key); + } + + /* + * If initial position is on or in, decide which, depending on the + * type of the parent. From this, expand initial position into a + * version 4.0 value. + */ + if (initialposition == 2) + { + sc_int count, parent, index_; + + /* Get parent container/surface index. */ + vt_key[1].integer = object; + vt_key[2].string = "Parent"; + count = prop_get_integer (parse_bundle, "I<-sis", vt_key); + + /* Convert container/surface index. */ + for (parent = 0; parent < object_count && count >= 0; parent++) + { + if (object_type[parent] == V380_OBJ_IS_CONTAINER + || object_type[parent] == V380_OBJ_IS_SURFACE) + count--; + } + parent--; + + /* If parent is a surface, adjust position. */ + if (object_type[parent] == V380_OBJ_IS_SURFACE) + { + vt_key[2].string = "InitialPosition"; + vt_value.integer = initialposition + 1; + prop_put (parse_bundle, "I->sis", vt_value, vt_key); + } + + /* + * For both, adjust parent to be an object index for that type + * of object only. + */ + count = 0; + for (index_ = 0; index_ < parent; index_++) + { + if (object_type[index_] == object_type[parent]) + count++; + } + vt_key[2].string = "Parent"; + vt_value.integer = count; + prop_put (parse_bundle, "I->sis", vt_value, vt_key); + } + } + + /* Done with temporary array. */ + sc_free (object_type); + } + + /* Convert carry limit into version 4.0-like size and weight limits. */ + else if (strcmp (fixup, "|V380_MaxSize_MaxWt_|") == 0) + { + sc_vartype_t vt_key, vt_value; + sc_int maxcarried; + + vt_key.string = "MaxCarried"; + parse_push_key (vt_key, PROP_KEY_STRING); + maxcarried = parse_get_integer_property (); + parse_pop_key (); + + vt_value.integer = maxcarried * V380_OBJ_CAPACITY_MULT + + V380_OBJ_DEFAULT_SIZE; + + vt_key.string = "MaxSize"; + parse_push_key (vt_key, PROP_KEY_STRING); + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + + vt_key.string = "MaxWt"; + parse_push_key (vt_key, PROP_KEY_STRING); + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + + /* Add up positive scoring tasks to arrive at max score. */ + else if (strcmp (fixup, "|V380_GLOBAL:_MaxScore_|") == 0) + { + sc_vartype_t vt_key[3], vt_value; + sc_int task_count, maxscore, task; + + /* Get a count of tasks. */ + vt_key[0].string = "Tasks"; + task_count = prop_get_child_count (parse_bundle, "I<-s", vt_key); + + /* Sum positive scoring tasks. */ + maxscore = 0; + for (task = 0; task < task_count; task++) + { + sc_int score; + + vt_key[1].integer = task; + vt_key[2].string = "Score"; + score = prop_get_integer (parse_bundle, "I<-sis", vt_key); + if (score > 0) + maxscore += score; + } + + /* Write MaxScore global property. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "MaxScore"; + vt_value.integer = maxscore; + prop_put (parse_bundle, "I->ss", vt_value, vt_key); + } + + /* Convert walk meetobject from dynamic index to object. */ + else if (strcmp (fixup, "|V380_WALK:_MeetObject_|") == 0) + { + sc_vartype_t vt_key, vt_value, vt_gkey[3]; + sc_int meetobject, count, object_count, object; + + vt_key.string = "MeetObject"; + parse_push_key (vt_key, PROP_KEY_STRING); + meetobject = parse_get_integer_property (); + + /* Get a count of objects. */ + vt_gkey[0].string = "Objects"; + object_count = prop_get_child_count (parse_bundle, "I<-s", vt_gkey); + + /* Convert dynamic index to object, and rewrite. */ + count = meetobject - 1; + for (object = 0; object < object_count && count >= 0; object++) + { + sc_bool bstatic; + + vt_gkey[1].integer = object; + vt_gkey[2].string = "Static"; + bstatic = prop_get_boolean (parse_bundle, "B<-sis", vt_gkey); + if (!bstatic) + count--; + } + object--; + + vt_value.integer = object; + parse_put_property (vt_value, PROP_INTEGER); + parse_pop_key (); + } + + /* Convert version 3.8 room data into a version 4.0 alts array. */ + else if (strcmp (fixup, "|V380_ROOM:_Alts_|") == 0) + { + parse_fixup_v390_v380_room_alts (); + } + + /* Error if no fixup special handler available. */ + else + { + sc_fatal ("parse_fixup_v380: no handler for \"%s\"\n", fixup); + } + + if (parse_trace) + sc_trace ("Parse: leaving version 3.8 fixup %s\n", fixup); +} + + +/* + * parse_fixup() + * + * Handler for fixup special items to help with conversions from TAF version + * 3.9 and version 3.8 formats into version 4.0. + */ +static void +parse_fixup (const sc_char *fixup) +{ + /* + * Pick a fixup handler specific to the TAF version. This helps keep + * fixup code separate, rather than glommed into one large function. + */ + switch (taf_get_version (parse_taf)) + { + case TAF_VERSION_400: + sc_fatal ("parse_fixup: unexpected call\n"); + break; + case TAF_VERSION_390: + parse_fixup_v390 (fixup); + break; + case TAF_VERSION_380: + parse_fixup_v380 (fixup); + break; + default: + sc_fatal ("parse_fixup: invalid TAF file version\n"); + break; + } +} + + +/* + * parse_element() + * + * Parse a class descriptor element. + */ +static void +parse_element (const sc_char *element) +{ + if (parse_trace) + sc_trace ("Parse: entering element %s\n", element); + + /* Determine the element type from the first character. */ + switch (element[0]) + { + case PARSE_ARRAY: + parse_array (element); + break; + case PARSE_VECTOR: + parse_vector (element); + break; + case PARSE_VECTOR_ALTERNATE: + parse_vector_alternate (element); + break; + case PARSE_CLASS: + parse_class (element); + break; + case PARSE_EXPRESSION: + parse_expression (element); + break; + case PARSE_SPECIAL: + parse_special (element); + break; + case PARSE_FIXUP: + parse_fixup (element); + break; + + case PARSE_INTEGER: + case PARSE_DEFAULT_ZERO: + case PARSE_BOOLEAN: + case PARSE_DEFAULT_TRUE: + case PARSE_DEFAULT_FALSE: + case PARSE_STRING: + case PARSE_DEFAULT_EMPTY: + case PARSE_IGNORE_INTEGER: + case PARSE_IGNORE_BOOLEAN: + case PARSE_IGNORE_STRING: + case PARSE_MULTILINE: + parse_terminal (element); + break; + default: + sc_fatal ("parse_element: bad type, %c\n", element[0]); + } + + if (parse_trace) + sc_trace ("Parse: leaving element %s\n", element); +} + + +/* + * parse_descriptor() + * + * Parse a class's properties descriptor list. + */ +static void +parse_descriptor (const sc_char *descriptor) +{ + sc_int next; + + /* Find and parse each element in the descriptor. */ + for (next = 0; descriptor[next] != NUL; ) + { + sc_char element[PARSE_TEMP_LENGTH]; + + /* Isolate the next descriptor element. */ + if (sscanf (descriptor + next, "%[^ ]", element) != 1) + sc_fatal ("parse_element: no element, %s\n", descriptor + next); + + /* Parse this isolated element. */ + parse_element (element); + + /* Advance over the element and any trailing whitespace. */ + next += strlen (element); + next += strspn (descriptor + next, " "); + } +} + + +/* + * parse_class() + * + * Parse a class of properties. + */ +static void +parse_class (const sc_char *class_) +{ + sc_char class_name[PARSE_TEMP_LENGTH]; + sc_int index_; + sc_vartype_t vt_key; + + /* Isolate the class name. */ + if (sscanf (class_, "<%[^>]", class_name) != 1) + sc_fatal ("parse_class: error in class, %s\n", class_); + if (parse_trace) + sc_trace ("Parse: entering class %s\n", class_name); + + /* Find the class in the parse schema, and fail if not found. */ + for (index_ = 0; parse_schema[index_].class_name; index_++) + { + if (strcmp (parse_schema[index_].class_name, class_name) == 0) + break; + } + if (!parse_schema[index_].class_name) + sc_fatal ("parse_class: class not described, %s\n", class_name); + + /* + * Unless we are at the top level of the parse schema, push the class tag + * as a key. The top level is "_GAME_", index_ 0, and isn't part of key + * formation. + */ + if (index_ > 0) + { + vt_key.string = class_ + strlen (class_name) + 2; + parse_push_key (vt_key, PROP_KEY_STRING); + } + + /* Parse each element in the descriptor. */ + parse_descriptor (parse_schema[index_].descriptor); + + /* Pop a key if the class tag was pushed above. */ + if (index_ > 0) + parse_pop_key (); + + if (parse_trace) + sc_trace ("Parse: leaving class %s\n", class_name); +} + + +/* + * parse_add_walkalerts() + * + * Add a list of all NPC walks started by each task. This is post-processing + * that occurs after the TAF file has been successfully parsed. + */ +static void +parse_add_walkalerts (sc_prop_setref_t bundle) +{ + sc_vartype_t vt_key[5]; + sc_int npcs_count, npc; + + /* Get the count of NPCs. */ + vt_key[0].string = "NPCs"; + npcs_count = prop_get_child_count (bundle, "I<-s", vt_key); + + /* Set up each NPC. */ + for (npc = 0; npc < npcs_count; npc++) + { + sc_int walk_count, walk; + + /* Get NPC walk details. */ + vt_key[1].integer = npc; + vt_key[2].string = "Walks"; + walk_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + for (walk = 0; walk < walk_count; walk++) + { + sc_int starttask; + + /* Get start task of walk. */ + vt_key[3].integer = walk; + vt_key[4].string = "StartTask"; + starttask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; + if (starttask >= 0) + { + sc_vartype_t vt_key2[4], vt_value; + sc_int count; + + /* Count existing walkalerts for the task. */ + vt_key2[0].string = "Tasks"; + vt_key2[1].integer = starttask; + vt_key2[2].string = "NPCWalkAlert"; + count = prop_get_child_count (bundle, "I<-sis", vt_key2); + + /* Add two more -- NPC and walk. */ + vt_key2[3].integer = count; + vt_value.integer = npc; + prop_put (bundle, "I->sisi", vt_value, vt_key2); + vt_key2[3].integer = count + 1; + vt_value.integer = walk; + prop_put (bundle, "I->sisi", vt_value, vt_key2); + } + } + } +} + + +/* + * parse_add_movetimes() + * + * Add a list of move times to all NPC walks. This is post-processing that + * occurs after the TAF file has been successfully parsed. + */ +static void +parse_add_movetimes (sc_prop_setref_t bundle) +{ + sc_vartype_t vt_key[6]; + sc_int npcs_count, npc; + + /* Get the count of NPCs. */ + vt_key[0].string = "NPCs"; + npcs_count = prop_get_child_count (bundle, "I<-s", vt_key); + + /* Set up each NPC. */ + for (npc = 0; npc < npcs_count; npc++) + { + sc_int walk_count, walk; + + /* Get NPC walk details. */ + vt_key[1].integer = npc; + vt_key[2].string = "Walks"; + walk_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + for (walk = 0; walk < walk_count; walk++) + { + sc_int waittimes; + sc_int *movetimes, index_; + sc_vartype_t vt_value; + + vt_key[3].integer = walk; + vt_key[4].string = "Times"; + waittimes = prop_get_child_count (bundle, "I<-sisis", vt_key); + + movetimes = (sc_int *)sc_malloc ((waittimes + 1) * sizeof (*movetimes)); + memset (movetimes, 0, (waittimes + 1) * sizeof (*movetimes)); + for (index_ = waittimes - 1; index_ >= 0; index_--) + { + vt_key[4].string = "Times"; + vt_key[5].integer = index_; + movetimes[index_] = prop_get_integer (bundle, "I<-sisisi", vt_key) + + movetimes[index_ + 1]; + } + movetimes[waittimes] = -2; + + for (index_ = 0; index_ <= waittimes; index_++) + { + vt_key[4].string = "MoveTimes"; + vt_key[5].integer = index_; + vt_value.integer = movetimes[index_]; + prop_put (bundle, "I->sisisi", vt_value, vt_key); + } + sc_free (movetimes); + } + } +} + + +/* + * parse_add_alrs_index() + * + * Sort ALRs by original string length and store an indexer property, so + * that ALR replacements look at longer strings before shorter ones. + */ +static void +parse_add_alrs_index (sc_prop_setref_t bundle) +{ + sc_vartype_t vt_key[3]; + sc_int alr_count, index_, alr; + sc_int *alr_lengths, longest, shortest, length; + + /* Count ALRs, and set invariant part of properties key. */ + vt_key[0].string = "ALRs"; + alr_count = prop_get_child_count (bundle, "I<-s", vt_key); + + /* + * Set up an array of the lengths of ALR original strings, and while at it, + * get the shortest and longest defined. + */ + alr_lengths = (sc_int *)sc_malloc(alr_count * sizeof (*alr_lengths)); + shortest = INT_MAX; + longest = 0; + for (index_ = 0; index_ < alr_count; index_++) + { + const sc_char *original; + + vt_key[1].integer = index_; + vt_key[2].string = "Original"; + original = prop_get_string (bundle, "S<-sis", vt_key); + length = strlen (original); + + alr_lengths[index_] = length; + shortest = (length < shortest) ? length : shortest; + longest = (length > longest) ? length : longest; + } + + /* + * Now write a set of secondary properties that define the order of handling + * for ALRs. Our friend qsort() can't help here as it doesn't define the + * final ordering of equal members, and we need here to retain file ordering + * for ALR originals of the same length. + */ + vt_key[0].string = "ALRs2"; + alr = 0; + for (length = longest; length >= shortest; length--) + { + /* Find and add each ALR of this length. */ + for (index_ = 0; index_ < alr_count; index_++) + { + if (alr_lengths[index_] == length) + { + sc_vartype_t vt_value; + + vt_key[1].integer = alr++; + vt_key[2].string = "ALRIndex"; + vt_value.integer = index_; + prop_put (bundle, "I->sis", vt_value, vt_key); + } + } + } + assert (alr == alr_count); + + /* Done with ALR lengths array. */ + sc_free (alr_lengths); +} + + +/* + * parse_add_resources_offset() + * + * Add the resources offset to the properties as an extra game property + * for version 4.0 games. For version 3.9 and version 3.8 games, write + * zero; only version 4.0 games can embed their resources into the TAF file. + */ +static void +parse_add_resources_offset (sc_prop_setref_t bundle, sc_tafref_t taf) +{ + sc_vartype_t vt_key[2], vt_value; + sc_bool embedded; + sc_int offset; + + /* + * Get the resources offset from the TAF, or default to zero. The resources + * offset is one byte after the end of game data. + */ + vt_key[0].string = "Globals"; + vt_key[1].string = "Embedded"; + embedded = prop_get_boolean (bundle, "B<-ss", vt_key); + offset = embedded ? taf_get_game_data_length (taf) + 1 : 0; + + /* Add this offset to the properties. */ + vt_key[0].string = "ResourceOffset"; + vt_value.integer = offset; + prop_put (bundle, "I->s", vt_value, vt_key); +} + + +/* + * parse_add_version() + * + * Add the TAF version to the properties, both integer and character forms + * for convenience. + */ +static void +parse_add_version (sc_prop_setref_t bundle, sc_tafref_t taf) +{ + sc_vartype_t vt_key, vt_value; + + /* Add the version integer to the properties. */ + vt_key.string = "Version"; + vt_value.integer = taf_get_version (taf); + prop_put (bundle, "I->s", vt_value, &vt_key); + + /* Add the version string to the properties. */ + switch (taf_get_version (taf)) + { + case TAF_VERSION_400: + vt_value.string = "4.00"; + break; + case TAF_VERSION_390: + vt_value.string = "3.90"; + break; + case TAF_VERSION_380: + vt_value.string = "3.80"; + break; + default: + sc_error ("parse_add_version_string: invalid TAF file version\n"); + vt_value.string = "[Unknown version]"; + break; + } + vt_key.string = "VersionString"; + prop_put (bundle, "S->s", vt_value, &vt_key); +} + + +/* + * parse_game() + * + * Parse a game into a set properties. Return TRUE on success, FALSE if + * it encountered an error reading the TAF file. + */ +sc_bool parse_game(sc_tafref_t taf, sc_prop_setref_t bundle) { + assert (taf && bundle); + + /* Store the TAF to read from, and the bundle to store into. */ + parse_taf = taf; + parse_bundle = bundle; + parse_schema = parse_select_schema (parse_taf); + parse_depth = 0; + + /* Try parsing, and catch errors from longjmp. */ + if (setjmp (parse_taf_error) == 0) + { + /* Parse a complete game. */ + taf_first_line (parse_taf); + parse_tafline = 0; + parse_class ("<_GAME_>"); + } + else + { + /* Error with one of the TAF file lines. */ + parse_clear_v400_resources_table (); + parse_taf = NULL; + parse_bundle = NULL; + parse_schema = NULL; + parse_depth = 0; + return FALSE; + } + + /* Free the accumulated version 4.0 resources details. */ + parse_clear_v400_resources_table (); + + /* See if we reached the end of the TAF. */ + if (taf_more_lines (parse_taf)) + sc_error ("parse_game: unexpected trailing data\n"); + + /* Append post-processing walkalerts and move times. */ + parse_add_walkalerts (parse_bundle); + parse_add_movetimes (parse_bundle); + + /* Append sorted ALR list and resources offset. */ + parse_add_alrs_index (parse_bundle); + parse_add_resources_offset (parse_bundle, parse_taf); + + /* Add a note of the TAF file version. */ + parse_add_version (parse_bundle, parse_taf); + + /* Trim excess allocations from properties. */ + prop_solidify (parse_bundle); + + /* Return successfully. */ + parse_taf = NULL; + parse_bundle = NULL; + parse_schema = NULL; + parse_depth = 0; + return TRUE; +} + + +/* + * parse_debug_trace() + * + * Set parse tracing on/off. + */ +void +parse_debug_trace (sc_bool flag) +{ + parse_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sctasks.cpp b/engines/glk/adrift/sctasks.cpp new file mode 100644 index 0000000000..4ced0c5099 --- /dev/null +++ b/engines/glk/adrift/sctasks.cpp @@ -0,0 +1,1414 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Implements task return FALSE on no output, a slight extension of + * current jAsea behavior. + */ + +/* + * Tasks can run other tasks, leading to the possibility of an infinite loop + * in the task calling sequence. It's a game error, and we'll apply a limit + * to the task recursion depth to try and catch it more controllably than + * waiting for memory exhaustion. + */ +enum { TASK_MAXIMUM_RECURSION = 128 }; + +/* Trace flag, set before running. */ +static sc_bool task_trace = FALSE; + + +/* + * task_get_hint_common() + * task_get_hint_question() + * task_get_hint_subtle() + * task_get_hint_unsubtle() + * task_has_hints() + * + * Return the assorted hint text strings, and TRUE if the given task offers + * hints. + */ +static const sc_char * +task_get_hint_common (sc_gameref_t game, sc_int task, const sc_char *hint) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *retval; + + /* Look up and return the requested hint string. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = hint; + retval = prop_get_string (bundle, "S<-sis", vt_key); + return retval; +} + +const sc_char * +task_get_hint_question (sc_gameref_t game, sc_int task) +{ + return task_get_hint_common (game, task, "Question"); +} + +const sc_char * +task_get_hint_subtle (sc_gameref_t game, sc_int task) +{ + return task_get_hint_common (game, task, "Hint1"); +} + +const sc_char * +task_get_hint_unsubtle (sc_gameref_t game, sc_int task) +{ + return task_get_hint_common (game, task, "Hint2"); +} + +sc_bool +task_has_hints (sc_gameref_t game, sc_int task) +{ + /* A non-empty question implies hints available. */ + return !sc_strempty (task_get_hint_question (game, task)); +} + + +/* + * task_can_run_task_directional() + * + * Return TRUE if player is in a room where the task can be run and the task + * is runnable in the given direction. + */ +sc_bool +task_can_run_task_directional (sc_gameref_t game, + sc_int task, sc_bool forwards) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int type; + + /* If already run, non-repeatable tasks are not re-runnable forwards. */ + if (forwards && gs_task_done (game, task)) + { + sc_bool repeatable; + const sc_char *repeattext; + + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Repeatable"; + repeatable = prop_get_boolean (bundle, "B<-sis", vt_key); + if (!repeatable) + return FALSE; + + vt_key[2].string = "RepeatText"; + repeattext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (repeattext)) + return FALSE; + } + + /* If checking for reverse, test the reversibility flag. */ + if (!forwards) + { + sc_bool reversible; + + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Reversible"; + reversible = prop_get_boolean (bundle, "B<-sis", vt_key); + if (!reversible) + return FALSE; + } + + /* Check room list for the task and return it. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Where"; + vt_key[3].string = "Type"; + type = prop_get_integer (bundle, "I<-siss", vt_key); + switch (type) + { + case ROOMLIST_NO_ROOMS: + return FALSE; + case ROOMLIST_ALL_ROOMS: + return TRUE; + + case ROOMLIST_ONE_ROOM: + vt_key[3].string = "Room"; + return prop_get_integer (bundle, + "I<-siss", vt_key) == gs_playerroom (game); + + case ROOMLIST_SOME_ROOMS: + vt_key[3].string = "Rooms"; + vt_key[4].integer = gs_playerroom (game); + return prop_get_boolean (bundle, "B<-sissi", vt_key); + + default: + sc_fatal ("task_can_run_task_directional: invalid type, %ld\n", type); + return FALSE; + } +} + + +/* + * task_can_run_task() + * + * Returns TRUE if the task can be run in either direction. + */ +sc_bool +task_can_run_task (sc_gameref_t game, sc_int task) +{ + /* + * Testing reversible tasks first may be a little more efficient if they + * aren't common in games. There is, though, probably a little bit of + * redundant work going on here. + */ + return task_can_run_task_directional (game, task, FALSE) + || task_can_run_task_directional (game, task, TRUE); +} + + +/* + * task_move_object() + * + * Move an object to a place. + */ +static void +task_move_object (sc_gameref_t game, sc_int object, sc_int var2, sc_int var3) +{ + const sc_var_setref_t vars = gs_get_vars (game); + + /* Select action depending on var2. */ + switch (var2) + { + case 0: /* To room */ + if (var3 == 0) + { + if (task_trace) + sc_trace ("Task: moving object %ld to hidden\n", object); + + gs_object_make_hidden (game, object); + } + else + { + if (task_trace) + { + sc_trace ("Task: moving object %ld to room %ld\n", + object, var3 - 1); + } + + if (var3 == 0) + gs_object_player_get (game, object); + else + gs_object_to_room (game, object, var3 - 1); + } + break; + + case 1: /* To roomgroup part */ + if (task_trace) + { + sc_trace ("Task: moving object %ld to random room in group %ld\n", + object, var3); + } + + gs_object_to_room (game, object, + lib_random_roomgroup_member (game, var3)); + break; + + case 2: /* Into object */ + if (task_trace) + sc_trace ("Task: moving object %ld into %ld\n", object, var3); + + gs_object_move_into (game, object, obj_container_object (game, var3)); + break; + + case 3: /* Onto object */ + if (task_trace) + sc_trace ("Task: moving object %ld onto %ld\n", object, var3); + + gs_object_move_onto (game, object, obj_surface_object (game, var3)); + break; + + case 4: /* Held by */ + if (task_trace) + sc_trace ("Task: moving object %ld to held by %ld\n", object, var3); + + if (var3 == 0) /* Player */ + gs_object_player_get (game, object); + else if (var3 == 1) /* Ref character */ + gs_object_npc_get (game, object, var_get_ref_character (vars)); + else /* NPC id */ + gs_object_npc_get (game, object, var3 - 2); + break; + + case 5: /* Worn by */ + if (task_trace) + sc_trace ("Task: moving object %ld to worn by %ld\n", object, var3); + + if (var3 == 0) /* Player */ + gs_object_player_wear (game, object); + else if (var3 == 1) /* Ref character */ + gs_object_npc_wear (game, object, var_get_ref_character (vars)); + else /* NPC id */ + gs_object_npc_wear (game, object, var3 - 2); + break; + + case 6: /* Same room as */ + { + sc_int room, npc; + + if (task_trace) + { + sc_trace ("Task: moving object %ld to same room as %ld\n", + object, var3); + } + + if (var3 == 0) /* Player */ + room = gs_playerroom (game); + else if (var3 == 1) /* Ref character */ + { + npc = var_get_ref_character (vars); + room = gs_npc_location (game, npc) - 1; + } + else /* NPC id */ + { + npc = var3 - 2; + room = gs_npc_location (game, npc) - 1; + } + gs_object_to_room (game, object, room); + break; + } + + default: + sc_fatal ("task_move_object: unknown move type, %ld\n", var2); + } +} + + +/* + * task_run_move_object_action() + * + * Demultiplex an object move action and execute it. + */ +static void +task_run_move_object_action (sc_gameref_t game, + sc_int var1, sc_int var2, sc_int var3) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int object; + + /* Select depending on value in var1. */ + switch (var1) + { + case 0: /* All held */ + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_HELD_PLAYER) + task_move_object (game, object, var2, var3); + } + break; + + case 1: /* All worn */ + for (object = 0; object < gs_object_count (game); object++) + { + if (gs_object_position (game, object) == OBJ_WORN_PLAYER) + task_move_object (game, object, var2, var3); + } + break; + + case 2: /* Ref object */ + object = var_get_ref_object (vars); + task_move_object (game, object, var2, var3); + break; + + default: /* Dynamic object */ + object = obj_dynamic_object (game, var1 - 3); + task_move_object (game, object, var2, var3); + break; + } +} + + +/* + * task_move_npc_to_room() + * + * Move an NPC to a given room. + */ +static void +task_move_npc_to_room (sc_gameref_t game, sc_int npc, sc_int room) +{ + if (task_trace) + sc_trace ("Task: moving NPC %ld to room %ld\n", npc, room); + + /* Update the NPC's state. */ + if (room < gs_room_count (game)) + gs_set_npc_location (game, npc, room + 1); + else + gs_set_npc_location (game, npc, + lib_random_roomgroup_member (game, + room - gs_room_count (game)) + 1); + + gs_set_npc_parent (game, npc, -1); + gs_set_npc_position (game, npc, 0); +} + + +/* + * task_run_move_npc_action() + * + * Move player or NPC. + */ +static void +task_run_move_npc_action (sc_gameref_t game, + sc_int var1, sc_int var2, sc_int var3) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int npc, room, ref_npc = -1; + + /* Player or NPC? */ + if (var1 == 0) + { + /* Player -- decide where to move player to. */ + switch (var2) + { + case 0: /* To room */ + gs_move_player_to_room (game, var3); + return; + + case 1: /* To roomgroup part */ + if (task_trace) + { + sc_trace ("Task: moving player to random room in group %ld\n", + var3); + } + + gs_move_player_to_room (game, + lib_random_roomgroup_member (game, var3)); + return; + + case 2: /* To same room as... */ + switch (var3) + { + case 0: /* ...player! */ + return; + case 1: /* ...referenced NPC */ + npc = var_get_ref_character (vars); + break; + default: /* ...specified NPC */ + npc = var3 - 2; + break; + } + + if (task_trace) + sc_trace ("Task: moving player to same room as NPC %ld\n", npc); + + room = gs_npc_location (game, npc) - 1; + if (room < 0) + { + if (task_trace) + sc_trace ("Task: silently suppressed player move to hidden\n"); + } + else + gs_move_player_to_room (game, room); + return; + + case 3: /* To standing on */ + gs_set_playerposition (game, 0); + gs_set_playerparent (game, obj_standable_object (game, var3 - 1)); + return; + + case 4: /* To sitting on */ + gs_set_playerposition (game, 1); + gs_set_playerparent (game, obj_standable_object (game, var3 - 1)); + return; + + case 5: /* To lying on */ + gs_set_playerposition (game, 2); + gs_set_playerparent (game, obj_lieable_object (game, var3 - 1)); + return; + + default: + sc_fatal ("task_run_move_npc_action:" + " unknown player move type, %ld\n", var2); + return; + } + } + else + { + /* NPC -- first find which NPC to move about. */ + if (var1 == 1) + npc = var_get_ref_character (vars); + else + npc = var1 - 2; + + /* Decide where to move the NPC to. */ + switch (var2) + { + case 0: /* To room */ + task_move_npc_to_room (game, npc, var3 - 1); + return; + + case 1: /* To roomgroup part */ + if (task_trace) + { + sc_trace ("Task: moving NPC %ld to random room in group %ld\n", + npc, var3); + } + + task_move_npc_to_room (game, npc, + lib_random_roomgroup_member (game, var3)); + return; + + case 2: /* To same room as... */ + switch (var3) + { + case 0: /* ...player */ + if (task_trace) + { + sc_trace ("Task: moving NPC %ld to same room as player\n", + npc); + } + + task_move_npc_to_room (game, npc, gs_playerroom (game)); + break; + case 1: /* ...referenced NPC */ + ref_npc = var_get_ref_character (vars); + if (task_trace) + { + sc_trace ("Task: moving NPC %ld to" + " same room as referenced NPC %ld\n", npc, ref_npc); + } + + room = gs_npc_location (game, ref_npc) - 1; + task_move_npc_to_room (game, npc, room); + break; + default: /* ...specified NPC */ + ref_npc = var3 - 2; + if (task_trace) + { + sc_trace ("Task: moving NPC %ld to" + " same room as NPC %ld\n", npc, ref_npc); + } + + room = gs_npc_location (game, ref_npc) - 1; + task_move_npc_to_room (game, npc, room); + break; + } + return; + + case 3: /* To standing on */ + gs_set_npc_position (game, npc, 0); + gs_set_npc_parent (game, npc, obj_standable_object (game, var3)); + return; + + case 4: /* To sitting on */ + gs_set_npc_position (game, npc, 1); + gs_set_npc_parent (game, npc, obj_standable_object (game, var3)); + return; + + case 5: /* To lying on */ + gs_set_npc_position (game, npc, 2); + gs_set_npc_parent (game, npc, obj_lieable_object (game, var3)); + return; + + default: + sc_fatal ("task_run_move_npc_action:" + " unknown NPC move type, %ld\n", var2); + return; + } + } +} + + +/* + * task_run_change_object_status() + * + * Change the status of an object. + */ +static void +task_run_change_object_status (sc_gameref_t game, sc_int var1, sc_int var2) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int object, openable, lockable; + + if (task_trace) + { + sc_trace ("Task: setting status of stateful object %ld to %ld\n", + var1, var2); + } + + /* Identify the target object. */ + object = obj_stateful_object (game, var1); + + /* See if openable. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Openable"; + openable = prop_get_integer (bundle, "I<-sis", vt_key); + if (openable > 0) + { + /* See if lockable. */ + vt_key[2].string = "Key"; + lockable = prop_get_integer (bundle, "I<-sis", vt_key); + if (lockable >= 0) + { + /* Lockable. */ + if (var2 <= 2) + gs_set_object_openness (game, object, var2 + 5); + else + gs_set_object_state (game, object, var2 - 2); + } + else + { + /* Not lockable, though openable. */ + if (var2 <= 1) + gs_set_object_openness (game, object, var2 + 5); + else + gs_set_object_state (game, object, var2 - 1); + } + } + else + /* Not openable. */ + gs_set_object_state (game, object, var2 + 1); + + if (task_trace) + { + sc_trace ("Task: openness of object %ld is now %ld\n", + object, gs_object_openness (game, object)); + sc_trace ("Task: state of object %ld is now %ld\n", + object, gs_object_state (game, object)); + } +} + + +/* + * task_run_change_variable_action() + * + * Change a variable's value in inscrutable ways. + */ +static void +task_run_change_variable_action (sc_gameref_t game, + sc_int var1, sc_int var2, sc_int var3, + const sc_char *expr, sc_int var5) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + const sc_var_setref_t vars = gs_get_vars (game); + sc_vartype_t vt_key[3]; + const sc_char *name, *string; + sc_char *mutable_string; + sc_int type, value; + + /* + * At this point, we need to checkpoint the filter. We're about to change + * a variable value, so interpolating here before doing that ensures that + * any currently buffered text gets the values that were set when the text + * was buffered. + */ + pf_checkpoint (filter, vars, bundle); + + /* Get the name and type of the variable being addressed. */ + vt_key[0].string = "Variables"; + vt_key[1].integer = var1; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + vt_key[2].string = "Type"; + type = prop_get_integer (bundle, "I<-sis", vt_key); + + /* Select first based on variable type. */ + switch (type) + { + case TAFVAR_NUMERIC: /* Integer */ + + /* Select again based on action type. */ + switch (var2) + { + case 0: /* Var = */ + if (task_trace) + sc_trace ("Task: variable %ld (%s) = %ld\n", var1, name, var3); + + var_put_integer (vars, name, var3); + return; + + case 1: /* Var += */ + if (task_trace) + sc_trace ("Task: variable %ld (%s) += %ld\n", var1, name, var3); + + value = var_get_integer (vars, name) + var3; + var_put_integer (vars, name, value); + return; + + case 2: /* Var = rnd(range) */ + if (task_trace) + { + sc_trace ("Task: variable %ld (%s) = random(%ld,%ld)\n", + var1, name, var3, var5); + } + + value = sc_randomint (var3, var5); + var_put_integer (vars, name, value); + return; + + case 3: /* Var += rnd(range) */ + if (task_trace) + { + sc_trace ("Task: variable %ld (%s) += random(%ld,%ld)\n", + var1, name, var3, var5); + } + + value = var_get_integer (vars, name) + sc_randomint (var3, var5); + var_put_integer (vars, name, value); + return; + + case 4: /* Var = ref */ + value = var_get_ref_number (vars); + if (task_trace) + { + sc_trace ("Task: variable %ld (%s) = ref, %ld\n", + var1, name, value); + } + + var_put_integer (vars, name, value); + return; + + case 5: /* Var = expr */ + if (!expr_eval_numeric_expression (expr, vars, &value)) + { + sc_error ("task_run_change_variable_action:" + " invalid expression, %s\n", expr); + value = 0; + } + if (task_trace) + { + sc_trace ("Task: variable %ld (%s) = %s, %ld\n", + var1, name, expr, value); + } + + var_put_integer (vars, name, value); + return; + + default: + sc_fatal ("task_run_change_variable_action:" + " unknown integer change type, %ld\n", var2); + } + + case TAFVAR_STRING: /* String */ + + /* Select again based on action type. */ + switch (var2) + { + case 0: /* Var = text literal */ + if (task_trace) + { + sc_trace ("Task: variable %ld (%s) = \"%s\"\n", + var1, name, expr); + } + + var_put_string (vars, name, expr); + return; + + case 1: /* Var = ref */ + string = var_get_ref_text (vars); + if (task_trace) + { + sc_trace ("Task: variable %ld (%s) = ref, \"%s\"\n", + var1, name, string); + } + + var_put_string (vars, name, string); + return; + + case 2: /* Var = expr */ + if (!expr_eval_string_expression (expr, vars, &mutable_string)) + { + sc_error ("task_run_change_variable_action:" + " invalid string expression, %s\n", expr); + mutable_string = (sc_char *)sc_malloc (strlen ("[expr error]") + 1); + strcpy (mutable_string, "[expr error]"); + } + if (task_trace) + { + sc_trace ("Task: variable %ld (%s) = %s, %s\n", + var1, name, expr, mutable_string); + } + + var_put_string (vars, name, mutable_string); + sc_free (mutable_string); + return; + + default: + sc_fatal ("task_run_change_variable_action:" + " unknown string change type, %ld\n", var2); + } + + default: + sc_fatal ("task_run_change_variable_action:" + " invalid variable type, %ld\n", type); + } +} + + +/* + * task_run_change_score_action() + * + * Change game score. + */ +static void +task_run_change_score_action (sc_gameref_t game, sc_int task, sc_int var1) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + + /* Increasing or decreasing the score? */ + if (var1 > 0) + { + sc_bool increase_score; + + /* See if this task is already scored. */ + increase_score = !gs_task_scored (game, task); + if (!increase_score) + { + sc_vartype_t vt_key[3]; + sc_int version; + + if (task_trace) + sc_trace ("Task: already scored task %ld\n", var1); + + /* Version 3.8 games permit tasks to rescore. */ + vt_key[0].string = "Version"; + version = prop_get_integer (bundle, "I<-s", vt_key); + if (version == TAF_VERSION_380) + { + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "SingleScore"; + increase_score = !prop_get_boolean (bundle, "B<-sis", vt_key); + + if (increase_score) + { + if (task_trace) + sc_trace ("Task: rescoring version 3.8 task anyway\n"); + } + } + } + + /* + * Increase the score if not yet scored or a version 3.8 multiple + * scoring task, and note as a scored task. + */ + if (increase_score) + { + if (task_trace) + sc_trace ("Task: increased score by %ld\n", var1); + + game->score += var1; + gs_set_task_scored (game, task, TRUE); + } + } + else if (var1 < 0) + { + /* Decrease the score. */ + if (task_trace) + sc_trace ("Task: decreased score by %ld\n", -(var1)); + + game->score += var1; + } +} + + +/* + * task_run_set_task_action() + * + * Redirect to another task. + */ +static sc_bool +task_run_set_task_action (sc_gameref_t game, sc_int var1, sc_int var2) +{ + sc_bool status = FALSE; + + /* Select based on var1. */ + if (var1 == 0) + { + /* Redirect forwards. */ + if (task_can_run_task_directional (game, var2, TRUE)) + { + if (task_trace) + sc_trace ("Task: redirecting to task %ld\n", var2); + + status = task_run_task (game, var2, TRUE); + } + else + { + if (task_trace) + sc_trace ("Task: can't redirect to task %ld\n", var2); + } + } + else + { + /* Undo task. */ + gs_set_task_done (game, var2, FALSE); + if (task_trace) + sc_trace ("Task: reversing task %ld\n", var2); + } + + return status; +} + + +/* + * task_run_end_game_action() + * + * End of game task action. + */ +static sc_bool +task_run_end_game_action (sc_gameref_t game, sc_int var1) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_bool status = FALSE; + + /* Print a message based on var1. */ + switch (var1) + { + case 0: + { + sc_vartype_t vt_key[2]; + const sc_char *wintext; + + /* Get game WinText. */ + vt_key[0].string = "Header"; + vt_key[1].string = "WinText"; + wintext = prop_get_string (bundle, "S<-ss", vt_key); + + /* Print WinText, if any defined, otherwise a default. */ + if (!sc_strempty (wintext)) + { + pf_buffer_string (filter, wintext); + pf_buffer_character (filter, '\n'); + } + else + pf_buffer_string (filter, "Congratulations!\n"); + + /* Handle any associated WinRes resource. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "WinRes"; + res_handle_resource (game, "ss", vt_key); + + status = TRUE; + break; + } + + case 1: + pf_buffer_string (filter, "Better luck next time.\n"); + status = TRUE; + break; + + case 2: + pf_buffer_string (filter, "I'm afraid you are dead!\n"); + status = TRUE; + break; + + case 3: + break; + + default: + sc_fatal ("task_run_end_game_action: invalid type, %ld\n", var1); + } + + /* Stop the game, and note that it's not resumeable. */ + game->is_running = FALSE; + game->has_completed = TRUE; + + return status; +} + + +/* + * task_run_task_action() + * + * Demultiplexer for task actions. + */ +static sc_bool +task_run_task_action (sc_gameref_t game, sc_int task, sc_int action) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[5]; + sc_int type, var1, var2, var3, var5; + const sc_char *expr; + sc_bool status = FALSE; + + /* Get the task action type. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Actions"; + vt_key[3].integer = action; + vt_key[4].string = "Type"; + type = prop_get_integer (bundle, "I<-sisis", vt_key); + + /* Demultiplex depending on type. */ + switch (type) + { + case 0: /* Move object. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + task_run_move_object_action (game, var1, var2, var3); + break; + + case 1: /* Move player/NPC. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + task_run_move_npc_action (game, var1, var2, var3); + break; + + case 2: /* Change object status. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + task_run_change_object_status (game, var1, var2); + break; + + case 3: /* Change variable. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var3"; + var3 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Expr"; + expr = prop_get_string (bundle, "S<-sisis", vt_key); + vt_key[4].string = "Var5"; + var5 = prop_get_integer (bundle, "I<-sisis", vt_key); + task_run_change_variable_action (game, var1, var2, var3, expr, var5); + break; + + case 4: /* Change score. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + task_run_change_score_action (game, task, var1); + break; + + case 5: /* Execute/unset task. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + vt_key[4].string = "Var2"; + var2 = prop_get_integer (bundle, "I<-sisis", vt_key); + status = task_run_set_task_action (game, var1, var2); + break; + + case 6: /* End game. */ + vt_key[4].string = "Var1"; + var1 = prop_get_integer (bundle, "I<-sisis", vt_key); + status = task_run_end_game_action (game, var1); + break; + + case 7: /* Battle options, ignored for now... */ + break; + + default: + sc_fatal ("task_run_task_action: unknown action type %ld\n", type); + } + + return status; +} + + +/* + * task_run_task_actions() + * + * Run every task action associated with the task. If any action ends the + * game, return immediately. Returns TRUE if any action ran and itself + * returned TRUE. + */ +static sc_bool +task_run_task_actions (sc_gameref_t game, sc_int task) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + sc_int action_count, action; + sc_bool status, muted; + + /* Get the count of task actions. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Actions"; + action_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + if (action_count > 0) + { + if (task_trace) + { + sc_trace ("Task: task %ld running %ld action%s\n", + task, action_count, action_count == 1 ? "" : "s"); + } + } + + /* + * Run all task actions, capturing any TRUE status returned. If any task + * ends the game, run the remaining tasks silently. + * + * This seems a little counterintuitive; a more conventional thing would be + * to just exit the actions loop early. However, Adrift appears to plough + * on, and there may be an action that changes the score in here somewhere, + * so we'll do the same. + */ + status = FALSE; + muted = FALSE; + for (action = 0; action < action_count; action++) + { + sc_bool was_running; + + was_running = game->is_running; + status |= task_run_task_action (game, task, action); + + /* Did this action end the game? */ + if (was_running && !game->is_running) + { + if (task_trace) + { + sc_trace ("Task: task %ld action %ld ended game\n", + task, action); + } + + /* Mute the filter, and note that we did it, but continue. */ + pf_mute (filter); + muted = TRUE; + } + } + + /* If this stack frame muted the filter, un-mute it now. */ + if (muted) + pf_clear_mute (filter); + + /* Return TRUE if any task action returned TRUE. */ + return status; +} + + +/* + * task_start_npc_walks() + * + * Start NPC walks based on alerts. + */ +static void +task_start_npc_walks (sc_gameref_t game, sc_int task) +{ + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[4]; + sc_int alert_count, alert; + + /* Get a count of NPC walk alerts. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "NPCWalkAlert"; + alert_count = prop_get_child_count (bundle, "I<-sis", vt_key); + + /* Check alerts, and start any walks that need starting. */ + for (alert = 0; alert < alert_count; alert += 2) + { + sc_int npc, walk; + + vt_key[3].integer = alert; + npc = prop_get_integer (bundle, "I<-sisi", vt_key); + vt_key[3].integer = alert + 1; + walk = prop_get_integer (bundle, "I<-sisi", vt_key); + npc_start_npc_walk (game, npc, walk); + } +} + + +/* + * task_run_task_unrestricted() + * + * Run a task, providing restrictions permit, in the given direction. Return + * TRUE if the task ran, or we handled it in some complete way, for example by + * outputting a message describing what prevented it, or why it couldn't be + * done. + */ +static sc_bool +task_run_task_unrestricted (sc_gameref_t game, sc_int task, sc_bool forwards) +{ + const sc_filterref_t filter = gs_get_filter (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *completetext, *additionalmessage; + sc_int action_count, showroomdesc; + sc_bool status; + + /* Start considering task output tracking. */ + status = FALSE; + + /* + * If reversing, print any reverse message for the task, and undo the task, + * then return. + */ + if (!forwards) + { + const sc_char *reversemessage; + + /* If not yet done, we can hardly reverse it. */ + if (gs_task_done (game, task)) + { + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "ReverseMessage"; + reversemessage = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (reversemessage)) + { + pf_buffer_string (filter, reversemessage); + pf_buffer_character (filter, '\n'); + status |= TRUE; + } + + /* Undo the task. */ + gs_set_task_done (game, task, FALSE); + } + + /* Return status of undo. */ + return status; + } + + /* See if we are trying to repeat a task that's not repeatable. */ + if (gs_task_done (game, task)) + { + sc_bool repeatable; + + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "Repeatable"; + repeatable = prop_get_boolean (bundle, "B<-sis", vt_key); + if (!repeatable) + { + const sc_char *repeattext; + + vt_key[2].string = "RepeatText"; + repeattext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (repeattext)) + { + if (task_trace) + { + sc_trace ("Task:" + " trying to repeat completed action, aborting\n"); + } + + pf_buffer_string (filter, repeattext); + pf_buffer_character (filter, '\n'); + status |= TRUE; + return status; + } + + /* + * Task done, yet not repeatable, so don't consider this case + * handled. + */ + return status; + } + } + + /* Mark the task as done. */ + gs_set_task_done (game, task, TRUE); + + /* Print any task completion text. */ + vt_key[0].string = "Tasks"; + vt_key[1].integer = task; + vt_key[2].string = "CompleteText"; + completetext = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (completetext)) + { + pf_buffer_string (filter, completetext); + pf_buffer_character (filter, '\n'); + status |= TRUE; + } + + /* Handle any task completion resource. */ + vt_key[2].string = "Res"; + res_handle_resource (game, "sis", vt_key); + + /* + * Things get slightly tricky here. We need to filter the completion text + * for the task using any final variable values generated or modified by + * task actions, but other task text, run by actions, according to the + * variable value in effect when it runs. + * + * To do this, we take a local copy of the filter's current buffer at this + * point, remove it from the filter, run task actions with checkpointing, + * then prepend it back into the filter after all the actions are done. + * + * As an optimization, we can avoid doing this if there are no task actions. + */ + vt_key[2].string = "Actions"; + action_count = prop_get_child_count (bundle, "I<-sis", vt_key); + if (action_count > 0) + { + sc_char *buffer; + + /* + * Take ownership of the current filter buffer text, then start NPC + * walks based on alerts, and run any and all task actions. Note that + * the buffer transferred out of the filter may be NULL if there is no + * text currently in the filter. + */ + buffer = pf_transfer_buffer (filter); + task_start_npc_walks (game, task); + status |= task_run_task_actions (game, task); + + /* Prepend the saved buffer data back onto the front of the filter. */ + if (buffer) + { + pf_prepend_string (filter, buffer); + sc_free (buffer); + } + } + else + { + /* Start NPC walks only; there are no task actions. */ + task_start_npc_walks (game, task); + } + + /* Append any room description and additional message for the task. */ + vt_key[2].string = "ShowRoomDesc"; + showroomdesc = prop_get_integer (bundle, "I<-sis", vt_key); + if (showroomdesc != 0) + { + lib_print_room_name (game, showroomdesc - 1); + lib_print_room_description (game, showroomdesc - 1); + status |= TRUE; + } + + vt_key[2].string = "AdditionalMessage"; + additionalmessage = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (additionalmessage)) + { + pf_buffer_string (filter, additionalmessage); + pf_buffer_character (filter, '\n'); + status |= TRUE; + } + + /* Return status -- TRUE if matched and we output something. */ + return status; +} + + +/* + * task_run_task() + * + * Run a task, providing restrictions permit, in the given direction. At the + * same time, check for signs of an infinite loop in game tasks, and fail the + * task with an error message if we seem to be in one. Checked by counting + * the call depth. + */ +sc_bool +task_run_task (sc_gameref_t game, sc_int task, sc_bool forwards) +{ + static sc_int recursion_depth = 0; + + const sc_filterref_t filter = gs_get_filter (game); + const sc_char *fail_message; + sc_bool restrictions_passed, status; + + if (task_trace) + { + sc_trace ("Task: running task %ld %s, depth %ld\n", + task, forwards ? "forwards" : "backwards", recursion_depth); + } + + /* Check restrictions. */ + if (!restr_eval_task_restrictions (game, task, + &restrictions_passed, &fail_message)) + { + sc_error ("task_run_task: restrictions error, %ld\n", task); + return FALSE; + } + if (!restrictions_passed) + { + if (task_trace) + { + sc_trace ("Task: restrictions failed, task %s\n", + fail_message ? "failed" : "aborted"); + } + + if (fail_message) + { + /* + * Print a message, and return TRUE since we can consider this task + * "done" (more accurately, we've output text, so the task command + * searching in the main run loop can exit...). + */ + pf_buffer_string (filter, fail_message); + pf_buffer_character (filter, '\n'); + return TRUE; + } + + /* Task not done; look for more possibilities. */ + return FALSE; + } + + /* Check for infinite recursion. */ + if (recursion_depth > TASK_MAXIMUM_RECURSION) + { + sc_error ("task_run_task: maximum recursion depth exceeded --" + " game task loop?\n"); + return FALSE; + } + + /* Increment depth, run the task, then decrement depth. */ + recursion_depth++; + status = task_run_task_unrestricted (game, task, forwards); + recursion_depth--; + + if (task_trace) + { + sc_trace ("Task: task %ld finished, return %s, depth %ld\n", + task, status ? "true" : "false", recursion_depth); + } + + /* Return the task's status. */ + return status; +} + + +/* + * task_debug_trace() + * + * Set task tracing on/off. + */ +void +task_debug_trace (sc_bool flag) +{ + task_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scutils.cpp b/engines/glk/adrift/scutils.cpp new file mode 100644 index 0000000000..98e10651e7 --- /dev/null +++ b/engines/glk/adrift/scutils.cpp @@ -0,0 +1,462 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/glk.h" +#include "glk/events.h" +#include "common/debug.h" +#include "common/str.h" +#include "common/textconsole.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Implement smarter selective module tracing. + */ + +/* + * sc_trace() + * + * Debugging trace function; printf wrapper that writes to stderr. + */ +void sc_trace (const sc_char *format, ...) { + va_list ap; + assert(format); + + va_start (ap, format); + Common::String s = Common::String::format(format, ap); + va_end (ap); + debug("%s", s.c_str()); +} + + +/* + * sc_error() + * sc_fatal() + * + * Error reporting functions. sc_error() prints a message and continues. + * sc_fatal() prints a message, then calls abort(). + */ +void sc_error (const sc_char *format, ...) { + va_list ap; + assert(format); + + va_start(ap, format); + Common::String s = Common::String::format(format, ap); + va_end(ap); + warning("%s", s.c_str()); +} + +void sc_fatal (const sc_char *format, ...) { + va_list ap; + assert(format); + + va_start(ap, format); + Common::String s = Common::String::format(format, ap); + va_end(ap); + error("%s", s.c_str()); +} + + +/* Unique non-heap address for zero size malloc() and realloc() requests. */ +static void *sc_zero_allocation = &sc_zero_allocation; + +/* + * sc_malloc() + * sc_realloc() + * sc_free() + * + * Non-failing wrappers around malloc functions. Newly allocated memory is + * cleared to zero. In ANSI/ISO C, zero byte allocations are implementation- + * defined, so we have to take special care to get predictable behavior. + */ +void * +sc_malloc (size_t size) +{ + void *allocated; + + if (size == 0) + return sc_zero_allocation; + + allocated = malloc (size); + if (!allocated) + sc_fatal ("sc_malloc: requested %lu bytes\n", (sc_uint) size); + else if (allocated == sc_zero_allocation) + sc_fatal ("sc_malloc: zero-byte allocation address returned\n"); + + memset (allocated, 0, size); + return allocated; +} + +void * +sc_realloc (void *pointer, size_t size) +{ + void *allocated; + + if (size == 0) + { + sc_free (pointer); + return sc_zero_allocation; + } + + if (pointer == sc_zero_allocation) + pointer = NULL; + + allocated = realloc (pointer, size); + if (!allocated) + sc_fatal ("sc_realloc: requested %lu bytes\n", (sc_uint) size); + else if (allocated == sc_zero_allocation) + sc_fatal ("sc_realloc: zero-byte allocation address returned\n"); + + if (!pointer) + memset (allocated, 0, size); + return allocated; +} + +void +sc_free (void *pointer) +{ + if (sc_zero_allocation != &sc_zero_allocation) + sc_fatal ("sc_free: write to zero-byte allocation address detected\n"); + + if (pointer && pointer != sc_zero_allocation) + free (pointer); +} + + +/* + * sc_strncasecmp() + * sc_strcasecmp() + * + * Strncasecmp and strcasecmp are not ANSI functions, so here are local + * definitions to do the same jobs. + */ +sc_int +sc_strncasecmp (const sc_char *s1, const sc_char *s2, sc_int n) +{ + sc_int index_; + assert (s1 && s2); + + for (index_ = 0; index_ < n; index_++) + { + sc_int diff; + + diff = sc_tolower (s1[index_]) - sc_tolower (s2[index_]); + if (diff < 0 || diff > 0) + return diff < 0 ? -1 : 1; + } + + return 0; +} + +sc_int +sc_strcasecmp (const sc_char *s1, const sc_char *s2) +{ + sc_int s1len, s2len, result; + assert (s1 && s2); + + s1len = strlen (s1); + s2len = strlen (s2); + + result = sc_strncasecmp (s1, s2, s1len < s2len ? s1len : s2len); + if (result < 0 || result > 0) + return result; + else + return s1len < s2len ? -1 : s1len > s2len ? 1 : 0; +} + + +/* + * sc_platform_rand() + * sc_congruential_rand() + * sc_set_random_handler() + * + * Internal random number generation functions. We offer two: one is a self- + * seeding wrapper around the platform's rand(), which should generate good + * random numbers but with a sequence that is platform-dependent; the other + * is a linear congruential generator with a long period that is guaranteed + * to return the same sequence for all platforms. The default is the first, + * with the latter intended for predictability of game actions. + */ +static sc_int +sc_platform_rand (sc_uint new_seed) +{ + static sc_bool is_seeded = FALSE; + + /* If reseeding, seed with the value supplied, note seeded, and return 0. */ + if (new_seed > 0) { + g_vm->setRandomNumberSeed(new_seed); + is_seeded = TRUE; + return 0; + } + else + { + /* If not explicitly seeded yet, generate a seed from time(). */ + if (!is_seeded) + { + //srand ((sc_uint) time (NULL)); + is_seeded = TRUE; + } + + /* Return the next rand() number in the sequence. */ + return g_vm->getRandomNumber(0xffffff); + } +} + +static sc_int +sc_congruential_rand (sc_uint new_seed) +{ + static sc_bool is_seeded = FALSE; + static sc_uint rand_state = 1; + + /* If reseeding, seed with the value supplied, and note seeded. */ + if (new_seed > 0) + { + rand_state = new_seed; + is_seeded = TRUE; + return 0; + } + else + { + /* If not explicitly seeded yet, generate a seed from time(). */ + if (!is_seeded) + { + rand_state = (sc_uint)g_vm->_events->getTotalPlayTicks(); + is_seeded = TRUE; + } + + /* + * Advance random state, using constants from Park & Miller (1988). + * To keep the values the same for both 32 and 64 bit longs, mask out + * any bits above the bottom 32. + */ + rand_state = (rand_state * 16807 + 2147483647) & 0xffffffff; + + /* + * Discard the lowest bit as a way to map 32-bits unsigned to a 32-bit + * positive signed. + */ + return rand_state >> 1; + } +} + + +/* Function pointer for the actual random number generator in use. */ +static sc_int (*sc_rand_function) (sc_uint) = sc_platform_rand; + +/* + * sc_set_congruential_random() + * sc_set_platform_random() + * sc_is_congruential_random() + * sc_seed_random() + * sc_rand() + * sc_randomint() + * + * Public interface to random functions; control and reseed the random + * handler in use, generate a random number, and a convenience function to + * generate a random value within a given range. + */ +void +sc_set_congruential_random (void) +{ + sc_rand_function = sc_congruential_rand; +} + +void +sc_set_platform_random (void) +{ + sc_rand_function = sc_platform_rand; +} + +sc_bool +sc_is_congruential_random (void) +{ + return sc_rand_function == sc_congruential_rand; +} + +void +sc_seed_random (sc_uint new_seed) +{ + /* Ignore zero values of new_seed by simply using 1 instead. */ + sc_rand_function (new_seed > 0 ? new_seed : 1); +} + +sc_int +sc_rand (void) +{ + sc_int retval; + + /* Passing zero indicates this is not a seed operation. */ + retval = sc_rand_function (0); + return retval; +} + +sc_int +sc_randomint (sc_int low, sc_int high) +{ + /* + * If the range is invalid, just return the low value given. This mimics + * Adrift under the same conditions, and also guards against division by + * zero in the mod operation. + */ + return (high < low) ? low : low + sc_rand () % (high - low + 1); +} + + +/* Miscellaneous general ascii constants. */ +static const sc_char NUL = '\0'; +static const sc_char SPACE = ' '; + +/* + * sc_strempty() + * + * Return TRUE if a string is either zero-length or contains only whitespace. + */ +sc_bool +sc_strempty (const sc_char *string) +{ + sc_int index_; + assert (string); + + /* Scan for any non-space character. */ + for (index_ = 0; string[index_] != NUL; index_++) + { + if (!sc_isspace (string[index_])) + return FALSE; + } + + /* None found, so string is empty. */ + return TRUE; +} + + +/* + * sc_trim_string() + * + * Trim leading and trailing whitespace from a string. Modifies the string + * in place, and returns the string address for convenience. + */ +sc_char * +sc_trim_string (sc_char *string) +{ + sc_int index_; + assert (string); + + for (index_ = strlen (string) - 1; + index_ >= 0 && sc_isspace (string[index_]); index_--) + string[index_] = NUL; + + for (index_ = 0; sc_isspace (string[index_]);) + index_++; + memmove (string, string + index_, strlen (string) - index_ + 1); + + return string; +} + + +/* + * sc_normalize_string() + * + * Trim a string, and set all runs of whitespace to a single space character. + * Modifies the string in place, and returns the string address for + * convenience. + */ +sc_char * +sc_normalize_string (sc_char *string) +{ + sc_int index_; + assert (string); + + /* Trim all leading and trailing spaces. */ + string = sc_trim_string (string); + + /* Compress multiple whitespace runs into a single space character. */ + for (index_ = 0; string[index_] != NUL; index_++) + { + if (sc_isspace (string[index_])) + { + sc_int cursor; + + string[index_] = SPACE; + for (cursor = index_ + 1; sc_isspace (string[cursor]);) + cursor++; + memmove (string + index_ + 1, + string + cursor, strlen (string + cursor) + 1); + } + } + + return string; +} + + +/* + * sc_compare_word() + * + * Return TRUE if the first word in the string is word, case insensitive. + */ +sc_bool +sc_compare_word (const sc_char *string, const sc_char *word, sc_int length) +{ + assert (string && word); + + /* Return TRUE if string starts with word, then space or string end. */ + return sc_strncasecmp (string, word, length) == 0 + && (string[length] == NUL || sc_isspace (string[length])); +} + + +/* + * sc_hash() + * + * Hash a string, hashpjw algorithm, from 'Compilers, principles, techniques, + * and tools', page 436, unmodulo'ed and somewhat restyled. + */ +sc_uint +sc_hash (const sc_char *string) +{ + sc_int index_; + sc_uint hash; + assert (string); + + hash = 0; + for (index_ = 0; string[index_] != NUL; index_++) + { + sc_uint temp; + + hash = (hash << 4) + string[index_]; + temp = hash & 0xf0000000; + if (temp != 0) + { + hash = hash ^ (temp >> 24); + hash = hash ^ temp; + } + } + + return hash; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/scvars.cpp b/engines/glk/adrift/scvars.cpp new file mode 100644 index 0000000000..0ed62446c1 --- /dev/null +++ b/engines/glk/adrift/scvars.cpp @@ -0,0 +1,1918 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/scprotos.h" +#include "glk/adrift/scgamest.h" +#include "glk/glk.h" +#include "glk/events.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Gender enumerations are 0/1/2, but 1/2/3 in jAsea. The 0/1/2 values + * seem to be right. Is jAsea off by one? + * + * o jAsea tries to read Globals.CompileDate. It's just CompileDate. + * + * o State_ and obstate are implemented, but not fully tested due to a lack + * of games that use them. + */ + +/* Assorted definitions and constants. */ +static const sc_uint VARS_MAGIC = 0xabcc7a71; +static const sc_char NUL = '\0'; + +/* Variables trace flag. */ +static sc_bool var_trace = FALSE; + +/* Table of numbers zero to twenty spelled out. */ +enum { VAR_NUMBERS_SIZE = 21 }; +static const sc_char *const VAR_NUMBERS[VAR_NUMBERS_SIZE] = { + "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", + "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", + "sixteen", "seventeen", "eighteen", "nineteen", "twenty" +}; + +/* Variable entry, held on a list hashed by variable name. */ +typedef struct sc_var_s +{ + struct sc_var_s *next; + + const sc_char *name; + sc_int type; + sc_vartype_t value; +} sc_var_t; +typedef sc_var_t *sc_varref_t; + +/* + * Variables set structure. A self-contained set of variables on which + * variables functions operate. 211 is prime, making it a reasonable hash + * divisor. There's no rehashing here; few games, if any, are likely to + * exceed a fill factor of two (~422 variables). + */ +enum { VAR_HASH_TABLE_SIZE = 211 }; +typedef struct sc_var_set_s +{ + sc_uint magic; + sc_prop_setref_t bundle; + sc_int referenced_character; + sc_int referenced_object; + sc_int referenced_number; + sc_bool is_number_referenced; + sc_char *referenced_text; + sc_char *temporary; + uint32 timestamp; + sc_uint time_offset; + sc_gameref_t game; + sc_varref_t variable[VAR_HASH_TABLE_SIZE]; +} sc_var_set_t; + + +/* + * var_is_valid() + * + * Return TRUE if pointer is a valid variables set, FALSE otherwise. + */ +static sc_bool +var_is_valid (sc_var_setref_t vars) +{ + return vars && vars->magic == VARS_MAGIC; +} + + +/* + * var_hash_name() + * + * Hash a variable name, modulo'ed to the number of buckets. + */ +static sc_uint +var_hash_name (const sc_char *name) +{ + return sc_hash (name) % VAR_HASH_TABLE_SIZE; +} + + +/* + * var_create_empty() + * + * Create and return a new empty set of variables. + */ +static sc_var_setref_t +var_create_empty (void) +{ + sc_var_setref_t vars; + sc_int index_; + + /* Create a clean set of variables. */ + vars = (sc_var_setref_t)sc_malloc(sizeof (*vars)); + vars->magic = VARS_MAGIC; + vars->bundle = nullptr; + vars->referenced_character = -1; + vars->referenced_object = -1; + vars->referenced_number = 0; + vars->is_number_referenced = FALSE; + vars->referenced_text = nullptr; + vars->temporary = nullptr; + vars->timestamp = g_vm->_events->getTotalPlayTicks() / 1000; + vars->time_offset = 0; + vars->game = nullptr; + + /* Clear all variable hash lists. */ + for (index_ = 0; index_ < VAR_HASH_TABLE_SIZE; index_++) + vars->variable[index_] = nullptr; + + return vars; +} + + +/* + * var_destroy() + * + * Destroy a variable set, and free its heap memory. + */ +void +var_destroy (sc_var_setref_t vars) +{ + sc_int index_; + assert (var_is_valid (vars)); + + /* + * Free the content of each string variable, and variable entry. String + * variable content needs to use mutable string instead of const string. + */ + for (index_ = 0; index_ < VAR_HASH_TABLE_SIZE; index_++) + { + sc_varref_t var, next; + + for (var = vars->variable[index_]; var; var = next) + { + next = var->next; + if (var->type == VAR_STRING) + sc_free (var->value.mutable_string); + sc_free (var); + } + } + + /* Free any temporary and reference text storage area. */ + sc_free (vars->temporary); + sc_free (vars->referenced_text); + + /* Poison and free the variable set itself. */ + memset (vars, 0xaa, sizeof (*vars)); + sc_free (vars); +} + + +/* + * var_find() + * var_add() + * + * Find and return a pointer to a named variable structure, or nullptr if no such + * variable exists, and add a new variable structure to the lists. + */ +static sc_varref_t +var_find (sc_var_setref_t vars, const sc_char *name) +{ + sc_uint hash; + sc_varref_t var; + + /* Hash name, search list and return if name match found. */ + hash = var_hash_name (name); + for (var = vars->variable[hash]; var; var = var->next) + { + if (strcmp (name, var->name) == 0) + break; + } + + /* Return variable, or nullptr if no such variable. */ + return var; +} + +static sc_varref_t +var_add (sc_var_setref_t vars, const sc_char *name, sc_int type) +{ + sc_varref_t var; + sc_uint hash; + + /* Create a new variable entry. */ + var = (sc_varref_t)sc_malloc (sizeof (*var)); + var->name = name; + var->type = type; + var->value.voidp = nullptr; + + /* Hash its name, and insert it at start of the relevant list. */ + hash = var_hash_name (name); + var->next = vars->variable[hash]; + vars->variable[hash] = var; + + return var; +} + + +/* + * var_get_scare_version() + * + * Return the value of %scare_version%. Used to generate the system version + * of this variable, and to re-initialize user versions initialized to zero. + */ +static sc_int +var_get_scare_version (void) +{ + sc_int major, minor, point, version; + + if (sscanf (SCARE_VERSION, "%ld.%ld.%ld", &major, &minor, &point) != 3) + { + sc_error ("var_get_scare_version: unable to generate scare_version\n"); + return 0; + } + + version = major * 10000 + minor * 100 + point; + return version; +} + + +/* + * var_put() + * + * Store a variable type in a named variable. If not present, the variable + * is created. Type is one of 'I' or 'S' for integer or string. + */ +void +var_put (sc_var_setref_t vars, + const sc_char *name, sc_int type, sc_vartype_t vt_value) +{ + sc_varref_t var; + sc_bool is_modification; + assert (var_is_valid (vars)); + assert (name); + + /* Check type is either integer or string. */ + switch (type) + { + case VAR_INTEGER: + case VAR_STRING: + break; + + default: + sc_fatal ("var_put: invalid variable type, %ld\n", type); + } + + /* See if the user variable already exists. */ + var = var_find (vars, name); + if (var) + { + /* Verify that nothing is trying to change the variable's type. */ + if (var->type != type) + sc_fatal ("var_put: variable type changed, %s\n", name); + + /* + * Special case %scare_version%. If a game changes its value, it may + * compromise version checking, so warn here, but continue. + */ + if (strcmp (name, "scare_version") == 0) + { + if (var->value.integer != vt_value.integer) + sc_error ("var_put: warning: %%%s%% value changed\n", name); + } + + is_modification = TRUE; + } + else + { + /* + * Special case %scare_version%. If a game defines this and initializes + * it to zero, re-initialize it to SCARE's version number. Games that + * define %scare_version%, initially zero, can use this to test if + * running under SCARE or Runner. + */ + if (strcmp (name, "scare_version") == 0 && vt_value.integer == 0) + { + vt_value.integer = var_get_scare_version (); + + if (var_trace) + sc_trace ("Variable: %%%s%% [new] caught and mapped\n", name); + } + + /* + * Create a new and empty variable entry. The mutable string needs to + * be set to nullptr here so that realloc works correctly on assigning + * the value below. + */ + var = var_add (vars, name, type); + var->value.mutable_string = nullptr; + + is_modification = FALSE; + } + + /* Update the existing variable, or populate the new one fully. */ + switch (var->type) + { + case VAR_INTEGER: + var->value.integer = vt_value.integer; + break; + + case VAR_STRING: + /* Use mutable string instead of const string. */ + var->value.mutable_string = (sc_char *)sc_realloc(var->value.mutable_string, + strlen (vt_value.string) + 1); + strcpy (var->value.mutable_string, vt_value.string); + break; + + default: + sc_fatal ("var_put: invalid variable type, %ld\n", var->type); + } + + if (var_trace) + { + sc_trace ("Variable: %%%s%%%s = ", + name, is_modification ? "" : " [new]"); + switch (var->type) + { + case VAR_INTEGER: + sc_trace ("%ld", var->value.integer); + break; + case VAR_STRING: + sc_trace ("\"%s\"", var->value.string); + break; + + default: + sc_trace ("[invalid variable type, %ld]", var->type); + break; + } + sc_trace ("\n"); + } +} + + +/* + * var_append_temp() + * + * Helper for object listers. Extends temporary, and appends the given text + * to the string. + */ +static void +var_append_temp (sc_var_setref_t vars, const sc_char *string) +{ + sc_bool new_sentence; + sc_int noted; + + if (!vars->temporary) + { + /* Create a new temporary area and copy string. */ + new_sentence = TRUE; + noted = 0; + vars->temporary = (sc_char *)sc_malloc (strlen (string) + 1); + strcpy (vars->temporary, string); + } + else + { + /* Append string to existing temporary. */ + new_sentence = (vars->temporary[0] == NUL); + noted = strlen (vars->temporary); + vars->temporary = (sc_char *)sc_realloc (vars->temporary, + strlen (vars->temporary) + + strlen (string) + 1); + strcat (vars->temporary, string); + } + + if (new_sentence) + vars->temporary[noted] = sc_toupper (vars->temporary[noted]); +} + + +/* + * var_print_object_np + * var_print_object + * + * Convenience functions to append an object's name, with and without any + * prefix, to variables temporary. + */ +static void +var_print_object_np (sc_gameref_t game, sc_int object) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *prefix, *normalized, *name; + + /* Get the object's prefix. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + + /* + * Try the same shenanigans as done by the equivalent function in the + * library. + */ + normalized = prefix; + if (sc_compare_word (prefix, "a", 1)) + { + normalized = prefix + 1; + var_append_temp (vars, "the"); + } + else if (sc_compare_word (prefix, "an", 2)) + { + normalized = prefix + 2; + var_append_temp (vars, "the"); + } + else if (sc_compare_word (prefix, "the", 3)) + { + normalized = prefix + 3; + var_append_temp (vars, "the"); + } + else if (sc_compare_word (prefix, "some", 4)) + { + normalized = prefix + 4; + var_append_temp (vars, "the"); + } + else if (sc_strempty (prefix)) + var_append_temp (vars, "the "); + + /* As with the library, handle the remaining prefix. */ + if (!sc_strempty (normalized)) + { + var_append_temp (vars, normalized); + var_append_temp (vars, " "); + } + else if (normalized > prefix) + var_append_temp (vars, " "); + + /* + * Print the object's name, again, as with the library, stripping any + * leading article + */ + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + if (sc_compare_word (name, "a", 1)) + name += 1; + else if (sc_compare_word (name, "an", 2)) + name += 2; + else if (sc_compare_word (name, "the", 3)) + name += 3; + else if (sc_compare_word (name, "some", 4)) + name += 4; + var_append_temp (vars, name); +} + +static void +var_print_object (sc_gameref_t game, sc_int object) +{ + const sc_var_setref_t vars = gs_get_vars (game); + const sc_prop_setref_t bundle = gs_get_bundle (game); + sc_vartype_t vt_key[3]; + const sc_char *prefix, *name; + + /* + * Get the object's prefix. As with the library, if the prefix is empty, + * put in an "a ". + */ + vt_key[0].string = "Objects"; + vt_key[1].integer = object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + if (!sc_strempty (prefix)) + { + var_append_temp (vars, prefix); + var_append_temp (vars, " "); + } + else + var_append_temp (vars, "a "); + + /* Print the object's name. */ + vt_key[2].string = "Short"; + name = prop_get_string (bundle, "S<-sis", vt_key); + var_append_temp (vars, name); +} + + +/* + * var_select_plurality() + * + * Convenience function for listers. Selects one of two responses depending + * on whether an object appears singular or plural. + */ +static const sc_char * +var_select_plurality (sc_gameref_t game, sc_int object, + const sc_char *singular, const sc_char *plural) +{ + return obj_appears_plural (game, object) ? plural : singular; +} + + +/* + * var_list_in_object() + * + * List the objects in a given container object. + */ +static void +var_list_in_object (sc_gameref_t game, sc_int container) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int object, count, trail; + + /* List out the objects contained in this object. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + /* Contained? */ + if (gs_object_position (game, object) == OBJ_IN_OBJECT + && gs_object_parent (game, object) == container) + { + if (count > 0) + { + if (count > 1) + var_append_temp (vars, ", "); + + /* Print out the current list object. */ + var_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + var_print_object (game, trail); + var_append_temp (vars, + var_select_plurality (game, trail, + " is inside ", + " are inside ")); + } + else + { + var_append_temp (vars, " and "); + var_print_object (game, trail); + var_append_temp (vars, " are inside "); + } + + /* Print out the container. */ + var_print_object_np (game, container); + var_append_temp (vars, "."); + } +} + + +/* + * var_list_on_object() + * + * List the objects on a given surface object. + */ +static void +var_list_on_object (sc_gameref_t game, sc_int supporter) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int object, count, trail; + + /* List out the objects standing on this object. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + /* Standing on? */ + if (gs_object_position (game, object) == OBJ_ON_OBJECT + && gs_object_parent (game, object) == supporter) + { + if (count > 0) + { + if (count > 1) + var_append_temp (vars, ", "); + + /* Print out the current list object. */ + var_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + var_print_object (game, trail); + var_append_temp (vars, + var_select_plurality (game, trail, + " is on ", " are on ")); + } + else + { + var_append_temp (vars, " and "); + var_print_object (game, trail); + var_append_temp (vars, " are on "); + } + + /* Print out the surface. */ + var_print_object_np (game, supporter); + var_append_temp (vars, "."); + } +} + + +/* + * var_list_onin_object() + * + * List the objects on and in a given associate object. + */ +static void +var_list_onin_object (sc_gameref_t game, sc_int associate) +{ + const sc_var_setref_t vars = gs_get_vars (game); + sc_int object, count, trail; + sc_bool supporting; + + /* List out the objects standing on this object. */ + count = 0; + trail = -1; + supporting = FALSE; + for (object = 0; object < gs_object_count (game); object++) + { + /* Standing on? */ + if (gs_object_position (game, object) == OBJ_ON_OBJECT + && gs_object_parent (game, object) == associate) + { + if (count > 0) + { + if (count > 1) + var_append_temp (vars, ", "); + + /* Print out the current list object. */ + var_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + var_print_object (game, trail); + var_append_temp (vars, + var_select_plurality (game, trail, + " is on ", " are on ")); + } + else + { + var_append_temp (vars, " and "); + var_print_object (game, trail); + var_append_temp (vars, " are on "); + } + + /* Print out the surface. */ + var_print_object_np (game, associate); + supporting = TRUE; + } + + /* List out the objects contained in this object. */ + count = 0; + trail = -1; + for (object = 0; object < gs_object_count (game); object++) + { + /* Contained? */ + if (gs_object_position (game, object) == OBJ_IN_OBJECT + && gs_object_parent (game, object) == associate) + { + if (count > 0) + { + if (count == 1) + { + if (supporting) + var_append_temp (vars, ", and "); + } + else + var_append_temp (vars, ", "); + + /* Print out the current list object. */ + var_print_object (game, trail); + } + trail = object; + count++; + } + } + if (count >= 1) + { + /* Print out final listed object. */ + if (count == 1) + { + if (supporting) + var_append_temp (vars, ", and "); + var_print_object (game, trail); + var_append_temp (vars, + var_select_plurality (game, trail, + " is inside ", + " are inside ")); + } + else + { + var_append_temp (vars, " and "); + var_print_object (game, trail); + var_append_temp (vars, " are inside"); + } + + /* Print out the container. */ + if (!supporting) + { + var_append_temp (vars, " "); + var_print_object_np (game, associate); + } + var_append_temp (vars, "."); + } + else + { + if (supporting) + var_append_temp (vars, "."); + } +} + + +/* + * var_return_integer() + * var_return_string() + * + * Convenience helpers for var_get_system(). Provide convenience and some + * mild syntactic sugar for making returning a value as a system variable + * a bit easier. Set appropriate values for return type and the relevant + * return value field, and always return TRUE. A macro was tempting here... + */ +static sc_bool +var_return_integer (sc_int value, sc_int *type, sc_vartype_t *vt_rvalue) +{ + *type = VAR_INTEGER; + vt_rvalue->integer = value; + return TRUE; +} + +static sc_bool +var_return_string (const sc_char *value, sc_int *type, sc_vartype_t *vt_rvalue) +{ + *type = VAR_STRING; + vt_rvalue->string = value; + return TRUE; +} + + +/* + * var_get_system() + * + * Construct a system variable, and return its type and value, or FALSE + * if invalid name passed in. Uses var_return_*() to reduce code untidiness. + */ +static sc_bool +var_get_system (sc_var_setref_t vars, + const sc_char *name, sc_int *type, sc_vartype_t *vt_rvalue) +{ + const sc_prop_setref_t bundle = vars->bundle; + const sc_gameref_t game = vars->game; + + /* Check name for known system variables. */ + if (strcmp (name, "author") == 0) + { + sc_vartype_t vt_key[2]; + const sc_char *author; + + /* Get and return the global gameauthor string. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "GameAuthor"; + author = prop_get_string (bundle, "S<-ss", vt_key); + if (sc_strempty (author)) + author = "[Author unknown]"; + + return var_return_string (author, type, vt_rvalue); + } + + else if (strcmp (name, "character") == 0) + { + /* See if there is a referenced character. */ + if (vars->referenced_character != -1) + { + sc_vartype_t vt_key[3]; + const sc_char *npc_name; + + /* Return the character name string. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = vars->referenced_character; + vt_key[2].string = "Name"; + npc_name = prop_get_string (bundle, "S<-sis", vt_key); + if (sc_strempty (npc_name)) + npc_name = "[Character unknown]"; + + return var_return_string (npc_name, type, vt_rvalue); + } + else + { + sc_error ("var_get_system: no referenced character yet\n"); + return var_return_string ("[Character unknown]", type, vt_rvalue); + } + } + + else if (strcmp (name, "heshe") == 0 || strcmp (name, "himher") == 0) + { + /* See if there is a referenced character. */ + if (vars->referenced_character != -1) + { + sc_vartype_t vt_key[3]; + sc_int gender; + const sc_char *retval; + + /* Return the appropriate character gender string. */ + vt_key[0].string = "NPCs"; + vt_key[1].integer = vars->referenced_character; + vt_key[2].string = "Gender"; + gender = prop_get_integer (bundle, "I<-sis", vt_key); + switch (gender) + { + case NPC_MALE: + retval = (strcmp (name, "heshe") == 0) ? "he" : "him"; + break; + case NPC_FEMALE: + retval = (strcmp (name, "heshe") == 0) ? "she" : "her"; + break; + case NPC_NEUTER: + retval = "it"; + break; + + default: + sc_error ("var_get_system: unknown gender, %ld\n", gender); + retval = "[Gender unknown]"; + break; + } + return var_return_string (retval, type, vt_rvalue); + } + else + { + sc_error ("var_get_system: no referenced character yet\n"); + return var_return_string ("[Gender unknown]", type, vt_rvalue); + } + } + + else if (strncmp (name, "in_", 3) == 0) + { + sc_int saved_ref_object = vars->referenced_object; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for in_\n"); + return var_return_string ("[In_ unavailable]", type, vt_rvalue); + } + if (!uip_match ("%object%", name + 3, game)) + { + sc_error ("var_get_system: invalid object for in_\n"); + return var_return_string ("[In_ unavailable]", type, vt_rvalue); + } + + /* Clear any current temporary for appends. */ + vars->temporary = (sc_char *)sc_realloc (vars->temporary, 1); + strcpy (vars->temporary, ""); + + /* Write what's in the object into temporary. */ + var_list_in_object (game, vars->referenced_object); + + /* Restore saved referenced object and return. */ + vars->referenced_object = saved_ref_object; + return var_return_string (vars->temporary, type, vt_rvalue); + } + + else if (strcmp (name, "maxscore") == 0) + { + sc_vartype_t vt_key[2]; + sc_int maxscore; + + /* Return the maximum score. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "MaxScore"; + maxscore = prop_get_integer (bundle, "I<-ss", vt_key); + + return var_return_integer (maxscore, type, vt_rvalue); + } + + else if (strcmp (name, "modified") == 0) + { + sc_vartype_t vt_key; + const sc_char *compiledate; + + /* Return the game compilation date. */ + vt_key.string = "CompileDate"; + compiledate = prop_get_string (bundle, "S<-s", &vt_key); + if (sc_strempty (compiledate)) + compiledate = "[Modified unknown]"; + + return var_return_string (compiledate, type, vt_rvalue); + } + + else if (strcmp (name, "number") == 0) + { + /* Return the referenced number, or 0 if none yet. */ + if (!vars->is_number_referenced) + sc_error ("var_get_system: no referenced number yet\n"); + + return var_return_integer (vars->referenced_number, type, vt_rvalue); + } + + else if (strcmp (name, "object") == 0) + { + /* See if we have a referenced object yet. */ + if (vars->referenced_object != -1) + { + /* Return object name with its prefix. */ + sc_vartype_t vt_key[3]; + const sc_char *prefix, *objname; + + vt_key[0].string = "Objects"; + vt_key[1].integer = vars->referenced_object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + + vars->temporary = (sc_char *)sc_realloc (vars->temporary, strlen (prefix) + 1); + strcpy (vars->temporary, prefix); + + vt_key[2].string = "Short"; + objname = prop_get_string (bundle, "S<-sis", vt_key); + + vars->temporary = (sc_char *)sc_realloc (vars->temporary, + strlen (vars->temporary) + + strlen (objname) + 2); + strcat (vars->temporary, " "); + strcat (vars->temporary, objname); + + return var_return_string (vars->temporary, type, vt_rvalue); + } + else + { + sc_error ("var_get_system: no referenced object yet\n"); + return var_return_string ("[Object unknown]", type, vt_rvalue); + } + } + + else if (strcmp (name, "obstate") == 0) + { + sc_vartype_t vt_key[3]; + sc_bool is_statussed; + sc_char *state; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for obstate\n"); + return var_return_string ("[Obstate unavailable]", type, vt_rvalue); + } + if (vars->referenced_object == -1) + { + sc_error ("var_get_system: no object for obstate\n"); + return var_return_string ("[Obstate unavailable]", type, vt_rvalue); + } + + /* + * If not a stateful object, Runner 4.0.45 crashes; we'll do something + * different here. + */ + vt_key[0].string = "Objects"; + vt_key[1].integer = vars->referenced_object; + vt_key[2].string = "CurrentState"; + is_statussed = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (!is_statussed) + return var_return_string ("stateless", type, vt_rvalue); + + /* Get state, and copy to temporary. */ + state = obj_state_name (game, vars->referenced_object); + if (!state) + { + sc_error ("var_get_system: invalid state for obstate\n"); + return var_return_string ("[Obstate unknown]", type, vt_rvalue); + } + vars->temporary = (sc_char *)sc_realloc (vars->temporary, strlen (state) + 1); + strcpy (vars->temporary, state); + sc_free (state); + + /* Return temporary. */ + return var_return_string (vars->temporary, type, vt_rvalue); + } + + else if (strcmp (name, "obstatus") == 0) + { + sc_vartype_t vt_key[3]; + sc_bool is_openable; + sc_int openness; + const sc_char *retval; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for obstatus\n"); + return var_return_string ("[Obstatus unavailable]", type, vt_rvalue); + } + if (vars->referenced_object == -1) + { + sc_error ("var_get_system: no object for obstatus\n"); + return var_return_string ("[Obstatus unavailable]", type, vt_rvalue); + } + + /* If not an openable object, return unopenable to match Adrift. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = vars->referenced_object; + vt_key[2].string = "Openable"; + is_openable = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (!is_openable) + return var_return_string ("unopenable", type, vt_rvalue); + + /* Return one of open, closed, or locked. */ + openness = gs_object_openness (game, vars->referenced_object); + switch (openness) + { + case OBJ_OPEN: + retval = "open"; + break; + case OBJ_CLOSED: + retval = "closed"; + break; + case OBJ_LOCKED: + retval = "locked"; + break; + default: + retval = "[Obstatus unknown]"; + break; + } + return var_return_string (retval, type, vt_rvalue); + } + + else if (strncmp (name, "on_", 3) == 0) + { + sc_int saved_ref_object = vars->referenced_object; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for on_\n"); + return var_return_string ("[On_ unavailable]", type, vt_rvalue); + } + if (!uip_match ("%object%", name + 3, game)) + { + sc_error ("var_get_system: invalid object for on_\n"); + return var_return_string ("[On_ unavailable]", type, vt_rvalue); + } + + /* Clear any current temporary for appends. */ + vars->temporary = (sc_char *)sc_realloc (vars->temporary, 1); + strcpy (vars->temporary, ""); + + /* Write what's on the object into temporary. */ + var_list_on_object (game, vars->referenced_object); + + /* Restore saved referenced object and return. */ + vars->referenced_object = saved_ref_object; + return var_return_string (vars->temporary, type, vt_rvalue); + } + + else if (strncmp (name, "onin_", 5) == 0) + { + sc_int saved_ref_object = vars->referenced_object; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for onin_\n"); + return var_return_string ("[Onin_ unavailable]", type, vt_rvalue); + } + if (!uip_match ("%object%", name + 5, game)) + { + sc_error ("var_get_system: invalid object for onin_\n"); + return var_return_string ("[Onin_ unavailable]", type, vt_rvalue); + } + + /* Clear any current temporary for appends. */ + vars->temporary = (sc_char *)sc_realloc (vars->temporary, 1); + strcpy (vars->temporary, ""); + + /* Write what's on/in the object into temporary. */ + var_list_onin_object (game, vars->referenced_object); + + /* Restore saved referenced object and return. */ + vars->referenced_object = saved_ref_object; + return var_return_string (vars->temporary, type, vt_rvalue); + } + + else if (strcmp (name, "player") == 0) + { + sc_vartype_t vt_key[2]; + const sc_char *playername; + + /* + * Return player's name from properties, or just "Player" if not set + * in the properties. + */ + vt_key[0].string = "Globals"; + vt_key[1].string = "PlayerName"; + playername = prop_get_string (bundle, "S<-ss", vt_key); + if (sc_strempty (playername)) + playername = "Player"; + + return var_return_string (playername, type, vt_rvalue); + } + + else if (strcmp (name, "room") == 0) + { + const sc_char *roomname; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for room\n"); + return var_return_string ("[Room unavailable]", type, vt_rvalue); + } + + /* Return the current player room. */ + roomname = lib_get_room_name (game, gs_playerroom (game)); + return var_return_string (roomname, type, vt_rvalue); + } + + else if (strcmp (name, "score") == 0) + { + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for score\n"); + return var_return_integer (0, type, vt_rvalue); + } + + /* Return the current game score. */ + return var_return_integer (game->score, type, vt_rvalue); + } + + else if (strncmp (name, "state_", 6) == 0) + { + sc_int saved_ref_object = vars->referenced_object; + sc_vartype_t vt_key[3]; + sc_bool is_statussed; + sc_char *state; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for state_\n"); + return var_return_string ("[State_ unavailable]", type, vt_rvalue); + } + if (!uip_match ("%object%", name + 6, game)) + { + sc_error ("var_get_system: invalid object for state_\n"); + return var_return_string ("[State_ unavailable]", type, vt_rvalue); + } + + /* Verify this is a stateful object. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = vars->referenced_object; + vt_key[2].string = "CurrentState"; + is_statussed = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (!is_statussed) + { + vars->referenced_object = saved_ref_object; + sc_error ("var_get_system: stateless object for state_\n"); + return var_return_string ("[State_ unavailable]", type, vt_rvalue); + } + + /* Get state, and copy to temporary. */ + state = obj_state_name (game, vars->referenced_object); + if (!state) + { + vars->referenced_object = saved_ref_object; + sc_error ("var_get_system: invalid state for state_\n"); + return var_return_string ("[State_ unknown]", type, vt_rvalue); + } + vars->temporary = (sc_char *)sc_realloc (vars->temporary, strlen (state) + 1); + strcpy (vars->temporary, state); + sc_free (state); + + /* Restore saved referenced object and return. */ + vars->referenced_object = saved_ref_object; + return var_return_string (vars->temporary, type, vt_rvalue); + } + + else if (strncmp (name, "status_", 7) == 0) + { + sc_int saved_ref_object = vars->referenced_object; + sc_vartype_t vt_key[3]; + sc_bool is_openable; + sc_int openness; + const sc_char *retval; + + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for status_\n"); + return var_return_string ("[Status_ unavailable]", type, vt_rvalue); + } + if (!uip_match ("%object%", name + 7, game)) + { + sc_error ("var_get_system: invalid object for status_\n"); + return var_return_string ("[Status_ unavailable]", type, vt_rvalue); + } + + /* Verify this is an openable object. */ + vt_key[0].string = "Objects"; + vt_key[1].integer = vars->referenced_object; + vt_key[2].string = "Openable"; + is_openable = prop_get_integer (bundle, "I<-sis", vt_key) != 0; + if (!is_openable) + { + vars->referenced_object = saved_ref_object; + sc_error ("var_get_system: stateless object for status_\n"); + return var_return_string ("[Status_ unavailable]", type, vt_rvalue); + } + + /* Return one of open, closed, or locked. */ + openness = gs_object_openness (game, vars->referenced_object); + switch (openness) + { + case OBJ_OPEN: + retval = "open"; + break; + case OBJ_CLOSED: + retval = "closed"; + break; + case OBJ_LOCKED: + retval = "locked"; + break; + default: + retval = "[Status_ unknown]"; + break; + } + + /* Restore saved referenced object and return. */ + vars->referenced_object = saved_ref_object; + return var_return_string (retval, type, vt_rvalue); + } + + else if (strcmp (name, "t_number") == 0) + { + /* See if we have a referenced number yet. */ + if (vars->is_number_referenced) + { + sc_int number; + const sc_char *retval; + + /* Return the referenced number as a string. */ + number = vars->referenced_number; + if (number >= 0 && number < VAR_NUMBERS_SIZE) + retval = VAR_NUMBERS[number]; + else + { + vars->temporary = (sc_char *)sc_realloc (vars->temporary, 32); + sprintf (vars->temporary, "%ld", number); + retval = vars->temporary; + } + + return var_return_string (retval, type, vt_rvalue); + } + else + { + sc_error ("var_get_system: no referenced number yet\n"); + return var_return_string ("[Number unknown]", type, vt_rvalue); + } + } + + else if (strncmp (name, "t_", 2) == 0) + { + sc_varref_t var; + + /* Find the variable; must be a user, not a system, one. */ + var = var_find (vars, name + 2); + if (!var) + { + sc_error ("var_get_system:" + " no such variable, %s\n", name + 2); + return var_return_string ("[Unknown variable]", type, vt_rvalue); + } + else if (var->type != VAR_INTEGER) + { + sc_error ("var_get_system:" + " not an integer variable, %s\n", name + 2); + return var_return_string (var->value.string, type, vt_rvalue); + } + else + { + sc_int number; + const sc_char *retval; + + /* Return the variable value as a string. */ + number = var->value.integer; + if (number >= 0 && number < VAR_NUMBERS_SIZE) + retval = VAR_NUMBERS[number]; + else + { + vars->temporary = (sc_char *)sc_realloc (vars->temporary, 32); + sprintf (vars->temporary, "%ld", number); + retval = vars->temporary; + } + + return var_return_string (retval, type, vt_rvalue); + } + } + + else if (strcmp (name, "text") == 0) + { + const sc_char *retval; + + /* Return any referenced text, otherwise a neutral string. */ + if (vars->referenced_text) + retval = vars->referenced_text; + else + { + sc_error ("var_get_system: no text yet to reference\n"); + retval = "[Text unknown]"; + } + + return var_return_string (retval, type, vt_rvalue); + } + + else if (strcmp (name, "theobject") == 0) + { + /* See if we have a referenced object yet. */ + if (vars->referenced_object != -1) + { + /* Return object name prefixed with "the"... */ + sc_vartype_t vt_key[3]; + const sc_char *prefix, *normalized, *objname; + + vt_key[0].string = "Objects"; + vt_key[1].integer = vars->referenced_object; + vt_key[2].string = "Prefix"; + prefix = prop_get_string (bundle, "S<-sis", vt_key); + + vars->temporary = (sc_char *)sc_realloc (vars->temporary, strlen (prefix) + 5); + strcpy (vars->temporary, ""); + + normalized = prefix; + if (sc_compare_word (prefix, "a", 1)) + { + strcat (vars->temporary, "the"); + normalized = prefix + 1; + } + else if (sc_compare_word (prefix, "an", 2)) + { + strcat (vars->temporary, "the"); + normalized = prefix + 2; + } + else if (sc_compare_word (prefix, "the", 3)) + { + strcat (vars->temporary, "the"); + normalized = prefix + 3; + } + else if (sc_compare_word (prefix, "some", 4)) + { + strcat (vars->temporary, "the"); + normalized = prefix + 4; + } + else if (sc_strempty (prefix)) + strcat (vars->temporary, "the "); + + if (!sc_strempty (normalized)) + { + strcat (vars->temporary, normalized); + strcat (vars->temporary, " "); + } + else if (normalized > prefix) + strcat (vars->temporary, " "); + + vt_key[2].string = "Short"; + objname = prop_get_string (bundle, "S<-sis", vt_key); + if (sc_compare_word (objname, "a", 1)) + objname += 1; + else if (sc_compare_word (objname, "an", 2)) + objname += 2; + else if (sc_compare_word (objname, "the", 3)) + objname += 3; + else if (sc_compare_word (objname, "some", 4)) + objname += 4; + + vars->temporary = (sc_char *)sc_realloc (vars->temporary, + strlen (vars->temporary) + + strlen (objname) + 1); + strcat (vars->temporary, objname); + + return var_return_string (vars->temporary, type, vt_rvalue); + } + else + { + sc_error ("var_get_system: no referenced object yet\n"); + return var_return_string ("[Object unknown]", type, vt_rvalue); + } + } + + else if (strcmp (name, "time") == 0) + { + double delta; + sc_int retval; + + /* Return the elapsed game time in seconds. */ + delta = vars->timestamp - (g_vm->_events->getTotalPlayTicks() / 1000); + retval = (sc_int) delta + vars->time_offset; + + return var_return_integer (retval, type, vt_rvalue); + } + + else if (strcmp (name, "title") == 0) + { + sc_vartype_t vt_key[2]; + const sc_char *gamename; + + /* Return the game's title. */ + vt_key[0].string = "Globals"; + vt_key[1].string = "GameName"; + gamename = prop_get_string (bundle, "S<-ss", vt_key); + if (sc_strempty (gamename)) + gamename = "[Title unknown]"; + + return var_return_string (gamename, type, vt_rvalue); + } + + else if (strcmp (name, "turns") == 0) + { + /* Check there's enough information to return a value. */ + if (!game) + { + sc_error ("var_get_system: no game for turns\n"); + return var_return_integer (0, type, vt_rvalue); + } + + /* Return the count of game turns. */ + return var_return_integer (game->turns, type, vt_rvalue); + } + + else if (strcmp (name, "version") == 0) + { + /* Return the Adrift emulation level of SCARE. */ + return var_return_integer (SCARE_EMULATION, type, vt_rvalue); + } + + else if (strcmp (name, "scare_version") == 0) + { + /* Private system variable, return SCARE's version number. */ + return var_return_integer (var_get_scare_version (), type, vt_rvalue); + } + + return FALSE; +} + + +/* + * var_get_user() + * + * Retrieve a user variable, and return its type and value, or FALSE if the + * name passed in is not a defined user variable. + */ +static sc_bool +var_get_user (sc_var_setref_t vars, + const sc_char *name, sc_int *type, sc_vartype_t *vt_rvalue) +{ + sc_varref_t var; + + /* Check user variables for a reference to the named variable. */ + var = var_find (vars, name); + if (var) + { + /* Copy out variable details. */ + *type = var->type; + switch (var->type) + { + case VAR_INTEGER: + vt_rvalue->integer = var->value.integer; + break; + case VAR_STRING: + vt_rvalue->string = var->value.string; + break; + + default: + sc_fatal ("var_get_user: invalid variable type, %ld\n", var->type); + } + + /* Return success. */ + return TRUE; + } + + return FALSE; +} + + +/* + * var_get() + * + * Retrieve a variable, and return its value and type. Returns FALSE if the + * named variable does not exist. + */ +sc_bool +var_get (sc_var_setref_t vars, + const sc_char *name, sc_int *type, sc_vartype_t *vt_rvalue) +{ + sc_bool status; + assert (var_is_valid (vars)); + assert (name && type && vt_rvalue); + + /* + * Check user and system variables for a reference to the name. User + * variables take precedence over system ones; that is, they may override + * them in a game. + */ + status = var_get_user (vars, name, type, vt_rvalue); + if (!status) + status = var_get_system (vars, name, type, vt_rvalue); + + if (var_trace) + { + if (status) + { + sc_trace ("Variable: %%%s%% retrieved, ", name); + switch (*type) + { + case VAR_INTEGER: + sc_trace ("%ld", vt_rvalue->integer); + break; + case VAR_STRING: + sc_trace ("\"%s\"", vt_rvalue->string); + break; + + default: + sc_trace ("Variable: invalid variable type, %ld\n", *type); + break; + } + sc_trace ("\n"); + } + else + sc_trace ("Variable: \"%s\", no such variable\n", name); + } + + return status; +} + + +/* + * var_put_integer() + * var_get_integer() + * + * Convenience functions to store and retrieve an integer variable. It is + * an error for the variable not to exist or to have the wrong type. + */ +void +var_put_integer (sc_var_setref_t vars, const sc_char *name, sc_int value) +{ + sc_vartype_t vt_value; + assert (var_is_valid (vars)); + + vt_value.integer = value; + var_put (vars, name, VAR_INTEGER, vt_value); +} + +sc_int +var_get_integer (sc_var_setref_t vars, const sc_char *name) +{ + sc_vartype_t vt_rvalue; + sc_int type; + assert (var_is_valid (vars)); + + if (!var_get (vars, name, &type, &vt_rvalue)) + sc_fatal ("var_get_integer: no such variable, %s\n", name); + else if (type != VAR_INTEGER) + sc_fatal ("var_get_integer: not an integer, %s\n", name); + + return vt_rvalue.integer; +} + + +/* + * var_put_string() + * var_get_string() + * + * Convenience functions to store and retrieve a string variable. It is + * an error for the variable not to exist or to have the wrong type. + */ +void +var_put_string (sc_var_setref_t vars, + const sc_char *name, const sc_char *string) +{ + sc_vartype_t vt_value; + assert (var_is_valid (vars)); + + vt_value.string = string; + var_put (vars, name, VAR_STRING, vt_value); +} + +const sc_char * +var_get_string (sc_var_setref_t vars, const sc_char *name) +{ + sc_vartype_t vt_rvalue; + sc_int type; + assert (var_is_valid (vars)); + + if (!var_get (vars, name, &type, &vt_rvalue)) + sc_fatal ("var_get_string: no such variable, %s\n", name); + else if (type != VAR_STRING) + sc_fatal ("var_get_string: not a string, %s\n", name); + + return vt_rvalue.string; +} + + +/* + * var_create() + * + * Create and return a new set of variables. Variables are created from the + * properties bundle passed in. + */ +sc_var_setref_t +var_create (sc_prop_setref_t bundle) +{ + sc_var_setref_t vars; + sc_int var_count, index_; + sc_vartype_t vt_key[3]; + assert (bundle); + + /* Create a clean set of variables to fill from the bundle. */ + vars = var_create_empty (); + vars->bundle = bundle; + + /* Retrieve the count of variables. */ + vt_key[0].string = "Variables"; + var_count = prop_get_child_count (bundle, "I<-s", vt_key); + + /* Create a variable for each variable property held. */ + for (index_ = 0; index_ < var_count; index_++) + { + const sc_char *name; + sc_int var_type; + const sc_char *value; + + /* Retrieve variable name, type, and string initial value. */ + vt_key[1].integer = index_; + vt_key[2].string = "Name"; + name = prop_get_string (bundle, "S<-sis", vt_key); + + vt_key[2].string = "Type"; + var_type = prop_get_integer (bundle, "I<-sis", vt_key); + + vt_key[2].string = "Value"; + value = prop_get_string (bundle, "S<-sis", vt_key); + + /* Handle numerics and strings differently. */ + switch (var_type) + { + case TAFVAR_NUMERIC: + { + sc_int integer_value; + if (sscanf (value, "%ld", &integer_value) != 1) + { + sc_error ("var_create:" + " invalid numeric variable %s, %s\n", name, value); + integer_value = 0; + } + var_put_integer (vars, name, integer_value); + break; + } + + case TAFVAR_STRING: + var_put_string (vars, name, value); + break; + + default: + sc_fatal ("var_create: invalid variable type, %ld\n", var_type); + } + } + + return vars; +} + + +/* + * var_register_game() + * + * Register the game, used by variables to satisfy requests for selected + * system variables. To ensure integrity, the game being registered must + * reference this variable set. + */ +void +var_register_game (sc_var_setref_t vars, sc_gameref_t game) +{ + assert (var_is_valid (vars)); + assert (gs_is_game_valid (game)); + + if (vars != gs_get_vars (game)) + sc_fatal ("var_register_game: game binding error\n"); + + vars->game = game; +} + + +/* + * var_set_ref_character() + * var_set_ref_object() + * var_set_ref_number() + * var_set_ref_text() + * + * Set the "referenced" character, object, number, and text. + */ +void +var_set_ref_character (sc_var_setref_t vars, sc_int character) +{ + assert (var_is_valid (vars)); + vars->referenced_character = character; +} + +void +var_set_ref_object (sc_var_setref_t vars, sc_int object) +{ + assert (var_is_valid (vars)); + vars->referenced_object = object; +} + +void +var_set_ref_number (sc_var_setref_t vars, sc_int number) +{ + assert (var_is_valid (vars)); + vars->referenced_number = number; + vars->is_number_referenced = TRUE; +} + +void +var_set_ref_text (sc_var_setref_t vars, const sc_char *text) +{ + assert (var_is_valid (vars)); + + /* Take a copy of the string, and retain it. */ + vars->referenced_text = (sc_char *)sc_realloc (vars->referenced_text, strlen (text) + 1); + strcpy (vars->referenced_text, text); +} + + +/* + * var_get_ref_character() + * var_get_ref_object() + * var_get_ref_number() + * var_get_ref_text() + * + * Get the "referenced" character, object, number, and text. + */ +sc_int +var_get_ref_character (sc_var_setref_t vars) +{ + assert (var_is_valid (vars)); + return vars->referenced_character; +} + +sc_int +var_get_ref_object (sc_var_setref_t vars) +{ + assert (var_is_valid (vars)); + return vars->referenced_object; +} + +sc_int +var_get_ref_number (sc_var_setref_t vars) +{ + assert (var_is_valid (vars)); + return vars->referenced_number; +} + +const sc_char * +var_get_ref_text (sc_var_setref_t vars) +{ + assert (var_is_valid (vars)); + + /* + * If currently nullptr, return "". A game may check restrictions involving + * referenced text before any value has been set; returning "" here for + * this case prevents problems later (strcmp (nullptr, ...), for example). + */ + return vars->referenced_text ? vars->referenced_text : ""; +} + + +/* + * var_get_elapsed_seconds() + * var_set_elapsed_seconds() + * + * Get a count of seconds elapsed since the variables were created (start + * of game), and set the count to a given value (game restore). + */ +sc_uint +var_get_elapsed_seconds (sc_var_setref_t vars) +{ + double delta; + assert (var_is_valid (vars)); + + delta = vars->timestamp - g_vm->_events->getTotalPlayTicks(); + return (sc_uint) delta + vars->time_offset; +} + +void +var_set_elapsed_seconds (sc_var_setref_t vars, sc_uint seconds) +{ + assert (var_is_valid (vars)); + + /* + * Reset the timestamp to now, and store seconds in offset. This is sort-of + * forced by the fact that ANSI offers difftime but no 'settime' -- here, + * we'd really want to set the timestamp to now less seconds. + */ + vars->timestamp = g_vm->_events->getTotalPlayTicks() / 1000; + vars->time_offset = seconds; +} + + +/* + * var_debug_trace() + * + * Set variable tracing on/off. + */ +void +var_debug_trace (sc_bool flag) +{ + var_trace = flag; +} + + +/* + * var_debug_dump() + * + * Print out a complete variables set. + */ +void +var_debug_dump (sc_var_setref_t vars) +{ + sc_int index_; + sc_varref_t var; + assert (var_is_valid (vars)); + + /* Dump complete structure. */ + sc_trace ("Variable: debug dump follows...\n"); + sc_trace ("vars->bundle = %p\n", (void *) vars->bundle); + sc_trace ("vars->referenced_character = %ld\n", vars->referenced_character); + sc_trace ("vars->referenced_object = %ld\n", vars->referenced_object); + sc_trace ("vars->referenced_number = %ld\n", vars->referenced_number); + sc_trace ("vars->is_number_referenced = %s\n", + vars->is_number_referenced ? "true" : "false"); + + sc_trace ("vars->referenced_text = "); + if (vars->referenced_text) + sc_trace ("\"%s\"\n", vars->referenced_text); + else + sc_trace ("(nil)\n"); + + sc_trace ("vars->temporary = %p\n", (void *) vars->temporary); + sc_trace("vars->timestamp = %lu\n", (sc_uint) vars->timestamp); + sc_trace ("vars->game = %p\n", (void *) vars->game); + + sc_trace ("vars->variables =\n"); + for (index_ = 0; index_ < VAR_HASH_TABLE_SIZE; index_++) + { + for (var = vars->variable[index_]; var; var = var->next) + { + if (var == vars->variable[index_]) + sc_trace ("%3ld : ", index_); + else + sc_trace (" : "); + switch (var->type) + { + case VAR_STRING: + sc_trace ("[String ] %s = \"%s\"", var->name, var->value.string); + break; + case VAR_INTEGER: + sc_trace ("[Integer] %s = %ld", var->name, var->value.integer); + break; + + default: + sc_trace ("[Invalid] %s = %p", var->name, var->value.voidp); + break; + } + sc_trace ("\n"); + } + } +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sxfile.cpp b/engines/glk/adrift/sxfile.cpp new file mode 100644 index 0000000000..41cfcc485e --- /dev/null +++ b/engines/glk/adrift/sxfile.cpp @@ -0,0 +1,201 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/sxprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * Structure for representing a fake game save/restore file. Used to catch + * a serialized gamestate, and return it later on restore. For now we allow + * only one of these to exist. + */ +struct sx_scr_stream_t { + sc_byte *data; + sc_int length; + sc_bool is_open; + sc_bool is_writable; +}; +static sx_scr_stream_t scr_serialization_stream = {NULL, 0, FALSE, FALSE}; + + +/* + * file_open_file_callback() + * file_read_file_callback() + * file_write_file_callback() + * file_close_file_callback() + * + * Fake a single gamestate save/restore file. Used to satisfy requests from + * the script to serialize and restore a gamestate. Only one "file" can + * exist, meaning that a script must restore a saved game before trying to + * save another. + */ +void *file_open_file_callback (sc_bool is_save) +{ + sx_scr_stream_t *const stream = &scr_serialization_stream; + + /* Detect any problems due to scripting limitations. */ + if (stream->is_open) + { + scr_test_failed ("File open error: %s", + "stream is in use (script limitation)"); + return NULL; + } + else if (is_save && stream->data) + { + scr_test_failed ("File open error: %s", + "stream has not been read (script limitation)"); + return NULL; + } + + /* + * Set up the stream for the requested mode. Act as if no such file if + * no data available for a read-only open. + */ + if (is_save) + { + stream->data = NULL; + stream->length = 0; + } + else if (!stream->data) + return NULL; + + stream->is_open = TRUE; + stream->is_writable = is_save; + return stream; +} + +sc_int +file_read_file_callback (void *opaque, sc_byte *buffer, sc_int length) +{ + sx_scr_stream_t *const stream = (sx_scr_stream_t *)opaque; + sc_int bytes; + assert (opaque && buffer && length > 0); + + /* Detect any problems with the callback parameters. */ + if (stream != &scr_serialization_stream) + { + scr_test_failed ("File read error: %s", "stream is invalid"); + return 0; + } + else if (!stream->is_open) + { + scr_test_failed ("File read error: %s", "stream is not open"); + return 0; + } + else if (stream->is_writable) + { + scr_test_failed ("File read error: %s", "stream is not open for read"); + return 0; + } + + /* Read and remove the first block of data (or all if less than length). */ + bytes = (stream->length < length) ? stream->length : length; + memcpy (buffer, stream->data, bytes); + memmove (stream->data, stream->data + bytes, stream->length - bytes); + stream->length -= bytes; + return bytes; +} + +void +file_write_file_callback (void *opaque, const sc_byte *buffer, sc_int length) +{ + sx_scr_stream_t *const stream = (sx_scr_stream_t *)opaque; + assert (opaque && buffer && length > 0); + + /* Detect any problems with the callback parameters. */ + if (stream != &scr_serialization_stream) + { + scr_test_failed ("File write error: %s", "stream is invalid"); + return; + } + else if (!stream->is_open) + { + scr_test_failed ("File write error: %s", "stream is not open"); + return; + } + else if (!stream->is_writable) + { + scr_test_failed ("File write error: %s", "stream is not open for write"); + return; + } + + /* Reallocate, then add this block of data to the buffer. */ + stream->data = (sc_byte *)sx_realloc(stream->data, stream->length + length); + memcpy (stream->data + stream->length, buffer, length); + stream->length += length; +} + +void +file_close_file_callback (void *opaque) +{ + sx_scr_stream_t *const stream = (sx_scr_stream_t *)opaque; + assert (opaque); + + /* Detect any problems with the callback parameters. */ + if (stream != &scr_serialization_stream) + { + scr_test_failed ("File close error: %s", "stream is invalid"); + return; + } + else if (!stream->is_open) + { + scr_test_failed ("File close error: %s", "stream is not open"); + return; + } + + /* + * If closing after a read, free allocations, and return the stream to + * its empty state; if after write, leave the data for the later read. + */ + if (!stream->is_writable) + { + sx_free (stream->data); + stream->data = NULL; + stream->length = 0; + } + stream->is_writable = FALSE; + stream->is_open = FALSE; +} + + +/* + * file_cleanup() + * + * Free any pending allocations and clean up on completion of a script. + */ +void +file_cleanup (void) +{ + sx_scr_stream_t *const stream = &scr_serialization_stream; + + sx_free (stream->data); + stream->data = NULL; + stream->length = 0; + stream->is_writable = FALSE; + stream->is_open = FALSE; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sxglob.cpp b/engines/glk/adrift/sxglob.cpp new file mode 100644 index 0000000000..3a4a0c6cc4 --- /dev/null +++ b/engines/glk/adrift/sxglob.cpp @@ -0,0 +1,320 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/sxprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * The glob matching functions in this module are derived from an original + * (and somewhat hairy) glob.c posted by Arjan Kenter from the University + * of Twente, NL, in an assortment of minor variations between 1993 and 1997. + * The major modifications are: + * + * o Added checks to ensure that invalid range patterns such as "[a-" or + * "[-" don't cause the loops to walk off the end of the pattern string + * and (usually) result in SIGSEGV. + * o Moved from plain char to unsigned char to avoid signedness problems + * with range comparisons. + * o Skipped the leading '[' in the range checker; the original was treating + * it as a possible first value of 'r'. + * o Moved the range checker while() from the bottom of the loop to the top, + * to avoid problems with invalid ranges. + * o Gave 'l' in the range checker an initial value that ensures that it + * can never match until it's been re-assigned to 'r'. + * o Used a return value rather than multiple returns in the matcher, for + * better debugability. + * o Applied some const-correctness, and replaced some pointers by indexing. + * o Added scanf-like special cases, making ']' a valid part of a range if + * first, and '-' if last. + * + * This glob accepts * and ? wild cards, and [] ranges. It does not check + * whether the range string is valid (for example, terminates with ']'), but + * simply returns the best it can under those circumstances. + * + * Example call: + * glob_match ("a*b?c[A-Za-z_0-9]d*", some_string) + */ + +/* + * glob_inrange_unsigned() + * glob_match_unsigned() + * + * Match a "[...]" character range, and match general glob wildcards. See + * above for notes on where these functions came from originally. + */ +static int +glob_inrange_unsigned (const unsigned char **const pattern, + unsigned char ch) +{ + const unsigned char *const pattern_ = *pattern; + int in_range = FALSE; + unsigned int l = 256, r = 0, index_; + + /* Skip the leading '[' on entry to a range check. */ + index_ = 1; + + /* Special-case a range that has ']' as its first character. */ + if (pattern_[index_] == ']') + { + r = pattern_[index_++]; + if (ch == r) + in_range = TRUE; + } + + /* + * Check at the loop top, rather than the bottom, to avoid problems with + * invalid or uncompleted ranges. + */ + while (pattern_[index_] && pattern_[index_] != ']') + { + r = pattern_[index_++]; + if (r == '-') + { + /* Special-case a range that has '-' as its last character. */ + if (pattern_[index_] == ']' || !pattern_[index_]) + { + if (ch == r) + in_range = TRUE; + break; + } + + /* Break the loop on unterminated range ending with '-'. */ + if (!pattern_[index_]) + break; + + r = pattern_[index_++]; + if (l <= ch && ch <= r) + in_range = TRUE; + } + else + { + l = r; + if (ch == r) + in_range = TRUE; + } + } + + /* Update pattern with characters consumed, return result. */ + *pattern += index_; + return in_range; +} + +static int +glob_match_unsigned (const unsigned char *pattern, + const unsigned char *string) +{ + int is_match = FALSE; + + if (!*string) + { + if (*pattern == '*') + is_match = glob_match_unsigned (pattern + 1, string); + else + is_match = !*pattern; + } + else + { + switch (*pattern) + { + case '\0': + is_match = !*string; + break; + case '*': + if (glob_match_unsigned (pattern + 1, string)) + is_match = TRUE; + else + is_match = glob_match_unsigned (pattern, string + 1); + break; + case '?': + is_match = glob_match_unsigned (pattern + 1, string + 1); + break; + case '[': + /* + * After a range check, we need to see if we hit the end of the + * pattern before recursively matching pattern + 1. + */ + is_match = glob_inrange_unsigned (&pattern, *string) + && (!*pattern + || glob_match_unsigned (pattern + 1, string + 1)); + break; + default: + is_match = *pattern == *string + && glob_match_unsigned (pattern + 1, string + 1); + break; + } + } + + return is_match; +} + + +/* Structures and data for the self test function. */ +typedef struct +{ + const sc_char *const pattern; + const sc_char *const string; +} sx_test_data_t; + +static const sx_test_data_t SHOULD_MATCH[] = { + {"a", "a"}, {"abc", "abc"}, {"", ""}, + {"*", ""}, {"*", "abc"}, {"*", "cba"}, + {"*c", "c"}, {"*c", "abc"}, {"*c", "cbac"}, + {"a*", "a"}, {"a*", "abc"}, {"a*", "abca"}, + {"a*c", "ac"}, {"a*c", "abc"}, {"a*c", "abcbcbc"}, + {"a**c", "ac"}, {"a**c", "abc"}, {"a**c", "abcbcbc"}, + {"*b*", "b"}, {"*b*", "abc"}, {"*b*", "ab"}, {"*b*", "bc"}, + {"?", "a"}, {"?", "z"}, {"?", "?"}, {"[?]", "?"}, + {"a?", "aa"}, {"a?", "az"}, {"a?", "a?"}, + {"?c", "ac"}, {"?c", "zc"}, {"?c", "?c"}, + {"[abz]", "a"}, {"[abz]", "b"}, {"[abz]", "z"}, + {"[a-c]", "a"}, {"[a-c]", "b"}, {"[a-c]", "c"}, + {"[ac]b[ac]", "abc"}, {"[ac]b[ac]", "cba"}, + + {"[]]", "]"}, {"[]a-c]", "a"}, {"[]a-c]", "b"}, {"[]a-c]", "c"}, + {"[?]", "?" }, {"[-]", "-"}, {"[z-]", "z"}, {"[z-]", "-"}, + {"[][-]", "]"}, {"[][-]", "["}, {"[][-]", "-"}, + {"[a-c-]", "a"}, {"[a-c-]", "b"}, {"[a-c-]", "c"}, {"[a-c-]", "-"}, + + {"*[a-z]*abc?xyz", "a<star>abcQxyz"}, {"*[a-z]*abc?xyz", "<star>aabcQxyz"}, + {"*[a-z]*abc?xyz", "aabcQxyz"}, {"*[a-z]*abc?xyz", "<star>a<star>abcQxyz"}, + + {"???]", "abc]"}, {"[z-a]", "z"}, + {"[a-z", "a"}, {"[a-", "a"}, {"[a", "a"}, {"[[", "["}, + {NULL, NULL} +}; + +static const sx_test_data_t SHOULD_NOT_MATCH[] = { + {"a", "b"}, {"abc", "abd"}, {"a", ""}, {"", "a"}, + {"*c", "a"}, {"*c", "ab"}, {"*c", "abca"}, + {"a*", "c"}, {"a*", "cba"}, {"a*", "cbac"}, + {"a*c", "ca"}, {"a*c", "cba"}, {"a*c", "cbababa"}, + {"a**c", "ca"}, {"a**c", "cba"}, {"a**c", "cbababa"}, + {"*b*", ""}, {"*b*", "z"}, {"*b*", "ac"}, {"*b*", "azc"}, + {"?", ""}, {"?", "ab"}, {"?", "abc"}, {"[?]", "a"}, + {"a?", "ca"}, {"a?", "cz"}, {"a?", "??"}, + {"?c", "ab"}, {"?c", "zb"}, {"?c", "??"}, + {"[bcy]", "a"}, {"[bcy]", "d"}, {"[bcy]", "z"}, + {"[b-d]", "a"}, {"[b-d]", "e"}, {"[b-d]", ""}, {"[b-d]", "bc"}, + {"[ac]b[ac]", "aaa"}, {"[ac]b[ac]", "bbb"}, {"[ac]b[ac]", "ccc"}, + + {"[]]", "["}, {"[]]", "a"}, {"[]a-c]", "z"}, + {"[?]", "a" }, {"[-]", "a"}, {"[z-]", "a"}, + {"[][-]", "a"}, {"[][-]", "z"}, + {"[a-c-]", "z"}, + + {"*[a-z]*abc?xyz", "A<STAR>abcQxyz"}, {"*[a-z]*abc?xyz", "<STAR>AabcQxyz"}, + {"*[a-z]*abc?xyz", "AabcQxyz"}, {"*[a-z]*abc?xyz", "aabcxyz"}, + + {"[z-a]", "a"}, {"[z-a]", "b"}, {"[", "a"}, {"[[", "a"}, + {NULL, NULL} +}; + + +/* + * glob_self_test() + * + * Sed quis custodiet ipsos custodes? + */ +static void +glob_self_test (void) +{ + const sx_test_data_t *test; + sc_int errors; + + /* + * Run each test case and compare against expected result. To avoid a lot + * of ugly casting, we use the main public glob_match() function. + */ + errors = 0; + for (test = SHOULD_MATCH; test->pattern; test++) + { + if (!glob_match (test->pattern, test->string)) + { + sx_error ("glob_self_test: \"%s\", \"%s\"" + " did not match, and should have matched\n", + test->pattern, test->string); + errors++; + } + } + + for (test = SHOULD_NOT_MATCH; test->pattern; test++) + { + if (glob_match (test->pattern, test->string)) + { + sx_error ("glob_self_test: \"%s\", \"%s\"" + " matched, and should not have matched\n", + test->pattern, test->string); + errors++; + } + } + + /* + * Abort if any error. As befits our distrustful nature, we won't even + * trust that sx_fatal() calls abort() (though it should). + */ + if (errors > 0) + { + sx_fatal("glob_self_test: %ld self-test error%s found, aborting\n", + errors, (errors == 1) ? "" : "s"); + } +} + + +/* + * glob_match() + * + * Adapter for the above globbing functions, presenting a more standard char- + * based interface. Here is where all the evil casting lives. + */ +sc_bool +glob_match (const sc_char *pattern, const sc_char *string) +{ + static sc_bool initialized = FALSE; + + const unsigned char *pattern_ = (const unsigned char *) pattern; + const unsigned char *string_ = (const unsigned char *) string; + sc_bool retval; + assert (pattern && string); + + /* On the first call, run a self-test to verify basic glob matching. */ + if (!initialized) + { + /* + * To avoid lots of icky casting, the self-test uses the core public + * glob_match() that we're in right here to run its tests. So set + * initialized _before_ the test, to avoid infinite recursion. + */ + initialized = TRUE; + glob_self_test (); + } + + retval = glob_match_unsigned (pattern_, string_) != 0; + return retval; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sxmain.cpp b/engines/glk/adrift/sxmain.cpp new file mode 100644 index 0000000000..2644a5d53e --- /dev/null +++ b/engines/glk/adrift/sxmain.cpp @@ -0,0 +1,175 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/sxprotos.h" +#include "common/textconsole.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o In order to validate all its arguments, this module creates and retains + * a copy of every valid game encountered, only freeing them all at the end. + * This could make it very memory-hungry when running a large number of + * games and scripts. The alternative is to only create games as needed, + * and free them once used, but it's nice to fully validate all command line + * arguments first, and work afterwards, so for now that's what's done. + * + * o ...Alternatively, we could validate by creating the game, destroy it, + * and then re-parse it later when running the test script. Unfortunately, + * parsing an Adrift game can be lengthy (~seconds), so paying this price + * twice isn't too attractive either. + * + * o For now, then, if running lots of test, run in batches of ten or so. + */ + +/* + * main() + * + * Validate the command line, and each argument as a game to be run. + * Execute scripts for each, and return with an error code if any test fails. + */ +int glk_main (int argc, const char *argv[]) +{ + const sc_char *const program = argv[0]; + sc_bool is_verbose = FALSE, is_tracing = FALSE; + const sc_char *trace_flags; + sx_test_descriptor_t *tests; + sc_int count, index_, errors; + assert (argc > 0 && argv); + + /* Get options and validate the command line. */ + if (argc > 1 + && (strcmp (argv[1], "-v") == 0 || strcmp (argv[1], "-vv") == 0)) + { + is_verbose = TRUE; + is_tracing = (strcmp (argv[1], "-vv") == 0); + argc--; + argv++; + } + + if (is_verbose) + { + sx_trace ("--- %s Test Suite [Adrift %ld compatible]\n", + sc_scare_version (), sc_scare_emulation ()); + if (argc < 2) + return EXIT_SUCCESS; + } + else if (argc < 2) + { + error("Usage: %s [-v | -vv] test [test...]\n", program); + return EXIT_FAILURE; + } + + /* Ensure that the interpreter is in the Latin1 locale, and stays there. */ + if (!sc_set_locale ("Latin1")) + { + error("%s: failed to set locale\n", program); + return EXIT_FAILURE; + } + + /* + * Force test reproducibility. Because game construction may use random + * numbers, we also need to remember to reseed this before constructing + * each game, and then again before running each. + */ + sc_set_portable_random (TRUE); + + /* Set verbosity and tracing for other modules. */ + scr_set_verbose (is_verbose); + stub_debug_trace (is_tracing); + trace_flags = 0; // getenv("SC_TRACE_FLAGS"); + if (trace_flags) + sc_set_trace_flags (strtoul (trace_flags, NULL, 0)); + + /* Create an array of test descriptors large enough for all tests. */ + tests = (sx_test_descriptor_t *)sx_malloc ((argc - 1) * sizeof (*tests)); + + /* Validate each test argument by opening a game and script for it. */ + count = 0; + for (index_ = 1; index_ < argc; index_++) + { + const sc_char *name; + Common::SeekableReadStream *stream; + sx_script script; + sc_game game; + + name = argv[index_]; + + script = sx_fopen(name, "scr", "r"); + if (!script) + { + error("%s: %s.scr: %s\n", program, name, strerror (errno)); + continue; + } + + stream = sx_fopen (name, "taf", "rb"); + if (!stream) + { + error("%s: %s.taf: %s\n", program, name, strerror (errno)); + delete script; + continue; + } + + sc_reseed_random_sequence (1); + game = sc_game_from_stream(stream); + delete stream; + if (!game) + { + error("%s: %s.taf: Unable to decode Adrift game\n", program, name); + delete script; + continue; + } + + tests[count].name = name; + tests[count].script = script; + tests[count].game = game; + count++; + } + + /* Run the available tests and report results. */ + if (count > 0) + errors = test_run_game_tests (tests, count, is_verbose); + else + errors = 1; + + /* Clean up allocations and opened files. */ + for (index_ = 0; index_ < count; index_++) + { + delete tests[index_].script; + sc_free_game (tests[index_].game); + } + sx_free (tests); + + /* Report results overall. */ + warning("%s [%ld test%s, %ld error%s]\n", + errors > 0 ? "FAIL" : "PASS", + count, count == 1 ? "" : "s", errors, errors == 1 ? "" : "s"); + + return errors > 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sxprotos.h b/engines/glk/adrift/sxprotos.h new file mode 100644 index 0000000000..6591b5d820 --- /dev/null +++ b/engines/glk/adrift/sxprotos.h @@ -0,0 +1,113 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SCAREEXT_PROTOTYPES_H +#define SCAREEXT_PROTOTYPES_H + +#include "glk/adrift/scare.h" +#include "common/stream.h" + +namespace Glk { +namespace Adrift { + +/* True and false, unless already defined. */ +#ifndef FALSE +# define FALSE 0 +#endif +#ifndef TRUE +# define TRUE (!FALSE) +#endif + +/* Alias typedef for a test script. */ +typedef Common::SeekableReadStream *sx_script; + +/* Typedef representing a test descriptor. */ +typedef struct sx_test_descriptor_s +{ + const sc_char *name; + sc_game game; + sx_script script; +} sx_test_descriptor_t; + +/* + * Small utility and wrapper functions. For printf wrappers, try to apply + * gcc printf argument checking; this code is cautious about applying the + * checks. + */ +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95) +extern void sx_trace (const sc_char *format, ...) + __attribute__ ((__format__ (__printf__, 1, 2))); +extern void sx_error (const sc_char *format, ...) + __attribute__ ((__format__ (__printf__, 1, 2))); +extern void sx_fatal (const sc_char *format, ...) + __attribute__ ((__format__ (__printf__, 1, 2))); +#else +extern void sx_trace (const sc_char *format, ...); +extern void sx_error (const sc_char *format, ...); +extern void sx_fatal (const sc_char *format, ...); +#endif +extern void *sx_malloc (size_t size); +extern void *sx_realloc (void *pointer, size_t size); +extern void sx_free (void *pointer); +extern Common::SeekableReadStream *sx_fopen(const sc_char *name, + const sc_char *extension, const sc_char *mode); +extern sc_char *sx_trim_string (sc_char *string); +extern sc_char *sx_normalize_string (sc_char *string); + +/* OS stub hooks controller functions. */ +extern void stub_attach_handlers (sc_bool (*read_line) (sc_char *, sc_int), + void (*print_string) (const sc_char *), + void *(*open_file) (sc_bool), + sc_int (*read_file) + (void*, sc_byte*, sc_int), + void (*write_file) + (void*, const sc_byte*, sc_int), + void (*close_file) (void*)); +extern void stub_detach_handlers (void); +extern void stub_debug_trace (sc_bool flag); + +/* Test controller function. */ +extern sc_int test_run_game_tests (const sx_test_descriptor_t tests[], + sc_int count, sc_bool is_verbose); + +/* Globbing function. */ +extern sc_bool glob_match (const sc_char *pattern, const sc_char *string); + +/* Script running and checking functions. */ +extern void scr_test_failed (const sc_char *format, const sc_char *string); +extern void scr_set_verbose (sc_bool flag); +extern void scr_start_script (sc_game game, Common::SeekableReadStream *script); +extern sc_int scr_finalize_script (void); + +/* Serialization helper for script running and checking. */ +extern void *file_open_file_callback (sc_bool is_save); +extern sc_int file_read_file_callback (void *opaque, + sc_byte *buffer, sc_int length); +extern void file_write_file_callback (void *opaque, + const sc_byte *buffer, sc_int length); +extern void file_close_file_callback (void *opaque); +extern void file_cleanup (void); + +} // End of namespace Adrift +} // End of namespace Glk + +#endif diff --git a/engines/glk/adrift/sxscript.cpp b/engines/glk/adrift/sxscript.cpp new file mode 100644 index 0000000000..41be4c2b77 --- /dev/null +++ b/engines/glk/adrift/sxscript.cpp @@ -0,0 +1,642 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/sxprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o The script file format is as follows. Lines beginning '#' are comments + * and empty lines are ignored, otherwise the file is composed of sections. + * The first section line is one that starts with either '>' or '~'. This + * is the next command. The following lines, up to the next '>' or '~' + * section start, are concatenated into the expectation for the command. + * Expectations are glob patterns. Commands starting with '>' are sent to + * the game; those starting with '~' are sent to the SCARE debugger. Before + * the game is running, debugger commands are valid. The first non-debugger + * command starts the game running. An empty debugger command ('~') that + * follows any introductory debugger commands both starts the game and sets + * an expectation for the game's introductory text. After the game has + * completed (or quit), only debugger commands are valid; others are ignored. + * + * o The script file structure is intentionally simple, but might be too + * simple for some purposes. + */ + +/* Assorted definitions and constants. */ +static const sc_int LINE_BUFFER_SIZE = 256; +static const sc_char NUL = '\0'; +static const sc_char SCRIPT_COMMENT = '#'; +static const sc_char GAME_COMMAND = '>'; +static const sc_char DEBUG_COMMAND = '~'; + +/* Verbosity, and references to the game and script being processed. */ +static sc_bool scr_is_verbose = FALSE; +static sc_game scr_game = NULL; +static sx_script scr_script = NULL; + +/* Script line number, and count of errors registered for the script. */ +static sc_int scr_line_number = 0; +static sc_int scr_errors = 0; + +/* + * Current expected output, and game accumulated output, used by the + * expectation checking function. + */ +static sc_char *scr_expectation = NULL; +static sc_char *scr_game_output = NULL; + + +/* + * scr_set_verbose() + * + * Set error reporting for expectation errors detected in the script. + */ +void +scr_set_verbose (sc_bool flag) +{ + scr_is_verbose = flag; +} + + +/* + * scr_test_message() + * scr_test_failed() + * + * Simple common message and test case failure handling functions. The second + * is used by the serialization helper, so is not static. + */ +static void +scr_test_message (const sc_char *format, const sc_char *string) +{ + if (scr_is_verbose) + { + sx_trace ("--- "); + sx_trace (format, string); + sx_trace ("\n"); + } +} + +void +scr_test_failed (const sc_char *format, const sc_char *string) +{ + assert (format && string); + + if (scr_is_verbose) + { + if (scr_line_number > 0) + sx_trace ("--- Near line %ld: ", scr_line_number); + else + sx_trace ("--- "); + sx_trace (format, string); + sx_trace ("\n"); + } + scr_errors++; +} + + +/* + * scr_is_line_type() + * scr_is_line_comment_or_empty() + * scr_is_line_game_command() + * scr_is_line_debug_command() + * scr_is_line_command() + * scr_is_line_empty_debug_command() + * + * Line classifiers, return TRUE if line has the given type. + */ +static sc_bool +scr_is_line_type (const sc_char *line, sc_char type) +{ + return line[0] == type; +} + +static sc_bool +scr_is_line_comment_or_empty (const sc_char *line) +{ + return scr_is_line_type (line, SCRIPT_COMMENT) + || strspn (line, "\t\n\v\f\r ") == strlen (line); +} + +static sc_bool +scr_is_line_game_command (const sc_char *line) +{ + return scr_is_line_type (line, GAME_COMMAND); +} + +static sc_bool +scr_is_line_debug_command (const sc_char *line) +{ + return scr_is_line_type (line, DEBUG_COMMAND); +} + +static sc_bool +scr_is_line_command (const sc_char *line) +{ + return scr_is_line_game_command (line) || scr_is_line_debug_command (line); +} + +static sc_bool +scr_is_line_empty_debug_command (const sc_char *line) +{ + return scr_is_line_type (line, DEBUG_COMMAND) && line[1] == NUL; +} + + +/* Script location, a pair holding the file location and the line number. */ +struct sx_scr_location_t { + size_t position; + sc_int line_number; +}; +typedef sx_scr_location_t *sx_scr_locationref_t; + +/* + * scr_save_location() + * scr_restore_location() + * + * Save and restore the script location in the given structure. + */ +static void +scr_save_location (sx_script script, sx_scr_locationref_t location) { + location->position = script->pos(); + location->line_number = scr_line_number; +} + +static void scr_restore_location (sx_script script, sx_scr_locationref_t location) { + script->seek(location->position); + scr_line_number = location->line_number; +} + + +/* + * scr_get_next_line() + * + * Helper for scr_get_next_section(). Returns the next non-comment, non-empty + * line from the script. Returns NULL if no more lines, or on file error. The + * return string is allocated, and it's the caller's responsibility to free it. + */ +static sc_char *scr_get_next_line (sx_script script) { + sc_char *buffer, *line = NULL; + + /* Allocate a buffer for line reads. */ + buffer = (sc_char *)sx_malloc(LINE_BUFFER_SIZE); + + /* Read until a significant line is found, or end of file or error. */ + while (adrift_fgets(buffer, LINE_BUFFER_SIZE, script)) + { + scr_line_number++; + if (!scr_is_line_comment_or_empty (buffer)) + { + line = buffer; + break; + } + } + + /* If no significant line read, free the read buffer. */ + if (!line) + sx_free (buffer); + + return line; +} + + +/* + * scr_concatenate() + * + * Helper for scr_get_next_section(). Builds a string formed by concatenating + * the second argument to the first. If the first is NULL, acts as strdup() + * instead. + */ +static sc_char * +scr_concatenate (sc_char *string, const sc_char *buffer) +{ + /* If string is not null, concatenate buffer, otherwise duplicate. */ + if (string) + { + string = (sc_char *)sx_realloc(string, + strlen (string) + 1 + strlen (buffer) + 1); + strcat (string, " "); + strcat (string, buffer); + } + else + { + string = (sc_char *)sx_malloc(strlen (buffer) + 1); + strcpy (string, buffer); + } + + return string; +} + + +/* + * scr_get_next_section() + * + * Retrieve the next command and any expectation from the script file. + * Returns TRUE if a line is returned, FALSE at end-of-file. Expectation may + * be NULL if this paragraph doesn't have one; command may not be (if TRUE is + * returned). Command and expectation are allocated, and the caller needs to + * free them. + */ +static sc_bool +scr_get_next_section (sx_script script, + sc_char **command, sc_char **expectation) +{ + sc_char *line, *first_line, *other_lines; + sx_scr_location_t location; + + /* Clear initial line accumulation. */ + first_line = other_lines = NULL; + + /* Read the next significant line from the script. */ + scr_save_location (script, &location); + line = scr_get_next_line (script); + while (line) + { + /* If already a first line, this is other lines or section end. */ + if (first_line) + { + /* + * If we found the start of the next section, reset the script + * location that saved on the line read, and we're done. + */ + if (scr_is_line_command (line)) + { + scr_restore_location (script, &location); + sx_free (line); + break; + } + else + other_lines = scr_concatenate (other_lines, line); + } + else + first_line = scr_concatenate (first_line, line); + + sx_free (line); + + /* Read the next significant line from the script. */ + scr_save_location (script, &location); + line = scr_get_next_line (script); + } + + /* Clean up and return nothing on file error. */ + if (script->err()) + { + scr_test_failed ("Script error: Failed reading script input file", ""); + sx_free (first_line); + sx_free (other_lines); + return FALSE; + } + + /* Return the command and the matching expectation string, if any. */ + if (first_line) + { + *command = sx_normalize_string (first_line); + *expectation = other_lines ? sx_normalize_string (other_lines) : NULL; + return TRUE; + } + + /* End of file, no command section read. */ + return FALSE; +} + + +/* + * scr_expect() + * scr_verify_expectation() + * + * Set an expectation, and compare the expectation, if any, with the + * accumulated game output, using glob matching. scr_verify_expectation() + * increments the error count if the expectation isn't met, and reports the + * error if required. It then frees both the expectation and accumulated + * input. + */ +static void +scr_expect (sc_char *expectation) +{ + /* + * Save the expectation, and set up collection of game output if needed. + * And if not needed, ensure expectation and game output are cleared. + */ + if (expectation) + { + scr_expectation = (sc_char *)sx_malloc(strlen (expectation) + 1); + strcpy (scr_expectation, expectation); + scr_game_output = (sc_char *)sx_malloc (1); + strcpy (scr_game_output, ""); + } + else + { + sx_free(scr_expectation); + scr_expectation = NULL; + sx_free(scr_game_output); + scr_game_output = NULL; + } +} + +static void +scr_verify_expectation (void) +{ + /* Compare expected with actual, and handle any error detected. */ + if (scr_expectation && scr_game_output) + { + scr_game_output = sx_normalize_string (scr_game_output); + if (!glob_match (scr_expectation, scr_game_output)) + { + scr_test_failed ("Expectation error:", ""); + scr_test_message (" Expected: \"%s\"", scr_expectation); + scr_test_message (" Received: \"%s\"", scr_game_output); + } + } + + /* Dispose of the expectation and accumulated game output. */ + sx_free (scr_expectation); + scr_expectation = NULL; + sx_free (scr_game_output); + scr_game_output = NULL; +} + + +/* + * scr_execute_debugger_command() + * + * Convenience interface for immediate execution of debugger commands. This + * function directly calls the debugger interface, and because it's immediate, + * can also verify the expectation before returning to the caller. + * + * Also, it turns on the game debugger, and it's the caller's responsibility + * to turn it off when it's no longer needed. + */ +static void +scr_execute_debugger_command (const sc_char *command, sc_char *expectation) +{ + sc_bool status; + + /* Set up the expectation. */ + scr_expect (expectation); + + /* + * Execute the command via the debugger interface. The "+1" on command + * skips the leading '~' read in from the game script. + */ + sc_set_game_debugger_enabled (scr_game, TRUE); + status = sc_run_game_debugger_command (scr_game, command + 1); + + if (!status) + { + scr_test_failed ("Script error:" + " Debug command \"%s\" is not valid", command); + } + + /* Check expectations immediately. */ + scr_verify_expectation (); +} + + +/* + * scr_read_line_callback() + * + * Check any expectations set for the last line. Consult the script for the + * next line to feed to the game, and any expectation for the game output + * for that line. If there is an expectation, save it and set scr_game_output + * to "" so that accumulation begins. Then pass the next line of data back + * to the game. + */ +static sc_bool +scr_read_line_callback (sc_char *buffer, sc_int length) +{ + sc_char *command, *expectation; + assert (buffer && length > 0); + + /* Check pending expectation, and clear settings for the next line. */ + scr_verify_expectation (); + + /* Get the next line-expectation pair from the script stream. */ + if (scr_get_next_section (scr_script, &command, &expectation)) + { + if (scr_is_line_debug_command (command)) + { + /* The debugger persists where debug commands are adjacent. */ + scr_execute_debugger_command (command, expectation); + sx_free (command); + sx_free (expectation); + + /* + * Returning FALSE here causes the game to re-prompt. We could + * loop (or tail recurse) ourselves, but returning is simpler. + */ + return FALSE; + } + else + sc_set_game_debugger_enabled (scr_game, FALSE); + + if (scr_is_line_game_command (command)) + { + /* Set up the expectation. */ + scr_expect (expectation); + + /* Copy out the line to the return buffer, and free the line. */ + strncpy (buffer, command + 1, length); + buffer[length - 1] = NUL; + sx_free (command); + sx_free (expectation); + return TRUE; + } + + /* Neither a '~' nor a '>' command. */ + scr_test_failed ("Script error:" + " Command \"%s\" is not valid, ignored", command); + sx_free (command); + sx_free (expectation); + return FALSE; + } + + /* Ensure the game debugger is off after this section. */ + sc_set_game_debugger_enabled (scr_game, FALSE); + + /* + * We reached the end of the script without finding a "quit" command. + * Supply one here, then. In the unlikely even that this does not quit + * the game, we'll iterate on this. + */ + assert (length > 4); + strcpy (buffer, "quit"); + return TRUE; +} + + +/* + * scr_print_string_callback() + * + * Handler function for game output. Accumulates strings received from the + * game into scr_game_output, unless no expectation is set, in which case + * the current game output will be NULL, and we can simply save the effort. + */ +static void +scr_print_string_callback (const sc_char *string) +{ + assert (string); + + if (scr_game_output) + { + scr_game_output = (sc_char *)sx_realloc (scr_game_output, + strlen (scr_game_output) + + strlen (string) + 1); + strcat (scr_game_output, string); + } +} + + +/* + * scr_start_script() + * + * Set up game monitoring so that each request for a line from the game + * enters this module. For each request, we grab the next "send" and + * "expect" pair from the script, satisfy the request with the send data, + * and match against the expectations on next request or on finalization. + */ +void +scr_start_script (sc_game game, sx_script script) +{ + sc_char *command, *expectation; + sx_scr_location_t location; + assert (game && script); + + /* Save the game and stream, and clear the line number and errors count. */ + assert (!scr_game && !scr_script); + scr_game = game; + scr_script = script; + scr_line_number = 0; + scr_errors = 0; + + /* Set up our callback functions to catch game i/o. */ + stub_attach_handlers (scr_read_line_callback, scr_print_string_callback, + file_open_file_callback, file_read_file_callback, + file_write_file_callback, file_close_file_callback); + + /* + * Handle any initial debugging commands, terminating on either a non- + * debugging one or an expectation for the game intro. + */ + scr_script->seek(0); + scr_save_location (scr_script, &location); + while (scr_get_next_section (scr_script, &command, &expectation)) + { + if (scr_is_line_debug_command (command)) + { + if (scr_is_line_empty_debug_command (command)) + { + /* It's an intro expectation - set and break loop. */ + scr_expect (expectation); + sx_free (command); + sx_free (expectation); + break; + } + else + { + /* It's a full debug command - execute it as one. */ + scr_execute_debugger_command (command, expectation); + sx_free (command); + sx_free (expectation); + } + } + else + { + /* + * It's an ordinary section - rewind so that it's the first one + * handled in the callback, and break loop. + */ + scr_restore_location (scr_script, &location); + sx_free (command); + sx_free (expectation); + break; + } + + /* Note script position before reading the next section. */ + scr_save_location (scr_script, &location); + } + + /* Ensure the game debugger is off after this section. */ + sc_set_game_debugger_enabled (scr_game, FALSE); +} + + +/* + * scr_finalize_script() + * + * Match any final received string against a possible expectation, and then + * clear local records of the game, stream, and error count. Returns the + * count of errors detected during the script. + */ +sc_int +scr_finalize_script (void) +{ + sc_char *command, *expectation; + sc_int errors; + + /* Check pending expectation, and clear settings. */ + scr_verify_expectation (); + + /* Drain the remainder of the script, ignoring non-debugging commands. */ + while (scr_get_next_section (scr_script, &command, &expectation)) + { + if (scr_is_line_debug_command (command)) + { + scr_execute_debugger_command (command, expectation); + sx_free (command); + sx_free (expectation); + } + else + { + /* Complain about script entries ignored because the game ended. */ + scr_test_failed ("Script error:" + " Game completed, command \"%s\" ignored", command); + sx_free (command); + sx_free (expectation); + } + } + + /* Ensure the game debugger is off after this section. */ + sc_set_game_debugger_enabled (scr_game, FALSE); + + /* + * Remove our callback functions from the stubs, and "close" any retained + * stream data from game save/load tests. + */ + stub_detach_handlers (); + file_cleanup (); + + /* Clear local records of game stream, line number, and errors count. */ + errors = scr_errors; + scr_game = NULL; + scr_script = NULL; + scr_line_number = 0; + scr_errors = 0; + + return errors; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sxstubs.cpp b/engines/glk/adrift/sxstubs.cpp new file mode 100644 index 0000000000..d55b4037a9 --- /dev/null +++ b/engines/glk/adrift/sxstubs.cpp @@ -0,0 +1,410 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/sxprotos.h" +#include "common/str.h" + +namespace Glk { +namespace Adrift { + +/* + * Module notes: + * + * o Better, or perhaps less strace-like, tracing might be more beneficial + * for game script debugging. + */ + +/* Stubs trace flag. */ +static sc_bool stub_trace = FALSE; + +/* + * Input/output handler functions. If assigned, calls to os_* functions are + * routed here to allow the script runner to catch interpeter i/o. + */ +static sc_bool (*stub_read_line) (sc_char*, sc_int) = NULL; +static void (*stub_print_string) (const sc_char*) = NULL; +static void *(*stub_open_file) (sc_bool) = NULL; +static sc_int (*stub_read_file) (void*, sc_byte*, sc_int) = NULL; +static void (*stub_write_file) (void*, const sc_byte*, sc_int) = NULL; +static void (*stub_close_file) (void*) = NULL; + +/* Flags for whether to report tags and resources via stub_print_string(). */ +static sc_int stub_show_resources = 0; +static sc_int stub_show_tags = 0; + + +/* + * stub_attach_handlers() + * stub_detach_handlers() + * + * Attach input/output handler functions, and reset to NULLs. + */ +void +stub_attach_handlers (sc_bool (*read_line) (sc_char*, sc_int), + void (*print_string) (const sc_char*), + void *(*open_file) (sc_bool), + sc_int (*read_file) (void*, sc_byte*, sc_int), + void (*write_file) (void*, const sc_byte*, sc_int), + void (*close_file) (void*)) +{ + stub_read_line = read_line; + stub_print_string = print_string; + stub_open_file = open_file; + stub_read_file = read_file; + stub_write_file = write_file; + stub_close_file = close_file; + + stub_show_resources = 0; + stub_show_tags = 0; +} + +void +stub_detach_handlers (void) +{ + stub_read_line = NULL; + stub_print_string = NULL; + stub_open_file = NULL; + stub_read_file = NULL; + stub_write_file = NULL; + stub_close_file = NULL; + + stub_show_resources = 0; + stub_show_tags = 0; +} + + +/* + * stub_adjust_test_control() + * stub_catch_test_control() + * + * Trap testing control tags from the game, incrementing or decrementing + * stub_show_resources or stub_show_tags if a testing control tag arrives. + * Returns TRUE if the tag trapped was one of our testing ones. + */ +static void +stub_adjust_test_control (sc_int *control, sc_bool is_begin) +{ + *control += is_begin ? 1 : (*control > 0 ? -1 : 0); +} + +static sc_bool +stub_catch_test_control (sc_int tag, const sc_char *argument) +{ + if (tag == SC_TAG_UNKNOWN && argument) + { + sc_bool is_begin; + const sc_char *name; + + is_begin = !(argument[0] == '/'); + name = is_begin ? argument : argument + 1; + + if (sc_strcasecmp (name, "sxshowresources") == 0) + { + stub_adjust_test_control (&stub_show_resources, is_begin); + return TRUE; + } + else if (sc_strcasecmp (name, "sxshowtags") == 0) + { + stub_adjust_test_control (&stub_show_tags, is_begin); + return TRUE; + } + } + + return FALSE; +} + + +/* + * stub_notnull() + * + * Returns the string address passed in, or "(nil)" if NULL, for printf + * safety. Most libc's handle this themselves, but it's not defined by ANSI. + */ +static const sc_char * +stub_notnull (const sc_char *string) +{ + return string ? string : "(nil)"; +} + + +/* + * os_*() + * + * Stub functions called by the interpreter core. + */ +void +os_print_tag (sc_int tag, const sc_char *argument) +{ + if (stub_trace) + sx_trace ("os_print_tag (%ld, \"%s\")\n", tag, stub_notnull (argument)); + + if (!stub_catch_test_control (tag, argument)) + { + if (stub_print_string) + { + if (stub_show_tags > 0) { + stub_print_string ("<<Tag: id="); + Common::String buffer = Common::String::format("%ld", tag); + stub_print_string (buffer.c_str()); + stub_print_string (", argument=\""); + stub_print_string (stub_notnull (argument)); + stub_print_string ("\">>"); + } + else if (tag == SC_TAG_WAITKEY || tag == SC_TAG_CLS) + stub_print_string (" "); + } + } +} + +void +os_print_string (const sc_char *string) +{ + if (stub_trace) + sx_trace ("os_print_string (\"%s\")\n", stub_notnull (string)); + + if (stub_print_string) + stub_print_string (string); +} + +void +os_print_string_debug (const sc_char *string) +{ + if (stub_trace) + sx_trace ("os_print_string_debug (\"%s\")\n", stub_notnull (string)); + + if (stub_print_string) + stub_print_string (string); +} + +void +os_play_sound (const sc_char *filepath, + sc_int offset, sc_int length, sc_bool is_looping) +{ + if (stub_trace) + sx_trace ("os_play_sound (\"%s\", %ld, %ld, %s)\n", + stub_notnull (filepath), offset, length, + is_looping ? "true" : "false"); + + if (stub_print_string && stub_show_resources > 0) + { + sc_char buffer[32]; + + stub_print_string ("<<Sound: id=\""); + stub_print_string (stub_notnull (filepath)); + stub_print_string ("\", offset="); + sprintf (buffer, "%ld", offset); + stub_print_string (buffer); + stub_print_string (", length="); + sprintf (buffer, "%ld", length); + stub_print_string (buffer); + stub_print_string (", looping="); + stub_print_string (is_looping ? "true" : "false"); + stub_print_string (">>"); + } +} + +void +os_stop_sound (void) +{ + if (stub_trace) + sx_trace ("os_stop_sound ()\n"); + + if (stub_print_string && stub_show_resources > 0) + stub_print_string ("<<Sound: stop>>"); +} + +void +os_show_graphic (const sc_char *filepath, sc_int offset, sc_int length) +{ + if (stub_trace) + sx_trace ("os_show_graphic (\"%s\", %ld, %ld)\n", + stub_notnull (filepath), offset, length); + + if (stub_print_string && stub_show_resources > 0) + { + sc_char buffer[32]; + + stub_print_string ("<<Graphic: id=\""); + stub_print_string (stub_notnull (filepath)); + stub_print_string ("\", offset="); + sprintf (buffer, "%ld", offset); + stub_print_string (buffer); + stub_print_string (", length="); + sprintf (buffer, "%ld", length); + stub_print_string (buffer); + stub_print_string (">>"); + } +} + +sc_bool +os_read_line (sc_char *buffer, sc_int length) +{ + sc_bool status; + + if (stub_read_line) + status = stub_read_line (buffer, length); + else + { + assert (buffer && length > 4); + sprintf (buffer, "%s", "quit"); + status = TRUE; + } + + if (stub_trace) + { + if (status) + sx_trace ("os_read_line (\"%s\", %ld) -> true\n", + stub_notnull (buffer), length); + else + sx_trace ("os_read_line (\"...\", %ld) -> false\n", length); + } + return status; +} + +sc_bool +os_read_line_debug (sc_char *buffer, sc_int length) +{ + assert (buffer && length > 8); + sprintf (buffer, "%s", "continue"); + + if (stub_trace) + sx_trace ("os_read_line_debug (\"%s\", %ld) -> true\n", buffer, length); + return TRUE; +} + +sc_bool +os_confirm (sc_int type) +{ + if (stub_trace) + sx_trace ("os_confirm (%ld) -> true\n", type); + return TRUE; +} + +void * +os_open_file (sc_bool is_save) +{ + void *opaque; + + if (stub_open_file) + opaque = stub_open_file (is_save); + else + opaque = NULL; + + if (stub_trace) + { + if (opaque) + sx_trace ("os_open_file (%s) -> %p\n", + is_save ? "true" : "false", opaque); + else + sx_trace ("os_open_file (%s) -> null\n", is_save ? "true" : "false"); + } + return opaque; +} + +sc_int +os_read_file (void *opaque, sc_byte *buffer, sc_int length) +{ + sc_int bytes; + + if (stub_read_file) + bytes = stub_read_file (opaque, buffer, length); + else + bytes = 0; + + if (stub_trace) + sx_trace ("os_read_file (%p, %p, %ld) -> %ld\n", + opaque, buffer, length, bytes); + return bytes; +} + +void +os_write_file (void *opaque, const sc_byte *buffer, sc_int length) +{ + if (stub_write_file) + stub_write_file (opaque, buffer, length); + + if (stub_trace) + sx_trace ("os_write_file (%p, %p, %ld)\n", opaque, buffer, length); +} + +void +os_close_file (void *opaque) +{ + if (stub_close_file) + stub_close_file (opaque); + + if (stub_trace) + sx_trace ("os_close_file (%p)\n", opaque); +} + +void +os_display_hints (sc_game game) +{ + if (stub_trace) + sx_trace ("os_display_hints (%p)\n", game); + + if (stub_print_string) + { + sc_game_hint hint; + + for (hint = sc_get_first_game_hint (game); + hint; hint = sc_get_next_game_hint (game, hint)) + { + const sc_char *hint_text; + + stub_print_string (sc_get_game_hint_question (game, hint)); + stub_print_string ("\n"); + + hint_text = sc_get_game_subtle_hint (game, hint); + if (hint_text) + { + stub_print_string ("- "); + stub_print_string (hint_text); + stub_print_string ("\n"); + } + + hint_text = sc_get_game_unsubtle_hint (game, hint); + if (hint_text) + { + stub_print_string ("- "); + stub_print_string (hint_text); + stub_print_string ("\n"); + } + } + } +} + + +/* + * stub_debug_trace() + * + * Set stubs tracing on/off. + */ +void +stub_debug_trace (sc_bool flag) +{ + stub_trace = flag; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sxtester.cpp b/engines/glk/adrift/sxtester.cpp new file mode 100644 index 0000000000..483d8df202 --- /dev/null +++ b/engines/glk/adrift/sxtester.cpp @@ -0,0 +1,101 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/sxprotos.h" + +namespace Glk { +namespace Adrift { + +/* + * test_run_game_script() + * + * Run the game using the given script. Returns the count of errors from + * the script's monitoring. + */ +static sc_int test_run_game_script(sc_game game, sx_script script) { + sc_int errors; + + /* Ensure completely repeatable random number sequences. */ + sc_reseed_random_sequence (1); + + /* Start interpreting the script stream. */ + scr_start_script (game, script); + sc_interpret_game (game); + + /* Wrap up the script interpreter and capture error count. */ + errors = scr_finalize_script (); + return errors; +} + + +/* + * test_run_game_tests() + * + * Run each test in the given descriptor array, reporting the results and + * accumulating an error count overall. Return the total error count. + */ +sc_int test_run_game_tests (const sx_test_descriptor_t tests[], + sc_int count, sc_bool is_verbose) { + const sx_test_descriptor_t *test; + sc_int errors; + assert(tests); + + errors = 0; + + /* Execute each game in turn. */ + for (test = tests; test < tests + count; test++) + { + sc_int test_errors; + + if (is_verbose) + { + sx_trace ("--- Running Test \"%s\" [\"%s\", by %s]...\n", + test->name, + sc_get_game_name (test->game), + sc_get_game_author (test->game)); + } + + test_errors = test_run_game_script (test->game, test->script); + errors += test_errors; + + if (is_verbose) + { + sx_trace ("--- Test \"%s\": ", test->name); + if (test_errors > 0) + sx_trace ("%s [%ld error%s]\n", + "FAIL", test_errors, test_errors == 1 ? "" : "s"); + else + sx_trace ("%s\n", "PASS"); + } + else + sx_trace ("%s", test_errors > 0 ? "F" : "."); + //fflush (stdout); + //fflush (stderr); + } + sx_trace ("%s", is_verbose ? "" : "\n"); + + return errors; +} + +} // End of namespace Adrift +} // End of namespace Glk diff --git a/engines/glk/adrift/sxutils.cpp b/engines/glk/adrift/sxutils.cpp new file mode 100644 index 0000000000..d52670db31 --- /dev/null +++ b/engines/glk/adrift/sxutils.cpp @@ -0,0 +1,262 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "glk/adrift/scare.h" +#include "glk/adrift/sxprotos.h" +#include "common/debug.h" +#include "common/str.h" +#include "common/file.h" +#include "common/textconsole.h" + +namespace Glk { +namespace Adrift { + +/* + * sx_trace() + * + * Debugging trace function; printf wrapper that writes to stdout. Note that + * this differs from sc_trace(), which writes to stderr. We use stdout so + * that trace output is synchronized to test expectation failure messages. + */ +void sx_trace(const sc_char *format, ...) { + va_list ap; + assert (format); + + va_start(ap, format); + Common::String line = Common::String::vformat(format, ap); + va_end(ap); + + debug("%s", line.c_str()); +} + +/* + * sx_error() + * sx_fatal() + * + * Error reporting functions. sx_error() prints a message and continues. + * sx_fatal() prints a message, then calls abort(). + */ +void sx_error(const sc_char *format, ...) { + va_list ap; + assert(format); + + va_start(ap, format); + Common::String line = Common::String::vformat(format, ap); + va_end(ap); + + warning("%s", line.c_str()); +} + +void sx_fatal(const sc_char *format, ...) { + va_list ap; + assert(format); + + va_start(ap, format); + Common::String line = Common::String::vformat(format, ap); + va_end(ap); + + error("%s", line.c_str()); +} + +/* Unique non-heap address for zero size malloc() and realloc() requests. */ +static void *sx_zero_allocation = &sx_zero_allocation; + +/* + * sx_malloc() + * sx_realloc() + * sx_free() + * + * Non-failing wrappers around malloc functions. Newly allocated memory is + * cleared to zero. In ANSI/ISO C, zero byte allocations are implementation- + * defined, so we have to take special care to get predictable behavior. + */ +void * +sx_malloc (size_t size) +{ + void *allocated; + + if (size == 0) + return sx_zero_allocation; + + allocated = malloc (size); + if (!allocated) + sx_fatal ("sx_malloc: requested %lu bytes\n", (sc_uint) size); + else if (allocated == sx_zero_allocation) + sx_fatal ("sx_malloc: zero-byte allocation address returned\n"); + + memset (allocated, 0, size); + return allocated; +} + +void * +sx_realloc (void *pointer, size_t size) +{ + void *allocated; + + if (size == 0) + { + sx_free (pointer); + return sx_zero_allocation; + } + + if (pointer == sx_zero_allocation) + pointer = NULL; + + allocated = realloc (pointer, size); + if (!allocated) + sx_fatal ("sx_realloc: requested %lu bytes\n", (sc_uint) size); + else if (allocated == sx_zero_allocation) + sx_fatal ("sx_realloc: zero-byte allocation address returned\n"); + + if (!pointer) + memset (allocated, 0, size); + return allocated; +} + +void sx_free (void *pointer) { + if (sx_zero_allocation != &sx_zero_allocation) + sx_fatal ("sx_free: write to zero-byte allocation address detected\n"); + + if (pointer && pointer != sx_zero_allocation) + free (pointer); +} + + +/* + * sx_fopen() + * + * Open a file for a given test name with the extension and mode supplied. + * Returns NULL if unsuccessful. + */ +Common::SeekableReadStream *sx_fopen(const sc_char *name, const sc_char *extension, const sc_char *mode) { + assert (name && extension && mode); + + Common::String filename = Common::String::format("%s.%s", name, extension); + Common::File *f = new Common::File(); + + if (f->open(filename)) + return f; + + delete f; + return nullptr; +} + + +/* Miscellaneous general ascii constants. */ +static const sc_char NUL = '\0'; + +/* + * sx_isspace() + * sx_isprint() + * + * Built in replacements for locale-sensitive libc ctype.h functions. + */ +static sc_bool +sx_isspace (sc_char character) +{ + static const sc_char *const WHITESPACE = "\t\n\v\f\r "; + + return character != NUL && strchr (WHITESPACE, character) != NULL; +} + +static sc_bool +sx_isprint (sc_char character) +{ + static const sc_int MIN_PRINTABLE = ' ', MAX_PRINTABLE = '~'; + + return character >= MIN_PRINTABLE && character <= MAX_PRINTABLE; +} + + +/* + * sx_trim_string() + * + * Trim leading and trailing whitespace from a string. Modifies the string + * in place, and returns the string address for convenience. + */ +sc_char * +sx_trim_string (sc_char *string) +{ + sc_int index_; + assert (string); + + for (index_ = strlen (string) - 1; + index_ >= 0 && sx_isspace (string[index_]); index_--) + string[index_] = NUL; + + for (index_ = 0; sx_isspace (string[index_]);) + index_++; + memmove (string, string + index_, strlen (string) - index_ + 1); + + return string; +} + + +/* + * sx_normalize_string() + * + * Trim a string, set all runs of whitespace to a single space character, + * and convert all non-printing characters to '?'. Modifies the string in + * place, and returns the string address for convenience. + */ +sc_char * +sx_normalize_string (sc_char *string) +{ + sc_int index_; + assert (string); + + string = sx_trim_string (string); + + for (index_ = 0; string[index_] != NUL; index_++) + { + if (sx_isspace (string[index_])) + { + sc_int cursor; + + string[index_] = ' '; + for (cursor = index_ + 1; sx_isspace (string[cursor]);) + cursor++; + memmove (string + index_ + 1, + string + cursor, strlen (string + cursor) + 1); + } + else if (!sx_isprint (string[index_])) + string[index_] = '?'; + } + + return string; +} + +char *adrift_fgets(char *buf, int max, Common::SeekableReadStream *s) { + char *ptr = buf; + char c; + while (s->pos() < s->size() && --max > 0) { + c = s->readByte(); + if (c == '\n' || c == '\0') + break; + *ptr++ = c; + } + *ptr++ = '\0'; + return buf; +} + +} // End of namespace Adrift +} // End of namespace Glk |