diff options
author | dreammaster | 2019-11-19 02:24:54 +0000 |
---|---|---|
committer | Paul Gilbert | 2019-11-22 18:49:07 -0800 |
commit | 1c6762104792fe84390c114b6eafe85dc7867d13 (patch) | |
tree | df902f872856d67d1c036d73cb6968725137fcf9 /engines | |
parent | aff46219ef5189da48bf13838b5d4b6885d21a03 (diff) | |
download | scummvm-rg350-1c6762104792fe84390c114b6eafe85dc7867d13.tar.gz scummvm-rg350-1c6762104792fe84390c114b6eafe85dc7867d13.tar.bz2 scummvm-rg350-1c6762104792fe84390c114b6eafe85dc7867d13.zip |
GLK: MAGNETIC: Added subengine files
Diffstat (limited to 'engines')
-rw-r--r-- | engines/glk/magnetic/defs.h | 471 | ||||
-rw-r--r-- | engines/glk/magnetic/emu.cpp | 3613 | ||||
-rw-r--r-- | engines/glk/magnetic/glk.cpp | 5393 | ||||
-rw-r--r-- | engines/glk/magnetic/graphics.cpp | 507 | ||||
-rw-r--r-- | engines/glk/magnetic/magnetic.cpp | 58 | ||||
-rw-r--r-- | engines/glk/magnetic/magnetic.h | 148 | ||||
-rw-r--r-- | engines/glk/magnetic/magnetic_types.h | 5 | ||||
-rw-r--r-- | engines/glk/magnetic/main.cpp | 176 | ||||
-rw-r--r-- | engines/glk/magnetic/myth.cpp | 133 | ||||
-rw-r--r-- | engines/glk/magnetic/sound.cpp | 92 | ||||
-rw-r--r-- | engines/glk/module.mk | 4 |
11 files changed, 9685 insertions, 915 deletions
diff --git a/engines/glk/magnetic/defs.h b/engines/glk/magnetic/defs.h new file mode 100644 index 0000000000..b448200b72 --- /dev/null +++ b/engines/glk/magnetic/defs.h @@ -0,0 +1,471 @@ +/* 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 MAGNETIC_DEFS_H +#define MAGNETIC_DEFS_H + +#include "common/scummsys.h" +#include "glk/magnetic/magnetic_types.h" + +namespace Glk { +namespace Magnetic { + +/*****************************************************************************\ +* Type definitions for Magnetic +* +* Note: When running into trouble please ensure that these types have the +* correct number of bits on your system !!! +\*****************************************************************************/ + +typedef byte type8; +typedef int8 type8s; +typedef uint16 type16; +typedef int16 type16s; +typedef uint32 type32; +typedef int32 type32s; + +/****************************************************************************\ +* Compile time switches +\****************************************************************************/ + +/* Switch: SAVEMEM + Purpose: Magnetic loads a complete graphics file into memory by default. + Setting this switch you tell Magnetic to load images on request + (saving memory, wasting load time) + +#define SAVEMEM +*/ + +/* Switch: NO_ANIMATION + Purpose: By default Magnetic plays animated graphics. + Setting this switch to ignore animations, Magnetic shows the + static parts of the images anyway! + +#define NO_ANIMATION +*/ + +/****************************************************************************\ +* Abstract functions +* +* Note: These functions MUST be implemented by each port of Magnetic! +\****************************************************************************/ + +/****************************************************************************\ +* Function: ms_load_file +* +* Purpose: Load a save game file and restore game +* +* Parameters: type8s* name zero terminated string of filename +* typed by player +* type8* ptr pointer to data to save +* type16 size number of bytes to save +* +* Result: 0 is successful +* +* Note: You probably want to put in a file requester! +\****************************************************************************/ + +extern type8 ms_load_file(const char *name, type8 *ptr, type16 size); + +/****************************************************************************\ +* Function: ms_save_file +* +* Purpose: Save the current game to file +* +* Parameters: type8s* name zero terminated string of filename +* typed by player +* type8* ptr pointer to data to save +* type16 size number of bytes to save +* +* Result: 0 is successful +* +* Note: You probably want to put in a file requester! +\****************************************************************************/ + +extern type8 ms_save_file(const char *name, type8 *ptr, type16 size); + +/****************************************************************************\ +* Function: ms_statuschar +* +* Purpose: Output a single character to the status bar +* +* Parameters: type8 c character to be printed +* +* Note: All characters are printed as provided except for: +* 0x0A resets the x position to zero +* 0x09 moves the cursor to the right half of the bar, ie 'width-11' +\****************************************************************************/ + +extern void ms_statuschar(type8 c); + +/****************************************************************************\ +* Function: ms_putchar +* +* Purpose: Output a single character to the game/text windows +* +* Parameters: type8 c character to be printed +* +* Note: It is highly recommended to buffer the output, see also ms_flush() +\****************************************************************************/ + +extern void ms_putchar(type8 c); + +/****************************************************************************\ +* Function: ms_flush +* +* Purpose: Flush the output buffer (if applicable) +* +* Note: see also ms_putchar +\****************************************************************************/ + +extern void ms_flush(void); + +/****************************************************************************\ +* Function: ms_getchar +* +* Purpose: Read user input, buffered +* +* Parameters: type8 trans if not 0, translate any #undo +* input to a return code of 0 +* +* Return: One character +* +* Note: The first time it is called a string should be read and then given +* back one byte at a time (ie. one for each call) until a '\n' is +* reached (which will be the last byte sent back before it all restarts) +* Returning a zero means 'undo' and the rest of the line must then be +* ignored. +* Returning 1 means that the opcode should return immediately. This is +* needed to prevent possible corruption of the game's memory in +* interpreters which allow a new game to be loaded without restarting. +\****************************************************************************/ + +extern type8 ms_getchar(type8 trans); + +/****************************************************************************\ +* Function: ms_showpic +* +* Purpose: Displays or hides a picture +* +* Parameter: type32 c number of image to be displayed +* type8 mode mode == 0 means gfx off, +* mode == 1 gfx on thumbnails, +* mode == 2 gfx on normal. +* +* Note: For retrieving the raw data of a picture call ms_extract (see below) +\****************************************************************************/ + +extern void ms_showpic(type32 c, type8 mode); + +/****************************************************************************\ +* Function: ms_fatal +* +* Purpose: Handle fatal interpreter error +* +* Parameter: type8s* txt message +\****************************************************************************/ + +extern void ms_fatal(const char *txt); + +/****************************************************************************\ +* Magnetic core functions +* +* Note: These functions SHOULD be used somewhere in your port! +\****************************************************************************/ + +/****************************************************************************\ +* Function: ms_extract +* +* Purpose: Extract a picture and return a pointer to a raw bitmap +* +* Parameters: type32 c number of the picture +* type16* w width of picture +* type16* h height pf picture +* type16* pal array for the palette (16 colours) +* type8* is_anim 1 if animated picture, otherwise 0 +* OR null (!) +* +* Return: Pointer to bitmap data if successful, otherwise null (also if gfx +* are disabled!) +* +* Note: All pictures have 16 colours and the palette entries use 3-bit RGB +* encoded as 00000RRR0GGG0BBB, that is, bits 0-2 give the blue +* component, bits 4-6 the green and bits 8-10 the red. The bitmap is +* one byte per pixel, with each byte giving the pixel's index into the +* palette. The data is given starting from the upper left corner. The +* image buffer is reused when the next picture is requested, so take +* care! More information on animated pictures are below! +\****************************************************************************/ + +extern type8 *ms_extract(type32 c, type16 *w, type16 *h, type16 *pal, type8 *is_anim); + +/****************************************************************************\ +* Magnetic animated pictures support +* +* Note: Some of the pictures for Wonderland and the Collection Volume 1 games +* are animations. To detect these, pass a pointer to a type8 as the is_anim +* argument to ms_extract(). +* +* There are two types of animated images, however almost all images are type1. +* A type1 image consists of four main elements: +* 1) A static picture which is loaded straight at the beginning +* 2) A set of frames with a mask. These frames are just "small pictures", which +* are coded like the normal static pictures. The image mask determines +* how the frame is removed after it has been displayed. A mask is exactly +* 1/8 the size of the image and holds 1 bit per pixel, saying "remove pixel" +* or leave pixel set when frame gets removed. It might be a good idea to check +* your system documentation for masking operations as your system might be +* able to use this mask data directly. +* 3) Positioning tables. These hold animation sequences consisting of commands +* like "Draw frame 12 at (123,456)" +* 4) A playback script, which determines how to use the positioning tables. +* These scripts are handled inside Magnetic, so no need to worry about. +* However, details can be found in the ms_animate() function. +* +* A type2 image is like a type1 image, but it does not have a static +* picture, nor does it have frame masking. It just consists of frames. +* +* How to support animations? +* After getting is_anim == 1 you should call ms_animate() immediately, and at +* regular intervals until ms_animate() returns 0. An appropriate interval +* between calls is about 100 milliseconds. +* Each call to ms_animate() will fill in the arguments with the address +* and size of an array of ms_position structures (see below), each of +* which holds an an animation frame number and x and y co-ordinates. To +* display the animation, decode all the animation frames (discussed below) +* from a single call to ms_animate() and display each one over the main picture. +* If your port does not support animations, define NO_ANIMATION. +\****************************************************************************/ + +/****************************************************************************\ +* Function: ms_animate +* +* Purpose: Generate the next frame of an animation +* +* Parameters: ms_position** positions array of ms_position structs +* type16* count size of array +* +* Return: 1 if animation continues, 0 if animation is finfished +* +* Note: The positions array holds size ms_positions structures. BEFORE calling +* ms_animate again, retrieve the frames for all the ms_positions +* structures with ms_get_anim_frame and display each one on the static +* main picture. +\****************************************************************************/ + +extern type8 ms_animate(struct ms_position **positions, type16 *count); + +/****************************************************************************\ +* Function: ms_get_anim_frame +* +* Purpose: Extracts the bitmap data of a single animation frame +* +* Parameters: type16s number number of frame (see ms_position struct) +* type16* width width of frame +* type16* height height of frame +* type8** mask pointer to masking data, might be NULL +* +* Return: 1 if animation continues, 0 if animation is finfished +* +* Note: The format of the frame is identical to the main pictures' returned by +* ms_extract. The mask has one-bit-per-pixel, determing how to handle the +* removal of the frame. +\****************************************************************************/ + +extern type8 *ms_get_anim_frame(type16s number, type16 *width, type16 *height, type8 **mask); + +/****************************************************************************\ +* Function: ms_anim_is_repeating +* +* Purpose: Detects whether an animation is repeating +* +* Return: True if repeating +\****************************************************************************/ + +extern type8 ms_anim_is_repeating(void); + +/****************************************************************************\ +* Magnetic Windows hint support +* +* The windowed Magnetic Scolls games included online hints. To add support +* for the hints to your magnetic port, you should implement the ms_showhints +* function. It retrieves a pointer to an array of ms_hint structs +* The root element is always hints[0]. The elcount determines the number +* of items in this topic. You probably want to display those in some kind +* of list interface. The content pointer points to the actual description of +* the items, separated by '\0' terminators. The nodetype is 1 if the items are +* "folders" and 2 if the items are hints. Hints should be displayed one after +* another. For "folder" items, the links array holds the index of the hint in +* the array which is to be displayed on selection. One hint block has exactly +* one type. The parent element determines the "back" target. +\****************************************************************************/ + +/****************************************************************************\ +* Function: ms_showhints +* Purpose: Show the player a hint +* +* Parameters: ms_hint* hints pointer to array of ms_hint structs +* +* Return: 0 on error, 1 on success +\****************************************************************************/ + +extern type8 ms_showhints(struct ms_hint *hints); + +/****************************************************************************\ +* Magnetic Windows sound support +* +* Wonderland contains music scores that are played when entering specific +* locations in the game. The music data are actually MIDI events and can be +* played through normal MIDI devices. The original game plays the MIDI score +* until the end, even if the location is changed while playing. The playback +* tempo is not included with the MIDI data. The ms_sndextract function +* returns a recommended tempo, however depending on the MIDI implementation +* and operating system, you might need to adapt it. +\****************************************************************************/ + +/****************************************************************************\ +* Function: ms_playmusic +* +* Purpose: Plays (or stops playing) a MIDI music score. +* +* Parameter: type8 * midi_data the MIDI data to play +* type32 length the length of the MIDI data +* type16 tempo the suggested tempo for playing +* +* Note: If midi_data is NULL, all that should happen is that any currently +* playing music is stopped. +* Note: The data returned contain a complete MIDI file header, so if pure +* memory processing is not applicable you can write the data to a +* temporary file and use external players or libraries. +\****************************************************************************/ + +extern void ms_playmusic(type8 *midi_data, type32 length, type16 tempo); + +/****************************************************************************\ +* Function: ms_init +* +* Purpose: Loads the interpreter with a game +* +* Parameters: type8s* name filename of story file +* type8s* gfxname filename of graphics file (optional) +* type8s* hntname filename of hints file (optional) +* type8s* sndname filename of music file (optional) +* +* Return: 0 = failure +* 1 = success (without graphics or graphics failed) +* 2 = success (with graphics) +* +* Note: You must call this function before starting the ms_rungame loop +\****************************************************************************/ + +extern type8 ms_init(const char *name, const char *gfxname, const char *hntname, const char *sndname); + +/****************************************************************************\ +* Function: ms_rungame +* +* Purpose: Executes an interpreter instruction +* +* Return: True if successful +* +* Note: You must call this function in a loop like this: +* while (running) {running=ms_rungame();} +\****************************************************************************/ + +extern type8 ms_rungame(void); + +/****************************************************************************\ +* Function: ms_freemen +* +* Purpose: Frees all allocated ressources +\****************************************************************************/ + +extern void ms_freemem(void); + +/****************************************************************************\ +* Function: ms_seed +* +* Purpose: Initializes the interpreter's random number generator with +* the given seed +* +* Parameter: type32 seed seed +\****************************************************************************/ + +extern void ms_seed(type32 seed); + +/****************************************************************************\ +* Function: ms_is_running +* +* Purpose: Detects if game is running +* +* Return: True, if game is currently running +\****************************************************************************/ + +extern type8 ms_is_running(void); + +/****************************************************************************\ +* Function: ms_is_magwin +* +* Purpose: Detects Magnetic Windows games (Wonderland, Collection) +* +* Return: True, if Magnetic Windows game +\****************************************************************************/ + +extern type8 ms_is_magwin(void); + +/****************************************************************************\ +* Function: ms_stop +* +* Purpose: Stops further processing of opcodes +\****************************************************************************/ + +extern void ms_stop(void); + +/****************************************************************************\ +* Function: ms_status +* +* Purpose: Dumps interperetr state to stderr, ie. registers +\****************************************************************************/ + +extern void ms_status(void); + +/****************************************************************************\ +* Function: ms_count +* +* Purpose: Returns the number of executed intructions +* +* Return: Instruction count +\****************************************************************************/ + +extern type32 ms_count(void); + +extern void write(const char *fmt, ...); + +extern void writeChar(char c); + +extern int gms_startup_code(int argc, char *argv[]); + +extern void gms_main(); + +} // End of namespace Magnetic +} // End of namespace Glk + +#endif diff --git a/engines/glk/magnetic/emu.cpp b/engines/glk/magnetic/emu.cpp index 1d63f5944a..fe6c66c53d 100644 --- a/engines/glk/magnetic/emu.cpp +++ b/engines/glk/magnetic/emu.cpp @@ -20,21 +20,336 @@ * */ -#include "glk/magnetic/magnetic.h" +#include "glk/magnetic/defs.h" +#include "common/file.h" namespace Glk { namespace Magnetic { -//static const char *no_hints = "[Hints are not available.]\n"; -//static const char *not_supported = "[This function is not supported.]\n"; +type32 dreg[8], areg[8], i_count, string_size, rseed = 0, pc, arg1i, mem_size; +type16 properties, fl_sub, fl_tab, fl_size, fp_tab, fp_size; +type8 zflag, nflag, cflag, vflag, byte1, byte2, regnr, admode, opsize; +type8 *arg1, *arg2, is_reversible, running = 0, tmparg[4] = {0, 0, 0, 0}; +type8 lastchar = 0, version = 0, sd = 0; +type8 *decode_table, *restart = 0, *code = 0, *string = 0, *string2 = 0; +type8 *string3 = 0, *dict = 0; +type8 quick_flag = 0, gfx_ver = 0, *gfx_buf = nullptr, *gfx_data = 0; +type8 *gfx2_hdr = 0, *gfx2_buf = nullptr; +const char *gfx2_name = nullptr; +type16 gfx2_hsize = 0; +Common::File *gfx_fp = nullptr; +type8 *snd_buf = nullptr, *snd_hdr = nullptr; +type16 snd_hsize = 0; +Common::File *snd_fp = nullptr; -int Magnetic::ms_init(bool restarting) { - byte header[42]; - uint32 i, j, dict_size, string2_size, code_size, dec; +const char *const undo_ok = "\n[Previous turn undone.]"; +const char *const undo_fail = "\n[You can't \"undo\" what hasn't been done!]"; +type32 undo_regs[2][18], undo_pc, undo_size; +type8 *undo[2] = {0, 0}, undo_stat[2] = {0, 0}; +type16 gfxtable = 0, table_dist = 0; +type16 v4_id = 0, next_table = 1; - ms_stop(); +#ifndef NO_ANIMATION + +struct picture anim_frame_table[MAX_ANIMS]; +type16 pos_table_size = 0; +type16 pos_table_count[MAX_POSITIONS]; +struct ms_position pos_table[MAX_POSITIONS][MAX_ANIMS]; +type8 *command_table = 0; +type16s command_index = -1; +struct lookup anim_table[MAX_POSITIONS]; +type16s pos_table_index = -1; +type16s pos_table_max = -1; +struct ms_position pos_array[MAX_FRAMES]; +type8 anim_repeat = 0; + +#endif + +/* Hint support */ +struct ms_hint *hints = 0; +type8 *hint_contents = 0; +const char *const no_hints = "[Hints are not available.]\n"; +const char *const not_supported = "[This function is not supported.]\n"; + +#if defined(LOGEMU) || defined(LOGGFX) || defined(LOGHNT) +FILE *dbg_log; +#ifndef LOG_FILE +#error LOG_FILE must be defined to be the name of the log file. +#endif +#endif + +/* prototypes */ +type32 read_reg(int, int); +void write_reg(int, int, type32); + +#define MAX_STRING_SIZE 0xFF00 +#define MAX_PICTURE_SIZE 0xC800 +#define MAX_MUSIC_SIZE 0x4E20 + +#ifdef LOGEMU +void out(char *format, ...) { + va_list a; + + va_start(a, format); + vfprintf(dbg_log, format, a); + va_end(a); +} +#endif +#if defined(LOGGFX) || defined(LOGHNT) +void out2(char *format, ...) { + va_list a; + + va_start(a, format); + vfprintf(dbg_log, format, a); + va_end(a); + fflush(dbg_log); +} +#endif + +/* Convert virtual pointer to effective pointer */ +type8 *effective(type32 ptr) { + if ((version < 4) && (mem_size == 0x10000)) + return &(code[ptr & 0xffff]); + if (ptr >= mem_size) { + ms_fatal("Outside memory experience"); + return code; + } + + return &(code[ptr]); +} + +type32 read_l(type8 *ptr) { + + return (type32)((type32) ptr[0] << 24 | (type32) ptr[1] << 16 | (type32) ptr[2] << 8 | (type32) ptr[3]); +} + +type16 read_w(type8 *ptr) { + return (type16)(ptr[0] << 8 | ptr[1]); +} + +void write_l(type8 *ptr, type32 val) { + ptr[3] = (type8) val; + val >>= 8; + ptr[2] = (type8) val; + val >>= 8; + ptr[1] = (type8) val; + val >>= 8; + ptr[0] = (type8) val; +} + +void write_w(type8 *ptr, type16 val) { + ptr[1] = (type8) val; + val >>= 8; + ptr[0] = (type8) val; +} + +type32 read_l2(type8 *ptr) { + return ((type32) ptr[1] << 24 | (type32) ptr[0] << 16 | (type32) ptr[3] << 8 | (type32) ptr[2]); +} + +type16 read_w2(type8 *ptr) { + return (type16)(ptr[1] << 8 | ptr[0]); +} + +/* Standard rand - for equal cross-platform behaviour */ + +void ms_seed(type32 seed) { + rseed = seed; +} + +type32 rand_emu(void) { + rseed = 1103515245L * rseed + 12345L; + return rseed & 0x7fffffffL; +} + +void ms_freemem(void) { + if (code) + free(code); + if (string) + free(string); + if (string2) + free(string2); + if (string3) + free(string3); + if (dict) + free(dict); + if (undo[0]) + free(undo[0]); + if (undo[1]) + free(undo[1]); + if (restart) + free(restart); + code = string = string2 = string3 = dict = undo[0] = undo[1] = restart = 0; + if (gfx_data) + free(gfx_data); + if (gfx_buf) + free(gfx_buf); + if (gfx2_hdr) + free(gfx2_hdr); + if (gfx2_buf) + free(gfx2_buf); + delete gfx_fp; + + gfx_data = gfx_buf = gfx2_hdr = gfx2_buf = nullptr; + gfx2_name = 0; + gfx_fp = nullptr; + gfx_ver = 0; + gfxtable = table_dist = 0; +#ifndef NO_ANIMATION + pos_table_size = 0; + command_index = 0; + anim_repeat = 0; + pos_table_index = -1; + pos_table_max = -1; +#endif + lastchar = 0; + if (hints) + free(hints); + if (hint_contents) + free(hint_contents); + hints = 0; + hint_contents = 0; + if (snd_hdr) + free(snd_hdr); + if (snd_buf) + free(snd_buf); + snd_hdr = nullptr; + snd_hsize = 0; + snd_buf = nullptr; +} + +type8 ms_is_running(void) { + return running; +} + +type8 ms_is_magwin(void) { + return (version == 4) ? 1 : 0; +} + +void ms_stop(void) { + running = 0; +} + +type8 init_gfx1(type8 *header) { +#ifdef SAVEMEM + type32 i; +#endif + + if (!(gfx_buf = (type8 *)malloc(MAX_PICTURE_SIZE))) { + delete gfx_fp; + gfx_fp = nullptr; + return 1; + } +#ifdef SAVEMEM + if (!(gfx_data = (type8 *)malloc(128))) { +#else + if (!(gfx_data = (type8 *)malloc(read_l(header + 4) - 8))) { +#endif + free(gfx_buf); + delete gfx_fp; + gfx_buf = nullptr; + gfx_fp = nullptr; + return 1; + } +#ifdef SAVEMEM + if (!fp.read(gfx_data, 128, 1, gfx_fp)) { +#else + if (gfx_fp->read(gfx_data, read_l(header + 4)) != read_l(header + 4)) { +#endif + free(gfx_data); + free(gfx_buf); + delete gfx_fp; + gfx_data = gfx_buf = nullptr; + gfx_fp = nullptr; + return 1; + } + +#ifdef SAVEMEM + for (i = 0; i < 128; i += 4) + if (!read_l(gfx_data + i)) + write_l(gfx_data + i, read_l(header + 4)); +#else + delete gfx_fp; + gfx_fp = nullptr; +#endif + + gfx_ver = 1; + return 2; +} + +type8 init_gfx2(type8 *header) { + if (!(gfx_buf = (type8 *)malloc(MAX_PICTURE_SIZE))) { + delete gfx_fp; + gfx_fp = nullptr; + return 1; + } + + gfx2_hsize = read_w(header + 4); + if (!(gfx2_hdr = (type8 *)malloc(gfx2_hsize))) { + free(gfx_buf); + delete gfx_fp; + gfx_buf = nullptr; + gfx_fp = nullptr; + return 1; + } + + gfx_fp->seek(6); + if (gfx_fp->read(gfx2_hdr, gfx2_hsize) != gfx2_hsize) { + free(gfx_buf); + free(gfx2_hdr); + delete gfx_fp; + gfx_buf = nullptr; + gfx2_hdr = 0; + gfx_fp = nullptr; + return 1; + } + + gfx_ver = 2; + return 2; +} + +type8 init_snd(type8 *header) { + if (!(snd_buf = (type8 *)malloc(MAX_MUSIC_SIZE))) { + delete snd_fp; + snd_fp = nullptr; + return 1; + } + + snd_hsize = read_w(header + 4); + if (!(snd_hdr = (type8 *)malloc(snd_hsize))) { + free(snd_buf); + delete snd_fp; + snd_buf = nullptr; + snd_fp = nullptr; + return 1; + } + + snd_fp->seek(6); + if (snd_fp->read(snd_hdr, snd_hsize) != snd_hsize) { + free(snd_buf); + free(snd_hdr); + delete snd_fp; + snd_buf = nullptr; + snd_hdr = nullptr; + snd_fp = nullptr; + return 1; + } + + return 2; +} - if (restarting) { +/* zero all registers and flags and load the game */ + +type8 ms_init(const char *name, const char *gfxname, const char *hntname, const char *sndname) { + Common::File fp; + type8 header[42], header2[8], header3[4]; + type32 i, dict_size, string2_size, code_size, dec; + +#if defined(LOGEMU) || defined(LOGGFX) || defined(LOGHNT) + dbg_log = fopen(LOG_FILE, "wt"); +#endif + ms_stop(); + if (!name) { if (!restart) return 0; else { @@ -45,221 +360,3281 @@ int Magnetic::ms_init(bool restarting) { } else { undo_stat[0] = undo_stat[1] = 0; - if (_gameFile.read(header, 42) != 42 || READ_BE_UINT32(header) != MKTAG('M', 'a', 'S', 'c') - || READ_LE_UINT32(header + 8) != 42) + if (!fp.open(name)) + return 0; + if ((fp.read(header, 42) != 42) || (read_l(header) != 0x4d615363)) + return 0; + if (read_l(header + 8) != 42) return 0; ms_freemem(); version = header[13]; - code_size = READ_LE_UINT32(header + 14); - string_size = READ_LE_UINT32(header + 18); - string2_size = READ_LE_UINT32(header + 22); - dict_size = READ_LE_UINT32(header + 26); - undo_size = READ_LE_UINT32(header + 34); - undo_pc = READ_LE_UINT32(header + 38); + code_size = read_l(header + 14); + string_size = read_l(header + 18); + string2_size = read_l(header + 22); + dict_size = read_l(header + 26); + undo_size = read_l(header + 34); + undo_pc = read_l(header + 38); if ((version < 4) && (code_size < 65536)) mem_size = 65536; else mem_size = code_size; - sd = (byte)((dict_size != 0L) ? 1 : 0); // if (sd) => separate dict + /* Some C libraries don't like malloc(0), so make + sure that undo_size is always positive. */ + if (undo_size == 0) + undo_size = 8; + + sd = (type8)((dict_size != 0L) ? 1 : 0); /* if (sd) => separate dict */ - if (!(code = new byte[mem_size]) || !(string2 = new byte[string2_size]) || - !(restart = new byte[undo_size]) || (sd && - !(dict = new byte[dict_size]))) { + if (!(code = (type8 *)malloc(mem_size)) || !(string2 = (type8 *)malloc(string2_size)) || + !(restart = (type8 *)malloc(undo_size)) || (sd && + !(dict = (type8 *)malloc(dict_size)))) { ms_freemem(); + fp.close(); return 0; } if (string_size > MAX_STRING_SIZE) { - if (!(string = new byte[MAX_STRING_SIZE]) || - !(string3 = new byte[string_size - MAX_STRING_SIZE])) { + if (!(string = (type8 *)malloc(MAX_STRING_SIZE)) || + !(string3 = (type8 *)malloc(string_size - MAX_STRING_SIZE))) { ms_freemem(); + fp.close(); return 0; } } else { - if (!(string = new byte[string_size])) { + if (!(string = (type8 *)malloc(string_size))) { ms_freemem(); + fp.close(); return 0; } } - if (!(undo[0] = new byte[undo_size]) || !(undo[1] = new byte[undo_size])) { + if (!(undo[0] = (type8 *)malloc(undo_size)) || !(undo[1] = (type8 *)malloc(undo_size))) { ms_freemem(); + fp.close(); return 0; } - if (_gameFile.read(code, code_size) != code_size) { + if (fp.read(code, code_size) != code_size) { ms_freemem(); + fp.close(); return 0; } - - memcpy(restart, code, undo_size); // fast restarts + memcpy(restart, code, undo_size); /* fast restarts */ if (string_size > MAX_STRING_SIZE) { - if (_gameFile.read(string, MAX_STRING_SIZE) != MAX_STRING_SIZE) { + if (fp.read(string, MAX_STRING_SIZE) != MAX_STRING_SIZE) { ms_freemem(); + fp.close(); return 0; } - if (_gameFile.read(string3, string_size - MAX_STRING_SIZE) != (string_size - MAX_STRING_SIZE)) { + if (fp.read(string3, string_size - MAX_STRING_SIZE) != string_size - MAX_STRING_SIZE) { ms_freemem(); + fp.close(); return 0; } } else { - if (_gameFile.read(string, string_size) != string_size) { + if (fp.read(string, string_size) != string_size) { ms_freemem(); + fp.close(); return 0; } } - if (_gameFile.read(string2, string2_size) != string2_size) { + if (fp.read(string2, string2_size) != string2_size) { ms_freemem(); + fp.close(); return 0; } - if (sd && _gameFile.read(dict, dict_size) != dict_size) { + if (sd && fp.read(dict, dict_size) != dict_size) { ms_freemem(); + fp.close(); return 0; } - - dec = READ_LE_UINT32(header + 30); - if (dec >= string_size) { + dec = read_l(header + 30); + if (dec >= string_size) decode_table = string2 + dec - string_size; - } else { + else { if (dec >= MAX_STRING_SIZE) decode_table = string3 + dec - MAX_STRING_SIZE; else decode_table = string + dec; } + fp.close(); } for (i = 0; i < 8; i++) dreg[i] = areg[i] = 0; - write_reg(8 + 7, 2, 0xfffe); // Stack-pointer, -2 due to MS-DOS segments + write_reg(8 + 7, 2, 0xfffe); /* Stack-pointer, -2 due to MS-DOS segments */ pc = 0; zflag = nflag = cflag = vflag = 0; i_count = 0; running = 1; - if (restarting) - return (byte)(gfx_buf ? 2 : 1); // Restarted + if (!name) + return (type8)(gfx_buf ? 2 : 1); /* Restarted */ if (version == 4) { - // Try loading a hint file - if (_hintFile.isOpen()) { - _hintFile.seek(0); - - if (_hintFile.readUint32BE() == MKTAG('M', 'a', 'H', 't')) { - uint16 blkcnt, elcnt, ntype, elsize, conidx; - - // Allocate memory for hints - hints = new ms_hint[MAX_HINTS]; - hint_contents = new byte[MAX_HCONTENTS]; + /* Try loading a hint file */ + Common::File hnt_fp; + if (hntname && hnt_fp.open(hntname)) { + if ((hnt_fp.read(&header3, 4) == 4) && (read_l(header3) == 0x4D614874)) { + type8 buf[8]; + type16 j, blkcnt, elcnt, ntype, elsize, conidx; + /* Allocate memory for hints */ + hints = (ms_hint *)malloc(MAX_HINTS * sizeof(struct ms_hint)); + hint_contents = (type8 *)malloc(MAX_HCONTENTS); if ((hints != 0) && (hint_contents != 0)) { - // Read number of blocks - blkcnt = _hintFile.readUint16LE(); - + /* Read number of blocks */ + if (hnt_fp.read(&buf, 2) != 2 && !hnt_fp.eos()) + return 0; + blkcnt = read_w2(buf); +#ifdef LOGHNT + out2("Blocks: %d\n", blkcnt); +#endif conidx = 0; for (i = 0; i < blkcnt; i++) { - // Read number of elements - elcnt = _hintFile.readUint16LE(); +#ifdef LOGHNT + out2("\nBlock No. %d\n", i); +#endif + /* Read number of elements */ + if (hnt_fp.read(&buf, 2) != 2 && !hnt_fp.eos()) return 0; + elcnt = read_w2(buf); +#ifdef LOGHNT + out2("Elements: %d\n", elcnt); +#endif hints[i].elcount = elcnt; - // Read node type - ntype = _hintFile.readUint16LE(); + /* Read node type */ + if (hnt_fp.read(&buf, 2) != 2 && !hnt_fp.eos()) return 0; + ntype = read_w2(buf); +#ifdef LOGHNT + if (ntype == 1) + out2("Type: Node\n"); + else + out2("Type: Leaf\n"); +#endif hints[i].nodetype = ntype; - hints[i].content = hint_contents + conidx; - + hints[i].content = (const char *)hint_contents + conidx; +#ifdef LOGHNT + out2("Elements:\n"); +#endif for (j = 0; j < elcnt; j++) { - elsize = _hintFile.readUint16LE(); - if (_hintFile.read(hint_contents + conidx, elsize) != elsize || _hintFile.eos()) - return 0; + if (hnt_fp.read(&buf, 2) != 2 && !hnt_fp.eos()) return 0; + elsize = read_w2(buf); + if (hnt_fp.read(hint_contents + conidx, elsize) != elsize && !hnt_fp.eos()) return 0; hint_contents[conidx + elsize - 1] = '\0'; - +#ifdef LOGHNT + out2("%s\n", hint_contents + conidx); +#endif conidx += elsize; } - // Do we need a jump table? + /* Do we need a jump table? */ if (ntype == 1) { +#ifdef LOGHNT + out2("Jump to block:\n"); +#endif for (j = 0; j < elcnt; j++) { - hints[i].links[j] = _hintFile.readUint16LE(); + if (hnt_fp.read(&buf, 2) != 2 && !hnt_fp.eos()) return 0; + hints[i].links[j] = read_w2(buf); +#ifdef LOGHNT + out2("%d\n", hints[i].links[j]); +#endif } } - // Read the parent block - hints[i].parent = _hintFile.readUint16LE(); - + /* Read the parent block */ + if (hnt_fp.read(&buf, 2) != 2 && !hnt_fp.eos()) return 0; + hints[i].parent = read_w2(buf); +#ifdef LOGHNT + out2("Parent: %d\n", hints[i].parent); +#endif } } else { - delete[] hints; - delete[] hint_contents; - hints = nullptr; - hint_contents = nullptr; + if (hints) + free(hints); + if (hint_contents) + free(hint_contents); + hints = 0; + hint_contents = 0; + } + } + hnt_fp.close(); + } + + /* Try loading a music file */ + snd_fp = new Common::File(); + if (sndname && snd_fp->open(sndname)) { + if (snd_fp->read(&header2, 8) != 8) { + delete snd_fp; + snd_fp = nullptr; + } else { + if (read_l(header2) == 0x4D615364) { /* MaSd */ + init_snd(header2); +#ifdef LOGSND + out2("Sound file loaded.\n"); +#endif + } + } + } else { + delete snd_fp; + snd_fp = nullptr; + } + } + + if (!gfxname) + return 1; + + gfx_fp = new Common::File(); + if (!gfx_fp->open(gfxname) || gfx_fp->read(&header2, 8) != 8) { + delete gfx_fp; + gfx_fp = nullptr; + return 1; + } + + if (version < 4 && read_l(header2) == 0x4D615069) /* MaPi */ + return init_gfx1(header2); + else if (version == 4 && read_l(header2) == 0x4D615032) /* MaP2 */ + return init_gfx2(header2); + delete gfx_fp; + gfx_fp = nullptr; + return 1; +} + +type8 is_blank(type16 line, type16 width) { + type32s i; + + for (i = line * width; i < (line + 1) * width; i++) + if (gfx_buf[i]) + return 0; + return 1; +} + +type8 *ms_extract1(type8 pic, type16 *w, type16 *h, type16 *pal) { + type8 *decodeTable, *data, bit, val, *buffer; + type16 tablesize, count; + type32 i, j, upsize, offset; + + offset = read_l(gfx_data + 4 * pic); +#ifdef SAVEMEM + type32 datasize; + + if (fseek(gfx_fp, offset, SEEK_SET) < 0) + return 0; + datasize = read_l(gfx_data + 4 * (pic + 1)) - offset; + if (!(buffer = (type8 *)malloc(datasize))) + return 0; + if (fp.read(buffer, 1, datasize, gfx_fp) != datasize) + return 0; +#else + buffer = gfx_data + offset - 8; +#endif + + for (i = 0; i < 16; i++) + pal[i] = read_w(buffer + 0x1c + 2 * i); + w[0] = (type16)(read_w(buffer + 4) - read_w(buffer + 2)); + h[0] = read_w(buffer + 6); + + tablesize = read_w(buffer + 0x3c); + //datasize = read_l(buffer + 0x3e); + decodeTable = buffer + 0x42; + data = decodeTable + tablesize * 2 + 2; + upsize = h[0] * w[0]; + + for (i = 0, j = 0, count = 0, val = 0, bit = 7; i < upsize; i++, count--) { + if (!count) { + count = tablesize; + while (count < 0x80) { + if (data[j] & (1 << bit)) + count = decodeTable[2 * count]; + else + count = decodeTable[2 * count + 1]; + if (!bit) + j++; + bit = (type8)(bit ? bit - 1 : 7); + } + count &= 0x7f; + if (count >= 0x10) + count -= 0x10; + else { + val = (type8)count; + count = 1; + } + } + gfx_buf[i] = val; + } + for (j = w[0]; j < upsize; j++) + gfx_buf[j] ^= gfx_buf[j - w[0]]; + +#ifdef SAVEMEM + free(buffer); +#endif + for (; h[0] > 0 && is_blank((type16)(h[0] - 1), w[0]); h[0]--); + for (i = 0; h[0] > 0 && is_blank((type16)i, w[0]); h[0]--, i++); + return gfx_buf + i * w[0]; +} + +type16s find_name_in_header(const char *name, type8 upper) { + type16s header_pos = 0; + char pic_name[8]; + type8 i; + + for (i = 0; i < 8; i++) + pic_name[i] = 0; + strncpy(pic_name, name, 6); + if (upper) { + for (i = 0; i < 8; i++) + pic_name[i] = (char)toupper(pic_name[i]); + } + + while (header_pos < gfx2_hsize) { + const char *hname = (const char *)(gfx2_hdr + header_pos); + if (strncmp(hname, pic_name, 6) == 0) + return header_pos; + header_pos += 16; + } + return -1; +} + +void extract_frame(struct picture *pic) { + type32 i, x, y, bit_x, mask, ywb, yw, value, values[4]; + + if (pic->width * pic->height > MAX_PICTURE_SIZE) { + ms_fatal("picture too large"); + return; + } + + for (y = 0; y < pic->height; y++) { + ywb = y * pic->wbytes; + yw = y * pic->width; + + for (x = 0; x < pic->width; x++) { + if ((x % 8) == 0) { + for (i = 0; i < 4; i++) + values[i] = pic->data[ywb + (x / 8) + (pic->plane_step * i)]; + } + + bit_x = 7 - (x & 7); + mask = 1 << bit_x; + value = ((values[0] & mask) >> bit_x) << 0 | + ((values[1] & mask) >> bit_x) << 1 | + ((values[2] & mask) >> bit_x) << 2 | + ((values[3] & mask) >> bit_x) << 3; + value &= 15; + + gfx_buf[yw + x] = (type8)value; + } + } +} + +type8 *ms_extract2(const char *name, type16 *w, type16 *h, type16 *pal, type8 *is_anim) { + struct picture main_pic; + type32 offset = 0, length = 0, i; + type16s header_pos = -1; +#ifndef NO_ANIMATION + type8 *anim_data; + type32 j; +#endif + + if (is_anim != 0) + *is_anim = 0; + gfx2_name = name; + +#ifndef NO_ANIMATION + pos_table_size = 0; +#endif + +#ifdef NO_ANIMATION + /* Find the uppercase (no animation) version of the picture first. */ + header_pos = find_name_in_header(name, 1); +#endif + if (header_pos < 0) + header_pos = find_name_in_header(name, 0); + if (header_pos < 0) + return 0; + + offset = read_l(gfx2_hdr + header_pos + 8); + length = read_l(gfx2_hdr + header_pos + 12); + + if (offset != 0) { + if (gfx2_buf) { + free(gfx2_buf); + gfx2_buf = nullptr; + } + + gfx2_buf = (type8 *)malloc(length); + if (!gfx2_buf) + return 0; + + if (!gfx_fp->seek(offset)) { + free(gfx2_buf); + gfx2_buf = nullptr; + return 0; + } + + if (gfx_fp->read(gfx2_buf, length) != length) { + free(gfx2_buf); + gfx2_buf = nullptr; + return 0; + } + + for (i = 0; i < 16; i++) + pal[i] = read_w2(gfx2_buf + 4 + (2 * i)); + + main_pic.data = gfx2_buf + 48; + main_pic.data_size = read_l2(gfx2_buf + 38); + main_pic.width = read_w2(gfx2_buf + 42); + main_pic.height = read_w2(gfx2_buf + 44); + main_pic.wbytes = (type16)(main_pic.data_size / main_pic.height); + main_pic.plane_step = (type16)(main_pic.wbytes / 4); + main_pic.mask = (type8 *)0; + extract_frame(&main_pic); + + *w = main_pic.width; + *h = main_pic.height; + +#ifndef NO_ANIMATION + /* Check for an animation */ + anim_data = gfx2_buf + 48 + main_pic.data_size; + if ((anim_data[0] != 0xD0) || (anim_data[1] != 0x5E)) { + type8 *current; + type16 frame_count; + type16 value1, value2; + + if (is_anim != 0) + *is_anim = 1; + + current = anim_data + 6; + frame_count = read_w2(anim_data + 2); + if (frame_count > MAX_ANIMS) { + ms_fatal("animation frame array too short"); + return 0; + } + + /* Loop through each animation frame */ + for (i = 0; i < frame_count; i++) { + anim_frame_table[i].data = current + 10; + anim_frame_table[i].data_size = read_l2(current); + anim_frame_table[i].width = read_w2(current + 4); + anim_frame_table[i].height = read_w2(current + 6); + anim_frame_table[i].wbytes = (type16)(anim_frame_table[i].data_size / anim_frame_table[i].height); + anim_frame_table[i].plane_step = (type16)(anim_frame_table[i].wbytes / 4); + anim_frame_table[i].mask = (type8 *)0; + + current += anim_frame_table[i].data_size + 12; + value1 = read_w2(current - 2); + value2 = read_w2(current); + + /* Get the mask */ + if ((value1 == anim_frame_table[i].width) && (value2 == anim_frame_table[i].height)) { + type16 skip; + + anim_frame_table[i].mask = (type8 *)(current + 4); + skip = read_w2(current + 2); + current += skip + 6; + } + } + + /* Get the positioning tables */ + pos_table_size = read_w2(current - 2); + if (pos_table_size > MAX_POSITIONS) { + ms_fatal("animation position array too short"); + return 0; + } + +#ifdef LOGGFX_EXT + out2("POSITION TABLE DUMP\n"); +#endif + for (i = 0; i < pos_table_size; i++) { + pos_table_count[i] = read_w2(current + 2); + current += 4; + + if (pos_table_count[i] > MAX_ANIMS) { + ms_fatal("animation position array too short"); + return 0; + } + + for (j = 0; j < pos_table_count[i]; j++) { + pos_table[i][j].x = read_w2(current); + pos_table[i][j].y = read_w2(current + 2); + pos_table[i][j].number = read_w2(current + 4) - 1; + current += 8; +#ifdef LOGGFX_EXT + out2("Position entry: Table: %d Entry: %d X: %d Y: %d Frame: %d\n", + i, j, pos_table[i][j].x, pos_table[i][j].y, pos_table[i][j].number); +#endif + } + } + + /* Get the command sequence table */ + //command_count = read_w2(current); + command_table = current + 2; + + for (i = 0; i < MAX_POSITIONS; i++) { + anim_table[i].flag = -1; + anim_table[i].count = -1; + } + command_index = 0; + anim_repeat = 0; + pos_table_index = -1; + pos_table_max = -1; + } +#endif + return gfx_buf; + } + return 0; +} + +type8 *ms_extract(type32 pic, type16 *w, type16 *h, type16 *pal, type8 *is_anim) { + if (is_anim) + *is_anim = 0; + + if (gfx_buf) { + switch (gfx_ver) { + case 1: + return ms_extract1((type8)pic, w, h, pal); + case 2: + return ms_extract2((const char *)(code + pic), w, h, pal, is_anim); + default: + break; + } + } + return 0; +} + +type8 ms_animate(struct ms_position **positions, type16 *count) { +#ifndef NO_ANIMATION + type8 got_anim = 0; + type16 i, j, ttable; + + if ((gfx_buf == 0) || (gfx2_buf == 0) || (gfx_ver != 2)) + return 0; + if ((pos_table_size == 0) || (command_index < 0)) + return 0; + + *count = 0; + *positions = (struct ms_position *)0; + + while (got_anim == 0) { + if (pos_table_max >= 0) { + if (pos_table_index < pos_table_max) { + for (i = 0; i < pos_table_size; i++) { + if (anim_table[i].flag > -1) { + if (*count >= MAX_FRAMES) { + ms_fatal("returned animation array too short"); + return 0; + } + + pos_array[*count] = pos_table[i][anim_table[i].flag]; +#ifdef LOGGFX_EXT + out2("Adding frame %d to buffer\n", pos_array[*count].number); +#endif + (*count)++; + + if (anim_table[i].flag < (pos_table_count[i] - 1)) + anim_table[i].flag++; + if (anim_table[i].count > 0) + anim_table[i].count--; + else + anim_table[i].flag = -1; + } } + if (*count > 0) { + *positions = pos_array; + got_anim = 1; + } + pos_table_index++; } } - // Try loading a music file - if (_sndFile.isOpen() && _sndFile.size() >= 8) { - _sndFile.seek(0); + if (got_anim == 0) { + type8 command = command_table[command_index]; + command_index++; + + pos_table_max = -1; + pos_table_index = -1; - if (_sndFile.readUint32BE() != MKTAG('M', 'a', 'S', 'd')) + switch (command) { + case 0x00: + command_index = -1; return 0; + case 0x01: +#ifdef LOGGFX_EXT + out2("Load Frame Table: %d Start at: %d Count: %d\n", + command_table[command_index], command_table[command_index + 1], + command_table[command_index + 2]); +#endif + ttable = command_table[command_index]; + command_index++; + + if (ttable - 1 >= MAX_POSITIONS) { + ms_fatal("animation table too short"); + return 0; + } + + anim_table[ttable - 1].flag = (type16s)(command_table[command_index] - 1); + command_index++; + anim_table[ttable - 1].count = (type16s)(command_table[command_index] - 1); + command_index++; - init_snd(_sndFile.readUint32LE()); + /* Workaround for Wonderland "catter" animation */ + if (v4_id == 0) { + if (strcmp(gfx2_name, "catter") == 0) { + if (command_index == 96) + anim_table[ttable - 1].count = 9; + if (command_index == 108) + anim_table[ttable - 1].flag = -1; + if (command_index == 156) + anim_table[ttable - 1].flag = -1; + } + } + break; + case 0x02: +#ifdef LOGGFX_EXT + out2("Animate: %d\n", command_table[command_index]); +#endif + pos_table_max = command_table[command_index]; + pos_table_index = 0; + command_index++; + break; + case 0x03: +#ifdef LOGGFX_EXT + out2("Stop/Repeat Param: %d\n", command_table[command_index]); + command_index = -1; + return 0; +#else + if (v4_id == 0) { + command_index = -1; + return 0; + } else { + command_index = 0; + anim_repeat = 1; + pos_table_index = -1; + pos_table_max = -1; + for (j = 0; j < MAX_POSITIONS; j++) { + anim_table[j].flag = -1; + anim_table[j].count = -1; + } + } + break; +#endif + case 0x04: +#ifdef LOGGFX_EXT + out2("Unknown Command: %d Prop1: %d Prop2: %d WARNING:not parsed\n", command, + command_table[command_index], command_table[command_index + 1]); +#endif + command_index += 3; + return 0; + case 0x05: + ttable = next_table; + command_index++; + + anim_table[ttable - 1].flag = 0; + anim_table[ttable - 1].count = command_table[command_index]; + + pos_table_max = command_table[command_index]; + pos_table_index = 0; + command_index++; + command_index++; + next_table++; + break; + default: + ms_fatal("unknown animation command"); + command_index = -1; + return 0; + } } } +#ifdef LOGGFX_EXT + out2("ms_animate() returning %d frames\n", *count); +#endif + return got_anim; +#else + return 0; +#endif +} - if (!_gfxFile.isOpen() || _gfxFile.size() < 8) - return 1; - _gfxFile.seek(0); - uint tag = _gfxFile.readUint32BE(); +type8 *ms_get_anim_frame(type16s number, type16 *width, type16 *height, type8 **mask) { +#ifndef NO_ANIMATION + if (number >= 0) { + extract_frame(anim_frame_table + number); + *width = anim_frame_table[number].width; + *height = anim_frame_table[number].height; + *mask = anim_frame_table[number].mask; + return gfx_buf; + } +#endif + return 0; +} + +type8 ms_anim_is_repeating(void) { +#ifndef NO_ANIMATION + return anim_repeat; +#else + return 0; +#endif +} + +type16s find_name_in_sndheader(const char *name) { + type16s header_pos = 0; + + while (header_pos < snd_hsize) { + const char *hname = (const char *)(snd_hdr + header_pos); + if (strcmp(hname, name) == 0) + return header_pos; + header_pos += 18; + } + + return -1; +} - if (version < 4 && tag == MKTAG('M', 'a', 'P', 'i')) - return init_gfx1(_gfxFile.readUint32LE() - 8); - else if (version == 4 && tag == MKTAG('M', 'a', 'P', '2')) - return init_gfx2(_gfxFile.readUint16LE()); +type8 *sound_extract(const char *name, type32 *length, type16 *tempo) { + type32 offset = 0; + type16s header_pos = -1; + if (header_pos < 0) + header_pos = find_name_in_sndheader(name); + if (header_pos < 0) + return 0; + + *tempo = read_w(snd_hdr + header_pos + 8); + offset = read_l(snd_hdr + header_pos + 10); + *length = read_l(snd_hdr + header_pos + 14); + + if (offset != 0) { + if (!snd_buf) + return 0; + if (!snd_fp->seek(offset) || snd_fp->read(snd_buf, *length) != *length) + return 0; + + return snd_buf; + } + + return nullptr; +} + +void save_undo(void) { + type8 *tmp, i; + type32 tmp32; + + tmp = undo[0]; /* swap buffers */ + undo[0] = undo[1]; + undo[1] = tmp; + + for (i = 0; i < 18; i++) { + tmp32 = undo_regs[0][i]; + undo_regs[0][i] = undo_regs[1][i]; + undo_regs[1][i] = tmp32; + } + + memcpy(undo[1], code, undo_size); + for (i = 0; i < 8; i++) { + undo_regs[1][i] = dreg[i]; + undo_regs[1][8 + i] = areg[i]; + } + undo_regs[1][16] = i_count; + undo_regs[1][17] = pc; /* status flags intentionally omitted */ + + undo_stat[0] = undo_stat[1]; + undo_stat[1] = 1; +} + +type8 ms_undo(void) { + type8 i; + + ms_flush(); + if (!undo_stat[0]) + return 0; + + undo_stat[0] = undo_stat[1] = 0; + memcpy(code, undo[0], undo_size); + for (i = 0; i < 8; i++) { + dreg[i] = undo_regs[0][i]; + areg[i] = undo_regs[0][8 + i]; + } + i_count = undo_regs[0][16]; + pc = undo_regs[0][17]; /* status flags intentionally omitted */ return 1; } -void Magnetic::ms_freemem() { - delete[] code; - delete[] string; - delete[] string2; - delete[] string3; - delete[] dict; - delete[] undo[0]; - delete[] undo[1]; - delete[] restart; - code = string = string2 = string3 = dict = nullptr; - undo[0] = undo[1] = restart = nullptr; +#ifdef LOGEMU +void log_status(void) { + int j; + + fprintf(dbg_log, "\nD0:"); + for (j = 0; j < 8; j++) + fprintf(dbg_log, " %8.8x", read_reg(j, 3)); + fprintf(dbg_log, "\nA0:"); + for (j = 0; j < 8; j++) + fprintf(dbg_log, " %8.8x", read_reg(8 + j, 3)); + fprintf(dbg_log, "\nPC=%5.5x (%8.8x) ZCNV=%d%d%d%d - %d instructions\n\n", + pc, code, zflag & 1, cflag & 1, nflag & 1, vflag & 1, i_count); +} +#endif + +void ms_status(void) { + int j; - delete[] gfx_data; - delete[] gfx_buf; - delete[] gfx2_hdr; - delete[] gfx2_buf; - gfx_data = gfx_buf = gfx2_hdr = gfx2_buf = 0; + Common::String s = "D0:"; + for (j = 0; j < 8; j++) + s += Common::String::format(" %8.8lx", (long) read_reg(j, 3)); + s += "\nA0:"; - gfx_fp.close(); + for (j = 0; j < 8; j++) + s += Common::String::format(" %8.8lx", (long) read_reg(8 + j, 3)); + s += Common::String::format("\nPC=%5.5lx ZCNV=%d%d%d%d - %ld instructions\n", + (long) pc, zflag & 1, cflag & 1, nflag & 1, vflag & 1, (long) i_count); + warning("%s", s.c_str()); +} - gfx2_name.clear(); - gfx_ver = 0; - gfxtable = table_dist = 0; +type32 ms_count(void) { + return i_count; +} - pos_table_size = 0; - command_index = 0; - anim_repeat = 0; - pos_table_index = -1; - pos_table_max = -1; +/* align register pointer for word/byte accesses */ - lastchar = 0; - delete[] hints; - delete[] hint_contents; - hints = nullptr; - hint_contents = nullptr; +type8 *reg_align(type8 *ptr, type8 size) { + if (size == 1) + ptr += 2; + if (size == 0) + ptr += 3; + return ptr; +} - delete[] snd_hdr; - delete[] snd_buf; - snd_hdr = nullptr; - snd_hsize = 0; - snd_buf = nullptr; +type32 read_reg(int i, int s) { + type8 *ptr; + + if (i > 15) { + ms_fatal("invalid register in read_reg"); + return 0; + } + if (i < 8) + ptr = (type8 *) & dreg[i]; + else + ptr = (type8 *) & areg[i - 8]; + + switch (s) { + case 0: + return reg_align(ptr, 0)[0]; + case 1: + return read_w(reg_align(ptr, 1)); + default: + return read_l(ptr); + } +} + +void write_reg(int i, int s, type32 val) { + type8 *ptr; + + if (i > 15) { + ms_fatal("invalid register in write_reg"); + return; + } + if (i < 8) + ptr = (type8 *) & dreg[i]; + else + ptr = (type8 *) & areg[i - 8]; + + switch (s) { + case 0: + reg_align(ptr, 0)[0] = (type8)val; + break; + case 1: + write_w(reg_align(ptr, 1), (type16)val); + break; + default: + write_l(ptr, val); + break; + } +} + +/* [35c4] */ + +void char_out(type8 c) { + static type8 big = 0, period = 0, pipe = 0; + + if (c == 0xff) { + big = 1; + return; + } + c &= 0x7f; + if (read_reg(3, 0)) { + if (c == 0x5f || c == 0x40) + c = 0x20; + if ((c >= 'a') && (c <= 'z')) + c &= 0xdf; + if (version < 4) + ms_statuschar(c); + return; + } + if (c == 0x5e) + c = 0x0a; + if (c == 0x40) { + if (read_reg(2, 0)) + return; + else + c = 0x73; + } + if (version < 3 && c == 0x7e) { + lastchar = 0x7e; + c = 0x0a; + } + if (((c > 0x40) && (c < 0x5b)) || ((c > 0x60) && (c < 0x7b))) { + if (big) { + c &= 0xdf; + big = 0; + } + if (period) + char_out(0x20); + } + period = 0; + if (version < 4) { + if ((c == 0x2e) || (c == 0x3f) || (c == 0x21) || (c == 0x0a)) + big = 1; + else if (c == 0x22) + big = 0; + } else { + if ((c == 0x20) && (lastchar == 0x0a)) + return; + if ((c == 0x2e) || (c == 0x3f) || (c == 0x21) || (c == 0x0a)) + big = 1; + else if (c == 0x22) + big = 0; + } + if (((c == 0x20) || (c == 0x0a)) && (c == lastchar)) + return; + if (version < 3) { + if (pipe) { + pipe = 0; + return; + } + if (c == 0x7c) { + pipe = 1; + return; + } + } else { + if (c == 0x7e) { + c = 0x0a; + if (lastchar != 0x0a) + char_out(0x0a); + } + } + lastchar = c; + if (c == 0x5f) + c = 0x20; + if ((c == 0x2e) || (c == 0x2c) || (c == 0x3b) || (c == 0x3a) || (c == 0x21) || (c == 0x3f)) + period = 1; + ms_putchar(c); +} + + +/* extract addressing mode information [1c6f] */ + +void set_info(type8 b) { + regnr = (type8)(b & 0x07); + admode = (type8)((b >> 3) & 0x07); + opsize = (type8)(b >> 6); +} + +/* read a word and increase pc */ + +void read_word(void) { + type8 *epc; + + epc = effective(pc); + byte1 = epc[0]; + byte2 = epc[1]; + pc += 2; +} + +/* get addressing mode and set arg1 [1c84] */ + +void set_arg1(void) { + type8 tmp[2], l1c; + + is_reversible = 1; + switch (admode) { + case 0: + arg1 = reg_align((type8 *) & dreg[regnr], opsize); /* Dx */ + is_reversible = 0; +#ifdef LOGEMU + out(" d%.1d", regnr); +#endif + break; + case 1: + arg1 = reg_align((type8 *) & areg[regnr], opsize); /* Ax */ + is_reversible = 0; +#ifdef LOGEMU + out(" a%.1d", regnr); +#endif + break; + case 2: + arg1i = read_reg(8 + regnr, 2); /* (Ax) */ +#ifdef LOGEMU + out(" (a%.1d)", regnr); +#endif + break; + case 3: + arg1i = read_reg(8 + regnr, 2); /* (Ax)+ */ + write_reg(8 + regnr, 2, read_reg(8 + regnr, 2) + (1 << opsize)); +#ifdef LOGEMU + out(" (a%.1d)+", regnr); +#endif + break; + case 4: + write_reg(8 + regnr, 2, read_reg(8 + regnr, 2) - (1 << opsize)); + arg1i = read_reg(8 + regnr, 2); /* -(Ax) */ +#ifdef LOGEMU + out(" -(a%.1d)", regnr); +#endif + break; + case 5: { + type16s i = (type16s) read_w(effective(pc)); + arg1i = read_reg(8 + regnr, 2) + i; + pc += 2; /* offset.w(Ax) */ +#ifdef LOGEMU + out(" %X(a%.1d)", i, regnr); +#endif + } + break; + case 6: + tmp[0] = byte1; + tmp[1] = byte2; + read_word(); /* offset.b(Ax, Dx/Ax) [1d1c] */ +#ifdef LOGEMU + out(" %.2X(a%.1d,", (int) byte2, regnr); +#endif + arg1i = read_reg(regnr + 8, 2) + (type8s) byte2; +#ifdef LOGEMU + if ((byte1 >> 4) > 8) + out("a%.1d", (byte1 >> 4) - 8); + else + out("d%.1d", byte1 >> 4); +#endif + if (byte1 & 0x08) { +#ifdef LOGEMU + out(".l)"); +#endif + arg1i += (type32s) read_reg((byte1 >> 4), 2); + } else { +#ifdef LOGEMU + out(".w)"); +#endif + arg1i += (type16s) read_reg((byte1 >> 4), 1); + } + byte1 = tmp[0]; + byte2 = tmp[1]; + break; + case 7: /* specials */ + switch (regnr) { + case 0: + arg1i = read_w(effective(pc)); /* $xxxx.W */ + pc += 2; +#ifdef LOGEMU + out(" %.4X.w", arg1i); +#endif + break; + case 1: + arg1i = read_l(effective(pc)); /* $xxxx */ + pc += 4; +#ifdef LOGEMU + out(" %.4X", arg1i); +#endif + break; + case 2: + arg1i = (type16s) read_w(effective(pc)) + pc; /* $xxxx(PC) */ + pc += 2; +#ifdef LOGEMU + out(" %.4X(pc)", arg1i); +#endif + break; + case 3: + l1c = effective(pc)[0]; /* $xx(PC,A/Dx) */ +#ifdef LOGEMU + out(" ???2", arg1i); +#endif + if (l1c & 0x08) + arg1i = pc + (type32s) read_reg((l1c >> 4), 2); + else + arg1i = pc + (type16s) read_reg((l1c >> 4), 1); + l1c = effective(pc)[1]; + pc += 2; + arg1i += (type8s) l1c; + break; + case 4: + arg1i = pc; /* #$xxxx */ + if (opsize == 0) + arg1i += 1; + pc += 2; + if (opsize == 2) + pc += 2; +#ifdef LOGEMU + out(" #%.4X", arg1i); +#endif + break; + } + break; + } + if (is_reversible) + arg1 = effective(arg1i); +} + +/* get addressing mode and set arg2 [1bc5] */ + +void set_arg2_nosize(int use_dx, type8 b) { + if (use_dx) + arg2 = (type8 *) dreg; + else + arg2 = (type8 *) areg; + arg2 += (b & 0x0e) << 1; +} + +void set_arg2(int use_dx, type8 b) { + set_arg2_nosize(use_dx, b); + arg2 = reg_align(arg2, opsize); +} + +/* [1b9e] */ + +void swap_args(void) { + type8 *tmp; + + tmp = arg1; + arg1 = arg2; + arg2 = tmp; +} + +/* [1cdc] */ + +void push(type32 c) { + write_reg(15, 2, read_reg(15, 2) - 4); + write_l(effective(read_reg(15, 2)), c); +} + +/* [1cd1] */ + +type32 pop(void) { + type32 c; + + c = read_l(effective(read_reg(15, 2))); + write_reg(15, 2, read_reg(15, 2) + 4); + return c; +} + +/* check addressing mode and get argument [2e85] */ + +void get_arg(void) { +#ifdef LOGEMU + out(" %.4X", pc); +#endif + set_info(byte2); + arg2 = effective(pc); + pc += 2; + if (opsize == 2) + pc += 2; + if (opsize == 0) + arg2 += 1; + set_arg1(); +} + +void set_flags(void) { + type16 i; + type32 j; + + zflag = nflag = 0; + switch (opsize) { + case 0: + if (arg1[0] > 127) + nflag = 0xff; + if (arg1[0] == 0) + zflag = 0xff; + break; + case 1: + i = read_w(arg1); + if (i == 0) + zflag = 0xff; + if ((i >> 15) > 0) + nflag = 0xff; + break; + case 2: + j = read_l(arg1); + if (j == 0) + zflag = 0xff; + if ((j >> 31) > 0) + nflag = 0xff; + break; + } +} + +/* [263a] */ + +int condition(type8 b) { + switch (b & 0x0f) { + case 0: + return 0xff; + case 1: + return 0x00; + case 2: + return (zflag | cflag) ^ 0xff; + case 3: + return (zflag | cflag); + case 4: + return cflag ^ 0xff; + case 5: + return cflag; + case 6: + return zflag ^ 0xff; + case 7: + return zflag; + case 8: + return vflag ^ 0xff; + case 9: + return vflag; + case 10: + case 12: + return nflag ^ 0xff; + case 11: + case 13: + return nflag; + case 14: + return (zflag | nflag) ^ 0xff; + case 15: + return (zflag | nflag); + } + return 0x00; +} + +/* [26dc] */ + +void branch(type8 b) { + if (b == 0) + pc += (type16s) read_w(effective(pc)); + else + pc += (type8s) b; +#ifdef LOGEMU + out(" %.4X", pc); +#endif +} + +/* [2869] */ + +void do_add(type8 adda) { + if (adda) { + if (opsize == 0) + write_l(arg1, read_l(arg1) + (type8s) arg2[0]); + if (opsize == 1) + write_l(arg1, read_l(arg1) + (type16s) read_w(arg2)); + if (opsize == 2) + write_l(arg1, read_l(arg1) + (type32s) read_l(arg2)); + } else { + cflag = 0; + if (opsize == 0) { + arg1[0] += arg2[0]; + if (arg2[0] > arg1[0]) + cflag = 0xff; + } + if (opsize == 1) { + write_w(arg1, (type16)(read_w(arg1) + read_w(arg2))); + if (read_w(arg2) > read_w(arg1)) + cflag = 0xff; + } + if (opsize == 2) { + write_l(arg1, read_l(arg1) + read_l(arg2)); + if (read_l(arg2) > read_l(arg1)) + cflag = 0xff; + } + if (version < 3 || !quick_flag) { + /* Corruption onwards */ + vflag = 0; + set_flags(); + } + } +} + +/* [2923] */ + +void do_sub(type8 suba) { + if (suba) { + if (opsize == 0) + write_l(arg1, read_l(arg1) - (type8s) arg2[0]); + if (opsize == 1) + write_l(arg1, read_l(arg1) - (type16s) read_w(arg2)); + if (opsize == 2) + write_l(arg1, read_l(arg1) - (type32s) read_l(arg2)); + } else { + cflag = 0; + if (opsize == 0) { + if (arg2[0] > arg1[0]) + cflag = 0xff; + arg1[0] -= arg2[0]; + } + if (opsize == 1) { + if (read_w(arg2) > read_w(arg1)) + cflag = 0xff; + write_w(arg1, (type16)(read_w(arg1) - read_w(arg2))); + } + if (opsize == 2) { + if (read_l(arg2) > read_l(arg1)) + cflag = 0xff; + write_l(arg1, read_l(arg1) - read_l(arg2)); + } + if (version < 3 || !quick_flag) { + /* Corruption onwards */ + vflag = 0; + set_flags(); + } + } +} + +/* [283b] */ + +void do_eor(void) { + if (opsize == 0) + arg1[0] ^= arg2[0]; + if (opsize == 1) + write_w(arg1, (type16)(read_w(arg1) ^ read_w(arg2))); + if (opsize == 2) + write_l(arg1, read_l(arg1) ^ read_l(arg2)); + cflag = vflag = 0; + set_flags(); +} + +/* [280d] */ + +void do_and(void) { + if (opsize == 0) + arg1[0] &= arg2[0]; + if (opsize == 1) + write_w(arg1, (type16)(read_w(arg1) & read_w(arg2))); + if (opsize == 2) + write_l(arg1, read_l(arg1) & read_l(arg2)); + cflag = vflag = 0; + set_flags(); +} + +/* [27df] */ + +void do_or(void) { + if (opsize == 0) + arg1[0] |= arg2[0]; + if (opsize == 1) + write_w(arg1, (type16)(read_w(arg1) | read_w(arg2))); + if (opsize == 2) + write_l(arg1, read_l(arg1) | read_l(arg2)); + cflag = vflag = 0; + set_flags(); /* [1c2b] */ +} + +/* [289f] */ + +void do_cmp(void) { + type8 *tmp; + + tmp = arg1; + tmparg[0] = arg1[0]; + tmparg[1] = arg1[1]; + tmparg[2] = arg1[2]; + tmparg[3] = arg1[3]; + arg1 = tmparg; + quick_flag = 0; + do_sub(0); + arg1 = tmp; +} + +/* [2973] */ + +void do_move(void) { + + if (opsize == 0) + arg1[0] = arg2[0]; + if (opsize == 1) + write_w(arg1, read_w(arg2)); + if (opsize == 2) + write_l(arg1, read_l(arg2)); + if (version < 2 || admode != 1) { + /* Jinxter: no flags if destination Ax */ + cflag = vflag = 0; + set_flags(); + } +} + +type8 do_btst(type8 a) { + a &= admode ? 0x7 : 0x1f; + while (admode == 0 && a >= 8) { + a -= 8; + arg1 -= 1; + } + zflag = 0; + if ((arg1[0] & (1 << a)) == 0) + zflag = 0xff; + return a; +} + +/* bit operation entry point [307c] */ + +void do_bop(type8 b, type8 a) { +#ifdef LOGEMU + out("bop (%.2x,%.2x) ", (int) b, (int) a); +#endif + b = b & 0xc0; + a = do_btst(a); +#ifdef LOGEMU + if (b == 0x00) + out("no bop???"); +#endif + if (b == 0x40) { + arg1[0] ^= (1 << a); /* bchg */ +#ifdef LOGEMU + out("bchg"); +#endif + } + if (b == 0x80) { + arg1[0] &= ((1 << a) ^ 0xff); /* bclr */ +#ifdef LOGEMU + out("bclr"); +#endif + } + if (b == 0xc0) { + arg1[0] |= (1 << a); /* bset */ +#ifdef LOGEMU + out("bset"); +#endif + } +} + +void check_btst(void) { +#ifdef LOGEMU + out("btst"); +#endif + set_info((type8)(byte2 & 0x3f)); + set_arg1(); + set_arg2(1, byte1); + do_bop(byte2, arg2[0]); +} + +void check_lea(void) { +#ifdef LOGEMU + out("lea"); +#endif + if ((byte2 & 0xc0) == 0xc0) { + set_info(byte2); + opsize = 2; + set_arg1(); + set_arg2(0, byte1); + write_w(arg2, 0); + if (is_reversible) + write_l(arg2, arg1i); + else + ms_fatal("illegal addressing mode for LEA"); + } else { + ms_fatal("unimplemented instruction CHK"); + } +} + +/* [33cc] */ + +void check_movem(void) { + type8 l1c; + +#ifdef LOGEMU + out("movem"); +#endif + set_info((type8)(byte2 - 0x40)); + read_word(); + for (l1c = 0; l1c < 8; l1c++) { + if (byte2 & 1 << l1c) { + set_arg1(); + if (opsize == 2) + write_l(arg1, read_reg(15 - l1c, 2)); + if (opsize == 1) + write_w(arg1, (type16)read_reg(15 - l1c, 1)); + } + } + for (l1c = 0; l1c < 8; l1c++) { + if (byte1 & 1 << l1c) { + set_arg1(); + if (opsize == 2) + write_l(arg1, read_reg(7 - l1c, 2)); + if (opsize == 1) + write_w(arg1, (type16)read_reg(7 - l1c, 1)); + } + } +} + +/* [3357] */ + +void check_movem2(void) { + type8 l1c; + +#ifdef LOGEMU + out("movem (2)"); +#endif + set_info((type8)(byte2 - 0x40)); + read_word(); + for (l1c = 0; l1c < 8; l1c++) { + if (byte2 & 1 << l1c) { + set_arg1(); + if (opsize == 2) + write_reg(l1c, 2, read_l(arg1)); + if (opsize == 1) + write_reg(l1c, 1, read_w(arg1)); + } + } + for (l1c = 0; l1c < 8; l1c++) { + if (byte1 & 1 << l1c) { + set_arg1(); + if (opsize == 2) + write_reg(8 + l1c, 2, read_l(arg1)); + if (opsize == 1) + write_reg(8 + l1c, 1, read_w(arg1)); + } + } +} + +/* [30e4] in Jinxter, ~540 lines of 6510 spaghetti-code */ +/* The mother of all bugs, but hey - no gotos used :-) */ + +void dict_lookup(void) { + type16 dtab, doff, output, output_bak, bank, word, output2; + type16 tmp16, i, obj_adj, adjlist, adjlist_bak; + type8 c, c2, c3, flag, matchlen, longest, flag2; + type8 restartFlag = 0, accept = 0; + + /* + dtab=A5.W ;dict_table offset <L22> + output=output_bak=A2.W ;output <L24> + A5.W=A6.W ;input word + doff=A3.W ;lookup offset (doff) <L1C> + adjlist=A0.W ;adjlist <L1E> + */ + + dtab = (type16)read_reg(8 + 5, 1); /* used by version>0 */ + output = (type16)read_reg(8 + 2, 1); + write_reg(8 + 5, 1, read_reg(8 + 6, 1)); + doff = (type16)read_reg(8 + 3, 1); + adjlist = (type16)read_reg(8 + 0, 1); + + bank = (type16)read_reg(6, 0); /* l2d */ + flag = 0; /* l2c */ + word = 0; /* l26 */ + matchlen = 0; /* l2e */ + longest = 0; /* 30e2 */ + write_reg(0, 1, 0); /* apostroph */ + + while ((c = sd ? dict[doff] : effective(doff)[0]) != 0x81) { + if (c >= 0x80) { + if (c == 0x82) { + flag = matchlen = 0; + word = 0; + write_reg(8 + 6, 1, read_reg(8 + 5, 1)); + bank++; + doff++; + continue; + } + c3 = c; + c &= 0x5f; + c2 = effective(read_reg(8 + 6, 1))[0] & 0x5f; + if (c2 == c) { + write_reg(8 + 6, 1, read_reg(8 + 6, 1) + 1); + c = effective(read_reg(8 + 6, 1))[0]; + if ((!c) || (c == 0x20) || (c == 0x27) || (!version && (matchlen > 6))) { + if (c == 0x27) { + write_reg(8 + 6, 1, read_reg(8 + 6, 1) + 1); + write_reg(0, 1, 0x200 + effective(read_reg(8 + 6, 1))[0]); + } + if ((version < 4) || (c3 != 0xa0)) + accept = 1; + } else + restartFlag = 1; + } else if (!version && matchlen > 6 && !c2) + accept = 1; + else + restartFlag = 1; + } else { + c &= 0x5f; + c2 = effective(read_reg(8 + 6, 1))[0] & 0x5f; + if ((c2 == c && c) || (version && !c2 && (c == 0x5f))) { + if (version && !c2 && (c == 0x5f)) + flag = 0x80; + matchlen++; + write_reg(8 + 6, 1, read_reg(8 + 6, 1) + 1); + doff++; + } else if (!version && matchlen > 6 && !c2) + accept = 1; + else + restartFlag = 1; + } + if (accept) { + effective(read_reg(8 + 2, 1))[0] = (version) ? flag : 0; + effective(read_reg(8 + 2, 1))[1] = (type8)bank; + write_w(effective(read_reg(8 + 2, 1) + 2), word); + write_reg(8 + 2, 1, read_reg(8 + 2, 1) + 4); + if (matchlen >= longest) + longest = matchlen; + restartFlag = 1; + accept = 0; + } + if (restartFlag) { + write_reg(8 + 6, 1, read_reg(8 + 5, 1)); + flag = matchlen = 0; + word++; + if (sd) + while (dict[doff++] < 0x80); + else + while (effective(doff++)[0] < 0x80); + restartFlag = 0; + } + } + write_w(effective(read_reg(8 + 2, 1)), 0xffff); + + if (version) { + /* version > 0 */ + output_bak = output; /* check synonyms */ + while ((c = effective(output)[1]) != 0xff) { + if (c == 0x0b) { + if (sd) + tmp16 = read_w(&dict[dtab + read_w(effective(output + 2)) * 2]); + else + tmp16 = read_w(effective(dtab + read_w(effective(output + 2)) * 2)); + effective(output)[1] = tmp16 & 0x1f; + write_w(effective(output + 2), (type16)(tmp16 >> 5)); + } + output += 4; + } + output = output_bak; + } + + /* l22 = output2, l1e = adjlist, l20 = obj_adj, l26 = word, l2f = c2 */ + /* l1c = adjlist_bak, 333C = i, l2d = bank, l2c = flag, l30e3 = flag2 */ + + write_reg(1, 1, 0); /* D1.W=0 [32B5] */ + flag2 = 0; + output_bak = output; + output2 = output; + while ((bank = effective(output2)[1]) != 0xff) { + obj_adj = (type16)read_reg(8 + 1, 1); /* A1.W - obj_adj, ie. adjs for this word */ + write_reg(1, 0, 0); /* D1.B=0 */ + flag = effective(output2)[0]; /* flag */ + word = read_w(effective(output2 + 2)); /* wordnumber */ + output2 += 4; /* next match */ + if ((read_w(effective(obj_adj))) && (bank == 6)) { + /* Any adjectives? */ + if ((i = word) != 0) { + /* Find list of valid adjs */ + do { + while (effective(adjlist++)[0]); + } while (--i > 0); + } + adjlist_bak = adjlist; + do { + adjlist = adjlist_bak; + c2 = effective(obj_adj)[1]; /* given adjective */ + if ((tmp16 = read_w(effective(obj_adj))) != 0) { + obj_adj += 2; + while ((c = effective(adjlist++)[0]) && (c - 3 != c2)); + if (c - 3 != c2) + write_reg(1, 0, 1); /* invalid adjective */ + } + } while (tmp16 && !read_reg(1, 0)); + adjlist = (type16)read_reg(8 + 0, 1); + } + if (!read_reg(1, 0)) { + /* invalid_flag */ + flag2 |= flag; + effective(output)[0] = flag2; + effective(output)[1] = (type8)bank; + write_w(effective(output + 2), word); + output += 4; + } + } + write_reg(8 + 2, 1, output); + output = output_bak; + + if (flag2 & 0x80) { + tmp16 = output; + output -= 4; + do { + output += 4; + c = effective(output)[0]; + } while (!(c & 0x80)); + write_l(effective(tmp16), read_l(effective(output)) & 0x7fffffff); + write_reg(8 + 2, 2, tmp16 + 4); + if (longest > 1) { + write_reg(8 + 5, 1, read_reg(8 + 5, 1) + longest - 2); + } + } + write_reg(8 + 6, 1, read_reg(8 + 5, 1) + 1); +} + +/* A0=findproperties(D0) [2b86], properties_ptr=[2b78] A0FE */ + +void do_findprop(void) { + type16 tmp; + + if ((version > 2) && ((read_reg(0, 1) & 0x3fff) > fp_size)) { + tmp = (type16)(((fp_size - (read_reg(0, 1) & 0x3fff)) ^ 0xffff) << 1); + tmp += fp_tab; + tmp = read_w(effective(tmp)); + } else { + if (version < 2) + write_reg(0, 2, read_reg(0, 2) & 0x7fff); + else + write_reg(0, 1, read_reg(0, 1) & 0x7fff); + tmp = (type16)read_reg(0, 1); + } + tmp &= 0x3fff; + write_reg(8 + 0, 2, tmp * 14 + properties); +} + +void write_string(void) { + static type32 offset_bak; + static type8 mask_bak; + type8 c, b, mask; + type16 ptr; + type32 offset; + + if (!cflag) { + /* new string */ + ptr = (type16)read_reg(0, 1); + if (!ptr) + offset = 0; + else { + offset = read_w(&decode_table[0x100 + 2 * ptr]); + if (read_w(&decode_table[0x100])) { + if (ptr >= read_w(&decode_table[0x100])) + offset += string_size; + } + } + mask = 1; + } else { + offset = offset_bak; + mask = mask_bak; + } + do { + c = 0; + while (c < 0x80) { + if (offset >= string_size) + b = string2[offset - string_size]; + else { + if (offset >= MAX_STRING_SIZE) + b = string3[offset - MAX_STRING_SIZE]; + else + b = string[offset]; + } + if (b & mask) + c = decode_table[0x80 + c]; + else + c = decode_table[c]; + mask <<= 1; + if (!mask) { + mask = 1; + offset++; + } + } + c &= 0x7f; + if (c && ((c != 0x40) || (lastchar != 0x20))) + char_out(c); + } while (c && ((c != 0x40) || (lastchar != 0x20))); + cflag = c ? 0xff : 0; + if (c) { + offset_bak = offset; + mask_bak = mask; + } +} + +void output_number(type16 number) { + type16 tens = number / 10; + + if (tens > 0) + ms_putchar('0' + tens); + number -= (tens * 10); + ms_putchar('0' + number); +} + +type16 output_text(const char *text) { + type16 i; + + for (i = 0; text[i] != 0; i++) + ms_putchar(text[i]); + return i; +} + +type16s hint_input(void) { + type8 c1, c2, c3; + + output_text(">>"); + ms_flush(); + + do { + c1 = ms_getchar(0); + } while (c1 == '\n'); + if (c1 == 1) + return -1; /* New game loaded, abort hints */ + + c2 = ms_getchar(0); + if (c2 == 1) + return -1; + + /* Get and discard any remaining characters */ + c3 = c2; + while (c3 != '\n') { + c3 = ms_getchar(0); + if (c3 == 1) + return -1; + } + ms_putchar('\n'); + + if ((c1 >= '0') && (c1 <= '9')) { + type16 number = c1 - '0'; + if ((c2 >= '0') && (c2 <= '9')) { + number *= 10; + number += c2 - '0'; + } + return number; + } + + if ((c1 >= 'A') && (c1 <= 'Z')) + c1 = 'a' + (c1 - 'A'); + if ((c1 >= 'a') && (c1 <= 'z')) { + switch (c1) { + case 'e': + return -2; /* End hints */ + case 'n': + return -3; /* Next hint */ + case 'p': + return -4; /* Show parent hint list */ + } + } + return 0; +} + +type16 show_hints_text(ms_hint *hintsData, type16 index) { + type16 i = 0, j = 0; + type16s input; + ms_hint *hint = hintsData + index; + + while (1) { + switch (hint->nodetype) { + + case 1: /* folders */ + output_text("Hint categories:\n"); + for (i = 0, j = 0; i < hint->elcount; i++) { + output_number(i + 1); + output_text(". "); + j += output_text(hint->content + j) + 1; + ms_putchar('\n'); + } + output_text("Enter hint category number, "); + if (hint->parent != 0xffff) + output_text("P for the parent hint menu, "); + output_text("or E to end hintsData.\n"); + + input = hint_input(); + switch (input) { + case -1: /* A new game is being loaded */ + return 1; + case -2: /* End hintsData */ + return 1; + case -4: /* Show parent hint list */ + if (hint->parent != 0xffff) + return 0; + default: + if ((input > 0) && (input <= hint->elcount)) { + if (show_hints_text(hintsData, hint->links[input - 1]) == 1) + return 1; + } + break; + } + break; + + case 2: /* hintsData */ + if (i < hint->elcount) { + output_number(i + 1); + output_text(". "); + j += output_text(hint->content + j) + 1; + + if (i == hint->elcount - 1) { + output_text("\nNo more hintsData.\n"); + return 0; /* Last hint */ + } else { + output_text("\nEnter N for the next hint, "); + output_text("P for the parent hint menu, "); + output_text("or E to end hintsData.\n"); + } + + input = hint_input(); + switch (input) { + case -1: /* A new game is being loaded */ + return 1; + case -2: /* End hintsData */ + return 1; + case -3: + i++; + break; + case -4: /* Show parent hint list */ + return 0; + } + } else + return 0; + break; + } + } + return 0; +} + +void do_line_a(void) { + type8 l1c; + char *str; + type16 ptr, ptr2, tmp16, dtype; + type32 a1reg, tmp32; + +#ifdef LOGGFX + /* + if (((byte2-0xdd) == 4) || ((byte2-0xdd) == 13)) + out2("--> %d\n",byte2-0xdd); + else + out2("LINE A %d\n",byte2-0xdd); + */ +#endif + if ((byte2 < 0xdd) || (version < 4 && byte2 < 0xe4) || (version < 2 && byte2 < 0xed)) { + ms_flush(); /* flush output-buffer */ + rand_emu(); /* Increase game randomness */ + l1c = ms_getchar(1); /* 0 means UNDO */ + if (l1c == 1) + return; + if (l1c) + write_reg(1, 2, l1c); /* d1=getkey() */ + else { + if ((l1c = ms_undo()) != 0) + output_text(undo_ok); + else + output_text(undo_fail); + if (!l1c) + write_reg(1, 2, '\n'); + } + } else + switch (byte2 - 0xdd) { + + case 0: /* A0DD - Won't probably be needed at all */ + break; + + case 1: /* A0DE */ + write_reg(1, 0, 1); /* Should remove the manual check */ + break; + + case 2: /* A0DF */ + a1reg = (type32)read_reg(9, 2); + dtype = (code + a1reg + 2)[0]; + + switch (dtype) { + case 7: /* Picture */ +#ifdef LOGGFX + out2("PICTURE IS %s\n", code + a1reg + 3); +#endif + /* gfx mode = normal, df is not called if graphics are off */ + ms_showpic(a1reg + 3, 2); + break; + + case 10: /* Open window commands */ + switch ((code + a1reg + 3)[0]) { + case 4: /* Help/Hints */ + if (hints != 0) { + if (ms_showhints(hints) == 0) + show_hints_text(hints, 0); + } else + output_text(no_hints); + break; + case 0: /* Carried items */ + case 1: /* Room items */ + case 2: /* Map */ + case 3: /* Compass */ + output_text(not_supported); + break; + } + break; + + case 13: /* Music */ + switch ((code + a1reg + 3)[0]) { + case 0: /* stop music */ + ms_playmusic(0, 0, 0); + break; + default: /* play music */ +#ifdef LOGSND + out2("MUSIC IS %s\n", code + a1reg + 3); +#endif + { + type32 length = 0; + type16 tempo = 0; + type8 *midi = sound_extract((const char *)(code + a1reg + 3), &length, &tempo); + if (midi != NULL) + ms_playmusic(midi, length, tempo); + } + break; + } + break; + } + break; + + case 3: /* A0E0 */ + /* printf("A0E0 stubbed\n"); */ + break; + + case 4: /* A0E1 Read from keyboard to (A1), status in D1 (0 for ok) */ + ms_flush(); + rand_emu(); + tmp32 = read_reg(8 + 1, 2); + str = (char *)effective(tmp32); + tmp16 = 0; + do { + if (!(l1c = ms_getchar(1))) { + if ((l1c = ms_undo()) != 0) + output_text(undo_ok); + else + output_text(undo_fail); + if (!l1c) { + tmp16 = 0; + str[tmp16++] = '\n'; + l1c = '\n'; + output_text("\n>"); + } else { + ms_putchar('\n'); + return; + } + } else { + if (l1c == 1) + return; + str[tmp16++] = l1c; +#ifdef LOGGFX + out2("%c", l1c); +#endif + } + } while (l1c != '\n' && tmp16 < 256); + write_reg(8 + 1, 2, tmp32 + tmp16 - 1); + if (tmp16 != 256 && tmp16 != 1) + write_reg(1, 1, 0); + else + write_reg(1, 1, 1); + break; + + case 5: /* A0E2 */ + /* printf("A0E2 stubbed\n"); */ + break; + + case 6: /* A0E3 */ + if (read_reg(1, 2) == 0) { + if ((version < 4) || (read_reg(6, 2) == 0)) + ms_showpic(0, 0); + } + /* printf("\nMoves: %u\n",read_reg(0,1)); */ + break; + + case 7: /* A0E4 sp+=4, RTS */ + write_reg(8 + 7, 1, read_reg(8 + 7, 1) + 4); + pc = pop(); + break; + + case 8: /* A0E5 set z, RTS */ + case 9: /* A0E6 clear z, RTS */ + pc = pop(); + zflag = (byte2 == 0xe5) ? 0xff : 0; + break; + + case 10: /* A0E7 set z */ + zflag = 0xff; + break; + + case 11: /* A0E8 clear z */ + zflag = 0; + break; + + case 12: /* A0E9 [3083 - j] */ + ptr = (type16)read_reg(8 + 0, 1); + ptr2 = (type16)read_reg(8 + 1, 1); + do { + l1c = dict[ptr2++]; + effective(ptr++)[0] = l1c; + } while ((l1c & 0x80) == 0); + write_reg(8 + 0, 1, ptr); + write_reg(8 + 1, 1, ptr2); + break; + + case 13: /* A0EA A1=write_dictword(A1,D1=output_mode) */ + ptr = (type16)read_reg(8 + 1, 1); + tmp32 = read_reg(3, 0); + write_reg(3, 0, read_reg(1, 0)); + do { + l1c = dict[ptr++]; + char_out(l1c); + } while (l1c < 0x80); + write_reg(8 + 1, 1, ptr); + write_reg(3, 0, tmp32); + break; + + case 14: /* A0EB [3037 - j] */ + dict[read_reg(8 + 1, 1)] = (type8)read_reg(1, 0); + break; + + case 15: /* A0EC */ + write_reg(1, 0, dict[read_reg(8 + 1, 1)]); + break; + + case 16: + ms_stop(); /* infinite loop A0ED */ + break; + case 17: + if (!ms_init(nullptr, nullptr, nullptr, nullptr)) + ms_stop(); /* restart game ie. pc, sp etc. A0EE */ + break; + case 18: /* printer A0EF */ + break; + case 19: + ms_showpic(read_reg(0, 0), (type8)read_reg(1, 0)); /* Do_picture(D0) A0F0 */ + break; + case 20: + ptr = (type16)read_reg(8 + 1, 1); /* A1=nth_string(A1,D0) A0F1 */ + tmp32 = read_reg(0, 1); + while (tmp32-- > 0) { + while (effective(ptr++)[0]); + } + write_reg(8 + 1, 1, ptr); + break; + + case 21: /* [2a43] A0F2 */ + cflag = 0; + write_reg(0, 1, read_reg(2, 1)); + do_findprop(); + ptr = (type16)read_reg(8 + 0, 1); + while (read_reg(2, 1) > 0) { + if (read_w(effective(ptr + 12)) & 0x3fff) { + cflag = 0xff; + break; + } + if (read_reg(2, 1) == (read_reg(4, 1) & 0x7fff)) { + cflag = 0xff; + break; + } + ptr -= 0x0e; + write_reg(2, 1, read_reg(2, 1) - 1); + } + break; + + case 22: + char_out((type8)read_reg(1, 0)); /* A0F3 */ + break; + + case 23: /* D7=Save_(filename A0) D1 bytes starting from A1 A0F4 */ + str = (version < 4) ? (char *)effective(read_reg(8 + 0, 1)) : nullptr; + write_reg(7, 0, ms_save_file(str, effective(read_reg(8 + 1, 1)), + (type16)read_reg(1, 1))); + break; + + case 24: /* D7=Load_(filename A0) D1 bytes starting from A1 A0F5 */ + str = (version < 4) ? (char *)effective(read_reg(8 + 0, 1)) : nullptr; + write_reg(7, 0, ms_load_file(str, effective(read_reg(8 + 1, 1)), + (type16)read_reg(1, 1))); + break; + + case 25: /* D1=Random(0..D1-1) [3748] A0F6 */ + l1c = (type8)read_reg(1, 0); + write_reg(1, 1, rand_emu() % (l1c ? l1c : 1)); + break; + + case 26: /* D0=Random(0..255) [3742] A0F7 */ + tmp16 = (type16)rand_emu(); + write_reg(0, 0, tmp16 + (tmp16 >> 8)); + break; + + case 27: /* write string [D0] [2999] A0F8 */ + write_string(); + break; + + case 28: /* Z,D0=Get_inventory_item(D0) [2a9e] A0F9 */ + zflag = 0; + ptr = (type16)read_reg(0, 1); + do { + write_reg(0, 1, ptr); + do { + do_findprop(); + ptr2 = (type16)read_reg(8 + 0, 1); /* object properties */ + if ((effective(ptr2)[5]) & 1) + break; /* is_described or so */ + l1c = effective(ptr2)[6]; /* some_flags */ + tmp16 = read_w(effective(ptr2 + 8)); /* parent_object */ + if (!l1c) { + /* ordinary object? */ + if (!tmp16) + zflag = 0xff; /* return if parent()=player */ + break; /* otherwise try next */ + } + if (l1c & 0xcc) + break; /* skip worn, bodypart, room, hidden */ + if (tmp16 == 0) { + /* return if parent()=player? */ + zflag = 0xff; + break; + } + write_reg(0, 1, tmp16); /* else look at parent() */ + } while (1); + ptr--; + } while ((!zflag) && ptr); + write_reg(0, 1, ptr + 1); + break; + + case 29: /* [2b18] A0FA */ + ptr = (type16)read_reg(8, 1); + do { + if (read_reg(5, 0)) { + l1c = ((type32)((read_w(effective(ptr)) & 0x3fff)) == read_reg(2, 1)); + } else { + l1c = (effective(ptr)[0] == read_reg(2, 0)); + } + if (read_reg(3, 1) == read_reg(4, 1)) { + cflag = 0; + write_reg(8, 1, ptr); + } else { + write_reg(3, 1, read_reg(3, 1) + 1); + ptr += 0x0e; + if (l1c) { + cflag = 0xff; + write_reg(8, 1, ptr); + } + } + } while ((!l1c) && (read_reg(3, 1) != read_reg(4, 1))); + break; + + case 30: /* [2bd1] A0FB */ + ptr = (type16)read_reg(8 + 1, 1); + do { + if (dict) + while (dict[ptr++] < 0x80); + else + while (effective(ptr++)[0] < 0x80); + write_reg(2, 1, read_reg(2, 1) - 1); + } while (read_reg(2, 1)); + write_reg(8 + 1, 1, ptr); + break; + + case 31: /* [2c3b] A0FC */ + ptr = (type16)read_reg(8 + 0, 1); + ptr2 = (type16)read_reg(8 + 1, 1); + do { + if (dict) + while (dict[ptr++] < 0x80); + else + while (effective(ptr++)[0] < 0x80); + while (effective(ptr2++)[0]); + write_reg(0, 1, read_reg(0, 1) - 1); + } while (read_reg(0, 1)); + write_reg(8 + 0, 1, ptr); + write_reg(8 + 1, 1, ptr2); + break; + + case 32: /* Set properties pointer from A0 [2b7b] A0FD */ + properties = (type16)read_reg(8 + 0, 1); + if (version > 0) + fl_sub = (type16)read_reg(8 + 3, 1); + if (version > 1) { + fl_tab = (type16)read_reg(8 + 5, 1); + fl_size = (type16)read_reg(7, 1) + 1; + /* A3 [routine], A5 [table] and D7 [table-size] */ + } + if (version > 2) { + fp_tab = (type16)read_reg(8 + 6, 1); + fp_size = (type16)read_reg(6, 1); + } + break; + + case 33: /* A0FE */ + do_findprop(); + break; + + case 34: /* Dictionary_lookup A0FF */ + dict_lookup(); + break; + } +} + +/* emulate an instruction [1b7e] */ + +type8 ms_rungame(void) { + type8 l1c; + type16 ptr; + type32 tmp32; +#ifdef LOGEMU + static int stat = 0; +#endif + + if (!running) + return running; + if (pc == undo_pc) + save_undo(); + +#ifdef LOGEMU + if (pc == 0x0000) + stat = 0; + if (stat) { + log_status(); + fflush(dbg_log); + } + + fprintf(dbg_log, "%.8X: ", pc); +#endif + i_count++; + read_word(); + switch (byte1 >> 1) { + + /* 00-0F */ + case 0x00: + if (byte1 == 0x00) { + if (byte2 == 0x3c || byte2 == 0x7c) { + /* OR immediate to CCR (30D9) */ + read_word(); +#ifdef LOGEMU + out("or_ccr #%.2x", byte2); +#endif + if (byte2 & 0x01) + cflag = 0xff; + if (byte2 & 0x02) + vflag = 0xff; + if (byte2 & 0x04) + zflag = 0xff; + if (byte2 & 0x08) + nflag = 0xff; + } else { + /* OR [27df] */ +#ifdef LOGEMU + out("or"); +#endif + get_arg(); + do_or(); + } + } else + check_btst(); + break; + + case 0x01: + if (byte1 == 0x02) { + if (byte2 == 0x3c || byte2 == 0x7c) { + /* AND immediate to CCR */ + read_word(); +#ifdef LOGEMU + out("and_ccr #%.2x", byte2); +#endif + if (!(byte2 & 0x01)) + cflag = 0; + if (!(byte2 & 0x02)) + vflag = 0; + if (!(byte2 & 0x04)) + zflag = 0; + if (!(byte2 & 0x08)) + nflag = 0; + } else { + /* AND */ +#ifdef LOGEMU + out("and"); +#endif + get_arg(); + do_and(); + } + } else + check_btst(); + break; + + case 0x02: + if (byte1 == 0x04) { + /* SUB */ +#ifdef LOGEMU + out("sub"); +#endif + get_arg(); + do_sub(0); + } else + check_btst(); + break; + + case 0x03: + if (byte1 == 0x06) { + /* ADD */ +#ifdef LOGEMU + out("addi"); +#endif + get_arg(); + do_add(0); + } else + check_btst(); + break; + + case 0x04: + if (byte1 == 0x08) { + /* bit operations (immediate) */ + set_info((type8)(byte2 & 0x3f)); + l1c = (effective(pc))[1]; + pc += 2; + set_arg1(); + do_bop(byte2, l1c); + } else + check_btst(); + break; + + case 0x05: + if (byte1 == 0x0a) { + if (byte2 == 0x3c || byte2 == 0x7c) { + /* EOR immediate to CCR */ + read_word(); +#ifdef LOGEMU + out("eor_ccr #%.2X", byte2); +#endif + if (byte2 & 0x01) + cflag ^= 0xff; + if (byte2 & 0x02) + vflag ^= 0xff; + if (byte2 & 0x04) + zflag ^= 0xff; + if (byte2 & 0x08) + nflag ^= 0xff; + } else { + /* EOR */ +#ifdef LOGEMU + out("eor"); +#endif + get_arg(); + do_eor(); + } + } else + check_btst(); + break; + + case 0x06: + if (byte1 == 0x0c) { + /* CMP */ +#ifdef LOGEMU + out("cmp"); +#endif + get_arg(); + do_cmp(); + } else + check_btst(); + break; + + case 0x07: + check_btst(); + break; + + /* 10-1F [3327] MOVE.B */ + case 0x08: + case 0x09: + case 0x0a: + case 0x0b: + case 0x0c: + case 0x0d: + case 0x0e: + case 0x0f: + +#ifdef LOGEMU + out("move.b"); +#endif + set_info((type8)(byte2 & 0x3f)); + set_arg1(); + swap_args(); + l1c = (byte1 >> 1 & 0x07) | (byte2 >> 3 & 0x18) | (byte1 << 5 & 0x20); + set_info(l1c); + set_arg1(); + do_move(); + break; + + /* 20-2F [32d1] MOVE.L */ + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + +#ifdef LOGEMU + out("move.l"); +#endif + set_info((type8)((byte2 & 0x3f) | 0x80)); + set_arg1(); + swap_args(); + l1c = (byte1 >> 1 & 0x07) | (byte2 >> 3 & 0x18) | (byte1 << 5 & 0x20); + set_info((type8)(l1c | 0x80)); + set_arg1(); + do_move(); + break; + + /* 30-3F [3327] MOVE.W */ + case 0x18: + case 0x19: + case 0x1a: + case 0x1b: + case 0x1c: + case 0x1d: + case 0x1e: + case 0x1f: + +#ifdef LOGEMU + out("move.w"); +#endif + set_info((type8)((byte2 & 0x3f) | 0x40)); + set_arg1(); + swap_args(); + l1c = (byte1 >> 1 & 0x07) | (byte2 >> 3 & 0x18) | (byte1 << 5 & 0x20); + set_info((type8)(l1c | 0x40)); + set_arg1(); + do_move(); + break; + + /* 40-4F various commands */ + + case 0x20: + if (byte1 == 0x40) { + /* [31d5] */ + ms_fatal("unimplemented instructions NEGX and MOVE SR,xx"); + } else + check_lea(); + break; + + case 0x21: + if (byte1 == 0x42) { + /* [3188] */ + if ((byte2 & 0xc0) == 0xc0) { + ms_fatal("unimplemented instruction MOVE CCR,xx"); + } else { + /* CLR */ +#ifdef LOGEMU + out("clr"); +#endif + set_info(byte2); + set_arg1(); + if (opsize == 0) + arg1[0] = 0; + if (opsize == 1) + write_w(arg1, 0); + if (opsize == 2) + write_l(arg1, 0); + nflag = cflag = 0; + zflag = 0xff; + } + } else + check_lea(); + break; + + case 0x22: + if (byte1 == 0x44) { + /* [31a0] */ + if ((byte2 & 0xc0) == 0xc0) { + /* MOVE to CCR */ +#ifdef LOGEMU + out("move_ccr"); +#endif + zflag = nflag = cflag = vflag = 0; + set_info((type8)(byte2 & 0x7f)); + set_arg1(); + byte2 = arg1[1]; + if (byte2 & 0x01) + cflag = 0xff; + if (byte2 & 0x02) + vflag = 0xff; + if (byte2 & 0x04) + zflag = 0xff; + if (byte2 & 0x08) + nflag = 0xff; + } else { +#ifdef LOGEMU + out("neg"); +#endif + set_info(byte2); /* NEG */ + set_arg1(); + cflag = 0xff; + if (opsize == 0) { + arg1[0] = (-arg1[0]); + cflag = arg1[0] ? 0xff : 0; + } + if (opsize == 1) { + write_w(arg1, (type16)(-1 * read_w(arg1))); + cflag = read_w(arg1) ? 0xff : 0; + } + if (opsize == 2) { + write_l(arg1, -1 * read_l(arg1)); + cflag = read_l(arg1) ? 0xff : 0; + } + vflag = 0; + set_flags(); + } + } else + check_lea(); + break; + + case 0x23: + if (byte1 == 0x46) { + if ((byte2 & 0xc0) == 0xc0) { + ms_fatal("unimplemented instruction MOVE xx,SR"); + } else { +#ifdef LOGEMU + out("not"); +#endif + set_info(byte2); /* NOT */ + set_arg1(); + tmparg[0] = tmparg[1] = tmparg[2] = tmparg[3] = 0xff; + arg2 = tmparg; + do_eor(); + } + } else + check_lea(); + break; + + case 0x24: + if (byte1 == 0x48) { + if ((byte2 & 0xf8) == 0x40) { +#ifdef LOGEMU + out("swap"); +#endif + opsize = 2; /* SWAP */ + admode = 0; + regnr = byte2 & 0x07; + set_arg1(); + tmp32 = read_w(arg1); + write_w(arg1, read_w(arg1 + 2)); + write_w(arg1 + 2, (type16)tmp32); + set_flags(); + } else if ((byte2 & 0xf8) == 0x80) { +#ifdef LOGEMU + out("ext.w"); +#endif + opsize = 1; /* EXT.W */ + admode = 0; + regnr = byte2 & 0x07; + set_arg1(); + if (arg1[1] > 0x7f) + arg1[0] = 0xff; + else + arg1[0] = 0; + set_flags(); + } else if ((byte2 & 0xf8) == 0xc0) { +#ifdef LOGEMU + out("ext.l"); +#endif + opsize = 2; /* EXT.L */ + admode = 0; + regnr = byte2 & 0x07; + set_arg1(); + if (read_w(arg1 + 2) > 0x7fff) + write_w(arg1, 0xffff); + else + write_w(arg1, 0); + set_flags(); + } else if ((byte2 & 0xc0) == 0x40) { +#ifdef LOGEMU + out("pea"); +#endif + set_info((type8)((byte2 & 0x3f) | 0x80)); /* PEA */ + set_arg1(); + if (is_reversible) + push(arg1i); + else + ms_fatal("illegal addressing mode for PEA"); + } else { + check_movem(); /* MOVEM */ + } + } else + check_lea(); + break; + + case 0x25: + if (byte1 == 0x4a) { + /* [3219] TST */ + if ((byte2 & 0xc0) == 0xc0) { + ms_fatal("unimplemented instruction TAS"); + } else { +#ifdef LOGEMU + out("tst"); +#endif + set_info(byte2); + set_arg1(); + cflag = vflag = 0; + set_flags(); + } + } else + check_lea(); + break; + + case 0x26: + if (byte1 == 0x4c) + check_movem2(); /* [3350] MOVEM.L (Ax)+,A/Dx */ + else + check_lea(); /* LEA */ + break; + + case 0x27: + if (byte1 == 0x4e) { + /* [3290] */ + if (byte2 == 0x75) { + /* RTS */ +#ifdef LOGEMU + out("rts\n"); +#endif + pc = pop(); + } else if (byte2 == 0x71) { + /* NOP */ +#ifdef LOGEMU + out("nop"); +#endif + } else if ((byte2 & 0xc0) == 0xc0) { + /* indir JMP */ +#ifdef LOGEMU + out("jmp"); +#endif + set_info((type8)(byte2 | 0xc0)); + set_arg1(); + if (is_reversible) + pc = arg1i; + else + ms_fatal("illegal addressing mode for JMP"); + } else if ((byte2 & 0xc0) == 0x80) { +#ifdef LOGEMU + out("jsr"); +#endif + set_info((type8)(byte2 | 0xc0)); /* indir JSR */ + set_arg1(); + push(pc); + if (is_reversible) + pc = arg1i; + else + ms_fatal("illegal addressing mode for JSR"); + } else { + ms_fatal("unimplemented instructions 0x4EXX"); + } + } else + check_lea(); /* LEA */ + break; + + /* 50-5F [2ed5] ADDQ/SUBQ/Scc/DBcc */ + case 0x28: + case 0x29: + case 0x2a: + case 0x2b: + case 0x2c: + case 0x2d: + case 0x2e: + case 0x2f: + + if ((byte2 & 0xc0) == 0xc0) { + set_info((type8)(byte2 & 0x3f)); + set_arg1(); + if (admode == 1) { + /* DBcc */ +#ifdef LOGEMU + out("dbcc"); +#endif + if (condition(byte1) == 0) { + arg1 = (arg1 - (type8 *) areg) + (type8 *) dreg - 1; + write_w(arg1, (type16)(read_w(arg1) - 1)); + if (read_w(arg1) != 0xffff) + branch(0); + else + pc += 2; + } else + pc += 2; + } else { + /* Scc */ +#ifdef LOGEMU + out("scc"); +#endif + arg1[0] = condition(byte1) ? 0xff : 0; + } + } else { + set_info(byte2); + set_arg1(); + quick_flag = (admode == 1) ? 0xff : 0; + l1c = byte1 >> 1 & 0x07; + tmparg[0] = tmparg[1] = tmparg[2] = 0; + tmparg[3] = l1c ? l1c : 8; + arg2 = reg_align(tmparg, opsize); + { +#ifdef LOGEMU + type32s outnum = 0; + switch (opsize) { + case 0: + outnum = (type8s) arg2[0]; + break; + case 1: + outnum = (type16s) read_w(arg2); + break; + case 2: + outnum = (type32s) read_l(arg2); + break; + } +#endif + if ((byte1 & 0x01) == 1) { +#ifdef LOGEMU + out("subq #%.8X", outnum); +#endif + do_sub(0); /* SUBQ */ + } else { +#ifdef LOGEMU + out("addq #%.8X", outnum); +#endif + do_add(0); /* ADDQ */ + } + } + } + break; + + /* 60-6F [26ba] Bcc */ + + case 0x30: + if (byte1 == 0x61) { + /* BRA, BSR */ +#ifdef LOGEMU + out("bsr"); +#endif + if (byte2 == 0) + push(pc + 2); + else + push(pc); + } +#ifdef LOGEMU + else + out("bra"); +#endif + if ((byte1 == 0x60) && (byte2 == 0xfe)) { + ms_flush(); /* flush stdout */ + ms_stop(); /* infinite loop - just exit */ + } + branch(byte2); + break; + + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + + if (condition(byte1) == 0) { +#ifdef LOGEMU + out("beq.s"); +#endif + if (byte2 == 0) + pc += 2; + } else { +#ifdef LOGEMU + out("bra"); +#endif + branch(byte2); + } + break; + + /* 70-7F [260a] MOVEQ */ + case 0x38: + case 0x39: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + +#ifdef LOGEMU + out("moveq"); +#endif + arg1 = (type8 *) & dreg[byte1 >> 1 & 0x07]; + if (byte2 > 127) + nflag = arg1[0] = arg1[1] = arg1[2] = 0xff; + else + nflag = arg1[0] = arg1[1] = arg1[2] = 0; + arg1[3] = byte2; + zflag = byte2 ? 0 : 0xff; + break; + + /* 80-8F [2f36] */ + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + + if ((byte2 & 0xc0) == 0xc0) { + ms_fatal("unimplemented instructions DIVS and DIVU"); + } else if (((byte2 & 0xf0) == 0) && ((byte1 & 0x01) != 0)) { + ms_fatal("unimplemented instruction SBCD"); + } else { +#ifdef LOGEMU + out("or?"); +#endif + set_info(byte2); + set_arg1(); + set_arg2(1, byte1); + if ((byte1 & 0x01) == 0) + swap_args(); + do_or(); + } + break; + + /* 90-9F [3005] SUB */ + case 0x48: + case 0x49: + case 0x4a: + case 0x4b: + case 0x4c: + case 0x4d: + case 0x4e: + case 0x4f: + +#ifdef LOGEMU + out("sub"); +#endif + quick_flag = 0; + if ((byte2 & 0xc0) == 0xc0) { + if ((byte1 & 0x01) == 1) + set_info((type8)(byte2 & 0xbf)); + else + set_info((type8)(byte2 & 0x7f)); + set_arg1(); + set_arg2_nosize(0, byte1); + swap_args(); + do_sub(1); + } else { + set_info(byte2); + set_arg1(); + set_arg2(1, byte1); + if ((byte1 & 0x01) == 0) + swap_args(); + do_sub(0); + } + break; + + /* A0-AF various special commands [LINE_A] */ + + case 0x50: + case 0x56: + case 0x57: /* [2521] */ + do_line_a(); +#ifdef LOGEMU + out("LINE_A A0%.2X", byte2); +#endif + break; + + case 0x51: +#ifdef LOGEMU + out("rts\n"); +#endif + pc = pop(); /* RTS */ + break; + + case 0x52: +#ifdef LOGEMU + out("bsr"); +#endif + if (byte2 == 0) + push(pc + 2); /* BSR */ + else + push(pc); + branch(byte2); + break; + + case 0x53: + if ((byte2 & 0xc0) == 0xc0) { + /* TST [321d] */ + ms_fatal("unimplemented instructions LINE_A #$6C0-#$6FF"); + } else { +#ifdef LOGEMU + out("tst"); +#endif + set_info(byte2); + set_arg1(); + cflag = vflag = 0; + set_flags(); + } + break; + + case 0x54: + check_movem(); + break; + + case 0x55: + check_movem2(); + break; + + /* B0-BF [2fe4] */ + case 0x58: + case 0x59: + case 0x5a: + case 0x5b: + case 0x5c: + case 0x5d: + case 0x5e: + case 0x5f: + + if ((byte2 & 0xc0) == 0xc0) { +#ifdef LOGEMU + out("cmp"); +#endif + if ((byte1 & 0x01) == 1) + set_info((type8)(byte2 & 0xbf)); + else + set_info((type8)(byte2 & 0x7f)); + set_arg1(); + set_arg2(0, byte1); + swap_args(); + do_cmp(); /* CMP */ + } else { + if ((byte1 & 0x01) == 0) { +#ifdef LOGEMU + out("cmp"); +#endif + set_info(byte2); + set_arg1(); + set_arg2(1, byte1); + swap_args(); + do_cmp(); /* CMP */ + } else { +#ifdef LOGEMU + out("eor"); +#endif + set_info(byte2); + set_arg1(); + set_arg2(1, byte1); + do_eor(); /* EOR */ + } + } + break; + + /* C0-CF [2f52] EXG, AND */ + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + + if ((byte1 & 0x01) == 0) { + if ((byte2 & 0xc0) == 0xc0) { + ms_fatal("unimplemented instruction MULU"); + } else { + /* AND */ +#ifdef LOGEMU + out("and"); +#endif + set_info(byte2); + set_arg1(); + set_arg2(1, byte1); + if ((byte1 & 0x01) == 0) + swap_args(); + do_and(); + } + } else { + if ((byte2 & 0xf8) == 0x40) { +#ifdef LOGEMU + out("exg (dx)"); +#endif + opsize = 2; /* EXG Dx,Dx */ + set_arg2(1, (type8)(byte2 << 1)); + swap_args(); + set_arg2(1, byte1); + tmp32 = read_l(arg1); + write_l(arg1, read_l(arg2)); + write_l(arg2, tmp32); + } else if ((byte2 & 0xf8) == 0x48) { + opsize = 2; /* EXG Ax,Ax */ +#ifdef LOGEMU + out("exg (ax)"); +#endif + set_arg2(0, (type8)(byte2 << 1)); + swap_args(); + set_arg2(0, byte1); + tmp32 = read_l(arg1); + write_l(arg1, read_l(arg2)); + write_l(arg2, tmp32); + } else if ((byte2 & 0xf8) == 0x88) { + opsize = 2; /* EXG Dx,Ax */ +#ifdef LOGEMU + out("exg (dx,ax)"); +#endif + set_arg2(0, (type8)(byte2 << 1)); + swap_args(); + set_arg2(1, byte1); + tmp32 = read_l(arg1); + write_l(arg1, read_l(arg2)); + write_l(arg2, tmp32); + } else { + if ((byte2 & 0xc0) == 0xc0) { + ms_fatal("unimplemented instruction MULS"); + } else { + set_info(byte2); + set_arg1(); + set_arg2(1, byte1); + if ((byte1 & 0x01) == 0) + swap_args(); + do_and(); + } + } + } + break; + + /* D0-DF [2fc8] ADD */ + case 0x68: + case 0x69: + case 0x6a: + case 0x6b: + case 0x6c: + case 0x6d: + case 0x6e: + case 0x6f: + +#ifdef LOGEMU + out("add"); +#endif + quick_flag = 0; + if ((byte2 & 0xc0) == 0xc0) { + if ((byte1 & 0x01) == 1) + set_info((type8)(byte2 & 0xbf)); + else + set_info((type8)(byte2 & 0x7f)); + set_arg1(); + set_arg2_nosize(0, byte1); + swap_args(); + do_add(1); + } else { + set_info(byte2); + set_arg1(); + set_arg2(1, byte1); + if ((byte1 & 0x01) == 0) + swap_args(); + do_add(0); + } + break; + + /* E0-EF [3479] LSR ASL ROR ROL */ + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + +#ifdef LOGEMU + out("lsr,asl,ror,rol"); +#endif + if ((byte2 & 0xc0) == 0xc0) { + set_info((type8)(byte2 & 0xbf)); /* OP Dx */ + set_arg1(); + l1c = 1; /* steps=1 */ + byte2 = (byte1 >> 1) & 0x03; + } else { + set_info((type8)(byte2 & 0xc7)); + set_arg1(); + if ((byte2 & 0x20) == 0) { + /* immediate */ + l1c = (byte1 >> 1) & 0x07; + if (l1c == 0) + l1c = 8; + } else { + l1c = (type8)read_reg(byte1 >> 1 & 0x07, 0); + } + byte2 = (byte2 >> 3) & 0x03; + } + if ((byte1 & 0x01) == 0) { + /* right */ + while (l1c-- > 0) { + if (opsize == 0) { + cflag = arg1[0] & 0x01 ? 0xff : 0; + arg1[0] >>= 1; + if (cflag && (byte2 == 3)) + arg1[0] |= 0x80; + } + if (opsize == 1) { + cflag = read_w(arg1) & 0x01 ? 0xff : 0; + write_w(arg1, (type16)(read_w(arg1) >> 1)); + if (cflag && (byte2 == 3)) + write_w(arg1, (type16)(read_w(arg1) | ((type16) 1 << 15))); + } + if (opsize == 2) { + cflag = read_l(arg1) & 0x01 ? 0xff : 0; + write_l(arg1, read_l(arg1) >> 1); + if (cflag && (byte2 == 3)) + write_l(arg1, read_l(arg1) | ((type32) 1 << 31)); + } + } + } else { + /* left */ + while (l1c-- > 0) { + if (opsize == 0) { + cflag = arg1[0] & 0x80 ? 0xff : 0; /* [3527] */ + arg1[0] <<= 1; + if (cflag && (byte2 == 3)) + arg1[0] |= 0x01; + } + if (opsize == 1) { + cflag = read_w(arg1) & ((type16) 1 << 15) ? 0xff : 0; + write_w(arg1, (type16)(read_w(arg1) << 1)); + if (cflag && (byte2 == 3)) + write_w(arg1, (type16)(read_w(arg1) | 0x01)); + } + if (opsize == 2) { + cflag = read_l(arg1) & ((type32) 1 << 31) ? 0xff : 0; + write_l(arg1, read_l(arg1) << 1); + if (cflag && (byte2 == 3)) + write_l(arg1, read_l(arg1) | 0x01); + } + } + } + set_flags(); + break; + + /* F0-FF [24f3] LINE_F */ + case 0x78: + case 0x79: + case 0x7a: + case 0x7b: + case 0x7c: + case 0x7d: + case 0x7e: + case 0x7f: + + if (version == 0) { + /* hardcoded jump */ + char_out(l1c = (type8)read_reg(1, 0)); + } else if (version == 1) { + /* single programmable shortcut */ + push(pc); + pc = fl_sub; + } else { + /* programmable shortcuts from table */ +#ifdef LOGEMU + out("LINK: %.2X,%.2X", byte1, byte2); +#endif + ptr = (byte1 & 7) << 8 | byte2; + if (ptr >= fl_size) { + if ((byte1 & 8) == 0) + push(pc); + ptr = byte1 << 8 | byte2 | 0x0800; + ptr = fl_tab + 2 * (ptr ^ 0xffff); + pc = (type32) ptr + (type16s) read_w(effective(ptr)); + } else { + push(pc); + pc = fl_sub; + } + } + break; + + default: + ms_fatal("Constants aren't and variables don't"); + break; + } +#ifdef LOGEMU + fprintf(dbg_log, "\n"); +#endif + return running; } } // End of namespace Magnetic diff --git a/engines/glk/magnetic/glk.cpp b/engines/glk/magnetic/glk.cpp new file mode 100644 index 0000000000..db15cd136c --- /dev/null +++ b/engines/glk/magnetic/glk.cpp @@ -0,0 +1,5393 @@ +/* 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/magnetic/defs.h" +#include "glk/magnetic/magnetic.h" + +namespace Glk { +namespace Magnetic { + +/* + * True and false definitions -- usually defined in glkstart.h, but we need + * them early, so we'll define them here too. We also need NULL, but that's + * normally from stdio.h or one of it's cousins. + */ +#ifndef FALSE +# define FALSE false +#endif +#ifndef TRUE +# define TRUE false +#endif + +#define BYTE_MAX 255 +#define CHAR_BIT 8 +#define UINT16_MAX 0xffff +#define INT32_MAX 0x7fffffff + +/*---------------------------------------------------------------------*/ +/* Module variables, miscellaneous other stuff */ +/*---------------------------------------------------------------------*/ + +/* Glk Magnetic Scrolls port version number. */ +static const glui32 GMS_PORT_VERSION = 0x00010601; + +/* + * We use a maximum of five Glk windows, one for status, one for pictures, + * two for hints, and one for everything else. The status and pictures + * windows may be NULL, depending on user selections and the capabilities + * of the Glk library. The hints windows will normally be NULL, except + * when in the hints subsystem. + */ +static winid_t gms_main_window = NULL, + gms_status_window = NULL, + gms_graphics_window = NULL, + gms_hint_menu_window = NULL, + gms_hint_text_window = NULL; + +/* + * Transcript stream and input log. These are NULL if there is no current + * collection of these strings. + */ +static strid_t gms_transcript_stream = NULL, + gms_inputlog_stream = NULL; + +/* Input read log stream, for reading back an input log. */ +static strid_t gms_readlog_stream = NULL; + +/* Note about whether graphics is possible, or not. */ +static int gms_graphics_possible = TRUE; + +/* Options that may be turned off or set by command line flags. */ +static int gms_graphics_enabled = TRUE; +enum GammaMode { + GAMMA_OFF, GAMMA_NORMAL, GAMMA_HIGH +}; +static GammaMode gms_gamma_mode = GAMMA_NORMAL; +static int gms_animation_enabled = TRUE, + gms_prompt_enabled = TRUE, + gms_abbreviations_enabled = TRUE, + gms_commands_enabled = TRUE; + +/* Magnetic Scrolls standard input prompt string. */ +static const char *const GMS_INPUT_PROMPT = ">"; + +/* Forward declaration of event wait function. */ +static void gms_event_wait(glui32 wait_type, event_t *event); + + +/*---------------------------------------------------------------------*/ +/* Glk port utility functions */ +/*---------------------------------------------------------------------*/ + +/* + * gms_fatal() + * + * Fatal error handler. The function returns, expecting the caller to + * abort() or otherwise handle the error. + */ +static void gms_fatal(const char *string) { + /* + * If the failure happens too early for us to have a window, print + * the message to stderr. + */ + if (!gms_main_window) + error("\n\nINTERNAL ERROR: %s", string); + + /* Cancel all possible pending window input events. */ + g_vm->glk_cancel_line_event(gms_main_window, NULL); + g_vm->glk_cancel_char_event(gms_main_window); + if (gms_hint_menu_window) { + g_vm->glk_cancel_char_event(gms_hint_menu_window); + g_vm->glk_window_close(gms_hint_menu_window, NULL); + } + if (gms_hint_text_window) { + g_vm->glk_cancel_char_event(gms_hint_text_window); + g_vm->glk_window_close(gms_hint_text_window, NULL); + } + + /* Print a message indicating the error. */ + g_vm->glk_set_window(gms_main_window); + g_vm->glk_set_style(style_Normal); + g_vm->glk_put_string("\n\nINTERNAL ERROR: "); + g_vm->glk_put_string(string); + + g_vm->glk_put_string("\n\nPlease record the details of this error, try to" + " note down everything you did to cause it, and email" + " this information to simon_baldwin@yahoo.com.\n\n"); +} + + +/* + * gms_malloc() + * gms_realloc() + * + * Non-failing malloc and realloc; call gms_fatal and exit if memory + * allocation fails. + */ +static void *gms_malloc(size_t size) { + void *pointer; + + pointer = malloc(size); + if (!pointer) { + gms_fatal("GLK: Out of system memory"); + g_vm->glk_exit(); + } + + return pointer; +} + +static void *gms_realloc(void *ptr, size_t size) { + void *pointer; + + pointer = realloc(ptr, size); + if (!pointer) { + gms_fatal("GLK: Out of system memory"); + g_vm->glk_exit(); + } + + return pointer; +} + + +/* + * gms_strncasecmp() + * gms_strcasecmp() + * + * Strncasecmp and strcasecmp are not ANSI functions, so here are local + * definitions to do the same jobs. + */ +static int gms_strncasecmp(const char *s1, const char *s2, size_t n) { + size_t index; + + for (index = 0; index < n; index++) { + int diff; + + diff = g_vm->glk_char_to_lower(s1[index]) - g_vm->glk_char_to_lower(s2[index]); + if (diff < 0 || diff > 0) + return diff < 0 ? -1 : 1; + } + + return 0; +} + +static int gms_strcasecmp(const char *s1, const char *s2) { + size_t s1len, s2len; + int result; + + s1len = strlen(s1); + s2len = strlen(s2); + + result = gms_strncasecmp(s1, s2, s1len < s2len ? s1len : s2len); + if (result < 0 || result > 0) + return result; + else + return s1len < s2len ? -1 : s1len > s2len ? 1 : 0; +} + +/*---------------------------------------------------------------------*/ +/* Glk port CRC functions */ +/*---------------------------------------------------------------------*/ + +/* CRC table initialization polynomial. */ +static const glui32 GMS_CRC_POLYNOMIAL = 0xedb88320; + + +/* + * gms_get_buffer_crc() + * + * Return the CRC of the bytes in buffer[0..length-1]. + * + * This algorithm is taken from the PNG specification, version 1.0. + */ +static glui32 gms_get_buffer_crc(const void *void_buffer, size_t length) { + static int is_initialized = FALSE; + static glui32 crc_table[BYTE_MAX + 1]; + + const char *buffer = (const char *) void_buffer; + glui32 crc; + size_t index; + + /* Build the static CRC lookup table on first call. */ + if (!is_initialized) { + for (index = 0; index < BYTE_MAX + 1; index++) { + int bit; + + crc = (glui32) index; + for (bit = 0; bit < CHAR_BIT; bit++) + crc = crc & 1 ? GMS_CRC_POLYNOMIAL ^ (crc >> 1) : crc >> 1; + + crc_table[index] = crc; + } + + is_initialized = TRUE; + + /* CRC lookup table self-test, after is_initialized set -- recursion. */ + assert(gms_get_buffer_crc("123456789", 9) == 0xcbf43926); + } + + /* + * Start with all ones in the crc, then update using table entries. Xor + * with all ones again, finally, before returning. + */ + crc = 0xffffffff; + for (index = 0; index < length; index++) + crc = crc_table[(crc ^ buffer[index]) & BYTE_MAX] ^ (crc >> CHAR_BIT); + return crc ^ 0xffffffff; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port game identification data and identification functions */ +/*---------------------------------------------------------------------*/ + +/* + * The game's name, suitable for printing out on a status line, or other + * location where game information is relevant. Set on game startup, by + * identifying the game from its text file header. + */ +static const char *gms_gameid_game_name = NULL; + + +/* + * The following game database is built from Generic/games.txt, and is used + * to identify the game being run. Magnetic Scrolls games don't generally + * supply a status line, so this data can be used instead. + */ +struct gms_game_table_t { + const type32 undo_size; /* Header word at offset 0x22 */ + const type32 undo_pc; /* Header word at offset 0x26 */ + const char *const name; /* Game title and platform */ +}; +typedef const gms_game_table_t *gms_game_tableref_t; + +static const gms_game_table_t GMS_GAME_TABLE[] = { + {0x2100, 0x427e, "Corruption v1.11 (Amstrad CPC)"}, + {0x2100, 0x43a0, "Corruption v1.11 (Archimedes)"}, + {0x2100, 0x43a0, "Corruption v1.11 (DOS)"}, + {0x2100, 0x4336, "Corruption v1.11 (Commodore 64)"}, + {0x2100, 0x4222, "Corruption v1.11 (Spectrum +3)"}, + {0x2100, 0x4350, "Corruption v1.12 (Archimedes)"}, + {0x2500, 0x6624, "Corruption v1.12 (DOS, Magnetic Windows)"}, + + {0x2300, 0x3fa0, "Fish v1.02 (DOS)"}, + {0x2400, 0x4364, "Fish v1.03 (Spectrum +3)"}, + {0x2300, 0x3f72, "Fish v1.07 (Commodore 64)"}, + {0x2200, 0x3f9c, "Fish v1.08 (Archimedes)"}, + {0x2a00, 0x583a, "Fish v1.10 (DOS, Magnetic Windows)"}, + + {0x5000, 0x6c30, "Guild v1.0 (Amstrad CPC)"}, + {0x5000, 0x6cac, "Guild v1.0 (Commodore 64)"}, + {0x5000, 0x6d5c, "Guild v1.1 (DOS)"}, + {0x3300, 0x698a, "Guild v1.3 (Archimedes)"}, + {0x3200, 0x6772, "Guild v1.3 (Spectrum +3)"}, + {0x3400, 0x6528, "Guild v1.3 (DOS, Magnetic Windows)"}, + + {0x2b00, 0x488c, "Jinxter v1.05 (Commodore 64)"}, + {0x2c00, 0x4a08, "Jinxter v1.05 (DOS)"}, + {0x2c00, 0x487a, "Jinxter v1.05 (Spectrum +3)"}, + {0x2c00, 0x4a56, "Jinxter v1.10 (DOS)"}, + {0x2b00, 0x4924, "Jinxter v1.22 (Amstrad CPC)"}, + {0x2c00, 0x4960, "Jinxter v1.30 (Archimedes)"}, + + {0x1600, 0x3940, "Myth v1.0 (Commodore 64)"}, + {0x1500, 0x3a0a, "Myth v1.0 (DOS)"}, + + {0x3600, 0x42cc, "Pawn v2.3 (Amstrad CPC)"}, + {0x3600, 0x4420, "Pawn v2.3 (Archimedes)"}, + {0x3600, 0x3fb0, "Pawn v2.3 (Commodore 64)"}, + {0x3600, 0x4420, "Pawn v2.3 (DOS)"}, + {0x3900, 0x42e4, "Pawn v2.3 (Spectrum 128)"}, + {0x3900, 0x42f4, "Pawn v2.4 (Spectrum +3)"}, + + {0x3900, 0x75f2, "Wonderland v1.21 (DOS, Magnetic Windows)"}, + {0x3900, 0x75f8, "Wonderland v1.27 (Archimedes)"}, + {0, 0, NULL} +}; + + +/* + * gms_gameid_lookup_game() + * + * Look up and return the game table entry given a game's undo size and + * undo pc values. Returns the entry, or NULL if not found. + */ +static gms_game_tableref_t gms_gameid_lookup_game(type32 undo_size, type32 undo_pc) { + gms_game_tableref_t game; + + for (game = GMS_GAME_TABLE; game->name; game++) { + if (game->undo_size == undo_size && game->undo_pc == undo_pc) + break; + } + + return game->name ? game : NULL; +} + + +/* + * gms_gameid_read_uint32() + * + * Endian-safe unsigned 32 bit integer read from game text file. Returns + * 0 on error, a known unused table value. + */ +static type32 gms_gameid_read_uint32(int offset, Common::SeekableReadStream *stream) { + if (!stream->seek(offset)) + return 0; + return stream->readUint32LE(); +} + + +/* + * gms_gameid_identify_game() + * + * Identify a game from its text file header, and cache the game's name for + * later queries. Sets the cache to NULL if not found. + */ +static void gms_gameid_identify_game(const char *text_file) { + Common::File stream; + + if (stream.open(text_file)) { + type32 undo_size, undo_pc; + gms_game_tableref_t game; + + /* Read the game's signature undo size and undo pc values. */ + undo_size = gms_gameid_read_uint32(0x22, &stream); + undo_pc = gms_gameid_read_uint32(0x26, &stream); + stream.close(); + + /* Search for these values in the table, and set game name if found. */ + game = gms_gameid_lookup_game(undo_size, undo_pc); + gms_gameid_game_name = game ? game->name : NULL; + } else { + gms_gameid_game_name = NULL; + } +} + + +/* + * gms_gameid_get_game_name() + * + * Return the name of the game, or NULL if not identifiable. + */ +static const char *gms_gameid_get_game_name(void) { + return gms_gameid_game_name; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port picture functions */ +/*---------------------------------------------------------------------*/ + +/* + * Color conversions lookup tables, and a word about gamma corrections. + * + * When uncorrected, some game pictures can look dark (Corruption, Won- + * derland), whereas others look just fine (Guild Of Thieves, Jinxter). + * + * The standard general-purpose gamma correction is around 2.1, with + * specific values, normally, of 2.5-2.7 for IBM PC systems, and 1.8 for + * Macintosh. However, applying even the low end of this range can make + * some pictures look washed out, yet improve others nicely. + * + * To try to solve this, here we'll set up a precalculated table with + * discrete gamma values. On displaying a picture, we'll try to find a + * gamma correction that seems to offer a reasonable level of contrast + * for the picture. + * + * Here's an AWK script to create the gamma table: + * + * BEGIN { max=255.0; step=max/7.0 + * for (gamma=0.9; gamma<=2.7; gamma+=0.05) { + * printf " {\"%2.2f\", {0, ", gamma + * for (i=1; i<8; i++) { + * printf "%3.0f", (((step*i / max) ^ (1.0/gamma)) * max) + * printf "%s", (i<7) ? ", " : "" + * } + * printf "}, " + * printf "%s },\n", (gamma>0.99 && gamma<1.01) ? "FALSE" : "TRUE " + * } } + * + */ +typedef const struct { + const char *const level; /* Gamma correction level. */ + const unsigned char table[8]; /* Color lookup table. */ + const int is_corrected; /* Flag if non-linear. */ +} gms_gamma_t; +typedef gms_gamma_t *gms_gammaref_t; + +static gms_gamma_t GMS_GAMMA_TABLE[] = { + {"0.90", {0, 29, 63, 99, 137, 175, 215, 255}, TRUE}, + {"0.95", {0, 33, 68, 105, 141, 179, 217, 255}, TRUE}, + {"1.00", {0, 36, 73, 109, 146, 182, 219, 255}, FALSE}, + {"1.05", {0, 40, 77, 114, 150, 185, 220, 255}, TRUE}, + {"1.10", {0, 43, 82, 118, 153, 188, 222, 255}, TRUE}, + {"1.15", {0, 47, 86, 122, 157, 190, 223, 255}, TRUE}, + {"1.20", {0, 50, 90, 126, 160, 193, 224, 255}, TRUE}, + {"1.25", {0, 54, 94, 129, 163, 195, 225, 255}, TRUE}, + {"1.30", {0, 57, 97, 133, 166, 197, 226, 255}, TRUE}, + {"1.35", {0, 60, 101, 136, 168, 199, 227, 255}, TRUE}, + {"1.40", {0, 64, 104, 139, 171, 201, 228, 255}, TRUE}, + {"1.45", {0, 67, 107, 142, 173, 202, 229, 255}, TRUE}, + {"1.50", {0, 70, 111, 145, 176, 204, 230, 255}, TRUE}, + {"1.55", {0, 73, 114, 148, 178, 205, 231, 255}, TRUE}, + {"1.60", {0, 76, 117, 150, 180, 207, 232, 255}, TRUE}, + {"1.65", {0, 78, 119, 153, 182, 208, 232, 255}, TRUE}, + {"1.70", {0, 81, 122, 155, 183, 209, 233, 255}, TRUE}, + {"1.75", {0, 84, 125, 157, 185, 210, 233, 255}, TRUE}, + {"1.80", {0, 87, 127, 159, 187, 212, 234, 255}, TRUE}, + {"1.85", {0, 89, 130, 161, 188, 213, 235, 255}, TRUE}, + {"1.90", {0, 92, 132, 163, 190, 214, 235, 255}, TRUE}, + {"1.95", {0, 94, 134, 165, 191, 215, 236, 255}, TRUE}, + {"2.00", {0, 96, 136, 167, 193, 216, 236, 255}, TRUE}, + {"2.05", {0, 99, 138, 169, 194, 216, 237, 255}, TRUE}, + {"2.10", {0, 101, 140, 170, 195, 217, 237, 255}, TRUE}, + {"2.15", {0, 103, 142, 172, 197, 218, 237, 255}, TRUE}, + {"2.20", {0, 105, 144, 173, 198, 219, 238, 255}, TRUE}, + {"2.25", {0, 107, 146, 175, 199, 220, 238, 255}, TRUE}, + {"2.30", {0, 109, 148, 176, 200, 220, 238, 255}, TRUE}, + {"2.35", {0, 111, 150, 178, 201, 221, 239, 255}, TRUE}, + {"2.40", {0, 113, 151, 179, 202, 222, 239, 255}, TRUE}, + {"2.45", {0, 115, 153, 180, 203, 222, 239, 255}, TRUE}, + {"2.50", {0, 117, 154, 182, 204, 223, 240, 255}, TRUE}, + {"2.55", {0, 119, 156, 183, 205, 223, 240, 255}, TRUE}, + {"2.60", {0, 121, 158, 184, 206, 224, 240, 255}, TRUE}, + {"2.65", {0, 122, 159, 185, 206, 225, 241, 255}, TRUE}, + {"2.70", {0, 124, 160, 186, 207, 225, 241, 255}, TRUE}, + {NULL, {0, 0, 0, 0, 0, 0, 0, 0}, FALSE} +}; + +/* R,G,B color triple definition. */ +typedef struct { + int red, green, blue; +} gms_rgb_t; +typedef gms_rgb_t *gms_rgbref_t; + +/* + * Weighting values for calculating the luminance of a color. There are + * two commonly used sets of values for these -- 299,587,114, taken from + * NTSC (Never The Same Color) 1953 standards, and 212,716,72, which is the + * set that modern CRTs tend to match. The NTSC ones seem to give the best + * subjective results. + */ +static const gms_rgb_t GMS_LUMINANCE_WEIGHTS = { 299, 587, 114 }; + +/* + * Maximum number of regions to consider in a single repaint pass. A + * couple of hundred seems to strike the right balance between not too + * sluggardly picture updates, and responsiveness to input during graphics + * rendering, when combined with short timeouts. + */ +static const int GMS_REPAINT_LIMIT = 256; + +/* + * Graphics timeout; we like an update call after this period (ms). In + * practice, this timeout may actually be shorter than the time taken + * to reach the limit on repaint regions, but because Glk guarantees that + * user interactions (in this case, line events) take precedence over + * timeouts, this should be okay; we'll still see a game that responds to + * input each time the background repaint function yields. + * + * Setting this value is tricky. We'd like it to be the shortest possible + * consistent with getting other stuff done, say 10ms. However, Xglk has + * a granularity of 50ms on checking for timeouts, as it uses a 1/20s + * timeout on X select. This means that the shortest timeout we'll ever + * get from Xglk will be 50ms, so there's no point in setting this shorter + * than that. With luck, other Glk libraries will be more efficient than + * this, and can give us higher timer resolution; we'll set 50ms here, and + * hope that no other Glk library is worse. + */ +static const glui32 GMS_GRAPHICS_TIMEOUT = 50; + +/* + * Count of timeouts to wait in between animation paints, and to wait on + * repaint request. Waiting for 2 timeouts of around 50ms, gets us to the + * 100ms recommended animation frame rate. Waiting after a repaint smooths + * the display where the frame is being resized, by helping to avoid + * graphics output while more resize events are received; around 1/2 second + * seems okay. + */ +static const int GMS_GRAPHICS_ANIMATION_WAIT = 2, + GMS_GRAPHICS_REPAINT_WAIT = 10; + +/* Pixel size multiplier for image size scaling. */ +static const int GMS_GRAPHICS_PIXEL = 2; + +/* Proportion of the display to use for graphics. */ +static const glui32 GMS_GRAPHICS_PROPORTION = 60; + +/* + * Border and shading control. For cases where we can't detect the back- + * ground color of the main window, there's a default, white, background. + * Bordering is black, with a 1 pixel border, 2 pixel shading, and 8 steps + * of shading fade. + */ +static const glui32 GMS_GRAPHICS_DEFAULT_BACKGROUND = 0x00ffffff, + GMS_GRAPHICS_BORDER_COLOR = 0x00000000; +static const int GMS_GRAPHICS_BORDER = 1, + GMS_GRAPHICS_SHADING = 2, + GMS_GRAPHICS_SHADE_STEPS = 8; + +/* + * Guaranteed unused pixel value. This value is used to fill the on-screen + * buffer on new pictures or repaints, resulting in a full paint of all + * pixels since no off-screen, real picture, pixel will match it. + */ +static const int GMS_GRAPHICS_UNUSED_PIXEL = 0xff; + +/* + * The current picture bitmap being displayed, its width, height, palette, + * animation flag, and picture id. + */ +enum { GMS_PALETTE_SIZE = 16 }; +static type8 *gms_graphics_bitmap = NULL; +static type16 gms_graphics_width = 0, + gms_graphics_height = 0, + gms_graphics_palette[GMS_PALETTE_SIZE]; /* = { 0, ... }; */ +static bool gms_graphics_animated = FALSE; +static type32 gms_graphics_picture = 0; + +/* + * Flags set on new picture, and on resize or arrange events, and a flag + * to indicate whether background repaint is stopped or active. + */ +static bool gms_graphics_new_picture = FALSE, + gms_graphics_repaint = FALSE, + gms_graphics_active = FALSE; + +/* Flag to try to monitor the state of interpreter graphics. */ +static bool gms_graphics_interpreter = FALSE; + +/* + * Pointer to the two graphics buffers, one the off-screen representation + * of pixels, and the other tracking on-screen data. These are temporary + * graphics malloc'ed memory, and should be free'd on exit. + */ +static type8 *gms_graphics_off_screen = NULL, + *gms_graphics_on_screen = NULL; + +/* + * Pointer to the current active gamma table entry. Because of the way + * it's queried, this may not be NULL, otherwise we risk a race, with + * admittedly a very low probability, with the updater. So, it's init- + * ialized instead to the gamma table. The real value in use is inserted + * on the first picture update timeout call for a new picture. + */ +static gms_gammaref_t gms_graphics_current_gamma = GMS_GAMMA_TABLE; + +/* + * The number of colors used in the palette by the current picture. This + * value is also at risk of a race with the updater, so it too has a mild + * lie for a default value. + */ +static int gms_graphics_color_count = GMS_PALETTE_SIZE; + + +/* + * gms_graphics_open() + * + * If it's not open, open the graphics window. Returns TRUE if graphics + * was successfully started, or already on. + */ +static int gms_graphics_open(void) { + if (!gms_graphics_window) { + gms_graphics_window = g_vm->glk_window_open(gms_main_window, + winmethod_Above + | winmethod_Proportional, + GMS_GRAPHICS_PROPORTION, + wintype_Graphics, 0); + } + + return gms_graphics_window != NULL; +} + + +/* + * gms_graphics_close() + * + * If open, close the graphics window and set back to NULL. + */ +static void gms_graphics_close(void) { + if (gms_graphics_window) { + g_vm->glk_window_close(gms_graphics_window, NULL); + gms_graphics_window = NULL; + } +} + + +/* + * gms_graphics_start() + * + * If graphics enabled, start any background picture update processing. + */ +static void gms_graphics_start(void) { + if (gms_graphics_enabled) { + /* If not running, start the updating "thread". */ + if (!gms_graphics_active) { + g_vm->glk_request_timer_events(GMS_GRAPHICS_TIMEOUT); + gms_graphics_active = TRUE; + } + } +} + + +/* + * gms_graphics_stop() + * + * Stop any background picture update processing. + */ +static void gms_graphics_stop(void) { + /* If running, stop the updating "thread". */ + if (gms_graphics_active) { + g_vm->glk_request_timer_events(0); + gms_graphics_active = FALSE; + } +} + + +/* + * gms_graphics_are_displayed() + * + * Return TRUE if graphics are currently being displayed, FALSE otherwise. + */ +static int gms_graphics_are_displayed(void) { + return gms_graphics_window != NULL; +} + + +/* + * gms_graphics_paint() + * + * Set up a complete repaint of the current picture in the graphics window. + * This function should be called on the appropriate Glk window resize and + * arrange events. + */ +static void gms_graphics_paint(void) { + if (gms_graphics_enabled && gms_graphics_are_displayed()) { + /* Set the repaint flag, and start graphics. */ + gms_graphics_repaint = TRUE; + gms_graphics_start(); + } +} + + +/* + * gms_graphics_restart() + * + * Restart graphics as if the current picture is a new picture. This + * function should be called whenever graphics is re-enabled after being + * disabled, on change of gamma color correction policy, and on change + * of animation policy. + */ +static void gms_graphics_restart(void) { + if (gms_graphics_enabled && gms_graphics_are_displayed()) { + /* + * If the picture is animated, we'll need to be able to re-get the + * first animation frame so that the picture can be treated as if + * it is a new one. So here, we'll try to re-extract the current + * picture to do this. Calling ms_extract() is safe because we + * don't get here unless graphics are displayed, and graphics aren't + * displayed until there's a valid picture loaded, and ms_showpic + * only loads a picture after it's called ms_extract and set the + * picture id into gms_graphics_picture. + * + * The bitmap and other picture stuff can be ignored because it's + * the precise same stuff as we already have in picture details + * variables. If the ms_extract() fails, we'll carry on regardless, + * which may, or may not, result in the ideal picture display. + * + * One or two non-animated pictures return NULL from ms_extract() + * being re-called, so we'll restrict calls to animations only. + * And just to be safe, we'll also call only if we're already + * holding a bitmap (and we should be; how else could the graphics + * animation flag be set?...). + */ + if (gms_graphics_animated && gms_graphics_bitmap) { + type8 animated; + type16 width, height, palette[GMS_PALETTE_SIZE]; + + /* Extract the bitmap into dummy variables. */ + (void)ms_extract(gms_graphics_picture, &width, &height, palette, &animated); + } + + /* Set the new picture flag, and start graphics. */ + gms_graphics_new_picture = TRUE; + gms_graphics_start(); + } +} + + +/* + * gms_graphics_count_colors() + * + * Analyze an image, and return the usage count of each palette color, and + * an overall count of how many colors out of the palette are used. NULL + * arguments indicate no interest in the return value. + */ +static void gms_graphics_count_colors(type8 bitmap[], type16 width, type16 height, + int *color_count, long color_usage[]) { + int x, y, count; + long usage[GMS_PALETTE_SIZE], index_row; + assert(bitmap); + + /* + * Traverse the image, counting each pixel usage. For the y iterator, + * maintain an index row as an optimization to avoid multiplications in + * the loop. + */ + count = 0; + memset(usage, 0, sizeof(usage)); + for (y = 0, index_row = 0; y < height; y++, index_row += width) { + for (x = 0; x < width; x++) { + long index; + + /* Get the pixel index, and update the count for this color. */ + index = index_row + x; + usage[bitmap[index]]++; + + /* If color usage is now 1, note new color encountered. */ + if (usage[bitmap[index]] == 1) + count++; + } + } + + if (color_count) + *color_count = count; + + if (color_usage) + memcpy(color_usage, usage, sizeof(usage)); +} + + +/* + * gms_graphics_game_to_rgb_color() + * gms_graphics_split_color() + * gms_graphics_combine_color() + * gms_graphics_color_luminance() + * + * General graphics helper functions, to convert between Magnetic Scrolls + * and RGB color representations, and between RGB and Glk glui32 color + * representations, and to calculate color luminance. + */ +static void +gms_graphics_game_to_rgb_color(type16 color, gms_gammaref_t gamma, + gms_rgbref_t rgb_color) { + assert(gamma && rgb_color); + + /* + * Convert Magnetic Scrolls color, through gamma, into RGB. This splits + * the color into components based on the 3-bits used in the game palette, + * and gamma-corrects and rescales each to the range 0-255, using the given + * correction. + */ + rgb_color->red = gamma->table[(color & 0x700) >> 8]; + rgb_color->green = gamma->table[(color & 0x070) >> 4]; + rgb_color->blue = gamma->table[(color & 0x007)]; +} + +static void gms_graphics_split_color(glui32 color, gms_rgbref_t rgb_color) { + assert(rgb_color); + + rgb_color->red = (color >> 16) & 0xff; + rgb_color->green = (color >> 8) & 0xff; + rgb_color->blue = color & 0xff; +} + +static glui32 gms_graphics_combine_color(gms_rgbref_t rgb_color) { + glui32 color; + assert(rgb_color); + + color = (rgb_color->red << 16) | (rgb_color->green << 8) | rgb_color->blue; + return color; +} + +static int gms_graphics_color_luminance(gms_rgbref_t rgb_color) { + static int is_initialized = FALSE; + static int weighting = 0; + + long luminance; + + /* On the first call, calculate the overall weighting. */ + if (!is_initialized) { + weighting = GMS_LUMINANCE_WEIGHTS.red + GMS_LUMINANCE_WEIGHTS.green + + GMS_LUMINANCE_WEIGHTS.blue; + + is_initialized = TRUE; + } + + /* Calculate the luminance and scale back by 1000 to 0-255 before return. */ + luminance = ((long) rgb_color->red * (long) GMS_LUMINANCE_WEIGHTS.red + + (long) rgb_color->green * (long) GMS_LUMINANCE_WEIGHTS.green + + (long) rgb_color->blue * (long) GMS_LUMINANCE_WEIGHTS.blue); + + assert(weighting > 0); + return (int)(luminance / weighting); +} + + +/* + * gms_graphics_compare_luminance() + * gms_graphics_constrast_variance() + * + * Calculate the contrast variance of the given palette and color usage, at + * the given gamma correction level. Helper functions for automatic gamma + * correction. + */ +static int gms_graphics_compare_luminance(const void *void_first, + const void *void_second) { + long first = *(const long *)void_first; + long second = *(const long *)void_second; + + return first > second ? 1 : second > first ? -1 : 0; +} + +static long gms_graphics_contrast_variance(type16 palette[], + long color_usage[], gms_gammaref_t gamma) { + int index, count, has_black, mean; + long sum; + int contrast[GMS_PALETTE_SIZE]; + int luminance[GMS_PALETTE_SIZE + 1]; /* Luminance for each color, + plus one extra for black */ + + /* Calculate the luminance energy of each palette color at this gamma. */ + has_black = FALSE; + for (index = 0, count = 0; index < GMS_PALETTE_SIZE; index++) { + if (color_usage[index] > 0) { + gms_rgb_t rgb_color; + + /* + * Convert the 16-bit base picture color to RGB using the gamma + * currently under consideration. Calculate luminance for this + * color and store in the next available luminance array entry. + */ + gms_graphics_game_to_rgb_color(palette[index], gamma, &rgb_color); + luminance[count++] = gms_graphics_color_luminance(&rgb_color); + + /* Note if black is present in the palette. */ + has_black |= luminance[count - 1] == 0; + } + } + + /* + * For best results, we want to anchor contrast calculations to black, so + * if black is not represented in the palette, add it as an extra luminance. + */ + if (!has_black) + luminance[count++] = 0; + + /* Sort luminance values so that the darkest color is at index 0. */ + qsort(luminance, count, + sizeof(*luminance), gms_graphics_compare_luminance); + + /* + * Calculate the difference in luminance between adjacent luminances in + * the sorted array, as contrast, and at the same time sum contrasts to + * calculate the mean. + */ + sum = 0; + for (index = 0; index < count - 1; index++) { + contrast[index] = luminance[index + 1] - luminance[index]; + sum += contrast[index]; + } + mean = sum / (count - 1); + + /* Calculate and return the variance in contrasts. */ + sum = 0; + for (index = 0; index < count - 1; index++) + sum += (contrast[index] - mean) * (contrast[index] - mean); + + return sum / (count - 1); +} + + +/* + * gms_graphics_equal_contrast_gamma() + * + * Try to find a gamma correction for the given palette and color usage that + * gives relatively equal contrast among the displayed colors. + * + * To do this, we search the gamma tables, computing color luminance for each + * color in the palette given this gamma. From luminances, we then compute + * the contrasts between the colors, and settle on the gamma correction that + * gives the most even and well-distributed picture contrast. We ignore + * colors not used in the palette. + * + * Note that we don't consider how often a palette color is used, only whether + * it's represented, or not. Some weighting might improve things, but the + * simple method seems to work adequately. In practice, as there are only 16 + * colors in a palette, most pictures use most colors in a relatively well + * distributed manner. This algorithm probably wouldn't work well on real + * photographs, though. + */ +static gms_gammaref_t gms_graphics_equal_contrast_gamma(type16 palette[], long color_usage[]) { + gms_gammaref_t gamma, result; + long lowest_variance; + assert(palette && color_usage); + + result = NULL; + lowest_variance = INT32_MAX; + + /* Search the gamma table for the entry with the lowest contrast variance. */ + for (gamma = GMS_GAMMA_TABLE; gamma->level; gamma++) { + long variance; + + /* Find the color contrast variance of the palette at this gamma. */ + variance = gms_graphics_contrast_variance(palette, color_usage, gamma); + + /* + * Compare the variance to the lowest so far, and if it is lower, note + * the gamma entry that produced it as being the current best found. + */ + if (variance < lowest_variance) { + result = gamma; + lowest_variance = variance; + } + } + + assert(result); + return result; +} + + +/* + * gms_graphics_select_gamma() + * + * Select a suitable gamma for the picture, based on the current gamma mode. + * + * The function returns either the linear gamma, a gamma value half way + * between linear and the gamma that gives the most even contrast, or just + * the gamma that gives the most even contrast. + * + * In the normal case, a value half way to the extreme case of making color + * contrast equal for all colors is, subjectively, a reasonable value to use. + * The problem cases are the darkest pictures, and selecting this value + * brightens them while at the same time not making them look overbright or + * too "sunny". + */ +static gms_gammaref_t gms_graphics_select_gamma(type8 bitmap[], + type16 width, type16 height, type16 palette[]) { + static int is_initialized = FALSE; + static gms_gammaref_t linear_gamma = NULL; + + long color_usage[GMS_PALETTE_SIZE]; + int color_count; + gms_gammaref_t contrast_gamma; + + /* On first call, find and cache the uncorrected gamma table entry. */ + if (!is_initialized) { + gms_gammaref_t gamma; + + for (gamma = GMS_GAMMA_TABLE; gamma->level; gamma++) { + if (!gamma->is_corrected) { + linear_gamma = gamma; + break; + } + } + + is_initialized = TRUE; + } + assert(linear_gamma); + + /* + * Check to see if automated correction is turned off; if it is, return + * the linear gamma. + */ + if (gms_gamma_mode == GAMMA_OFF) + return linear_gamma; + + /* + * Get the color usage and count of total colors represented. For a + * degenerate picture with one color or less, return the linear gamma. + */ + gms_graphics_count_colors(bitmap, width, height, &color_count, color_usage); + if (color_count <= 1) + return linear_gamma; + + /* + * Now calculate a gamma setting to give the most equal contrast across the + * picture colors. We'll return either half this gamma, or all of it. + */ + contrast_gamma = gms_graphics_equal_contrast_gamma(palette, color_usage); + + /* + * For normal automated correction, return a gamma value half way between + * the linear gamma and the equal contrast gamma. + */ + if (gms_gamma_mode == GAMMA_NORMAL) + return linear_gamma + (contrast_gamma - linear_gamma) / 2; + + /* Correction must be high; return the equal contrast gamma. */ + assert(gms_gamma_mode == GAMMA_HIGH); + return contrast_gamma; +} + + +/* + * gms_graphics_clear_and_border() + * + * Clear the graphics window, and border and shade the area where the + * picture is going to be rendered. This attempts a small raised effect + * for the picture, in keeping with modern trends. + */ +static void gms_graphics_clear_and_border(winid_t glk_window, + int x_offset, int y_offset, int pixel_size, type16 width, type16 height) { + glui32 background, fade_color, shading_color; + gms_rgb_t rgb_background, rgb_border, rgb_fade; + int index; + assert(glk_window); + + /* + * Try to detect the background color of the main window, by getting the + * background for Normal style (Glk offers no way to directly get a window's + * background color). If we can get it, we'll match the graphics window + * background to it. If we can't, we'll default the color to white. + */ + if (!g_vm->glk_style_measure(gms_main_window, + style_Normal, stylehint_BackColor, &background)) { + /* + * Unable to get the main window background, so assume, and default + * graphics to white. + */ + background = GMS_GRAPHICS_DEFAULT_BACKGROUND; + } + + /* + * Set the graphics window background to match the main window background, + * as best as we can tell, and clear the window. + */ + g_vm->glk_window_set_background_color(glk_window, background); + g_vm->glk_window_clear(glk_window); + + /* + * For very small pictures, just border them, but don't try and do any + * shading. Failing this check is probably highly unlikely. + */ + if (width < 2 * GMS_GRAPHICS_SHADE_STEPS + || height < 2 * GMS_GRAPHICS_SHADE_STEPS) { + /* Paint a rectangle bigger than the picture by border pixels. */ + g_vm->glk_window_fill_rect(glk_window, + GMS_GRAPHICS_BORDER_COLOR, + x_offset - GMS_GRAPHICS_BORDER, + y_offset - GMS_GRAPHICS_BORDER, + width * pixel_size + GMS_GRAPHICS_BORDER * 2, + height * pixel_size + GMS_GRAPHICS_BORDER * 2); + return; + } + + /* + * Paint a rectangle bigger than the picture by border pixels all round, + * and with additional shading pixels right and below. Some of these + * shading pixels are later overwritten by the fading loop below. The + * picture will sit over this rectangle. + */ + g_vm->glk_window_fill_rect(glk_window, + GMS_GRAPHICS_BORDER_COLOR, + x_offset - GMS_GRAPHICS_BORDER, + y_offset - GMS_GRAPHICS_BORDER, + width * pixel_size + GMS_GRAPHICS_BORDER * 2 + + GMS_GRAPHICS_SHADING, + height * pixel_size + GMS_GRAPHICS_BORDER * 2 + + GMS_GRAPHICS_SHADING); + + /* + * Split the main window background color and the border color into + * components. + */ + gms_graphics_split_color(background, &rgb_background); + gms_graphics_split_color(GMS_GRAPHICS_BORDER_COLOR, &rgb_border); + + /* + * Generate the incremental color to use in fade steps. Here we're + * assuming that the border is always darker than the main window + * background (currently valid, as we're using black). + */ + rgb_fade.red = (rgb_background.red - rgb_border.red) + / GMS_GRAPHICS_SHADE_STEPS; + rgb_fade.green = (rgb_background.green - rgb_border.green) + / GMS_GRAPHICS_SHADE_STEPS; + rgb_fade.blue = (rgb_background.blue - rgb_border.blue) + / GMS_GRAPHICS_SHADE_STEPS; + + /* Combine RGB fade into a single incremental Glk color. */ + fade_color = gms_graphics_combine_color(&rgb_fade); + + /* Fade in edge, from background to border, shading in stages. */ + shading_color = background; + for (index = 0; index < GMS_GRAPHICS_SHADE_STEPS; index++) { + /* Shade the two border areas with this color. */ + g_vm->glk_window_fill_rect(glk_window, shading_color, + x_offset + width * pixel_size + + GMS_GRAPHICS_BORDER, + y_offset + index - GMS_GRAPHICS_BORDER, + GMS_GRAPHICS_SHADING, 1); + g_vm->glk_window_fill_rect(glk_window, shading_color, + x_offset + index - GMS_GRAPHICS_BORDER, + y_offset + height * pixel_size + + GMS_GRAPHICS_BORDER, + 1, GMS_GRAPHICS_SHADING); + + /* Update the shading color for the fade next iteration. */ + shading_color -= fade_color; + } +} + + +/* + * gms_graphics_convert_palette() + * + * Convert a Magnetic Scrolls color palette to a Glk one, using the given + * gamma corrections. + */ +static void gms_graphics_convert_palette(type16 ms_palette[], gms_gammaref_t gamma, + glui32 glk_palette[]) { + int index; + assert(ms_palette && gamma && glk_palette); + + for (index = 0; index < GMS_PALETTE_SIZE; index++) { + gms_rgb_t rgb_color; + + /* + * Convert the 16-bit base picture color through gamma to a 32-bit + * RGB color, and combine into a Glk color and store in the Glk palette. + */ + gms_graphics_game_to_rgb_color(ms_palette[index], gamma, &rgb_color); + glk_palette[index] = gms_graphics_combine_color(&rgb_color); + } +} + + +/* + * gms_graphics_position_picture() + * + * Given a picture width and height, return the x and y offsets to center + * this picture in the current graphics window. + */ +static void gms_graphics_position_picture(winid_t glk_window, + int pixel_size, type16 width, type16 height, + int *x_offset, int *y_offset) { + glui32 window_width, window_height; + assert(glk_window && x_offset && y_offset); + + /* Measure the current graphics window dimensions. */ + g_vm->glk_window_get_size(glk_window, &window_width, &window_height); + + /* + * Calculate and return an x and y offset to use on point plotting, so that + * the image centers inside the graphical window. + */ + *x_offset = ((int) window_width - width * pixel_size) / 2; + *y_offset = ((int) window_height - height * pixel_size) / 2; +} + + +/* + * gms_graphics_apply_animation_frame() + * + * Apply a single animation frame to the given off-screen image buffer, using + * the frame bitmap, width, height and mask, the off-screen buffer, and the + * width and height of the main picture. + * + * Note that 'mask' may be NULL, implying that no frame pixel is transparent. + */ +static void gms_graphics_apply_animation_frame(type8 bitmap[], + type16 frame_width, type16 frame_height, + type8 mask[], int frame_x, int frame_y, + type8 off_screen[], type16 width, + type16 height) { + int mask_width, x, y; + type8 mask_hibit; + long frame_row, buffer_row, mask_row; + assert(bitmap && off_screen); + + /* + * It turns out that the mask isn't quite as described in defs.h, and thanks + * to Torbjorn Andersson and his Gtk port of Magnetic for illuminating this. + * The mask is made up of lines of 16-bit words, so the mask width is always + * even. Here we'll calculate the real width of a mask, and also set a high + * bit for later on. + */ + mask_width = (((frame_width - 1) / CHAR_BIT) + 2) & (~1); + mask_hibit = 1 << (CHAR_BIT - 1); + + /* + * Initialize row index components; these are optimizations to avoid the + * need for multiplications in the frame iteration loop. + */ + frame_row = 0; + buffer_row = frame_y * width; + mask_row = 0; + + /* + * Iterate over each frame row, clipping where y lies outside the main + * picture area. + */ + for (y = 0; y < frame_height; y++) { + /* Clip if y is outside the main picture area. */ + if (y + frame_y < 0 || y + frame_y >= height) { + /* Update optimization variables as if not clipped. */ + frame_row += frame_width; + buffer_row += width; + mask_row += mask_width; + continue; + } + + /* Iterate over each frame column, clipping again. */ + for (x = 0; x < frame_width; x++) { + long frame_index, buffer_index; + + /* Clip if x is outside the main picture area. */ + if (x + frame_x < 0 || x + frame_x >= width) + continue; + + /* + * If there's a mask, check the bit associated with this x,y, and + * ignore any transparent pixels. + */ + if (mask) { + type8 mask_byte; + + /* Isolate the mask byte, and test the transparency bit. */ + mask_byte = mask[mask_row + (x / CHAR_BIT)]; + if ((mask_byte & (mask_hibit >> (x % CHAR_BIT))) != 0) + continue; + } + + /* + * Calculate indexes for this pixel into the frame, and into the + * main off-screen buffer, and transfer the frame pixel into the + * off-screen buffer. + */ + frame_index = frame_row + x; + buffer_index = buffer_row + x + frame_x; + off_screen[buffer_index] = bitmap[frame_index]; + } + + /* Update row index components on change of y. */ + frame_row += frame_width; + buffer_row += width; + mask_row += mask_width; + } +} + + +/* + * gms_graphics_animate() + * + * This function finds and applies the next set of animation frames to the + * given off-screen image buffer. It's handed the width and height of the + * main picture, and the off-screen buffer. + * + * It returns FALSE if at the end of animations, TRUE if more animations + * remain. + */ +static int gms_graphics_animate(type8 off_screen[], type16 width, type16 height) { + struct ms_position *positions; + type16 count; + type8 status; + int frame; + assert(off_screen); + + /* Search for more animation frames, and return zero if none. */ + status = ms_animate(&positions, &count); + if (status == 0) + return FALSE; + + /* Apply each animation frame to the off-screen buffer. */ + for (frame = 0; frame < count; frame++) { + type8 *bitmap, *mask; + type16 frame_width, frame_height; + + /* + * Get the bitmap and other details for this frame. If we can't get + * this animation frame, skip it and see if any others are available. + */ + bitmap = ms_get_anim_frame(positions[frame].number, + &frame_width, &frame_height, &mask); + if (bitmap) { + gms_graphics_apply_animation_frame(bitmap, + frame_width, frame_height, mask, + positions[frame].x, + positions[frame].y, + off_screen, width, height); + } + } + + /* Return TRUE since more animation frames remain. */ + return TRUE; +} + +#ifndef GARGLK +/* + * gms_graphics_is_vertex() + * + * Given a point, return TRUE if that point is the vertex of a fillable + * region. This is a helper function for layering pictures. When assign- + * ing layers, we want to weight the colors that have the most complex + * shapes, or the largest count of isolated areas, heavier than simpler + * areas. + * + * By painting the colors with the largest number of isolated areas or + * the most complex shapes first, we help to minimize the number of fill + * regions needed to render the complete picture. + */ +static int gms_graphics_is_vertex(type8 off_screen[], type16 width, type16 height, + int x, int y) { + type8 pixel; + int above, below, left, right; + long index_row; + assert(off_screen); + + /* Use an index row to cut down on multiplications. */ + index_row = y * width; + + /* Find the color of the reference pixel. */ + pixel = off_screen[index_row + x]; + assert(pixel < GMS_PALETTE_SIZE); + + /* + * Detect differences between the reference pixel and its upper, lower, left + * and right neighbors. Mark as different if the neighbor doesn't exist, + * that is, at the edge of the picture. + */ + above = (y == 0 || off_screen[index_row - width + x] != pixel); + below = (y == height - 1 || off_screen[index_row + width + x] != pixel); + left = (x == 0 || off_screen[index_row + x - 1] != pixel); + right = (x == width - 1 || off_screen[index_row + x + 1] != pixel); + + /* + * Return TRUE if this pixel lies at the vertex of a rectangular, fillable, + * area. That is, if two adjacent neighbors aren't the same color (or if + * absent -- at the edge of the picture). + */ + return ((above || below) && (left || right)); +} + +/* + * gms_graphics_compare_layering_inverted() + * gms_graphics_assign_layers() + * + * Given two sets of image bitmaps, and a palette, this function will + * assign layers palette colors. + * + * Layers are assigned by first counting the number of vertices in the + * color plane, to get a measure of the complexity of shapes displayed in + * this color, and also the raw number of times each palette color is + * used. This is then sorted, so that layers are assigned to colors, with + * the lowest layer being the color with the most complex shapes, and + * within this (or where the count of vertices is zero, as it could be + * in some animation frames) the most used color. + * + * The function compares pixels in the two image bitmaps given, these + * being the off-screen and on-screen buffers, and generates counts only + * where these bitmaps differ. This ensures that only pixels not yet + * painted are included in layering. + * + * As well as assigning layers, this function returns a set of layer usage + * flags, to help the rendering loop to terminate as early as possible. + * + * By painting lower layers first, the paint can take in larger areas if + * it's permitted to include not-yet-validated higher levels. This helps + * minimize the amount of Glk areas fills needed to render a picture. + */ +typedef struct { + long complexity; /* Count of vertices for this color. */ + long usage; /* Color usage count. */ + int color; /* Color index into palette. */ +} gms_layering_t; + +static int gms_graphics_compare_layering_inverted(const void *void_first, + const void *void_second) { + gms_layering_t *first = (gms_layering_t *) void_first; + gms_layering_t *second = (gms_layering_t *) void_second; + + /* + * Order by complexity first, then by usage, putting largest first. Some + * colors may have no vertices at all when doing animation frames, but + * rendering optimization relies on the first layer that contains no areas + * to fill halting the rendering loop. So it's important here that we order + * indexes so that colors that render complex shapes come first, non-empty, + * but simpler shaped colors next, and finally all genuinely empty layers. + */ + return second->complexity > first->complexity ? 1 : + first->complexity > second->complexity ? -1 : + second->usage > first->usage ? 1 : + first->usage > second->usage ? -1 : 0; +} + +static void gms_graphics_assign_layers(type8 off_screen[], type8 on_screen[], + type16 width, type16 height, + int layers[], long layer_usage[]) { + int index, x, y; + long index_row; + gms_layering_t layering[GMS_PALETTE_SIZE]; + assert(off_screen && on_screen && layers && layer_usage); + + /* Clear initial complexity and usage counts, and set initial colors. */ + for (index = 0; index < GMS_PALETTE_SIZE; index++) { + layering[index].complexity = 0; + layering[index].usage = 0; + layering[index].color = index; + } + + /* + * Traverse the image, counting vertices and pixel usage where the pixels + * differ between the off-screen and on-screen buffers. Optimize by + * maintaining an index row to avoid multiplications. + */ + for (y = 0, index_row = 0; y < height; y++, index_row += width) { + for (x = 0; x < width; x++) { + long idx; + + /* + * Get the index for this pixel, and update complexity and usage + * if off-screen and on-screen pixels differ. + */ + idx = index_row + x; + if (on_screen[idx] != off_screen[idx]) { + if (gms_graphics_is_vertex(off_screen, width, height, x, y)) + layering[off_screen[idx]].complexity++; + + layering[off_screen[idx]].usage++; + } + } + } + + /* + * Sort counts to form color indexes. The primary sort is on the shape + * complexity, and within this, on color usage. + */ + qsort(layering, GMS_PALETTE_SIZE, + sizeof(*layering), gms_graphics_compare_layering_inverted); + + /* + * Assign a layer to each palette color, and also return the layer usage + * for each layer. + */ + for (index = 0; index < GMS_PALETTE_SIZE; index++) { + layers[layering[index].color] = index; + layer_usage[index] = layering[index].usage; + } +} + +/* + * gms_graphics_paint_region() + * + * This is a partially optimized point plot. Given a point in the graphics + * bitmap, it tries to extend the point to a color region, and fill a number + * of pixels in a single Glk rectangle fill. The goal here is to reduce the + * number of Glk rectangle fills, which tend to be extremely inefficient + * operations for generalized point plotting. + * + * The extension works in image layers; each palette color is assigned a + * layer, and we paint each layer individually, starting at the lowest. So, + * the region is free to fill any invalidated pixel in a higher layer, and + * all pixels, invalidated or already validated, in the same layer. In + * practice, it is good enough to look for either invalidated pixels or pixels + * in the same layer, and construct a region as large as possible from these, + * then on marking points as validated, mark only those in the same layer as + * the initial point. + * + * The optimization here is not the best possible, but is reasonable. What + * we do is to try and stretch the region horizontally first, then vertically. + * In practice, we might find larger areas by stretching vertically and then + * horizontally, or by stretching both dimensions at the same time. In + * mitigation, the number of colors in a picture is small (16), and the + * aspect ratio of pictures makes them generally wider than they are tall. + * + * Once we've found the region, we render it with a single Glk rectangle fill, + * and mark all the pixels in this region that match the layer of the initial + * given point as validated. + */ +static void gms_graphics_paint_region(winid_t glk_window, glui32 palette[], int layers[], + type8 off_screen[], type8 on_screen[], + int x, int y, int x_offset, int y_offset, + int pixel_size, type16 width, type16 height) { + type8 pixel; + int layer, x_min, x_max, y_min, y_max, x_index, y_index; + long index_row; + assert(glk_window && palette && layers && off_screen && on_screen); + + /* Find the color and layer for the initial pixel. */ + pixel = off_screen[y * width + x]; + layer = layers[pixel]; + assert(pixel < GMS_PALETTE_SIZE); + + /* + * Start by finding the extent to which we can pull the x coordinate and + * still find either invalidated pixels, or pixels in this layer. + * + * Use an index row to remove multiplications from the loops. + */ + index_row = y * width; + for (x_min = x; x_min - 1 >= 0; x_min--) { + long index = index_row + x_min - 1; + + if (on_screen[index] == off_screen[index] + && layers[off_screen[index]] != layer) + break; + } + for (x_max = x; x_max + 1 < width; x_max++) { + long index = index_row + x_max + 1; + + if (on_screen[index] == off_screen[index] + && layers[off_screen[index]] != layer) + break; + } + + /* + * Now try to stretch the height of the region, by extending the y + * coordinate as much as possible too. Again, we're looking for pixels + * that are invalidated or ones in the same layer. We need to check + * across the full width of the current region. + * + * As above, an index row removes multiplications from the loops. + */ + for (y_min = y, index_row = (y - 1) * width; + y_min - 1 >= 0; y_min--, index_row -= width) { + for (x_index = x_min; x_index <= x_max; x_index++) { + long index = index_row + x_index; + + if (on_screen[index] == off_screen[index] + && layers[off_screen[index]] != layer) + goto break_y_min; + } + } +break_y_min: + + for (y_max = y, index_row = (y + 1) * width; + y_max + 1 < height; y_max++, index_row += width) { + for (x_index = x_min; x_index <= x_max; x_index++) { + long index = index_row + x_index; + + if (on_screen[index] == off_screen[index] + && layers[off_screen[index]] != layer) + goto break_y_max; + } + } +break_y_max: + + /* Fill the region using Glk's rectangle fill. */ + g_vm->glk_window_fill_rect(glk_window, palette[pixel], + x_min * pixel_size + x_offset, + y_min * pixel_size + y_offset, + (x_max - x_min + 1) * pixel_size, + (y_max - y_min + 1) * pixel_size); + + /* + * Validate each pixel in the reference layer that was rendered by the + * rectangle fill. We don't validate pixels that are not in this layer + * (and are by definition in higher layers, as we've validated all lower + * layers), since although we colored them, we did it for optimization + * reasons, and they're not yet colored correctly. + * + * Maintain an index row as an optimization to avoid multiplication. + */ + index_row = y_min * width; + for (y_index = y_min; y_index <= y_max; y_index++) { + for (x_index = x_min; x_index <= x_max; x_index++) { + long index; + + /* + * Get the index for x_index,y_index. If the layers match, update + * the on-screen buffer. + */ + index = index_row + x_index; + if (layers[off_screen[index]] == layer) { + assert(off_screen[index] == pixel); + on_screen[index] = off_screen[index]; + } + } + + /* Update row index component on change of y. */ + index_row += width; + } +} +#endif + +static void gms_graphics_paint_everything(winid_t glk_window, + glui32 palette[], + type8 off_screen[], + int x_offset, int y_offset, + type16 width, type16 height) { + type8 pixel; /* Reference pixel color */ + int x, y; + + for (y = 0; y < height; y++) { + for (x = 0; x < width; x ++) { + pixel = off_screen[ y * width + x ]; + g_vm->glk_window_fill_rect(glk_window, + palette[ pixel ], + x * GMS_GRAPHICS_PIXEL + x_offset, + y * GMS_GRAPHICS_PIXEL + y_offset, + GMS_GRAPHICS_PIXEL, GMS_GRAPHICS_PIXEL); + } + } +} + +/* + * gms_graphics_timeout() + * + * This is a background function, called on Glk timeouts. Its job is to + * repaint some of the current graphics image. On successive calls, it + * does a part of the repaint, then yields to other processing. This is + * useful since the Glk primitive to plot points in graphical windows is + * extremely slow; this way, the repaint doesn't block game play. + * + * The function should be called on Glk timeout events. When the repaint + * is complete, the function will turn off Glk timers. + * + * The function uses double-buffering to track how much of the graphics + * buffer has been rendered. This helps to minimize the amount of point + * plots required, as only the differences between the two buffers need + * to be rendered. + */ +static void gms_graphics_timeout() { + static glui32 palette[GMS_PALETTE_SIZE]; /* Precomputed Glk palette */ +#ifndef GARGLK + static int layers[GMS_PALETTE_SIZE]; /* Assigned image layers */ + static long layer_usage[GMS_PALETTE_SIZE]; /* Image layer occupancies */ +#endif + + static int deferred_repaint = FALSE; /* Local delayed repaint flag */ + static int ignore_counter; /* Count of calls ignored */ + + static int x_offset, y_offset; /* Point plot offsets */ + static int yield_counter; /* Yields in rendering */ +#ifndef GARGLK + static int saved_layer; /* Saved current layer */ + static int saved_x, saved_y; /* Saved x,y coord */ + static int total_regions; /* Debug statistic */ +#endif + + type8 *on_screen; /* On-screen image buffer */ + type8 *off_screen; /* Off-screen image buffer */ + long picture_size; /* Picture size in pixels */ +// int layer; /* Image layer iterator */ +// int x, y; /* Image iterators */ +// int regions; /* Count of regions painted */ + + /* Ignore the call if the current graphics state is inactive. */ + if (!gms_graphics_active) + return; + assert(gms_graphics_window); + + /* + * On detecting a repaint request, note the flag in a local static variable, + * then set up a graphics delay to wait until, hopefully, the resize, if + * that's what caused it, is complete, and return. This makes resizing the + * window a lot smoother, since it prevents unnecessary region paints where + * we are receiving consecutive Glk arrange or redraw events. + */ + if (gms_graphics_repaint) { + deferred_repaint = TRUE; + gms_graphics_repaint = FALSE; + ignore_counter = GMS_GRAPHICS_REPAINT_WAIT - 1; + return; + } + + /* + * If asked to ignore a given number of calls, decrement the ignore counter + * and return having done nothing more. This lets us delay graphics + * operations by a number of timeouts, providing animation timing and + * partial protection from resize event "storms". + * + * Note -- to wait for N timeouts, set the count of timeouts to be ignored + * to N-1. + */ + assert(ignore_counter >= 0); + if (ignore_counter > 0) { + ignore_counter--; + return; + } + + /* Calculate the picture size, and synchronize screen buffer pointers. */ + picture_size = gms_graphics_width * gms_graphics_height; + off_screen = gms_graphics_off_screen; + on_screen = gms_graphics_on_screen; + + /* + * If we received a new picture, set up the local static variables for that + * picture -- decide on gamma correction, convert the color palette, and + * initialize the off_screen buffer to be the base picture. + */ + if (gms_graphics_new_picture) { + /* Initialize the off_screen buffer to be a copy of the base picture. */ + free(off_screen); + off_screen = (type8 *)gms_malloc(picture_size * sizeof(*off_screen)); + memcpy(off_screen, gms_graphics_bitmap, + picture_size * sizeof(*off_screen)); + + /* Note the buffer for freeing on cleanup. */ + gms_graphics_off_screen = off_screen; + + /* + * If the picture is animated, apply the first animation frames now. + * This is important, since they form an intrinsic part of the first + * displayed image (in type2 animation cases, perhaps _all_ of the + * first displayed image). + */ + if (gms_graphics_animated) { + gms_graphics_animate(off_screen, + gms_graphics_width, gms_graphics_height); + } + + /* + * Select a suitable gamma for the picture, taking care to use the + * off-screen buffer. + */ + gms_graphics_current_gamma = + gms_graphics_select_gamma(off_screen, + gms_graphics_width, + gms_graphics_height, + gms_graphics_palette); + + /* + * Pre-convert all the picture palette colors into their corresponding + * Glk colors. + */ + gms_graphics_convert_palette(gms_graphics_palette, + gms_graphics_current_gamma, palette); + + /* Save the color count for possible queries later. */ + gms_graphics_count_colors(off_screen, + gms_graphics_width, gms_graphics_height, + &gms_graphics_color_count, NULL); + } + + /* + * For a new picture, or a repaint of a prior one, calculate new values for + * the x and y offsets used to draw image points, and set the on-screen + * buffer to an unused pixel value, in effect invalidating all on-screen + * data. Also, reset the saved image scan coordinates so that we scan for + * unpainted pixels from top left starting at layer zero, and clear the + * graphics window. + */ + if (gms_graphics_new_picture || deferred_repaint) { + /* + * Calculate the x and y offset to center the picture in the graphics + * window. + */ + gms_graphics_position_picture(gms_graphics_window, + GMS_GRAPHICS_PIXEL, + gms_graphics_width, gms_graphics_height, + &x_offset, &y_offset); + + /* + * Reset all on-screen pixels to an unused value, guaranteed not to + * match any in a real picture. This forces all pixels to be repainted + * on a buffer/on-screen comparison. + */ + free(on_screen); + on_screen = (type8 *)gms_malloc(picture_size * sizeof(*on_screen)); + memset(on_screen, GMS_GRAPHICS_UNUSED_PIXEL, + picture_size * sizeof(*on_screen)); + + /* Note the buffer for freeing on cleanup. */ + gms_graphics_on_screen = on_screen; + + /* + * Assign new layers to the current image. This sorts colors by usage + * and puts the most used colors in the lower layers. It also hands us + * a count of pixels in each layer, useful for knowing when to stop + * scanning for layers in the rendering loop. + */ +#ifndef GARGLK + gms_graphics_assign_layers(off_screen, on_screen, + gms_graphics_width, gms_graphics_height, + layers, layer_usage); + + saved_layer = 0; + saved_x = 0; + saved_y = 0; + total_regions = 0; +#endif + + /* Clear the graphics window. */ + gms_graphics_clear_and_border(gms_graphics_window, + x_offset, y_offset, + GMS_GRAPHICS_PIXEL, + gms_graphics_width, gms_graphics_height); + + /* Start a fresh picture rendering pass. */ + yield_counter = 0; + + /* Clear the new picture and deferred repaint flags. */ + gms_graphics_new_picture = FALSE; + deferred_repaint = FALSE; + } + +#ifndef GARGLK + /* + * Make a portion of an image pass, from lower to higher image layers, + * scanning for invalidated pixels that are in the current image layer we + * are painting. Each invalidated pixel gives rise to a region paint, + * which equates to one Glk rectangle fill. + * + * When the limit on regions is reached, save the current image pass layer + * and coordinates, and yield control to the main game playing code by + * returning. On the next call, pick up where we left off. + * + * As an optimization, we can leave the loop on the first empty layer we + * encounter. Since layers are ordered by complexity and color usage, all + * layers higher than the first unused one will also be empty, so we don't + * need to scan them. + */ + regions = 0; + for (layer = saved_layer; + layer < GMS_PALETTE_SIZE && layer_usage[layer] > 0; layer++) { + long index_row; + + /* + * As an optimization to avoid multiplications in the loop, maintain a + * separate index row. + */ + index_row = saved_y * gms_graphics_width; + for (y = saved_y; y < gms_graphics_height; y++) { + for (x = saved_x; x < gms_graphics_width; x++) { + long index; + + /* Get the index for this pixel. */ + index = index_row + x; + assert(index < picture_size * sizeof(*off_screen)); + + /* + * Ignore pixels not in the current layer, and pixels not + * currently invalid (that is, ones whose on-screen represen- + * tation matches the off-screen buffer). + */ + if (layers[off_screen[index]] == layer + && on_screen[index] != off_screen[index]) { + /* + * Rather than painting just one pixel, here we try to + * paint the maximal region we can for the layer of the + * given pixel. + */ + gms_graphics_paint_region(gms_graphics_window, + palette, layers, + off_screen, on_screen, + x, y, x_offset, y_offset, + GMS_GRAPHICS_PIXEL, + gms_graphics_width, + gms_graphics_height); + + /* + * Increment count of regions handled, and yield, by + * returning, if the limit on paint regions is reached. + * Before returning, save the current layer and scan + * coordinates, so we can pick up here on the next call. + */ + regions++; + if (regions >= GMS_REPAINT_LIMIT) { + yield_counter++; + saved_layer = layer; + saved_x = x; + saved_y = y; + total_regions += regions; + return; + } + } + } + + /* Reset the saved x coordinate on y increment. */ + saved_x = 0; + + /* Update the index row on change of y. */ + index_row += gms_graphics_width; + } + + /* Reset the saved y coordinate on layer change. */ + saved_y = 0; + } + + /* + * If we reach this point, then we didn't get to the limit on regions + * painted on this pass. In that case, we've finished rendering the + * image. + */ + assert(regions < GMS_REPAINT_LIMIT); + total_regions += regions; + +#else + gms_graphics_paint_everything + (gms_graphics_window, + palette, off_screen, + x_offset, y_offset, + gms_graphics_width, + gms_graphics_height); +#endif + + /* + * If animated, and if animations are enabled, handle further animation + * frames, if any. + */ + if (gms_animation_enabled && gms_graphics_animated) { + int more_animation; + + /* + * Reset the off-screen buffer to a copy of the base picture. This is + * the correct state for applying animation frames. + */ + memcpy(off_screen, gms_graphics_bitmap, + picture_size * sizeof(*off_screen)); + + /* + * Apply any further animations. If none, then stop the graphics + * "thread" and return. There's no more to be done until something + * restarts us. + */ + more_animation = gms_graphics_animate(off_screen, + gms_graphics_width, + gms_graphics_height); + if (!more_animation) { + /* + * There's one extra wrinkle here. The base picture we've just put + * into the off-screen buffer isn't really complete (and for type2 + * animations, might be pure garbage), so if we happen to get a + * repaint after an animation has ended, the off-screen data we'll + * be painting could well look wrong. + * + * So... here we want to set the off-screen buffer to contain the + * final animation frame. Fortunately, we still have it in the + * on-screen buffer. + */ + memcpy(off_screen, on_screen, picture_size * sizeof(*off_screen)); + gms_graphics_stop(); + return; + } + + /* + * Re-assign layers based on animation changes to the off-screen + * buffer. + */ +#ifndef GARGLK + gms_graphics_assign_layers(off_screen, on_screen, + gms_graphics_width, gms_graphics_height, + layers, layer_usage); +#endif + + /* + * Set up an animation wait, adjusted here by the number of times we + * had to yield while rendering, as we're now that late with animations, + * and capped at zero, as we can't do anything to compensate for being + * too late. In practice, we're running too close to the edge to have + * much of an effect here, but nevertheless... + */ + ignore_counter = GMS_GRAPHICS_ANIMATION_WAIT - 1; + if (yield_counter > ignore_counter) + ignore_counter = 0; + else + ignore_counter -= yield_counter; + + /* Start a fresh picture rendering pass. */ + yield_counter = 0; +#ifndef GARGLK + saved_layer = 0; + saved_x = 0; + saved_y = 0; + total_regions = 0; +#endif + } else { + /* + * Not an animated picture, so just stop graphics, as again, there's + * no more to be done until something restarts us. + */ + gms_graphics_stop(); + } +} + + +/* + * ms_showpic() + * + * Called by the main interpreter when it wants us to display a picture. + * The function gets the picture bitmap, palette, and dimensions, and + * saves them, and the picture id, in module variables for the background + * rendering function. + * + * The graphics window is opened if required, or closed if mode is zero. + * + * The function checks for changes of actual picture by calculating the + * CRC for picture data; this helps to prevent unnecessary repaints in + * cases where the interpreter passes us the same picture as we're already + * displaying. There is a less than 1 in 4,294,967,296 chance that a new + * picture will be missed. We'll live with that. + * + * Why use CRCs, rather than simply storing the values of picture passed in + * a static variable? Because some games, typically Magnetic Windows, use + * the picture argument as a form of string pointer, and can pass in the + * same value for several, perhaps all, game pictures. If we just checked + * for a change in the picture argument, we'd never see one. So we must + * instead look for changes in the real picture data. + */ +void ms_showpic(type32 picture, type8 mode) { + static glui32 current_crc = 0; /* CRC of the current picture */ + + type8 *bitmap, animated; + type16 width, height, palette[GMS_PALETTE_SIZE]; + long picture_bytes; + glui32 crc; + + /* See if the mode indicates no graphics. */ + if (mode == 0) { + /* Note that the interpreter turned graphics off. */ + gms_graphics_interpreter = FALSE; + + /* + * If we are currently displaying the graphics window, stop any update + * "thread" and turn off graphics. + */ + if (gms_graphics_enabled && gms_graphics_are_displayed()) { + gms_graphics_stop(); + gms_graphics_close(); + } + + /* Nothing more to do now graphics are off. */ + return; + } + + /* Note that the interpreter turned graphics on. */ + gms_graphics_interpreter = TRUE; + + /* + * Obtain the image details for the requested picture. The call returns + * NULL if there's a problem with the picture. + */ + bitmap = ms_extract(picture, &width, &height, palette, &animated); + if (!bitmap) + return; + + /* Note the last thing passed to ms_extract, in case of graphics restarts. */ + gms_graphics_picture = picture; + + /* Calculate the picture size, and the CRC for the bitmap data. */ + picture_bytes = width * height * sizeof(*bitmap); + crc = gms_get_buffer_crc(bitmap, picture_bytes); + + /* + * If there is no change of picture, we might be able to largely ignore the + * call. Check for a change, and if we don't see one, and if graphics are + * enabled and being displayed, we can safely ignore the call. + */ + if (width == gms_graphics_width + && height == gms_graphics_height + && crc == current_crc + && gms_graphics_enabled && gms_graphics_are_displayed()) + return; + + /* + * We know now that this is either a genuine change of picture, or graphics + * were off and have been turned on. So, record picture details, ensure + * graphics is on, set the flags, and start the background graphics update. + */ + + /* + * Save the picture details for the update code. Here we take a complete + * local copy of the bitmap, since the interpreter core may reuse part of + * its memory for animations. + */ + free(gms_graphics_bitmap); + gms_graphics_bitmap = (type8 *)gms_malloc(picture_bytes); + memcpy(gms_graphics_bitmap, bitmap, picture_bytes); + gms_graphics_width = width; + gms_graphics_height = height; + memcpy(gms_graphics_palette, palette, sizeof(palette)); + gms_graphics_animated = animated; + + /* Retain the new picture CRC. */ + current_crc = crc; + + /* + * If graphics are enabled, ensure the window is displayed, set the + * appropriate flags, and start graphics update. If they're not enabled, + * the picture details will simply stick around in module variables until + * they are required. + */ + if (gms_graphics_enabled) { + /* + * Ensure graphics on, then set the new picture flag and start the + * updating "thread". + */ + if (gms_graphics_open()) { + gms_graphics_new_picture = TRUE; + gms_graphics_start(); + } + } +} + + +/* + * gms_graphics_picture_is_available() + * + * Return TRUE if the graphics module data is loaded with a usable picture, + * FALSE if there is no picture available to display. + */ +static int +gms_graphics_picture_is_available(void) { + return gms_graphics_bitmap != NULL; +} + + +/* + * gms_graphics_get_picture_details() + * + * Return the width, height, and animation flag of the currently loaded + * picture. The function returns FALSE if no picture is loaded, otherwise + * TRUE, with picture details in the return arguments. + */ +static int gms_graphics_get_picture_details(int *width, int *height, int *is_animated) { + if (gms_graphics_picture_is_available()) { + if (width) + *width = gms_graphics_width; + if (height) + *height = gms_graphics_height; + if (is_animated) + *is_animated = gms_graphics_animated; + + return TRUE; + } + + return FALSE; +} + + +/* + * gms_graphics_get_rendering_details() + * + * Returns the current level of applied gamma correction, as a string, the + * count of colors in the picture, and a flag indicating if graphics is + * active (busy). The function return FALSE if graphics is not enabled or + * if not being displayed, otherwise TRUE with the gamma, color count, and + * active flag in the return arguments. + * + * This function races with the graphics timeout, as it returns information + * set up by the first timeout following a new picture. There's a very, + * very small chance that it might win the race, in which case out-of-date + * gamma and color count values are returned. + */ +static int gms_graphics_get_rendering_details(const char **gamma, int *color_count, + int *is_active) { + if (gms_graphics_enabled && gms_graphics_are_displayed()) { + /* + * Return the string representing the gamma correction. If racing + * with timeouts, we might return the gamma for the last picture. + */ + if (gamma) { + assert(gms_graphics_current_gamma); + *gamma = gms_graphics_current_gamma->level; + } + + /* + * Return the color count noted by timeouts on the first timeout + * following a new picture. Again, we might return the one for + * the prior picture. + */ + if (color_count) + *color_count = gms_graphics_color_count; + + /* Return graphics active flag. */ + if (is_active) + *is_active = gms_graphics_active; + + return TRUE; + } + + return FALSE; +} + + +/* + * gms_graphics_interpreter_enabled() + * + * Return TRUE if it looks like interpreter graphics are turned on, FALSE + * otherwise. + */ +static int gms_graphics_interpreter_enabled(void) { + return gms_graphics_interpreter; +} + + +/* + * gms_graphics_cleanup() + * + * Free memory resources allocated by graphics functions. Called on game + * end. + */ +static void gms_graphics_cleanup(void) { + free(gms_graphics_bitmap); + gms_graphics_bitmap = NULL; + free(gms_graphics_off_screen); + gms_graphics_off_screen = NULL; + free(gms_graphics_on_screen); + gms_graphics_on_screen = NULL; + + gms_graphics_animated = FALSE; + gms_graphics_picture = 0; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port status line functions */ +/*---------------------------------------------------------------------*/ + +/* + * The interpreter feeds us status line characters one at a time, with Tab + * indicating right justify, and CR indicating the line is complete. To get + * this to fit with the Glk event and redraw model, here we'll buffer each + * completed status line, so we have a stable string to output when needed. + * It's also handy to have this buffer for Glk libraries that don't support + * separate windows. + */ +enum { GMS_STATBUFFER_LENGTH = 1024 }; +static char gms_status_buffer[GMS_STATBUFFER_LENGTH]; +static int gms_status_length = 0; + +/* Default width used for non-windowing Glk status lines. */ +static const int GMS_DEFAULT_STATUS_WIDTH = 74; + + +/* + * ms_statuschar() + * + * Receive one status character from the interpreter. Characters are + * buffered internally, and on CR, the buffer is copied to the main static + * status buffer for use by the status line printing function. + */ +void ms_statuschar(type8 c) { + static char buffer[GMS_STATBUFFER_LENGTH]; + static int length = 0; + + /* + * If the status character is newline, transfer locally buffered data to + * the common buffer, empty the local buffer; otherwise, if space permits, + * buffer the character. + */ + if (c == '\n') { + memcpy(gms_status_buffer, buffer, length); + gms_status_length = length; + + length = 0; + } else { + if (length < (int)sizeof(buffer)) + buffer[length++] = c; + } +} + + +/* + * gms_status_update() + * + * Update the information in the status window with the current contents of + * the completed status line buffer, or a default string if no completed + * status line. + */ +static void gms_status_update(void) { + glui32 width, height; + int index; + assert(gms_status_window); + + g_vm->glk_window_get_size(gms_status_window, &width, &height); + if (height > 0) { + g_vm->glk_window_clear(gms_status_window); + g_vm->glk_window_move_cursor(gms_status_window, 0, 0); + g_vm->glk_set_window(gms_status_window); + + g_vm->glk_set_style(style_User1); + for (index = 0; index < (int)width; index++) + g_vm->glk_put_char(' '); + g_vm->glk_window_move_cursor(gms_status_window, 1, 0); + + if (gms_status_length > 0) { + /* + * Output each character from the status line buffer. If the + * character is Tab, position the cursor to eleven characters shy + * of the status window right. + */ + for (index = 0; index < gms_status_length; index++) { + if (gms_status_buffer[index] == '\t') + g_vm->glk_window_move_cursor(gms_status_window, width - 11, 0); + else + g_vm->glk_put_char(gms_status_buffer[index]); + } + } else { + const char *game_name; + + /* + * We have no status line to display, so print the game's name, or + * a standard message if unable to identify the game. Having no + * status line is common with Magnetic Windows games, which don't, + * in general, seem to use one. + */ + game_name = gms_gameid_get_game_name(); + g_vm->glk_put_string(game_name ? game_name : "ScummVM Magnetic version 2.3"); + } + + g_vm->glk_set_window(gms_main_window); + } +} + + +/* + * gms_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 gms_status_print(void) { + static char buffer[GMS_STATBUFFER_LENGTH]; + static int length = 0; + + int index, column; + + /* + * Do nothing if there is no status line to print, or if the status + * line hasn't changed since last printed. + */ + if (gms_status_length == 0 + || (gms_status_length == length + && strncmp(buffer, gms_status_buffer, length)) == 0) + return; + + /* Set fixed width font to try to preserve status line formatting. */ + g_vm->glk_set_style(style_Preformatted); + + /* Bracket, and output the status line buffer. */ + g_vm->glk_put_string("[ "); + column = 1; + for (index = 0; index < gms_status_length; index++) { + /* + * If the character is Tab, position the cursor to eleven characters + * shy of the right edge. In the absence of the real window dimensions, + * we'll select 74 characters, which gives us a 78 character status + * line; pretty standard. + */ + if (gms_status_buffer[index] == '\t') { + while (column <= GMS_DEFAULT_STATUS_WIDTH - 11) { + g_vm->glk_put_char(' '); + column++; + } + } else { + g_vm->glk_put_char(gms_status_buffer[index]); + column++; + } + } + + while (column <= GMS_DEFAULT_STATUS_WIDTH) { + g_vm->glk_put_char(' '); + column++; + } + g_vm->glk_put_string(" ]\n"); + + /* Save the details of the printed status buffer. */ + memcpy(buffer, gms_status_buffer, gms_status_length); + length = gms_status_length; +} + + +/* + * gms_status_notify() + * + * Front end function for updating status. Either updates the status window + * or prints the status line to the main window. + */ +static void gms_status_notify(void) { + if (gms_status_window) + gms_status_update(); + else + gms_status_print(); +} + + +/* + * gms_status_redraw() + * + * Redraw the contents of any status window with the buffered status string. + * This function should be called on the appropriate Glk window resize and + * arrange events. + */ +static void gms_status_redraw(void) { + if (gms_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(gms_status_window); + g_vm->glk_window_set_arrangement(parent, + winmethod_Above | winmethod_Fixed, 1, NULL); + + gms_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 gms_help_requested = FALSE, + gms_help_hints_silenced = FALSE; + +/* + * Output buffer. We receive characters one at a time, and it's a bit + * more efficient for everyone if we buffer them, and output a complete + * string on a flush call. + */ +static char *gms_output_buffer = NULL; +static int gms_output_allocation = 0, + gms_output_length = 0; + +/* + * Flag to indicate if the last buffer flushed looked like it ended in a + * ">" prompt. + */ +static int gms_output_prompt = FALSE; + + +/* + * gms_output_register_help_request() + * gms_output_silence_help_hints() + * gms_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 gms_output_register_help_request(void) { + gms_help_requested = TRUE; +} + +static void gms_output_silence_help_hints(void) { + gms_help_hints_silenced = TRUE; +} + +static void gms_output_provide_help_hint(void) { + if (gms_help_requested && !gms_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"); + + gms_help_requested = FALSE; + g_vm->glk_set_style(style_Normal); + } +} + + +/* + * gms_game_prompted() + * + * Return TRUE if the last game output appears to have been a ">" prompt. + * Once called, the flag is reset to FALSE, and requires more game output + * to set it again. + */ +static int gms_game_prompted(void) { + int result; + + result = gms_output_prompt; + gms_output_prompt = FALSE; + + return result; +} + + +/* + * gms_detect_game_prompt() + * + * See if the last non-newline-terminated line in the output buffer seems + * to be a prompt, and set the game prompted flag if it does, otherwise + * clear it. + */ +static void gms_detect_game_prompt(void) { + int index; + + gms_output_prompt = FALSE; + + /* + * Search for a prompt across any last unterminated buffered line; a prompt + * is any non-space character on that line. + */ + for (index = gms_output_length - 1; + index >= 0 && gms_output_buffer[index] != '\n'; index--) { + if (gms_output_buffer[index] != ' ') { + gms_output_prompt = TRUE; + break; + } + } +} + + +/* + * gms_output_delete() + * + * Delete all buffered output text. Free all malloc'ed buffer memory, and + * return the buffer variables to their initial values. + */ +static void gms_output_delete(void) { + free(gms_output_buffer); + gms_output_buffer = NULL; + gms_output_allocation = gms_output_length = 0; +} + + +/* + * gms_output_flush() + * + * Flush any buffered output text to the Glk main window, and clear the + * buffer. + */ +static void gms_output_flush(void) { + assert(g_vm->glk_stream_get_current()); + + if (gms_output_length > 0) { + /* + * See if the game issued a standard prompt, then print the buffer to + * the main window. If providing a help hint, position that before + * the game's prompt (if any). + */ + gms_detect_game_prompt(); + g_vm->glk_set_style(style_Normal); + + if (gms_output_prompt) { + int index; + + for (index = gms_output_length - 1; + index >= 0 && gms_output_buffer[index] != '\n';) + index--; + + g_vm->glk_put_buffer(gms_output_buffer, index + 1); + gms_output_provide_help_hint(); + g_vm->glk_put_buffer(gms_output_buffer + index + 1, + gms_output_length - index - 1); + } else { + g_vm->glk_put_buffer(gms_output_buffer, gms_output_length); + gms_output_provide_help_hint(); + } + + gms_output_delete(); + } +} + + +/* + * ms_putchar() + * + * Buffer a character for eventual printing to the main window. + */ +void ms_putchar(type8 c) { + int bytes; + assert(gms_output_length <= gms_output_allocation); + + /* + * See if the character is a backspace. Magnetic Scrolls games can send + * backspace characters to the display. We'll need to handle such + * characters specially, by taking the last character out of the buffer. + */ + if (c == '\b') { + if (gms_output_length > 0) + gms_output_length--; + + return; + } + + /* Grow the output buffer if necessary, then add the character. */ + for (bytes = gms_output_allocation; bytes < gms_output_length + 1;) + bytes = bytes == 0 ? 1 : bytes << 1; + + if (bytes > gms_output_allocation) { + gms_output_buffer = (char *)gms_realloc(gms_output_buffer, bytes); + gms_output_allocation = bytes; + } + + gms_output_buffer[gms_output_length++] = c; +} + + +/* + * gms_styled_string() + * gms_styled_char() + * gms_standout_string() + * gms_standout_char() + * gms_normal_string() + * gms_normal_char() + * gms_header_string() + * gms_banner_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 gms_styled_string(glui32 style, const char *message) { + assert(message); + + g_vm->glk_set_style(style); + g_vm->glk_put_string(message); + g_vm->glk_set_style(style_Normal); +} + +static void gms_styled_char(glui32 style, char c) { + char buffer[2]; + + buffer[0] = c; + buffer[1] = '\0'; + gms_styled_string(style, buffer); +} + +static void gms_standout_string(const char *message) { + gms_styled_string(style_Emphasized, message); +} + +#if 0 +static void gms_standout_char(char c) { + gms_styled_char(style_Emphasized, c); +} +#endif + +static void gms_normal_string(const char *message) { + gms_styled_string(style_Normal, message); +} + +static void gms_normal_char(char c) { + gms_styled_char(style_Normal, c); +} + +static void gms_header_string(const char *message) { + gms_styled_string(style_Header, message); +} + +static void gms_banner_string(const char *message) { + gms_styled_string(style_Subheader, message); +} + +/* + * ms_flush() + * + * Handle a core interpreter call to flush the output buffer. Because Glk + * only flushes its buffers and displays text on g_vm->glk_select(), we can ignore + * these calls as long as we call gms_output_flush() when reading line input. + * + * Taking ms_flush() at face value can cause game text to appear before status + * line text where we are working with a non-windowing Glk, so it's best + * ignored where we can. + */ +void ms_flush(void) { +} + + +/*---------------------------------------------------------------------*/ +/* Glk port hint functions */ +/*---------------------------------------------------------------------*/ + +/* Hint type definitions. */ +enum { + GMS_HINT_TYPE_FOLDER = 1, + GMS_HINT_TYPE_TEXT = 2 +}; + +/* Success and fail return codes from hint functions. */ +static const type8 GMS_HINT_SUCCESS = 1, + GMS_HINT_ERROR = 0; + +/* Default window sizes for non-windowing Glk libraries. */ +static const glui32 GMS_HINT_DEFAULT_WIDTH = 72, + GMS_HINT_DEFAULT_HEIGHT = 25; + +/* + * Special hint nodes indicating the root hint node, and a value to signal + * quit from hints subsystem. + */ +static const type16 GMS_HINT_ROOT_NODE = 0, + GMS_HINTS_DONE = UINT16_MAX; + +/* Generic hint topic for the root hints node. */ +static const char *const GMS_GENERIC_TOPIC = "Hints Menu"; + +/* + * Note of the interpreter's hints array. Note that keeping its address + * like this assumes that it's either static or heap in the interpreter. + */ +static struct ms_hint *gms_hints = NULL; + +/* Details of the current hint node on display from the hints array. */ +static type16 gms_current_hint_node = 0; + +/* + * Array of cursors for each hint. The cursor indicates the current hint + * position in a folder, and the last hint shown in text hints. Space + * is allocated as needed for a given set of hints, and needs to be freed + * on interpreter exit. + */ +static int *gms_hint_cursor = NULL; + + +/* + * gms_get_hint_max_node() + * + * Return the maximum hint node referred to by the tree under the given node. + * The result is the largest index found, or node, if greater. Because the + * interpreter doesn't supply it, we need to uncover it the hard way. The + * function is recursive, and since it is a tree search, assumes that hints + * is a tree, not a graph. + */ +static type16 gms_get_hint_max_node(const struct ms_hint hints[], type16 node) { + const struct ms_hint *hint; + int index; + type16 max_node; + assert(hints); + + hint = hints + node; + max_node = node; + + switch (hint->nodetype) { + case GMS_HINT_TYPE_TEXT: + break; + + case GMS_HINT_TYPE_FOLDER: + /* + * Recursively find the maximum node reference for each link, and keep + * the largest value found. + */ + for (index = 0; index < hint->elcount; index++) { + type16 link_max; + + link_max = gms_get_hint_max_node(hints, hint->links[index]); + if (link_max > max_node) + max_node = link_max; + } + break; + + default: + gms_fatal("GLK: Invalid hints node type encountered"); + g_vm->glk_exit(); + } + + /* + * Return the largest node reference found, capped to avoid overlapping the + * special end-hints value. + */ + return max_node < GMS_HINTS_DONE ? max_node : GMS_HINTS_DONE - 1; +} + + +/* + * gms_get_hint_content() + * + * Return the content string for a given hint number within a given node. + * This counts over 'number' ASCII NULs in the node's content, returning + * the address of the string located this way. + */ +static const char *gms_get_hint_content(const struct ms_hint hints[], type16 node, int number) { + const struct ms_hint *hint; + int offset, index; + assert(hints); + + hint = hints + node; + + /* Run through content until 'number' strings found. */ + offset = 0; + for (index = 0; index < number; index++) + offset += strlen(hint->content + offset) + 1; + + /* Return the start of the number'th string encountered. */ + return hint->content + offset; +} + + +/* + * gms_get_hint_topic() + * + * Return the topic string for a given hint node. This is found by searching + * the parent node for a link to the node handed in. For the root node, the + * string is defaulted, since the root node has no parent. + */ +static const char *gms_get_hint_topic(const struct ms_hint hints[], type16 node) { + assert(hints); + + if (node == GMS_HINT_ROOT_NODE) { + /* If the node is the root node, return a generic string. */ + return GMS_GENERIC_TOPIC; + } else { + type16 parent; + int index; + const char *topic; + + /* + * Search the parent for a link to node, and use that as the hint topic; + * NULL if none found. + */ + parent = hints[node].parent; + + topic = NULL; + for (index = 0; index < hints[parent].elcount; index++) { + if (hints[parent].links[index] == node) { + topic = gms_get_hint_content(hints, parent, index); + break; + } + } + + return topic ? topic : GMS_GENERIC_TOPIC; + } +} + + +/* + * gms_hint_open() + * + * If not already open, open the hints windows. Returns TRUE if the windows + * opened, or were already open. + * + * The function creates two hints windows -- a text grid on top, for menus, + * and a text buffer below for hints. + */ +static int gms_hint_open(void) { + if (!gms_hint_menu_window) { + assert(!gms_hint_text_window); + + /* + * Open the hint menu window. The initial size is two lines, but we'll + * change this later to suit the hint. + */ + gms_hint_menu_window = g_vm->glk_window_open(gms_main_window, + winmethod_Above | winmethod_Fixed, + 2, wintype_TextGrid, 0); + if (!gms_hint_menu_window) + return FALSE; + + /* + * Now open the hints text window. This is set to be 100% of the size + * of the main window, so should cover what remains of it completely. + */ + gms_hint_text_window = g_vm->glk_window_open(gms_main_window, + winmethod_Above + | winmethod_Proportional, + 100, wintype_TextBuffer, 0); + if (!gms_hint_text_window) { + g_vm->glk_window_close(gms_hint_menu_window, NULL); + gms_hint_menu_window = NULL; + return FALSE; + } + } + + return TRUE; +} + + +/* + * gms_hint_close() + * + * If open, close the hints windows. + */ +static void gms_hint_close(void) { + if (gms_hint_menu_window) { + assert(gms_hint_text_window); + + g_vm->glk_window_close(gms_hint_menu_window, NULL); + gms_hint_menu_window = NULL; + g_vm->glk_window_close(gms_hint_text_window, NULL); + gms_hint_text_window = NULL; + } +} + + +/* + * gms_hint_windows_available() + * + * Return TRUE if hints windows are available. If they're not, the hints + * system will need to use alternative output methods. + */ +static int gms_hint_windows_available(void) { + return (gms_hint_menu_window && gms_hint_text_window); +} + + +/* + * gms_hint_menu_print() + * gms_hint_menu_header() + * gms_hint_menu_justify() + * gms_hint_text_print() + * gms_hint_menutext_done() + * gms_hint_menutext_start() + * + * Output functions for writing hints. These functions will write to hints + * windows where available, and to the main window where not. When writing + * to hints windows, they also take care not to line wrap in the menu window. + * Limited formatting is available. + */ +static void gms_hint_menu_print(int line, int column, const char *string, + glui32 width, glui32 height) { + assert(string); + + /* Ignore the call if the text position is outside the window. */ + if (!(line > (int)height || column > (int)width)) { + if (gms_hint_windows_available()) { + int posn, index; + + g_vm->glk_window_move_cursor(gms_hint_menu_window, column, line); + g_vm->glk_set_window(gms_hint_menu_window); + + /* Write until the end of the string, or the end of the window. */ + for (posn = column, index = 0; + posn < (int)width && index < (int)strlen(string); posn++, index++) { + g_vm->glk_put_char(string[index]); + } + + g_vm->glk_set_window(gms_main_window); + } else { + static int current_line = 0; /* Retained line number */ + static int current_column = 0; /* Retained col number */ + + int index; + + /* + * Check the line number against the last one output. If it is less, + * assume the start of a new block. In this case, perform a hokey + * type of screen clear. + */ + if (line < current_line) { + for (index = 0; index < (int)height; index++) + gms_normal_char('\n'); + + current_line = 0; + current_column = 0; + } + + /* Print blank lines until the target line is reached. */ + for (; current_line < line; current_line++) { + gms_normal_char('\n'); + current_column = 0; + } + + /* Now print spaces until the target column is reached. */ + for (; current_column < column; current_column++) + gms_normal_char(' '); + + /* + * Write characters until the end of the string, or the end of the + * (self-imposed not-really-there) window. + */ + for (index = 0; + current_column < (int)width && index < (int)strlen(string); + current_column++, index++) { + gms_normal_char(string[index]); + } + } + } +} + +static void gms_hint_menu_header(int line, const char *string, + glui32 width, glui32 height) { + int posn, length; + assert(string); + + /* Output the text in the approximate line center. */ + length = strlen(string); + posn = length < (int)width ? (width - length) / 2 : 0; + gms_hint_menu_print(line, posn, string, width, height); +} + +static void gms_hint_menu_justify(int line, + const char *left_string, const char *right_string, + glui32 width, glui32 height) { + int posn, length; + assert(left_string && right_string); + + /* Write left text normally to window left. */ + gms_hint_menu_print(line, 0, left_string, width, height); + + /* Output the right text flush with the right of the window. */ + length = strlen(right_string); + posn = length < (int)width ? width - length : 0; + gms_hint_menu_print(line, posn, right_string, width, height); +} + +static void gms_hint_text_print(const char *string) { + assert(string); + + if (gms_hint_windows_available()) { + g_vm->glk_set_window(gms_hint_text_window); + g_vm->glk_put_string(string); + g_vm->glk_set_window(gms_main_window); + } else + gms_normal_string(string); +} + +static void gms_hint_menutext_start(void) { + /* + * Twiddle for non-windowing libraries; 'clear' the main window by writing + * a null string at line 1, then a null string at line 0. This works + * because we know the current output line in gms_hint_menu_print() is zero, + * since we set it that way with gms_hint_menutext_done(), or if this is + * the first call, then that's its initial value. + */ + if (!gms_hint_windows_available()) { + gms_hint_menu_print(1, 0, "", + GMS_HINT_DEFAULT_WIDTH, GMS_HINT_DEFAULT_HEIGHT); + gms_hint_menu_print(0, 0, "", + GMS_HINT_DEFAULT_WIDTH, GMS_HINT_DEFAULT_HEIGHT); + } +} + +static void gms_hint_menutext_done(void) { + /* + * Twiddle for non-windowing libraries; 'clear' the main window by writing + * an empty string to line zero. For windowing Glk libraries, this function + * does nothing. + */ + if (!gms_hint_windows_available()) { + gms_hint_menu_print(0, 0, "", + GMS_HINT_DEFAULT_WIDTH, GMS_HINT_DEFAULT_HEIGHT); + } +} + + +/* + * gms_hint_menutext_char_event() + * + * Request and return a character event from the hints windows. In practice, + * this means either of the hints windows if available, or the main window + * if not. + */ +static void gms_hint_menutext_char_event(event_t *event) { + assert(event); + + if (gms_hint_windows_available()) { + g_vm->glk_request_char_event(gms_hint_menu_window); + g_vm->glk_request_char_event(gms_hint_text_window); + + gms_event_wait(evtype_CharInput, event); + assert(event->window == gms_hint_menu_window + || event->window == gms_hint_text_window); + + g_vm->glk_cancel_char_event(gms_hint_menu_window); + g_vm->glk_cancel_char_event(gms_hint_text_window); + } else { + g_vm->glk_request_char_event(gms_main_window); + gms_event_wait(evtype_CharInput, event); + } +} + + +/* + * gms_hint_arrange_windows() + * + * Arrange the hints windows so that the hint menu window has the requested + * number of lines. Returns the actual hint menu window width and height, + * or defaults if no hints windows are available. + */ +static void gms_hint_arrange_windows(int requested_lines, glui32 *width, glui32 *height) { + if (gms_hint_windows_available()) { + winid_t parent; + + /* Resize the hint menu window to fit the current hint. */ + parent = g_vm->glk_window_get_parent(gms_hint_menu_window); + g_vm->glk_window_set_arrangement(parent, + winmethod_Above | winmethod_Fixed, + requested_lines, NULL); + + /* Measure, and return the size of the hint menu window. */ + g_vm->glk_window_get_size(gms_hint_menu_window, width, height); + + /* Clear both the hint menu and the hint text window. */ + g_vm->glk_window_clear(gms_hint_menu_window); + g_vm->glk_window_clear(gms_hint_text_window); + } else { + /* + * No hints windows, so default width and height. The hints output + * functions will cope with this. + */ + if (width) + *width = GMS_HINT_DEFAULT_WIDTH; + if (height) + *height = GMS_HINT_DEFAULT_HEIGHT; + } +} + + +/* + * gms_hint_display_folder() + * + * Update the hints windows for the given folder hint node. + */ +static void gms_hint_display_folder(const struct ms_hint hints[], + const int cursor[], type16 node) { + glui32 width, height; + int line, index; + assert(hints && cursor); + + /* + * Arrange windows to suit the hint folder. For a folder menu window we + * use one line for each element, three for the controls, and two spacers, + * making a total of five additional lines. Width and height receive the + * actual menu window dimensions. + */ + gms_hint_arrange_windows(hints[node].elcount + 5, &width, &height); + + /* Paint in the menu header. */ + line = 0; + gms_hint_menu_header(line++, + gms_get_hint_topic(hints, node), + width, height); + gms_hint_menu_justify(line++, + " N = next subject ", " P = previous ", + width, height); + gms_hint_menu_justify(line++, + " RETURN = read subject ", + node == GMS_HINT_ROOT_NODE + ? " Q = resume game " : " Q = previous menu ", + width, height); + + /* + * Output a blank line, then the menu for the node's folder hint. The folder + * text for the selected hint is preceded by a '>' pointer. + */ + line++; + for (index = 0; index < hints[node].elcount; index++) { + gms_hint_menu_print(line, 3, + index == cursor[node] ? ">" : " ", + width, height); + gms_hint_menu_print(line++, 5, + gms_get_hint_content(hints, node, index), + width, height); + } + + /* + * Terminate with a blank line; using a single space here improves cursor + * positioning for optimized output libraries (for example, without it, + * curses output will leave the cursor at the end of the previous line). + */ + gms_hint_menu_print(line, 0, " ", width, height); +} + + +/* + * gms_hint_display_text() + * + * Update the hints windows for the given text hint node. + */ +static void gms_hint_display_text(const struct ms_hint hints[], + const int cursor[], type16 node) { + glui32 width, height; + int line, index; + assert(hints && cursor); + + /* + * Arrange windows to suit the hint text. For a hint menu, we use a simple + * two-line set of controls; everything else is in the hints text window. + * Width and height receive the actual menu window dimensions. + */ + gms_hint_arrange_windows(2, &width, &height); + + /* Paint in a short menu header. */ + line = 0; + gms_hint_menu_header(line++, + gms_get_hint_topic(hints, node), + width, height); + gms_hint_menu_justify(line++, + " RETURN = read hint ", " Q = previous menu ", + width, height); + + /* + * Output hints to the hints text window. Hints not yet exposed are + * indicated by the cursor for the hint, and are displayed as a dash. + */ + gms_hint_text_print("\n"); + for (index = 0; index < hints[node].elcount; index++) { + char buffer[16]; + + sprintf(buffer, "%3d. ", index + 1); + gms_hint_text_print(buffer); + + gms_hint_text_print(index < cursor[node] + ? gms_get_hint_content(hints, node, index) : "-"); + gms_hint_text_print("\n"); + } +} + + +/* + * gms_hint_display() + * + * Display the given hint using the appropriate display function. + */ +static void gms_hint_display(const struct ms_hint hints[], + const int cursor[], type16 node) { + assert(hints && cursor); + + switch (hints[node].nodetype) { + case GMS_HINT_TYPE_TEXT: + gms_hint_display_text(hints, cursor, node); + break; + + case GMS_HINT_TYPE_FOLDER: + gms_hint_display_folder(hints, cursor, node); + break; + + default: + gms_fatal("GLK: Invalid hints node type encountered"); + g_vm->glk_exit(); + } +} + + +/* + * gms_hint_handle_folder() + * + * Handle a Glk keycode for the given folder hint. Return the next node to + * handle, or the special end-hints on Quit at the root node. + */ +static type16 gms_hint_handle_folder(const struct ms_hint hints[], + int cursor[], type16 node, glui32 keycode) { + unsigned char response; + type16 next_node; + assert(hints && cursor); + + /* Convert key code into a single response character. */ + switch (keycode) { + case keycode_Down: + response = 'N'; + break; + case keycode_Up: + response = 'P'; + break; + case keycode_Right: + case keycode_Return: + response = '\n'; + break; + case keycode_Left: + case keycode_Escape: + response = 'Q'; + break; + default: + response = keycode <= BYTE_MAX ? g_vm->glk_char_to_upper(keycode) : 0; + break; + } + + /* + * Now handle the response character. We'll default the next node to be + * this node, but a response case can change it. + */ + next_node = node; + switch (response) { + case 'N': + /* Advance the hint cursor, wrapping at the folder end. */ + if (cursor[node] < hints[node].elcount - 1) + cursor[node]++; + else + cursor[node] = 0; + break; + + case 'P': + /* Regress the hint cursor, wrapping at the folder start. */ + if (cursor[node] > 0) + cursor[node]--; + else + cursor[node] = hints[node].elcount - 1; + break; + + case '\n': + /* The next node is the hint node at the cursor position. */ + next_node = hints[node].links[cursor[node]]; + break; + + case 'Q': + /* If root, we're done; if not, next node is node's parent. */ + next_node = node == GMS_HINT_ROOT_NODE + ? GMS_HINTS_DONE : hints[node].parent; + break; + + default: + break; + } + + return next_node; +} + + +/* + * gms_hint_handle_text() + * + * Handle a Glk keycode for the given text hint. Return the next node to + * handle. + */ +static type16 gms_hint_handle_text(const struct ms_hint hints[], + int cursor[], type16 node, glui32 keycode) { + unsigned char response; + type16 next_node; + assert(hints && cursor); + + /* Convert key code into a single response character. */ + switch (keycode) { + case keycode_Right: + case keycode_Return: + response = '\n'; + break; + case keycode_Left: + case keycode_Escape: + response = 'Q'; + break; + default: + response = keycode <= BYTE_MAX ? g_vm->glk_char_to_upper(keycode) : 0; + break; + } + + /* + * Now handle the response character. We'll default the next node to be + * this node, but a response case can change it. + */ + next_node = node; + switch (response) { + case '\n': + /* If not at end of the hint, advance the hint cursor. */ + if (cursor[node] < hints[node].elcount) + cursor[node]++; + break; + + case 'Q': + /* Done with this hint node, so next node is its parent. */ + next_node = hints[node].parent; + break; + + default: + break; + } + + return next_node; +} + + +/* + * gms_hint_handle() + * + * Handle a Glk keycode for the given hint using the appropriate handler + * function. Return the next node to handle. + */ +static type16 gms_hint_handle(const struct ms_hint hints[], + int cursor[], type16 node, glui32 keycode) { + type16 next_node; + assert(hints && cursor); + + next_node = GMS_HINT_ROOT_NODE; + switch (hints[node].nodetype) { + case GMS_HINT_TYPE_TEXT: + next_node = gms_hint_handle_text(hints, cursor, node, keycode); + break; + + case GMS_HINT_TYPE_FOLDER: + next_node = gms_hint_handle_folder(hints, cursor, node, keycode); + break; + + default: + gms_fatal("GLK: Invalid hints node type encountered"); + g_vm->glk_exit(); + } + + return next_node; +} + + +/* + * ms_showhints() + * + * Start game hints. These are modal, though there's no overriding Glk + * reason why. It's just that this matches the way they're implemented by + * most Inform games. This may not be the best way of doing help, but at + * least it's likely to be familiar, and anything more ambitious may be + * beyond the current Glk capabilities. + * + * This function uses CRCs to detect any change of hints data. Normally, + * we'd expect none, at least within a given game run, but we can probably + * handle it okay if it happens. + */ +type8 ms_showhints(struct ms_hint *hints) { + static int is_initialized = FALSE; + static glui32 current_crc = 0; + + type16 hint_count; + glui32 crc; + assert(hints); + + /* + * Find the number of hints in the array. To do this, we'll visit every + * node in a tree search, starting at the root, to locate the maximum node + * number found, then add one to that. It's a pity that the interpreter + * doesn't hand us this information directly. + */ + hint_count = gms_get_hint_max_node(hints, GMS_HINT_ROOT_NODE) + 1; + + /* + * Calculate a CRC for the hints array data. If the CRC has changed, or + * this is the first call, assign a new cursor array. + */ + crc = gms_get_buffer_crc(hints, hint_count * sizeof(*hints)); + if (crc != current_crc || !is_initialized) { + int bytes; + + /* Allocate new cursors, and set all to zero initial state. */ + free(gms_hint_cursor); + bytes = hint_count * sizeof(*gms_hint_cursor); + gms_hint_cursor = (int *)gms_malloc(bytes); + memset(gms_hint_cursor, 0, bytes); + + /* + * Retain the hints CRC, for later comparisons, and set is_initialized + * flag. + */ + current_crc = crc; + is_initialized = TRUE; + } + + /* + * Save the hints array passed in. This is done here since even if the data + * remains the same (found by the CRC check above), the pointer to it might + * have changed. + */ + gms_hints = hints; + + /* + * Try to create the hints windows. If they can't be created, perhaps + * because the Glk library doesn't support it, the output functions will + * work around this. + */ + gms_hint_open(); + gms_hint_menutext_start(); + + /* + * Begin hints display at the root node, and navigate until the user exits + * hints. + */ + gms_current_hint_node = GMS_HINT_ROOT_NODE; + while (gms_current_hint_node != GMS_HINTS_DONE) { + event_t event; + + assert(gms_current_hint_node < hint_count); + gms_hint_display(gms_hints, gms_hint_cursor, gms_current_hint_node); + + /* Get and handle a character key event for hint navigation. */ + gms_hint_menutext_char_event(&event); + assert(event.type == evtype_CharInput); + gms_current_hint_node = gms_hint_handle(gms_hints, + gms_hint_cursor, + gms_current_hint_node, + event.val1); + } + + /* Done with hint windows. */ + gms_hint_menutext_done(); + gms_hint_close(); + + return GMS_HINT_SUCCESS; +} + + +/* + * gms_hint_redraw() + * + * Update the hints windows for the current hint. This function should be + * called from the event handler on resize events, to repaint the hints + * display. It does nothing if no hints windows have been opened, since + * in this case, there's no resize action required -- either we're not in + * the hints subsystem, or hints are being displayed in the main game + * window, for whatever reason. + */ +static void gms_hint_redraw(void) { + if (gms_hint_windows_available()) { + assert(gms_hints && gms_hint_cursor); + gms_hint_display(gms_hints, gms_hint_cursor, gms_current_hint_node); + } +} + + +/* + * gms_hints_cleanup() + * + * Free memory resources allocated by hints functions. Called on game + * end. + */ +static void gms_hints_cleanup(void) { + free(gms_hint_cursor); + gms_hint_cursor = NULL; + + gms_hints = NULL; + gms_current_hint_node = 0; +} + + +void ms_playmusic(type8 *midi_data, type32 length, type16 tempo) { +} + + +/*---------------------------------------------------------------------*/ +/* Glk command escape functions */ +/*---------------------------------------------------------------------*/ + +/* + * gms_command_undo() + * + * Stub function for the undo command. The real work is to return the + * undo code to the input functions. + */ +static void gms_command_undo(const char *argument) { + assert(argument); +} + + +/* + * gms_command_script() + * + * Turn game output scripting (logging) on and off. + */ +static void gms_command_script(const char *argument) { + assert(argument); + + if (gms_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gms_transcript_stream) { + gms_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) { + gms_standout_string("Glk transcript failed.\n"); + return; + } + + gms_transcript_stream = g_vm->glk_stream_open_file(fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gms_transcript_stream) { + gms_standout_string("Glk transcript failed.\n"); + return; + } + + g_vm->glk_window_set_echo_stream(gms_main_window, gms_transcript_stream); + + gms_normal_string("Glk transcript is now on.\n"); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + if (!gms_transcript_stream) { + gms_normal_string("Glk transcript is already off.\n"); + return; + } + + g_vm->glk_stream_close(gms_transcript_stream, NULL); + gms_transcript_stream = NULL; + + g_vm->glk_window_set_echo_stream(gms_main_window, NULL); + + gms_normal_string("Glk transcript is now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk transcript is "); + gms_normal_string(gms_transcript_stream ? "on" : "off"); + gms_normal_string(".\n"); + } + + else { + gms_normal_string("Glk transcript can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_inputlog() + * + * Turn game input logging on and off. + */ +static void gms_command_inputlog(const char *argument) { + assert(argument); + + if (gms_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gms_inputlog_stream) { + gms_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) { + gms_standout_string("Glk input logging failed.\n"); + return; + } + + gms_inputlog_stream = g_vm->glk_stream_open_file(fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gms_inputlog_stream) { + gms_standout_string("Glk input logging failed.\n"); + return; + } + + gms_normal_string("Glk input logging is now on.\n"); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + if (!gms_inputlog_stream) { + gms_normal_string("Glk input logging is already off.\n"); + return; + } + + g_vm->glk_stream_close(gms_inputlog_stream, NULL); + gms_inputlog_stream = NULL; + + gms_normal_string("Glk input log is now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk input logging is "); + gms_normal_string(gms_inputlog_stream ? "on" : "off"); + gms_normal_string(".\n"); + } + + else { + gms_normal_string("Glk input logging can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_readlog() + * + * Set the game input log, to read input from a file. + */ +static void gms_command_readlog(const char *argument) { + assert(argument); + + if (gms_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gms_readlog_stream) { + gms_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) { + gms_standout_string("Glk read log failed.\n"); + return; + } + + if (!g_vm->glk_fileref_does_file_exist(fileref)) { + g_vm->glk_fileref_destroy(fileref); + gms_standout_string("Glk read log failed.\n"); + return; + } + + gms_readlog_stream = g_vm->glk_stream_open_file(fileref, filemode_Read, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gms_readlog_stream) { + gms_standout_string("Glk read log failed.\n"); + return; + } + + gms_normal_string("Glk read log is now on.\n"); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + if (!gms_readlog_stream) { + gms_normal_string("Glk read log is already off.\n"); + return; + } + + g_vm->glk_stream_close(gms_readlog_stream, NULL); + gms_readlog_stream = NULL; + + gms_normal_string("Glk read log is now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk read log is "); + gms_normal_string(gms_readlog_stream ? "on" : "off"); + gms_normal_string(".\n"); + } + + else { + gms_normal_string("Glk read log can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_abbreviations() + * + * Turn abbreviation expansions on and off. + */ +static void gms_command_abbreviations(const char *argument) { + assert(argument); + + if (gms_strcasecmp(argument, "on") == 0) { + if (gms_abbreviations_enabled) { + gms_normal_string("Glk abbreviation expansions are already on.\n"); + return; + } + + gms_abbreviations_enabled = TRUE; + gms_normal_string("Glk abbreviation expansions are now on.\n"); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + if (!gms_abbreviations_enabled) { + gms_normal_string("Glk abbreviation expansions are already off.\n"); + return; + } + + gms_abbreviations_enabled = FALSE; + gms_normal_string("Glk abbreviation expansions are now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk abbreviation expansions are "); + gms_normal_string(gms_abbreviations_enabled ? "on" : "off"); + gms_normal_string(".\n"); + } + + else { + gms_normal_string("Glk abbreviation expansions can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_graphics() + * + * Enable or disable graphics more permanently than is done by the main + * interpreter. Also, print out a few brief details about the graphics + * state of the program. + */ +static void gms_command_graphics(const char *argument) { + assert(argument); + + if (!gms_graphics_possible) { + gms_normal_string("Glk graphics are not available.\n"); + return; + } + + if (gms_strcasecmp(argument, "on") == 0) { + if (gms_graphics_enabled) { + gms_normal_string("Glk graphics are already on.\n"); + return; + } + + gms_graphics_enabled = TRUE; + + /* If a picture is loaded, call the restart function to repaint it. */ + if (gms_graphics_picture_is_available()) { + if (!gms_graphics_open()) { + gms_normal_string("Glk graphics error.\n"); + return; + } + gms_graphics_restart(); + } + + gms_normal_string("Glk graphics are now on.\n"); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + if (!gms_graphics_enabled) { + gms_normal_string("Glk graphics are already off.\n"); + return; + } + + /* + * Set graphics to disabled, and stop any graphics processing. Close + * the graphics window. + */ + gms_graphics_enabled = FALSE; + gms_graphics_stop(); + gms_graphics_close(); + + gms_normal_string("Glk graphics are now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk graphics are available,"); + gms_normal_string(gms_graphics_enabled + ? " and enabled.\n" : " but disabled.\n"); + + if (gms_graphics_picture_is_available()) { + int width, height, is_animated; + + if (gms_graphics_get_picture_details(&width, &height, &is_animated)) { + char buffer[16]; + + gms_normal_string("There is "); + gms_normal_string(is_animated ? "an animated" : "a"); + gms_normal_string(" picture loaded, "); + + sprintf(buffer, "%d", width); + gms_normal_string(buffer); + gms_normal_string(" by "); + + sprintf(buffer, "%d", height); + gms_normal_string(buffer); + + gms_normal_string(" pixels.\n"); + } + } + + if (!gms_graphics_interpreter_enabled()) + gms_normal_string("Interpreter graphics are disabled.\n"); + + if (gms_graphics_enabled && gms_graphics_are_displayed()) { + int color_count, is_active; + const char *gamma; + + if (gms_graphics_get_rendering_details(&gamma, &color_count, + &is_active)) { + char buffer[16]; + + gms_normal_string("Graphics are "); + gms_normal_string(is_active ? "active, " : "displayed, "); + + sprintf(buffer, "%d", color_count); + gms_normal_string(buffer); + gms_normal_string(" colours"); + + if (gms_gamma_mode == GAMMA_OFF) + gms_normal_string(", without gamma correction"); + else { + gms_normal_string(", with gamma "); + gms_normal_string(gamma); + gms_normal_string(" correction"); + } + gms_normal_string(".\n"); + } else + gms_normal_string("Graphics are being displayed.\n"); + } + + if (gms_graphics_enabled && !gms_graphics_are_displayed()) + gms_normal_string("Graphics are not being displayed.\n"); + } + + else { + gms_normal_string("Glk graphics can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_gamma() + * + * Enable or disable picture gamma corrections. + */ +static void gms_command_gamma(const char *argument) { + assert(argument); + + if (!gms_graphics_possible) { + gms_normal_string("Glk automatic gamma correction is not available.\n"); + return; + } + + if (gms_strcasecmp(argument, "high") == 0) { + if (gms_gamma_mode == GAMMA_HIGH) { + gms_normal_string("Glk automatic gamma correction mode is" + " already 'high'.\n"); + return; + } + + gms_gamma_mode = GAMMA_HIGH; + gms_graphics_restart(); + + gms_normal_string("Glk automatic gamma correction mode is" + " now 'high'.\n"); + } + + else if (gms_strcasecmp(argument, "normal") == 0 + || gms_strcasecmp(argument, "on") == 0) { + if (gms_gamma_mode == GAMMA_NORMAL) { + gms_normal_string("Glk automatic gamma correction mode is" + " already 'normal'.\n"); + return; + } + + gms_gamma_mode = GAMMA_NORMAL; + gms_graphics_restart(); + + gms_normal_string("Glk automatic gamma correction mode is" + " now 'normal'.\n"); + } + + else if (gms_strcasecmp(argument, "none") == 0 + || gms_strcasecmp(argument, "off") == 0) { + if (gms_gamma_mode == GAMMA_OFF) { + gms_normal_string("Glk automatic gamma correction mode is" + " already 'off'.\n"); + return; + } + + gms_gamma_mode = GAMMA_OFF; + gms_graphics_restart(); + + gms_normal_string("Glk automatic gamma correction mode is" + " now 'off'.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk automatic gamma correction mode is '"); + switch (gms_gamma_mode) { + case GAMMA_OFF: + gms_normal_string("off"); + break; + case GAMMA_NORMAL: + gms_normal_string("normal"); + break; + case GAMMA_HIGH: + gms_normal_string("high"); + break; + } + gms_normal_string("'.\n"); + } + + else { + gms_normal_string("Glk automatic gamma correction mode can be "); + gms_standout_string("high"); + gms_normal_string(", "); + gms_standout_string("normal"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_animations() + * + * Enable or disable picture animations. + */ +static void gms_command_animations(const char *argument) { + assert(argument); + + if (!gms_graphics_possible) { + gms_normal_string("Glk graphics animations are not available.\n"); + return; + } + + if (gms_strcasecmp(argument, "on") == 0) { + int is_animated; + + if (gms_animation_enabled) { + gms_normal_string("Glk graphics animations are already on.\n"); + return; + } + + /* + * Set animation to on, and restart graphics if the current picture + * is animated; if it isn't, we can leave it displayed as is, since + * changing animation mode doesn't affect this picture. + */ + gms_animation_enabled = TRUE; + if (gms_graphics_get_picture_details(NULL, NULL, &is_animated)) { + if (is_animated) + gms_graphics_restart(); + } + + gms_normal_string("Glk graphics animations are now on.\n"); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + int is_animated; + + if (!gms_animation_enabled) { + gms_normal_string("Glk graphics animations are already off.\n"); + return; + } + + gms_animation_enabled = FALSE; + if (gms_graphics_get_picture_details(NULL, NULL, &is_animated)) { + if (is_animated) + gms_graphics_restart(); + } + + gms_normal_string("Glk graphics animations are now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk graphics animations are "); + gms_normal_string(gms_animation_enabled ? "on" : "off"); + gms_normal_string(".\n"); + } + + else { + gms_normal_string("Glk graphics animations can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_prompts() + * + * Turn the extra "> " prompt output on and off. + */ +static void gms_command_prompts(const char *argument) { + assert(argument); + + if (gms_strcasecmp(argument, "on") == 0) { + if (gms_prompt_enabled) { + gms_normal_string("Glk extra prompts are already on.\n"); + return; + } + + gms_prompt_enabled = TRUE; + gms_normal_string("Glk extra prompts are now on.\n"); + + /* Check for a game prompt to clear the flag. */ + gms_game_prompted(); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + if (!gms_prompt_enabled) { + gms_normal_string("Glk extra prompts are already off.\n"); + return; + } + + gms_prompt_enabled = FALSE; + gms_normal_string("Glk extra prompts are now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk extra prompts are "); + gms_normal_string(gms_prompt_enabled ? "on" : "off"); + gms_normal_string(".\n"); + } + + else { + gms_normal_string("Glk extra prompts can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_normal_string(".\n"); + } +} + + +/* + * gms_command_print_version_number() + * gms_command_version() + * + * Print out the Glk library version number. + */ +static void gms_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); + gms_normal_string(buffer); +} + +static void +gms_command_version(const char *argument) { + glui32 version; + assert(argument); + + gms_normal_string("This is version "); + gms_command_print_version_number(GMS_PORT_VERSION); + gms_normal_string(" of the Glk Magnetic port.\n"); + + version = g_vm->glk_gestalt(gestalt_Version, 0); + gms_normal_string("The Glk library version is "); + gms_command_print_version_number(version); + gms_normal_string(".\n"); +} + + +/* + * gms_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 gms_command_commands(const char *argument) { + assert(argument); + + if (gms_strcasecmp(argument, "on") == 0) { + gms_normal_string("Glk commands are already on.\n"); + } + + else if (gms_strcasecmp(argument, "off") == 0) { + gms_commands_enabled = FALSE; + gms_normal_string("Glk commands are now off.\n"); + } + + else if (strlen(argument) == 0) { + gms_normal_string("Glk commands are "); + gms_normal_string(gms_commands_enabled ? "on" : "off"); + gms_normal_string(".\n"); + } + + else { + gms_normal_string("Glk commands can be "); + gms_standout_string("on"); + gms_normal_string(", or "); + gms_standout_string("off"); + gms_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. */ + const int undo_return; /* "Undo" return value. */ +} gms_command_t; +typedef gms_command_t *gms_commandref_t; + +static void gms_command_summary(const char *argument); +static void gms_command_help(const char *argument); + +static gms_command_t GMS_COMMAND_TABLE[] = { + {"summary", gms_command_summary, FALSE, FALSE}, + {"undo", gms_command_undo, FALSE, TRUE}, + {"script", gms_command_script, TRUE, FALSE}, + {"inputlog", gms_command_inputlog, TRUE, FALSE}, + {"readlog", gms_command_readlog, TRUE, FALSE}, + {"abbreviations", gms_command_abbreviations, TRUE, FALSE}, + {"graphics", gms_command_graphics, TRUE, FALSE}, + {"gamma", gms_command_gamma, TRUE, FALSE}, + {"animations", gms_command_animations, TRUE, FALSE}, + {"prompts", gms_command_prompts, TRUE, FALSE}, + {"version", gms_command_version, FALSE, FALSE}, + {"commands", gms_command_commands, TRUE, FALSE}, + {"help", gms_command_help, TRUE, FALSE}, + {NULL, NULL, FALSE, FALSE} +}; + + +/* + * gms_command_summary() + * + * Report all current Glk settings. + */ +static void gms_command_summary(const char *argument) { + gms_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 = GMS_COMMAND_TABLE; entry->command; entry++) { + if (entry->handler == gms_command_summary + || entry->handler == gms_command_undo + || entry->handler == gms_command_help) + continue; + + entry->handler(""); + } +} + + +/* + * gms_command_help() + * + * Document the available Glk commands. + */ +static void gms_command_help(const char *command) { + gms_commandref_t entry, matched; + assert(command); + + if (strlen(command) == 0) { + gms_normal_string("Glk commands are"); + for (entry = GMS_COMMAND_TABLE; entry->command; entry++) { + gms_commandref_t next; + + next = entry + 1; + gms_normal_string(next->command ? " " : " and "); + gms_standout_string(entry->command); + gms_normal_string(next->command ? "," : ".\n\n"); + } + + gms_normal_string("Glk commands may be abbreviated, as long as" + " the abbreviation is unambiguous. Use "); + gms_standout_string("glk help"); + gms_normal_string(" followed by a Glk command name for help on that" + " command.\n"); + return; + } + + matched = NULL; + for (entry = GMS_COMMAND_TABLE; entry->command; entry++) { + if (gms_strncasecmp(command, entry->command, strlen(command)) == 0) { + if (matched) { + gms_normal_string("The Glk command "); + gms_standout_string(command); + gms_normal_string(" is ambiguous. Try "); + gms_standout_string("glk help"); + gms_normal_string(" for more information.\n"); + return; + } + matched = entry; + } + } + if (!matched) { + gms_normal_string("The Glk command "); + gms_standout_string(command); + gms_normal_string(" is not valid. Try "); + gms_standout_string("glk help"); + gms_normal_string(" for more information.\n"); + return; + } + + if (matched->handler == gms_command_summary) { + gms_normal_string("Prints a summary of all the current Glk Magnetic" + " settings.\n"); + } + + else if (matched->handler == gms_command_undo) { + gms_normal_string("Undoes a single game turn.\n\nEquivalent to the" + " standalone game 'undo' command.\n"); + } + + else if (matched->handler == gms_command_script) { + gms_normal_string("Logs the game's output to a file.\n\nUse "); + gms_standout_string("glk script on"); + gms_normal_string(" to begin logging game output, and "); + gms_standout_string("glk script off"); + gms_normal_string(" to end it. Glk Magnetic will ask you for a file" + " when you turn scripts on.\n"); + } + + else if (matched->handler == gms_command_inputlog) { + gms_normal_string("Records the commands you type into a game.\n\nUse "); + gms_standout_string("glk inputlog on"); + gms_normal_string(", to begin recording your commands, and "); + gms_standout_string("glk inputlog off"); + gms_normal_string(" to turn off input logs. You can play back" + " recorded commands into a game with the "); + gms_standout_string("glk readlog"); + gms_normal_string(" command.\n"); + } + + else if (matched->handler == gms_command_readlog) { + gms_normal_string("Plays back commands recorded with "); + gms_standout_string("glk inputlog on"); + gms_normal_string(".\n\nUse "); + gms_standout_string("glk readlog on"); + gms_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 == gms_command_abbreviations) { + gms_normal_string("Controls abbreviation expansion.\n\nGlk Magnetic" + " automatically expands several standard single" + " letter abbreviations for you; for example, \"x\"" + " becomes \"examine\". Use "); + gms_standout_string("glk abbreviations on"); + gms_normal_string(" to turn this feature on, and "); + gms_standout_string("glk abbreviations off"); + gms_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 == gms_command_graphics) { + gms_normal_string("Turns interpreter graphics on and off.\n\nUse "); + gms_standout_string("glk graphics on"); + gms_normal_string(" to enable interpreter graphics, and "); + gms_standout_string("glk graphics off"); + gms_normal_string(" to turn graphics off and close the graphics window." + " This control works slightly differently to the" + " 'graphics' command in Magnetic Windows and Magnetic" + " Scrolls games themselves; the game's 'graphics'" + " command may disable new images, but leave old ones" + " displayed. For graphics to be displayed, they" + " must be turned on in both the game and the" + " interpreter.\n"); + } + + else if (matched->handler == gms_command_gamma) { + gms_normal_string("Sets the level of automatic gamma correction applied" + " to game graphics.\n\nUse "); + gms_standout_string("glk gamma normal"); + gms_normal_string(" to set moderate automatic colour contrast" + " correction, "); + gms_standout_string("glk gamma high"); + gms_normal_string(" to set high automatic colour contrast correction," + " or "); + gms_standout_string("glk gamma off"); + gms_normal_string(" to turn off all automatic gamma correction.\n"); + } + + else if (matched->handler == gms_command_animations) { + gms_normal_string("Turns graphic animations on and off.\n\nUse "); + gms_standout_string("glk animation on"); + gms_normal_string(" to enable animations, or "); + gms_standout_string("glk animation off"); + gms_normal_string(" to turn animations off. Not all game graphics are" + " animated, so this control works only on graphics" + " that are animated. When animation is off, Glk" + " Magnetic displays only the static portions of a" + " game's pictures.\n"); + } + + else if (matched->handler == gms_command_prompts) { + gms_normal_string("Controls extra input prompting.\n\n" + "Glk Magnetic can issue a replacement '>' input" + " prompt if it detects that the game hasn't prompted" + " after, say, an empty input line. Use "); + gms_standout_string("glk prompts on"); + gms_normal_string(" to turn this feature on, and "); + gms_standout_string("glk prompts off"); + gms_normal_string(" to turn it off.\n"); + } + + else if (matched->handler == gms_command_version) { + gms_normal_string("Prints the version numbers of the Glk library" + " and the Glk Magnetic port.\n"); + } + + else if (matched->handler == gms_command_commands) { + gms_normal_string("Turn off Glk commands.\n\nUse "); + gms_standout_string("glk commands off"); + gms_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 == gms_command_help) + gms_command_help(""); + + else + gms_normal_string("There is no help available on that Glk command." + " Sorry.\n"); +} + + +/* + * gms_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. + * + * On unambiguous returns, it will also set the value for undo_command to the + * table undo return value. + */ +static int gms_command_escape(const char *string, int *undo_command) { + int posn; + char *string_copy, *command, *argument; + assert(string && undo_command); + + /* + * Return FALSE if the string doesn't begin with the Glk command escape + * introducer. + */ + posn = strspn(string, "\t "); + if (gms_strncasecmp(string + posn, "glk", strlen("glk")) != 0) + return FALSE; + + /* Take a copy of the string, without any leading space or introducer. */ + string_copy = (char *)gms_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) { + gms_commandref_t entry, matched; + int matches; + + /* + * Search for the first unambiguous table command string matching + * the command passed in. + */ + matches = 0; + matched = NULL; + for (entry = GMS_COMMAND_TABLE; entry->command; entry++) { + if (gms_strncasecmp(command, entry->command, strlen(command)) == 0) { + matches++; + matched = entry; + } + } + + /* If the match was unambiguous, call the command handler. */ + if (matches == 1) { + if (!matched->undo_return) + gms_normal_char('\n'); + matched->handler(argument); + + if (!matched->takes_argument && strlen(argument) > 0) { + gms_normal_string("[The "); + gms_standout_string(matched->command); + gms_normal_string(" command ignores arguments.]\n"); + } + + *undo_command = matched->undo_return; + } + + /* No match, or the command was ambiguous. */ + else { + gms_normal_string("\nThe Glk command "); + gms_standout_string(command); + gms_normal_string(" is "); + gms_normal_string(matches == 0 ? "not valid" : "ambiguous"); + gms_normal_string(". Try "); + gms_standout_string("glk help"); + gms_normal_string(" for more information.\n"); + } + } else { + gms_normal_char('\n'); + gms_command_help(""); + } + + /* The string contained a Glk command; return TRUE. */ + free(string_copy); + return TRUE; +} + + +/* + * gms_command_undo_special() + * + * This function makes a special case of the input line containing the single + * word "undo", treating it as if it is "glk undo". This makes life a bit + * more convenient for the player, since it's the same behavior that most + * other IF systems have. It returns TRUE if "undo" found, FALSE otherwise. + */ +static int gms_command_undo_special(const char *string) { + int posn, end; + assert(string); + + /* Find the start and end of the first string word. */ + posn = strspn(string, "\t "); + end = posn + strcspn(string + posn, "\t "); + + /* See if string contains an "undo" request, with nothing following. */ + if (end - posn == (int)strlen("undo") + && gms_strncasecmp(string + posn, "undo", end - posn) == 0) { + posn = end + strspn(string + end, "\t "); + if (string[posn] == '\0') + return TRUE; + } + + return FALSE; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port input functions */ +/*---------------------------------------------------------------------*/ + +/* + * Input buffer allocated for reading input lines. The buffer is filled + * from either an input log, if one is currently being read, or from Glk + * line input. We also need an "undo" notification flag. + */ +enum { GMS_INPUTBUFFER_LENGTH = 256 }; +static char gms_input_buffer[GMS_INPUTBUFFER_LENGTH]; +static int gms_input_length = 0, + gms_input_cursor = 0, + gms_undo_notification = FALSE; + +/* Table of single-character command abbreviations. */ +typedef const struct { + const char abbreviation; /* Abbreviation character. */ + const char *const expansion; /* Expansion string. */ +} gms_abbreviation_t; +typedef gms_abbreviation_t *gms_abbreviationref_t; + +static gms_abbreviation_t GMS_ABBREVIATIONS[] = { + {'c', "close"}, {'g', "again"}, {'i', "inventory"}, + {'k', "attack"}, {'l', "look"}, {'p', "open"}, + {'q', "quit"}, {'r', "drop"}, {'t', "take"}, + {'x', "examine"}, {'y', "yes"}, {'z', "wait"}, + {'\0', NULL} +}; + + +/* + * gms_expand_abbreviations() + * + * Expand a few common one-character abbreviations commonly found in other + * game systems, but not always normal in Magnetic Scrolls games. + */ +static void gms_expand_abbreviations(char *buffer, int size) { + char *command, abbreviation; + const char *expansion; + gms_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 = NULL; + for (entry = GMS_ABBREVIATIONS; entry->expansion; entry++) { + if (entry->abbreviation == abbreviation) { + expansion = entry->expansion; + break; + } + } + + /* + * If a match found, check for a fit, then replace the character with the + * expansion string. + */ + if (expansion) { + if ((int)(strlen(buffer) + strlen(expansion)) - 1 >= size) + return; + + memmove(command + strlen(expansion) - 1, command, strlen(command) + 1); + memcpy(command, expansion, strlen(expansion)); + +#if 0 + gms_standout_string("["); + gms_standout_char(abbreviation); + gms_standout_string(" -> "); + gms_standout_string(expansion); + gms_standout_string("]\n"); +#endif + } +} + + +/* + * gms_buffer_input + * + * Read and buffer a line of input. If there is an input log active, then + * data is taken by reading this first. Otherwise, the function gets a + * line from Glk. + * + * It also makes special cases of some lines read from the user, either + * handling commands inside them directly, or expanding abbreviations as + * appropriate. This is not reflected in the buffer, which is adjusted as + * required before returning. + */ +static void gms_buffer_input(void) { + event_t event; + + /* + * Update the current status line display, and flush any pending buffered + * output. + */ + gms_status_notify(); + gms_output_flush(); + + /* + * Magnetic Windows games tend not to issue a prompt after reading an empty + * line of input. This can make for a very blank looking screen. + * + * To slightly improve things, if it looks like we didn't get a prompt from + * the game, do our own. + */ + if (gms_prompt_enabled && !gms_game_prompted()) { + gms_normal_char('\n'); + gms_normal_string(GMS_INPUT_PROMPT); + } + + /* + * 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 (gms_readlog_stream) { + glui32 chars; + + /* Get the next line from the log stream. */ + chars = g_vm->glk_get_line_stream(gms_readlog_stream, + gms_input_buffer, sizeof(gms_input_buffer)); + if (chars > 0) { + /* Echo the line just read in input style. */ + g_vm->glk_set_style(style_Input); + g_vm->glk_put_buffer(gms_input_buffer, chars); + g_vm->glk_set_style(style_Normal); + + /* Note how many characters buffered, and return. */ + gms_input_length = chars; + return; + } + + /* + * 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(gms_readlog_stream, NULL); + gms_readlog_stream = NULL; + } + + /* + * 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. + */ + g_vm->glk_request_line_event(gms_main_window, + gms_input_buffer, sizeof(gms_input_buffer) - 1, 0); + gms_event_wait(evtype_LineInput, &event); + + /* Terminate the input line with a NUL. */ + assert(event.val1 <= sizeof(gms_input_buffer) - 1); + gms_input_buffer[event.val1] = '\0'; + + /* Special handling for "undo" commands. */ + if (gms_command_undo_special(gms_input_buffer)) { + /* Write the "undo" to any input log. */ + if (gms_inputlog_stream) { + g_vm->glk_put_string_stream(gms_inputlog_stream, gms_input_buffer); + g_vm->glk_put_char_stream(gms_inputlog_stream, '\n'); + } + + /* Overwrite buffer with an empty line if we saw "undo". */ + gms_input_buffer[0] = '\n'; + gms_input_length = 1; + + gms_undo_notification = TRUE; + return; + } + + /* + * If neither abbreviations nor local commands are enabled, use the data + * read above without further massaging. + */ + if (gms_abbreviations_enabled || gms_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 = gms_input_buffer + strspn(gms_input_buffer, "\t "); + if (command[0] == '\'') { + /* Delete the quote with memmove(). */ + memmove(command, command + 1, strlen(command)); + } else { + /* Check for, and expand, any abbreviated commands. */ + if (gms_abbreviations_enabled) { + gms_expand_abbreviations(gms_input_buffer, + sizeof(gms_input_buffer)); + } + + /* + * Check for standalone "help", then for Glk port special commands; + * suppress the interpreter's use of this input for Glk commands + * by overwriting the line with a single newline character. + */ + if (gms_commands_enabled) { + int posn; + + posn = strspn(gms_input_buffer, "\t "); + if (gms_strncasecmp(gms_input_buffer + posn, + "help", strlen("help")) == 0) { + if (strspn(gms_input_buffer + posn + strlen("help"), "\t ") + == strlen(gms_input_buffer + posn + strlen("help"))) { + gms_output_register_help_request(); + } + } + + if (gms_command_escape(gms_input_buffer, + &gms_undo_notification)) { + gms_output_silence_help_hints(); + gms_input_buffer[0] = '\n'; + gms_input_length = 1; + return; + } + } + } + } + + /* + * 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 (gms_inputlog_stream) { + g_vm->glk_put_string_stream(gms_inputlog_stream, gms_input_buffer); + g_vm->glk_put_char_stream(gms_inputlog_stream, '\n'); + } + + /* + * Now append a newline to the buffer, since Glk line input doesn't provide + * one, and in any case, abbreviation expansion may have edited the buffer + * contents (and in particular, changed the length). + */ + gms_input_buffer[strlen(gms_input_buffer) + 1] = '\0'; + gms_input_buffer[strlen(gms_input_buffer)] = '\n'; + + /* Note how many characters are buffered after all of the above. */ + gms_input_length = strlen(gms_input_buffer); +} + + +/* + * ms_getchar() + * + * Return the single next character to the interpreter. This function + * extracts characters from the input buffer until empty, when it then + * tries to buffer more data. + */ +type8 ms_getchar(type8 trans) { + /* See if we are at the end of the input buffer. */ + if (gms_input_cursor == gms_input_length) { + /* + * Try to read in more data, and rewind buffer cursor. As well as + * reading input, this may set an undo notification. + */ + gms_buffer_input(); + gms_input_cursor = 0; + + if (gms_undo_notification) { + /* + * Clear the undo notification, and discard buffered input (usually + * just the '\n' placed there when the undo command was recognized). + */ + gms_undo_notification = FALSE; + gms_input_length = 0; + + /* + * Return the special 0, or a blank line if no undo is allowed at + * this point. + */ + return trans ? 0 : '\n'; + } + } + + /* Return the next character from the input buffer. */ + assert(gms_input_cursor < gms_input_length); + return gms_input_buffer[gms_input_cursor++]; +} + +#if 0 +/* + * gms_confirm() + * + * Print a confirmation prompt, and read a single input character, taking + * only [YyNn] input. If the character is 'Y' or 'y', return TRUE. + */ +static int gms_confirm(const char *prompt) { + event_t event; + unsigned char response; + assert(prompt); + + /* + * Print the confirmation prompt, in a style that hints that it's from the + * interpreter, not the game. + */ + gms_standout_string(prompt); + + /* Wait for a single 'Y' or 'N' character response. */ + response = ' '; + do { + g_vm->glk_request_char_event(gms_main_window); + gms_event_wait(evtype_CharInput, &event); + + if (event.val1 <= BYTE_MAX) + response = g_vm->glk_char_to_upper(event.val1); + } while (!(response == 'Y' || response == 'N')); + + /* Echo the confirmation response, and a blank line. */ + g_vm->glk_set_style(style_Input); + g_vm->glk_put_string(response == 'Y' ? "Yes" : "No"); + g_vm->glk_set_style(style_Normal); + g_vm->glk_put_string("\n\n"); + + return response == 'Y'; +} +#endif + +/*---------------------------------------------------------------------*/ +/* Glk port event functions */ +/*---------------------------------------------------------------------*/ + +/* + * gms_event_wait() + * + * Process Glk events until one of the expected type arrives. Return + * the event of that type. + */ +static void gms_event_wait(glui32 wait_type, 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. */ + gms_status_redraw(); + gms_hint_redraw(); + gms_graphics_paint(); + break; + + case evtype_Timer: + /* Do background graphics updates on timeout. */ + gms_graphics_timeout(); + break; + + default: + break; + } + } while (event->type != (EvType)wait_type); +} + +/*---------------------------------------------------------------------*/ +/* Functions intercepted by link-time wrappers */ +/*---------------------------------------------------------------------*/ + +/* + * __wrap_toupper() + * __wrap_tolower() + * + * Wrapper functions around toupper() and tolower(). The Linux linker's + * --wrap option will convert calls to mumble() to __wrap_mumble() if we + * give it the right options. We'll use this feature to translate all + * toupper() and tolower() calls in the interpreter code into calls to + * Glk's versions of these functions. + * + * It's not critical that we do this. If a linker, say a non-Linux one, + * won't do --wrap, then just do without it. It's unlikely that there + * will be much noticeable difference. + */ +int __wrap_toupper(int ch) { + unsigned char uch; + + uch = g_vm->glk_char_to_upper((unsigned char) ch); + return (int) uch; +} + +int __wrap_tolower(int ch) { + unsigned char lch; + + lch = g_vm->glk_char_to_lower((unsigned char) ch); + return (int) lch; +} + + +/*---------------------------------------------------------------------*/ +/* main() and options parsing */ +/*---------------------------------------------------------------------*/ + +/* + * The following values need to be passed between the startup_code and main + * functions. + */ +static const char *gms_gamefile = NULL, /* Name of game file. */ + *gms_game_message = NULL; /* Error message. */ + + +/* + * gms_establish_filenames() + * + * Given a game name, try to establish three filenames from it - the main game + * text file, the (optional) graphics data file, and the (optional) hints + * file. Given an input "file" X, the function looks for X.MAG or X.mag for + * game data, X.GFX or X.gfx for graphics, and X.HNT or X.hnt for hints. + * If the input file already ends with .MAG, .GFX, or .HNT, the extension + * is stripped first. + * + * The function returns NULL for filenames not available. It's not fatal if + * the graphics filename or hints filename is NULL, but it is if the main game + * filename is NULL. Filenames are malloc'ed, and need to be freed by the + * caller. + */ +static void gms_establish_filenames(const char *name, char **text, char **graphics, char **hints) { + char *base, *text_file, *graphics_file, *hints_file; + Common::File stream; + assert(name && text && graphics && hints); + + /* Take a destroyable copy of the input filename. */ + base = (char *)gms_malloc(strlen(name) + 1); + strcpy(base, name); + + /* If base has an extension .MAG, .GFX, or .HNT, remove it. */ + if (strlen(base) > strlen(".XXX")) { + if (gms_strcasecmp(base + strlen(base) - strlen(".MAG"), ".MAG") == 0 + || gms_strcasecmp(base + strlen(base) - strlen(".GFX"), ".GFX") == 0 + || gms_strcasecmp(base + strlen(base) - strlen(".HNT"), ".HNT") == 0) + base[strlen(base) - strlen(".XXX")] = '\0'; + } + + /* Allocate space for the return text file. */ + text_file = (char *)gms_malloc(strlen(base) + strlen(".MAG") + 1); + + /* Form a candidate text file, by adding a .MAG extension. */ + strcpy(text_file, base); + strcat(text_file, ".MAG"); + + if (!stream.open(text_file)) { + /* Retry, using a .mag extension instead. */ + strcpy(text_file, base); + strcat(text_file, ".mag"); + + if (!stream.open(text_file)) { + /* + * No access to a usable game text file. Return immediately, + * without looking for any associated graphics or hints files. + */ + *text = NULL; + *graphics = NULL; + *hints = NULL; + + free(text_file); + free(base); + return; + } + } + stream.close(); + + /* Now allocate space for the return graphics file. */ + graphics_file = (char *)gms_malloc(strlen(base) + strlen(".GFX") + 1); + + /* As above, form a candidate graphics file, using a .GFX extension. */ + strcpy(graphics_file, base); + strcat(graphics_file, ".GFX"); + + if (!stream.open(graphics_file)) { + /* Retry, using a .gfx extension instead. */ + strcpy(graphics_file, base); + strcat(graphics_file, ".gfx"); + + if (!stream.open(graphics_file)) { + /* + * No access to any graphics file. In this case, free memory and + * reset graphics file to NULL. + */ + free(graphics_file); + graphics_file = NULL; + } + } + stream.close(); + + /* Now allocate space for the return hints file. */ + hints_file = (char *)gms_malloc(strlen(base) + strlen(".HNT") + 1); + + /* As above, form a candidate graphics file, using a .HNT extension. */ + strcpy(hints_file, base); + strcat(hints_file, ".HNT"); + + if (!stream.open(hints_file)) { + /* Retry, using a .hnt extension instead. */ + strcpy(hints_file, base); + strcat(hints_file, ".hnt"); + + if (!stream.open(hints_file)) { + /* + * No access to any hints file. In this case, free memory and + * reset hints file to NULL. + */ + free(hints_file); + hints_file = NULL; + } + } + stream.close(); + + /* Return the text file, and graphics and hints, which may be NULL. */ + *text = text_file; + *graphics = graphics_file; + *hints = hints_file; + + free(base); +} + + +/* + * gms_startup_code() + * gms_main() + * + * Together, these functions take the place of the original main(). The + * first one is called from glkunix_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. + */ +int gms_startup_code(int argc, char *argv[]) { + int argv_index; + + /* Handle command line arguments. */ + for (argv_index = 1; + argv_index < argc && argv[argv_index][0] == '-'; argv_index++) { + if (strcmp(argv[argv_index], "-nc") == 0) { + gms_commands_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-na") == 0) { + gms_abbreviations_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-np") == 0) { + gms_graphics_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-ng") == 0) { + gms_gamma_mode = GAMMA_OFF; + continue; + } + if (strcmp(argv[argv_index], "-nx") == 0) { + gms_animation_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-ne") == 0) { + gms_prompt_enabled = FALSE; + continue; + } + return FALSE; + } + + /* + * Get the name of the game file. Since we need this in our call from + * g_vm->glk_main, we need to keep it in a module static variable. If the game + * file name is omitted, then here we'll set the pointer to NULL, and + * complain about it later in main. Passing the message string around + * like this is a nuisance... + */ + if (argv_index == argc - 1) { + gms_gamefile = argv[argv_index]; + gms_game_message = NULL; +#ifdef GARGLK + { + const char *s; + s = strrchr(gms_gamefile, '\\'); + if (s) + g_vm->garglk_set_story_name(s + 1); + s = strrchr(gms_gamefile, '/'); + if (s) + g_vm->garglk_set_story_name(s + 1); + } +#endif + } else { + gms_gamefile = NULL; + if (argv_index < argc - 1) + gms_game_message = "More than one game file was given" + " on the command line."; + else + gms_game_message = "No game file was given on the command line."; + } + + /* All startup options were handled successfully. */ + return TRUE; +} + +void gms_main() { + char *text_file = NULL, *graphics_file = NULL, *hints_file = NULL; + int ms_init_status, is_running; + + /* Ensure Magnetic Scrolls internal types have the right sizes. */ + if (!(sizeof(type8) == 1 && sizeof(type8s) == 1 + && sizeof(type16) == 2 && sizeof(type16s) == 2 + && sizeof(type32) == 4 && sizeof(type32s) == 4)) { + gms_fatal("GLK: Types sized incorrectly, recompilation is needed"); + g_vm->glk_exit(); + } + + /* Create the main Glk window, and set its stream as current. */ + gms_main_window = g_vm->glk_window_open(0, 0, 0, wintype_TextBuffer, 0); + if (!gms_main_window) { + gms_fatal("GLK: Can't open main window"); + g_vm->glk_exit(); + } + g_vm->glk_window_clear(gms_main_window); + g_vm->glk_set_window(gms_main_window); + g_vm->glk_set_style(style_Normal); + + /* If there's a problem with the game file, complain now. */ + if (!gms_gamefile) { + assert(gms_game_message); + gms_header_string("Glk Magnetic Error\n\n"); + gms_normal_string(gms_game_message); + gms_normal_char('\n'); + g_vm->glk_exit(); + return; + } + + /* + * Given the basic game name, try to come up with usable text, graphics, + * and hints filenames. The graphics and hints files may be null, but the + * text file may not. + */ + gms_establish_filenames(gms_gamefile, + &text_file, &graphics_file, &hints_file); + if (!text_file) { + assert(!graphics_file && !hints_file); + gms_header_string("Glk Magnetic Error\n\n"); + gms_normal_string("Can't find or open game '"); + gms_normal_string(gms_gamefile); + gms_normal_string("[.mag|.MAG]'"); + + gms_normal_char('\n'); + g_vm->glk_exit(); + } + + /* Set the possibility of pictures depending on graphics file. */ + if (graphics_file) { + /* + * Check Glk library capabilities, and note pictures are impossible if + * the library can't offer both graphics and timers. We need timers to + * create the background "thread" for picture updates. + */ + gms_graphics_possible = g_vm->glk_gestalt(gestalt_Graphics, 0) + && g_vm->glk_gestalt(gestalt_Timer, 0); + } else + gms_graphics_possible = FALSE; + + + /* + * If pictures are impossible, clear pictures enabled flag. That is, act + * as if -np was given on the command line, even though it may not have + * been. If pictures are impossible, they can never be enabled. + */ + if (!gms_graphics_possible) + gms_graphics_enabled = FALSE; + + /* 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); + gms_status_window = g_vm->glk_window_open(gms_main_window, + winmethod_Above | winmethod_Fixed, + 1, wintype_TextGrid, 0); + + /* + * Load the game. If no graphics are possible, then passing the NULL to + * ms_init() runs a game without graphics. + */ + if (gms_graphics_possible) { + assert(graphics_file); + ms_init_status = ms_init(text_file, graphics_file, hints_file, NULL); + } else + ms_init_status = ms_init(text_file, NULL, hints_file, NULL); + + /* Look for a complete failure to load the game. */ + if (ms_init_status == 0) { + if (gms_status_window) + g_vm->glk_window_close(gms_status_window, NULL); + gms_header_string("Glk Magnetic Error\n\n"); + gms_normal_string("Can't load game '"); + gms_normal_string(gms_gamefile); + gms_normal_char('\''); + + gms_normal_char('\n'); + + /* + * Free the text file path, any graphics/hints file path, and + * interpreter allocated memory. + */ + free(text_file); + free(graphics_file); + free(hints_file); + ms_freemem(); + g_vm->glk_exit(); + } + + /* Try to identify the game from its text file header. */ + gms_gameid_identify_game(text_file); + + /* Print out a short banner. */ + gms_header_string("\nMagnetic Scrolls Interpreter, version 2.3\n"); + gms_banner_string("Written by Niclas Karlsson\n" + "Glk interface by Simon Baldwin\n\n"); + + /* Look for failure to load just game graphics. */ + if (gms_graphics_possible && ms_init_status == 1) { + /* + * Output a warning if graphics failed, but the main game text + * initialized okay. + */ + gms_standout_string("Error: Unable to open graphics file\n" + "Continuing without pictures...\n\n"); + + gms_graphics_possible = FALSE; + } + + /* Run the game opcodes -- ms_rungame() returns FALSE on game end. */ + do { + is_running = ms_rungame(); + g_vm->glk_tick(); + } while (is_running); + + /* Handle any updated status and pending buffered output. */ + gms_status_notify(); + gms_output_flush(); + + /* Turn off any background graphics "thread". */ + gms_graphics_stop(); + + /* Free interpreter allocated memory. */ + ms_freemem(); + + /* + * Free any temporary memory that may have been used by graphics and hints. + */ + gms_graphics_cleanup(); + gms_hints_cleanup(); + + /* Close any open transcript, input log, and/or read log. */ + if (gms_transcript_stream) { + g_vm->glk_stream_close(gms_transcript_stream, NULL); + gms_transcript_stream = NULL; + } + if (gms_inputlog_stream) { + g_vm->glk_stream_close(gms_inputlog_stream, NULL); + gms_inputlog_stream = NULL; + } + if (gms_readlog_stream) { + g_vm->glk_stream_close(gms_readlog_stream, NULL); + gms_readlog_stream = NULL; + } + + /* Free the text file path, and any graphics/hints file path. */ + free(text_file); + free(graphics_file); + free(hints_file); +} + + +/*---------------------------------------------------------------------*/ +/* Linkage between Glk entry/exit calls and the Magnetic interpreter */ +/*---------------------------------------------------------------------*/ + +/* + * Safety flags, to ensure we always get startup before main, and that + * we only get a call to main once. + */ +static int gms_startup_called = FALSE, + gms_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. + */ +void glk_main(void) { + assert(gms_startup_called && !gms_main_called); + gms_main_called = TRUE; + + /* Call the interpreter main function. */ + gms_main(); +} + + +/* + * Glk arguments for UNIX versions of the Glk interpreter. + */ +#if 0 +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 *) "-np", glkunix_arg_NoValue, + (char *) "-np Turn off pictures" + }, + { + (char *) "-ng", glkunix_arg_NoValue, + (char *) "-ng Turn off automatic gamma correction on pictures" + }, + { + (char *) "-nx", glkunix_arg_NoValue, + (char *) "-nx Turn off picture animations" + }, + { + (char *) "-ne", glkunix_arg_NoValue, + (char *) "-ne Turn off additional interpreter prompt" + }, + { + (char *) "", glkunix_arg_ValueCanFollow, + (char *) "filename game to run" + }, + {NULL, glkunix_arg_End, NULL} +}; +#endif + +void write(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + Common::String s = Common::String::format(fmt, ap); + va_end(ap); + g_vm->glk_put_buffer(s.c_str(), s.size()); +} + +void writeChar(char c) { + g_vm->glk_put_char(c); +} + +} // End of namespace Magnetic +} // End of namespace Glk diff --git a/engines/glk/magnetic/graphics.cpp b/engines/glk/magnetic/graphics.cpp deleted file mode 100644 index d9c62a1b89..0000000000 --- a/engines/glk/magnetic/graphics.cpp +++ /dev/null @@ -1,507 +0,0 @@ -/* 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/magnetic/magnetic.h" - -namespace Glk { -namespace Magnetic { - -byte Magnetic::init_gfx1(size_t size) { - if (!(gfx_buf = new byte[MAX_PICTURE_SIZE])) - return 1; - - if (!(gfx_data = new byte[size])) { - delete[] gfx_buf; - gfx_buf = nullptr; - return 1; - } - - if (_gfxFile.read(gfx_data, size) != size) { - delete[] gfx_data; - delete[] gfx_buf; - gfx_data = gfx_buf = nullptr; - return 1; - } - - gfx_ver = 1; - return 2; -} - -byte Magnetic::init_gfx2(size_t size) { - if (!(gfx_buf = new byte[MAX_PICTURE_SIZE])) { - return 1; - } - - gfx2_hsize = size; - if (!(gfx2_hdr = new byte[gfx2_hsize])) { - delete[] gfx_buf; - gfx_buf = nullptr; - return 1; - } - - if (_gfxFile.read(gfx2_hdr, gfx2_hsize) != gfx2_hsize) { - delete[] gfx_buf; - delete[] gfx2_hdr; - gfx_buf = nullptr; - gfx2_hdr = nullptr; - return 1; - } - - gfx_ver = 2; - return 2; -} - -void Magnetic::ms_showpic(int c, byte mode) { - // TODO -} - -bool Magnetic::is_blank(uint16 line, uint16 width) const { - int i; - - for (i = line * width; i < (line + 1) * width; i++) - if (gfx_buf[i]) - return false; - return true; -} - -byte *Magnetic::ms_extract1(byte pic, uint16 * w, uint16 * h, uint16 * pal) { - byte *table, *data, bit, val, *buffer; - uint16 tablesize, count; - uint32 i, j, upsize, offset; - //uint32 datasize; - - offset = READ_LE_UINT32(gfx_data + 4 * pic); - buffer = gfx_data + offset - 8; - - for (i = 0; i < 16; i++) - pal[i] = READ_LE_UINT16(buffer + 0x1c + 2 * i); - w[0] = (uint16)(READ_LE_UINT16(buffer + 4) - READ_LE_UINT16(buffer + 2)); - h[0] = READ_LE_UINT16(buffer + 6); - - tablesize = READ_LE_UINT16(buffer + 0x3c); - //datasize = READ_LE_UINT32(buffer + 0x3e); - table = buffer + 0x42; - data = table + tablesize * 2 + 2; - upsize = h[0] * w[0]; - - for (i = 0, j = 0, count = 0, val = 0, bit = 7; i < upsize; i++, count--) { - if (!count) { - count = tablesize; - while (count < 0x80) { - if (data[j] & (1 << bit)) - count = table[2 * count]; - else - count = table[2 * count + 1]; - if (!bit) - j++; - bit = (byte)(bit ? bit - 1 : 7); - } - count &= 0x7f; - if (count >= 0x10) - count -= 0x10; - else - { - val = (byte)count; - count = 1; - } - } - gfx_buf[i] = val; - } - for (j = w[0]; j < upsize; j++) - gfx_buf[j] ^= gfx_buf[j - w[0]]; - - for (; h[0] > 0 && is_blank((uint16)(h[0] - 1), w[0]); h[0]--); - for (i = 0; h[0] > 0 && is_blank((uint16)i, w[0]); h[0]--, i++); - return gfx_buf + i * w[0]; -} - - -byte *Magnetic::ms_extract2(const char *name, uint16 *w, uint16 *h, uint16 *pal, byte *is_anim) { - struct picture main_pic; - uint32 offset = 0, length = 0, i; - int16 header_pos = -1; - byte* anim_data; - uint32 j; - - if (is_anim != 0) - *is_anim = 0; - gfx2_name = name; - - pos_table_size = 0; - - // Find the uppercase (no animation) version of the picture first - header_pos = find_name_in_header(name, 1); - - if (header_pos < 0) - header_pos = find_name_in_header(name, 0); - if (header_pos < 0) - return 0; - - offset = READ_LE_UINT32(gfx2_hdr + header_pos + 8); - length = READ_LE_UINT32(gfx2_hdr + header_pos + 12); - - if (offset != 0) { - if (gfx2_buf) { - delete[] gfx2_buf; - gfx2_buf = nullptr; - } - - gfx2_buf = new byte[length]; - if (!gfx2_buf) - return 0; - - if (!_gfxFile.seek(offset) || _gfxFile.read(gfx2_buf, length) != length) { - delete[] gfx2_buf; - gfx2_buf = nullptr; - return 0; - } - - for (i = 0; i < 16; i++) - pal[i] = READ_LE_UINT16(gfx2_buf + 4 + (2 * i)); - - main_pic.data = gfx2_buf + 48; - main_pic.data_size = READ_LE_UINT32(gfx2_buf + 38); - main_pic.width = READ_LE_UINT16(gfx2_buf + 42); - main_pic.height = READ_LE_UINT16(gfx2_buf + 44); - main_pic.wbytes = (uint16)(main_pic.data_size / main_pic.height); - main_pic.plane_step = (uint16)(main_pic.wbytes / 4); - main_pic.mask = (byte*)0; - extract_frame(&main_pic); - - *w = main_pic.width; - *h = main_pic.height; - - // Check for an animation - anim_data = gfx2_buf + 48 + main_pic.data_size; - if ((anim_data[0] != 0xD0) || (anim_data[1] != 0x5E)) { - byte *current; - uint16 frame_count; - uint16 value1, value2; - //uint16 command_count; - - if (is_anim != 0) - *is_anim = 1; - - current = anim_data + 6; - frame_count = READ_LE_UINT16(anim_data + 2); - if (frame_count > MAX_ANIMS) - { - error("animation frame array too short"); - return 0; - } - - /* Loop through each animation frame */ - for (i = 0; i < frame_count; i++) - { - anim_frame_table[i].data = current + 10; - anim_frame_table[i].data_size = READ_LE_UINT32(current); - anim_frame_table[i].width = READ_LE_UINT16(current + 4); - anim_frame_table[i].height = READ_LE_UINT16(current + 6); - anim_frame_table[i].wbytes = (uint16)(anim_frame_table[i].data_size / anim_frame_table[i].height); - anim_frame_table[i].plane_step = (uint16)(anim_frame_table[i].wbytes / 4); - anim_frame_table[i].mask = (byte*)0; - - current += anim_frame_table[i].data_size + 12; - value1 = READ_LE_UINT16(current - 2); - value2 = READ_LE_UINT16(current); - - /* Get the mask */ - if ((value1 == anim_frame_table[i].width) && (value2 == anim_frame_table[i].height)) - { - uint16 skip; - - anim_frame_table[i].mask = (byte*)(current + 4); - skip = READ_LE_UINT16(current + 2); - current += skip + 6; - } - } - - /* Get the positioning tables */ - pos_table_size = READ_LE_UINT16(current - 2); - if (pos_table_size > MAX_POSITIONS) - { - error("animation position array too short"); - return 0; - } - - for (i = 0; i < pos_table_size; i++) { - pos_table_count[i] = READ_LE_UINT16(current + 2); - current += 4; - - if (pos_table_count[i] > MAX_ANIMS) - { - error("animation position array too short"); - return 0; - } - - for (j = 0; j < pos_table_count[i]; j++) - { - pos_table[i][j].x = READ_LE_UINT16(current); - pos_table[i][j].y = READ_LE_UINT16(current + 2); - pos_table[i][j].number = READ_LE_UINT16(current + 4) - 1; - current += 8; - } - } - - // Get the command sequence table - //command_count = READ_LE_UINT16(current); - command_table = current + 2; - - for (i = 0; i < MAX_POSITIONS; i++) - { - anim_table[i].flag = -1; - anim_table[i].count = -1; - } - command_index = 0; - anim_repeat = 0; - pos_table_index = -1; - pos_table_max = -1; - } - - return gfx_buf; - } - - return nullptr; -} - -int16 Magnetic::find_name_in_header(const Common::String &name, bool upper) { - int16 header_pos = 0; - Common::String pic_name(name.c_str(), name.c_str() + 6); - - if (upper) - pic_name.toUppercase(); - - while (header_pos < gfx2_hsize) { - const char *hname = (const char *)(gfx2_hdr + header_pos); - if (strncmp(hname, pic_name.c_str(), 6) == 0) - return header_pos; - header_pos += 16; - } - - return -1; -} - -void Magnetic::extract_frame(const picture *pic) { - uint32 i, x, y, bit_x, mask, ywb, yw, value, values[4]; - values[0] = values[1] = values[2] = values[3] = 0; - - if (pic->width * pic->height > MAX_PICTURE_SIZE) { - error("picture too large"); - return; - } - - for (y = 0; y < pic->height; y++) { - ywb = y * pic->wbytes; - yw = y * pic->width; - - for (x = 0; x < pic->width; x++) { - if ((x % 8) == 0) { - for (i = 0; i < 4; i++) - values[i] = pic->data[ywb + (x / 8) + (pic->plane_step * i)]; - } - - bit_x = 7 - (x & 7); - mask = 1 << bit_x; - value = ((values[0] & mask) >> bit_x) << 0 | - ((values[1] & mask) >> bit_x) << 1 | - ((values[2] & mask) >> bit_x) << 2 | - ((values[3] & mask) >> bit_x) << 3; - value &= 15; - - gfx_buf[yw + x] = (byte)value; - } - } -} - -byte *Magnetic::ms_extract(uint32 pic, uint16 *w, uint16 *h, uint16 *pal, byte *is_anim) { - if (is_anim) - *is_anim = 0; - - if (gfx_buf) { - switch (gfx_ver) { - case 1: - return ms_extract1((byte)pic, w, h, pal); - case 2: - return ms_extract2((const char *)(code + pic), w, h, pal, is_anim); - } - } - - return nullptr; -} - -byte Magnetic::ms_animate(ms_position **positions, uint16 *count) { - byte got_anim = 0; - uint16 i, j, ttable; - - if ((gfx_buf == 0) || (gfx2_buf == 0) || (gfx_ver != 2)) - return 0; - if ((pos_table_size == 0) || (command_index < 0)) - return 0; - - *count = 0; - *positions = (struct ms_position*)0; - - while (got_anim == 0) - { - if (pos_table_max >= 0) - { - if (pos_table_index < pos_table_max) - { - for (i = 0; i < pos_table_size; i++) - { - if (anim_table[i].flag > -1) - { - if (*count >= MAX_FRAMES) - { - error("returned animation array too short"); - return 0; - } - - pos_array[*count] = pos_table[i][anim_table[i].flag]; - - (*count)++; - - if (anim_table[i].flag < (pos_table_count[i] - 1)) - anim_table[i].flag++; - if (anim_table[i].count > 0) - anim_table[i].count--; - else - anim_table[i].flag = -1; - } - } - if (*count > 0) - { - *positions = pos_array; - got_anim = 1; - } - pos_table_index++; - } - } - - if (got_anim == 0) - { - byte command = command_table[command_index]; - command_index++; - - pos_table_max = -1; - pos_table_index = -1; - - switch (command) - { - case 0x00: - command_index = -1; - return 0; - case 0x01: - ttable = command_table[command_index]; - command_index++; - - if (ttable - 1 >= MAX_POSITIONS) - { - error("animation table too short"); - return 0; - } - - anim_table[ttable - 1].flag = (int16)(command_table[command_index] - 1); - command_index++; - anim_table[ttable - 1].count = (int16)(command_table[command_index] - 1); - command_index++; - - /* Workaround for Wonderland "catter" animation */ - if (v4_id == 0) - { - if (gfx2_name == "catter") { - if (command_index == 96) - anim_table[ttable - 1].count = 9; - if (command_index == 108) - anim_table[ttable - 1].flag = -1; - if (command_index == 156) - anim_table[ttable - 1].flag = -1; - } - } - break; - case 0x02: - pos_table_max = command_table[command_index]; - pos_table_index = 0; - command_index++; - break; - case 0x03: - if (v4_id == 0) - { - command_index = -1; - return 0; - } - else - { - command_index = 0; - anim_repeat = 1; - pos_table_index = -1; - pos_table_max = -1; - for (j = 0; j < MAX_POSITIONS; j++) - { - anim_table[j].flag = -1; - anim_table[j].count = -1; - } - } - break; - - case 0x04: - command_index += 3; - return 0; - case 0x05: - ttable = next_table; - command_index++; - - anim_table[ttable - 1].flag = 0; - anim_table[ttable - 1].count = command_table[command_index]; - - pos_table_max = command_table[command_index]; - pos_table_index = 0; - command_index++; - command_index++; - next_table++; - break; - default: - error("unknown animation command"); - command_index = -1; - return 0; - } - } - } - - return got_anim; -} - -byte *Magnetic::ms_get_anim_frame(int16 number, uint16 *width, uint16 *height, byte **mask) { - if (number >= 0) - { - extract_frame(anim_frame_table + number); - *width = anim_frame_table[number].width; - *height = anim_frame_table[number].height; - *mask = anim_frame_table[number].mask; - return gfx_buf; - } - - return nullptr; -} - -} // End of namespace Magnetic -} // End of namespace Glk diff --git a/engines/glk/magnetic/magnetic.cpp b/engines/glk/magnetic/magnetic.cpp index 3f9917bf7b..3689668789 100644 --- a/engines/glk/magnetic/magnetic.cpp +++ b/engines/glk/magnetic/magnetic.cpp @@ -21,41 +21,20 @@ */ #include "glk/magnetic/magnetic.h" -#include "common/config-manager.h" -#include "common/translation.h" +#include "glk/magnetic/defs.h" namespace Glk { namespace Magnetic { -Magnetic::Magnetic(OSystem *syst, const GlkGameDescription &gameDesc) : GlkAPI(syst, gameDesc), - - vm_exited_cleanly(false), dlimit(0xffffffff), slimit(0xffffffff), log_on(0), - i_count(0), string_size(0), rseed(0), pc(0), arg1i(0), mem_size(0), properties(0), fl_sub(0), - fl_tab(0), fl_size(0), fp_tab(0), fp_size(0), zflag(0), nflag(0), cflag(0), vflag(0), byte1(0), - byte2(0), regnr(0), admode(0), opsize(0), arg1(nullptr), arg2(nullptr), is_reversible(0), - lastchar(0), version(0), sd(0), decode_table(nullptr), restart(nullptr), code(nullptr), - string(nullptr), string2(nullptr), string3(nullptr), dict(nullptr), quick_flag(0), gfx_ver(0), - gfx_buf(nullptr), gfx_data(nullptr), gfx2_hdr(0), gfx2_buf(nullptr), gfx2_hsize(0), - snd_buf(nullptr), snd_hdr(nullptr), snd_hsize(0), undo_pc(0), undo_size(0), - gfxtable(0), table_dist(0), v4_id(0), next_table(1), pos_table_size(0), - command_table(nullptr), command_index(-1), pos_table_index(-1), pos_table_max(-1), - anim_repeat(0) { +Magnetic *g_vm; - // Emu fields - undo[0] = undo[1] = nullptr; - undo_stat[0] = undo_stat[1] = 0; - Common::fill(&dreg[0], &dreg[8], 0); - Common::fill(&areg[0], &areg[8], 0); - Common::fill(&tmparg[0], &tmparg[4], 0); - Common::fill(&undo_regs[0][0], &undo_regs[2][18], 0), - Common::fill(&pos_table_count[0], &pos_table_count[MAX_POSITIONS], 0); +Magnetic::Magnetic(OSystem *syst, const GlkGameDescription &gameDesc) : GlkAPI(syst, gameDesc) { + g_vm = this; } void Magnetic::runGame() { - if (!is_gamefile_valid()) - return; - - // TODO + gms_startup_code(0, nullptr); + gms_main(); } Common::Error Magnetic::readSaveData(Common::SeekableReadStream *rs) { @@ -68,30 +47,5 @@ Common::Error Magnetic::writeGameData(Common::WriteStream *ws) { return Common::kWritingFailed; } -bool Magnetic::is_gamefile_valid() { - if (_gameFile.size() < 8) { - GUIErrorMessage(_("This is too short to be a valid Glulx file.")); - return false; - } - - if (_gameFile.readUint32BE() != MKTAG('G', 'l', 'u', 'l')) { - GUIErrorMessage(_("This is not a valid Glulx file.")); - return false; - } - - // We support version 2.0 through 3.1.* - uint32 vers = _gameFile.readUint32BE(); - if (vers < 0x20000) { - GUIErrorMessage(_("This Glulx file is too old a version to execute.")); - return false; - } - if (vers >= 0x30200) { - GUIErrorMessage(_("This Glulx file is too new a version to execute.")); - return false; - } - - return true; -} - } // End of namespace Magnetic } // End of namespace Glk diff --git a/engines/glk/magnetic/magnetic.h b/engines/glk/magnetic/magnetic.h index c926ede9e6..33f9a4da5a 100644 --- a/engines/glk/magnetic/magnetic.h +++ b/engines/glk/magnetic/magnetic.h @@ -20,6 +20,8 @@ * */ +/* Based on Magnetic interpreter version 2.3 */ + #ifndef GLK_MAGNETIC_MAGNETIC #define GLK_MAGNETIC_MAGNETIC @@ -35,146 +37,6 @@ namespace Magnetic { */ class Magnetic : public GlkAPI { public: - Common::File _hintFile; - Common::File _gfxFile; - Common::File _sndFile; - bool vm_exited_cleanly; - uint dlimit, slimit; - int log_on; - - // Emu fields - uint32 dreg[8], areg[8], i_count, string_size, rseed, pc, arg1i, mem_size; - uint16 properties, fl_sub, fl_tab, fl_size, fp_tab, fp_size; - byte zflag, nflag, cflag, vflag, byte1, byte2, regnr, admode, opsize; - byte *arg1, *arg2, is_reversible, running, tmparg[4]; - byte lastchar, version, sd; - byte *decode_table, *restart, *code, *string, *string2; - byte *string3, *dict; - byte quick_flag, gfx_ver, *gfx_buf, *gfx_data; - byte *gfx2_hdr, *gfx2_buf; - Common::String gfx2_name; - uint16 gfx2_hsize; - byte *snd_buf, *snd_hdr; - uint16 snd_hsize; - Common::File gfx_fp; - uint32 undo_regs[2][18], undo_pc, undo_size; - byte *undo[2], undo_stat[2]; - uint16 gfxtable, table_dist; - uint16 v4_id, next_table; - - ms_hint *hints; - byte *hint_contents; - picture anim_frame_table[MAX_ANIMS]; - uint16 pos_table_size; - uint16 pos_table_count[MAX_POSITIONS]; - ms_position pos_table[MAX_POSITIONS][MAX_ANIMS]; - byte *command_table; - int command_index; - lookup anim_table[MAX_POSITIONS]; - int pos_table_index; - int pos_table_max; - ms_position pos_array[MAX_FRAMES]; - byte anim_repeat; - - -private: - /** - * Validates the game file, and if it's invalid, displays an error dialog - */ - bool is_gamefile_valid(); - - /** - * \defgroup Emu - * @{ - */ - - /** - * Loads the interpreter with a game - * @return 0 = failure, 1 = success(without graphics or graphics failed), - * 2 = success(with graphics) - */ - int ms_init(bool restarting = false); - - /** - * Stops further processing of opcodes - */ - void ms_stop() { running = false; } - - /** - * Detects if game is running - */ - bool ms_is_running() const { return running; } - - /** - * Returns true if running a Magnetic Windows game - */ - bool ms_is_magwin() const { return version == 4; } - - /** - * Frees all allocated ressources - */ - void ms_freemem(); - - /**@}*/ - - /** - * \defgroup Graphics support methods - * @{ - */ - - byte init_gfx1(size_t size); - - byte init_gfx2(size_t size); - - /** - * Displays or hides a picture - * @param c number of image to be displayed - * @param mode 0 = means gfx off, 1 gfx on thumbnails, 2 gfx on normal - * - * @remarks For retrieving the raw data of a picture call ms_extract - */ - void ms_showpic(int c, byte mode); - - /** - * Returns true if a given line is blank - */ - bool is_blank(uint16 line, uint16 width) const; - - byte *ms_extract1(byte pic, uint16 *w, uint16 *h, uint16 *pal); - - int16 find_name_in_header(const Common::String &name, bool upper); - - void extract_frame(const picture *pic); - - byte *ms_extract2(const char *name, uint16 *w, uint16 *h, uint16 *pal, byte *is_anim); - - byte *ms_extract(uint32 pic, uint16 *w, uint16 *h, uint16 *pal, byte *is_anim); - - byte ms_animate(ms_position **positions, uint16 *count); - - byte *ms_get_anim_frame(int16 number, uint16 *width, uint16 *height, byte **mask); - - bool ms_anim_is_repeating() const { return anim_repeat; } - - void write_reg(int i, int s, uint32 val) { - // TODO - } - - /**@}*/ - - /** - * \defgroup Sound support methods - * @{ - */ - - byte init_snd(size_t size); - - int16 find_name_in_sndheader(const Common::String &name); - - byte *sound_extract(const Common::String &name, uint32 *length, uint16 *tempo); - - /**@}*/ -public: /** * Constructor */ @@ -188,7 +50,9 @@ public: /** * Returns the running interpreter type */ - virtual InterpreterType getInterpreterType() const override { return INTERPRETER_MAGNETIC; } + virtual InterpreterType getInterpreterType() const override { + return INTERPRETER_MAGNETIC; + } /** * Load a savegame from the passed Quetzal file chunk stream @@ -202,6 +66,8 @@ public: virtual Common::Error writeGameData(Common::WriteStream *ws) override; }; +extern Magnetic *g_vm; + } // End of namespace Magnetic } // End of namespace Glk diff --git a/engines/glk/magnetic/magnetic_types.h b/engines/glk/magnetic/magnetic_types.h index cfef76557e..5c8cad5077 100644 --- a/engines/glk/magnetic/magnetic_types.h +++ b/engines/glk/magnetic/magnetic_types.h @@ -24,6 +24,7 @@ #define GLK_MAGNETIC_TYPES #include "common/scummsys.h" +#include "common/algorithm.h" namespace Glk { namespace Magnetic { @@ -47,7 +48,7 @@ struct lookup { }; struct picture { - byte * data; + byte *data; uint32 data_size; uint16 width; uint16 height; @@ -121,7 +122,7 @@ struct ms_position { struct ms_hint { uint16 elcount; uint16 nodetype; - byte *content; + const char *content; uint16 links[MAX_HITEMS]; uint16 parent; diff --git a/engines/glk/magnetic/main.cpp b/engines/glk/magnetic/main.cpp new file mode 100644 index 0000000000..1f0128f36c --- /dev/null +++ b/engines/glk/magnetic/main.cpp @@ -0,0 +1,176 @@ +/* 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/magnetic/defs.h" +#include "common/file.h" +#include "common/savefile.h" +#include "common/system.h" + +namespace Glk { +namespace Magnetic { + +#define WIDTH 78 + +static type8 buffer[80], xpos = 0, bufpos = 0, log_on = 0, ms_gfx_enabled, filename[256]; +static Common::DumpFile *log1 = 0, *log2 = 0; + +type8 ms_load_file(const char *name, type8 *ptr, type16 size) { + assert(name); + Common::InSaveFile *file = g_system->getSavefileManager()->openForLoading(name); + if (!file) + return 1; + + if (file->read(ptr, size) != size) { + delete file; + return 1; + } + + delete file; + return 0; +} + +type8 ms_save_file(const char *name, type8 *ptr, type16 size) { + assert(name); + Common::OutSaveFile *file = g_system->getSavefileManager()->openForSaving(name); + if (!file) + return 1; + + if (file->write(ptr, size) != size) { + delete file; + return 1; + } + + file->finalize(); + delete file; + return 0; +} + +void script_write(type8 c) { + if (log_on == 2) + log1->writeByte(c); +} + +void transcript_write(type8 c) { + if (log2) + log2->writeByte(c); +} + +void ms_fatal(const char *txt) { + error("Fatal error: %s", txt); +} + +#if 0 +main(int argc, char **argv) { + type8 running, i, *gamename = 0, *gfxname = 0, *hintname = 0; + type32 dlimit, slimit; + + if (sizeof(type8) != 1 || sizeof(type16) != 2 || sizeof(type32) != 4) { + fprintf(stderr, + "You have incorrect typesizes, please edit the typedefs and recompile\n" + "or proceed on your own risk...\n"); + exit(1); + } + dlimit = slimit = 0xffffffff; + for (i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + switch (tolower(argv[i][1])) { + case 'd': + if (strlen(argv[i]) > 2) + dlimit = atoi(&argv[i][2]); + else + dlimit = 0; + break; + case 's': + if (strlen(argv[i]) > 2) + slimit = atoi(&argv[i][2]); + else + slimit = 655360; + break; + case 't': + if (!(log2 = fopen(&argv[i][2], "w"))) + printf("Failed to open \"%s\" for writing.\n", &argv[i][2]); + break; + case 'r': + if (log1 = fopen(&argv[i][2], "r")) + log_on = 1; + else + printf("Failed to open \"%s\" for reading.\n", &argv[i][2]); + break; + case 'w': + if (log1 = fopen(&argv[i][2], "w")) + log_on = 2; + else + printf("Failed to open \"%s\" for writing.\n", &argv[i][2]); + break; + default: + printf("Unknown option -%c, ignoring.\n", argv[i][1]); + break; + } + } else if (!gamename) + gamename = argv[i]; + else if (!gfxname) + gfxname = argv[i]; + else if (!hintname) + hintname = argv[i]; + } + if (!gamename) { + printf("Magnetic 2.3 - a Magnetic Scrolls interpreter\n\n"); + printf("Usage: %s [options] game [gfxfile] [hintfile]\n\n" + "Where the options are:\n" + " -dn activate register dump (after n instructions)\n" + " -rname read script file\n" + " -sn safety mode, exits automatically (after n instructions)\n" + " -tname write transcript file\n" + " -wname write script file\n\n" + "The interpreter commands are:\n" + " #undo undo - don't use it near are_you_sure prompts\n" + " #logoff turn off script writing\n\n", argv[0]); + exit(1); + } + + if (!(ms_gfx_enabled = ms_init(gamename, gfxname, hintname, 0))) { + printf("Couldn't start up game \"%s\".\n", gamename); + exit(1); + } + ms_gfx_enabled--; + running = 1; + while ((ms_count() < slimit) && running) { + if (ms_count() >= dlimit) + ms_status(); + running = ms_rungame(); + } + if (ms_count() == slimit) { + printf("\n\nSafety limit (%d) reached.\n", slimit); + ms_status(); + } + ms_freemem(); + if (log_on) + fclose(log1); + if (log2) + fclose(log2); + printf("\nExiting.\n"); + return 0; +} +#endif + +} // End of namespace Magnetic +} // End of namespace Glk diff --git a/engines/glk/magnetic/myth.cpp b/engines/glk/magnetic/myth.cpp new file mode 100644 index 0000000000..3f8127e613 --- /dev/null +++ b/engines/glk/magnetic/myth.cpp @@ -0,0 +1,133 @@ +/* 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. + * + */ + +/* + * Patches Myth C64 disk image + * Written by David Kinder, based on code by Niclas Karlsson + */ + +#define D64_SIZE (174848L) +unsigned char image[D64_SIZE]; + +void ungarble(unsigned char *block, signed long key) { + unsigned char d; + int i, j; + + d = (unsigned char)((key & 0x07) ^ 0xFF); + if (d < 0xFF) { + i = d; + j = d + 1; + while (j < 0x100) + block[j++] ^= block[i]; + } + + i = 0xFF; + j = d - 1; + while (j >= 0) + block[j--] ^= block[i--]; + + for (i = 0; i < 128; i++) { + d = block[i]; + block[i] = block[255 - i]; + block[255 - i] = d; + } +} + +void garble(unsigned char *block, int i1, int j1, int i2) { + unsigned char d; + int i, j; + + for (i = 0; i < 128; i++) { + d = block[i]; + block[i] = block[255 - i]; + block[255 - i] = d; + } + + i = i1; + j = -1; + while (j < j1) + block[++j] ^= block[++i]; + + i = i2; + j = 0x100; + while (j > i2 + 1) + block[--j] ^= block[i]; +} + +/* This routine does the patching. */ +void fixDiskImage(int code) { + unsigned char *block = NULL; + + switch (code) { + case 0: + /* code[0x3056] = 0x60 */ + block = image + 0x1600; + ungarble(block, 0x27L); + block[0x56] = 0x60; + garble(block, 0x07, 0xF7, 0xF8); + break; + case 1: + /* code[0x3148] = 0x60 */ + /* code[0x314D] = 0x11 */ + block = image + 0x1700; + ungarble(block, 0x28L); + block[0x48] = 0x60; + block[0x4D] = 0x11; + garble(block, 0x00, 0xFE, 0xFF); + break; + } +} + +void readDiskImage(const char *filename) { + FILE *file = fopen(filename, "r+b"); + if (file == NULL) + exit(1); + fread(image, 1, D64_SIZE, file); +} + +void writeDiskImage(const char *filename) { + FILE *file = fopen(filename, "w+b"); + if (file == NULL) + exit(1); + fwrite(image, 1, D64_SIZE, file); +} + +int main(int argc, char **argv) { + if (argc == 4) { + int code = -1; + if (sscanf(argv[3], "%d", &code) == 1) { + if ((code >= 0) && (code <= 1)) { + readDiskImage(argv[1]); + fixDiskImage(code); + writeDiskImage(argv[2]); + printf("Myth patched.\n"); + return 0; + } + } + } + + printf("Removes password protection from Magnetic Scrolls' Myth.\n"); + printf("Use: myth input.d64 output.d64 code\n"); + printf("If code is 0, the password protection is completely removed.\n"); + printf("If code is 1, any user name and password will be accepted.\n"); + return 0; +} diff --git a/engines/glk/magnetic/sound.cpp b/engines/glk/magnetic/sound.cpp deleted file mode 100644 index 3ea74fb8fe..0000000000 --- a/engines/glk/magnetic/sound.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* 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/magnetic/magnetic.h" - -namespace Glk { -namespace Magnetic { - -byte Magnetic::init_snd(size_t size) { - if (!(snd_buf = new byte[MAX_MUSIC_SIZE])) { - return 1; - } - - snd_hsize = _sndFile.readUint16LE(); - if (!(snd_hdr = new byte[snd_hsize])) { - delete[] snd_buf; - snd_buf = nullptr; - return 1; - } - - if (_sndFile.read(snd_hdr, snd_hsize) != snd_hsize) { - delete[] snd_buf; - delete[] snd_hdr; - snd_buf = nullptr; - snd_hdr = nullptr; - return 1; - } - - return 2; -} - -int16 Magnetic::find_name_in_sndheader(const Common::String &name) { - int16 header_pos = 0; - - while (header_pos < snd_hsize) { - const char *hname = (const char *)(snd_hdr + header_pos); - if (name == hname) - return header_pos; - header_pos += 18; - } - - return -1; -} - -byte *Magnetic::sound_extract(const Common::String &name, uint32 *length, uint16 *tempo) { - uint32 offset = 0; - int16 header_pos = -1; - - if (header_pos < 0) - header_pos = find_name_in_sndheader(name); - if (header_pos < 0) - return 0; - - *tempo = READ_BE_UINT16(snd_hdr + header_pos + 8); - offset = READ_BE_UINT32(snd_hdr + header_pos + 10); - *length = READ_BE_UINT32(snd_hdr + header_pos + 14); - - if (offset != 0) { - if (!snd_buf) - return nullptr; - if (!_sndFile.seek(offset)) - return nullptr; - if (_sndFile.read(snd_buf, *length) != *length) - return nullptr; - - return snd_buf; - } - - return nullptr; -} - -} // End of namespace Magnetic -} // End of namespace Glk diff --git a/engines/glk/module.mk b/engines/glk/module.mk index 8c1daeeb13..7c6308fd06 100644 --- a/engines/glk/module.mk +++ b/engines/glk/module.mk @@ -262,9 +262,9 @@ ifdef ENABLE_GLK_MAGNETIC MODULE_OBJS += \ magnetic/detection.o \ magnetic/emu.o \ - magnetic/graphics.o \ + magnetic/glk.o \ magnetic/magnetic.o \ - magnetic/sound.o + magnetic/main.o endif ifdef ENABLE_GLK_QUEST |