X-Git-Url: https://git.stderr.nl/gitweb?a=blobdiff_plain;f=interpreters%2Fbocfel%2Fzterp.c;fp=interpreters%2Fbocfel%2Fzterp.c;h=49b84a0a8d4b428c4f0e3dec860d78897ec3a8af;hb=aa30979369091c96bca34499c28cb01bc16efb1d;hp=0000000000000000000000000000000000000000;hpb=61180dab8f5c29f5a29b83fcb7d62942f7a741d1;p=projects%2Fchimara%2Fchimara.git
diff --git a/interpreters/bocfel/zterp.c b/interpreters/bocfel/zterp.c
new file mode 100644
index 0000000..49b84a0
--- /dev/null
+++ b/interpreters/bocfel/zterp.c
@@ -0,0 +1,1053 @@
+/*-
+ * Copyright 2009-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Bocfel. If not, see .
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "zterp.h"
+#include "blorb.h"
+#include "branch.h"
+#include "io.h"
+#include "memory.h"
+#include "osdep.h"
+#include "process.h"
+#include "random.h"
+#include "screen.h"
+#include "stack.h"
+#include "unicode.h"
+#include "util.h"
+
+#ifdef ZTERP_GLK
+#include
+#include
+#ifdef GARGLK
+#include
+#include
+
+static schanid_t sound_channel = NULL;
+#endif
+#endif
+
+#define MAX_LINE 2048
+#define MAX_PATH 4096
+
+#define ZTERP_VERSION "0.6.1"
+
+const char *game_file;
+struct options options = {
+ .eval_stack_size = DEFAULT_STACK_SIZE,
+ .call_stack_size = DEFAULT_CALL_DEPTH,
+ .disable_color = 0,
+ .disable_config = 0,
+ .disable_sound = 0,
+ .disable_timed = 0,
+ .enable_escape = 0,
+ .escape_string = NULL,
+ .disable_fixed = 0,
+ .assume_fixed = 0,
+ .disable_graphics_font = 0,
+ .enable_alt_graphics = 0,
+ .show_id = 0,
+ .disable_term_keys = 0,
+ .disable_utf8 = 0,
+ .force_utf8 = 0,
+ .disable_meta_commands = 0,
+ .int_number = 1, /* DEC */
+ .int_version = 'C',
+ .replay_on = 0,
+ .replay_name = NULL,
+ .record_on = 0,
+ .record_name = NULL,
+ .transcript_on = 0,
+ .transcript_name = NULL,
+ .max_saves = 10,
+ .disable_undo_compression = 0,
+ .show_version = 0,
+ .disable_abbreviations = 0,
+ .enable_censorship = 0,
+ .overwrite_transcript = 0,
+ .random_seed = -1,
+ .random_device = NULL,
+};
+
+static char story_id[64];
+
+uint32_t pc;
+
+/* zversion stores the Z-machine version of the story: 1â6.
+ *
+ * Z-machine versions 7 and 8 are identical to version 5 but for a
+ * couple of tiny details. They are thus classified as version 5.
+ *
+ * zwhich stores the actual version (1â8) for the few rare times where
+ * this knowledge is necessary.
+ */
+int zversion;
+static int zwhich;
+
+struct header header;
+
+static struct
+{
+ zterp_io *io;
+ long offset;
+} story;
+
+/* The null character in the alphabet table does not actually signify a
+ * null character: character 6 from A2 is special in that it specifies
+ * that the next two characters form a 10-bit ZSCII character (§3.4).
+ * The code that uses the alphabet table will step around this character
+ * when necessary, so itâs safe to use a null character here to mean
+ * ânothingâ.
+ */
+uint8_t atable[26 * 3] =
+{
+ /* A0 */
+ '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',
+
+ /* A1 */
+ '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',
+
+ /* A2 */
+ 0x0, 0xd, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.',
+ ',', '!', '?', '_', '#', '\'','"', '/', '\\','-', ':', '(', ')',
+};
+
+void znop(void)
+{
+}
+
+void zquit(void)
+{
+ break_from(0);
+}
+
+void zverify(void)
+{
+ uint16_t checksum = 0;
+ uint32_t remaining = header.file_length - 0x40;
+
+ if(zterp_io_seek(story.io, story.offset + 0x40, SEEK_SET) == -1)
+ {
+ branch_if(0);
+ return;
+ }
+
+ while(remaining != 0)
+ {
+ uint8_t buf[8192];
+ uint32_t to_read = remaining < sizeof buf ? remaining : sizeof buf;
+
+ if(zterp_io_read(story.io, buf, to_read) != to_read)
+ {
+ branch_if(0);
+ return;
+ }
+
+ for(uint32_t i = 0; i < to_read; i++) checksum += buf[i];
+
+ remaining -= to_read;
+ }
+
+ branch_if(checksum == header.checksum);
+}
+
+uint32_t unpack(uint16_t addr, int string)
+{
+ switch(zwhich)
+ {
+ case 1: case 2: case 3:
+ return addr * 2UL;
+ case 4: case 5:
+ return addr * 4UL;
+ case 6: case 7:
+ return (addr * 4UL) + (string ? header.S_O : header.R_O);
+ case 8:
+ return addr * 8UL;
+ default:
+ die("unhandled z-machine version: %d", zwhich);
+ }
+}
+
+void store(uint16_t v)
+{
+ store_variable(BYTE(pc++), v);
+}
+
+void zsave5(void)
+{
+ zterp_io *savefile;
+ size_t n;
+
+ if(znargs == 0)
+ {
+ zsave();
+ return;
+ }
+
+ /* This should be able to suggest a filename, but Glk doesnât support that. */
+ savefile = zterp_io_open(NULL, ZTERP_IO_WRONLY | ZTERP_IO_SAVE);
+ if(savefile == NULL)
+ {
+ store(0);
+ return;
+ }
+
+ ZASSERT(zargs[0] + zargs[1] < memory_size, "attempt to save beyond the end of memory");
+ n = zterp_io_write(savefile, &memory[zargs[0]], zargs[1]);
+
+ zterp_io_close(savefile);
+
+ store(n == zargs[1]);
+}
+
+void zrestore5(void)
+{
+ zterp_io *savefile;
+ uint8_t *buf;
+ size_t n;
+
+ if(znargs == 0)
+ {
+ zrestore();
+ return;
+ }
+
+ savefile = zterp_io_open(NULL, ZTERP_IO_RDONLY | ZTERP_IO_SAVE);
+ if(savefile == NULL)
+ {
+ store(0);
+ return;
+ }
+
+ buf = malloc(zargs[1]);
+ if(buf == NULL)
+ {
+ store(0);
+ return;
+ }
+
+ n = zterp_io_read(savefile, buf, zargs[1]);
+ for(size_t i = 0; i < n; i++) user_store_byte(zargs[0] + i, buf[i]);
+
+ free(buf);
+
+ zterp_io_close(savefile);
+
+ store(n);
+}
+
+void zpiracy(void)
+{
+ branch_if(1);
+}
+
+void zsound_effect(void)
+{
+#ifdef GARGLK
+ uint8_t repeats, volume;
+ static uint32_t vols[8] = {
+ 0x02000, 0x04000, 0x06000, 0x08000,
+ 0x0a000, 0x0c000, 0x0e000, 0x10000
+ };
+
+ if(sound_channel == NULL || zargs[0] < 3) return;
+
+ repeats = zargs[2] >> 8;
+ volume = zargs[2] & 0xff;
+
+ if(volume == 0) volume = 1;
+ if(volume > 8) volume = 8;
+
+ glk_schannel_set_volume(sound_channel, vols[volume - 1]);
+
+ switch(zargs[1])
+ {
+ case 1: /* prepare */
+ glk_sound_load_hint(zargs[0], 1);
+ break;
+ case 2: /* start */
+ glk_schannel_play_ext(sound_channel, zargs[0], repeats == 255 ? -1 : repeats, 0);
+ break;
+ case 3: /* stop */
+ glk_schannel_stop(sound_channel);
+ break;
+ case 4: /* finish with */
+ glk_sound_load_hint(zargs[0], 0);
+ break;
+ }
+#endif
+}
+
+/* Find a story ID roughly in the form of an IFID according to §2.2.2.1
+ * of draft 7 of the Treaty of Babel.
+ *
+ * This does not add a ZCODE- prefix, and will not search for a manually
+ * created IFID.
+ */
+static void find_id(void)
+{
+ char serial[] = "------";
+
+ for(int i = 0; i < 6; i++)
+ {
+ /* isalnum() cannot be used because it is locale-aware, and this
+ * must only check for AâZ, aâz, and 0â9. Because ASCII (or a
+ * compatible charset) is required, testing against 'a', 'z', etc.
+ * is OK.
+ */
+#define ALNUM(c) ( ((c) >= 'a' && (c) <= 'z') || ((c) >= 'A' && (c) <= 'Z') || ((c) >= '0' && (c) <= '9') )
+ if(ALNUM(header.serial[i])) serial[i] = header.serial[i];
+#undef ALNUM
+ }
+
+ if(strchr("012345679", serial[0]) != NULL && strcmp(serial, "000000") != 0)
+ {
+ snprintf(story_id, sizeof story_id, "%d-%s-%04x", header.release, serial, (unsigned)header.checksum);
+ }
+ else
+ {
+ snprintf(story_id, sizeof story_id, "%d-%s", header.release, serial);
+ }
+}
+
+int is_story(const char *id)
+{
+ return strcmp(story_id, id) == 0;
+}
+
+#ifndef ZTERP_NO_CHEAT
+/* The index into these arrays is the address to freeze.
+ * The first array tracks whether the address is frozen, while the
+ * second holds the frozen value.
+ */
+static char freezew_cheat[UINT16_MAX + 1];
+static uint16_t freezew_val [UINT16_MAX + 1];
+
+static void cheat(char *how)
+{
+ char *p;
+
+ p = strtok(how, ":");
+ if(p == NULL) return;
+
+ if(strcmp(p, "freezew") == 0)
+ {
+ uint16_t addr;
+
+ p = strtok(NULL, ":");
+ if(p == NULL) return;
+
+ if(*p == 'G')
+ {
+ addr = strtoul(p + 1, NULL, 16);
+ if(addr > 239) return;
+
+ addr = header.globals + (addr * 2);
+ }
+ else
+ {
+ addr = strtoul(p, NULL, 16);
+ }
+
+ p = strtok(NULL, ":");
+ if(p == NULL) return;
+
+ freezew_cheat[addr] = 1;
+ freezew_val [addr] = strtoul(p, NULL, 0);
+ }
+}
+
+int cheat_find_freezew(uint32_t addr, uint16_t *val)
+{
+ if(addr > UINT16_MAX || !freezew_cheat[addr]) return 0;
+
+ *val = freezew_val[addr];
+
+ return 1;
+}
+#endif
+
+static void read_config(void)
+{
+ FILE *fp;
+ char file[MAX_PATH + 1];
+ char line[MAX_LINE];
+ char *key, *val, *p;
+ long n;
+ int story_matches = 1;
+
+ zterp_os_rcfile(file, sizeof file);
+
+ fp = fopen(file, "r");
+ if(fp == NULL) return;
+
+ while(fgets(line, sizeof line, fp) != NULL)
+ {
+ line[strcspn(line, "#\n")] = 0;
+ if(line[0] == 0) continue;
+
+ if(line[0] == '[')
+ {
+ p = strrchr(line, ']');
+ if(p != NULL && p[1] == 0)
+ {
+ *p = 0;
+
+ story_matches = 0;
+ for(p = strtok(line + 1, " ,"); p != NULL; p = strtok(NULL, " ,"))
+ {
+ if(is_story(p)) story_matches = 1;
+ }
+ }
+
+ continue;
+ }
+
+ if(!story_matches) continue;
+
+ key = strtok(line, " \t=");
+ if(key == NULL) continue;
+ val = strtok(NULL, "=");
+ if(val == NULL) continue;
+
+ /* Trim whitespace. */
+ while(isspace((unsigned char)*val)) val++;
+ if(*val == 0) continue;
+ p = val + strlen(val) - 1;
+ while(isspace((unsigned char)*p)) *p-- = 0;
+
+ n = strtol(val, NULL, 10);
+
+#define BOOL(name) else if(strcmp(key, #name) == 0) options.name = (n != 0)
+#define NUMBER(name) else if(strcmp(key, #name) == 0) options.name = n
+#define STRING(name) else if(strcmp(key, #name) == 0) do { free(options.name); options.name = xstrdup(val); } while(0)
+#define CHAR(name) else if(strcmp(key, #name) == 0) options.name = val[0]
+#ifdef GARGLK
+#define COLOR(name, num)else if(strcmp(key, "color_" #name) == 0) update_color(num, strtol(val, NULL, 16))
+#else
+#define COLOR(name, num)else if(0)
+#endif
+
+ if(0);
+
+ NUMBER(eval_stack_size);
+ NUMBER(call_stack_size);
+ BOOL (disable_color);
+ BOOL (disable_timed);
+ BOOL (disable_sound);
+ BOOL (enable_escape);
+ STRING(escape_string);
+ BOOL (disable_fixed);
+ BOOL (assume_fixed);
+ BOOL (disable_graphics_font);
+ BOOL (enable_alt_graphics);
+ BOOL (disable_term_keys);
+ BOOL (disable_utf8);
+ BOOL (force_utf8);
+ BOOL (disable_meta_commands);
+ NUMBER(max_saves);
+ BOOL (disable_undo_compression);
+ NUMBER(int_number);
+ CHAR (int_version);
+ BOOL (replay_on);
+ STRING(replay_name);
+ BOOL (record_on);
+ STRING(record_name);
+ BOOL (transcript_on);
+ STRING(transcript_name);
+ BOOL (disable_abbreviations);
+ BOOL (enable_censorship);
+ BOOL (overwrite_transcript);
+ NUMBER(random_seed);
+ STRING(random_device);
+
+ COLOR(black, 2);
+ COLOR(red, 3);
+ COLOR(green, 4);
+ COLOR(yellow, 5);
+ COLOR(blue, 6);
+ COLOR(magenta, 7);
+ COLOR(cyan, 8);
+ COLOR(white, 9);
+
+#ifndef ZTERP_NO_CHEAT
+ else if(strcmp(key, "cheat") == 0) cheat(val);
+#endif
+
+#undef BOOL
+#undef NUMBER
+#undef STRING
+#undef CHAR
+#undef COLOR
+ }
+
+ fclose(fp);
+}
+
+static int have_statuswin = 0;
+static int have_upperwin = 0;
+
+/* Various parts of the header (those marked âRstâ in §11) should be
+ * updated by the interpreter. This function does that. This is also
+ * used when restoring, because the save file might have come from an
+ * interpreter with vastly different settings.
+ */
+void write_header(void)
+{
+ uint8_t flags1;
+
+ flags1 = BYTE(0x01);
+
+ if(zversion == 3)
+ {
+ flags1 |= FLAGS1_NOSTATUS;
+ flags1 &= ~(FLAGS1_SCREENSPLIT | FLAGS1_VARIABLE);
+
+#ifdef GARGLK
+ /* Assume that if Gargoyle is being used, the default font is not fixed. */
+ flags1 |= FLAGS1_VARIABLE;
+#endif
+
+ if(have_statuswin) flags1 &= ~FLAGS1_NOSTATUS;
+ if(have_upperwin) flags1 |= FLAGS1_SCREENSPLIT;
+ if(options.enable_censorship) flags1 |= FLAGS1_CENSOR;
+ }
+ else if(zversion >= 4)
+ {
+ flags1 |= (FLAGS1_BOLD | FLAGS1_ITALIC | FLAGS1_FIXED);
+ flags1 &= ~FLAGS1_TIMED;
+
+ if(zversion >= 5) flags1 &= ~FLAGS1_COLORS;
+
+ if(zversion == 6)
+ {
+ flags1 &= ~(FLAGS1_PICTURES | FLAGS1_SOUND);
+#ifdef GARGLK
+ if(sound_channel != NULL) flags1 |= FLAGS1_SOUND;
+#endif
+ }
+
+#ifdef ZTERP_GLK
+ if(glk_gestalt(gestalt_Timer, 0)) flags1 |= FLAGS1_TIMED;
+#ifdef GARGLK
+ if(zversion >= 5) flags1 |= FLAGS1_COLORS;
+#endif
+#else
+ if(!zterp_os_have_style(STYLE_BOLD)) flags1 &= ~FLAGS1_BOLD;
+ if(!zterp_os_have_style(STYLE_ITALIC)) flags1 &= ~FLAGS1_ITALIC;
+ if(zversion >= 5 && zterp_os_have_colors()) flags1 |= FLAGS1_COLORS;
+#endif
+
+ if(zversion >= 5 && options.disable_color) flags1 &= ~FLAGS1_COLORS;
+ if(options.disable_timed) flags1 &= ~FLAGS1_TIMED;
+ if(options.disable_fixed) flags1 &= ~FLAGS1_FIXED;
+ }
+
+ STORE_BYTE(0x01, flags1);
+
+ if(zversion >= 5)
+ {
+ uint16_t flags2 = WORD(0x10);
+
+ flags2 &= ~FLAGS2_MOUSE;
+#ifdef GARGLK
+ if(sound_channel == NULL) flags2 &= ~FLAGS2_SOUND;
+#else
+ flags2 &= ~FLAGS2_SOUND;
+#endif
+ if(zversion >= 6) flags2 &= ~FLAGS2_MENUS;
+
+ if(options.disable_graphics_font) flags2 &= ~FLAGS2_PICTURES;
+
+ if(options.max_saves == 0) flags2 &= ~FLAGS2_UNDO;
+
+ STORE_WORD(0x10, flags2);
+ }
+
+ if(zversion >= 4)
+ {
+ unsigned int width, height;
+
+ /* Interpreter number & version. */
+ if(options.int_number < 1 || options.int_number > 11) options.int_number = 1; /* DEC */
+ STORE_BYTE(0x1e, options.int_number);
+ STORE_BYTE(0x1f, options.int_version);
+
+ get_screen_size(&width, &height);
+
+ /* Screen height and width.
+ * A height of 255 means infinite, so cap at 254.
+ */
+ STORE_BYTE(0x20, height > 254 ? 254 : height);
+ STORE_BYTE(0x21, width > 255 ? 255 : width);
+
+ if(zversion >= 5)
+ {
+ /* Screen width and height in units. */
+ STORE_WORD(0x22, width > UINT16_MAX ? UINT16_MAX : width);
+ STORE_WORD(0x24, height > UINT16_MAX ? UINT16_MAX : height);
+
+ /* Font width and height in units. */
+ STORE_BYTE(0x26, 1);
+ STORE_BYTE(0x27, 1);
+
+ /* Default background and foreground colors. */
+ STORE_BYTE(0x2c, 1);
+ STORE_BYTE(0x2d, 1);
+ }
+ }
+
+ /* Standard revision # */
+ STORE_BYTE(0x32, 1);
+ STORE_BYTE(0x33, 1);
+}
+
+void process_story(void)
+{
+ if(zterp_io_seek(story.io, story.offset, SEEK_SET) == -1) die("unable to rewind story");
+
+ if(zterp_io_read(story.io, memory, memory_size) != memory_size) die("unable to read from story file");
+
+ zversion = BYTE(0x00);
+ if(zversion < 1 || zversion > 8) die("only z-code versions 1-8 are supported");
+
+ zwhich = zversion;
+ if(zversion == 7 || zversion == 8) zversion = 5;
+
+ pc = WORD(0x06);
+ if(pc >= memory_size) die("corrupted story: initial pc out of range");
+
+ header.release = WORD(0x02);
+ header.dictionary = WORD(0x08);
+ header.objects = WORD(0x0a);
+ header.globals = WORD(0x0c);
+ header.static_start = WORD(0x0e);
+ header.abbr = WORD(0x18);
+
+ memcpy(header.serial, &memory[0x12], sizeof header.serial);
+
+ /* There is no explicit âend of staticâ tag; but it must end by 0xffff
+ * or the end of the story file, whichever is smaller.
+ */
+ header.static_end = memory_size < 0xffff ? memory_size : 0xffff;
+
+#define PROPSIZE (zversion <= 3 ? 62L : 126L)
+
+ /* There must be at least enough room in dynamic memory for the header
+ * (64 bytes), the global variables table (480 bytes), and the
+ * property defaults table (62 or 126 bytes).
+ */
+ if(header.static_start < 64 + 480 + PROPSIZE) die("corrupted story: dynamic memory too small (%d bytes)", (int)header.static_start);
+ if(header.static_start >= memory_size) die("corrupted story: static memory out of range");
+
+ if(header.dictionary != 0 &&
+ header.dictionary < header.static_start) die("corrupted story: dictionary is not in static memory");
+
+ if(header.objects < 64 ||
+ header.objects + PROPSIZE > header.static_start)
+ die("corrupted story: object table is not in dynamic memory");
+
+#undef PROPSIZE
+
+ if(header.globals < 64 ||
+ header.globals + 480L > header.static_start) die("corrupted story: global variables are not in dynamic memory");
+
+ if(header.abbr >= memory_size) die("corrupted story: abbreviation table out of range");
+
+ header.file_length = WORD(0x1a) * (zwhich <= 3 ? 2UL : zwhich <= 5 ? 4UL : 8UL);
+ if(header.file_length > memory_size) die("story's reported size (%lu) greater than file size (%lu)", (unsigned long)header.file_length, (unsigned long)memory_size);
+
+ header.checksum = WORD(0x1c);
+
+ if(zwhich == 6 || zwhich == 7)
+ {
+ header.R_O = WORD(0x28) * 8UL;
+ header.S_O = WORD(0x2a) * 8UL;
+ }
+
+ if(dynamic_memory == NULL)
+ {
+ dynamic_memory = malloc(header.static_start);
+ if(dynamic_memory == NULL) die("unable to allocate memory for dynamic memory");
+ memcpy(dynamic_memory, memory, header.static_start);
+ }
+
+#ifdef GLK_MODULE_LINE_TERMINATORS
+ if(!options.disable_term_keys)
+ {
+ if(zversion >= 5 && WORD(0x2e) != 0)
+ {
+ term_keys_reset();
+
+ for(uint32_t i = WORD(0x2e); i < memory_size && memory[i] != 0; i++)
+ {
+ term_keys_add(memory[i]);
+ }
+ }
+ }
+#endif
+
+ if(zversion == 1)
+ {
+ memcpy(&atable[26 * 2], " 0123456789.,!?_#'\"/\\<-:()", 26);
+ }
+ else if(zversion >= 5 && WORD(0x34) != 0)
+ {
+ if(WORD(0x34) + 26 * 3 >= memory_size) die("corrupted story: alphabet table out of range");
+
+ memcpy(atable, &memory[WORD(0x34)], 26 * 3);
+
+ /* Even with a new alphabet table, characters 6 and 7 from A2 must
+ * remain the same (§3.5.5.1).
+ */
+ atable[52] = 0x00;
+ atable[53] = 0x0d;
+ }
+
+ /* Check for a header extension table. */
+ if(zversion >= 5)
+ {
+ uint16_t etable = WORD(0x36);
+
+ if(etable != 0)
+ {
+ uint16_t nentries = user_word(etable);
+
+ if(etable + (2 * nentries) >= memory_size) die("corrupted story: header extension table out of range");
+
+ /* Unicode table. */
+ if(nentries >= 3 && WORD(etable + (2 * 3)) != 0)
+ {
+ uint16_t utable = WORD(etable + (2 * 3));
+
+ parse_unicode_table(utable);
+ }
+
+ /* Flags3. */
+ if(nentries >= 4) STORE_WORD(etable + (2 * 4), 0);
+ /* True default foreground color. */
+ if(nentries >= 5) STORE_WORD(etable + (2 * 5), 0x0000);
+ /* True default background color. */
+ if(nentries >= 6) STORE_WORD(etable + (2 * 6), 0x7fff);
+ }
+ }
+
+ /* The configuration file cannot be read until the ID of the current
+ * story is known, and the ID of the current story is not known until
+ * the file has been processed; so do both of those here.
+ */
+ find_id();
+ if(!options.disable_config) read_config();
+
+ /* Prevent the configuration file from unexpectedly being reread after
+ * @restart or @restore.
+ */
+ options.disable_config = 1;
+
+ /* Most options directly set their respective variables, but a few
+ * require intervention. Delay that intervention until here so that
+ * the configuration file is taken into account.
+ */
+ if(options.disable_utf8)
+ {
+#ifndef ZTERP_GLK
+ /* If Glk is not being used, the ZSCII to Unicode table needs to be
+ * aligned with the IO character set.
+ */
+ have_unicode = 0;
+#endif
+ use_utf8_io = 0;
+ }
+ if(options.force_utf8)
+ {
+#ifndef ZTERP_GLK
+ /* See above. */
+ have_unicode = 1;
+#endif
+ use_utf8_io = 1;
+ }
+ if(options.escape_string == NULL) options.escape_string = xstrdup("1m");
+
+#ifdef GARGLK
+ if(options.disable_sound && sound_channel != NULL)
+ {
+ glk_schannel_destroy(sound_channel);
+ sound_channel = NULL;
+ }
+#endif
+
+ /* Now that we have a Unicode table and the userâs Unicode
+ * preferences, build the ZSCII to Unicode and Unicode to ZSCII
+ * tables.
+ */
+ setup_tables();
+
+ if(zversion <= 3) have_statuswin = create_statuswin();
+ if(zversion >= 3) have_upperwin = create_upperwin();
+
+ write_header();
+ /* Put everything in a clean state. */
+ seed_random(0);
+ init_stack();
+ init_screen();
+
+ /* Unfortunately, Beyond Zork behaves badly when the interpreter
+ * number is set to DOS: it assumes that it can print out IBM PC
+ * character codes and get useful results (e.g. it writes out 0x18
+ * expecting an up arrow); however, if the pictures bit is set, it
+ * uses the character graphics font like a good citizen. Thus turn
+ * that bit on when Beyond Zork is being used and the interpreter is
+ * set to DOS. It might make sense to do this generally, not just for
+ * Beyond Zork; but this is such a minor corner of the Z-machine that
+ * it probably doesnât matter. For now, peg this to Beyond Zork.
+ */
+ if(options.int_number == 6 &&
+ (is_story("47-870915") || is_story("49-870917") ||
+ is_story("51-870923") || is_story("57-871221")))
+ {
+ STORE_WORD(0x10, WORD(0x10) | FLAGS2_PICTURES);
+ }
+
+ if(options.transcript_on)
+ {
+ STORE_WORD(0x10, WORD(0x10) | FLAGS2_TRANSCRIPT);
+ options.transcript_on = 0;
+ }
+
+ if(options.record_on)
+ {
+ output_stream(OSTREAM_RECORD, 0);
+ options.record_on = 0;
+ }
+
+ if(options.replay_on)
+ {
+ input_stream(ISTREAM_FILE);
+ options.replay_on = 0;
+ }
+
+ if(zversion == 6)
+ {
+ zargs[0] = pc;
+ call(0);
+ }
+}
+
+#ifdef ZTERP_GLK
+zexternally_visible
+void glk_main(void)
+#else
+int main(int argc, char **argv)
+#endif
+{
+ zterp_blorb *blorb;
+
+#ifdef ZTERP_GLK
+ if(!create_mainwin()) return;
+#ifdef GLK_MODULE_UNICODE
+ have_unicode = glk_gestalt(gestalt_Unicode, 0);
+#endif
+#else
+ have_unicode = zterp_os_have_unicode();
+#endif
+
+ use_utf8_io = zterp_os_have_unicode();
+
+#ifndef ZTERP_GLK
+ if(!process_arguments(argc, argv)) exit(EXIT_FAILURE);
+
+ zterp_os_init_term();
+#endif
+
+#ifdef ZTERP_GLK
+#define PRINT(s) do { glk_put_string(s); glk_put_char(UNICODE_LINEFEED); } while(0)
+#else
+#define PRINT(s) puts(s)
+#endif
+
+ if(options.show_version)
+ {
+ char config[MAX_PATH] = "Configuration file: ";
+
+ PRINT("Bocfel " ZTERP_VERSION);
+#ifdef ZTERP_NO_SAFETY_CHECKS
+ PRINT("Runtime assertions disabled");
+#else
+ PRINT("Runtime assertions enabled");
+#endif
+#ifdef ZTERP_NO_CHEAT
+ PRINT("Cheat support disabled");
+#else
+ PRINT("Cheat support enabled");
+#endif
+#ifdef ZTERP_TANDY
+ PRINT("The Tandy bit can be set");
+#else
+ PRINT("The Tandy bit cannot be set");
+#endif
+
+ zterp_os_rcfile(config + strlen(config), sizeof config - strlen(config));
+ PRINT(config);
+
+#ifdef ZTERP_GLK
+ glk_exit();
+#else
+ exit(0);
+#endif
+ }
+
+#undef PRINT
+
+#ifdef GARGLK
+ if(game_file == NULL)
+ {
+ frefid_t ref;
+
+ ref = glk_fileref_create_by_prompt(fileusage_Data | fileusage_BinaryMode, filemode_Read, 0);
+ if(ref != NULL)
+ {
+ game_file = xstrdup(garglk_fileref_get_name(ref));
+ glk_fileref_destroy(ref);
+ }
+ }
+#endif
+
+ if(game_file == NULL) die("no story provided");
+
+ story.io = zterp_io_open(game_file, ZTERP_IO_RDONLY);
+ if(story.io == NULL) die("cannot open file %s", game_file);
+
+ blorb = zterp_blorb_parse(story.io);
+ if(blorb != NULL)
+ {
+ const zterp_blorb_chunk *chunk;
+
+ chunk = zterp_blorb_find(blorb, BLORB_EXEC, 0);
+ if(chunk == NULL) die("no EXEC resource found");
+ if(strcmp(chunk->name, "ZCOD") != 0)
+ {
+ if(strcmp(chunk->name, "GLUL") == 0) die("Glulx stories are not supported (try git or glulxe)");
+
+ die("unknown story type: %s", chunk->name);
+ }
+
+ if(chunk->offset > LONG_MAX) die("zcode offset too large");
+
+ memory_size = chunk->size;
+ story.offset = chunk->offset;
+
+ zterp_blorb_free(blorb);
+ }
+ else
+ {
+ long size = zterp_io_filesize(story.io);
+
+ if(size == -1) die("unable to determine file size");
+ if(size > UINT32_MAX) die("file too large");
+
+ memory_size = size;
+ story.offset = 0;
+ }
+
+#ifdef GARGLK
+ if(glk_gestalt(gestalt_Sound, 0))
+ {
+ /* 5 for the worst case of needing to add .blb to the end plus the
+ * null character.
+ */
+ char *blorb_file = malloc(strlen(game_file) + 5);
+ if(blorb_file != NULL)
+ {
+ char *p;
+ strid_t file;
+
+ strcpy(blorb_file, game_file);
+ p = strrchr(blorb_file, '.');
+ if(p != NULL) *p = 0;
+ strcat(blorb_file, ".blb");
+
+ file = glkunix_stream_open_pathname(blorb_file, 0, 0);
+ if(file != NULL)
+ {
+ giblorb_set_resource_map(file);
+ sound_channel = glk_schannel_create(0);
+ }
+
+ free(blorb_file);
+ }
+ }
+#endif
+
+ if(memory_size < 64) die("story file too small");
+ if(memory_size > SIZE_MAX - 22) die("story file too large");
+
+ /* Itâs possible for a story to be cut short in the middle of an
+ * instruction. If so, the processing loop will run past the end of
+ * memory. Either pc needs to be checked each and every time it is
+ * incremented, or a small guard needs to be placed at the end of
+ * memory that will trigger an illegal instruction error. The latter
+ * is done by filling the end of memory with zeroes, which do not
+ * represent a valid instruction.
+ *
+ * There need to be at least 22 bytes for the worst case: 0xec
+ * (call_vs2) as the last byte in memory. The next two bytes, which
+ * will be zeroes, indicate that 8 large constants, or 16 bytes, will
+ * be next. This is a store instruction, so one more byte will be
+ * read to determine where to store. Another byte is read to
+ * determine the next opcode; this will be zero, which is nominally a
+ * 2OP, requiring two more bytes to be read. At this point the opcode
+ * will be looked up, resulting in an illegal instruction error.
+ */
+ memory = malloc(memory_size + 22);
+ if(memory == NULL) die("unable to allocate memory for story file");
+ memset(memory + memory_size, 0, 22);
+
+ process_story();
+
+ /* If header transcript/fixed bits have been set, either by the
+ * story or by the user, this will activate them.
+ */
+ user_store_word(0x10, WORD(0x10));
+
+ if(options.show_id)
+ {
+#ifdef ZTERP_GLK
+ glk_put_string(story_id);
+ glk_exit();
+#else
+ puts(story_id);
+ exit(0);
+#endif
+ }
+
+ setup_opcodes();
+
+ process_instructions();
+
+#ifndef ZTERP_GLK
+ return 0;
+#endif
+}