aboutsummaryrefslogtreecommitdiff
path: root/engines/glk/adrift
diff options
context:
space:
mode:
authorPaul Gilbert2019-09-02 20:57:19 -0700
committerPaul Gilbert2019-09-25 20:13:26 -0700
commit60c860c6a6c37b95ab8265d658cbcc144d51153b (patch)
treee21dac197fcd374bb8d348873c91230141e1fddf /engines/glk/adrift
parentc893c5a60b8298aa99ae1a47dea51c6f04c419bc (diff)
downloadscummvm-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')
-rw-r--r--engines/glk/adrift/adrift.cpp56
-rw-r--r--engines/glk/adrift/adrift.h84
-rw-r--r--engines/glk/adrift/os_glk.cpp3565
-rw-r--r--engines/glk/adrift/scare.h188
-rw-r--r--engines/glk/adrift/scdebug.cpp2717
-rw-r--r--engines/glk/adrift/scevents.cpp855
-rw-r--r--engines/glk/adrift/scexpr.cpp1677
-rw-r--r--engines/glk/adrift/scgamest.cpp1195
-rw-r--r--engines/glk/adrift/scgamest.h192
-rw-r--r--engines/glk/adrift/scinterf.cpp1192
-rw-r--r--engines/glk/adrift/sclibrar.cpp10983
-rw-r--r--engines/glk/adrift/sclocale.cpp573
-rw-r--r--engines/glk/adrift/scmemos.cpp622
-rw-r--r--engines/glk/adrift/scnpcs.cpp640
-rw-r--r--engines/glk/adrift/scobjcts.cpp1036
-rw-r--r--engines/glk/adrift/scparser.cpp2139
-rw-r--r--engines/glk/adrift/scprintf.cpp1588
-rw-r--r--engines/glk/adrift/scprops.cpp1059
-rw-r--r--engines/glk/adrift/scprotos.h799
-rw-r--r--engines/glk/adrift/scresour.cpp349
-rw-r--r--engines/glk/adrift/screstrs.cpp1163
-rw-r--r--engines/glk/adrift/scrunner.cpp2252
-rw-r--r--engines/glk/adrift/scserial.cpp826
-rw-r--r--engines/glk/adrift/sctaffil.cpp860
-rw-r--r--engines/glk/adrift/sctafpar.cpp3552
-rw-r--r--engines/glk/adrift/sctasks.cpp1414
-rw-r--r--engines/glk/adrift/scutils.cpp462
-rw-r--r--engines/glk/adrift/scvars.cpp1918
-rw-r--r--engines/glk/adrift/sxfile.cpp201
-rw-r--r--engines/glk/adrift/sxglob.cpp320
-rw-r--r--engines/glk/adrift/sxmain.cpp175
-rw-r--r--engines/glk/adrift/sxprotos.h113
-rw-r--r--engines/glk/adrift/sxscript.cpp642
-rw-r--r--engines/glk/adrift/sxstubs.cpp410
-rw-r--r--engines/glk/adrift/sxtester.cpp101
-rw-r--r--engines/glk/adrift/sxutils.cpp262
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, &notify);
+ 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, &notify);
+ 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, &notify);
+ 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, &timestamp, &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, &timestamp, &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 = "&lt;",
+ *const ENTITY_GREATERTHAN = "&gt;",
+ *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 &lt; &gt;
+ * +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